apple creative-assets: place Header and Search Results assets once Apple approved them (the version page offers only approved assets); waiting files are reported per locale and placement with their upload date and the re-run command, in the run, --verify, validate and the report; iPhone Duo: the published API spec still has no Duo type, the note says the console takes Duo and the API does not yet
This commit is contained in:
parent
47dfd372fe
commit
c2f34c62f3
13 changed files with 163 additions and 78 deletions
|
|
@ -4,7 +4,10 @@
|
|||
// --add-for-review into the draft submission without submitting, --verify read-only).
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
|
||||
import { md5 } from '../cli/apple/asc';
|
||||
import { classify, MAX_FAILED, pickAsset, processingTiming, REF, run, selectable } from '../cli/apple/creative-assets';
|
||||
import { classify, creativeWaiting, MAX_FAILED, pickAsset, processingTiming, REF, run, selectable, waitingText } from '../cli/apple/creative-assets';
|
||||
import { ascClient } from '../cli/apple/asc';
|
||||
import { appleRules } from '../cli/lib/validate-rules';
|
||||
import { creativeFiles as filesOf } from '../cli/shots/creative';
|
||||
import { checkCreativeImage, checkCreativeVideo, CREATIVE_NOTE, creativeFiles, creativeNote } from '../cli/shots/creative';
|
||||
import { imageInfo } from '../cli/shots/spec';
|
||||
import { upsertTiming } from '../cli/lib/upsert';
|
||||
|
|
@ -111,29 +114,35 @@ describe('apple creative-assets against the fake App Store Connect', () => {
|
|||
const lib = () => [...s.asc.all('appAssetLibraryImages'), ...s.asc.all('appAssetLibraryVideos')];
|
||||
const files = (h = 1, q = 2) => ({ 'store/renders/en/creative/header.png': png(3840, 1646, false, h), 'store/renders/en/creative/search.png': png(1920, 1280, false, q) });
|
||||
|
||||
test('dry run, upload + select in one run (no approval wait), same file = nothing, changed file = replace', async () => {
|
||||
test('dry run, upload, wait for approval per locale and placement, a re-run places them; same file = nothing; changed file = replace once approved', async () => {
|
||||
const p = setup(files());
|
||||
const dry = await go(p, ['--dry']);
|
||||
expect(dry.w).toEqual([]);
|
||||
expect(dry.out).toContain('would upload header.png');
|
||||
|
||||
const first = await go(p);
|
||||
expect(first.w).toEqual(['POST /v1/appAssetLibraryImages', 'PATCH /v1/appAssetLibraryImages/{id}', 'POST /v1/appAssetLibraryPlacements',
|
||||
'POST /v1/appAssetLibraryImages', 'PATCH /v1/appAssetLibraryImages/{id}', 'POST /v1/appAssetLibraryPlacements']);
|
||||
expect(first.w).toEqual(['POST /v1/appAssetLibraryImages', 'PATCH /v1/appAssetLibraryImages/{id}', 'POST /v1/appAssetLibraryImages', 'PATCH /v1/appAssetLibraryImages/{id}']);
|
||||
expect(lib().map(x => x.attributes.referenceName)).toContain(REF(md5(files()['store/renders/en/creative/header.png'])));
|
||||
expect(s.asc.all('appAssetLibraryPlacements').map(x => x.attributes.placementType).sort()).toEqual(['APP_STORE_SEARCH_RESULTS_ASSET', 'PRODUCT_PAGE_HEADER_ASSET']);
|
||||
expect(s.asc.all('appAssetLibraryPlacements')).toHaveLength(0);
|
||||
expect(first.out).toMatch(/en-US PRODUCT_PAGE_HEADER_ASSET: header\.png (uploaded \d{4}-\d\d-\d\d, )?waiting for Apple's approval \(PREPARE_FOR_SUBMISSION\); run `storebound apple creative-assets` again later/);
|
||||
expect(first.out).toContain('2 waiting for Apple\'s approval');
|
||||
expect(first.out).toContain('--add-for-review');
|
||||
expect(first.out).not.toContain("waiting for Apple's approval");
|
||||
|
||||
expect((await go(p)).w).toEqual([]); // still waiting: nothing sent again
|
||||
for (const x of lib()) x.attributes.state = 'APPROVED';
|
||||
expect((await go(p)).w).toEqual(['POST /v1/appAssetLibraryPlacements', 'POST /v1/appAssetLibraryPlacements']);
|
||||
expect(s.asc.all('appAssetLibraryPlacements').map(x => x.attributes.placementType).sort()).toEqual(['APP_STORE_SEARCH_RESULTS_ASSET', 'PRODUCT_PAGE_HEADER_ASSET']);
|
||||
expect((await go(p)).w).toEqual([]);
|
||||
|
||||
const q = project(CFG, files(9));
|
||||
const rep = await go(q);
|
||||
expect(rep.w).toEqual(['POST /v1/appAssetLibraryImages', 'PATCH /v1/appAssetLibraryImages/{id}', 'DELETE /v1/appAssetLibraryPlacements/{id}', 'POST /v1/appAssetLibraryPlacements']);
|
||||
expect((await go(q)).w).toEqual(['POST /v1/appAssetLibraryImages', 'PATCH /v1/appAssetLibraryImages/{id}']);
|
||||
expect(s.asc.all('appAssetLibraryPlacements')).toHaveLength(2); // the approved old one stays shown meanwhile
|
||||
for (const x of lib()) x.attributes.state = 'APPROVED';
|
||||
expect((await go(q)).w).toEqual(['DELETE /v1/appAssetLibraryPlacements/{id}', 'POST /v1/appAssetLibraryPlacements']);
|
||||
expect(s.asc.all('appAssetLibraryPlacements')).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('processing that outlasts the bounded wait: not selected, re-run selects without a second upload', async () => {
|
||||
test('processing that outlasts the bounded wait: not selected, a re-run once approved selects without a second upload', async () => {
|
||||
const f = files();
|
||||
const p = setup(f);
|
||||
for (const [k, id] of [['header', 'img-h'], ['search', 'img-s']] as const)
|
||||
|
|
@ -143,6 +152,8 @@ describe('apple creative-assets against the fake App Store Connect', () => {
|
|||
expect(r1.out).toContain('still UPLOAD_COMPLETE');
|
||||
expect(r1.out).toContain('2 still processing');
|
||||
for (const x of lib()) x.attributes.state = 'PREPARE_FOR_SUBMISSION';
|
||||
expect((await go(p)).w).toEqual([]);
|
||||
for (const x of lib()) x.attributes.state = 'APPROVED';
|
||||
expect((await go(p)).w).toEqual(['POST /v1/appAssetLibraryPlacements', 'POST /v1/appAssetLibraryPlacements']);
|
||||
});
|
||||
|
||||
|
|
@ -153,8 +164,8 @@ describe('apple creative-assets against the fake App Store Connect', () => {
|
|||
s.asc.put({ type: 'appAssetLibraryImages', id: 'img-arc', attributes: { category: 'CREATIVE_ASSETS', referenceName: REF(md5(f['store/renders/en/creative/search.png'])), state: 'ARCHIVED', fileName: 'search.png' }, rels: { assetLibrary: 'lib-1' } });
|
||||
const r = await go(p, [], 1);
|
||||
expect(r.out).toContain('rejected by App Review');
|
||||
expect(r.w).toEqual(['PATCH /v1/appAssetLibraryImages/{id}', 'POST /v1/appAssetLibraryPlacements']);
|
||||
expect(s.asc.all('appAssetLibraryPlacements').map(x => x.attributes.placementType)).toEqual(['APP_STORE_SEARCH_RESULTS_ASSET']);
|
||||
expect(r.w).toEqual(['PATCH /v1/appAssetLibraryImages/{id}']);
|
||||
expect(lib().find(x => x.id === 'img-arc')!.attributes.state).not.toBe('ARCHIVED');
|
||||
});
|
||||
|
||||
test('--add-for-review: unreviewed assets go into the draft once, nothing is submitted', async () => {
|
||||
|
|
@ -175,6 +186,12 @@ describe('apple creative-assets against the fake App Store Connect', () => {
|
|||
expect(before.w).toEqual([]);
|
||||
expect(before.out).toContain('shows nothing, wanted header.png');
|
||||
await go(p);
|
||||
const wait = await go(p, ['--verify']);
|
||||
expect(wait.w).toEqual([]);
|
||||
expect(wait.out).toMatch(/waiting en-US PRODUCT_PAGE_HEADER_ASSET: header\.png .*waiting for Apple's approval/);
|
||||
expect(wait.out).toContain("2 waiting for Apple's approval");
|
||||
for (const x of lib()) x.attributes.state = 'APPROVED';
|
||||
await go(p);
|
||||
const ok = await go(p, ['--verify']);
|
||||
expect(ok.w).toEqual([]);
|
||||
expect(ok.out).toContain('every slot shows the local file');
|
||||
|
|
@ -187,7 +204,7 @@ describe('apple creative-assets against the fake App Store Connect', () => {
|
|||
expect(up.out).toContain('using the live version 1.0.0');
|
||||
expect(up.w).toEqual(['POST /v1/appAssetLibraryImages', 'PATCH /v1/appAssetLibraryImages/{id}', 'POST /v1/appAssetLibraryImages', 'PATCH /v1/appAssetLibraryImages/{id}',
|
||||
'POST /v1/reviewSubmissions', 'POST /v1/reviewSubmissionItems', 'POST /v1/reviewSubmissionItems']);
|
||||
expect(up.out).toContain('2 waiting for App Review (live version)');
|
||||
expect(up.out).toContain("2 waiting for Apple's approval");
|
||||
expect(s.asc.all('appAssetLibraryPlacements')).toHaveLength(0);
|
||||
for (const x of lib()) x.attributes.state = 'APPROVED';
|
||||
expect((await go(p)).w).toEqual(['POST /v1/appAssetLibraryPlacements', 'POST /v1/appAssetLibraryPlacements']);
|
||||
|
|
@ -218,7 +235,24 @@ describe('apple creative-assets against the fake App Store Connect', () => {
|
|||
expect(dry.out).toContain('1 file(s) to upload (used by 2 slots)');
|
||||
const r = await go(p);
|
||||
expect(r.w.filter(x => x === 'POST /v1/appAssetLibraryImages')).toHaveLength(1);
|
||||
expect(r.w.filter(x => x === 'POST /v1/appAssetLibraryPlacements')).toHaveLength(2);
|
||||
for (const x of lib()) x.attributes.state = 'APPROVED';
|
||||
expect((await go(p)).w.filter(x => x === 'POST /v1/appAssetLibraryPlacements')).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('validate / report: a file waiting for approval is named per locale and placement', async () => {
|
||||
const p = setup(files());
|
||||
await go(p);
|
||||
const c = await ascClient(p.ctx([]).loaded.root, 'com.example.notes', APP);
|
||||
const local = new Map([['en-US', filesOf(`${p.ctx([]).loaded.root}/store/renders`, 'en')]]);
|
||||
const w = await creativeWaiting(c, APP, local);
|
||||
expect(w.map(x => [x.locale, x.placement, x.file])).toEqual([['en-US', 'PRODUCT_PAGE_HEADER_ASSET', 'header.png'], ['en-US', 'APP_STORE_SEARCH_RESULTS_ASSET', 'search.png']]);
|
||||
expect(waitingText({ attributes: { state: 'IN_REVIEW', createdDate: '2026-10-19T10:00:00Z' } })).toBe("uploaded 2026-10-19, waiting for Apple's approval (IN_REVIEW); run `storebound apple creative-assets` again later");
|
||||
const f = appleRules({ version: null, build: null, expectedLocales: [], locales: [], requiredDisplayTypes: [], reviewDetail: null, ageRating: null, priceSchedule: true, availability: true,
|
||||
iaps: [], expectedProducts: [], privacyUrl: { url: 'https://example.com/privacy', status: 200 }, creativeWaiting: w }).filter(x => x.code === 'creative-waiting');
|
||||
expect(f).toHaveLength(2);
|
||||
expect(f[0]!.msg).toContain("en-US PRODUCT_PAGE_HEADER_ASSET: header.png");
|
||||
for (const x of lib()) x.attributes.state = 'APPROVED';
|
||||
expect(await creativeWaiting(c, APP, local)).toEqual([]);
|
||||
});
|
||||
|
||||
test('a bad file stops the run before any call', async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue