const { test, expect } = require('@playwright/test');
const EDITOR_LOAD_TIMEOUT = 15000;
async function waitForEditor(page) {
await page.goto('/', { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => window.editor && window.editor.getHtml, { timeout: EDITOR_LOAD_TIMEOUT });
}
// Helper: get block element from the block panel by label
async function getBlockElement(page, label) {
// GrapesJS blocks have title attributes or contain text matching the label
return page.locator(`.gjs-block[title="${label}"], .gjs-block:has-text("${label}")`).first();
}
// Helper: get the canvas iframe body
async function getCanvasBody(page) {
const frame = page.frameLocator('.gjs-frame');
return frame.locator('body');
}
test.describe('Site Builder Integration Tests', () => {
test.describe('Block Drag & Drop', () => {
test('text block can be added to canvas', async ({ page }) => {
await waitForEditor(page);
// Use GrapesJS API to add a text block
const result = await page.evaluate(() => {
const editor = window.editor;
const block = editor.BlockManager.get('text-block');
if (!block) return { error: 'text-block not found' };
// Add component directly
const comp = editor.addComponents(block.get('content'));
return { success: true, count: editor.getComponents().length };
});
expect(result.success).toBe(true);
expect(result.count).toBeGreaterThan(0);
});
test('anchor block can be added to canvas', async ({ page }) => {
await waitForEditor(page);
const result = await page.evaluate(() => {
const editor = window.editor;
const block = editor.BlockManager.get('anchor-point');
if (!block) return { error: 'anchor-point block not found', blocks: editor.BlockManager.getAll().map(b => b.id) };
const comp = editor.addComponents(block.get('content'));
const html = editor.getHtml();
return { success: true, hasAnchor: html.includes('data-anchor'), html: html.substring(0, 200) };
});
expect(result.error).toBeUndefined();
expect(result.success).toBe(true);
expect(result.hasAnchor).toBe(true);
});
test('image block can be added to canvas', async ({ page }) => {
await waitForEditor(page);
const result = await page.evaluate(() => {
const editor = window.editor;
const block = editor.BlockManager.get('image-block');
if (!block) return { error: 'image-block not found' };
editor.addComponents(block.get('content'));
return { success: true, hasImg: editor.getHtml().includes(' {
await waitForEditor(page);
const result = await page.evaluate(() => {
const editor = window.editor;
const block = editor.BlockManager.get('video-block');
if (!block) return { error: 'video-block not found' };
editor.addComponents(block.get('content'));
return { success: true, hasVideo: editor.getHtml().includes('video-wrapper') };
});
expect(result.success).toBe(true);
expect(result.hasVideo).toBe(true);
});
test('all expected blocks are registered', async ({ page }) => {
await waitForEditor(page);
const blocks = await page.evaluate(() => {
return window.editor.BlockManager.getAll().map(b => b.id);
});
const expectedBlocks = ['text-block', 'heading', 'button-block', 'anchor-point', 'image-block', 'video-block', 'section', 'footer'];
for (const name of expectedBlocks) {
expect(blocks, `Missing block: ${name}`).toContain(name);
}
});
});
test.describe('Save & Load', () => {
test('save works with localStorage fallback (no WHP API)', async ({ page }) => {
await waitForEditor(page);
// Wait for whpInt to initialize
await page.waitForFunction(() => !!window.whpInt, { timeout: 10000 });
// Add some content then save
const result = await page.evaluate(async () => {
const editor = window.editor;
editor.addComponents('
Test save content
'); const site = await window.whpInt.saveToWHP(null, 'Test Site'); const stored = localStorage.getItem('whp-sites'); return { success: !!site, stored: !!stored, siteName: site?.name }; }); expect(result.success).toBe(true); expect(result.stored).toBe(true); expect(result.siteName).toBe('Test Site'); }); test('GrapesJS autosave to localStorage works', async ({ page }) => { await waitForEditor(page); const result = await page.evaluate(async () => { const editor = window.editor; editor.addComponents('Autosave test
'); // Trigger storage save and wait await editor.store(); // GrapesJS may use a different key format const keys = Object.keys(localStorage).filter(k => k.includes('sitebuilder') || k.includes('gjsProject')); const stored = keys.length > 0 || !!localStorage.getItem('sitebuilder-project'); return { hasData: stored, keys }; }); expect(result.hasData).toBe(true); }); }); test.describe('Asset Management', () => { test('asset manager initializes', async ({ page }) => { await waitForEditor(page); const result = await page.evaluate(() => { return { hasAssetManager: !!window.assetManager, assetsArray: Array.isArray(window.assetManager?.assets) }; }); expect(result.hasAssetManager).toBe(true); expect(result.assetsArray).toBe(true); }); test('can add image asset programmatically', async ({ page }) => { await waitForEditor(page); const result = await page.evaluate(() => { const am = window.assetManager; if (!am) return { error: 'no asset manager' }; const asset = am.addAssetUrl('https://example.com/photo.jpg'); return { type: asset.type, name: asset.name, count: am.assets.length }; }); expect(result.type).toBe('image'); expect(result.name).toBe('photo.jpg'); expect(result.count).toBeGreaterThan(0); }); test('can add video asset programmatically', async ({ page }) => { await waitForEditor(page); const result = await page.evaluate(() => { const am = window.assetManager; if (!am) return { error: 'no asset manager' }; const asset = am.addAssetUrl('https://example.com/clip.mp4'); return { type: asset.type, name: asset.name }; }); expect(result.type).toBe('video'); expect(result.name).toBe('clip.mp4'); }); test('asset browser filters video assets correctly', async ({ page }) => { await waitForEditor(page); const result = await page.evaluate(() => { const am = window.assetManager; if (!am) return { error: 'no asset manager' }; // Clear and add test assets am.assets = []; am.addAssetUrl('https://example.com/photo.jpg'); am.addAssetUrl('https://example.com/clip.mp4'); am.addAssetUrl('https://example.com/movie.webm'); am.addAssetUrl('https://example.com/style.css'); const videoAssets = am.assets.filter(a => a.type === 'video' || (a.url && a.url.match(/\.(mp4|webm|ogg|mov|avi)$/i))); const imageAssets = am.assets.filter(a => a.type === 'image'); return { videos: videoAssets.length, images: imageAssets.length, total: am.assets.length }; }); expect(result.videos).toBe(2); expect(result.images).toBe(1); expect(result.total).toBe(4); }); test('asset browser modal opens and shows assets', async ({ page }) => { await waitForEditor(page); // Add assets then open browser await page.evaluate(() => { const am = window.assetManager; am.assets = []; am.addAssetUrl('https://example.com/photo.jpg'); am.addAssetUrl('https://example.com/clip.mp4'); }); // Open the browser for images await page.evaluate(() => { window.assetManager.openBrowser('image'); }); // Modal should be visible const modal = page.locator('#asset-browser-modal.visible'); await expect(modal).toBeVisible({ timeout: 3000 }); // Should show image items const items = page.locator('#asset-browser-grid .asset-browser-item'); await expect(items).toHaveCount(1); // Switch to video tab await page.click('.asset-tab[data-type="video"]'); await expect(items).toHaveCount(1); // Switch to all tab await page.click('.asset-tab[data-type="all"]'); await expect(items).toHaveCount(2); }); }); test.describe('Templates', () => { test('template list loads', async ({ page }) => { await waitForEditor(page); // Check if template modal button exists const templateBtn = page.locator('#btn-templates, button:has-text("Templates")'); const exists = await templateBtn.count(); expect(exists).toBeGreaterThan(0); }); }); test.describe('Export', () => { test('can export HTML', async ({ page }) => { await waitForEditor(page); // Add content and export const result = await page.evaluate(() => { const editor = window.editor; editor.addComponents('Export test content
'); const html = editor.getHtml(); const css = editor.getCss(); return { hasHtml: html.length > 0, hasCss: typeof css === 'string', htmlContains: html.includes('Export test content') }; }); expect(result.hasHtml).toBe(true); expect(result.hasCss).toBe(true); expect(result.htmlContains).toBe(true); }); }); test.describe('Editor Core', () => { test('editor loads without errors', async ({ page }) => { const errors = []; page.on('pageerror', err => errors.push(err.message)); await waitForEditor(page); // Filter out non-critical errors (network errors for fonts etc are ok) const criticalErrors = errors.filter(e => !e.includes('net::') && !e.includes('Failed to load resource')); expect(criticalErrors).toHaveLength(0); }); test('canvas iframe is present', async ({ page }) => { await waitForEditor(page); const frame = page.locator('.gjs-frame'); await expect(frame).toBeVisible(); }); test('block panel is visible', async ({ page }) => { await waitForEditor(page); const blocks = page.locator('#blocks-container .gjs-block'); const count = await blocks.count(); expect(count).toBeGreaterThan(5); }); }); });