import { afterEach, describe, expect, it, vi } from "vitest"; import { render, screen } from "@testing-library/react"; import { DiveEnvironmentCard } from "./dive-environment-card"; import type { Dive } from "@/lib/api/dives"; import { dive, diveConditions } from "@/test/dive-fixtures"; import type { UnitSystem } from "@/lib/units"; const auth = vi.hoisted(() => ({ units: "metric" as UnitSystem })); vi.mock("@/contexts/AuthContext", () => ({ useAuth: () => ({ user: { uuid: "user-1", units: auth.units } }), })); afterEach(() => { auth.units = "metric"; }); // The Blue Hole dive as the worker leaves it: three fields filled, and the // service's own figures beside them. const filledDive = (overrides: Partial = {}) => dive({ start_time: "2026-09-15T10:00:00+03:00", air_temperature: 38.1, weather: "clear", waves: "slight", conditions: diveConditions(), ...overrides, }); const figure = (label: string | RegExp) => screen.getByText(label).closest("div")!; const tideDiagram = () => screen.getByRole("img", { name: /^Tide:/ }); const isMarked = (label: string | RegExp) => figure(label).querySelector('[title^="Filled in from weather data"]') !== null; describe("DiveEnvironmentCard, with fetched conditions", () => { it("shows the service's figures beside the diver's", () => { render(); expect(screen.getByText("38.1°C")).toBeInTheDocument(); expect(screen.getByText("Clear")).toBeInTheDocument(); expect(screen.getByText("Slight")).toBeInTheDocument(); expect(screen.getByText("Wind from NNE")).toBeInTheDocument(); expect(screen.getByText("11 kn")).toBeInTheDocument(); expect(figure("Sea temperature")).toHaveTextContent("27.7°C"); expect(figure("Wave height")).toHaveTextContent("0.6 m"); expect(tideDiagram()).toBeInTheDocument(); }); // Each turn at the record's offset, whatever offset the instant was written // at: 06:30 UTC is 09:30 at +03:00. it("draws the tide between its turns, at the record's offset", () => { render(); expect(tideDiagram()).toHaveAccessibleName( "Tide: high water at 09:30, low water at 15:00; " + "the dive began at 10:00, at high water", ); expect(tideDiagram()).toHaveTextContent("High 09:30"); expect(tideDiagram()).toHaveTextContent("Low 15:00"); expect(screen.queryByText(/^(Last|Next) /)).toBeNull(); }); // The curve runs on past each turn by a quarter of the 5.5 h between them, so // 8.25 h are drawn from 08:07:30: the turns sit a sixth in from either end, and // the dive's 10:00 is 1.875 h along, just past the top of the high. it("marks the dive where it falls between the turns", () => { render(); const start = screen.getByTestId("tide-dive-start"); expect(start.style.left).toBe("22.73%"); expect(start.style.top).toBe("11.62%"); expect(screen.getByText("09:30").parentElement!.style.left).toBe("16.67%"); expect(screen.getByText("15:00").parentElement!.style.left).toBe("83.33%"); expect(screen.getByTestId("tide-dive-span")).toBeInTheDocument(); }); // A dive whose own offset is unknown takes the one the record resolved, and // one turn gives a curve nothing to span, so it stays the figures. it("spells them at the record's offset for a dive with none of its own", () => { render( , ); expect(figure("Next high")).toHaveTextContent("11:20"); expect(figure("Tide")).toHaveTextContent("Rising"); expect(screen.queryByRole("img", { name: /^Tide:/ })).toBeNull(); }); it("shows the wind in knots and the waves in feet on an imperial account", () => { auth.units = "imperial"; render(); expect(screen.getByText("11 kn")).toBeInTheDocument(); expect(figure("Wave height")).toHaveTextContent("2 ft"); }); it("marks the filled fields and nothing else", () => { render(); expect(isMarked("Air temperature")).toBe(true); expect(isMarked("Weather")).toBe(true); expect(isMarked("Waves")).toBe(true); expect(isMarked("Current")).toBe(false); }); // The diver's own value, and a value replaced since the fetch wrote it, carry // no mark. it("leaves a typed value unmarked", () => { render( , ); expect(isMarked("Weather")).toBe(false); expect(isMarked("Air temperature")).toBe(false); expect(isMarked("Waves")).toBe(true); }); it("credits the service as visible text, with its links", () => { render(); expect( screen.getByRole("link", { name: "Weather data by Open-Meteo.com" }), ).toHaveAttribute("href", "https://open-meteo.com/"); expect(screen.getByRole("link", { name: "CC BY 4.0" })).toHaveAttribute( "href", "https://creativecommons.org/licenses/by/4.0/", ); expect( screen.getByText(/with weather, waves and tide times derived from it/), ).toBeInTheDocument(); }); it("renders on the fetched figures alone", () => { render( , ); expect(screen.getByText("Environment")).toBeInTheDocument(); expect(screen.getByText("11 kn")).toBeInTheDocument(); expect( screen.getByRole("link", { name: "Weather data by Open-Meteo.com" }), ).toBeInTheDocument(); }); // A freshwater dive is answered with no sea block. it("draws no sea figure the record lacks", () => { render( , ); expect(screen.queryByText("Sea temperature")).toBeNull(); expect(screen.queryByText("Wave height")).toBeNull(); expect(screen.queryByText("Tide")).toBeNull(); expect(screen.queryByText(/^(Last|Next) /)).toBeNull(); expect(screen.getByText("Wind from NNE")).toBeInTheDocument(); }); }); describe("DiveEnvironmentCard, without fetched conditions", () => { it.each([ ["absent", undefined], ["null", null], ])("draws nothing new when they are %s", (_label, conditions) => { render( , ); expect(isMarked("Weather")).toBe(false); expect(screen.queryByText(/Wind/)).toBeNull(); expect(screen.queryByText(/Open-Meteo/)).toBeNull(); }); it("renders nothing for a dive with nothing to show", () => { const { container } = render(); expect(container).toBeEmptyDOMElement(); }); });