/** * Mock WHP site-builder API + static server for the real-editor colour-system e2e * (scripts/e2e-color-system.mjs). Unlike the parity mock it STORES what the editor * saves (so a reload restores it), and serves a same-origin PNG through the * list_assets / upload_asset endpoints in the shape the real api/site-builder.php returns. */ import http from 'node:http'; import fs from 'node:fs'; import path from 'node:path'; import zlib from 'node:zlib'; function crc32(buf) { let c, crc = 0xffffffff; for (let n = 0; n < buf.length; n++) { c = (crc ^ buf[n]) & 0xff; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; crc = (crc >>> 8) ^ c; } return (crc ^ 0xffffffff) >>> 0; } function chunk(type, data) { const len = Buffer.alloc(4); len.writeUInt32BE(data.length); const td = Buffer.concat([Buffer.from(type), data]); const crc = Buffer.alloc(4); crc.writeUInt32BE(crc32(td)); return Buffer.concat([len, td, crc]); } /** w*h RGB PNG; px(x,y) -> [r,g,b]. */ export function makePng(w, h, px) { const raw = Buffer.alloc((w * 3 + 1) * h); for (let y = 0; y < h; y++) { raw[y * (w * 3 + 1)] = 0; for (let x = 0; x < w; x++) { const [r, g, b] = px(x, y); const o = y * (w * 3 + 1) + 1 + x * 3; raw[o] = r; raw[o + 1] = g; raw[o + 2] = b; } } const ihdr = Buffer.alloc(13); ihdr.writeUInt32BE(w, 0); ihdr.writeUInt32BE(h, 4); ihdr[8] = 8; ihdr[9] = 2; return Buffer.concat([Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), chunk('IHDR', ihdr), chunk('IDAT', zlib.deflateSync(raw)), chunk('IEND', Buffer.alloc(0))]); } /** 100x100 test image: four flat quadrants + a few colour bands so palette extraction has >= 5 colours. */ export const TEST_PNG_COLORS = { tl: [255, 0, 0], tr: [0, 0, 255], bl: [0, 160, 0], br: [255, 200, 0], band1: [128, 0, 128], band2: [0, 200, 200], band3: [40, 40, 40], }; export function testPng() { const C = TEST_PNG_COLORS; return makePng(100, 100, (x, y) => { if (y >= 90) return x < 34 ? C.band1 : x < 67 ? C.band2 : C.band3; return y < 45 ? (x < 50 ? C.tl : C.tr) : (x < 50 ? C.bl : C.br); }); } export async function startServer({ buildDir, project, siteId = 1, extraAssets = [] }) { const state = { project, rev: 1, saves: 0, lastSaveBody: null, assets: [{ name: 'test.png', type: 'image/png', buf: testPng() }] }; const mime = { '.js': 'text/javascript', '.css': 'text/css', '.html': 'text/html', '.png': 'image/png', '.svg': 'image/svg+xml' }; const json = (res, o, code = 200) => { res.statusCode = code; res.setHeader('content-type', 'application/json'); res.end(JSON.stringify(o)); }; const server = http.createServer((req, res) => { const u = new URL(req.url, 'http://x'); if (u.pathname === '/api/site-builder.php') { const a = u.searchParams.get('action'); if (a === 'load') return json(res, { success: true, rev: 'r' + state.rev, limits: {}, project: state.project }); if (a === 'list_assets') { return json(res, { success: true, assets: [...extraAssets, ...state.assets.map((s) => ({ name: s.name, url: `/api/assets-proxy/?site_id=${siteId}&filename=${encodeURIComponent(s.name)}`, type: s.type, size: s.buf.length, modified: 1 }))] }); } if (a === 'save' && req.method === 'POST') { const chunks = []; req.on('data', (c) => chunks.push(c)); req.on('end', () => { const body = JSON.parse(Buffer.concat(chunks).toString('utf8')); const { base_rev, client_id, force_empty, force_overwrite, ...proj } = body; state.project = proj; state.lastSaveBody = body; state.saves++; state.rev++; json(res, { success: true, rev: 'r' + state.rev }); }); return; } if (a === 'upload_asset' && req.method === 'POST') { const chunks = []; req.on('data', (c) => chunks.push(c)); req.on('end', () => json(res, { success: false, error: 'uploads not supported by the test mock' }, 400)); return; } return json(res, { success: true }); } if (u.pathname === '/api/assets-proxy/') { const a = state.assets.find((s) => s.name === u.searchParams.get('filename')); if (!a) { res.statusCode = 404; return res.end('nf'); } res.setHeader('content-type', a.type); return res.end(a.buf); } const p = u.pathname === '/' ? '/index.html' : u.pathname; if (p === '/index.html') { const cfg = { user: 't', apiUrl: '/api/site-builder.php', csrfToken: 'x', siteId, siteDomain: 'example.test', siteName: 'Colours', backUrl: '/', isRoot: false }; const h = fs.readFileSync(path.join(buildDir, 'index.html'), 'utf8').replace('', () => ``); res.setHeader('content-type', 'text/html'); return res.end(h); } const f = path.join(buildDir, p); if (f.startsWith(buildDir) && fs.existsSync(f) && fs.statSync(f).isFile()) { res.setHeader('content-type', mime[path.extname(f)] || 'application/octet-stream'); return res.end(fs.readFileSync(f)); } res.statusCode = 404; res.end('nf'); }); await new Promise((r) => server.listen(0, '127.0.0.1', r)); return { server, state, base: `http://localhost:${server.address().port}` }; }