129 lines
8.9 KiB
TypeScript
129 lines
8.9 KiB
TypeScript
// The Play Console app id (play.appId): taken from the URL Create app redirects to (`console verify play-app-record
|
|
// --url`), or found by package name in captured app list pages (`play app-id`, a modeled list of 10 apps per page with
|
|
// the new app on page 3), then written into the config as an upsert after a dry diff. Placeholder ids only.
|
|
import { afterAll, describe, expect, test } from 'bun:test';
|
|
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import type { ShipConfig } from '../cli/lib/config';
|
|
import { consolePlan } from '../cli/lib/console-plan';
|
|
import { PLAY_CREATE_CAPTURE, verifyText } from '../cli/lib/console-verify';
|
|
import { APP_LIST_CAPTURE, appIdChange, appIdDiff, appIdSteps, appListPage, type AppListPage, findPlayAppId, parseAppListPages, playAppIdInText, writePlayAppId } from '../cli/lib/play-app-id';
|
|
import { upsertNestedString } from '../cli/lib/scan/config-edit';
|
|
|
|
const PKG = 'com.example.game';
|
|
const base: ShipConfig = {
|
|
app: { name: 'Example Game', profile: 'game', privacyPolicyUrl: 'https://example.com/privacy' },
|
|
languages: [{ id: 'en', play: 'en-US', apple: 'en-US' }],
|
|
play: { packageName: PKG, developerId: '4444' },
|
|
} as ShipConfig;
|
|
const dash = (app: string) => `https://play.google.com/console/u/0/developers/4444/app/${app}/app-dashboard`;
|
|
|
|
/** A modeled app list: 25 apps, 10 per page, the new app (com.example.game) on page 3, a look-alike on page 1. */
|
|
function list(): AppListPage[] {
|
|
const apps = Array.from({ length: 24 }, (_, i) => ({ name: `Other App ${i + 1}`, pkg: `com.example.other${i + 1}`, id: String(4975000000000000000 + i) }));
|
|
apps.splice(1, 0, { name: 'Example Game Pro', pkg: 'com.example.game2', id: '4975999999999999998' }); // not a whole-token match
|
|
apps.push({ name: 'Example Game', pkg: PKG, id: '4975123456789012345' });
|
|
const pages: AppListPage[] = [];
|
|
for (let i = 0; i < apps.length; i += 10) {
|
|
const slice = apps.slice(i, i + 10);
|
|
pages.push({ url: appListPage('4444', pages.length + 1), hasNext: i + 10 < apps.length,
|
|
rows: slice.map(a => ({ text: `${a.name} ${a.pkg} Production Published`, href: `/console/u/0/developers/4444/app/${a.id}/app-dashboard` })) });
|
|
}
|
|
return pages;
|
|
}
|
|
|
|
describe('the app id from a URL or a capture', () => {
|
|
test('playAppIdInText: the /developers/<id>/app/<id> digits, anywhere', () => {
|
|
expect(playAppIdInText(dash('4975123456789012345'))).toBe('4975123456789012345');
|
|
expect(playAppIdInText(JSON.stringify({ url: dash('9876'), fields: {} }))).toBe('9876');
|
|
expect(playAppIdInText('https://play.google.com/console/u/0/developers/4444/app-list')).toBeNull();
|
|
expect(playAppIdInText('https://play.google.com/console/u/0/developers/4444/create-new-app')).toBeNull();
|
|
});
|
|
|
|
test('console verify play-app-record --url: the created app\'s id matches and is handed to the upsert', () => {
|
|
const r = verifyText('play-app-record', base, dash('9876'));
|
|
expect(r).toMatchObject({ ok: true, playAppId: '9876', matched: 1 });
|
|
expect(r.rows[0]!.expected).toContain('not in the config yet');
|
|
// the capture snippet carries the URL too
|
|
expect(PLAY_CREATE_CAPTURE).toContain('url: location.href');
|
|
expect(verifyText('play-app-record', base, JSON.stringify({ url: dash('9876'), fields: {}, checked: [], text: 'Dashboard' })).playAppId).toBe('9876');
|
|
// a different id in the config: a mismatch, and --write would replace it
|
|
const other = verifyText('play-app-record', { ...base, play: { ...base.play!, appId: '1111' } } as ShipConfig, dash('9876'));
|
|
expect(other).toMatchObject({ ok: false, mismatches: 1, playAppId: '9876' });
|
|
// the form still open (before Create app): no app id, the usual field rows
|
|
const form = verifyText('play-app-record', base, JSON.stringify({ url: 'https://play.google.com/console/u/0/developers/4444/create-new-app', fields: { 'App name': 'Example Game' }, checked: [], text: '' }));
|
|
expect(form.playAppId).toBeUndefined();
|
|
expect(form.rows.length).toBeGreaterThan(1);
|
|
});
|
|
|
|
test('the plan says how to capture it after Create app', () => {
|
|
const p = consolePlan('play-app-record', base);
|
|
expect(p.steps.map(s => s.text).join('\n')).toContain('console verify play-app-record --url');
|
|
expect(p.uiFacts.join('\n')).toContain('10 apps per page');
|
|
});
|
|
});
|
|
|
|
describe('play app-id: the package name across the app list pages', () => {
|
|
test('found on page 3 of a modeled 3-page list; the look-alike com.example.game2 never matches', () => {
|
|
const pages = list();
|
|
expect(pages).toHaveLength(3);
|
|
expect(pages[0]!.rows).toHaveLength(10);
|
|
expect(findPlayAppId(pages.slice(0, 1), PKG)).toEqual({ found: false, pages: 1, hasNext: true });
|
|
expect(findPlayAppId(pages.slice(0, 2), PKG)).toEqual({ found: false, pages: 2, hasNext: true });
|
|
expect(findPlayAppId(pages, PKG)).toMatchObject({ found: true, appId: '4975123456789012345', page: 3 });
|
|
expect(findPlayAppId(pages, 'com.example.missing')).toEqual({ found: false, pages: 3, hasNext: false });
|
|
});
|
|
|
|
test('captures: one object, a JSON array or one per line', () => {
|
|
const pages = list();
|
|
expect(parseAppListPages(JSON.stringify(pages[2]))).toHaveLength(1);
|
|
expect(parseAppListPages(JSON.stringify(pages))).toHaveLength(3);
|
|
expect(parseAppListPages(pages.map(p => JSON.stringify(p)).join('\n'))).toHaveLength(3);
|
|
expect(() => parseAppListPages('{"nope":1}')).toThrow('rows');
|
|
expect(findPlayAppId(parseAppListPages(pages.map(p => JSON.stringify(p)).join('\n')), PKG)).toMatchObject({ found: true, page: 3 });
|
|
});
|
|
|
|
test('the agent steps: the search box first, then the pages; the capture snippet is valid JavaScript', () => {
|
|
const steps = appIdSteps(base, '4444').join('\n');
|
|
for (const s of ['https://play.google.com/console/u/0/developers/4444/app-list', 'search box', PKG, 'Next page', '/app-list?page=', '--write']) expect(steps).toContain(s);
|
|
expect(() => new Function(`return ${APP_LIST_CAPTURE}`)).not.toThrow();
|
|
expect(appListPage('4444', 3)).toBe('https://play.google.com/console/u/0/developers/4444/app-list?page=3');
|
|
});
|
|
});
|
|
|
|
describe('play.appId written into the config: a dry diff, then an upsert', () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'sb-app-id-'));
|
|
afterAll(() => rmSync(dir, { recursive: true, force: true }));
|
|
const TS = `import { defineConfig } from 'storebound';\n\nexport default defineConfig({\n app: { name: 'Example Game' },\n languages: [{ id: 'en', play: 'en-US', apple: 'en-US' }],\n play: {\n packageName: '${PKG}', // the package\n developerId: '4444',\n listing: { title: 'x' },\n },\n});\n`;
|
|
|
|
test('adds play.appId inside play (only there), then replaces it; the rest of the file stays', () => {
|
|
const added = upsertNestedString(TS, 'storebound.config.ts', 'play', 'appId', '9876');
|
|
expect(added).toContain(` play: {\n appId: '9876',\n packageName: '${PKG}', // the package`);
|
|
expect(added.match(/appId/g)).toHaveLength(1);
|
|
expect(added.replace(" appId: '9876',\n", '')).toBe(TS);
|
|
const again = upsertNestedString(added, 'storebound.config.ts', 'play', 'appId', '1111');
|
|
expect(again).toBe(added.replace("'9876'", "'1111'"));
|
|
expect(upsertNestedString("export default { app: { name: 'X' }, play: { packageName: 'a.b' } };\n", 'c.ts', 'play', 'appId', '5')).toBe("export default { app: { name: 'X' }, play: { packageName: 'a.b', appId: '5' } };\n");
|
|
expect(JSON.parse(upsertNestedString(JSON.stringify({ play: { packageName: 'a.b' } }), 'c.json', 'play', 'appId', '5')).play).toEqual({ packageName: 'a.b', appId: '5' });
|
|
expect(() => upsertNestedString("export default { app: {} };\n", 'c.ts', 'play', 'appId', '5')).toThrow('no play block');
|
|
// a nested appId (another block inside play) is not the one replaced
|
|
const nested = "export default {\n play: {\n extra: { appId: 'keep' },\n packageName: 'a.b',\n },\n};\n";
|
|
expect(upsertNestedString(nested, 'c.ts', 'play', 'appId', '5')).toContain(" appId: '5',\n extra: { appId: 'keep' },");
|
|
});
|
|
|
|
test('writePlayAppId: a dry diff first, a write only when it changes, re-runs converge', () => {
|
|
const file = join(dir, 'storebound.config.ts');
|
|
writeFileSync(file, TS);
|
|
const cfg = { ...base } as ShipConfig;
|
|
const change = appIdChange(cfg, '9876');
|
|
expect(change).toEqual({ from: null, to: '9876', changed: true });
|
|
expect(appIdDiff(change, 'storebound.config.ts')).toBe("storebound.config.ts: play.appId (not set) -> '9876'");
|
|
expect(writePlayAppId(file, cfg, '9876')).toBe(true);
|
|
expect(readFileSync(file, 'utf8')).toContain("appId: '9876'");
|
|
const now = { ...cfg, play: { ...cfg.play!, appId: '9876' } } as ShipConfig;
|
|
expect(writePlayAppId(file, now, '9876')).toBe(false); // already there: nothing written
|
|
expect(appIdDiff(appIdChange(now, '9876'), 'storebound.config.ts')).toContain('already');
|
|
expect(() => writePlayAppId(file, cfg, 'abc')).toThrow('not a Play Console app id');
|
|
});
|
|
});
|