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
|
|
@ -12,7 +12,8 @@
|
|||
// play-content-rating, play-target-audience page text (summary pages: a label line, its answer on the same or the
|
||||
// next line). Modeled, not recorded: rows that cannot be found are "not found", never a match.
|
||||
import type { ShipConfig } from './config';
|
||||
import { type Answer, consoleAnswers, type FormId, valueText } from './console-answers';
|
||||
import { type Answer, consoleAnswers, type FormId, type PlanFormId, type PlanOnlyFormId, valueText } from './console-answers';
|
||||
import { APP_CONTENT_CARDS, playOnlyPart, startLabel } from './console-play-plans';
|
||||
import { APPLE_PRIVACY_TYPES } from './app-privacy';
|
||||
import { typeLabels } from './console-forms';
|
||||
import { APPLE_PURPOSES } from './console-answers';
|
||||
|
|
@ -20,7 +21,7 @@ import { APPLE_PURPOSES } from './console-answers';
|
|||
export type RowStatus = 'match' | 'mismatch' | 'not-found' | 'ask-owner';
|
||||
export interface VerifyRow { item: string; expected: string; seen: string; status: RowStatus; note?: string }
|
||||
export interface VerifyResult {
|
||||
form: FormId;
|
||||
form: PlanFormId;
|
||||
source: 'page-text' | 'inputs' | 'api';
|
||||
rows: VerifyRow[];
|
||||
mismatches: number;
|
||||
|
|
@ -34,7 +35,7 @@ const lines = (text: string): string[] => text.normalize('NFC').split(/\r?\n/).m
|
|||
/** Straight and curly quotes as one, case and spaces aside: for comparing labels. */
|
||||
export const fold = (s: string): string => s.normalize('NFC').replace(/[‘’]/g, "'").replace(/[“”]/g, '"').replace(/\s+/g, ' ').trim().toLowerCase();
|
||||
|
||||
function result(form: FormId, source: VerifyResult['source'], rows: VerifyRow[], notes: string[] = []): VerifyResult {
|
||||
function result(form: PlanFormId, source: VerifyResult['source'], rows: VerifyRow[], notes: string[] = []): VerifyResult {
|
||||
const mismatches = rows.filter(r => r.status === 'mismatch').length, matched = rows.filter(r => r.status === 'match').length;
|
||||
return { form, source, rows, mismatches, matched, ok: mismatches === 0 && matched > 0, notes };
|
||||
}
|
||||
|
|
@ -122,9 +123,16 @@ export function verifyAppPrivacy(config: ShipConfig, text: string): VerifyResult
|
|||
else rows.push({ item: `${t}: ${label}`, expected: yn(x.value as boolean), seen: yn(v), status: v === null ? 'not-found' : v === x.value ? 'match' : 'mismatch', ...(inferred ? { note: inferred } : {}) });
|
||||
}
|
||||
}
|
||||
// after the owner's Publish (verified 2026-10-12): the page shows "Published <when> by <who>"; the name is never echoed
|
||||
const published = appPrivacyPublished(text);
|
||||
rows.push({ item: 'Published (the owner\'s Publish)', expected: '"Published … ago by …"', seen: published ? 'Published' : 'no "Published …" line yet', status: published ? 'match' : 'not-found',
|
||||
...(published ? {} : { note: 'a draft until the owner presses Publish (and Publish in the "Publish Your App Privacy Responses?" dialog)' }) });
|
||||
return result('apple-app-privacy', 'page-text', rows, notes);
|
||||
}
|
||||
|
||||
/** App Privacy was published: the "Published … ago by …" line is on the page (the owner pressed Publish). */
|
||||
export const appPrivacyPublished = (text: string): boolean => /\bpublished\b.{0,60}\bago\b|\bpublished (on|at)\b/i.test(text.replace(/\s+/g, ' '));
|
||||
|
||||
// ---- Play Create app ----
|
||||
|
||||
/** The input states the plan's capture snippet returns. */
|
||||
|
|
@ -299,8 +307,76 @@ export function verifyAgeRatingApi(config: ShipConfig, attrs: Record<string, unk
|
|||
return result('apple-age-rating', 'api', rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* The App content overview: the capture JSON ({ start: the aria-labels of the "Start <Name> declaration" buttons still
|
||||
* on the page, text }) or the plain page text. A card whose start button is still there is not done.
|
||||
*/
|
||||
export function verifyAppContent(text: string): VerifyResult {
|
||||
let st: { start: string[] } | null = null;
|
||||
try { const j = JSON.parse(text) as unknown; if (j && typeof j === "object" && Array.isArray((j as { start?: unknown }).start)) st = j as { start: string[] }; } catch { /* page text */ }
|
||||
const pending = new Set((st?.start ?? []).map(s => fold(String(s))));
|
||||
const rows: VerifyRow[] = APP_CONTENT_CARDS.map(c => {
|
||||
const open = pending.has(fold(startLabel(c.name)));
|
||||
return { item: c.name, expected: 'declared (no Start button)', seen: st ? (open ? `"${startLabel(c.name)}" still shown` : 'no Start button') : 'not readable from page text: pass the plan capture JSON', status: st ? (open ? 'mismatch' : 'match') : 'not-found' };
|
||||
});
|
||||
return result('play-app-content', st ? 'inputs' : 'page-text', rows, st ? [] : ['The start buttons are aria-labels, not page text: pass the JSON of the plan\'s capture snippet.']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store settings and the single App content declarations, from the page text after Save: text answers (the category,
|
||||
* the privacy policy URL) and option labels must appear; the App category's "Not selected" is a mismatch. Yes / No
|
||||
* radios cannot be told apart in page text: reported, never counted as a match.
|
||||
*/
|
||||
export function verifyPlayDeclaration(form: Exclude<PlanOnlyFormId, 'play-app-content'>, config: ShipConfig, text: string): VerifyResult {
|
||||
const part = playOnlyPart(form, config, {});
|
||||
const blob = fold(text);
|
||||
const rows: VerifyRow[] = [];
|
||||
for (const a of part.answers) {
|
||||
if (a.status === 'agreement') continue;
|
||||
if (a.status === 'ask-owner' || a.value === null) { rows.push({ item: a.question, expected: '(owner decides)', seen: '-', status: 'ask-owner' }); continue; }
|
||||
const want = a.action === 'type' || a.action === 'choose' ? String(a.value) : a.label ?? '';
|
||||
const plain = !want || /^(yes|no)$/i.test(want) || a.action === 'untick';
|
||||
if (plain) { rows.push({ item: a.question, expected: a.text, seen: 'not readable from page text', status: 'not-found', note: 'read the control (aria-checked) in the page' }); continue; }
|
||||
const shown = blob.includes(fold(want));
|
||||
const unset = a.id === 'category' && /not selected|select a category/.test(blob);
|
||||
rows.push({ item: a.question, expected: want, seen: shown ? want : unset ? 'Not selected' : 'not found', status: shown && !unset ? 'match' : unset ? 'mismatch' : 'not-found' });
|
||||
}
|
||||
return result(form, 'page-text', rows, ['Page text after Save: Yes / No radios and unticked boxes are not readable from text and are never counted as a match.']);
|
||||
}
|
||||
|
||||
/** The production track's Countries / regions tab: "<N> countries / regions" (0 is a mismatch) and, with
|
||||
* play.countries, each country's English name on the page. */
|
||||
export function verifyProductionCountries(config: ShipConfig, text: string): VerifyResult {
|
||||
const m = /(\d+)\s+(countries|countries\s*\/\s*regions|regions)\b/i.exec(text);
|
||||
const n = m ? Number(m[1]) : null;
|
||||
const want = config.play?.countries;
|
||||
const rows: VerifyRow[] = [{ item: 'Countries / regions', expected: want?.length ? String(want.length) : 'all (more than 0)', seen: n === null ? 'not found' : String(n),
|
||||
status: n === null ? 'not-found' : n === 0 || (want?.length && n !== want.length) ? 'mismatch' : 'match' }];
|
||||
const blob = fold(text);
|
||||
for (const c of want ?? []) {
|
||||
let name = c; try { name = new Intl.DisplayNames(['en'], { type: 'region' }).of(c) ?? c; } catch { /* the code */ }
|
||||
rows.push({ item: `${name} (${c})`, expected: 'listed', seen: blob.includes(fold(name)) ? 'listed' : 'not found', status: blob.includes(fold(name)) ? 'match' : 'not-found' });
|
||||
}
|
||||
return result('play-production-countries', 'page-text', rows);
|
||||
}
|
||||
|
||||
/** Publishing overview after the owner sent the changes: nothing left "not yet sent for review". */
|
||||
export function verifyProductionRelease(text: string): VerifyResult {
|
||||
const f = fold(text);
|
||||
const pending = /not yet sent for review|submit \d+ changes? for review/.test(f);
|
||||
const inReview = /in review|changes in review|sent for review/.test(f);
|
||||
const rows: VerifyRow[] = [{ item: 'Changes sent for review', expected: 'sent (in review)', seen: pending ? 'changes not yet sent for review' : inReview ? 'in review' : 'not found', status: pending ? 'mismatch' : inReview ? 'match' : 'not-found',
|
||||
...(pending ? { note: 'the owner presses "Submit N changes for review", then "Send changes for review" (a real click)' } : {}) }];
|
||||
return result('play-production-release', 'page-text', rows);
|
||||
}
|
||||
|
||||
/** Page-text verification of any form that has one. */
|
||||
export function verifyText(form: FormId, config: ShipConfig, text: string): VerifyResult {
|
||||
export function verifyText(form: PlanFormId, config: ShipConfig, text: string): VerifyResult {
|
||||
if (form === 'play-app-content') return verifyAppContent(text);
|
||||
if (form === 'play-production-countries') return verifyProductionCountries(config, text);
|
||||
if (form === 'play-production-release') return verifyProductionRelease(text);
|
||||
if (form === 'play-store-settings' || form === 'play-privacy-policy' || form === 'play-ads' || form === 'play-sign-in-details' || form === 'play-advertising-id'
|
||||
|| form === 'play-government-apps' || form === 'play-financial-features' || form === 'play-health-apps') return verifyPlayDeclaration(form, config, text);
|
||||
switch (form) {
|
||||
case 'apple-app-privacy': return verifyAppPrivacy(config, text);
|
||||
case 'play-app-record': return verifyPlayAppRecord(config, text);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue