Files
dsh-context/tests/client/components/richText.spec.ts
T
Bowen Liang 4d144147a0 refactor(client): import React members directly, dropping the central react module
Every client file reached hooks and element types through the central
require seam (src/client/react.ts) and spelled them React.useState /
ReactNS.ReactElement. Import the members directly instead — named value
imports for hooks, inline type-only imports for element types — and let
the bundler own the single require('react'), which is exactly what the
platform module table answers.

This needs JSX off the classic runtime (it required a React identifier
in every tsx scope): tsconfig moves to jsx: react-jsx, so the bundle
sources jsx from 'react/jsx-runtime', already seeded by every supported
harness (verified against the dsh-v0.1.2-rc.1 platform.ts). The bundle
smoke's fake module table gains the specifier to stay a faithful shell
stand-in. h() survives as a local createElement alias at the slot
render-prop seams and the lightbox portal imports createPortal from
react-dom directly.

Verified: the built client.js requires react, react/jsx-runtime, and
react-dom exactly once each; the compat lane (bundle smoke + the
real-baseline matrix) passes against the fresh artifacts; the full gate
stays green with per-file coverage at 100%.
2026-09-07 08:59:03 +08:00

98 lines
4.2 KiB
TypeScript

// RichText/RichSwitch/useRichMode (src/client/components/richText.tsx):
// markdown mode renders through the REAL shared MarkdownText; raw mode is an
// exact-text line-numbered <pre>; the switch drives the mode hook.
import { createElement as h } from 'react'
import assert from 'node:assert/strict'
import { describe, test } from 'vitest'
import { makeRichText } from '../../../src/client/components/richText'
import type { RichMode } from '../../../src/client/components/richText'
import { click, makeKit, mount, query, queryAll } from '../helpers/kit'
const kit = makeKit()
const { RichText, RichSwitch, useRichMode } = makeRichText(kit)
const SAMPLE = '# Title\n\nsome **bold** text'
/** The raw body re-joined from its rendered line spans, i.e. the exact source text. */
function rawText(container: ParentNode): string {
return queryAll(container, '.lc-ts-line').map(line => line.textContent).join('\n')
}
/** Harness wiring the hook to the switch and the body, like the detail cards do. */
function Harness(props: { text: string }) {
const [mode, setMode] = useRichMode()
return h('div', {}, h(RichSwitch, { mode, onPick: setMode }), h(RichText, { text: props.text, mode }))
}
describe('RichText', () => {
test('md mode renders real markdown through the shared MarkdownText', async () => {
const m = await mount(h(RichText, { text: SAMPLE, mode: 'md' }))
const box = query(m.container, '.lc-ts-desc-md')
assert.equal(query(box, 'h1').textContent, 'Title')
assert.equal(query(box, 'strong').textContent, 'bold')
await m.unmount()
})
test('raw mode renders the exact source as one numbered line span per source line', async () => {
const m = await mount(h(RichText, { text: SAMPLE, mode: 'raw' }))
const pre = query(m.container, 'pre.lc-ts-desc-body')
assert.equal(rawText(m.container), SAMPLE)
assert.equal(queryAll(pre, '.lc-ts-line').length, 3)
assert.equal(queryAll(m.container, 'h1').length, 0)
await m.unmount()
})
test('raw mode drops the row of a trailing newline but keeps interior empty lines', async () => {
const m = await mount(h(RichText, { text: 'a\n\nb\n', mode: 'raw' }))
assert.deepEqual(queryAll(m.container, '.lc-ts-line').map(line => line.textContent), ['a', '', 'b'])
await m.unmount()
})
test('raw mode of empty text keeps a single empty line', async () => {
const m = await mount(h(RichText, { text: '', mode: 'raw' }))
assert.deepEqual(queryAll(m.container, '.lc-ts-line').map(line => line.textContent), [''])
await m.unmount()
})
})
describe('RichSwitch', () => {
test('two segments with titles; the active mode carries the on class; clicks report the pick', async () => {
const picks: RichMode[] = []
const m = await mount(h(RichSwitch, { mode: 'md', onPick: m2 => picks.push(m2) }))
const buttons = queryAll(m.container, '.lc-rich-seg-btn')
assert.equal(buttons.length, 2)
assert.equal(buttons[0].textContent, 'Raw')
assert.equal(buttons[0].getAttribute('title'), 'View Raw Text')
assert.ok(!buttons[0].className.includes('lc-rich-seg-on'))
assert.equal(buttons[1].textContent, 'Markdown')
assert.equal(buttons[1].getAttribute('title'), 'View as Markdown')
assert.ok(buttons[1].className.includes('lc-rich-seg-on'))
await click(buttons[0])
await click(buttons[1])
assert.deepEqual(picks, ['raw', 'md'])
await m.unmount()
})
test('raw mode marks the raw segment active instead', async () => {
const m = await mount(h(RichSwitch, { mode: 'raw', onPick: () => {} }))
const buttons = queryAll(m.container, '.lc-rich-seg-btn')
assert.ok(buttons[0].className.includes('lc-rich-seg-on'))
assert.ok(!buttons[1].className.includes('lc-rich-seg-on'))
await m.unmount()
})
})
describe('useRichMode', () => {
test('defaults to markdown and flips to raw via the switch', async () => {
const m = await mount(h(Harness, { text: SAMPLE }))
assert.ok(query(m.container, '.lc-ts-desc-md'))
const buttons = queryAll(m.container, '.lc-rich-seg-btn')
await click(buttons[0]) // Raw
assert.equal(rawText(m.container), SAMPLE)
await click(queryAll(m.container, '.lc-rich-seg-btn')[1]) // back to Markdown
assert.equal(query(m.container, '.lc-ts-desc-md h1').textContent, 'Title')
await m.unmount()
})
})