Files
site-builder/craft/scripts/e2e-color-system-server.mjs

107 lines
5.2 KiB
JavaScript

/**
* 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('</head>', () => `<script>window.WHP_CONFIG=${JSON.stringify(cfg)};</script></head>`);
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}` };
}