play images: same languages as play listing (drafts skipped unless --include-drafts); a language with no Play listing (404) is skipped with an info line, never fatal

This commit is contained in:
CRTkafa 2026-10-12 08:40:27 +08:00
parent 147cee9721
commit 720cb2a13a
6 changed files with 71 additions and 7 deletions

View file

@ -17,7 +17,7 @@ import { collectPlayHealth } from '../cli/health';
import { parseArgs } from '../cli/lib/args';
import type { Loaded, ShipConfig } from '../cli/lib/config';
import { healthFindings, healthTable, issuesQuery, metricQuery } from '../cli/lib/health';
import { imageChange, missingChange, missingInfo, run as imagesRun } from '../cli/play/images';
import { imageChange, missingChange, missingInfo, noListing, run as imagesRun } from '../cli/play/images';
import { defaultSizes } from '../cli/shots/lib';
import { requiredSets, resolvedTablets } from '../cli/shots/report';
import { playRequiredImages } from '../cli/lib/validate-rules';
@ -285,6 +285,37 @@ 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 () => {
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`;
const files: Record<string, string> = { 'store/icons/play-512.png': 'icon',
'store/listing.md': sec('English', 'en-US') + sec('German', 'de-DE') + sec('French', 'fr-FR', '<!-- storebound-translation from=en-US status=draft -->\n') };
// de-DE has local renders but no listing on Play; fr-FR (draft) has no renders at all and must not fail
for (const l of ['en', 'de']) { files[`store/renders/${l}/feature/1024x500.png`] = 'feature'; for (const n of ['01', '02']) files[`store/renders/${l}/play/${n}.png`] = `play${n}`; }
for (const [p, body] of Object.entries(files)) { mkdirSync(join(root, p, '..'), { recursive: true }); writeFileSync(join(root, p), body); }
const config = { app: { name: 'Example Game' }, languages: [{ id: 'en', play: 'en-US' }, { id: 'de', play: 'de-DE' }, { id: 'fr', play: 'fr-FR' }], play: { packageName: PKG } } as unknown as ShipConfig;
const loaded: Loaded = { config, root, path: join(root, 'storebound.config.ts'), resolve: p => join(root, p) };
const calls = fake(c => {
if (c.method === 'POST' && c.url === `${API}/edits`) return json({ id: 'edit-4' });
if (c.method === 'GET' && c.url.includes('/listings/de-DE/')) return json({ error: { code: 404, message: "Listing for language 'de-DE' not found.", status: 'NOT_FOUND' } }, 404);
if (c.method === 'GET' && c.url.includes('/listings/en-US/')) return json({});
if (c.method === 'DELETE' && c.url === `${API}/edits/edit-4`) return new Response(null, { status: 204 });
return json({ error: { code: 400, message: `unexpected ${c.method} ${c.url}` } }, 400);
});
const before = process.exitCode;
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('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);
expect(noListing(new Error('GET /edits/1/listings/en-US/icon -> 500 backend'))).toBe(false);
});
test('pure parts: missing optional sets, required sets and render sizes follow play.tablets', () => {
expect(missingChange('tenInchScreenshots', 0, false).change).toBe('none');
expect(missingChange('tenInchScreenshots', 3, false).change).toBe('keep');