`);
const { window } = dom;
const api = window.CodemanI18n;
api.start();
api.configure({ language: 'zh-CN', displayName: '小码助手' });
expect(window.document.querySelector('.logo')?.textContent).toBe('小码助手');
expect(window.document.getElementById('settings')?.textContent).toBe('应用设置');
expect(window.document.getElementById('settings')?.getAttribute('title')).toBe('应用设置');
expect(window.document.getElementById('user')?.textContent).toBe('Home');
expect(window.document.querySelector('.xterm')?.textContent).toBe('Run');
expect(window.document.querySelector('.history-item-title')?.textContent).toBe('Run');
expect(window.document.querySelector('.history-detail-prompt')?.textContent).toBe('Settings saved');
expect(window.document.querySelector('.history-item')?.getAttribute('title')).toBe('Run');
expect(window.document.title).toBe('小码助手:test-host');
const dynamicButton = window.document.createElement('button');
dynamicButton.textContent = 'Settings saved';
dynamicButton.title = 'Open Codeman across all displays';
window.document.getElementById('dynamic')?.appendChild(dynamicButton);
await new Promise((resolve) => window.setTimeout(resolve, 0));
expect(dynamicButton.textContent).toBe('设置已保存');
expect(dynamicButton.title).toBe('在所有显示器上打开 小码助手');
api.configure({ language: 'en', displayName: 'Workbench' });
expect(window.document.getElementById('settings')?.textContent).toBe('App Settings');
expect(window.document.querySelector('.logo')?.textContent).toBe('Workbench');
expect(dynamicButton.textContent).toBe('Settings saved');
expect(window.document.title).toBe('Workbench:test-host');
dom.window.close();
});
it('does not walk what a terminal renders: one check per mutation inside a skipped surface', async () => {
const dom = makeDom('
');
const { window } = dom;
const api = window.CodemanI18n;
api.start();
api.configure({ language: 'zh-CN' });
// i18n.js also starts itself on DOMContentLoaded (one walk of the body).
await new Promise((resolve) => window.setTimeout(resolve, 0));
let walks = 0;
const createTreeWalker = window.document.createTreeWalker.bind(window.document);
window.document.createTreeWalker = (...args: Parameters) => {
walks++;
return createTreeWalker(...args);
};
// What xterm's DOM renderer does every frame: replace the rows.
const rows = window.document.getElementById('rows')!;
for (let frame = 0; frame < 3; frame++) {
const fresh = Array.from({ length: 20 }, () => {
const row = window.document.createElement('div');
row.innerHTML = 'Settings savedRun';
return row;
});
rows.replaceChildren(...fresh);
}
rows.firstElementChild!.firstChild!.firstChild!.nodeValue = 'Settings saved';
await new Promise((resolve) => window.setTimeout(resolve, 0));
expect(walks).toBe(0);
expect(rows.firstElementChild?.textContent).toBe('Settings savedRun');
expect(rows.querySelector('[title]')?.getAttribute('title')).toBe('Run');
// Application DOM next to it is still translated.
const button = window.document.createElement('button');
button.textContent = 'Settings saved';
window.document.getElementById('app')!.appendChild(button);
await new Promise((resolve) => window.setTimeout(resolve, 0));
expect(button.textContent).toBe('设置已保存');
expect(walks).toBe(1);
dom.window.close();
});
it('decides an xterm row by its rows container, with the verdict closest() gives', async () => {
// A terminal (rows inside .xterm) and a stray `.xterm-rows` outside any
// skipped surface, which the full check translates.
const dom = makeDom(
'
' +
''
);
const { window } = dom;
const api = window.CodemanI18n;
api.start();
api.configure({ language: 'zh-CN' });
await new Promise((resolve) => window.setTimeout(resolve, 0));
const rowsOf = (id: string) =>
Array.from({ length: 20 }, () => {
const row = window.document.createElement('div');
window.document.getElementById(id)!.appendChild(row);
return row;
});
const rows = rowsOf('rows');
const stray = rowsOf('stray');
await new Promise((resolve) => window.setTimeout(resolve, 0));
let closest = 0;
const original = window.Element.prototype.closest;
window.Element.prototype.closest = function (this: Element, selector: string) {
closest++;
return original.call(this, selector);
};
// What xterm's DOM renderer does every frame: replace each row's children.
const frames = (list: Element[]) => {
for (let frame = 0; frame < 3; frame++) {
for (const row of list) {
const span = window.document.createElement('span');
span.textContent = 'Settings saved';
row.replaceChildren(span);
}
}
};
frames(rows);
await new Promise((resolve) => window.setTimeout(resolve, 0));
// 60 row records, ONE closest(): the container's own verdict, then kept.
expect(closest).toBe(1);
expect(rows.every((row) => row.textContent === 'Settings saved')).toBe(true);
// A rows container outside any skipped surface keeps the full check, and
// with it the verdict closest() gives: its rows are translated.
frames(stray);
await new Promise((resolve) => window.setTimeout(resolve, 0));
window.Element.prototype.closest = original;
expect(stray.every((row) => row.textContent === '设置已保存')).toBe(true);
dom.window.close();
});
it('keeps a quoted group name apart from the fixed "Move to" entries in zh-CN', () => {
const dom = makeDom('');
const api = dom.window.CodemanI18n;
api.configure({ language: 'zh-CN' });
const labels = ['Move to "New group"', 'Move to new group', 'Move to "ungrouped"', 'Move to Ungrouped'].map(
(label) => api.t(label)
);
expect(labels).toEqual(['移到“New group”', '移到新分组', '移到“ungrouped”', '移到未分组']);
expect(new Set(labels).size).toBe(4);
dom.window.close();
});
it('never translates a tab name, even one that reads like a UI string', async () => {
const tab = (name: string) =>
`
` +
`${name}
`;
const dom = makeDom(
`
${tab('New session')}${tab('Follows up on bug')}
`
);
const { window } = dom;
const api = window.CodemanI18n;
api.start();
api.configure({ language: 'zh-CN' });
const names = () => [...window.document.querySelectorAll('.tab-name')].map((el) => el.textContent);
expect(names()).toEqual(['New session', 'Follows up on bug']);
// A tab drawn after the switch stays untranslated too, while the same
// strings as real UI text still translate.
window.document.getElementById('sessionTabs')!.insertAdjacentHTML('beforeend', tab('New session'));
const menu = window.document.getElementById('menu')!;
menu.innerHTML = '
Follows One
';
await new Promise((resolve) => setTimeout(resolve, 0));
expect(names()).toEqual(['New session', 'Follows up on bug', 'New session']);
expect(menu.querySelector('button')?.textContent).toBe('新建会话');
expect(menu.querySelector('div')?.textContent).toBe('跟随 One');
dom.window.close();
});
it('renders hostile-looking names as text rather than HTML', () => {
const dom = makeDom('Codeman');
const api = dom.window.CodemanI18n;
api.start();
api.configure({ displayName: '', language: 'en' });
const logo = dom.window.document.querySelector('.logo');
expect(logo?.textContent).toBe('');
expect(logo?.querySelector('img')).toBeNull();
dom.window.close();
});
it('wires the two settings with language kept per-device and display name synced', () => {
expect(indexSource).toContain('id="appSettingsDisplayName"');
expect(indexSource).toContain('id="appSettingsLanguage"');
expect(indexSource).toContain('');
expect(settingsSource).toMatch(/language:\s*_language/);
expect(settingsSource).toContain("'language',");
expect(settingsSource).not.toMatch(/displayName:\s*_displayName/);
});
it('uses the escaped custom name in the server-rendered window title', async () => {
const dataDir = mkdtempSync(join(tmpdir(), 'codeman-brand-title-'));
process.env.CODEMAN_DATA_DIR = dataDir;
writeFileSync(join(dataDir, 'settings.json'), JSON.stringify({ displayName: '小码<&助手' }));
const server = new WebServer(0, false, true, '127.0.0.1', 'laptop');
const html = await (server as unknown as { renderIndexHtml: () => Promise }).renderIndexHtml();
expect(html).toContain('小码<&助手:laptop');
expect(html).not.toContain('小码<&助手:laptop');
});
});