Ctrl K
ComunidadGuías…

src/components/dives/gas-use-chart.render.test.tsx

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { afterEach, describe, it, expect, vi } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import { GasUseChart } from "./gas-use-chart";
import type { DiveGasUsePoint } from "@/lib/api/dive-stats";
import type { UnitSystem } from "@/lib/units";

// These renders read the diver's units, so they need an auth context. Held in a
// mutable box rather than a fixed literal so a test can switch systems - `vi.mock`'s
// factory is hoisted above the file, and `vi.hoisted` is what lets it close over
// something the tests can still reach.
const auth = vi.hoisted(() => ({ units: "metric" as UnitSystem }));

vi.mock("@/contexts/AuthContext", () => ({
  useAuth: () => ({ user: { uuid: "user-1", units: auth.units } }),
}));

afterEach(() => {
  auth.units = "metric";
});

// Multi-cylinder dives reach this chart for the first time in Phase 4, and they arrive
// with an RMV that was *not* divided by the dive's average depth. What a render pins is
// that the dot's own description knows the difference — the accessible name especially,
// since it is the only version of the sentence a screen-reader user gets.

function point(overrides: Partial<DiveGasUsePoint> = {}): DiveGasUsePoint {
  return {
    dive_uuid: "d1",
    dive_number: 493,
    start_time: "2026-04-17T11:49:00+02:00",
    avg_depth: 20.87,
    gas_use: { gas_used: 1520.7, rmv: 10.31, sac_bar_per_min: 0.93 },
    ...overrides,
  } as DiveGasUsePoint;
}

// Dive #493 exactly: the rate is 12.4 L/min derived at a mean 33.99 m over the attributed
// stretch, on a dive whose own average depth is 20.87 m.
function multiTankPoint(): DiveGasUsePoint {
  return point({
    gas_use: {
      gas_used: 1887,
      rmv: 12.4,
      sac_bar_per_min: null,
      tanks: [
        {
          gas_number: 0,
          gas_used: 1887,
          rmv: 12.4,
          sac_bar_per_min: 0.56,
          seconds_on_gas: 2075,
          mean_depth: 33.99,
        },
      ],
      attributed_seconds: 2075,
      duration: 4300,
    },
  });
}

// The chart shows an empty state below two dives, so every case needs a companion
// point. It is deliberately an ordinary single-cylinder one, so the assertions below
// are about the point under test rather than about the only dot on the chart.
function renderChart(points: DiveGasUsePoint[]) {
  render(
    <GasUseChart
      points={[
        point({
          dive_uuid: "d0",
          dive_number: 1,
          start_time: "2026-04-16T09:00:00+02:00",
        }),
        ...points,
      ]}
      scope="all"
      anchor={Date.parse("2026-04-17T11:49:00+02:00")}
    />,
  );
}

describe("GasUseChart point descriptions", () => {
  it("writes the whole dot description in imperial", () => {
    auth.units = "imperial";
    render(
      <GasUseChart
        points={[point(), multiTankPoint()]}
        scope="all"
        anchor={Date.parse("2026-04-17T11:49:00+02:00")}
      />,
    );

    // The rate in cuft/min spelled out, and the depth it was divided by in feet.
    expect(
      screen.getByLabelText(
        /Dive #493,.* 0.36 cubic feet per minute at 68 ft average/,
      ),
    ).toBeTruthy();
  });

  // The chart's own accessible name, which is the one figure the whole plot is read
  // against. It has to carry the same precision as the visible "Median" stat above
  // it: a cubic foot is 28 litres, so one decimal would quantize every plausible RMV
  // into about five buckets.
  it("gives the summary the same precision as the visible median", () => {
    auth.units = "imperial";
    render(
      <GasUseChart
        points={[point(), multiTankPoint()]}
        scope="all"
        anchor={Date.parse("2026-04-17T11:49:00+02:00")}
      />,
    );

    // The median of 10.31 and 12.4 L/min is 11.355, which is 0.40 cuft/min - not "0.4".
    expect(
      screen.getByLabelText(/all-time median of 0.40 cubic feet per minute\./),
    ).toBeTruthy();
  });

  it("names the average depth behind a single-cylinder rate", () => {
    // Unchanged, and the reason the depth is worth naming at all: it is the
    // denominator, so quoting it makes the figure checkable.
    renderChart([point()]);

    expect(
      screen.getByLabelText(
        /Dive #493,.* 10.31 liters per minute at 20.87 m average/,
      ),
    ).toBeInTheDocument();
  });

  it("does not pin a per-tank rate to the dive's average depth", () => {
    // The whole point of the split is that each cylinder is normalized against
    // its own mean depth. Naming 20.87 m beside a rate derived at 33.99 m would
    // be a denominator the figure was never divided by.
    renderChart([multiTankPoint()]);

    const dot = screen.getByLabelText(/12.4 liters per minute/);

    expect(dot).toHaveAccessibleName(/derived per cylinder/);
    expect(dot).not.toHaveAccessibleName(/20.87/);
    expect(dot).not.toHaveAccessibleName(/average/);
  });

  // The two assertions above cover the announced label. The two below cover the
  // *visible* one, which is the path almost every diver takes and which the
  // first version of this file left untested - a regression putting `avg_depth`
  // back into the tooltip while leaving the aria label alone would have shipped
  // green.
  it("does not pin the visible tooltip to the dive's average depth either", () => {
    renderChart([multiTankPoint()]);

    // Focus, not hover: the chart drives its tooltip from both, and focus is
    // the one that works without a layout engine.
    fireEvent.focus(screen.getByLabelText(/12.4 liters per minute/));

    expect(screen.getByText(/Per tank across 1 cylinder/)).toBeInTheDocument();
    expect(screen.queryByText(/20.87 m average/)).not.toBeInTheDocument();
  });

  it("keeps the average depth in the tooltip of a single-cylinder dot", () => {
    renderChart([point({ dive_number: 493 })]);

    fireEvent.focus(
      screen.getByLabelText(/Dive #493,.* 10.31 liters per minute/),
    );

    expect(
      screen.getByText(/20.87 m average · 1520.7 L used/),
    ).toBeInTheDocument();
  });
});

describe("GasUseChart bins", () => {
  it("never draws a box with a negative width at the window's edge", () => {
    // At "all" the window starts at the first dive, so the first bin is cut to
    // whatever of it the dives occupy: three dives on the last two days of a
    // month, in a four-year career read in month bins, leave it under a unit
    // wide - narrower than the inset either side of a box.
    const dives = [
      "2022-01-30T09:00:00+02:00",
      "2022-01-30T14:00:00+02:00",
      "2022-01-31T09:00:00+02:00",
      "2024-06-10T09:00:00+02:00",
      "2026-01-15T09:00:00+02:00",
    ];
    const { container } = render(
      <GasUseChart
        points={dives.map((start_time, index) =>
          point({ dive_uuid: `d${index}`, dive_number: index + 1, start_time }),
        )}
        scope="all"
        anchor={Date.parse(dives[0])}
      />,
    );

    const boxes = [...container.querySelectorAll("g.pointer-events-none rect")];
    expect(boxes).toHaveLength(1);
    expect(Number(boxes[0].getAttribute("width"))).toBeGreaterThanOrEqual(0);
  });
});