Ctrl K
ComunidadGuías…

src/components/dives/dive-hero.render.test.tsx

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

Archivo original ↗Preguntar a KiBot
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();
  });
});