// @vitest-environment happy-dom // Pure prop-driven component, but match the act() convention used by // the other ClaimDrawer test files (ClaimDrawerHeader / // ClaimDrawerSkeleton / ClaimDrawerError). (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; import React, { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { describe, expect, it, beforeEach } from "vitest"; import { ValidationPanel } from "./ValidationPanel"; import { DrillStackProvider } from "@/components/drill/DrillStackProvider"; import type { ClaimDetailValidation, ClaimDetailValidationIssue } from "@/types"; function renderIntoContainer(element: React.ReactElement): { container: HTMLDivElement; unmount: () => void; } { const container = document.createElement("div"); document.body.appendChild(container); const root: Root = createRoot(container); // SP21 Phase 5 Task 5.9: ValidationPanel now uses useDrillStack() // to open validation-rule peeks. Wrap every render in a // DrillStackProvider so the hook has a context to read from. act(() => { root.render({element}); }); return { container, unmount: () => { act(() => root.unmount()); container.remove(); }, }; } function makeIssue( overrides: Partial = {} ): ClaimDetailValidationIssue { return { rule: "R050_diagnosis_present", message: "Primary diagnosis is required", severity: "error", ...overrides, }; } function makeValidation( overrides: Partial = {} ): ClaimDetailValidation { return { passed: true, errors: [], warnings: [], ...overrides, }; } describe("ValidationPanel", () => { it("test_all_passed_state_renders_checkmark_and_message", () => { const { container, unmount } = renderIntoContainer( ); // The pass-state root has data-testid="validation-passed". const passedRoot = container.querySelector('[data-testid="validation-passed"]'); expect(passedRoot).not.toBeNull(); // The success copy is in the rendered text. const text = (container.textContent ?? "").toLowerCase(); expect(text).toContain("all checks passed"); // The checkmark icon (lucide-react `CheckCircle2`) is an inline SVG. const svg = passedRoot?.querySelector("svg"); expect(svg).not.toBeNull(); // Neither sub-section is rendered when nothing is wrong. expect(container.querySelector('[data-testid="validation-errors"]')).toBeNull(); expect(container.querySelector('[data-testid="validation-warnings"]')).toBeNull(); unmount(); }); it("test_errors_only_renders_errors_subsection", () => { const { container, unmount } = renderIntoContainer( ); const errorsBlock = container.querySelector('[data-testid="validation-errors"]'); expect(errorsBlock).not.toBeNull(); // Red left-border token applied to the inner block — sniff the className. const cls = errorsBlock?.className ?? ""; expect(cls).toContain("border-[color:var(--m-error)]"); expect(cls).toContain("border-l-2"); // Rule code + message visible in the rendered text. const text = container.textContent ?? ""; expect(text).toContain("R050_diagnosis_present"); expect(text).toContain("Primary diagnosis is required"); // No warnings sub-section when only errors are present. expect(container.querySelector('[data-testid="validation-warnings"]')).toBeNull(); // And no all-passed pill. expect(container.querySelector('[data-testid="validation-passed"]')).toBeNull(); unmount(); }); it("test_warnings_only_renders_warnings_subsection", () => { const { container, unmount } = renderIntoContainer( ); const warningsBlock = container.querySelector('[data-testid="validation-warnings"]'); expect(warningsBlock).not.toBeNull(); // Amber left-border token applied to the inner block. const cls = warningsBlock?.className ?? ""; expect(cls).toContain("border-[color:var(--m-warning)]"); expect(cls).toContain("border-l-2"); const text = container.textContent ?? ""; expect(text).toContain("R200_units_recommended"); expect(text).toContain("Units reported as 0"); // No errors sub-section when only warnings are present. expect(container.querySelector('[data-testid="validation-errors"]')).toBeNull(); unmount(); }); it("test_both_errors_and_warnings_renders_both", () => { const { container, unmount } = renderIntoContainer( ); expect(container.querySelector('[data-testid="validation-errors"]')).not.toBeNull(); expect(container.querySelector('[data-testid="validation-warnings"]')).not.toBeNull(); // Errors sub-section appears before warnings sub-section (blocking // issues first, then advisories). const errorsEl = container.querySelector('[data-testid="validation-errors"]'); const warningsEl = container.querySelector('[data-testid="validation-warnings"]'); expect(errorsEl).not.toBeNull(); expect(warningsEl).not.toBeNull(); expect( errorsEl!.compareDocumentPosition(warningsEl!) & Node.DOCUMENT_POSITION_FOLLOWING ).toBeTruthy(); unmount(); }); it("test_errors_are_grouped_by_rule_with_count", () => { const { container, unmount } = renderIntoContainer( ); const errorsBlock = container.querySelector('[data-testid="validation-errors"]'); expect(errorsBlock).not.toBeNull(); const text = errorsBlock?.textContent ?? ""; // Header should show the rule code with the grouped count "(2)". expect(text).toContain("R050_diagnosis_present"); expect(text).toContain("(2)"); // Both messages appear underneath the group header. expect(text).toContain("Primary diagnosis is required"); expect(text).toContain("Diagnosis qualifier is required"); unmount(); }); it("test_passed_with_warnings_still_shows_warnings", () => { // The "all checks passed" pill is reserved for passed && no-warnings; // passed=true with warnings present should still surface the warnings // sub-section so the user notices the advisories. const { container, unmount } = renderIntoContainer( ); expect(container.querySelector('[data-testid="validation-warnings"]')).not.toBeNull(); expect(container.querySelector('[data-testid="validation-passed"]')).toBeNull(); unmount(); }); it("SP21 Task 5.9: rule code is drillable (renders as a button)", () => { // The rule code in each IssueGroup now wraps in a button with // data-testid="...-rule-drill". The other rule codes also drill // the same way. const { container, unmount } = renderIntoContainer( ); const errorRuleDrill = container.querySelector( '[data-testid="validation-errors-rule-drill"]', ); const warningRuleDrill = container.querySelector( '[data-testid="validation-warnings-rule-drill"]', ); expect(errorRuleDrill).not.toBeNull(); expect(errorRuleDrill?.tagName).toBe("BUTTON"); expect(errorRuleDrill?.textContent).toContain("R050_diagnosis_present"); expect(warningRuleDrill).not.toBeNull(); expect(warningRuleDrill?.tagName).toBe("BUTTON"); expect(warningRuleDrill?.textContent).toContain("R200_units_recommended"); unmount(); }); it("SP21 Task 5.9: clicking the rule code opens the PeekModal", async () => { // Clicking the rule code in the errors sub-section pushes a // `{ kind: "rule", rule }` peek onto the drill stack. The peek // is a Radix Dialog portal with the eyebrow "Validation rule" // and the rule code as the title. const { container, unmount } = renderIntoContainer( ); const ruleBtn = container.querySelector( '[data-testid="validation-errors-rule-drill"]', ) as HTMLButtonElement | null; expect(ruleBtn).not.toBeNull(); await act(async () => { ruleBtn!.click(); await Promise.resolve(); }); // The PeekModal portals to document.body as a Radix dialog. Find // the dialog with the "Validation rule" eyebrow. const dialogs = document.body.querySelectorAll('[role="dialog"]'); const peekDialog = Array.from(dialogs).find((d) => d.textContent?.includes("Validation rule"), ); expect(peekDialog).not.toBeNull(); // Title is the rule code. expect(peekDialog?.textContent).toContain("R050_diagnosis_present"); // Body includes the catalog description for R050. expect(peekDialog?.textContent).toContain("Diagnosis pointer present"); unmount(); }); it("SP21 Task 5.9: unknown rule still opens the peek (fallback note)", async () => { // Rules not in the catalog still open the peek — operators // should be able to correlate unknown rule codes to whatever // they were just looking at. The peek shows an "Unknown rule" // note instead of the catalog text. const { container, unmount } = renderIntoContainer( ); const ruleBtn = container.querySelector( '[data-testid="validation-errors-rule-drill"]', ) as HTMLButtonElement | null; expect(ruleBtn).not.toBeNull(); await act(async () => { ruleBtn!.click(); await Promise.resolve(); }); const dialogs = document.body.querySelectorAll('[role="dialog"]'); const peekDialog = Array.from(dialogs).find((d) => d.textContent?.includes("Validation rule"), ); expect(peekDialog).not.toBeNull(); expect(peekDialog?.textContent).toContain("R999_totally_made_up"); expect(peekDialog?.textContent).toContain("Unknown rule"); unmount(); }); }); beforeEach(() => { // The PeekModal portals to document.body as a Radix dialog. Wipe // any leftover dialogs between tests so the "find dialog by // eyebrow" assertions in the rule-drill tests don't see stale // portals from a prior test. document.body.querySelectorAll('[role="dialog"]').forEach((d) => d.remove()); });