// @vitest-environment jsdom // // The time field: invalid only once a time was offered, said in words on the caller's line, and a // draft that never outlives the value it was typed over. import { afterEach, describe, expect, it, vi } from "vitest"; import { cleanup, fireEvent, render, screen } from "@testing-library/react"; import { TimecodeError, TimecodeField } from "./TimecodeField"; import { useTimecodeField } from "./useTimecodeField"; vi.mock("react-i18next", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useTranslation: () => ({ t: (k: string) => k }) }; }); afterEach(cleanup); function Harness({ value, onCommit }: { value: number; onCommit: (s: number) => void }) { const field = useTimecodeField(value, false, onCommit); return ( <> {field.invalid && } ); } const field = () => screen.getByRole("textbox") as HTMLInputElement; describe("TimecodeField", () => { it("is not invalid while a time is still being typed, only once it is offered", () => { render( {}} />); fireEvent.change(field(), { target: { value: "" } }); fireEvent.change(field(), { target: { value: "1:" } }); expect(field().getAttribute("aria-invalid")).toBeNull(); fireEvent.blur(field()); expect(field().getAttribute("aria-invalid")).toBe("true"); // Once one was offered, the field re-checks as it is typed: the mark goes the moment it reads. fireEvent.change(field(), { target: { value: "1:0" } }); expect(field().getAttribute("aria-invalid")).toBeNull(); }); it("says in words, read out and linked to the field, how a time is typed", () => { render( {}} />); fireEvent.change(field(), { target: { value: "abc" } }); fireEvent.keyDown(field(), { key: "Enter" }); const id = field().getAttribute("aria-describedby"); expect(id).toBeTruthy(); expect(document.getElementById(id!)?.textContent).toBe("common.timecodeInvalid"); expect(screen.getByRole("alert").id).toBe(id); fireEvent.change(field(), { target: { value: "0:12" } }); fireEvent.keyDown(field(), { key: "Enter" }); expect(field().getAttribute("aria-describedby")).toBeNull(); }); it("drops a draft when the value moves under it, and never commits it to the new value", () => { const onCommit = vi.fn(); const { rerender } = render(); fireEvent.change(field(), { target: { value: "0:05" } }); // Another segment selected under the same field (or a nudge): the field edits a new value now. rerender(); expect(field().value).toBe("0:40.0"); fireEvent.blur(field()); expect(onCommit).not.toHaveBeenCalled(); // An unreadable one goes too, and its mark with it. fireEvent.change(field(), { target: { value: "x" } }); fireEvent.blur(field()); expect(field().getAttribute("aria-invalid")).toBe("true"); rerender(); expect(field().value).toBe("0:41.0"); expect(field().getAttribute("aria-invalid")).toBeNull(); }); });