src/components/dives/dive-section-dialog.render.test.tsx
Rama main · réplica de lectura del repositorio original.
import { beforeEach, describe, expect, it, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { DiveSectionDialog } from "./dive-section-dialog";
import { divesAPI, type Dive } from "@/lib/api/dives";
import { diveConditions } from "@/test/dive-fixtures";
// The seam is the PATCH body: a card's dialog sends its section and nothing else,
// whatever the rest of the dive holds. `user` is returned by identity - see the note
// in `dives/new/page.render.test.tsx`.
const stable = vi.hoisted(() => ({
auth: {
user: { uuid: "user-1", dive_form_hidden_fields: [] as string[] },
mergeUser: vi.fn(),
},
toast: { toast: vi.fn() },
}));
vi.mock("@/contexts/AuthContext", () => ({
useAuth: () => stable.auth,
}));
vi.mock("@/components/ui/use-toast", () => ({
useToast: () => stable.toast,
}));
vi.mock("@/lib/api/dives", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/api/dives")>();
return {
...actual,
divesAPI: { ...actual.divesAPI, updateDive: vi.fn() },
};
});
vi.mock("@/lib/api/tags", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/api/tags")>();
return { ...actual, fetchAllTags: vi.fn() };
});
const tags = await import("@/lib/api/tags");
const storedDive = (overrides: Partial<Dive> = {}): Dive =>
({
uuid: "dive-1",
user_uuid: "user-1",
dive_number: 41,
start_time: "2026-07-01T09:30:00+02:00",
submerged_time: 2700,
max_depth: 18.3,
rating: 3,
tags: ["night"],
notes: "Saw a turtle.",
mixtures: [],
gear_items: [],
dive_sites: [],
sightings: [],
...overrides,
}) as Dive;
const renderNotes = (dive: Dive, onSaved = vi.fn()) => {
const onOpenChange = vi.fn();
render(
<DiveSectionDialog
open
onOpenChange={onOpenChange}
dive={dive}
section="Notes"
onSaved={onSaved}
/>,
);
return { onSaved, onOpenChange };
};
const saveChanges = () =>
userEvent.click(screen.getByRole("button", { name: /save changes/i }));
beforeEach(() => {
vi.clearAllMocks();
stable.auth.user.dive_form_hidden_fields = [];
vi.mocked(divesAPI.updateDive).mockResolvedValue({ message: "ok" });
vi.mocked(tags.fetchAllTags).mockResolvedValue([]);
});
describe("a card's edit dialog", () => {
it("sends its section's fields and nothing else", async () => {
const { onSaved, onOpenChange } = renderNotes(storedDive());
// Wait for the dialog's initial focus before editing a field inside it.
await waitFor(() => expect(screen.getByRole("dialog")).toHaveFocus());
const notes = screen.getByRole("textbox", { name: /^notes$/i });
await userEvent.clear(notes);
await userEvent.type(notes, "Two turtles.");
await saveChanges();
await waitFor(() => expect(divesAPI.updateDive).toHaveBeenCalled());
expect(vi.mocked(divesAPI.updateDive).mock.calls[0]).toEqual([
"dive-1",
{ rating: 3, tags: ["night"], notes: "Two turtles." },
]);
expect(onSaved).toHaveBeenCalled();
expect(onOpenChange).toHaveBeenCalledWith(false);
});
it("is not blocked by a stored value it does not show", async () => {
// Below the schema's floor, and nowhere in the Notes section to correct.
renderNotes(storedDive({ max_depth: -1 }));
await saveChanges();
await waitFor(() => expect(divesAPI.updateDive).toHaveBeenCalled());
expect(vi.mocked(divesAPI.updateDive).mock.calls[0][1]).not.toHaveProperty(
"max_depth",
);
});
it("shows a field of its section that the stored set hides", () => {
stable.auth.user.dive_form_hidden_fields = ["rating"];
renderNotes(storedDive({ rating: null }));
expect(
screen.getByRole("radiogroup", { name: /^rating$/i }),
).toBeInTheDocument();
});
it("stays open and says why when the save fails", async () => {
vi.mocked(divesAPI.updateDive).mockRejectedValue(new Error("boom"));
const { onSaved, onOpenChange } = renderNotes(storedDive());
await saveChanges();
expect(
await screen.findByText("Failed to update dive. Please try again."),
).toBeInTheDocument();
expect(onSaved).not.toHaveBeenCalled();
expect(onOpenChange).not.toHaveBeenCalled();
});
});
describe("the Environment dialog on a dive with fetched conditions", () => {
const filled = () =>
storedDive({
air_temperature: 38.1,
weather: "clear",
conditions: diveConditions({
filled: { air_temperature: 38.1, weather: "clear", waves: null },
}),
});
const renderEnvironment = (dive: Dive) =>
render(
<DiveSectionDialog
open
onOpenChange={vi.fn()}
dive={dive}
section="Environment"
onSaved={vi.fn()}
/>,
);
const marked = () =>
[...document.querySelectorAll('[title^="Filled in from weather data"]')]
.map((mark) => mark.closest("label")?.childNodes[0]?.textContent ?? "")
.map((text) => text.trim());
it("marks the fetched fields and credits the service", () => {
renderEnvironment(filled());
expect(marked()).toEqual(
expect.arrayContaining([
expect.stringMatching(/^Weather/),
expect.stringMatching(/^Air temperature/),
]),
);
expect(marked()).toHaveLength(2);
expect(
screen.getByRole("link", { name: "Weather data by Open-Meteo.com" }),
).toBeInTheDocument();
});
it("takes the mark off a field the diver changes, and the credit with the last", async () => {
renderEnvironment(
storedDive({
air_temperature: 38.1,
conditions: diveConditions({
filled: { air_temperature: 38.1, weather: null, waves: null },
}),
}),
);
// Wait for the dialog's initial focus before editing a field inside it.
await waitFor(() => expect(screen.getByRole("dialog")).toHaveFocus());
const air = screen.getByRole("spinbutton", { name: /air temperature/i });
await userEvent.clear(air);
await userEvent.type(air, "30");
expect(air).toHaveValue(30);
expect(marked()).toHaveLength(0);
expect(screen.queryByText(/Open-Meteo/)).toBeNull();
});
// The API keeps a fill it is sent a blank for, so the field would come back
// fetched after the save.
it("keeps the mark and the credit on a field the diver empties", async () => {
renderEnvironment(
storedDive({
air_temperature: 38.1,
conditions: diveConditions({
filled: { air_temperature: 38.1, weather: null, waves: null },
}),
}),
);
await userEvent.clear(
screen.getByRole("spinbutton", { name: /air temperature/i }),
);
expect(marked()).toHaveLength(1);
expect(
screen.getByRole("link", { name: "Weather data by Open-Meteo.com" }),
).toBeInTheDocument();
});
it("marks nothing on a dive with no conditions", () => {
renderEnvironment(storedDive({ weather: "clear", conditions: null }));
expect(marked()).toHaveLength(0);
expect(screen.queryByText(/Open-Meteo/)).toBeNull();
});
});