Ctrl K
ComunidadGuías…

src/components/certifications/certification-card.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 { act, render, screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import type {
  Certification,
  CertificationFileInfo,
} from "@/lib/api/certifications";
import { reveal } from "@/test/intersection";
import { CertificationCard } from "./certification-card";

vi.mock("@/lib/api/certifications", async (importOriginal) => {
  const actual =
    await importOriginal<typeof import("@/lib/api/certifications")>();
  return {
    ...actual,
    certificationsAPI: {
      ...actual.certificationsAPI,
      getCertificationFileBlob: vi.fn(),
    },
  };
});

const { certificationsAPI } = await import("@/lib/api/certifications");
const getBlob = vi.mocked(certificationsAPI.getCertificationFileBlob);

// jsdom implements neither.
const originalCreate = URL.createObjectURL;
const originalRevoke = URL.revokeObjectURL;
beforeEach(() => {
  getBlob.mockReset();
  getBlob.mockResolvedValue(new Blob(["card"], { type: "image/webp" }));
  URL.createObjectURL = vi.fn(() => "blob:front");
  URL.revokeObjectURL = vi.fn();
});
afterEach(() => {
  URL.createObjectURL = originalCreate;
  URL.revokeObjectURL = originalRevoke;
});

const front = (
  overrides: Partial<CertificationFileInfo> = {},
): CertificationFileInfo => ({
  uuid: "file-1",
  side: "front",
  content_type: "image/webp",
  byte_size: 1024,
  original_filename: "front.webp",
  updated_at: "2026-03-01T00:00:00Z",
  ...overrides,
});

const certification = (
  overrides: Partial<Certification> = {},
): Certification => ({
  uuid: "cert-1",
  agency: "padi",
  name: "Advanced Nitrox",
  user_uuid: "user-1",
  created_at: "2026-01-01T00:00:00+00:00",
  ...overrides,
});

const handlers = () => ({
  onView: vi.fn(),
  onEdit: vi.fn(),
  onDelete: vi.fn(),
});

// The card is a list item, as every backdrop card is.
const card = (
  overrides: Partial<Certification> = {},
  props: Partial<ReturnType<typeof handlers>> = {},
) =>
  render(
    <ul>
      <CertificationCard
        certification={certification(overrides)}
        {...handlers()}
        {...props}
        isDeleting={false}
      />
    </ul>,
  );

const figure = (label: string) =>
  screen.getByText(label, { selector: "dt" }).nextElementSibling;

describe("CertificationCard", () => {
  it("opens the certification from its level, a certification having no page", async () => {
    const onView = vi.fn();
    card({}, { onView });

    await userEvent.click(
      screen.getByRole("button", { name: "Advanced Nitrox" }),
    );

    expect(onView).toHaveBeenCalledOnce();
  });

  it("is drawn in the c-card's shape rather than over a band of backdrop", () => {
    // jsdom answers no layout question, so this is as far as the shape can be
    // checked here: the class that gives it, in place of the band's.
    const { container } = card();
    const item = container.querySelector("li");

    expect(item).toHaveClass("aspect-[1013/638]");
    expect(item).not.toHaveClass("pt-33");
  });

  it("names its agency under the level, the diver's own for another one", () => {
    card({ agency: "other", agency_other: "Freediving Federation" });

    expect(screen.getByText("Freediving Federation")).toBeInTheDocument();
  });

  it("carries its number, then the day it was earned", () => {
    card({ certified_on: "2025-03-12", certification_number: "2309AB1234" });

    expect(figure("Number")).toHaveTextContent("2309AB1234");
    expect(figure("Certified")).toHaveTextContent("Mar 12, 2025");
    expect(screen.getAllByRole("term").map((term) => term.textContent)).toEqual(
      ["Number", "Certified"],
    );
  });

  it("shows only the figures it has", () => {
    card({ certification_number: "2309AB1234" });

    expect(screen.getAllByRole("term").map((term) => term.textContent)).toEqual(
      ["Number"],
    );
  });

  it("badges a lapsed card, and leaves one with no expiry alone", () => {
    const { unmount } = card({ expires_on: "2000-01-01" });
    expect(screen.getByText("Expired")).toBeInTheDocument();
    unmount();

    card();
    expect(screen.queryByText("Expired")).not.toBeInTheDocument();
    expect(screen.queryByText("Expiring soon")).not.toBeInTheDocument();
  });

  it("edits and deletes from a menu named after the agency and level", async () => {
    const onEdit = vi.fn();
    const onDelete = vi.fn();
    const { container } = card({}, { onEdit, onDelete });
    const item = container.querySelector("li") as HTMLElement;

    const menu = within(item).getByRole("button", {
      name: "Actions for PADI Advanced Nitrox",
    });
    await userEvent.click(menu);
    await userEvent.click(
      await screen.findByRole("menuitem", { name: "Edit" }),
    );
    expect(onEdit).toHaveBeenCalledOnce();

    await userEvent.click(menu);
    await userEvent.click(
      await screen.findByRole("menuitem", { name: "Delete" }),
    );
    expect(onDelete).toHaveBeenCalledOnce();
  });
});

describe("CertificationCard's backdrop", () => {
  it("draws the card's front once it is on screen, versioned by the file", async () => {
    const { container } = card({ files: [front()] });
    expect(getBlob).not.toHaveBeenCalled();

    act(() => reveal());

    await waitFor(() =>
      expect(container.querySelector("img")).toHaveAttribute(
        "src",
        "blob:front",
      ),
    );
    expect(getBlob).toHaveBeenCalledWith(
      "cert-1",
      "front",
      "file-1:2026-03-01T00:00:00Z",
    );
    // Decorative: the level over it carries the name.
    expect(container.querySelector("img")).toHaveAttribute("alt", "");
  });

  it("draws the water and asks for nothing where no front is stored", () => {
    const { container } = card({
      files: [front({ side: "back" })],
    });
    act(() => reveal());

    expect(getBlob).not.toHaveBeenCalled();
    expect(container.querySelector("img")).toBeNull();
    expect(container.querySelector(".lucide-badge-check")).not.toBeNull();
  });

  it("never fetches a PDF, which is not drawn", () => {
    const { container } = card({
      files: [front({ content_type: "application/pdf" })],
    });
    act(() => reveal());

    expect(getBlob).not.toHaveBeenCalled();
    expect(container.querySelector(".lucide-file-text")).not.toBeNull();
  });

  it("draws the water, not a broken image, when the front cannot be had", async () => {
    getBlob.mockRejectedValue(new Error("gone"));
    const { container } = card({ files: [front()] });
    act(() => reveal());

    await waitFor(() =>
      expect(container.querySelector(".lucide-image-off")).not.toBeNull(),
    );
    expect(container.querySelector("img")).toBeNull();
  });
});