fix(client): refine workspace view controls
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/ui-workspace/README.md
|
||||
README.md: f54b8b0b2070c81089be1703b536492522d38774
|
||||
README.zh.md: b1977e7f6a67fa7bcb6751d7b214cfff4bfdbb4f
|
||||
README.md: 24d73beba56f527eb57276accb2693255bad9020
|
||||
README.zh.md: 85a5ce9210dd084e6e6e0746eb19aac12d98d54d
|
||||
@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
|
||||
|
||||
Shared Workspace browser and picker plugin. `WorkspaceBrowser` fills the sidebar's `sidebar.workspaces` slot, while `WorkspacePicker` fills the page-local Session Intent hero's `conversation.hero.workspace` slot; both surfaces use the same Workspace menu and add flow.
|
||||
|
||||
The browser renders grouped or flat Session rows from the global runtime hooks and owns Workspace add/rename/reorder plus in-Workspace Session reorder. A Workspace remembers whether it is closed or showing Sessions; an open Workspace shows five Sessions by default, offers a transient **Show more** control for the remainder, and returns to five after the whole Workspace is closed and reopened. View options combine grouping with Session order: **Manual** follows the Host Workspace account, while **Last updated** keeps a browser-local editable order and moves a Session to the front whenever a newer `updatedAt` arrives. Workspace drag order is Host-durable in either Session order mode.
|
||||
The browser renders grouped or flat Session rows from the global runtime hooks and owns Workspace add/rename/reorder plus in-Workspace Session reorder. A Workspace remembers whether it is closed or showing Sessions; an open Workspace shows five Sessions by default, offers a transient **Show more** control for the remainder, and returns to five after the whole Workspace is closed and reopened. View options combine grouping with one browser-persisted Session order: entering **Last updated** performs a complete recency sort and later user prompts or steers promote their Session once, while entering **Manual** preserves every current position and disables later promotion. Dragging edits the current order in either mode; Manual-mode drags also update the Host Workspace account. Workspace drag order is Host-durable in either Session order mode.
|
||||
|
||||
Collapsed search is one header action beside the view and add actions. Activating it expands the field across the header; an outside click collapses only an empty query, while the clear control always resets and collapses it. A non-blank search query replaces either browsing mode with one flat result list: case-insensitive title and Workspace substring matches appear immediately, while a 250 ms debounced Host request adds ranked current-conversation content matches and snippets. The English search input and its defensive request path remove NUL, cap the query at the wire schema's 500 UTF-16 code units without splitting a surrogate pair, and preserve the existing debounce and cancellation behavior. Each new query aborts the preceding request; a failed content search leaves metadata matches visible with a warning. The list is capped at 20, asks the user to narrow broader queries, and opens the selected Session without clearing the query or jumping to a specific event.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
共享 Workspace 浏览器与选择器插件。`WorkspaceBrowser` 填充侧边栏的 `sidebar.workspaces` slot,`WorkspacePicker` 则填充页面局部 Session Intent 主视觉区的 `conversation.hero.workspace` slot;两个界面使用同一套 Workspace 菜单和添加流程。
|
||||
|
||||
该浏览器通过全局运行时钩子将 Session 行渲染为分组或扁平形式,并负责 Workspace 添加/重命名/重排序以及 Workspace 内的 Session 重排序。每个 Workspace 会记住自身是关闭还是显示 Session;打开后默认显示五条 Session,其余条目通过临时的**展开其余**控件显示,而关闭并重新打开整个 Workspace 后会恢复为五条。视图选项把分组方式和 Session 顺序放在一起:**手动排序**遵循 Host Workspace 记账顺序,**最近更新**则维护可编辑的浏览器本地顺序,并在收到更大的 `updatedAt` 时把该 Session 移到首位。无论采用哪种 Session 顺序,Workspace 拖拽顺序都由 Host 持久化。
|
||||
该浏览器通过全局运行时钩子将 Session 行渲染为分组或扁平形式,并负责 Workspace 添加/重命名/重排序以及 Workspace 内的 Session 重排序。每个 Workspace 会记住自身是关闭还是显示 Session;打开后默认显示五条 Session,其余条目通过临时的**展开其余**控件显示,而关闭并重新打开整个 Workspace 后会恢复为五条。视图选项把分组方式和一份浏览器持久化的 Session 顺序放在一起:进入**最近更新**时执行一次完整的时间排序,后续 user prompt 或 steer 会将对应 Session 置顶一次;进入**手动排序**则保留所有当前位置并停用后续置顶。两种模式下的拖拽都会编辑当前顺序,手动模式下的拖拽还会更新 Host Workspace 记账。无论采用哪种 Session 顺序,Workspace 拖拽顺序都由 Host 持久化。
|
||||
|
||||
折叠搜索是视图和添加操作旁的一枚区头按钮。激活后,输入框会扩展并占据区头;点击外部只会收起空查询,而清除控件总会重置并收起搜索。非空白查询会以单一扁平结果列表替代任一浏览模式:不区分大小写的标题和 Workspace 子串匹配项会立即显示,经 250 ms 防抖的 Host 请求则会加入经过排序的当前对话内容匹配项及其摘要片段。英文搜索输入框及其防御性请求路径会移除 NUL,将查询限制在传输 schema 规定的 500 个 UTF-16 代码单元内且不会拆分代理项对,并保留现有的防抖与取消行为。每次新查询都会中止前一个请求;内容搜索失败时,元数据匹配项仍会显示,同时给出警告。列表最多显示 20 条结果,并会在查询过宽时提示用户缩小范围;打开所选 Session 时既不会清除查询,也不会跳转至特定事件。
|
||||
|
||||
|
||||
@@ -118,11 +118,11 @@ function ViewOptionsMenu({ groupBy, orderBy, onGroupPick, onOrderPick, t }: {
|
||||
// be cut off at the header's bounds.
|
||||
portal
|
||||
anchor={(
|
||||
<Tooltip label={t('groupBy.label')} side="bottom" delayMs={500}>
|
||||
<Tooltip label={t('viewOptions.label')} side="bottom" delayMs={500}>
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(css.iconButton, css.wide)}
|
||||
aria-label={t('groupBy.label')}
|
||||
aria-label={t('viewOptions.label')}
|
||||
onClick={() => { setOpen(v => !v) }}
|
||||
>
|
||||
<IconPersonalizationOutline16 />
|
||||
@@ -163,13 +163,13 @@ type SessionTreeProps = Pick<
|
||||
workspaceExpansion: Readonly<Record<string, boolean>>
|
||||
/** Persist one Workspace group's zero-or-five-session state. */
|
||||
setWorkspaceExpanded: (key: string, expanded: boolean) => void
|
||||
/** Editable orders used by recent-update mode. */
|
||||
/** Shared editable orders used by both Session order modes. */
|
||||
recentSessionOrder: Readonly<Record<string, readonly string[]>>
|
||||
/** Last update timestamps observed by recent-update mode. */
|
||||
/** Last update timestamps observed for one-time recent-update promotions. */
|
||||
recentSessionUpdatedAt: Readonly<Record<string, Readonly<Record<string, number>>>>
|
||||
/** Replace one recent-mode order and its observed timestamps. */
|
||||
/** Replace one shared order and its observed timestamps. */
|
||||
syncRecentSessions: (workspaceKey: string, order: string[], updatedAt: Record<string, number>) => void
|
||||
/** Apply a manual drag inside one recent-mode order. */
|
||||
/** Apply a drag to one shared order. */
|
||||
setRecentSessionOrder: (workspaceKey: string, order: string[]) => void
|
||||
/** Registry-global archive set (hidden rows). */
|
||||
archivedSessionIds: readonly SessionNode['id'][]
|
||||
@@ -181,7 +181,7 @@ type SessionTreeProps = Pick<
|
||||
onSessionRename: (sessionId: SessionNode['id'], currentTitle: string) => void
|
||||
/** Archive a session (row menu action; the row disappears on the state echo). */
|
||||
onSessionArchive: (sessionId: SessionNode['id']) => void
|
||||
/** Session visual order; manual mode drags durable order, updated mode drags its view order. */
|
||||
/** Session order behavior: fixed after edits, or additionally promoted by user activity. */
|
||||
orderBy: SessionOrderBy
|
||||
}
|
||||
|
||||
@@ -201,6 +201,7 @@ function SessionTree({
|
||||
const sessionDropCommitted = useRef(false)
|
||||
const [workspaceDrag, setWorkspaceDrag] = useState<WorkspaceDragState | null>(null)
|
||||
const workspaceDropCommitted = useRef(false)
|
||||
const previousOrderBy = useRef(orderBy)
|
||||
const nativeDragActive = drag !== null || workspaceDrag !== null
|
||||
useEffect(() => {
|
||||
if (!nativeDragActive) return
|
||||
@@ -232,16 +233,18 @@ function SessionTree({
|
||||
[workspaceExpansion],
|
||||
)
|
||||
useEffect(() => {
|
||||
if (orderBy !== 'updated' || list.phase !== 'ready') return
|
||||
if (list.phase !== 'ready') return
|
||||
const switchedToUpdated = previousOrderBy.current !== 'updated' && orderBy === 'updated'
|
||||
previousOrderBy.current = orderBy
|
||||
for (const workspace of workspaces) {
|
||||
const key = workspace.workspaceId as string
|
||||
const sessionIds = workspace.sessionIds.filter(id => list.byId[id] !== undefined)
|
||||
const previousOrder = recentSessionOrder[key]
|
||||
const previousUpdatedAt = recentSessionUpdatedAt[key] ?? {}
|
||||
let nextOrder = reconciledSessionOrder(sessionIds, previousOrder)
|
||||
if (previousOrder === undefined) {
|
||||
if (orderBy === 'updated' && (previousOrder === undefined || switchedToUpdated)) {
|
||||
nextOrder.sort((a, b) => compareSessionRecency(a, b, list.byId))
|
||||
} else {
|
||||
} else if (orderBy === 'updated') {
|
||||
const promoted = sessionIds
|
||||
.filter((id) => {
|
||||
const session = list.byId[id]
|
||||
@@ -270,14 +273,12 @@ function SessionTree({
|
||||
}
|
||||
}, [list, orderBy, recentSessionOrder, recentSessionUpdatedAt, syncRecentSessions, workspaces])
|
||||
const orderedWorkspaces = useMemo(() => {
|
||||
if (orderBy !== 'updated') return workspaces
|
||||
return workspaces.map((workspace) => {
|
||||
const stored = recentSessionOrder[workspace.workspaceId as string]
|
||||
const sessionIds = reconciledSessionOrder(workspace.sessionIds, stored)
|
||||
if (stored === undefined) sessionIds.sort((a, b) => compareSessionRecency(a, b, list.byId))
|
||||
return { ...workspace, sessionIds }
|
||||
})
|
||||
}, [list.byId, orderBy, recentSessionOrder, workspaces])
|
||||
}, [recentSessionOrder, workspaces])
|
||||
const groups = useMemo(
|
||||
() => deriveGroups(list, orderedWorkspaces, archivedSessionIds, { expandedProjects }, 'manual'),
|
||||
[list, orderedWorkspaces, archivedSessionIds, expandedProjects],
|
||||
@@ -846,7 +847,7 @@ export function WorkspaceBrowser({
|
||||
searchInput.current?.focus()
|
||||
}}
|
||||
>
|
||||
<Tooltip label={t('search')} disabled={searchExpanded}>
|
||||
<Tooltip label={t('search')} side="bottom" delayMs={500} disabled={searchExpanded}>
|
||||
<button
|
||||
type="button"
|
||||
className={css.searchButton}
|
||||
|
||||
@@ -10,6 +10,7 @@ export const zh = {
|
||||
'session.new': '新会话',
|
||||
'section.workspaces': '工作区',
|
||||
'section.sessions': '会话',
|
||||
'viewOptions.label': '视图选项',
|
||||
'groupBy.label': '分组方式',
|
||||
'groupBy.workspace': '按工作区',
|
||||
'groupBy.flat': '单列表',
|
||||
@@ -22,7 +23,7 @@ export const zh = {
|
||||
'empty.noMatches': '无匹配结果',
|
||||
'workspace.add': '添加工作区',
|
||||
'search.sessions.aria': '搜索会话',
|
||||
'search.placeholder': '搜索名称、关键词…',
|
||||
'search.placeholder': '搜索会话…',
|
||||
'search.clear': '清除搜索',
|
||||
'search.results.aria': '搜索结果',
|
||||
'search.pending': '正在搜索会话历史…',
|
||||
@@ -78,6 +79,7 @@ export const en = {
|
||||
'session.new': 'New Session',
|
||||
'section.workspaces': 'Workspaces',
|
||||
'section.sessions': 'Sessions',
|
||||
'viewOptions.label': 'View options',
|
||||
'groupBy.label': 'Group by',
|
||||
'groupBy.workspace': 'WorkSpace',
|
||||
'groupBy.flat': 'In one list',
|
||||
@@ -90,7 +92,7 @@ export const en = {
|
||||
'empty.noMatches': 'No matches',
|
||||
'workspace.add': 'Add workspace',
|
||||
'search.sessions.aria': 'Search sessions',
|
||||
'search.placeholder': 'Search name, keywords...',
|
||||
'search.placeholder': 'Search sessions...',
|
||||
'search.clear': 'Clear search',
|
||||
'search.results.aria': 'Search results',
|
||||
'search.pending': 'Searching session history…',
|
||||
|
||||
@@ -9,7 +9,7 @@ import { defineStore, type EngineStoreHandle } from '@deepseek-ai/dsh-client-run
|
||||
|
||||
/** Session-list grouping mode: workspace sections or one flat recency list. */
|
||||
export type WorkspaceGroupBy = 'workspace' | 'flat'
|
||||
/** Session order: durable Workspace order or an activity-promoted editable order. */
|
||||
/** Session order: user-arranged only, or user-arranged plus activity promotion. */
|
||||
export type WorkspaceOrderBy = 'manual' | 'updated'
|
||||
|
||||
/** Workspace browser viewing state persisted across surface remounts and reloads. */
|
||||
@@ -18,9 +18,9 @@ type WorkspaceViewState = {
|
||||
orderBy: WorkspaceOrderBy
|
||||
/** Explicit zero-or-five-session state keyed by Workspace group identity. */
|
||||
workspaceExpansion: Record<string, boolean>
|
||||
/** Editable per-Workspace order used by recent-update mode. */
|
||||
/** Shared editable per-Workspace order; recent-update mode may promote rows within it. */
|
||||
recentSessionOrder: Record<string, string[]>
|
||||
/** Last observed update timestamps used to detect promotion events. */
|
||||
/** Last observed update timestamps used to detect one-time promotion events. */
|
||||
recentSessionUpdatedAt: Record<string, Record<string, number>>
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ describe('WorkspaceBrowser', () => {
|
||||
// Sessions hidden while their group is folded.
|
||||
expect(screen.queryByText('alpha-s')).toBeNull()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '分组方式' }))
|
||||
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()
|
||||
@@ -118,13 +118,13 @@ describe('WorkspaceBrowser', () => {
|
||||
expect(screen.getByText('beta-s')).toBeTruthy()
|
||||
|
||||
// Back to workspace grouping through the same menu.
|
||||
fireEvent.click(screen.getByRole('button', { name: '分组方式' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '视图选项' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '按工作区' }))
|
||||
expect(b.store.getSnapshot().groupBy).toBe('workspace')
|
||||
expect(screen.getByText('工作区')).toBeTruthy()
|
||||
|
||||
// Escape closes the menu without picking.
|
||||
fireEvent.click(screen.getByRole('button', { name: '分组方式' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '视图选项' }))
|
||||
fireEvent.keyDown(document, { key: 'Escape' })
|
||||
expect(screen.queryByRole('menu')).toBeNull()
|
||||
expect(b.store.getSnapshot().groupBy).toBe('workspace')
|
||||
@@ -169,14 +169,14 @@ describe('WorkspaceBrowser', () => {
|
||||
expect(screen.getByRole('button', { name: '展开其余 2 个会话' })).toBeTruthy()
|
||||
})
|
||||
|
||||
it('keeps recent-update order editable and promotes a Session when its timestamp advances', async () => {
|
||||
it('shares one editable order across modes and promotes only while Last updated is active', 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('button', { name: '视图选项' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '最近更新' }))
|
||||
await waitFor(() => {
|
||||
const rows = screen.getAllByRole('treeitem').slice(1)
|
||||
@@ -192,12 +192,44 @@ describe('WorkspaceBrowser', () => {
|
||||
fireDrag(two, 'drop', 180)
|
||||
expect(b.store.getSnapshot().recentSessionOrder.alpha).toEqual(['two', 'one'])
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '视图选项' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '手动排序' }))
|
||||
expect(screen.getAllByRole('treeitem').slice(1)[0]?.textContent).toContain('two')
|
||||
|
||||
// User activity updates the timestamp baseline in Manual mode without
|
||||
// changing the shared visual order.
|
||||
const updated = sessionState([summary('one', 4), summary('two', 2)])
|
||||
rerender(b, { useSessions: hook(updated) })
|
||||
await waitFor(() => {
|
||||
expect(b.store.getSnapshot().recentSessionUpdatedAt.alpha).toEqual({ one: 4, two: 2 })
|
||||
})
|
||||
expect(b.store.getSnapshot().recentSessionOrder.alpha).toEqual(['two', 'one'])
|
||||
expect(screen.getAllByRole('treeitem').slice(1)[0]?.textContent).toContain('two')
|
||||
|
||||
// Entering Last updated performs one complete recency sort.
|
||||
fireEvent.click(screen.getByRole('button', { name: '视图选项' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '最近更新' }))
|
||||
await waitFor(() => {
|
||||
expect(b.store.getSnapshot().recentSessionOrder.alpha).toEqual(['one', 'two'])
|
||||
expect(screen.getAllByRole('treeitem').slice(1)[0]?.textContent).toContain('one')
|
||||
})
|
||||
|
||||
// A later user activity timestamp promotes that Session once while the
|
||||
// mode remains active.
|
||||
const promoted = sessionState([summary('one', 4), summary('two', 5)])
|
||||
rerender(b, { useSessions: hook(promoted) })
|
||||
await waitFor(() => {
|
||||
expect(b.store.getSnapshot().recentSessionOrder.alpha).toEqual(['two', 'one'])
|
||||
expect(screen.getAllByRole('treeitem').slice(1)[0]?.textContent).toContain('two')
|
||||
})
|
||||
|
||||
b.view.unmount()
|
||||
const restored = mount({
|
||||
useSessions: hook(promoted),
|
||||
useWorkspaces: hook(workspaceState([workspace('alpha', ['two', 'one'])])),
|
||||
})
|
||||
expect(restored.store.getSnapshot().recentSessionOrder.alpha).toEqual(['two', 'one'])
|
||||
expect(screen.getAllByRole('treeitem').slice(1)[0]?.textContent).toContain('two')
|
||||
})
|
||||
|
||||
it('archives a session from the row menu and hides archived rows in both modes', async () => {
|
||||
@@ -215,7 +247,7 @@ describe('WorkspaceBrowser', () => {
|
||||
// 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()
|
||||
fireEvent.click(screen.getByRole('button', { name: '分组方式' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '视图选项' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '单列表' }))
|
||||
expect(screen.getByText('kept-s')).toBeTruthy()
|
||||
expect(screen.queryByText('gone-s')).toBeNull()
|
||||
@@ -322,9 +354,9 @@ describe('WorkspaceBrowser', () => {
|
||||
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' } })
|
||||
fireEvent.change(screen.getByPlaceholderText('搜索会话…'), { target: { value: 'new session' } })
|
||||
expect(screen.queryByText('新会话')).toBeNull()
|
||||
fireEvent.change(screen.getByPlaceholderText('搜索名称、关键词…'), { target: { value: '新会话' } })
|
||||
fireEvent.change(screen.getByPlaceholderText('搜索会话…'), { target: { value: '新会话' } })
|
||||
expect(screen.queryByText('新会话')).toBeNull()
|
||||
})
|
||||
|
||||
@@ -340,7 +372,7 @@ describe('WorkspaceBrowser', () => {
|
||||
useWorkspaces: hook(workspaceState([workspace('alpha', ['needle-row', 'other-row'])])),
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: '搜索会话' }))
|
||||
const input = screen.getByPlaceholderText<HTMLInputElement>('搜索名称、关键词…')
|
||||
const input = screen.getByPlaceholderText<HTMLInputElement>('搜索会话…')
|
||||
fireEvent.change(input, { target: { value: 'needle' } })
|
||||
const resultTree = screen.getByRole('tree', { name: '搜索结果' })
|
||||
expect(screen.getByText('Needle row')).toBeTruthy()
|
||||
@@ -372,7 +404,7 @@ describe('WorkspaceBrowser', () => {
|
||||
expect(search.getAttribute('aria-expanded')).toBe('false')
|
||||
|
||||
fireEvent.click(search)
|
||||
const input = screen.getByPlaceholderText<HTMLInputElement>('搜索名称、关键词…')
|
||||
const input = screen.getByPlaceholderText<HTMLInputElement>('搜索会话…')
|
||||
fireEvent.change(input, { target: { value: 'kept' } })
|
||||
fireEvent.click(document.body)
|
||||
expect(search.getAttribute('aria-expanded')).toBe('true')
|
||||
@@ -397,7 +429,7 @@ describe('WorkspaceBrowser', () => {
|
||||
open,
|
||||
searchSessions,
|
||||
})
|
||||
const input = screen.getByPlaceholderText<HTMLInputElement>('搜索名称、关键词…')
|
||||
const input = screen.getByPlaceholderText<HTMLInputElement>('搜索会话…')
|
||||
fireEvent.change(input, { target: { value: 'waterfall token' } })
|
||||
expect(screen.getByText('正在搜索会话历史…')).toBeTruthy()
|
||||
expect(screen.queryByText('Research notes')).toBeNull()
|
||||
@@ -422,7 +454,7 @@ describe('WorkspaceBrowser', () => {
|
||||
try {
|
||||
const searchSessions = vi.fn(async () => ({ items: [], hasMore: false }))
|
||||
mount({ searchSessions })
|
||||
const input = screen.getByPlaceholderText<HTMLInputElement>('搜索名称、关键词…')
|
||||
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))
|
||||
@@ -453,7 +485,7 @@ describe('WorkspaceBrowser', () => {
|
||||
useWorkspaces: hook(workspaceState([workspace('alpha', ['local-hit'])])),
|
||||
searchSessions,
|
||||
})
|
||||
fireEvent.change(screen.getByPlaceholderText('搜索名称、关键词…'), {
|
||||
fireEvent.change(screen.getByPlaceholderText('搜索会话…'), {
|
||||
target: { value: 'needle' },
|
||||
})
|
||||
expect(screen.getByText('Needle title')).toBeTruthy()
|
||||
@@ -490,7 +522,7 @@ describe('WorkspaceBrowser', () => {
|
||||
])),
|
||||
searchSessions,
|
||||
})
|
||||
const input = screen.getByPlaceholderText('搜索名称、关键词…')
|
||||
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
|
||||
@@ -524,7 +556,7 @@ describe('WorkspaceBrowser', () => {
|
||||
? first
|
||||
: Promise.resolve({ items: [], hasMore: false }))
|
||||
mount({ searchSessions })
|
||||
const input = screen.getByPlaceholderText('搜索名称、关键词…')
|
||||
const input = screen.getByPlaceholderText('搜索会话…')
|
||||
fireEvent.change(input, { target: { value: 'first' } })
|
||||
await act(async () => { await vi.advanceTimersByTimeAsync(250) })
|
||||
|
||||
@@ -547,7 +579,7 @@ describe('WorkspaceBrowser', () => {
|
||||
b.store.actions.setGroupBy('flat')
|
||||
rerender(b, {})
|
||||
expect(screen.getByText('暂无会话')).toBeTruthy()
|
||||
fireEvent.change(screen.getByPlaceholderText('搜索名称、关键词…'), { target: { value: 'x' } })
|
||||
fireEvent.change(screen.getByPlaceholderText('搜索会话…'), { target: { value: 'x' } })
|
||||
expect(screen.getByText('正在搜索会话历史…')).toBeTruthy()
|
||||
await act(async () => { await vi.advanceTimersByTimeAsync(250) })
|
||||
expect(screen.getByText('无匹配会话')).toBeTruthy()
|
||||
@@ -563,12 +595,12 @@ describe('WorkspaceBrowser', () => {
|
||||
const b = mount({ wide: false, expandSidebar })
|
||||
// No wide chrome in rail state.
|
||||
expect(screen.queryByText('工作区')).toBeNull()
|
||||
expect(screen.queryByPlaceholderText('搜索名称、关键词…')).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('搜索名称、关键词…')
|
||||
const input = screen.getByPlaceholderText('搜索会话…')
|
||||
act(() => { vi.advanceTimersByTime(300) })
|
||||
expect(document.activeElement).toBe(input)
|
||||
// Wide search button is decorative (tabIndex -1, no expand call).
|
||||
@@ -933,7 +965,7 @@ describe('WorkspaceBrowser', () => {
|
||||
useSessions: hook(sessions),
|
||||
useWorkspaces: hook(workspaceState([workspace('alpha', ['needle-a'])])),
|
||||
})
|
||||
fireEvent.change(screen.getByPlaceholderText('搜索名称、关键词…'), { 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