mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* fix: show cumulative conversation duration * fix: include usage-only run durations --------- Co-authored-by: Lanzhou3 <217479610+Lanzhou3@users.noreply.github.com>
158 lines
4.7 KiB
TypeScript
158 lines
4.7 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
// Polyfill scrollTo for jsdom (not available in jsdom's HTMLElement)
|
|
if (typeof HTMLElement.prototype.scrollTo !== 'function') {
|
|
HTMLElement.prototype.scrollTo = function (
|
|
options?: ScrollToOptions | number,
|
|
_y?: number,
|
|
) {
|
|
if (typeof options === 'object' && options !== null) {
|
|
if (options.top !== undefined) this.scrollTop = options.top;
|
|
if (options.left !== undefined) this.scrollLeft = options.left;
|
|
}
|
|
};
|
|
}
|
|
|
|
import { cleanup, render, screen } from '@testing-library/react';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { ChatPane, conversationMetaLabel, isAssistantMessageStreaming } from '../../src/components/ChatPane';
|
|
import type { ChatMessage, Conversation } from '../../src/types';
|
|
|
|
function renderChatPane(messages: ChatMessage[]) {
|
|
return render(
|
|
<ChatPane
|
|
projectKindForTracking="prototype"
|
|
messages={messages}
|
|
streaming={false}
|
|
error={null}
|
|
projectId="project-1"
|
|
projectFiles={[]}
|
|
onEnsureProject={async () => 'project-1'}
|
|
onSend={() => {}}
|
|
onStop={() => {}}
|
|
conversations={[]}
|
|
activeConversationId={null}
|
|
onSelectConversation={() => {}}
|
|
onDeleteConversation={() => {}}
|
|
/>,
|
|
);
|
|
}
|
|
|
|
describe('conversation timestamps', () => {
|
|
afterEach(() => {
|
|
cleanup();
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('shows inline relative message times with exact hover text', () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date('2025-01-15T14:00:00Z'));
|
|
|
|
renderChatPane([
|
|
{
|
|
id: 'user-1',
|
|
role: 'user',
|
|
content: 'Create a landing page',
|
|
createdAt: Date.parse('2025-01-15T12:00:00Z'),
|
|
},
|
|
{
|
|
id: 'assistant-1',
|
|
role: 'assistant',
|
|
content: 'Done',
|
|
createdAt: Date.parse('2025-01-15T12:01:00Z'),
|
|
},
|
|
]);
|
|
|
|
const firstTime = screen.getByText('2h ago');
|
|
expect(firstTime.tagName).toBe('TIME');
|
|
expect(firstTime.getAttribute('title')).toContain('2025');
|
|
expect(screen.getByText('1h ago').tagName).toBe('TIME');
|
|
});
|
|
|
|
it('adds day separators when a conversation crosses days', () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date('2025-01-16T14:00:00Z'));
|
|
|
|
renderChatPane([
|
|
{
|
|
id: 'user-1',
|
|
role: 'user',
|
|
content: 'First request',
|
|
createdAt: Date.parse('2025-01-15T12:00:00Z'),
|
|
},
|
|
{
|
|
id: 'user-2',
|
|
role: 'user',
|
|
content: 'Follow-up',
|
|
createdAt: Date.parse('2025-01-16T12:00:00Z'),
|
|
},
|
|
]);
|
|
|
|
expect(screen.getAllByRole('separator')).toHaveLength(2);
|
|
});
|
|
|
|
it('does not treat a completed last assistant message as streaming just because another conversation is running', () => {
|
|
const message: ChatMessage = {
|
|
id: 'assistant-1',
|
|
role: 'assistant',
|
|
content: 'Done',
|
|
createdAt: 100,
|
|
startedAt: 100,
|
|
runStatus: 'succeeded',
|
|
};
|
|
|
|
expect(isAssistantMessageStreaming(message, true, 'assistant-1')).toBe(false);
|
|
expect(
|
|
isAssistantMessageStreaming(
|
|
{ ...message, id: 'assistant-2', runStatus: 'running' },
|
|
false,
|
|
'assistant-1',
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it('shows fixed latest run duration in the conversation menu instead of live relative age', () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date('2025-01-15T14:00:00Z'));
|
|
const t = (key: string, vars?: Record<string, string | number>) =>
|
|
key === 'common.minutesShort' ? `${vars?.n}m` : key;
|
|
const conversation: Conversation = {
|
|
id: 'conv-1',
|
|
projectId: 'project-1',
|
|
title: 'Done run',
|
|
createdAt: Date.parse('2025-01-15T12:00:00Z'),
|
|
updatedAt: Date.parse('2025-01-15T12:01:00Z'),
|
|
latestRun: {
|
|
status: 'succeeded',
|
|
startedAt: 1_000,
|
|
endedAt: 16_000,
|
|
durationMs: 15_000,
|
|
},
|
|
};
|
|
|
|
expect(conversationMetaLabel(conversation, t as never)).toBe('15s');
|
|
});
|
|
|
|
it('prefers cumulative conversation duration over the latest run duration', () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date('2025-01-15T14:00:00Z'));
|
|
const t = (key: string, vars?: Record<string, string | number>) =>
|
|
key === 'common.minutesShort' ? `${vars?.n}m` : key;
|
|
const conversation = {
|
|
id: 'conv-1',
|
|
projectId: 'project-1',
|
|
title: 'Multi-run session',
|
|
createdAt: Date.parse('2025-01-15T12:00:00Z'),
|
|
updatedAt: Date.parse('2025-01-15T12:03:00Z'),
|
|
totalDurationMs: 85_000,
|
|
latestRun: {
|
|
status: 'succeeded',
|
|
startedAt: 120_000,
|
|
endedAt: 130_000,
|
|
durationMs: 10_000,
|
|
},
|
|
} satisfies Conversation & { totalDurationMs: number };
|
|
|
|
expect(conversationMetaLabel(conversation, t as never)).toBe('1m 25s');
|
|
});
|
|
});
|