- BrowserView: 即使 host 在白名单,GUI 自身 origin 永不给 allow-same-origin(裸 host 条目覆盖所有端口时可提权) - intercept: revealInExplorer 相对路径经 resolveSidebarPath 解析后再 revealPaths(交付物路径是相对的) - intercept: ctx.betterSidebar 改为 ctx.get(#357 纤维链读取约定) - FileTree/EditorHost/TreePanel/tabs: 把 revealed 贯通到树行渲染 + 高亮类 + scrollIntoView(此前高亮永不渲染) - state.revealPaths: 重建祖先目录时保留前导分隔符(POSIX/UNC 此前目录永远折叠) - state.schedulePersist: 仅活动会话写入全局宽度(targeted open 不再用非活动会话的过期宽度覆盖拖拽值) - browser.probe: GET 回退显式取消 body(否则流式/大响应挂住 socket) - terminal-font: leading generic 前只放 symbols-only 字体;fully-patched 放 generic 后(避免 hijack 度量字体) - sidebar.module.css: 恢复拖拽反馈(#130 的 silent resize 与 PR 描述冲突,取 HEAD) - Sidebar: 剩余 4 处 ctx.betterSidebar 改 ctx.get - tests: 补 revealPaths/iframeSandboxFor/terminal-font 回归测试 + EditorHost/FileTree 测试补 revealed prop
183 lines
9.0 KiB
TypeScript
183 lines
9.0 KiB
TypeScript
/**
|
||
* Terminal font resolution tests: the custom font prefs (side card settings,
|
||
* terminal card) resolve into xterm options with a strict fallback chain —
|
||
* user family > theme code font > the built-in monospace stack, and the size
|
||
* clamped into the 9–32 contract.
|
||
*
|
||
* On top of the base family, icon fonts (Nerd Font + color emoji) are
|
||
* appended so shell prompts drawing from the Private Use Areas render real
|
||
* glyphs instead of the missing-glyph box.
|
||
*/
|
||
import { describe, expect, it } from 'vitest'
|
||
import { SIDEBAR_PREFS_DEFAULTS, clampTerminalFontSize, TERMINAL_FONT_SIZE_MAX, TERMINAL_FONT_SIZE_MIN } from '../src/prefs-shared.ts'
|
||
import {
|
||
DEFAULT_TERMINAL_FONT_FAMILY,
|
||
ICON_FONT_FALLBACKS,
|
||
resolveTerminalFont,
|
||
withIconFontFallbacks,
|
||
} from '../src/client/terminal-font.ts'
|
||
|
||
describe('clampTerminalFontSize', () => {
|
||
it('rounds and clamps into the 9–32 contract', () => {
|
||
expect(clampTerminalFontSize(13)).toBe(13)
|
||
expect(clampTerminalFontSize(15.6)).toBe(16)
|
||
expect(clampTerminalFontSize(5)).toBe(TERMINAL_FONT_SIZE_MIN)
|
||
expect(clampTerminalFontSize(40)).toBe(TERMINAL_FONT_SIZE_MAX)
|
||
})
|
||
})
|
||
|
||
describe('withIconFontFallbacks', () => {
|
||
/** Index of a family in a resolved stack, -1 when absent. */
|
||
function indexOfFamily(stack: string, family: string): number {
|
||
return stack.split(',').map(part => part.trim()).indexOf(family)
|
||
}
|
||
|
||
it('keeps the base font first so xterm measures cells against it', () => {
|
||
const stack = withIconFontFallbacks('"JetBrains Mono", monospace')
|
||
expect(stack.startsWith('"JetBrains Mono"')).toBe(true)
|
||
})
|
||
|
||
it('splices icon fonts ahead of a trailing generic family (a generic always resolves)', () => {
|
||
const stack = withIconFontFallbacks('"JetBrains Mono", monospace')
|
||
const parts = stack.split(',').map(part => part.trim())
|
||
expect(parts[parts.length - 1]).toBe('monospace')
|
||
expect(parts.indexOf('"Symbols Nerd Font Mono"')).toBeGreaterThan(0)
|
||
expect(parts.indexOf('"Symbols Nerd Font Mono"')).toBeLessThan(parts.length - 1)
|
||
})
|
||
|
||
it('splices ahead of a generic sitting MID-stack, not just a trailing one', () => {
|
||
// Regression guard: a generic anywhere is a catch-all, so appending
|
||
// after it silently defeats the whole fix (the icon fonts would never
|
||
// be consulted). Stacks of this shape come from theme tokens.
|
||
const stack = withIconFontFallbacks('ui-monospace, Menlo, monospace, "Apple Color Emoji"')
|
||
expect(indexOfFamily(stack, '"Symbols Nerd Font Mono"'))
|
||
.toBeLessThan(indexOfFamily(stack, 'ui-monospace'))
|
||
})
|
||
|
||
it('keeps fully-patched fonts BEHIND a leading generic (they carry Latin and would become the base)', () => {
|
||
// A leading generic resolves, so prepending a fully-patched Nerd Font
|
||
// (which ships Latin glyphs) would make it xterm's measuring/base font
|
||
// and override the user/theme family precedence. Only the symbols-only
|
||
// patches (no Latin) may precede it.
|
||
const stack = withIconFontFallbacks('monospace')
|
||
expect(indexOfFamily(stack, '"Symbols Nerd Font Mono"')).toBeLessThan(indexOfFamily(stack, 'monospace'))
|
||
expect(indexOfFamily(stack, '"Hack Nerd Font Mono"')).toBeGreaterThan(indexOfFamily(stack, 'monospace'))
|
||
expect(indexOfFamily(stack, '"JetBrainsMono Nerd Font Mono"')).toBeGreaterThan(indexOfFamily(stack, 'monospace'))
|
||
})
|
||
|
||
it('splices ahead of the FIRST generic when several are present', () => {
|
||
const stack = withIconFontFallbacks('Menlo, monospace, sans-serif')
|
||
const nerd = indexOfFamily(stack, '"Symbols Nerd Font Mono"')
|
||
expect(nerd).toBeGreaterThan(indexOfFamily(stack, 'Menlo'))
|
||
expect(nerd).toBeLessThan(indexOfFamily(stack, 'monospace'))
|
||
expect(indexOfFamily(stack, 'sans-serif')).toBe(stack.split(',').length - 1)
|
||
})
|
||
|
||
it('appends at the end when the stack has no generic family', () => {
|
||
const stack = withIconFontFallbacks('"JetBrains Mono"')
|
||
expect(stack.startsWith('"JetBrains Mono", "Symbols Nerd Font Mono"')).toBe(true)
|
||
})
|
||
|
||
it('orders symbols-only patches ahead of fully-patched distributions', () => {
|
||
// Order IS the priority contract: the symbols-only families carry no
|
||
// Latin glyphs, so they can never hijack ASCII cell metrics.
|
||
const stack = withIconFontFallbacks('Menlo, monospace')
|
||
expect(indexOfFamily(stack, '"Symbols Nerd Font Mono"'))
|
||
.toBeLessThan(indexOfFamily(stack, '"Hack Nerd Font Mono"'))
|
||
})
|
||
|
||
it('names Nerd Font families explicitly to cover both PUA planes', () => {
|
||
const stack = withIconFontFallbacks('Menlo, monospace')
|
||
expect(stack).toContain('"Symbols Nerd Font Mono"')
|
||
expect(stack).toContain('"Hack Nerd Font Mono"')
|
||
})
|
||
|
||
it('omits color-emoji families (Chromium routes emoji through its own fallback)', () => {
|
||
// Listing a color font ahead of the generic would let it capture BMP
|
||
// symbols the prompt expects in monospace and break the cell grid.
|
||
const stack = withIconFontFallbacks('Menlo, monospace')
|
||
expect(stack).not.toContain('Apple Color Emoji')
|
||
expect(stack).not.toContain('Segoe UI Emoji')
|
||
expect(stack).not.toContain('Noto Color Emoji')
|
||
})
|
||
|
||
it('does not duplicate a family the caller already listed, preserving its priority', () => {
|
||
const stack = withIconFontFallbacks("'hack nerd font mono', monospace")
|
||
expect(stack.toLowerCase().split('hack nerd font mono').length - 1).toBe(1)
|
||
// The caller's entry keeps pole position.
|
||
expect(stack.startsWith("'hack nerd font mono'")).toBe(true)
|
||
})
|
||
|
||
it('treats quote, case and whitespace differences as the same family', () => {
|
||
const stack = withIconFontFallbacks('"Symbols Nerd Font Mono", monospace')
|
||
expect(stack.toLowerCase().split('symbols').length - 1).toBe(2) // Mono + proportional
|
||
})
|
||
|
||
it('is idempotent so TerminalView\'s option diff does not thrash', () => {
|
||
const once = withIconFontFallbacks('Menlo, monospace')
|
||
expect(withIconFontFallbacks(once)).toBe(once)
|
||
})
|
||
|
||
it('splits on top-level commas only, keeping quoted and function values whole', () => {
|
||
expect(withIconFontFallbacks('"Foo,Bar", monospace')).toContain('"Foo,Bar"')
|
||
expect(withIconFontFallbacks('var(--code, monospace)')).toContain('var(--code, monospace)')
|
||
})
|
||
|
||
it('falls back to the icon list alone for an empty stack (defensive)', () => {
|
||
expect(withIconFontFallbacks('')).toBe(ICON_FONT_FALLBACKS.join(', '))
|
||
expect(withIconFontFallbacks(' , ')).toBe(ICON_FONT_FALLBACKS.join(', '))
|
||
})
|
||
})
|
||
|
||
describe('resolveTerminalFont', () => {
|
||
it('falls back to the theme code font when the family pref is empty', () => {
|
||
const { fontFamily, fontSize } = resolveTerminalFont(SIDEBAR_PREFS_DEFAULTS, 'var-theme-font')
|
||
// The theme font stays the base (first) entry; icon fonts top it up.
|
||
expect(fontFamily).toBe(withIconFontFallbacks('var-theme-font'))
|
||
expect(fontFamily.startsWith('var-theme-font')).toBe(true)
|
||
expect(fontSize).toBe(13)
|
||
})
|
||
|
||
it('falls back to the built-in monospace stack when neither the pref nor the theme provides one', () => {
|
||
expect(resolveTerminalFont(SIDEBAR_PREFS_DEFAULTS, undefined).fontFamily)
|
||
.toBe(withIconFontFallbacks(DEFAULT_TERMINAL_FONT_FAMILY))
|
||
// A whitespace-only pref counts as empty (theme default).
|
||
expect(resolveTerminalFont({ ...SIDEBAR_PREFS_DEFAULTS, terminalFontFamily: ' ' }, undefined).fontFamily)
|
||
.toBe(withIconFontFallbacks(DEFAULT_TERMINAL_FONT_FAMILY))
|
||
})
|
||
|
||
it('skips a CSS-wide keyword instead of building an invalid declaration', () => {
|
||
// Skins do set tokens to these; appending to one yields an invalid
|
||
// font-family that the CSSOM discards, losing every font at once.
|
||
for (const keyword of ['inherit', 'initial', 'unset', 'revert', 'REVERT-LAYER']) {
|
||
const { fontFamily } = resolveTerminalFont(SIDEBAR_PREFS_DEFAULTS, keyword)
|
||
expect(fontFamily).toBe(withIconFontFallbacks(DEFAULT_TERMINAL_FONT_FAMILY))
|
||
expect(fontFamily.toLowerCase()).not.toContain(keyword.toLowerCase())
|
||
}
|
||
// Same guard on the user pref, which then defers to the theme font.
|
||
expect(resolveTerminalFont({ ...SIDEBAR_PREFS_DEFAULTS, terminalFontFamily: 'inherit' }, 'var-theme-font').fontFamily)
|
||
.toBe(withIconFontFallbacks('var-theme-font'))
|
||
})
|
||
|
||
it('prefers the custom family as the base and clamps the size', () => {
|
||
const { fontFamily, fontSize } = resolveTerminalFont(
|
||
{ ...SIDEBAR_PREFS_DEFAULTS, terminalFontFamily: '"JetBrains Mono", monospace', terminalFontSize: 40 },
|
||
'var-theme-font',
|
||
)
|
||
expect(fontFamily).toBe(withIconFontFallbacks('"JetBrains Mono", monospace'))
|
||
expect(fontFamily.startsWith('"JetBrains Mono"')).toBe(true)
|
||
expect(fontFamily).not.toContain('var-theme-font')
|
||
expect(fontSize).toBe(TERMINAL_FONT_SIZE_MAX)
|
||
})
|
||
|
||
it('applies icon fallbacks no matter which base family wins', () => {
|
||
for (const resolved of [
|
||
resolveTerminalFont(SIDEBAR_PREFS_DEFAULTS, 'var-theme-font'),
|
||
resolveTerminalFont(SIDEBAR_PREFS_DEFAULTS, undefined),
|
||
resolveTerminalFont({ ...SIDEBAR_PREFS_DEFAULTS, terminalFontFamily: 'Menlo' }, undefined),
|
||
]) {
|
||
expect(resolved.fontFamily).toContain('"Symbols Nerd Font Mono"')
|
||
}
|
||
})
|
||
})
|