Visual drag-and-drop website builder using GrapesJS with: - Multi-page editor with live preview - File-based asset storage via PHP API (no localStorage base64) - Template library, Docker support, and Playwright test suite Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
306 lines
12 KiB
JavaScript
306 lines
12 KiB
JavaScript
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('<img') };
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.hasImg).toBe(true);
|
|
});
|
|
|
|
test('video 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('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('<p>Test save content</p>');
|
|
|
|
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('<p>Autosave test</p>');
|
|
// 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('<p>Export test content</p>');
|
|
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);
|
|
});
|
|
});
|
|
});
|