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
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);
|
|
});
|
|
});
|