@bycrux/editor 0.11.2 → 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 +83 -1
- package/src/preview/OverlayPreview.tsx +2 -24
- package/src/schema.ts +146 -3
- 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 +601 -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 +137 -0
- 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 +2229 -243
- 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/imageTone.ts +58 -0
- package/src/video/imageToneExamples.ts +10 -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,159 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach, onTestFinished } from 'vitest'
|
|
2
|
+
import { render, waitFor, act } from '@testing-library/react'
|
|
3
|
+
import type { EditorAdapter, Project, RenderEvent, VersionEntry, WaveformChunk } from '../../types'
|
|
4
|
+
import type { ImageElement } from '../../types'
|
|
5
|
+
import VideoEditor from '../VideoEditor'
|
|
6
|
+
import { installCanvasHarness, selectCanvasItem } from '../timeline/__tests__/_canvasSelect'
|
|
7
|
+
|
|
8
|
+
// ── FIX 3 regression ──────────────────────────────────────────────────────────
|
|
9
|
+
// Shift+Delete (VideoEditor.tsx's `handleRippleDelete`) reaches cuts.ts's
|
|
10
|
+
// `rippleDelete`, which reaches `applyCutToCaptions` — the function that DROPS
|
|
11
|
+
// a caption segment outright when it falls entirely inside the removed span,
|
|
12
|
+
// rather than shifting it like `collapseGaps` does. Dropping the only caption
|
|
13
|
+
// in a row leaves a hole lane.
|
|
14
|
+
//
|
|
15
|
+
// IMPORTANT: VideoEditor's own general-purpose lane-normalization effect
|
|
16
|
+
// (mount/SSE/regen/etc, keyed on `sync.project.captions`) ALSO catches and
|
|
17
|
+
// closes that hole shortly afterward — via `sync.applyExternal`, since a
|
|
18
|
+
// ripple-delete isn't a caption drag gesture. So asserting on the LAST
|
|
19
|
+
// `onProjectChange` call (client-side state) passes with or without this fix
|
|
20
|
+
// and proves nothing — `applyExternal` never calls `adapter.saveProject`, only
|
|
21
|
+
// `sync.mutate`/`commit`/`undo`/`redo` do. The actual bug is that the SAVED
|
|
22
|
+
// project — what a reload or another client would see — is whatever
|
|
23
|
+
// `handleRippleDelete`'s own `sync.mutate` call handed to the adapter, which
|
|
24
|
+
// without this fix is the sparse, hole-lane version. So this test asserts on
|
|
25
|
+
// the adapter's `saveProject` payload, not on local `onProjectChange` state.
|
|
26
|
+
// (Verified against a scratchpad revert: without the fix this fails with
|
|
27
|
+
// cap-2 stuck at lane 2 instead of renumbering to lane 1.)
|
|
28
|
+
|
|
29
|
+
function makeVideoProject(overrides: Partial<Project> = {}): Project {
|
|
30
|
+
return {
|
|
31
|
+
id: 'vid-1',
|
|
32
|
+
name: 'Test Video',
|
|
33
|
+
status: 'draft',
|
|
34
|
+
editingPrompt: '',
|
|
35
|
+
projectType: 'video',
|
|
36
|
+
settings: { resolution: [1080, 1920], fps: 30 },
|
|
37
|
+
tracks: [
|
|
38
|
+
[{ id: 'clip-0', type: 'video', src: 'a.mp4', start: 0, end: 4, inPoint: 0, outPoint: 4 }],
|
|
39
|
+
],
|
|
40
|
+
audio: { tracks: [] },
|
|
41
|
+
assets: [],
|
|
42
|
+
...overrides,
|
|
43
|
+
} as Project
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface FakeAdapter extends EditorAdapter<Project> {
|
|
47
|
+
saveCalls: Array<{ id: string; project: Project }>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function makeFakeAdapter(): FakeAdapter {
|
|
51
|
+
let subscribers: Array<(project: Project) => void> = []
|
|
52
|
+
const saveCalls: Array<{ id: string; project: Project }> = []
|
|
53
|
+
return {
|
|
54
|
+
loadProject: vi.fn(async () => makeVideoProject()),
|
|
55
|
+
saveProject: vi.fn(async (id: string, project: Project) => { saveCalls.push({ id, project }) }),
|
|
56
|
+
subscribe: (_id: string, onFrame: (project: Project) => void) => {
|
|
57
|
+
subscribers.push(onFrame)
|
|
58
|
+
return () => { subscribers = subscribers.filter((s) => s !== onFrame) }
|
|
59
|
+
},
|
|
60
|
+
render: async function* (): AsyncIterable<RenderEvent> {
|
|
61
|
+
yield { type: 'done', outputPath: '/out.mp4' }
|
|
62
|
+
},
|
|
63
|
+
resolveImageSrc: (el: ImageElement) => el.src,
|
|
64
|
+
compileOverlay: vi.fn(async () => () => null),
|
|
65
|
+
listGlobalOverlays: vi.fn(async () => []),
|
|
66
|
+
listSystemOverlays: vi.fn(async () => []),
|
|
67
|
+
uploadFile: vi.fn(async () => '/path'),
|
|
68
|
+
fileUrl: (path: string) => path,
|
|
69
|
+
listVersionHistory: vi.fn(async (): Promise<VersionEntry[]> => []),
|
|
70
|
+
restoreVersion: vi.fn(async () => makeVideoProject()),
|
|
71
|
+
getWaveformChunks: vi.fn(async (): Promise<WaveformChunk[]> => []),
|
|
72
|
+
resolveCaptionTemplate: (style: string) => `/caption/${style}`,
|
|
73
|
+
getInfo: vi.fn(async () => ({ root_skill_path: undefined })),
|
|
74
|
+
saveCalls,
|
|
75
|
+
} as unknown as FakeAdapter
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
80
|
+
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
81
|
+
;(globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver = class {
|
|
82
|
+
observe() {}
|
|
83
|
+
unobserve() {}
|
|
84
|
+
disconnect() {}
|
|
85
|
+
}
|
|
86
|
+
;(globalThis as unknown as { HTMLMediaElement: { prototype: HTMLMediaElement } }).HTMLMediaElement.prototype.play = vi.fn(async () => {}) as never
|
|
87
|
+
;(globalThis as unknown as { HTMLMediaElement: { prototype: HTMLMediaElement } }).HTMLMediaElement.prototype.pause = vi.fn(() => {}) as never
|
|
88
|
+
;(globalThis as unknown as { AudioContext: unknown }).AudioContext = class {
|
|
89
|
+
state = 'running'
|
|
90
|
+
createGain() { return { gain: { value: 1 }, connect() {}, disconnect() {} } }
|
|
91
|
+
createMediaElementSource() { return { connect() {}, disconnect() {} } }
|
|
92
|
+
get destination() { return {} }
|
|
93
|
+
close() {}
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
afterEach(() => vi.restoreAllMocks())
|
|
97
|
+
|
|
98
|
+
describe('VideoEditor — ripple-delete densifies caption lanes (FIX 3)', () => {
|
|
99
|
+
it('Shift+Delete on a clip whose span contains the only caption in its row PERSISTS a collapsed, dense row in the same commit', async () => {
|
|
100
|
+
const adapter = makeFakeAdapter()
|
|
101
|
+
const initial = makeVideoProject({
|
|
102
|
+
tracks: [{
|
|
103
|
+
id: 'trk-0',
|
|
104
|
+
items: [
|
|
105
|
+
{ id: 'clip-A', type: 'video', src: 'a.mp4', start: 0, end: 2, inPoint: 0, outPoint: 2 },
|
|
106
|
+
{ id: 'clip-B', type: 'video', src: 'a.mp4', start: 2, end: 4, inPoint: 2, outPoint: 4 },
|
|
107
|
+
],
|
|
108
|
+
}],
|
|
109
|
+
captions: {
|
|
110
|
+
style: 'clean',
|
|
111
|
+
segments: [
|
|
112
|
+
// Outside clip-A's [0,2) span — survives, shifts left with clip-B.
|
|
113
|
+
{ id: 'cap-0', text: 'ground', start: 2.5, end: 3, words: [{ word: 'ground', start: 2.5, end: 3 }] },
|
|
114
|
+
// Entirely inside clip-A's [0,2) span, alone in row 1 — dropped by
|
|
115
|
+
// applyCutToCaptions when clip-A is ripple-deleted.
|
|
116
|
+
{ id: 'cap-1', text: 'middle', start: 0.5, end: 1, lane: 1, words: [{ word: 'middle', start: 0.5, end: 1 }] },
|
|
117
|
+
// Outside clip-A's span, row 2 — survives, shifts left, and should
|
|
118
|
+
// renumber into the row-1 hole cap-1 left behind.
|
|
119
|
+
{ id: 'cap-2', text: 'top', start: 3, end: 3.5, lane: 2, words: [{ word: 'top', start: 3, end: 3.5 }] },
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
} as Partial<Project>)
|
|
123
|
+
|
|
124
|
+
onTestFinished(installCanvasHarness())
|
|
125
|
+
const { container } = render(
|
|
126
|
+
<VideoEditor
|
|
127
|
+
project={initial}
|
|
128
|
+
adapter={adapter}
|
|
129
|
+
onProjectChange={vi.fn()}
|
|
130
|
+
slots={{ exportActions: <div /> }}
|
|
131
|
+
/>,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
// Additive (metaKey) click, matching the DOM version's modifier.
|
|
135
|
+
// `{ type: 'video' }` resolves the FIRST video item in draw order —
|
|
136
|
+
// clip-A — same as the DOM version's `getAllByText('▪ video')[0]`.
|
|
137
|
+
selectCanvasItem(container, initial, { type: 'video' }, { metaKey: true })
|
|
138
|
+
|
|
139
|
+
await act(async () => {
|
|
140
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', shiftKey: true }))
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
await waitFor(() => expect(adapter.saveCalls.length).toBeGreaterThan(0))
|
|
144
|
+
// Give any redundant client-side patch-up effect a chance to also fire a
|
|
145
|
+
// second save — there shouldn't be one; applyExternal never saves.
|
|
146
|
+
await act(async () => { await new Promise((resolve) => setTimeout(resolve, 0)) })
|
|
147
|
+
|
|
148
|
+
const saved = adapter.saveCalls[0].project
|
|
149
|
+
const segs = saved.captions?.segments ?? []
|
|
150
|
+
expect(segs.map((s) => s.id)).toEqual(['cap-0', 'cap-2'])
|
|
151
|
+
// Row 1 (cap-1) is gone; row 2 (cap-2) drops into the vacated row — this
|
|
152
|
+
// is the assertion that fails without the fix (cap-2 stays at lane 2).
|
|
153
|
+
expect(segs.map((s) => s.lane ?? 0)).toEqual([0, 1])
|
|
154
|
+
expect(segs.map((s) => s.start)).toEqual([0.5, 1])
|
|
155
|
+
// Exactly one save for the whole gesture — densifying didn't cost a
|
|
156
|
+
// second commit.
|
|
157
|
+
expect(adapter.saveCalls.length).toBe(1)
|
|
158
|
+
})
|
|
159
|
+
})
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { render, waitFor, act } from '@testing-library/react'
|
|
3
|
+
import type { EditorAdapter, Project, RenderEvent, VersionEntry, WaveformChunk } from '../../types'
|
|
4
|
+
import type { ImageElement } from '../../types'
|
|
5
|
+
import VideoEditor from '../VideoEditor'
|
|
6
|
+
import { createSourcePreviewStore } from '../source-preview'
|
|
7
|
+
|
|
8
|
+
// ── Source-preview overlay (opt-in footage-bin → main-preview scrub) ─────────
|
|
9
|
+
// When a host wires the `sourcePreview` store and sets a value, VideoEditor
|
|
10
|
+
// mounts a paused <video> overlay (its src = the value's url) above PreviewPlayer
|
|
11
|
+
// in the preview stage; clearing the store unmounts it. Absent store → no
|
|
12
|
+
// overlay, and the classic preview is unchanged. Setup mirrors
|
|
13
|
+
// VideoEditor.layout.test.tsx.
|
|
14
|
+
|
|
15
|
+
function makeVideoProject(overrides: Partial<Project> = {}): Project {
|
|
16
|
+
return {
|
|
17
|
+
id: 'vid-1',
|
|
18
|
+
name: 'Test Video',
|
|
19
|
+
status: 'draft',
|
|
20
|
+
editingPrompt: '',
|
|
21
|
+
projectType: 'video',
|
|
22
|
+
settings: { resolution: [1080, 1920], fps: 30 },
|
|
23
|
+
tracks: [
|
|
24
|
+
[
|
|
25
|
+
{ id: 'clip-0', type: 'video', src: 'a.mp4', start: 0, end: 4, inPoint: 0, outPoint: 4 },
|
|
26
|
+
],
|
|
27
|
+
],
|
|
28
|
+
audio: { tracks: [] },
|
|
29
|
+
assets: [],
|
|
30
|
+
...overrides,
|
|
31
|
+
} as Project
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function makeFakeAdapter(): EditorAdapter<Project> {
|
|
35
|
+
let subscribers: Array<(project: Project) => void> = []
|
|
36
|
+
return {
|
|
37
|
+
loadProject: vi.fn(async () => makeVideoProject()),
|
|
38
|
+
saveProject: vi.fn(async () => {}),
|
|
39
|
+
subscribe: (_id: string, onFrame: (project: Project) => void) => {
|
|
40
|
+
subscribers.push(onFrame)
|
|
41
|
+
return () => { subscribers = subscribers.filter((s) => s !== onFrame) }
|
|
42
|
+
},
|
|
43
|
+
render: async function* (): AsyncIterable<RenderEvent> {
|
|
44
|
+
yield { type: 'done', outputPath: '/out.mp4' }
|
|
45
|
+
},
|
|
46
|
+
resolveImageSrc: (el: ImageElement) => el.src,
|
|
47
|
+
compileOverlay: vi.fn(async () => () => null),
|
|
48
|
+
listGlobalOverlays: vi.fn(async () => []),
|
|
49
|
+
listSystemOverlays: vi.fn(async () => []),
|
|
50
|
+
uploadFile: vi.fn(async () => '/path'),
|
|
51
|
+
fileUrl: (path: string) => path,
|
|
52
|
+
listVersionHistory: vi.fn(async (): Promise<VersionEntry[]> => []),
|
|
53
|
+
restoreVersion: vi.fn(async () => makeVideoProject()),
|
|
54
|
+
getWaveformChunks: vi.fn(async (): Promise<WaveformChunk[]> => []),
|
|
55
|
+
resolveCaptionTemplate: (style: string) => `/caption/${style}`,
|
|
56
|
+
getInfo: vi.fn(async () => ({ root_skill_path: undefined })),
|
|
57
|
+
} as unknown as EditorAdapter<Project>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const SCRUB_URL = 'proxy-scrub.mp4'
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
64
|
+
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
65
|
+
;(globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver = class {
|
|
66
|
+
observe() {}
|
|
67
|
+
unobserve() {}
|
|
68
|
+
disconnect() {}
|
|
69
|
+
}
|
|
70
|
+
;(globalThis as unknown as { HTMLMediaElement: { prototype: HTMLMediaElement } }).HTMLMediaElement.prototype.play = vi.fn(async () => {}) as never
|
|
71
|
+
;(globalThis as unknown as { HTMLMediaElement: { prototype: HTMLMediaElement } }).HTMLMediaElement.prototype.pause = vi.fn(() => {}) as never
|
|
72
|
+
;(globalThis as unknown as { AudioContext: unknown }).AudioContext = class {
|
|
73
|
+
state = 'running'
|
|
74
|
+
createGain() { return { gain: { value: 1 }, connect() {}, disconnect() {} } }
|
|
75
|
+
createMediaElementSource() { return { connect() {}, disconnect() {} } }
|
|
76
|
+
get destination() { return {} }
|
|
77
|
+
close() {}
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
afterEach(() => vi.restoreAllMocks())
|
|
81
|
+
|
|
82
|
+
describe('VideoEditor — source-preview overlay', () => {
|
|
83
|
+
it('mounts a <video> overlay with the store value url, and unmounts on clear', async () => {
|
|
84
|
+
const adapter = makeFakeAdapter()
|
|
85
|
+
const store = createSourcePreviewStore()
|
|
86
|
+
const { container, getByLabelText } = render(
|
|
87
|
+
<VideoEditor
|
|
88
|
+
project={makeVideoProject()}
|
|
89
|
+
adapter={adapter}
|
|
90
|
+
onProjectChange={vi.fn()}
|
|
91
|
+
sourcePreview={store}
|
|
92
|
+
/>,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
await waitFor(() => getByLabelText('Editor controls & shortcuts'))
|
|
96
|
+
// No value yet → no scrub overlay.
|
|
97
|
+
expect(container.querySelector(`video[src="${SCRUB_URL}"]`)).toBeNull()
|
|
98
|
+
|
|
99
|
+
// Host hovers a bin card → the overlay appears with that src.
|
|
100
|
+
act(() => { store.set({ url: SCRUB_URL, fraction: 0.5 }) })
|
|
101
|
+
await waitFor(() => expect(container.querySelector(`video[src="${SCRUB_URL}"]`)).toBeTruthy())
|
|
102
|
+
|
|
103
|
+
// Clearing (pointer leaves the card) unmounts the overlay.
|
|
104
|
+
act(() => { store.set(null) })
|
|
105
|
+
await waitFor(() => expect(container.querySelector(`video[src="${SCRUB_URL}"]`)).toBeNull())
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('is inert when no sourcePreview store is supplied (classic path)', async () => {
|
|
109
|
+
const adapter = makeFakeAdapter()
|
|
110
|
+
const { container, getByLabelText } = render(
|
|
111
|
+
<VideoEditor
|
|
112
|
+
project={makeVideoProject()}
|
|
113
|
+
adapter={adapter}
|
|
114
|
+
onProjectChange={vi.fn()}
|
|
115
|
+
/>,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
await waitFor(() => getByLabelText('Editor controls & shortcuts'))
|
|
119
|
+
// Only PreviewPlayer's own slots exist; none carry the scrub url.
|
|
120
|
+
expect(container.querySelector(`video[src="${SCRUB_URL}"]`)).toBeNull()
|
|
121
|
+
})
|
|
122
|
+
})
|