play: a new language is created with everything Play needs to commit it (icon, feature graphic, phone and tablet screenshots); play images creates a missing listing
This commit is contained in:
parent
2ec1f0ab13
commit
ed8d138a8e
7 changed files with 172 additions and 32 deletions
|
|
@ -421,6 +421,10 @@ export class FakePlay {
|
|||
priceLimits: Record<string, { min: number; max: number; symbol: string }> = {};
|
||||
/** product id -> 400 message for its PATCH */
|
||||
rejectProducts: Record<string, string> = {};
|
||||
/** live Play: committing a language that is new in the edit needs its hi-res icon, feature graphic and 2+ phone screenshots
|
||||
* in the same edit (403 PERMISSION_DENIED), 2+ tablet sets too when `requireTablets` */
|
||||
requireNewLangAssets = false;
|
||||
requireTablets = false;
|
||||
/** validate calls that carried changesNotSentForReview (live Play answers them 400 "Unknown name") */
|
||||
validateWithNotSent = 0;
|
||||
|
||||
|
|
@ -445,12 +449,21 @@ export class FakePlay {
|
|||
return json({ error: { code: 400, status: 'INVALID_ARGUMENT', message: 'Changes cannot be sent for review automatically. Please set the query parameter changesNotSentForReview to true. Once committed, the changes in this edit can be sent for review from the Google Play Console UI.', errors: [{ message: 'Changes cannot be sent for review automatically. Please set the query parameter changesNotSentForReview to true.', domain: 'global', reason: 'badRequest' }] } }, 400);
|
||||
}
|
||||
if (method === 'POST' && rest === ':validate') return json({ id });
|
||||
if (method === 'POST' && rest === ':commit' && this.requireNewLangAssets) {
|
||||
for (const lang of Object.keys(e.listings).filter(l => !this.live.listings[l])) {
|
||||
const n = (t: string) => (e.images[`${lang}/${t}`] ?? []).length;
|
||||
const miss = n('icon') < 1 ? `This app has no high resolution icon set for language ${lang}` : n('featureGraphic') < 1 ? `This app has no feature graphic set for language ${lang}`
|
||||
: n('phoneScreenshots') < 2 || (this.requireTablets && (n('sevenInchScreenshots') < 2 || n('tenInchScreenshots') < 2)) ? `This app has too few screenshots for language ${lang}` : '';
|
||||
if (miss) return err(403, 'PERMISSION_DENIED', miss);
|
||||
}
|
||||
}
|
||||
if (method === 'POST' && rest === ':commit') { this.live = e; this.edits.delete(id!); this.committed.push(id!); return json({ id }); }
|
||||
if (rest === '/details') { if (method === 'PUT') e.details = body as Record<string, unknown>; return json(e.details); }
|
||||
if (rest === '/listings') return json({ listings: Object.values(e.listings) });
|
||||
if ((m = /^\/listings\/([^/]+)$/.exec(rest!))) { if (method === 'PUT') e.listings[m[1]!] = body as Record<string, unknown>; return json(e.listings[m[1]!] ?? {}); }
|
||||
if ((m = /^\/listings\/([^/]+)\/(\w+)$/.exec(rest!))) {
|
||||
const k = `${m[1]}/${m[2]}`;
|
||||
if (this.requireNewLangAssets && method === 'GET' && !e.listings[m[1]!]) return err(404, 'NOT_FOUND', `Listing for language '${m[1]}' not found.`);
|
||||
if (method === 'DELETE') { e.images[k] = []; return json({ deleted: [] }); }
|
||||
if (method === 'POST') { const h = new Bun.CryptoHasher('sha256').update(body as Uint8Array).digest('hex'); (e.images[k] ??= []).push(h); return json({ image: { sha256: h } }); }
|
||||
return json({ images: (e.images[k] ?? []).map(sha256 => ({ sha256 })) });
|
||||
|
|
|
|||
|
|
@ -370,6 +370,7 @@ describe('listing, appinfo, data safety and the owner commands: a second run wri
|
|||
|
||||
test('play listing: one committed edit with only what differs; the second run opens no write edit and commits nothing', async () => {
|
||||
s.play.live.listings['de-DE'] = { language: 'de-DE', title: 'Notizen' };
|
||||
s.play.live.listings['en-US'] = { language: 'en-US', title: 'Old' };
|
||||
const p = project(CONFIG, { 'store/listing.md': LISTING });
|
||||
const r = await twice(() => playListing(p.ctx([])));
|
||||
expect(r.first).toEqual(['PUT /androidpublisher/v3/applications/com.example.notes/edits/edit-2/details', 'PUT /androidpublisher/v3/applications/com.example.notes/edits/edit-2/listings/en-US',
|
||||
|
|
@ -380,6 +381,71 @@ describe('listing, appinfo, data safety and the owner commands: a second run wri
|
|||
expect(s.play.live.listings['de-DE']).toEqual({ language: 'de-DE', title: 'Notizen' }); // a language not in the file is left alone
|
||||
});
|
||||
|
||||
// A new Play language (tr-TR next to a live en-US): the commit is refused 403 "too few screenshots" unless the
|
||||
// screenshots, icon and feature graphic travel in the same edit (pitfall 102)
|
||||
const trConfig = (): ShipConfig => ({ ...CONFIG, languages: [{ id: 'en', play: 'en-US' }, { id: 'tr', play: 'tr-TR' }] as never });
|
||||
const trListing = LISTING + ['', '## Turkish', '<!-- play: tr-TR -->', '', '### Play short description', '```', 'Notlar.', '```', '', '### Play full description', '```', 'Herkes icin notlar.', '```', ''].join('\n');
|
||||
const renders = (lang: string, base: number, tablets = false): Record<string, Uint8Array> => {
|
||||
const f: Record<string, Uint8Array> = { [`store/renders/${lang}/feature/1024x500.png`]: png(base) };
|
||||
for (const n of [1, 2]) { f[`store/renders/${lang}/play/0${n}.png`] = png(base + n); if (tablets) for (const t of ['tab7', 'tab10']) f[`store/renders/${lang}/${t}/0${n}.png`] = png(base + n + 5); }
|
||||
return f;
|
||||
};
|
||||
const liveEn = () => {
|
||||
s.play.live.listings['en-US'] = { language: 'en-US', title: 'Example Notes', shortDescription: 'Notes.', fullDescription: 'Notes for everyone.' };
|
||||
s.play.live.details = { defaultLanguage: 'en-US', contactEmail: 'support@example.com', contactWebsite: 'https://example.com' };
|
||||
s.play.requireNewLangAssets = true;
|
||||
};
|
||||
|
||||
test('play listing: a new language uploads icon, feature graphic and phone screenshots in the same edit, so the commit passes', async () => {
|
||||
liveEn();
|
||||
const p = project(trConfig(), { 'store/listing.md': trListing, 'store/icons/play-512.png': png(1), ...renders('tr', 40) });
|
||||
const r = await capture(() => playListing(p.ctx([])));
|
||||
expect(r.exit).toBe(null);
|
||||
expect(s.play.committed).toHaveLength(1);
|
||||
expect(s.play.live.images['tr-TR/phoneScreenshots']).toHaveLength(2);
|
||||
expect(s.play.live.images['tr-TR/icon']).toHaveLength(1);
|
||||
expect(s.play.live.images['tr-TR/featureGraphic']).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('play listing: a new language without its own screenshots uses the default language images; none anywhere is refused in the dry run, nothing written', async () => {
|
||||
liveEn();
|
||||
const base = { 'store/listing.md': trListing, 'store/icons/play-512.png': png(1) };
|
||||
const ok = project(trConfig(), { ...base, ...renders('en', 10) });
|
||||
const r1 = await capture(() => playListing(ok.ctx([])));
|
||||
expect(r1.exit).toBe(null);
|
||||
expect(r1.out).toContain("no phoneScreenshots of its own, using en-US's");
|
||||
expect(s.play.live.images['tr-TR/phoneScreenshots']).toHaveLength(2);
|
||||
s.play.live.listings = { 'en-US': s.play.live.listings['en-US']! };
|
||||
s.play.live.images = {};
|
||||
const bad = project(trConfig(), base);
|
||||
const before = s.play.committed.length;
|
||||
const mark = s.calls.length;
|
||||
await expect(playListing(bad.ctx(['--dry']))).rejects.toThrow('tr-TR: no phoneScreenshots');
|
||||
expect(s.play.committed).toHaveLength(before);
|
||||
expect(writes(s.calls.slice(mark))).toEqual([]);
|
||||
});
|
||||
|
||||
test('play listing: the dry run names the uploads of a new language; tablet sets are required when the default language has them on Play', async () => {
|
||||
liveEn();
|
||||
s.play.requireTablets = true;
|
||||
s.play.live.images['en-US/sevenInchScreenshots'] = ['x', 'y'];
|
||||
const p = project(trConfig(), { 'store/listing.md': trListing, 'store/icons/play-512.png': png(1), ...renders('tr', 40), ...renders('en', 10, true) });
|
||||
const r = await capture(() => playListing(p.ctx(['--dry'])));
|
||||
expect(r.out).toContain('tr-TR is new on Play: uploads');
|
||||
expect(r.out).toContain('sevenInchScreenshots (2)');
|
||||
expect(s.play.committed).toEqual([]);
|
||||
});
|
||||
|
||||
test('play images: a language with text in the listing file but no Play listing yet is created with its images, commit passes', async () => {
|
||||
liveEn();
|
||||
const p = project(trConfig(), { 'store/listing.md': trListing, 'store/icons/play-512.png': png(1), ...renders('en', 10), ...renders('tr', 40) });
|
||||
const r = await capture(() => playImages(p.ctx([])));
|
||||
expect(r.exit).toBe(null);
|
||||
expect(r.out).toContain('tr-TR: no Play listing yet, this run creates it');
|
||||
expect(s.play.live.listings['tr-TR']!.title).toBeDefined();
|
||||
expect(s.play.live.images['tr-TR/phoneScreenshots']).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('play listing: the store reached the wanted state between plan and edit: the edit is deleted, not committed', async () => {
|
||||
const p = project(CONFIG, { 'store/listing.md': LISTING });
|
||||
// the plan sees an empty listing; right after it (before the write edit opens) another run makes it current
|
||||
|
|
|
|||
|
|
@ -287,7 +287,7 @@ describe('play images: tablet, TV and Wear sets are optional (play.tablets)', ()
|
|||
expect(resolvedTablets({ play: {} }, join(phoneOnly().root, 'store/renders'))).toBe(false);
|
||||
});
|
||||
|
||||
test('languages without a Play listing are skipped: a draft section before any call, a 404 listing as info; exit 0', async () => {
|
||||
test('a draft section is skipped before any call, a language with a 404 listing is created by the run (dry: named); exit 0', async () => {
|
||||
playKey();
|
||||
const root = mkdtempSync(join(tmpdir(), 'sb-langs-'));
|
||||
const sec = (name: string, code: string, marker = '') => `## ${name}\n<!-- play: ${code} -->\n${marker}\n### Play short description\n\`\`\`\nShort.\n\`\`\`\n\n### Play full description\n\`\`\`\nFull.\n\`\`\`\n\n`;
|
||||
|
|
@ -309,9 +309,9 @@ describe('play images: tablet, TV and Wear sets are optional (play.tablets)', ()
|
|||
const out = await capture(() => imagesRun({ loaded, args: parseArgs(['--dry']) }));
|
||||
expect(process.exitCode).toBe(before);
|
||||
expect(out).toContain('skipped fr-FR: no Play listing yet (machine-translated draft, not reviewed');
|
||||
expect(out).toContain('skipped de-DE: no Play listing yet (run storebound play listing first');
|
||||
expect(out).toContain('de-DE: no Play listing yet, this run creates it from the listing file with its images');
|
||||
expect(out).toContain('de-DE: would replace icon (0 -> 1)');
|
||||
expect(out).toContain('en-US: would replace icon (0 -> 1)');
|
||||
expect(out).not.toMatch(/de-DE: would/);
|
||||
expect(calls.some(c => c.url.includes('fr-FR'))).toBe(false);
|
||||
expect(calls.every(readOnly)).toBe(true);
|
||||
expect(noListing(new Error("GET /edits/1/listings/de-DE/icon -> 404 Listing for language 'de-DE' not found."))).toBe(true);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue