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,96 @@
const { test, expect } = require('@playwright/test');
const { waitForEditor, addBlockById, clearCanvas } = require('./helpers');
test.describe('Anchor Point Visibility', () => {
test.beforeEach(async ({ page }) => {
await waitForEditor(page);
});
test('should show anchors in editor mode', async ({ page }) => {
// Add an anchor point via the API
const result = await addBlockById(page, 'anchor-point');
expect(result.success).toBe(true);
await page.waitForTimeout(500);
// Check that anchor is visible in editor canvas
const canvas = page.frameLocator('#gjs iframe').first();
const anchor = canvas.locator('[data-anchor="true"]').first();
await expect(anchor).toBeVisible({ timeout: 5000 });
// Verify it has the editor-anchor class
const anchorClass = await anchor.getAttribute('class');
expect(anchorClass).toContain('editor-anchor');
// Verify it has visual styling (border exists via computed style)
const borderStyle = await anchor.evaluate(el => window.getComputedStyle(el).borderStyle);
expect(borderStyle).toBeTruthy();
});
test('should hide anchors in preview mode', async ({ page, context }) => {
// Add an anchor point via API
const result = await addBlockById(page, 'anchor-point');
expect(result.success).toBe(true);
await page.waitForTimeout(500);
// Wait for auto-save to persist changes
await page.waitForTimeout(2000);
// Open preview in new page
const [previewPage] = await Promise.all([
context.waitForEvent('page'),
page.locator('#btn-preview').click()
]);
await previewPage.waitForLoadState('domcontentloaded');
await previewPage.waitForTimeout(1000);
// Check that anchor exists in DOM but is hidden
const anchor = previewPage.locator('[data-anchor="true"]').first();
if (await anchor.count() > 0) {
await expect(anchor).toBeHidden();
}
// If anchor was stripped entirely, that's also valid
});
test('should remove anchors from exported HTML', async ({ page }) => {
// Add an anchor point via API
const result = await addBlockById(page, 'anchor-point');
expect(result.success).toBe(true);
await page.waitForTimeout(500);
// Verify the export process strips anchors
const exportedHtml = await page.evaluate(() => {
const editor = window.editor;
let html = editor.getHtml();
// The export process strips anchors via regex (same as generatePageHtml)
html = html.replace(/<div[^>]*data-anchor="true"[^>]*>[\s\S]*?<\/div>/g, '');
html = html.replace(/<div[^>]*class="editor-anchor"[^>]*>[\s\S]*?<\/div>/g, '');
return html;
});
// Verify no anchor elements remain in cleaned HTML
expect(exportedHtml).not.toMatch(/data-anchor="true"/);
expect(exportedHtml).not.toMatch(/class="editor-anchor"/);
});
test('anchor should have visual indicator with ID in editor', async ({ page }) => {
// Add an anchor point via API
const result = await addBlockById(page, 'anchor-point');
expect(result.success).toBe(true);
await page.waitForTimeout(500);
// Select the anchor
const canvas = page.frameLocator('#gjs iframe').first();
const anchor = canvas.locator('[data-anchor="true"]').first();
await expect(anchor).toBeVisible({ timeout: 5000 });
await anchor.click();
// Verify the anchor has the icon span (not ::before pseudo-element)
const anchorIcon = canvas.locator('[data-anchor="true"] .anchor-icon').first();
await expect(anchorIcon).toBeVisible();
// Verify anchor has an input for the name
const anchorInput = canvas.locator('[data-anchor="true"] .anchor-name-input').first();
await expect(anchorInput).toBeVisible();
});
});

17
tests/debug-check.js Normal file
View File

@@ -0,0 +1,17 @@
const pw = require('playwright-core');
(async () => {
const browser = await pw.chromium.launch({ headless: true });
const page = await browser.newPage();
const failed = [];
page.on('requestfailed', req => failed.push(req.url().substring(0,100)));
await page.goto('/', {timeout:30000});
await page.waitForTimeout(15000);
console.log('editor:', await page.evaluate(() => typeof window.editor));
console.log('grapesjs:', await page.evaluate(() => typeof grapesjs));
if (failed.length) console.log('Failed requests:', JSON.stringify(failed));
await browser.close();
process.exit(0);
})().catch(e => { console.error(e.message); process.exit(1); });

View File

@@ -0,0 +1,46 @@
const { test, expect } = require('@playwright/test');
const { waitForEditor } = require('./helpers');
test.describe('Debug Media Category Blocks', () => {
test('investigate what blocks are in Media category', async ({ page }) => {
await waitForEditor(page);
// Check all registered blocks via the GrapesJS API
const blockManagerInfo = await page.evaluate(() => {
const editor = window.editor;
if (!editor) return { error: 'No editor found' };
const blockManager = editor.BlockManager;
const allBlocks = blockManager.getAll();
const blockInfo = allBlocks.map(block => {
const cat = block.get('category');
return {
id: block.id,
label: block.get('label'),
category: typeof cat === 'string' ? cat : (cat && cat.id),
};
});
const mediaBlocks = blockInfo.filter(b => b.category === 'Media');
return {
totalBlocks: allBlocks.length,
blocks: blockInfo,
mediaBlocks: mediaBlocks
};
});
console.log('Total blocks registered:', blockManagerInfo.totalBlocks);
console.log('Media category blocks:', JSON.stringify(blockManagerInfo.mediaBlocks, null, 2));
// Verify we have at least 3 media blocks
expect(blockManagerInfo.mediaBlocks.length).toBeGreaterThanOrEqual(3);
// Verify expected blocks are present
const mediaIds = blockManagerInfo.mediaBlocks.map(b => b.id);
expect(mediaIds).toContain('image-block');
expect(mediaIds).toContain('video-block');
expect(mediaIds).toContain('file-embed');
});
});

View File

@@ -0,0 +1,38 @@
const { test, expect } = require('@playwright/test');
const { waitForEditor, addBlockById } = require('./helpers');
test('Media category should have 3 blocks: Image, Video, File/PDF', async ({ page }) => {
await waitForEditor(page);
// Verify all 3 media blocks are registered
const mediaBlocks = await page.evaluate(() => {
const editor = window.editor;
const bm = editor.BlockManager;
return {
image: !!bm.get('image-block'),
video: !!bm.get('video-block'),
fileEmbed: !!bm.get('file-embed'),
};
});
expect(mediaBlocks.image).toBe(true);
expect(mediaBlocks.video).toBe(true);
expect(mediaBlocks.fileEmbed).toBe(true);
// Verify they are in the Media category
const categories = await page.evaluate(() => {
const editor = window.editor;
const bm = editor.BlockManager;
return ['image-block', 'video-block', 'file-embed'].map(id => {
const block = bm.get(id);
const cat = block.get('category');
return { id, category: typeof cat === 'string' ? cat : (cat && cat.id) };
});
});
for (const block of categories) {
expect(block.category).toBe('Media');
}
console.log('All 3 Media blocks verified: Image, Video, File/PDF');
});

View File

@@ -0,0 +1,52 @@
const { test, expect } = require('@playwright/test');
const { waitForEditor, addBlockById, clearCanvas } = require('./helpers');
test.describe('Media Category Blocks Verification', () => {
test('should show Image, Video, and File/PDF blocks in Media category', async ({ page }) => {
await waitForEditor(page);
// Verify all three blocks exist and are in Media category
const mediaBlocks = await page.evaluate(() => {
const editor = window.editor;
const bm = editor.BlockManager;
return ['image-block', 'video-block', 'file-embed'].map(id => {
const block = bm.get(id);
if (!block) return { id, found: false };
const cat = block.get('category');
return {
id,
found: true,
label: block.get('label'),
category: typeof cat === 'string' ? cat : (cat && cat.id),
};
});
});
for (const block of mediaBlocks) {
expect(block.found).toBe(true);
expect(block.category).toBe('Media');
}
console.log('All Media blocks are registered!');
// Test adding Image block via API
const imgResult = await addBlockById(page, 'image-block');
expect(imgResult.success).toBe(true);
const canvas = page.frameLocator('#gjs iframe').first();
const img = canvas.locator('img');
await expect(img).toBeVisible({ timeout: 3000 });
console.log('Image block added successfully');
// Clear canvas
await clearCanvas(page);
// Test adding Video block via API
const vidResult = await addBlockById(page, 'video-block');
expect(vidResult.success).toBe(true);
const videoWrapper = canvas.locator('.video-wrapper');
await expect(videoWrapper).toBeVisible({ timeout: 3000 });
console.log('Video block added successfully');
});
});

View File

@@ -0,0 +1,153 @@
const { test, expect } = require('@playwright/test');
const { waitForEditor, addBlockById, clearCanvas } = require('./helpers');
test.describe('Video Background Section', () => {
test.beforeEach(async ({ page }) => {
await waitForEditor(page);
await clearCanvas(page);
});
test('should add video background section and set YouTube URL', async ({ page }) => {
// Step 1: Add Video Background Section via API
const result = await addBlockById(page, 'section-video-bg');
expect(result.success).toBe(true);
await page.waitForTimeout(500);
// Step 2: Verify section exists in HTML
let html = await page.evaluate(() => window.editor.getHtml());
expect(html).toContain('data-video-section="true"');
// Step 3: Select the section via API
await page.evaluate(() => {
const editor = window.editor;
const wrapper = editor.getWrapper();
const section = wrapper.find('[data-video-section]')[0];
if (section) editor.select(section);
});
await page.waitForTimeout(500);
// Step 4: Open Settings panel
await page.locator('button[data-panel="traits"]').click();
await page.waitForTimeout(500);
// Step 5: Find Video URL input in traits container
const videoUrlInput = page.locator('#traits-container input[placeholder*="YouTube"]');
await expect(videoUrlInput).toBeVisible({ timeout: 5000 });
// Step 6: Enter YouTube URL
const testVideoUrl = 'https://www.youtube.com/watch?v=OC7sNfNuTNU';
await videoUrlInput.fill(testVideoUrl);
await videoUrlInput.press('Enter');
await page.waitForTimeout(1000);
// Click "Apply Video" button if present
const applyBtn = page.locator('#traits-container button:has-text("Apply Video")');
if (await applyBtn.isVisible({ timeout: 1000 }).catch(() => false)) {
await applyBtn.click();
await page.waitForTimeout(1000);
}
// Step 7: Verify via editor HTML
html = await page.evaluate(() => window.editor.getHtml());
expect(html).toContain('OC7sNfNuTNU');
expect(html).toContain('youtube');
// Step 8: Background videos SHOULD have autoplay=1
expect(html).toContain('autoplay=1');
});
test('should handle video URL changes', async ({ page }) => {
// Add video background section
const result = await addBlockById(page, 'section-video-bg');
expect(result.success).toBe(true);
await page.waitForTimeout(500);
// Select section via API
await page.evaluate(() => {
const editor = window.editor;
const wrapper = editor.getWrapper();
const section = wrapper.find('[data-video-section]')[0];
if (section) editor.select(section);
});
await page.waitForTimeout(500);
// Open Settings
await page.locator('button[data-panel="traits"]').click();
await page.waitForTimeout(500);
// Find video URL input
const videoUrlInput = page.locator('#traits-container input[placeholder*="YouTube"]');
await expect(videoUrlInput).toBeVisible({ timeout: 5000 });
// Enter first video
await videoUrlInput.fill('https://www.youtube.com/watch?v=OC7sNfNuTNU');
await videoUrlInput.press('Enter');
await page.waitForTimeout(500);
const applyBtn = page.locator('#traits-container button:has-text("Apply Video")');
if (await applyBtn.isVisible({ timeout: 1000 }).catch(() => false)) {
await applyBtn.click();
await page.waitForTimeout(1000);
}
let html = await page.evaluate(() => window.editor.getHtml());
expect(html).toContain('OC7sNfNuTNU');
// Change to different video
await videoUrlInput.fill('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
await videoUrlInput.press('Enter');
await page.waitForTimeout(500);
if (await applyBtn.isVisible({ timeout: 1000 }).catch(() => false)) {
await applyBtn.click();
await page.waitForTimeout(1000);
}
// Verify HTML changed
html = await page.evaluate(() => window.editor.getHtml());
expect(html).toContain('dQw4w9WgXcQ');
expect(html).not.toContain('OC7sNfNuTNU');
});
test('should work with direct video files', async ({ page }) => {
// Add video background section
const result = await addBlockById(page, 'section-video-bg');
expect(result.success).toBe(true);
await page.waitForTimeout(500);
// Select section via API
await page.evaluate(() => {
const editor = window.editor;
const wrapper = editor.getWrapper();
const section = wrapper.find('[data-video-section]')[0];
if (section) editor.select(section);
});
await page.waitForTimeout(500);
// Open Settings
await page.locator('button[data-panel="traits"]').click();
await page.waitForTimeout(500);
// Find video URL input
const videoUrlInput = page.locator('#traits-container input[placeholder*="YouTube"]');
await expect(videoUrlInput).toBeVisible({ timeout: 5000 });
// Enter direct .mp4 URL
const mp4Url = 'https://www.w3schools.com/html/mov_bbb.mp4';
await videoUrlInput.fill(mp4Url);
await videoUrlInput.press('Enter');
await page.waitForTimeout(500);
const applyBtn = page.locator('#traits-container button:has-text("Apply Video")');
if (await applyBtn.isVisible({ timeout: 1000 }).catch(() => false)) {
await applyBtn.click();
await page.waitForTimeout(1000);
}
// For direct video, the HTML should have the video element with the src
const html = await page.evaluate(() => window.editor.getHtml());
expect(html).toContain(mp4Url);
// The iframe should have display:none (hidden)
expect(html).toContain('bg-video-player');
});
});

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();
});
});

130
tests/youtube-embed.spec.js Normal file
View File

@@ -0,0 +1,130 @@
const { test, expect } = require('@playwright/test');
const { waitForEditor, addBlockById, clearCanvas } = require('./helpers');
test.describe('YouTube Embed Fix (Error 153)', () => {
test.beforeEach(async ({ page }) => {
await waitForEditor(page);
await clearCanvas(page);
});
test('should use youtube-nocookie.com domain for YouTube embeds', async ({ page }) => {
// Add Video block via API
const result = await addBlockById(page, 'video-block');
expect(result.success).toBe(true);
await page.waitForTimeout(500);
// Select the video wrapper via API
await page.evaluate(() => {
const editor = window.editor;
const wrapper = editor.getWrapper();
const videoWrapper = wrapper.find('[data-video-wrapper]')[0];
if (videoWrapper) editor.select(videoWrapper);
});
await page.waitForTimeout(500);
// Open Settings tab
await page.locator('button[data-panel="traits"]').click();
await page.waitForTimeout(500);
// Find Video URL input in traits container
const videoUrlInput = page.locator('#traits-container input[placeholder*="YouTube"]');
await expect(videoUrlInput).toBeVisible({ timeout: 5000 });
const testYouTubeUrl = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ';
await videoUrlInput.fill(testYouTubeUrl);
await videoUrlInput.press('Enter');
await page.waitForTimeout(1000);
// Click "Apply Video" button if present
const applyBtn = page.locator('#traits-container button:has-text("Apply Video")');
if (await applyBtn.isVisible({ timeout: 1000 }).catch(() => false)) {
await applyBtn.click();
await page.waitForTimeout(1000);
}
// Verify via editor HTML output (canvas renders iframes as divs)
const html = await page.evaluate(() => window.editor.getHtml());
expect(html).toContain('youtube-nocookie.com');
expect(html).toContain('embed/dQw4w9WgXcQ');
expect(html).not.toContain('www.youtube.com/embed');
});
test('should have correct referrerpolicy attribute', async ({ page }) => {
// Add Video block and check via editor HTML
const result = await addBlockById(page, 'video-block');
expect(result.success).toBe(true);
await page.waitForTimeout(500);
const html = await page.evaluate(() => window.editor.getHtml());
expect(html).toContain('referrerpolicy="strict-origin-when-cross-origin"');
});
test('should have required allow attributes', async ({ page }) => {
// Add Video block and check via editor HTML
const result = await addBlockById(page, 'video-block');
expect(result.success).toBe(true);
await page.waitForTimeout(500);
const html = await page.evaluate(() => window.editor.getHtml());
expect(html).toContain('accelerometer');
expect(html).toContain('encrypted-media');
expect(html).toContain('gyroscope');
expect(html).toContain('picture-in-picture');
});
test('should work with youtu.be short URLs', async ({ page }) => {
// Add Video block
const result = await addBlockById(page, 'video-block');
expect(result.success).toBe(true);
await page.waitForTimeout(500);
// Select the wrapper
await page.evaluate(() => {
const editor = window.editor;
const wrapper = editor.getWrapper();
const videoWrapper = wrapper.find('[data-video-wrapper]')[0];
if (videoWrapper) editor.select(videoWrapper);
});
await page.waitForTimeout(500);
// Open Settings
await page.locator('button[data-panel="traits"]').click();
await page.waitForTimeout(500);
// Fill URL
const videoUrlInput = page.locator('#traits-container input[placeholder*="YouTube"]');
await expect(videoUrlInput).toBeVisible({ timeout: 5000 });
await videoUrlInput.fill('https://youtu.be/dQw4w9WgXcQ');
await videoUrlInput.press('Enter');
await page.waitForTimeout(1000);
// Click Apply if present
const applyBtn = page.locator('#traits-container button:has-text("Apply Video")');
if (await applyBtn.isVisible({ timeout: 1000 }).catch(() => false)) {
await applyBtn.click();
await page.waitForTimeout(1000);
}
// Verify via editor HTML
const html = await page.evaluate(() => window.editor.getHtml());
expect(html).toContain('youtube-nocookie.com');
expect(html).toContain('embed/dQw4w9WgXcQ');
});
test('video block uses unified Video element with iframe and video tags', async ({ page }) => {
// Add Video block and verify structure via editor HTML
const result = await addBlockById(page, 'video-block');
expect(result.success).toBe(true);
await page.waitForTimeout(500);
const html = await page.evaluate(() => window.editor.getHtml());
// The block should contain both iframe and video elements
expect(html).toContain('<iframe');
expect(html).toContain('class="video-frame"');
expect(html).toContain('<video');
expect(html).toContain('class="video-player"');
expect(html).toContain('class="video-wrapper"');
});
});