diff --git a/src/components/ClaimDrawer/RawSegmentsPanel.test.tsx b/src/components/ClaimDrawer/RawSegmentsPanel.test.tsx new file mode 100644 index 0000000..d64407d --- /dev/null +++ b/src/components/ClaimDrawer/RawSegmentsPanel.test.tsx @@ -0,0 +1,203 @@ +// @vitest-environment happy-dom +// Pure prop-driven component; match the act() convention used by the +// other ClaimDrawer test files (ClaimDrawerHeader / ClaimDrawerSkeleton / +// ClaimDrawerError / ValidationPanel / ServiceLinesTable / DiagnosesList +// / PartiesGrid). +(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 } from "vitest"; +import { RawSegmentsPanel } from "./RawSegmentsPanel"; + +function renderIntoContainer(element: React.ReactElement): { + container: HTMLDivElement; + unmount: () => void; +} { + const container = document.createElement("div"); + document.body.appendChild(container); + const root: Root = createRoot(container); + act(() => { + root.render(element); + }); + return { + container, + unmount: () => { + act(() => root.unmount()); + container.remove(); + }, + }; +} + +describe("RawSegmentsPanel", () => { + it("test_section_label_includes_count", () => { + const segments = [ + ["ISA", "00", " ", "00", " ", "ZZ", "SUBMITTERID ", "ZZ", "RECEIVERID ", "260615", "1200", "^", "00501", "000000001", "0", "P", ":"], + ["GS", "HC", "SUBMITTERID", "RECEIVERID", "20260615", "1200", "1", "X", "005010X222A1"], + ["ST", "837", "0001", "005010X222A1"], + ]; + + const { container, unmount } = renderIntoContainer( + + ); + + const label = container.querySelector('[data-testid="section-label"]'); + expect(label).not.toBeNull(); + + // `uppercase` is applied via Tailwind CSS, so textContent gives the + // source-case string "Raw Segments"; assert case-insensitively and + // confirm the visual transform via the className sniff below. + const text = (label?.textContent ?? "").toLowerCase(); + expect(text).toContain("raw segments"); + expect(text).toContain("(3)"); + + // Eyebrow style (uppercase + tracking) — sniff the className. + const cls = label?.className ?? ""; + expect(cls).toContain("uppercase"); + expect(cls).toContain("tracking-[0.18em]"); + + unmount(); + }); + + it("test_collapsed_by_default", () => { + const segments = [ + ["ISA", "00", " "], + ["GS", "HC", "X"], + ]; + + const { container, unmount } = renderIntoContainer( + + ); + + const details = container.querySelector("details"); + expect(details).not.toBeNull(); + // The HTML `open` attribute is absent (not just `false`) when + // collapsed — assert the attribute is literally missing. + expect(details?.hasAttribute("open")).toBe(false); + + unmount(); + }); + + it("test_clicking_summary_expands_the_panel", () => { + const segments = [ + ["ISA", "00"], + ["GS", "HC"], + ]; + + const { container, unmount } = renderIntoContainer( + + ); + + const details = container.querySelector("details"); + const summary = container.querySelector("summary"); + expect(details).not.toBeNull(); + expect(summary).not.toBeNull(); + // Pre-click: collapsed. + expect(details?.hasAttribute("open")).toBe(false); + + // Click the summary to expand. + act(() => { + summary?.dispatchEvent( + new MouseEvent("click", { bubbles: true, cancelable: true }) + ); + }); + + expect(details?.open).toBe(true); + + unmount(); + }); + + it("test_expanded_panel_renders_segments_with_star_separator", () => { + const segments = [ + ["ISA", "00", " ", "00", " ", "ZZ", "SUBMITTERID "], + ["GS", "HC", "SUBMITTERID", "RECEIVERID", "20260615", "1200", "1", "X", "005010X222A1"], + ]; + + const { container, unmount } = renderIntoContainer( + + ); + + const details = container.querySelector("details"); + const summary = container.querySelector("summary"); + expect(summary).not.toBeNull(); + act(() => { + summary?.dispatchEvent( + new MouseEvent("click", { bubbles: true, cancelable: true }) + ); + }); + expect(details?.open).toBe(true); + + const pre = container.querySelector('[data-testid="raw-segments-pre"]'); + expect(pre).not.toBeNull(); + const text = pre?.textContent ?? ""; + + // First segment: elements joined by "*" on a single line. + expect(text).toContain("ISA*00*"); + expect(text).toContain("SUBMITTERID "); + // Second segment also rendered. + expect(text).toContain("GS*HC*SUBMITTERID*RECEIVERID*20260615*1200*1*X*005010X222A1"); + + // Lines are separated — newlines in the rendered text. + const lines = text.split("\n"); + expect(lines.length).toBeGreaterThanOrEqual(2); + + unmount(); + }); + + it("test_empty_array_renders_count_and_empty_state", () => { + const { container, unmount } = renderIntoContainer( + + ); + + // Section label still present with count "(0)". + const label = container.querySelector('[data-testid="section-label"]'); + expect(label?.textContent ?? "").toContain("(0)"); + + // Empty-state body is present. + const empty = container.querySelector('[data-testid="raw-segments-empty"]'); + expect(empty).not.toBeNull(); + expect(empty?.textContent ?? "").toContain("No segments"); + + // The
 with segments is NOT rendered in the empty state.
+    expect(
+      container.querySelector('[data-testid="raw-segments-pre"]')
+    ).toBeNull();
+
+    // The 
/ are still present so the user can see + // the section header even when there's nothing inside. + const details = container.querySelector("details"); + expect(details).not.toBeNull(); + const summary = container.querySelector("summary"); + expect(summary).not.toBeNull(); + + unmount(); + }); + + it("test_uses_monospace_font_for_pre_block", () => { + const segments = [["ST", "837", "0001"]]; + + const { container, unmount } = renderIntoContainer( + + ); + + const summary = container.querySelector("summary"); + act(() => { + summary?.dispatchEvent( + new MouseEvent("click", { bubbles: true, cancelable: true }) + ); + }); + + const pre = container.querySelector('[data-testid="raw-segments-pre"]'); + expect(pre).not.toBeNull(); + // The mono styling is either via class or inline style; sniff both. + const cls = pre?.className ?? ""; + const style = pre?.getAttribute("style") ?? ""; + const isMono = + cls.includes("font-mono") || + style.includes("--m-font-mono") || + style.includes("font-mono"); + expect(isMono).toBe(true); + + unmount(); + }); +}); diff --git a/src/components/ClaimDrawer/RawSegmentsPanel.tsx b/src/components/ClaimDrawer/RawSegmentsPanel.tsx new file mode 100644 index 0000000..2ae2d74 --- /dev/null +++ b/src/components/ClaimDrawer/RawSegmentsPanel.tsx @@ -0,0 +1,75 @@ +import React from "react"; +import type { ClaimDetail } from "@/types"; + +type RawSegmentsPanelProps = { + rawSegments: ClaimDetail["rawSegments"]; +}; + +/** + * Format a single segment by joining its elements with `*` (the X12 + * element separator). Trailing/leading whitespace inside individual + * elements is preserved — these strings come straight off the parser + * and any trimming would change the visible byte-for-byte content. + */ +function formatSegment(elements: string[]): string { + return elements.join("*"); +} + +/** + * Raw-segments debug section of the claim detail drawer (SP4). + * + * Renders the exact X12 segment strings the parser saw, collapsed by + * default behind a native
/ so the user can opt in + * to a wall of monospace when they're debugging. Element separator is + * `*` and segment terminator is implicit at the end of each line — + * matches what the parser produces. This is a read-only diagnostic; + * the plan keeps it intentionally simple (no copy button for v1). + */ +export function RawSegmentsPanel({ rawSegments }: RawSegmentsPanelProps) { + return ( +
+

+ Raw Segments ({rawSegments.length}) +

+ +
+ + {rawSegments.length === 0 + ? "Show raw segments" + : "Show raw segments"} + + + {rawSegments.length === 0 ? ( +

+ No segments +

+ ) : ( +
+            {rawSegments.map((segment, i) => (
+              
+                {i > 0 ? "\n" : null}
+                {formatSegment(segment)}
+              
+            ))}
+          
+ )} +
+
+ ); +}