test(web): cover workspace sidebar behavior

This commit is contained in:
_Kerman
2026-08-11 15:24:40 +08:00
parent 8005ab78bd
commit 6e303ac0b7
22 changed files with 420 additions and 48 deletions

View File

@@ -8,6 +8,7 @@ import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
const css = readFileSync(fileURLToPath(new URL('../src/client/WorkspaceBrowser.module.css', import.meta.url)), 'utf8')
const rowsCss = readFileSync(fileURLToPath(new URL('../src/client/rows/Rows.module.css', import.meta.url)), 'utf8')
/**
* Declarations of one selector rule, keyed by property with whitespace collapsed.
@@ -15,21 +16,23 @@ const css = readFileSync(fileURLToPath(new URL('../src/client/WorkspaceBrowser.m
* @param selector - one exact selector, including a leading dot for local classes.
* @returns the rule's declarations, or undefined when no such rule exists.
*/
function declarations(selector: string): Map<string, string> | undefined {
const withoutComments = css.replace(/\/\*[\s\S]*?\*\//g, ' ')
function declarationsFrom(source: string, selector: string): Map<string, string> | undefined {
const withoutComments = source.replace(/\/\*[\s\S]*?\*\//g, ' ')
const found = new Map<string, string>()
for (const [, selectorList = '', body = ''] of withoutComments.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
if (!selectorList.split(',').map(value => value.trim()).includes(selector)) continue
const found = new Map<string, string>()
for (const part of body.split(';')) {
const colon = part.indexOf(':')
if (colon === -1) continue
found.set(part.slice(0, colon).trim(), part.slice(colon + 1).trim().replace(/\s+/g, ' '))
}
return found
}
return undefined
return found.size === 0 ? undefined : found
}
const declarations = (selector: string): Map<string, string> | undefined => declarationsFrom(css, selector)
const rowDeclarations = (selector: string): Map<string, string> | undefined => declarationsFrom(rowsCss, selector)
describe('WorkspaceBrowser.module.css list', () => {
const root = declarations('.root')
const listArea = declarations('.listArea')
@@ -68,4 +71,15 @@ describe('WorkspaceBrowser.module.css list', () => {
expect(declarations('.groupSection > * + *')?.get('margin-top')).toBe('2px')
expect(declarations('.groupSection + .groupSection')?.get('margin-top')).toBe('4px')
})
it('keeps the compact fade, overflow control, search field, and row heights', () => {
expect(declarations('.fade')?.get('height')).toBe('24px')
expect(declarations('.sessionOverflowButton')?.get('height')).toBe('28px')
expect(declarations('.searchExpanded')?.get('height')).toBe('34px')
expect(rowDeclarations('.projectRow')?.get('height')).toBe('34px')
expect(rowDeclarations('.sessionRow')?.get('height')).toBe('32px')
expect(rowDeclarations('.searchResultRow')?.get('min-height')).toBe('48px')
expect(rowDeclarations('.sessionRow.selected')?.get('background'))
.toBe('var(--dsw-alias-interactive-bg-hover)')
})
})

View File

@@ -46,7 +46,7 @@ function installClipboard(writeText: (text: string) => Promise<void>): () => voi
}
}
const dataTransfer = { effectAllowed: '', dropEffect: '' }
const dataTransfer = { effectAllowed: '', dropEffect: '', setData: vi.fn() }
/** jsdom lacks DragEvent — the fireEvent fallback drops clientY, so pin it on the built event. */
function fireDrag(row: HTMLElement, kind: 'dragOver' | 'drop', clientY: number): void {
@@ -105,7 +105,6 @@ describe('workspace browser rows', () => {
}
render(<ProjectRowItem group={group} onToggle={onToggle} onCreate={onCreate} t={t} />)
expect(screen.getByText('1 个会话')).toBeTruthy()
expect(screen.getByRole('treeitem').getAttribute('aria-expanded')).toBe('true')
fireEvent.click(screen.getByRole('button', { name: '在“Project”中新建会话' }))
expect(onCreate).toHaveBeenCalledOnce()
@@ -117,7 +116,7 @@ describe('workspace browser rows', () => {
it('renders and opens a selected running Session row', () => {
const node: SessionNode = {
id: sid('session'), title: 'Session', blank: false, running: true,
runningSubagentCount: 0, completed: false, updatedAt: 0,
runningSubagentCount: 0, completed: false, createdAt: 0, updatedAt: 0,
}
const onOpen = vi.fn()
render(
@@ -138,7 +137,7 @@ describe('workspace browser rows', () => {
<SessionNodeItem
node={{
id: sid('s1'), title: 'One', blank: false, running: false,
runningSubagentCount: 0, completed: false, updatedAt: 0, ...over,
runningSubagentCount: 0, completed: false, createdAt: 0, updatedAt: 0, ...over,
}}
currentId={undefined} now={0} onOpen={vi.fn()}
onRename={vi.fn()} onFork={vi.fn()} onArchive={vi.fn()} t={t}
@@ -170,7 +169,7 @@ describe('workspace browser rows', () => {
try {
const node: SessionNode = {
id: sid('owner'), title: 'Delegating', blank: false, running: false,
runningSubagentCount: 2, completed: false, updatedAt: 0,
runningSubagentCount: 2, completed: false, createdAt: 0, updatedAt: 0,
}
render(<SessionNodeItem node={node} currentId={undefined} now={0} onOpen={vi.fn()}
onRename={vi.fn()} onFork={vi.fn()} onArchive={vi.fn()} t={t} />)
@@ -192,7 +191,7 @@ describe('workspace browser rows', () => {
try {
const node: SessionNode = {
id: sid('owner'), title: 'Delegating', blank: false, running: true,
runningSubagentCount: 1, completed: false, updatedAt: 0,
runningSubagentCount: 1, completed: false, createdAt: 0, updatedAt: 0,
}
render(<SessionNodeItem node={node} currentId={undefined} now={0} onOpen={vi.fn()}
onRename={vi.fn()} onFork={vi.fn()} onArchive={vi.fn()} t={t} />)
@@ -213,7 +212,7 @@ describe('workspace browser rows', () => {
it('keeps child activity as a secondary status while user attention is primary', () => {
const node: SessionNode = {
id: sid('owner'), title: 'Needs input', blank: false, pendingInteraction: 'question',
running: false, runningSubagentCount: 1, completed: false, updatedAt: 0,
running: false, runningSubagentCount: 1, completed: false, createdAt: 0, updatedAt: 0,
}
render(<SessionNodeItem node={node} currentId={undefined} now={0} onOpen={vi.fn()}
onRename={vi.fn()} onFork={vi.fn()} onArchive={vi.fn()} t={t} />)
@@ -304,7 +303,7 @@ describe('workspace browser rows', () => {
try {
const node: SessionNode = {
id: sid('s-blank'), title: 'ignored', blank: true, running: false,
runningSubagentCount: 0, completed: false, updatedAt: 0,
runningSubagentCount: 0, completed: false, createdAt: 0, updatedAt: 0,
}
render(<SessionNodeItem node={node} currentId={node.id} now={0} onOpen={vi.fn()}
onRename={vi.fn()} onFork={vi.fn()} onArchive={vi.fn()} t={t} />)
@@ -331,7 +330,7 @@ describe('workspace browser rows', () => {
const onArchive = vi.fn()
const node: SessionNode = {
id: sid('s1'), title: 'One', blank: false, running: false,
runningSubagentCount: 0, completed: false, updatedAt: 0,
runningSubagentCount: 0, completed: false, createdAt: 0, updatedAt: 0,
}
render(<SessionNodeItem node={node} currentId={undefined} now={0} onOpen={onOpen}
onRename={onRename} onFork={onFork} onArchive={onArchive} t={t} />)
@@ -365,7 +364,7 @@ describe('workspace browser rows', () => {
try {
const node: SessionNode = {
id: sid('s1'), title: 'Hovered', blank: false, running: true,
runningSubagentCount: 0, completed: false, updatedAt: 0,
runningSubagentCount: 0, completed: false, createdAt: 0, updatedAt: 0,
}
render(<SessionNodeItem node={node} currentId={undefined} now={60_000} onOpen={vi.fn()}
onRename={vi.fn()} onFork={vi.fn()} onArchive={vi.fn()} t={t} />)
@@ -396,7 +395,7 @@ describe('workspace browser rows', () => {
try {
const node: SessionNode = {
id: sid(pendingInteraction), title: 'Needs input', blank: false,
pendingInteraction, running: true, runningSubagentCount: 0, completed: false, updatedAt: 0,
pendingInteraction, running: true, runningSubagentCount: 0, completed: false, createdAt: 0, updatedAt: 0,
}
const view = render(<SessionNodeItem node={node} currentId={undefined} now={0} onOpen={vi.fn()}
onRename={vi.fn()} onFork={vi.fn()} onArchive={vi.fn()} t={t} />)
@@ -423,7 +422,7 @@ describe('workspace browser rows', () => {
try {
const node: SessionNode = {
id: sid('s1'), title: 'Quiet', blank: false, running: false,
runningSubagentCount: 0, completed: false, updatedAt: 0,
runningSubagentCount: 0, completed: false, createdAt: 0, updatedAt: 0,
}
render(<SessionNodeItem node={node} currentId={undefined} now={0} onOpen={vi.fn()}
onRename={vi.fn()} onFork={vi.fn()} onArchive={vi.fn()} t={t} />)
@@ -441,7 +440,7 @@ describe('workspace browser rows', () => {
try {
const node: SessionNode = {
id: sid('s1'), title: 'Done', blank: false, running: false,
runningSubagentCount: 0, completed: true, updatedAt: 0,
runningSubagentCount: 0, completed: true, createdAt: 0, updatedAt: 0,
}
render(<SessionNodeItem node={node} currentId={undefined} now={0} onOpen={vi.fn()}
onRename={vi.fn()} onFork={vi.fn()} onArchive={vi.fn()} t={t} />)
@@ -457,7 +456,7 @@ describe('workspace browser rows', () => {
it('draggable row wires start/end and gates hover/drop on an active same-group drag', () => {
const node: SessionNode = {
id: sid('s1'), title: 'Drag me', blank: false, running: false,
runningSubagentCount: 0, completed: false, updatedAt: 0,
runningSubagentCount: 0, completed: false, createdAt: 0, updatedAt: 0,
}
const inactive = dragProps()
const { rerender } = render(

View File

@@ -11,7 +11,8 @@ import { createWorkspaceViewStore } from '../src/client/stores.ts'
const sid = (id: string) => id as SessionId
const wid = (id: string) => id as WorkspaceId
const summary = (id: string, updatedAt: number, cwd?: string): SessionSummary => ({
id: sid(id), displayTitle: id, running: false, blank: false, updatedAt, ...(cwd === undefined ? {} : { cwd }),
id: sid(id), displayTitle: id, running: false, blank: false,
createdAt: updatedAt, updatedAt, ...(cwd === undefined ? {} : { cwd }),
})
const list = (...items: SessionSummary[]): SessionListState => ({
ids: items.map(item => item.id),
@@ -377,11 +378,22 @@ describe('deriveSearchResults', () => {
})
describe('createWorkspaceViewStore', () => {
it('defaults to workspace grouping; setGroupBy is the sole mutation', () => {
it('stores grouping, ordering, Workspace expansion, and recent-session view order', () => {
const store = createWorkspaceViewStore().create()
expect(store.getSnapshot().groupBy).toBe('workspace')
expect(store.getSnapshot().orderBy).toBe('manual')
store.actions.setGroupBy('flat')
store.actions.setOrderBy('updated')
store.actions.setWorkspaceExpanded('alpha', true)
store.actions.syncRecentSessions('alpha', ['two', 'one'], { one: 1, two: 2 })
store.actions.setRecentSessionOrder('alpha', ['one', 'two'])
expect(store.getSnapshot().groupBy).toBe('flat')
expect(store.getSnapshot()).toMatchObject({
orderBy: 'updated',
workspaceExpansion: { alpha: true },
recentSessionOrder: { alpha: ['one', 'two'] },
recentSessionUpdatedAt: { alpha: { one: 1, two: 2 } },
})
})
})

View File

@@ -22,7 +22,7 @@ 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 => ({
id: sid(id), displayTitle: id, running: false, blank: false, updatedAt, ...overrides,
id: sid(id), displayTitle: id, running: false, blank: false, createdAt: updatedAt, updatedAt, ...overrides,
})
const sessionState = (items: readonly SessionSummary[], overrides: Partial<SessionListState> = {}): SessionListState => ({
ids: items.map(item => item.id),
@@ -53,6 +53,10 @@ function fireDrag(row: HTMLElement, kind: 'dragOver' | 'drop', clientY: number):
fireEvent(row, event)
}
function dragData(): Pick<DataTransfer, 'effectAllowed' | 'dropEffect' | 'setData'> {
return { effectAllowed: 'uninitialized', dropEffect: 'none', setData: vi.fn() }
}
function mount(overrides: Partial<WorkspaceBrowserProps> = {}) {
const store = createWorkspaceViewStore().create()
const props: WorkspaceBrowserProps = {
@@ -71,6 +75,7 @@ function mount(overrides: Partial<WorkspaceBrowserProps> = {}) {
renameWorkspace: vi.fn(async () => {}),
deleteWorkspace: vi.fn(async () => {}),
archiveSession: vi.fn(async () => {}),
insertWorkspaceBefore: vi.fn(async () => {}),
insertSessionBefore: vi.fn(async () => {}),
createWorkspace: vi.fn(async () => workspace('created', [])),
useDirectoryFlow: bindSnapshotSelector({ getSnapshot: () => true, subscribe: () => () => {} }),
@@ -102,6 +107,8 @@ describe('WorkspaceBrowser', () => {
fireEvent.click(screen.getByRole('button', { name: '分组方式' }))
expect(screen.getByText('分组方式')).toBeTruthy() // the menu heading label
expect(screen.getByRole('menuitem', { name: '手动排序' }).querySelector('svg')).toBeTruthy()
expect(screen.queryByText('创建时间')).toBeNull()
fireEvent.click(screen.getByRole('menuitem', { name: '单列表' }))
// Store-driven flip: title changes, rows flatten newest-first, headers gone.
expect(b.store.getSnapshot().groupBy).toBe('flat')
@@ -138,6 +145,61 @@ describe('WorkspaceBrowser', () => {
expect(screen.queryByText('alpha-s')).toBeNull()
})
it('shows five sessions by default and clears transient show-all when the Workspace collapses', () => {
const items = Array.from({ length: 7 }, (_, index) => summary(`session-${index + 1}`, 7 - index))
const b = mount({
useSessions: hook(sessionState(items)),
useWorkspaces: hook(workspaceState([workspace('alpha', items.map(item => item.id))])),
})
fireEvent.click(screen.getByText('alpha'))
for (const item of items.slice(0, 5)) expect(screen.getByText(item.displayTitle)).toBeTruthy()
expect(screen.queryByText('session-6')).toBeNull()
expect(screen.queryByText('session-7')).toBeNull()
fireEvent.click(screen.getByRole('button', { name: '展开其余 2 个会话' }))
expect(screen.getByText('session-6')).toBeTruthy()
expect(screen.getByText('session-7')).toBeTruthy()
expect(screen.getByRole('button', { name: '收起' })).toBeTruthy()
fireEvent.click(screen.getByText('alpha'))
expect(b.store.getSnapshot().workspaceExpansion).toEqual({ alpha: false })
fireEvent.click(screen.getByText('alpha'))
expect(b.store.getSnapshot().workspaceExpansion).toEqual({ alpha: true })
expect(screen.queryByText('session-6')).toBeNull()
expect(screen.getByRole('button', { name: '展开其余 2 个会话' })).toBeTruthy()
})
it('keeps recent-update order editable and promotes a Session when its timestamp advances', async () => {
const initial = sessionState([summary('one', 3), summary('two', 2)])
const b = mount({
useSessions: hook(initial),
useWorkspaces: hook(workspaceState([workspace('alpha', ['two', 'one'])])),
})
fireEvent.click(screen.getByText('alpha'))
fireEvent.click(screen.getByRole('button', { name: '分组方式' }))
fireEvent.click(screen.getByRole('menuitem', { name: '最近更新' }))
await waitFor(() => {
const rows = screen.getAllByRole('treeitem').slice(1)
expect(rows[0]?.textContent).toContain('one')
expect(rows[1]?.textContent).toContain('two')
})
const [one, two] = screen.getAllByRole('treeitem').slice(1) as [HTMLElement, HTMLElement]
two.getBoundingClientRect = () => ({
top: 150, bottom: 184, left: 0, right: 200, width: 200, height: 34, x: 0, y: 150, toJSON: () => ({}),
})
fireEvent.dragStart(one, { dataTransfer: dragData() })
fireDrag(two, 'drop', 180)
expect(b.store.getSnapshot().recentSessionOrder.alpha).toEqual(['two', 'one'])
const updated = sessionState([summary('one', 4), summary('two', 2)])
rerender(b, { useSessions: hook(updated) })
await waitFor(() => {
expect(b.store.getSnapshot().recentSessionOrder.alpha).toEqual(['one', 'two'])
expect(screen.getAllByRole('treeitem').slice(1)[0]?.textContent).toContain('one')
})
})
it('archives a session from the row menu and hides archived rows in both modes', async () => {
const archiveSession = vi.fn(async () => {})
const b = mount({
@@ -150,10 +212,9 @@ describe('WorkspaceBrowser', () => {
fireEvent.click(screen.getByRole('menuitem', { name: '归档会话' }))
expect(archiveSession).toHaveBeenCalledWith(sid('gone-s'))
// The archive-set echo hides the row in grouped mode (count included) and flat mode.
// The archive-set echo hides the row in grouped and flat modes.
rerender(b, { useWorkspaces: hook(workspaceState([workspace('alpha', ['kept-s', 'gone-s'])], [sid('gone-s')])) })
expect(screen.queryByText('gone-s')).toBeNull()
expect(screen.getByText('1 个会话')).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: '分组方式' }))
fireEvent.click(screen.getByRole('menuitem', { name: '单列表' }))
expect(screen.getByText('kept-s')).toBeTruthy()
@@ -253,7 +314,6 @@ describe('WorkspaceBrowser', () => {
expect(screen.getByText('新会话')).toBeTruthy()
expect(screen.queryByText('alpha-blank')).toBeNull()
expect(screen.queryByText('beta-blank')).toBeNull()
expect(screen.getByText('1 个会话')).toBeTruthy()
rerender(b, { useSessions: hook({ ...sessions, current: staleBlank.id }) })
expect(screen.getAllByText('新会话')).toHaveLength(1)
@@ -279,6 +339,7 @@ describe('WorkspaceBrowser', () => {
useSessions: hook(sessions),
useWorkspaces: hook(workspaceState([workspace('alpha', ['needle-row', 'other-row'])])),
})
fireEvent.click(screen.getByRole('button', { name: '搜索会话' }))
const input = screen.getByPlaceholderText<HTMLInputElement>('搜索名称、关键词…')
fireEvent.change(input, { target: { value: 'needle' } })
const resultTree = screen.getByRole('tree', { name: '搜索结果' })
@@ -302,6 +363,22 @@ describe('WorkspaceBrowser', () => {
}
})
it('collapses an empty search on outside click but keeps a non-empty query expanded', () => {
mount()
const search = screen.getByRole('button', { name: '搜索会话' })
fireEvent.click(search)
expect(search.getAttribute('aria-expanded')).toBe('true')
fireEvent.click(document.body)
expect(search.getAttribute('aria-expanded')).toBe('false')
fireEvent.click(search)
const input = screen.getByPlaceholderText<HTMLInputElement>('搜索名称、关键词…')
fireEvent.change(input, { target: { value: 'kept' } })
fireEvent.click(document.body)
expect(search.getAttribute('aria-expanded')).toBe('true')
expect(input.value).toBe('kept')
})
it('adds Host content hits with context, shows the result bound, and opens without clearing the query', async () => {
vi.useFakeTimers()
try {
@@ -524,6 +601,34 @@ describe('WorkspaceBrowser', () => {
expect(screen.getByText('alpha')).toBeTruthy()
})
it('uses the full expanded Workspace section when resolving a Workspace drop half', () => {
const insertWorkspaceBefore = vi.fn(async () => {})
const sessions = sessionState(Array.from({ length: 5 }, (_, index) => summary(`beta-${index}`, index)))
mount({
useSessions: hook(sessions),
useWorkspaces: hook(workspaceState([
workspace('alpha', []),
workspace('beta', sessions.ids),
workspace('tail', []),
])),
insertWorkspaceBefore,
})
fireEvent.click(screen.getByText('beta'))
const source = screen.getByText('tail').closest('[role="treeitem"]') as HTMLElement
let targetSection = screen.getByText('beta').closest('[role="treeitem"]')?.parentElement as HTMLElement
while (targetSection.parentElement?.getAttribute('role') !== 'tree') {
targetSection = targetSection.parentElement as HTMLElement
}
targetSection.getBoundingClientRect = () => ({
top: 100, bottom: 300, left: 0, right: 200, width: 200, height: 200, x: 0, y: 100, toJSON: () => ({}),
})
fireEvent.dragStart(source, { dataTransfer: dragData() })
// y=190 is below the header row but still in the top half of the whole
// expanded section, so the target is before beta rather than after it.
fireDrag(targetSection, 'drop', 190)
expect(insertWorkspaceBefore).toHaveBeenCalledWith(wid('tail'), wid('beta'))
})
it('drag reorder reports the anchor to insertSessionBefore and skips no-op drops', () => {
const insertSessionBefore = vi.fn(async () => {})
const sessions = sessionState([summary('one', 3), summary('two', 2), summary('three', 1)])
@@ -538,7 +643,7 @@ describe('WorkspaceBrowser', () => {
three.getBoundingClientRect = () => ({
top: 200, bottom: 234, left: 0, right: 200, width: 200, height: 34, x: 0, y: 200, toJSON: () => ({}),
})
const dataTransfer = { effectAllowed: '', dropEffect: '' }
const dataTransfer = dragData()
fireEvent.dragStart(one, { dataTransfer })
// Drop on the top half of "three": insert one before three.
fireDrag(three, 'dragOver', 205)
@@ -569,7 +674,7 @@ describe('WorkspaceBrowser', () => {
})
fireEvent.click(screen.getByText('alpha'))
const one = screen.getByText('one').closest('[role="treeitem"]') as HTMLElement
fireEvent.dragStart(one, { dataTransfer: { effectAllowed: '', dropEffect: '' } })
fireEvent.dragStart(one, { dataTransfer: dragData() })
// The host dropped "one" from the workspace account while the drag is in
// flight: the source index is gone but the drop still resolves its anchor.
rerender(b, { useWorkspaces: hook(workspaceState([workspace('alpha', ['two'])])) })
@@ -594,7 +699,7 @@ describe('WorkspaceBrowser', () => {
two.getBoundingClientRect = () => ({
top: 150, bottom: 184, left: 0, right: 200, width: 200, height: 34, x: 0, y: 150, toJSON: () => ({}),
})
const dataTransfer = { effectAllowed: '', dropEffect: '' }
const dataTransfer = dragData()
fireEvent.dragStart(one, { dataTransfer })
fireEvent.dragEnd(one)
// The drag ended: rows no longer accept drops.
@@ -608,6 +713,28 @@ describe('WorkspaceBrowser', () => {
expect(insertSessionBefore).toHaveBeenCalledWith(wid('alpha'), sid('one'), undefined)
})
it('accepts a document-level drop and commits the last Session marker on drag end', () => {
const insertSessionBefore = vi.fn(async () => {})
mount({
useSessions: hook(sessionState([summary('one', 2), summary('two', 1)])),
useWorkspaces: hook(workspaceState([workspace('alpha', ['one', 'two'])])),
insertSessionBefore,
})
fireEvent.click(screen.getByText('alpha'))
const [one, two] = screen.getAllByRole('treeitem').slice(1) as [HTMLElement, HTMLElement]
two.getBoundingClientRect = () => ({
top: 150, bottom: 184, left: 0, right: 200, width: 200, height: 34, x: 0, y: 150, toJSON: () => ({}),
})
fireEvent.dragStart(one, { dataTransfer: dragData() })
fireDrag(two, 'dragOver', 180)
const outsideDrop = createEvent.drop(document.body)
Object.defineProperty(outsideDrop, 'dataTransfer', { value: dragData() })
fireEvent(document.body, outsideDrop)
expect(outsideDrop.defaultPrevented).toBe(true)
fireEvent.dragEnd(one)
expect(insertSessionBefore).toHaveBeenCalledWith(wid('alpha'), sid('one'), undefined)
})
it('logs and keeps the order when the reorder call rejects', async () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
try {
@@ -623,7 +750,7 @@ describe('WorkspaceBrowser', () => {
two.getBoundingClientRect = () => ({
top: 150, bottom: 184, left: 0, right: 200, width: 200, height: 34, x: 0, y: 150, toJSON: () => ({}),
})
const dataTransfer = { effectAllowed: '', dropEffect: '' }
const dataTransfer = dragData()
fireEvent.dragStart(one, { dataTransfer })
fireDrag(two, 'drop', 180)
await waitFor(() => { expect(warn).toHaveBeenCalledWith('session reorder rejected:', expect.any(Error)) })