console fill: labels match straight and curly quotes (Developer’s Advertising, the user’s identity); an App Privacy dialog step with no inputs (the definitions page) is passed with Next
This commit is contained in:
parent
0035931734
commit
18fde37b9b
4 changed files with 33 additions and 9 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, forbiddenTick, hasToken, isAppleSignInFrame, isSignInText, isSignInUrl, type Name, navAllowed, type NavBases, type NavStep, navPlan, setUpAllowed, setUpTexts, typeLabels } from '../lib/console-forms';
|
||||
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, 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';
|
||||
|
|
@ -51,7 +51,7 @@ const CHECK = 'storebound: check this';
|
|||
|
||||
/** Locators with only the visible matches (hidden earlier pages stay in many consoles' DOM). */
|
||||
export const vis = (l: Locator): Locator => { try { return l.filter({ visible: true }); } catch { return l; } };
|
||||
const nameOpts = (n: Name) => (typeof n === 'string' ? { name: n, exact: true } : { name: n });
|
||||
const nameOpts = (n: Name) => (typeof n === 'string' ? (/['"\u2018\u2019\u201c\u201d]/.test(n) ? { name: quoteTolerant(n) } : { name: n, exact: true }) : { name: n });
|
||||
|
||||
type Found = { state: 'ok'; loc: Locator } | { state: 'none' | 'ambiguous' };
|
||||
|
||||
|
|
@ -1011,6 +1011,15 @@ export async function fillSession(b: ConsoleBrowser, o: FillSessionOpts): Promis
|
|||
if (privacy && !d) return null;
|
||||
return controlWhere(d ?? p, FLOW_NAMES.next, w => !privacy || w.inDialog, dataTypes);
|
||||
};
|
||||
/** An open App Privacy dialog whose current step has no input at all (text only, plus Back / Cancel / Next). */
|
||||
let infoRounds = 0;
|
||||
const infoOnlyStep = async (p: Page): Promise<Locator | null> => {
|
||||
// the last filled field sits on a step that is hidden now, so the dialog is found on the page itself
|
||||
const d = await anchorDialog() ?? await (async () => { const v = vis(p.getByRole('dialog')); return (await v.count().catch(() => 0)) === 1 ? v : null; })();
|
||||
if (!d) return null;
|
||||
const inputs = await d.locator('input:not([type=hidden]), select, textarea, [role=checkbox], [role=radio], [role=switch], [role=textbox], [role=combobox]').filter({ visible: true }).count().catch(() => 1);
|
||||
return inputs === 0 ? d : null;
|
||||
};
|
||||
/** The section filled last, and the content rating pages whose Save draft was clicked. */
|
||||
let filledAt: number | null = null;
|
||||
const drafted = new Set<number>();
|
||||
|
|
@ -1226,6 +1235,16 @@ export async function fillSession(b: ConsoleBrowser, o: FillSessionOpts): Promis
|
|||
if (moved === 'draft') pendingAdvance = true; // Next follows the saved draft
|
||||
if (moved) { await Bun.sleep(tick); continue; }
|
||||
}
|
||||
// an App Privacy dialog step with nothing to answer (the "Let's define two important terms" page between linked and
|
||||
// tracking): no inputs, only Back / Cancel / Next -> Next is navigation
|
||||
// seen twice in a row, so a step still rendering its inputs is never skipped
|
||||
const info = privacy ? await infoOnlyStep(p) : null;
|
||||
if (!info) infoRounds = 0;
|
||||
if (info && (infoRounds += 1) >= 2) {
|
||||
infoRounds = 0;
|
||||
const next = await controlWhere(info, FLOW_NAMES.next, () => true, dataTypes);
|
||||
if (next && await flowClick(next, form, dataTypes, say, clicked)) { say('an information-only step of the dialog: storebound pressed Next'); progress('the next step of the dialog'); await Bun.sleep(tick); continue; }
|
||||
}
|
||||
if (privacy) await privacyStep(p).catch(() => { /* navigating */ });
|
||||
await Bun.sleep(tick);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,11 @@ export interface SectionDef {
|
|||
|
||||
export interface FormDef { id: FormId; sections: SectionDef[] }
|
||||
|
||||
const esc = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
// Console labels use typographic quotes ("Developer’s Advertising", "the user’s identity"): every pattern built from a
|
||||
// label accepts straight and curly quotes alike.
|
||||
const esc = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/['\u2018\u2019]/g, "['\u2018\u2019]").replace(/["\u201c\u201d]/g, '["\u201c\u201d]');
|
||||
/** A label as an exact-match pattern that tolerates straight vs curly quotes (for names that contain a quote). */
|
||||
export const quoteTolerant = (s: string): RegExp => new RegExp(`^${esc(s)}$`);
|
||||
const yesNo = (yes: Name[] = ['Yes'], no: Name[] = ['No']): Record<string, Name[]> => ({ true: yes, false: no });
|
||||
|
||||
/** IARC topics -> candidate radiogroup names (modeled; see the header). */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue