const { test, expect } = require('@playwright/test'); const { freshEditor, addBlockById, selectComponent, openSettingsTab } = require('./helpers'); test.describe('Video System - Comprehensive', () => { test('Check for JS errors on page load', async ({ page }) => { const errors = []; page.on('pageerror', err => errors.push(err.message)); page.on('console', msg => { if (msg.type() === 'error') errors.push(msg.text()); }); await freshEditor(page); await page.waitForTimeout(2000); console.log('JS Errors:', errors); // Filter out expected 404s const realErrors = errors.filter(e => !e.includes('404') && !e.includes('Not Found')); expect(realErrors).toEqual([]); }); test('Video block: full flow - add, browse, apply, verify in preview', async ({ page }) => { await freshEditor(page); await page.waitForFunction(() => !!window.assetManager, { timeout: 10000 }); // 1. Add video block const addResult = await addBlockById(page, 'video-block'); expect(addResult.success).toBe(true); await page.waitForTimeout(500); // 2. Verify video-wrapper structure const structure = await page.evaluate(() => { const wrapper = window.editor.getWrapper(); const videoWrapper = wrapper.find('[data-video-wrapper]')[0]; if (!videoWrapper) return { error: 'video-wrapper not found' }; const children = videoWrapper.components().models.map(c => ({ tag: c.get('tagName'), classes: c.getClasses(), type: c.get('type'), })); return { type: videoWrapper.get('type'), children }; }); console.log('Video block structure:', JSON.stringify(structure, null, 2)); expect(structure.type).toBe('video-wrapper'); // 3. Select and check traits await selectComponent(page, '[data-video-wrapper]'); await page.waitForTimeout(300); await openSettingsTab(page); await page.waitForTimeout(300); const traits = await page.evaluate(() => { const sel = window.editor.getSelected(); return sel.getTraits().map(t => ({ type: t.get('type'), text: t.get('text'), name: t.get('name') })); }); console.log('Video block traits:', JSON.stringify(traits)); const hasBrowse = traits.some(t => t.text && t.text.includes('Browse Video')); expect(hasBrowse).toBe(true); // 4. Apply a direct video URL manually await page.evaluate(() => { const sel = window.editor.getSelected(); sel.addAttributes({ videoUrl: 'https://www.w3schools.com/html/mov_bbb.mp4' }); sel.trigger('change:attributes:videoUrl'); }); await page.waitForTimeout(500); // 5. Check that video-player got the src and placeholder updated const afterApply = await page.evaluate(() => { const sel = window.editor.getSelected(); const videoPlayer = sel.components().find(c => c.getClasses().includes('video-player')); const placeholder = sel.components().find(c => c.getClasses().includes('video-placeholder')); const iframe = sel.components().find(c => c.getClasses().includes('video-frame')); return { videoPlayer: { src: videoPlayer?.getAttributes()?.src, display: videoPlayer?.getStyle()?.display, }, iframe: { src: iframe?.getAttributes()?.src, display: iframe?.getStyle()?.display, }, placeholder: { display: placeholder?.getStyle()?.display, html: placeholder?.toHTML()?.substring(0, 200), }, }; }); console.log('After apply (direct file):', JSON.stringify(afterApply, null, 2)); expect(afterApply.videoPlayer.src).toBe('https://www.w3schools.com/html/mov_bbb.mp4'); expect(afterApply.videoPlayer.display).toBe('block'); // 6. Check the full HTML output includes proper video tag const html = await page.evaluate(() => { const sel = window.editor.getSelected(); return sel.toHTML(); }); console.log('Video block HTML:', html); expect(html).toContain(' { await freshEditor(page); await addBlockById(page, 'video-block'); await page.waitForTimeout(500); await selectComponent(page, '[data-video-wrapper]'); await page.waitForTimeout(300); // Apply YouTube URL await page.evaluate(() => { const sel = window.editor.getSelected(); sel.addAttributes({ videoUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' }); sel.trigger('change:attributes:videoUrl'); }); await page.waitForTimeout(500); const state = await page.evaluate(() => { const sel = window.editor.getSelected(); const iframe = sel.components().find(c => c.getClasses().includes('video-frame')); const video = sel.components().find(c => c.getClasses().includes('video-player')); const placeholder = sel.components().find(c => c.getClasses().includes('video-placeholder')); return { iframe: { src: iframe?.getAttributes()?.src, display: iframe?.getStyle()?.display }, video: { display: video?.getStyle()?.display }, placeholder: { display: placeholder?.getStyle()?.display }, }; }); console.log('YouTube apply state:', JSON.stringify(state, null, 2)); expect(state.iframe.src).toContain('youtube'); expect(state.iframe.display).toBe('block'); expect(state.video.display).toBe('none'); // Check HTML const html = await page.evaluate(() => window.editor.getSelected().toHTML()); console.log('YouTube HTML:', html.substring(0, 300)); expect(html).toContain('iframe'); expect(html).toContain('youtube'); }); test('Section (Video BG): full flow', async ({ page }) => { await freshEditor(page); await page.waitForFunction(() => !!window.assetManager, { timeout: 10000 }); await addBlockById(page, 'section-video-bg'); await page.waitForTimeout(500); // Check structure const structure = await page.evaluate(() => { const wrapper = window.editor.getWrapper(); const section = wrapper.find('[data-video-section]')[0]; if (!section) return { error: 'video-section not found' }; return { type: section.get('type'), childTypes: section.components().models.map(c => ({ classes: c.getClasses(), type: c.get('type'), })), }; }); console.log('Section Video BG structure:', JSON.stringify(structure, null, 2)); expect(structure.type).toBe('video-section'); // Select and verify traits await selectComponent(page, '[data-video-section]'); await page.waitForTimeout(300); await openSettingsTab(page); await page.waitForTimeout(300); const traits = await page.evaluate(() => { const sel = window.editor.getSelected(); return sel.getTraits().map(t => ({ type: t.get('type'), text: t.get('text'), name: t.get('name') })); }); console.log('Section Video BG traits:', JSON.stringify(traits)); expect(traits.some(t => t.text?.includes('Browse Video'))).toBe(true); // Apply video and verify it propagates to bg-video-wrapper await page.evaluate(() => { const sel = window.editor.getSelected(); sel.addAttributes({ videoUrl: 'https://www.w3schools.com/html/mov_bbb.mp4' }); sel.trigger('change:attributes:videoUrl'); }); await page.waitForTimeout(500); const bgState = await page.evaluate(() => { const sel = window.editor.getSelected(); const bgWrapper = sel.components().find(c => c.getAttributes()['data-bg-video'] === 'true'); if (!bgWrapper) return { error: 'bg-video-wrapper not found' }; const bgVideo = bgWrapper.components().find(c => c.getClasses().includes('bg-video-player')); const bgIframe = bgWrapper.components().find(c => c.getClasses().includes('bg-video-frame')); const bgPlaceholder = bgWrapper.components().find(c => c.getClasses().includes('bg-video-placeholder')); return { bgVideo: { src: bgVideo?.getAttributes()?.src, display: bgVideo?.getStyle()?.display }, bgIframe: { display: bgIframe?.getStyle()?.display }, bgPlaceholder: { display: bgPlaceholder?.getStyle()?.display }, }; }); console.log('Section Video BG after apply:', JSON.stringify(bgState, null, 2)); expect(bgState.bgVideo.src).toBe('https://www.w3schools.com/html/mov_bbb.mp4'); expect(bgState.bgVideo.display).toBe('block'); // Check HTML output const html = await page.evaluate(() => window.editor.getSelected().toHTML()); console.log('Section Video BG HTML:', html.substring(0, 500)); expect(html).toContain('video'); expect(html).toContain('mov_bbb.mp4'); }); test('Hero (Video): full flow', async ({ page }) => { await freshEditor(page); await page.waitForFunction(() => !!window.assetManager, { timeout: 10000 }); await addBlockById(page, 'hero-video'); await page.waitForTimeout(500); // Check it uses video-section type const structure = await page.evaluate(() => { const wrapper = window.editor.getWrapper(); const section = wrapper.find('[data-video-section]')[0]; if (!section) return { error: 'video-section not found' }; return { type: section.get('type'), hasOverlay: !!section.components().find(c => c.getClasses().includes('bg-overlay')), hasContent: !!section.components().find(c => c.getClasses().includes('bg-content')), hasBgVideo: !!section.components().find(c => c.getAttributes()['data-bg-video'] === 'true'), }; }); console.log('Hero Video structure:', JSON.stringify(structure, null, 2)); expect(structure.type).toBe('video-section'); expect(structure.hasBgVideo).toBe(true); expect(structure.hasContent).toBe(true); // Select and apply video await selectComponent(page, '[data-video-section]'); await page.waitForTimeout(300); await openSettingsTab(page); await page.waitForTimeout(300); // Verify browse button exists const browseBtn = page.locator('.panel-right button', { hasText: 'Browse Video Assets' }); await expect(browseBtn).toBeVisible(); // Apply video await page.evaluate(() => { const sel = window.editor.getSelected(); sel.addAttributes({ videoUrl: 'https://www.w3schools.com/html/mov_bbb.mp4' }); sel.trigger('change:attributes:videoUrl'); }); await page.waitForTimeout(500); const html = await page.evaluate(() => window.editor.getSelected().toHTML()); console.log('Hero Video HTML:', html.substring(0, 500)); expect(html).toContain('video'); expect(html).toContain('mov_bbb.mp4'); expect(html).toContain('Video Background Hero'); }); test('Preview renders video correctly', async ({ page, context }) => { await freshEditor(page); // Add a video block and set URL await addBlockById(page, 'video-block'); await page.waitForTimeout(500); await selectComponent(page, '[data-video-wrapper]'); await page.waitForTimeout(300); await page.evaluate(() => { const sel = window.editor.getSelected(); sel.addAttributes({ videoUrl: 'https://www.w3schools.com/html/mov_bbb.mp4' }); sel.trigger('change:attributes:videoUrl'); }); await page.waitForTimeout(500); // Check model state: placeholder hidden, video visible const modelState = await page.evaluate(() => { const sel = window.editor.getSelected(); const videoPlayer = sel.components().find(c => c.getClasses().includes('video-player')); const placeholder = sel.components().find(c => c.getClasses().includes('video-placeholder')); return { videoDisplay: videoPlayer?.getStyle()?.display, videoSrc: videoPlayer?.getAttributes()?.src, placeholderDisplay: placeholder?.getStyle()?.display, }; }); console.log('Model state for export:', JSON.stringify(modelState)); // Model: video shown, placeholder hidden (correct for preview/export) expect(modelState.videoDisplay).toBe('block'); expect(modelState.videoSrc).toBe('https://www.w3schools.com/html/mov_bbb.mp4'); expect(modelState.placeholderDisplay).toBe('none'); // Check HTML output includes proper video tag with src const exportedHtml = await page.evaluate(() => { const wrapper = window.editor.getWrapper(); return wrapper.toHTML(); }); console.log('Exported HTML snippet:', exportedHtml.substring(0, 500)); expect(exportedHtml).toContain(' window.editor.getCss()); console.log('CSS for video-player:', exportedCss.match(/video-player[^}]+}/)?.[0]?.substring(0, 200)); console.log('CSS for placeholder:', exportedCss.match(/video-placeholder[^}]+}/)?.[0]?.substring(0, 200)); expect(exportedCss).toContain('display:block'); }); test('Browse modal opens and shows server assets', async ({ page }) => { await freshEditor(page); await page.waitForFunction(() => !!window.assetManager, { timeout: 10000 }); // Check server has assets const serverAssets = await page.evaluate(async () => { const resp = await fetch('/api/assets'); const data = await resp.json(); return data.assets.filter(a => a.type === 'video'); }); console.log('Server video assets:', serverAssets.map(a => a.name)); // Add video block and open browse await addBlockById(page, 'video-block'); await page.waitForTimeout(500); await selectComponent(page, '[data-video-wrapper]'); await page.waitForTimeout(300); await openSettingsTab(page); await page.waitForTimeout(300); const browseBtn = page.locator('.panel-right button', { hasText: 'Browse Video Assets' }); await browseBtn.click(); await page.waitForTimeout(1500); const modalState = await page.evaluate(() => { const modal = document.getElementById('asset-browser-modal'); const grid = modal?.querySelector('#asset-browser-grid'); const items = grid?.querySelectorAll('.asset-browser-item'); return { visible: modal?.classList.contains('visible'), activeTab: modal?.querySelector('.asset-tab.active')?.dataset.type, itemCount: items?.length || 0, items: Array.from(items || []).map(i => i.querySelector('.asset-browser-item-name')?.textContent), }; }); console.log('Browse modal state:', JSON.stringify(modalState, null, 2)); expect(modalState.visible).toBe(true); expect(modalState.activeTab).toBe('video'); // If we have items, select one if (modalState.itemCount > 0) { await page.locator('#asset-browser-grid .asset-browser-item').first().click(); await page.waitForTimeout(500); const appliedUrl = await page.evaluate(() => { const sel = window.editor.getSelected(); return sel?.getAttributes()?.videoUrl; }); console.log('Applied URL from browse:', appliedUrl); expect(appliedUrl).toBeTruthy(); } }); test('No duplicate image/video blocks in Basic section', async ({ page }) => { await freshEditor(page); const blocks = await page.evaluate(() => { return window.editor.BlockManager.getAll().map(b => ({ id: b.id, label: b.get('label'), category: typeof b.get('category') === 'string' ? b.get('category') : b.get('category')?.id || b.get('category')?.label || 'unknown', })); }); const basicBlocks = blocks.filter(b => b.category === 'Basic'); const mediaBlocks = blocks.filter(b => b.category === 'Media'); console.log('Basic blocks:', basicBlocks.map(b => b.id)); console.log('Media blocks:', mediaBlocks.map(b => b.id)); // No 'image' or 'video' in Basic (only 'image-block' and 'video-block' in Media) expect(basicBlocks.find(b => b.id === 'image')).toBeUndefined(); expect(basicBlocks.find(b => b.id === 'video')).toBeUndefined(); expect(mediaBlocks.find(b => b.id === 'image-block')).toBeTruthy(); expect(mediaBlocks.find(b => b.id === 'video-block')).toBeTruthy(); }); });