CI green on a loaded shared machine: browser suites (now also glyph-ligatures and feature-fit, through the shared launch helper) in one dedicated shard started first at concurrency 2; timeouts scaled by load ÷ cores (tests/helpers/load.ts, max(1-, 5-min average), capped at 5x, logged) for browser tests, waits, launch/close, the other shards' default and the desktop boot check; one retry for a browser test that timed out while the factor was above 2, listed as 'retried under load' in the CI summary, never for an assertion failure; the six-type App Privacy walk no longer dumps every step (the --debug-dump check walks two types), wait timeouts print per-line times; play-edit-lock checks reads in flight instead of wall time; tests/load-retry.test.ts
This commit is contained in:
parent
21fa91afd1
commit
918146d4a6
13 changed files with 261 additions and 96 deletions
|
|
@ -13,7 +13,7 @@ import { APPLE_DATA_TYPE_IDS, type AppleDataTypeId, type AppleTypeAnswer } from
|
|||
import type { ShipConfig } from '../cli/lib/config';
|
||||
import { type FlowWhere, flowAllowed, flowClass, FORM_BUTTONS, labelNames, setUpAllowed, setUpTexts, TYPE_ALIASES } from '../cli/lib/console-forms';
|
||||
import { APPLE_PRIVACY_TYPES } from '../cli/lib/app-privacy';
|
||||
import { AFTER_ALL_MS, AFTER_EACH_MS, launchTestBrowser, logLines, TEST_TICK_MS, TEST_WAITS, testScope } from './helpers/browser';
|
||||
import { AFTER_ALL_MS, AFTER_EACH_MS, launchTestBrowser, logLines, scaled, TEST_TICK_MS, TEST_WAITS, testScope } from './helpers/browser';
|
||||
|
||||
const FIX = join(import.meta.dir, 'fixtures', 'console');
|
||||
const base0: ShipConfig = {
|
||||
|
|
@ -38,8 +38,8 @@ const { browser, skipWhy } = shared;
|
|||
if (!browser) console.warn(`console-flow browser tests skipped: no Chromium installed (${skipWhy}). Run \`bunx playwright install chromium\`.`);
|
||||
const scope = testScope(() => browser);
|
||||
// a whole App Privacy walk (6 types x 5 dialog steps) on a loaded CI machine: generous, still bounded
|
||||
const TEST_MS = 90_000;
|
||||
const WALK_MS = 75_000;
|
||||
const TEST_MS = scaled(90_000);
|
||||
const WALK_MS = scaled(75_000);
|
||||
|
||||
let server: ReturnType<typeof Bun.serve> | null = null;
|
||||
let base = '';
|
||||
|
|
@ -63,7 +63,7 @@ async function local(): Promise<ConsoleBrowser> {
|
|||
const done = new Promise<void>(r => ctx.on('close', () => { closed = true; r(); }));
|
||||
return { mode: 'profile', context: ctx, page, pages: () => ctx.pages(), closed: done, isClosed: () => closed, close: () => ctx.close(), cleanup: () => {} };
|
||||
}
|
||||
const fast = (log: ReturnType<typeof logLines>) => ({ say: log.say, tickMs: TEST_TICK_MS, navTimeoutMs: 5000, signInWaitMs: 30_000, env: {} });
|
||||
const fast = (log: ReturnType<typeof logLines>) => ({ say: log.say, tickMs: TEST_TICK_MS, navTimeoutMs: scaled(5000), signInWaitMs: 30_000, env: {} });
|
||||
const clicks = (b: ConsoleBrowser): Promise<string[]> => b.page.evaluate(() => (window as any).__clicks);
|
||||
const navClicks = (b: ConsoleBrowser): Promise<string[]> => b.page.evaluate(() => (window as any).__navClicks());
|
||||
/** Guardrail: a line that tells the owner to click a navigation control themselves. */
|
||||
|
|
@ -233,30 +233,46 @@ const SETUP = 'apple-app-privacy-setup.html';
|
|||
const answersOf = (b: ConsoleBrowser) => b.page.evaluate(() => (window as any).__answers);
|
||||
|
||||
describe.skipIf(!browser)('App Privacy: each data type set up from the real page ("Set Up <type>" as plain text; fixture apple-app-privacy-setup.html)', () => {
|
||||
scope.concurrent('every collected type is set up through its dialog (purposes > Next > linked > Next > two info steps > Next > Next > tracking > Save; purpose checkboxes without an accessible name); "?", Publish and the Edits never clicked; every step dumped', async () => {
|
||||
scope.concurrent('every collected type is set up through its dialog (purposes > Next > linked > Next > two info steps > Next > Next > tracking > Save; purpose checkboxes without an accessible name); "?", Publish and the Edits never clicked', async () => {
|
||||
const b = await local();
|
||||
const log = logLines();
|
||||
const run = scope.run(fillSession(b, { form: 'apple-app-privacy', config: six(), startUrl: base + SETUP, ...fast(log) }));
|
||||
await log.wait('the Publish line', l => l.some(m => m.startsWith(PUBLISH)), WALK_MS);
|
||||
expect(await b.page.evaluate(() => (window as any).__setups)).toEqual(SIX_NAMES.map(n => `Set Up ${n}`));
|
||||
expect(await b.page.evaluate(() => (window as any).__saves)).toBe(6);
|
||||
const got = await answersOf(b);
|
||||
expect(got['Device ID']).toEqual({ purposes: ['Third-Party Advertising', 'Analytics'], linked: 'no', tracking: 'yes' });
|
||||
expect(got['Purchase History']).toEqual({ purposes: ['App Functionality'], linked: 'yes', tracking: 'no' });
|
||||
expect(got['Product Interaction']).toEqual({ purposes: ['Analytics', 'Product Personalization'], linked: 'no', tracking: 'no' });
|
||||
expect(Object.keys(got).sort()).toEqual([...SIX_NAMES].sort());
|
||||
const c = await clicks(b);
|
||||
for (const never of ['?', 'Publish', 'Edit', 'Cancel', 'Back', 'Definitions and Examples']) expect(c).not.toContain(never);
|
||||
expect(c.filter(x => x === 'Next')).toHaveLength(24); // purposes, linked and the TWO info steps of each of 6 types
|
||||
expect(log.lines.filter(m => m === 'an information-only step of the dialog: storebound pressed Next')).toHaveLength(12);
|
||||
expect(c.filter(x => x === 'Save')).toHaveLength(6);
|
||||
expect(log.lines).toContain('the Data Types section lists exactly the collected types of the config: storebound sets up each one');
|
||||
expect(log.lines).toContain('clicked "Set Up Purchase History" (navigation)');
|
||||
expect(log.lines.some(m => m.startsWith('answer needed'))).toBe(false);
|
||||
expect(navAsked(log.lines)).toEqual([]);
|
||||
await b.context.close();
|
||||
expect((await run).fallback).toBe(false);
|
||||
}, TEST_MS);
|
||||
|
||||
// --debug-dump writes a page dump (summary, HTML, screenshot) at every step: the first and the last type are walked
|
||||
// (the four between were set up before with the config's purposes and are left as they are), which covers every
|
||||
// kind of dump without six types' worth of screenshots on a loaded machine
|
||||
scope.concurrent('every step dumped with --debug-dump: the start, each Set Up, each dialog step and each filled step', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'sb-console-debug-'));
|
||||
try {
|
||||
const b = await local();
|
||||
const log = logLines();
|
||||
const run = scope.run(fillSession(b, { form: 'apple-app-privacy', config: six(), startUrl: base + SETUP, ...fast(log), debugDir: dir, debugEveryStep: true }));
|
||||
const before = ['Emails or Text Messages:App Functionality', 'Device ID:Third-Party Advertising|Analytics', 'Purchase History:App Functionality', 'Product Interaction:Analytics|Product Personalization'];
|
||||
const url = `${base}${SETUP}?${before.map(p => `preset=${encodeURIComponent(p)}`).join('&')}`;
|
||||
const run = scope.run(fillSession(b, { form: 'apple-app-privacy', config: six(), startUrl: url, ...fast(log), debugDir: dir, debugEveryStep: true }));
|
||||
await log.wait('the Publish line', l => l.some(m => m.startsWith(PUBLISH)), WALK_MS);
|
||||
expect(await b.page.evaluate(() => (window as any).__setups)).toEqual(SIX_NAMES.map(n => `Set Up ${n}`));
|
||||
expect(await b.page.evaluate(() => (window as any).__saves)).toBe(6);
|
||||
const got = await answersOf(b);
|
||||
expect(got['Device ID']).toEqual({ purposes: ['Third-Party Advertising', 'Analytics'], linked: 'no', tracking: 'yes' });
|
||||
expect(got['Purchase History']).toEqual({ purposes: ['App Functionality'], linked: 'yes', tracking: 'no' });
|
||||
expect(got['Product Interaction']).toEqual({ purposes: ['Analytics', 'Product Personalization'], linked: 'no', tracking: 'no' });
|
||||
expect(Object.keys(got).sort()).toEqual([...SIX_NAMES].sort());
|
||||
const c = await clicks(b);
|
||||
for (const never of ['?', 'Publish', 'Edit', 'Cancel', 'Back', 'Definitions and Examples']) expect(c).not.toContain(never);
|
||||
expect(c.filter(x => x === 'Next')).toHaveLength(24); // purposes, linked and the TWO info steps of each of 6 types
|
||||
expect(log.lines.filter(m => m === 'an information-only step of the dialog: storebound pressed Next')).toHaveLength(12);
|
||||
expect(c.filter(x => x === 'Save')).toHaveLength(6);
|
||||
expect(log.lines).toContain('the Data Types section lists exactly the collected types of the config: storebound sets up each one');
|
||||
expect(log.lines).toContain('clicked "Set Up Purchase History" (navigation)');
|
||||
expect(log.lines.some(m => m.startsWith('answer needed'))).toBe(false);
|
||||
expect(await b.page.evaluate(() => (window as any).__setups)).toEqual(['Set Up Coarse Location', 'Set Up Crash Data']);
|
||||
expect(log.lines).toContain('Device ID was set up before with the purposes of the config: left as it is');
|
||||
expect(navAsked(log.lines)).toEqual([]);
|
||||
// --debug-dump: the start, each Set Up, each dialog step and each filled step
|
||||
const names = readdirSync(dir);
|
||||
for (const want of [/-after-set-up-coarse-location$/, /-coarse-location-after-next$/, /-filled-coarse-location-purposes$/, /-filled-coarse-location-linked-to-/, /-filled-crash-data-tracking$/])
|
||||
expect(names.some(n => want.test(n))).toBe(true);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue