fix(core): guard timeline method calls for non-conformant objects (#1098)

* fix(core): guard timeline method calls for non-conformant objects

User compositions can register timeline-like objects on window.__timeline
where .duration is a number property (not a function) and .pause/.play
may be missing entirely. The runtime player called these unconditionally,
causing ~166 "duration is not a function" and ~38 "pause is not a function"
errors per day.

Add safeNum() and safeVoid() helpers that check typeof before calling,
falling back to reading numbers as properties and silently skipping
missing void methods. Applied consistently across all timeline method
call sites in player.ts.

* fix(core): add observability for non-conformant timeline properties
This commit is contained in:
Miguel Ángel
2026-05-27 20:16:38 -04:00
committed by GitHub
parent cbb7831eb2
commit 2d7b9e5245
2 changed files with 114 additions and 18 deletions
+67
View File
@@ -488,6 +488,73 @@ describe("createRuntimePlayer", () => {
});
});
describe("tolerates non-conformant timeline objects", () => {
it("handles duration as a number property instead of a function", () => {
const timeline = {
play: vi.fn(),
pause: vi.fn(),
seek: vi.fn(),
totalTime: vi.fn(),
time: vi.fn(() => 2),
duration: 10,
add: vi.fn(),
paused: vi.fn(),
set: vi.fn(),
} as unknown as RuntimeTimelineLike;
const deps = createMockDeps(timeline);
const player = createRuntimePlayer(deps);
expect(player.getDuration()).toBe(10);
expect(() => player.play()).not.toThrow();
});
it("handles missing pause method", () => {
const timeline = {
play: vi.fn(),
seek: vi.fn(),
time: vi.fn(() => 0),
duration: vi.fn(() => 10),
add: vi.fn(),
paused: vi.fn(),
set: vi.fn(),
} as unknown as RuntimeTimelineLike;
const deps = createMockDeps(timeline);
const player = createRuntimePlayer(deps);
expect(() => player.pause()).not.toThrow();
expect(() => player.seek(3)).not.toThrow();
});
it("handles missing play method", () => {
const timeline = {
pause: vi.fn(),
seek: vi.fn(),
time: vi.fn(() => 0),
duration: vi.fn(() => 10),
add: vi.fn(),
paused: vi.fn(),
set: vi.fn(),
} as unknown as RuntimeTimelineLike;
const deps = createMockDeps(timeline);
const player = createRuntimePlayer(deps);
expect(() => player.play()).not.toThrow();
});
it("handles time as a number property instead of a function", () => {
const timeline = {
play: vi.fn(),
pause: vi.fn(),
seek: vi.fn(),
time: 5,
duration: vi.fn(() => 10),
add: vi.fn(),
paused: vi.fn(),
set: vi.fn(),
} as unknown as RuntimeTimelineLike;
const deps = createMockDeps(timeline);
const player = createRuntimePlayer(deps);
expect(player.getTime()).toBe(5);
});
});
describe("getters", () => {
it("getTime returns timeline time", () => {
const timeline = createMockTimeline({ time: 7.5 });