Desktop boot check: tabs opened like a person (the More menu when a tab is not in the row); the fake host answers window dry runs like the Rust host (dryRun forced), art.pull preview dry run

This commit is contained in:
CRTkafa 2026-10-23 10:31:40 +08:00
parent ffaf42c55d
commit 8106d7c9a2

View file

@ -162,7 +162,9 @@ function host(fx: HostFixture, sidecar: Sidecar, unmocked: Set<string>, logged:
return ownerDry(method, (params.args ?? {}) as Record<string, unknown>);
}
if (kind === 'read' || kind === 'write' || kind === 'owner') {
if (kind !== 'read') fail(`boot check: ${method} writes`);
// a write from the window is its dry run (the in-app preview): the Rust host forces dryRun, as here
if ('dryRun' in params || 'confirmToken' in params) fail(`boot check: ${method}: the window sent host params`);
if (kind !== 'read') params.dryRun = true;
const p = projects().find(x => x.id === a.projectId);
if (!p) fail(String(a.projectId).startsWith('store:') ? 'boot check: store reads are not part of the boot check' : 'unknown project');
params.project = p!.path;
@ -240,6 +242,17 @@ async function newPage(ctx: { newPage(): Promise<import('playwright').Page> }):
return page;
}
/** Opens an app tab like a person: the tab itself when it is in the row, else through the row's "More" menu (tabs
* that do not fit the window go there). */
async function clickTab(page: Page, id: string, o: { timeout?: number } = {}): Promise<void> {
const tab = page.locator(`[role="tab"]#${id}`);
await tab.waitFor({ state: 'attached', timeout: o.timeout });
if (await tab.isVisible()) { await tab.click(o); return; }
const label = ((await tab.textContent()) ?? '').replace(/\d+$/, '').trim();
await page.locator('.ds-tab-more').click(o);
await page.locator('.ds-menu [role="menuitem"]', { hasText: label }).first().click(o);
}
async function state(page: Page) {
return page.evaluate(() => ({
booted: document.documentElement.dataset.booted === '1',
@ -435,7 +448,7 @@ async function runSubmitCase(browser: Browser, sidecar: Sidecar, base: 'd3-full'
const item = page.locator('nav.side .nav-app').filter({ hasText: base === 'd3-full' ? /Prism Puddle|demo-game/ : new RegExp(app.name) }).first();
await item.click({ timeout: WAIT_MS }).catch(e => problems.push(`could not open the app: ${String(e).split('\n')[0]}`));
await settle(page);
await page.locator('[role="tab"]#tab-releases').click({ timeout: WAIT_MS }).catch(() => {});
await clickTab(page, 'tab-releases', { timeout: WAIT_MS }).catch(() => {});
await page.waitForSelector('[data-section="submit"]', { timeout: WAIT_MS }).catch(() => problems.push('Releases: no send-for-review section'));
await settle(page);
const rel = await text('[data-section="submit"]');
@ -493,7 +506,7 @@ async function runSubmitCase(browser: Browser, sidecar: Sidecar, base: 'd3-full'
// every tab of the app, then App info's console steps and the Dependencies screen
const tabs = await page.locator('[role="tab"]').evaluateAll(ts => ts.map(x => x.id));
for (const id of tabs) {
await page.locator(`[role="tab"]#${id}`).click({ timeout: WAIT_MS }).catch(e => problems.push(`tab ${id}: ${String(e).split('\n')[0]}`));
await clickTab(page, id, { timeout: WAIT_MS }).catch(e => problems.push(`tab ${id}: ${String(e).split('\n')[0]}`));
await settle(page);
await crashCheck(id);
visited++;
@ -577,7 +590,7 @@ async function runReleaseStateCase(browser: Browser, sidecar: Sidecar, page404:
const item = page.locator('nav.side .nav-app').filter({ hasText: /Prism Puddle|demo-game/ }).first();
await item.click({ timeout: WAIT_MS }).catch(e => problems.push(`could not open the app: ${String(e).split('\n')[0]}`));
await settle(page);
await page.locator('[role="tab"]#tab-releases').click({ timeout: WAIT_MS }).catch(() => {});
await clickTab(page, 'tab-releases', { timeout: WAIT_MS }).catch(() => {});
await shows(page, '#content', 'Versions on the stores');
await settle(page);
const rows = await page.locator('#content table tbody tr').evaluateAll(rs => rs.map(r => Array.from(r.querySelectorAll('td')).map(td => (td.textContent ?? '').trim())));
@ -692,7 +705,7 @@ async function runAutoReadCase(browser: Browser): Promise<CaseResult> {
const appName = (await page.locator('nav.side .nav-app:visible').nth(i).innerText().catch(() => `app ${i}`)).split('\n')[0]!.trim();
const tabs = await page.locator('[role="tab"]').evaluateAll(ts => ts.map(x => x.id));
for (const id of tabs) {
await page.locator(`[role="tab"]#${id}`).click({ timeout: WAIT_MS }).catch(e => problems.push(`${appName} ${id}: ${String(e).split('\n')[0]}`));
await clickTab(page, id, { timeout: WAIT_MS }).catch(e => problems.push(`${appName} ${id}: ${String(e).split('\n')[0]}`));
await settle(page);
await crashCheck(`${appName} ${id}`);
const tx = await text();
@ -710,7 +723,7 @@ async function runAutoReadCase(browser: Browser): Promise<CaseResult> {
// the stores' current versions differ (Play 3.1.0, App Store 3.0.4): a neutral header, each store its own state
await page.locator('nav.side .nav-app', { hasText: 'Orbit Hop' }).first().click({ timeout: WAIT_MS });
await settle(page);
await page.locator('[role="tab"]#tab-releases').click({ timeout: WAIT_MS });
await clickTab(page, 'tab-releases', { timeout: WAIT_MS });
await settle(page);
const rel = await text();
for (const w of ['Current', 'Play 3.1.0 · App Store 3.0.4', '3.0.4 · Live (3.1.0 not submitted)']) if (!rel.includes(w)) problems.push(`Orbit Hop Releases: missing "${w}"`);
@ -720,7 +733,7 @@ async function runAutoReadCase(browser: Browser): Promise<CaseResult> {
// Health: the first top issue opens with its full title, the sample stack and Copy for AI
await page.locator('nav.side .nav-app', { hasText: 'Prism Puddle' }).first().click({ timeout: WAIT_MS });
await settle(page);
await page.locator('[role="tab"]#tab-health').click({ timeout: WAIT_MS });
await clickTab(page, 'tab-health', { timeout: WAIT_MS });
await settle(page);
await page.locator('.issue-row').first().click({ timeout: WAIT_MS }).catch(() => problems.push('Health: no issue row to open'));
if (!(await shows(page, '[data-testid="issue-stack"]', 'ComboMeter.update(ComboMeter.kt:88)'))) problems.push('Health: the opened issue shows no sample stack');
@ -805,7 +818,7 @@ async function runErrorCase(browser: Browser, sidecar: Sidecar, variant: 'api-di
await item.click({ timeout: WAIT_MS }).catch(e => problems.push(`could not open the app: ${String(e).split('\n')[0]}`));
await settle(page);
if (variant === 'api-disabled') {
await page.locator('[role="tab"]#tab-health').click({ timeout: WAIT_MS }).catch(e => problems.push(`no Health tab: ${String(e).split('\n')[0]}`));
await clickTab(page, 'tab-health', { timeout: WAIT_MS }).catch(e => problems.push(`no Health tab: ${String(e).split('\n')[0]}`));
await shows(page, '#content', 'Turn it on in Google Cloud');
await settle(page);
const h = await text();
@ -822,7 +835,7 @@ async function runErrorCase(browser: Browser, sidecar: Sidecar, variant: 'api-di
await crashCheck('Health');
visited++;
} else {
await page.locator('[role="tab"]#tab-checks').click({ timeout: WAIT_MS }).catch(e => problems.push(`no Checks tab: ${String(e).split('\n')[0]}`));
await clickTab(page, 'tab-checks', { timeout: WAIT_MS }).catch(e => problems.push(`no Checks tab: ${String(e).split('\n')[0]}`));
await shows(page, '#content', variant === 'scan-findings' ? 'Sign-up without in-app account deletion' : SCAN_FAILED_LINE);
await settle(page);
const c = await text();
@ -894,7 +907,7 @@ async function runEditorCase(browser: Browser, sidecar: Sidecar): Promise<CaseRe
// every other write: the owner presses Cancel in the native dialog (nothing is sent)
if (cmd === 'write_with_confirm') { writes.push(String(a.method)); return { confirmed: false, stage: 'dialog', dryRun: { ...toolResult(String(a.method), null), dryRun: true, output: APPLY_DRY[String(a.method)] ?? '' } }; }
// "Review and save": the window's dry run of project.edit (the Rust host forces dryRun for calls from the window)
if (cmd === 'sidecar_call' && a.method === 'project.edit') return sidecar.call('project.edit', { project: dir, args: (a.params as { args?: unknown } | null)?.args ?? {} });
if (cmd === 'sidecar_call' && a.method === 'project.edit') return sidecar.call('project.edit', { project: dir, args: (a.params as { args?: unknown } | null)?.args ?? {}, dryRun: true });
if (cmd === 'sidecar_call' && APPLY_DRY[String(a.method)] !== undefined) return { ...toolResult(String(a.method), null), dryRun: true, output: APPLY_DRY[String(a.method)] };
return base(cmd, a);
};
@ -915,7 +928,7 @@ async function runEditorCase(browser: Browser, sidecar: Sidecar): Promise<CaseRe
if (await wizard.count()) { await wizard.press('Escape').catch(() => {}); await gone(page, '.wizard[role="dialog"]'); }
await page.locator('nav.side .nav-app', { hasText: 'Tidy Lists' }).first().click({ timeout: WAIT_MS }).catch(e => problems.push(`could not open the app: ${String(e).split('\n')[0]}`));
await settle(page);
await page.locator('[role="tab"]#tab-listings').click({ timeout: WAIT_MS }).catch(e => problems.push(`no Listings tab: ${String(e).split('\n')[0]}`));
await clickTab(page, 'tab-listings', { timeout: WAIT_MS }).catch(e => problems.push(`no Listings tab: ${String(e).split('\n')[0]}`));
await page.waitForSelector(field, { timeout: WAIT_MS }).catch(() => problems.push('the listing editor has no App Store en-US subtitle field'));
// over the limit: the counter and the check say so while typing
await page.locator(field).fill('Lists that stay tidy and organized forever');
@ -957,11 +970,11 @@ async function runEditorCase(browser: Browser, sidecar: Sidecar): Promise<CaseRe
if (!writes.includes('apple.listing')) problems.push('Apply did not go through the confirm dialog (write_with_confirm)');
// the other editors draw, and a config edit (App info) saves only that value, comments kept
for (const [tab, root] of [['appinfo', '#appinfo-editor'], ['testflight', '#testflight-editor'], ['art', '#art-editor'], ['catalog', '#catalog-editor, [data-edit-bar], .scr-catalog']] as const) {
await page.locator(`[role="tab"]#tab-${tab}`).click({ timeout: WAIT_MS }).catch(e => problems.push(`no ${tab} tab: ${String(e).split('\n')[0]}`));
await clickTab(page, `tab-${tab}`, { timeout: WAIT_MS }).catch(e => problems.push(`no ${tab} tab: ${String(e).split('\n')[0]}`));
if (!(await page.waitForSelector(root, { timeout: WAIT_MS }).then(() => true, () => false))) problems.push(`${tab}: the editor did not draw (${root})`);
await settle(page);
}
await page.locator('[role="tab"]#tab-appinfo').click({ timeout: WAIT_MS });
await clickTab(page, 'tab-appinfo', { timeout: WAIT_MS });
const notes1 = '[data-edit="config:apple.info.reviewNotes"]';
await page.waitForSelector(notes1, { timeout: WAIT_MS }).catch(() => problems.push('App info: no review notes field'));
await page.locator(notes1).fill('Sign in with the demo account.');
@ -1047,6 +1060,8 @@ async function runLiveArtCase(browser: Browser, sidecar: Sidecar): Promise<CaseR
if (cmd === 'sidecar_call' && a.method === 'art.live') return toolResult('art.live', LIVE_ART);
if (cmd === 'sidecar_call' && a.method === 'art.thumb') { thumbs.push(String((a.params as { args?: { url?: string } } | null)?.args?.url)); return toolResult('art.thumb', { dataUrl: THUMB, bytes: 68 }); }
if (cmd === 'image_pick') return picked;
// the in-app preview's dry run of a pull (the real one reads the stores): the same text the native dialog shows
if (cmd === 'sidecar_call' && a.method === 'art.pull') return { ...toolResult('art.pull', null), dryRun: true, output: ART_PULL_DRY };
if (cmd === 'write_with_confirm') {
const method = String(a.method);
if (method === 'art.replace') {
@ -1075,7 +1090,7 @@ async function runLiveArtCase(browser: Browser, sidecar: Sidecar): Promise<CaseR
if (await wizard.count()) { await wizard.press('Escape').catch(() => {}); await gone(page, '.wizard[role="dialog"]'); }
await page.locator('nav.side .nav-app', { hasText: 'Tidy Lists' }).first().click({ timeout: WAIT_MS }).catch(e => problems.push(`could not open the app: ${String(e).split('\n')[0]}`));
await settle(page);
await page.locator('[role="tab"]#tab-art').click({ timeout: WAIT_MS }).catch(e => problems.push(`no Store art tab: ${String(e).split('\n')[0]}`));
await clickTab(page, 'tab-art', { timeout: WAIT_MS }).catch(e => problems.push(`no Store art tab: ${String(e).split('\n')[0]}`));
await page.waitForSelector('#live-art', { timeout: WAIT_MS }).catch(() => problems.push('no live store art card'));
// nothing is read from the stores until asked (auto-read is off in the saved state)
await page.locator('#live-art button', { hasText: 'Read the stores' }).click({ timeout: WAIT_MS }).catch(e => problems.push(`no "Read the stores" button: ${String(e).split('\n')[0]}`));