import { describe, it, expect } from "vitest"; import { render, screen } from "@testing-library/react"; import { DiveExposureCard } from "./dive-exposure-card"; import type { Dive, Recording, RecordingReadouts } from "@/lib/api/dives"; import { dive as baseDive } from "@/test/dive-fixtures"; // Nothing here is derived - the card renders five stored numbers exactly as the device // recorded them - so what a render actually tests is the decisions layered on top of // that: which recording it reads, whether the card appears at all, how a half-recorded // pair reads, and whether passing the CNS line reaches a diver who cannot see the // colour it is drawn in. function recording( readouts: RecordingReadouts, overrides: Partial = {}, ): Recording { return { uuid: "r0", ordinal: 0, files: [], ...readouts, ...overrides }; } // A dive whose one recording - the primary - carries `readouts`. function dive( readouts: RecordingReadouts, recordings: Recording[] = [recording(readouts)], ): Dive { return baseDive({ recordings }); } describe("DiveExposureCard's recording", () => { it("reads the primary recording's, not a second computer's", () => { // Listed out of order on purpose: ordinal 0 is primary however the list // arrives, and the backup's own clock is a different machine's accounting. render( , ); expect(screen.getByText("9%")).toBeInTheDocument(); expect(screen.queryByText("50%")).not.toBeInTheDocument(); }); it("renders nothing when only a second computer reported anything", () => { const { container } = render( , ); expect(container).toBeEmptyDOMElement(); }); it("renders nothing on a dive logged by hand, which has no recording", () => { const { container } = render(); expect(container).toBeEmptyDOMElement(); }); }); describe("DiveExposureCard visibility", () => { it("renders nothing when the recording carries none of the three", () => { // Every hand-logged dive, and every FIT or 2026 Suunto Ocean import. An empty card // headed "Oxygen Exposure" would read as something the diver forgot to fill in, // for fields no form lets them fill in at all. const { container } = render(); expect(container).toBeEmptyDOMElement(); }); it("renders nothing when the API spells those five out as null", () => { // The shape actually on the wire: `RecordingReadouts` declares all five // `float | None` with no `exclude_none`, so a recording that reported none // sends explicit nulls rather than omitting the keys. The `!= null` guards cover both, and // this is here so they keep having to - a fixture built from absent keys is // how three mixture fields shipped a form that could not be saved. const { container } = render( , ); expect(container).toBeEmptyDOMElement(); }); it.each([ ["CNS alone", { cns_end: 9 }], ["OTU alone", { otu_end: 23 }], ["surface pressure alone", { surface_pressure_bar: 1.057 }], // A FIT file records an end-OTU and has no start-OTU field at all, so this // pair arrives half-null on every one of them. ["a half-null pair", { otu_start: null, otu_end: 23 }], ])("renders on %s", (_label, exposure) => { render(); expect(screen.getByText(/Exposure & Pressure/)).toBeInTheDocument(); }); it("is not titled for the two readings it may not contain", () => { // 42 of the 384 XML exports in the corpus record a surface pressure and neither // CNS nor OTU, so "Oxygen Exposure" would head a card holding one barometer // reading. Gating the card on CNS/OTU instead would drop that reading entirely. render(); expect(screen.queryByText(/Oxygen Exposure/)).not.toBeInTheDocument(); expect(screen.getByText("1.057 bar")).toBeInTheDocument(); }); it("shows only the readings the recording actually has", () => { render(); expect(screen.getByText("1.057 bar")).toBeInTheDocument(); expect(screen.queryByText("CNS")).not.toBeInTheDocument(); expect(screen.queryByText("OTU")).not.toBeInTheDocument(); }); }); describe("DiveExposureCard readings", () => { it("keeps the pair visible when a format records only the end", () => { // Every FIT file: the profile has no start-OTU field at all. Collapsing to a lone // "23" would claim the dive began at zero loading, which for a repetitive dive is // the one thing the number is there to contradict. render(); expect(screen.getByText("—")).toBeInTheDocument(); expect(screen.getByText("23")).toBeInTheDocument(); }); it("renders a recorded zero as a reading, not as an absence", () => { // `!= null`, not truthiness: a dive that began with no oxygen loading recorded a // real 0, and the API keeps it apart from "didn't record it" on purpose. render(); expect(screen.getByText("0%")).toBeInTheDocument(); expect(screen.queryByText("—")).not.toBeInTheDocument(); }); }); describe("DiveExposureCard CNS limit", () => { it("says nothing beyond the number below the limit", () => { render(); expect(screen.queryByText(/single-dive limit/i)).not.toBeInTheDocument(); }); it.each([100, 105])("marks a CNS of %s in text as well as colour", (cns) => { // The boundary is `>=`, so exactly 100 alerts. Asserted through the accessible // name rather than the class: a `text-warning` that reached no screen reader is // the failure this covers (WCAG 2.1 SC 1.4.1), and the class alone can't show it. render(); expect( screen.getByText(/over the 100% single-dive limit/i), ).toBeInTheDocument(); }); it("marks the end value, not the start the diver arrived with", () => { render(); expect(screen.queryByText(/single-dive limit/i)).not.toBeInTheDocument(); }); it("never marks OTU, which has no comparable single-dive line", () => { render(); expect(screen.queryByText(/single-dive limit/i)).not.toBeInTheDocument(); }); });