Play edits under a machine-wide per-package lock (parallel reads of one app no longer delete each other's edits; a read whose edit was deleted is redone once, writes are not); a store that failed to read is 'Couldn't read Google Play' and keeps its last good releases with 'couldn't refresh'; stores on different versions: neutral 'Current · Play x · App Store y' header and each lane its own state, 'Not on this store' only without a record; Play status decided from the public store page's live version (Live / In review / In review (live: x) / managed publishing / Rolling out / Halted / Draft; only a network failure says 'Couldn't check the store page', with the last known answer and its time); tests/play-edit-lock.test.ts, recorded page fixture, boot check auto-read cases (pitfall 58)

This commit is contained in:
CRTkafa 2026-10-14 00:14:32 +08:00
parent e59f439101
commit 714352ec18
24 changed files with 570 additions and 103 deletions

View file

@ -541,7 +541,7 @@ async function runReleaseStateCase(browser: Browser, sidecar: Sidecar, page404:
let visited = 0;
const text = async (sel = '#content') => (await page.locator(sel).first().innerText().catch(() => '')).replace(/\s+/g, ' ');
const crashCheck = async (where: string) => { for (const c of (await state(page)).crash) problems.push(`${where}: error panel ${c}`); };
const SENT = 'Sent to Google (in review or not yet published)';
const SENT = 'In review'; // decided from the store page: 404 = in review (no uncertain wording)
try {
await page.goto(`http://127.0.0.1:${server.port}/`, { waitUntil: 'load' });
await page.waitForFunction(() => document.documentElement.dataset.booted === '1', null, { timeout: WAIT_MS }).catch(() => problems.push('React never drew the window (no data-booted)'));
@ -568,6 +568,7 @@ async function runReleaseStateCase(browser: Browser, sidecar: Sidecar, page404:
if (row[1] !== 'In review') problems.push(`Releases: App Store cell "${row[1]}" (want "In review")`);
if (row[3] !== (page404 ? SENT : 'Live')) problems.push(`Releases: Google Play cell "${row[3]}" (want "${page404 ? SENT : 'Live'}")`);
const rel = await text();
if (/not yet published|may still be reviewing|Sent to Google/.test(rel + pf)) problems.push('uncertain Play wording on screen');
if (/is not on (App Store|Google Play) yet/.test(rel)) problems.push('Releases: a lane says the version is not on a store (1.0 and 1.0.0 not joined)');
for (const w of ['1.0 (3)', '1.0.0 (3)']) if (!rel.includes(w)) problems.push(`Releases: the lane does not show the store's own "${w}"`);
if (page404) {
@ -609,6 +610,8 @@ async function runAutoReadCase(browser: Browser): Promise<CaseResult> {
const notes: string[] = [];
const logged: string[] = [];
let reads = 0;
const playOpen = new Map<string, number>();
let playPeak = 0;
// fixture art is file: URLs (blocked by the CSP): icons are left out
const clean = (v: unknown): unknown => (v === undefined ? null : JSON.parse(JSON.stringify(v), (_k, x) => (typeof x === 'string' && x.startsWith('file:') ? null : x)));
const answer = async (cmd: string, a: Record<string, unknown>): Promise<unknown> => {
@ -621,6 +624,15 @@ async function runAutoReadCase(browser: Browser): Promise<CaseResult> {
const method = String(a.method);
if (method === 'system.tools') return clean(await fx.mockInvoke(cmd, a));
reads++;
if (method.startsWith('play.') || ['health', 'reviews.list', 'catalog.diff', 'prices.current', 'validate'].includes(method)) {
// Play reads of one app may run at once (the scheduler's 4 per store, starts 250 ms apart per key): count the
// overlap and hold each read long enough for it to happen
const k = String(a.projectId);
const n = (playOpen.get(k) ?? 0) + 1;
playOpen.set(k, n); playPeak = Math.max(playPeak, n);
await new Promise(r => setTimeout(r, 400));
playOpen.set(k, playOpen.get(k)! - 1);
}
return clean(fx.fixtureRead(method, String(a.projectId), ((a.params as { args?: Record<string, unknown> } | null)?.args) ?? {}));
}
default: return clean(await fx.mockInvoke(cmd, a));
@ -664,10 +676,24 @@ async function runAutoReadCase(browser: Browser): Promise<CaseResult> {
await settle(page);
await crashCheck(`${appName} ${id}`);
const tx = await text();
// a Play read that lost its edit to a parallel read (pitfall), or a store that failed called empty
for (const bad of [/Edit has been deleted/i, /is not on Google Play yet/i]) if (bad.test(tx)) problems.push(`${appName} ${id}: says "${(bad.exec(tx) ?? [''])[0]}"`);
if (/not read yet|Read the stores to see this/i.test(tx)) problems.push(`${appName} ${id}: says "${(/not read yet|Read the stores to see this/i.exec(tx) ?? [''])[0]}" after the background read`);
visited++;
}
}
notes.push(`Play reads of one app at once: up to ${playPeak}`);
if (playPeak < 2) problems.push(`no two Play reads of one app ran at once (peak ${playPeak}): the case does not exercise parallel reads`);
// 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 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}"`);
for (const w of ['Not on this store', 'is not on App Store yet', '3.1.0 Released']) if (rel.includes(w)) problems.push(`Orbit Hop Releases: says "${w}"`);
notes.push(`Orbit Hop releases: ${rel.slice(0, 200)}`);
await crashCheck('Orbit Hop Releases');
// 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);