@editframe/elements 0.26.3-beta.0 → 0.26.4-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/package.json +2 -2
  2. package/scripts/build-css.js +3 -3
  3. package/tsdown.config.ts +1 -1
  4. package/src/elements/ContextProxiesController.ts +0 -124
  5. package/src/elements/CrossUpdateController.ts +0 -22
  6. package/src/elements/EFAudio.browsertest.ts +0 -706
  7. package/src/elements/EFAudio.ts +0 -56
  8. package/src/elements/EFCaptions.browsertest.ts +0 -1960
  9. package/src/elements/EFCaptions.ts +0 -823
  10. package/src/elements/EFImage.browsertest.ts +0 -120
  11. package/src/elements/EFImage.ts +0 -113
  12. package/src/elements/EFMedia/AssetIdMediaEngine.test.ts +0 -224
  13. package/src/elements/EFMedia/AssetIdMediaEngine.ts +0 -110
  14. package/src/elements/EFMedia/AssetMediaEngine.browsertest.ts +0 -140
  15. package/src/elements/EFMedia/AssetMediaEngine.ts +0 -385
  16. package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +0 -400
  17. package/src/elements/EFMedia/BaseMediaEngine.ts +0 -505
  18. package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +0 -386
  19. package/src/elements/EFMedia/BufferedSeekingInput.ts +0 -430
  20. package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +0 -226
  21. package/src/elements/EFMedia/JitMediaEngine.ts +0 -256
  22. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.ts +0 -679
  23. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +0 -117
  24. package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +0 -246
  25. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.ts +0 -59
  26. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts +0 -27
  27. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.ts +0 -55
  28. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +0 -53
  29. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +0 -207
  30. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +0 -72
  31. package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +0 -32
  32. package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +0 -29
  33. package/src/elements/EFMedia/audioTasks/makeAudioTasksVideoOnly.browsertest.ts +0 -95
  34. package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +0 -184
  35. package/src/elements/EFMedia/shared/AudioSpanUtils.ts +0 -129
  36. package/src/elements/EFMedia/shared/BufferUtils.ts +0 -342
  37. package/src/elements/EFMedia/shared/GlobalInputCache.ts +0 -77
  38. package/src/elements/EFMedia/shared/MediaTaskUtils.ts +0 -44
  39. package/src/elements/EFMedia/shared/PrecisionUtils.ts +0 -46
  40. package/src/elements/EFMedia/shared/RenditionHelpers.browsertest.ts +0 -246
  41. package/src/elements/EFMedia/shared/RenditionHelpers.ts +0 -56
  42. package/src/elements/EFMedia/shared/ThumbnailExtractor.ts +0 -227
  43. package/src/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.ts +0 -167
  44. package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +0 -88
  45. package/src/elements/EFMedia/videoTasks/MainVideoInputCache.ts +0 -76
  46. package/src/elements/EFMedia/videoTasks/ScrubInputCache.ts +0 -61
  47. package/src/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.ts +0 -114
  48. package/src/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.ts +0 -35
  49. package/src/elements/EFMedia/videoTasks/makeScrubVideoInputTask.ts +0 -52
  50. package/src/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.ts +0 -124
  51. package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.ts +0 -44
  52. package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.ts +0 -32
  53. package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +0 -370
  54. package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +0 -109
  55. package/src/elements/EFMedia.browsertest.ts +0 -872
  56. package/src/elements/EFMedia.ts +0 -341
  57. package/src/elements/EFSourceMixin.ts +0 -60
  58. package/src/elements/EFSurface.browsertest.ts +0 -151
  59. package/src/elements/EFSurface.ts +0 -142
  60. package/src/elements/EFTemporal.browsertest.ts +0 -215
  61. package/src/elements/EFTemporal.ts +0 -800
  62. package/src/elements/EFThumbnailStrip.browsertest.ts +0 -585
  63. package/src/elements/EFThumbnailStrip.media-engine.browsertest.ts +0 -714
  64. package/src/elements/EFThumbnailStrip.ts +0 -906
  65. package/src/elements/EFTimegroup.browsertest.ts +0 -934
  66. package/src/elements/EFTimegroup.ts +0 -882
  67. package/src/elements/EFVideo.browsertest.ts +0 -1482
  68. package/src/elements/EFVideo.ts +0 -564
  69. package/src/elements/EFWaveform.ts +0 -547
  70. package/src/elements/FetchContext.browsertest.ts +0 -401
  71. package/src/elements/FetchMixin.ts +0 -38
  72. package/src/elements/SampleBuffer.ts +0 -94
  73. package/src/elements/TargetController.browsertest.ts +0 -230
  74. package/src/elements/TargetController.ts +0 -224
  75. package/src/elements/TimegroupController.ts +0 -26
  76. package/src/elements/durationConverter.ts +0 -35
  77. package/src/elements/parseTimeToMs.ts +0 -9
  78. package/src/elements/printTaskStatus.ts +0 -16
  79. package/src/elements/renderTemporalAudio.ts +0 -108
  80. package/src/elements/updateAnimations.browsertest.ts +0 -1884
  81. package/src/elements/updateAnimations.ts +0 -217
  82. package/src/elements/util.ts +0 -24
  83. package/src/gui/ContextMixin.browsertest.ts +0 -860
  84. package/src/gui/ContextMixin.ts +0 -562
  85. package/src/gui/Controllable.browsertest.ts +0 -258
  86. package/src/gui/Controllable.ts +0 -41
  87. package/src/gui/EFConfiguration.ts +0 -40
  88. package/src/gui/EFControls.browsertest.ts +0 -389
  89. package/src/gui/EFControls.ts +0 -195
  90. package/src/gui/EFDial.browsertest.ts +0 -84
  91. package/src/gui/EFDial.ts +0 -172
  92. package/src/gui/EFFilmstrip.browsertest.ts +0 -712
  93. package/src/gui/EFFilmstrip.ts +0 -1349
  94. package/src/gui/EFFitScale.ts +0 -152
  95. package/src/gui/EFFocusOverlay.ts +0 -79
  96. package/src/gui/EFPause.browsertest.ts +0 -202
  97. package/src/gui/EFPause.ts +0 -73
  98. package/src/gui/EFPlay.browsertest.ts +0 -202
  99. package/src/gui/EFPlay.ts +0 -73
  100. package/src/gui/EFPreview.ts +0 -74
  101. package/src/gui/EFResizableBox.browsertest.ts +0 -79
  102. package/src/gui/EFResizableBox.ts +0 -898
  103. package/src/gui/EFScrubber.ts +0 -151
  104. package/src/gui/EFTimeDisplay.browsertest.ts +0 -237
  105. package/src/gui/EFTimeDisplay.ts +0 -55
  106. package/src/gui/EFToggleLoop.ts +0 -35
  107. package/src/gui/EFTogglePlay.ts +0 -70
  108. package/src/gui/EFWorkbench.ts +0 -115
  109. package/src/gui/PlaybackController.ts +0 -527
  110. package/src/gui/TWMixin.css +0 -6
  111. package/src/gui/TWMixin.ts +0 -61
  112. package/src/gui/TargetOrContextMixin.ts +0 -185
  113. package/src/gui/currentTimeContext.ts +0 -5
  114. package/src/gui/durationContext.ts +0 -3
  115. package/src/gui/efContext.ts +0 -6
  116. package/src/gui/fetchContext.ts +0 -5
  117. package/src/gui/focusContext.ts +0 -7
  118. package/src/gui/focusedElementContext.ts +0 -5
  119. package/src/gui/playingContext.ts +0 -5
  120. package/src/otel/BridgeSpanExporter.ts +0 -150
  121. package/src/otel/setupBrowserTracing.ts +0 -73
  122. package/src/otel/tracingHelpers.ts +0 -251
  123. package/src/transcoding/cache/RequestDeduplicator.test.ts +0 -170
  124. package/src/transcoding/cache/RequestDeduplicator.ts +0 -65
  125. package/src/transcoding/cache/URLTokenDeduplicator.test.ts +0 -182
  126. package/src/transcoding/cache/URLTokenDeduplicator.ts +0 -101
  127. package/src/transcoding/types/index.ts +0 -312
  128. package/src/transcoding/utils/MediaUtils.ts +0 -63
  129. package/src/transcoding/utils/UrlGenerator.ts +0 -68
  130. package/src/transcoding/utils/constants.ts +0 -36
  131. package/src/utils/LRUCache.test.ts +0 -274
  132. package/src/utils/LRUCache.ts +0 -696
@@ -1,1884 +0,0 @@
1
- import { LitElement } from "lit";
2
- import { customElement } from "lit/decorators.js";
3
- import { assert, beforeEach, describe, test } from "vitest";
4
- import { EFTemporal } from "./EFTemporal.js";
5
- import type { EFTimegroup } from "./EFTimegroup.js";
6
- import {
7
- type AnimatableElement,
8
- updateAnimations,
9
- } from "./updateAnimations.js";
10
-
11
- import "./EFTimegroup.js";
12
-
13
- // Create proper temporal test elements
14
- @customElement("test-temporal-element")
15
- class TestTemporalElement extends EFTemporal(LitElement) {
16
- get intrinsicDurationMs() {
17
- return this._durationMs;
18
- }
19
-
20
- private _durationMs = 1000;
21
- setDuration(duration: number) {
22
- this._durationMs = duration;
23
- }
24
- }
25
-
26
- declare global {
27
- interface HTMLElementTagNameMap {
28
- "test-temporal-element": TestTemporalElement;
29
- }
30
- }
31
-
32
- beforeEach(() => {
33
- // Clean up DOM
34
- while (document.body.children.length) {
35
- document.body.children[0]?.remove();
36
- }
37
- window.localStorage.clear();
38
- });
39
-
40
- function createTestElement(
41
- props: Partial<AnimatableElement> = {},
42
- ): AnimatableElement {
43
- const element = document.createElement("div") as unknown as AnimatableElement;
44
- // Override readonly properties for testing
45
- Object.defineProperty(element, "currentTimeMs", {
46
- value: props.currentTimeMs ?? 0,
47
- writable: true,
48
- });
49
- Object.defineProperty(element, "durationMs", {
50
- value: props.durationMs ?? 1000,
51
- writable: true,
52
- });
53
- Object.defineProperty(element, "startTimeMs", {
54
- value: props.startTimeMs ?? 0,
55
- writable: true,
56
- });
57
- Object.defineProperty(element, "endTimeMs", {
58
- value: props.endTimeMs ?? 1000,
59
- writable: true,
60
- });
61
- Object.defineProperty(element, "rootTimegroup", {
62
- value: props.rootTimegroup,
63
- writable: true,
64
- });
65
- Object.defineProperty(element, "parentTimegroup", {
66
- value: props.parentTimegroup,
67
- writable: true,
68
- });
69
- Object.defineProperty(element, "ownCurrentTimeMs", {
70
- value: props.ownCurrentTimeMs ?? 0,
71
- writable: true,
72
- });
73
- document.body.appendChild(element);
74
- return element;
75
- }
76
-
77
- describe("Timeline Element Synchronizer", () => {
78
- describe("CSS custom properties", () => {
79
- test("sets --ef-progress based on currentTimeMs/durationMs ratio", () => {
80
- const element = createTestElement({
81
- currentTimeMs: 250,
82
- durationMs: 1000,
83
- });
84
-
85
- updateAnimations(element);
86
-
87
- assert.equal(element.style.getPropertyValue("--ef-progress"), "25%");
88
- });
89
-
90
- test("clamps --ef-progress to 0-100% range", () => {
91
- const element1 = createTestElement({
92
- currentTimeMs: -100,
93
- durationMs: 1000,
94
- });
95
- const element2 = createTestElement({
96
- currentTimeMs: 1500,
97
- durationMs: 1000,
98
- });
99
-
100
- updateAnimations(element1);
101
- updateAnimations(element2);
102
-
103
- assert.equal(element1.style.getPropertyValue("--ef-progress"), "0%");
104
- assert.equal(element2.style.getPropertyValue("--ef-progress"), "100%");
105
- });
106
-
107
- test("sets --ef-duration to element durationMs", () => {
108
- const element = createTestElement({
109
- durationMs: 2000,
110
- });
111
-
112
- updateAnimations(element);
113
-
114
- assert.equal(element.style.getPropertyValue("--ef-duration"), "2000ms");
115
- });
116
-
117
- test("sets --ef-transition-duration based on parentTimegroup overlapMs", () => {
118
- const parentTimegroup = document.createElement(
119
- "ef-timegroup",
120
- ) as EFTimegroup;
121
- parentTimegroup.overlapMs = 500;
122
-
123
- const element = createTestElement({
124
- parentTimegroup,
125
- });
126
-
127
- updateAnimations(element);
128
-
129
- assert.equal(
130
- element.style.getPropertyValue("--ef-transition-duration"),
131
- "500ms",
132
- );
133
- });
134
-
135
- test("sets --ef-transition-duration to 0ms when no parentTimegroup", () => {
136
- const element = createTestElement();
137
-
138
- updateAnimations(element);
139
-
140
- assert.equal(
141
- element.style.getPropertyValue("--ef-transition-duration"),
142
- "0ms",
143
- );
144
- });
145
-
146
- test("sets --ef-transition-out-start correctly", () => {
147
- const parentTimegroup = document.createElement(
148
- "ef-timegroup",
149
- ) as EFTimegroup;
150
- parentTimegroup.overlapMs = 200;
151
-
152
- const element = createTestElement({
153
- durationMs: 1000,
154
- parentTimegroup,
155
- });
156
-
157
- updateAnimations(element);
158
-
159
- assert.equal(
160
- element.style.getPropertyValue("--ef-transition-out-start"),
161
- "800ms",
162
- );
163
- });
164
-
165
- test("sets animation-related CSS properties when animations are present", () => {
166
- const element = createTestElement({
167
- durationMs: 2000,
168
- });
169
-
170
- // Create an animation to trigger CSS property setting
171
- element.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 1000 });
172
-
173
- updateAnimations(element);
174
-
175
- assert.equal(element.style.getPropertyValue("--ef-duration"), "2000ms");
176
- assert.equal(
177
- element.style.getPropertyValue("--ef-transition-duration"),
178
- "0ms",
179
- );
180
- assert.equal(
181
- element.style.getPropertyValue("--ef-transition-out-start"),
182
- "2000ms",
183
- );
184
- });
185
- });
186
-
187
- describe("element visibility", () => {
188
- test("hides element when timeline is before startTimeMs", () => {
189
- const rootTimegroup = document.createElement(
190
- "ef-timegroup",
191
- ) as EFTimegroup;
192
- rootTimegroup.currentTimeMs = 100;
193
-
194
- const element = createTestElement({
195
- startTimeMs: 200,
196
- endTimeMs: 800,
197
- rootTimegroup,
198
- });
199
-
200
- updateAnimations(element);
201
-
202
- assert.equal(element.style.display, "none");
203
- });
204
-
205
- test("hides element when timeline is after endTimeMs", () => {
206
- const rootTimegroup = document.createElement(
207
- "ef-timegroup",
208
- ) as EFTimegroup;
209
- rootTimegroup.currentTimeMs = 900;
210
-
211
- const element = createTestElement({
212
- startTimeMs: 200,
213
- endTimeMs: 800,
214
- rootTimegroup,
215
- });
216
-
217
- updateAnimations(element);
218
-
219
- assert.equal(element.style.display, "none");
220
- });
221
-
222
- test("shows element when timeline is within start/end range (using element currentTimeMs)", () => {
223
- const element = createTestElement({
224
- currentTimeMs: 500,
225
- startTimeMs: 200,
226
- endTimeMs: 800,
227
- });
228
- // Start with element hidden
229
- element.style.display = "none";
230
-
231
- updateAnimations(element);
232
-
233
- assert.equal(element.style.display, "");
234
- });
235
-
236
- test("sequence elements remain coordinated at exact end boundary", () => {
237
- // Create a root timegroup mock
238
- const rootTimegroup = {
239
- currentTimeMs: 3000,
240
- durationMs: 3000,
241
- startTimeMs: 0,
242
- endTimeMs: 3000,
243
- tagName: "EF-TIMEGROUP",
244
- } as any;
245
-
246
- // Create a child element in sequence that spans 2000-3000ms
247
- const element = createTestElement({
248
- startTimeMs: 2000,
249
- endTimeMs: 3000,
250
- durationMs: 1000,
251
- ownCurrentTimeMs: 1000, // At exact end of its own duration
252
- rootTimegroup: rootTimegroup,
253
- });
254
-
255
- // Create REAL animations using the Web Animations API
256
- const animation1 = element.animate([{ opacity: 0 }, { opacity: 1 }], {
257
- duration: 1000,
258
- delay: 0,
259
- iterations: 1,
260
- });
261
-
262
- const animation2 = element.animate(
263
- [{ transform: "scale(1)" }, { transform: "scale(1.5)" }],
264
- {
265
- duration: 1000,
266
- delay: 0,
267
- iterations: 1,
268
- },
269
- );
270
-
271
- // Start with animations running
272
- animation1.play();
273
- animation2.play();
274
-
275
- // Verify we have real animations
276
- const animations = element.getAnimations({ subtree: true });
277
- assert.equal(animations.length, 2, "Should have 2 real animations");
278
-
279
- updateAnimations(element);
280
-
281
- // The element should be hidden due to exclusive end condition (3000 > 3000 = false)
282
- assert.equal(
283
- element.style.display,
284
- "",
285
- "Element should be hidden at exact end boundary due to inclusive end",
286
- );
287
-
288
- // BUT animations should still be coordinated to prevent jarring visual jumps
289
- // This is the fix we want: animations coordinated even when element is hidden at exact boundary
290
- animations.forEach((animation, index) => {
291
- assert.approximately(
292
- animation.currentTime as number,
293
- 999,
294
- 1,
295
- `Animation ${index + 1} should be coordinated at exact end boundary to prevent visual jumps`,
296
- );
297
- assert.equal(
298
- animation.playState,
299
- "paused",
300
- `Animation ${index + 1} should be paused after coordination`,
301
- );
302
- });
303
- });
304
-
305
- test("uses element currentTimeMs when no rootTimegroup", () => {
306
- const element = createTestElement({
307
- currentTimeMs: 500,
308
- startTimeMs: 200,
309
- endTimeMs: 800,
310
- });
311
- element.style.display = "none";
312
-
313
- updateAnimations(element);
314
-
315
- assert.equal(element.style.display, "");
316
- });
317
-
318
- test("element at exact start boundary is visible (using element currentTimeMs)", () => {
319
- const element = createTestElement({
320
- currentTimeMs: 200,
321
- startTimeMs: 200,
322
- endTimeMs: 800,
323
- });
324
- element.style.display = "none";
325
-
326
- updateAnimations(element);
327
-
328
- assert.equal(element.style.display, "");
329
- });
330
-
331
- test("bare temporal element at its exact end is visible (root element)", () => {
332
- const element = createTestElement({
333
- currentTimeMs: 1000,
334
- startTimeMs: 0,
335
- endTimeMs: 1000,
336
- durationMs: 1000,
337
- });
338
- element.style.display = "";
339
-
340
- updateAnimations(element);
341
-
342
- assert.equal(
343
- element.style.display,
344
- "",
345
- "Root element should remain visible at exact end to show final frame",
346
- );
347
- });
348
-
349
- test("deeply nested element aligned with root end is visible (2 levels)", () => {
350
- const rootTimegroup = {
351
- currentTimeMs: 3000,
352
- durationMs: 3000,
353
- startTimeMs: 0,
354
- endTimeMs: 3000,
355
- tagName: "EF-TIMEGROUP",
356
- } as any;
357
-
358
- const childTimegroup = {
359
- currentTimeMs: 3000,
360
- durationMs: 2000,
361
- startTimeMs: 1000,
362
- endTimeMs: 3000,
363
- rootTimegroup,
364
- parentTimegroup: rootTimegroup,
365
- tagName: "EF-TIMEGROUP",
366
- } as any;
367
-
368
- const element = createTestElement({
369
- currentTimeMs: 1000,
370
- startTimeMs: 2000,
371
- endTimeMs: 3000,
372
- durationMs: 1000,
373
- ownCurrentTimeMs: 1000,
374
- rootTimegroup,
375
- parentTimegroup: childTimegroup,
376
- });
377
- element.style.display = "";
378
-
379
- updateAnimations(element);
380
-
381
- assert.equal(
382
- element.style.display,
383
- "",
384
- "Deeply nested element aligned with root end should remain visible",
385
- );
386
- });
387
-
388
- test("deeply nested element aligned with root end is visible (3 levels)", () => {
389
- const rootTimegroup = {
390
- currentTimeMs: 4000,
391
- durationMs: 4000,
392
- startTimeMs: 0,
393
- endTimeMs: 4000,
394
- tagName: "EF-TIMEGROUP",
395
- } as any;
396
-
397
- const childTimegroup1 = {
398
- currentTimeMs: 4000,
399
- durationMs: 3000,
400
- startTimeMs: 1000,
401
- endTimeMs: 4000,
402
- rootTimegroup,
403
- parentTimegroup: rootTimegroup,
404
- tagName: "EF-TIMEGROUP",
405
- } as any;
406
-
407
- const childTimegroup2 = {
408
- currentTimeMs: 4000,
409
- durationMs: 2000,
410
- startTimeMs: 2000,
411
- endTimeMs: 4000,
412
- rootTimegroup,
413
- parentTimegroup: childTimegroup1,
414
- tagName: "EF-TIMEGROUP",
415
- } as any;
416
-
417
- const element = createTestElement({
418
- currentTimeMs: 1000,
419
- startTimeMs: 3000,
420
- endTimeMs: 4000,
421
- durationMs: 1000,
422
- ownCurrentTimeMs: 1000,
423
- rootTimegroup,
424
- parentTimegroup: childTimegroup2,
425
- });
426
- element.style.display = "";
427
-
428
- updateAnimations(element);
429
-
430
- assert.equal(
431
- element.style.display,
432
- "",
433
- "3+ level nested element aligned with root end should remain visible",
434
- );
435
- });
436
-
437
- test("mid-composition element is hidden when timeline passes its end", () => {
438
- const rootTimegroup = {
439
- currentTimeMs: 3000,
440
- durationMs: 3000,
441
- startTimeMs: 0,
442
- endTimeMs: 3000,
443
- tagName: "EF-TIMEGROUP",
444
- } as any;
445
-
446
- const element = createTestElement({
447
- currentTimeMs: 1000,
448
- startTimeMs: 1000,
449
- endTimeMs: 2000,
450
- durationMs: 1000,
451
- rootTimegroup,
452
- parentTimegroup: rootTimegroup,
453
- });
454
- element.style.display = "";
455
-
456
- updateAnimations(element);
457
-
458
- assert.equal(
459
- element.style.display,
460
- "none",
461
- "Mid-composition element should be hidden when timeline is past its end",
462
- );
463
- });
464
-
465
- test("root timegroup at exact end is visible", () => {
466
- const rootTimegroup = document.createElement(
467
- "ef-timegroup",
468
- ) as EFTimegroup;
469
- Object.defineProperty(rootTimegroup, "currentTimeMs", {
470
- value: 1000,
471
- writable: true,
472
- });
473
- Object.defineProperty(rootTimegroup, "durationMs", {
474
- value: 1000,
475
- writable: true,
476
- });
477
- Object.defineProperty(rootTimegroup, "startTimeMs", {
478
- value: 0,
479
- writable: true,
480
- });
481
- Object.defineProperty(rootTimegroup, "endTimeMs", {
482
- value: 1000,
483
- writable: true,
484
- });
485
- Object.defineProperty(rootTimegroup, "parentTimegroup", {
486
- value: undefined,
487
- writable: true,
488
- });
489
- document.body.appendChild(rootTimegroup);
490
-
491
- updateAnimations(rootTimegroup as any);
492
-
493
- assert.equal(
494
- rootTimegroup.style.display,
495
- "",
496
- "Root timegroup should remain visible at exact end",
497
- );
498
-
499
- document.body.removeChild(rootTimegroup);
500
- });
501
-
502
- test("element at exact end boundary is visible when it is root (using element currentTimeMs)", () => {
503
- const element = createTestElement({
504
- currentTimeMs: 800,
505
- startTimeMs: 200,
506
- endTimeMs: 800,
507
- });
508
- element.style.display = "";
509
-
510
- updateAnimations(element);
511
-
512
- assert.equal(
513
- element.style.display,
514
- "",
515
- "Root element should remain visible at exact end boundary",
516
- );
517
- });
518
-
519
- test("element just before start boundary is hidden", () => {
520
- const element = createTestElement({
521
- currentTimeMs: 199,
522
- startTimeMs: 200,
523
- endTimeMs: 800,
524
- });
525
-
526
- updateAnimations(element);
527
-
528
- assert.equal(element.style.display, "none");
529
- });
530
-
531
- test("element just after end boundary is hidden", () => {
532
- const element = createTestElement({
533
- currentTimeMs: 801,
534
- startTimeMs: 200,
535
- endTimeMs: 800,
536
- });
537
-
538
- updateAnimations(element);
539
-
540
- assert.equal(element.style.display, "none");
541
- });
542
- });
543
-
544
- describe("Web Animations API integration", () => {
545
- test("skips animation processing when getAnimations is not available", () => {
546
- const element = createTestElement();
547
- // Mock missing getAnimations
548
- delete (element as any).getAnimations;
549
-
550
- // Should not throw and should still set CSS properties
551
- updateAnimations(element);
552
-
553
- assert.equal(element.style.getPropertyValue("--ef-progress"), "0%");
554
- });
555
-
556
- test("pauses running animations", async () => {
557
- const element = createTestElement();
558
-
559
- // Create a test animation
560
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
561
- duration: 1000,
562
- });
563
- animation.play();
564
-
565
- updateAnimations(element);
566
-
567
- assert.equal(animation.playState, "paused");
568
- });
569
-
570
- test("ignores animations without KeyframeEffect", async () => {
571
- const element = createTestElement();
572
-
573
- // Create animation with non-KeyframeEffect (this is tricky to test, but we can verify no errors)
574
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
575
- duration: 1000,
576
- });
577
-
578
- // Mock the effect to not be a KeyframeEffect
579
- Object.defineProperty(animation, "effect", {
580
- value: {},
581
- writable: false,
582
- });
583
-
584
- // Should not throw
585
- updateAnimations(element);
586
- });
587
-
588
- test("ignores animations without target", async () => {
589
- const element = createTestElement();
590
-
591
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
592
- duration: 1000,
593
- });
594
-
595
- // Mock the effect target to be null
596
- if (animation.effect instanceof KeyframeEffect) {
597
- Object.defineProperty(animation.effect, "target", {
598
- value: null,
599
- writable: false,
600
- });
601
- }
602
-
603
- // Should not throw
604
- updateAnimations(element);
605
- });
606
-
607
- test("handles missing timeTarget gracefully", async () => {
608
- const element = createTestElement();
609
-
610
- const target = document.createElement("div");
611
- element.appendChild(target);
612
-
613
- // Should not throw when target has no temporal parent
614
- updateAnimations(element);
615
- });
616
-
617
- test("processes multiple animations on same element", async () => {
618
- const element = createTestElement();
619
-
620
- const animation1 = element.animate([{ opacity: 0 }, { opacity: 1 }], {
621
- duration: 1000,
622
- });
623
- const animation2 = element.animate(
624
- [{ transform: "scale(1)" }, { transform: "scale(2)" }],
625
- { duration: 500 },
626
- );
627
-
628
- animation1.play();
629
- animation2.play();
630
-
631
- updateAnimations(element);
632
-
633
- // Both animations should be paused
634
- assert.equal(animation1.playState, "paused");
635
- assert.equal(animation2.playState, "paused");
636
- });
637
-
638
- test("handles animations with zero duration", async () => {
639
- const element = createTestElement();
640
-
641
- // Should not throw
642
- updateAnimations(element);
643
- });
644
-
645
- test("handles animations with zero iterations", async () => {
646
- const element = createTestElement();
647
-
648
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
649
- duration: 1000,
650
- iterations: 0,
651
- });
652
-
653
- updateAnimations(element);
654
-
655
- // With 0 iterations and no timeTarget, currentIteration (0) >= iterations (0), so should be set to duration - epsilon
656
- // But since there's no timeTarget, the code path continues and doesn't set currentTime
657
- // The animation will continue with its default behavior
658
- assert.equal(animation.currentTime, 0);
659
- });
660
-
661
- test("handles animations that are already paused", async () => {
662
- const element = createTestElement();
663
-
664
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
665
- duration: 1000,
666
- });
667
- animation.pause();
668
-
669
- assert.equal(animation.playState, "paused");
670
-
671
- // Should not throw and should still set currentTime
672
- updateAnimations(element);
673
-
674
- assert.equal(animation.playState, "paused");
675
- });
676
-
677
- test("keeps completed animations available for scrubbing", async () => {
678
- // Create a timegroup with 10s duration
679
- const timegroup = document.createElement("ef-timegroup") as EFTimegroup;
680
- timegroup.setAttribute("mode", "fixed");
681
- timegroup.setAttribute("duration", "10000ms");
682
- document.body.appendChild(timegroup);
683
-
684
- // Create a child element with a 5s animation
685
- const child = document.createElement("div");
686
- timegroup.appendChild(child);
687
-
688
- child.animate([{ opacity: 0 }, { opacity: 1 }], {
689
- duration: 5000, // 5s animation
690
- iterations: 1,
691
- delay: 0,
692
- });
693
- timegroup.currentTime = 6;
694
- await timegroup.seekTask.run();
695
-
696
- // Animation should still be available even though timeline (6s) > animation duration (5s)
697
- // This prevents animations from being removed, enabling scrubbing backwards
698
- const animations = timegroup.getAnimations({ subtree: true });
699
- assert.equal(
700
- animations.length,
701
- 1,
702
- "REGRESSION TEST: Animation should remain available for scrubbing. This would fail with Number.EPSILON due to insufficient precision offset.",
703
- );
704
- });
705
- });
706
-
707
- describe("child element animation coordination", () => {
708
- test("coordinates animations on non-temporal child elements", async () => {
709
- // Create root timegroup
710
- const rootTimegroup = document.createElement(
711
- "ef-timegroup",
712
- ) as EFTimegroup;
713
- rootTimegroup.currentTimeMs = 150; // Timeline at 150ms
714
- document.body.appendChild(rootTimegroup);
715
-
716
- // Create parent temporal element
717
- const parentElement = document.createElement(
718
- "test-temporal-element",
719
- ) as TestTemporalElement;
720
- parentElement.setDuration(300); // 300ms duration
721
- parentElement.setAttribute("offset", "100ms"); // Start at 100ms in root timeline
722
- rootTimegroup.appendChild(parentElement);
723
-
724
- // Create a regular NON-temporal HTML element inside the temporal element
725
- const nonTemporalDiv = document.createElement("div");
726
- parentElement.appendChild(nonTemporalDiv);
727
-
728
- // Wait for elements to be connected and updated
729
- await rootTimegroup.updateComplete;
730
- await parentElement.updateComplete;
731
-
732
- // Create animation on the NON-temporal child element
733
- const nonTemporalAnimation = nonTemporalDiv.animate(
734
- [{ opacity: 0 }, { opacity: 1 }],
735
- {
736
- duration: 1000,
737
- },
738
- );
739
- nonTemporalAnimation.play();
740
-
741
- // Call updateAnimations on root timegroup
742
- updateAnimations(rootTimegroup);
743
-
744
- // Parent should be visible at current timeline position (150ms is between 100ms-400ms)
745
- assert.notEqual(
746
- parentElement.style.display,
747
- "",
748
- "Parent should be visible at current timeline time",
749
- );
750
-
751
- // FIXED: Non-temporal child animation should be paused and coordinated
752
- assert.equal(
753
- nonTemporalAnimation.playState,
754
- "paused",
755
- "Non-temporal child element animation should be paused and coordinated with timeline",
756
- );
757
- });
758
-
759
- test("coordinates animations on deeply nested non-temporal elements", async () => {
760
- // Create root timegroup
761
- const rootTimegroup = document.createElement(
762
- "ef-timegroup",
763
- ) as EFTimegroup;
764
- rootTimegroup.currentTimeMs = 150; // Timeline at 150ms
765
- document.body.appendChild(rootTimegroup);
766
-
767
- // Create parent temporal element
768
- const parentElement = document.createElement(
769
- "test-temporal-element",
770
- ) as TestTemporalElement;
771
- parentElement.setDuration(300); // 300ms duration
772
- parentElement.setAttribute("offset", "100ms"); // Start at 100ms in root timeline
773
- rootTimegroup.appendChild(parentElement);
774
-
775
- // Create nested non-temporal structure: temporal > div > div > span
776
- const outerDiv = document.createElement("div");
777
- const innerDiv = document.createElement("div");
778
- const span = document.createElement("span");
779
-
780
- parentElement.appendChild(outerDiv);
781
- outerDiv.appendChild(innerDiv);
782
- innerDiv.appendChild(span);
783
-
784
- // Wait for elements to be connected and updated
785
- await rootTimegroup.updateComplete;
786
- await parentElement.updateComplete;
787
-
788
- // Create animations on different levels of nesting
789
- const outerAnimation = outerDiv.animate(
790
- [{ transform: "scale(1)" }, { transform: "scale(1.1)" }],
791
- {
792
- duration: 800,
793
- },
794
- );
795
- const innerAnimation = innerDiv.animate(
796
- [{ opacity: 0.5 }, { opacity: 1 }],
797
- {
798
- duration: 1200,
799
- },
800
- );
801
- const spanAnimation = span.animate(
802
- [{ color: "red" }, { color: "blue" }],
803
- {
804
- duration: 600,
805
- },
806
- );
807
-
808
- outerAnimation.play();
809
- innerAnimation.play();
810
- spanAnimation.play();
811
-
812
- // Call updateAnimations on root timegroup
813
- updateAnimations(rootTimegroup);
814
-
815
- // All nested non-temporal animations should be coordinated
816
- assert.equal(
817
- outerAnimation.playState,
818
- "paused",
819
- "Outer div animation should be coordinated",
820
- );
821
- assert.equal(
822
- innerAnimation.playState,
823
- "paused",
824
- "Inner div animation should be coordinated",
825
- );
826
- assert.equal(
827
- spanAnimation.playState,
828
- "paused",
829
- "Span animation should be coordinated",
830
- );
831
- });
832
-
833
- test("coordinates animations on child temporal elements when they are visible", async () => {
834
- // Create root timegroup
835
- const rootTimegroup = document.createElement(
836
- "ef-timegroup",
837
- ) as EFTimegroup;
838
- rootTimegroup.currentTimeMs = 150; // Timeline at 150ms
839
- document.body.appendChild(rootTimegroup);
840
-
841
- // Create parent element (timegroup acts as parent)
842
- const parentTimegroup = document.createElement(
843
- "ef-timegroup",
844
- ) as EFTimegroup;
845
- parentTimegroup.setAttribute("duration", "1000ms");
846
- rootTimegroup.appendChild(parentTimegroup);
847
-
848
- // Create child temporal element that WILL be visible at timeline time 150ms
849
- const childElement = document.createElement(
850
- "test-temporal-element",
851
- ) as TestTemporalElement;
852
- childElement.setDuration(300); // 300ms duration (from 100ms to 400ms in root timeline)
853
- childElement.setAttribute("offset", "100ms"); // Start at 100ms in root timeline
854
- parentTimegroup.appendChild(childElement);
855
-
856
- // Wait for elements to be connected and updated
857
- await rootTimegroup.updateComplete;
858
- await parentTimegroup.updateComplete;
859
- await childElement.updateComplete;
860
-
861
- // Create animation on child element
862
- const childAnimation = childElement.animate(
863
- [{ opacity: 0 }, { opacity: 1 }],
864
- {
865
- duration: 1000,
866
- },
867
- );
868
- childAnimation.play();
869
-
870
- // Call updateAnimations on parent timegroup - this should coordinate child animations too
871
- updateAnimations(parentTimegroup);
872
-
873
- // Child should be visible at current timeline position (150ms is between 100ms-400ms)
874
- assert.notEqual(
875
- childElement.style.display,
876
- "",
877
- "Child should be visible at current timeline time",
878
- );
879
-
880
- // FIXED: Child animation should be paused and coordinated
881
- assert.equal(
882
- childAnimation.playState,
883
- "paused",
884
- "Child element animation should be paused and coordinated with timeline",
885
- );
886
- });
887
-
888
- test("does not coordinate animations on child temporal elements when they are not visible", async () => {
889
- // Create root timegroup
890
- const rootTimegroup = document.createElement(
891
- "ef-timegroup",
892
- ) as EFTimegroup;
893
- rootTimegroup.currentTimeMs = 100; // Timeline at 100ms
894
- document.body.appendChild(rootTimegroup);
895
-
896
- // Create parent element (timegroup acts as parent)
897
- const parentTimegroup = document.createElement(
898
- "ef-timegroup",
899
- ) as EFTimegroup;
900
- parentTimegroup.setAttribute("duration", "1000ms");
901
- rootTimegroup.appendChild(parentTimegroup);
902
-
903
- // Create child temporal element that will NOT be visible at timeline time 100ms
904
- const childElement = document.createElement(
905
- "test-temporal-element",
906
- ) as TestTemporalElement;
907
- childElement.setDuration(200); // 200ms duration
908
- childElement.setAttribute("offset", "500ms"); // Start at 500ms in root timeline (way after current time)
909
- parentTimegroup.appendChild(childElement);
910
-
911
- // Wait for elements to be connected and updated
912
- await rootTimegroup.updateComplete;
913
- await parentTimegroup.updateComplete;
914
- await childElement.updateComplete;
915
-
916
- // Create animation on child element
917
- const childAnimation = childElement.animate(
918
- [{ opacity: 0 }, { opacity: 1 }],
919
- {
920
- duration: 1000,
921
- },
922
- );
923
- childAnimation.play();
924
-
925
- // Call updateAnimations on parent timegroup
926
- updateAnimations(parentTimegroup);
927
-
928
- // Child should be hidden (display: none)
929
- assert.equal(
930
- childElement.style.display,
931
- "none",
932
- "Child should be hidden when not in visible time range",
933
- );
934
-
935
- // Child animation should still be running (not coordinated since child is not visible)
936
- assert.equal(
937
- childAnimation.playState,
938
- "paused",
939
- "Child animation should remain running when child element is not visible",
940
- );
941
- });
942
- });
943
-
944
- describe("edge cases", () => {
945
- test("handles zero duration gracefully", () => {
946
- const element = createTestElement({
947
- currentTimeMs: 100,
948
- durationMs: 0,
949
- });
950
-
951
- updateAnimations(element);
952
-
953
- // Should handle division by zero
954
- assert.equal(element.style.getPropertyValue("--ef-progress"), "100%");
955
- });
956
-
957
- test("handles negative currentTimeMs", () => {
958
- const element = createTestElement({
959
- currentTimeMs: -100,
960
- durationMs: 1000,
961
- });
962
-
963
- updateAnimations(element);
964
-
965
- assert.equal(element.style.getPropertyValue("--ef-progress"), "0%");
966
- });
967
-
968
- test("handles missing parentTimegroup overlapMs", () => {
969
- const parentTimegroup = {} as EFTimegroup; // Missing overlapMs property
970
-
971
- const element = createTestElement({
972
- parentTimegroup,
973
- durationMs: 1000,
974
- });
975
-
976
- updateAnimations(element);
977
-
978
- assert.equal(
979
- element.style.getPropertyValue("--ef-transition-duration"),
980
- "0ms",
981
- );
982
- assert.equal(
983
- element.style.getPropertyValue("--ef-transition-out-start"),
984
- "1000ms",
985
- );
986
- });
987
-
988
- test("handles large duration values", () => {
989
- const element = createTestElement({
990
- currentTimeMs: 5000,
991
- durationMs: 1000000, // 1000 seconds
992
- });
993
-
994
- updateAnimations(element);
995
-
996
- assert.equal(element.style.getPropertyValue("--ef-progress"), "0.5%");
997
- });
998
-
999
- test("handles very small time values", () => {
1000
- const element = createTestElement({
1001
- currentTimeMs: 0.5,
1002
- durationMs: 10,
1003
- });
1004
-
1005
- updateAnimations(element);
1006
-
1007
- assert.equal(element.style.getPropertyValue("--ef-progress"), "5%");
1008
- });
1009
-
1010
- test("does not modify display when element should remain visible", () => {
1011
- const element = createTestElement({
1012
- currentTimeMs: 500,
1013
- startTimeMs: 200,
1014
- endTimeMs: 800,
1015
- });
1016
- // Element starts visible (default state)
1017
- const initialDisplay = element.style.display;
1018
-
1019
- updateAnimations(element);
1020
-
1021
- // Should not have been modified
1022
- assert.equal(element.style.display, initialDisplay);
1023
- });
1024
-
1025
- test("does not modify display when element should remain hidden", () => {
1026
- const element = createTestElement({
1027
- currentTimeMs: 100,
1028
- startTimeMs: 200,
1029
- endTimeMs: 800,
1030
- });
1031
- element.style.display = "none";
1032
-
1033
- updateAnimations(element);
1034
-
1035
- // Should still be hidden
1036
- assert.equal(element.style.display, "none");
1037
- });
1038
- });
1039
-
1040
- describe("CSS variables with zero overlap", () => {
1041
- test("correctly sets transition duration to 0ms when overlap is 0ms", () => {
1042
- const element = createTestElement({
1043
- currentTimeMs: 500,
1044
- startTimeMs: 0,
1045
- endTimeMs: 1000,
1046
- durationMs: 1000,
1047
- parentTimegroup: {
1048
- overlapMs: 0, // Zero overlap case
1049
- } as any,
1050
- });
1051
-
1052
- updateAnimations(element);
1053
-
1054
- // When overlap is 0, transition duration should be 0ms (no overlap means no transition)
1055
- const transitionDuration = element.style.getPropertyValue(
1056
- "--ef-transition-duration",
1057
- );
1058
- assert.equal(
1059
- transitionDuration,
1060
- "0ms",
1061
- "Transition duration should be 0ms when no overlap",
1062
- );
1063
-
1064
- // Transition out start should equal full duration (no transition period)
1065
- const transitionOutStart = element.style.getPropertyValue(
1066
- "--ef-transition-out-start",
1067
- );
1068
- assert.equal(
1069
- transitionOutStart,
1070
- "1000ms",
1071
- "Transition out start should equal full duration when no overlap",
1072
- );
1073
-
1074
- // Duration variable should still be set correctly for within-clip animations
1075
- const duration = element.style.getPropertyValue("--ef-duration");
1076
- assert.equal(
1077
- duration,
1078
- "1000ms",
1079
- "Duration should be set for within-clip animation calculations",
1080
- );
1081
- });
1082
- });
1083
-
1084
- describe("animation-direction support", () => {
1085
- test("normal direction: maintains forward playback at start", () => {
1086
- const element = createTestElement({
1087
- ownCurrentTimeMs: 0,
1088
- });
1089
-
1090
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1091
- duration: 1000,
1092
- direction: "normal",
1093
- });
1094
-
1095
- updateAnimations(element);
1096
-
1097
- assert.equal(animation.currentTime, 0);
1098
- });
1099
-
1100
- test("normal direction: maintains forward playback at middle", () => {
1101
- const element = createTestElement({
1102
- ownCurrentTimeMs: 500,
1103
- });
1104
-
1105
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1106
- duration: 1000,
1107
- direction: "normal",
1108
- });
1109
-
1110
- updateAnimations(element);
1111
-
1112
- assert.approximately(animation.currentTime as number, 500, 1);
1113
- });
1114
-
1115
- test("normal direction: maintains forward playback near end", () => {
1116
- const element = createTestElement({
1117
- ownCurrentTimeMs: 999,
1118
- });
1119
-
1120
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1121
- duration: 1000,
1122
- direction: "normal",
1123
- });
1124
-
1125
- updateAnimations(element);
1126
-
1127
- assert.approximately(animation.currentTime as number, 999, 1);
1128
- });
1129
-
1130
- test("reverse direction: shows end frame at start", () => {
1131
- const element = createTestElement({
1132
- ownCurrentTimeMs: 0,
1133
- });
1134
-
1135
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1136
- duration: 1000,
1137
- direction: "reverse",
1138
- });
1139
-
1140
- updateAnimations(element);
1141
-
1142
- assert.approximately(animation.currentTime as number, 1000, 1);
1143
- });
1144
-
1145
- test("reverse direction: shows reversed progress at middle", () => {
1146
- const element = createTestElement({
1147
- ownCurrentTimeMs: 300,
1148
- });
1149
-
1150
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1151
- duration: 1000,
1152
- direction: "reverse",
1153
- });
1154
-
1155
- updateAnimations(element);
1156
-
1157
- assert.approximately(animation.currentTime as number, 700, 1);
1158
- });
1159
-
1160
- test("reverse direction: shows start frame near end", () => {
1161
- const element = createTestElement({
1162
- ownCurrentTimeMs: 999,
1163
- });
1164
-
1165
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1166
- duration: 1000,
1167
- direction: "reverse",
1168
- });
1169
-
1170
- updateAnimations(element);
1171
-
1172
- assert.approximately(animation.currentTime as number, 1, 2);
1173
- });
1174
-
1175
- test("alternate direction: plays forward in iteration 0", () => {
1176
- const element = createTestElement({
1177
- ownCurrentTimeMs: 250,
1178
- });
1179
-
1180
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1181
- duration: 1000,
1182
- iterations: 3,
1183
- direction: "alternate",
1184
- });
1185
-
1186
- updateAnimations(element);
1187
-
1188
- assert.approximately(animation.currentTime as number, 250, 1);
1189
- });
1190
-
1191
- test("alternate direction: plays backward in iteration 1", () => {
1192
- const element = createTestElement({
1193
- ownCurrentTimeMs: 1250,
1194
- });
1195
-
1196
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1197
- duration: 1000,
1198
- iterations: 3,
1199
- direction: "alternate",
1200
- });
1201
-
1202
- updateAnimations(element);
1203
-
1204
- assert.approximately(animation.currentTime as number, 750, 1);
1205
- });
1206
-
1207
- test("alternate direction: plays forward in iteration 2", () => {
1208
- const element = createTestElement({
1209
- ownCurrentTimeMs: 2250,
1210
- });
1211
-
1212
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1213
- duration: 1000,
1214
- iterations: 3,
1215
- direction: "alternate",
1216
- });
1217
-
1218
- updateAnimations(element);
1219
-
1220
- assert.approximately(animation.currentTime as number, 250, 1);
1221
- });
1222
-
1223
- test("alternate-reverse direction: plays backward in iteration 0", () => {
1224
- const element = createTestElement({
1225
- ownCurrentTimeMs: 250,
1226
- });
1227
-
1228
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1229
- duration: 1000,
1230
- iterations: 3,
1231
- direction: "alternate-reverse",
1232
- });
1233
-
1234
- updateAnimations(element);
1235
-
1236
- assert.approximately(animation.currentTime as number, 750, 1);
1237
- });
1238
-
1239
- test("alternate-reverse direction: plays forward in iteration 1", () => {
1240
- const element = createTestElement({
1241
- ownCurrentTimeMs: 1250,
1242
- });
1243
-
1244
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1245
- duration: 1000,
1246
- iterations: 3,
1247
- direction: "alternate-reverse",
1248
- });
1249
-
1250
- updateAnimations(element);
1251
-
1252
- assert.approximately(animation.currentTime as number, 250, 1);
1253
- });
1254
-
1255
- test("alternate-reverse direction: plays backward in iteration 2", () => {
1256
- const element = createTestElement({
1257
- ownCurrentTimeMs: 2250,
1258
- });
1259
-
1260
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1261
- duration: 1000,
1262
- iterations: 3,
1263
- direction: "alternate-reverse",
1264
- });
1265
-
1266
- updateAnimations(element);
1267
-
1268
- assert.approximately(animation.currentTime as number, 750, 1);
1269
- });
1270
-
1271
- test("alternate direction at exact iteration boundary (start of iteration 1)", () => {
1272
- const element = createTestElement({
1273
- ownCurrentTimeMs: 1000,
1274
- });
1275
-
1276
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1277
- duration: 1000,
1278
- iterations: 3,
1279
- direction: "alternate",
1280
- });
1281
-
1282
- updateAnimations(element);
1283
-
1284
- assert.approximately(animation.currentTime as number, 1000, 1);
1285
- });
1286
-
1287
- test("alternate direction at exact iteration boundary (start of iteration 2)", () => {
1288
- const element = createTestElement({
1289
- ownCurrentTimeMs: 2000,
1290
- });
1291
-
1292
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1293
- duration: 1000,
1294
- iterations: 3,
1295
- direction: "alternate",
1296
- });
1297
-
1298
- updateAnimations(element);
1299
-
1300
- assert.approximately(animation.currentTime as number, 0, 1);
1301
- });
1302
-
1303
- test("reverse direction with single iteration", () => {
1304
- const element = createTestElement({
1305
- ownCurrentTimeMs: 400,
1306
- });
1307
-
1308
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1309
- duration: 1000,
1310
- iterations: 1,
1311
- direction: "reverse",
1312
- });
1313
-
1314
- updateAnimations(element);
1315
-
1316
- assert.approximately(animation.currentTime as number, 600, 1);
1317
- });
1318
-
1319
- test("multiple animations with different directions on same element", () => {
1320
- const element = createTestElement({
1321
- ownCurrentTimeMs: 300,
1322
- });
1323
-
1324
- const normalAnimation = element.animate(
1325
- [{ opacity: 0 }, { opacity: 1 }],
1326
- {
1327
- duration: 1000,
1328
- direction: "normal",
1329
- },
1330
- );
1331
-
1332
- const reverseAnimation = element.animate(
1333
- [{ transform: "scale(1)" }, { transform: "scale(2)" }],
1334
- {
1335
- duration: 1000,
1336
- direction: "reverse",
1337
- },
1338
- );
1339
-
1340
- const alternateAnimation = element.animate(
1341
- [{ color: "red" }, { color: "blue" }],
1342
- {
1343
- duration: 1000,
1344
- iterations: 3,
1345
- direction: "alternate",
1346
- },
1347
- );
1348
-
1349
- updateAnimations(element);
1350
-
1351
- assert.approximately(normalAnimation.currentTime as number, 300, 1);
1352
- assert.approximately(reverseAnimation.currentTime as number, 700, 1);
1353
- assert.approximately(alternateAnimation.currentTime as number, 300, 1);
1354
- });
1355
-
1356
- test("alternate direction with delay: iteration 0 plays forward", () => {
1357
- const element = createTestElement({
1358
- ownCurrentTimeMs: 750,
1359
- });
1360
-
1361
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1362
- duration: 1000,
1363
- delay: 500,
1364
- iterations: 3,
1365
- direction: "alternate",
1366
- });
1367
-
1368
- updateAnimations(element);
1369
-
1370
- assert.approximately(animation.currentTime as number, 750, 1);
1371
- });
1372
-
1373
- test("alternate direction with delay: iteration 1 plays backward", () => {
1374
- const element = createTestElement({
1375
- ownCurrentTimeMs: 1750,
1376
- });
1377
-
1378
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1379
- duration: 1000,
1380
- delay: 500,
1381
- iterations: 3,
1382
- direction: "alternate",
1383
- });
1384
-
1385
- updateAnimations(element);
1386
-
1387
- assert.approximately(animation.currentTime as number, 1250, 1);
1388
- });
1389
-
1390
- test("reverse direction respects precision offset to prevent completion", () => {
1391
- const element = createTestElement({
1392
- ownCurrentTimeMs: 1000,
1393
- });
1394
-
1395
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1396
- duration: 1000,
1397
- direction: "reverse",
1398
- });
1399
-
1400
- updateAnimations(element);
1401
-
1402
- assert.isBelow(
1403
- animation.currentTime as number,
1404
- 1000,
1405
- "Animation should not reach exact completion",
1406
- );
1407
- assert.approximately(animation.currentTime as number, 999, 1);
1408
- });
1409
-
1410
- test("alternate direction at end of final iteration respects precision offset", () => {
1411
- const element = createTestElement({
1412
- ownCurrentTimeMs: 2999,
1413
- });
1414
-
1415
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1416
- duration: 1000,
1417
- iterations: 3,
1418
- direction: "alternate",
1419
- });
1420
-
1421
- updateAnimations(element);
1422
-
1423
- assert.isBelow(
1424
- animation.currentTime as number,
1425
- 1000,
1426
- "Animation should not reach iteration completion",
1427
- );
1428
- assert.approximately(
1429
- animation.currentTime as number,
1430
- 999,
1431
- 1,
1432
- "Should be at end of iteration 2 with precision offset",
1433
- );
1434
- });
1435
- });
1436
-
1437
- describe("animation-fill-mode support", () => {
1438
- test("fill-mode none: animation before delay shows no effect", () => {
1439
- const element = createTestElement({
1440
- ownCurrentTimeMs: 250,
1441
- });
1442
-
1443
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1444
- duration: 1000,
1445
- delay: 500,
1446
- fill: "none",
1447
- });
1448
-
1449
- updateAnimations(element);
1450
-
1451
- assert.equal(
1452
- animation.currentTime,
1453
- 0,
1454
- "Animation should be at start when before delay with fill: none",
1455
- );
1456
- });
1457
-
1458
- test("fill-mode backwards: animation before delay applies starting values", () => {
1459
- const element = createTestElement({
1460
- ownCurrentTimeMs: 250,
1461
- });
1462
-
1463
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1464
- duration: 1000,
1465
- delay: 500,
1466
- fill: "backwards",
1467
- });
1468
-
1469
- updateAnimations(element);
1470
-
1471
- assert.equal(
1472
- animation.currentTime,
1473
- 0,
1474
- "Animation should be at start when before delay with fill: backwards",
1475
- );
1476
- });
1477
-
1478
- test("fill-mode forwards: animation after completion holds final state", () => {
1479
- const element = createTestElement({
1480
- ownCurrentTimeMs: 1500,
1481
- });
1482
-
1483
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1484
- duration: 1000,
1485
- fill: "forwards",
1486
- });
1487
-
1488
- updateAnimations(element);
1489
-
1490
- assert.approximately(
1491
- animation.currentTime as number,
1492
- 999,
1493
- 1,
1494
- "Animation should be held at end with precision offset",
1495
- );
1496
- });
1497
-
1498
- test("fill-mode both: applies both backwards and forwards behavior", () => {
1499
- const element = createTestElement({
1500
- ownCurrentTimeMs: 250,
1501
- });
1502
-
1503
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1504
- duration: 1000,
1505
- delay: 500,
1506
- fill: "both",
1507
- });
1508
-
1509
- updateAnimations(element);
1510
-
1511
- assert.equal(
1512
- animation.currentTime,
1513
- 0,
1514
- "Animation should be at start when before delay with fill: both",
1515
- );
1516
- });
1517
-
1518
- test("fill-mode forwards with element at exact end boundary", () => {
1519
- const rootTimegroup = {
1520
- currentTimeMs: 1000,
1521
- durationMs: 1000,
1522
- startTimeMs: 0,
1523
- endTimeMs: 1000,
1524
- tagName: "EF-TIMEGROUP",
1525
- } as any;
1526
-
1527
- const element = createTestElement({
1528
- startTimeMs: 0,
1529
- endTimeMs: 1000,
1530
- durationMs: 1000,
1531
- ownCurrentTimeMs: 1000,
1532
- rootTimegroup,
1533
- });
1534
-
1535
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1536
- duration: 1000,
1537
- fill: "forwards",
1538
- });
1539
-
1540
- updateAnimations(element);
1541
-
1542
- assert.equal(
1543
- element.style.display,
1544
- "",
1545
- "Element should be visible at exact end boundary (root element)",
1546
- );
1547
-
1548
- assert.approximately(
1549
- animation.currentTime as number,
1550
- 999,
1551
- 1,
1552
- "Animation should be coordinated near completion with precision offset",
1553
- );
1554
- });
1555
-
1556
- test("fill-mode none: animation past completion has no effect", () => {
1557
- const element = createTestElement({
1558
- ownCurrentTimeMs: 1500,
1559
- });
1560
-
1561
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1562
- duration: 1000,
1563
- fill: "none",
1564
- });
1565
-
1566
- updateAnimations(element);
1567
-
1568
- assert.approximately(
1569
- animation.currentTime as number,
1570
- 999,
1571
- 1,
1572
- "Animation should still be coordinated at end even with fill: none",
1573
- );
1574
- });
1575
-
1576
- test("fill-mode forwards with reverse direction", () => {
1577
- const element = createTestElement({
1578
- ownCurrentTimeMs: 1500,
1579
- });
1580
-
1581
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1582
- duration: 1000,
1583
- direction: "reverse",
1584
- fill: "forwards",
1585
- });
1586
-
1587
- updateAnimations(element);
1588
-
1589
- assert.approximately(
1590
- animation.currentTime as number,
1591
- 999,
1592
- 1,
1593
- "Reverse animation with forwards fill should hold at logical end (visual start)",
1594
- );
1595
- });
1596
-
1597
- test("fill-mode backwards with reverse direction", () => {
1598
- const element = createTestElement({
1599
- ownCurrentTimeMs: 250,
1600
- });
1601
-
1602
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1603
- duration: 1000,
1604
- delay: 500,
1605
- direction: "reverse",
1606
- fill: "backwards",
1607
- });
1608
-
1609
- updateAnimations(element);
1610
-
1611
- assert.equal(
1612
- animation.currentTime,
1613
- 0,
1614
- "Reverse animation with backwards fill should apply logical start (visual end) during delay",
1615
- );
1616
- });
1617
- });
1618
-
1619
- describe("animation-timing-function support", () => {
1620
- test("ease timing function: correctly interpolates at midpoint", () => {
1621
- const element = createTestElement({
1622
- ownCurrentTimeMs: 500,
1623
- });
1624
-
1625
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1626
- duration: 1000,
1627
- easing: "ease",
1628
- });
1629
-
1630
- updateAnimations(element);
1631
-
1632
- assert.approximately(
1633
- animation.currentTime as number,
1634
- 500,
1635
- 1,
1636
- "Timeline position should be correct regardless of easing",
1637
- );
1638
- });
1639
-
1640
- test("linear timing function: evenly distributes time", () => {
1641
- const element = createTestElement({
1642
- ownCurrentTimeMs: 500,
1643
- });
1644
-
1645
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1646
- duration: 1000,
1647
- easing: "linear",
1648
- });
1649
-
1650
- updateAnimations(element);
1651
-
1652
- assert.approximately(animation.currentTime as number, 500, 1);
1653
- });
1654
-
1655
- test("ease-in timing function: slow start", () => {
1656
- const element = createTestElement({
1657
- ownCurrentTimeMs: 250,
1658
- });
1659
-
1660
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1661
- duration: 1000,
1662
- easing: "ease-in",
1663
- });
1664
-
1665
- updateAnimations(element);
1666
-
1667
- assert.approximately(
1668
- animation.currentTime as number,
1669
- 250,
1670
- 1,
1671
- "Timeline coordination should be independent of easing curve",
1672
- );
1673
- });
1674
-
1675
- test("ease-out timing function: slow end", () => {
1676
- const element = createTestElement({
1677
- ownCurrentTimeMs: 750,
1678
- });
1679
-
1680
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1681
- duration: 1000,
1682
- easing: "ease-out",
1683
- });
1684
-
1685
- updateAnimations(element);
1686
-
1687
- assert.approximately(animation.currentTime as number, 750, 1);
1688
- });
1689
-
1690
- test("ease-in-out timing function: slow start and end", () => {
1691
- const element = createTestElement({
1692
- ownCurrentTimeMs: 500,
1693
- });
1694
-
1695
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1696
- duration: 1000,
1697
- easing: "ease-in-out",
1698
- });
1699
-
1700
- updateAnimations(element);
1701
-
1702
- assert.approximately(animation.currentTime as number, 500, 1);
1703
- });
1704
-
1705
- test("cubic-bezier timing function: custom curve", () => {
1706
- const element = createTestElement({
1707
- ownCurrentTimeMs: 400,
1708
- });
1709
-
1710
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1711
- duration: 1000,
1712
- easing: "cubic-bezier(0.42, 0, 0.58, 1)",
1713
- });
1714
-
1715
- updateAnimations(element);
1716
-
1717
- assert.approximately(animation.currentTime as number, 400, 1);
1718
- });
1719
-
1720
- test("steps timing function: start - discrete jumps at interval starts", () => {
1721
- const element = createTestElement({
1722
- ownCurrentTimeMs: 250,
1723
- });
1724
-
1725
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1726
- duration: 1000,
1727
- easing: "steps(4, start)",
1728
- });
1729
-
1730
- updateAnimations(element);
1731
-
1732
- assert.approximately(
1733
- animation.currentTime as number,
1734
- 250,
1735
- 1,
1736
- "Steps timing should work with timeline coordination",
1737
- );
1738
- });
1739
-
1740
- test("steps timing function: end - discrete jumps at interval ends", () => {
1741
- const element = createTestElement({
1742
- ownCurrentTimeMs: 250,
1743
- });
1744
-
1745
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1746
- duration: 1000,
1747
- easing: "steps(4, end)",
1748
- });
1749
-
1750
- updateAnimations(element);
1751
-
1752
- assert.approximately(animation.currentTime as number, 250, 1);
1753
- });
1754
-
1755
- test("step-start timing function: immediate jump to end value", () => {
1756
- const element = createTestElement({
1757
- ownCurrentTimeMs: 100,
1758
- });
1759
-
1760
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1761
- duration: 1000,
1762
- easing: "step-start",
1763
- });
1764
-
1765
- updateAnimations(element);
1766
-
1767
- assert.approximately(
1768
- animation.currentTime as number,
1769
- 100,
1770
- 1,
1771
- "Step-start should work with timeline coordination",
1772
- );
1773
- });
1774
-
1775
- test("step-end timing function: hold start value until end", () => {
1776
- const element = createTestElement({
1777
- ownCurrentTimeMs: 999,
1778
- });
1779
-
1780
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1781
- duration: 1000,
1782
- easing: "step-end",
1783
- });
1784
-
1785
- updateAnimations(element);
1786
-
1787
- assert.approximately(animation.currentTime as number, 999, 1);
1788
- });
1789
-
1790
- test("timing function with reverse direction", () => {
1791
- const element = createTestElement({
1792
- ownCurrentTimeMs: 300,
1793
- });
1794
-
1795
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1796
- duration: 1000,
1797
- direction: "reverse",
1798
- easing: "ease-in",
1799
- });
1800
-
1801
- updateAnimations(element);
1802
-
1803
- assert.approximately(
1804
- animation.currentTime as number,
1805
- 700,
1806
- 1,
1807
- "Reverse direction should correctly invert time with easing",
1808
- );
1809
- });
1810
-
1811
- test("timing function with alternate direction", () => {
1812
- const element = createTestElement({
1813
- ownCurrentTimeMs: 1300,
1814
- });
1815
-
1816
- const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
1817
- duration: 1000,
1818
- iterations: 3,
1819
- direction: "alternate",
1820
- easing: "ease-out",
1821
- });
1822
-
1823
- updateAnimations(element);
1824
-
1825
- assert.approximately(
1826
- animation.currentTime as number,
1827
- 700,
1828
- 1,
1829
- "Alternate direction should work with easing on reversed iterations",
1830
- );
1831
- });
1832
-
1833
- test("multiple animations with different timing functions", () => {
1834
- const element = createTestElement({
1835
- ownCurrentTimeMs: 500,
1836
- });
1837
-
1838
- const linearAnimation = element.animate(
1839
- [{ opacity: 0 }, { opacity: 1 }],
1840
- {
1841
- duration: 1000,
1842
- easing: "linear",
1843
- },
1844
- );
1845
-
1846
- const easeAnimation = element.animate(
1847
- [{ transform: "scale(1)" }, { transform: "scale(2)" }],
1848
- {
1849
- duration: 1000,
1850
- easing: "ease-in-out",
1851
- },
1852
- );
1853
-
1854
- const stepsAnimation = element.animate(
1855
- [{ color: "red" }, { color: "blue" }],
1856
- {
1857
- duration: 1000,
1858
- easing: "steps(5, end)",
1859
- },
1860
- );
1861
-
1862
- updateAnimations(element);
1863
-
1864
- assert.approximately(
1865
- linearAnimation.currentTime as number,
1866
- 500,
1867
- 1,
1868
- "Linear animation should be at midpoint",
1869
- );
1870
- assert.approximately(
1871
- easeAnimation.currentTime as number,
1872
- 500,
1873
- 1,
1874
- "Ease animation should be at midpoint timeline",
1875
- );
1876
- assert.approximately(
1877
- stepsAnimation.currentTime as number,
1878
- 500,
1879
- 1,
1880
- "Steps animation should be at midpoint timeline",
1881
- );
1882
- });
1883
- });
1884
- });