test(specs): 收敛 28 个组件 spec 的渲染样板到共享 test-utils
- 7 个 spec 的本地 mount()(createRoot + act 容器循环)替换为 renderRoot():sidechat-view、subagent-jobs-view、subagent-live-polling、 lazy-chunk、side-card-section-rows(本地 mount 删除,调用点改名); editor-refresh、markdown-manual-refresh(EditorHost 包装 mount 改为 委托 renderRoot,调用点不动) - 24 个 spec 的内联 IS_REACT_ACT_ENVIRONMENT 赋值替换为 setupReactAct() (subagent-jobs-view / subagent-live-polling / lazy-chunk 原本未设 flag,保持原样不引入) 全部为语义相同的样板替换:迁移后全量 118 files / 1241 passed + 9 skipped,与迁移前一致。
This commit is contained in:
@@ -26,7 +26,8 @@ import { createBetterSidebarService } from '../src/client/service.ts'
|
||||
import { createSidebarStore } from '../src/client/state.ts'
|
||||
|
||||
// The act() environment flag (React 18.2 reads it before flushing effects).
|
||||
;(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true
|
||||
import { setupReactAct } from './test-utils.ts'
|
||||
setupReactAct()
|
||||
|
||||
const officeEntry = builtinViewerPlugins[0] as PluginEntry
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ 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 { setupReactAct } from './test-utils.ts'
|
||||
setupReactAct()
|
||||
|
||||
import { Sidebar } from '../src/client/Sidebar.tsx'
|
||||
import { allLeaves, createSidebarStore, toggleBottomPanel, type SidebarStore } from '../src/client/state.ts'
|
||||
|
||||
@@ -16,7 +16,8 @@ import { api, type GitStatusResult, type GitWorktree } from '../src/client/api.t
|
||||
import type { Context } from '../src/context-types.ts'
|
||||
import type { SidebarTab } from '../src/client/state.ts'
|
||||
|
||||
;(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true
|
||||
import { setupReactAct } from './test-utils.ts'
|
||||
setupReactAct()
|
||||
|
||||
const MAIN = 'C:/repo/main'
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ import { createRoot, type Root } from 'react-dom/client'
|
||||
import { act } from 'react-dom/test-utils'
|
||||
import { DiffFiles } from '../src/client/diff/DiffFiles.tsx'
|
||||
|
||||
;(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true
|
||||
import { setupReactAct } from './test-utils.ts'
|
||||
setupReactAct()
|
||||
|
||||
const diff = [
|
||||
'diff --git a/src/a.ts b/src/a.ts',
|
||||
|
||||
@@ -19,7 +19,8 @@ import { createBetterSidebarService } from '../src/client/service.ts'
|
||||
import { allLeaves, createSidebarStore, type SidebarTab } from '../src/client/state.ts'
|
||||
|
||||
// The act() environment flag (React 18.2 reads it before flushing effects).
|
||||
;(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true
|
||||
import { setupReactAct } from './test-utils.ts'
|
||||
setupReactAct()
|
||||
|
||||
/** A store with the seeded editor-home tab (default prefs: separate mode;
|
||||
* merged-mode scenarios re-enable editorExplorer explicitly). */
|
||||
|
||||
@@ -8,15 +8,15 @@
|
||||
// @vitest-environment jsdom
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createElement, useEffect, useState } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { act } from 'react-dom/test-utils'
|
||||
import { renderRoot, setupReactAct } from './test-utils.ts'
|
||||
import type { Context } from '../src/context-types.ts'
|
||||
import { EditorHost } from '../src/client/EditorHost.tsx'
|
||||
import { createBetterSidebarService, type FileViewerProps } from '../src/client/service.ts'
|
||||
import { allLeaves, createSidebarStore, type SidebarTab } from '../src/client/state.ts'
|
||||
|
||||
// The act() environment flag (React 18.2 reads it before flushing effects).
|
||||
;(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true
|
||||
setupReactAct()
|
||||
|
||||
const fsRead = vi.fn()
|
||||
vi.mock('../src/client/api.ts', () => ({
|
||||
@@ -90,22 +90,10 @@ function setup(initialMode: 'preview' | 'edit' = 'preview', dirty = false): {
|
||||
}
|
||||
|
||||
function mount(ctx: Context, tab: () => SidebarTab): { container: HTMLDivElement; unmount: () => void } {
|
||||
const container = document.createElement('div')
|
||||
document.body.append(container)
|
||||
const root = createRoot(container)
|
||||
act(() => {
|
||||
root.render(createElement(EditorHost, {
|
||||
ctx, store: ctx.betterSidebar as never, scope: { sessionId: 'editor-home-session' },
|
||||
tab: tab(), expanded: [], revealed: [], onToggleDir: () => {}, onReferenceFile: () => {},
|
||||
}))
|
||||
})
|
||||
return {
|
||||
container,
|
||||
unmount: () => {
|
||||
act(() => { root.unmount() })
|
||||
container.remove()
|
||||
},
|
||||
}
|
||||
return renderRoot(createElement(EditorHost, {
|
||||
ctx, store: ctx.betterSidebar as never, scope: { sessionId: 'editor-home-session' },
|
||||
tab: tab(), expanded: [], revealed: [], onToggleDir: () => {}, onReferenceFile: () => {},
|
||||
}))
|
||||
}
|
||||
|
||||
/** The header's refresh button (locale-dependent aria-label), or null. */
|
||||
|
||||
@@ -19,7 +19,8 @@ import { TAB_DRAG_TYPE } from '../src/client/TabBar.tsx'
|
||||
import type { UploadItem } from '../src/client/upload.ts'
|
||||
|
||||
// The act() environment flag (React 18.2 reads it before flushing effects).
|
||||
;(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true
|
||||
import { setupReactAct } from './test-utils.ts'
|
||||
setupReactAct()
|
||||
|
||||
// vitest 4.1.11+ follows the OS locale; pin en-US so hint assertions are
|
||||
// deterministic regardless of the developer machine.
|
||||
|
||||
@@ -15,7 +15,8 @@ import { createSidebarStore } from '../src/client/state.ts'
|
||||
import type { OpenWithTarget } from '../src/client/open-with.ts'
|
||||
|
||||
// The act() environment flag (React 18.2 reads it before flushing effects).
|
||||
;(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true
|
||||
import { setupReactAct } from './test-utils.ts'
|
||||
setupReactAct()
|
||||
|
||||
// vitest 4.1.11+ follows the OS locale; pin en-US so menu copy is English.
|
||||
beforeAll(() => {
|
||||
|
||||
@@ -16,7 +16,8 @@ import { act } from 'react-dom/test-utils'
|
||||
import { FileTree } from '../src/client/FileTree.tsx'
|
||||
import { createSidebarStore } from '../src/client/state.ts'
|
||||
|
||||
;(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true
|
||||
import { setupReactAct } from './test-utils.ts'
|
||||
setupReactAct()
|
||||
|
||||
// jsdom defines no scrollIntoView; counting calls needs a real function.
|
||||
const scrollIntoView = vi.fn()
|
||||
|
||||
@@ -20,7 +20,8 @@ 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 { setupReactAct } from './test-utils.ts'
|
||||
setupReactAct()
|
||||
|
||||
import { Sidebar } from '../src/client/Sidebar.tsx'
|
||||
import { createSidebarStore, togglePanel, type SidebarStore, floatTab as floatTabReducer } from '../src/client/state.ts'
|
||||
|
||||
@@ -13,7 +13,8 @@ import { GitLens } from '../src/client/changes/GitLens.tsx'
|
||||
import { createSidebarStore } from '../src/client/state.ts'
|
||||
import { api, type GitLogEntry, type GitStatusResult, type GitWorktree } from '../src/client/api.ts'
|
||||
|
||||
;(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true
|
||||
import { setupReactAct } from './test-utils.ts'
|
||||
setupReactAct()
|
||||
|
||||
const MAIN = 'C:/repo/main'
|
||||
const AGENT = 'C:/repo/agent'
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { createElement, type ComponentType, type ReactNode } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { act } from 'react-dom/test-utils'
|
||||
import { renderRoot } from './test-utils.ts'
|
||||
import { builtinTabs } from '../src/client/builtins/tabs.tsx'
|
||||
import { builtinViewers } from '../src/client/builtins/viewers.tsx'
|
||||
import { registerChunkForTests, resetChunks } from '../src/client/chunk-loader.ts'
|
||||
@@ -21,21 +21,6 @@ import type { Context } from '../src/context-types.ts'
|
||||
import type { FileViewerProps, TabComponentProps } from '../src/client/service.ts'
|
||||
import css from '../src/client/sidebar.module.css'
|
||||
|
||||
/** Render `node` into a detached body container under React's act(). */
|
||||
function mount(node: ReactNode): { container: HTMLDivElement; unmount: () => void } {
|
||||
const container = document.createElement('div')
|
||||
document.body.append(container)
|
||||
const root: Root = createRoot(container)
|
||||
// act() flushes the (concurrent) initial commit — the placeholder must be
|
||||
// in the DOM before the async load flushes below.
|
||||
act(() => { root.render(node) })
|
||||
const unmount = (): void => {
|
||||
act(() => { root.unmount() })
|
||||
container.remove()
|
||||
}
|
||||
return { container, unmount }
|
||||
}
|
||||
|
||||
const Marker = (): ReactNode => createElement('div', { 'data-testid': 'chunk-rendered' }, 'loaded')
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -55,7 +40,7 @@ describe('lazyChunkComponent', () => {
|
||||
return { TextEditor: Marker }
|
||||
})
|
||||
const Wrapper = lazyChunkComponent<{ label: string }>('editor', (mod) => mod.TextEditor as ComponentType<{ label: string }> | undefined)
|
||||
const { container, unmount } = mount(createElement(Wrapper, { label: 'x' }))
|
||||
const { container, unmount } = renderRoot(createElement(Wrapper, { label: 'x' }))
|
||||
// Initial paint: the loading placeholder (no chunk loaded yet).
|
||||
expect(container.querySelector(`.${css.editorPlaceholder}`)).not.toBeNull()
|
||||
await act(async () => {})
|
||||
@@ -72,7 +57,7 @@ describe('lazyChunkComponent', () => {
|
||||
return { TextEditor: Marker }
|
||||
})
|
||||
const Wrapper = lazyChunkComponent<Record<string, never>>('editor', (mod) => mod.TextEditor as ComponentType<Record<string, never>> | undefined)
|
||||
const { container, unmount } = mount(createElement(Wrapper, {}))
|
||||
const { container, unmount } = renderRoot(createElement(Wrapper, {}))
|
||||
await act(async () => {})
|
||||
expect(container.textContent).toContain('boom')
|
||||
// The failed load cleared the loader cache; retry now succeeds.
|
||||
@@ -89,7 +74,7 @@ describe('lazyChunkComponent', () => {
|
||||
const Recorder = (props: { label: string }): ReactNode => createElement('div', { 'data-testid': 'rec', 'data-label': props.label })
|
||||
registerChunkForTests('terminal', async () => ({ TerminalView: Recorder }))
|
||||
const Wrapper = lazyChunkComponent<{ label: string }>('terminal', (mod) => mod.TerminalView as ComponentType<{ label: string }> | undefined)
|
||||
const { container, unmount } = mount(createElement(Wrapper, { label: 'hello' }))
|
||||
const { container, unmount } = renderRoot(createElement(Wrapper, { label: 'hello' }))
|
||||
await act(async () => {})
|
||||
expect(container.querySelector('[data-testid="rec"]')?.getAttribute('data-label')).toBe('hello')
|
||||
unmount()
|
||||
@@ -123,7 +108,7 @@ describe('built-in descriptor contract (render-prop functions)', () => {
|
||||
// load fails and the wrapper must degrade gracefully, never throw.
|
||||
const viewers = builtinViewers()
|
||||
const code = viewers.find(viewer => viewer.id === 'code')!
|
||||
const { container, unmount } = mount(createElement(code.component, {} as FileViewerProps))
|
||||
const { container, unmount } = renderRoot(createElement(code.component, {} as FileViewerProps))
|
||||
await act(async () => {})
|
||||
expect(container.querySelector(`.${css.editorError}`)).not.toBeNull()
|
||||
expect(container.querySelector('button')).not.toBeNull()
|
||||
@@ -135,7 +120,7 @@ describe('built-in descriptor contract (render-prop functions)', () => {
|
||||
const viewers = builtinViewers()
|
||||
const markdown = viewers.find(viewer => viewer.id === 'markdown')!
|
||||
// EditorHost renders viewer components via createElement(component, props).
|
||||
const { container, unmount } = mount(createElement(markdown.component, {
|
||||
const { container, unmount } = renderRoot(createElement(markdown.component, {
|
||||
ctx: {},
|
||||
store: undefined,
|
||||
scope: { sessionId: 's1', cwd: '/p' },
|
||||
@@ -165,7 +150,7 @@ describe('built-in descriptor contract (render-prop functions)', () => {
|
||||
tab: { id: 'terminal:2', type: 'terminal', title: '终端 2' },
|
||||
visible: true,
|
||||
} as unknown as TabComponentProps
|
||||
const { container, unmount } = mount(createElement(terminal.component, props))
|
||||
const { container, unmount } = renderRoot(createElement(terminal.component, props))
|
||||
await act(async () => {})
|
||||
expect(container.querySelector('[data-testid="terminal-tabid"]')?.textContent).toBe('terminal:2')
|
||||
expect(received).toBe('terminal:2')
|
||||
@@ -181,11 +166,11 @@ describe('built-in descriptor contract (render-prop functions)', () => {
|
||||
const tabs = builtinTabs({} as Context)
|
||||
const terminal = tabs.find(tab => tab.id === 'terminal')!
|
||||
const props = { tab: { id: 'terminal:1', type: 'terminal', title: '终端 1' } } as unknown as TabComponentProps
|
||||
const { container: first, unmount: unmountFirst } = mount(createElement(terminal.component, props))
|
||||
const { container: first, unmount: unmountFirst } = renderRoot(createElement(terminal.component, props))
|
||||
await act(async () => {})
|
||||
expect(first.querySelector('[data-testid="chunk-rendered"]')).not.toBeNull()
|
||||
unmountFirst()
|
||||
const { container: second, unmount: unmountSecond } = mount(createElement(terminal.component, props))
|
||||
const { container: second, unmount: unmountSecond } = renderRoot(createElement(terminal.component, props))
|
||||
await act(async () => {})
|
||||
expect(second.querySelector('[data-testid="chunk-rendered"]')).not.toBeNull()
|
||||
unmountSecond()
|
||||
|
||||
@@ -15,7 +15,8 @@ import { MarkdownDocument, type MarkdownHtmlMedia } from '../src/client/Markdown
|
||||
import { analyzeMarkdownHtml } from '../src/client/markdown-html.ts'
|
||||
|
||||
// The act() environment flag (React 18.2 reads it before flushing effects).
|
||||
;(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true
|
||||
import { setupReactAct } from './test-utils.ts'
|
||||
setupReactAct()
|
||||
|
||||
const media: MarkdownHtmlMedia = {
|
||||
scope: { sessionId: 's1', cwd: '/ws' },
|
||||
|
||||
@@ -5,15 +5,15 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createElement, useEffect } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { act } from 'react-dom/test-utils'
|
||||
import { renderRoot, setupReactAct } from './test-utils.ts'
|
||||
import type { Context } from '../src/context-types.ts'
|
||||
import { EditorHost } from '../src/client/EditorHost.tsx'
|
||||
import { api } from '../src/client/api.ts'
|
||||
import { createBetterSidebarService, type FileViewerProps } from '../src/client/service.ts'
|
||||
import { allLeaves, createSidebarStore, type SidebarTab } from '../src/client/state.ts'
|
||||
|
||||
;(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true
|
||||
setupReactAct()
|
||||
|
||||
function toolbar(dirty: boolean) {
|
||||
return {
|
||||
@@ -68,28 +68,16 @@ function setup(): {
|
||||
}
|
||||
|
||||
function mount(ctx: Context, store: ReturnType<typeof createSidebarStore>, tab: SidebarTab) {
|
||||
const container = document.createElement('div')
|
||||
document.body.append(container)
|
||||
const root = createRoot(container)
|
||||
act(() => {
|
||||
root.render(createElement(EditorHost, {
|
||||
ctx,
|
||||
store,
|
||||
scope: { sessionId: 'markdown-manual-refresh-session', cwd: '/tmp' },
|
||||
tab,
|
||||
expanded: [],
|
||||
revealed: [],
|
||||
onToggleDir: () => {},
|
||||
onReferenceFile: () => {},
|
||||
}))
|
||||
})
|
||||
return {
|
||||
container,
|
||||
unmount: () => {
|
||||
act(() => { root.unmount() })
|
||||
container.remove()
|
||||
},
|
||||
}
|
||||
return renderRoot(createElement(EditorHost, {
|
||||
ctx,
|
||||
store,
|
||||
scope: { sessionId: 'markdown-manual-refresh-session', cwd: '/tmp' },
|
||||
tab,
|
||||
expanded: [],
|
||||
revealed: [],
|
||||
onToggleDir: () => {},
|
||||
onReferenceFile: () => {},
|
||||
}))
|
||||
}
|
||||
|
||||
async function flush(): Promise<void> {
|
||||
|
||||
@@ -13,7 +13,8 @@ import { act } from 'react-dom/test-utils'
|
||||
import { MdToc, TOC_MIN_HEADINGS } from '../src/client/md-toc.tsx'
|
||||
|
||||
// The act() environment flag (React 18.2 reads it before flushing effects).
|
||||
;(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true
|
||||
import { setupReactAct } from './test-utils.ts'
|
||||
setupReactAct()
|
||||
|
||||
const scrollIntoView = vi.fn()
|
||||
Element.prototype.scrollIntoView = scrollIntoView
|
||||
|
||||
@@ -12,7 +12,8 @@ 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 { setupReactAct } from './test-utils.ts'
|
||||
setupReactAct()
|
||||
|
||||
vi.mock('mermaid', () => ({
|
||||
default: {
|
||||
|
||||
@@ -11,7 +11,8 @@ import { createRoot, type Root } from 'react-dom/client'
|
||||
import { act } from 'react-dom/test-utils'
|
||||
import { OpenWithSettings } from '../src/client/open-with-settings.tsx'
|
||||
|
||||
;(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true
|
||||
import { setupReactAct } from './test-utils.ts'
|
||||
setupReactAct()
|
||||
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(window.navigator, 'language', { value: 'en-US', configurable: true })
|
||||
|
||||
@@ -16,7 +16,8 @@ import { act } from 'react-dom/test-utils'
|
||||
import { useSelectionPopup } from '../src/client/selection-popup.ts'
|
||||
|
||||
// The act() environment flag (React 18.2 reads it before flushing effects).
|
||||
;(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true
|
||||
import { setupReactAct } from './test-utils.ts'
|
||||
setupReactAct()
|
||||
|
||||
/** jsdom ships no IntersectionObserver; a controllable stand-in. */
|
||||
class FakeIntersectionObserver implements IntersectionObserver {
|
||||
|
||||
@@ -12,32 +12,16 @@
|
||||
*/
|
||||
// @vitest-environment jsdom
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createElement, type ReactNode } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { createElement } from 'react'
|
||||
import { act } from 'react-dom/test-utils'
|
||||
import { renderRoot, setupReactAct } from './test-utils.ts'
|
||||
|
||||
// The act() environment flag (React 18.2 reads it before flushing effects).
|
||||
;(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true
|
||||
setupReactAct()
|
||||
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. */
|
||||
@@ -60,7 +44,7 @@ describe('FeatureSettingsRows typed rows (interactive)', () => {
|
||||
type: 'text',
|
||||
title: () => 'Font family',
|
||||
}
|
||||
const { container, unmount } = mount(createElement(FeatureSettingsRows, {
|
||||
const { container, unmount } = renderRoot(createElement(FeatureSettingsRows, {
|
||||
toggles: [toggle],
|
||||
prefs,
|
||||
onToggle: () => {},
|
||||
@@ -86,7 +70,7 @@ describe('FeatureSettingsRows typed rows (interactive)', () => {
|
||||
min: 9,
|
||||
max: 32,
|
||||
}
|
||||
const { container, unmount } = mount(createElement(FeatureSettingsRows, {
|
||||
const { container, unmount } = renderRoot(createElement(FeatureSettingsRows, {
|
||||
toggles: [toggle],
|
||||
prefs: { ...prefs, terminalFontSize: 13 },
|
||||
onToggle: () => {},
|
||||
@@ -113,7 +97,7 @@ describe('FeatureSettingsRows typed rows (interactive)', () => {
|
||||
min: 9,
|
||||
max: 32,
|
||||
}
|
||||
const { container, unmount } = mount(createElement(FeatureSettingsRows, {
|
||||
const { container, unmount } = renderRoot(createElement(FeatureSettingsRows, {
|
||||
toggles: [toggle],
|
||||
prefs: { ...prefs, terminalFontSize: 13 },
|
||||
onToggle: () => {},
|
||||
@@ -142,7 +126,7 @@ describe('FeatureSettingsRows typed rows (interactive)', () => {
|
||||
// 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, {
|
||||
const { container, rerender, unmount } = renderRoot(createElement(FeatureSettingsRows, {
|
||||
toggles: [toggle],
|
||||
prefs: prefsNow,
|
||||
onToggle: () => {},
|
||||
@@ -198,7 +182,7 @@ describe('FeatureSettingsRows select rows (interactive)', () => {
|
||||
title: () => 'Editor explorer',
|
||||
options,
|
||||
}
|
||||
const { container, unmount } = mount(createElement(FeatureSettingsRows, {
|
||||
const { container, unmount } = renderRoot(createElement(FeatureSettingsRows, {
|
||||
toggles: [toggle],
|
||||
// Merged is the selected option for this scenario (the default is now
|
||||
// separate — the anchor must render whatever value the prefs carry).
|
||||
@@ -239,7 +223,7 @@ describe('FeatureSettingsRows select rows (interactive)', () => {
|
||||
onSelectValue: (t, next) => { commits.push([t.key, next]) },
|
||||
valueSource: () => value,
|
||||
})
|
||||
const { container, rerender, unmount } = mount(rows(['c', 'a']))
|
||||
const { container, rerender, unmount } = renderRoot(rows(['c', 'a']))
|
||||
// The anchor shows the picked titles; options follow the declared order.
|
||||
const items = openSelect(container)
|
||||
expect(items.map(item => item.textContent)).toEqual(['Alpha', 'Beta', 'Gamma'])
|
||||
@@ -263,7 +247,7 @@ describe('FeatureSettingsRows select rows (interactive)', () => {
|
||||
title: () => 'Editor explorer',
|
||||
options,
|
||||
}
|
||||
const { container, unmount } = mount(createElement(FeatureSettingsRows, {
|
||||
const { container, unmount } = renderRoot(createElement(FeatureSettingsRows, {
|
||||
toggles: [toggle],
|
||||
prefs,
|
||||
onToggle: () => {},
|
||||
|
||||
@@ -9,7 +9,8 @@ import { createElement } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { act } from 'react-dom/test-utils'
|
||||
|
||||
;(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true
|
||||
import { setupReactAct } from './test-utils.ts'
|
||||
setupReactAct()
|
||||
|
||||
import { Sidebar } from '../src/client/Sidebar.tsx'
|
||||
import { allLeaves, createSidebarStore, firstLeaf, type SidebarStore } from '../src/client/state.ts'
|
||||
|
||||
@@ -23,7 +23,8 @@ 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 { setupReactAct } from './test-utils.ts'
|
||||
setupReactAct()
|
||||
|
||||
import { Sidebar } from '../src/client/Sidebar.tsx'
|
||||
import { createSidebarStore, setBottomHeight, toggleBottomPanel, type SidebarStore } from '../src/client/state.ts'
|
||||
|
||||
@@ -8,15 +8,15 @@
|
||||
*/
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createElement, type ReactNode } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { createElement } from 'react'
|
||||
import { act } from 'react-dom/test-utils'
|
||||
import { renderRoot, setupReactAct } from './test-utils.ts'
|
||||
import { SideChatView } from '../src/client/SideChatView.tsx'
|
||||
import { attachLocale } from '../src/client/locales.ts'
|
||||
import type { Context, SidebarSessionList } from '../src/context-types.ts'
|
||||
import type { SidebarTab } from '../src/client/state.ts'
|
||||
|
||||
;(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true
|
||||
setupReactAct()
|
||||
|
||||
/** Minimal structural fake of the DSH LocaleService face the sidebar uses. */
|
||||
class FakeLocale {
|
||||
@@ -110,19 +110,6 @@ function jsonResponse(value: unknown): Response {
|
||||
return { ok: true, status: 200, json: async () => value } as unknown as Response
|
||||
}
|
||||
|
||||
/** Render `node` into a detached body container under React's act(). */
|
||||
function mount(node: ReactNode): { container: HTMLDivElement; unmount: () => void } {
|
||||
const container = document.createElement('div')
|
||||
document.body.append(container)
|
||||
const root: Root = createRoot(container)
|
||||
act(() => { root.render(node) })
|
||||
const unmount = (): void => {
|
||||
act(() => { root.unmount() })
|
||||
container.remove()
|
||||
}
|
||||
return { container, unmount }
|
||||
}
|
||||
|
||||
let eventsPulls = 0
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -168,7 +155,7 @@ function threadStore(): ReturnType<typeof makeStore> {
|
||||
describe('SideChatView rendering', () => {
|
||||
it('renders the bash pair as a TerminalBlock and the turn tail with usage + duration', async () => {
|
||||
const props = viewProps(makeCtx(threadStore()))
|
||||
const { container, unmount } = mount(createElement(SideChatView, props))
|
||||
const { container, unmount } = renderRoot(createElement(SideChatView, props))
|
||||
await act(async () => {}) // flush the initial transcript pull
|
||||
const text = container.textContent ?? ''
|
||||
// Terminal surface: the command line and the marker-stripped output body.
|
||||
@@ -182,7 +169,7 @@ describe('SideChatView rendering', () => {
|
||||
it('shows the disconnect banner, reconnects on click, and pulls immediately on recovery', async () => {
|
||||
const connection = makeConnection('disconnected')
|
||||
const props = viewProps(makeCtx(threadStore(), connection))
|
||||
const { container, unmount } = mount(createElement(SideChatView, props))
|
||||
const { container, unmount } = renderRoot(createElement(SideChatView, props))
|
||||
await act(async () => {}) // flush the initial transcript pull
|
||||
expect(container.textContent ?? '').toContain('连接已断开')
|
||||
|
||||
@@ -200,12 +187,12 @@ describe('SideChatView rendering', () => {
|
||||
})
|
||||
|
||||
it('renders no banner without a connection service or while connected', async () => {
|
||||
const absent = mount(createElement(SideChatView, viewProps(makeCtx(threadStore()))))
|
||||
const absent = renderRoot(createElement(SideChatView, viewProps(makeCtx(threadStore()))))
|
||||
expect(absent.container.textContent ?? '').not.toContain('连接已断开')
|
||||
absent.unmount()
|
||||
|
||||
const connection = makeConnection('connected')
|
||||
const connected = mount(createElement(SideChatView, viewProps(makeCtx(threadStore(), connection))))
|
||||
const connected = renderRoot(createElement(SideChatView, viewProps(makeCtx(threadStore(), connection))))
|
||||
expect(connected.container.textContent ?? '').not.toContain('连接已断开')
|
||||
connected.unmount()
|
||||
})
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
*/
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createElement, type ReactNode } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { createElement } from 'react'
|
||||
import { act } from 'react-dom/test-utils'
|
||||
import { renderRoot } from './test-utils.ts'
|
||||
import { SubagentView } from '../src/client/SubagentView.tsx'
|
||||
import type { Context, SidebarSessionList } from '../src/context-types.ts'
|
||||
|
||||
@@ -52,19 +52,6 @@ function makeCtx(store: Store): Context {
|
||||
} as unknown as Context
|
||||
}
|
||||
|
||||
/** Render `node` into a detached body container under React's act(). */
|
||||
function mount(node: ReactNode): { container: HTMLDivElement; unmount: () => void } {
|
||||
const container = document.createElement('div')
|
||||
document.body.append(container)
|
||||
const root: Root = createRoot(container)
|
||||
act(() => { root.render(node) })
|
||||
const unmount = (): void => {
|
||||
act(() => { root.unmount() })
|
||||
container.remove()
|
||||
}
|
||||
return { container, unmount }
|
||||
}
|
||||
|
||||
const outputCalls: Array<{ sessionId: string; id: string }> = []
|
||||
const killCalls: Array<{ sessionId: string; id: string }> = []
|
||||
|
||||
@@ -129,7 +116,7 @@ afterEach(() => {
|
||||
describe('SubagentView background jobs', () => {
|
||||
it('renders the tree jobs with status, durations, and owner labels', () => {
|
||||
const store = makeStore(baseSnapshot())
|
||||
const { container, unmount } = mount(
|
||||
const { container, unmount } = renderRoot(
|
||||
createElement(SubagentView, { sessionId: 'root', active: true, ctx: makeCtx(store) }),
|
||||
)
|
||||
const text = container.textContent ?? ''
|
||||
@@ -147,7 +134,7 @@ describe('SubagentView background jobs', () => {
|
||||
|
||||
it('renders nothing job-related when the mirror is empty', () => {
|
||||
const store = makeStore({ current: 'root', byId: { root: { id: 'root', displayTitle: '主会话' } }, subagentsByParent: {}, jobsBySession: {} })
|
||||
const { container, unmount } = mount(
|
||||
const { container, unmount } = renderRoot(
|
||||
createElement(SubagentView, { sessionId: 'root', active: true, ctx: makeCtx(store) }),
|
||||
)
|
||||
expect(container.textContent).not.toContain('后台任务')
|
||||
@@ -156,7 +143,7 @@ describe('SubagentView background jobs', () => {
|
||||
|
||||
it('survives the mirror emptying while mounted (hook-order regression #300)', async () => {
|
||||
const store = makeStore(baseSnapshot())
|
||||
const { container, unmount } = mount(
|
||||
const { container, unmount } = renderRoot(
|
||||
createElement(SubagentView, { sessionId: 'root', active: true, ctx: makeCtx(store) }),
|
||||
)
|
||||
expect(container.textContent).toContain('sleep 300')
|
||||
@@ -174,7 +161,7 @@ describe('SubagentView background jobs', () => {
|
||||
|
||||
it('shows the selected job output in the bottom dock, closeable', async () => {
|
||||
const store = makeStore(baseSnapshot())
|
||||
const { container, unmount } = mount(
|
||||
const { container, unmount } = renderRoot(
|
||||
createElement(SubagentView, { sessionId: 'root', active: true, ctx: makeCtx(store) }),
|
||||
)
|
||||
const row = container.querySelector('button[aria-label*="sleep 300"]') as HTMLButtonElement
|
||||
@@ -195,7 +182,7 @@ describe('SubagentView background jobs', () => {
|
||||
|
||||
it('switches the single dock between selected rows', async () => {
|
||||
const store = makeStore(baseSnapshot())
|
||||
const { container, unmount } = mount(
|
||||
const { container, unmount } = renderRoot(
|
||||
createElement(SubagentView, { sessionId: 'root', active: true, ctx: makeCtx(store) }),
|
||||
)
|
||||
const first = container.querySelector('button[aria-label*="sleep 300"]') as HTMLButtonElement
|
||||
@@ -223,7 +210,7 @@ describe('SubagentView background jobs', () => {
|
||||
],
|
||||
}
|
||||
const store = makeStore(snapshot)
|
||||
const { container, unmount } = mount(
|
||||
const { container, unmount } = renderRoot(
|
||||
createElement(SubagentView, { sessionId: 'root', active: true, ctx: makeCtx(store) }),
|
||||
)
|
||||
const row = container.querySelector('button[aria-label*="unread cmd"]') as HTMLButtonElement
|
||||
@@ -249,7 +236,7 @@ describe('SubagentView background jobs', () => {
|
||||
subagentsByParent: {},
|
||||
jobsBySession: { root: many },
|
||||
})
|
||||
const { container, unmount } = mount(
|
||||
const { container, unmount } = renderRoot(
|
||||
createElement(SubagentView, { sessionId: 'root', active: true, ctx: makeCtx(store) }),
|
||||
)
|
||||
expect(container.textContent).toContain('60 个后台任务 · 30 运行中')
|
||||
@@ -264,7 +251,7 @@ describe('SubagentView background jobs', () => {
|
||||
|
||||
it('kills a live job only after the two-click confirm', async () => {
|
||||
const store = makeStore(baseSnapshot())
|
||||
const { container, unmount } = mount(
|
||||
const { container, unmount } = renderRoot(
|
||||
createElement(SubagentView, { sessionId: 'root', active: true, ctx: makeCtx(store) }),
|
||||
)
|
||||
const kill = container.querySelector('button[aria-label="终止"]') as HTMLButtonElement
|
||||
@@ -282,7 +269,7 @@ describe('SubagentView background jobs', () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const store = makeStore(baseSnapshot())
|
||||
const { container, unmount } = mount(
|
||||
const { container, unmount } = renderRoot(
|
||||
createElement(SubagentView, { sessionId: 'root', active: false, ctx: makeCtx(store) }),
|
||||
)
|
||||
const row = container.querySelector('button[aria-label*="sleep 300"]') as HTMLButtonElement
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
*/
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createElement, type ReactNode } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { createElement } from 'react'
|
||||
import { act } from 'react-dom/test-utils'
|
||||
import { renderRoot } from './test-utils.ts'
|
||||
import { SubagentView } from '../src/client/SubagentView.tsx'
|
||||
import type { Context, SidebarSessionList } from '../src/context-types.ts'
|
||||
|
||||
@@ -51,24 +51,6 @@ function makeCtx(store: Store, historySpy: ReturnType<typeof vi.fn>): Context {
|
||||
} as unknown as Context
|
||||
}
|
||||
|
||||
/** Render `node` into a detached body container under React's act(). */
|
||||
function mount(node: ReactNode): {
|
||||
container: HTMLDivElement
|
||||
rerender: (next: ReactNode) => void
|
||||
unmount: () => void
|
||||
} {
|
||||
const container = document.createElement('div')
|
||||
document.body.append(container)
|
||||
const root: Root = createRoot(container)
|
||||
const rerender = (next: ReactNode): void => { act(() => { root.render(next) }) }
|
||||
act(() => { root.render(node) })
|
||||
const unmount = (): void => {
|
||||
act(() => { root.unmount() })
|
||||
container.remove()
|
||||
}
|
||||
return { container, rerender, unmount }
|
||||
}
|
||||
|
||||
function jsonResponse(value: unknown): Response {
|
||||
return { ok: true, status: 200, json: async () => value } as unknown as Response
|
||||
}
|
||||
@@ -156,7 +138,7 @@ describe('SubagentView live polling', () => {
|
||||
})
|
||||
|
||||
const store = makeStore(runningSnapshot())
|
||||
const { container, unmount } = mount(
|
||||
const { container, unmount } = renderRoot(
|
||||
createElement(SubagentView, { sessionId: 'root', active: true, ctx: makeCtx(store, historySpy) }),
|
||||
)
|
||||
// Initial load plus the first scheduled tick.
|
||||
@@ -187,7 +169,7 @@ describe('SubagentView live polling', () => {
|
||||
})
|
||||
|
||||
const store = makeStore(runningSnapshot())
|
||||
const { unmount } = mount(
|
||||
const { unmount } = renderRoot(
|
||||
createElement(SubagentView, { sessionId: 'root', active: true, ctx: makeCtx(store, historySpy) }),
|
||||
)
|
||||
await act(async () => { await Promise.resolve() })
|
||||
@@ -225,7 +207,7 @@ describe('SubagentView live polling', () => {
|
||||
})
|
||||
|
||||
const store = makeStore(runningSnapshot())
|
||||
const { rerender, unmount } = mount(
|
||||
const { rerender, unmount } = renderRoot(
|
||||
createElement(SubagentView, { sessionId: 'root', active: true, ctx: makeCtx(store, historySpy) }),
|
||||
)
|
||||
await act(async () => { await Promise.resolve() })
|
||||
@@ -264,7 +246,7 @@ describe('SubagentView live polling', () => {
|
||||
})
|
||||
|
||||
const store = makeStore(runningSnapshot())
|
||||
const { container, unmount } = mount(
|
||||
const { container, unmount } = renderRoot(
|
||||
createElement(SubagentView, { sessionId: 'root', active: true, ctx: makeCtx(store, historySpy) }),
|
||||
)
|
||||
await act(async () => { await Promise.resolve() })
|
||||
|
||||
@@ -16,7 +16,8 @@ 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 { setupReactAct } from './test-utils.ts'
|
||||
setupReactAct()
|
||||
|
||||
import { TabBar } from '../src/client/TabBar.tsx'
|
||||
import type { SidebarTab } from '../src/client/state.ts'
|
||||
|
||||
@@ -17,7 +17,8 @@ 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 { setupReactAct } from './test-utils.ts'
|
||||
setupReactAct()
|
||||
|
||||
import { TabBar } from '../src/client/TabBar.tsx'
|
||||
import type { SidebarTab } from '../src/client/state.ts'
|
||||
|
||||
@@ -14,7 +14,8 @@ 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 { setupReactAct } from './test-utils.ts'
|
||||
setupReactAct()
|
||||
|
||||
import { TabBar } from '../src/client/TabBar.tsx'
|
||||
import type { SidebarTab } from '../src/client/state.ts'
|
||||
|
||||
@@ -19,7 +19,8 @@ import * as primitives from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { TerminalDepsBanner } from '../src/client/TerminalView.tsx'
|
||||
|
||||
// The act() environment flag (React 18.2 reads it before flushing effects).
|
||||
;(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true
|
||||
import { setupReactAct } from './test-utils.ts'
|
||||
setupReactAct()
|
||||
|
||||
const deps = {
|
||||
ok: false as const,
|
||||
|
||||
Reference in New Issue
Block a user