Merge branch 'master' into fix/todo-tool-ui
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
// hero (blank session) and active phases — same textarea DOM node, machine-
|
||||
// owned draft, and the hero workspace picker (switching = retargetWorkspace).
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, fireEvent, render } from '@testing-library/react'
|
||||
import { act, cleanup, fireEvent, render } from '@testing-library/react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type {
|
||||
@@ -55,7 +55,11 @@ function conversationSnapshot(overrides: Partial<ConversationSnapshot> = {}): Co
|
||||
}
|
||||
}
|
||||
|
||||
function mount(snapshot: ConversationSnapshot, workspaceRows: WorkspaceView[] = [{ ...workspace('one'), sessionIds: [SID] }]) {
|
||||
function mount(
|
||||
snapshot: ConversationSnapshot,
|
||||
workspaceRows: WorkspaceView[] = [{ ...workspace('one'), sessionIds: [SID] }],
|
||||
retargetWorkspace = vi.fn(async (_workspaceId: WorkspaceId) => {}),
|
||||
) {
|
||||
const root = sid('root')
|
||||
const sessions = createSnapshotStore<SessionListState>({
|
||||
ids: [root, SID],
|
||||
@@ -76,7 +80,6 @@ function mount(snapshot: ConversationSnapshot, workspaceRows: WorkspaceView[] =
|
||||
const inputActions = wiring.actions
|
||||
const stop = vi.fn()
|
||||
const open = vi.fn()
|
||||
const retargetWorkspace = vi.fn()
|
||||
const slotCalls: string[] = []
|
||||
let pickerOwner: unknown
|
||||
const renderSlot = ((key: string, owner: object, opts?: { only?: string }) => {
|
||||
@@ -158,7 +161,13 @@ describe('ConversationRoot resident composer', () => {
|
||||
})
|
||||
|
||||
it('hero phase: same textarea, hero chrome, no header, picker switches the workspace', () => {
|
||||
const b = mount(conversationSnapshot({ composerPhase: 'blank', blank: true }))
|
||||
const b = mount(
|
||||
conversationSnapshot({ composerPhase: 'blank', blank: true }),
|
||||
[
|
||||
{ ...workspace('one'), sessionIds: [SID] },
|
||||
{ ...workspace('second'), title: 'Selected Folder' },
|
||||
],
|
||||
)
|
||||
// Hero chrome present, view ring absent.
|
||||
expect(b.view.getByText("Let's start building")).toBeTruthy()
|
||||
expect(b.view.queryByTestId('view-chat')).toBeNull()
|
||||
@@ -173,8 +182,9 @@ describe('ConversationRoot resident composer', () => {
|
||||
fireEvent.click(b.view.getByRole('button', { name: 'Choose workspace' }))
|
||||
const owner = b.pickerOwner() as { open: boolean; onPick(id: WorkspaceId): void }
|
||||
expect(owner.open).toBe(true)
|
||||
owner.onPick(wid('second'))
|
||||
act(() => { owner.onPick(wid('second')) })
|
||||
expect(b.retargetWorkspace).toHaveBeenCalledWith(wid('second'))
|
||||
expect(b.view.getByText('Selected Folder')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('textarea DOM identity survives the hero → active flip', () => {
|
||||
@@ -191,6 +201,24 @@ describe('ConversationRoot resident composer', () => {
|
||||
expect(b.view.getByTestId('view-chat')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('rolls the pending workspace label back when switching fails', async () => {
|
||||
const selectWorkspace = vi.fn(async () => { throw new Error('connect failed') })
|
||||
const b = mount(
|
||||
conversationSnapshot({ composerPhase: 'blank', blank: true }),
|
||||
[
|
||||
{ ...workspace('one'), sessionIds: [SID] },
|
||||
{ ...workspace('second'), title: 'Selected Folder' },
|
||||
],
|
||||
selectWorkspace,
|
||||
)
|
||||
fireEvent.click(b.view.getByRole('button', { name: 'Choose workspace' }))
|
||||
const owner = b.pickerOwner() as { onPick(id: WorkspaceId): void }
|
||||
await act(async () => { owner.onPick(wid('second')); await Promise.resolve() })
|
||||
expect(selectWorkspace).toHaveBeenCalledWith(wid('second'))
|
||||
expect(b.view.queryByText('Selected Folder')).toBeNull()
|
||||
expect(b.view.getByText('one')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('blank session keeps the interactive picker chip (workspace switchable until the first message)', () => {
|
||||
const b = mount(conversationSnapshot({ composerPhase: 'blank', blank: true }))
|
||||
const chip = b.view.getByRole('button', { name: 'Choose workspace' })
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// @vitest-environment jsdom
|
||||
/**
|
||||
* Todo display acceptance: the TodoPanel plan strip (empty-hidden, status
|
||||
* rows, collapse with active hint), its TodoDock adapter (selects the plan off
|
||||
* the session snapshot and follows changes), and the todo_write toolview row
|
||||
* (progress summary from args, generic fallback on malformed JSON, error badge,
|
||||
* rows, collapse), its TodoDock adapter (selects the plan off the session
|
||||
* snapshot and follows changes), and the todo_write toolview row (progress
|
||||
* summary from args, generic fallback on malformed JSON, error badge,
|
||||
* keyboard activation).
|
||||
*/
|
||||
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
@@ -31,32 +31,36 @@ describe('TodoPanel', () => {
|
||||
expect(container.innerHTML).toBe('')
|
||||
})
|
||||
|
||||
it('shows progress, one row per item with its status, and strikes done items', () => {
|
||||
it('shows progress, one row per item with its status glyph', () => {
|
||||
render(<TodoPanel todos={LIST} />)
|
||||
expect(screen.getByTestId('todo-panel')).toBeTruthy()
|
||||
expect(screen.getByText('1/3')).toBeTruthy()
|
||||
expect(screen.getByText('To-dos')).toBeTruthy()
|
||||
expect(screen.getByText('1/3 tasks · 1 in progress')).toBeTruthy()
|
||||
const items = screen.getAllByRole('listitem')
|
||||
expect(items.map(li => li.getAttribute('data-status'))).toEqual(['completed', 'in_progress', 'pending'])
|
||||
expect(screen.getByText('搭骨架')).toBeTruthy()
|
||||
expect(screen.getByText('写组件')).toBeTruthy()
|
||||
// Each status row carries an SVG glyph (not a text bullet).
|
||||
expect(items.every(li => li.querySelector('svg') !== null)).toBe(true)
|
||||
})
|
||||
|
||||
it('collapse hides the list and surfaces the active item in the header; expand restores', () => {
|
||||
it('collapse hides the list; expand restores; header keeps the count summary', () => {
|
||||
render(<TodoPanel todos={LIST} />)
|
||||
const header = screen.getByRole('button', { expanded: true })
|
||||
fireEvent.click(header)
|
||||
expect(screen.queryByRole('list')).toBeNull()
|
||||
// Collapsed header carries the in-progress content as the one-line hint.
|
||||
expect(screen.getByText('写组件')).toBeTruthy()
|
||||
// Collapsed header is title + progress only (no in-progress content hint).
|
||||
expect(screen.getByText('1/3 tasks · 1 in progress')).toBeTruthy()
|
||||
expect(screen.queryByText('写组件')).toBeNull()
|
||||
fireEvent.click(screen.getByRole('button', { expanded: false }))
|
||||
expect(screen.getAllByRole('listitem')).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('collapsed header omits the hint when nothing is in progress', () => {
|
||||
it('collapsed header still shows zero in-progress when nothing is active', () => {
|
||||
render(<TodoPanel todos={[{ content: '都完了', status: 'completed' }]} />)
|
||||
fireEvent.click(screen.getByRole('button', { expanded: true }))
|
||||
expect(screen.queryByText('都完了')).toBeNull()
|
||||
expect(screen.getByText('1/1')).toBeTruthy()
|
||||
expect(screen.getByText('1/1 tasks · 0 in progress')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -71,7 +75,7 @@ describe('TodoDock', () => {
|
||||
render(<TodoDock {...dockProps(store)} />)
|
||||
expect(screen.queryByTestId('todo-panel')).toBeNull()
|
||||
act(() => { store.set({ todos: LIST }) })
|
||||
expect(screen.getByText('1/3')).toBeTruthy()
|
||||
expect(screen.getByText('1/3 tasks · 1 in progress')).toBeTruthy()
|
||||
// A rollback to the empty list retires the strip (the panel owns no data).
|
||||
act(() => { store.set({ todos: [] }) })
|
||||
expect(screen.queryByTestId('todo-panel')).toBeNull()
|
||||
|
||||
Reference in New Issue
Block a user