Files
apes-sidebar/tests/side-card-section-rows.spec.tsx
T
Menghuan1918 d83490c93e feat: 终端二级设置支持自定义字体(字体族 + 9–32px 字号,实时生效)
- SidebarPrefs 新增 terminalFontFamily(空 = 跟随主题等宽字体)/ terminalFontSize(9–32,默认 13),PrefsSchema 与 parsePrefs 同步校验
- 声明式设置 API 扩展:SidebarSettingToggle 支持 type: 'switch' | 'text' | 'number'(缺省 switch,向后兼容)+ min/max/placeholder/unit
- 终端卡片齿轮弹窗新增「终端字体」「终端字号」两行:blur/Enter 提交、number 钳制、提交失败回滚草稿
- TerminalView 经 store 订阅实时应用字体(options + re-fit + resize),懒加载 chunk 无新增依赖
- 新增 resolveTerminalFont 纯函数(回退链:用户字体 → 主题 token → 内置等宽栈)并单测
- 版本 0.11.0,README/AGENTS.md/设计文档同步
2026-08-14 14:36:49 +08:00

176 lines
6.5 KiB
TypeScript

/**
* Interactive tests for the settings popup's text/number rows: the draft is
* local state committed on blur through the parent's onCommit; the parent's
* canonical return is adopted (clamped numbers, stored value for invalid
* input), and the row remounts when the committed pref value changes (a
* failed commit reverts prefs → the stored value reappears).
*
* Rendered with createRoot + act() in jsdom (the SSR specs stay in
* side-card-section.spec.tsx; this file exercises the event paths).
*/
// @vitest-environment jsdom
import { describe, expect, it } from 'vitest'
import { createElement, type ReactNode } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { act } from 'react-dom/test-utils'
// The act() environment flag (React 18.2 reads it before flushing effects).
;(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true
import type { SidebarSettingToggle } from '../src/client/service.ts'
import { FeatureSettingsRows } from '../src/client/SideCardSection.tsx'
import { SIDEBAR_PREFS_DEFAULTS } from '../src/prefs-shared.ts'
/** Render the rows into a detached container under React's act(). */
function mount(node: ReactNode): { container: HTMLDivElement; rerender: (node: ReactNode) => void; unmount: () => void } {
const container = document.createElement('div')
document.body.append(container)
const root: Root = createRoot(container)
act(() => { root.render(node) })
return {
container,
rerender: (next) => { act(() => { root.render(next) }) },
unmount: () => {
act(() => { root.unmount() })
container.remove()
},
}
}
/** Type into an input and commit it via blur (React 18: input event +
* focusout). The native setter bypasses React's value tracker so the
* change is actually seen. */
function typeAndBlur(input: HTMLInputElement, value: string): void {
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')!.set!
act(() => {
setter.call(input, value)
input.dispatchEvent(new Event('input', { bubbles: true }))
input.dispatchEvent(new FocusEvent('focusout', { bubbles: true }))
})
}
const prefs = { ...SIDEBAR_PREFS_DEFAULTS }
describe('FeatureSettingsRows typed rows (interactive)', () => {
it('commits the raw text on blur and adopts the canonical return', () => {
const commits: Array<[string, string]> = []
const toggle: SidebarSettingToggle = {
key: 'terminalFontFamily',
type: 'text',
title: () => 'Font family',
}
const { container, unmount } = mount(createElement(FeatureSettingsRows, {
toggles: [toggle],
prefs,
onToggle: () => {},
onCommit: (t, raw) => {
commits.push([t.key, raw])
return raw
},
}))
const input = container.querySelector('input')!
typeAndBlur(input, 'Monaco')
expect(commits).toEqual([['terminalFontFamily', 'Monaco']])
// The canonical return is adopted into the draft.
expect(input.value).toBe('Monaco')
unmount()
})
it('clamps numbers into the declared bounds on commit', () => {
const commits: Array<[string, number]> = []
const toggle: SidebarSettingToggle = {
key: 'terminalFontSize',
type: 'number',
title: () => 'Font size',
min: 9,
max: 32,
}
const { container, unmount } = mount(createElement(FeatureSettingsRows, {
toggles: [toggle],
prefs: { ...prefs, terminalFontSize: 13 },
onToggle: () => {},
onCommit: (t, raw) => {
const parsed = Number(raw)
const clamped = Math.min(32, Math.max(9, Math.round(parsed)))
commits.push([t.key, clamped])
return String(clamped)
},
}))
const input = container.querySelector('input')!
typeAndBlur(input, '40')
expect(commits).toEqual([['terminalFontSize', 32]])
expect(input.value).toBe('32')
unmount()
})
it('clamps an emptied number input to the lower bound on commit (width-row precedent)', () => {
const commits: Array<[string, number]> = []
const toggle: SidebarSettingToggle = {
key: 'terminalFontSize',
type: 'number',
title: () => 'Font size',
min: 9,
max: 32,
}
const { container, unmount } = mount(createElement(FeatureSettingsRows, {
toggles: [toggle],
prefs: { ...prefs, terminalFontSize: 13 },
onToggle: () => {},
// The parent mirrors the real handler: an emptied number parses to 0
// and clamps into the bounds (a browser number input never holds a
// non-numeric string — the draft can only be empty or numeric).
onCommit: (t, raw) => {
const clamped = Math.min(32, Math.max(9, Math.round(Number(raw))))
commits.push([t.key, clamped])
return String(clamped)
},
}))
const input = container.querySelector('input')!
typeAndBlur(input, '')
expect(commits).toEqual([['terminalFontSize', 9]])
expect(input.value).toBe('9')
unmount()
})
it('reverts the draft to the committed value after a failed commit reverts prefs', () => {
const toggle: SidebarSettingToggle = {
key: 'terminalFontFamily',
type: 'text',
title: () => 'Font family',
}
// The parent mirrors the real handler: the optimistic commit adopts the
// typed value, and a FAILED write reverts prefs to the stored one.
let prefsNow = { ...prefs, terminalFontFamily: 'Old' }
const { container, rerender, unmount } = mount(createElement(FeatureSettingsRows, {
toggles: [toggle],
prefs: prefsNow,
onToggle: () => {},
onCommit: (_t, raw) => raw,
}))
const input = container.querySelector('input')!
typeAndBlur(input, 'New')
// Optimistic commit: the draft adopts the typed value.
expect(input.value).toBe('New')
// The optimistic pref lands (parent state): the key changes, the row
// remounts with the same value — no visible reset while editing.
prefsNow = { ...prefsNow, terminalFontFamily: 'New' }
rerender(createElement(FeatureSettingsRows, {
toggles: [toggle],
prefs: prefsNow,
onToggle: () => {},
onCommit: (_t, raw) => raw,
}))
expect(container.querySelector('input')!.value).toBe('New')
// The write fails: prefs revert to the stored value and the row
// remounts with it (the stale draft is gone).
prefsNow = { ...prefsNow, terminalFontFamily: 'Old' }
rerender(createElement(FeatureSettingsRows, {
toggles: [toggle],
prefs: prefsNow,
onToggle: () => {},
onCommit: (_t, raw) => raw,
}))
expect(container.querySelector('input')!.value).toBe('Old')
unmount()
})
})