Final review edit modals created
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
import { useEffect, useLayoutEffect } from "react";
|
||||
import { describe, it, expect, afterEach } from "vitest";
|
||||
import {
|
||||
renderWithProviders as render,
|
||||
screen,
|
||||
cleanup,
|
||||
within,
|
||||
waitFor,
|
||||
} from "../utils/test-utils";
|
||||
import { fireEvent } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { CommunicationMethodsScreen } from "../../app/(app)/create/screens/card/CommunicationMethodsScreen";
|
||||
import { useCreateFlow } from "../../app/(app)/create/context/CreateFlowContext";
|
||||
import type { CreateFlowState } from "../../app/(app)/create/types";
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
/**
|
||||
* Mounts the screen with optional starting state and exposes the latest
|
||||
* `state` to the test harness so we can assert the persistence side of
|
||||
* the Add Platform flow without driving the wizard's Next chain.
|
||||
*/
|
||||
const EMPTY_STATE: CreateFlowState = {};
|
||||
|
||||
function ScreenWithStateProbe({
|
||||
onState,
|
||||
initial = EMPTY_STATE,
|
||||
}: {
|
||||
onState: (_state: CreateFlowState) => void;
|
||||
initial?: CreateFlowState;
|
||||
}) {
|
||||
const { state, replaceState } = useCreateFlow();
|
||||
useLayoutEffect(() => {
|
||||
replaceState(initial);
|
||||
}, [replaceState, initial]);
|
||||
useEffect(() => {
|
||||
onState(state);
|
||||
}, [state, onState]);
|
||||
return <CommunicationMethodsScreen />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms the persistence half of the Add-Platform flow that lets the
|
||||
* final-review chip edit modal start from a known seed instead of always
|
||||
* snapping back to preset copy. See {@link CommunicationMethodEditFields}
|
||||
* and `buildPublishPayload` for the read side.
|
||||
*/
|
||||
describe("CommunicationMethodsScreen — Add Platform persistence", () => {
|
||||
it("seeds the modal from preset and persists edits + selection on Confirm", async () => {
|
||||
let latest: CreateFlowState = {};
|
||||
render(
|
||||
<ScreenWithStateProbe
|
||||
onState={(s) => {
|
||||
latest = s;
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
|
||||
);
|
||||
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
const textareas = within(dialog).getAllByRole("textbox");
|
||||
expect(textareas.length).toBe(3);
|
||||
// Preset corePrinciple must seed into the first textarea so the user
|
||||
// edits a real starting point rather than an empty field.
|
||||
expect((textareas[0] as HTMLTextAreaElement).value.length).toBeGreaterThan(
|
||||
0,
|
||||
);
|
||||
|
||||
fireEvent.change(textareas[0], { target: { value: "Custom principle" } });
|
||||
fireEvent.click(
|
||||
within(dialog).getByRole("button", { name: "Add Platform" }),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(latest.selectedCommunicationMethodIds).toContain("signal");
|
||||
});
|
||||
expect(
|
||||
latest.communicationMethodDetailsById?.signal?.corePrinciple,
|
||||
).toBe("Custom principle");
|
||||
});
|
||||
|
||||
it("does not persist edits when the modal closes without Confirm", async () => {
|
||||
let latest: CreateFlowState = {};
|
||||
render(
|
||||
<ScreenWithStateProbe
|
||||
onState={(s) => {
|
||||
latest = s;
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
|
||||
);
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
const [firstTextarea] = within(dialog).getAllByRole("textbox");
|
||||
fireEvent.change(firstTextarea, {
|
||||
target: { value: "Should NOT persist" },
|
||||
});
|
||||
|
||||
fireEvent.keyDown(document, { key: "Escape" });
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(latest.selectedCommunicationMethodIds ?? []).not.toContain("signal");
|
||||
expect(latest.communicationMethodDetailsById).toBeUndefined();
|
||||
});
|
||||
|
||||
it("re-seeds the modal from a saved override when reopening the same chip", async () => {
|
||||
let latest: CreateFlowState = {};
|
||||
render(
|
||||
<ScreenWithStateProbe
|
||||
onState={(s) => {
|
||||
latest = s;
|
||||
}}
|
||||
initial={{
|
||||
selectedCommunicationMethodIds: ["signal"],
|
||||
communicationMethodDetailsById: {
|
||||
signal: {
|
||||
corePrinciple: "Saved principle",
|
||||
logisticsAdmin: "Saved logistics",
|
||||
codeOfConduct: "Saved coc",
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
void latest;
|
||||
|
||||
fireEvent.click(
|
||||
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
|
||||
);
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
const textareas = within(dialog).getAllByRole(
|
||||
"textbox",
|
||||
) as HTMLTextAreaElement[];
|
||||
expect(textareas[0].value).toBe("Saved principle");
|
||||
expect(textareas[1].value).toBe("Saved logistics");
|
||||
expect(textareas[2].value).toBe("Saved coc");
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useLayoutEffect } from "react";
|
||||
import { useEffect, useLayoutEffect } from "react";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { fireEvent, within } from "@testing-library/react";
|
||||
import {
|
||||
renderWithProviders as render,
|
||||
screen,
|
||||
@@ -8,6 +9,30 @@ import {
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { FinalReviewScreen } from "../../app/(app)/create/screens/review/FinalReviewScreen";
|
||||
import { useCreateFlow } from "../../app/(app)/create/context/CreateFlowContext";
|
||||
import type { CreateFlowState } from "../../app/(app)/create/types";
|
||||
|
||||
/**
|
||||
* Mounts the screen with a Customize-style preset selection and exposes the
|
||||
* latest `state` to the test via `onState`. Used by the edit-modal save
|
||||
* semantics suite below to assert what the user's edits actually persist
|
||||
* (or don't, on close).
|
||||
*/
|
||||
function FinalReviewWithStateProbe({
|
||||
onState,
|
||||
initial,
|
||||
}: {
|
||||
onState: (_state: CreateFlowState) => void;
|
||||
initial: CreateFlowState;
|
||||
}) {
|
||||
const { state, replaceState } = useCreateFlow();
|
||||
useLayoutEffect(() => {
|
||||
replaceState(initial);
|
||||
}, [replaceState, initial]);
|
||||
useEffect(() => {
|
||||
onState(state);
|
||||
}, [state, onState]);
|
||||
return <FinalReviewScreen />;
|
||||
}
|
||||
|
||||
const FALLBACK_CARD_TITLE = "Your community";
|
||||
const FALLBACK_CARD_DESCRIPTION_SNIPPET =
|
||||
@@ -143,3 +168,221 @@ describe("FinalReviewScreen — prefilled selections", () => {
|
||||
expect(screen.queryByText("Consciousness")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("FinalReviewScreen — chip detail modal", () => {
|
||||
it("opens the read-only detail modal when a chip is clicked, matching the preset copy", async () => {
|
||||
render(<FinalReviewWithCustomizeSelections />);
|
||||
|
||||
const signalChip = await screen.findByRole("button", { name: "Signal" });
|
||||
fireEvent.click(signalChip);
|
||||
|
||||
// Modal subtitle is the `supportText` from communication.json for the
|
||||
// "signal" method — proves the chip click resolved the correct preset
|
||||
// and reused the TemplateChipDetailModal's by-label lookup.
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(
|
||||
/Encrypted messaging for high-security, private coordination\./i,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
// Core-principle section heading is shared copy from the same messages
|
||||
// file; assert it renders to confirm the modal body hydrated.
|
||||
expect(
|
||||
screen.getAllByText(/core principle/i).length,
|
||||
).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("opens a core-values chip with the matching preset meaning/signals", async () => {
|
||||
function CoreValuesHarness() {
|
||||
const { replaceState } = useCreateFlow();
|
||||
useLayoutEffect(() => {
|
||||
replaceState({
|
||||
selectedCoreValueIds: ["1"],
|
||||
coreValuesChipsSnapshot: [
|
||||
{ id: "1", label: "Accessibility", state: "selected" },
|
||||
],
|
||||
});
|
||||
}, [replaceState]);
|
||||
return <FinalReviewScreen />;
|
||||
}
|
||||
render(<CoreValuesHarness />);
|
||||
|
||||
const chip = await screen.findByRole("button", { name: "Accessibility" });
|
||||
fireEvent.click(chip);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(/what does this value mean to your group\?/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(
|
||||
screen.getByText(/signals of violation/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens the editable Save modal for a values chip (parity with method chips)", async () => {
|
||||
// Customize / plain custom-rule path: snapshot is set, sections is not.
|
||||
function CoreValuesHarness() {
|
||||
const { replaceState } = useCreateFlow();
|
||||
useLayoutEffect(() => {
|
||||
replaceState({
|
||||
selectedCoreValueIds: ["1"],
|
||||
coreValuesChipsSnapshot: [
|
||||
{ id: "1", label: "Accessibility", state: "selected" },
|
||||
],
|
||||
});
|
||||
}, [replaceState]);
|
||||
return <FinalReviewScreen />;
|
||||
}
|
||||
render(<CoreValuesHarness />);
|
||||
|
||||
fireEvent.click(
|
||||
await screen.findByRole("button", { name: "Accessibility" }),
|
||||
);
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
expect(
|
||||
within(dialog).getByRole("button", { name: "Save" }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(dialog).queryByRole("button", { name: "Close" }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens the editable Save modal for a values chip in the use-without-changes flow", async () => {
|
||||
// Mirrors the post-fix payload from `handleUseTemplateWithoutChanges`:
|
||||
// template Values section is stripped from `sections`, snapshot +
|
||||
// selected ids are seeded so the chip carries an `overrideKey`.
|
||||
function UseWithoutChangesHarness() {
|
||||
const { replaceState } = useCreateFlow();
|
||||
useLayoutEffect(() => {
|
||||
replaceState({
|
||||
title: "Oak Park Commons",
|
||||
// Values section deliberately absent — apply handler scrubs it.
|
||||
sections: [
|
||||
{
|
||||
categoryName: "Communication",
|
||||
entries: [{ title: "Signal", body: "…" }],
|
||||
},
|
||||
],
|
||||
selectedCoreValueIds: ["1"],
|
||||
coreValuesChipsSnapshot: [
|
||||
{ id: "1", label: "Accessibility", state: "selected" },
|
||||
],
|
||||
});
|
||||
}, [replaceState]);
|
||||
return <FinalReviewScreen />;
|
||||
}
|
||||
render(<UseWithoutChangesHarness />);
|
||||
|
||||
fireEvent.click(
|
||||
await screen.findByRole("button", { name: "Accessibility" }),
|
||||
);
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
expect(
|
||||
within(dialog).getByRole("button", { name: "Save" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* Save semantics for {@link FinalReviewChipEditModal}. Mirrors the
|
||||
* "edits ride along to publish" promise documented in the modal:
|
||||
*
|
||||
* 1. Save starts disabled (no edits yet → nothing to persist).
|
||||
* 2. Editing any field flips Save on; clicking it writes the typed
|
||||
* `{group}MethodDetailsById[id]` entry into create-flow state and
|
||||
* closes the modal.
|
||||
* 3. Closing without Save discards every typed change.
|
||||
*/
|
||||
describe("FinalReviewScreen — chip edit modal save semantics", () => {
|
||||
const baseSelections: CreateFlowState = {
|
||||
title: "Oak Park Commons",
|
||||
selectedCommunicationMethodIds: ["signal"],
|
||||
};
|
||||
|
||||
it("starts with the Save button disabled until the user edits a field", async () => {
|
||||
let latest: CreateFlowState = {};
|
||||
render(
|
||||
<FinalReviewWithStateProbe
|
||||
onState={(s) => {
|
||||
latest = s;
|
||||
}}
|
||||
initial={baseSelections}
|
||||
/>,
|
||||
);
|
||||
void latest;
|
||||
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
const saveButton = within(dialog).getByRole("button", { name: "Save" });
|
||||
expect(saveButton).toBeDisabled();
|
||||
|
||||
const [firstTextarea] = within(dialog).getAllByRole("textbox");
|
||||
fireEvent.change(firstTextarea, {
|
||||
target: { value: "Edited principle" },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
within(dialog).getByRole("button", { name: "Save" }),
|
||||
).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it("writes edits into communicationMethodDetailsById when Save is clicked", async () => {
|
||||
let latest: CreateFlowState = {};
|
||||
render(
|
||||
<FinalReviewWithStateProbe
|
||||
onState={(s) => {
|
||||
latest = s;
|
||||
}}
|
||||
initial={baseSelections}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
const [firstTextarea] = within(dialog).getAllByRole("textbox");
|
||||
fireEvent.change(firstTextarea, {
|
||||
target: { value: "Edited principle" },
|
||||
});
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: "Save" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
latest.communicationMethodDetailsById?.signal?.corePrinciple,
|
||||
).toBe("Edited principle");
|
||||
});
|
||||
});
|
||||
|
||||
it("discards typed edits when the modal closes without Save", async () => {
|
||||
let latest: CreateFlowState = {};
|
||||
render(
|
||||
<FinalReviewWithStateProbe
|
||||
onState={(s) => {
|
||||
latest = s;
|
||||
}}
|
||||
initial={baseSelections}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
const [firstTextarea] = within(dialog).getAllByRole("textbox");
|
||||
fireEvent.change(firstTextarea, {
|
||||
target: { value: "Should NOT persist" },
|
||||
});
|
||||
|
||||
fireEvent.keyDown(document, { key: "Escape" });
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(latest.communicationMethodDetailsById).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildTemplateCustomizePrefill } from "../../lib/create/applyTemplatePrefill";
|
||||
import {
|
||||
buildCoreValuesPrefillFromTemplateBody,
|
||||
buildTemplateCustomizePrefill,
|
||||
} from "../../lib/create/applyTemplatePrefill";
|
||||
import coreValuesMessages from "../../messages/en/create/customRule/coreValues.json";
|
||||
|
||||
function coreValuePresetId(label: string): string {
|
||||
@@ -107,3 +110,46 @@ describe("buildTemplateCustomizePrefill", () => {
|
||||
expect(prefill).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildCoreValuesPrefillFromTemplateBody", () => {
|
||||
it("returns {} for malformed bodies", () => {
|
||||
expect(buildCoreValuesPrefillFromTemplateBody(null)).toEqual({});
|
||||
expect(buildCoreValuesPrefillFromTemplateBody({})).toEqual({});
|
||||
expect(
|
||||
buildCoreValuesPrefillFromTemplateBody({ sections: "nope" }),
|
||||
).toEqual({});
|
||||
});
|
||||
|
||||
it("returns {} when the body has no Values section", () => {
|
||||
expect(
|
||||
buildCoreValuesPrefillFromTemplateBody({
|
||||
sections: [
|
||||
{ categoryName: "Communication", entries: [{ title: "Signal" }] },
|
||||
],
|
||||
}),
|
||||
).toEqual({});
|
||||
});
|
||||
|
||||
it("seeds the snapshot + selected ids from the Values section only", () => {
|
||||
const prefill = buildCoreValuesPrefillFromTemplateBody({
|
||||
sections: [
|
||||
{
|
||||
categoryName: "Values",
|
||||
entries: [
|
||||
{ title: "Consensus", body: "" },
|
||||
{ title: "Community Care", body: "" },
|
||||
],
|
||||
},
|
||||
{
|
||||
categoryName: "Communication",
|
||||
entries: [{ title: "Signal", body: "" }],
|
||||
},
|
||||
],
|
||||
});
|
||||
const selected = prefill.selectedCoreValueIds ?? [];
|
||||
expect(selected).toContain(coreValuePresetId("Consensus"));
|
||||
expect(selected).toContain(coreValuePresetId("Community Care"));
|
||||
// Methods should not be touched by the values-only helper.
|
||||
expect(prefill.selectedCommunicationMethodIds).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -114,7 +114,11 @@ describe("buildFinalReviewCategoriesFromState", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not duplicate Values when sections already includes one", () => {
|
||||
it("prefers the chip snapshot over a duplicated Values section in sections", () => {
|
||||
// The use-without-changes handler now strips Values from `sections` and
|
||||
// seeds the snapshot, but legacy drafts persisted before that fix can
|
||||
// still arrive here with both sources present. The snapshot wins so the
|
||||
// final-review chip modal can attach edits via the per-chip id.
|
||||
const state: CreateFlowState = {
|
||||
sections: [
|
||||
{
|
||||
@@ -129,7 +133,7 @@ describe("buildFinalReviewCategoriesFromState", () => {
|
||||
};
|
||||
const rows = buildFinalReviewCategoriesFromState(state, NAMES);
|
||||
expect(rows).toEqual([
|
||||
{ name: "Values", chips: ["Consciousness"] },
|
||||
{ name: "Values", chips: ["Accessibility"] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -113,6 +113,89 @@ describe("buildPublishPayload", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildPublishPayload — methodSelections", () => {
|
||||
it("omits document.methodSelections when no method group is selected", () => {
|
||||
const r = buildPublishPayload({ title: "T" });
|
||||
expect(r.ok).toBe(true);
|
||||
if (!r.ok) return;
|
||||
expect(r.document.methodSelections).toBeUndefined();
|
||||
});
|
||||
|
||||
it("emits preset-only sections when a method is selected without an override", () => {
|
||||
const r = buildPublishPayload({
|
||||
title: "T",
|
||||
selectedCommunicationMethodIds: ["signal"],
|
||||
});
|
||||
expect(r.ok).toBe(true);
|
||||
if (!r.ok) return;
|
||||
const ms = r.document.methodSelections as
|
||||
| Record<string, Array<Record<string, unknown>>>
|
||||
| undefined;
|
||||
expect(ms).toBeDefined();
|
||||
expect(ms?.communication?.length).toBe(1);
|
||||
const entry = ms?.communication?.[0] as {
|
||||
id: string;
|
||||
label: string;
|
||||
sections: { corePrinciple: string };
|
||||
};
|
||||
expect(entry.id).toBe("signal");
|
||||
expect(entry.label).toBe("Signal");
|
||||
// Preset corePrinciple is non-empty for `signal` in the shipped messages
|
||||
// file — proves we read presets when no override is present.
|
||||
expect(entry.sections.corePrinciple.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("merges override on top of preset for the selected method", () => {
|
||||
const r = buildPublishPayload({
|
||||
title: "T",
|
||||
selectedCommunicationMethodIds: ["signal"],
|
||||
communicationMethodDetailsById: {
|
||||
signal: {
|
||||
corePrinciple: "OVERRIDE PRINCIPLE",
|
||||
logisticsAdmin: "OVERRIDE LOGISTICS",
|
||||
codeOfConduct: "OVERRIDE COC",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(r.ok).toBe(true);
|
||||
if (!r.ok) return;
|
||||
const ms = r.document.methodSelections as
|
||||
| Record<string, Array<Record<string, unknown>>>
|
||||
| undefined;
|
||||
const entry = ms?.communication?.[0] as {
|
||||
sections: {
|
||||
corePrinciple: string;
|
||||
logisticsAdmin: string;
|
||||
codeOfConduct: string;
|
||||
};
|
||||
};
|
||||
expect(entry.sections.corePrinciple).toBe("OVERRIDE PRINCIPLE");
|
||||
expect(entry.sections.logisticsAdmin).toBe("OVERRIDE LOGISTICS");
|
||||
expect(entry.sections.codeOfConduct).toBe("OVERRIDE COC");
|
||||
});
|
||||
|
||||
it("emits a methodSelections entry per selected group", () => {
|
||||
const r = buildPublishPayload({
|
||||
title: "T",
|
||||
selectedCommunicationMethodIds: ["signal"],
|
||||
selectedMembershipMethodIds: ["open-access"],
|
||||
selectedDecisionApproachIds: ["lazy-consensus"],
|
||||
selectedConflictManagementIds: ["peer-mediation"],
|
||||
});
|
||||
expect(r.ok).toBe(true);
|
||||
if (!r.ok) return;
|
||||
const ms = r.document.methodSelections as
|
||||
| Record<string, Array<unknown>>
|
||||
| undefined;
|
||||
expect(Object.keys(ms ?? {}).sort()).toEqual([
|
||||
"communication",
|
||||
"conflictManagement",
|
||||
"decisionApproaches",
|
||||
"membership",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseDocumentSectionsForDisplay", () => {
|
||||
it("returns empty for non-object", () => {
|
||||
expect(parseDocumentSectionsForDisplay(null)).toEqual([]);
|
||||
|
||||
Reference in New Issue
Block a user