fix(clip): typed times belong to their value, and every mark on the player chrome can be seen
TimecodeField drops a draft when the value moves under it (another segment selected, a nudge), so leaving the field never commits it to another cut; it is invalid only once offered, with a line under it saying how to type a time. On the always-dark chrome media-scope points every focus ring at --media-mark and errors at the new --media-danger (a light theme's accent and danger were under 3:1 there). A track whose dragged handle unmounts ends the drag on the pointer's release instead of locking up; a remove refused while clips are queued leaves no focus to take later; each remove-cut button names its cut and is a 24px target. DESIGN.md records the media tokens; stale comments fixed.
This commit is contained in:
@@ -102,6 +102,41 @@ describe("VideoEditor — a split export that fails part-way", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// A time typed into the selected segment's field belongs to that segment: selecting another one
|
||||
// drops it, and never commits it to the new selection's cut.
|
||||
describe("VideoEditor — a typed time and another segment", () => {
|
||||
it("does not move the newly selected segment's cut with a time typed for the old one", () => {
|
||||
vi.spyOn(api, "downloadStoryboard").mockResolvedValue({ available: false } as never);
|
||||
render(
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<VideoEditor job={job} onClose={() => {}} />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
const video = document.querySelector("video")!;
|
||||
for (const at of [30, 60]) {
|
||||
Object.defineProperty(video, "currentTime", {
|
||||
value: at,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
fireEvent.timeUpdate(video);
|
||||
fireEvent.click(screen.getByText("editor.splitHere"));
|
||||
}
|
||||
const segs = () => screen.getAllByTitle("editor.keptSeg");
|
||||
fireEvent.pointerDown(segs()[1]!);
|
||||
const inField = screen.getByRole("textbox", { name: "editor.in" });
|
||||
act(() => inField.focus());
|
||||
fireEvent.change(inField, { target: { value: "0:40" } });
|
||||
// Another segment is selected before the field loses the focus (a press on it).
|
||||
fireEvent.pointerDown(segs()[2]!);
|
||||
fireEvent.blur(inField);
|
||||
const cuts = screen
|
||||
.getAllByRole("slider", { name: "editor.cutN" })
|
||||
.map((c) => c.getAttribute("aria-valuenow"));
|
||||
expect(cuts).toEqual(["30", "60"]);
|
||||
});
|
||||
});
|
||||
|
||||
// The cuts sit on the shared time track: each is a slider the keyboard steps, and a step stops a
|
||||
// tenth short of the next cut, so no segment ever closes up.
|
||||
describe("VideoEditor — a cut on the keyboard", () => {
|
||||
@@ -172,6 +207,34 @@ describe("VideoEditor — a cut's remove button", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("a remove refused while the clips are queued leaves no focus to take later", async () => {
|
||||
let refuse: (e: unknown) => void = () => {};
|
||||
vi.spyOn(api, "enqueueEdit").mockImplementation(
|
||||
() =>
|
||||
new Promise((_, reject) => {
|
||||
refuse = reject;
|
||||
}) as never,
|
||||
);
|
||||
split();
|
||||
fireEvent.click(screen.getByText("editor.createN"));
|
||||
await waitFor(() => expect(api.enqueueEdit).toHaveBeenCalled());
|
||||
// The cut list holds still while the clips are queued: the remove is refused.
|
||||
const remove = screen.getByRole("button", { name: "editor.removeCut" });
|
||||
act(() => remove.focus());
|
||||
fireEvent.click(remove);
|
||||
expect(screen.getByRole("slider", { name: "editor.cutN" })).toBeTruthy();
|
||||
await act(async () => refuse(new Error("refused")));
|
||||
// The create failed and the editor stays; a later split keeps the focus where it was.
|
||||
const video = document.querySelector("video")!;
|
||||
Object.defineProperty(video, "currentTime", { value: 70, configurable: true, writable: true });
|
||||
fireEvent.timeUpdate(video);
|
||||
const splitButton = screen.getByText("editor.splitHere").closest("button")!;
|
||||
act(() => splitButton.focus());
|
||||
fireEvent.click(splitButton);
|
||||
expect(screen.getAllByRole("slider", { name: "editor.cutN" })).toHaveLength(2);
|
||||
expect(document.activeElement).toBe(splitButton);
|
||||
});
|
||||
|
||||
it("sits at the cut's foot, under the next segment's keep toggle at its head", () => {
|
||||
split();
|
||||
const remove = screen.getByRole("button", { name: "editor.removeCut" });
|
||||
|
||||
Reference in New Issue
Block a user