From 8106d7c9a225ccc0ac3b37355099a2671a0ea350 Mon Sep 17 00:00:00 2001 From: CRT Date: Fri, 23 Oct 2026 10:31:40 +0800 Subject: [PATCH] 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 --- tools/desktop-boot-check.ts | 43 +++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/tools/desktop-boot-check.ts b/tools/desktop-boot-check.ts index f40b1c4..ece6e97 100644 --- a/tools/desktop-boot-check.ts +++ b/tools/desktop-boot-check.ts @@ -162,7 +162,9 @@ function host(fx: HostFixture, sidecar: Sidecar, unmocked: Set, logged: return ownerDry(method, (params.args ?? {}) as Record); } 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 }): 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 { + 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 { 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 { // 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 { // 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 {}); 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 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 {}); 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]}`));