import { describe, expect, it } from "vitest";
import { useState } from "react";
import { fireEvent, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { DiveStartTimeField } from "./dive-start-time-field";
// The composite field owns the one conversion in the app that turns two
// controls into a single ISO `start_time`, so what it does with a half-empty
// pair is its own question rather than either control's.
function Field({ initial }: { initial: string }) {
const [value, setValue] = useState(initial);
return (
<>
>
);
}
const box = () => screen.getByRole("textbox");
const committed = () => screen.getByTestId("committed").textContent;
describe("DiveStartTimeField", () => {
it("keeps a typed edit and the dive's own offset together", async () => {
render();
await userEvent.clear(box());
await userEvent.type(box(), "2021-04-04 11:30:00");
await userEvent.tab();
expect(committed()).toBe("2021-04-04T11:30:00+02:00");
});
it("empties the whole value when the box is emptied", async () => {
// Not `combineStartTime("", 120)`, which is the bare string "+02:00": it
// has no time for the offset matcher to anchor on, so it reads back as an
// offsetless value, `new Date()` refuses it, and the field redraws as
// "NaN-NaN-NaN NaN:NaN:NaN" beside an offset that has gone "Not recorded".
render();
await userEvent.clear(box());
await userEvent.tab();
expect(committed()).toBe("");
expect(box()).toHaveValue("");
expect(screen.queryByText(/NaN/)).not.toBeInTheDocument();
});
});
// A dive imported with only its date. Only a logbook import makes one, and the
// API keeps the state for as long as the form sends the bare date back.
describe("DiveStartTimeField on a dive whose time of day was never recorded", () => {
const time = () => screen.getByLabelText("Time");
const offset = () => screen.getByRole("combobox", { name: "UTC offset" });
it("shows the date, an empty time and no offset to choose", () => {
render();
expect(box()).toHaveValue("2002-06-18");
expect(time()).toHaveValue("");
expect(offset()).toBeDisabled();
expect(offset()).toHaveDisplayValue("Not recorded");
expect(screen.queryByDisplayValue(/00:00/)).not.toBeInTheDocument();
});
it("re-dates the dive and keeps it a date", async () => {
render();
await userEvent.clear(box());
await userEvent.type(box(), "2002-06-19");
await userEvent.tab();
expect(committed()).toBe("2002-06-19");
});
it("becomes a date-time with no offset once a time is typed", () => {
render();
fireEvent.change(time(), { target: { value: "10:30" } });
expect(committed()).toBe("2002-06-18T10:30:00");
// The same controls stay on screen, now with a clock to put a zone on.
expect(time()).toHaveValue("10:30:00");
expect(offset()).toBeEnabled();
});
it("puts the typed time in the zone picked for it", async () => {
render();
fireEvent.change(time(), { target: { value: "10:30" } });
await userEvent.selectOptions(offset(), "UTC+05:45");
expect(committed()).toBe("2002-06-18T10:30:00+05:45");
// A zone once adopted is not given back: the API refuses to remove one.
expect(
screen.queryByRole("option", { name: "Not recorded" }),
).not.toBeInTheDocument();
});
it("goes back to the bare date when the typed time is emptied", () => {
render();
fireEvent.change(time(), { target: { value: "10:30" } });
fireEvent.change(time(), { target: { value: "" } });
expect(committed()).toBe("2002-06-18");
expect(offset()).toBeDisabled();
});
it("takes the date-only shape when the value arrives after mount", () => {
// The edit form mounts with `""` and resets to the dive once it loads.
function Late() {
const [value, setValue] = useState("");
return (
<>
>
);
}
render();
fireEvent.click(screen.getByRole("button", { name: "load" }));
expect(box()).toHaveValue("2002-06-18");
expect(time()).toHaveValue("");
});
});