feat(frontend): RawSegmentsPanel collapsible debug aid

This commit is contained in:
Tyler
2026-06-20 11:39:59 -06:00
parent 94a39e9dd6
commit 7c7e724464
2 changed files with 278 additions and 0 deletions
@@ -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(
<RawSegmentsPanel rawSegments={segments} />
);
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(
<RawSegmentsPanel rawSegments={segments} />
);
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(
<RawSegmentsPanel rawSegments={segments} />
);
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(
<RawSegmentsPanel rawSegments={segments} />
);
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(
<RawSegmentsPanel rawSegments={[]} />
);
// 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 <pre> with segments is NOT rendered in the empty state.
expect(
container.querySelector('[data-testid="raw-segments-pre"]')
).toBeNull();
// The <details>/<summary> 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(
<RawSegmentsPanel rawSegments={segments} />
);
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();
});
});
@@ -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 <details>/<summary> 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 (
<section
className="flex flex-col gap-3 px-6 py-4"
data-testid="raw-segments-panel"
>
<h3
data-testid="section-label"
className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-[color:var(--m-ink-tertiary)]"
>
Raw Segments ({rawSegments.length})
</h3>
<details className="rounded-lg border border-[color:var(--m-border-heavy)]/15 bg-[color:var(--m-surface)]">
<summary
className="cursor-pointer select-none px-4 py-2 text-sm text-[color:var(--m-ink-secondary)] hover:text-[color:var(--m-ink-primary)]"
data-testid="raw-segments-summary"
>
{rawSegments.length === 0
? "Show raw segments"
: "Show raw segments"}
</summary>
{rawSegments.length === 0 ? (
<p
data-testid="raw-segments-empty"
className="px-4 pb-3 text-sm text-[color:var(--m-ink-tertiary)]"
>
No segments
</p>
) : (
<pre
data-testid="raw-segments-pre"
className="overflow-x-auto whitespace-pre-wrap px-4 pb-3 pt-1 text-[12px] leading-relaxed text-[color:var(--m-ink-primary)] font-mono"
style={{ fontFamily: "var(--m-font-mono)" }}
>
{rawSegments.map((segment, i) => (
<React.Fragment key={i}>
{i > 0 ? "\n" : null}
<span data-testid="raw-segment-line">{formatSegment(segment)}</span>
</React.Fragment>
))}
</pre>
)}
</details>
</section>
);
}