Exclude the absolute column move controls from FLIP content animations so their translateY centering remains intact throughout left and right column switches.
229 lines
9.5 KiB
TypeScript
229 lines
9.5 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import * as columnDrag from "../src/renderer/views/downloads/column-drag";
|
|
import { calculateColumnDragPreview, updateDownloadColumnDrag, type DownloadColumnDragSession } from "../src/renderer/views/downloads/column-drag";
|
|
|
|
const columns = [
|
|
{ id: "name", left: 0, width: 300 },
|
|
{ id: "size", left: 300, width: 150 },
|
|
{ id: "status", left: 450, width: 100 }
|
|
];
|
|
|
|
describe("animated download column drag", () => {
|
|
it("moves a narrow right column left while adjacent columns make room", () => {
|
|
expect(calculateColumnDragPreview(columns, "status", -420)).toEqual({
|
|
order: ["status", "name", "size"],
|
|
offsets: { name: 100, size: 100, status: -420 },
|
|
settleOffsets: { name: 100, size: 100, status: -450 }
|
|
});
|
|
});
|
|
|
|
it("moves a wide left column right using the measured column widths", () => {
|
|
expect(calculateColumnDragPreview(columns, "name", 500)).toEqual({
|
|
order: ["size", "status", "name"],
|
|
offsets: { name: 500, size: -300, status: -300 },
|
|
settleOffsets: { name: 250, size: -300, status: -300 }
|
|
});
|
|
});
|
|
|
|
it("keeps the original order while the dragged center has not crossed a neighbor", () => {
|
|
expect(calculateColumnDragPreview(columns, "size", 20).order).toEqual(["name", "size", "status"]);
|
|
});
|
|
|
|
it("writes only the continuous active offset while the target slot stays unchanged", () => {
|
|
const setProperty = vi.fn();
|
|
const root = {
|
|
classList: { add: vi.fn() },
|
|
dataset: { columnDragging: "size" },
|
|
querySelectorAll: vi.fn(() => []),
|
|
style: { setProperty }
|
|
} as unknown as HTMLElement;
|
|
const session = {
|
|
active: true,
|
|
draggedId: "size",
|
|
measurements: columns,
|
|
pointerId: 1,
|
|
preview: calculateColumnDragPreview(columns, "size", 20),
|
|
root,
|
|
startX: 0
|
|
} as DownloadColumnDragSession;
|
|
|
|
updateDownloadColumnDrag(session, 25);
|
|
|
|
expect(setProperty).toHaveBeenCalledTimes(1);
|
|
expect(setProperty).toHaveBeenCalledWith("--downloads-active-drag-x", "25px");
|
|
});
|
|
|
|
it.each([
|
|
{ afterLeft: 150, beforeLeft: 0, expectedDelta: -150, measurements: columns, next: ["size", "name", "status"] },
|
|
{
|
|
afterLeft: 0,
|
|
beforeLeft: 150,
|
|
expectedDelta: 150,
|
|
measurements: [
|
|
{ id: "size", left: 0, width: 150 },
|
|
{ id: "name", left: 150, width: 300 },
|
|
{ id: "status", left: 450, width: 100 }
|
|
],
|
|
next: ["name", "size", "status"]
|
|
}
|
|
])("commits the target grid before animating from a $expectedDelta px inverse offset", ({ afterLeft, beforeLeft, expectedDelta, measurements, next }) => {
|
|
const commitDownloadColumnDrag = (columnDrag as unknown as {
|
|
commitDownloadColumnDrag?: (session: DownloadColumnDragSession, order: string[], commit: (order: string[]) => void, prepare: () => void) => Animation[];
|
|
}).commitDownloadColumnDrag;
|
|
expect(commitDownloadColumnDrag).toBeTypeOf("function");
|
|
if (!commitDownloadColumnDrag) return;
|
|
|
|
const events: string[] = [];
|
|
const selectors: string[] = [];
|
|
let committed = false;
|
|
const outerAnimate = vi.fn(() => ({ finished: Promise.resolve() } as unknown as Animation));
|
|
const animate = vi.fn(() => ({ finished: Promise.resolve() } as unknown as Animation));
|
|
const motionTarget = { animate, matches: () => false } as unknown as HTMLElement;
|
|
const controlsAnimate = vi.fn(() => ({ finished: Promise.resolve() } as unknown as Animation));
|
|
const controls = {
|
|
animate: controlsAnimate,
|
|
matches: (selector: string) => selector === ".downloads-column-move-controls"
|
|
} as unknown as HTMLElement;
|
|
const element = {
|
|
animate: outerAnimate,
|
|
children: [motionTarget, controls],
|
|
getBoundingClientRect: () => ({ left: committed ? afterLeft : beforeLeft, width: 300 })
|
|
} as unknown as HTMLElement;
|
|
const root = {
|
|
classList: { add: () => events.push("add-class"), remove: () => events.push("clear-classes") },
|
|
dataset: { columnDragging: "name" },
|
|
querySelectorAll: (selector: string) => {
|
|
selectors.push(selector);
|
|
return selector.includes('data-download-column="name"') ? [element] : [];
|
|
},
|
|
style: {
|
|
removeProperty: (property: string) => events.push(`clear:${property}`)
|
|
}
|
|
} as unknown as HTMLElement;
|
|
const session = {
|
|
active: true,
|
|
draggedId: "name",
|
|
measurements,
|
|
pointerId: -1,
|
|
preview: calculateColumnDragPreview(measurements, "name", next[0] === "name" ? -160 : 160),
|
|
root,
|
|
startX: 0
|
|
} as DownloadColumnDragSession;
|
|
|
|
const animations = commitDownloadColumnDrag(session, next, (order) => {
|
|
committed = true;
|
|
events.push(`commit:${order.join("|")}`);
|
|
}, () => events.push("prepare-grid"));
|
|
|
|
expect(animations).toHaveLength(1);
|
|
expect(selectors[0]).toContain('data-download-column="name"');
|
|
expect(selectors[0]).toContain('data-download-column="size"');
|
|
expect(selectors[0]).not.toContain('data-download-column="status"');
|
|
expect(events).toContain("prepare-grid");
|
|
expect(events.indexOf("prepare-grid")).toBeLessThan(events.indexOf(`commit:${next.join("|")}`));
|
|
expect(events).toContain(`commit:${next.join("|")}`);
|
|
expect(outerAnimate).not.toHaveBeenCalled();
|
|
expect(controlsAnimate).not.toHaveBeenCalled();
|
|
expect(animate).toHaveBeenCalledWith([
|
|
{ transform: `translate3d(${expectedDelta}px, 0, 0)` },
|
|
{ transform: "translate3d(0, 0, 0)" }
|
|
], expect.objectContaining({ duration: 220 }));
|
|
});
|
|
|
|
it("commits immediately without WAAPI when application animations are disabled", () => {
|
|
const animate = vi.fn(() => ({ finished: Promise.resolve() } as unknown as Animation));
|
|
const querySelectorAll = vi.fn((selector: string) => selector === "[data-column-dragging]" ? [] : [element]);
|
|
let committed = false;
|
|
const element = {
|
|
children: [{ animate }],
|
|
getBoundingClientRect: () => ({ left: committed ? 150 : 0, width: 300 })
|
|
} as unknown as HTMLElement;
|
|
const root = {
|
|
classList: { add: vi.fn(), remove: vi.fn() },
|
|
dataset: {},
|
|
querySelectorAll,
|
|
style: { removeProperty: vi.fn() }
|
|
} as unknown as HTMLElement;
|
|
const session = {
|
|
active: true,
|
|
draggedId: "name",
|
|
measurements: columns,
|
|
pointerId: -1,
|
|
preview: calculateColumnDragPreview(columns, "name", 160),
|
|
root,
|
|
startX: 0
|
|
} as DownloadColumnDragSession;
|
|
|
|
const animations = columnDrag.commitDownloadColumnDrag(session, ["size", "name", "status"], () => {
|
|
committed = true;
|
|
}, () => {}, false);
|
|
|
|
expect(committed).toBe(true);
|
|
expect(animations).toEqual([]);
|
|
expect(querySelectorAll).toHaveBeenCalledTimes(1);
|
|
expect(querySelectorAll).toHaveBeenCalledWith("[data-column-dragging]");
|
|
expect(animate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("serializes and coalesces rapid persistence requests", async () => {
|
|
const createDownloadColumnOrderPersistence = (columnDrag as unknown as {
|
|
createDownloadColumnOrderPersistence?: (
|
|
initial: string[],
|
|
persist: (order: string[]) => Promise<string[]>,
|
|
apply: (order: string[]) => void
|
|
) => { enqueue: (order: string[]) => void; whenIdle: () => Promise<void> };
|
|
}).createDownloadColumnOrderPersistence;
|
|
expect(createDownloadColumnOrderPersistence).toBeTypeOf("function");
|
|
if (!createDownloadColumnOrderPersistence) return;
|
|
|
|
const pending: Array<(order: string[]) => void> = [];
|
|
const persisted: string[][] = [];
|
|
const applied: string[][] = [];
|
|
const coordinator = createDownloadColumnOrderPersistence(["name", "size"], (order) => {
|
|
persisted.push(order);
|
|
return new Promise((resolve) => pending.push(resolve));
|
|
}, (order) => applied.push(order));
|
|
|
|
coordinator.enqueue(["size", "name"]);
|
|
coordinator.enqueue(["name", "size"]);
|
|
coordinator.enqueue(["size", "name"]);
|
|
expect(persisted).toEqual([["size", "name"]]);
|
|
|
|
pending.shift()?.(["size", "name"]);
|
|
await vi.waitFor(() => expect(persisted).toEqual([["size", "name"], ["size", "name"]]));
|
|
pending.shift()?.(["size", "name"]);
|
|
await coordinator.whenIdle();
|
|
|
|
expect(applied.at(-1)).toEqual(["size", "name"]);
|
|
});
|
|
|
|
it("reasserts an authoritative import after an older request finishes", async () => {
|
|
const createDownloadColumnOrderPersistence = (columnDrag as unknown as {
|
|
createDownloadColumnOrderPersistence?: (
|
|
initial: string[],
|
|
persist: (order: string[]) => Promise<string[]>,
|
|
apply: (order: string[]) => void
|
|
) => { applyAuthoritative: (order: string[]) => void; enqueue: (order: string[]) => void; whenIdle: () => Promise<void> };
|
|
}).createDownloadColumnOrderPersistence;
|
|
expect(createDownloadColumnOrderPersistence).toBeTypeOf("function");
|
|
if (!createDownloadColumnOrderPersistence) return;
|
|
|
|
const pending: Array<(order: string[]) => void> = [];
|
|
const persisted: string[][] = [];
|
|
const applied: string[][] = [];
|
|
const coordinator = createDownloadColumnOrderPersistence(["name", "size"], (order) => {
|
|
persisted.push(order);
|
|
return new Promise((resolve) => pending.push(resolve));
|
|
}, (order) => applied.push(order));
|
|
|
|
coordinator.enqueue(["size", "name"]);
|
|
coordinator.applyAuthoritative(["status", "name", "size"]);
|
|
pending.shift()?.(["size", "name"]);
|
|
await vi.waitFor(() => expect(persisted.at(-1)).toEqual(["status", "name", "size"]));
|
|
pending.shift()?.(["status", "name", "size"]);
|
|
await coordinator.whenIdle();
|
|
|
|
expect(applied.at(-1)).toEqual(["status", "name", "size"]);
|
|
});
|
|
});
|