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:
parent
1366e7e231
commit
f741c15829
29 changed files with 1029 additions and 165 deletions
|
|
@ -33,7 +33,7 @@ import type { Ctx } from '../lib/ctx';
|
|||
import { type Answer, type ConsoleResult, consoleResult, DISCLAIMER, FORM_IDS, type FormId, fillValue, formMeta, isForm, valueText } from '../lib/console-answers';
|
||||
import type { ShipConfig } from '../lib/config';
|
||||
import { APPLE_PRIVACY_TYPES } from '../lib/app-privacy';
|
||||
import { ALL_APPS, APP_PRIVACY_HEADING, consoleReached, CREATE_FORM_FIELD, createAppAllowed, DATA_TYPES_HEADING, type FieldDef, FLOW_NAMES, flowAllowed, flowClass, type FlowWhere, type FormDef, formDef, FORM_OPENERS, forbiddenControl, quoteTolerant, forbiddenTick, hasToken, isAppleSignInFrame, isSignInText, isSignInUrl, type Name, navAllowed, type NavBases, type NavStep, navPlan, type SectionDef, setUpAllowed, setUpTexts, typeLabels } from '../lib/console-forms';
|
||||
import { ALL_APPS, APP_PRIVACY_HEADING, consoleReached, CREATE_FORM_FIELD, createAppAllowed, DATA_TYPES_HEADING, DECLARATION_SAVE_FORMS, type FieldDef, FLOW_NAMES, flowAllowed, flowClass, type FlowWhere, type FormDef, formDef, FORM_OPENERS, forbiddenControl, quoteTolerant, forbiddenTick, hasToken, isAppleSignInFrame, isSignInText, isSignInUrl, type Name, navAllowed, type NavBases, type NavStep, navPlan, type SectionDef, setUpAllowed, setUpTexts, typeLabels } from '../lib/console-forms';
|
||||
import { log, printJson, warn } from '../lib/log';
|
||||
import { openAcross, withAccountIds } from './accounts';
|
||||
import { appleAppId, withAppleAppId } from './app-id';
|
||||
|
|
@ -180,9 +180,24 @@ export async function highlight(loc: Locator, text = CHECK): Promise<void> {
|
|||
}, text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Types a text field like a person: click it, select what is there, delete it, type the value key by key, read it back.
|
||||
* App Store Connect's New App dialog (React; verified 2026-10-13) kept its state empty ("") after a programmatic value
|
||||
* set on Name and SKU (a form_input-style set of the value), while clicking and typing worked. true when the field
|
||||
* shows the value afterwards.
|
||||
*/
|
||||
export async function typeText(loc: Locator, v: string): Promise<boolean> {
|
||||
await loc.click({ timeout: ACTION_MS });
|
||||
await loc.press('ControlOrMeta+A', { timeout: ACTION_MS });
|
||||
await loc.press('Backspace', { timeout: ACTION_MS });
|
||||
await loc.pressSequentially(v, { delay: 15, timeout: ACTION_MS + v.length * 50 });
|
||||
const now: string = await loc.inputValue({ timeout: ACTION_MS }).catch(() => v);
|
||||
return now === v;
|
||||
}
|
||||
|
||||
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 === 'text') return typeText(r.loc, String(v));
|
||||
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);
|
||||
|
|
@ -266,8 +281,11 @@ export interface FillStep {
|
|||
fallback: boolean;
|
||||
/** A control of the filled section (the flow finds its dialog and its Next / Save from it). */
|
||||
anchor?: Locator;
|
||||
/** Per uncertain answer (agreements left out), its outlined controls: the flow waits until the owner set each one. */
|
||||
/** Per uncertain answer (agreements left out, except one that blocks Next), its outlined controls: the flow waits
|
||||
* until the owner set each one. */
|
||||
pending?: Locator[][];
|
||||
/** Agreements on the page that Next waits for and that are not ticked yet (the IARC Terms of Use). */
|
||||
blocked?: string[];
|
||||
}
|
||||
|
||||
/** The dialog of a section: by its names, else the one visible dialog with such a heading, else (the data type
|
||||
|
|
@ -321,7 +339,7 @@ export async function fillPage(page: Page, def: FormDef, answers: Answer[], done
|
|||
// missing fields to the owner (reported as "check")
|
||||
// (a page still loading can also miss a field for a moment: the caller retries before it passes partial = true)
|
||||
if (missing.length && (!partial || missing.length === rs.length)) return { section: i, title: sec.title, filled: [], highlighted: [], missing, fallback: true };
|
||||
const filled: string[] = [], highlighted: string[] = [...missing], pending: Locator[][] = [];
|
||||
const filled: string[] = [], highlighted: string[] = [...missing], pending: Locator[][] = [], blocked: string[] = [];
|
||||
let anchor: Locator | undefined;
|
||||
for (const r of rs) {
|
||||
if (!r.ok) continue;
|
||||
|
|
@ -329,7 +347,12 @@ export async function fillPage(page: Page, def: FormDef, answers: Answer[], done
|
|||
const a = byId.get(r.field.id);
|
||||
if (!a) continue;
|
||||
// an agreement is never ticked, whatever the answer says: highlighted for the owner
|
||||
if (r.field.agreement || a.agreement) { await mark(r, 'storebound: you agree to this, never ticked'); highlighted.push(a.id); continue; }
|
||||
if (r.field.agreement || a.agreement) {
|
||||
await mark(r, 'storebound: you agree to this, never ticked'); highlighted.push(a.id);
|
||||
// an agreement Next waits for (the IARC Terms of Use, verified 2026-10-13): the flow waits until the owner ticked it
|
||||
if (r.field.blocksNext && r.loc && !await r.loc.isChecked({ timeout: ACTION_MS }).catch(() => false)) { pending.push([r.loc]); blocked.push(a.id); }
|
||||
continue;
|
||||
}
|
||||
const set = a.certain && a.value !== null ? await setField(r, a.value) : false;
|
||||
if (set) { filled.push(a.id); if (a.ownerCheck) await mark(r, 'storebound: owner check'); } else {
|
||||
await mark(r); highlighted.push(a.id);
|
||||
|
|
@ -337,7 +360,7 @@ export async function fillPage(page: Page, def: FormDef, answers: Answer[], done
|
|||
}
|
||||
}
|
||||
done.add(i);
|
||||
return { section: i, title: sec.title, filled, highlighted, missing, fallback: false, ...(anchor ? { anchor } : {}), pending };
|
||||
return { section: i, title: sec.title, filled, highlighted, missing, fallback: false, ...(anchor ? { anchor } : {}), pending, ...(blocked.length ? { blocked } : {}) };
|
||||
}
|
||||
return { section: null, filled: [], highlighted: [], missing: [], fallback: false };
|
||||
}
|
||||
|
|
@ -1363,7 +1386,8 @@ export async function fillSession(b: ConsoleBrowser, o: FillSessionOpts): Promis
|
|||
waitFor = open.length ? { title: s.title ?? '', pending: open } : null;
|
||||
if (waitFor) {
|
||||
const need = s.highlighted.filter(id => !pageAnswers.find(a => a.id === id)?.agreement);
|
||||
say(`answer needed on "${s.title}": ${need.map(id => labels.get(id) ?? id).join('; ')}. Answer ${need.length === 1 ? 'it' : 'each one'} in the browser (outlined in red); storebound goes on by itself once every outlined answer is set (a box that stays empty: click it twice)`);
|
||||
if (need.length) say(`answer needed on "${s.title}": ${need.map(id => labels.get(id) ?? id).join('; ')}. Answer ${need.length === 1 ? 'it' : 'each one'} in the browser (outlined in red); storebound goes on by itself once every outlined answer is set (a box that stays empty: click it twice)`);
|
||||
if (s.blocked?.length) say(`agreement needed on "${s.title}": ${s.blocked.map(id => `"${labels.get(id) ?? id}"`).join(' and ')} is yours to tick (outlined in red; storebound never ticks an agreement; the page waits for it). storebound goes on by itself once it is ticked`);
|
||||
}
|
||||
continue; // more of the form may be on this page
|
||||
}
|
||||
|
|
@ -1392,14 +1416,31 @@ export async function fillSession(b: ConsoleBrowser, o: FillSessionOpts): Promis
|
|||
if (privacy) await privacyStep(p).catch(() => { /* navigating */ });
|
||||
await rt.sleep(tick);
|
||||
}
|
||||
/** The declaration's Save (the one visible button named exactly Save / Kaydet, no Next left on the page), polled at
|
||||
* most stepMs: the content rating Summary renders after the last Next. true when it was pressed. */
|
||||
async function declarationSave(p: Page): Promise<boolean> {
|
||||
const btn = await poll(stepMs, async () => {
|
||||
if (await nextControl(p).catch(() => null)) return null;
|
||||
return controlWhere(p, FLOW_NAMES.save, w => !w.inDialog, dataTypes).catch(() => null);
|
||||
}, undefined, p).catch(() => null);
|
||||
if (!btn) return false;
|
||||
return flowClick(btn, form, dataTypes, say, clicked).catch(() => false);
|
||||
}
|
||||
if (timedOut) {
|
||||
say(`nothing storebound knows happened for ${mins(idleMs)}: it stops here and closes the window${o.debugDir && dumps ? ` (debug: ${o.debugDir})` : ''}. Answers saved as drafts stay; run it again when the page is fixed.`);
|
||||
await b.close();
|
||||
return { filled, highlighted, fallback, timedOut: true, ...(o.debugDir && dumps ? { debugDir: o.debugDir } : {}) };
|
||||
}
|
||||
// a Play App content declaration (content rating Summary, target audience): its Save is the agent's (verified
|
||||
// 2026-10-13: nothing goes to Google before the owner's "Send changes for review" in Publishing overview)
|
||||
const declaration = DECLARATION_SAVE_FORMS.includes(form);
|
||||
const saved = declaration && !b.isClosed() && !fallback ? await declarationSave(active() ?? b.page) : false;
|
||||
if (!b.isClosed() && !fallback) say(privacy
|
||||
? 'review the summary, then press Publish yourself. storebound saved the answers as a draft (Save in each dialog); nothing is public until you press Publish. Close the window when done.'
|
||||
: `review every field and press ${meta.stopBefore} yourself. storebound never saves, submits or publishes. Close the window when done.`);
|
||||
: declaration ? (saved
|
||||
? `storebound pressed Save: the declaration is stored, nothing is sent yet. Once every App content card is done, press ${meta.stopBefore} in Publishing overview yourself. Close the window when done.`
|
||||
: `storebound did not find the declaration's Save on this page: press Save (it only stores the declaration), then ${meta.stopBefore} in Publishing overview once every App content card is done. Close the window when done.`)
|
||||
: `review every field and press ${meta.stopBefore} yourself. storebound never saves, submits or publishes. Close the window when done.`);
|
||||
else if (!b.isClosed()) say('close the browser window when done');
|
||||
// the owner reviews and presses the final button, then closes the window: a reminder every 10 heartbeats, at most finalMs
|
||||
const end = Date.now() + finalMs;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue