You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

439 lines
12 KiB

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
cleanup,
fireEvent,
render,
screen,
waitFor,
} from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useFormSchemaQuery } from "@/hooks/marriage/use-form-schema";
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
import {
useMarriageSectionDataQuery,
useUpdateMarriageSectionDataMutation,
} from "@/hooks/marriage/use-section-data";
import { convertSchemaToFrontendItems } from "@/lib/schema-adapter";
import {
QuestionAnswersProvider,
useQuestionAnswers,
} from "./question-answer-storage";
vi.mock("@/hooks/marriage/use-form-schema", () => ({
useFormSchemaQuery: vi.fn(),
}));
vi.mock("@/hooks/marriage/use-profile-main", () => ({
useMarriageProfileQuery: vi.fn(),
}));
vi.mock("@/hooks/marriage/use-section-data", () => ({
applyProfilePatchResultToCache: vi.fn(),
useMarriageSectionDataQuery: vi.fn(),
useUpdateMarriageSectionDataMutation: vi.fn(),
}));
// Dummy component to interact with the context
function TestComponent({ slug }: { slug: string }) {
const { setAnswerValue, flushAnswers } = useQuestionAnswers();
return (
<div>
<button
data-testid="set-radio"
onClick={() =>
setAnswerValue(
{
id: "q1",
type: "radio",
title: "Q1",
order: 1,
required: true,
baseRequired: true,
isVisible: true,
description: "",
tooltip: "",
extras: { placeHolder: "", options: [], range: [0, 0] },
options: [
{ id: "opt1", value: "A", label: "Option A", order: 1 },
],
},
"opt1",
)
}
>
Set Radio
</button>
<button
data-testid="set-checkbox"
onClick={() =>
setAnswerValue(
{
id: "q2",
type: "checkbox",
title: "Q2",
order: 2,
required: true,
baseRequired: true,
isVisible: true,
description: "",
tooltip: "",
extras: { placeHolder: "", options: [], range: [0, 0] },
options: [
{ id: "opt2", value: "B", label: "Option B", order: 1 },
{ id: "opt3", value: "C", label: "Option C", order: 2 },
],
},
["opt2", "opt3"],
)
}
>
Set Checkbox
</button>
<button data-testid="save" onClick={() => flushAnswers()}>
Save
</button>
</div>
);
}
describe("Question Answer & Schema Integration", () => {
let capturedPayload: any = null;
beforeEach(() => {
capturedPayload = null;
const updateMutateAsync = vi.fn(async (payload) => {
capturedPayload = payload;
return payload;
});
(useUpdateMarriageSectionDataMutation as any).mockReturnValue({
mutateAsync: updateMutateAsync,
isPending: false,
});
(useMarriageProfileQuery as any).mockReturnValue({
data: { can_edit_profile: true },
});
(useMarriageSectionDataQuery as any).mockReturnValue({
data: [],
isLoading: false,
});
(useFormSchemaQuery as any).mockReturnValue({
data: { is_completed: false },
isLoading: false,
isFetching: false,
});
});
afterEach(() => {
cleanup();
});
it("should send option_id instead of label/value for radio", async () => {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
render(
<QueryClientProvider client={queryClient}>
<QuestionAnswersProvider
slug="test_slug"
questions={[
{
id: "q1",
type: "radio",
title: "Q1",
order: 1,
required: true,
baseRequired: true,
isVisible: true,
description: "",
tooltip: "",
extras: { placeHolder: "", options: [], range: [0, 0] },
options: [
{ id: "opt1", value: "A", label: "Option A", order: 1 },
],
},
]}
>
<TestComponent slug="test_slug" />
</QuestionAnswersProvider>
</QueryClientProvider>,
);
fireEvent.click(screen.getByTestId("set-radio"));
fireEvent.click(screen.getByTestId("save"));
await waitFor(() => {
expect(capturedPayload).not.toBeNull();
const field = capturedPayload.fields.find((f: any) => f.key === "q1");
expect(field.option_id).toBe("opt1");
expect(field.value).toBe("opt1"); // ui value is option_id
});
});
it("should send array of option_ids for checkbox", async () => {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
render(
<QueryClientProvider client={queryClient}>
<QuestionAnswersProvider
slug="test_slug"
questions={[
{
id: "q2",
type: "checkbox",
title: "Q2",
order: 2,
required: true,
baseRequired: true,
isVisible: true,
description: "",
tooltip: "",
extras: { placeHolder: "", options: [], range: [0, 0] },
options: [
{ id: "opt2", value: "B", label: "Option B", order: 1 },
{ id: "opt3", value: "C", label: "Option C", order: 2 },
],
},
]}
>
<TestComponent slug="test_slug" />
</QuestionAnswersProvider>
</QueryClientProvider>,
);
fireEvent.click(screen.getByTestId("set-checkbox"));
fireEvent.click(screen.getByTestId("save"));
await waitFor(() => {
expect(capturedPayload).not.toBeNull();
const field = capturedPayload.fields.find((f: any) => f.key === "q2");
expect(field.option_id).toEqual(["opt2", "opt3"]);
});
});
it("batches all dirty answers into one mutation", async () => {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
render(
<QueryClientProvider client={queryClient}>
<QuestionAnswersProvider
slug="test_slug"
questions={[
{
id: "q1",
type: "radio",
title: "Q1",
order: 1,
required: true,
baseRequired: true,
isVisible: true,
description: "",
tooltip: "",
extras: { placeHolder: "", options: [], range: [0, 0] },
options: [
{ id: "opt1", value: "A", label: "Option A", order: 1 },
],
},
{
id: "q2",
type: "checkbox",
title: "Q2",
order: 2,
required: true,
baseRequired: true,
isVisible: true,
description: "",
tooltip: "",
extras: { placeHolder: "", options: [], range: [0, 0] },
options: [
{ id: "opt2", value: "B", label: "Option B", order: 1 },
{ id: "opt3", value: "C", label: "Option C", order: 2 },
],
},
]}
>
<TestComponent slug="test_slug" />
</QuestionAnswersProvider>
</QueryClientProvider>,
);
fireEvent.click(screen.getByTestId("set-radio"));
fireEvent.click(screen.getByTestId("set-checkbox"));
fireEvent.click(screen.getByTestId("save"));
await waitFor(() => expect(capturedPayload?.fields).toHaveLength(2));
});
it("should sort schema questions and options by order correctly", () => {
const mockSchema = {
sections: [
{
id: "sec1",
title: "Section",
order: 1,
icon: "user-circle",
is_required: true,
estimated_minutes: 5,
cards: [
{
id: "card1",
title: "Card",
order: 2,
questions: [
{
id: "q1",
title: "Q1",
type: "text",
order: 10,
required: true,
is_visible: true,
ui_config: {},
options: [],
},
{
id: "q2",
title: "Q2",
type: "text",
order: 5,
required: true,
is_visible: true,
ui_config: {},
options: [
{ id: "opt1", value: "A", label: "Option A", order: 2 },
{ id: "opt2", value: "B", label: "Option B", order: 1 },
],
},
],
},
{
id: "card2",
title: "Card 2",
order: 1,
questions: [
{
id: "q3",
title: "Q3",
type: "text",
order: 1,
required: true,
is_visible: true,
ui_config: {},
options: [],
},
],
},
],
},
],
progress: { sections_progress: {} },
} as any;
const frontendItems = convertSchemaToFrontendItems(mockSchema, "en");
const questions = frontendItems[0].questions;
expect(questions[0].id).toBe("q3");
expect(questions[1].id).toBe("q2");
expect(questions[2].id).toBe("q1");
expect(questions[1].options[0].id).toBe("opt2");
expect(questions[1].options[1].id).toBe("opt1");
});
it("should hydrate radio/dropdown/checkbox with option_id instead of canonical value", async () => {
// Mock the backend sending canonical value but keeping option_id
(useMarriageSectionDataQuery as any).mockReturnValue({
data: {
slug: "test_slug",
data: [
{
key: "q_radio",
type: "radio",
value: "Server Canonical Value",
option_id: "opt_radio",
},
{
key: "q_check",
type: "checkbox",
value: ["Server Val 1", "Server Val 2"],
option_id: ["opt_check1", "opt_check2"],
},
],
},
isLoading: false,
});
const capturedValues: any = {};
function HydrationTestComponent() {
const { getAnswerValue } = useQuestionAnswers();
capturedValues.radio = getAnswerValue({ id: "q_radio" } as any);
capturedValues.check = getAnswerValue({ id: "q_check" } as any);
return <div />;
}
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
render(
<QueryClientProvider client={queryClient}>
<QuestionAnswersProvider slug="test_slug" questions={[]}>
<HydrationTestComponent />
</QuestionAnswersProvider>
</QueryClientProvider>,
);
// Give it a moment to reconcile useEffect in QuestionAnswersProvider
await waitFor(() => {
expect(capturedValues.radio).toBe("opt_radio");
expect(capturedValues.check).toEqual(["opt_check1", "opt_check2"]);
});
});
it("should respect bq.is_required over bq.required for required state", () => {
const mockSchema = {
sections: [
{
id: "sec1",
title: "Section",
order: 1,
icon: "user-circle",
is_required: true,
estimated_minutes: 5,
cards: [
{
id: "card1",
title: "Card",
order: 1,
questions: [
{
id: "q1",
title: "Q1",
type: "text",
order: 1,
required: false,
is_required: true,
is_visible: true,
ui_config: {},
options: [],
},
],
},
],
},
],
progress: { sections_progress: {} },
} as any;
const frontendItems = convertSchemaToFrontendItems(mockSchema, "en");
const question = frontendItems[0].questions[0];
expect(question.baseRequired).toBe(false);
expect(question.required).toBe(true);
});
});