import { describe, expect, it } from 'vitest'; import { createElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { EMPTY_STATE_HINT, EMPTY_STATE_TITLE, shouldShowWorkbenchEmptyState, WorkbenchChatEmptyState, } from '@/components/workbench/chat/empty-state'; import type { ChatNode } from '@/lib/workbench/session-store'; import { createWorkbenchTranslator } from '@/lib/i18n/workbench'; import { supportedLocales } from '@/lib/i18n/locales'; function userMessage(key = 'u1'): ChatNode { return { key, kind: 'user', text: '改一下' }; } describe('shouldShowWorkbenchEmptyState', () => { it('shows the placeholder on an empty, idle conversation', () => { expect(shouldShowWorkbenchEmptyState({ chat: [], catchingUp: false, live: false })).toBe(true); }); it('hides the placeholder the moment a message exists', () => { expect( shouldShowWorkbenchEmptyState({ chat: [userMessage()], catchingUp: false, live: false }), ).toBe(false); }); it('hides the placeholder while the pane is still catching up', () => { expect(shouldShowWorkbenchEmptyState({ chat: [], catchingUp: true, live: false })).toBe(false); }); it('shows it for a conversation that does not exist yet', () => { // A brand-new conversation has no session and therefore nothing to catch up // to. Reading the store's raw `replaying` here is what left it on a spinner // that nothing could turn off — see `catchingUp` in `WorkbenchChat`. expect(shouldShowWorkbenchEmptyState({ chat: [], catchingUp: false, live: false })).toBe(true); }); it('hides the placeholder while a run is live or a send is in flight', () => { expect(shouldShowWorkbenchEmptyState({ chat: [], catchingUp: false, live: true })).toBe(false); }); }); describe('WorkbenchChatEmptyState', () => { it('renders a quiet two-line hint about describing the change', () => { const html = renderToStaticMarkup(createElement(WorkbenchChatEmptyState)); expect(html).toContain('data-testid="workbench-chat-empty"'); expect(html).toContain(EMPTY_STATE_TITLE); expect(html).toContain(EMPTY_STATE_HINT); }); /** * The copy used to read "this course has no workbench record yet" — the * conversation described as the course's record, which is the binding the two * independent columns removed, restated in words. An empty conversation is a * NEW conversation, and the hint is where `@` is taught. */ it('invites a new conversation instead of calling itself a course’s record', () => { for (const locale of supportedLocales.map((entry) => entry.code)) { const t = createWorkbenchTranslator(locale); const title = t('workbench.chat.emptyTitle'); const hint = t('workbench.chat.emptyHint'); expect(title.trim(), `${locale} title`).not.toBe(''); expect(hint.trim(), `${locale} hint`).not.toBe(''); // No key leaked through, and no "this course has no record" framing. expect(title).not.toContain('workbench.chat'); expect(hint).not.toContain('workbench.chat'); for (const bound of ['这节课', '這堂課', 'this classroom yet', 'workbench activity']) { expect(title, `${locale} title still binds the chat to a course`).not.toContain(bound); expect(hint, `${locale} hint still binds the chat to a course`).not.toContain(bound); } } // And the English hint names the affordance the user now needs. expect(createWorkbenchTranslator('en-US')('workbench.chat.emptyHint')).toContain('@'); }); });