src/components/dives/dive-hero.render.test.tsx
Rama main · réplica de lectura del repositorio original.
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<Dive> = {}) =>
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(
<DiveHero
back={BACK}
dive={dive({
dive_number: 212,
dive_sites: [
{
uuid: "site-1",
name: "The Canyon",
location: { name: "Dahab, Egypt" },
},
] as Dive["dive_sites"],
max_depth: 30.52,
avg_depth: 18.2,
})}
/>,
);
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(
<DiveHero
back={BACK}
dive={dive({
water_type: "fresh",
entry_type: "boat",
boat_name: "Legend",
type: "closed_circuit",
})}
/>,
);
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(
<DiveHero
back={BACK}
dive={dive({
recordings: [
{
uuid: "rec-1",
ordinal: 0,
files: [],
profile: {
uuid: "p1",
duration: 3_163_000,
depth_sample_count: 314,
provenance: "file",
channels: ["depth", "ceiling"],
max_ceiling: 6,
},
},
],
})}
/>,
);
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(
<DiveHero
back={BACK}
dive={dive({
recordings: [
{ uuid: "rec-1", ordinal: 0, files: [] },
{ uuid: "rec-2", ordinal: 1, files: [] },
],
})}
/>,
);
expect(
screen.getByRole("heading", { level: 1 }).nextElementSibling,
).toHaveTextContent("Apr 4, 2021, 10:04 · 2 dive computers");
unmount();
render(<DiveHero back={BACK} dive={dive({ recordings: [] })} />);
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(
<DiveHero
back={BACK}
dive={dive({ water_type: "salt", tags: ["night", "Wreck"], rating: 4 })}
/>,
);
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(
<DiveHero
back={BACK}
dive={dive({ water_type: "salt", type: "open_circuit" })}
/>,
);
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(<DiveHero back={BACK} dive={dive({ course_uuid: "course-1" })} />);
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(<DiveHero back={BACK} dive={dive({ max_depth: 30.48 })} />);
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(<DiveHero back={BACK} dive={dive()} />);
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(
<DiveHero
back={BACK}
dive={dive({
max_depth: 30.52,
bottom_temperature: 25.05,
visibility: 15,
})}
/>,
);
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<Dive["sightings"]>[number];
render(
<DiveHero
back={BACK}
dive={dive({
sightings: [sighting("a"), sighting("b")],
visibility: 15,
})}
/>,
);
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(<DiveHero back={BACK} dive={dive({ sightings: [] })} />);
expect(figure("Species seen")).toBeUndefined();
});
it("leaves the average depth off, however few figures it has", () => {
render(<DiveHero back={BACK} dive={dive({ avg_depth: 18.2 })} />);
expect(labels()).toEqual(["Dive time"]);
});
it("keeps a zero-degree temperature, which is a reading rather than an absence", () => {
render(<DiveHero back={BACK} dive={dive({ bottom_temperature: 0 })} />);
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(
<DiveHero
back={BACK}
dive={dive({ bottom_temperature: 22, visibility: 15 })}
/>,
);
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(<DiveHero back={BACK} dive={dive()} />);
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(<DiveHero back={BACK} dive={dive(EXIT)} />);
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(<DiveHero back={BACK} dive={dive(EXIT)} />);
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(<DiveHero back={BACK} dive={dive(EXIT)} />);
expect(screen.queryByRole("link", { name: /OpenStreetMap/ })).toBeNull();
});
it("tells a recorded fix apart from a placed pin on the map", () => {
render(
<DiveHero back={BACK} dive={dive({ dive_sites: [SITE], ...EXIT })} />,
);
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(
<DiveHero
back={BACK}
dive={dive({ exit_latitude: 0, exit_longitude: 0 })}
/>,
);
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(
<DiveHero
back={BACK}
dive={dive({
dive_sites: [{ ...SITE, latitude: null, longitude: null }],
})}
/>,
);
expect((mapProps().water as React.ReactElement).type).toBe(
UnplacedBackdrop,
);
expect(screen.queryByRole("link", { name: /OpenStreetMap/ })).toBeNull();
});
});