[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
315
infrastructure/config/cjkFonts.test.ts
Normal file
315
infrastructure/config/cjkFonts.test.ts
Normal file
@@ -0,0 +1,315 @@
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
composeFontFamilyStack,
|
||||
getDefaultCjkFallback,
|
||||
getRecommendedCjkFor,
|
||||
splitFontFamilyList,
|
||||
CJK_SYSTEM_FALLBACK_STACK,
|
||||
} from './cjkFonts';
|
||||
|
||||
describe('composeFontFamilyStack', () => {
|
||||
it('puts the primary font first', () => {
|
||||
const stack = composeFontFamilyStack({
|
||||
primaryFamily: 'Menlo, monospace',
|
||||
userFallback: '',
|
||||
latinFontId: 'menlo',
|
||||
platform: 'darwin',
|
||||
});
|
||||
assert.match(stack, /^Menlo,\s*/);
|
||||
});
|
||||
|
||||
it('inserts user fallback right after primary when provided', () => {
|
||||
const stack = composeFontFamilyStack({
|
||||
primaryFamily: '"Fira Code", monospace',
|
||||
userFallback: 'Sarasa Mono SC',
|
||||
latinFontId: 'fira-code',
|
||||
platform: 'darwin',
|
||||
});
|
||||
const firaIdx = stack.indexOf('Fira Code');
|
||||
const userIdx = stack.indexOf('Sarasa Mono SC');
|
||||
assert.ok(firaIdx >= 0 && userIdx > firaIdx, 'user fallback after primary');
|
||||
});
|
||||
|
||||
it('uses per-Latin-font recommended CJK when user fallback is empty', () => {
|
||||
const stack = composeFontFamilyStack({
|
||||
primaryFamily: '"Cascadia Code", monospace',
|
||||
userFallback: '',
|
||||
latinFontId: 'cascadia-code',
|
||||
platform: 'win32',
|
||||
});
|
||||
// Cascadia Code now recommends Sarasa Mono SC (true monospace).
|
||||
assert.match(stack, /Sarasa Mono SC/);
|
||||
});
|
||||
|
||||
it('falls back to OS default when Latin font has no recommendation', () => {
|
||||
const stack = composeFontFamilyStack({
|
||||
primaryFamily: '"Unknown Font", monospace',
|
||||
userFallback: '',
|
||||
latinFontId: 'unknown',
|
||||
platform: 'darwin',
|
||||
});
|
||||
// macOS no-recommendation default is now Sarasa Mono SC (bundled).
|
||||
assert.match(stack, /Sarasa Mono SC/);
|
||||
});
|
||||
|
||||
it('quotes multi-word user fallback names', () => {
|
||||
const stack = composeFontFamilyStack({
|
||||
primaryFamily: 'Menlo, monospace',
|
||||
userFallback: 'Source Han Mono SC',
|
||||
latinFontId: 'menlo',
|
||||
platform: 'linux',
|
||||
});
|
||||
assert.match(stack, /"Source Han Mono SC"/);
|
||||
});
|
||||
|
||||
it('keeps an unquoted user fallback containing a comma as one font family', () => {
|
||||
const stack = composeFontFamilyStack({
|
||||
primaryFamily: 'Menlo, monospace',
|
||||
userFallback: 'Foo, Inc. Mono',
|
||||
latinFontId: 'menlo',
|
||||
platform: 'darwin',
|
||||
});
|
||||
|
||||
assert.ok(stack.includes('"Foo, Inc. Mono"'));
|
||||
assert.ok(splitFontFamilyList(stack).includes('"Foo, Inc. Mono"'));
|
||||
});
|
||||
|
||||
it('escapes quotes inside a manually entered font family', () => {
|
||||
const stack = composeFontFamilyStack({
|
||||
primaryFamily: 'Menlo, monospace',
|
||||
userFallback: 'Foo"Bar',
|
||||
latinFontId: 'menlo',
|
||||
platform: 'darwin',
|
||||
});
|
||||
|
||||
assert.ok(stack.includes('"Foo\\"Bar"'));
|
||||
assert.ok(splitFontFamilyList(stack).includes('"Foo\\"Bar"'));
|
||||
});
|
||||
|
||||
it('quotes a manually entered font family containing an apostrophe', () => {
|
||||
const stack = composeFontFamilyStack({
|
||||
primaryFamily: 'Menlo, monospace',
|
||||
userFallback: "Rock'nRoll",
|
||||
latinFontId: 'menlo',
|
||||
platform: 'darwin',
|
||||
});
|
||||
|
||||
assert.ok(stack.includes('"Rock\'nRoll"'));
|
||||
assert.ok(splitFontFamilyList(stack).includes('"Rock\'nRoll"'));
|
||||
});
|
||||
|
||||
it('quotes a manually entered font family that starts with a digit', () => {
|
||||
const stack = composeFontFamilyStack({
|
||||
primaryFamily: 'Menlo, monospace',
|
||||
userFallback: '3270font',
|
||||
latinFontId: 'menlo',
|
||||
platform: 'darwin',
|
||||
});
|
||||
|
||||
assert.ok(stack.includes('"3270font"'));
|
||||
assert.ok(splitFontFamilyList(stack).includes('"3270font"'));
|
||||
});
|
||||
|
||||
it('keeps CSS generic font families unquoted', () => {
|
||||
const stack = composeFontFamilyStack({
|
||||
primaryFamily: 'Menlo, monospace',
|
||||
userFallback: 'ui-monospace',
|
||||
latinFontId: 'menlo',
|
||||
platform: 'darwin',
|
||||
});
|
||||
|
||||
assert.ok(splitFontFamilyList(stack).includes('ui-monospace'));
|
||||
});
|
||||
|
||||
it('does not duplicate identical fallback entries', () => {
|
||||
// User explicitly picks the same font the per-font pairing would,
|
||||
// and that font also lives in the system stack — should appear once.
|
||||
const stack = composeFontFamilyStack({
|
||||
primaryFamily: '"Cascadia Code", monospace',
|
||||
userFallback: 'Sarasa Mono SC',
|
||||
latinFontId: 'cascadia-code',
|
||||
platform: 'win32',
|
||||
});
|
||||
const matches = stack.match(/Sarasa Mono SC/g) || [];
|
||||
assert.equal(matches.length, 1);
|
||||
});
|
||||
|
||||
it('inserts JetBrains Mono as Latin-only fallback right after the primary family', () => {
|
||||
const stack = composeFontFamilyStack({
|
||||
primaryFamily: 'Menlo',
|
||||
userFallback: '',
|
||||
latinFontId: 'menlo',
|
||||
platform: 'darwin',
|
||||
});
|
||||
const families = stack.split(',').map((s) => s.trim().replace(/^"|"$/g, ''));
|
||||
assert.equal(families[0], 'Menlo');
|
||||
assert.equal(families[1], 'JetBrains Mono');
|
||||
});
|
||||
|
||||
it('Latin fallback (JetBrains Mono) precedes every CJK family', () => {
|
||||
// Regression guard for codex P1 review on PR #940 (first round):
|
||||
// when the primary font isn't installed, Latin glyphs must fall to
|
||||
// a Latin-only monospace face — NOT a CJK font's full-width Latin
|
||||
// variant — to keep xterm's fixed cell grid aligned. JetBrains Mono
|
||||
// is bundled via @fontsource and contains no CJK glyphs, so it
|
||||
// catches Latin while letting CJK glyphs flow past.
|
||||
const stack = composeFontFamilyStack({
|
||||
primaryFamily: '"Fira Code", monospace',
|
||||
userFallback: 'LXGW WenKai Mono',
|
||||
latinFontId: 'fira-code',
|
||||
platform: 'darwin',
|
||||
});
|
||||
const jbIdx = stack.indexOf('JetBrains Mono');
|
||||
const sarasaIdx = stack.indexOf('Sarasa Mono SC');
|
||||
const userFallbackIdx = stack.indexOf('LXGW WenKai Mono');
|
||||
const simSunIdx = stack.indexOf('SimSun');
|
||||
assert.ok(jbIdx > 0, 'JetBrains Mono must appear in the stack');
|
||||
assert.ok(jbIdx < userFallbackIdx, 'JetBrains Mono before user CJK');
|
||||
assert.ok(jbIdx < sarasaIdx, 'JetBrains Mono before Sarasa system fallback');
|
||||
assert.ok(jbIdx < simSunIdx, 'JetBrains Mono before SimSun system fallback');
|
||||
});
|
||||
|
||||
it('preserves a quoted primary family name that contains a comma', () => {
|
||||
// Regression guard for codex P2 review on PR #940: when the primary
|
||||
// family is something like `"Foo, Inc. Mono"`, the composed stack
|
||||
// must keep that token intact rather than splitting on the internal
|
||||
// comma and emitting fragmented pieces.
|
||||
const stack = composeFontFamilyStack({
|
||||
primaryFamily: '"Foo, Inc. Mono", monospace',
|
||||
userFallback: '',
|
||||
latinFontId: 'foo-inc-mono',
|
||||
platform: 'darwin',
|
||||
});
|
||||
assert.ok(
|
||||
stack.includes('"Foo, Inc. Mono"'),
|
||||
'quoted family with comma stays a single token',
|
||||
);
|
||||
assert.ok(
|
||||
!stack.includes('"Foo,') || stack.includes('"Foo, Inc. Mono"'),
|
||||
'must not produce a dangling `"Foo,` fragment',
|
||||
);
|
||||
});
|
||||
|
||||
it('user-chosen CJK fallback precedes generic monospace', () => {
|
||||
// Regression guard for codex P1 review on PR #940 (second round):
|
||||
// generic `monospace` on macOS Chrome resolves Chinese glyphs to
|
||||
// PingFang via Chromium's CJK system fallback. If `monospace`
|
||||
// appeared in the chain BEFORE the user's CJK pick, CSS per-glyph
|
||||
// fallback would stop at monospace for CJK characters and never
|
||||
// consult the user's choice, silently nullifying the CJK picker.
|
||||
const stack = composeFontFamilyStack({
|
||||
primaryFamily: '"Fira Code", monospace',
|
||||
userFallback: 'LXGW WenKai Mono',
|
||||
latinFontId: 'fira-code',
|
||||
platform: 'darwin',
|
||||
});
|
||||
const userFallbackIdx = stack.indexOf('LXGW WenKai Mono');
|
||||
// Match `monospace` as a standalone token (after the comma+space).
|
||||
const monospaceIdx = stack.lastIndexOf(', monospace');
|
||||
assert.ok(userFallbackIdx > 0, 'user CJK must appear');
|
||||
assert.ok(monospaceIdx > userFallbackIdx, 'user CJK must come before generic monospace');
|
||||
});
|
||||
|
||||
it('explicit user fallback overrides the per-font recommendation', () => {
|
||||
const stack = composeFontFamilyStack({
|
||||
primaryFamily: '"JetBrains Mono", monospace',
|
||||
userFallback: 'LXGW WenKai Mono',
|
||||
latinFontId: 'jetbrains-mono',
|
||||
platform: 'darwin',
|
||||
});
|
||||
// User chose LXGW WenKai Mono; the JetBrains Mono recommendation
|
||||
// (Sarasa Mono SC) should be suppressed, so Sarasa only shows up
|
||||
// later in the system fallback stack, AFTER the user choice.
|
||||
const userIdx = stack.indexOf('LXGW WenKai Mono');
|
||||
const sarasaIdx = stack.indexOf('Sarasa Mono SC');
|
||||
assert.ok(userIdx >= 0);
|
||||
assert.ok(sarasaIdx > userIdx, 'system Sarasa appears after explicit user choice');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDefaultCjkFallback', () => {
|
||||
it('returns SimSun on Windows (always installed, monospace)', () => {
|
||||
assert.equal(getDefaultCjkFallback('win32'), 'SimSun');
|
||||
});
|
||||
it('returns Sarasa Mono SC on macOS (bundled by app)', () => {
|
||||
assert.equal(getDefaultCjkFallback('darwin'), 'Sarasa Mono SC');
|
||||
});
|
||||
it('returns Noto Sans Mono CJK SC on Linux', () => {
|
||||
assert.equal(getDefaultCjkFallback('linux'), 'Noto Sans Mono CJK SC');
|
||||
});
|
||||
it('never returns a known proportional font', () => {
|
||||
const proportional = ['PingFang SC', 'Microsoft YaHei UI', 'Microsoft YaHei', 'Hiragino Sans GB'];
|
||||
for (const platform of ['darwin', 'win32', 'linux'] as const) {
|
||||
const v = getDefaultCjkFallback(platform);
|
||||
assert.ok(!proportional.includes(v), `${platform} default ${v} must not be proportional`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRecommendedCjkFor', () => {
|
||||
it('returns null for unknown fonts', () => {
|
||||
assert.equal(getRecommendedCjkFor('unknown-font-id', 'darwin'), null);
|
||||
});
|
||||
it('returns a non-empty string for known fonts', () => {
|
||||
const v = getRecommendedCjkFor('jetbrains-mono', 'darwin');
|
||||
assert.ok(v && v.length > 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('splitFontFamilyList', () => {
|
||||
it('splits a simple comma-separated list', () => {
|
||||
assert.deepEqual(
|
||||
splitFontFamilyList('Menlo, monospace'),
|
||||
['Menlo', 'monospace'],
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps quoted family names with commas intact', () => {
|
||||
// Regression guard for codex P2 review on PR #940: a font family
|
||||
// name like `"Foo, Inc. Mono"` is a single token in CSS, not two.
|
||||
assert.deepEqual(
|
||||
splitFontFamilyList('"Foo, Inc. Mono", monospace'),
|
||||
['"Foo, Inc. Mono"', 'monospace'],
|
||||
);
|
||||
});
|
||||
|
||||
it('handles a single unquoted name', () => {
|
||||
assert.deepEqual(splitFontFamilyList('Iosevka'), ['Iosevka']);
|
||||
});
|
||||
|
||||
it('handles single quotes too', () => {
|
||||
assert.deepEqual(
|
||||
splitFontFamilyList("'Foo, Inc.', serif"),
|
||||
["'Foo, Inc.'", 'serif'],
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps escaped quotes inside a quoted family name', () => {
|
||||
assert.deepEqual(
|
||||
splitFontFamilyList('"Foo\\"Bar", monospace'),
|
||||
['"Foo\\"Bar"', 'monospace'],
|
||||
);
|
||||
});
|
||||
|
||||
it('drops empty segments produced by double commas', () => {
|
||||
assert.deepEqual(
|
||||
splitFontFamilyList('Menlo,, monospace'),
|
||||
['Menlo', 'monospace'],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CJK_SYSTEM_FALLBACK_STACK', () => {
|
||||
it('contains true-monospace CJK fonts only', () => {
|
||||
assert.match(CJK_SYSTEM_FALLBACK_STACK, /Sarasa Mono SC/);
|
||||
assert.match(CJK_SYSTEM_FALLBACK_STACK, /Noto Sans Mono CJK SC/);
|
||||
assert.match(CJK_SYSTEM_FALLBACK_STACK, /SimSun/);
|
||||
});
|
||||
|
||||
it('does not include known proportional CJK fonts', () => {
|
||||
assert.doesNotMatch(CJK_SYSTEM_FALLBACK_STACK, /PingFang SC/);
|
||||
assert.doesNotMatch(CJK_SYSTEM_FALLBACK_STACK, /Microsoft YaHei UI/);
|
||||
assert.doesNotMatch(CJK_SYSTEM_FALLBACK_STACK, /Hiragino Sans GB/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user