@editframe/elements 0.26.2-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 (135) hide show
  1. package/dist/elements/EFTimegroup.js +7 -2
  2. package/dist/elements/EFTimegroup.js.map +1 -1
  3. package/package.json +2 -2
  4. package/scripts/build-css.js +3 -3
  5. package/tsdown.config.ts +1 -1
  6. package/types.json +1 -1
  7. package/src/elements/ContextProxiesController.ts +0 -124
  8. package/src/elements/CrossUpdateController.ts +0 -22
  9. package/src/elements/EFAudio.browsertest.ts +0 -706
  10. package/src/elements/EFAudio.ts +0 -56
  11. package/src/elements/EFCaptions.browsertest.ts +0 -1960
  12. package/src/elements/EFCaptions.ts +0 -823
  13. package/src/elements/EFImage.browsertest.ts +0 -120
  14. package/src/elements/EFImage.ts +0 -113
  15. package/src/elements/EFMedia/AssetIdMediaEngine.test.ts +0 -224
  16. package/src/elements/EFMedia/AssetIdMediaEngine.ts +0 -110
  17. package/src/elements/EFMedia/AssetMediaEngine.browsertest.ts +0 -140
  18. package/src/elements/EFMedia/AssetMediaEngine.ts +0 -385
  19. package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +0 -400
  20. package/src/elements/EFMedia/BaseMediaEngine.ts +0 -505
  21. package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +0 -386
  22. package/src/elements/EFMedia/BufferedSeekingInput.ts +0 -430
  23. package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +0 -226
  24. package/src/elements/EFMedia/JitMediaEngine.ts +0 -256
  25. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.ts +0 -679
  26. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +0 -117
  27. package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +0 -246
  28. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.ts +0 -59
  29. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts +0 -27
  30. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.ts +0 -55
  31. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +0 -53
  32. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +0 -207
  33. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +0 -72
  34. package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +0 -32
  35. package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +0 -29
  36. package/src/elements/EFMedia/audioTasks/makeAudioTasksVideoOnly.browsertest.ts +0 -95
  37. package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +0 -184
  38. package/src/elements/EFMedia/shared/AudioSpanUtils.ts +0 -129
  39. package/src/elements/EFMedia/shared/BufferUtils.ts +0 -342
  40. package/src/elements/EFMedia/shared/GlobalInputCache.ts +0 -77
  41. package/src/elements/EFMedia/shared/MediaTaskUtils.ts +0 -44
  42. package/src/elements/EFMedia/shared/PrecisionUtils.ts +0 -46
  43. package/src/elements/EFMedia/shared/RenditionHelpers.browsertest.ts +0 -246
  44. package/src/elements/EFMedia/shared/RenditionHelpers.ts +0 -56
  45. package/src/elements/EFMedia/shared/ThumbnailExtractor.ts +0 -227
  46. package/src/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.ts +0 -167
  47. package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +0 -88
  48. package/src/elements/EFMedia/videoTasks/MainVideoInputCache.ts +0 -76
  49. package/src/elements/EFMedia/videoTasks/ScrubInputCache.ts +0 -61
  50. package/src/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.ts +0 -114
  51. package/src/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.ts +0 -35
  52. package/src/elements/EFMedia/videoTasks/makeScrubVideoInputTask.ts +0 -52
  53. package/src/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.ts +0 -124
  54. package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.ts +0 -44
  55. package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.ts +0 -32
  56. package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +0 -370
  57. package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +0 -109
  58. package/src/elements/EFMedia.browsertest.ts +0 -872
  59. package/src/elements/EFMedia.ts +0 -341
  60. package/src/elements/EFSourceMixin.ts +0 -60
  61. package/src/elements/EFSurface.browsertest.ts +0 -151
  62. package/src/elements/EFSurface.ts +0 -142
  63. package/src/elements/EFTemporal.browsertest.ts +0 -215
  64. package/src/elements/EFTemporal.ts +0 -800
  65. package/src/elements/EFThumbnailStrip.browsertest.ts +0 -585
  66. package/src/elements/EFThumbnailStrip.media-engine.browsertest.ts +0 -714
  67. package/src/elements/EFThumbnailStrip.ts +0 -906
  68. package/src/elements/EFTimegroup.browsertest.ts +0 -870
  69. package/src/elements/EFTimegroup.ts +0 -878
  70. package/src/elements/EFVideo.browsertest.ts +0 -1482
  71. package/src/elements/EFVideo.ts +0 -564
  72. package/src/elements/EFWaveform.ts +0 -547
  73. package/src/elements/FetchContext.browsertest.ts +0 -401
  74. package/src/elements/FetchMixin.ts +0 -38
  75. package/src/elements/SampleBuffer.ts +0 -94
  76. package/src/elements/TargetController.browsertest.ts +0 -230
  77. package/src/elements/TargetController.ts +0 -224
  78. package/src/elements/TimegroupController.ts +0 -26
  79. package/src/elements/durationConverter.ts +0 -35
  80. package/src/elements/parseTimeToMs.ts +0 -9
  81. package/src/elements/printTaskStatus.ts +0 -16
  82. package/src/elements/renderTemporalAudio.ts +0 -108
  83. package/src/elements/updateAnimations.browsertest.ts +0 -1884
  84. package/src/elements/updateAnimations.ts +0 -217
  85. package/src/elements/util.ts +0 -24
  86. package/src/gui/ContextMixin.browsertest.ts +0 -860
  87. package/src/gui/ContextMixin.ts +0 -562
  88. package/src/gui/Controllable.browsertest.ts +0 -258
  89. package/src/gui/Controllable.ts +0 -41
  90. package/src/gui/EFConfiguration.ts +0 -40
  91. package/src/gui/EFControls.browsertest.ts +0 -389
  92. package/src/gui/EFControls.ts +0 -195
  93. package/src/gui/EFDial.browsertest.ts +0 -84
  94. package/src/gui/EFDial.ts +0 -172
  95. package/src/gui/EFFilmstrip.browsertest.ts +0 -712
  96. package/src/gui/EFFilmstrip.ts +0 -1349
  97. package/src/gui/EFFitScale.ts +0 -152
  98. package/src/gui/EFFocusOverlay.ts +0 -79
  99. package/src/gui/EFPause.browsertest.ts +0 -202
  100. package/src/gui/EFPause.ts +0 -73
  101. package/src/gui/EFPlay.browsertest.ts +0 -202
  102. package/src/gui/EFPlay.ts +0 -73
  103. package/src/gui/EFPreview.ts +0 -74
  104. package/src/gui/EFResizableBox.browsertest.ts +0 -79
  105. package/src/gui/EFResizableBox.ts +0 -898
  106. package/src/gui/EFScrubber.ts +0 -151
  107. package/src/gui/EFTimeDisplay.browsertest.ts +0 -237
  108. package/src/gui/EFTimeDisplay.ts +0 -55
  109. package/src/gui/EFToggleLoop.ts +0 -35
  110. package/src/gui/EFTogglePlay.ts +0 -70
  111. package/src/gui/EFWorkbench.ts +0 -115
  112. package/src/gui/PlaybackController.ts +0 -527
  113. package/src/gui/TWMixin.css +0 -6
  114. package/src/gui/TWMixin.ts +0 -61
  115. package/src/gui/TargetOrContextMixin.ts +0 -185
  116. package/src/gui/currentTimeContext.ts +0 -5
  117. package/src/gui/durationContext.ts +0 -3
  118. package/src/gui/efContext.ts +0 -6
  119. package/src/gui/fetchContext.ts +0 -5
  120. package/src/gui/focusContext.ts +0 -7
  121. package/src/gui/focusedElementContext.ts +0 -5
  122. package/src/gui/playingContext.ts +0 -5
  123. package/src/otel/BridgeSpanExporter.ts +0 -150
  124. package/src/otel/setupBrowserTracing.ts +0 -73
  125. package/src/otel/tracingHelpers.ts +0 -251
  126. package/src/transcoding/cache/RequestDeduplicator.test.ts +0 -170
  127. package/src/transcoding/cache/RequestDeduplicator.ts +0 -65
  128. package/src/transcoding/cache/URLTokenDeduplicator.test.ts +0 -182
  129. package/src/transcoding/cache/URLTokenDeduplicator.ts +0 -101
  130. package/src/transcoding/types/index.ts +0 -312
  131. package/src/transcoding/utils/MediaUtils.ts +0 -63
  132. package/src/transcoding/utils/UrlGenerator.ts +0 -68
  133. package/src/transcoding/utils/constants.ts +0 -36
  134. package/src/utils/LRUCache.test.ts +0 -274
  135. 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
- });