/** * Unit Tests for YT2AI Bookmarklet Core Functionality * Testing Framework: Jest + jsdom for DOM simulation * Mobile environment mocking included */ // Mock mobile environment const mockMobileEnvironment = () => { // Mock mobile user agent Object.defineProperty(navigator, 'userAgent', { writable: true, value: 'Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36' }); // Mock mobile viewport Object.defineProperty(window, 'innerWidth', { writable: true, value: 375 }); Object.defineProperty(window, 'innerHeight', { writable: true, value: 667 }); }; // Mock YouTube environment const mockYouTubeEnvironment = (videoId = 'dQw4w9WgXcQ') => { Object.defineProperty(window, 'location', { value: { href: `https://www.youtube.com/watch?v=${videoId}`, hostname: 'youtube.com', search: '' }, writable: true }); }; // Clean environment before each test const cleanEnvironment = () => { // Reset globals delete window.__YT2AI_INITIALIZED__; delete window.__YT2AI_VERSION__; delete window.__YT2AI__; // Clean DOM document.head.innerHTML = ''; document.body.innerHTML = ''; document.body.style.overflow = ''; // Clear console spies jest.clearAllMocks(); }; describe('YT2AI Bookmarklet Core', () => { let consoleLogSpy, consoleInfoSpy, consoleWarnSpy, consoleErrorSpy; // Helper function to load bookmarklet const loadBookmarklet = () => { const fs = require('fs'); const path = require('path'); const bookmarkletCode = fs.readFileSync(path.join(__dirname, '../../../src/bookmarklet.js'), 'utf8'); eval(bookmarkletCode); }; beforeEach(() => { cleanEnvironment(); mockMobileEnvironment(); mockYouTubeEnvironment(); // Spy on console methods consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(); consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(); consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); }); afterEach(() => { cleanEnvironment(); // Restore console methods if they exist if (consoleLogSpy && consoleLogSpy.mockRestore) consoleLogSpy.mockRestore(); if (consoleInfoSpy && consoleInfoSpy.mockRestore) consoleInfoSpy.mockRestore(); if (consoleWarnSpy && consoleWarnSpy.mockRestore) consoleWarnSpy.mockRestore(); if (consoleErrorSpy && consoleErrorSpy.mockRestore) consoleErrorSpy.mockRestore(); }); describe('Initialization', () => { test('should initialize bookmarklet without errors on YouTube page', () => { console.log('Before load - location:', window.location); console.log('Before load - initialized:', window.__YT2AI_INITIALIZED__); expect(() => { loadBookmarklet(); }).not.toThrow(); console.log('After load - initialized:', window.__YT2AI_INITIALIZED__); console.log('After load - version:', window.__YT2AI_VERSION__); expect(window.__YT2AI_INITIALIZED__).toBe(true); expect(window.__YT2AI_VERSION__).toBeDefined(); expect(consoleInfoSpy).toHaveBeenCalledWith( '[YT2AI:INFO]', 'Bookmarklet initialized', expect.any(Object) ); }); test('should prevent double initialization', () => { // First initialization loadBookmarklet(); // Try to initialize again loadBookmarklet(); expect(consoleWarnSpy).toHaveBeenCalledWith( '[YT2AI] Bookmarklet already initialized' ); }); test('should detect mobile environment correctly', () => { loadBookmarklet(); if (window.__YT2AI__) { expect(window.__YT2AI__.environment.isMobile).toBe(true); expect(window.__YT2AI__.environment.isAndroid).toBe(true); } }); test('should set development mode correctly', () => { // Mock development environment window.location.search = '?debug=true'; loadBookmarklet(); if (window.__YT2AI__) { expect(window.__YT2AI__.environment.isDevelopment).toBe(true); } }); }); describe('YouTube Context Detection', () => { test('should extract video ID from standard YouTube URLs', () => { const testUrls = [ { url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', expected: 'dQw4w9WgXcQ' }, { url: 'https://youtu.be/dQw4w9WgXcQ', expected: 'dQw4w9WgXcQ' }, { url: 'https://www.youtube.com/shorts/dQw4w9WgXcQ', expected: 'dQw4w9WgXcQ' }, { url: 'https://www.youtube.com/embed/dQw4w9WgXcQ', expected: 'dQw4w9WgXcQ' } ]; loadBookmarklet(); if (window.__YT2AI__) { testUrls.forEach(({ url, expected }) => { const videoId = window.__YT2AI__.extractVideoId(url); expect(videoId).toBe(expected); }); } }); test('should return null for invalid URLs', () => { loadBookmarklet(); if (window.__YT2AI__) { const invalidUrls = [ 'https://www.google.com', 'https://www.youtube.com/playlist?list=123', 'https://www.youtube.com', 'not-a-url' ]; invalidUrls.forEach(url => { const videoId = window.__YT2AI__.extractVideoId(url); expect(videoId).toBeNull(); }); } }); test('should validate YouTube context successfully', () => { loadBookmarklet(); if (window.__YT2AI__) { const context = window.__YT2AI__.validateYouTubeContext(); expect(context.videoId).toBe('dQw4w9WgXcQ'); expect(context.url).toContain('youtube.com'); } }); test('should throw error for non-YouTube pages', () => { // Mock non-YouTube page Object.defineProperty(window, 'location', { value: { href: 'https://www.google.com', hostname: 'google.com' } }); expect(() => { loadBookmarklet(); }).not.toThrow(); // Initialization shouldn't throw, but validation should if (window.__YT2AI__) { expect(() => { window.__YT2AI__.validateYouTubeContext(); }).toThrow('This bookmarklet only works on YouTube pages'); } }); test('should throw error for YouTube pages without video', () => { // Mock YouTube homepage Object.defineProperty(window, 'location', { value: { href: 'https://www.youtube.com', hostname: 'youtube.com' } }); if (window.__YT2AI__) { expect(() => { window.__YT2AI__.validateYouTubeContext(); }).toThrow('Please navigate to a specific YouTube video page'); } }); }); describe('Mobile Overlay System', () => { test('should create overlay with mobile-optimized styling', () => { loadBookmarklet(); if (window.__YT2AI__) { const overlay = new window.__YT2AI__.MobileOverlay({ id: 'test-overlay', type: 'info', content: 'Test content' }); overlay.show(); // Check if overlay exists in DOM const overlayElement = document.querySelector('.yt2ai-overlay'); expect(overlayElement).toBeTruthy(); expect(overlayElement.classList.contains('yt2ai-info')).toBe(true); // Check mobile-specific styling const styles = document.getElementById('yt2ai-overlay-styles'); expect(styles).toBeTruthy(); expect(styles.textContent).toContain('95vw'); // Mobile max-width expect(styles.textContent).toContain('44px'); // Touch target size overlay.destroy(); } }); test('should handle touch events correctly', () => { loadBookmarklet(); if (window.__YT2AI__) { const overlay = new window.__YT2AI__.MobileOverlay({ id: 'touch-test', type: 'success', content: 'Touch test' }); overlay.show(); const closeBtn = document.querySelector('.yt2ai-close-btn'); expect(closeBtn).toBeTruthy(); // Simulate touch event const touchEvent = new Event('touchend', { bubbles: true }); closeBtn.dispatchEvent(touchEvent); // Should close overlay expect(overlay.isVisible).toBe(false); } }); test('should prevent body scroll when shown', () => { loadBookmarklet(); if (window.__YT2AI__) { const overlay = new window.__YT2AI__.MobileOverlay({ id: 'scroll-test', content: 'Scroll test' }); overlay.show(); expect(document.body.style.overflow).toBe('hidden'); overlay.hide(); expect(document.body.style.overflow).toBe(''); overlay.destroy(); } }); test('should auto-close when configured', (done) => { loadBookmarklet(); if (window.__YT2AI__) { const overlay = new window.__YT2AI__.MobileOverlay({ id: 'autoclose-test', content: 'Auto close test', autoClose: 100 }); overlay.show(); expect(overlay.isVisible).toBe(true); setTimeout(() => { expect(overlay.isVisible).toBe(false); done(); }, 150); } else { done(); } }); }); describe('Error Handling', () => { test('should handle and display errors with mobile-friendly UI', () => { // Mock error scenario Object.defineProperty(window, 'location', { value: { href: 'https://www.google.com', hostname: 'google.com' } }); loadBookmarklet(); // Error should be logged expect(consoleErrorSpy).toHaveBeenCalled(); // Error overlay should appear setTimeout(() => { const errorOverlay = document.querySelector('.yt2ai-error'); expect(errorOverlay).toBeTruthy(); }, 100); }); test('should cleanup properly on error', () => { // Mock error scenario and test cleanup Object.defineProperty(window, 'location', { value: { href: 'https://www.google.com', hostname: 'google.com' } }); loadBookmarklet(); if (window.__YT2AI__) { window.__YT2AI__.cleanup(); // Check cleanup expect(document.querySelectorAll('.yt2ai-overlay')).toHaveLength(0); expect(document.body.style.overflow).toBe(''); expect(window.__YT2AI__.stateManager.getState().initialized).toBe(false); } }); }); describe('State Management', () => { test('should manage state correctly', () => { loadBookmarklet(); if (window.__YT2AI__) { const stateManager = window.__YT2AI__.stateManager; // Initial state const initialState = stateManager.getState(); expect(initialState.initialized).toBe(true); expect(initialState.videoId).toBe('dQw4w9WgXcQ'); // Update state stateManager.updateState({ processing: true, currentStep: 'test' }); const updatedState = stateManager.getState(); expect(updatedState.processing).toBe(true); expect(updatedState.currentStep).toBe('test'); // Reset state stateManager.reset(); const resetState = stateManager.getState(); expect(resetState.initialized).toBe(false); expect(resetState.videoId).toBeNull(); } }); }); describe('Performance Tracking', () => { test('should track performance in development mode', () => { // Mock development environment window.location.search = '?debug=true'; loadBookmarklet(); // In test environment, performance API is not available in eval context // so trackPerformance should gracefully handle this and continue execution expect(window.__YT2AI_INITIALIZED__).toBe(true); expect(window.__YT2AI__.environment.isDevelopment).toBe(true); }); }); describe('Cleanup', () => { test('should cleanup all resources properly', () => { loadBookmarklet(); if (window.__YT2AI__) { // Create some overlays const overlay1 = new window.__YT2AI__.MobileOverlay({ id: 'test1', content: 'Test 1' }); const overlay2 = new window.__YT2AI__.MobileOverlay({ id: 'test2', content: 'Test 2' }); overlay1.show(); overlay2.show(); // Perform cleanup window.__YT2AI__.cleanup(); // Verify cleanup expect(document.querySelectorAll('.yt2ai-overlay')).toHaveLength(0); expect(document.getElementById('yt2ai-overlay-styles')).toBeNull(); expect(document.body.style.overflow).toBe(''); expect(window.__YT2AI_INITIALIZED__).toBe(false); } }); }); });