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
225 lines
8.4 KiB
TypeScript
225 lines
8.4 KiB
TypeScript
import { describe, it, beforeEach } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
extractPrimaryFamily,
|
|
detectInstalledWithContext,
|
|
isFontInstalled,
|
|
setSystemFamilies,
|
|
hasAuthoritativeData,
|
|
clearFontAvailabilityCache,
|
|
subscribeFontAvailability,
|
|
getFontAvailabilityVersion,
|
|
bundledFamiliesInStack,
|
|
} from './fontAvailability';
|
|
|
|
describe('bundledFamiliesInStack', () => {
|
|
it('returns bundled webfonts present in a composed stack, in order', () => {
|
|
const stack = 'Consolas, "JetBrains Mono", "Sarasa Mono SC", monospace';
|
|
assert.deepEqual(bundledFamiliesInStack(stack), ['JetBrains Mono', 'Sarasa Mono SC']);
|
|
});
|
|
|
|
it('returns nothing when only system fonts are referenced', () => {
|
|
assert.deepEqual(bundledFamiliesInStack('Menlo, monospace'), []);
|
|
});
|
|
|
|
it('ignores the generic monospace keyword and dedupes', () => {
|
|
const stack = '"JetBrains Mono", "JetBrains Mono", monospace';
|
|
assert.deepEqual(bundledFamiliesInStack(stack), ['JetBrains Mono']);
|
|
});
|
|
|
|
it('is case-insensitive on family names', () => {
|
|
assert.deepEqual(bundledFamiliesInStack('"jetbrains mono"'), ['JetBrains Mono']);
|
|
});
|
|
});
|
|
|
|
describe('extractPrimaryFamily', () => {
|
|
it('strips quotes from a quoted name', () => {
|
|
assert.equal(extractPrimaryFamily('"Fira Code", monospace'), 'Fira Code');
|
|
});
|
|
it('returns unquoted single-word names as-is', () => {
|
|
assert.equal(extractPrimaryFamily('Menlo, monospace'), 'Menlo');
|
|
});
|
|
it('returns the first family in a list', () => {
|
|
assert.equal(
|
|
extractPrimaryFamily('"Source Code Pro", "Fira Code", monospace'),
|
|
'Source Code Pro',
|
|
);
|
|
});
|
|
it('handles a single name without comma', () => {
|
|
assert.equal(extractPrimaryFamily('Iosevka'), 'Iosevka');
|
|
});
|
|
});
|
|
|
|
function makeContextWithInstalledFamilies(installed: Set<string>) {
|
|
// Mock canvas measurement: each generic fallback has a stable width;
|
|
// a "real" installed font produces a different width per fallback.
|
|
// Collision-resistant: position-weighted polynomial hash.
|
|
const widthFor = (family: string): number => {
|
|
let h = 0;
|
|
for (let i = 0; i < family.length; i++) {
|
|
h = (h * 31 + family.charCodeAt(i)) >>> 0;
|
|
}
|
|
return 100 + (h % 9973);
|
|
};
|
|
return {
|
|
measureText: (font: string, _text: string) => {
|
|
const match = font.match(/^\d+px\s+(.+)$/);
|
|
if (!match) return 0;
|
|
const familyList = match[1];
|
|
const families = familyList
|
|
.split(',')
|
|
.map((f) => f.trim().replace(/^["']|["']$/g, ''));
|
|
for (const f of families) {
|
|
if (installed.has(f) || ['serif', 'sans-serif', 'monospace'].includes(f)) {
|
|
return widthFor(f);
|
|
}
|
|
}
|
|
return 0;
|
|
},
|
|
};
|
|
}
|
|
|
|
describe('detectInstalledWithContext (canvas fallback)', () => {
|
|
it('detects an installed font (width differs from all 3 generic fallbacks)', () => {
|
|
const ctx = makeContextWithInstalledFamilies(new Set(['Fira Code']));
|
|
assert.equal(detectInstalledWithContext('Fira Code', ctx), true);
|
|
});
|
|
|
|
it('rejects a non-installed font (falls through to fallback)', () => {
|
|
const ctx = makeContextWithInstalledFamilies(new Set(['Fira Code']));
|
|
assert.equal(detectInstalledWithContext('Definitely Not A Font', ctx), false);
|
|
});
|
|
|
|
it('treats KNOWN_BUNDLED_FAMILIES as installed regardless of canvas evidence', () => {
|
|
const ctx = makeContextWithInstalledFamilies(new Set());
|
|
assert.equal(detectInstalledWithContext('JetBrains Mono', ctx), true);
|
|
assert.equal(detectInstalledWithContext('Sarasa Mono SC', ctx), true);
|
|
});
|
|
|
|
it('treats a font as installed when it matches one generic but differs from the others', () => {
|
|
// Regression guard for codex P2 review on PR #940: on macOS the
|
|
// `monospace` generic resolves to Menlo, so measure(`"Menlo", monospace`)
|
|
// equals measure(`monospace`). The detector must NOT report Menlo
|
|
// as uninstalled just because of that single collision — it should
|
|
// recognize installation via the other two generic baselines.
|
|
const ctx = {
|
|
measureText: (font: string): number => {
|
|
// "Menlo", X → Menlo's metrics (always 100, regardless of fallback)
|
|
if (font.includes('"Menlo"')) return 100;
|
|
// Generic baselines
|
|
if (font === '72px serif') return 50;
|
|
if (font === '72px sans-serif') return 80;
|
|
if (font === '72px monospace') return 100; // identical to Menlo
|
|
// Unknown family followed by a generic → falls to that generic
|
|
const tail = font.split(',').pop()?.trim() ?? '';
|
|
if (tail === 'serif') return 50;
|
|
if (tail === 'sans-serif') return 80;
|
|
if (tail === 'monospace') return 100;
|
|
return 0;
|
|
},
|
|
};
|
|
assert.equal(detectInstalledWithContext('Menlo', ctx), true);
|
|
});
|
|
|
|
it('still reports a clearly-uninstalled font as missing even with the looser rule', () => {
|
|
// "Some" semantics must not introduce false positives for fonts
|
|
// that genuinely aren't installed — those fall through to each
|
|
// generic and match all three baselines.
|
|
const ctx = makeContextWithInstalledFamilies(new Set(['Menlo']));
|
|
assert.equal(detectInstalledWithContext('Definitely Not Installed', ctx), false);
|
|
});
|
|
});
|
|
|
|
describe('isFontInstalled with authoritative system data', () => {
|
|
beforeEach(() => {
|
|
clearFontAvailabilityCache();
|
|
});
|
|
|
|
it('returns true for bundled families even without authoritative data', () => {
|
|
assert.equal(hasAuthoritativeData(), false);
|
|
assert.equal(isFontInstalled('JetBrains Mono'), true);
|
|
assert.equal(isFontInstalled('Sarasa Mono SC'), true);
|
|
});
|
|
|
|
it('answers from authoritative set once setSystemFamilies has run', () => {
|
|
setSystemFamilies(new Set(['menlo', 'fira code']));
|
|
assert.equal(hasAuthoritativeData(), true);
|
|
assert.equal(isFontInstalled('Menlo'), true);
|
|
assert.equal(isFontInstalled('Fira Code'), true);
|
|
assert.equal(isFontInstalled('Sarasa Mono SC'), true, 'bundled wins over set');
|
|
assert.equal(isFontInstalled('PingFang SC'), false, 'not in authoritative set');
|
|
assert.equal(isFontInstalled('Programmer Fonts'), false, 'fictitious name');
|
|
});
|
|
|
|
it('lookup is case-insensitive (set stores lowercase)', () => {
|
|
setSystemFamilies(new Set(['microsoft yahei ui']));
|
|
assert.equal(isFontInstalled('Microsoft YaHei UI'), true);
|
|
assert.equal(isFontInstalled('MICROSOFT YAHEI UI'), true);
|
|
});
|
|
|
|
it('falls back to safe-default (true) without DOM and without authoritative data', () => {
|
|
assert.equal(hasAuthoritativeData(), false);
|
|
assert.equal(isFontInstalled('Some Unknown Font'), true);
|
|
});
|
|
|
|
it('a null authoritative set means we re-enter fallback mode', () => {
|
|
setSystemFamilies(new Set(['menlo']));
|
|
assert.equal(hasAuthoritativeData(), true);
|
|
setSystemFamilies(null);
|
|
assert.equal(hasAuthoritativeData(), false);
|
|
});
|
|
});
|
|
|
|
describe('font availability subscription', () => {
|
|
beforeEach(() => {
|
|
clearFontAvailabilityCache();
|
|
});
|
|
|
|
it('notifies subscribers when setSystemFamilies is called', () => {
|
|
// Regression guard for codex P2 review on PR #940:
|
|
// TerminalCjkFontSelect memoizes visibleOptions on [value] but the
|
|
// filter calls isFontInstalled which depends on systemFamilies.
|
|
// Subscribers wired via useSyncExternalStore must fire so memos
|
|
// recompute when authoritative data arrives.
|
|
let calls = 0;
|
|
const unsubscribe = subscribeFontAvailability(() => {
|
|
calls += 1;
|
|
});
|
|
|
|
setSystemFamilies(new Set(['menlo']));
|
|
assert.equal(calls, 1);
|
|
|
|
setSystemFamilies(new Set(['menlo', 'fira code']));
|
|
assert.equal(calls, 2);
|
|
|
|
setSystemFamilies(null);
|
|
assert.equal(calls, 3);
|
|
|
|
unsubscribe();
|
|
setSystemFamilies(new Set(['menlo']));
|
|
assert.equal(calls, 3, 'unsubscribe stops notifications');
|
|
});
|
|
|
|
it('version monotonically increases on each setSystemFamilies call', () => {
|
|
const v0 = getFontAvailabilityVersion();
|
|
setSystemFamilies(new Set(['menlo']));
|
|
const v1 = getFontAvailabilityVersion();
|
|
setSystemFamilies(new Set(['menlo', 'fira code']));
|
|
const v2 = getFontAvailabilityVersion();
|
|
|
|
assert.ok(v1 > v0, 'first call bumps version');
|
|
assert.ok(v2 > v1, 'second call bumps version');
|
|
});
|
|
|
|
it('clearFontAvailabilityCache also notifies subscribers', () => {
|
|
let calls = 0;
|
|
subscribeFontAvailability(() => {
|
|
calls += 1;
|
|
});
|
|
setSystemFamilies(new Set(['menlo']));
|
|
const after = calls;
|
|
clearFontAvailabilityCache();
|
|
assert.ok(calls > after, 'clear notifies too');
|
|
});
|
|
});
|