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:
parent
ffaf42c55d
commit
8106d7c9a2
1 changed files with 29 additions and 14 deletions
|
|
@ -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]}`));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue