Add templates, tests, and miscellaneous project files

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>
This commit is contained in:
2026-03-01 14:15:58 -08:00
parent 03f573b451
commit b511a6684d
61 changed files with 6919 additions and 6 deletions

View File

@@ -0,0 +1,386 @@
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();
});
});