Play console steps without an API as agent plans: play-store-settings (App category; play listing cannot set it), play-app-content (every card in order, one owner batch) and its cards (privacy policy, ads, sign in details, advertising ID, government, financial, health), play-production-countries and play-production-release; IARC plan with the Terms of Use agreement, follow-ups (console.contentRating.details, products[].randomRewards, social) and the Summary's final Save; config play.category / countries / managedPublishing, console.appContent; check, validate, play status and play release reminders; inPageLoop helper and material checkbox clicks; App Privacy published signal; first IAPs via apple iap-submit --with-version. Pitfalls 27, 28
This commit is contained in:
parent
dbd9f2973c
commit
b7bfe89151
37 changed files with 1624 additions and 121 deletions
|
|
@ -122,6 +122,7 @@ async function resolve(root: Locator, f: FieldDef): Promise<Resolved> {
|
|||
// App Store Connect labels "User Access" with a plain heading, not an ARIA group: then the options are looked up
|
||||
// in the whole dialog (their own names, e.g. "Full Access", are unique there)
|
||||
if (g.state === 'ok') { scope = g.loc; seen = true; }
|
||||
else if (f.scoped) return { field: f, ok: false, seen: false };
|
||||
}
|
||||
const options: Record<string, Locator> = {};
|
||||
let ok = true;
|
||||
|
|
@ -181,7 +182,11 @@ export async function highlight(loc: Locator, text = CHECK): Promise<void> {
|
|||
async function setField(r: Resolved, v: string | boolean): Promise<boolean> {
|
||||
const f = r.field;
|
||||
if (f.kind === 'text') { await r.loc.fill(String(v)); return true; }
|
||||
if (f.kind === 'checkbox') return safeClick(r.loc, await accName(r.loc), v ? 'check' : 'uncheck');
|
||||
if (f.kind === 'checkbox') {
|
||||
const name = await accName(r.loc);
|
||||
if (await inMaterial(r.loc, 'material-checkbox')) return forbiddenTick(name) ? safeClick(r.loc, name, 'check') : materialToggle(r.loc, v === true);
|
||||
try { return await safeClick(r.loc, name, v ? 'check' : 'uncheck'); } catch (e) { if (!await inMaterial(r.loc, 'material-checkbox')) throw e; return materialToggle(r.loc, v === true); }
|
||||
}
|
||||
if (f.kind === 'radio') {
|
||||
const o = r.options?.[String(v)];
|
||||
if (!o) return false;
|
||||
|
|
@ -209,8 +214,24 @@ async function setField(r: Resolved, v: string | boolean): Promise<boolean> {
|
|||
return true;
|
||||
}
|
||||
|
||||
/** The radio sits in Play Console's <material-radio> (or is a [role=radio] itself). */
|
||||
const inMaterialRadio = (o: Locator): Promise<boolean> => o.evaluate((el: any) => !!el.closest('material-radio')).catch(() => false);
|
||||
/** The control sits in one of Play Console's material elements (<material-radio>, <material-checkbox>). */
|
||||
const inMaterial = (o: Locator, tag: string): Promise<boolean> => o.evaluate((el: any, t: string) => !!el.closest(t), tag).catch(() => false);
|
||||
const inMaterialRadio = (o: Locator): Promise<boolean> => inMaterial(o, 'material-radio');
|
||||
|
||||
/**
|
||||
* Play Console's <material-checkbox> (the same model problem as material-radio, seen on App content and the IARC
|
||||
* questionnaire, 2026-10-12): a DOM click on the inner [role=checkbox] when its aria-checked differs from `want`.
|
||||
* The name gate (agreements) runs before. true when the box ends up as wanted.
|
||||
*/
|
||||
export async function materialToggle(o: Locator, want: boolean): Promise<boolean> {
|
||||
return o.evaluate((el: any, w: boolean) => {
|
||||
const host = el.closest('material-checkbox');
|
||||
const inner = el.getAttribute('role') === 'checkbox' ? el : host?.querySelector('[role="checkbox"]') ?? el;
|
||||
const state = () => (inner.hasAttribute('aria-checked') ? inner.getAttribute('aria-checked') === 'true' : el.checked === true);
|
||||
if (state() !== w) inner.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
|
||||
return state() === w;
|
||||
}, want).catch(() => false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Play Console's <material-radio> (verified 2026-10-12 on Create app): setting the native input, or a click that does
|
||||
|
|
@ -1038,7 +1059,9 @@ export async function fillSession(b: ConsoleBrowser, o: FillSessionOpts): Promis
|
|||
progress(nav?.ok ? 'the form' : 'the form (opened by you)');
|
||||
if (o.debugEveryStep && !b.isClosed()) await dump(active() ?? b.page, 'start');
|
||||
|
||||
const last = def.sections.length - 1;
|
||||
// the IARC follow-ups (optional sections) appear only after a Yes: the form ends with its last required section
|
||||
const required = def.sections.flatMap((x, i) => (x.optional ? [] : [i]));
|
||||
const last = required.at(-1) ?? def.sections.length - 1;
|
||||
const privacy = form === 'apple-app-privacy';
|
||||
/** App Privacy: the data types storebound answers (their entries in the Data Types section open their questions). */
|
||||
const types = [...new Set(def.sections.map(x => x.type).filter((t): t is string => !!t))];
|
||||
|
|
@ -1056,7 +1079,7 @@ export async function fillSession(b: ConsoleBrowser, o: FillSessionOpts): Promis
|
|||
/** App Privacy openers clicked (each at most twice: a click that opens nothing is a UI change). */
|
||||
const opened = new Map<string, number>();
|
||||
let askedCollects = false;
|
||||
const finished = () => !pendingAdvance && !waitFor && (done.size === def.sections.length || (!privacy && done.has(last)));
|
||||
const finished = () => !pendingAdvance && !waitFor && (required.every(i => done.has(i)) || (!privacy && done.has(last)));
|
||||
|
||||
/** The dialog of the last filled section when it is still visible, else null. */
|
||||
const anchorDialog = async (): Promise<Locator | null> => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue