mirror of
https://github.com/penpot/penpot.git
synced 2026-09-23 04:45:09 -04:00
* ⚡ Build the frontend bundle once for all E2E suites Merge tests-integration, tests-composable-suite and tests-plugin-api-suite into one "CI: E2E" workflow. Each of the three ran its own full frontend/scripts/build on every PR, so one PR paid the build three times. The new build-bundle job restores actions/cache key frontend-bundle-<sha>, runs frontend/scripts/build only on a miss and saves the key before the job ends. The integration shards, the composable suite and the mocked Plugin API suite now all need build-bundle and restore the same key with fail-on-cache-miss, so none of them builds. A workflow re-run of the same SHA reuses the cached bundle instead of rebuilding it. Triggers become the union of the previous paths (frontend, common, render-wasm, plugins): the bundle embeds the built plugins, so a plugins change runs the whole set. workflow_dispatch keeps running the integration job only, as before. Job names are kept identical on purpose: they are the GitHub check contexts and branch protection may match them by name. Docs: new mem:frontend/e2e-ci-workflow records the build-once contract, referenced from mem:frontend/core and mem:frontend/testing; the composable memory and both suite READMEs are updated. AI-assisted-by: deepseek-v4.1-flash * 🐛 Fix mocked plugin suites crashing without frontend deps The mocked CI drivers shelled out to frontend/scripts/e2e-server.js, which imports express from frontend/node_modules. CI jobs install only plugins/ deps, so the import failed with ERR_MODULE_NOT_FOUND and the run timed out waiting for localhost:3000. Serve the prebuilt bundle with a zero-dependency static server built into each driver (ci/static-server.ts, kept in sync in both suites) plus node:test coverage for it. AI-assisted-by: muse-spark-1.3-contributor
80 lines
2.9 KiB
TypeScript
80 lines
2.9 KiB
TypeScript
import { strict as assert } from 'node:assert';
|
|
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { after, before, describe, it } from 'node:test';
|
|
import { startStaticServer, type StaticServer } from './static-server.ts';
|
|
|
|
describe('static-server', () => {
|
|
let dir: string = '';
|
|
let server: StaticServer | undefined;
|
|
const baseUrl = (): string => {
|
|
if (!server) throw new Error('static server not started');
|
|
return server.url;
|
|
};
|
|
|
|
before(async () => {
|
|
dir = await mkdtemp(join(tmpdir(), 'penpot-static-server-'));
|
|
await mkdir(join(dir, 'js'), { recursive: true });
|
|
await writeFile(join(dir, 'index.html'), '<!doctype html><html></html>');
|
|
await writeFile(join(dir, 'js', 'app.js'), 'console.log("hi");');
|
|
await writeFile(join(dir, 'data.bin'), Buffer.from([0, 1, 2]));
|
|
server = await startStaticServer(dir, 0);
|
|
});
|
|
|
|
after(async () => {
|
|
await server?.close();
|
|
// Closing twice must be safe (the driver closes unconditionally).
|
|
await server?.close();
|
|
await rm(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('serves / as index.html', async () => {
|
|
const res = await fetch(`${baseUrl()}/`);
|
|
assert.equal(res.status, 200);
|
|
assert.match(res.headers.get('content-type') ?? '', /text\/html/);
|
|
assert.match(await res.text(), /<!doctype html>/);
|
|
});
|
|
|
|
it('serves nested files with a javascript content type', async () => {
|
|
const res = await fetch(`${baseUrl()}/js/app.js`);
|
|
assert.equal(res.status, 200);
|
|
assert.match(res.headers.get('content-type') ?? '', /javascript/);
|
|
assert.equal(await res.text(), 'console.log("hi");');
|
|
});
|
|
|
|
it('ignores query strings', async () => {
|
|
const res = await fetch(`${baseUrl()}/index.html?rev=123`);
|
|
assert.equal(res.status, 200);
|
|
assert.match(await res.text(), /<!doctype html>/);
|
|
});
|
|
|
|
it('falls back to octet-stream for unknown extensions', async () => {
|
|
const res = await fetch(`${baseUrl()}/data.bin`);
|
|
assert.equal(res.status, 200);
|
|
assert.equal(res.headers.get('content-type'), 'application/octet-stream');
|
|
});
|
|
|
|
it('answers HEAD without a body', async () => {
|
|
const res = await fetch(`${baseUrl()}/index.html`, { method: 'HEAD' });
|
|
assert.equal(res.status, 200);
|
|
assert.match(res.headers.get('content-type') ?? '', /text\/html/);
|
|
assert.equal(await res.text(), '');
|
|
});
|
|
|
|
it('rejects other methods', async () => {
|
|
const res = await fetch(`${baseUrl()}/index.html`, { method: 'POST' });
|
|
assert.equal(res.status, 405);
|
|
});
|
|
|
|
it('returns 404 for missing files', async () => {
|
|
const res = await fetch(`${baseUrl()}/nope/missing.js`);
|
|
assert.equal(res.status, 404);
|
|
});
|
|
|
|
it('blocks path traversal outside the root', async () => {
|
|
const res = await fetch(`${baseUrl()}/..%2f..%2fsecret`);
|
|
assert.equal(res.status, 403);
|
|
});
|
|
});
|