fix(keys): epic-end review round 3 — the part-way clip count shows, busy buttons keep focus, cards say their names
Targeted round over 3cd0c525's fix surfaces: 14 raised, 14 fixed. - VideoEditor: the "only X of Y clips" toast went silent exactly on the job-cap/quota refusal it was written for (req had already reported the error); it now always shows. The cut list holds still while Create queues, and a retry continues the "(i/N)" series. - ChannelPage: Subscribe/Unsubscribe use Button `loading`, "Load more" and ChannelDiscovery's Subscribe block the click with aria-disabled instead of native disabled; busy Block/Hide no longer light up on hover. - listNav: an item can name itself (`navItemAttrs(i, name)`) — Plex posters, seasons, episodes, message rows and download rows pass their title; focus on a toast's Undo keeps the removed card's slot. - AnchoredPopover: the hand-back only for focus-managed surfaces (a hover card no longer pulls focus back); the dead panel-ref check is gone. - AccountPanel: no confirmation mail offered to the demo account; send_failed copy no longer blames the message for an unreachable server. Pager comment points at scrollParent.
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
// @vitest-environment jsdom
|
||||
//
|
||||
// A split export that fails part-way (deep-review E11 round 2). Every clip is its own job, committed as
|
||||
// it goes, so a failure at clip 2 of 2 (the job cap) left clip 1 queued while the editor reported a
|
||||
// plain failure and kept both selected — and trying again queued clip 1 a second time. It now says how
|
||||
// many made it and deselects them, so another try adds only the rest.
|
||||
// A split export that fails part-way (deep-review E11 rounds 2 and 3). Every clip is its own job,
|
||||
// committed as it goes, so a failure at clip 2 of 2 (the job cap) left clip 1 queued while the editor
|
||||
// reported a plain failure and kept both selected — and trying again queued clip 1 a second time. It
|
||||
// now says how many made it (also when `req` has already shown the refusal in its dialog), deselects
|
||||
// them, holds the cut list still while it queues, and a retry goes on with the same "(i/N)" series.
|
||||
import { describe, it, expect, vi, beforeAll, afterEach } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor, cleanup } from "@testing-library/react";
|
||||
import { render, screen, fireEvent, waitFor, cleanup, act } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
vi.mock("react-i18next", async (importOriginal) => ({
|
||||
@@ -20,6 +21,7 @@ vi.mock("../lib/notifications", async (importOriginal) => ({
|
||||
|
||||
import VideoEditor from "./VideoEditor";
|
||||
import { api, HttpError, type DownloadJob } from "../lib/api";
|
||||
import { markReported } from "../lib/api/core";
|
||||
|
||||
beforeAll(() => {
|
||||
class Stub {
|
||||
@@ -48,10 +50,16 @@ const job = {
|
||||
describe("VideoEditor — a split export that fails part-way", () => {
|
||||
it("says how many clips made it, and a retry queues only the rest", async () => {
|
||||
vi.spyOn(api, "downloadStoryboard").mockResolvedValue({ available: false } as never);
|
||||
let refuse: (e: unknown) => void = () => {};
|
||||
const enqueue = vi
|
||||
.spyOn(api, "enqueueEdit")
|
||||
.mockResolvedValueOnce({} as never)
|
||||
.mockRejectedValueOnce(new HttpError(422, "errors.generic", { code: "quota" }))
|
||||
.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((_, reject) => {
|
||||
refuse = reject;
|
||||
}) as never,
|
||||
)
|
||||
.mockResolvedValue({} as never);
|
||||
render(
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
@@ -59,23 +67,37 @@ describe("VideoEditor — a split export that fails part-way", () => {
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
// Split at 50 s: the playhead is where the video element says it is.
|
||||
// The playhead is where the video element says it is.
|
||||
const video = document.querySelector("video")!;
|
||||
Object.defineProperty(video, "currentTime", { value: 50, configurable: true, writable: true });
|
||||
fireEvent.timeUpdate(video);
|
||||
const playhead = (s: number) => {
|
||||
Object.defineProperty(video, "currentTime", { value: s, configurable: true, writable: true });
|
||||
fireEvent.timeUpdate(video);
|
||||
};
|
||||
playhead(50);
|
||||
fireEvent.click(screen.getByText("editor.splitHere"));
|
||||
|
||||
fireEvent.click(screen.getByText("editor.createN"));
|
||||
await waitFor(() => expect(enqueue).toHaveBeenCalledTimes(2));
|
||||
// While the clips are queued the cut list holds still: a split now would be a range never queued.
|
||||
playhead(75);
|
||||
fireEvent.click(screen.getByText("editor.splitHere"));
|
||||
|
||||
// The job cap, as `req` delivers it: already shown in the error dialog, and marked reported.
|
||||
const refused = new HttpError(422, "errors.generic", { code: "quota" });
|
||||
markReported(refused);
|
||||
await act(async () => refuse(refused));
|
||||
await waitFor(() =>
|
||||
expect(notify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ level: "error", title: "editor.partlyCreated" }),
|
||||
),
|
||||
);
|
||||
expect(enqueue).toHaveBeenCalledTimes(2);
|
||||
expect(enqueue.mock.calls[0]![0].display_name).toBe("Talk (1/2)");
|
||||
|
||||
// The queued first clip is deselected: one clip left to create, and only it is sent again.
|
||||
// The queued first clip is deselected: one clip left to create, and only it is sent again — as
|
||||
// the second of the same two.
|
||||
fireEvent.click(screen.getByText("editor.create"));
|
||||
await waitFor(() => expect(enqueue).toHaveBeenCalledTimes(3));
|
||||
expect(enqueue.mock.calls[2]![0].edit_spec.trim).toEqual({ start_s: 50, end_s: 100 });
|
||||
expect(enqueue.mock.calls[2]![0].display_name).toBe("Talk (2/2)");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user