commit d940b846aa7d055dd4d13b2d3c8532887eb24515
parent 9d2bd76b6f911152667623f503731478bb54d81b
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Thu, 1 Oct 2026 15:19:57 -0400
umtool e2e: the posts and mute specs follow whole-frame holds and the build's mute fade
Holds round to whole frames (2.5 s at the fixture's 15 fps is 38 frames,
2.533 s), so every pinned start, window and tail moved with them; the mid-move
seek comes from the schedule's own move. The mute mark is silent AT the mark
(the build's fade ends there), not 0.05 s after it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
2 files changed, 38 insertions(+), 30 deletions(-)
diff --git a/umtool/e2e/clip-bench.spec.ts b/umtool/e2e/clip-bench.spec.ts
@@ -1009,13 +1009,13 @@ test("the mute mark: picked on the waveform, heard at once, saved, shown, cleare
// The pick plays ACROSS the mark -- up to three seconds before it (here
// from the selection's start), to the end of the selection -- and the sound
- // goes at the mark: the last non-zero frame is where the fade (0.05 s) runs
- // out, not the 6.00 the tone runs on to.
+ // goes at the mark: the fade (MUTE_FADE, the build's) ENDS there, so the
+ // last non-zero frame is at the mark, not the 6.00 the tone runs on to.
expect(play.engine).toBe("webaudio");
expect(play.from).toBeCloseTo(Math.max(3, mark - 3), 2);
expect(play.to).toBeCloseTo(6, 2);
const { last, sr } = tap!;
- const silentAt = play.ctxStart + (mark - play.from) + 0.05;
+ const silentAt = play.ctxStart + (mark - play.from);
expect(Math.abs((last + 1) / sr - silentAt), "silent at the mark, not at the end").toBeLessThan(0.01);
// Saved by `save window`, like the edges; the manifest has it.
diff --git a/umtool/e2e/onscreen-posts.spec.ts b/umtool/e2e/onscreen-posts.spec.ts
@@ -17,13 +17,14 @@ import { deckGeometry, postWindows, postsGeometry, shiftedFootage } from "umtool
//
// With the defaults (posts.seconds 4, hold 2.5, shift on) and the fixture's
// 0.2 s crossfade, the estimate is:
-// c01 0 → 5.5 3 s clip + a 2.5 s hold; two posts in 5.3 s share it,
-// p-early at 0, p-mid at 2.65
-// c02 5.3 → 10.8 3 s clip + a 2.5 s hold; p-late at 6.6, 4 s before
-// its leave at 10.6
-// k01 10.6 → 13.6 the card
+// c01 0 → 5.533 3 s clip + a 2.5 s hold, which at the fixture's
+// 15 fps is 38 frames (2.533 s); two posts in
+// 5.333 s share it, p-early at 0, p-mid at 2.667
+// c02 5.333 → 10.867 the same; p-late at 6.667, 4 s before its leave
+// at 10.667
+// k01 10.667 → 13.667 the card
// and the footage moves aside as each clip's first post appears (c01 at 0,
-// c02 at 6.6), with the posts column at the frame's right edge.
+// c02 at 6.667), with the posts column at the frame's right edge.
//
// The posts region's COMPOSITION is compose-chrome's (`region: "posts"`): the
// preview test asserts every window composes and its page reports
@@ -275,20 +276,22 @@ test("the preview composes the posts region per window and overlays it while the
// The defaults' timing: 4 s per post, each carrying clip held 2.5 s, every
// start and the total measured with the holds.
expect(pv.schedule.segments.map((s) => [s.id, s.start, s.duration, s.hold ?? 0])).toEqual([
- ["c01", 0, 5.5, 2.5],
- ["c02", 5.3, 5.5, 2.5],
- ["k01", 10.6, 3, 0],
+ // The hold is rounded to whole frames: 2.5 s at the fixture's 15 fps is
+ // 37.5 frames, so 38 -- 2.533 s.
+ ["c01", 0, 5.533, 2.533],
+ ["c02", 5.333, 5.533, 2.533],
+ ["k01", 10.667, 3, 0],
]);
- expect(pv.schedule.total).toBe(13.6);
+ expect(pv.schedule.total).toBe(13.667);
expect(pv.schedule.posts.map((p) => [p.id, p.segment, p.appear, p.out])).toEqual([
- ["p-early", "c01", 0, [5.3, 5.5]],
- ["p-mid", "c01", 2.65, [5.3, 5.5]],
- ["p-late", "c02", 6.6, [10.6, 10.8]],
+ ["p-early", "c01", 0, [5.333, 5.533]],
+ ["p-mid", "c01", 2.667, [5.333, 5.533]],
+ ["p-late", "c02", 6.667, [10.667, 10.867]],
]);
- expect(pv.posts.windows.map(({ from, to }) => [from, to])).toEqual([[0, 5.5], [6.6, 10.8]]);
+ expect(pv.posts.windows.map(({ from, to }) => [from, to])).toEqual([[0, 5.533], [6.667, 10.867]]);
// The footage makes room: one move per carrying clip, and the column at the frame's edge.
const render = readManifest().render;
- expect(pv.schedule.moves?.map((m) => [m.segment, m.at, m.seconds])).toEqual([["c01", 0, 0.6], ["c02", 6.6, 0.6]]);
+ expect(pv.schedule.moves?.map((m) => [m.segment, m.at, m.seconds])).toEqual([["c01", 0, 0.6], ["c02", 6.667, 0.6]]);
expect(pv.schedule.moves?.[0].from).toEqual(deckGeometry(render).footage);
expect(pv.schedule.moves?.[0].to).toEqual(shiftedFootage(render));
expect(pv.posts.geometry).toEqual(postsGeometry(render));
@@ -310,25 +313,25 @@ test("the preview composes the posts region per window and overlays it while the
await expect(page.locator("[data-posts-window]")).toHaveCount(2);
// The scrubber runs the held length, and the window marks sit on its clock.
- await expect(page.getByTestId("onscreen-scrubber")).toHaveAttribute("max", "13.6");
- await expect(page.getByTestId("onscreen-time")).toContainText("/ 0:13.6");
- for (const [seg, from, to] of [["c01", 0, 5.5], ["c02", 6.6, 10.8]] as const) {
+ await expect(page.getByTestId("onscreen-scrubber")).toHaveAttribute("max", "13.667");
+ await expect(page.getByTestId("onscreen-time")).toContainText("/ 0:13.7");
+ for (const [seg, from, to] of [["c01", 0, 5.533], ["c02", 6.667, 10.867]] as const) {
const style = await page.locator(`[data-posts-window="${seg}"]`).evaluate((el) => [
parseFloat((el as HTMLElement).style.left),
parseFloat((el as HTMLElement).style.width),
]);
- expect(style[0]).toBeCloseTo((from / 13.6) * 100, 2);
- expect(style[1]).toBeCloseTo(((to - from) / 13.6) * 100, 2);
+ expect(style[0]).toBeCloseTo((from / 13.667) * 100, 2);
+ expect(style[1]).toBeCloseTo(((to - from) / 13.667) * 100, 2);
}
// A held segment shows its hold: a hatched tail, hold/duration of its block.
await expect(page.getByTestId("onscreen-segment-hold")).toHaveCount(2);
- await expect(page.locator('[data-seg-jump="c01"]')).toHaveAttribute("data-hold", "2.5");
+ await expect(page.locator('[data-seg-jump="c01"]')).toHaveAttribute("data-hold", "2.533");
await expect(page.locator('[data-seg-jump="k01"]')).toHaveAttribute("data-hold", "0");
- await expect(page.locator('[data-seg-jump="c01"]')).toHaveAttribute("title", /held 2\.5 s for its posts/);
+ await expect(page.locator('[data-seg-jump="c01"]')).toHaveAttribute("title", /held 2\.533 s for its posts/);
const block = (await page.locator('[data-seg-jump="c02"]').boundingBox())!;
const tail = (await page.locator('[data-seg-hold="c02"]').boundingBox())!;
- expect(tail.width / block.width).toBeCloseTo(2.5 / 5.5, 1);
+ expect(tail.width / block.width).toBeCloseTo(2.533 / 5.533, 1);
expect(Math.abs(tail.x + tail.width - (block.x + block.width))).toBeLessThan(2);
// Outside every window: nothing over the footage. k01 starts after the last.
await page.locator('[data-seg-jump="k01"]').click();
@@ -352,7 +355,7 @@ test("the preview composes the posts region per window and overlays it while the
expect(Math.abs(box.width / frame.width - pv.posts.geometry.width / W)).toBeLessThan(0.01);
// The backdrop moves inside the window: the window mark put the scrubber at
- // 8.7, past c02's move (6.6 → 7.2), so the footage is all the way aside...
+ // 8.7, past c02's move (6.667 → 7.267), so the footage is all the way aside...
const from = pv.schedule.moves![1].from;
const to = pv.schedule.moves![1].to;
const backdrop = page.getByTestId("onscreen-backdrop-frame");
@@ -364,10 +367,15 @@ test("the preview composes the posts region per window and overlays it while the
// ...its box's left edge where the build puts it (in frame pixels)...
expect((aside[2] / aside[4]) * W + aside[0] * from.x).toBeCloseTo(to.x, 0);
// ...half way through the move at its middle, on the smoothstep curve...
- await seek(page, 6.9);
- await expect(backdrop).toHaveAttribute("data-move-progress", "0.5");
+ // (the move's middle from the schedule itself; the scrubber steps in 0.01 s,
+ // so the progress lands within a step of 0.5, not on it)
+ const mid = pv.schedule.moves![1].at + pv.schedule.moves![1].seconds / 2;
+ await seek(page, Math.round(mid * 100) / 100);
+ await expect
+ .poll(async () => Math.abs(Number(await backdrop.getAttribute("data-move-progress")) - 0.5))
+ .toBeLessThan(0.02);
const half = (await backdropMatrix(page))!;
- expect(half[0]).toBeCloseTo((1 + to.width / from.width) / 2, 3);
+ expect(half[0]).toBeCloseTo((1 + to.width / from.width) / 2, 2);
// ...in its box before the move...
await seek(page, 6.5);
await expect(page.getByTestId("onscreen-current")).toHaveText("c02");