@bycrux/editor 0.12.0 → 1.0.1
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.
- package/package.json +3 -1
- package/src/ControlsInfoModal.tsx +251 -61
- package/src/__tests__/ControlsInfoModal.test.tsx +43 -0
- package/src/__tests__/adapter.test.ts +59 -1
- package/src/__tests__/schema-assignability.test.ts +93 -0
- package/src/__tests__/schema.test.ts +15 -0
- package/src/__tests__/video-adapter-contract.test.ts +128 -5
- package/src/carousel/AddElementMenu.tsx +10 -4
- package/src/carousel/CarouselEditor.tsx +46 -8
- package/src/carousel/CarouselRenderModal.tsx +15 -10
- package/src/carousel/OverlayPicker.tsx +10 -3
- package/src/carousel/SlidePropertyPanel.tsx +43 -21
- package/src/components/FilmstripScrubber.tsx +277 -0
- package/src/components/__tests__/FilmstripScrubber.test.tsx +216 -0
- package/src/engine/__tests__/audio-clock.test.ts +655 -0
- package/src/engine/__tests__/audio-worklet-source.test.ts +316 -0
- package/src/engine/__tests__/batch-planner.test.ts +298 -0
- package/src/engine/__tests__/decode-worker-source.test.ts +249 -0
- package/src/engine/__tests__/demux-ranged.test.ts +495 -0
- package/src/engine/__tests__/demux-truncated.test.ts +136 -0
- package/src/engine/__tests__/demux.test.ts +305 -0
- package/src/engine/__tests__/eligibility.test.ts +146 -0
- package/src/engine/__tests__/engine-recovery.test.ts +263 -0
- package/src/engine/__tests__/engine.test.ts +326 -0
- package/src/engine/__tests__/frame-server-ranged.test.ts +261 -0
- package/src/engine/__tests__/frame-server.test.ts +326 -0
- package/src/engine/__tests__/media-loader-ranged.test.ts +289 -0
- package/src/engine/__tests__/media-loader.test.ts +100 -0
- package/src/engine/__tests__/scheduler-crop.test.ts +353 -0
- package/src/engine/__tests__/scheduler.test.ts +1310 -0
- package/src/engine/__tests__/scrub-resolve.test.ts +96 -0
- package/src/engine/__tests__/scrub-source.test.ts +195 -0
- package/src/engine/__tests__/source-host.test.ts +455 -0
- package/src/engine/__tests__/time-stretch.test.ts +182 -0
- package/src/engine/audio-clock.ts +1179 -0
- package/src/engine/audio-worklet-source.ts +160 -0
- package/src/engine/batch-planner.ts +308 -0
- package/src/engine/debug-hud.tsx +54 -0
- package/src/engine/decode-worker-source.ts +142 -0
- package/src/engine/demux.ts +900 -0
- package/src/engine/eligibility.ts +140 -0
- package/src/engine/frame-server.ts +644 -0
- package/src/engine/index.ts +950 -0
- package/src/engine/media-loader.ts +380 -0
- package/src/engine/mp4box.d.ts +214 -0
- package/src/engine/scheduler.ts +1324 -0
- package/src/engine/scrub-resolve.ts +66 -0
- package/src/engine/scrub-source.ts +496 -0
- package/src/engine/time-stretch.ts +224 -0
- package/src/index.ts +75 -1
- package/src/preview/OverlayPreview.tsx +2 -24
- package/src/schema.ts +134 -2
- package/src/state/__tests__/use-project-sync.test.tsx +56 -0
- package/src/state/use-project-sync.ts +17 -0
- package/src/test-setup.ts +32 -0
- package/src/text/FontPicker.tsx +85 -51
- package/src/text/TextFormattingToolbar.tsx +6 -1
- package/src/text/__tests__/FontPicker.options.test.ts +43 -0
- package/src/text/__tests__/TextFormattingToolbar.test.tsx +4 -4
- package/src/theme.ts +108 -3
- package/src/types.ts +586 -22
- package/src/ui/Loader.tsx +64 -0
- package/src/ui/NumberField.tsx +254 -0
- package/src/ui/Slider.tsx +128 -0
- package/src/ui/Tooltip.tsx +118 -0
- package/src/ui/__tests__/NumberField.test.tsx +298 -0
- package/src/ui/__tests__/Slider.test.tsx +150 -0
- package/src/ui/__tests__/Tooltip.test.tsx +105 -0
- package/src/ui/__tests__/usePersistentState.test.tsx +112 -0
- package/src/ui/badge.tsx +12 -4
- package/src/ui/index.ts +6 -0
- package/src/ui/input.tsx +1 -1
- package/src/ui/select.tsx +1 -1
- package/src/ui/switch.tsx +12 -2
- package/src/ui/textarea.tsx +1 -1
- package/src/ui/usePersistentState.ts +63 -0
- package/src/video/AudioPolishModal.tsx +983 -0
- package/src/video/CaptionListPanel.test.tsx +944 -0
- package/src/video/CaptionListPanel.tsx +1106 -0
- package/src/video/CaptionRegenModal.tsx +37 -9
- package/src/video/CaptionSpecimen.tsx +160 -0
- package/src/video/CaptionStyleGallery.tsx +466 -0
- package/src/video/CommandPalette.tsx +165 -0
- package/src/video/ImageToneMenu.tsx +6 -2
- package/src/video/OverlayInspector.tsx +977 -0
- package/src/video/RenderModal.tsx +1035 -62
- package/src/video/VersionCompare.tsx +258 -0
- package/src/video/VersionPanel.tsx +117 -42
- package/src/video/VideoEditor.tsx +2202 -251
- package/src/video/__tests__/AudioPolishModal.test.tsx +825 -0
- package/src/video/__tests__/CaptionListPanel.font.test.tsx +397 -0
- package/src/video/__tests__/CaptionListPanel.generate.test.tsx +153 -0
- package/src/video/__tests__/CaptionRegenModal.test.tsx +29 -0
- package/src/video/__tests__/CaptionSpecimen.test.tsx +213 -0
- package/src/video/__tests__/CaptionStyleGallery.test.tsx +362 -0
- package/src/video/__tests__/CommandPalette.test.tsx +119 -0
- package/src/video/__tests__/OverlayInspector.test.tsx +1367 -0
- package/src/video/__tests__/RenderModal.exportControls.test.tsx +319 -0
- package/src/video/__tests__/RenderModal.options.test.tsx +562 -0
- package/src/video/__tests__/RenderModal.progress.test.ts +65 -0
- package/src/video/__tests__/VersionCompare.test.tsx +182 -0
- package/src/video/__tests__/VersionPanel.test.tsx +279 -0
- package/src/video/__tests__/VideoEditor.audioPolish.test.tsx +241 -0
- package/src/video/__tests__/VideoEditor.captionDelete.test.tsx +147 -0
- package/src/video/__tests__/VideoEditor.captionGesture.test.tsx +170 -0
- package/src/video/__tests__/VideoEditor.captionSeam.test.tsx +134 -0
- package/src/video/__tests__/VideoEditor.clipKeyframes.test.tsx +59 -0
- package/src/video/__tests__/VideoEditor.context.test.tsx +44 -0
- package/src/video/__tests__/VideoEditor.editFocus.test.tsx +55 -0
- package/src/video/__tests__/VideoEditor.keymap.test.tsx +725 -0
- package/src/video/__tests__/VideoEditor.layout.test.tsx +338 -0
- package/src/video/__tests__/VideoEditor.propertiesPanel.test.tsx +765 -0
- package/src/video/__tests__/VideoEditor.rippleDeleteCaptions.test.tsx +159 -0
- package/src/video/__tests__/VideoEditor.sourcePreview.test.tsx +122 -0
- package/src/video/__tests__/VideoEditor.test.tsx +403 -50
- package/src/video/__tests__/audioMagnet.test.ts +158 -0
- package/src/video/__tests__/audioPolish.test.ts +1202 -0
- package/src/video/__tests__/captionActiveWord.test.ts +107 -0
- package/src/video/__tests__/captionLanes.test.ts +262 -0
- package/src/video/__tests__/captionPositioning.test.tsx +8 -3
- package/src/video/__tests__/captionWordFloor.test.ts +228 -0
- package/src/video/__tests__/clipboard-ops.test.ts +430 -0
- package/src/video/__tests__/cuts.insert.test.ts +244 -0
- package/src/video/__tests__/cuts.test.ts +1213 -34
- package/src/video/__tests__/export-limits.test.ts +195 -0
- package/src/video/__tests__/hover-scrub.test.ts +163 -0
- package/src/video/__tests__/keyframeOps.canKeyframeProp.test.ts +61 -0
- package/src/video/__tests__/keyframeOps.test.ts +643 -0
- package/src/video/__tests__/keymap.test.tsx +171 -0
- package/src/video/__tests__/render-progress.test.tsx +20 -4
- package/src/video/__tests__/shuttle.test.ts +210 -0
- package/src/video/__tests__/source-preview.test.ts +60 -0
- package/src/video/__tests__/timecode.test.ts +77 -0
- package/src/video/__tests__/use-report-context.test.tsx +101 -0
- package/src/video/audioMagnet.ts +72 -0
- package/src/video/audioPolish.ts +774 -0
- package/src/video/captionActiveWord.ts +74 -0
- package/src/video/captionLanes.ts +202 -0
- package/src/video/captionRepair.ts +9 -5
- package/src/video/captionStyleDefaults.ts +100 -0
- package/src/video/captionWordFloor.ts +83 -0
- package/src/video/clipboard-ops.ts +377 -0
- package/src/video/cuts.ts +713 -37
- package/src/video/export-limits.ts +102 -0
- package/src/video/hover-scrub.ts +102 -0
- package/src/video/keyframeOps.ts +384 -0
- package/src/video/keymap.ts +146 -0
- package/src/video/panels/ClipPropertiesPanel.tsx +706 -0
- package/src/video/panels/LeftPanelTabs.tsx +187 -0
- package/src/video/panels/OverlayContentPanel.tsx +351 -0
- package/src/video/panels/TabNav.tsx +60 -0
- package/src/video/panels/__tests__/ClipPropertiesPanel.test.tsx +645 -0
- package/src/video/panels/__tests__/LeftPanelTabs.test.tsx +189 -0
- package/src/video/panels/__tests__/OverlayContentPanel.test.tsx +222 -0
- package/src/video/panels/__tests__/TabNav.test.tsx +71 -0
- package/src/video/preview/CaptionPreview.tsx +57 -69
- package/src/video/preview/EngineSurface.tsx +103 -0
- package/src/video/preview/OverlayItemsLayer.tsx +315 -103
- package/src/video/preview/PreviewPlayer.tsx +337 -56
- package/src/video/preview/SocialPreviewMenu.tsx +214 -0
- package/src/video/preview/SocialSafeZoneOverlay.tsx +478 -0
- package/src/video/preview/__tests__/CaptionPreview.fonts.test.tsx +97 -0
- package/src/video/preview/__tests__/EngineSurface.test.tsx +114 -0
- package/src/video/preview/__tests__/OverlayItemsLayer.edit.test.tsx +4 -2
- package/src/video/preview/__tests__/OverlayItemsLayer.keyframes.test.tsx +363 -0
- package/src/video/preview/__tests__/OverlayItemsLayer.selection.test.tsx +329 -0
- package/src/video/preview/__tests__/PreviewPlayer.engine.test.tsx +139 -0
- package/src/video/preview/__tests__/SocialPreviewMenu.test.tsx +121 -0
- package/src/video/preview/__tests__/SocialSafeZoneOverlay.test.tsx +165 -0
- package/src/video/preview/__tests__/captionDragState.test.ts +120 -1
- package/src/video/preview/__tests__/latencyCompensation.test.tsx +451 -0
- package/src/video/preview/__tests__/proxySupport.test.ts +75 -0
- package/src/video/preview/__tests__/transformStyle.test.ts +29 -1
- package/src/video/preview/__tests__/useDragOverlay.perAxis.test.ts +197 -0
- package/src/video/preview/__tests__/useEnginePlayback.test.tsx +530 -0
- package/src/video/preview/__tests__/useVideoPlayback.corpus.test.ts +313 -0
- package/src/video/preview/__tests__/useVideoPlayback.test.ts +38 -5
- package/src/video/preview/__tests__/useVideoPlayback.trackAudio.test.ts +278 -0
- package/src/video/preview/audio-context.ts +111 -0
- package/src/video/preview/captionDragState.ts +91 -1
- package/src/video/preview/proxySupport.ts +86 -0
- package/src/video/preview/transformStyle.ts +22 -12
- package/src/video/preview/useDragOverlay.ts +92 -18
- package/src/video/preview/useEnginePlayback.ts +625 -0
- package/src/video/preview/useVideoPlayback.ts +211 -167
- package/src/video/sdrCurves.ts +56 -0
- package/src/video/shuttle.ts +159 -0
- package/src/video/source-preview.ts +66 -0
- package/src/video/timecode.ts +59 -0
- package/src/video/timeline/EditableSegment.tsx +1 -1
- package/src/video/timeline/Scrubber.tsx +46 -174
- package/src/video/timeline/SpeedControl.tsx +95 -0
- package/src/video/timeline/Timeline.tsx +1061 -328
- package/src/video/timeline/TimelineContext.ts +17 -10
- package/src/video/timeline/TrackGutter.tsx +560 -0
- package/src/video/timeline/TrackSettingsPopover.tsx +228 -0
- package/src/video/timeline/VolumeControl.tsx +113 -0
- package/src/video/timeline/__tests__/Timeline.backgroundClick.test.tsx +110 -0
- package/src/video/timeline/__tests__/Timeline.crossfade.test.tsx +104 -0
- package/src/video/timeline/__tests__/Timeline.fadeCurveMenu.test.tsx +174 -0
- package/src/video/timeline/__tests__/Timeline.keyframeDelete.test.tsx +273 -0
- package/src/video/timeline/__tests__/Timeline.keyframeFollow.test.tsx +215 -0
- package/src/video/timeline/__tests__/Timeline.keyframeMenu.test.tsx +253 -0
- package/src/video/timeline/__tests__/Timeline.keymap.test.tsx +372 -0
- package/src/video/timeline/__tests__/Timeline.subcutRegen.test.tsx +351 -0
- package/src/video/timeline/__tests__/TrackGutter.test.tsx +372 -0
- package/src/video/timeline/__tests__/_canvasSelect.test.tsx +273 -0
- package/src/video/timeline/__tests__/_canvasSelect.ts +414 -0
- package/src/video/timeline/__tests__/dragdrop-math.test.ts +135 -0
- package/src/video/timeline/__tests__/effectiveItemAudio.test.ts +50 -0
- package/src/video/timeline/__tests__/enabledTrackItems.test.ts +164 -0
- package/src/video/timeline/__tests__/moveItemAcrossTracks.test.ts +363 -0
- package/src/video/timeline/__tests__/multiSelectOps.test.ts +447 -0
- package/src/video/timeline/__tests__/placement.test.ts +278 -0
- package/src/video/timeline/__tests__/resizeWindowedItem.test.ts +140 -0
- package/src/video/timeline/__tests__/timeline-model.test.ts +576 -0
- package/src/video/timeline/__tests__/visualItemLabel.test.ts +69 -0
- package/src/video/timeline/canvas/TimelineCanvas.tsx +1447 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.drop.test.tsx +439 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.edgeScroll.test.tsx +346 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.panefill.test.tsx +122 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.pendingDrops.test.tsx +316 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.pointer.test.tsx +606 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.test.tsx +407 -0
- package/src/video/timeline/canvas/__tests__/clip-bands.test.ts +77 -0
- package/src/video/timeline/canvas/__tests__/draw.test.ts +2198 -0
- package/src/video/timeline/canvas/__tests__/fade-curve.test.ts +187 -0
- package/src/video/timeline/canvas/__tests__/filmstrips.test.ts +561 -0
- package/src/video/timeline/canvas/__tests__/hit-test.test.ts +818 -0
- package/src/video/timeline/canvas/__tests__/pending-drop.test.ts +210 -0
- package/src/video/timeline/canvas/__tests__/pointer-machine.test.ts +3358 -0
- package/src/video/timeline/canvas/__tests__/snap.test.ts +257 -0
- package/src/video/timeline/canvas/__tests__/viewport.test.ts +399 -0
- package/src/video/timeline/canvas/__tests__/waveforms.test.ts +946 -0
- package/src/video/timeline/canvas/clip-bands.ts +56 -0
- package/src/video/timeline/canvas/draw.ts +2187 -0
- package/src/video/timeline/canvas/fade-curve.ts +111 -0
- package/src/video/timeline/canvas/filmstrips.ts +418 -0
- package/src/video/timeline/canvas/hit-test.ts +501 -0
- package/src/video/timeline/canvas/keyframe-strip.ts +73 -0
- package/src/video/timeline/canvas/pointer-machine.ts +1828 -0
- package/src/video/timeline/canvas/snap.ts +232 -0
- package/src/video/timeline/canvas/viewport.ts +457 -0
- package/src/video/timeline/canvas/waveforms.ts +664 -0
- package/src/video/timeline/makeCaptionEdit.ts +5 -1
- package/src/video/timeline/multiSelectOps.ts +214 -55
- package/src/video/timeline/placement.ts +282 -0
- package/src/video/timeline/timeline-model.ts +919 -0
- package/src/video/timeline/useItemDragDrop.ts +151 -178
- package/src/video/timeline/useTimelineZoom.ts +25 -60
- package/src/video/timeline/utils.ts +0 -12
- package/src/video/use-report-context.ts +75 -0
- package/src/video/preview/OverlayPropsModal.tsx +0 -292
- package/src/video/preview/__tests__/OverlayPropsModal.test.tsx +0 -32
- package/src/video/timeline/AudioTrackRow.tsx +0 -404
- package/src/video/timeline/AudioWaveformLayer.tsx +0 -117
- package/src/video/timeline/CaptionTrackRow.tsx +0 -235
- package/src/video/timeline/PlayheadLine.tsx +0 -18
- package/src/video/timeline/TranscriptModal.tsx +0 -70
- package/src/video/timeline/TranscriptPanel.tsx +0 -273
- package/src/video/timeline/VisualTrackRow.tsx +0 -300
- package/src/video/timeline/__tests__/CaptionTrackRow.test.tsx +0 -241
- package/src/video/timeline/__tests__/PlayheadLine.test.tsx +0 -60
- package/src/video/timeline/__tests__/TranscriptModal.test.tsx +0 -41
- package/src/video/timeline/__tests__/TranscriptPanel.test.tsx +0 -184
- package/src/video/timeline/__tests__/useItemDragDrop.test.ts +0 -72
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
/// <reference types="vitest/globals" />
|
|
2
|
+
/**
|
|
3
|
+
* `applyMoveDeltaToSelection` — the rigid group move behind every multi-select
|
|
4
|
+
* drag, now that caption segments join clips and audio bars in the SAME
|
|
5
|
+
* `selectedIds` array.
|
|
6
|
+
*
|
|
7
|
+
* The fixture keeps every kind at a different place on the timeline so a clamp
|
|
8
|
+
* can be attributed to exactly one member:
|
|
9
|
+
*
|
|
10
|
+
* clips c0 0–5, c1 5–10
|
|
11
|
+
* audio a0 1–6
|
|
12
|
+
* captions s0 1–2 (with word timings), s1 3–4, and one segment with no id
|
|
13
|
+
*
|
|
14
|
+
* Word timings are absolute seconds, which is the whole reason they have to
|
|
15
|
+
* travel with the segment rather than being left to a respread.
|
|
16
|
+
*/
|
|
17
|
+
import { describe, it, expect } from 'vitest'
|
|
18
|
+
import type { Project } from '../../../types'
|
|
19
|
+
import { applyMoveDeltaToSelection, deleteSelection } from '../multiSelectOps'
|
|
20
|
+
import { trackItems } from '../timeline-model'
|
|
21
|
+
|
|
22
|
+
const TOTAL_DURATION = 20
|
|
23
|
+
|
|
24
|
+
function project(): Project {
|
|
25
|
+
return {
|
|
26
|
+
id: 'p',
|
|
27
|
+
tracks: [{
|
|
28
|
+
id: 'trk-0',
|
|
29
|
+
items: [
|
|
30
|
+
{ id: 'c0', type: 'video', src: 'a.mp4', start: 0, end: 5 },
|
|
31
|
+
{ id: 'c1', type: 'video', src: 'b.mp4', start: 5, end: 10 },
|
|
32
|
+
],
|
|
33
|
+
}],
|
|
34
|
+
audio: { tracks: [{ id: 'a0', src: 'v.mp3', start: 1, end: 6, lane: 0 }] },
|
|
35
|
+
captions: {
|
|
36
|
+
style: 'pop',
|
|
37
|
+
segments: [
|
|
38
|
+
{
|
|
39
|
+
id: 's0',
|
|
40
|
+
text: 'hello there',
|
|
41
|
+
start: 1,
|
|
42
|
+
end: 2,
|
|
43
|
+
words: [
|
|
44
|
+
{ word: 'hello', start: 1, end: 1.6 },
|
|
45
|
+
{ word: 'there', start: 1.6, end: 2 },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
{ id: 's1', text: 'second', start: 3, end: 4 },
|
|
49
|
+
// No id yet — `backfillCaptionIds` has not run. Can never be selected.
|
|
50
|
+
{ text: 'unminted', start: 8, end: 9 },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
} as unknown as Project
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const segments = (p: Project) => p.captions!.segments
|
|
57
|
+
const seg = (p: Project, id: string) => segments(p).find(s => s.id === id)!
|
|
58
|
+
const visual = (p: Project, id: string) => trackItems(p).flat().find(i => i.id === id)!
|
|
59
|
+
|
|
60
|
+
describe('applyMoveDeltaToSelection — caption segments', () => {
|
|
61
|
+
it('shifts start, end and every word by the same delta', () => {
|
|
62
|
+
const before = project()
|
|
63
|
+
const after = applyMoveDeltaToSelection(before, ['s0'], 0.5, TOTAL_DURATION)
|
|
64
|
+
expect(seg(after, 's0')).toMatchObject({ start: 1.5, end: 2.5 })
|
|
65
|
+
expect(seg(after, 's0').words).toEqual([
|
|
66
|
+
{ word: 'hello', start: 1.5, end: 2.1 },
|
|
67
|
+
{ word: 'there', start: 2.1, end: 2.5 },
|
|
68
|
+
])
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('leaves the text alone — a move is a retime, not an edit', () => {
|
|
72
|
+
const after = applyMoveDeltaToSelection(project(), ['s0'], 0.5, TOTAL_DURATION)
|
|
73
|
+
expect(seg(after, 's0').text).toBe('hello there')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('leaves a segment with no words a segment with no words', () => {
|
|
77
|
+
const after = applyMoveDeltaToSelection(project(), ['s1'], 0.5, TOTAL_DURATION)
|
|
78
|
+
expect(seg(after, 's1')).toMatchObject({ start: 3.5, end: 4.5 })
|
|
79
|
+
expect(seg(after, 's1').words).toBeUndefined()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('does not touch an unselected segment, id-less or not', () => {
|
|
83
|
+
const before = project()
|
|
84
|
+
const after = applyMoveDeltaToSelection(before, ['s0'], 0.5, TOTAL_DURATION)
|
|
85
|
+
// Same object references, not merely equal values.
|
|
86
|
+
expect(segments(after)[1]).toBe(segments(before)[1])
|
|
87
|
+
expect(segments(after)[2]).toBe(segments(before)[2])
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('moves captions and clips together under one delta', () => {
|
|
91
|
+
const after = applyMoveDeltaToSelection(project(), ['c0', 's0'], 1, TOTAL_DURATION)
|
|
92
|
+
expect(visual(after, 'c0')).toMatchObject({ start: 1, end: 6 })
|
|
93
|
+
expect(seg(after, 's0')).toMatchObject({ start: 2, end: 3 })
|
|
94
|
+
// Nothing outside the selection moves.
|
|
95
|
+
expect(visual(after, 'c1')).toMatchObject({ start: 5, end: 10 })
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('counts a caption in the group extent, so one clamp holds the whole body', () => {
|
|
99
|
+
// s0 starts at 1 and c1 at 5, so the CAPTION is the leftmost member: the
|
|
100
|
+
// group can only travel 1s left. Without captions in the extent scan the
|
|
101
|
+
// clamp would allow -5 and drag s0 to -4.
|
|
102
|
+
const after = applyMoveDeltaToSelection(project(), ['c1', 's0'], -5, TOTAL_DURATION)
|
|
103
|
+
expect(seg(after, 's0')).toMatchObject({ start: 0, end: 1 })
|
|
104
|
+
expect(visual(after, 'c1')).toMatchObject({ start: 4, end: 9 })
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('counts a caption at the right-hand extreme too', () => {
|
|
108
|
+
// Push s1's end (4) out to the horizon: the group may travel 16s. Isolated
|
|
109
|
+
// to a project with only s1's caption present — `project()`'s own s0 and
|
|
110
|
+
// id-less segment would otherwise trip the caption-neighbour clamp (see
|
|
111
|
+
// the dedicated describe block below) before this ever reaches the
|
|
112
|
+
// timeline edge under test here.
|
|
113
|
+
const before = { ...project(), captions: { style: 'pop', segments: [{ id: 's1', text: 'second', start: 3, end: 4 }] } } as unknown as Project
|
|
114
|
+
const after = applyMoveDeltaToSelection(before, ['s1'], 99, TOTAL_DURATION)
|
|
115
|
+
expect(seg(after, 's1')).toMatchObject({ start: 19, end: 20 })
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('moves a caption whose id is the only one selected', () => {
|
|
119
|
+
// The shape `applyCaptionMove` leans on when a drag starts on an
|
|
120
|
+
// UNSELECTED caption: a one-id selection still moves. Isolated to a
|
|
121
|
+
// project with only s0's caption present, for the same reason as above —
|
|
122
|
+
// `project()`'s s1 sits only 1s away and would clamp this delta to 1
|
|
123
|
+
// rather than the full 2 this test means to demonstrate.
|
|
124
|
+
const before = { ...project(), captions: { style: 'pop', segments: [{ id: 's0', text: 'hello there', start: 1, end: 2 }] } } as unknown as Project
|
|
125
|
+
const after = applyMoveDeltaToSelection(before, ['s0'], 2, TOTAL_DURATION)
|
|
126
|
+
expect(seg(after, 's0')).toMatchObject({ start: 3, end: 4 })
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('returns the same project reference on a no-op', () => {
|
|
130
|
+
const before = project()
|
|
131
|
+
expect(applyMoveDeltaToSelection(before, ['s0'], 0, TOTAL_DURATION)).toBe(before)
|
|
132
|
+
expect(applyMoveDeltaToSelection(before, [], 1, TOTAL_DURATION)).toBe(before)
|
|
133
|
+
// A selection of ids that aren't in the project at all is also a no-op.
|
|
134
|
+
expect(applyMoveDeltaToSelection(before, ['gone'], 1, TOTAL_DURATION)).toBe(before)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('builds no new captions object when no caption moved', () => {
|
|
138
|
+
const before = project()
|
|
139
|
+
const after = applyMoveDeltaToSelection(before, ['c0'], 1, TOTAL_DURATION)
|
|
140
|
+
expect(after).not.toBe(before)
|
|
141
|
+
expect(after.captions).toBe(before.captions)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('leaves captions undefined on a project that has none', () => {
|
|
145
|
+
const before = { ...project(), captions: undefined } as unknown as Project
|
|
146
|
+
const after = applyMoveDeltaToSelection(before, ['c0'], 1, TOTAL_DURATION)
|
|
147
|
+
expect(after.captions).toBeUndefined()
|
|
148
|
+
expect(visual(after, 'c0')).toMatchObject({ start: 1, end: 6 })
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// ── Caption-neighbour clamp ─────────────────────────────────────────────
|
|
153
|
+
//
|
|
154
|
+
// Captions may never overlap WITHIN A LANE: `activeCaptionSegments`
|
|
155
|
+
// (timeline-core) hands the preview and every render template EVERY live
|
|
156
|
+
// segment, lane-ascending, and a lane is a z-order with no vertical offset of
|
|
157
|
+
// its own — so two segments sharing a lane and an instant paint one straight
|
|
158
|
+
// over the other. A selected caption may therefore not cross a NON-selected
|
|
159
|
+
// SAME-LANE caption's edge, folded into the same lo/hi the timeline-edge clamp
|
|
160
|
+
// already uses (see `captionNeighbourBounds`). The per-lane half of that rule
|
|
161
|
+
// has its own describe block below.
|
|
162
|
+
|
|
163
|
+
function neighbourProject(): Project {
|
|
164
|
+
return {
|
|
165
|
+
id: 'p',
|
|
166
|
+
captions: {
|
|
167
|
+
style: 'pop',
|
|
168
|
+
segments: [
|
|
169
|
+
{ id: 's0', text: 'hello there', start: 1, end: 2 },
|
|
170
|
+
{ id: 's1', text: 'second', start: 3, end: 4 },
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
} as unknown as Project
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
describe('applyMoveDeltaToSelection — caption-neighbour clamp', () => {
|
|
177
|
+
it('does not let a single selected caption cross its previous (left) neighbour', () => {
|
|
178
|
+
// s1 (3–4) may travel left only until its start meets s0's end (2) — a 1s
|
|
179
|
+
// gap — not the -5 requested, and not the -3 a bare timeline-edge clamp
|
|
180
|
+
// (t=0) alone would allow.
|
|
181
|
+
const after = applyMoveDeltaToSelection(neighbourProject(), ['s1'], -5, TOTAL_DURATION)
|
|
182
|
+
expect(seg(after, 's1')).toMatchObject({ start: 2, end: 3 })
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('does not let a single selected caption cross its next (right) neighbour', () => {
|
|
186
|
+
// s0 (1–2) may travel right only until its end meets s1's start (3).
|
|
187
|
+
const after = applyMoveDeltaToSelection(neighbourProject(), ['s0'], 5, TOTAL_DURATION)
|
|
188
|
+
expect(seg(after, 's0')).toMatchObject({ start: 2, end: 3 })
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('preserves duration when the neighbour clamp bites', () => {
|
|
192
|
+
// The clamp shifts the whole delta, not just one edge — a clamped move is
|
|
193
|
+
// still a rigid shift, never a shrink. Pinned against the exact clamped
|
|
194
|
+
// position (not just the duration, which a differently-clamped result —
|
|
195
|
+
// e.g. the timeline-edge clamp alone — would also happen to preserve):
|
|
196
|
+
// duration survives a clamp landing AT the neighbour bound specifically.
|
|
197
|
+
const after = applyMoveDeltaToSelection(neighbourProject(), ['s0'], 5, TOTAL_DURATION)
|
|
198
|
+
const clamped = seg(after, 's0')
|
|
199
|
+
expect(clamped).toMatchObject({ start: 2, end: 3 })
|
|
200
|
+
expect(clamped.end - clamped.start).toBe(1)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('moves an adjacent run of selected captions rigidly, without clamping the pair against each other', () => {
|
|
204
|
+
const before: Project = {
|
|
205
|
+
id: 'p',
|
|
206
|
+
captions: {
|
|
207
|
+
style: 'pop',
|
|
208
|
+
segments: [
|
|
209
|
+
{ id: 'a', text: 'a', start: 1, end: 2 },
|
|
210
|
+
{ id: 'b', text: 'b', start: 2, end: 3 }, // touches a — both selected
|
|
211
|
+
{ id: 'c', text: 'c', start: 4, end: 5 }, // NOT selected — the real stop
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
} as unknown as Project
|
|
215
|
+
|
|
216
|
+
// Small delta, nowhere near c: both move by the full amount and stay
|
|
217
|
+
// exactly as adjacent as they started. If the neighbour search didn't
|
|
218
|
+
// exclude selected ids, b would see a as a "left neighbour" (or vice
|
|
219
|
+
// versa) and refuse to move at all.
|
|
220
|
+
const nudged = applyMoveDeltaToSelection(before, ['a', 'b'], 0.5, TOTAL_DURATION)
|
|
221
|
+
expect(seg(nudged, 'a')).toMatchObject({ start: 1.5, end: 2.5 })
|
|
222
|
+
expect(seg(nudged, 'b')).toMatchObject({ start: 2.5, end: 3.5 })
|
|
223
|
+
|
|
224
|
+
// Large delta: the pair travels together and stops the instant b's end
|
|
225
|
+
// reaches c's start (4) — 1s of the 5 requested — never crossing into c.
|
|
226
|
+
const stopped = applyMoveDeltaToSelection(before, ['a', 'b'], 5, TOTAL_DURATION)
|
|
227
|
+
expect(seg(stopped, 'a')).toMatchObject({ start: 2, end: 3 })
|
|
228
|
+
expect(seg(stopped, 'b')).toMatchObject({ start: 3, end: 4 })
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('leaves a clips-only selection unaffected by any caption, however it overlaps in time', () => {
|
|
232
|
+
const before: Project = {
|
|
233
|
+
id: 'p',
|
|
234
|
+
tracks: [{ id: 'trk-0', items: [{ id: 'c0', type: 'video', src: 'a.mp4', start: 0, end: 5 }] }],
|
|
235
|
+
captions: {
|
|
236
|
+
style: 'pop',
|
|
237
|
+
// Overlaps c0's whole span in time — irrelevant, since no caption is
|
|
238
|
+
// selected, `captionNeighbourBounds` never even looks at it.
|
|
239
|
+
segments: [{ id: 's0', text: 'hello', start: 0.5, end: 4.5 }],
|
|
240
|
+
},
|
|
241
|
+
} as unknown as Project
|
|
242
|
+
const after = applyMoveDeltaToSelection(before, ['c0'], 10, TOTAL_DURATION)
|
|
243
|
+
expect(visual(after, 'c0')).toMatchObject({ start: 10, end: 15 })
|
|
244
|
+
})
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
// ── The neighbour clamp is PER LANE ────────────────────────────────────────
|
|
248
|
+
//
|
|
249
|
+
// Captions on different rows may overlap and all render — that is what caption
|
|
250
|
+
// rows are for — so only a segment sharing the dragged one's lane is a bound.
|
|
251
|
+
// A track-wide clamp (what this was before caption lanes existed) would stop a
|
|
252
|
+
// lane-0 caption dead at a lane-1 caption it can legally pass straight under.
|
|
253
|
+
|
|
254
|
+
describe('applyMoveDeltaToSelection — the caption-neighbour clamp is per lane', () => {
|
|
255
|
+
function twoLanes(): Project {
|
|
256
|
+
return {
|
|
257
|
+
id: 'p',
|
|
258
|
+
captions: {
|
|
259
|
+
style: 'pop',
|
|
260
|
+
segments: [
|
|
261
|
+
{ id: 'ground', text: 'ground', start: 1, end: 2 }, // lane 0 (absent ⇒ 0)
|
|
262
|
+
{ id: 'upstairs', text: 'upstairs', start: 3, end: 4, lane: 1 },
|
|
263
|
+
],
|
|
264
|
+
},
|
|
265
|
+
} as unknown as Project
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
it('lets a caption pass clean under a caption on another lane', () => {
|
|
269
|
+
// Track-wide, `upstairs` (3–4) would stop `ground` at a delta of 1. Per
|
|
270
|
+
// lane it is not a neighbour at all, so the full 5s delta lands.
|
|
271
|
+
const after = applyMoveDeltaToSelection(twoLanes(), ['ground'], 5, TOTAL_DURATION)
|
|
272
|
+
expect(seg(after, 'ground')).toMatchObject({ start: 6, end: 7 })
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('still stops at a neighbour that DOES share the lane', () => {
|
|
276
|
+
const sameLane = {
|
|
277
|
+
...twoLanes(),
|
|
278
|
+
captions: {
|
|
279
|
+
style: 'pop',
|
|
280
|
+
segments: [
|
|
281
|
+
{ id: 'ground', text: 'ground', start: 1, end: 2 },
|
|
282
|
+
{ id: 'upstairs', text: 'upstairs', start: 3, end: 4 }, // now lane 0 too
|
|
283
|
+
],
|
|
284
|
+
},
|
|
285
|
+
} as unknown as Project
|
|
286
|
+
const after = applyMoveDeltaToSelection(sameLane, ['ground'], 5, TOTAL_DURATION)
|
|
287
|
+
expect(seg(after, 'ground')).toMatchObject({ start: 2, end: 3 })
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('treats an absent lane and an explicit `lane: 0` as the same row', () => {
|
|
291
|
+
// `laneOf` collapses both to 0, so these two must clamp against each other.
|
|
292
|
+
const mixed = {
|
|
293
|
+
id: 'p',
|
|
294
|
+
captions: {
|
|
295
|
+
style: 'pop',
|
|
296
|
+
segments: [
|
|
297
|
+
{ id: 'ground', text: 'ground', start: 1, end: 2 },
|
|
298
|
+
{ id: 'explicit', text: 'explicit', start: 3, end: 4, lane: 0 },
|
|
299
|
+
],
|
|
300
|
+
},
|
|
301
|
+
} as unknown as Project
|
|
302
|
+
expect(seg(applyMoveDeltaToSelection(mixed, ['ground'], 5, TOTAL_DURATION), 'ground'))
|
|
303
|
+
.toMatchObject({ start: 2, end: 3 })
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('takes the TIGHTEST bound across the lanes the selection actually occupies', () => {
|
|
307
|
+
// Two selected captions, one per lane, each with its own unselected
|
|
308
|
+
// same-lane neighbour: lane 0 leaves 3s of room, lane 1 leaves 4s. The
|
|
309
|
+
// group moves by the tighter of the two, 3s, and both keep their lanes.
|
|
310
|
+
//
|
|
311
|
+
// `decoy` is the discriminating part: it sits 0.5s to the right of both
|
|
312
|
+
// selected captions but on a lane NEITHER of them is on, so it bounds
|
|
313
|
+
// nothing. Track-wide it was the tightest bound of all and pinned the
|
|
314
|
+
// whole group to 0.5s.
|
|
315
|
+
const before: Project = {
|
|
316
|
+
id: 'p',
|
|
317
|
+
captions: {
|
|
318
|
+
style: 'pop',
|
|
319
|
+
segments: [
|
|
320
|
+
{ id: 'a', text: 'a', start: 1, end: 2 },
|
|
321
|
+
{ id: 'blockA', text: 'blockA', start: 5, end: 6 }, // 3s of room for a
|
|
322
|
+
{ id: 'b', text: 'b', start: 1, end: 2, lane: 1 },
|
|
323
|
+
{ id: 'blockB', text: 'blockB', start: 6, end: 7, lane: 1 }, // 4s of room for b
|
|
324
|
+
{ id: 'decoy', text: 'decoy', start: 2.5, end: 3.5, lane: 2 },
|
|
325
|
+
],
|
|
326
|
+
},
|
|
327
|
+
} as unknown as Project
|
|
328
|
+
const after = applyMoveDeltaToSelection(before, ['a', 'b'], 5, TOTAL_DURATION)
|
|
329
|
+
expect(seg(after, 'a')).toMatchObject({ start: 4, end: 5 })
|
|
330
|
+
expect(seg(after, 'b')).toMatchObject({ start: 4, end: 5 })
|
|
331
|
+
expect(seg(after, 'decoy')).toMatchObject({ start: 2.5, end: 3.5 })
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it('carries `lane` through a group move untouched — a group move is horizontal only', () => {
|
|
335
|
+
const after = applyMoveDeltaToSelection(twoLanes(), ['ground', 'upstairs'], 1, TOTAL_DURATION)
|
|
336
|
+
expect(seg(after, 'ground').lane).toBeUndefined() // absent stays absent, not written to 0
|
|
337
|
+
expect(seg(after, 'upstairs').lane).toBe(1)
|
|
338
|
+
expect(seg(after, 'upstairs')).toMatchObject({ start: 4, end: 5 })
|
|
339
|
+
})
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
// ── A caption overhanging totalDuration ────────────────────────────────────
|
|
343
|
+
//
|
|
344
|
+
// `totalDuration` is derived from clips and audio only (timeline-model.ts), so
|
|
345
|
+
// a caption can legally end past it once clips are trimmed or deleted after
|
|
346
|
+
// captioning. Mirrors the overhang fixture in pointer-machine.test.ts's caption
|
|
347
|
+
// trim describe: `hi` must be floored at 0 so an already-overhanging member
|
|
348
|
+
// can't be pushed further out — an unfloored `hi` goes negative there and
|
|
349
|
+
// (via `Math.min(hi, requestedDelta)`) drags the whole group backwards on a
|
|
350
|
+
// RIGHTWARD drag instead of refusing to move it.
|
|
351
|
+
|
|
352
|
+
const OVERHANG_TOTAL_DURATION = 15
|
|
353
|
+
|
|
354
|
+
function overhangProject(): Project {
|
|
355
|
+
return {
|
|
356
|
+
id: 'p',
|
|
357
|
+
tracks: [{
|
|
358
|
+
id: 'trk-0',
|
|
359
|
+
items: [{ id: 'c0', type: 'video', src: 'a.mp4', start: 0, end: 5 }],
|
|
360
|
+
}],
|
|
361
|
+
captions: {
|
|
362
|
+
style: 'pop',
|
|
363
|
+
segments: [{ id: 'over', text: 'trailing', start: 16, end: 18 }],
|
|
364
|
+
},
|
|
365
|
+
} as unknown as Project
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
describe('applyMoveDeltaToSelection — a caption overhanging totalDuration', () => {
|
|
369
|
+
it('does not move a lone overhanging caption right, and does not teleport it either', () => {
|
|
370
|
+
// Unfloored: lo=-16, hi=15-18=-3, lo>hi is false, so delta clamps to
|
|
371
|
+
// min(-3, 0.5) = -3 — a 3s jump LEFT on a rightward drag. Floored: hi=0,
|
|
372
|
+
// delta clamps to 0.
|
|
373
|
+
const before = overhangProject()
|
|
374
|
+
const after = applyMoveDeltaToSelection(before, ['over'], 0.5, OVERHANG_TOTAL_DURATION)
|
|
375
|
+
expect(after).toBe(before)
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
it('still moves a lone overhanging caption left by the requested amount', () => {
|
|
379
|
+
// hi=0 does not touch `lo`, so a leftward drag is unaffected by the floor.
|
|
380
|
+
const after = applyMoveDeltaToSelection(overhangProject(), ['over'], -2, OVERHANG_TOTAL_DURATION)
|
|
381
|
+
expect(seg(after, 'over')).toMatchObject({ start: 14, end: 16 })
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
it('leaves a clip mixed with an overhanging caption immovable, not dragged backwards', () => {
|
|
385
|
+
// The clip pins lo at 0 (already at t=0) and the overhanging caption pins
|
|
386
|
+
// hi at 0 (already past totalDuration), so the group cannot move either
|
|
387
|
+
// way — a real "can't move" case, not the old sign-flip bug (which also
|
|
388
|
+
// produced delta 0 here, via `lo > hi`, but for the wrong reason: `hi` was
|
|
389
|
+
// -3, not 0).
|
|
390
|
+
const before = overhangProject()
|
|
391
|
+
const after = applyMoveDeltaToSelection(before, ['c0', 'over'], 0.5, OVERHANG_TOTAL_DURATION)
|
|
392
|
+
expect(after).toBe(before)
|
|
393
|
+
})
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
// ── deleteSelection — track grouping survives a prune (Part B) ─────────────
|
|
397
|
+
//
|
|
398
|
+
// `deleteSelection` re-groups its result (`normalizeTrackOrder`) after
|
|
399
|
+
// pruning emptied tracks, because a prune can change a SURVIVING track's
|
|
400
|
+
// coarse kind: a mixed track that loses its only video item becomes
|
|
401
|
+
// overlay-kind, and the canonical video-block/overlay-block stack has to
|
|
402
|
+
// re-absorb it rather than leave it stranded where it used to sit.
|
|
403
|
+
|
|
404
|
+
describe('deleteSelection', () => {
|
|
405
|
+
function trackProject(
|
|
406
|
+
tracks: Array<{ id: string; items: Array<{ id: string; type: string; start: number; end: number }> }>,
|
|
407
|
+
): Project {
|
|
408
|
+
return { id: 'p', tracks } as unknown as Project
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
it('returns the project unchanged when nothing is selected', () => {
|
|
412
|
+
const p = trackProject([{ id: 'trk-0', items: [{ id: 'v0', type: 'video', start: 0, end: 5 }] }])
|
|
413
|
+
expect(deleteSelection(p, [])).toBe(p)
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
it('prunes a track left empty by the delete (baseline, unaffected by grouping)', () => {
|
|
417
|
+
const p = trackProject([
|
|
418
|
+
{ id: 'trk-0', items: [{ id: 'v0', type: 'video', start: 0, end: 5 }] },
|
|
419
|
+
{ id: 'trk-1', items: [{ id: 'o0', type: 'overlay', start: 0, end: 5 }] },
|
|
420
|
+
])
|
|
421
|
+
const next = deleteSelection(p, ['o0'])
|
|
422
|
+
expect(next.tracks!.map(t => t.id)).toEqual(['trk-0'])
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
it('re-groups a track whose coarse kind changed after the prune — losing its only video item drops it out of the video block', () => {
|
|
426
|
+
const p = trackProject([
|
|
427
|
+
{
|
|
428
|
+
id: 'trk-mixed',
|
|
429
|
+
items: [
|
|
430
|
+
{ id: 'v-mixed', type: 'video', start: 0, end: 5 },
|
|
431
|
+
{ id: 'o-mixed', type: 'overlay', start: 0, end: 5 },
|
|
432
|
+
],
|
|
433
|
+
}, // mixed — video-kind today (has a video item), so it's in the video
|
|
434
|
+
// group AHEAD of trk-video, matching their raw array order.
|
|
435
|
+
{ id: 'trk-video', items: [{ id: 'v0', type: 'video', start: 0, end: 5 }] },
|
|
436
|
+
{ id: 'trk-overlay', items: [{ id: 'o0', type: 'overlay', start: 0, end: 5 }] },
|
|
437
|
+
])
|
|
438
|
+
|
|
439
|
+
const next = deleteSelection(p, ['v-mixed'])
|
|
440
|
+
|
|
441
|
+
// trk-mixed lost its only video item and is now overlay-kind, so it
|
|
442
|
+
// drops OUT of the video block — trk-video (still video-kind) moves
|
|
443
|
+
// ahead of it, even though trk-mixed came first in the raw array.
|
|
444
|
+
expect(next.tracks!.map(t => t.id)).toEqual(['trk-video', 'trk-mixed', 'trk-overlay'])
|
|
445
|
+
expect(next.tracks!.find(t => t.id === 'trk-mixed')!.items.map(i => i.id)).toEqual(['o-mixed'])
|
|
446
|
+
})
|
|
447
|
+
})
|