Merge pull request #1069 from func25/fix/preview-reuse-and-waapi-seek-baselines

This commit is contained in:
Miguel Ángel
2026-05-25 09:33:54 -04:00
committed by GitHub
7 changed files with 290 additions and 16 deletions
@@ -1,7 +1,24 @@
import { describe, it, expect, vi } from "vitest";
import { beforeEach, afterEach, describe, it, expect, vi } from "vitest";
import { createWaapiAdapter } from "./waapi";
describe("waapi adapter", () => {
const originalDocument = (globalThis as { document?: unknown }).document;
beforeEach(() => {
(globalThis as { document?: unknown }).document = {
getAnimations: vi.fn(() => []),
};
});
afterEach(() => {
if (originalDocument === undefined) {
delete (globalThis as { document?: unknown }).document;
return;
}
(globalThis as { document?: unknown }).document = originalDocument;
});
it("has correct name", () => {
expect(createWaapiAdapter().name).toBe("waapi");
});
@@ -87,4 +104,76 @@ describe("waapi adapter", () => {
const adapter = createWaapiAdapter();
expect(() => adapter.discover()).not.toThrow();
});
it("anchors newly discovered WAAPI animations to the seek where they first appear", () => {
const existing = { pause: vi.fn(), currentTime: 0 };
const dynamic = { pause: vi.fn(), currentTime: 0 };
let includeDynamic = false;
(document as any).getAnimations = vi.fn(() =>
includeDynamic ? [existing, dynamic] : [existing],
);
const adapter = createWaapiAdapter();
adapter.discover();
adapter.seek({ time: 0.6 });
expect(existing.currentTime).toBe(600);
includeDynamic = true;
adapter.seek({ time: 0.7 });
expect(existing.currentTime).toBe(700);
expect(dynamic.currentTime).toBe(0);
adapter.seek({ time: 0.8 });
expect(dynamic.currentTime).toBe(100);
delete (document as any).getAnimations;
});
it("rebases newly discovered WAAPI animations that inherit absolute composition time", () => {
const existing = { pause: vi.fn(), currentTime: 0 };
const dynamic = { pause: vi.fn(), currentTime: 700 };
let includeDynamic = false;
(document as any).getAnimations = vi.fn(() =>
includeDynamic ? [existing, dynamic] : [existing],
);
const adapter = createWaapiAdapter();
adapter.discover();
adapter.seek({ time: 0.6 });
expect(existing.currentTime).toBe(600);
includeDynamic = true;
adapter.seek({ time: 0.7 });
expect(dynamic.currentTime).toBe(0);
adapter.seek({ time: 0.8 });
expect(dynamic.currentTime).toBe(100);
delete (document as any).getAnimations;
});
it("does not double-count inherited absolute time when discover runs again after time has advanced", () => {
const existing = { pause: vi.fn(), currentTime: 0 };
const dynamic = { pause: vi.fn(), currentTime: 700 };
let includeDynamic = false;
(document as any).getAnimations = vi.fn(() =>
includeDynamic ? [existing, dynamic] : [existing],
);
const adapter = createWaapiAdapter();
adapter.discover();
adapter.seek({ time: 0.6 });
expect(existing.currentTime).toBe(600);
includeDynamic = true;
adapter.discover();
adapter.seek({ time: 0.7 });
expect(dynamic.currentTime).toBe(200);
delete (document as any).getAnimations;
});
});
+66 -4
View File
@@ -2,15 +2,77 @@ import type { RuntimeDeterministicAdapter } from "../types";
import { swallow } from "../diagnostics";
export function createWaapiAdapter(): RuntimeDeterministicAdapter {
let didDiscover = false;
let lastSeekTimeMs = 0;
const baselines = new WeakMap<
Animation,
{
compositionTimeMs: number;
animationTimeMs: number;
}
>();
const snapshotAnimations = () => {
if (!document.getAnimations) return [];
try {
return document.getAnimations();
} catch {
return [];
}
};
const readAnimationTimeMs = (animation: Animation) => {
const raw = Number(animation.currentTime);
return Number.isFinite(raw) && raw > 0 ? raw : 0;
};
const normalizeInitialAnimationTimeMs = (animationTimeMs: number, compositionTimeMs: number) => {
if (compositionTimeMs <= 0) {
return animationTimeMs;
}
if (animationTimeMs >= compositionTimeMs) {
return Math.max(0, animationTimeMs - compositionTimeMs);
}
return animationTimeMs;
};
const ensureBaseline = (animation: Animation, compositionTimeMs: number) => {
const existing = baselines.get(animation);
if (existing) {
return existing;
}
const baseline = {
compositionTimeMs,
animationTimeMs: didDiscover
? normalizeInitialAnimationTimeMs(readAnimationTimeMs(animation), compositionTimeMs)
: readAnimationTimeMs(animation),
};
baselines.set(animation, baseline);
return baseline;
};
return {
name: "waapi",
discover: () => {},
discover: () => {
didDiscover = true;
for (const animation of snapshotAnimations()) {
ensureBaseline(animation, lastSeekTimeMs);
}
},
seek: (ctx) => {
if (!document.getAnimations) return;
const timeMs = Math.max(0, (Number(ctx.time) || 0) * 1000);
for (const animation of document.getAnimations()) {
lastSeekTimeMs = timeMs;
for (const animation of snapshotAnimations()) {
const baseline = didDiscover
? ensureBaseline(animation, timeMs)
: ensureBaseline(animation, 0);
const localTimeMs =
baseline.animationTimeMs + Math.max(0, timeMs - baseline.compositionTimeMs);
try {
animation.currentTime = timeMs;
animation.currentTime = localTimeMs;
} catch (err) {
// ignore animations that reject currentTime writes
swallow("runtime.adapters.waapi.site1", err);