mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
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.
152 lines
4.1 KiB
TypeScript
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);
|
|
});
|
|
});
|