85453da49f
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
128 lines
4.6 KiB
TypeScript
128 lines
4.6 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
||
import {
|
||
clampGroupMoveDelta,
|
||
isMultiDragActive,
|
||
isMultiDragPassenger,
|
||
multiDragDeltaSeconds,
|
||
multiDragPassengerOffsetPx,
|
||
type MultiDragPreviewInput,
|
||
} from "./timelineMultiDragPreview";
|
||
|
||
const base = (over: Partial<MultiDragPreviewInput> = {}): MultiDragPreviewInput => ({
|
||
dragStarted: true,
|
||
draggedKey: "a",
|
||
draggedOriginStart: 2,
|
||
draggedPreviewStart: 5,
|
||
selectedKeys: new Set(["a", "b", "c"]),
|
||
...over,
|
||
});
|
||
|
||
describe("isMultiDragActive", () => {
|
||
it("is active when a started drag's clip is part of a 2+ selection", () => {
|
||
expect(isMultiDragActive(base())).toBe(true);
|
||
});
|
||
|
||
it("is inactive before the drag starts", () => {
|
||
expect(isMultiDragActive(base({ dragStarted: false }))).toBe(false);
|
||
});
|
||
|
||
it("is inactive for a single-clip selection (single-drag behavior)", () => {
|
||
expect(isMultiDragActive(base({ selectedKeys: new Set(["a"]) }))).toBe(false);
|
||
});
|
||
|
||
it("is inactive when the dragged clip is not itself selected", () => {
|
||
expect(isMultiDragActive(base({ draggedKey: "z" }))).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe("multiDragDeltaSeconds (the one formation delta)", () => {
|
||
it("is the grabbed clip's preview − origin start when active", () => {
|
||
// The preview start is already group-clamped upstream, so this delta is the
|
||
// clamped delta every member (ghost + passengers) moves by.
|
||
expect(multiDragDeltaSeconds(base())).toBe(3);
|
||
});
|
||
|
||
it("supports a leftward (negative) delta", () => {
|
||
expect(multiDragDeltaSeconds(base({ draggedPreviewStart: 0.5 }))).toBeCloseTo(-1.5);
|
||
});
|
||
|
||
it("is zero when no multi-drag is active", () => {
|
||
expect(multiDragDeltaSeconds(base({ selectedKeys: new Set(["a"]) }))).toBe(0);
|
||
});
|
||
});
|
||
|
||
describe("isMultiDragPassenger", () => {
|
||
it("marks a selected non-dragged clip as a passenger", () => {
|
||
expect(isMultiDragPassenger("b", base())).toBe(true);
|
||
expect(isMultiDragPassenger("c", base())).toBe(true);
|
||
});
|
||
|
||
it("never marks the dragged clip itself (it is the free ghost)", () => {
|
||
expect(isMultiDragPassenger("a", base())).toBe(false);
|
||
});
|
||
|
||
it("never marks an unselected clip", () => {
|
||
expect(isMultiDragPassenger("d", base())).toBe(false);
|
||
});
|
||
|
||
it("marks nothing when the drag is a single-drag", () => {
|
||
const single = base({ selectedKeys: new Set(["a"]) });
|
||
expect(isMultiDragPassenger("b", single)).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe("multiDragPassengerOffsetPx (rigid: every passenger shares the delta)", () => {
|
||
it("converts the one formation delta to pixels for every passenger", () => {
|
||
// Both passengers move by the SAME 3s × 100pps = 300px — spacing locked.
|
||
expect(multiDragPassengerOffsetPx("b", 100, base())).toBe(300);
|
||
expect(multiDragPassengerOffsetPx("c", 100, base())).toBe(300);
|
||
});
|
||
|
||
it("is zero for the dragged clip and for non-passengers", () => {
|
||
expect(multiDragPassengerOffsetPx("a", 100, base())).toBe(0);
|
||
expect(multiDragPassengerOffsetPx("d", 100, base())).toBe(0);
|
||
});
|
||
|
||
it("is zero for a non-finite pps", () => {
|
||
expect(multiDragPassengerOffsetPx("b", Number.NaN, base())).toBe(0);
|
||
});
|
||
|
||
it("follows a leftward delta", () => {
|
||
expect(multiDragPassengerOffsetPx("c", 50, base({ draggedPreviewStart: 0 }))).toBe(-100);
|
||
});
|
||
});
|
||
|
||
describe("clampGroupMoveDelta (rigid group move)", () => {
|
||
it("passes a rightward delta through unchanged (no right wall)", () => {
|
||
expect(clampGroupMoveDelta(3, [2, 5, 9])).toBe(3);
|
||
expect(clampGroupMoveDelta(1000, [0, 4])).toBe(1000);
|
||
});
|
||
|
||
it("passes a leftward delta through when no member would cross 0", () => {
|
||
// Leftmost member at 5, moving left by 3 → 2 ≥ 0, so unclamped.
|
||
expect(clampGroupMoveDelta(-3, [5, 8, 12])).toBe(-3);
|
||
});
|
||
|
||
it("clamps a leftward delta so the leftmost member stops exactly at 0", () => {
|
||
// Leftmost at 2 → the furthest left the group can move is -2 (that member → 0).
|
||
// A pointer asking for -5 is clamped to -2: the grabbed clip stops with the
|
||
// formation instead of out-running it.
|
||
expect(clampGroupMoveDelta(-5, [2, 6, 10])).toBe(-2);
|
||
});
|
||
|
||
it("is bounded by the MOST-constrained (leftmost) member, not the grabbed one", () => {
|
||
// Grabbed clip is at 10; a passenger at 1 is the constraint. Max left = -1.
|
||
expect(clampGroupMoveDelta(-8, [10, 1, 4])).toBe(-1);
|
||
});
|
||
|
||
it("already-at-0 member forbids any leftward move", () => {
|
||
expect(clampGroupMoveDelta(-4, [0, 3, 7])).toBe(0);
|
||
// rightward still allowed
|
||
expect(clampGroupMoveDelta(2, [0, 3, 7])).toBe(2);
|
||
});
|
||
|
||
it("returns the raw delta for an empty formation", () => {
|
||
expect(clampGroupMoveDelta(-9, [])).toBe(-9);
|
||
});
|
||
});
|