ci speed: desktop boot check runs its cases in parallel (one browser, killed at the end; a page per case; condition waits instead of fixed sleeps: element, text or a quiet window of host calls and DOM); shots capture / render close the browser with a 3 s cap, then kill it; console engine waits injectable for tests (WAITS / engineWaits: poll rounds, a missing step ends once its loaded page stays quiet, team switch settle, field-miss grace), fillPage looks up each data type's dialog once per round and skips dialog sections when no dialog is open; browser tests run 4 at a time per file (testScope().concurrent with a scope per test, --max-concurrency in ci), 20 ms rounds; capture-web counts boots per test and runs its browser tests concurrently; console-session / -persist Chrome tests concurrent; recorded test timings smoothed (mergeTimings) so one loaded run does not skew the shard split
This commit is contained in:
parent
f2d7062bcb
commit
9ba8471002
16 changed files with 390 additions and 178 deletions
|
|
@ -53,14 +53,17 @@ describe('capture helpers', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ---- a local fixture web app: /scene/<name>?lang=xx, a SPA (popstate) that counts its boots ----
|
||||
let boots = 0;
|
||||
// ---- a local fixture web app: /<key>/scene/<name>?lang=xx, a SPA (popstate) that counts its boots per key: each
|
||||
// browser test has its own key, so they run at the same time (test.concurrent) ----
|
||||
const boots = new Map<string, number>();
|
||||
const booted = (key: string) => boots.get(key) ?? 0;
|
||||
const server = Bun.serve({
|
||||
port: 0, hostname: '127.0.0.1',
|
||||
fetch(req) {
|
||||
const u = new URL(req.url);
|
||||
if (u.pathname === '/app.js') {
|
||||
boots++;
|
||||
const key = u.searchParams.get('k') ?? '';
|
||||
boots.set(key, booted(key) + 1);
|
||||
return new Response(`
|
||||
const COLORS = { en: '#1e66f5', de: '#40a02b', tr: '#d20f39' };
|
||||
function show() {
|
||||
|
|
@ -74,7 +77,8 @@ const server = Bun.serve({
|
|||
addEventListener('popstate', show);
|
||||
show();`, { headers: { 'content-type': 'text/javascript' } });
|
||||
}
|
||||
return new Response('<!doctype html><html><head><meta charset="utf-8"><script src="/app.js" defer></script></head><body></body></html>', { headers: { 'content-type': 'text/html' } });
|
||||
const key = encodeURIComponent(u.pathname.split('/')[1] ?? '');
|
||||
return new Response(`<!doctype html><html><head><meta charset="utf-8"><script src="/app.js?k=${key}" defer></script></head><body></body></html>`, { headers: { 'content-type': 'text/html' } });
|
||||
},
|
||||
});
|
||||
afterAll(() => server.stop(true));
|
||||
|
|
@ -99,15 +103,16 @@ function project(cfg: ShipConfig, files: Record<string, string>): { root: string
|
|||
}
|
||||
|
||||
const shot = (name: string, extra: object = {}) => ({ path: `/scene/${name}`, waitFor: `window.scene === '${name}'`, delayMs: 20, ...extra });
|
||||
function config(capture: Partial<ShotsCapture> = {}): ShipConfig {
|
||||
/** `key`: the fixture app's boot counter this config's runs count on (one per browser test). */
|
||||
function config(capture: Partial<ShotsCapture> = {}, key = 'app'): ShipConfig {
|
||||
return { ...BASE, shots: { copy: 'store/shots/copy.json', frames: 'store/shots/frames', out: 'store/renders', scenes: [{ id: 'hero', palette: 'fire', frames: [{ src: 'hero.png' }] }], phone: { w: 180, h: 240, dpr: 1 },
|
||||
capture: { source: 'web', baseUrl: `http://127.0.0.1:${server.port}`, query: 'lang={lang}', build: 'src', shots: { hero: shot('hero'), battle: shot('battle'), king: shot('king') }, ...capture } } };
|
||||
capture: { source: 'web', baseUrl: `http://127.0.0.1:${server.port}/${key}`, query: 'lang={lang}', build: 'src', shots: { hero: shot('hero'), battle: shot('battle'), king: shot('king') }, ...capture } } };
|
||||
}
|
||||
const files = { 'store/shots/copy.json': '{}', 'src/main.js': 'v1' };
|
||||
const frames = (root: string) => ['en', 'de', 'tr'].flatMap(l => ['hero', 'battle', 'king'].map(n => join(root, 'store/shots/frames', l, 'phone', `${n}.png`)));
|
||||
|
||||
describe.skipIf(!canLaunch)('shots capture (web) with a fixture app', () => {
|
||||
test('concurrency 3 gives the same files as serial', async () => {
|
||||
test.concurrent('concurrency 3 gives the same files as serial', async () => {
|
||||
const p = project(config(), files);
|
||||
try {
|
||||
await p.cli(['--device', 'phone', '--concurrency', '1', '--force']);
|
||||
|
|
@ -120,34 +125,31 @@ describe.skipIf(!canLaunch)('shots capture (web) with a fixture app', () => {
|
|||
} finally { rmSync(p.root, { recursive: true, force: true }); }
|
||||
}, 120_000);
|
||||
|
||||
test('per-language session boots the app once per language; per-shot once per frame', async () => {
|
||||
const p = project(config({ session: 'per-language' }), files);
|
||||
test.concurrent('per-language session boots the app once per language; per-shot once per frame', async () => {
|
||||
const p = project(config({ session: 'per-language' }, 'lang'), files);
|
||||
try {
|
||||
boots = 0;
|
||||
const r = await p.cli(['--device', 'phone', '--concurrency', '2', '--force']);
|
||||
expect(boots).toBe(3);
|
||||
expect(booted('lang')).toBe(3);
|
||||
expect(r.out).toContain('(reused page)');
|
||||
const report = JSON.parse(readFileSync(join(p.root, '.storebound/capture-report.json'), 'utf8')) as CaptureReport;
|
||||
expect(report.frames.filter(f => f.reused).length).toBe(6);
|
||||
expect(frames(p.root).every(f => existsSync(f))).toBe(true);
|
||||
// a shot whose precondition fails in the reused page (it needs a freshly booted page) falls back to a fresh page
|
||||
const bad = project(config({ session: 'per-language', reuseTimeoutMs: 300, shots: { hero: shot('hero'), fresh: { path: '/scene/fresh', waitFor: "window.scene === 'fresh' && window.shows === 1", delayMs: 20 } } }), files);
|
||||
const bad = project(config({ session: 'per-language', reuseTimeoutMs: 300, shots: { hero: shot('hero'), fresh: { path: '/scene/fresh', waitFor: "window.scene === 'fresh' && window.shows === 1", delayMs: 20 } } }, 'fresh'), files);
|
||||
try {
|
||||
boots = 0;
|
||||
const r2 = await bad.cli(['--device', 'phone', '--lang', 'en', '--force']);
|
||||
expect(r2.err).toContain('en/phone/fresh: reused page failed');
|
||||
expect(r2.out).toContain('capture done: 2 frames');
|
||||
expect(boots).toBe(2);
|
||||
expect(booted('fresh')).toBe(2);
|
||||
} finally { rmSync(bad.root, { recursive: true, force: true }); }
|
||||
boots = 0;
|
||||
const q = project(config(), files);
|
||||
const q = project(config({}, 'shot'), files);
|
||||
await q.cli(['--device', 'phone', '--lang', 'en', '--force']);
|
||||
rmSync(q.root, { recursive: true, force: true });
|
||||
expect(boots).toBe(3);
|
||||
expect(booted('shot')).toBe(3);
|
||||
} finally { rmSync(p.root, { recursive: true, force: true }); }
|
||||
}, 180_000);
|
||||
|
||||
test('cache skips unchanged frames, re-runs changed ones; --only-missing; timing breakdown', async () => {
|
||||
test.concurrent('cache skips unchanged frames, re-runs changed ones; --only-missing; timing breakdown', async () => {
|
||||
const p = project(config(), files);
|
||||
try {
|
||||
let r = await p.cli(['--device', 'phone']);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue