import assert from 'node:assert/strict' import { getFontLevel, getFontLevelSpec, initFontSize } from '../src/renderer/src/fontSize' import { buildFontSizeOverrideCSS, buildWbFontSizeTokenCSS, createFontSizeDomAdapter, getFontSizeShortcutAction } from '../src/renderer/src/workbuddy/typography/font-size' let assertions = 0 function equal(actual: T, expected: T, message: string): void { assert.equal(actual, expected, message) assertions += 1 } function includes(value: string, expected: string, message: string): void { assert.ok(value.includes(expected), message) assertions += 1 } function excludes(value: string, forbidden: string, message: string): void { assert.ok(!value.includes(forbidden), message) assertions += 1 } class FakeStyleDeclaration { private readonly entries: Array<[string, string]> constructor(values: Record) { this.entries = Object.entries(values) } get length(): number { return this.entries.length } item(index: number): string { return this.entries[index]?.[0] ?? '' } getPropertyValue(name: string): string { return this.entries.find(([key]) => key === name)?.[1] ?? '' } } function styleRule(selectorText: string, values: Record): CSSRule { return { type: 1, selectorText, style: new FakeStyleDeclaration(values) } as unknown as CSSRule } function mediaRule(conditionText: string, cssRules: CSSRule[]): CSSRule { return { type: 4, conditionText, cssRules: cssRules as unknown as CSSRuleList } as unknown as CSSRule } function stylesheet(cssRules: CSSRule[], ownerId?: string): CSSStyleSheet { return { ownerNode: ownerId ? ({ id: ownerId } as unknown as Element) : null, cssRules: cssRules as unknown as CSSRuleList } as unknown as CSSStyleSheet } function testRuleConstruction(): void { const rules = [ styleRule('.card, .dialog', { 'font-size': '14px' }), styleRule('.fallback', { 'font-size': 'var(--unresolved-size, 13.5px)' }), styleRule(':root', { '--stable-size': '18px' }), styleRule('.stable', { 'font-size': 'var(--stable-size, 14px)' }), styleRule(':root', { '--dynamic-size': 'calc(15px + var(--yc-font-offset) * 1px)' }), styleRule('.dynamic', { 'font-size': 'var(--dynamic-size, 15px)' }), styleRule('.already-dynamic', { 'font-size': 'calc(16px + var(--cb-font-size-offset))' }), styleRule('.token', { 'font-size': 'var(--yc-text-body-size)' }), styleRule('.relative', { 'font-size': '1rem' }), styleRule('.repeated', { 'font-size': '11px' }), mediaRule('(min-width: 720px)', [styleRule('.media', { 'font-size': '12px' })]), mediaRule('(min-width: 720px)', [styleRule('.media', { 'font-size': '12px' })]), mediaRule('(max-width: 500px)', [styleRule('.repeated', { 'font-size': '11px' })]) ] const css = buildFontSizeOverrideCSS([stylesheet(rules), stylesheet([styleRule('.repeated', { 'font-size': '11px' })])]) includes(css, '.card:not(.cb-font-size-fixed):not(.cb-font-size-fixed *)', 'adds the fixed-zone exclusion to the first selector') includes(css, '.dialog:not(.cb-font-size-fixed):not(.cb-font-size-fixed *)', 'splits grouped selectors before adding the fixed-zone exclusion') includes(css, 'font-size: max(8px, calc(14px + var(--cb-font-size-offset) * 1px)) !important', 'scales a literal px size with an 8px floor') includes(css, 'calc(13.5px + var(--cb-font-size-offset) * 1px)', 'uses the numeric px fallback from var() declarations') includes(css, 'calc(14px + var(--cb-font-size-offset) * 1px)', 'uses the declared static fallback without resolving the custom property') includes(css, '.dynamic:not(.cb-font-size-fixed)', 'matches the reference fallback behavior for var() declarations') includes(css, '@media (min-width: 720px)', 'keeps media rules scoped') includes(css, '@media (max-width: 500px)', 'keeps separate media conditions distinct') excludes(css, '.already-dynamic:not(', 'does not rescale an existing calc() declaration') excludes(css, '.token:not(', 'does not replace a token-only font-size declaration') excludes(css, '.relative:not(', 'does not convert relative units to px') equal((css.match(/\.repeated:not\(/g) ?? []).length, 2, 'dedupes within a CSS context but retains a different media context') equal((css.match(/\.media:not\(/g) ?? []).length, 2, 'uses a fresh dedupe set for each media rule') const inaccessible = { get cssRules(): CSSRuleList { throw new Error('cross-origin stylesheet') } } as unknown as CSSStyleSheet equal(buildFontSizeOverrideCSS([inaccessible]), '', 'skips stylesheets whose cssRules cannot be read') } function testTokenCSS(): void { const css = buildWbFontSizeTokenCSS() includes(css, '--wb-font-display-size: calc(32px + var(--wb-font-size-offset, 0) * 1px)', 'provides the display token override') includes(css, '--wb-button-icon-size-medium: calc(16px + var(--wb-font-size-offset, 0) * 1px)', 'scales button icon tokens with the same offset') includes(css, '--wb-button-height-large: calc(var(--wb-button-line-height-large) + 13px * 2)', 'derives the large button height from line height and padding') includes(css, '.cb-font-size-fixed,\n.cb-font-size-fixed *', 'creates the fixed-zone token reset') includes(css, '--wb-font-body-size: 14px', 'restores token base sizes inside fixed zones') includes(css, '--wb-button-line-height-small: 20px', 'restores button line-height tokens inside fixed zones') } class FakeStyleElement { id = '' textContent: string | null = '' readonly tagName = 'STYLE' parent: FakeHead | null = null remove(): void { this.parent?.removeChild(this) } } class FakeHead { readonly children: FakeStyleElement[] = [] appendChild(element: FakeStyleElement): FakeStyleElement { element.parent = this this.children.push(element) return element } removeChild(element: FakeStyleElement): void { const index = this.children.indexOf(element) if (index >= 0) this.children.splice(index, 1) element.parent = null } } class FakeMutationObserver { disconnected = false private readonly callback: MutationCallback constructor(callback: MutationCallback) { this.callback = callback } observe(_target: Node, _options?: MutationObserverInit): void {} disconnect(): void { this.disconnected = true } triggerAddedNode(node: Node): void { const record = { addedNodes: [node] as unknown as NodeList } this.callback([record as MutationRecord], this as unknown as MutationObserver) } } function testDOMAdapter(): void { const head = new FakeHead() const properties = new Map() const styleSheets: CSSStyleSheet[] = [stylesheet([styleRule('.live', { 'font-size': '14px' })])] const rootStyle = { setProperty: (name: string, value: string): void => properties.set(name, value), removeProperty: (name: string): void => { properties.delete(name) }, getPropertyValue: (name: string): string => properties.get(name) ?? '', get length(): number { return properties.size }, item: (index: number): string => [...properties.keys()][index] ?? '' } as unknown as CSSStyleDeclaration const documentRef = { documentElement: { style: rootStyle, dataset: {} }, styleSheets, head, body: null, getElementById: (id: string): FakeStyleElement | null => head.children.find((child) => child.id === id) ?? null, createElement: (name: string): FakeStyleElement => { assert.equal(name, 'style') return new FakeStyleElement() } } as unknown as Document const observerInstances: FakeMutationObserver[] = [] const timers = new Map void>() let timerId = 0 const windowRef = { MutationObserver: class extends FakeMutationObserver { constructor(callback: MutationCallback) { super(callback) observerInstances.push(this) } }, setTimeout: (callback: () => void): number => { timerId += 1 timers.set(timerId, callback) return timerId }, clearTimeout: (timer: number): void => { timers.delete(timer) } } as unknown as Window & typeof globalThis const adapter = createFontSizeDomAdapter(documentRef, windowRef) adapter.apply(3, '18.286px') equal(properties.get('--yc-font-offset-global'), '3', 'writes the unbounded YCode offset') equal(properties.get('--cb-font-size-offset-global'), '3', 'writes the unbounded compatibility offset') equal(properties.get('--wb-font-size-offset'), 'clamp(-1, var(--cb-font-size-offset-global, 0), 2)', 'clamps token consumers independently') equal(properties.get('--yc-md-root'), '18.286px', 'keeps the YCode markdown root') equal(properties.get('--cb-md-root'), '18.286px', 'keeps the compatibility markdown root') includes(head.children.find((child) => child.id === 'yc-font-size-override')?.textContent ?? '', '.live:not(', 'injects scanned CSS rules at nonzero levels') includes(head.children.find((child) => child.id === 'yc-wb-token-font-size-override')?.textContent ?? '', '--wb-font-body-size', 'injects WB token rules at nonzero levels') equal(observerInstances.length, 1, 'starts one observer for active global sizing') const initialOverride = documentRef.getElementById('yc-font-size-override') adapter.apply(4, '20.571px') equal(documentRef.getElementById('yc-font-size-override'), initialOverride, 'reuses scanned CSS between nonzero levels') equal(head.children.filter((child) => child.id === 'yc-wb-token-font-size-override').length, 1, 'does not duplicate token overrides') styleSheets.push(stylesheet([styleRule('.late-style', { 'font-size': '15px' })])) observerInstances[0].triggerAddedNode({ nodeType: 1, id: 'late-style-node', tagName: 'STYLE' } as unknown as Node) equal(timers.size, 1, 'debounces a newly inserted stylesheet') for (const callback of timers.values()) callback() timers.clear() includes(head.children.find((child) => child.id === 'yc-font-size-override')?.textContent ?? '', '.late-style:not(', 'rebuilds scanned overrides after a style insertion') adapter.apply(0, '17.143px') equal(documentRef.getElementById('yc-font-size-override'), null, 'removes scanned CSS overrides at zero offset') equal(documentRef.getElementById('yc-wb-token-font-size-override'), null, 'removes WB token overrides at zero offset') adapter.dispose() equal(observerInstances[0].disconnected, true, 'disconnects the observer during cleanup') } function testPublicMappingAndShortcuts(): void { equal(getFontLevelSpec(1).offset, -1, 'level one keeps the raw -1 offset') equal(getFontLevelSpec(2).offset, 0, 'level two is the zero-offset default') equal(getFontLevelSpec(5).offset, 3, 'level five retains raw +3 before consumer clamping') equal(getFontLevelSpec(6).offset, 4, 'level six retains raw +4 before consumer clamping') equal(getFontSizeShortcutAction({ key: '-', code: 'Minus', shiftKey: false }), 'decrease', 'maps the minus key') equal(getFontSizeShortcutAction({ key: 'Unidentified', code: 'Equal', shiftKey: true }), 'increase', 'maps the Equal code fallback') equal(getFontSizeShortcutAction({ key: 'Unidentified', code: 'Digit0', shiftKey: false }), 'reset', 'maps the Digit0 code fallback') equal(getFontSizeShortcutAction({ key: '-', code: 'Minus', shiftKey: true }), null, 'does not treat shifted minus as decrease') equal(getFontSizeShortcutAction({ key: '0', code: 'Digit0', shiftKey: true }), null, 'does not treat shifted zero as reset') } function testKeyboardIntegration(): void { const descriptors = new Map() const originalGlobal = globalThis as unknown as Record const styleValues = new Map() const head = new FakeHead() const listeners = new Map() const eventNames: string[] = [] const timeoutCallbacks = new Map void>() let nextTimerId = 0 const documentRef = { documentElement: { style: { setProperty: (name: string, value: string): void => styleValues.set(name, value), removeProperty: (name: string): void => { styleValues.delete(name) }, getPropertyValue: (name: string): string => styleValues.get(name) ?? '', get length(): number { return styleValues.size }, item: (index: number): string => [...styleValues.keys()][index] ?? '' }, dataset: {} }, styleSheets: [], head, body: null, getElementById: (id: string): FakeStyleElement | null => head.children.find((child) => child.id === id) ?? null, createElement: (): FakeStyleElement => new FakeStyleElement(), addEventListener: (type: string, listener: EventListener, options?: boolean | AddEventListenerOptions): void => { listeners.set(type, { listener, capture: options === true || (typeof options === 'object' && options.capture === true) }) }, removeEventListener: (type: string): void => { listeners.delete(type) } } as unknown as Document class TestCustomEvent { readonly type: string readonly detail: unknown constructor(type: string, init?: { detail?: unknown }) { this.type = type this.detail = init?.detail } } const windowRef = { MutationObserver: FakeMutationObserver, setTimeout: (callback: () => void): number => { nextTimerId += 1 timeoutCallbacks.set(nextTimerId, callback) return nextTimerId }, clearTimeout: (id: number): void => { timeoutCallbacks.delete(id) }, dispatchEvent: (event: Event): boolean => { eventNames.push((event as unknown as TestCustomEvent).type) return true } } as unknown as Window & typeof globalThis const storage = { value: '2', getItem(_key: string): string | null { return this.value }, setItem(_key: string, value: string): void { this.value = value } } const globals: Record = { document: documentRef, window: windowRef, localStorage: storage, CustomEvent: TestCustomEvent } for (const [key, value] of Object.entries(globals)) { descriptors.set(key, Object.getOwnPropertyDescriptor(globalThis, key)) Object.defineProperty(originalGlobal, key, { value, configurable: true, writable: true }) } try { const cleanup = initFontSize() equal(listeners.get('keydown')?.capture, true, 'registers shortcuts in capture phase') const listener = listeners.get('keydown')?.listener assert.ok(listener, 'registers a keydown listener') assertions += 1 const sendKey = (key: string, code: string, options: { ctrl?: boolean; meta?: boolean; alt?: boolean; shift?: boolean; target?: EventTarget | null } = {}): { prevented: boolean } => { let prevented = false listener!({ key, code, ctrlKey: options.ctrl ?? true, metaKey: options.meta ?? false, altKey: options.alt ?? false, shiftKey: options.shift ?? false, target: options.target ?? null, preventDefault: () => { prevented = true } } as unknown as KeyboardEvent) return { prevented } } const focusedComposer = { tagName: 'TEXTAREA' } as unknown as EventTarget equal(sendKey('+', 'Equal', { target: focusedComposer }).prevented, true, 'keeps shortcuts available while a composer input is focused') equal(getFontLevel(), 3, 'increases the current level from the key handler') equal(storage.value, '3', 'persists the selected YCode level') equal(sendKey('-', 'Minus', { alt: true }).prevented, false, 'ignores Alt-modified shortcut combinations') equal(getFontLevel(), 3, 'Alt-modified input leaves the current level unchanged') equal(sendKey('Unidentified', 'Minus').prevented, true, 'uses the physical Minus code when key is unavailable') equal(getFontLevel(), 2, 'decreases the current level from the code fallback') equal(eventNames.filter((name) => name === 'ycode:font-level-change').length, 2, 'preserves the YCode public event') for (const callback of timeoutCallbacks.values()) callback() timeoutCallbacks.clear() equal(eventNames.filter((name) => name === 'cb-font-size-change').length, 1, 'dispatches the compatible offset event after coalescing changes') equal(eventNames.filter((name) => name === 'cb-font-size-level-change').length, 1, 'dispatches the compatible level event after coalescing changes') cleanup() equal(listeners.has('keydown'), false, 'removes the keydown listener during cleanup') } finally { for (const [key, descriptor] of descriptors) { if (descriptor) Object.defineProperty(originalGlobal, key, descriptor) else delete originalGlobal[key] } } } testRuleConstruction() testTokenCSS() testDOMAdapter() testPublicMappingAndShortcuts() testKeyboardIntegration() console.log(`Font-size regression passed (${assertions} assertions).`)