Merge remote-tracking branch 'origin/master' into mergebot/pr711
# Conflicts: # packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx # packages/client/ui-workspace/src/client/index.ts # packages/client/ui-workspace/src/client/rows/Rows.tsx # packages/client/ui-workspace/src/client/tree.ts # packages/client/ui-workspace/tests/apply.spec.ts # packages/client/ui-workspace/tests/rows.spec.tsx # packages/client/ui-workspace/tests/tree.spec.ts # packages/client/ui-workspace/tests/workspace-browser.spec.tsx
This commit is contained in:
@@ -5,13 +5,20 @@ import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import type {
|
||||
SessionId, SessionListState, SessionSummary, WorkspaceId, WorkspaceListState, WorkspaceView,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
|
||||
import type { WorkspaceBrowserProps } from '../src/client/contract/slots.ts'
|
||||
import { createWorkspaceViewStore } from '../src/client/stores.ts'
|
||||
import { WorkspaceBrowser } from '../src/client/WorkspaceBrowser.tsx'
|
||||
import { zh } from '../src/client/locales.ts'
|
||||
|
||||
afterEach(cleanup)
|
||||
beforeEach(() => { localStorage.clear() })
|
||||
|
||||
// The seat's key domain is workspace ∪ common; the stub mirrors the real
|
||||
// lookup chain (namespace, then common vocabulary, then the key).
|
||||
const t: WorkspaceBrowserProps['t'] = makeTranslate(zh, commonZh)
|
||||
|
||||
const sid = (id: string) => id as SessionId
|
||||
const wid = (id: string) => id as WorkspaceId
|
||||
const summary = (id: string, updatedAt: number, overrides: Partial<SessionSummary> = {}): SessionSummary => ({
|
||||
@@ -65,6 +72,7 @@ function mount(overrides: Partial<WorkspaceBrowserProps> = {}) {
|
||||
createWorkspace: vi.fn(async () => workspace('created', [])),
|
||||
useDirectoryFlow: bindSnapshotSelector({ getSnapshot: () => true, subscribe: () => () => {} }),
|
||||
renderSlot: ((_name: string, owner: { open: boolean }) => (owner.open ? <div data-testid="directory-flow" /> : null)) as never,
|
||||
t,
|
||||
...overrides,
|
||||
}
|
||||
const view = render(<WorkspaceBrowser {...props} />)
|
||||
@@ -84,29 +92,29 @@ describe('WorkspaceBrowser', () => {
|
||||
useSessions: hook(sessions),
|
||||
useWorkspaces: hook(workspaceState([workspace('alpha', ['alpha-s']), workspace('beta', ['beta-s'])])),
|
||||
})
|
||||
expect(screen.getByText('Workspaces')).toBeTruthy()
|
||||
expect(screen.getByText('工作区')).toBeTruthy()
|
||||
expect(screen.getByText('alpha')).toBeTruthy()
|
||||
// Sessions hidden while their group is folded.
|
||||
expect(screen.queryByText('alpha-s')).toBeNull()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Group by' }))
|
||||
expect(screen.getByText('Group by')).toBeTruthy() // the menu heading label
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: 'In one list' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '分组方式' }))
|
||||
expect(screen.getByText('分组方式')).toBeTruthy() // the menu heading label
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '单列表' }))
|
||||
// Store-driven flip: title changes, rows flatten newest-first, headers gone.
|
||||
expect(b.store.getSnapshot().groupBy).toBe('flat')
|
||||
expect(screen.getByText('Sessions')).toBeTruthy()
|
||||
expect(screen.getByText('会话')).toBeTruthy()
|
||||
expect(screen.queryByText('alpha')).toBeNull()
|
||||
expect(screen.getByText('alpha-s')).toBeTruthy()
|
||||
expect(screen.getByText('beta-s')).toBeTruthy()
|
||||
|
||||
// Back to workspace grouping through the same menu.
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Group by' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: 'WorkSpace' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '分组方式' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '按工作区' }))
|
||||
expect(b.store.getSnapshot().groupBy).toBe('workspace')
|
||||
expect(screen.getByText('Workspaces')).toBeTruthy()
|
||||
expect(screen.getByText('工作区')).toBeTruthy()
|
||||
|
||||
// Escape closes the menu without picking.
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Group by' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '分组方式' }))
|
||||
fireEvent.keyDown(document, { key: 'Escape' })
|
||||
expect(screen.queryByRole('menu')).toBeNull()
|
||||
expect(b.store.getSnapshot().groupBy).toBe('workspace')
|
||||
@@ -136,7 +144,7 @@ describe('WorkspaceBrowser', () => {
|
||||
})
|
||||
fireEvent.click(screen.getByText('alpha'))
|
||||
expect(screen.getByText('child-s')).toBeTruthy()
|
||||
expect(screen.queryByRole('button', { name: /Expand|Collapse/ })).toBeNull()
|
||||
expect(screen.queryByRole('button', { name: /展开|收起/ })).toBeNull()
|
||||
expect(screen.getByText('child-s').closest('[role="treeitem"]')?.getAttribute('draggable')).toBe('true')
|
||||
})
|
||||
|
||||
@@ -149,7 +157,7 @@ describe('WorkspaceBrowser', () => {
|
||||
})
|
||||
// The current-group effect expanded the owning group without a click.
|
||||
expect(screen.getByText('alpha-s')).toBeTruthy()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'New session in alpha' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '在“alpha”中新建会话' }))
|
||||
expect(startSession).toHaveBeenCalledWith(wid('alpha'))
|
||||
})
|
||||
|
||||
@@ -162,8 +170,8 @@ describe('WorkspaceBrowser', () => {
|
||||
})
|
||||
// The loose session's group is UNGROUPED_KEY: expanded by the effect.
|
||||
expect(screen.getByText('loose')).toBeTruthy()
|
||||
expect(screen.queryByRole('button', { name: 'Workspace actions for Ungrouped' })).toBeNull()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'New session in Ungrouped' }))
|
||||
expect(screen.queryByRole('button', { name: '工作区“未分组”的操作' })).toBeNull()
|
||||
fireEvent.click(screen.getByRole('button', { name: '在“未分组”中新建会话' }))
|
||||
expect(startSession).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -182,7 +190,7 @@ describe('WorkspaceBrowser', () => {
|
||||
expect(screen.queryByText('b')).toBeNull()
|
||||
})
|
||||
|
||||
it('shows only the current blank session as New Session in grouped, flat, and search modes', () => {
|
||||
it('shows only the current blank session as the localized New Session, excluded from search', () => {
|
||||
const currentBlank = summary('alpha-blank', 9, { blank: true })
|
||||
const staleBlank = summary('beta-blank', 8, { blank: true })
|
||||
const sessions = sessionState(
|
||||
@@ -195,18 +203,22 @@ describe('WorkspaceBrowser', () => {
|
||||
workspace('alpha', ['alpha-blank']), workspace('beta', ['beta-blank']),
|
||||
])),
|
||||
})
|
||||
expect(screen.getByText('New Session')).toBeTruthy()
|
||||
expect(screen.getByText('新会话')).toBeTruthy()
|
||||
expect(screen.queryByText('alpha-blank')).toBeNull()
|
||||
expect(screen.queryByText('beta-blank')).toBeNull()
|
||||
expect(screen.getByText('1 session')).toBeTruthy()
|
||||
expect(screen.getByText('1 个会话')).toBeTruthy()
|
||||
|
||||
rerender(b, { useSessions: hook({ ...sessions, current: staleBlank.id }) })
|
||||
expect(screen.getAllByText('New Session')).toHaveLength(1)
|
||||
expect(screen.getAllByText('新会话')).toHaveLength(1)
|
||||
b.store.actions.setGroupBy('flat')
|
||||
rerender(b, {})
|
||||
expect(screen.getAllByText('New Session')).toHaveLength(1)
|
||||
fireEvent.change(screen.getByPlaceholderText('Search names or content…'), { target: { value: 'new session' } })
|
||||
expect(screen.getAllByText('New Session')).toHaveLength(1)
|
||||
expect(screen.getAllByText('新会话')).toHaveLength(1)
|
||||
// Search excludes blank rows entirely — neither the canonical stored
|
||||
// title nor the localized display label participates in matching.
|
||||
fireEvent.change(screen.getByPlaceholderText('搜索名称、关键词…'), { target: { value: 'new session' } })
|
||||
expect(screen.queryByText('新会话')).toBeNull()
|
||||
fireEvent.change(screen.getByPlaceholderText('搜索名称、关键词…'), { target: { value: '新会话' } })
|
||||
expect(screen.queryByText('新会话')).toBeNull()
|
||||
})
|
||||
|
||||
it('shows local metadata matches immediately, then clears back to the grouped tree', async () => {
|
||||
@@ -220,21 +232,21 @@ describe('WorkspaceBrowser', () => {
|
||||
useSessions: hook(sessions),
|
||||
useWorkspaces: hook(workspaceState([workspace('alpha', ['needle-row', 'other-row'])])),
|
||||
})
|
||||
const input = screen.getByPlaceholderText<HTMLInputElement>('Search names or content…')
|
||||
const input = screen.getByPlaceholderText<HTMLInputElement>('搜索名称、关键词…')
|
||||
fireEvent.change(input, { target: { value: 'needle' } })
|
||||
const resultTree = screen.getByRole('tree', { name: 'Search results' })
|
||||
const resultTree = screen.getByRole('tree', { name: '搜索结果' })
|
||||
expect(screen.getByText('Needle row')).toBeTruthy()
|
||||
expect(screen.queryByText('Other row')).toBeNull()
|
||||
const status = screen.getByRole('status')
|
||||
expect(status.textContent).toBe('Searching session history…')
|
||||
expect(status.textContent).toBe('正在搜索会话历史…')
|
||||
expect(resultTree.contains(status)).toBe(false)
|
||||
|
||||
fireEvent.change(input, { target: { value: 'zzz' } })
|
||||
await act(async () => { await vi.advanceTimersByTimeAsync(250) })
|
||||
expect(screen.getByText('No matching sessions')).toBeTruthy()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Clear search' }))
|
||||
expect(screen.getByText('无匹配会话')).toBeTruthy()
|
||||
fireEvent.click(screen.getByRole('button', { name: '清除搜索' }))
|
||||
expect(input.value).toBe('')
|
||||
expect(screen.getByRole('tree', { name: 'Sessions' })).toBeTruthy()
|
||||
expect(screen.getByRole('tree', { name: '会话' })).toBeTruthy()
|
||||
// Clicking the field row focuses the input (wide mode).
|
||||
fireEvent.click(input.parentElement as HTMLElement)
|
||||
expect(document.activeElement).toBe(input)
|
||||
@@ -261,9 +273,9 @@ describe('WorkspaceBrowser', () => {
|
||||
open,
|
||||
searchSessions,
|
||||
})
|
||||
const input = screen.getByPlaceholderText<HTMLInputElement>('Search names or content…')
|
||||
const input = screen.getByPlaceholderText<HTMLInputElement>('搜索名称、关键词…')
|
||||
fireEvent.change(input, { target: { value: 'waterfall token' } })
|
||||
expect(screen.getByText('Searching session history…')).toBeTruthy()
|
||||
expect(screen.getByText('正在搜索会话历史…')).toBeTruthy()
|
||||
expect(screen.queryByText('Research notes')).toBeNull()
|
||||
|
||||
await act(async () => { await vi.advanceTimersByTimeAsync(250) })
|
||||
@@ -272,7 +284,7 @@ describe('WorkspaceBrowser', () => {
|
||||
expect(screen.getByText('Research notes')).toBeTruthy()
|
||||
expect(screen.getByText('Research Workspace')).toBeTruthy()
|
||||
expect(screen.getByText('…the waterfall token appears here…')).toBeTruthy()
|
||||
expect(screen.getByText('Showing the first 20 results. Narrow your search.')).toBeTruthy()
|
||||
expect(screen.getByText('仅显示前 20 条结果,请缩小搜索范围。')).toBeTruthy()
|
||||
fireEvent.click(screen.getByRole('treeitem'))
|
||||
expect(open).toHaveBeenCalledWith(sid('body-hit'))
|
||||
expect(input.value).toBe('waterfall token')
|
||||
@@ -286,7 +298,7 @@ describe('WorkspaceBrowser', () => {
|
||||
try {
|
||||
const searchSessions = vi.fn(async () => ({ items: [], hasMore: false }))
|
||||
mount({ searchSessions })
|
||||
const input = screen.getByPlaceholderText<HTMLInputElement>('Search names or content…')
|
||||
const input = screen.getByPlaceholderText<HTMLInputElement>('搜索名称、关键词…')
|
||||
expect(input.maxLength).toBe(500)
|
||||
fireEvent.change(input, { target: { value: 'y'.repeat(501) } })
|
||||
expect(input.value).toBe('y'.repeat(500))
|
||||
@@ -317,14 +329,14 @@ describe('WorkspaceBrowser', () => {
|
||||
useWorkspaces: hook(workspaceState([workspace('alpha', ['local-hit'])])),
|
||||
searchSessions,
|
||||
})
|
||||
fireEvent.change(screen.getByPlaceholderText('Search names or content…'), {
|
||||
fireEvent.change(screen.getByPlaceholderText('搜索名称、关键词…'), {
|
||||
target: { value: 'needle' },
|
||||
})
|
||||
expect(screen.getByText('Needle title')).toBeTruthy()
|
||||
await act(async () => { await vi.advanceTimersByTimeAsync(250) })
|
||||
expect(screen.getByText('Needle title')).toBeTruthy()
|
||||
expect(screen.getByText('Content search is temporarily unavailable. Showing name matches.')).toBeTruthy()
|
||||
expect(screen.queryByText('No matching sessions')).toBeNull()
|
||||
expect(screen.getByText('内容搜索暂不可用,仅显示名称匹配。')).toBeTruthy()
|
||||
expect(screen.queryByText('无匹配会话')).toBeNull()
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
@@ -354,7 +366,7 @@ describe('WorkspaceBrowser', () => {
|
||||
])),
|
||||
searchSessions,
|
||||
})
|
||||
const input = screen.getByPlaceholderText('Search names or content…')
|
||||
const input = screen.getByPlaceholderText('搜索名称、关键词…')
|
||||
fireEvent.change(input, { target: { value: 'first' } })
|
||||
await act(async () => { await vi.advanceTimersByTimeAsync(250) })
|
||||
const firstSignal = searchSessions.mock.calls[0]?.[1] as AbortSignal
|
||||
@@ -388,7 +400,7 @@ describe('WorkspaceBrowser', () => {
|
||||
? first
|
||||
: Promise.resolve({ items: [], hasMore: false }))
|
||||
mount({ searchSessions })
|
||||
const input = screen.getByPlaceholderText('Search names or content…')
|
||||
const input = screen.getByPlaceholderText('搜索名称、关键词…')
|
||||
fireEvent.change(input, { target: { value: 'first' } })
|
||||
await act(async () => { await vi.advanceTimersByTimeAsync(250) })
|
||||
|
||||
@@ -397,7 +409,7 @@ describe('WorkspaceBrowser', () => {
|
||||
rejectFirst(new Error('stale failure'))
|
||||
await Promise.resolve()
|
||||
})
|
||||
expect(screen.queryByText('Content search is temporarily unavailable. Showing name matches.')).toBeNull()
|
||||
expect(screen.queryByText('内容搜索暂不可用,仅显示名称匹配。')).toBeNull()
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
@@ -407,14 +419,14 @@ describe('WorkspaceBrowser', () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const b = mount()
|
||||
expect(screen.getByText('No sessions yet')).toBeTruthy()
|
||||
expect(screen.getByText('暂无会话')).toBeTruthy()
|
||||
b.store.actions.setGroupBy('flat')
|
||||
rerender(b, {})
|
||||
expect(screen.getByText('No sessions yet')).toBeTruthy()
|
||||
fireEvent.change(screen.getByPlaceholderText('Search names or content…'), { target: { value: 'x' } })
|
||||
expect(screen.getByText('Searching session history…')).toBeTruthy()
|
||||
expect(screen.getByText('暂无会话')).toBeTruthy()
|
||||
fireEvent.change(screen.getByPlaceholderText('搜索名称、关键词…'), { target: { value: 'x' } })
|
||||
expect(screen.getByText('正在搜索会话历史…')).toBeTruthy()
|
||||
await act(async () => { await vi.advanceTimersByTimeAsync(250) })
|
||||
expect(screen.getByText('No matching sessions')).toBeTruthy()
|
||||
expect(screen.getByText('无匹配会话')).toBeTruthy()
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
@@ -426,17 +438,17 @@ describe('WorkspaceBrowser', () => {
|
||||
const expandSidebar = vi.fn()
|
||||
const b = mount({ wide: false, expandSidebar })
|
||||
// No wide chrome in rail state.
|
||||
expect(screen.queryByText('Workspaces')).toBeNull()
|
||||
expect(screen.queryByPlaceholderText('Search names or content…')).toBeNull()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Search sessions' }))
|
||||
expect(screen.queryByText('工作区')).toBeNull()
|
||||
expect(screen.queryByPlaceholderText('搜索名称、关键词…')).toBeNull()
|
||||
fireEvent.click(screen.getByRole('button', { name: '搜索会话' }))
|
||||
expect(expandSidebar).toHaveBeenCalledTimes(1)
|
||||
// The wide flip mounts the input and focuses it after the slide.
|
||||
rerender(b, { wide: true })
|
||||
const input = screen.getByPlaceholderText('Search names or content…')
|
||||
const input = screen.getByPlaceholderText('搜索名称、关键词…')
|
||||
act(() => { vi.advanceTimersByTime(300) })
|
||||
expect(document.activeElement).toBe(input)
|
||||
// Wide search button is decorative (tabIndex -1, no expand call).
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Search sessions' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '搜索会话' }))
|
||||
expect(expandSidebar).toHaveBeenCalledTimes(1)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
@@ -446,17 +458,17 @@ describe('WorkspaceBrowser', () => {
|
||||
it('rail create-workspace toggles the create-only picker in place, without expanding', () => {
|
||||
const expandSidebar = vi.fn()
|
||||
mount({ wide: false, expandSidebar, useWorkspaces: hook(workspaceState([workspace('alpha', [])])) })
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Create workspace' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '创建工作区' }))
|
||||
expect(expandSidebar).not.toHaveBeenCalled()
|
||||
// createOnly: existing workspaces are not listed, only the create actions.
|
||||
expect(screen.queryByRole('menuitem', { name: 'alpha' })).toBeNull()
|
||||
expect(screen.getByRole('menuitem', { name: 'Open local folder…' })).toBeTruthy()
|
||||
expect(screen.getByRole('menuitem', { name: '打开本地文件夹…' })).toBeTruthy()
|
||||
// Toggle: open and close in place.
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Create workspace' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '创建工作区' }))
|
||||
expect(screen.queryByRole('menu')).toBeNull()
|
||||
|
||||
// Escape closes the picker through its own onClose.
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Create workspace' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '创建工作区' }))
|
||||
expect(screen.getByRole('menu')).toBeTruthy()
|
||||
fireEvent.keyDown(document, { key: 'Escape' })
|
||||
expect(screen.queryByRole('menu')).toBeNull()
|
||||
@@ -577,20 +589,20 @@ describe('WorkspaceBrowser', () => {
|
||||
useWorkspaces: hook(workspaceState([workspace('alpha', [], 'Alpha'), workspace('beta', [], 'Beta')])),
|
||||
renameWorkspace,
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Workspace actions for Alpha' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: 'Rename' }))
|
||||
const input = screen.getByLabelText<HTMLInputElement>('Workspace name')
|
||||
fireEvent.click(screen.getByRole('button', { name: '工作区“Alpha”的操作' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '重命名' }))
|
||||
const input = screen.getByLabelText<HTMLInputElement>('工作区名称')
|
||||
expect(input.value).toBe('Alpha')
|
||||
// Unchanged and blank names stay blocked.
|
||||
expect(screen.getByRole<HTMLButtonElement>('button', { name: 'Rename' }).disabled).toBe(true)
|
||||
expect(screen.getByRole<HTMLButtonElement>('button', { name: '重命名' }).disabled).toBe(true)
|
||||
fireEvent.change(input, { target: { value: ' ' } })
|
||||
expect(screen.getByRole<HTMLButtonElement>('button', { name: 'Rename' }).disabled).toBe(true)
|
||||
expect(screen.getByRole<HTMLButtonElement>('button', { name: '重命名' }).disabled).toBe(true)
|
||||
// A duplicate of another workspace's title shows the inline conflict.
|
||||
fireEvent.change(input, { target: { value: ' Beta ' } })
|
||||
expect(screen.getByRole('alert').textContent).toBe('A workspace named “Beta” already exists.')
|
||||
expect(screen.getByRole<HTMLButtonElement>('button', { name: 'Rename' }).disabled).toBe(true)
|
||||
expect(screen.getByRole('alert').textContent).toBe('已存在名为“Beta”的工作区。')
|
||||
expect(screen.getByRole<HTMLButtonElement>('button', { name: '重命名' }).disabled).toBe(true)
|
||||
fireEvent.change(input, { target: { value: 'Gamma' } })
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Rename' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '重命名' }))
|
||||
expect(renameWorkspace).toHaveBeenCalledWith(wid('alpha'), 'Gamma')
|
||||
// While renaming: input disabled, close blocked, Enter ignored.
|
||||
expect(input.disabled).toBe(true)
|
||||
@@ -606,9 +618,9 @@ describe('WorkspaceBrowser', () => {
|
||||
useWorkspaces: hook(workspaceState([workspace('alpha', [], 'Alpha')])),
|
||||
renameWorkspace,
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Workspace actions for Alpha' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: 'Rename' }))
|
||||
const input = screen.getByLabelText<HTMLInputElement>('Workspace name')
|
||||
fireEvent.click(screen.getByRole('button', { name: '工作区“Alpha”的操作' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '重命名' }))
|
||||
const input = screen.getByLabelText<HTMLInputElement>('工作区名称')
|
||||
// Enter with a blocked draft (unchanged) does nothing.
|
||||
fireEvent.keyDown(input, { key: 'Enter' })
|
||||
expect(renameWorkspace).not.toHaveBeenCalled()
|
||||
@@ -620,7 +632,7 @@ describe('WorkspaceBrowser', () => {
|
||||
// The dialog stays for retry; typing clears the error; Cancel closes.
|
||||
fireEvent.change(input, { target: { value: 'Renamed2' } })
|
||||
expect(screen.queryByRole('alert')).toBeNull()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '取消' }))
|
||||
expect(screen.queryByRole('dialog')).toBeNull()
|
||||
})
|
||||
|
||||
@@ -630,10 +642,10 @@ describe('WorkspaceBrowser', () => {
|
||||
useWorkspaces: hook(workspaceState([workspace('alpha', [], 'Alpha')])),
|
||||
renameWorkspace,
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Workspace actions for Alpha' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: 'Rename' }))
|
||||
fireEvent.change(screen.getByLabelText('Workspace name'), { target: { value: 'Other' } })
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Rename' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '工作区“Alpha”的操作' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '重命名' }))
|
||||
fireEvent.change(screen.getByLabelText('工作区名称'), { target: { value: 'Other' } })
|
||||
fireEvent.click(screen.getByRole('button', { name: '重命名' }))
|
||||
await waitFor(() => { expect(screen.getByRole('alert').textContent).toBe('denied') })
|
||||
})
|
||||
|
||||
@@ -644,31 +656,31 @@ describe('WorkspaceBrowser', () => {
|
||||
useWorkspaces: hook(workspaceState([workspace('alpha', ['session'], 'Alpha')])),
|
||||
deleteWorkspace,
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Workspace actions for Alpha' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: 'Delete workspace' }))
|
||||
const dialog = screen.getByRole('dialog', { name: 'Delete workspace' })
|
||||
expect(dialog.textContent).toContain('removes “Alpha” from the workspace list')
|
||||
expect(dialog.textContent).toContain('folder and session logs will be kept')
|
||||
expect(dialog.textContent).toContain('sessions will appear under Ungrouped')
|
||||
fireEvent.click(screen.getByRole('button', { name: '工作区“Alpha”的操作' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '删除工作区' }))
|
||||
const dialog = screen.getByRole('dialog', { name: '删除工作区' })
|
||||
expect(dialog.textContent).toContain('将把“Alpha”从工作区列表中移除')
|
||||
expect(dialog.textContent).toContain('文件夹与会话记录会保留')
|
||||
expect(dialog.textContent).toContain('其会话将显示在“未分组”下')
|
||||
|
||||
const confirm = screen.getByRole<HTMLButtonElement>('button', { name: 'Delete workspace' })
|
||||
const confirm = screen.getByRole<HTMLButtonElement>('button', { name: '删除工作区' })
|
||||
fireEvent.click(confirm)
|
||||
fireEvent.click(confirm)
|
||||
expect(deleteWorkspace).toHaveBeenCalledOnce()
|
||||
expect(deleteWorkspace).toHaveBeenCalledWith(wid('alpha'))
|
||||
expect(confirm.disabled).toBe(true)
|
||||
expect(screen.getByRole<HTMLButtonElement>('button', { name: 'Cancel' }).disabled).toBe(true)
|
||||
expect(screen.getByRole('status').textContent).toBe('Deleting workspace…')
|
||||
expect(screen.getByRole<HTMLButtonElement>('button', { name: '取消' }).disabled).toBe(true)
|
||||
expect(screen.getByRole('status').textContent).toBe('正在删除工作区…')
|
||||
fireEvent.keyDown(document, { key: 'Escape' })
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Close' }))
|
||||
expect(screen.getByRole('dialog', { name: 'Delete workspace' })).toBeTruthy()
|
||||
fireEvent.click(screen.getByRole('button', { name: '关闭' }))
|
||||
expect(screen.getByRole('dialog', { name: '删除工作区' })).toBeTruthy()
|
||||
await act(async () => { resolveDelete() })
|
||||
// RPC success alone does not close: the component waits until its
|
||||
// useWorkspaces projection has committed the removal, preventing a stale
|
||||
// duplicate-name frame from leaking into the next create gesture.
|
||||
expect(screen.getByRole('dialog', { name: 'Delete workspace' })).toBeTruthy()
|
||||
expect(screen.getByRole('dialog', { name: '删除工作区' })).toBeTruthy()
|
||||
rerender(browser, { useWorkspaces: hook(workspaceState([])) })
|
||||
expect(screen.queryByRole('dialog', { name: 'Delete workspace' })).toBeNull()
|
||||
expect(screen.queryByRole('dialog', { name: '删除工作区' })).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps the delete dialog open on failure and allows retry or cancellation', async () => {
|
||||
@@ -679,15 +691,15 @@ describe('WorkspaceBrowser', () => {
|
||||
useWorkspaces: hook(workspaceState([workspace('alpha', [], 'Alpha')])),
|
||||
deleteWorkspace,
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Workspace actions for Alpha' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: 'Delete workspace' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Delete workspace' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '工作区“Alpha”的操作' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '删除工作区' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '删除工作区' }))
|
||||
await waitFor(() => { expect(screen.getByRole('alert').textContent).toBe('storage unavailable') })
|
||||
expect(screen.getByRole('dialog', { name: 'Delete workspace' })).toBeTruthy()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Delete workspace' }))
|
||||
expect(screen.getByRole('dialog', { name: '删除工作区' })).toBeTruthy()
|
||||
fireEvent.click(screen.getByRole('button', { name: '删除工作区' }))
|
||||
await waitFor(() => { expect(screen.getByRole('alert').textContent).toBe('denied') })
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
|
||||
expect(screen.queryByRole('dialog', { name: 'Delete workspace' })).toBeNull()
|
||||
fireEvent.click(screen.getByRole('button', { name: '取消' }))
|
||||
expect(screen.queryByRole('dialog', { name: '删除工作区' })).toBeNull()
|
||||
})
|
||||
|
||||
it('Cancel, Escape, and Close dismiss deletion without calling the action', () => {
|
||||
@@ -697,17 +709,17 @@ describe('WorkspaceBrowser', () => {
|
||||
deleteWorkspace,
|
||||
})
|
||||
const open = () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Workspace actions for Alpha' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: 'Delete workspace' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '工作区“Alpha”的操作' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '删除工作区' }))
|
||||
}
|
||||
open()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '取消' }))
|
||||
open()
|
||||
fireEvent.keyDown(document, { key: 'Escape' })
|
||||
open()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Close' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '关闭' }))
|
||||
expect(deleteWorkspace).not.toHaveBeenCalled()
|
||||
expect(screen.queryByRole('dialog', { name: 'Delete workspace' })).toBeNull()
|
||||
expect(screen.queryByRole('dialog', { name: '删除工作区' })).toBeNull()
|
||||
})
|
||||
|
||||
it('search hides drag affordances (rows are not draggable during search)', () => {
|
||||
@@ -716,7 +728,7 @@ describe('WorkspaceBrowser', () => {
|
||||
useSessions: hook(sessions),
|
||||
useWorkspaces: hook(workspaceState([workspace('alpha', ['needle-a'])])),
|
||||
})
|
||||
fireEvent.change(screen.getByPlaceholderText('Search names or content…'), { target: { value: 'needle' } })
|
||||
fireEvent.change(screen.getByPlaceholderText('搜索名称、关键词…'), { target: { value: 'needle' } })
|
||||
const row = screen.getByText('Needle A').closest('[role="treeitem"]') as HTMLElement
|
||||
expect(row.hasAttribute('draggable')).toBe(false)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user