ci speed: desktop boot check runs its cases in parallel (one browser, killed at the end; a page per case; condition waits instead of fixed sleeps: element, text or a quiet window of host calls and DOM); shots capture / render close the browser with a 3 s cap, then kill it; console engine waits injectable for tests (WAITS / engineWaits: poll rounds, a missing step ends once its loaded page stays quiet, team switch settle, field-miss grace), fillPage looks up each data type's dialog once per round and skips dialog sections when no dialog is open; browser tests run 4 at a time per file (testScope().concurrent with a scope per test, --max-concurrency in ci), 20 ms rounds; capture-web counts boots per test and runs its browser tests concurrently; console-session / -persist Chrome tests concurrent; recorded test timings smoothed (mergeTimings) so one loaded run does not skew the shard split

This commit is contained in:
CRTkafa 2026-10-13 02:20:03 +08:00
parent f2d7062bcb
commit 9ba8471002
16 changed files with 390 additions and 178 deletions

View file

@ -3,21 +3,22 @@
// the public help pages, not recorded) served on 127.0.0.1. No network. Browser tests skip when no Chromium is
// installed (a launch that times out fails instead). Load-proofing (tests/helpers/browser.ts): one launch with a long
// launch timeout, every context closed right after its test, event-driven waits for log lines, explicit test timeouts.
// Slow on purpose (marked SLOW): the sign-in / watching tests and the account tests wait out real "not here" timeouts
// of the engine (navTimeoutMs / navMs, scaled down through the options). The relaunch of storebound's own profile
// Speed: the browser tests run BROWSER_CONCURRENCY at a time (scope.concurrent, each with its own contexts) and the
// engine's waits are the test ones (TEST_WAITS / TEST_TICK_MS: 20 ms rounds, a missing step ends once its page is
// loaded and quiet). SLOW tests still wait out some "not here" timeouts of the engine (navMs, scaled down). The relaunch of storebound's own profile
// (session cookies kept) needs a real Chrome: tests/console-persist.test.ts.
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'bun:test';
import { mkdtempSync, readdirSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { matchAccount, openAcross, readAccounts, rememberApple, rememberPlay, withAccountIds } from '../cli/console/accounts';
import { createAppHere, fillPage, fillSession, navClick, navigate, resumeAt, run, safeClick, signInPage, signInReason } from '../cli/console/fill';
import { createAppHere, engineWaits, fillPage, fillSession, navClick, navigate, resumeAt, run, safeClick, signInPage, signInReason } from '../cli/console/fill';
import type { ConsoleBrowser } from '../cli/console/session';
import { parseArgs } from '../cli/lib/args';
import { type ShipConfig, validateExtras } from '../cli/lib/config';
import { AGE_RATING_API, consoleAnswers, consoleResult, fillValue, FORM_IDS, type FormId, formMeta } from '../cli/lib/console-answers';
import { consoleReached, CREATE_APP_OPENERS, createAppAllowed, formDef, FORM_OPENERS, forbiddenControl, hasToken, isSignInText, isSignInUrl, navAllowed, navPlan, playDeveloperId, switchAllowed } from '../cli/lib/console-forms';
import { AFTER_ALL_MS, AFTER_EACH_MS, launchTestBrowser, logLines, testScope } from './helpers/browser';
import { AFTER_ALL_MS, AFTER_EACH_MS, launchTestBrowser, logLines, TEST_TICK_MS, TEST_WAITS, testScope } from './helpers/browser';
const config: ShipConfig = {
app: { name: 'Example Game', profile: 'game', privacyPolicyUrl: 'https://example.com/privacy', supportUrl: 'https://example.com/support', website: 'https://example.com', contactEmail: 'support@example.com' },
@ -208,7 +209,9 @@ beforeAll(() => {
});
// bounded: every context is closed per test (afterEach) and each close is capped (tests/helpers/browser.ts)
afterEach(() => scope.settle(), AFTER_EACH_MS);
afterAll(async () => { await shared.close(); server?.stop(true); }, AFTER_ALL_MS);
let restoreWaits = () => {};
beforeAll(() => { restoreWaits = engineWaits(TEST_WAITS); });
afterAll(async () => { restoreWaits(); await shared.close(); server?.stop(true); }, AFTER_ALL_MS);
async function open(file: string) {
const page = await scope.newPage();
@ -221,7 +224,7 @@ const flagged = (page: any) => page.evaluate(() => document.querySelectorAll('[d
const clicks = (page: any) => page.evaluate(() => (window as any).__clicks);
describe.skipIf(!browser)('engine (headless, modeled pages)', () => {
test('app record: certain fields filled, uncertain empty + highlighted, Create never clicked', async () => {
scope.concurrent('app record: certain fields filled, uncertain empty + highlighted, Create never clicked', async () => {
const page = await open('apple-app-record.html');
const def = formDef('apple-app-record', pageAnswers('apple-app-record'));
const s = await fillPage(page, def, pageAnswers('apple-app-record'));
@ -242,7 +245,7 @@ describe.skipIf(!browser)('engine (headless, modeled pages)', () => {
await page.close();
}, BROWSER_TEST_MS);
test('User Access under a plain heading (real App Store Connect): the radios are found in the dialog', async () => {
scope.concurrent('User Access under a plain heading (real App Store Connect): the radios are found in the dialog', async () => {
const page = await open('apple-app-record-heading.html');
const s = await fillPage(page, formDef('apple-app-record', pageAnswers('apple-app-record')), pageAnswers('apple-app-record'));
expect(s.fallback).toBe(false);
@ -252,7 +255,7 @@ describe.skipIf(!browser)('engine (headless, modeled pages)', () => {
await page.close();
}, BROWSER_TEST_MS);
test('a lasting partial miss fills every field that was found and leaves only the missing one', async () => {
scope.concurrent('a lasting partial miss fills every field that was found and leaves only the missing one', async () => {
const page = await open('apple-app-record-renamed.html');
const def = formDef('apple-app-record', pageAnswers('apple-app-record'));
const s = await fillPage(page, def, pageAnswers('apple-app-record'), new Set(), true);
@ -265,7 +268,7 @@ describe.skipIf(!browser)('engine (headless, modeled pages)', () => {
await page.close();
}, BROWSER_TEST_MS);
test('a renamed label stops filling and falls back to guidance', async () => {
scope.concurrent('a renamed label stops filling and falls back to guidance', async () => {
const page = await open('apple-app-record-renamed.html');
const s = await fillPage(page, formDef('apple-app-record', pageAnswers('apple-app-record')), pageAnswers('apple-app-record'));
expect(s.fallback).toBe(true);
@ -277,7 +280,7 @@ describe.skipIf(!browser)('engine (headless, modeled pages)', () => {
await page.close();
}, BROWSER_TEST_MS);
test('app privacy (fillPage alone): section by section as the page changes; fillPage itself never clicks Next / Save / Publish', async () => {
scope.concurrent('app privacy (fillPage alone): section by section as the page changes; fillPage itself never clicks Next / Save / Publish', async () => {
const page = await open('apple-app-privacy.html');
const ans = pageAnswers('apple-app-privacy');
const def = formDef('apple-app-privacy', ans);
@ -311,7 +314,7 @@ describe.skipIf(!browser)('engine (headless, modeled pages)', () => {
await page.close();
}, BROWSER_TEST_MS);
test('content rating: full email in the page, Game, purchases yes, the rest empty + highlighted', async () => {
scope.concurrent('content rating: full email in the page, Game, purchases yes, the rest empty + highlighted', async () => {
const page = await open('play-content-rating.html');
const ans = pageAnswers('play-content-rating');
const def = formDef('play-content-rating', ans);
@ -332,7 +335,7 @@ describe.skipIf(!browser)('engine (headless, modeled pages)', () => {
await page.close();
}, BROWSER_TEST_MS);
test('target audience: ages, appeal highlighted, ads yes', async () => {
scope.concurrent('target audience: ages, appeal highlighted, ads yes', async () => {
const page = await open('play-target-audience.html');
const ans = pageAnswers('play-target-audience');
const def = formDef('play-target-audience', ans);
@ -349,7 +352,7 @@ describe.skipIf(!browser)('engine (headless, modeled pages)', () => {
await page.close();
}, BROWSER_TEST_MS);
test('sign-in detector and safeClick', async () => {
scope.concurrent('sign-in detector and safeClick', async () => {
const signin = await open('signin.html');
expect(await signInPage(signin)).toBe(true);
await signin.close();
@ -419,7 +422,7 @@ describe.skipIf(!browser)('navigation (headless, modeled pages)', () => {
const noneDenied = (names: string[]) => { for (const n of names) expect(navAllowed(n, 'link')).toBe(true); };
const fresh = async () => { const p = await scope.newPage(); await p.goto(base + 'blank.html'); await p.evaluate(() => sessionStorage.clear()); return p; };
test('App Store Connect (EN): + > New App opens the dialog and it is filled; Create and Submit for Review never clicked', async () => {
scope.concurrent('App Store Connect (EN): + > New App opens the dialog and it is filled; Create and Submit for Review never clicked', async () => {
const page = await fresh();
const r = await navigate(page, navPlan('apple-app-record', config, bases()), quiet);
expect(r).toMatchObject({ ok: true, steps: ['open Apps', 'the add button (+)', 'New App in the menu', 'the New App dialog'] });
@ -431,7 +434,7 @@ describe.skipIf(!browser)('navigation (headless, modeled pages)', () => {
await page.close();
}, BROWSER_TEST_MS);
test('App Store Connect (TR): Uygulama Ekle > Yeni Uygulama; Oluştur and İncelemeye Gönder never clicked', async () => {
scope.concurrent('App Store Connect (TR): Uygulama Ekle > Yeni Uygulama; Oluştur and İncelemeye Gönder never clicked', async () => {
const page = await fresh();
const r = await navigate(page, navPlan('apple-app-record', config, bases('asc-tr')), quiet);
expect(r.ok).toBe(true);
@ -440,7 +443,7 @@ describe.skipIf(!browser)('navigation (headless, modeled pages)', () => {
await page.close();
}, BROWSER_TEST_MS);
test('a missing step stops navigation, names the step and clicks nothing', async () => {
scope.concurrent('a missing step stops navigation, names the step and clicks nothing', async () => {
const page = await fresh();
const said: string[] = [];
const r = await navigate(page, navPlan('apple-app-record', config, bases('asc-noplus')), { timeoutMs: 800, say: m => said.push(m) });
@ -451,7 +454,7 @@ describe.skipIf(!browser)('navigation (headless, modeled pages)', () => {
await page.close();
}, BROWSER_TEST_MS);
test('App Privacy: appId deep link, or app list > the app > App Privacy; two apps of one name -> stop and list', async () => {
scope.concurrent('App Privacy: appId deep link, or app list > the app > App Privacy; two apps of one name -> stop and list', async () => {
let page = await fresh();
let r = await navigate(page, navPlan('apple-app-privacy', { ...config, apple: { ...config.apple!, appId: '123' } } as ShipConfig, bases()), quiet);
expect(r).toMatchObject({ ok: true, steps: ['open App Privacy of the app (apple.appId)', 'the App Privacy page'] });
@ -469,7 +472,7 @@ describe.skipIf(!browser)('navigation (headless, modeled pages)', () => {
await page.close();
}, BROWSER_TEST_MS);
test('App Privacy: an app card that names the app twice (icon aria-label + title link, same /apps/<id>) is one app', async () => {
scope.concurrent('App Privacy: an app card that names the app twice (icon aria-label + title link, same /apps/<id>) is one app', async () => {
const page = await fresh();
const r = await navigate(page, navPlan('apple-app-privacy', config, bases('asc-card')), quiet);
expect(r).toMatchObject({ ok: true });
@ -479,7 +482,7 @@ describe.skipIf(!browser)('navigation (headless, modeled pages)', () => {
await page.close();
}, BROWSER_TEST_MS);
test('Play: the app by package name > Policy and programs > App content > Start / Manage; nothing deny-listed clicked', async () => {
scope.concurrent('Play: the app by package name > Policy and programs > App content > Start / Manage; nothing deny-listed clicked', async () => {
for (const [form, link, heading] of [['play-content-rating', 'Start', 'Content rating'], ['play-target-audience', 'Manage', 'Target audience and content']] as const) {
const page = await fresh();
const r = await navigate(page, navPlan(form, config, bases()), quiet);
@ -492,7 +495,7 @@ describe.skipIf(!browser)('navigation (headless, modeled pages)', () => {
}
}, BROWSER_TEST_MS);
test('navClick refuses a Start button, Start rollout, Save and Publish even when pointed at them', async () => {
scope.concurrent('navClick refuses a Start button, Start rollout, Save and Publish even when pointed at them', async () => {
const page = await open('play-app-content.html');
expect((await navClick(page.getByRole('button', { name: 'Start' }))).clicked).toBe(false);
expect((await navClick(page.getByRole('button', { name: 'Save' }))).clicked).toBe(false);
@ -504,7 +507,7 @@ describe.skipIf(!browser)('navigation (headless, modeled pages)', () => {
await page.close();
}, BROWSER_TEST_MS);
test('already on the form: nothing is clicked', async () => {
scope.concurrent('already on the form: nothing is clicked', async () => {
const page = await open('play-content-rating.html');
const r = await navigate(page, navPlan('play-content-rating', config, bases()), quiet);
expect(r).toMatchObject({ ok: true, steps: ['already open'] });
@ -535,14 +538,14 @@ afterAll(() => { for (const d of tmpDirs) rmSync(d, { recursive: true, force: tr
const tmpAccounts = () => { const d = mkdtempSync(join(tmpdir(), 'storebound-accounts-')); tmpDirs.push(d); return join(d, 'console-accounts.json'); };
/**
* The engine's timings scaled down for tests (production: tick 1 s, 20 s per navigation step, 10 min for a sign-in).
* navTimeoutMs is both how long a step that IS there may take to show up (headroom for a loaded machine) and what a
* step that is NOT there costs (the watching tests wait that out once), so it stays a few seconds, not FOUND_MS.
* navTimeoutMs is how long a step that IS there may take to show up (headroom for a loaded machine); a step that is
* NOT there ends earlier, once its loaded page stays unchanged for TEST_WAITS.quietMs (tests/helpers/browser.ts).
*/
const fast = (log: ReturnType<typeof logLines>) => ({ say: log.say, tickMs: 100, navTimeoutMs: 5000, signInWaitMs: 30_000, accountsFile: tmpAccounts(), env: {} });
const fast = (log: ReturnType<typeof logLines>) => ({ say: log.say, tickMs: TEST_TICK_MS, navTimeoutMs: 5000, signInWaitMs: 30_000, accountsFile: tmpAccounts(), env: {} });
const FILLED = (lines: string[]) => lines.some(m => /^New App: filled \d+/.test(m));
describe.skipIf(!browser)('sign-in and watching (headless, modeled pages on the real console hosts, routed locally)', () => {
test('ASC /login with an idmsa frame and no password field: detected mid-navigation, never touched; after sign-in navigation restarts at step 1', async () => {
scope.concurrent('ASC /login with an idmsa frame and no password field: detected mid-navigation, never touched; after sign-in navigation restarts at step 1', async () => {
const b = await consoleBrowser((u, cookie) => u.hostname === 'idmsa.apple.com' ? 'idmsa-signin.html'
: u.hostname !== 'appstoreconnect.apple.com' ? null
: u.pathname === '/login' ? 'asc-login.html' : u.pathname === '/apps' ? (cookie.includes('myacinfo=') ? 'asc-apps.html' : 'asc-shell.html') : null);
@ -565,7 +568,7 @@ describe.skipIf(!browser)('sign-in and watching (headless, modeled pages on the
expect((await run).filled).toBeGreaterThan(0);
}, SLOW_TEST_MS);
test('after the printed steps storebound keeps watching: the owner opens Apps by hand, navigation resumes at step 2 and fills', async () => {
scope.concurrent('after the printed steps storebound keeps watching: the owner opens Apps by hand, navigation resumes at step 2 and fills', async () => {
const b = await consoleBrowser(u => u.hostname === 'appstoreconnect.apple.com' && u.pathname === '/apps' ? (u.search.includes('owner') ? 'asc-apps.html' : 'asc-apps-noplus.html') : null);
const log = logLines(), said = log.lines;
const run = scope.run(fillSession(b, { form: 'apple-app-record', config, ...fast(log) }));
@ -580,7 +583,7 @@ describe.skipIf(!browser)('sign-in and watching (headless, modeled pages on the
await run;
}, SLOW_TEST_MS);
test('after the printed steps: the owner opens the dialog by hand, storebound sees it and fills it', async () => {
scope.concurrent('after the printed steps: the owner opens the dialog by hand, storebound sees it and fills it', async () => {
const b = await consoleBrowser(u => u.hostname === 'appstoreconnect.apple.com' && u.pathname === '/apps' ? (u.search.includes('dialog') ? 'apple-app-record.html' : 'asc-apps-noplus.html') : null);
const log = logLines();
const run = scope.run(fillSession(b, { form: 'apple-app-record', config, ...fast(log) }));
@ -592,7 +595,7 @@ describe.skipIf(!browser)('sign-in and watching (headless, modeled pages on the
await run;
}, SLOW_TEST_MS);
test('resumeAt: the furthest step visible now, never the initial page load', async () => {
scope.concurrent('resumeAt: the furthest step visible now, never the initial page load', async () => {
const page = await open('asc-apps.html');
const plan = navPlan('apple-app-record', config, bases());
expect(await resumeAt(page, plan)).toBe(1);
@ -657,7 +660,7 @@ describe.skipIf(!browser)('accounts (headless, modeled pages)', () => {
const fresh = async () => { const p = await scope.newPage(); await p.goto(base + 'blank.html'); await p.evaluate(() => sessionStorage.clear()); return p; };
const DENIED = ['Sign Out', 'Edit Profile', 'Create', 'Submit for Review', 'Create app', 'Create developer account', 'Save', 'Publish'];
test('App Store Connect, 3 teams, the bundle id in the 2nd: switched through the account menu, remembered, then picked first', async () => {
scope.concurrent('App Store Connect, 3 teams, the bundle id in the 2nd: switched through the account menu, remembered, then picked first', async () => {
const file = tmpAccounts(), said: string[] = [];
let page = await fresh();
await page.goto(base + 'teams/apps');
@ -684,7 +687,7 @@ describe.skipIf(!browser)('accounts (headless, modeled pages)', () => {
await page.close();
}, SLOW_TEST_MS);
test('Play, one login in 5 developer accounts, the app in the 4th: found by package name, remembered, then opened directly', async () => {
scope.concurrent('Play, one login in 5 developer accounts, the app in the 4th: found by package name, remembered, then opened directly', async () => {
const file = tmpAccounts(), said: string[] = [];
const pb = { asc: base + 'asc', play: base + 'playm' };
let page = await fresh();
@ -711,7 +714,7 @@ describe.skipIf(!browser)('accounts (headless, modeled pages)', () => {
await page.close();
}, SLOW_TEST_MS);
test('Play: a package in none of the developer accounts is said plainly; nothing deny-listed clicked', async () => {
scope.concurrent('Play: a package in none of the developer accounts is said plainly; nothing deny-listed clicked', async () => {
const page = await fresh();
const absent = { ...config, play: { packageName: 'com.example.absent' } } as ShipConfig;
const r = await openAcross(page, { form: 'play-content-rating', config: absent, say: () => {}, bases: { asc: base + 'asc', play: base + 'playm' }, navMs: 800, file: tmpAccounts(), env: {} });
@ -828,7 +831,7 @@ describe.skipIf(!browser)('play-app-record (headless, modeled pages)', () => {
}, BROWSER_TEST_MS);
}
test('an agreement answer that says yes is still never ticked', async () => {
scope.concurrent('an agreement answer that says yes is still never ticked', async () => {
const page = await open('play-create-app.html');
const yes = ans().map(a => (a.agreement ? { ...a, value: true, certain: true } : a));
const s = await fillPage(page, def(), yes);
@ -838,7 +841,7 @@ describe.skipIf(!browser)('play-app-record (headless, modeled pages)', () => {
await page.close();
}, BROWSER_TEST_MS);
test('a renamed field: partial fill, the missing one left to the owner; nothing ticked or submitted', async () => {
scope.concurrent('a renamed field: partial fill, the missing one left to the owner; nothing ticked or submitted', async () => {
const page = await open('play-create-app-renamed.html');
expect((await fillPage(page, def(), ans())).fallback).toBe(true); // first rounds: wait (the page may be loading)
const s = await fillPage(page, def(), ans(), new Set(), true);
@ -851,7 +854,7 @@ describe.skipIf(!browser)('play-app-record (headless, modeled pages)', () => {
await page.close();
}, BROWSER_TEST_MS);
test('the opener inside a dialog, or on a page that is not the app list, is refused', async () => {
scope.concurrent('the opener inside a dialog, or on a page that is not the app list, is refused', async () => {
const page = await open('blank.html');
await page.setContent('<h1>All apps</h1><div role="dialog" aria-label="x"><button type="button">Create app</button></div>');
expect((await navClick(page.getByRole('button', { name: 'Create app' }), true)).clicked).toBe(false);
@ -862,7 +865,7 @@ describe.skipIf(!browser)('play-app-record (headless, modeled pages)', () => {
await page.close();
}, BROWSER_TEST_MS);
test('accounts: several and none picked -> stop and list; by play.developerAccount -> that one, remembered; by play.developerId -> directly', async () => {
scope.concurrent('accounts: several and none picked -> stop and list; by play.developerAccount -> that one, remembered; by play.developerId -> directly', async () => {
const go = async (c: ShipConfig, file: string, said: string[], env: Record<string, string> = {}) => {
const page = await fresh();
const r = await openAcross(page, { form: 'play-app-record', config: c, say: m => { said.push(m); }, bases: pb('playn'), navMs: FOUND_MS, file, env });
@ -904,7 +907,7 @@ describe.skipIf(!browser)('play-app-record (headless, modeled pages)', () => {
await page.close();
}, SLOW_TEST_MS);
test('the chooser as it really looks (nested spans, icon fonts, role=button rows with data ids, an avatar row without an id): exact, case pair, prefix, not found', async () => {
scope.concurrent('the chooser as it really looks (nested spans, icon fonts, role=button rows with data ids, an avatar row without an id): exact, case pair, prefix, not found', async () => {
const go = async (account: string | undefined, said: string[], play = 'playr') => {
const page = await fresh();
const c = { ...config, play: { ...config.play!, ...(account ? { developerAccount: account } : {}) } } as ShipConfig;
@ -955,7 +958,7 @@ describe.skipIf(!browser)('play-app-record (headless, modeled pages)', () => {
await page.close();
}, SLOW_TEST_MS);
test('fillSession: notes logged, form filled, the run ends without any submit', async () => {
scope.concurrent('fillSession: notes logged, form filled, the run ends without any submit', async () => {
const b = await consoleBrowser(u => (u.hostname !== 'play.google.com' ? null : u.pathname === '/console' || u.pathname.endsWith('/app-list') ? 'play-home.html' : u.pathname === '/play-create-app.html' || u.pathname.endsWith('/create-new-app') ? 'play-create-app.html' : null));
const log = logLines(), said = log.lines;
const c = { ...config, play: { ...config.play!, developerId: '4444' } } as ShipConfig;
@ -1007,7 +1010,7 @@ async function until(pred: () => boolean): Promise<void> {
}
describe.skipIf(!browser)('sign-in detection: only the top-level URL, the main frame\'s form or an Apple sign-in frame', () => {
test('the Play account chooser with the account widget (accounts.google.com frames, avatar and email header) is NOT a sign-in page', async () => {
scope.concurrent('the Play account chooser with the account widget (accounts.google.com frames, avatar and email header) is NOT a sign-in page', async () => {
const b = await consoleBrowser(playOgb);
await b.page.goto('https://play.google.com/console/u/0/developers');
await until(() => frameOn(b.page, 'accounts.google.com') && frameOn(b.page, 'ogs.google.com'));
@ -1016,7 +1019,7 @@ describe.skipIf(!browser)('sign-in detection: only the top-level URL, the main f
await b.context.close();
}, BROWSER_TEST_MS);
test('Play home with the widget frame is NOT a sign-in page', async () => {
scope.concurrent('Play home with the widget frame is NOT a sign-in page', async () => {
const b = await consoleBrowser(playOgb);
await b.page.goto('https://play.google.com/console/u/0/developers/7000000001/app-list');
await until(() => frameOn(b.page, 'accounts.google.com') && frameOn(b.page, 'ogs.google.com'));
@ -1024,7 +1027,7 @@ describe.skipIf(!browser)('sign-in detection: only the top-level URL, the main f
await b.context.close();
}, BROWSER_TEST_MS);
test('a top-level accounts.google.com page IS a sign-in page, and the rule says why (URL without its query)', async () => {
scope.concurrent('a top-level accounts.google.com page IS a sign-in page, and the rule says why (URL without its query)', async () => {
const b = await consoleBrowser(playOgb);
await b.page.goto('https://accounts.google.com/v3/signin/identifier?continue=https%3A%2F%2Fplay.google.com%2Fconsole');
expect(await signInReason(b.page)).toEqual({ rule: 'the top-level URL is a sign-in page', url: 'https://accounts.google.com/v3/signin/identifier' });
@ -1035,7 +1038,7 @@ describe.skipIf(!browser)('sign-in detection: only the top-level URL, the main f
await page.close();
}, BROWSER_TEST_MS);
test('fillSession play-app-record: the chooser with the widget frames is passed, the form filled, no sign-in said', async () => {
scope.concurrent('fillSession play-app-record: the chooser with the widget frames is passed, the form filled, no sign-in said', async () => {
const b = await consoleBrowser(playOgb);
const log = logLines(), said = log.lines;
const done = scope.run(fillSession(b, { form: 'play-app-record', config, ...fast(log) }));
@ -1047,7 +1050,7 @@ describe.skipIf(!browser)('sign-in detection: only the top-level URL, the main f
await done;
}, SLOW_TEST_MS);
test('a copied Chrome session that lands on accounts.google.com: said plainly with the rule; --debug-dump dumps the sign-in wait (at most 3), the watchdog once', async () => {
scope.concurrent('a copied Chrome session that lands on accounts.google.com: said plainly with the rule; --debug-dump dumps the sign-in wait (at most 3), the watchdog once', async () => {
const dir = mkdtempSync(join(tmpdir(), 'sb-signin-debug-'));
tmpDirs.push(dir);
const b: ConsoleBrowser = { ...await consoleBrowser(u => (u.hostname === 'play.google.com' ? 'play-signed-out.html' : playOgb(u))), mode: 'copy' };