fix(web): localize the trajectory toolbar and simplify the download
Toolbar strings route through the locale dictionary's standard t seat (the export button no longer mixes languages in the English golden), exportLog passes the response blob straight to the browser save instead of copying it three times, the client id sanitizer rejects dot segments like the host one, and the fixture stub comment no longer misattributes the 404.
This commit is contained in:
@@ -2834,9 +2834,9 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
|
||||
})
|
||||
return Promise.resolve({ accepted: true })
|
||||
},
|
||||
// The host-only streaming download has no in-memory counterpart: fixture
|
||||
// mode answers 404 so the export button's error bar explains the gap
|
||||
// instead of hanging.
|
||||
// Satisfies the ApiProxy contract type only: the browser export button
|
||||
// fetches GET /api/session.export directly (window.fetch), so this stub is
|
||||
// never reached through the fixture's dispatch.
|
||||
downloads: {
|
||||
sessionLog: () => Promise.resolve(new Response('fixture mode does not serve session export', { status: 404 })),
|
||||
},
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/** Trajectory toolbar: timeline and ledger fold controls. */
|
||||
|
||||
import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { IconSearchOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { NS } from './locales.ts'
|
||||
import css from './TrajectoryToolbar.module.css'
|
||||
|
||||
export interface TrajectoryToolbarProps {
|
||||
@@ -30,6 +32,8 @@ export interface TrajectoryToolbarProps {
|
||||
onExport: () => void
|
||||
/** Export failure message, shown while set; null while idle or successful. */
|
||||
exportError: string | null
|
||||
/** Translate a toolbar dictionary key. */
|
||||
t: TranslateNS<typeof NS>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,17 +55,18 @@ export function TrajectoryToolbar({
|
||||
exporting,
|
||||
onExport,
|
||||
exportError,
|
||||
t,
|
||||
}: TrajectoryToolbarProps) {
|
||||
return (
|
||||
<div className={css.root} role="toolbar" aria-label="Trajectory toolbar">
|
||||
<div className={css.root} role="toolbar" aria-label={t('toolbar.aria')}>
|
||||
<div className={css.inner}>
|
||||
<div className={css.actions}>
|
||||
<button
|
||||
type="button"
|
||||
className={css.toggle}
|
||||
aria-label="Use actual duration"
|
||||
aria-label={t('toolbar.useActualDuration')}
|
||||
aria-pressed={actualDuration}
|
||||
title={actualDuration ? 'Use equal-width operations' : 'Use actual duration'}
|
||||
title={actualDuration ? t('toolbar.useEqualWidth') : t('toolbar.useActualDuration')}
|
||||
onClick={() => { onActualDurationChange(!actualDuration) }}
|
||||
>
|
||||
<svg
|
||||
@@ -73,7 +78,7 @@ export function TrajectoryToolbar({
|
||||
<circle cx="8" cy="8" r="5.25" />
|
||||
<path d="M8 4.75V8l2.25 1.5" />
|
||||
</svg>
|
||||
Duration
|
||||
{t('toolbar.duration')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -83,7 +88,7 @@ export function TrajectoryToolbar({
|
||||
hidden
|
||||
onClick={() => { onActualTimeChange(!actualTime) }}
|
||||
>
|
||||
<span>Actual time</span>
|
||||
<span>{t('toolbar.actualTime')}</span>
|
||||
<span className={css.controlTrack} data-on={actualTime || undefined} aria-hidden="true">
|
||||
<span className={css.controlThumb} />
|
||||
</span>
|
||||
@@ -91,34 +96,34 @@ export function TrajectoryToolbar({
|
||||
<button
|
||||
type="button"
|
||||
className={css.action}
|
||||
aria-label={allTurnsCollapsed ? 'Expand turns' : 'Collapse turns'}
|
||||
aria-label={allTurnsCollapsed ? t('toolbar.expandTurns') : t('toolbar.collapseTurns')}
|
||||
aria-pressed={allTurnsCollapsed}
|
||||
title={allTurnsCollapsed ? 'Expand turns' : 'Collapse turns'}
|
||||
title={allTurnsCollapsed ? t('toolbar.expandTurns') : t('toolbar.collapseTurns')}
|
||||
onClick={onToggleAllTurns}
|
||||
>
|
||||
<span className={css.actionIcon} aria-hidden="true">
|
||||
{allTurnsCollapsed ? '⊞' : '⊟'}
|
||||
</span>
|
||||
Turns
|
||||
{t('toolbar.turns')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={css.action}
|
||||
aria-label={allAssistantsCollapsed ? 'Expand calls' : 'Collapse calls'}
|
||||
aria-label={allAssistantsCollapsed ? t('toolbar.expandCalls') : t('toolbar.collapseCalls')}
|
||||
aria-pressed={allAssistantsCollapsed}
|
||||
title={allAssistantsCollapsed ? 'Expand calls' : 'Collapse calls'}
|
||||
title={allAssistantsCollapsed ? t('toolbar.expandCalls') : t('toolbar.collapseCalls')}
|
||||
onClick={onToggleAllAssistants}
|
||||
>
|
||||
<span className={css.actionIcon} aria-hidden="true">
|
||||
{allAssistantsCollapsed ? '⊞' : '⊟'}
|
||||
</span>
|
||||
Calls
|
||||
{t('toolbar.calls')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={css.export}
|
||||
aria-label="导出会话日志"
|
||||
title={exportError ?? (exporting ? '导出中…' : '导出会话日志(ZIP,含子代理)')}
|
||||
aria-label={t('toolbar.exportAria')}
|
||||
title={exportError ?? (exporting ? t('toolbar.exporting') : t('toolbar.exportTitle'))}
|
||||
disabled={exporting}
|
||||
onClick={onExport}
|
||||
>
|
||||
@@ -126,7 +131,7 @@ export function TrajectoryToolbar({
|
||||
<path d="M8 3v7m0 0 3-3m-3 3L5 7" />
|
||||
<path d="M3 11.5V13a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-1.5" />
|
||||
</svg>
|
||||
导出
|
||||
{t('toolbar.export')}
|
||||
</button>
|
||||
</div>
|
||||
<div className={css.search}>
|
||||
@@ -134,8 +139,8 @@ export function TrajectoryToolbar({
|
||||
<input
|
||||
type="search"
|
||||
className={css.searchInput}
|
||||
aria-label="Search trajectory"
|
||||
placeholder="Search"
|
||||
aria-label={t('toolbar.search')}
|
||||
placeholder={t('toolbar.searchPlaceholder')}
|
||||
value={searchQuery}
|
||||
onChange={(event) => { onSearchQueryChange(event.currentTarget.value) }}
|
||||
/>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import type { ConvViewProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type { InjectFace } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type { InjectFace, PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type {
|
||||
AssistantBlock, AssistantMessageNode, ConversationContext, ConversationSnapshot,
|
||||
SessionHistoryFace, SnapshotStore,
|
||||
@@ -187,8 +187,8 @@ function mergeSearchMatches(
|
||||
|
||||
export function TrajectoryView({
|
||||
useHistory, useDuration, loadHistoryTail, loadOlderHistory, setActualDuration, exportLog,
|
||||
inspect, onInspectDone,
|
||||
}: ConvViewProps & InjectFace<TrajectoryViewInjected>) {
|
||||
inspect, onInspectDone, t,
|
||||
}: ConvViewProps & InjectFace<TrajectoryViewInjected> & PropsLocale<'trajectory'>) {
|
||||
const [collapsedTurns, setCollapsedTurns] = useState<ReadonlySet<number>>(EMPTY_TURN_IDS)
|
||||
const [collapsedAssistants, setCollapsedAssistants] =
|
||||
useState<ReadonlySet<string>>(EMPTY_RECORD_IDS)
|
||||
@@ -563,6 +563,7 @@ export function TrajectoryView({
|
||||
exporting={exporting}
|
||||
onExport={onExport}
|
||||
exportError={exportError}
|
||||
t={t}
|
||||
/>
|
||||
{exportError !== null && (
|
||||
<div className={css.exportError} role="alert">
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
|
||||
/**
|
||||
* Collapse an untrusted session id into one safe path/filename segment.
|
||||
* Distinct ids may collapse onto one segment (impossible for the host-minted
|
||||
* UUIDs, so no uniqueness suffix is kept).
|
||||
* @param id - the raw session id.
|
||||
* @returns a filesystem-safe single segment.
|
||||
*/
|
||||
function safeSessionIdSegment(id: string): string {
|
||||
return id.replace(/[^A-Za-z0-9._-]/g, '_')
|
||||
return id.replace(/[^A-Za-z0-9_-]/g, '_')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,16 +27,16 @@ export function sessionLogZipFilename(sessionId: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a browser download of raw bytes.
|
||||
* @param bytes - the file content.
|
||||
* Trigger a browser download of a blob response.
|
||||
* @param blob - the response body to save (passed straight through, no copy).
|
||||
* @param filename - the download filename.
|
||||
* @param type - the MIME type.
|
||||
*/
|
||||
export function downloadBytes(bytes: Uint8Array<ArrayBuffer>, filename: string, type: string): void {
|
||||
const url = URL.createObjectURL(new Blob([bytes], { type }))
|
||||
export function downloadBlob(blob: Blob, filename: string): void {
|
||||
const url = URL.createObjectURL(blob)
|
||||
const anchor = document.createElement('a')
|
||||
anchor.href = url
|
||||
anchor.download = filename
|
||||
anchor.click()
|
||||
URL.revokeObjectURL(url)
|
||||
// Revoke one tick later: some browsers read the blob URL after click().
|
||||
setTimeout(() => URL.revokeObjectURL(url), 0)
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import { createTrajectoryDurationStore } from './duration-store.ts'
|
||||
import { TrajectoryView, type TrajectoryViewInjected } from './TrajectoryView.tsx'
|
||||
import { en, NS, zh } from './locales.ts'
|
||||
import { downloadBytes, sessionLogZipFilename } from './export-log.ts'
|
||||
import { downloadBlob, sessionLogZipFilename } from './export-log.ts'
|
||||
|
||||
/** Required services: the conversation view slot, the independent history source, and the locale service. */
|
||||
export const inject = ['slots', 'sessionHistory', 'locale']
|
||||
@@ -33,6 +33,7 @@ export function apply(ctx: Context): void {
|
||||
name: 'conversation.view',
|
||||
id: 'trajectory',
|
||||
order: 10,
|
||||
locale: NS,
|
||||
label: () => t('view.trajectory'),
|
||||
inject: (sessionId: SessionId): TrajectoryViewInjected => {
|
||||
const history = ctx.sessionHistory.source(sessionId)
|
||||
@@ -44,7 +45,11 @@ export function apply(ctx: Context): void {
|
||||
exportLog: async () => {
|
||||
// The host streams the ZIP (root + descendant artifacts verbatim)
|
||||
// from GET /api/session.export; the browser downloads the response.
|
||||
const url = new URL('/api/session.export', window.location.origin)
|
||||
// A null origin (no-location Node contexts) falls back like the
|
||||
// carrier's resolveBase so the URL stays valid.
|
||||
const loc = (globalThis as { location?: { origin?: string } }).location
|
||||
const origin = loc?.origin !== undefined && loc.origin !== 'null' ? loc.origin : 'http://dsh.internal'
|
||||
const url = new URL('/api/session.export', origin)
|
||||
url.searchParams.set('sessionId', sessionId)
|
||||
url.searchParams.set('includeDescendants', 'true')
|
||||
const response = await fetch(url)
|
||||
@@ -52,12 +57,7 @@ export function apply(ctx: Context): void {
|
||||
const detail = await response.text().catch(() => '')
|
||||
throw new Error(`导出失败:HTTP ${response.status}${detail === '' ? '' : ` ${detail}`}`)
|
||||
}
|
||||
const blob = await response.blob()
|
||||
downloadBytes(
|
||||
new Uint8Array(await blob.arrayBuffer()),
|
||||
sessionLogZipFilename(sessionId),
|
||||
'application/zip',
|
||||
)
|
||||
downloadBlob(await response.blob(), sessionLogZipFilename(sessionId))
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,14 +1,32 @@
|
||||
/** `trajectory` namespace dictionaries (the view tab label). */
|
||||
/** `trajectory` namespace dictionaries (view tab label + toolbar strings). */
|
||||
|
||||
/** Dictionary namespace owned by this plugin. */
|
||||
export const NS = 'trajectory'
|
||||
|
||||
/** The trajectory dictionary key set (the source of truth for both locales). */
|
||||
export type TrajectoryKey = 'view.trajectory'
|
||||
export type TrajectoryKey =
|
||||
| 'view.trajectory'
|
||||
| 'toolbar.aria'
|
||||
| 'toolbar.duration'
|
||||
| 'toolbar.useActualDuration'
|
||||
| 'toolbar.useEqualWidth'
|
||||
| 'toolbar.actualTime'
|
||||
| 'toolbar.turns'
|
||||
| 'toolbar.expandTurns'
|
||||
| 'toolbar.collapseTurns'
|
||||
| 'toolbar.calls'
|
||||
| 'toolbar.expandCalls'
|
||||
| 'toolbar.collapseCalls'
|
||||
| 'toolbar.export'
|
||||
| 'toolbar.exportAria'
|
||||
| 'toolbar.exporting'
|
||||
| 'toolbar.exportTitle'
|
||||
| 'toolbar.search'
|
||||
| 'toolbar.searchPlaceholder'
|
||||
|
||||
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
interface LocaleNamespaceMap {
|
||||
/** The trajectory view tab label. */
|
||||
/** The trajectory view tab label and toolbar strings. */
|
||||
'trajectory': TrajectoryKey
|
||||
}
|
||||
}
|
||||
@@ -16,9 +34,43 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
/** Simplified Chinese dictionary (the key-set source of truth). */
|
||||
export const zh: Record<TrajectoryKey, string> = {
|
||||
'view.trajectory': '轨迹',
|
||||
'toolbar.aria': '轨迹工具栏',
|
||||
'toolbar.duration': '时长',
|
||||
'toolbar.useActualDuration': '使用实际时长',
|
||||
'toolbar.useEqualWidth': '使用等宽时长',
|
||||
'toolbar.actualTime': '实际时间',
|
||||
'toolbar.turns': '轮次',
|
||||
'toolbar.expandTurns': '展开轮次',
|
||||
'toolbar.collapseTurns': '折叠轮次',
|
||||
'toolbar.calls': '调用',
|
||||
'toolbar.expandCalls': '展开调用',
|
||||
'toolbar.collapseCalls': '折叠调用',
|
||||
'toolbar.export': '导出',
|
||||
'toolbar.exportAria': '导出会话日志',
|
||||
'toolbar.exporting': '导出中…',
|
||||
'toolbar.exportTitle': '导出会话日志(ZIP,含子代理)',
|
||||
'toolbar.search': '搜索轨迹',
|
||||
'toolbar.searchPlaceholder': '搜索',
|
||||
}
|
||||
|
||||
/** English dictionary. */
|
||||
export const en: Record<TrajectoryKey, string> = {
|
||||
'view.trajectory': 'Trajectory',
|
||||
'toolbar.aria': 'Trajectory toolbar',
|
||||
'toolbar.duration': 'Duration',
|
||||
'toolbar.useActualDuration': 'Use actual duration',
|
||||
'toolbar.useEqualWidth': 'Use equal-width operations',
|
||||
'toolbar.actualTime': 'Actual time',
|
||||
'toolbar.turns': 'Turns',
|
||||
'toolbar.expandTurns': 'Expand turns',
|
||||
'toolbar.collapseTurns': 'Collapse turns',
|
||||
'toolbar.calls': 'Calls',
|
||||
'toolbar.expandCalls': 'Expand calls',
|
||||
'toolbar.collapseCalls': 'Collapse calls',
|
||||
'toolbar.export': 'Export',
|
||||
'toolbar.exportAria': 'Export session log',
|
||||
'toolbar.exporting': 'Exporting…',
|
||||
'toolbar.exportTitle': 'Export session log (ZIP, includes subagents)',
|
||||
'toolbar.search': 'Search trajectory',
|
||||
'toolbar.searchPlaceholder': 'Search',
|
||||
}
|
||||
@@ -10,11 +10,15 @@ import { sessionLogZipFilename } from '../src/client/export-log.ts'
|
||||
|
||||
describe('sessionLogZipFilename', () => {
|
||||
it('keeps safe session ids verbatim', () => {
|
||||
expect(sessionLogZipFilename('session-abc_1.2')).toBe('dsh-session-session-abc_1.2.zip')
|
||||
expect(sessionLogZipFilename('session-abc_1-2')).toBe('dsh-session-session-abc_1-2.zip')
|
||||
})
|
||||
|
||||
it('neutralizes unsafe id characters that could shape the filename', () => {
|
||||
expect(sessionLogZipFilename('../evil')).toBe('dsh-session-.._evil.zip')
|
||||
expect(sessionLogZipFilename('../evil')).toBe('dsh-session-___evil.zip')
|
||||
expect(sessionLogZipFilename('a/b')).toBe('dsh-session-a_b.zip')
|
||||
})
|
||||
|
||||
it('strips dots so a dot-only id cannot shape a dot segment', () => {
|
||||
expect(sessionLogZipFilename('..')).toBe('dsh-session-__.zip')
|
||||
})
|
||||
})
|
||||
@@ -3,7 +3,12 @@
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import type { LocaleKeysOf } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { TrajectoryToolbar, type TrajectoryToolbarProps } from '../src/client/TrajectoryToolbar.tsx'
|
||||
import { zh, type TrajectoryKey } from '../src/client/locales.ts'
|
||||
|
||||
/** Test translator pinned to the Simplified Chinese dictionary. */
|
||||
const zhT = (key: LocaleKeysOf<'trajectory'>): string => zh[key as TrajectoryKey] ?? key
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
@@ -25,6 +30,7 @@ function baseProps(overrides: Partial<TrajectoryToolbarProps> = {}): TrajectoryT
|
||||
exporting: false,
|
||||
onExport: vi.fn(),
|
||||
exportError: null,
|
||||
t: zhT,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ import {
|
||||
import { createChatStore } from '@deepseek-ai/dsh-client-ui-conversation/src/client/stores.ts'
|
||||
import { zh as conversationZh } from '@deepseek-ai/dsh-client-ui-conversation/src/client/locales.ts'
|
||||
import { apply as localeApply, inject as localeInject } from '@deepseek-ai/dsh-client-locale/client'
|
||||
import type { LocaleKeysOf } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { zh, type TrajectoryKey } from '../src/client/locales.ts'
|
||||
import { apply, inject } from '@deepseek-ai/dsh-client-ui-trajectory/client'
|
||||
import { apply as nodeApply } from '@deepseek-ai/dsh-client-ui-trajectory'
|
||||
import type { TrajectoryTurnModel } from '../src/client/layout.ts'
|
||||
@@ -142,14 +144,18 @@ function emptyWorkspaces() {
|
||||
}
|
||||
|
||||
/** Standalone view props: the session-scope standard kit the outlet would bake. */
|
||||
function standaloneProps(nodes: ConversationSnapshot['nodes']): ConvViewProps {
|
||||
function standaloneProps(
|
||||
nodes: ConversationSnapshot['nodes'],
|
||||
): ConvViewProps & { t: (key: LocaleKeysOf<'trajectory'>) => string } {
|
||||
return {
|
||||
sessionId: SID,
|
||||
useSession: fakeSession(nodes).useSession,
|
||||
useSessions: emptySessions(),
|
||||
useWorkspaces: emptyWorkspaces(),
|
||||
useProjection: (() => undefined) as never,
|
||||
} as unknown as ConvViewProps
|
||||
// The locale seat the outlet would inject for the declared namespace.
|
||||
t: (key: LocaleKeysOf<'trajectory'>) => zh[key as TrajectoryKey] ?? key,
|
||||
} as unknown as ConvViewProps & { t: (key: LocaleKeysOf<'trajectory'>) => string }
|
||||
}
|
||||
|
||||
/** Real-stack bench: root Context + real SlotsService ring + the plugin fiber. */
|
||||
@@ -230,6 +236,7 @@ function mount(slots: SlotsService, nodes: ConversationSnapshot['nodes'] = NODES
|
||||
exportLog: trajectory.exportLog,
|
||||
useHistory: bindSnapshotSelector(trajectory.hooks.history),
|
||||
useDuration: bindSnapshotSelector(trajectory.hooks.duration),
|
||||
t: (key: TrajectoryKey) => zh[key],
|
||||
}
|
||||
})()
|
||||
: injected
|
||||
@@ -324,12 +331,12 @@ describe('tab switching in ConversationRoot', () => {
|
||||
expect(screen.queryByText(/turns ·/)).toBeNull()
|
||||
expect(view.container.querySelectorAll('tr[data-turn-start="true"]')).toHaveLength(2)
|
||||
expect(screen.queryByRole('columnheader')).toBeNull()
|
||||
expect(screen.getByRole('toolbar', { name: 'Trajectory toolbar' })).toBeTruthy()
|
||||
expect(screen.getByRole('toolbar', { name: '轨迹工具栏' })).toBeTruthy()
|
||||
expect(screen.getByRole('region', { name: 'Trajectory timeline' })).toBeTruthy()
|
||||
expect(view.container.querySelector('[data-conversation-composer-overlay]')).toBeTruthy()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Collapse turns' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '折叠轮次' }))
|
||||
expect(view.container.querySelector('[data-collapsed-summary="turn"]')).toBeTruthy()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Expand turns' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '展开轮次' }))
|
||||
expect(screen.getByRole('row', { name: /USER/ })).toBeTruthy()
|
||||
expect(screen.queryByTestId('chat-body')).toBeNull()
|
||||
await vi.waitFor(() => {
|
||||
@@ -540,13 +547,13 @@ describe('tab switching in ConversationRoot', () => {
|
||||
const b = await bench(historySnapshot([]))
|
||||
mount(b.slots)
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Trajectory' }))
|
||||
expect(screen.getByRole('toolbar', { name: 'Trajectory toolbar' })).toBeTruthy()
|
||||
expect(screen.getByRole('toolbar', { name: '轨迹工具栏' })).toBeTruthy()
|
||||
expect(screen.getByText('No timing data')).toBeTruthy()
|
||||
expect(screen.getByRole<HTMLButtonElement>('button', {
|
||||
name: 'Collapse turns',
|
||||
name: '折叠轮次',
|
||||
}).disabled).toBe(false)
|
||||
expect(screen.getByRole<HTMLButtonElement>('button', {
|
||||
name: 'Collapse calls',
|
||||
name: '折叠调用',
|
||||
}).disabled).toBe(false)
|
||||
expect(screen.queryByRole('row')).toBeNull()
|
||||
expect(screen.queryByText(/turns ·/)).toBeNull()
|
||||
@@ -1090,7 +1097,7 @@ describe('timeline projection', () => {
|
||||
...standaloneExport(),
|
||||
},
|
||||
))
|
||||
expect(screen.getByRole('toolbar', { name: 'Trajectory toolbar' })).toBeTruthy()
|
||||
expect(screen.getByRole('toolbar', { name: '轨迹工具栏' })).toBeTruthy()
|
||||
expect(screen.queryByRole('row')).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1123,7 +1130,6 @@ describe('session log export', () => {
|
||||
expect(fetchMock).toHaveBeenCalledOnce()
|
||||
})
|
||||
// The blob download lands a few microtasks after the fetch settles.
|
||||
// The blob download lands a few microtasks after the fetch settles.
|
||||
await vi.waitFor(() => {
|
||||
expect(createObjectURL).toHaveBeenCalled()
|
||||
})
|
||||
@@ -1159,7 +1165,7 @@ describe('TrajectoryView branches', () => {
|
||||
setActualDuration={(value) => { firstDuration.set(value) }}
|
||||
/>,
|
||||
)
|
||||
const duration = screen.getByRole('button', { name: 'Use actual duration' })
|
||||
const duration = screen.getByRole('button', { name: '使用实际时长' })
|
||||
|
||||
expect(duration.getAttribute('aria-pressed')).toBe('false')
|
||||
fireEvent.click(duration)
|
||||
@@ -1175,7 +1181,7 @@ describe('TrajectoryView branches', () => {
|
||||
setActualDuration={(value) => { restoredDuration.set(value) }}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByRole('button', { name: 'Use actual duration' }).getAttribute('aria-pressed'))
|
||||
expect(screen.getByRole('button', { name: '使用实际时长' }).getAttribute('aria-pressed'))
|
||||
.toBe('true')
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user