desktop: five sections by job (Release, Store page, Revenue, Players, Health), Game services view, sidebar by attention
- app view: 12 tabs become 5 sections, each with a header (what it is for, the one next step from state already read) and its sub views in a segmented control; 1-5 jump, Ctrl+Shift+[ ] cycle, Ctrl+K and the context menu list sections and sub views; old tab ids, section and section/sub links all resolve - Players > Game services (games only): both stores, one row per leaderboard / achievement with its languages and each store's state, Game Center locale map, Play API-disabled state with the console plan - games status --json: items, localeMap, playApiDisabled; inspect summary: games - sidebar: apps grouped by attention / in progress / live / not on a store, one status line each, studio filter, workspace as a footer icon row - boot check opens places like a person (section, then sub view)
This commit is contained in:
parent
98cd1d3c92
commit
0ef27cf2e4
22 changed files with 918 additions and 115 deletions
|
|
@ -244,20 +244,51 @@ 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). */
|
||||
/** The app's places (docs IA): five sections, each with sub views in a segmented control. A place is named by its
|
||||
* sub view's old tab id ("tab-prices"); the section of each, for opening it like a person. */
|
||||
const SECTION_OF: Record<string, string> = {
|
||||
releases: 'release', testflight: 'release', listings: 'store', art: 'store', appinfo: 'store', catalog: 'revenue', prices: 'revenue',
|
||||
reviews: 'players', games: 'players', health: 'health', growth: 'health', checks: 'health', tools: 'health',
|
||||
};
|
||||
|
||||
/** Opens a place in an app like a person: its section in the section row (or through the row's "More" menu when it
|
||||
* does not fit), then the sub view in the section's segmented control when the section has more than one. */
|
||||
async function clickTab(page: Page, id: string, o: { timeout?: number } = {}): Promise<void> {
|
||||
const tab = page.locator(`[role="tab"]#${id}`);
|
||||
const sub = id.replace(/^tab-/, '');
|
||||
const sec = SECTION_OF[sub];
|
||||
if (!sec) throw new Error(`unknown place ${id}`);
|
||||
const tab = page.locator(`[role="tab"]#tab-${sec}`);
|
||||
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();
|
||||
try {
|
||||
await page.locator('.ds-tab-more').click(o);
|
||||
await page.locator('.ds-menu [role="menuitem"]', { hasText: label }).first().click(o);
|
||||
} catch (e) {
|
||||
const why = await page.evaluate(() => JSON.stringify({ more: document.querySelectorAll('.ds-tab-more').length, menu: [...document.querySelectorAll('.ds-menu [role^="menuitem"]')].map(x => x.textContent), w: innerWidth }));
|
||||
throw new Error(`${String(e).split('\n')[0]} (${label}; ${why})`);
|
||||
if (await tab.isVisible()) await tab.click(o);
|
||||
else {
|
||||
const label = ((await tab.textContent()) ?? '').replace(/\d+$/, '').trim();
|
||||
try {
|
||||
await page.locator('.ds-tab-more').click(o);
|
||||
await page.locator('.ds-menu [role="menuitem"]', { hasText: label }).first().click(o);
|
||||
} catch (e) {
|
||||
const why = await page.evaluate(() => JSON.stringify({ more: document.querySelectorAll('.ds-tab-more').length, menu: [...document.querySelectorAll('.ds-menu [role^="menuitem"]')].map(x => x.textContent), w: innerWidth }));
|
||||
throw new Error(`${String(e).split('\n')[0]} (${label}; ${why})`);
|
||||
}
|
||||
}
|
||||
const btn = page.locator(`.sec-subs #sub-${sub}`);
|
||||
if (await btn.count()) { await btn.click(o); return; }
|
||||
// a section with one sub view has no segmented control: the section itself is the place
|
||||
if (await page.locator('.sec-subs button').count()) throw new Error(`${id}: not in the ${sec} section's views`);
|
||||
}
|
||||
|
||||
/** Every place of the open app, in order: each section, then each of its sub views (as clickTab ids). */
|
||||
async function places(page: Page, o: { timeout?: number } = {}): Promise<string[]> {
|
||||
const secs = await page.locator('[role="tab"]').evaluateAll(ts => ts.map(x => x.id.replace(/^tab-/, '')));
|
||||
const out: string[] = [];
|
||||
for (const sec of secs) {
|
||||
await page.locator(`[role="tab"]#tab-${sec}`).click(o).catch(() => {});
|
||||
await page.locator('[data-testid="section-head"]').waitFor({ state: 'visible', timeout: o.timeout }).catch(() => {});
|
||||
const subs = await page.locator('.sec-subs button').evaluateAll(bs => bs.map(b => b.id.replace(/^sub-/, '')));
|
||||
const first = Object.entries(SECTION_OF).find(([, s]) => s === sec)?.[0];
|
||||
// a one-view section: its only view (Players without games is Reviews)
|
||||
out.push(...(subs.length ? subs : first ? [first] : []).map(x => `tab-${x}`));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async function state(page: Page) {
|
||||
|
|
@ -342,10 +373,12 @@ async function runCase(browser: Browser, name: string, sidecar: Sidecar, o: { br
|
|||
if (await wizard.count()) problems.push('the welcome guide does not close with Escape');
|
||||
}
|
||||
// every sidebar entry: the portfolio, each app (folders that are gone included), each workspace screen
|
||||
const count = await page.locator('nav.side button.nav').count();
|
||||
// (apps are .nav rows; the workspace is the footer's icon row, named by aria-label)
|
||||
const SIDE = 'nav.side button.nav, nav.side button.nav-ws';
|
||||
const count = await page.locator(SIDE).count();
|
||||
for (let i = 0; i < count; i++) {
|
||||
const b = page.locator('nav.side button.nav').nth(i);
|
||||
const label = ((await b.innerText().catch(() => '')) || `entry ${i}`).split('\n')[0]!.trim();
|
||||
const b = page.locator(SIDE).nth(i);
|
||||
const label = ((await b.getAttribute('aria-label').catch(() => null)) || (await b.innerText().catch(() => '')) || `entry ${i}`).split('\n')[0]!.trim();
|
||||
await b.click({ timeout: WAIT_MS }).catch(e => problems.push(`could not open ${label}: ${String(e).split('\n')[0]}`));
|
||||
await settle(page);
|
||||
await check(label);
|
||||
|
|
@ -365,7 +398,7 @@ async function runCase(browser: Browser, name: string, sidecar: Sidecar, o: { br
|
|||
// shared with the CLI: each account names its credential profile (or that its old keychain key has not moved)
|
||||
const accts = fx.accounts_list as { label?: string; profile?: string | null }[];
|
||||
if (accts.length) {
|
||||
const nav = page.locator('nav.side button.nav', { hasText: 'Accounts' }).first();
|
||||
const nav = page.locator('nav.side [data-screen="accounts"]').first();
|
||||
if (await nav.count()) {
|
||||
await nav.click({ timeout: WAIT_MS });
|
||||
await settle(page);
|
||||
|
|
@ -510,8 +543,8 @@ async function runSubmitCase(browser: Browser, sidecar: Sidecar, base: 'd3-full'
|
|||
await gone(page, '.cp-panel');
|
||||
await crashCheck('Releases');
|
||||
visited++;
|
||||
// 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));
|
||||
// every place of the app (each section and its sub views), then App info's console steps and Dependencies
|
||||
const tabs = await places(page, { timeout: WAIT_MS });
|
||||
for (const id of tabs) {
|
||||
await clickTab(page, id, { timeout: WAIT_MS }).catch(e => problems.push(`tab ${id}: ${String(e).split('\n')[0]}`));
|
||||
await settle(page);
|
||||
|
|
@ -524,7 +557,7 @@ async function runSubmitCase(browser: Browser, sidecar: Sidecar, base: 'd3-full'
|
|||
if (id === 'tab-testflight' && !(await text()).length) problems.push('TestFlight: empty');
|
||||
}
|
||||
notes.push(`tabs: ${tabs.join(', ')}`);
|
||||
const deps = page.locator('nav.side button.nav', { hasText: 'Dependencies' }).first();
|
||||
const deps = page.locator('nav.side [data-screen="dependencies"]').first();
|
||||
await deps.click({ timeout: WAIT_MS }).catch(() => {});
|
||||
await shows(page, '#content', 'Android SDK');
|
||||
await settle(page);
|
||||
|
|
@ -710,7 +743,7 @@ async function runAutoReadCase(browser: Browser): Promise<CaseResult> {
|
|||
await page.locator('nav.side .nav-app:visible').nth(i).click({ timeout: WAIT_MS }).catch(e => problems.push(`app ${i}: ${String(e).split('\n')[0]}`));
|
||||
await settle(page);
|
||||
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));
|
||||
const tabs = await places(page, { timeout: WAIT_MS });
|
||||
for (const id of tabs) {
|
||||
await clickTab(page, id, { timeout: WAIT_MS }).catch(e => problems.push(`${appName} ${id}: ${String(e).split('\n')[0]}`));
|
||||
await settle(page);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue