@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
@@ -1,9 +1,14 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "vitest";
2
2
  import "../gui/EFPreview.js";
3
3
  import "./EFCaptions.js";
4
+ import "./EFTimegroup.js";
4
5
  import "./EFVideo.js";
5
6
  import { v4 } from "uuid";
6
7
 
8
+ beforeEach(() => {
9
+ window.localStorage.clear();
10
+ });
11
+
7
12
  describe("EFCaptions", () => {
8
13
  describe("when rendering", () => {
9
14
  beforeEach(() => {
@@ -65,4 +70,1819 @@ describe("EFCaptions", () => {
65
70
  );
66
71
  });
67
72
  });
73
+
74
+ describe("custom captions data loading", () => {
75
+ test("loads captions from external JSON file (captions-src)", async () => {
76
+ const id = v4();
77
+ const target = document.createElement("ef-video");
78
+ target.setAttribute("id", id);
79
+ target.src = "bars-n-tone.mp4";
80
+ document.body.appendChild(target);
81
+
82
+ const captions = document.createElement("ef-captions");
83
+ captions.setAttribute("target", id);
84
+ captions.captionsSrc = "test-captions-simple.json";
85
+ document.body.appendChild(captions);
86
+
87
+ await captions.updateComplete;
88
+ // @ts-expect-error accessing private property for testing
89
+ const captionsTask = captions.customCaptionsDataTask;
90
+
91
+ await captionsTask.taskComplete;
92
+
93
+ expect(captionsTask.value).toBeTruthy();
94
+ expect(captionsTask.value?.segments).toHaveLength(3);
95
+ expect(captionsTask.value?.word_segments).toHaveLength(9);
96
+ expect(captionsTask.value?.segments[0]?.text).toBe("First test segment");
97
+ });
98
+
99
+ test("loads captions from script element (captions-script)", async () => {
100
+ const id = v4();
101
+ const scriptId = v4();
102
+
103
+ const target = document.createElement("ef-video");
104
+ target.setAttribute("id", id);
105
+ target.src = "bars-n-tone.mp4";
106
+ document.body.appendChild(target);
107
+
108
+ // Create script element with captions data
109
+ const script = document.createElement("script");
110
+ script.id = scriptId;
111
+ script.type = "application/json";
112
+ script.textContent = JSON.stringify({
113
+ segments: [{ start: 0, end: 2, text: "Script-based captions" }],
114
+ word_segments: [
115
+ { text: "Script-based", start: 0, end: 1 },
116
+ { text: " captions", start: 1, end: 2 },
117
+ ],
118
+ });
119
+ document.body.appendChild(script);
120
+
121
+ const captions = document.createElement("ef-captions");
122
+ captions.setAttribute("target", id);
123
+ captions.captionsScript = scriptId;
124
+ document.body.appendChild(captions);
125
+
126
+ await captions.updateComplete;
127
+
128
+ // @ts-expect-error accessing private property for testing
129
+ const captionsTask = captions.customCaptionsDataTask;
130
+ await captionsTask.taskComplete;
131
+
132
+ expect(captionsTask.value).toEqual({
133
+ segments: [{ start: 0, end: 2, text: "Script-based captions" }],
134
+ word_segments: [
135
+ { text: "Script-based", start: 0, end: 1 },
136
+ { text: " captions", start: 1, end: 2 },
137
+ ],
138
+ });
139
+ });
140
+
141
+ test("uses direct captionsData property", async () => {
142
+ const id = v4();
143
+ const target = document.createElement("ef-video");
144
+ target.setAttribute("id", id);
145
+ target.src = "bars-n-tone.mp4";
146
+ document.body.appendChild(target);
147
+
148
+ const captions = document.createElement("ef-captions");
149
+ captions.setAttribute("target", id);
150
+
151
+ const testData = {
152
+ segments: [{ start: 0, end: 2, text: "Direct property caption" }],
153
+ word_segments: [
154
+ { text: "Direct", start: 0, end: 0.6 },
155
+ { text: " property", start: 0.6, end: 1.3 },
156
+ { text: " caption", start: 1.3, end: 2 },
157
+ ],
158
+ };
159
+
160
+ captions.captionsData = testData;
161
+ document.body.appendChild(captions);
162
+
163
+ await captions.updateComplete;
164
+ await captions.unifiedCaptionsDataTask.taskComplete;
165
+
166
+ expect(captions.unifiedCaptionsDataTask.value).toEqual(testData);
167
+ });
168
+
169
+ test("prioritizes captionsData > captions-script > captions-src", async () => {
170
+ const id = v4();
171
+ const scriptId = v4();
172
+
173
+ const target = document.createElement("ef-video");
174
+ target.setAttribute("id", id);
175
+ target.src = "bars-n-tone.mp4";
176
+ document.body.appendChild(target);
177
+
178
+ // Create script element
179
+ const script = document.createElement("script");
180
+ script.id = scriptId;
181
+ script.type = "application/json";
182
+ script.textContent = JSON.stringify({
183
+ segments: [{ start: 0, end: 2, text: "Script caption" }],
184
+ word_segments: [
185
+ { text: "Script", start: 0, end: 1 },
186
+ { text: " caption", start: 1, end: 2 },
187
+ ],
188
+ });
189
+ document.body.appendChild(script);
190
+
191
+ const captions = document.createElement("ef-captions");
192
+ captions.setAttribute("target", id);
193
+ captions.captionsSrc = "test-captions-simple.json";
194
+ captions.captionsScript = scriptId;
195
+
196
+ const directData = {
197
+ segments: [{ start: 0, end: 2, text: "Direct property wins" }],
198
+ word_segments: [
199
+ { text: "Direct", start: 0, end: 1 },
200
+ { text: " property", start: 1, end: 1.5 },
201
+ { text: " wins", start: 1.5, end: 2 },
202
+ ],
203
+ };
204
+
205
+ captions.captionsData = directData;
206
+ document.body.appendChild(captions);
207
+
208
+ await captions.updateComplete;
209
+ await captions.unifiedCaptionsDataTask.taskComplete;
210
+
211
+ // Should use direct property data, not script or file
212
+ expect(captions.unifiedCaptionsDataTask.value).toEqual(directData);
213
+ expect(captions.unifiedCaptionsDataTask.value?.segments[0]?.text).toBe(
214
+ "Direct property wins",
215
+ );
216
+ });
217
+
218
+ test("handles fetch errors gracefully", async () => {
219
+ const id = v4();
220
+ const target = document.createElement("ef-video");
221
+ target.setAttribute("id", id);
222
+ target.src = "bars-n-tone.mp4";
223
+ document.body.appendChild(target);
224
+
225
+ const captions = document.createElement("ef-captions");
226
+ captions.setAttribute("target", id);
227
+ captions.captionsSrc = "nonexistent-file.json";
228
+ document.body.appendChild(captions);
229
+
230
+ await captions.updateComplete;
231
+ await captions.unifiedCaptionsDataTask.taskComplete;
232
+
233
+ // @ts-expect-error accessing private property for testing
234
+ const captionsTask = captions.customCaptionsDataTask;
235
+
236
+ expect(captionsTask.value ?? null).toBeNull();
237
+ });
238
+
239
+ test("handles invalid JSON in script gracefully", async () => {
240
+ const id = v4();
241
+ const scriptId = v4();
242
+
243
+ const target = document.createElement("ef-video");
244
+ target.setAttribute("id", id);
245
+ target.src = "bars-n-tone.mp4";
246
+ document.body.appendChild(target);
247
+
248
+ const script = document.createElement("script");
249
+ script.id = scriptId;
250
+ script.type = "application/json";
251
+ script.textContent = "invalid json {";
252
+ document.body.appendChild(script);
253
+
254
+ const captions = document.createElement("ef-captions");
255
+ captions.setAttribute("target", id);
256
+ captions.captionsScript = scriptId;
257
+ document.body.appendChild(captions);
258
+
259
+ await captions.updateComplete;
260
+
261
+ // @ts-expect-error accessing private property for testing
262
+ const captionsTask = captions.customCaptionsDataTask;
263
+ await captionsTask.taskComplete;
264
+
265
+ expect(captionsTask.value).toBeNull();
266
+ });
267
+ });
268
+
269
+ describe("text visibility and timing", () => {
270
+ test("displays correct segment text at different time points", async () => {
271
+ const id = v4();
272
+ const timegroup = document.createElement("ef-timegroup");
273
+ const target = document.createElement("ef-video");
274
+ target.setAttribute("id", id);
275
+ target.src = "bars-n-tone.mp4";
276
+ timegroup.appendChild(target);
277
+
278
+ const captions = document.createElement("ef-captions");
279
+ captions.setAttribute("target", id);
280
+ captions.captionsSrc = "test-captions-simple.json";
281
+
282
+ // Create segment container
283
+ const segmentContainer = document.createElement("ef-captions-segment");
284
+ captions.appendChild(segmentContainer);
285
+ timegroup.appendChild(captions);
286
+ document.body.appendChild(timegroup);
287
+
288
+ await captions.unifiedCaptionsDataTask.taskComplete;
289
+
290
+ // Test at t=0 (first segment)
291
+ timegroup.currentTimeMs = 0;
292
+ await timegroup.seekTask.taskComplete;
293
+ expect(segmentContainer.segmentText).toBe("First test segment");
294
+ expect(segmentContainer.segmentStartMs).toBe(0);
295
+ expect(segmentContainer.segmentEndMs).toBe(3000);
296
+
297
+ // Test at t=4000ms (second segment)
298
+ timegroup.currentTimeMs = 4000;
299
+ await timegroup.seekTask.taskComplete;
300
+ expect(segmentContainer.segmentText).toBe("Second test segment");
301
+ expect(segmentContainer.segmentStartMs).toBe(3000);
302
+ expect(segmentContainer.segmentEndMs).toBe(6000);
303
+
304
+ // Test at t=7500ms (third segment)
305
+ timegroup.currentTimeMs = 7500;
306
+ await timegroup.seekTask.taskComplete;
307
+
308
+ expect(segmentContainer.segmentText).toBe("Third test segment");
309
+ expect(segmentContainer.segmentStartMs).toBe(6000);
310
+ expect(segmentContainer.segmentEndMs).toBe(9000);
311
+ });
312
+
313
+ test("displays correct word text and timing", async () => {
314
+ const id = v4();
315
+ const timegroup = document.createElement("ef-timegroup");
316
+ const target = document.createElement("ef-video");
317
+ target.setAttribute("id", id);
318
+ target.src = "bars-n-tone.mp4";
319
+ timegroup.appendChild(target);
320
+
321
+ const captions = document.createElement("ef-captions");
322
+ captions.setAttribute("target", id);
323
+ captions.captionsSrc = "test-captions-simple.json";
324
+
325
+ const wordContainer = document.createElement("ef-captions-active-word");
326
+ captions.appendChild(wordContainer);
327
+ timegroup.appendChild(captions);
328
+ document.body.appendChild(timegroup);
329
+
330
+ // @ts-expect-error accessing private property for testing
331
+ const captionsTask = captions.customCaptionsDataTask;
332
+ await captionsTask.taskComplete;
333
+
334
+ // Test at t=0.3s (should be "First")
335
+ timegroup.currentTimeMs = 300;
336
+ await timegroup.seekTask.taskComplete;
337
+ expect(wordContainer.wordText).toBe("First");
338
+ expect(wordContainer.wordStartMs).toBe(0);
339
+ expect(wordContainer.wordEndMs).toBe(600);
340
+
341
+ // Test at t=0.9s (should be " test")
342
+ timegroup.currentTimeMs = 900;
343
+ await timegroup.seekTask.taskComplete;
344
+ expect(wordContainer.wordText).toBe(" test");
345
+ expect(wordContainer.wordStartMs).toBe(600);
346
+ expect(wordContainer.wordEndMs).toBe(1200);
347
+
348
+ // Test at t=1.8s (should be " segment")
349
+ timegroup.currentTimeMs = 1800;
350
+ await timegroup.seekTask.taskComplete;
351
+ expect(wordContainer.wordText).toBe(" segment");
352
+ expect(wordContainer.wordStartMs).toBe(1200);
353
+ expect(wordContainer.wordEndMs).toBe(3000);
354
+ });
355
+
356
+ test("displays context words correctly", async () => {
357
+ const id = v4();
358
+ const timegroup = document.createElement("ef-timegroup");
359
+ const target = document.createElement("ef-video");
360
+ target.setAttribute("id", id);
361
+ target.src = "bars-n-tone.mp4";
362
+ timegroup.appendChild(target);
363
+
364
+ const captions = document.createElement("ef-captions");
365
+ captions.setAttribute("target", id);
366
+ captions.captionsSrc = "test-captions-complex.json";
367
+
368
+ const beforeContainer = document.createElement(
369
+ "ef-captions-before-active-word",
370
+ );
371
+ const activeContainer = document.createElement("ef-captions-active-word");
372
+ const afterContainer = document.createElement(
373
+ "ef-captions-after-active-word",
374
+ );
375
+
376
+ captions.appendChild(beforeContainer);
377
+ captions.appendChild(activeContainer);
378
+ captions.appendChild(afterContainer);
379
+ timegroup.appendChild(captions);
380
+ document.body.appendChild(timegroup);
381
+
382
+ // @ts-expect-error accessing private property for testing
383
+ const captionsTask = captions.customCaptionsDataTask;
384
+ await captionsTask.taskComplete;
385
+
386
+ // Test at t=1.0s (active word: "longer", context should be available)
387
+ timegroup.currentTimeMs = 1000;
388
+ await timegroup.seekTask.taskComplete;
389
+
390
+ expect(activeContainer.wordText).toBe(" longer");
391
+ expect(beforeContainer.segmentText).toBe("This is a");
392
+ expect(afterContainer.segmentText).toBe(
393
+ "segment with multiple words for testing context",
394
+ );
395
+
396
+ // Verify timing properties - all context containers sync with active word
397
+ expect(beforeContainer.segmentStartMs).toBe(800); // active word start
398
+ expect(beforeContainer.segmentEndMs).toBe(1300); // active word end
399
+ expect(afterContainer.segmentStartMs).toBe(800); // active word start
400
+ expect(afterContainer.segmentEndMs).toBe(1300); // active word end
401
+ });
402
+
403
+ test("handles stop words correctly", async () => {
404
+ const id = v4();
405
+ const timegroup = document.createElement("ef-timegroup");
406
+ const target = document.createElement("ef-video");
407
+ target.setAttribute("id", id);
408
+ target.src = "bars-n-tone.mp4";
409
+ timegroup.appendChild(target);
410
+
411
+ const captions = document.createElement("ef-captions");
412
+ captions.setAttribute("target", id);
413
+
414
+ // Add data with stop words
415
+ captions.captionsData = {
416
+ segments: [{ start: 0, end: 2, text: "Hello, world!" }],
417
+ word_segments: [
418
+ { text: "Hello", start: 0, end: 0.5 },
419
+ { text: ",", start: 0.5, end: 0.6 },
420
+ { text: " world", start: 0.6, end: 1.2 },
421
+ { text: "!", start: 1.2, end: 1.5 },
422
+ ],
423
+ };
424
+
425
+ const wordContainer = document.createElement("ef-captions-active-word");
426
+ captions.appendChild(wordContainer);
427
+ timegroup.appendChild(captions);
428
+ document.body.appendChild(timegroup);
429
+
430
+ // @ts-expect-error accessing private property for testing
431
+ const captionsTask = captions.customCaptionsDataTask;
432
+ await captionsTask.taskComplete;
433
+
434
+ // Test punctuation is hidden
435
+ timegroup.currentTimeMs = 550; // comma time
436
+ await timegroup.seekTask.taskComplete;
437
+ await wordContainer.updateComplete;
438
+ expect(wordContainer.hidden).toBe(true);
439
+
440
+ timegroup.currentTimeMs = 1350; // exclamation time
441
+ await timegroup.seekTask.taskComplete;
442
+ await wordContainer.updateComplete;
443
+ expect(wordContainer.hidden).toBe(true);
444
+
445
+ // Test regular word is visible
446
+ timegroup.currentTimeMs = 250; // "Hello" time
447
+ await timegroup.seekTask.taskComplete;
448
+ await wordContainer.updateComplete;
449
+ expect(wordContainer.hidden).toBe(false);
450
+ expect(wordContainer.wordText).toBe("Hello");
451
+ });
452
+ });
453
+
454
+ describe("display modes", () => {
455
+ test("segment mode shows segment text", async () => {
456
+ const id = v4();
457
+ const timegroup = document.createElement("ef-timegroup");
458
+ const target = document.createElement("ef-video");
459
+ target.setAttribute("id", id);
460
+ target.src = "bars-n-tone.mp4";
461
+ timegroup.appendChild(target);
462
+
463
+ const captions = document.createElement("ef-captions");
464
+ captions.setAttribute("target", id);
465
+ captions.displayMode = "segment";
466
+ captions.captionsSrc = "test-captions-simple.json";
467
+
468
+ const segmentContainer = document.createElement("ef-captions-segment");
469
+ captions.appendChild(segmentContainer);
470
+ timegroup.appendChild(captions);
471
+ document.body.appendChild(timegroup);
472
+
473
+ // @ts-expect-error accessing private property for testing
474
+ const captionsTask = captions.customCaptionsDataTask;
475
+ await captionsTask.taskComplete;
476
+
477
+ timegroup.currentTimeMs = 1500;
478
+ await timegroup.seekTask.taskComplete;
479
+ expect(segmentContainer.segmentText).toBe("First test segment");
480
+ });
481
+
482
+ test("word mode shows active word", async () => {
483
+ const id = v4();
484
+ const timegroup = document.createElement("ef-timegroup");
485
+ const target = document.createElement("ef-video");
486
+ target.setAttribute("id", id);
487
+ target.src = "bars-n-tone.mp4";
488
+ timegroup.appendChild(target);
489
+
490
+ const captions = document.createElement("ef-captions");
491
+ captions.setAttribute("target", id);
492
+ captions.displayMode = "word";
493
+ captions.captionsSrc = "test-captions-simple.json";
494
+
495
+ const wordContainer = document.createElement("ef-captions-active-word");
496
+ captions.appendChild(wordContainer);
497
+ timegroup.appendChild(captions);
498
+ document.body.appendChild(timegroup);
499
+
500
+ // @ts-expect-error accessing private property for testing
501
+ const captionsTask = captions.customCaptionsDataTask;
502
+ await captionsTask.taskComplete;
503
+
504
+ timegroup.currentTimeMs = 900;
505
+ await timegroup.seekTask.taskComplete;
506
+ expect(wordContainer.wordText).toBe(" test");
507
+ });
508
+
509
+ test("context mode shows before/active/after words", async () => {
510
+ const id = v4();
511
+ const timegroup = document.createElement("ef-timegroup");
512
+ const target = document.createElement("ef-video");
513
+ target.setAttribute("id", id);
514
+ target.src = "bars-n-tone.mp4";
515
+ timegroup.appendChild(target);
516
+
517
+ const captions = document.createElement("ef-captions");
518
+ captions.setAttribute("target", id);
519
+ captions.displayMode = "context";
520
+ captions.captionsSrc = "test-captions-complex.json";
521
+
522
+ const beforeContainer = document.createElement(
523
+ "ef-captions-before-active-word",
524
+ );
525
+ const activeContainer = document.createElement("ef-captions-active-word");
526
+ const afterContainer = document.createElement(
527
+ "ef-captions-after-active-word",
528
+ );
529
+
530
+ captions.appendChild(beforeContainer);
531
+ captions.appendChild(activeContainer);
532
+ captions.appendChild(afterContainer);
533
+ timegroup.appendChild(captions);
534
+ document.body.appendChild(timegroup);
535
+
536
+ // @ts-expect-error accessing private property for testing
537
+ const captionsTask = captions.customCaptionsDataTask;
538
+ await captionsTask.taskComplete;
539
+
540
+ // Test middle of first segment
541
+ timegroup.currentTimeMs = 2400; // during "multiple"
542
+ await timegroup.seekTask.taskComplete;
543
+
544
+ expect(activeContainer.wordText).toBe(" multiple");
545
+ expect(beforeContainer.segmentText).toBeTruthy();
546
+ expect(afterContainer.segmentText).toBeTruthy();
547
+
548
+ // Verify all three components have content
549
+ expect(beforeContainer.segmentText.length).toBeGreaterThan(0);
550
+ expect(activeContainer.wordText.length).toBeGreaterThan(0);
551
+ expect(afterContainer.segmentText.length).toBeGreaterThan(0);
552
+ });
553
+ });
554
+
555
+ describe("animation properties validation", () => {
556
+ test("word containers have correct startTimeMs and durationMs", async () => {
557
+ const id = v4();
558
+ const timegroup = document.createElement("ef-timegroup");
559
+ const target = document.createElement("ef-video");
560
+ target.setAttribute("id", id);
561
+ target.src = "bars-n-tone.mp4";
562
+ timegroup.appendChild(target);
563
+
564
+ const captions = document.createElement("ef-captions");
565
+ captions.setAttribute("target", id);
566
+ captions.captionsSrc = "test-captions-simple.json";
567
+
568
+ const wordContainer = document.createElement("ef-captions-active-word");
569
+ captions.appendChild(wordContainer);
570
+ timegroup.appendChild(captions);
571
+ document.body.appendChild(timegroup);
572
+
573
+ // @ts-expect-error accessing private property for testing
574
+ const captionsTask = captions.customCaptionsDataTask;
575
+ await captionsTask.taskComplete;
576
+
577
+ // Test first word "First" (0-0.6s)
578
+ timegroup.currentTimeMs = 300;
579
+ await timegroup.seekTask.taskComplete;
580
+
581
+ expect(wordContainer.startTimeMs).toBe(0);
582
+ expect(wordContainer.durationMs).toBe(600);
583
+
584
+ // Test second word " test" (0.6-1.2s)
585
+ timegroup.currentTimeMs = 900;
586
+ await timegroup.seekTask.taskComplete;
587
+
588
+ expect(wordContainer.startTimeMs).toBe(600);
589
+ expect(wordContainer.durationMs).toBe(600);
590
+ });
591
+
592
+ test("segment containers have correct startTimeMs and durationMs", async () => {
593
+ const id = v4();
594
+ const timegroup = document.createElement("ef-timegroup");
595
+ const target = document.createElement("ef-video");
596
+ target.setAttribute("id", id);
597
+ target.src = "bars-n-tone.mp4";
598
+ timegroup.appendChild(target);
599
+
600
+ const captions = document.createElement("ef-captions");
601
+ captions.setAttribute("target", id);
602
+ captions.captionsSrc = "test-captions-simple.json";
603
+
604
+ const segmentContainer = document.createElement("ef-captions-segment");
605
+ captions.appendChild(segmentContainer);
606
+ timegroup.appendChild(captions);
607
+ document.body.appendChild(timegroup);
608
+
609
+ // @ts-expect-error accessing private property for testing
610
+ const captionsTask = captions.customCaptionsDataTask;
611
+ await captionsTask.taskComplete;
612
+
613
+ // Test first segment (0-3s)
614
+ timegroup.currentTimeMs = 1500;
615
+ await timegroup.seekTask.taskComplete;
616
+
617
+ expect(segmentContainer.startTimeMs).toBe(0);
618
+ expect(segmentContainer.durationMs).toBe(3000);
619
+
620
+ // Test second segment (3-6s)
621
+ timegroup.currentTimeMs = 4500;
622
+ await timegroup.seekTask.taskComplete;
623
+
624
+ expect(segmentContainer.startTimeMs).toBe(3000);
625
+ expect(segmentContainer.durationMs).toBe(3000);
626
+ });
627
+
628
+ test("context containers have correct timing boundaries", async () => {
629
+ const id = v4();
630
+ const timegroup = document.createElement("ef-timegroup");
631
+ const target = document.createElement("ef-video");
632
+ target.setAttribute("id", id);
633
+ target.src = "bars-n-tone.mp4";
634
+ timegroup.appendChild(target);
635
+
636
+ const captions = document.createElement("ef-captions");
637
+ captions.setAttribute("target", id);
638
+ captions.captionsSrc = "test-captions-complex.json";
639
+
640
+ const beforeContainer = document.createElement(
641
+ "ef-captions-before-active-word",
642
+ );
643
+ const activeContainer = document.createElement("ef-captions-active-word");
644
+ const afterContainer = document.createElement(
645
+ "ef-captions-after-active-word",
646
+ );
647
+
648
+ captions.appendChild(beforeContainer);
649
+ captions.appendChild(activeContainer);
650
+ captions.appendChild(afterContainer);
651
+ timegroup.appendChild(captions);
652
+ document.body.appendChild(timegroup);
653
+
654
+ // @ts-expect-error accessing private property for testing
655
+ const captionsTask = captions.customCaptionsDataTask;
656
+ await captionsTask.taskComplete;
657
+
658
+ // Test during "longer" word (0.8-1.3s) in first segment (0-4s)
659
+ timegroup.currentTimeMs = 1000;
660
+ await timegroup.seekTask.taskComplete;
661
+
662
+ // Active word timing
663
+ expect(activeContainer.startTimeMs).toBe(800);
664
+ expect(activeContainer.durationMs).toBe(500);
665
+
666
+ // Before and after context: synchronized with active word timing
667
+ expect(beforeContainer.startTimeMs).toBe(800);
668
+ expect(beforeContainer.durationMs).toBe(500); // 1300 - 800
669
+
670
+ // After context: same timing as active word
671
+ expect(afterContainer.startTimeMs).toBe(800);
672
+ expect(afterContainer.durationMs).toBe(500); // 1300 - 800
673
+ });
674
+
675
+ test("timing properties update correctly as time progresses", async () => {
676
+ const id = v4();
677
+ const timegroup = document.createElement("ef-timegroup");
678
+ const target = document.createElement("ef-video");
679
+ target.setAttribute("id", id);
680
+ target.src = "bars-n-tone.mp4";
681
+ timegroup.appendChild(target);
682
+
683
+ const captions = document.createElement("ef-captions");
684
+ captions.setAttribute("target", id);
685
+ captions.captionsSrc = "test-captions-simple.json";
686
+
687
+ const wordContainer = document.createElement("ef-captions-active-word");
688
+ captions.appendChild(wordContainer);
689
+ timegroup.appendChild(captions);
690
+ document.body.appendChild(timegroup);
691
+
692
+ // @ts-expect-error accessing private property for testing
693
+ const captionsTask = captions.customCaptionsDataTask;
694
+ await captionsTask.taskComplete;
695
+
696
+ // Track timing changes across different words
697
+ const timingSteps = [
698
+ { time: 300, expectedStart: 0, expectedDuration: 600 }, // "First"
699
+ { time: 900, expectedStart: 600, expectedDuration: 600 }, // " test"
700
+ { time: 2100, expectedStart: 1200, expectedDuration: 1800 }, // " segment"
701
+ { time: 3500, expectedStart: 3000, expectedDuration: 800 }, // "Second"
702
+ { time: 4100, expectedStart: 3800, expectedDuration: 600 }, // " test"
703
+ ];
704
+
705
+ for (const step of timingSteps) {
706
+ timegroup.currentTimeMs = step.time;
707
+ await timegroup.seekTask.taskComplete;
708
+
709
+ expect(wordContainer.startTimeMs).toBe(step.expectedStart);
710
+ expect(wordContainer.durationMs).toBe(step.expectedDuration);
711
+ }
712
+ });
713
+ });
714
+
715
+ describe("captions duration integration (EFMedia pattern)", () => {
716
+ test("calculates intrinsicDurationMs from captions data", async () => {
717
+ const captions = document.createElement("ef-captions");
718
+ captions.captionsData = {
719
+ segments: [
720
+ { start: 0, end: 2, text: "First segment" },
721
+ { start: 2, end: 5, text: "Second segment" },
722
+ ],
723
+ word_segments: [
724
+ { text: "First", start: 0, end: 1 },
725
+ { text: " segment", start: 1, end: 2 },
726
+ { text: "Second", start: 2, end: 3.5 },
727
+ { text: " segment", start: 3.5, end: 5 },
728
+ ],
729
+ };
730
+
731
+ document.body.appendChild(captions);
732
+ // @ts-expect-error accessing private property for testing
733
+ const captionsTask = captions.customCaptionsDataTask;
734
+ await captionsTask.taskComplete;
735
+
736
+ // Duration should be calculated from captions data (5 seconds = 5000ms)
737
+ expect(captions.intrinsicDurationMs).toBe(5000);
738
+ expect(captions.durationMs).toBe(5000);
739
+ expect(captions.hasOwnDuration).toBe(true);
740
+ });
741
+
742
+ test("handles empty captions data gracefully", async () => {
743
+ const captions = document.createElement("ef-captions");
744
+ captions.captionsData = {
745
+ segments: [],
746
+ word_segments: [],
747
+ };
748
+
749
+ document.body.appendChild(captions);
750
+ // @ts-expect-error accessing private property for testing
751
+ const captionsTask = captions.customCaptionsDataTask;
752
+ await captionsTask.taskComplete;
753
+
754
+ expect(captions.intrinsicDurationMs).toBe(0);
755
+ expect(captions.durationMs).toBe(0);
756
+ expect(captions.hasOwnDuration).toBe(true);
757
+ });
758
+
759
+ test("reports no own duration when no custom captions data", () => {
760
+ const captions = document.createElement("ef-captions");
761
+
762
+ expect(captions.hasCustomCaptionsData).toBe(false);
763
+ expect(captions.hasOwnDuration).toBe(false);
764
+ expect(captions.intrinsicDurationMs).toBeUndefined();
765
+ });
766
+
767
+ test("sequence timegroup includes captions duration", async () => {
768
+ const timegroup = document.createElement("ef-timegroup");
769
+ timegroup.mode = "sequence";
770
+
771
+ // Add captions with 3s duration
772
+ const captions = document.createElement("ef-captions");
773
+ captions.captionsData = {
774
+ segments: [{ start: 0, end: 3, text: "Caption test" }],
775
+ word_segments: [
776
+ { text: "Caption", start: 0, end: 1.5 },
777
+ { text: " test", start: 1.5, end: 3 },
778
+ ],
779
+ };
780
+ timegroup.appendChild(captions);
781
+
782
+ document.body.appendChild(timegroup);
783
+
784
+ // @ts-expect-error accessing private property for testing
785
+ const captionsTask = captions.customCaptionsDataTask;
786
+ await captionsTask.taskComplete;
787
+
788
+ // Captions should have proper duration properties
789
+ expect(captions.hasOwnDuration).toBe(true);
790
+ expect(captions.intrinsicDurationMs).toBe(3000);
791
+ expect(captions.durationMs).toBe(3000);
792
+
793
+ // Sequence timegroup should include captions duration
794
+ expect(timegroup.durationMs).toBe(3000);
795
+ });
796
+
797
+ test("contain timegroup uses max duration including captions", async () => {
798
+ const timegroup = document.createElement("ef-timegroup");
799
+ timegroup.mode = "contain";
800
+
801
+ // Add captions with 4s duration
802
+ const captions = document.createElement("ef-captions");
803
+ captions.captionsData = {
804
+ segments: [
805
+ { start: 0, end: 2, text: "First part" },
806
+ { start: 2, end: 4, text: "Second part" },
807
+ ],
808
+ word_segments: [
809
+ { text: "First", start: 0, end: 1 },
810
+ { text: " part", start: 1, end: 2 },
811
+ { text: "Second", start: 2, end: 3 },
812
+ { text: " part", start: 3, end: 4 },
813
+ ],
814
+ };
815
+ timegroup.appendChild(captions);
816
+
817
+ document.body.appendChild(timegroup);
818
+ // @ts-expect-error accessing private property for testing
819
+ const captionsTask = captions.customCaptionsDataTask;
820
+ await captionsTask.taskComplete;
821
+
822
+ // Captions should have proper duration properties
823
+ expect(captions.hasOwnDuration).toBe(true);
824
+ expect(captions.intrinsicDurationMs).toBe(4000);
825
+ expect(captions.durationMs).toBe(4000);
826
+
827
+ // Contain timegroup should use captions duration
828
+ expect(timegroup.durationMs).toBe(4000);
829
+ });
830
+
831
+ test("handles exact boundary timing correctly", async () => {
832
+ const timegroup = document.createElement("ef-timegroup");
833
+
834
+ const captions = document.createElement("ef-captions");
835
+ captions.captionsData = {
836
+ segments: [{ start: 0, end: 4, text: "Boundary timing test" }],
837
+ word_segments: [
838
+ { text: "Boundary", start: 0, end: 1.5 },
839
+ { text: " timing", start: 1.5, end: 2.6 },
840
+ { text: " test", start: 2.6, end: 4 },
841
+ ],
842
+ };
843
+
844
+ const wordContainer = document.createElement("ef-captions-active-word");
845
+ captions.appendChild(wordContainer);
846
+ timegroup.appendChild(captions);
847
+ document.body.appendChild(timegroup);
848
+
849
+ // @ts-expect-error accessing private property for testing
850
+ const captionsTask = captions.customCaptionsDataTask;
851
+ await captionsTask.taskComplete;
852
+
853
+ // Test at exact boundary 2.6s - should show " test" (the starting word)
854
+ timegroup.currentTimeMs = 2600;
855
+ await timegroup.seekTask.taskComplete;
856
+ await wordContainer.updateComplete;
857
+
858
+ console.log(`At 2600ms: wordText="${wordContainer.wordText}"`);
859
+ expect(wordContainer.wordText).toBe(" test");
860
+
861
+ // Test just before boundary 2.599s - should show " timing"
862
+ timegroup.currentTimeMs = 2599;
863
+ await timegroup.seekTask.taskComplete;
864
+ await wordContainer.updateComplete;
865
+
866
+ console.log(`At 2599ms: wordText="${wordContainer.wordText}"`);
867
+ expect(wordContainer.wordText).toBe(" timing");
868
+ });
869
+
870
+ test("handles demo captions data boundary correctly", async () => {
871
+ const timegroup = document.createElement("ef-timegroup");
872
+
873
+ const captions = document.createElement("ef-captions");
874
+ captions.captionsData = {
875
+ segments: [
876
+ { start: 0, end: 4, text: "Welcome to the custom captions demo!" },
877
+ ],
878
+ word_segments: [
879
+ { text: " captions", start: 1.8, end: 2.6 },
880
+ { text: " demo!", start: 2.6, end: 4 },
881
+ ],
882
+ };
883
+
884
+ const wordContainer = document.createElement("ef-captions-active-word");
885
+ captions.appendChild(wordContainer);
886
+ timegroup.appendChild(captions);
887
+ document.body.appendChild(timegroup);
888
+
889
+ // @ts-expect-error accessing private property for testing
890
+ const captionsTask = captions.customCaptionsDataTask;
891
+ await captionsTask.taskComplete;
892
+
893
+ // Test at exact boundary 2.6s from user's example
894
+ timegroup.currentTimeMs = 2600;
895
+ await timegroup.seekTask.taskComplete;
896
+ await wordContainer.updateComplete;
897
+
898
+ console.log(
899
+ `Demo case - At 2600ms: wordText="${wordContainer.wordText}", hidden=${wordContainer.hidden}`,
900
+ );
901
+ expect(wordContainer.wordText).toBe(" demo!");
902
+ expect(wordContainer.hidden).toBe(false);
903
+
904
+ // Test just before boundary
905
+ timegroup.currentTimeMs = 2590;
906
+ await timegroup.seekTask.taskComplete;
907
+ await wordContainer.updateComplete;
908
+
909
+ console.log(
910
+ `Demo case - At 2590ms: wordText="${wordContainer.wordText}", hidden=${wordContainer.hidden}`,
911
+ );
912
+ expect(wordContainer.wordText).toBe(" captions");
913
+ });
914
+
915
+ test("standalone captions sync with timegroup (demo structure)", async () => {
916
+ // Mimic exact demo structure: sequence > contain > captions
917
+ const rootTimegroup = document.createElement("ef-timegroup");
918
+ rootTimegroup.mode = "sequence";
919
+
920
+ const containTimegroup = document.createElement("ef-timegroup");
921
+ containTimegroup.mode = "contain";
922
+ rootTimegroup.appendChild(containTimegroup);
923
+
924
+ // Create script element like in demo
925
+ const scriptId = "test-demo-script";
926
+ const script = document.createElement("script");
927
+ script.id = scriptId;
928
+ script.type = "application/json";
929
+ script.textContent = JSON.stringify({
930
+ segments: [
931
+ { start: 0, end: 4, text: "Welcome to the custom captions demo!" },
932
+ ],
933
+ word_segments: [
934
+ { text: " captions", start: 1.8, end: 2.6 },
935
+ { text: " demo!", start: 2.6, end: 4 },
936
+ ],
937
+ });
938
+ document.body.appendChild(script);
939
+
940
+ // Standalone captions (no target) like in updated demo
941
+ const captions = document.createElement("ef-captions");
942
+ captions.captionsScript = scriptId;
943
+
944
+ const wordContainer = document.createElement("ef-captions-active-word");
945
+ captions.appendChild(wordContainer);
946
+ containTimegroup.appendChild(captions);
947
+ document.body.appendChild(rootTimegroup);
948
+
949
+ // @ts-expect-error accessing private property for testing
950
+ const captionsTask = captions.customCaptionsDataTask;
951
+ await captionsTask.taskComplete;
952
+
953
+ // Debug timeline sync
954
+ console.log(
955
+ `Initial: rootTimegroup.currentTimeMs=${rootTimegroup.currentTimeMs}, captions.ownCurrentTimeMs=${captions.ownCurrentTimeMs}`,
956
+ );
957
+
958
+ // Test the problematic timing
959
+ rootTimegroup.currentTimeMs = 2600;
960
+ await rootTimegroup.seekTask.taskComplete;
961
+ await captions.updateComplete;
962
+
963
+ console.log(
964
+ `After seek: rootTimegroup.currentTimeMs=${rootTimegroup.currentTimeMs}, captions.ownCurrentTimeMs=${captions.ownCurrentTimeMs}`,
965
+ );
966
+ console.log(
967
+ `Standalone demo - At 2600ms: wordText="${wordContainer.wordText}", hidden=${wordContainer.hidden}`,
968
+ );
969
+
970
+ expect(wordContainer.wordText).toBe(" demo!");
971
+ expect(wordContainer.hidden).toBe(false);
972
+ });
973
+
974
+ test("context words (before/after) work correctly", async () => {
975
+ const timegroup = document.createElement("ef-timegroup");
976
+
977
+ const captions = document.createElement("ef-captions");
978
+ captions.captionsData = {
979
+ segments: [
980
+ { start: 0, end: 4, text: "Welcome to the custom captions demo!" },
981
+ ],
982
+ word_segments: [
983
+ { text: "Welcome", start: 0, end: 0.6 },
984
+ { text: " to", start: 0.6, end: 0.9 },
985
+ { text: " the", start: 0.9, end: 1.2 },
986
+ { text: " custom", start: 1.2, end: 1.8 },
987
+ { text: " captions", start: 1.8, end: 2.6 },
988
+ { text: " demo!", start: 2.6, end: 4 },
989
+ ],
990
+ };
991
+
992
+ const beforeContainer = document.createElement(
993
+ "ef-captions-before-active-word",
994
+ );
995
+ const activeContainer = document.createElement("ef-captions-active-word");
996
+ const afterContainer = document.createElement(
997
+ "ef-captions-after-active-word",
998
+ );
999
+
1000
+ captions.appendChild(beforeContainer);
1001
+ captions.appendChild(activeContainer);
1002
+ captions.appendChild(afterContainer);
1003
+ timegroup.appendChild(captions);
1004
+ document.body.appendChild(timegroup);
1005
+
1006
+ // @ts-expect-error accessing private property for testing
1007
+ const captionsTask = captions.customCaptionsDataTask;
1008
+ await captionsTask.taskComplete;
1009
+
1010
+ // Test during " custom" word (1.2-1.8s) - should have before/after context
1011
+ timegroup.currentTimeMs = 1500;
1012
+ await timegroup.seekTask.taskComplete;
1013
+ await captions.updateComplete;
1014
+
1015
+ console.log("Context test - At 1500ms:");
1016
+ console.log(` activeWord: "${activeContainer.wordText}"`);
1017
+ console.log(` beforeWords: "${beforeContainer.segmentText}"`);
1018
+ console.log(` afterWords: "${afterContainer.segmentText}"`);
1019
+ console.log(
1020
+ ` beforeHidden: ${beforeContainer.hidden}, afterHidden: ${afterContainer.hidden}`,
1021
+ );
1022
+
1023
+ expect(activeContainer.wordText).toBe(" custom");
1024
+ expect(beforeContainer.segmentText).toBeTruthy();
1025
+ expect(afterContainer.segmentText).toBeTruthy();
1026
+ expect(beforeContainer.segmentText.length).toBeGreaterThan(0);
1027
+ expect(afterContainer.segmentText.length).toBeGreaterThan(0);
1028
+ });
1029
+
1030
+ test("debug context words with demo data structure", async () => {
1031
+ const timegroup = document.createElement("ef-timegroup");
1032
+
1033
+ // Create script element with EXACT demo data
1034
+ const scriptId = "demo-debug-script";
1035
+ const script = document.createElement("script");
1036
+ script.id = scriptId;
1037
+ script.type = "application/json";
1038
+ script.textContent = `{
1039
+ "segments": [
1040
+ { "start": 0, "end": 4, "text": "Welcome to the custom captions demo!" },
1041
+ { "start": 4, "end": 8, "text": "This demonstrates word-by-word highlighting." },
1042
+ { "start": 8, "end": 12, "text": "You can provide your own timing data." }
1043
+ ],
1044
+ "word_segments": [
1045
+ {"text": "Welcome", "start": 0, "end": 0.6},
1046
+ {"text": " to", "start": 0.6, "end": 0.9},
1047
+ {"text": " the", "start": 0.9, "end": 1.2},
1048
+ {"text": " custom", "start": 1.2, "end": 1.8},
1049
+ {"text": " captions", "start": 1.8, "end": 2.6},
1050
+ {"text": " demo!", "start": 2.6, "end": 4},
1051
+
1052
+ {"text": "This", "start": 4, "end": 4.3},
1053
+ {"text": " demonstrates", "start": 4.3, "end": 5.3},
1054
+ {"text": " word-by-word", "start": 5.3, "end": 6.3},
1055
+ {"text": " highlighting.", "start": 6.3, "end": 8}
1056
+ ]
1057
+ }`;
1058
+ document.body.appendChild(script);
1059
+
1060
+ const captions = document.createElement("ef-captions");
1061
+ captions.captionsScript = scriptId;
1062
+
1063
+ const beforeContainer = document.createElement(
1064
+ "ef-captions-before-active-word",
1065
+ );
1066
+ const activeContainer = document.createElement("ef-captions-active-word");
1067
+ const afterContainer = document.createElement(
1068
+ "ef-captions-after-active-word",
1069
+ );
1070
+
1071
+ captions.appendChild(beforeContainer);
1072
+ captions.appendChild(activeContainer);
1073
+ captions.appendChild(afterContainer);
1074
+ timegroup.appendChild(captions);
1075
+ document.body.appendChild(timegroup);
1076
+
1077
+ // @ts-expect-error accessing private property for testing
1078
+ const captionsTask = captions.customCaptionsDataTask;
1079
+ await captionsTask.taskComplete;
1080
+
1081
+ // Test during " custom" word (1.2-1.8s) - within first segment (0-4s)
1082
+ timegroup.currentTimeMs = 1500;
1083
+ await timegroup.seekTask.taskComplete;
1084
+ await captions.updateComplete;
1085
+
1086
+ console.log("Demo debug - At 1500ms:");
1087
+ console.log(
1088
+ ` Current segment found: ${!!captions.segmentContainers.length}`,
1089
+ );
1090
+ console.log(
1091
+ ` Current word found: ${activeContainer.wordText ? "yes" : "no"}`,
1092
+ );
1093
+ console.log(` activeWord: "${activeContainer.wordText}"`);
1094
+ console.log(` beforeWords: "${beforeContainer.segmentText}"`);
1095
+ console.log(` afterWords: "${afterContainer.segmentText}"`);
1096
+ console.log(
1097
+ ` beforeHidden: ${beforeContainer.hidden}, afterHidden: ${afterContainer.hidden}`,
1098
+ );
1099
+
1100
+ // Try different timing in second segment
1101
+ timegroup.currentTimeMs = 5000; // During "demonstrates" in second segment
1102
+ await timegroup.seekTask.taskComplete;
1103
+ await captions.updateComplete;
1104
+
1105
+ console.log("Demo debug - At 5000ms (second segment):");
1106
+ console.log(` activeWord: "${activeContainer.wordText}"`);
1107
+ console.log(` beforeWords: "${beforeContainer.segmentText}"`);
1108
+ console.log(` afterWords: "${afterContainer.segmentText}"`);
1109
+ console.log(
1110
+ ` beforeHidden: ${beforeContainer.hidden}, afterHidden: ${afterContainer.hidden}`,
1111
+ );
1112
+ });
1113
+
1114
+ test("context containers have synchronized timing with active word", async () => {
1115
+ const timegroup = document.createElement("ef-timegroup");
1116
+
1117
+ const captions = document.createElement("ef-captions");
1118
+ captions.captionsData = {
1119
+ segments: [
1120
+ {
1121
+ start: 0,
1122
+ end: 8,
1123
+ text: "This is a test segment with multiple words",
1124
+ },
1125
+ ],
1126
+ word_segments: [
1127
+ { text: "This", start: 0, end: 1 },
1128
+ { text: " is", start: 1, end: 2 },
1129
+ { text: " a", start: 2, end: 2.5 },
1130
+ { text: " test", start: 2.5, end: 3.5 },
1131
+ { text: " segment", start: 3.5, end: 4.5 },
1132
+ { text: " with", start: 4.5, end: 5 },
1133
+ { text: " multiple", start: 5, end: 6 },
1134
+ { text: " words", start: 6, end: 8 },
1135
+ ],
1136
+ };
1137
+
1138
+ const beforeContainer = document.createElement(
1139
+ "ef-captions-before-active-word",
1140
+ );
1141
+ const activeContainer = document.createElement("ef-captions-active-word");
1142
+ const afterContainer = document.createElement(
1143
+ "ef-captions-after-active-word",
1144
+ );
1145
+
1146
+ captions.appendChild(beforeContainer);
1147
+ captions.appendChild(activeContainer);
1148
+ captions.appendChild(afterContainer);
1149
+ timegroup.appendChild(captions);
1150
+ document.body.appendChild(timegroup);
1151
+
1152
+ // @ts-expect-error accessing private property for testing
1153
+ const captionsTask = captions.customCaptionsDataTask;
1154
+ await captionsTask.taskComplete;
1155
+
1156
+ // Test during " test" word (2.5-3.5s)
1157
+ timegroup.currentTimeMs = 3000;
1158
+ await timegroup.seekTask.taskComplete;
1159
+ await captions.updateComplete;
1160
+
1161
+ console.log("Timing sync test - At 3000ms:");
1162
+ console.log(
1163
+ ` Before: ${beforeContainer.segmentStartMs}-${beforeContainer.segmentEndMs}`,
1164
+ );
1165
+ console.log(
1166
+ ` Active: ${activeContainer.wordStartMs}-${activeContainer.wordEndMs}`,
1167
+ );
1168
+ console.log(
1169
+ ` After: ${afterContainer.segmentStartMs}-${afterContainer.segmentEndMs}`,
1170
+ );
1171
+
1172
+ // All three should have the same timing as the active word
1173
+ expect(beforeContainer.segmentStartMs).toBe(activeContainer.wordStartMs);
1174
+ expect(beforeContainer.segmentEndMs).toBe(activeContainer.wordEndMs);
1175
+ expect(afterContainer.segmentStartMs).toBe(activeContainer.wordStartMs);
1176
+ expect(afterContainer.segmentEndMs).toBe(activeContainer.wordEndMs);
1177
+
1178
+ // And they should all have the expected active word timing
1179
+ expect(activeContainer.wordStartMs).toBe(2500);
1180
+ expect(activeContainer.wordEndMs).toBe(3500);
1181
+ });
1182
+
1183
+ test("measures actual DOM widths for layout stability", async () => {
1184
+ const timegroup = document.createElement("ef-timegroup");
1185
+
1186
+ // Create exact demo structure
1187
+ const scriptId = "width-test-script";
1188
+ const script = document.createElement("script");
1189
+ script.id = scriptId;
1190
+ script.type = "application/json";
1191
+ script.textContent = JSON.stringify({
1192
+ segments: [
1193
+ { start: 0, end: 4, text: "Welcome to the custom captions demo!" },
1194
+ ],
1195
+ word_segments: [
1196
+ { text: "Welcome", start: 0, end: 0.6 },
1197
+ { text: " to", start: 0.6, end: 0.9 },
1198
+ { text: " the", start: 0.9, end: 1.2 },
1199
+ { text: " custom", start: 1.2, end: 1.8 },
1200
+ ],
1201
+ });
1202
+ document.body.appendChild(script);
1203
+
1204
+ const captions = document.createElement("ef-captions");
1205
+ captions.captionsScript = scriptId;
1206
+ captions.style.cssText =
1207
+ "display: block !important; text-align: center; background: green; padding: 16px;";
1208
+
1209
+ const beforeContainer = document.createElement(
1210
+ "ef-captions-before-active-word",
1211
+ );
1212
+ beforeContainer.className = "text-green-200";
1213
+
1214
+ const activeContainer = document.createElement("ef-captions-active-word");
1215
+ activeContainer.className =
1216
+ "bg-lime-400 text-green-900 rounded font-bold";
1217
+
1218
+ const afterContainer = document.createElement(
1219
+ "ef-captions-after-active-word",
1220
+ );
1221
+ afterContainer.className = "text-green-200";
1222
+
1223
+ captions.appendChild(beforeContainer);
1224
+ captions.appendChild(activeContainer);
1225
+ captions.appendChild(afterContainer);
1226
+ timegroup.appendChild(captions);
1227
+ document.body.appendChild(timegroup);
1228
+
1229
+ // @ts-expect-error accessing private property for testing
1230
+ const captionsTask = captions.customCaptionsDataTask;
1231
+ await captionsTask.taskComplete;
1232
+
1233
+ // Measure at different word timings and check for width/position changes
1234
+ const measurements: Array<{
1235
+ time: number;
1236
+ word: string;
1237
+ measurements: any;
1238
+ }> = [];
1239
+
1240
+ const measureElements = () => {
1241
+ const captionsRect = captions.getBoundingClientRect();
1242
+ const beforeRect = beforeContainer.getBoundingClientRect();
1243
+ const activeRect = activeContainer.getBoundingClientRect();
1244
+ const afterRect = afterContainer.getBoundingClientRect();
1245
+
1246
+ return {
1247
+ captions: { width: captionsRect.width, x: captionsRect.x },
1248
+ before: {
1249
+ width: beforeRect.width,
1250
+ x: beforeRect.x,
1251
+ text: beforeContainer.segmentText,
1252
+ },
1253
+ active: {
1254
+ width: activeRect.width,
1255
+ x: activeRect.x,
1256
+ text: activeContainer.wordText,
1257
+ },
1258
+ after: {
1259
+ width: afterRect.width,
1260
+ x: afterRect.x,
1261
+ text: afterContainer.segmentText,
1262
+ },
1263
+ totalContentWidth:
1264
+ beforeRect.width + activeRect.width + afterRect.width,
1265
+ };
1266
+ };
1267
+
1268
+ // Test each word and measure layout
1269
+ const testWords = [
1270
+ { time: 300, expectedWord: "Welcome" },
1271
+ { time: 750, expectedWord: " to" },
1272
+ { time: 1050, expectedWord: " the" },
1273
+ { time: 1500, expectedWord: " custom" },
1274
+ ];
1275
+
1276
+ for (const test of testWords) {
1277
+ timegroup.currentTimeMs = test.time;
1278
+ await timegroup.seekTask.taskComplete;
1279
+ await captions.updateComplete;
1280
+
1281
+ const measurement = measureElements();
1282
+ measurements.push({
1283
+ time: test.time,
1284
+ word: test.expectedWord,
1285
+ measurements: measurement,
1286
+ });
1287
+
1288
+ console.log(`At ${test.time}ms (${test.expectedWord}):`);
1289
+ console.log(
1290
+ ` Active word: "${measurement.active.text}" width=${measurement.active.width}px x=${measurement.active.x}`,
1291
+ );
1292
+ console.log(
1293
+ ` Before: "${measurement.before.text}" width=${measurement.before.width}px`,
1294
+ );
1295
+ console.log(
1296
+ ` After: "${measurement.after.text}" width=${measurement.after.width}px`,
1297
+ );
1298
+ console.log(
1299
+ ` Total content: ${measurement.totalContentWidth}px, Container: ${measurement.captions.width}px`,
1300
+ );
1301
+ console.log("");
1302
+ }
1303
+
1304
+ // Check if total width stays consistent
1305
+ const firstTotal = measurements[0]?.measurements.totalContentWidth;
1306
+ const allTotalsConsistent = measurements.every(
1307
+ (m) => Math.abs(m.measurements.totalContentWidth - firstTotal) < 2, // Allow 1-2px tolerance
1308
+ );
1309
+
1310
+ if (!allTotalsConsistent) {
1311
+ console.log("Width inconsistency detected:");
1312
+ measurements.forEach((m) => {
1313
+ console.log(` ${m.word}: ${m.measurements.totalContentWidth}px`);
1314
+ });
1315
+ }
1316
+
1317
+ expect(allTotalsConsistent).toBe(true);
1318
+ });
1319
+
1320
+ test("measures font weight differences causing layout shifts", async () => {
1321
+ const timegroup = document.createElement("ef-timegroup");
1322
+
1323
+ const scriptId = "font-test-script";
1324
+ const script = document.createElement("script");
1325
+ script.id = scriptId;
1326
+ script.type = "application/json";
1327
+ script.textContent = JSON.stringify({
1328
+ segments: [{ start: 0, end: 4, text: "Welcome to the custom" }],
1329
+ word_segments: [
1330
+ { text: "Welcome", start: 0, end: 0.6 },
1331
+ { text: " to", start: 0.6, end: 0.9 },
1332
+ { text: " the", start: 0.9, end: 1.2 },
1333
+ { text: " custom", start: 1.2, end: 1.8 },
1334
+ ],
1335
+ });
1336
+ document.body.appendChild(script);
1337
+
1338
+ const captions = document.createElement("ef-captions");
1339
+ captions.captionsScript = scriptId;
1340
+ captions.style.cssText =
1341
+ "display: block !important; text-align: center; background: green; padding: 16px; font-weight: bold;";
1342
+
1343
+ const beforeContainer = document.createElement(
1344
+ "ef-captions-before-active-word",
1345
+ );
1346
+ beforeContainer.className = "text-green-200";
1347
+
1348
+ const activeContainer = document.createElement("ef-captions-active-word");
1349
+ activeContainer.className = "bg-lime-400 text-green-900 rounded";
1350
+
1351
+ const afterContainer = document.createElement(
1352
+ "ef-captions-after-active-word",
1353
+ );
1354
+ afterContainer.className = "text-green-200";
1355
+
1356
+ captions.appendChild(beforeContainer);
1357
+ captions.appendChild(activeContainer);
1358
+ captions.appendChild(afterContainer);
1359
+ timegroup.appendChild(captions);
1360
+ document.body.appendChild(timegroup);
1361
+
1362
+ // @ts-expect-error accessing private property for testing
1363
+ const captionsTask = captions.customCaptionsDataTask;
1364
+ await captionsTask.taskComplete;
1365
+
1366
+ // Test with consistent font weight
1367
+ timegroup.currentTimeMs = 300;
1368
+ await timegroup.seekTask.taskComplete;
1369
+ await captions.updateComplete;
1370
+
1371
+ const welcomeRect = activeContainer.getBoundingClientRect();
1372
+ console.log(
1373
+ `"Welcome" with consistent bold: width=${welcomeRect.width}px`,
1374
+ );
1375
+
1376
+ timegroup.currentTimeMs = 750;
1377
+ await timegroup.seekTask.taskComplete;
1378
+ await captions.updateComplete;
1379
+
1380
+ const toRect = activeContainer.getBoundingClientRect();
1381
+ console.log(`" to" with consistent bold: width=${toRect.width}px`);
1382
+
1383
+ // Check if the text positioning stays stable with consistent font weight
1384
+ const totalWidth1 =
1385
+ beforeContainer.getBoundingClientRect().width +
1386
+ activeContainer.getBoundingClientRect().width +
1387
+ afterContainer.getBoundingClientRect().width;
1388
+
1389
+ timegroup.currentTimeMs = 1050;
1390
+ await timegroup.seekTask.taskComplete;
1391
+ await captions.updateComplete;
1392
+
1393
+ const totalWidth2 =
1394
+ beforeContainer.getBoundingClientRect().width +
1395
+ activeContainer.getBoundingClientRect().width +
1396
+ afterContainer.getBoundingClientRect().width;
1397
+
1398
+ console.log(
1399
+ `Total width consistency: ${totalWidth1}px vs ${totalWidth2}px (diff: ${Math.abs(totalWidth1 - totalWidth2)}px)`,
1400
+ );
1401
+
1402
+ expect(Math.abs(totalWidth1 - totalWidth2)).toBeLessThan(1);
1403
+ });
1404
+
1405
+ test("captions work naturally without CSS overrides", async () => {
1406
+ const timegroup = document.createElement("ef-timegroup");
1407
+
1408
+ const scriptId = "natural-test-script";
1409
+ const script = document.createElement("script");
1410
+ script.id = scriptId;
1411
+ script.type = "application/json";
1412
+ script.textContent = JSON.stringify({
1413
+ segments: [{ start: 0, end: 4, text: "Natural flow test" }],
1414
+ word_segments: [
1415
+ { text: "Natural", start: 0, end: 1 },
1416
+ { text: " flow", start: 1, end: 2 },
1417
+ { text: " test", start: 2, end: 4 },
1418
+ ],
1419
+ });
1420
+ document.body.appendChild(script);
1421
+
1422
+ // Test with NO CSS overrides - should just work naturally
1423
+ const captions = document.createElement("ef-captions");
1424
+ captions.captionsScript = scriptId;
1425
+ captions.className = "font-bold text-center p-4"; // Simple, natural styling
1426
+
1427
+ const beforeContainer = document.createElement(
1428
+ "ef-captions-before-active-word",
1429
+ );
1430
+ const activeContainer = document.createElement("ef-captions-active-word");
1431
+ activeContainer.className = "bg-yellow-400 text-black rounded"; // Only background change
1432
+ const afterContainer = document.createElement(
1433
+ "ef-captions-after-active-word",
1434
+ );
1435
+
1436
+ captions.appendChild(beforeContainer);
1437
+ captions.appendChild(activeContainer);
1438
+ captions.appendChild(afterContainer);
1439
+ timegroup.appendChild(captions);
1440
+ document.body.appendChild(timegroup);
1441
+
1442
+ // @ts-expect-error accessing private property for testing
1443
+ const captionsTask = captions.customCaptionsDataTask;
1444
+ await captionsTask.taskComplete;
1445
+
1446
+ // Measure widths with natural component behavior
1447
+ timegroup.currentTimeMs = 500;
1448
+ await timegroup.seekTask.taskComplete;
1449
+ await captions.updateComplete;
1450
+
1451
+ const naturalRect1 = captions.getBoundingClientRect();
1452
+ console.log(
1453
+ `Natural captions width at "Natural": ${naturalRect1.width}px`,
1454
+ );
1455
+
1456
+ timegroup.currentTimeMs = 1500;
1457
+ await timegroup.seekTask.taskComplete;
1458
+ await captions.updateComplete;
1459
+
1460
+ const naturalRect2 = captions.getBoundingClientRect();
1461
+ console.log(`Natural captions width at " flow": ${naturalRect2.width}px`);
1462
+
1463
+ const widthDiff = Math.abs(naturalRect2.width - naturalRect1.width);
1464
+ console.log(`Width difference: ${widthDiff}px`);
1465
+
1466
+ // Should have minimal width difference with natural component behavior
1467
+ expect(widthDiff).toBeLessThan(2); // Allow small rounding tolerance
1468
+ });
1469
+
1470
+ test("transform scaling keeps surrounding text positions stable", async () => {
1471
+ const timegroup = document.createElement("ef-timegroup");
1472
+
1473
+ const captions = document.createElement("ef-captions");
1474
+ captions.captionsData = {
1475
+ segments: [{ start: 0, end: 4, text: "Before active after text" }],
1476
+ word_segments: [
1477
+ { text: "Before", start: 0, end: 1 },
1478
+ { text: " active", start: 1, end: 2 },
1479
+ { text: " after", start: 2, end: 3 },
1480
+ { text: " text", start: 3, end: 4 },
1481
+ ],
1482
+ };
1483
+
1484
+ const beforeContainer = document.createElement(
1485
+ "ef-captions-before-active-word",
1486
+ );
1487
+ const activeContainer = document.createElement("ef-captions-active-word");
1488
+ activeContainer.style.cssText =
1489
+ "background: yellow; color: black; transform: scale(1.2); transform-origin: center; transition: transform 200ms;";
1490
+ const afterContainer = document.createElement(
1491
+ "ef-captions-after-active-word",
1492
+ );
1493
+
1494
+ captions.appendChild(beforeContainer);
1495
+ captions.appendChild(activeContainer);
1496
+ captions.appendChild(afterContainer);
1497
+ timegroup.appendChild(captions);
1498
+ document.body.appendChild(timegroup);
1499
+
1500
+ // @ts-expect-error accessing private property for testing
1501
+ const captionsTask = captions.customCaptionsDataTask;
1502
+ await captionsTask.taskComplete;
1503
+
1504
+ // Test different words and ensure before/after text positions don't jump
1505
+ const positionTests = [
1506
+ { time: 500, word: "Before" },
1507
+ { time: 1500, word: " active" },
1508
+ { time: 2500, word: " after" },
1509
+ ];
1510
+
1511
+ let previousBeforeX: number | null = null;
1512
+ let previousAfterX: number | null = null;
1513
+
1514
+ for (const test of positionTests) {
1515
+ timegroup.currentTimeMs = test.time;
1516
+ await timegroup.seekTask.taskComplete;
1517
+ await captions.updateComplete;
1518
+
1519
+ const beforeRect = beforeContainer.getBoundingClientRect();
1520
+ const activeRect = activeContainer.getBoundingClientRect();
1521
+ const afterRect = afterContainer.getBoundingClientRect();
1522
+
1523
+ console.log(`At ${test.time}ms (active word: "${test.word}"):`);
1524
+ console.log(` Before text X: ${beforeRect.x.toFixed(1)}px`);
1525
+ console.log(` Active text X: ${activeRect.x.toFixed(1)}px (scaled)`);
1526
+ console.log(` After text X: ${afterRect.x.toFixed(1)}px`);
1527
+
1528
+ if (previousBeforeX !== null && beforeContainer.textContent) {
1529
+ const beforeXDiff = Math.abs(beforeRect.x - previousBeforeX);
1530
+ console.log(` Before X movement: ${beforeXDiff.toFixed(1)}px`);
1531
+ expect(beforeXDiff).toBeLessThan(2); // Should be very stable
1532
+ }
1533
+
1534
+ if (previousAfterX !== null && afterContainer.textContent) {
1535
+ const afterXDiff = Math.abs(afterRect.x - previousAfterX);
1536
+ console.log(` After X movement: ${afterXDiff.toFixed(1)}px`);
1537
+ expect(afterXDiff).toBeLessThan(2); // Should be very stable
1538
+ }
1539
+
1540
+ previousBeforeX = beforeRect.x;
1541
+ previousAfterX = afterRect.x;
1542
+ console.log("");
1543
+ }
1544
+ });
1545
+
1546
+ test("CSS animations trigger with timegroup timing", async () => {
1547
+ const timegroup = document.createElement("ef-timegroup");
1548
+
1549
+ // Add bounce animation keyframes to test environment
1550
+ const style = document.createElement("style");
1551
+ style.textContent = `
1552
+ @keyframes bounceIn {
1553
+ 0% { transform: scale(calc(0.6 + var(--ef-word-seed, 0.5) * 0.3))
1554
+ rotate(calc(-10deg + var(--ef-word-seed, 0.5) * 20deg))
1555
+ skewX(calc(-15deg + var(--ef-word-seed, 0.5) * 30deg)); }
1556
+ 50% { transform: scale(calc(1.05 + var(--ef-word-seed, 0.5) * 0.2))
1557
+ rotate(calc(-3deg + var(--ef-word-seed, 0.5) * 6deg))
1558
+ skewX(calc(-5deg + var(--ef-word-seed, 0.5) * 10deg)); }
1559
+ 100% { transform: scale(1) rotate(0deg) skewX(0deg); }
1560
+ }
1561
+ .bounce-in { animation: 0.3s ease-out 0s 1 normal none running bounceIn; }
1562
+ .bounce-scale-125 {
1563
+ animation: 0.3s ease-out 0s 1 normal none running bounceIn;
1564
+ transform: scale(1.25);
1565
+ }
1566
+ `;
1567
+ document.head.appendChild(style);
1568
+
1569
+ const captions = document.createElement("ef-captions");
1570
+ captions.captionsData = {
1571
+ segments: [{ start: 0, end: 3, text: "Bounce test animation" }],
1572
+ word_segments: [
1573
+ { text: "Bounce", start: 0, end: 1 },
1574
+ { text: " test", start: 1, end: 2 },
1575
+ { text: " animation", start: 2, end: 3 },
1576
+ ],
1577
+ };
1578
+
1579
+ const activeContainer = document.createElement("ef-captions-active-word");
1580
+ activeContainer.className = "bounce-scale-125";
1581
+ captions.appendChild(activeContainer);
1582
+ timegroup.appendChild(captions);
1583
+ document.body.appendChild(timegroup);
1584
+
1585
+ // @ts-expect-error accessing private property for testing
1586
+ const captionsTask = captions.customCaptionsDataTask;
1587
+ await captionsTask.taskComplete;
1588
+
1589
+ // Test animation properties when different words become active
1590
+ const animationTests = [
1591
+ { time: 500, word: "Bounce" },
1592
+ { time: 1500, word: " test" },
1593
+ { time: 2500, word: " animation" },
1594
+ ];
1595
+
1596
+ for (const test of animationTests) {
1597
+ console.log(
1598
+ `\nTesting animation at ${test.time}ms (word: "${test.word}"):`,
1599
+ );
1600
+
1601
+ timegroup.currentTimeMs = test.time;
1602
+ await timegroup.seekTask.taskComplete;
1603
+ await captions.updateComplete;
1604
+
1605
+ // Check that element is visible and has animation properties
1606
+ const computedStyle = getComputedStyle(activeContainer);
1607
+ const animationName = computedStyle.animationName;
1608
+ const animationDuration = computedStyle.animationDuration;
1609
+ const animationTimingFunction = computedStyle.animationTimingFunction;
1610
+
1611
+ console.log(` Element text: "${activeContainer.textContent}"`);
1612
+ console.log(` Animation name: ${animationName}`);
1613
+ console.log(` Animation duration: ${animationDuration}`);
1614
+ console.log(` Animation timing: ${animationTimingFunction}`);
1615
+ console.log(` Element visible: ${!activeContainer.hidden}`);
1616
+
1617
+ // Element should be visible when in its time range
1618
+ expect(activeContainer.hidden).toBe(false);
1619
+
1620
+ // Should have the bounce animation applied correctly
1621
+ expect(animationName).toBe("bounceIn");
1622
+ expect(animationDuration).toBe("0.3s");
1623
+ }
1624
+
1625
+ // Test that element is hidden when outside time range
1626
+ timegroup.currentTimeMs = 3500; // After all words
1627
+ await timegroup.seekTask.taskComplete;
1628
+ await captions.updateComplete;
1629
+
1630
+ console.log("\nAfter time range (3500ms):");
1631
+ console.log(` Element hidden: ${activeContainer.hidden}`);
1632
+ console.log(` Element text: "${activeContainer.textContent}"`);
1633
+
1634
+ // Should be hidden when no word is active
1635
+ expect(activeContainer.hidden).toBe(true);
1636
+
1637
+ document.head.removeChild(style); // Clean up
1638
+ });
1639
+ });
1640
+
1641
+ describe("sequence timing integration", () => {
1642
+ test("sequence duration updates when captions data is set via captionsData property", async () => {
1643
+ // Test the exact scenario: sequence > contain > captions, contain > captions
1644
+ const sequence = document.createElement("ef-timegroup");
1645
+ sequence.mode = "sequence";
1646
+
1647
+ const container1 = document.createElement("ef-timegroup");
1648
+ container1.mode = "contain";
1649
+ const captions1 = document.createElement("ef-captions");
1650
+ const word1 = document.createElement("ef-captions-active-word");
1651
+ captions1.appendChild(word1);
1652
+ container1.appendChild(captions1);
1653
+
1654
+ const container2 = document.createElement("ef-timegroup");
1655
+ container2.mode = "contain";
1656
+ const captions2 = document.createElement("ef-captions");
1657
+ const word2 = document.createElement("ef-captions-active-word");
1658
+ captions2.appendChild(word2);
1659
+ container2.appendChild(captions2);
1660
+
1661
+ sequence.appendChild(container1);
1662
+ sequence.appendChild(container2);
1663
+ document.body.appendChild(sequence);
1664
+
1665
+ // Initially, sequence should have no duration
1666
+ await sequence.updateComplete;
1667
+ expect(sequence.durationMs).toBe(0);
1668
+
1669
+ // Set captions data for both elements
1670
+ captions1.captionsData = {
1671
+ segments: [{ start: 0, end: 5, text: "First" }],
1672
+ word_segments: [{ text: "First", start: 0, end: 5 }],
1673
+ };
1674
+
1675
+ captions2.captionsData = {
1676
+ segments: [{ start: 0, end: 3, text: "Second" }],
1677
+ word_segments: [{ text: "Second", start: 0, end: 3 }],
1678
+ };
1679
+
1680
+ // Wait for updates to propagate
1681
+ await captions1.updateComplete;
1682
+ await captions2.updateComplete;
1683
+ await sequence.updateComplete;
1684
+
1685
+ // Sequence should now have combined duration (5s + 3s = 8s)
1686
+ expect(sequence.durationMs).toBe(8000);
1687
+
1688
+ try {
1689
+ document.body.removeChild(sequence);
1690
+ } catch (_e) {
1691
+ // Cleanup may fail in test environment, ignore
1692
+ }
1693
+ });
1694
+
1695
+ test("second captions timegroup is visible when timeline is positioned in second segment", async () => {
1696
+ // Create exact demo structure: sequence > contain > captions, contain > captions
1697
+ const sequence = document.createElement("ef-timegroup");
1698
+ sequence.mode = "sequence";
1699
+
1700
+ const container1 = document.createElement("ef-timegroup");
1701
+ container1.mode = "contain";
1702
+ const captions1 = document.createElement("ef-captions");
1703
+ captions1.captionsData = {
1704
+ segments: [{ start: 0, end: 5, text: "First" }],
1705
+ word_segments: [{ text: "First", start: 0, end: 5 }],
1706
+ };
1707
+ const word1 = document.createElement("ef-captions-active-word");
1708
+ captions1.appendChild(word1);
1709
+ container1.appendChild(captions1);
1710
+
1711
+ const container2 = document.createElement("ef-timegroup");
1712
+ container2.mode = "contain";
1713
+ const captions2 = document.createElement("ef-captions");
1714
+ captions2.captionsData = {
1715
+ segments: [{ start: 0, end: 3, text: "Second" }],
1716
+ word_segments: [{ text: "Second", start: 0, end: 3 }],
1717
+ };
1718
+ const word2 = document.createElement("ef-captions-active-word");
1719
+ captions2.appendChild(word2);
1720
+ container2.appendChild(captions2);
1721
+
1722
+ sequence.appendChild(container1);
1723
+ sequence.appendChild(container2);
1724
+ document.body.appendChild(sequence);
1725
+
1726
+ // Set timeline to be in second timegroup (7s = 2s into second captions)
1727
+ sequence.currentTimeMs = 7000;
1728
+ await sequence.seekTask.taskComplete;
1729
+
1730
+ console.log(
1731
+ `Timeline at 7000ms - Second captions word: "${word2.wordText}", hidden: ${word2.hidden}`,
1732
+ );
1733
+
1734
+ // The second captions must be visible
1735
+ expect(word2.wordText).toBe("Second");
1736
+ expect(word2.hidden).toBe(false);
1737
+ });
1738
+
1739
+ test("shows completed words in before container when segment extends beyond words", async () => {
1740
+ // Test case where segment duration extends beyond last word
1741
+ const timegroup = document.createElement("ef-timegroup");
1742
+
1743
+ const captions = document.createElement("ef-captions");
1744
+ captions.captionsData = {
1745
+ segments: [
1746
+ { start: 0, end: 5, text: "Complete segment text" }, // 5 second segment
1747
+ ],
1748
+ word_segments: [
1749
+ { text: "Complete", start: 0, end: 1 },
1750
+ { text: " segment", start: 1, end: 2 },
1751
+ { text: " text", start: 2, end: 3 },
1752
+ // Words end at 3s but segment continues until 5s
1753
+ ],
1754
+ };
1755
+
1756
+ const beforeContainer = document.createElement(
1757
+ "ef-captions-before-active-word",
1758
+ );
1759
+ const wordContainer = document.createElement("ef-captions-active-word");
1760
+ const segmentContainer = document.createElement("ef-captions-segment");
1761
+
1762
+ captions.appendChild(beforeContainer);
1763
+ captions.appendChild(wordContainer);
1764
+ captions.appendChild(segmentContainer);
1765
+ timegroup.appendChild(captions);
1766
+ document.body.appendChild(timegroup);
1767
+
1768
+ // Test at 2.5s - should show " text" word normally
1769
+ timegroup.currentTimeMs = 2500;
1770
+ await timegroup.seekTask.taskComplete;
1771
+ await captions.updateComplete;
1772
+
1773
+ expect(wordContainer.wordText).toBe(" text");
1774
+ expect(wordContainer.hidden).toBe(false);
1775
+
1776
+ // Test at 4s - after all words finished but segment still active
1777
+ timegroup.currentTimeMs = 4000;
1778
+ await timegroup.seekTask.taskComplete;
1779
+ await captions.updateComplete;
1780
+
1781
+ console.log(
1782
+ ` Active word: "${wordContainer.wordText}", hidden=${wordContainer.hidden}`,
1783
+ );
1784
+ console.log(
1785
+ ` Before container: "${beforeContainer.segmentText}", hidden=${beforeContainer.hidden}`,
1786
+ );
1787
+ console.log(
1788
+ ` Segment: "${segmentContainer.segmentText}", hidden=${segmentContainer.hidden}`,
1789
+ );
1790
+
1791
+ // Word container should be empty/hidden since no active word
1792
+ expect(wordContainer.wordText).toBe("");
1793
+ expect(wordContainer.hidden).toBe(true);
1794
+
1795
+ // Before container should show all completed words to maintain visual continuity
1796
+ expect(beforeContainer.segmentText).toBe("Complete segment text");
1797
+ expect(beforeContainer.hidden).toBe(false);
1798
+
1799
+ // Segment should still be active
1800
+ expect(segmentContainer.segmentText).toBe("Complete segment text");
1801
+ expect(segmentContainer.hidden).toBe(false);
1802
+
1803
+ try {
1804
+ document.body.removeChild(timegroup);
1805
+ } catch (_e) {
1806
+ // Cleanup may fail in test environment, ignore
1807
+ }
1808
+ });
1809
+
1810
+ test("shows all words in after container when in segment but before first word", async () => {
1811
+ // Test case where we're in a segment but before the first word starts
1812
+ const timegroup = document.createElement("ef-timegroup");
1813
+
1814
+ const captions = document.createElement("ef-captions");
1815
+ captions.captionsData = {
1816
+ segments: [
1817
+ { start: 0, end: 10, text: "Complete test segment" }, // 10 second segment
1818
+ ],
1819
+ word_segments: [
1820
+ { text: "Complete", start: 2, end: 4 }, // First word starts at 2s
1821
+ { text: " test", start: 5, end: 7 },
1822
+ { text: " segment", start: 8, end: 9 },
1823
+ // Gap from 0-2s before first word
1824
+ ],
1825
+ };
1826
+
1827
+ const beforeContainer = document.createElement(
1828
+ "ef-captions-before-active-word",
1829
+ );
1830
+ const wordContainer = document.createElement("ef-captions-active-word");
1831
+ const afterContainer = document.createElement(
1832
+ "ef-captions-after-active-word",
1833
+ );
1834
+ const segmentContainer = document.createElement("ef-captions-segment");
1835
+
1836
+ captions.appendChild(beforeContainer);
1837
+ captions.appendChild(wordContainer);
1838
+ captions.appendChild(afterContainer);
1839
+ captions.appendChild(segmentContainer);
1840
+ timegroup.appendChild(captions);
1841
+ document.body.appendChild(timegroup);
1842
+
1843
+ // @ts-expect-error accessing private property for testing
1844
+ const captionsTask = captions.customCaptionsDataTask;
1845
+ await captionsTask.taskComplete;
1846
+
1847
+ // Test at 1s - in segment but before first word starts
1848
+ timegroup.currentTimeMs = 1000;
1849
+ await timegroup.seekTask.taskComplete;
1850
+ await captions.updateComplete;
1851
+
1852
+ console.log(
1853
+ ` Active word: "${wordContainer.wordText}", hidden=${wordContainer.hidden}`,
1854
+ );
1855
+ console.log(
1856
+ ` Before container: "${beforeContainer.segmentText}", hidden=${beforeContainer.hidden}`,
1857
+ );
1858
+ console.log(
1859
+ ` After container: "${afterContainer.segmentText}", hidden=${afterContainer.hidden}`,
1860
+ );
1861
+ console.log(
1862
+ ` Segment: "${segmentContainer.segmentText}", hidden=${segmentContainer.hidden}`,
1863
+ );
1864
+
1865
+ // Active word should be empty since no word is active yet
1866
+ expect(wordContainer.wordText).toBe("");
1867
+ expect(wordContainer.hidden).toBe(true);
1868
+
1869
+ // Before container should be empty (nothing has happened yet)
1870
+ expect(beforeContainer.segmentText).toBe("");
1871
+ expect(beforeContainer.hidden).toBe(true);
1872
+
1873
+ // After container should show all upcoming words
1874
+ expect(afterContainer.segmentText).toBe("Complete test segment");
1875
+ expect(afterContainer.hidden).toBe(false);
1876
+
1877
+ // Segment should be active
1878
+ expect(segmentContainer.segmentText).toBe("Complete test segment");
1879
+ expect(segmentContainer.hidden).toBe(false);
1880
+
1881
+ try {
1882
+ document.body.removeChild(timegroup);
1883
+ } catch (_e) {
1884
+ // Cleanup may fail in test environment, ignore
1885
+ }
1886
+ });
1887
+ });
68
1888
  });