open-design/apps/web/tests/components/QuestionsPanel.reveal.test.tsx
qiongyu1999 9bb787e173 feat(web): move discovery questions into a right-side Questions tab
Surface the active discovery question-form in a dedicated right-hand
Questions tab instead of inline in the chat. The form streams in there
(frame first, questions revealed one-by-one), with a Continue button and
a "skip all" affordance backed by a 120s auto-continue countdown. The
chat shows a banner that focuses the tab. Every question is optional;
submission is gated only by checkbox selection caps. Answered/historical
forms still render inline so the scrollback reads naturally.
2026-05-31 14:02:42 +08:00

152 lines
4.1 KiB
TypeScript

// @vitest-environment jsdom
import { act, cleanup, render } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { QuestionsPanel } from '../../src/components/QuestionsPanel';
import type { QuestionForm } from '../../src/artifacts/question-form';
const form: QuestionForm = {
id: 'discovery',
title: 'A few quick questions',
questions: [
{ id: 'q1', label: 'What is it about?', type: 'text' },
{ id: 'q2', label: 'Who is the audience?', type: 'text' },
{ id: 'q3', label: 'How long?', type: 'text' },
{ id: 'q4', label: 'What style?', type: 'text' },
],
};
function fieldCount() {
return document.querySelectorAll('.qf-field').length;
}
// Each reveal schedules the next only after its effect re-runs, so the clock
// must be stepped one interval per question rather than all at once.
function revealAll() {
for (let i = 0; i < form.questions.length; i++) {
act(() => {
vi.advanceTimersByTime(280);
});
}
}
afterEach(() => {
cleanup();
vi.useRealTimers();
});
describe('QuestionsPanel staggered reveal', () => {
it('reveals questions one-by-one even when the complete form arrives at once', () => {
vi.useFakeTimers();
act(() => {
render(
<QuestionsPanel
form={form}
interactive
generating={false}
onSubmit={() => {}}
/>,
);
});
// Frame first: the title is present but no questions yet.
expect(document.querySelector('.question-form-title')?.textContent).toBe(
'A few quick questions',
);
expect(fieldCount()).toBe(0);
// Each interval surfaces exactly one more question.
act(() => {
vi.advanceTimersByTime(280);
});
expect(fieldCount()).toBe(1);
act(() => {
vi.advanceTimersByTime(280);
});
expect(fieldCount()).toBe(2);
// One interval at a time — each reveal schedules the next after its effect
// re-runs, so we step the clock once per question.
act(() => {
vi.advanceTimersByTime(280);
});
expect(fieldCount()).toBe(3);
act(() => {
vi.advanceTimersByTime(280);
});
expect(fieldCount()).toBe(4);
// Stays capped at the total — no overshoot.
act(() => {
vi.advanceTimersByTime(280 * 3);
});
expect(fieldCount()).toBe(4);
});
it('does not replay the reveal when the same occurrence remounts', () => {
vi.useFakeTimers();
const props = {
form,
formKey: 'remount-test:discovery',
interactive: true,
generating: false,
onSubmit: () => {},
} as const;
const { unmount } = render(<QuestionsPanel {...props} />);
revealAll();
expect(fieldCount()).toBe(4);
// The streaming→persisted swap unmounts the panel and re-focuses the tab,
// remounting it. The same occurrence must come back fully revealed.
unmount();
act(() => {
render(<QuestionsPanel {...props} />);
});
expect(fieldCount()).toBe(4);
});
it('still animates a different occurrence after one has completed', () => {
vi.useFakeTimers();
const base = {
form,
interactive: true,
generating: false,
onSubmit: () => {},
} as const;
const first = render(<QuestionsPanel {...base} formKey="distinct-a:discovery" />);
revealAll();
expect(fieldCount()).toBe(4);
first.unmount();
// A brand-new form in another conversation has its own key, so the reveal
// plays again from the frame.
act(() => {
render(<QuestionsPanel {...base} formKey="distinct-b:discovery" />);
});
expect(fieldCount()).toBe(0);
act(() => {
vi.advanceTimersByTime(280);
});
expect(fieldCount()).toBe(1);
});
it('shows an already-answered form in full immediately (no re-animation)', () => {
vi.useFakeTimers();
act(() => {
render(
<QuestionsPanel
form={form}
interactive={false}
generating={false}
submittedAnswers={{ q1: 'x', q2: 'y', q3: 'z', q4: 'w' }}
onSubmit={() => {}}
/>,
);
});
expect(fieldCount()).toBe(4);
});
});