import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { render, screen } from "@testing-library/react"; import { DiveHero } from "./dive-hero"; import { DiveIcon } from "@/components/logo"; import { UnplacedBackdrop } from "@/components/ui/backdrop-card"; import type { Dive } from "@/lib/api/dives"; import { dive as baseDive } from "@/test/dive-fixtures"; import type { UnitSystem } from "@/lib/units"; // The dive page's heading: its title and line, the three numbers that describe // the shape of the dive, and what its map is handed - the map itself is covered // where it lives. The dive time's arithmetic is `formatDurationHoursMinutes`'s, // tested in `lib/date-time.test.ts`; what a render adds is which figures stand. const auth = vi.hoisted(() => ({ units: "metric" as UnitSystem })); vi.mock("@/contexts/AuthContext", () => ({ useAuth: () => ({ user: { uuid: "user-1", units: auth.units } }), })); const tiles = vi.hoisted(() => ({ drawn: true as boolean | undefined })); vi.mock("@/components/map/map-backdrop", () => ({ MapBackdrop: vi.fn(() => null), useMapTiles: () => tiles.drawn, })); const { MapBackdrop } = await import("@/components/map/map-backdrop"); const mapProps = () => vi.mocked(MapBackdrop).mock.lastCall![0]; beforeEach(() => { vi.mocked(MapBackdrop).mockClear(); }); afterEach(() => { auth.units = "metric"; tiles.drawn = true; }); const dive = (overrides: Partial = {}) => baseDive({ start_time: "2021-04-04T10:04:47+02:00", ...overrides }); // Dahab: an exit fix from the corpus, and a site's pin. const EXIT = { exit_latitude: 28.4375, exit_longitude: 34.4584 }; const SITE = { uuid: "site-uuid", name: "Blue Hole", latitude: 28.5721, longitude: 34.5372, } as Dive["dive_sites"][number]; const BACK = { href: "/dives", label: "Back to dives" }; const figure = (label: string) => screen.queryByText(label, { selector: "dt" })?.nextElementSibling; describe("DiveHero", () => { it("heads the page with the dive's title, its start and place, and its figures", () => { render( , ); const heading = screen.getByRole("heading", { level: 1 }); expect(heading).toHaveTextContent("#212 The Canyon"); // Its card's line, in the dive's own timezone. expect(heading.nextElementSibling).toHaveTextContent( "Apr 4, 2021, 10:04 · Dahab, Egypt", ); expect(figure("Dive time")).toHaveTextContent("45min"); // Whole units, as the dive's card rounds them. expect(figure("Max depth")).toHaveTextContent("31 m"); expect(figure("Avg depth")).toBeUndefined(); expect(screen.getByRole("link", { name: "Back to dives" })).toHaveAttribute( "href", "/dives", ); }); it("adds the boat to its line, and tags its title with the water and the kind", () => { render( , ); expect( screen.getByRole("heading", { level: 1 }).nextElementSibling, ).toHaveTextContent("Apr 4, 2021, 10:04 · Boat Legend"); expect( screen.getByText("CCR").parentElement!.parentElement, ).toHaveTextContent("FreshFresh waterCCRClosed circuit"); }); it("tags a dive whose charted profile has a ceiling as a deco dive", () => { render( , ); expect(screen.getByText("Deco")).toHaveAttribute("aria-hidden"); expect(screen.getByText("Decompression dive")).toHaveClass("sr-only"); }); it("ends its line with a dive computer for each recording", () => { const { unmount } = render( , ); expect( screen.getByRole("heading", { level: 1 }).nextElementSibling, ).toHaveTextContent("Apr 4, 2021, 10:04 · 2 dive computers"); unmount(); render(); expect( screen.getByRole("heading", { level: 1 }).nextElementSibling, ).not.toHaveTextContent("dive computer"); }); it("tags its title with the diver's tags in teal, and rates the dive after it", () => { render( , ); expect(screen.getByText("Wreck").parentElement).toHaveTextContent( /^nightWreck$/, ); expect(screen.getByText("Wreck")).toHaveClass("bg-teal"); expect( screen.getByRole("heading", { level: 1, name: "Dive #1 4 of 5 stars" }), ).toBeInTheDocument(); }); // Each goes without saying. it("tags neither salt water nor open circuit", () => { render( , ); expect( screen.getByRole("heading", { level: 1 }).nextElementSibling, ).toHaveTextContent(/^Apr 4, 2021, 10:04$/); expect(screen.queryByText("Salt water")).not.toBeInTheDocument(); expect(screen.queryByText("Open circuit")).not.toBeInTheDocument(); }); it("marks a training dive's title with the course's icon", () => { render(); expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent( "Dive #1 (training dive)", ); }); it("shows the depth in feet for an imperial diver", () => { // Whole feet, and the value behind them is still the 30.48 m the API sent. auth.units = "imperial"; render(); expect(figure("Max depth")).toHaveTextContent("100 ft"); }); it("leaves the dive time on its own for a dive with no depths", () => { // Every hand-logged dive that skipped them. An empty depth beside the // dive time would read as something the diver failed to fill in. render(); expect(figure("Dive time")).toHaveTextContent("45min"); expect(figure("Max depth")).toBeUndefined(); }); const labels = () => Array.from(document.querySelectorAll("dt"), (dt) => dt.textContent); it("shows the water's temperature and the visibility after the depths, rounded", () => { render( , ); expect(figure("Water temp")).toHaveTextContent("25°C"); expect(figure("Visibility")).toHaveTextContent("15 m"); expect(labels()).toEqual([ "Dive time", "Max depth", "Water temp", "Visibility", ]); }); it("counts the species seen, after the other figures", () => { const sighting = (uuid: string) => ({ uuid }) as NonNullable[number]; render( , ); expect(figure("Species seen")).toHaveTextContent("2"); expect(labels()).toEqual(["Dive time", "Visibility", "Species seen"]); }); it("leaves the species off a dive whose sightings are empty", () => { render(); expect(figure("Species seen")).toBeUndefined(); }); it("leaves the average depth off, however few figures it has", () => { render(); expect(labels()).toEqual(["Dive time"]); }); it("keeps a zero-degree temperature, which is a reading rather than an absence", () => { render(); expect(figure("Water temp")).toHaveTextContent("0°C"); }); it("reads the water's temperature and the visibility in the diver's units", () => { auth.units = "imperial"; render( , ); expect(figure("Water temp")).toHaveTextContent("°F"); expect(figure("Visibility")).toHaveTextContent("ft"); }); it("leaves off a temperature and a visibility the dive does not record", () => { render(); expect(figure("Water temp")).toBeUndefined(); expect(figure("Visibility")).toBeUndefined(); }); it("hands its map the dive's places, laid out as a hero's, and credits it itself", () => { render(); expect(mapProps()).toMatchObject({ locations: [ { name: "Exit", latitude: 28.4375, longitude: 34.4584, variant: "fix" }, ], subject: "the location of dive #1", hero: true, }); expect( screen.getByRole("link", { name: /OpenStreetMap/ }), ).toBeInTheDocument(); }); // The water its unplaced sibling shows, icon and all. it("gives its map the card's water, and credits no map, where this instance draws none", () => { tiles.drawn = false; render(); const { water } = mapProps() as { water: React.ReactElement }; expect(water.type).toBe(UnplacedBackdrop); expect(water.props).toMatchObject({ icon: DiveIcon }); expect(screen.queryByRole("link", { name: /OpenStreetMap/ })).toBeNull(); }); it("credits no map before the instance has said whether it draws one", () => { tiles.drawn = undefined; render(); expect(screen.queryByRole("link", { name: /OpenStreetMap/ })).toBeNull(); }); it("tells a recorded fix apart from a placed pin on the map", () => { render( , ); expect(mapProps().locations).toEqual([ { name: "Blue Hole", latitude: 28.5721, longitude: 34.5372 }, { name: "Exit", latitude: 28.4375, longitude: 34.4584, variant: "fix" }, ]); }); it("keeps an equator fix, which is a position rather than an absence", () => { // A dive off West Africa exits at longitude 0. render( , ); expect(mapProps().locations).toEqual([ { name: "Exit", latitude: 0, longitude: 0, variant: "fix" }, ]); }); it("draws the map's water for a dive with no position, and credits no map", () => { // A site without a pin is no position either. render( , ); expect((mapProps().water as React.ReactElement).type).toBe( UnplacedBackdrop, ); expect(screen.queryByRole("link", { name: /OpenStreetMap/ })).toBeNull(); }); });