test(client): close the merged-architecture coverage and jsdom gaps

- user-text.tsx enters ui-primitives' per-file 100% gate: replace three
  regex-guaranteed impossible ?? fallbacks with asserted captures, make
  the precedence sort a branch-free rank comparator, and pin the nested
  recall-label ordering and the no-basename quoted-path fallback with
  tests (100% statements/branches/functions locally).
- assembled-boot stubs Range.prototype.getBoundingClientRect: jsdom has
  no Range geometry, and Lexical's selection reveal now reaches it in the
  built-graph lane after the architecture merge (the unhandled TypeError
  behind command-image-envelope and preview-boot).
This commit is contained in:
Yichen Jiang
2026-08-24 11:39:34 +08:00
parent 5f94875a3d
commit 04caa1248d
3 changed files with 25 additions and 5 deletions
+7
View File
@@ -159,6 +159,13 @@ export function installAssembledBootEnv(): void {
if (typeof Element.prototype.scrollIntoView !== 'function') {
Element.prototype.scrollIntoView = () => {}
}
// jsdom implements no Range geometry either: Lexical's selection reveal
// measures the caret with one after a programmatic edit settles focus.
if (typeof Range.prototype.getBoundingClientRect !== 'function') {
Range.prototype.getBoundingClientRect = () => ({
top: 0, bottom: 0, left: 0, right: 0, width: 0, height: 0, x: 0, y: 0, toJSON: () => ({}),
}) as DOMRect
}
beforeEach(() => {
localStorage.clear()
// The locale service derives its provisional locale from the browser and
@@ -41,7 +41,7 @@ export function projectUserText(text: string, sessionLabels: readonly string[]):
end: wire.index + wire[0].length,
label: wire[0],
kind: 'session',
display: wire[1] ?? '',
display: wire[1] as string, // non-optional capture in SESSION_WIRE_RE
})
}
for (const rawLabel of [...new Set(sessionLabels)].sort((a, b) => b.length - a.length)) {
@@ -55,16 +55,16 @@ export function projectUserText(text: string, sessionLabels: readonly string[]):
const re = /(^|\s)(\/[\w-]+|@"[^"\n]+"|@[^\s]+)/gu
let m: RegExpExecArray | null
while ((m = re.exec(text)) !== null) {
const tokenStart = m.index + (m[1]?.length ?? 0)
const rawLabel = m[2] ?? ''
const tokenStart = m.index + (m[1] as string).length // (^|\s) captures '' at line start
const rawLabel = m[2] as string // non-optional alternation capture
const label = rawLabel.startsWith('@"')
? rawLabel
: rawLabel.replace(/[.,;:!?]+$/gu, '')
if (label.length <= 1) continue
ranges.push({ start: tokenStart, end: tokenStart + label.length, label, kind: 'plain' })
}
ranges.sort((a, b) => a.start - b.start
|| (a.kind === b.kind ? b.end - a.end : a.kind === 'session' ? -1 : 1))
const rankOf = (range: DecorationRange): number => range.kind === 'session' ? 0 : 1
ranges.sort((a, b) => a.start - b.start || rankOf(a) - rankOf(b) || b.end - a.end)
const parts: ReactNode[] = []
let cursor = 0
const pushPlain = (from: number, to: number): void => {
@@ -62,6 +62,19 @@ describe('projectUserText', () => {
expect(host.textContent).toBe('用 /plan。 试试 @。')
})
it('prefers the longer recall label when one nests inside another', () => {
const host = project('@会话一 收尾', ['会话', '会话一'])
const chips = [...host.querySelectorAll('[data-ref-chip="session"]')]
expect(chips.map(c => c.textContent)).toEqual(['会话一'])
expect(host.textContent).toBe('会话一 收尾')
})
it('falls back to the raw quoted label when the path has no basename', () => {
const host = project('看 @"/" 下面')
const chip = host.querySelector('[data-ref-chip="file"]')!
expect(chip.textContent).toBe('"/"')
})
it('renders undecorated text as one inline run', () => {
const host = project('纯文本,无引用')
expect(host.querySelectorAll('div').length).toBe(0)