Includes new page templates (fitness-gym, nonprofit, online-course, photography-studio, real-estate, startup-company, travel-blog, wedding-invitation) with thumbnail SVGs, test specs, documentation files, and minor updates to index.html, router.php, and playwright config. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
387 lines
18 KiB
JavaScript
387 lines
18 KiB
JavaScript
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('<video');
|
|
expect(html).toContain('mov_bbb.mp4');
|
|
});
|
|
|
|
test('Video block: YouTube URL applies correctly', async ({ page }) => {
|
|
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('<video');
|
|
expect(exportedHtml).toContain('mov_bbb.mp4');
|
|
|
|
// Check CSS output: video-player display:block, placeholder display:none
|
|
const exportedCss = await page.evaluate(() => 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();
|
|
});
|
|
});
|