Files
Codeman/test/whats-new.test.ts
T
Codeman maintainer a61db218ee fix(#618): translate the release toast for zh-CN
The toast every user sees once per release had no zh-CN entries, so a
Chinese interface showed it, button included, in English. The button
gets an entry and the frame a translateDynamic pattern that keeps the
version, the "(+N more)" count and the highlights (those come from
CHANGELOG.md and stay English). Tested through the real i18n.js in JSDOM,
plus a pin on the strings settings-ui.js builds, which the pattern
depends on.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 01:28:54 +02:00

129 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import vm from 'node:vm';
import { JSDOM } from 'jsdom';
// @ts-expect-error plain .mjs helper shared with scripts/generate-whats-new.mjs
import { extractHighlights, versionSection, buildWhatsNew } from '../scripts/lib/whats-new.mjs';
const root = resolve(import.meta.dirname, '..');
const CHANGELOG = `# aicodeman
## 1.2.0
### Minor Changes
- ### Thanks
- @someone for a fix
**The tile grid: six sessions side by side.** Body text with **inline bold** later.
- **Split view (#560).** More text.
- A bullet with no bold lead.
**Behaviour change: \`Ctrl+W\` no longer closes a session.** Details.
## 1.1.0
- **Old feature.** Not for 1.2.0.
`;
describe('whats-new extraction', () => {
it('limits itself to the requested version section', () => {
const section = versionSection(CHANGELOG, '1.2.0');
expect(section).toContain('tile grid');
expect(section).not.toContain('Old feature');
expect(versionSection(CHANGELOG, '9.9.9')).toBe('');
});
it('takes bold lead-ins only, cleaned, and skips the Thanks block', () => {
expect(extractHighlights(versionSection(CHANGELOG, '1.2.0'))).toEqual([
'The tile grid: six sessions side by side',
'Split view (#560)',
'Behaviour change: `Ctrl+W` no longer closes a session',
]);
});
it('reads a lead-in behind the commit prefix changesets writes for a committed changeset', () => {
const section = '- d18deed: **Each release announces itself (#618).** Text.\n- 13e652e: A fix with no lead-in.';
expect(extractHighlights(section)).toEqual(['Each release announces itself (#618)']);
});
it('skips group labels the release notes put in bold (Fixes, Fixes applied while landing)', () => {
const section = [
' **Tab layouts (#538).** Text.',
' **Fixes.**',
' - **Shells reload cleanly (#601).** Text.',
' **Fixes applied while landing.** Remote tabs keep their scrollback.',
].join('\n');
expect(extractHighlights(section)).toEqual(['Tab layouts (#538)', 'Shells reload cleanly (#601)']);
});
it('yields no highlights for a release written without bold lead-ins', () => {
const changelog =
'## 1.32.1\n\n### Patch Changes\n\n- 13e652e: Terminal copy: no gutter on the clipboard (#469).\n';
expect(buildWhatsNew(changelog, '1.32.1')).toEqual({ version: '1.32.1', highlights: [] });
});
it('caps the list', () => {
const many = Array.from({ length: 20 }, (_, i) => `**Feature number ${i}.** text`).join('\n\n');
expect(extractHighlights(many)).toHaveLength(8);
});
it('builds the payload the client reads', () => {
expect(buildWhatsNew(CHANGELOG, '1.2.0').version).toBe('1.2.0');
});
});
describe('shipped whats-new.json', () => {
it("matches the package version, so a release cannot ship the previous release's notes", () => {
const pkg = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8'));
const notes = JSON.parse(readFileSync(resolve(root, 'src/web/public/whats-new.json'), 'utf8'));
expect(notes.version).toBe(pkg.version);
// A release written without bold lead-ins legitimately has none (the client then stays
// quiet), so an empty list must not turn the release commit's CI red.
expect(Array.isArray(notes.highlights)).toBe(true);
});
it('is regenerated by the release script and consumed once per version by the client', () => {
const pkg = readFileSync(resolve(root, 'package.json'), 'utf8');
expect(pkg).toMatch(/"version-packages": "changeset version && node scripts\/generate-whats-new\.mjs/);
const ui = readFileSync(resolve(root, 'src/web/public/settings-ui.js'), 'utf8');
expect(ui).toContain("'codeman-whats-new-seen'");
expect(ui).toContain("fetch('/whats-new.json'");
expect(readFileSync(resolve(root, 'src/web/public/app.js'), 'utf8')).toContain(
'this.checkWhatsNew?.(data.version)'
);
});
});
describe('the announcement in zh-CN', () => {
const dom = new JSDOM('<!doctype html><html><body></body></html>', {
runScripts: 'outside-only',
url: 'http://localhost/',
});
vm.runInContext(readFileSync(resolve(root, 'src/web/public/i18n.js'), 'utf8'), dom.getInternalVMContext());
const api = (
dom.window as unknown as { CodemanI18n: { t(s: string): string; configure(o: Record<string, string>): void } }
).CodemanI18n;
api.configure({ language: 'zh-CN' });
it('translates the button and the toast frame, keeping the version and the English highlights', () => {
expect(api.t("See what's new")).toBe('查看更新内容');
expect(api.t('Codeman updated to v1.41.2: Tab layouts (#538) · Split view')).toBe(
'Codeman 已更新到 v1.41.2:Tab layouts (#538) · Split view'
);
expect(api.t('Codeman updated to v1.41.2: A · B · C (+5 more)')).toBe(
'Codeman 已更新到 v1.41.2:A · B · C(另有 5 项)'
);
});
it('builds the strings the pattern expects', () => {
const ui = readFileSync(resolve(root, 'src/web/public/settings-ui.js'), 'utf8');
expect(ui).toContain('`Codeman updated to v${runningVersion}: ${shown}${more}`');
expect(ui).toContain('` (+${notes.highlights.length - 3} more)`');
expect(ui).toContain(`label: "See what's new"`);
});
});