console: Play App content card Saves are the agent's (declaration-save; Send changes for review, Submit, Publish, Restart review stay forbidden), one owner batch per plan with the IARC Terms tick before the run, console plan play-app-content --all, New App fields typed not set, play.appId from the Create app redirect (console verify --url --write) and storebound play app-id across app list pages

This commit is contained in:
CRTkafa 2026-10-13 05:43:56 +08:00
parent 1366e7e231
commit f741c15829
29 changed files with 1029 additions and 165 deletions

View file

@ -117,7 +117,7 @@ describe('button classes (FORM_BUTTONS, flowClass, flowAllowed)', () => {
expect(setUpAllowed('Set Up Coarse Location', { ...w, privacyPage: false })).toBe(false);
expect(setUpTexts('Purchase History')).toContain('Set Up Purchases');
});
test('other forms: Next is navigation; the content rating\'s Save draft is clicked; Save, Submit, Create, Done stay the owner\'s', () => {
test('other forms: Next is navigation; the content rating\'s Save draft and the Play declarations\' Save are clicked; Submit, Create, Done, Send changes for review stay the owner\'s', () => {
const w = at({ privacyPage: false });
for (const f of ['play-content-rating', 'play-target-audience', 'apple-app-record', 'play-app-record'] as const) {
expect(flowAllowed(f, 'Next', 'button', w)).toBe(true);
@ -125,18 +125,25 @@ describe('button classes (FORM_BUTTONS, flowClass, flowAllowed)', () => {
expect(flowAllowed(f, 'Get Started', 'button', w)).toBe(false);
expect(flowAllowed(f, 'Save draft', 'button', w)).toBe(f === 'play-content-rating');
expect(flowAllowed(f, 'Taslağı kaydet', 'button', w)).toBe(f === 'play-content-rating');
for (const n of ['Save', 'Submit', 'Create', 'Create app', 'Done', 'Start rollout', 'Publish']) expect(flowAllowed(f, n, 'button', w)).toBe(false);
// an App content declaration's Save goes to Google only with Send changes for review (verified 2026-10-13)
expect(flowAllowed(f, 'Save', 'button', w)).toBe(f === 'play-content-rating' || f === 'play-target-audience');
for (const n of ['Submit', 'Create', 'Create app', 'Done', 'Start rollout', 'Publish', 'Send changes for review', 'Submit 3 changes for review', 'Restart review']) expect(flowAllowed(f, n, 'button', w)).toBe(false);
}
expect(flowClass('play-content-rating', 'Save draft', 'button', w)).toBe('draft-save');
expect(flowClass('play-target-audience', 'Save', 'button', w)).toBe('final-submit');
expect(flowClass('play-target-audience', 'Save', 'button', w)).toBe('declaration-save');
expect(flowClass('play-content-rating', 'Save', 'button', w)).toBe('declaration-save');
expect(flowClass('apple-app-record', 'Save', 'button', w)).toBe('final-submit');
expect(flowClass('play-content-rating', 'Submit', 'button', w)).toBe('final-submit');
expect(flowAllowed('play-target-audience', 'Save', 'button', at({ privacyPage: false, inDialog: true }))).toBe(false);
});
test('every form has its buttons classified; only navigation and the agreed draft saves (App Privacy, content rating) are automatic', () => {
test('every form has its buttons classified; only navigation, the agreed draft saves (App Privacy, content rating) and the Play declaration saves are automatic', () => {
for (const [f, rules] of Object.entries(FORM_BUTTONS)) {
expect(rules.length).toBeGreaterThan(0);
for (const r of rules) if (r.auto) expect(r.class === 'navigation' || (['apple-app-privacy', 'play-content-rating'].includes(f) && r.class === 'draft-save')).toBe(true);
for (const r of rules) if (r.auto) expect(r.class === 'navigation' || (['apple-app-privacy', 'play-content-rating'].includes(f) && r.class === 'draft-save')
|| (['play-content-rating', 'play-target-audience'].includes(f) && r.class === 'declaration-save')).toBe(true);
expect(rules.some(r => r.class === 'final-submit' && !r.auto)).toBe(true);
}
for (const f of ['play-content-rating', 'play-target-audience'] as const) expect(FORM_BUTTONS[f].some(r => r.button === 'Send changes for review' && r.class === 'final-submit' && !r.auto)).toBe(true);
});
});
@ -330,10 +337,16 @@ describe.skipIf(!browser)('App Privacy: each data type set up from the real page
});
describe.skipIf(!browser)('the other forms: navigation by itself, final buttons and draft saves left to the owner', () => {
scope.concurrent('content rating: Save draft and Next by itself on each page, a stop at the uncertain questions until answered, the Summary; its Save never clicked', async () => {
scope.concurrent('content rating: waits for the owner\'s IARC Terms tick, Save draft and Next by itself on each page, a stop at the uncertain questions until answered, then the Summary\'s Save (a declaration save)', async () => {
const b = await local();
const log = logLines();
const run = scope.run(fillSession(b, { form: 'play-content-rating', config: base0, startUrl: base + 'play-content-rating.html', ...fast(log) }));
// page 1: Next does nothing until the IARC Terms box is ticked (verified 2026-10-13): storebound waits for the owner
await log.wait('the agreement stop', l => l.some(m => m.startsWith('agreement needed on "Email address and category"') && m.includes('IARC Terms of Use')));
await Bun.sleep(300);
expect(await clicks(b)).toEqual([]);
expect(await b.page.getByRole('checkbox', { name: /IARC Terms of Use/ }).isChecked()).toBe(false); // never ticked by storebound
await b.page.getByRole('checkbox', { name: /IARC Terms of Use/ }).check(); // the owner
await log.wait('the questionnaire stop', l => l.some(m => m.startsWith('answer needed on "Questionnaire"')));
expect(log.lines).toContain('clicked "Save draft" (draft-save)');
expect(log.lines).toContain('clicked "Next" (navigation)');
@ -343,23 +356,24 @@ describe.skipIf(!browser)('the other forms: navigation by itself, final buttons
// the owner answers every outlined question
const open = b.page.locator('[role="radiogroup"][data-storebound-check]');
for (let i = 0, n = await open.count(); i < n; i++) await open.nth(i).getByRole('radio', { name: 'No' }).check();
await log.wait('the final line', l => l.some(m => m.includes('press Save yourself')));
expect(await clicks(b)).toEqual(['Save draft', 'Next', 'Save draft', 'Next']);
await log.wait('the final line', l => l.some(m => m.includes('storebound pressed Save') && m.includes('Send changes for review in Publishing overview yourself')));
expect(await clicks(b)).toEqual(['Save draft', 'Next', 'Save draft', 'Next', 'Save']);
expect(log.lines).toContain('clicked "Save" (declaration-save)');
expect(await b.page.getByRole('heading', { name: 'Summary' }).isVisible()).toBe(true);
expect(navAsked(log.lines)).toEqual([]);
await b.context.close();
await run;
}, TEST_MS);
scope.concurrent('target audience: Next by itself, a stop at the uncertain appeal question until answered, then Next; Save stays the owner\'s', async () => {
scope.concurrent('target audience: Next by itself, a stop at the uncertain appeal question until answered, then Next and the declaration\'s Save; Send changes for review stays the owner\'s', async () => {
const b = await local();
const log = logLines();
const run = scope.run(fillSession(b, { form: 'play-target-audience', config: base0, startUrl: base + 'play-target-audience.html', ...fast(log) }));
await log.wait('the appeal stop', l => l.some(m => m.startsWith('answer needed on "Appeal to children"')));
expect(await clicks(b)).toEqual(['Next']);
await b.page.getByRole('radiogroup', { name: /appeal to children/ }).getByRole('radio', { name: 'No' }).check();
await log.wait('the final line', l => l.some(m => m.includes('press Save yourself')));
expect(await clicks(b)).toEqual(['Next', 'Next']);
await log.wait('the final line', l => l.some(m => m.includes('storebound pressed Save')));
expect(await clicks(b)).toEqual(['Next', 'Next', 'Save']);
expect(navAsked(log.lines)).toEqual([]);
await b.context.close();
await run;
@ -373,6 +387,14 @@ describe.skipIf(!browser)('the other forms: navigation by itself, final buttons
await log.wait('the final line', l => l.some(m => m.includes(`press ${final} yourself`)));
// Play: only the read-only package name check (verified 2026-10-12); never the final button
expect(await clicks(b)).toEqual(form === 'play-app-record' ? ['Check availability'] : []);
// App Store Connect's New App (React, verified 2026-10-13): a programmatic value set left its state empty; the
// engine types like a person (the fixture's state only follows key presses)
if (form === 'apple-app-record') {
const state = await b.page.evaluate(() => (window as any).__state);
const shown = await b.page.evaluate(() => [...document.querySelectorAll('input[type=text]')].map(i => (i as HTMLInputElement).value));
expect(state.Name).toBe('Example Game');
expect([state.Name, state.SKU]).toEqual(shown);
}
expect(navAsked(log.lines)).toEqual([]);
await b.context.close();
await run;