107 lines
5.2 KiB
JavaScript
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}` };
|
|
}
|