diff --git a/frontend/src/components/FiveW1H.stories.tsx b/frontend/src/components/FiveW1H.stories.tsx new file mode 100644 index 000000000..adb76d322 --- /dev/null +++ b/frontend/src/components/FiveW1H.stories.tsx @@ -0,0 +1,90 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { FiveW1H } from "./FiveW1H"; + +const meta = { + title: "Ask Agent/FiveW1H", + component: FiveW1H, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Loading: Story = { + args: { slots: null }, +}; + +export const AllSlotsEmpty: Story = { + args: { + slots: (["who", "what", "when", "where", "why", "how"] as const).map((slot_code) => ({ + slot_code, + values: [], + empty_next_action_code: "none", + })), + }, +}; + +export const GroundedAnswer: Story = { + args: { + slots: [ + { + slot_code: "who", + empty_next_action_code: "none", + values: [ + { + text: "Ada West", + source: "post_summary_role", + evidence_text: "“Ada West signed off on the renewal”", + ontology_codes: [], + ontology_annotations: {}, + }, + { + text: "Northwind Logistics", + source: "post_summary_role.affiliated_organization_name", + ontology_codes: [], + ontology_annotations: {}, + }, + ], + }, + { + slot_code: "what", + empty_next_action_code: "none", + values: [ + { + text: "Renewed the vendor contract", + source: "post_summary_event", + evidence_text: "“we renewed the contract through Q4”", + ontology_codes: ["evt-42"], + ontology_annotations: { ontology_label: "Contract renewal" }, + }, + ], + }, + { + slot_code: "when", + empty_next_action_code: "none", + values: [], + }, + ], + }, +}; + +// Edge case: an evidence source with no human-label mapping yet must still +// render something readable instead of disappearing. +export const UnmappedEvidenceSource: Story = { + args: { + slots: [ + { + slot_code: "how", + empty_next_action_code: "none", + values: [ + { + text: "Filed via the vendor portal", + source: "some_future_source", + ontology_codes: [], + ontology_annotations: {}, + }, + ], + }, + ], + }, +}; diff --git a/frontend/src/components/FiveW1H.test.tsx b/frontend/src/components/FiveW1H.test.tsx new file mode 100644 index 000000000..278d656c3 --- /dev/null +++ b/frontend/src/components/FiveW1H.test.tsx @@ -0,0 +1,156 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it } from "vitest"; +import { FiveW1H } from "./FiveW1H"; +import type { FiveW1HSlot } from "../api"; + +function slot(overrides: Partial): FiveW1HSlot { + return { + slot_code: "who", + values: [], + empty_next_action_code: "none", + ...overrides, + }; +} + +describe("FiveW1H", () => { + it("shows a loading state while slots have not arrived yet", () => { + render(); + expect(screen.getByText("Loading 5W1H...")).toBeInTheDocument(); + }); + + it("shows a no-evidence message for a slot with no values", () => { + render(); + expect(screen.getByText("No grounded evidence for this dimension.")).toBeInTheDocument(); + }); + + it("renders a human label for a raw evidence source instead of the dotted path", () => { + render( + , + ); + expect(screen.getByText("Ada West")).toBeInTheDocument(); + expect(screen.getByText("Extracted affiliation")).toBeInTheDocument(); + expect(screen.queryByText("post_summary_role.affiliated_organization_name")).not.toBeInTheDocument(); + }); + + it("falls back to the raw source string for an unmapped evidence source", () => { + render( + , + ); + expect(screen.getByText("some_future_source")).toBeInTheDocument(); + }); + + it("shows optional evidence text and ontology class badges only when present", async () => { + render( + , + ); + + await userEvent.click(screen.getByText("Evidence provenance")); + expect(screen.getByText("“we renewed the contract”")).toBeInTheDocument(); + expect(screen.getByText("Ontology class: Contract renewal")).toBeInTheDocument(); + }); + + it("falls back to the ontology code when no ontology label is annotated", async () => { + render( + , + ); + + await userEvent.click(screen.getByText("Evidence provenance")); + expect(screen.getByText("Ontology class: evt-42")).toBeInTheDocument(); + }); + + it("renders one definition entry per slot with its human label", () => { + render( + , + ); + expect(screen.getByText("Who")).toBeInTheDocument(); + expect(screen.getByText("When")).toBeInTheDocument(); + expect(screen.getByText("Where")).toBeInTheDocument(); + }); + + it("renders multiple values for the same slot as separate list items", () => { + render( + , + ); + expect(screen.getByText("Ada West")).toBeInTheDocument(); + expect(screen.getByText("Priya Nair")).toBeInTheDocument(); + }); + + it("exposes the section under an accessible landmark name", () => { + render(); + expect(screen.getByRole("region", { name: "5W1H" })).toBeInTheDocument(); + }); +});