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:
96
tests/anchor-visibility.spec.js
Normal file
96
tests/anchor-visibility.spec.js
Normal 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
17
tests/debug-check.js
Normal 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); });
|
||||
46
tests/debug-media-blocks.spec.js
Normal file
46
tests/debug-media-blocks.spec.js
Normal 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');
|
||||
});
|
||||
});
|
||||
38
tests/media-blocks-simple.spec.js
Normal file
38
tests/media-blocks-simple.spec.js
Normal 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');
|
||||
});
|
||||
52
tests/media-blocks-verify.spec.js
Normal file
52
tests/media-blocks-verify.spec.js
Normal 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');
|
||||
});
|
||||
});
|
||||
153
tests/video-background.spec.js
Normal file
153
tests/video-background.spec.js
Normal 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');
|
||||
});
|
||||
});
|
||||
386
tests/video-comprehensive.spec.js
Normal file
386
tests/video-comprehensive.spec.js
Normal 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
130
tests/youtube-embed.spec.js
Normal 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"');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user