Files
openchamber/packages/ui/src/components/chat/message/parts/ReasoningPart.test.tsx
T

100 lines
3.6 KiB
TypeScript

import React from 'react';
import { describe, expect, test } from 'bun:test';
import { renderToStaticMarkup } from 'react-dom/server';
import { I18nProvider } from '@/lib/i18n';
import { ReasoningTimelineBlock } from './ReasoningPart';
// A reasoning text whose summary (first 120 chars) fits in the header but
// whose expanded body content should only appear when the disclosure is open.
const LONG_REASONING =
'First thought about the task at hand and how to approach it carefully.\n' +
'This second line goes into much deeper detail about the internal reasoning ' +
'process that should remain hidden in the collapsed header view.';
// A long text that should render the collapsible header with a label
const LONG_JUSTIFICATION =
'Sorting by activity first because the active session needs immediate attention.\n' +
'Secondary sort by last updated timestamp ensures a stable deterministic ordering ' +
'when multiple sessions have the same activity state.';
describe('ReasoningTimelineBlock', () => {
test('renders reasoning traces behind an accessible collapsed disclosure by default', () => {
const markup = renderToStaticMarkup(
<I18nProvider>
<ReasoningTimelineBlock
text={LONG_REASONING}
variant="thinking"
blockId="reasoning-test"
showDuration={false}
/>
</I18nProvider>,
);
// Accessible toggle row is rendered
expect(markup).toContain('role="button"');
expect(markup).toContain('aria-expanded="false"');
expect(markup).toContain('aria-label="Expand reasoning trace"');
// Summary preview (beginning of text) is visible in the header
expect(markup).toContain('First thought');
// Historical collapsed blocks do not mount the expanded body, avoiding a
// first-frame flash when Activity reveals previously hidden rows.
expect(markup).not.toContain('data-message-text-export-source');
});
test('renders "Justification" label for justification variant when pre-expanded and not streaming', () => {
const markup = renderToStaticMarkup(
<I18nProvider>
<ReasoningTimelineBlock
text={LONG_JUSTIFICATION}
variant="justification"
blockId="justification-test"
showDuration={false}
defaultExpanded={true}
/>
</I18nProvider>,
);
// Label shown in expanded header should be "Justification" not "Thinking"
expect(markup).toContain('Justification');
expect(markup).not.toContain('Thinking');
});
test('renders "Thinking" label for thinking variant when pre-expanded and not streaming', () => {
const markup = renderToStaticMarkup(
<I18nProvider>
<ReasoningTimelineBlock
text={LONG_REASONING}
variant="thinking"
blockId="thinking-test"
showDuration={false}
defaultExpanded={true}
/>
</I18nProvider>,
);
// Label shown in expanded header should be "Thinking"
expect(markup).toContain('Thinking');
});
test('header summary is a truncated excerpt from the beginning', () => {
const markup = renderToStaticMarkup(
<I18nProvider>
<ReasoningTimelineBlock
text={LONG_REASONING}
variant="thinking"
blockId="reasoning-test"
showDuration={false}
/>
</I18nProvider>,
);
// Deep body content beyond 120 chars should be cut from the summary span
expect(markup).not.toContain('remain hidden in the collapsed header view');
// The ellipsis character marks that the text was truncated
expect(markup).toContain('…');
});
});