test(e2e): scrub tier — currentTime + gains track dial, reverse, commit-lock, wheel
Per plan Task 5. Replaces the .rev-asserting zoom-out test with a turn-back test (same forward morph seeked backward, audio recedes, re-locks the start clip); adds a detent-crossing commit+lock test via a new window.__hefState diagnostic seam. Full tier (6 tests) green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -105,35 +105,57 @@ test("zoom in plays the matching morph, lands locked, and stays put while parked
|
||||
expect((await morphs(page)).length).toBe(countAfterLanding); // no extra swap
|
||||
});
|
||||
|
||||
test("zoom back out plays a reverse morph and lands locked", async ({ page }) => {
|
||||
test("turn-back scrubs the same morph in reverse and re-locks the start clip", async ({ page }) => {
|
||||
await boot(page);
|
||||
const start = (await page.locator("#scale-name").textContent())!;
|
||||
await enableAudio(page);
|
||||
const box = (await page.locator("#dial").boundingBox())!;
|
||||
const cx = box.x + box.width / 2, cy = box.y + box.height / 2;
|
||||
const read = () => page.evaluate(() => ({
|
||||
t: (document.querySelector("video") as HTMLVideoElement).currentTime,
|
||||
ga: (document.querySelector("#aud") as HTMLAudioElement).volume,
|
||||
gb: (document.querySelector("#aud-b") as HTMLAudioElement).volume,
|
||||
name: document.querySelector("#scale-name")?.textContent,
|
||||
morphs: ((window as any).__hefMorphs || []).slice(),
|
||||
}));
|
||||
|
||||
// Zoom in one detent to get off cosmos (-> orbit)...
|
||||
await wheelOnStage(page, 60);
|
||||
await page.waitForFunction(
|
||||
(s) => document.querySelector("#scale-name")?.textContent !== s,
|
||||
start,
|
||||
{ timeout: 20_000 },
|
||||
);
|
||||
const atOrbit = (await page.locator("#scale-name").textContent())!;
|
||||
expect(atOrbit).toContain("orbit");
|
||||
await page.mouse.move(cx, cy - 30);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(cx + 22, cy - 20, { steps: 10 }); // descend partway into the segment
|
||||
await page.waitForFunction(() => (document.querySelector("video") as HTMLVideoElement).currentTime > 0, null, { timeout: 5000 });
|
||||
const fwd = await read();
|
||||
// The single segment morph is the descend/forward file (cosmos__orbit), NOT a `.rev`.
|
||||
expect(fwd.morphs[fwd.morphs.length - 1]).toMatch(/^transitions\/cosmos.*\.mp4$/);
|
||||
expect(fwd.morphs[fwd.morphs.length - 1]).not.toMatch(/\.rev\.mp4$/);
|
||||
|
||||
// ...then zoom back OUT to cosmos.
|
||||
await wheelOnStage(page, -60);
|
||||
await page.waitForFunction(
|
||||
(s) => document.querySelector("#scale-name")?.textContent !== s,
|
||||
atOrbit,
|
||||
{ timeout: 20_000 },
|
||||
);
|
||||
await page.mouse.move(cx + 4, cy - 30, { steps: 10 }); // turn back toward the start
|
||||
await page.waitForTimeout(120); // let the throttled rAF seek land
|
||||
const back = await read();
|
||||
await page.mouse.up();
|
||||
|
||||
const played = await morphs(page);
|
||||
// The most recent morph is the zoom-out: a `.rev` companion. Its file is named
|
||||
// for the descending pair (cosmos<...>__orbit<...>), so it starts "cosmos".
|
||||
expect(played[played.length - 1]).toMatch(/^transitions\/cosmos.*\.rev\.mp4$/);
|
||||
|
||||
const landed = (await page.locator("#scale-name").textContent())!;
|
||||
expect(landed).toContain("cosmos");
|
||||
await page.waitForTimeout(2500);
|
||||
expect(await page.locator("#scale-name").textContent()).toBe(landed); // locked
|
||||
expect(back.t).toBeLessThan(fwd.t); // SAME morph file, seeking backward
|
||||
expect(back.gb).toBeLessThan(fwd.gb); // next-scale audio receding
|
||||
expect(back.name).toContain("cosmos"); // re-locked the altitude we left
|
||||
});
|
||||
|
||||
test("crossing a detent commits and locks the destination clip", async ({ page }) => {
|
||||
await boot(page);
|
||||
const box = (await page.locator("#dial").boundingBox())!;
|
||||
const cx = box.x + box.width / 2, cy = box.y + box.height / 2;
|
||||
// Drag clockwise across one full detent (top -> right ~= 90deg = 1.25 detents),
|
||||
// crossing integer 1 (orbit) so it commits, then hold past it (no auto-complete).
|
||||
await page.mouse.move(cx, cy - 30);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(cx + 30, cy + 6, { steps: 14 });
|
||||
await page.waitForFunction(() => (window as any).__hefState().ringIndex === 1, null, { timeout: 5000 });
|
||||
const st = await page.evaluate(() => (window as any).__hefState());
|
||||
expect(st.ringIndex).toBe(1); // committed to orbit
|
||||
expect(String(st.activeClipId)).toMatch(/^orbit/); // locked the destination pool pick
|
||||
const played = await page.evaluate(() => (window as any).__hefMorphs || []);
|
||||
expect(played[0]).toMatch(/^transitions\/cosmos.*\.mp4$/);
|
||||
expect(played[0]).not.toMatch(/\.rev\.mp4$/);
|
||||
// Hold: the committed clip does not change while parked mid-gesture.
|
||||
await page.waitForTimeout(800);
|
||||
const st2 = await page.evaluate(() => (window as any).__hefState());
|
||||
expect(st2.activeClipId).toBe(st.activeClipId);
|
||||
await page.mouse.up();
|
||||
});
|
||||
|
||||
@@ -636,6 +636,10 @@ let pos = 0; // continuous knob position; rest value == ringInd
|
||||
let activeSeg = null; // { lo, clipLo, clipHi, file } for the segment being scrubbed
|
||||
let seekPending = false; // throttle: at most one currentTime seek per animation frame
|
||||
|
||||
// Diagnostic seam (sibling of window.__hefMorphs): the committed altitude state, so
|
||||
// E2E can assert the locked clip mid-scrub when #scale-name (settle-only) is stale.
|
||||
if (typeof window !== "undefined") window.__hefState = () => ({ ringIndex, activeClipId, pos });
|
||||
|
||||
function dialStep() { return ring && ring.scales.length ? 360 / ring.scales.length : 360; }
|
||||
|
||||
function _xy(r, deg) {
|
||||
|
||||
Reference in New Issue
Block a user