@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,338 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach, onTestFinished } from 'vitest'
|
|
2
|
+
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'
|
|
3
|
+
import type { CaptionEvent, EditorAdapter, Project, RenderEvent, VersionEntry, WaveformChunk } from '../../types'
|
|
4
|
+
import type { ImageElement } from '../../types'
|
|
5
|
+
import VideoEditor from '../VideoEditor'
|
|
6
|
+
import { installCanvasHarness, selectCanvasItem, type CanvasItemSelector } from '../timeline/__tests__/_canvasSelect'
|
|
7
|
+
|
|
8
|
+
// ── Layout gating: classic vs. CapCut media-panel branch ─────────────────────
|
|
9
|
+
// ReviewSurface renders the classic layout (preview above a timeline pane, plus
|
|
10
|
+
// the version rail) UNLESS the host supplies `slots.mediaPanel`, which switches
|
|
11
|
+
// it to the CapCut layout: [media | preview | rail] across the top with a
|
|
12
|
+
// full-width timeline strip below. These tests assert the gate both directions —
|
|
13
|
+
// the media panel appears/disappears, and (in CapCut) the timeline strip is a
|
|
14
|
+
// sibling below the top row rather than nested inside the preview column.
|
|
15
|
+
|
|
16
|
+
function makeVideoProject(overrides: Partial<Project> = {}): Project {
|
|
17
|
+
return {
|
|
18
|
+
id: 'vid-1',
|
|
19
|
+
name: 'Test Video',
|
|
20
|
+
status: 'draft',
|
|
21
|
+
editingPrompt: '',
|
|
22
|
+
projectType: 'video',
|
|
23
|
+
settings: { resolution: [1080, 1920], fps: 30 },
|
|
24
|
+
tracks: [
|
|
25
|
+
[
|
|
26
|
+
{ id: 'clip-0', type: 'video', src: 'a.mp4', start: 0, end: 4, inPoint: 0, outPoint: 4 },
|
|
27
|
+
],
|
|
28
|
+
],
|
|
29
|
+
audio: { tracks: [] },
|
|
30
|
+
assets: [],
|
|
31
|
+
...overrides,
|
|
32
|
+
} as Project
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function makeFakeAdapter(): EditorAdapter<Project> {
|
|
36
|
+
let subscribers: Array<(project: Project) => void> = []
|
|
37
|
+
return {
|
|
38
|
+
loadProject: vi.fn(async () => makeVideoProject()),
|
|
39
|
+
saveProject: vi.fn(async () => {}),
|
|
40
|
+
subscribe: (_id: string, onFrame: (project: Project) => void) => {
|
|
41
|
+
subscribers.push(onFrame)
|
|
42
|
+
return () => { subscribers = subscribers.filter((s) => s !== onFrame) }
|
|
43
|
+
},
|
|
44
|
+
render: async function* (): AsyncIterable<RenderEvent> {
|
|
45
|
+
yield { type: 'done', outputPath: '/out.mp4' }
|
|
46
|
+
},
|
|
47
|
+
resolveImageSrc: (el: ImageElement) => el.src,
|
|
48
|
+
compileOverlay: vi.fn(async () => () => null),
|
|
49
|
+
listGlobalOverlays: vi.fn(async () => []),
|
|
50
|
+
listSystemOverlays: vi.fn(async () => []),
|
|
51
|
+
uploadFile: vi.fn(async () => '/path'),
|
|
52
|
+
fileUrl: (path: string) => path,
|
|
53
|
+
listVersionHistory: vi.fn(async (): Promise<VersionEntry[]> => []),
|
|
54
|
+
restoreVersion: vi.fn(async () => makeVideoProject()),
|
|
55
|
+
getWaveformChunks: vi.fn(async (): Promise<WaveformChunk[]> => []),
|
|
56
|
+
resolveCaptionTemplate: (style: string) => `/caption/${style}`,
|
|
57
|
+
getInfo: vi.fn(async () => ({ root_skill_path: undefined })),
|
|
58
|
+
} as unknown as EditorAdapter<Project>
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
// The caption panel now splits into Style / Captions sub-tabs and defaults
|
|
63
|
+
// to Style; these tests select captions from the transcript list, so pin the
|
|
64
|
+
// sub-tab to 'captions' (usePersistentState reads this at mount).
|
|
65
|
+
window.localStorage.setItem('montaj.editor.captionPanelTab', JSON.stringify('captions'))
|
|
66
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
67
|
+
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
68
|
+
;(globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver = class {
|
|
69
|
+
observe() {}
|
|
70
|
+
unobserve() {}
|
|
71
|
+
disconnect() {}
|
|
72
|
+
}
|
|
73
|
+
;(globalThis as unknown as { HTMLMediaElement: { prototype: HTMLMediaElement } }).HTMLMediaElement.prototype.play = vi.fn(async () => {}) as never
|
|
74
|
+
;(globalThis as unknown as { HTMLMediaElement: { prototype: HTMLMediaElement } }).HTMLMediaElement.prototype.pause = vi.fn(() => {}) as never
|
|
75
|
+
;(globalThis as unknown as { AudioContext: unknown }).AudioContext = class {
|
|
76
|
+
state = 'running'
|
|
77
|
+
createGain() { return { gain: { value: 1 }, connect() {}, disconnect() {} } }
|
|
78
|
+
createMediaElementSource() { return { connect() {}, disconnect() {} } }
|
|
79
|
+
get destination() { return {} }
|
|
80
|
+
close() {}
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
afterEach(() => vi.restoreAllMocks())
|
|
84
|
+
|
|
85
|
+
describe('VideoEditor — layout gating (classic vs. CapCut media panel)', () => {
|
|
86
|
+
it('renders the CapCut layout when slots.mediaPanel is provided', async () => {
|
|
87
|
+
const adapter = makeFakeAdapter()
|
|
88
|
+
const { getByTestId, getByLabelText, getByRole } = render(
|
|
89
|
+
<VideoEditor
|
|
90
|
+
project={makeVideoProject()}
|
|
91
|
+
adapter={adapter}
|
|
92
|
+
onProjectChange={vi.fn()}
|
|
93
|
+
slots={{ mediaPanel: <div data-testid="media-panel">Footage bin</div> }}
|
|
94
|
+
/>,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
// The host's media panel is rendered (its content is present). It sits in a
|
|
98
|
+
// Media TAB of the left panel now, and the tabs mount LAZILY, so this test
|
|
99
|
+
// clicks that tab before asserting rather than relying on it happening to
|
|
100
|
+
// be open. It would otherwise pass only incidentally: this fixture has no
|
|
101
|
+
// captions and no `generateCaptions`, so there is no Captions tab for
|
|
102
|
+
// `defaultTabId="captions"` to open and Media is tabs[0] by fallback — and
|
|
103
|
+
// giving this adapter caption support later would break the assertion for
|
|
104
|
+
// reasons that have nothing to do with layout. See
|
|
105
|
+
// VideoEditor.propertiesPanel.test.tsx for the tab behaviour itself.
|
|
106
|
+
fireEvent.click(await waitFor(() => getByRole('tab', { name: 'Media' })))
|
|
107
|
+
const media = await waitFor(() => getByTestId('media-panel'))
|
|
108
|
+
// The CapCut-only media-column resize divider exists.
|
|
109
|
+
expect(getByLabelText('Resize media panel')).toBeTruthy()
|
|
110
|
+
|
|
111
|
+
// Structural: the timeline strip (its Controls button lives in the timeline
|
|
112
|
+
// pane) is a SIBLING below the top row, not nested inside it or the preview.
|
|
113
|
+
const controls = getByLabelText('Editor controls & shortcuts')
|
|
114
|
+
expect(media.contains(controls)).toBe(false)
|
|
115
|
+
expect(controls.contains(media)).toBe(false)
|
|
116
|
+
// Top row (media | preview | rail) comes before the timeline strip in the DOM.
|
|
117
|
+
expect(
|
|
118
|
+
media.compareDocumentPosition(controls) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
119
|
+
).toBeTruthy()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('renders the classic layout (no media panel) when slots.mediaPanel is absent', async () => {
|
|
123
|
+
const adapter = makeFakeAdapter()
|
|
124
|
+
const { queryByTestId, queryByLabelText, getByLabelText } = render(
|
|
125
|
+
<VideoEditor
|
|
126
|
+
project={makeVideoProject()}
|
|
127
|
+
adapter={adapter}
|
|
128
|
+
onProjectChange={vi.fn()}
|
|
129
|
+
/>,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
// The classic timeline pane still renders (its Controls button is present)…
|
|
133
|
+
await waitFor(() => getByLabelText('Editor controls & shortcuts'))
|
|
134
|
+
// …and the CapCut media panel is entirely inert: no media-panel content and
|
|
135
|
+
// no media-column resize divider.
|
|
136
|
+
expect(queryByTestId('media-panel')).toBeNull()
|
|
137
|
+
expect(queryByLabelText('Resize media panel')).toBeNull()
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// ── Right-rail visibility gate (SP5-captions Phase 5) ─────────────────────────
|
|
142
|
+
// The rail (version history / run history / sidebar assets / CaptionListPanel)
|
|
143
|
+
// only mounts when it has something to show. A project with NO captions and NO
|
|
144
|
+
// other rail content, on a host that DOES support `generateCaptions`, must
|
|
145
|
+
// still get a rail — otherwise "Generate captions" (CaptionListPanel's only way to
|
|
146
|
+
// create captions from scratch) is unreachable. The retired bottom
|
|
147
|
+
// TranscriptPanel offered Regenerate unconditionally whenever the host
|
|
148
|
+
// supported it; the rail gate must preserve that reachability now that the
|
|
149
|
+
// control lives in the sidebar instead.
|
|
150
|
+
describe('VideoEditor — right-rail gate includes the regenerate capability', () => {
|
|
151
|
+
it('renders the rail (and a working Regenerate) with no captions and no version/run/assets content, purely because the adapter supports generateCaptions', async () => {
|
|
152
|
+
const project = makeVideoProject() // no `captions` field at all
|
|
153
|
+
const adapter = {
|
|
154
|
+
loadProject: vi.fn(async () => project),
|
|
155
|
+
saveProject: vi.fn(async () => {}),
|
|
156
|
+
subscribe: () => () => {},
|
|
157
|
+
render: async function* (): AsyncIterable<RenderEvent> {
|
|
158
|
+
yield { type: 'done', outputPath: '/out.mp4' }
|
|
159
|
+
},
|
|
160
|
+
resolveImageSrc: (el: ImageElement) => el.src,
|
|
161
|
+
compileOverlay: vi.fn(async () => () => null),
|
|
162
|
+
listGlobalOverlays: vi.fn(async () => []),
|
|
163
|
+
listSystemOverlays: vi.fn(async () => []),
|
|
164
|
+
uploadFile: vi.fn(async () => '/path'),
|
|
165
|
+
fileUrl: (path: string) => path,
|
|
166
|
+
// Deliberately NO listVersionHistory, and no slots below — the rail has
|
|
167
|
+
// nothing else to justify its presence except the regenerate capability.
|
|
168
|
+
resolveCaptionTemplate: (style: string) => `/caption/${style}`,
|
|
169
|
+
getInfo: vi.fn(async () => ({ root_skill_path: undefined })),
|
|
170
|
+
generateCaptions: async function* (): AsyncIterable<CaptionEvent> {
|
|
171
|
+
yield { type: 'log', message: 'transcribing audio…' }
|
|
172
|
+
},
|
|
173
|
+
} as unknown as EditorAdapter<Project>
|
|
174
|
+
|
|
175
|
+
render(
|
|
176
|
+
<VideoEditor
|
|
177
|
+
project={project}
|
|
178
|
+
adapter={adapter}
|
|
179
|
+
onProjectChange={vi.fn()}
|
|
180
|
+
/>,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
// The rail mounted at all — its divider is the structural tell.
|
|
184
|
+
const divider = await waitFor(() => screen.getByLabelText('Resize sidebar'))
|
|
185
|
+
expect(divider).toBeTruthy()
|
|
186
|
+
|
|
187
|
+
// And the trigger is not just present but actually reachable: clicking it
|
|
188
|
+
// opens CaptionRegenModal (asserted via its immediate "Starting
|
|
189
|
+
// transcription…" line, present before any stream event arrives).
|
|
190
|
+
fireEvent.click(screen.getByText('Generate captions'))
|
|
191
|
+
expect(await screen.findByText(/starting transcription/i)).toBeTruthy()
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
// ── primarySelectedId must skip caption ids (FIX 1) ─────────────────────────
|
|
196
|
+
//
|
|
197
|
+
// Under D1, a caption id shares `selectedIds` with clips and audio — selected
|
|
198
|
+
// on the canvas timeline exactly like either, or (as exercised here, since
|
|
199
|
+
// captions have no DOM-mode timeline row of their own any more — see the
|
|
200
|
+
// retired CaptionTrackRow) via a plain click on its row in the sidebar
|
|
201
|
+
// CaptionListPanel. `primarySelectedId` used to be `selectedIds[0]` verbatim,
|
|
202
|
+
// so a caption id at index 0 blanked the selected clip's crop/overlay-edit
|
|
203
|
+
// affordances and scoped Split to an id no track item matches. These are full
|
|
204
|
+
// <VideoEditor> DOM-mode renders, not source-level assertions: selecting a
|
|
205
|
+
// caption via its sidebar row, then additively (metaKey) selecting a clip or
|
|
206
|
+
// overlay block via the ordinary DOM-mode timeline click, is a real user
|
|
207
|
+
// gesture and needs no canvas/engine internals to drive.
|
|
208
|
+
describe('VideoEditor — a caption id ahead of a clip id in selectedIds (D1)', () => {
|
|
209
|
+
function makeMixedProject(): Project {
|
|
210
|
+
return makeVideoProject({
|
|
211
|
+
tracks: [
|
|
212
|
+
[{ id: 'clip-0', type: 'video', src: 'a.mp4', start: 0, end: 4, inPoint: 0, outPoint: 4 }],
|
|
213
|
+
[{ id: 'overlay-1', type: 'overlay', src: 'overlay.jsx', start: 0, end: 4, props: { text: 'Hello' } }],
|
|
214
|
+
],
|
|
215
|
+
captions: { style: 'pop', segments: [{ id: 'cap-0', text: 'caption one', start: 0, end: 1 }] },
|
|
216
|
+
} as unknown as Partial<Project>)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Click the caption's sidebar row (plain, single-select — replaces
|
|
220
|
+
* `selectedIds`), then additively (metaKey) select the given canvas
|
|
221
|
+
* item — `toggleSelection` appends, so the caption id lands FIRST. */
|
|
222
|
+
async function selectCaptionThenAdditively(container: HTMLElement, project: Project, selector: CanvasItemSelector) {
|
|
223
|
+
fireEvent.click(await screen.findByText('caption one'))
|
|
224
|
+
selectCanvasItem(container, project, selector, { metaKey: true })
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function seekTo(seconds: string) {
|
|
228
|
+
fireEvent.click(await screen.findByLabelText('Go to time'))
|
|
229
|
+
const input = await screen.findByPlaceholderText(/mm:ss/)
|
|
230
|
+
fireEvent.change(input, { target: { value: seconds } })
|
|
231
|
+
fireEvent.keyDown(input, { key: 'Enter' })
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
it('resolves cropTarget to the clip, not null, and S still splits it', async () => {
|
|
235
|
+
onTestFinished(installCanvasHarness())
|
|
236
|
+
const adapter = makeFakeAdapter()
|
|
237
|
+
const onProjectChange = vi.fn()
|
|
238
|
+
const project = makeMixedProject()
|
|
239
|
+
const { container } = render(
|
|
240
|
+
<VideoEditor
|
|
241
|
+
project={project}
|
|
242
|
+
adapter={adapter}
|
|
243
|
+
onProjectChange={onProjectChange}
|
|
244
|
+
/>,
|
|
245
|
+
)
|
|
246
|
+
await selectCaptionThenAdditively(container, project, { type: 'video' })
|
|
247
|
+
|
|
248
|
+
// cropTarget resolved past the caption to the clip: the button is
|
|
249
|
+
// enabled, not stuck on "Select a clip to crop".
|
|
250
|
+
expect(screen.getByLabelText('Crop source').hasAttribute('disabled')).toBe(false)
|
|
251
|
+
// selectedOverlayItem correctly stayed null — the resolved item is the
|
|
252
|
+
// video clip, not the overlay, so the right panel's Content/Transform tab
|
|
253
|
+
// strip (`role="group" aria-label="Overlay panel view"`, only drawn when
|
|
254
|
+
// `overlayPropertiesPanel` has a truthy `selectedOverlayItem`) is absent.
|
|
255
|
+
expect(screen.queryByRole('group', { name: 'Overlay panel view' })).toBeNull()
|
|
256
|
+
|
|
257
|
+
// Seek inside the clip (splitting exactly at a clip's start/end is a
|
|
258
|
+
// no-op — see splitAtTime), then Split. Pre-fix, primarySelectedId was
|
|
259
|
+
// 'cap-0': no track item has that id, splitAtTime returned the same
|
|
260
|
+
// project reference, and handleSplit's `if (updated === base) return`
|
|
261
|
+
// swallowed the keypress with no onProjectChange call at all.
|
|
262
|
+
await seekTo('2')
|
|
263
|
+
await act(async () => {
|
|
264
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 's' }))
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
await waitFor(() => {
|
|
268
|
+
const last = onProjectChange.mock.calls[onProjectChange.mock.calls.length - 1][0] as Project
|
|
269
|
+
// `mapTrackItems` normalizes to `VisualTrack[]` ({ id, items }) on the
|
|
270
|
+
// way out, whatever shape the fixture went in with.
|
|
271
|
+
expect(last.tracks?.[0]?.items).toHaveLength(2) // clip-0 split into two
|
|
272
|
+
expect(last.tracks?.[1]?.items).toHaveLength(1) // overlay untouched — Split scoped to the clip only
|
|
273
|
+
})
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('resolves selectedOverlayItem to the overlay, not null, when it (not a clip) is the non-caption member', async () => {
|
|
277
|
+
onTestFinished(installCanvasHarness())
|
|
278
|
+
const adapter = makeFakeAdapter()
|
|
279
|
+
const project = makeMixedProject()
|
|
280
|
+
const { container } = render(
|
|
281
|
+
<VideoEditor
|
|
282
|
+
project={project}
|
|
283
|
+
adapter={adapter}
|
|
284
|
+
onProjectChange={vi.fn()}
|
|
285
|
+
/>,
|
|
286
|
+
)
|
|
287
|
+
await selectCaptionThenAdditively(container, project, { type: 'overlay' })
|
|
288
|
+
|
|
289
|
+
// selectedOverlayItem resolved to the overlay (not null): `overlayPropertiesPanel`
|
|
290
|
+
// (VideoEditor.tsx, shared by both layouts' right rail) only draws its
|
|
291
|
+
// Content/Transform tab strip — `role="group" aria-label="Overlay panel
|
|
292
|
+
// view"` — when `selectedOverlayItem` is truthy; with nothing (or a
|
|
293
|
+
// non-overlay) selected it falls through to OverlayInspector's bare empty
|
|
294
|
+
// state instead. The floating "Edit overlay" Pencil button this test used
|
|
295
|
+
// to probe with is gone (folded into this same panel), but the tab strip
|
|
296
|
+
// proves the same resolution this test is actually about.
|
|
297
|
+
expect(screen.getByRole('group', { name: 'Overlay panel view' })).toBeTruthy()
|
|
298
|
+
// The overlay is not croppable — cropTarget correctly stayed null.
|
|
299
|
+
expect(screen.getByLabelText('Crop source').hasAttribute('disabled')).toBe(true)
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it('a caption-only selection leaves primarySelectedId null — Split then scopes to the MAIN video track only, not every clip under the playhead', async () => {
|
|
303
|
+
onTestFinished(installCanvasHarness())
|
|
304
|
+
const adapter = makeFakeAdapter()
|
|
305
|
+
const onProjectChange = vi.fn()
|
|
306
|
+
render(
|
|
307
|
+
<VideoEditor
|
|
308
|
+
project={makeMixedProject()}
|
|
309
|
+
adapter={adapter}
|
|
310
|
+
onProjectChange={onProjectChange}
|
|
311
|
+
/>,
|
|
312
|
+
)
|
|
313
|
+
fireEvent.click(await screen.findByText('caption one'))
|
|
314
|
+
|
|
315
|
+
expect(screen.getByLabelText('Crop source').hasAttribute('disabled')).toBe(true)
|
|
316
|
+
// primarySelectedId is null for a caption-only selection, so
|
|
317
|
+
// selectedOverlayItem is null too — the Content/Transform tab strip
|
|
318
|
+
// (`role="group" aria-label="Overlay panel view"`) doesn't render.
|
|
319
|
+
expect(screen.queryByRole('group', { name: 'Overlay panel view' })).toBeNull()
|
|
320
|
+
|
|
321
|
+
// primarySelectedId is null (caption-only selection). Split with nothing
|
|
322
|
+
// selected resolves the MAIN video track's clip under the playhead
|
|
323
|
+
// (`trackItems(base)[0]`) and scopes the split to its id — it does NOT
|
|
324
|
+
// razor every track. Passing null straight to `splitAtTime` used to cut
|
|
325
|
+
// the overlay under the playhead too, which was wrong (Sam): nothing
|
|
326
|
+
// selected means the main track only.
|
|
327
|
+
await seekTo('2')
|
|
328
|
+
await act(async () => {
|
|
329
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 's' }))
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
await waitFor(() => {
|
|
333
|
+
const last = onProjectChange.mock.calls[onProjectChange.mock.calls.length - 1][0] as Project
|
|
334
|
+
expect(last.tracks?.[0]?.items).toHaveLength(2) // main video clip split
|
|
335
|
+
expect(last.tracks?.[1]?.items).toHaveLength(1) // overlay untouched — split scoped to the main track
|
|
336
|
+
})
|
|
337
|
+
})
|
|
338
|
+
})
|