chore: establish YCode replication baseline and TODO
This commit is contained in:
commit
6dc59f89ba
1118 files changed
+196002
No files matched your search
@@ -0,0 +1,395 @@
|
||||
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<T>(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<string, string>) {
|
||||
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<string, string>): 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<string, string>()
|
||||
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<number, () => 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<string, PropertyDescriptor | undefined>()
|
||||
const originalGlobal = globalThis as unknown as Record<string, unknown>
|
||||
const styleValues = new Map<string, string>()
|
||||
const head = new FakeHead()
|
||||
const listeners = new Map<string, { listener: EventListener; capture: boolean }>()
|
||||
const eventNames: string[] = []
|
||||
const timeoutCallbacks = new Map<number, () => 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<string, unknown> = {
|
||||
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).`)
|
||||
Reference in new issue
Block a user