- useTimecodeField holds a time field's draft for its caller, which places TimecodeError (role alert) in its own flow under the fields' row: it no longer lies over the crop row taking its clicks, runs out of the editor's card, or joins the field's <label> name. - The editor's remove-cut buttons sit in a row under the track at their cut (trackLeft), off the cut's grab area, where a press drags it; RangeTrack's adornment slot is gone. - media-focus re-points --focus to the media amber on the Plex player, the YouTube stage and every bg-media-chrome surface; a contract test holds each such class string to media-focus or media-scope, the field's focus edge to the focus token, and --color-focus to the inline theme. - An upload's quota headline says the converted file does not fit, not "Storage full". - DESIGN.md: the tokens paragraph says what is re-pointed where; the decision row measures all three colours on the chrome.
73 lines
3.2 KiB
TypeScript
73 lines
3.2 KiB
TypeScript
// @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<typeof import("react-i18next")>();
|
|
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 (
|
|
<>
|
|
<TimecodeField aria-label="In" field={field} errorId="err" />
|
|
{field.invalid && <TimecodeError id="err" hours={false} />}
|
|
</>
|
|
);
|
|
}
|
|
|
|
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(<Harness value={10} onCommit={() => {}} />);
|
|
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(<Harness value={10} onCommit={() => {}} />);
|
|
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(<Harness value={10} onCommit={onCommit} />);
|
|
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(<Harness value={40} onCommit={onCommit} />);
|
|
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(<Harness value={41} onCommit={onCommit} />);
|
|
expect(field().value).toBe("0:41.0");
|
|
expect(field().getAttribute("aria-invalid")).toBeNull();
|
|
});
|
|
});
|