storebound/tests/apple-name.test.ts

78 lines
6.3 KiB
TypeScript

// apple.name: the App Store name when the plain app.name is taken on the App Store. Fallback order (### name block >
// apple.name > app.name), validation, the console answer, check's length error and same-name hint, listing pull.
import { afterEach, describe, expect, test } from 'bun:test';
import { run as check } from '../cli/check';
import { appleName, type ShipConfig, validateConfig } from '../cli/lib/config';
import { consoleAnswers } from '../cli/lib/console-answers';
import { navPlan } from '../cli/lib/console-forms';
import { appleNameIssues } from '../cli/lib/copy';
import { mergeAppleListing } from '../cli/lib/listing-pull';
import { listingCompleteness } from '../cli/lib/project-files';
import { BASE, capture, jsonOut, project, restore } from './helpers/store-mock';
afterEach(restore);
const NAMED: ShipConfig = { ...BASE, app: { ...BASE.app, name: 'STAR DASH' }, apple: { ...BASE.apple!, name: 'STAR DASH: Stone Age to 2100', primaryLocale: 'en-US' } };
const PLAIN: ShipConfig = { ...BASE, app: { ...BASE.app, name: 'STAR DASH', copyright: '2026 Example Brand', privacyPolicyUrl: 'https://rush.example.com/privacy', supportUrl: 'https://rush.example.com/support', website: 'https://rush.example.com', contactEmail: 'support@rush.example.com' }, apple: { ...BASE.apple!, primaryLocale: 'en-US' } };
const sec = (loc: string, name?: string) => `## ${loc}\n\n${name ? `### name\n\`\`\`\n${name}\n\`\`\`\n\n` : ''}### subtitle\n\`\`\`\nSub\n\`\`\`\n\n### promotionalText\n\`\`\`\nPromo\n\`\`\`\n\n### keywords\n\`\`\`\nrun,time\n\`\`\`\n\n### description\n\`\`\`\nText.\n\`\`\`\n\n`;
describe('apple.name', () => {
test('fallback order: apple.name, else app.name', () => {
expect(appleName(NAMED)).toBe('STAR DASH: Stone Age to 2100');
expect(appleName(PLAIN)).toBe('STAR DASH');
expect(appleName({ ...PLAIN, apple: { ...PLAIN.apple!, name: ' ' } })).toBe('STAR DASH');
});
test('validated: at most 30 characters, text', () => {
expect(validateConfig(NAMED)).toEqual([]);
expect(validateConfig({ ...NAMED, apple: { ...NAMED.apple!, name: 'STAR DASH: Stone Age to the Year 2100' } }).join('\n')).toContain('apple.name: 37 characters > 30');
expect(validateConfig({ ...NAMED, apple: { ...NAMED.apple!, name: '' } }).join('\n')).toContain('apple.name: the App Store name');
});
test('console fill apple-app-record answers Name from apple.name, else app.name', () => {
const a = new Map(consoleAnswers('apple-app-record', NAMED).map(x => [x.id, x]));
expect(a.get('name')).toMatchObject({ value: 'STAR DASH: Stone Age to 2100', source: 'apple.name', certain: true });
const b = new Map(consoleAnswers('apple-app-record', PLAIN).map(x => [x.id, x]));
expect(b.get('name')).toMatchObject({ value: 'STAR DASH', source: 'app.name' });
expect(b.get('name')!.note).toContain('apple.name');
const pick = navPlan('apple-app-privacy', { ...NAMED, apple: { ...NAMED.apple!, appId: undefined } }).find(s => s.kind === 'pickApp') as { name?: string } | undefined;
expect(pick?.name).toBe('STAR DASH: Stone Age to 2100');
});
test('name issues: length error per locale, same-name hint only without apple.name or own names', () => {
const long = 'A Name That Is Way Too Long For Apple';
expect(appleNameIssues({}, ['en-US'], long, long)).toEqual([expect.objectContaining({ level: 'error', msg: expect.stringContaining('app.name; set apple.name') }), expect.objectContaining({ level: 'info' })]);
expect(appleNameIssues({ 'de-DE': { name: long } }, ['en-US'], 'Short', 'Short').map(i => `${i.level} ${i.msg.slice(0, 31)}`)).toEqual(['error App Store name of de-DE is 37 >']);
const hint = appleNameIssues({ 'en-US': {} , 'de-DE': {} }, ['en-US', 'de-DE'], 'STAR DASH', 'STAR DASH');
expect(hint).toHaveLength(1);
expect(hint[0]!.msg).toContain('already being used');
expect(appleNameIssues({}, ['en-US', 'de-DE'], 'STAR DASH: Stone Age to 2100', 'STAR DASH')).toEqual([]);
expect(appleNameIssues({ 'de-DE': { name: 'STAR DASH: Steinzeit bis 2100' } }, ['en-US', 'de-DE'], 'STAR DASH', 'STAR DASH')).toEqual([]);
});
test('check: hint with the plain name, error when the fallback is too long', async () => {
const files = { 'store/appstore.md': `# App Store\n\n${sec('en-US')}${sec('de-DE')}${sec('tr')}`, '.gitignore': '.storebound/\n' };
let p = project({ ...PLAIN, play: undefined }, files);
let j = jsonOut((await capture(() => check(p.ctx(['--json'], ['json'])))).out);
expect(j.ok).toBe(true);
expect((j.hints as string[]).join('\n')).toContain('already being used');
p = project({ ...NAMED, play: undefined }, files);
j = jsonOut((await capture(() => check(p.ctx(['--json'], ['json'])))).out);
// only the name hint is under test (the Duo and Header and Search Results notes are hints of their own)
expect((j.hints as string[]).filter(h => /App Store name|already being used/.test(h))).toEqual([]);
p = project({ ...PLAIN, app: { ...PLAIN.app, name: 'STAR DASH Stone Age to the Year 2100' }, play: undefined }, files);
const r = await capture(() => check(p.ctx(['--json'], ['json'])));
j = jsonOut(r.out);
expect(j.ok).toBe(false);
expect((j.problems as string[]).filter(x => x.includes('App Store name of'))).toHaveLength(3);
});
test('listing completeness and listing pull use apple.name as the default name', () => {
const p = project({ ...NAMED, play: undefined }, { 'store/appstore.md': `# App Store\n\n${sec('en-US')}${sec('de-DE', 'STAR DASH: Steinzeit bis 2100')}` });
const l = listingCompleteness(p.loaded.config, p.root);
const name = (code: string) => l.locales.find(x => x.apple?.code === code)!.apple!.fields.find(f => f.field === 'name');
expect(name('en-US')).toMatchObject({ length: 28, state: 'ok' });
expect(name('de-DE')).toMatchObject({ length: 29, state: 'ok' });
const md = `# App Store\n\n${sec('en-US')}`;
const r = mergeAppleListing(md, { 'en-US': { name: 'STAR DASH: Stone Age to 2100' }, 'fr-FR': { name: 'STAR DASH: Stone Age to 2100', subtitle: 'Sous-titre' } }, 'STAR DASH: Stone Age to 2100');
expect(r.changes.map(c => `${c.section} ${c.field}`)).toEqual(['fr-FR subtitle']);
expect(r.md).not.toContain('### name');
const own = mergeAppleListing(`# App Store\n\n${sec('en-US', 'Old')}`, { 'en-US': { name: 'STAR DASH: Stone Age to 2100' } }, 'STAR DASH: Stone Age to 2100');
expect(own.changes.map(c => c.field)).toEqual(['name']);
});
});