Files
site-builder/tests/integration.spec.js
Josh Knapp a71b58c2c7 Initial commit: Site Builder with PHP API backend
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>
2026-02-28 19:25:42 +00:00

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