src/components/dives/dive-exposure-card.render.test.tsx
Rama main · réplica de lectura del repositorio original.
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> = {},
): 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(
<DiveExposureCard
dive={dive({}, [
recording({ cns_start: 0, cns_end: 50 }, { uuid: "r1", ordinal: 1 }),
recording({ cns_start: 0, cns_end: 9 }),
])}
/>,
);
expect(screen.getByText("9%")).toBeInTheDocument();
expect(screen.queryByText("50%")).not.toBeInTheDocument();
});
it("renders nothing when only a second computer reported anything", () => {
const { container } = render(
<DiveExposureCard
dive={dive({}, [
recording({}),
recording({ otu_end: 23 }, { uuid: "r1", ordinal: 1 }),
])}
/>,
);
expect(container).toBeEmptyDOMElement();
});
it("renders nothing on a dive logged by hand, which has no recording", () => {
const { container } = render(<DiveExposureCard dive={dive({}, [])} />);
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(<DiveExposureCard dive={dive({})} />);
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(
<DiveExposureCard
dive={dive({
cns_start: null,
cns_end: null,
otu_start: null,
otu_end: null,
surface_pressure_bar: null,
})}
/>,
);
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(<DiveExposureCard dive={dive(exposure)} />);
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(<DiveExposureCard dive={dive({ surface_pressure_bar: 1.057 })} />);
expect(screen.queryByText(/Oxygen Exposure/)).not.toBeInTheDocument();
expect(screen.getByText("1.057 bar")).toBeInTheDocument();
});
it("shows only the readings the recording actually has", () => {
render(<DiveExposureCard dive={dive({ surface_pressure_bar: 1.057 })} />);
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(<DiveExposureCard dive={dive({ otu_end: 23 })} />);
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(<DiveExposureCard dive={dive({ cns_start: 0, cns_end: 9 })} />);
expect(screen.getByText("0%")).toBeInTheDocument();
expect(screen.queryByText("—")).not.toBeInTheDocument();
});
});
describe("DiveExposureCard CNS limit", () => {
it("says nothing beyond the number below the limit", () => {
render(<DiveExposureCard dive={dive({ cns_start: 8, cns_end: 99 })} />);
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(<DiveExposureCard dive={dive({ cns_start: 8, cns_end: cns })} />);
expect(
screen.getByText(/over the 100% single-dive limit/i),
).toBeInTheDocument();
});
it("marks the end value, not the start the diver arrived with", () => {
render(<DiveExposureCard dive={dive({ cns_start: 120, cns_end: 9 })} />);
expect(screen.queryByText(/single-dive limit/i)).not.toBeInTheDocument();
});
it("never marks OTU, which has no comparable single-dive line", () => {
render(<DiveExposureCard dive={dive({ otu_start: 0, otu_end: 350 })} />);
expect(screen.queryByText(/single-dive limit/i)).not.toBeInTheDocument();
});
});