console fill play-app-record: read the real account chooser, match names loosely
With the copied Chrome profile, `console fill play-app-record` stopped with "no developer account named "<name>" (play.developerAccount) on the chooser" although the name was there: the real "Choose developer account" rows are not plain /developers/<id> text links (nested spans, icon-font glyphs, role=button rows with the id in a data attribute, an avatar row whose id only its click reveals). - readChooser: every visible clickable row in the page content (header and navigation left out); the name is the first visible line (icon fonts, images and aria-hidden parts skipped); the id comes from /developers/<id> in any attribute, else a data-*id attribute, else '' (the row is then opened through the account-switch gate and the id is read from the URL). "Create a new developer account" is not an account. - matchAccount: exact spelling, then spaces and case aside (Turkish I/ı folded), then a unique prefix. A case pair (ACME / Acme) needs the exact spelling; otherwise it stops and lists both with their ids. - One developer account: picked (a different play.developerAccount is said). Not found: every visible account is listed with its id. - The resolved developerId is remembered as before. Fixtures play-chooser-real.html (the owner's chooser shape, placeholder names) and play-chooser-one.html; tests for exact, case pair, prefix, single account and not found.
This commit is contained in:
parent
81313b6fa4
commit
8b669f5c86
5 changed files with 244 additions and 32 deletions
|
|
@ -10,7 +10,7 @@ import { afterAll, afterEach, beforeAll, describe, expect, test } from 'bun:test
|
|||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { openAcross, readAccounts, rememberApple, rememberPlay, withAccountIds } from '../cli/console/accounts';
|
||||
import { matchAccount, openAcross, readAccounts, rememberApple, rememberPlay, withAccountIds } from '../cli/console/accounts';
|
||||
import { createAppHere, fillPage, fillSession, navClick, navigate, resumeAt, run, safeClick, signInPage } from '../cli/console/fill';
|
||||
import type { ConsoleBrowser } from '../cli/console/session';
|
||||
import { parseArgs } from '../cli/lib/args';
|
||||
|
|
@ -193,8 +193,9 @@ beforeAll(() => {
|
|||
const path = new URL(req.url).pathname;
|
||||
const dev = path.match(/^\/playm\/u\/0\/developers(?:\/(\d+)\/app-list)?$/);
|
||||
// playn: the same chooser; every account's app list has a Create app button that opens the Create app form
|
||||
const devNew = path.match(/^\/playn\/u\/0\/developers(?:\/(\d+)\/app-list)?$/);
|
||||
const name = path.endsWith('/_common.js') ? '_common.js' : devNew ? (devNew[1] ? 'play-home.html' : 'play-chooser.html')
|
||||
const devNew = path.match(/^\/play([nrs])\/u\/0\/developers(?:\/(\d+)\/app-list)?$/);
|
||||
// playn: the modeled chooser; playr: the chooser as the owner's screenshot shows it; plays: one account only
|
||||
const name = path.endsWith('/_common.js') ? '_common.js' : devNew ? (devNew[2] ? 'play-home.html' : ({ n: 'play-chooser.html', r: 'play-chooser-real.html', s: 'play-chooser-one.html' } as Record<string, string>)[devNew[1]!]!)
|
||||
: dev ? (!dev[1] ? 'play-chooser.html' : dev[1] === '4444' ? 'play-apps.html' : 'play-apps-other.html')
|
||||
: /^\/asc-card\/apps\/\d+\/distribution\//.test(path) ? 'asc-app.html'
|
||||
: ROUTES[path] ?? path.slice(1);
|
||||
|
|
@ -605,6 +606,19 @@ describe.skipIf(!browser)('sign-in and watching (headless, modeled pages on the
|
|||
// ---- one login, several developer accounts / teams ----
|
||||
|
||||
describe('accounts (pure)', () => {
|
||||
test('matchAccount: exact spelling, then case aside, then a unique prefix; a case pair needs the exact spelling', () => {
|
||||
const all = [{ name: "Studio's Apps" }, { name: 'NORTHWIND' }, { name: 'Northwind' }, { name: 'Example Org' }, { name: 'Example AI' }, { name: 'IŞIK Oyun' }];
|
||||
expect(matchAccount('Example AI', all).hit?.name).toBe('Example AI');
|
||||
expect(matchAccount(' example ai ', all).hit?.name).toBe('Example AI');
|
||||
expect(matchAccount('Northwind', all).hit?.name).toBe('Northwind');
|
||||
expect(matchAccount('NORTHWIND', all).hit?.name).toBe('NORTHWIND');
|
||||
expect(matchAccount('northwind', all).ambiguous?.map(a => a.name)).toEqual(['NORTHWIND', 'Northwind']);
|
||||
expect(matchAccount('Example O', all).hit?.name).toBe('Example Org');
|
||||
expect(matchAccount('Example', all).ambiguous).toHaveLength(2);
|
||||
expect(matchAccount('ışık oyun', all).hit?.name).toBe('IŞIK Oyun');
|
||||
expect(matchAccount('Nope', all)).toEqual({});
|
||||
});
|
||||
|
||||
test('cache: ids only, malformed entries dropped, atomic rewrite', async () => {
|
||||
const f = tmpAccounts();
|
||||
expect(readAccounts(f)).toEqual({ version: 1, play: {}, apple: {} });
|
||||
|
|
@ -852,7 +866,7 @@ describe.skipIf(!browser)('play-app-record (headless, modeled pages)', () => {
|
|||
const file = tmpAccounts();
|
||||
let said: string[] = [];
|
||||
let { r, page } = await go(config, file, said);
|
||||
expect(r.nav).toMatchObject({ ok: false, failed: 'the developer account', candidates: ['Studio One', 'Studio Two', 'Studio Three', 'Example Games Ltd', 'Studio Five'] });
|
||||
expect(r.nav).toMatchObject({ ok: false, failed: 'the developer account', candidates: ['Studio One (1111)', 'Studio Two (2222)', 'Studio Three (3333)', 'Example Games Ltd (4444)', 'Studio Five (5555)'] });
|
||||
expect(r.nav.why).toContain('set play.developerAccount');
|
||||
expect(await navClicks(page)).toEqual([]);
|
||||
expect(readAccounts(file).play).toEqual({});
|
||||
|
|
@ -860,7 +874,7 @@ describe.skipIf(!browser)('play-app-record (headless, modeled pages)', () => {
|
|||
|
||||
({ r, page } = await go({ ...config, play: { ...config.play!, developerAccount: 'example games ltd' } } as ShipConfig, file, said = []));
|
||||
expect(r.nav.ok).toBe(true);
|
||||
expect(said).toContain('creating the app in developer account "Example Games Ltd" (play.developerAccount)');
|
||||
expect(said).toContain('creating the app in developer account "Example Games Ltd" (4444, play.developerAccount)');
|
||||
expect(new URL(page.url()).pathname).toBe('/play-create-app.html');
|
||||
expect(await navClicks(page)).toEqual(['Create app']);
|
||||
expect(readAccounts(file).play['com.example.game']).toMatchObject({ developerId: '4444', account: 'Example Games Ltd' });
|
||||
|
|
@ -885,6 +899,57 @@ 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 () => {
|
||||
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;
|
||||
const r = await openAcross(page, { form: 'play-app-record', config: c, say: m => { said.push(m); }, bases: pb(play), navMs: FOUND_MS, file: tmpAccounts(), env: {} });
|
||||
return { r, page };
|
||||
};
|
||||
// the avatar row: its id only shows after the click, the click goes through the account-switch gate
|
||||
let said: string[] = [];
|
||||
let { r, page } = await go('Example AI', said);
|
||||
expect(r.nav.ok).toBe(true);
|
||||
expect(said).toContain('creating the app in developer account "Example AI" (7000000005, play.developerAccount)');
|
||||
expect(new URL(page.url()).pathname).toBe('/play-create-app.html');
|
||||
await page.close();
|
||||
// spaces and case aside
|
||||
({ r, page } = await go(' example ai ', said = []));
|
||||
expect(said).toContain('creating the app in developer account "Example AI" (7000000005, play.developerAccount)');
|
||||
await page.close();
|
||||
// the case pair: the exact spelling picks; a spelling that matches both only case-insensitively stops and lists both
|
||||
({ r, page } = await go('Northwind', said = []));
|
||||
expect(said).toContain('creating the app in developer account "Northwind" (7000000003, play.developerAccount)');
|
||||
await page.close();
|
||||
({ r, page } = await go('NORTHWIND', said = []));
|
||||
expect(said).toContain('creating the app in developer account "NORTHWIND" (7000000002, play.developerAccount)');
|
||||
await page.close();
|
||||
({ r, page } = await go('northwind', said = []));
|
||||
expect(r.nav).toMatchObject({ ok: false, failed: 'the developer account' });
|
||||
expect(r.nav.why).toContain('matches more than one developer account: NORTHWIND (7000000002), Northwind (7000000003)');
|
||||
expect(await navClicks(page)).toEqual([]);
|
||||
await page.close();
|
||||
// a unique prefix; the id from a data attribute
|
||||
({ r, page } = await go('Example O', said = []));
|
||||
expect(said).toContain('creating the app in developer account "Example Org" (7000000004, play.developerAccount)');
|
||||
await page.close();
|
||||
// not found: every visible account with its id (the avatar row: "id not shown"); Create a new developer account is not one
|
||||
({ r, page } = await go('Nope Studio', said = []));
|
||||
expect(r.nav.why).toBe('no developer account named "Nope Studio" (play.developerAccount) on the chooser: Studio\'s Apps (7000000001), NORTHWIND (7000000002), Northwind (7000000003), Example Org (7000000004), Example AI (id not shown). Set play.developerAccount to one of them, or play.developerId');
|
||||
expect(r.nav.candidates).toHaveLength(5);
|
||||
expect(await navClicks(page)).toEqual([]);
|
||||
await page.close();
|
||||
// one developer account: picked, also when play.developerAccount names another
|
||||
({ r, page } = await go(undefined, said = [], 'plays'));
|
||||
expect(r.nav.ok).toBe(true);
|
||||
expect(said).toContain('creating the app in developer account "Example AI" (7000000005, the only developer account of this login)');
|
||||
await page.close();
|
||||
({ r, page } = await go('Other Name', said = [], 'plays'));
|
||||
expect(r.nav.ok).toBe(true);
|
||||
expect(said[0]).toBe('play.developerAccount is "Other Name", but this Google login has only the developer account "Example AI": using it');
|
||||
await page.close();
|
||||
}, SLOW_TEST_MS);
|
||||
|
||||
test('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' ? 'play-create-app.html' : null));
|
||||
const log = logLines(), said = log.lines;
|
||||
|
|
|
|||
14
tests/fixtures/console/play-chooser-one.html
vendored
Normal file
14
tests/fixtures/console/play-chooser-one.html
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<!doctype html>
|
||||
<!-- MODELED: Play Console's "Choose developer account" for a Google login with ONE developer account (an avatar row
|
||||
whose id only its click shows) plus "Create a new developer account", which is not an account. Placeholder data. -->
|
||||
<html lang="en"><head><meta charset="utf-8"><title>Choose developer account - Play Console</title><script src="/_common.js"></script></head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Choose developer account</h1>
|
||||
<div role="list">
|
||||
<div role="listitem"><div role="button" tabindex="0" id="only"><img alt="Example AI" width="24" height="24" src="data:image/gif;base64,R0lGODlhAQABAAAAACw="><span>Example AI</span></div></div>
|
||||
<div role="listitem"><a href="/signup"><span class="material-icons">add</span><span>Create a new developer account</span></a></div>
|
||||
</div>
|
||||
</main>
|
||||
<script>document.getElementById('only').addEventListener('click', () => { location.href = './developers/7000000005/app-list'; });</script>
|
||||
</body></html>
|
||||
30
tests/fixtures/console/play-chooser-real.html
vendored
Normal file
30
tests/fixtures/console/play-chooser-real.html
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<!doctype html>
|
||||
<!-- MODELED from an owner's screenshot of Play Console's account chooser (the account names replaced by placeholders, the
|
||||
shape kept): a page titled "Choose developer account" whose rows are NOT plain text links.
|
||||
Rows mix the shapes seen in Material lists: an icon font glyph (its ligature text, e.g. "account_circle", sits in
|
||||
the row's text), nested spans with a second line, a role=button row with the id in a data attribute, and one row
|
||||
with an avatar image and no id at all (only its click shows where it goes). Two accounts differ only in case
|
||||
(NORTHWIND / Northwind). "Create a new developer account" must never be read as an account nor clicked. A header with
|
||||
its own buttons sits above. Ids are placeholders. -->
|
||||
<html lang="en"><head><meta charset="utf-8"><title>Choose developer account - Play Console</title><script src="/_common.js"></script>
|
||||
<style>.material-icons { font-family: monospace; } .sub { display: block; color: #666; font-size: 12px; } .row { display: flex; gap: 8px; align-items: center; padding: 6px; cursor: pointer; }</style></head>
|
||||
<body>
|
||||
<header><button type="button" aria-label="Help">?</button> <button type="button">Example Owner</button></header>
|
||||
<main>
|
||||
<h1>Choose developer account</h1>
|
||||
<div role="list">
|
||||
<div role="listitem"><a class="row" href="./developers/7000000001/app-list"><span class="material-icons" aria-hidden="true">account_circle</span><span><span>Studio's Apps</span><span class="sub">Developer account</span></span></a></div>
|
||||
<div role="listitem"><a class="row" href="./developers/7000000002/app-list"><span class="material-icons">account_circle</span><span><span>NORTHWIND</span></span></a></div>
|
||||
<div role="listitem"><div class="row" role="button" tabindex="0" data-developer-id="7000000003"><mat-icon class="material-icons">account_circle</mat-icon><span>Northwind</span></div></div>
|
||||
<div role="listitem"><div class="row" role="button" tabindex="0" data-account-id="7000000004"><mat-icon class="material-icons">account_circle</mat-icon><span><span>Example Org</span> <span class="sub">Organization</span></span></div></div>
|
||||
<div role="listitem"><div class="row" role="button" tabindex="0" id="avatar-row"><img alt="Example AI" width="24" height="24" src="data:image/gif;base64,R0lGODlhAQABAAAAACw="><span><span>Example</span> <span>AI</span></span></div></div>
|
||||
<div role="listitem"><a class="row" href="/signup"><span class="material-icons">add</span><span>Create a new developer account</span></a></div>
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
document.querySelectorAll('[data-developer-id], [data-account-id]').forEach(r => r.addEventListener('click', () => {
|
||||
location.href = `./developers/${r.getAttribute('data-developer-id') || r.getAttribute('data-account-id')}/app-list`;
|
||||
}));
|
||||
document.getElementById('avatar-row').addEventListener('click', () => { location.href = './developers/7000000005/app-list'; });
|
||||
</script>
|
||||
</body></html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue