@editframe/elements 0.19.4-beta.0 → 0.20.0-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 (96) hide show
  1. package/dist/elements/ContextProxiesController.d.ts +40 -0
  2. package/dist/elements/ContextProxiesController.js +69 -0
  3. package/dist/elements/EFCaptions.d.ts +45 -6
  4. package/dist/elements/EFCaptions.js +220 -26
  5. package/dist/elements/EFImage.js +4 -1
  6. package/dist/elements/EFMedia/AssetIdMediaEngine.d.ts +2 -1
  7. package/dist/elements/EFMedia/AssetIdMediaEngine.js +9 -0
  8. package/dist/elements/EFMedia/AssetMediaEngine.d.ts +1 -0
  9. package/dist/elements/EFMedia/AssetMediaEngine.js +11 -0
  10. package/dist/elements/EFMedia/BaseMediaEngine.d.ts +13 -1
  11. package/dist/elements/EFMedia/BaseMediaEngine.js +9 -0
  12. package/dist/elements/EFMedia/JitMediaEngine.d.ts +7 -1
  13. package/dist/elements/EFMedia/JitMediaEngine.js +24 -0
  14. package/dist/elements/EFMedia/shared/GlobalInputCache.d.ts +39 -0
  15. package/dist/elements/EFMedia/shared/GlobalInputCache.js +57 -0
  16. package/dist/elements/EFMedia/shared/ThumbnailExtractor.d.ts +27 -0
  17. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +106 -0
  18. package/dist/elements/EFMedia.js +25 -1
  19. package/dist/elements/EFSurface.browsertest.d.ts +0 -0
  20. package/dist/elements/EFSurface.d.ts +30 -0
  21. package/dist/elements/EFSurface.js +96 -0
  22. package/dist/elements/EFTemporal.js +7 -6
  23. package/dist/elements/EFThumbnailStrip.browsertest.d.ts +0 -0
  24. package/dist/elements/EFThumbnailStrip.d.ts +86 -0
  25. package/dist/elements/EFThumbnailStrip.js +490 -0
  26. package/dist/elements/EFThumbnailStrip.media-engine.browsertest.d.ts +0 -0
  27. package/dist/elements/EFTimegroup.d.ts +6 -1
  28. package/dist/elements/EFTimegroup.js +46 -10
  29. package/dist/elements/updateAnimations.browsertest.d.ts +13 -0
  30. package/dist/elements/updateAnimations.d.ts +5 -0
  31. package/dist/elements/updateAnimations.js +37 -13
  32. package/dist/getRenderInfo.js +1 -1
  33. package/dist/gui/ContextMixin.js +27 -14
  34. package/dist/gui/EFControls.browsertest.d.ts +0 -0
  35. package/dist/gui/EFControls.d.ts +38 -0
  36. package/dist/gui/EFControls.js +51 -0
  37. package/dist/gui/EFFilmstrip.d.ts +40 -1
  38. package/dist/gui/EFFilmstrip.js +240 -3
  39. package/dist/gui/EFPreview.js +2 -1
  40. package/dist/gui/EFScrubber.d.ts +6 -5
  41. package/dist/gui/EFScrubber.js +31 -21
  42. package/dist/gui/EFTimeDisplay.browsertest.d.ts +0 -0
  43. package/dist/gui/EFTimeDisplay.d.ts +2 -6
  44. package/dist/gui/EFTimeDisplay.js +13 -23
  45. package/dist/gui/TWMixin.js +1 -1
  46. package/dist/gui/currentTimeContext.d.ts +3 -0
  47. package/dist/gui/currentTimeContext.js +3 -0
  48. package/dist/gui/durationContext.d.ts +3 -0
  49. package/dist/gui/durationContext.js +3 -0
  50. package/dist/index.d.ts +3 -0
  51. package/dist/index.js +4 -1
  52. package/dist/style.css +1 -1
  53. package/dist/transcoding/types/index.d.ts +11 -0
  54. package/dist/utils/LRUCache.d.ts +46 -0
  55. package/dist/utils/LRUCache.js +382 -1
  56. package/dist/utils/LRUCache.test.d.ts +1 -0
  57. package/package.json +2 -2
  58. package/src/elements/ContextProxiesController.ts +123 -0
  59. package/src/elements/EFCaptions.browsertest.ts +1820 -0
  60. package/src/elements/EFCaptions.ts +373 -36
  61. package/src/elements/EFImage.ts +4 -1
  62. package/src/elements/EFMedia/AssetIdMediaEngine.ts +30 -1
  63. package/src/elements/EFMedia/AssetMediaEngine.ts +33 -0
  64. package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +3 -8
  65. package/src/elements/EFMedia/BaseMediaEngine.ts +35 -0
  66. package/src/elements/EFMedia/JitMediaEngine.ts +48 -0
  67. package/src/elements/EFMedia/shared/GlobalInputCache.ts +77 -0
  68. package/src/elements/EFMedia/shared/ThumbnailExtractor.ts +227 -0
  69. package/src/elements/EFMedia.ts +38 -1
  70. package/src/elements/EFSurface.browsertest.ts +155 -0
  71. package/src/elements/EFSurface.ts +141 -0
  72. package/src/elements/EFTemporal.ts +14 -8
  73. package/src/elements/EFThumbnailStrip.browsertest.ts +591 -0
  74. package/src/elements/EFThumbnailStrip.media-engine.browsertest.ts +713 -0
  75. package/src/elements/EFThumbnailStrip.ts +905 -0
  76. package/src/elements/EFTimegroup.browsertest.ts +56 -7
  77. package/src/elements/EFTimegroup.ts +70 -11
  78. package/src/elements/updateAnimations.browsertest.ts +333 -11
  79. package/src/elements/updateAnimations.ts +68 -19
  80. package/src/gui/ContextMixin.browsertest.ts +0 -25
  81. package/src/gui/ContextMixin.ts +44 -20
  82. package/src/gui/EFControls.browsertest.ts +175 -0
  83. package/src/gui/EFControls.ts +84 -0
  84. package/src/gui/EFFilmstrip.ts +323 -4
  85. package/src/gui/EFPreview.ts +2 -1
  86. package/src/gui/EFScrubber.ts +29 -25
  87. package/src/gui/EFTimeDisplay.browsertest.ts +237 -0
  88. package/src/gui/EFTimeDisplay.ts +12 -40
  89. package/src/gui/currentTimeContext.ts +5 -0
  90. package/src/gui/durationContext.ts +3 -0
  91. package/src/transcoding/types/index.ts +13 -0
  92. package/src/utils/LRUCache.test.ts +272 -0
  93. package/src/utils/LRUCache.ts +543 -0
  94. package/types.json +1 -1
  95. package/dist/transcoding/cache/CacheManager.d.ts +0 -73
  96. package/src/transcoding/cache/CacheManager.ts +0 -208
@@ -420,13 +420,57 @@ describe("setting currentTime", () => {
420
420
  await timegroup.waitForMediaDurations();
421
421
 
422
422
  timegroup.currentTime = 5_000; // 5000 seconds, should clamp to 10s
423
- await timegroup.seekTask.taskComplete;
423
+ await timegroup.seekTask.run();
424
424
 
425
425
  const storedValue = localStorage.getItem(timegroup.storageKey);
426
426
  assert.equal(storedValue, "10"); // Should store 10 (clamped from 5000 to duration)
427
427
  timegroup.remove();
428
428
  });
429
429
 
430
+ test("root timegroup remains visible when currentTime equals duration exactly", async () => {
431
+ const timegroup = renderTimegroup(
432
+ html`<ef-timegroup id="end-time-test" mode="fixed" duration="10s"></ef-timegroup>`,
433
+ );
434
+ await timegroup.waitForMediaDurations();
435
+
436
+ // Set currentTime to exactly the duration
437
+ timegroup.currentTime = 10; // 10 seconds
438
+ await timegroup.seekTask.taskComplete;
439
+ await timegroup.frameTask.taskComplete;
440
+
441
+ // The root timegroup should still be visible at the exact end time
442
+ assert.notEqual(
443
+ timegroup.style.display,
444
+ "none",
445
+ "Root timegroup should be visible at exact end time",
446
+ );
447
+ });
448
+
449
+ test("root timegroup becomes hidden only after currentTime exceeds duration", async () => {
450
+ const timegroup = renderTimegroup(
451
+ html`<ef-timegroup id="beyond-end-test" mode="fixed" duration="10s"></ef-timegroup>`,
452
+ );
453
+ await timegroup.waitForMediaDurations();
454
+
455
+ // Set currentTime beyond the duration (should be clamped to duration)
456
+ timegroup.currentTime = 15; // 15 seconds, should clamp to 10s
457
+ await timegroup.seekTask.taskComplete;
458
+ await timegroup.frameTask.taskComplete;
459
+
460
+ // Even when clamped, it should still be visible at the end
461
+ assert.notEqual(
462
+ timegroup.style.display,
463
+ "none",
464
+ "Root timegroup should be visible even when time is clamped to duration",
465
+ );
466
+ // Verify that the time was actually clamped
467
+ assert.equal(
468
+ timegroup.currentTime,
469
+ 10,
470
+ "Time should be clamped to duration",
471
+ );
472
+ });
473
+
430
474
  test("does not persist in localStorage if the timegroup has no id", async () => {
431
475
  const timegroup = renderTimegroup(
432
476
  html`<ef-timegroup mode="fixed" duration="10s"></ef-timegroup>`,
@@ -572,19 +616,23 @@ describe("Dynamic content updates", () => {
572
616
  const frameTaskB = timegroup.querySelector("test-frame-task-b")!;
573
617
  const frameTaskC = timegroup.querySelector("test-frame-task-c")!;
574
618
 
575
- assert.equal(frameTaskA.frameTaskCount, 0);
619
+ // following the initial update, the first frame tasks have run once.
620
+ await timegroup.updateComplete;
621
+
622
+ assert.equal(frameTaskA.frameTaskCount, 1);
576
623
  assert.equal(frameTaskB.frameTaskCount, 0);
577
- assert.equal(frameTaskC.frameTaskCount, 0);
624
+ assert.equal(frameTaskC.frameTaskCount, 1);
578
625
 
626
+ // Then we run them manually.
579
627
  await timegroup.frameTask.run();
580
628
 
581
629
  // At timeline time 0ms:
582
630
  // - frameTaskA (0-1000ms) should run
583
631
  // - frameTaskB (1000-2000ms) should NOT run
584
632
  // - frameTaskC (0-1000ms) should run (inherits root positioning)
585
- assert.equal(frameTaskA.frameTaskCount, 1);
633
+ assert.equal(frameTaskA.frameTaskCount, 2);
586
634
  assert.equal(frameTaskB.frameTaskCount, 0); // Not visible at time 0
587
- assert.equal(frameTaskC.frameTaskCount, 1); // Nested in B but inherits root positioning
635
+ assert.equal(frameTaskC.frameTaskCount, 2); // Nested in B but inherits root positioning
588
636
  });
589
637
  });
590
638
 
@@ -599,9 +647,10 @@ describe("Dynamic content updates", () => {
599
647
  );
600
648
  const nonRootTimegroup = timegroup.querySelector("ef-timegroup")!;
601
649
  const frameTaskA = timegroup.querySelector("test-frame-task-a")!;
602
- assert.equal(frameTaskA.frameTaskCount, 0);
650
+ await timegroup.updateComplete;
651
+ assert.equal(frameTaskA.frameTaskCount, 1);
603
652
  await nonRootTimegroup.seekTask.run();
604
- assert.equal(frameTaskA.frameTaskCount, 0);
653
+ assert.equal(frameTaskA.frameTaskCount, 1);
605
654
  });
606
655
 
607
656
  test("waits for media durations", async () => {
@@ -1,7 +1,7 @@
1
1
  import { provide } from "@lit/context";
2
2
  import { Task, TaskStatus } from "@lit/task";
3
3
  import debug from "debug";
4
- import { css, html, LitElement } from "lit";
4
+ import { css, html, LitElement, type PropertyValues } from "lit";
5
5
  import { customElement, property } from "lit/decorators.js";
6
6
 
7
7
  import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
@@ -17,7 +17,10 @@ import {
17
17
  timegroupContext,
18
18
  } from "./EFTemporal.js";
19
19
  import { TimegroupController } from "./TimegroupController.js";
20
- import { evaluateTemporalState, updateAnimations } from "./updateAnimations.ts";
20
+ import {
21
+ evaluateTemporalStateForAnimation,
22
+ updateAnimations,
23
+ } from "./updateAnimations.ts";
21
24
 
22
25
  const log = debug("ef:elements:EFTimegroup");
23
26
 
@@ -104,6 +107,46 @@ export class EFTimegroup extends EFTemporal(LitElement) {
104
107
 
105
108
  #processingPendingSeek = false;
106
109
 
110
+ #frameTaskInProgress = false;
111
+
112
+ #pendingFrameTaskRun = false;
113
+
114
+ #processingPendingFrameTask = false;
115
+
116
+ /**
117
+ * Throttles frameTask execution to ensure only one runs at a time while preserving the last request
118
+ */
119
+ private async runThrottledFrameTask(): Promise<void> {
120
+ if (this.#frameTaskInProgress) {
121
+ this.#pendingFrameTaskRun = true;
122
+ // Wait for the current frame task to complete
123
+ while (this.#frameTaskInProgress) {
124
+ await this.frameTask.taskComplete;
125
+ }
126
+ return;
127
+ }
128
+
129
+ this.#frameTaskInProgress = true;
130
+
131
+ try {
132
+ await this.frameTask.run();
133
+ } finally {
134
+ this.#frameTaskInProgress = false;
135
+
136
+ if (this.#pendingFrameTaskRun && !this.#processingPendingFrameTask) {
137
+ this.#pendingFrameTaskRun = false;
138
+ this.#processingPendingFrameTask = true;
139
+ try {
140
+ await this.runThrottledFrameTask();
141
+ } finally {
142
+ this.#processingPendingFrameTask = false;
143
+ }
144
+ } else {
145
+ this.#pendingFrameTaskRun = false;
146
+ }
147
+ }
148
+ }
149
+
107
150
  @property({ type: Number, attribute: "currenttime" })
108
151
  set currentTime(time: number) {
109
152
  time = Math.max(0, time);
@@ -127,7 +170,6 @@ export class EFTimegroup extends EFTemporal(LitElement) {
127
170
  }
128
171
 
129
172
  this.#currentTime = time;
130
- // This will be set to false in the seekTask
131
173
  this.#seekInProgress = true;
132
174
 
133
175
  this.seekTask.run().finally(() => {
@@ -232,6 +274,15 @@ export class EFTimegroup extends EFTemporal(LitElement) {
232
274
  }
233
275
  }
234
276
 
277
+ #previousDurationMs = 0;
278
+
279
+ protected updated(_changedProperties: PropertyValues): void {
280
+ if (this.#previousDurationMs !== this.durationMs) {
281
+ this.#previousDurationMs = this.durationMs;
282
+ this.runThrottledFrameTask();
283
+ }
284
+ }
285
+
235
286
  disconnectedCallback() {
236
287
  super.disconnectedCallback();
237
288
  this.#resizeObserver?.disconnect();
@@ -333,7 +384,14 @@ export class EFTimegroup extends EFTemporal(LitElement) {
333
384
  const startTimeMs = (temporal as any).startTimeMs as number;
334
385
  const endTimeMs = (temporal as any).endTimeMs as number;
335
386
  const elementStartsBeforeEnd = startTimeMs <= timelineTimeMs + epsilon;
336
- const elementEndsAfterStart = endTimeMs > timelineTimeMs; // Exclusive end for clean transitions
387
+ // Root timegroups should remain visible at exact end time, but other elements use exclusive end for clean transitions
388
+ const isRootTimegroup =
389
+ temporal.tagName.toLowerCase() === "ef-timegroup" &&
390
+ !(temporal as any).parentTimegroup;
391
+ const useInclusiveEnd = isRootTimegroup;
392
+ const elementEndsAfterStart = useInclusiveEnd
393
+ ? endTimeMs >= timelineTimeMs
394
+ : endTimeMs > timelineTimeMs;
337
395
  return elementStartsBeforeEnd && elementEndsAfterStart;
338
396
  });
339
397
 
@@ -366,15 +424,14 @@ export class EFTimegroup extends EFTemporal(LitElement) {
366
424
  const temporalElements = deepGetElementsWithFrameTasks(this);
367
425
 
368
426
  // Filter to only include temporally visible elements for frame processing
427
+ // Use animation-friendly visibility to prevent animation jumps at exact boundaries
369
428
  const visibleElements = temporalElements.filter((element) => {
370
- const temporalState = evaluateTemporalState(element);
371
- return temporalState.isVisible;
429
+ const animationState = evaluateTemporalStateForAnimation(element);
430
+ return animationState.isVisible;
372
431
  });
373
432
 
374
433
  await Promise.all(
375
- visibleElements.map((element) => {
376
- return element.frameTask.run();
377
- }),
434
+ visibleElements.map((element) => element.frameTask.run()),
378
435
  );
379
436
  }
380
437
 
@@ -679,9 +736,11 @@ export class EFTimegroup extends EFTemporal(LitElement) {
679
736
  0,
680
737
  Math.min(targetTime ?? 0, this.durationMs / 1000),
681
738
  );
739
+ // Apply the clamped time back to currentTime
740
+ this.#currentTime = newTime;
682
741
  this.requestUpdate("currentTime");
683
- await this.frameTask.run();
684
- this.#saveTimeToLocalStorage(newTime);
742
+ await this.runThrottledFrameTask();
743
+ this.#saveTimeToLocalStorage(this.#currentTime);
685
744
  // This has to be set false here so any following seeks are not treated as pending
686
745
  this.#seekInProgress = false;
687
746
  return newTime;
@@ -1,4 +1,7 @@
1
+ import { LitElement } from "lit";
2
+ import { customElement } from "lit/decorators.js";
1
3
  import { assert, beforeEach, describe, test } from "vitest";
4
+ import { EFTemporal } from "./EFTemporal.js";
2
5
  import type { EFTimegroup } from "./EFTimegroup.js";
3
6
  import {
4
7
  type AnimatableElement,
@@ -7,17 +10,31 @@ import {
7
10
 
8
11
  import "./EFTimegroup.js";
9
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
+
10
32
  beforeEach(() => {
11
33
  // Clean up DOM
12
34
  while (document.body.children.length) {
13
35
  document.body.children[0]?.remove();
14
36
  }
15
- // Clean up localStorage
16
- for (let i = 0; i < localStorage.length; i++) {
17
- const key = localStorage.key(i);
18
- if (typeof key !== "string") continue;
19
- localStorage.removeItem(key);
20
- }
37
+ window.localStorage.clear();
21
38
  });
22
39
 
23
40
  function createTestElement(
@@ -49,6 +66,10 @@ function createTestElement(
49
66
  value: props.parentTimegroup,
50
67
  writable: true,
51
68
  });
69
+ Object.defineProperty(element, "ownCurrentTimeMs", {
70
+ value: props.ownCurrentTimeMs ?? 0,
71
+ writable: true,
72
+ });
52
73
  document.body.appendChild(element);
53
74
  return element;
54
75
  }
@@ -212,6 +233,75 @@ describe("Timeline Element Synchronizer", () => {
212
233
  assert.equal(element.style.display, "");
213
234
  });
214
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
+ "none",
285
+ "Element should be hidden at exact end boundary due to exclusive 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
+
215
305
  test("uses element currentTimeMs when no rootTimegroup", () => {
216
306
  const element = createTestElement({
217
307
  currentTimeMs: 500,
@@ -297,8 +387,6 @@ describe("Timeline Element Synchronizer", () => {
297
387
  });
298
388
  animation.play();
299
389
 
300
- assert.equal(animation.playState, "running");
301
-
302
390
  updateAnimations(element);
303
391
 
304
392
  assert.equal(animation.playState, "paused");
@@ -365,9 +453,6 @@ describe("Timeline Element Synchronizer", () => {
365
453
  animation1.play();
366
454
  animation2.play();
367
455
 
368
- assert.equal(animation1.playState, "running");
369
- assert.equal(animation2.playState, "running");
370
-
371
456
  updateAnimations(element);
372
457
 
373
458
  // Both animations should be paused
@@ -444,6 +529,243 @@ describe("Timeline Element Synchronizer", () => {
444
529
  });
445
530
  });
446
531
 
532
+ describe("child element animation coordination", () => {
533
+ test("coordinates animations on non-temporal child elements", async () => {
534
+ // Create root timegroup
535
+ const rootTimegroup = document.createElement(
536
+ "ef-timegroup",
537
+ ) as EFTimegroup;
538
+ rootTimegroup.currentTimeMs = 150; // Timeline at 150ms
539
+ document.body.appendChild(rootTimegroup);
540
+
541
+ // Create parent temporal element
542
+ const parentElement = document.createElement(
543
+ "test-temporal-element",
544
+ ) as TestTemporalElement;
545
+ parentElement.setDuration(300); // 300ms duration
546
+ parentElement.setAttribute("offset", "100ms"); // Start at 100ms in root timeline
547
+ rootTimegroup.appendChild(parentElement);
548
+
549
+ // Create a regular NON-temporal HTML element inside the temporal element
550
+ const nonTemporalDiv = document.createElement("div");
551
+ parentElement.appendChild(nonTemporalDiv);
552
+
553
+ // Wait for elements to be connected and updated
554
+ await rootTimegroup.updateComplete;
555
+ await parentElement.updateComplete;
556
+
557
+ // Create animation on the NON-temporal child element
558
+ const nonTemporalAnimation = nonTemporalDiv.animate(
559
+ [{ opacity: 0 }, { opacity: 1 }],
560
+ {
561
+ duration: 1000,
562
+ },
563
+ );
564
+ nonTemporalAnimation.play();
565
+
566
+ // Call updateAnimations on root timegroup
567
+ updateAnimations(rootTimegroup);
568
+
569
+ // Parent should be visible at current timeline position (150ms is between 100ms-400ms)
570
+ assert.notEqual(
571
+ parentElement.style.display,
572
+ "none",
573
+ "Parent should be visible at current timeline time",
574
+ );
575
+
576
+ // FIXED: Non-temporal child animation should be paused and coordinated
577
+ assert.equal(
578
+ nonTemporalAnimation.playState,
579
+ "paused",
580
+ "Non-temporal child element animation should be paused and coordinated with timeline",
581
+ );
582
+ });
583
+
584
+ test("coordinates animations on deeply nested non-temporal elements", async () => {
585
+ // Create root timegroup
586
+ const rootTimegroup = document.createElement(
587
+ "ef-timegroup",
588
+ ) as EFTimegroup;
589
+ rootTimegroup.currentTimeMs = 150; // Timeline at 150ms
590
+ document.body.appendChild(rootTimegroup);
591
+
592
+ // Create parent temporal element
593
+ const parentElement = document.createElement(
594
+ "test-temporal-element",
595
+ ) as TestTemporalElement;
596
+ parentElement.setDuration(300); // 300ms duration
597
+ parentElement.setAttribute("offset", "100ms"); // Start at 100ms in root timeline
598
+ rootTimegroup.appendChild(parentElement);
599
+
600
+ // Create nested non-temporal structure: temporal > div > div > span
601
+ const outerDiv = document.createElement("div");
602
+ const innerDiv = document.createElement("div");
603
+ const span = document.createElement("span");
604
+
605
+ parentElement.appendChild(outerDiv);
606
+ outerDiv.appendChild(innerDiv);
607
+ innerDiv.appendChild(span);
608
+
609
+ // Wait for elements to be connected and updated
610
+ await rootTimegroup.updateComplete;
611
+ await parentElement.updateComplete;
612
+
613
+ // Create animations on different levels of nesting
614
+ const outerAnimation = outerDiv.animate(
615
+ [{ transform: "scale(1)" }, { transform: "scale(1.1)" }],
616
+ {
617
+ duration: 800,
618
+ },
619
+ );
620
+ const innerAnimation = innerDiv.animate(
621
+ [{ opacity: 0.5 }, { opacity: 1 }],
622
+ {
623
+ duration: 1200,
624
+ },
625
+ );
626
+ const spanAnimation = span.animate(
627
+ [{ color: "red" }, { color: "blue" }],
628
+ {
629
+ duration: 600,
630
+ },
631
+ );
632
+
633
+ outerAnimation.play();
634
+ innerAnimation.play();
635
+ spanAnimation.play();
636
+
637
+ // Call updateAnimations on root timegroup
638
+ updateAnimations(rootTimegroup);
639
+
640
+ // All nested non-temporal animations should be coordinated
641
+ assert.equal(
642
+ outerAnimation.playState,
643
+ "paused",
644
+ "Outer div animation should be coordinated",
645
+ );
646
+ assert.equal(
647
+ innerAnimation.playState,
648
+ "paused",
649
+ "Inner div animation should be coordinated",
650
+ );
651
+ assert.equal(
652
+ spanAnimation.playState,
653
+ "paused",
654
+ "Span animation should be coordinated",
655
+ );
656
+ });
657
+
658
+ test("coordinates animations on child temporal elements when they are visible", async () => {
659
+ // Create root timegroup
660
+ const rootTimegroup = document.createElement(
661
+ "ef-timegroup",
662
+ ) as EFTimegroup;
663
+ rootTimegroup.currentTimeMs = 150; // Timeline at 150ms
664
+ document.body.appendChild(rootTimegroup);
665
+
666
+ // Create parent element (timegroup acts as parent)
667
+ const parentTimegroup = document.createElement(
668
+ "ef-timegroup",
669
+ ) as EFTimegroup;
670
+ parentTimegroup.setAttribute("duration", "1000ms");
671
+ rootTimegroup.appendChild(parentTimegroup);
672
+
673
+ // Create child temporal element that WILL be visible at timeline time 150ms
674
+ const childElement = document.createElement(
675
+ "test-temporal-element",
676
+ ) as TestTemporalElement;
677
+ childElement.setDuration(300); // 300ms duration (from 100ms to 400ms in root timeline)
678
+ childElement.setAttribute("offset", "100ms"); // Start at 100ms in root timeline
679
+ parentTimegroup.appendChild(childElement);
680
+
681
+ // Wait for elements to be connected and updated
682
+ await rootTimegroup.updateComplete;
683
+ await parentTimegroup.updateComplete;
684
+ await childElement.updateComplete;
685
+
686
+ // Create animation on child element
687
+ const childAnimation = childElement.animate(
688
+ [{ opacity: 0 }, { opacity: 1 }],
689
+ {
690
+ duration: 1000,
691
+ },
692
+ );
693
+ childAnimation.play();
694
+
695
+ // Call updateAnimations on parent timegroup - this should coordinate child animations too
696
+ updateAnimations(parentTimegroup);
697
+
698
+ // Child should be visible at current timeline position (150ms is between 100ms-400ms)
699
+ assert.notEqual(
700
+ childElement.style.display,
701
+ "none",
702
+ "Child should be visible at current timeline time",
703
+ );
704
+
705
+ // FIXED: Child animation should be paused and coordinated
706
+ assert.equal(
707
+ childAnimation.playState,
708
+ "paused",
709
+ "Child element animation should be paused and coordinated with timeline",
710
+ );
711
+ });
712
+
713
+ test("does not coordinate animations on child temporal elements when they are not visible", async () => {
714
+ // Create root timegroup
715
+ const rootTimegroup = document.createElement(
716
+ "ef-timegroup",
717
+ ) as EFTimegroup;
718
+ rootTimegroup.currentTimeMs = 100; // Timeline at 100ms
719
+ document.body.appendChild(rootTimegroup);
720
+
721
+ // Create parent element (timegroup acts as parent)
722
+ const parentTimegroup = document.createElement(
723
+ "ef-timegroup",
724
+ ) as EFTimegroup;
725
+ parentTimegroup.setAttribute("duration", "1000ms");
726
+ rootTimegroup.appendChild(parentTimegroup);
727
+
728
+ // Create child temporal element that will NOT be visible at timeline time 100ms
729
+ const childElement = document.createElement(
730
+ "test-temporal-element",
731
+ ) as TestTemporalElement;
732
+ childElement.setDuration(200); // 200ms duration
733
+ childElement.setAttribute("offset", "500ms"); // Start at 500ms in root timeline (way after current time)
734
+ parentTimegroup.appendChild(childElement);
735
+
736
+ // Wait for elements to be connected and updated
737
+ await rootTimegroup.updateComplete;
738
+ await parentTimegroup.updateComplete;
739
+ await childElement.updateComplete;
740
+
741
+ // Create animation on child element
742
+ const childAnimation = childElement.animate(
743
+ [{ opacity: 0 }, { opacity: 1 }],
744
+ {
745
+ duration: 1000,
746
+ },
747
+ );
748
+ childAnimation.play();
749
+
750
+ // Call updateAnimations on parent timegroup
751
+ updateAnimations(parentTimegroup);
752
+
753
+ // Child should be hidden (display: none)
754
+ assert.equal(
755
+ childElement.style.display,
756
+ "none",
757
+ "Child should be hidden when not in visible time range",
758
+ );
759
+
760
+ // Child animation should still be running (not coordinated since child is not visible)
761
+ assert.equal(
762
+ childAnimation.playState,
763
+ "paused",
764
+ "Child animation should remain running when child element is not visible",
765
+ );
766
+ });
767
+ });
768
+
447
769
  describe("edge cases", () => {
448
770
  test("handles zero duration gracefully", () => {
449
771
  const element = createTestElement({