@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,134 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { render, screen, waitFor, fireEvent } from '@testing-library/react'
|
|
3
|
+
import type { CaptionEvent, EditorAdapter, ImageElement, Project, RenderEvent, VersionEntry, WaveformChunk } from '../../types'
|
|
4
|
+
import VideoEditor from '../VideoEditor'
|
|
5
|
+
|
|
6
|
+
// ── T1 — host-driven caption trigger seam ────────────────────────────────────
|
|
7
|
+
//
|
|
8
|
+
// `onRegenerateCaptions` / `captionsGenerating` (VideoEditorProps) let a host
|
|
9
|
+
// run caption generation as its own background job instead of the package's
|
|
10
|
+
// built-in blocking `CaptionRegenModal`. Purely additive: mounts the REAL
|
|
11
|
+
// VideoEditor (classic layout — no `slots.mediaPanel`, so the caption panel
|
|
12
|
+
// renders directly in the right rail, no tab navigation needed) and drives
|
|
13
|
+
// the actual "Regenerate captions" footer button, mirroring
|
|
14
|
+
// VideoEditor.captionDelete.test.tsx's mounting pattern.
|
|
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
|
+
[{ id: 'clip-0', type: 'video', src: 'a.mp4', start: 0, end: 4, inPoint: 0, outPoint: 4 }],
|
|
26
|
+
],
|
|
27
|
+
audio: { tracks: [] },
|
|
28
|
+
assets: [],
|
|
29
|
+
// Non-empty caption track: the "Regenerate captions" footer button (the
|
|
30
|
+
// one under test) only renders once there is at least one segment — an
|
|
31
|
+
// empty track shows a different "Generate captions" empty-state button.
|
|
32
|
+
captions: {
|
|
33
|
+
style: 'clean',
|
|
34
|
+
segments: [{ id: 'cap-0', text: 'hello', start: 0, end: 1, words: [{ word: 'hello', start: 0, end: 1 }] }],
|
|
35
|
+
},
|
|
36
|
+
...overrides,
|
|
37
|
+
} as Project
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function makeFakeAdapter(): EditorAdapter<Project> {
|
|
41
|
+
return {
|
|
42
|
+
loadProject: vi.fn(async () => makeVideoProject()),
|
|
43
|
+
saveProject: vi.fn(async () => {}),
|
|
44
|
+
subscribe: () => () => {},
|
|
45
|
+
render: async function* (): AsyncIterable<RenderEvent> {
|
|
46
|
+
yield { type: 'done', outputPath: '/out.mp4' }
|
|
47
|
+
},
|
|
48
|
+
resolveImageSrc: (el: ImageElement) => el.src,
|
|
49
|
+
compileOverlay: vi.fn(async () => () => null),
|
|
50
|
+
listGlobalOverlays: vi.fn(async () => []),
|
|
51
|
+
listSystemOverlays: vi.fn(async () => []),
|
|
52
|
+
uploadFile: vi.fn(async () => '/path'),
|
|
53
|
+
fileUrl: (path: string) => path,
|
|
54
|
+
listVersionHistory: vi.fn(async (): Promise<VersionEntry[]> => []),
|
|
55
|
+
restoreVersion: vi.fn(async () => makeVideoProject()),
|
|
56
|
+
getWaveformChunks: vi.fn(async (): Promise<WaveformChunk[]> => []),
|
|
57
|
+
resolveCaptionTemplate: (style: string) => `/caption/${style}`,
|
|
58
|
+
getInfo: vi.fn(async () => ({ root_skill_path: undefined })),
|
|
59
|
+
// Present in BOTH tests below, deliberately: the host-prop test proves
|
|
60
|
+
// `onRegenerateCaptions` wins even though `adapter.generateCaptions` is
|
|
61
|
+
// also available — a real host that passes the prop still implements
|
|
62
|
+
// this to power the background job it owns (see types.ts's doc comment
|
|
63
|
+
// on `onRegenerateCaptions`).
|
|
64
|
+
generateCaptions: vi.fn(async function* (): AsyncIterable<CaptionEvent> {
|
|
65
|
+
yield { type: 'log', message: 'transcribing…' }
|
|
66
|
+
}),
|
|
67
|
+
} as unknown as EditorAdapter<Project>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
// The caption panel now splits into Style / Captions sub-tabs and defaults
|
|
72
|
+
// to Style; these tests want the transcript + Regenerate trigger visible, so
|
|
73
|
+
// pin the sub-tab to 'captions' (usePersistentState reads this at mount).
|
|
74
|
+
window.localStorage.setItem('montaj.editor.captionPanelTab', JSON.stringify('captions'))
|
|
75
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
76
|
+
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
77
|
+
;(globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver = class {
|
|
78
|
+
observe() {}
|
|
79
|
+
unobserve() {}
|
|
80
|
+
disconnect() {}
|
|
81
|
+
}
|
|
82
|
+
;(globalThis as unknown as { HTMLMediaElement: { prototype: HTMLMediaElement } }).HTMLMediaElement.prototype.play = vi.fn(async () => {}) as never
|
|
83
|
+
;(globalThis as unknown as { HTMLMediaElement: { prototype: HTMLMediaElement } }).HTMLMediaElement.prototype.pause = vi.fn(() => {}) as never
|
|
84
|
+
;(globalThis as unknown as { AudioContext: unknown }).AudioContext = class {
|
|
85
|
+
state = 'running'
|
|
86
|
+
createGain() { return { gain: { value: 1 }, connect() {}, disconnect() {} } }
|
|
87
|
+
createMediaElementSource() { return { connect() {}, disconnect() {} } }
|
|
88
|
+
get destination() { return {} }
|
|
89
|
+
close() {}
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
afterEach(() => vi.restoreAllMocks())
|
|
93
|
+
|
|
94
|
+
describe('VideoEditor — host-driven caption trigger seam', () => {
|
|
95
|
+
it('clicking the trigger calls the host onRegenerateCaptions and never mounts the built-in modal', async () => {
|
|
96
|
+
// Proves a host that owns the caption job (Mel's Hub running it as a
|
|
97
|
+
// background task) gets its callback invoked instead of the package
|
|
98
|
+
// racing its own blocking modal against it.
|
|
99
|
+
const adapter = makeFakeAdapter()
|
|
100
|
+
const onRegenerateCaptions = vi.fn()
|
|
101
|
+
render(
|
|
102
|
+
<VideoEditor
|
|
103
|
+
project={makeVideoProject()}
|
|
104
|
+
adapter={adapter}
|
|
105
|
+
onProjectChange={vi.fn()}
|
|
106
|
+
onRegenerateCaptions={onRegenerateCaptions}
|
|
107
|
+
/>,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
const trigger = await screen.findByText('Regenerate captions')
|
|
111
|
+
fireEvent.click(trigger)
|
|
112
|
+
|
|
113
|
+
expect(onRegenerateCaptions).toHaveBeenCalledTimes(1)
|
|
114
|
+
expect(screen.queryByText('Regenerating captions…')).toBeNull()
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('clicking the trigger still opens the built-in modal when the host prop is absent', async () => {
|
|
118
|
+
// Hub and Los Parceros never pass `onRegenerateCaptions` — this pins the
|
|
119
|
+
// pre-existing blocking-modal path so it can never silently regress.
|
|
120
|
+
const adapter = makeFakeAdapter()
|
|
121
|
+
render(
|
|
122
|
+
<VideoEditor
|
|
123
|
+
project={makeVideoProject()}
|
|
124
|
+
adapter={adapter}
|
|
125
|
+
onProjectChange={vi.fn()}
|
|
126
|
+
/>,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
const trigger = await screen.findByText('Regenerate captions')
|
|
130
|
+
fireEvent.click(trigger)
|
|
131
|
+
|
|
132
|
+
await waitFor(() => expect(screen.getByText('Regenerating captions…')).toBeTruthy())
|
|
133
|
+
})
|
|
134
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { geometryAt, geometryFor } from '@bycrux/timeline-core'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SP9d — clips animate their geometry in the preview, and their opacity does not.
|
|
6
|
+
*
|
|
7
|
+
* OverlayItemsLayer resolves an item's live transform through the same two
|
|
8
|
+
* timeline-core calls asserted here; this pins the RULE those calls encode, which
|
|
9
|
+
* is the part that must not drift from the renderer.
|
|
10
|
+
*/
|
|
11
|
+
describe('SP9d — preview geometry for clips', () => {
|
|
12
|
+
const clip = {
|
|
13
|
+
id: 'v1', type: 'video' as const, src: '/tmp/a.mp4', start: 0, end: 3,
|
|
14
|
+
scale: 0.75, offsetX: 0, offsetY: 0, opacity: 0.4,
|
|
15
|
+
keyframes: [
|
|
16
|
+
{ prop: 'offsetX' as const, points: [{ t: 0, value: -20 }, { t: 3, value: 20 }] },
|
|
17
|
+
{ prop: 'scale' as const, points: [{ t: 0, value: 0.5 }, { t: 3, value: 1 }] },
|
|
18
|
+
{ prop: 'rotation' as const, points: [{ t: 0, value: 0 }, { t: 3, value: 30 }] },
|
|
19
|
+
{ prop: 'opacity' as const, points: [{ t: 0, value: 0 }, { t: 3, value: 1 }] },
|
|
20
|
+
],
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Exactly what OverlayItemsLayer computes for a non-overlay item.
|
|
24
|
+
const previewGeometry = (t: number) => ({
|
|
25
|
+
...geometryAt(clip, 'video', t - clip.start),
|
|
26
|
+
opacity: geometryFor(clip, 'video').opacity,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('position, scale and rotation all advance with the playhead', () => {
|
|
30
|
+
const a = previewGeometry(0)
|
|
31
|
+
const b = previewGeometry(1.5)
|
|
32
|
+
const c = previewGeometry(3)
|
|
33
|
+
expect(a.offsetX).toBeLessThan(b.offsetX)
|
|
34
|
+
expect(b.offsetX).toBeLessThan(c.offsetX)
|
|
35
|
+
expect(a.scale).toBeLessThan(c.scale)
|
|
36
|
+
expect(a.rotation).toBeLessThan(c.rotation)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('opacity does NOT animate — ffmpeg cannot express it, so neither may the preview', () => {
|
|
40
|
+
// The whole point: an opacity curve exists on this clip and is ignored.
|
|
41
|
+
expect(previewGeometry(0).opacity).toBe(0.4)
|
|
42
|
+
expect(previewGeometry(1.5).opacity).toBe(0.4)
|
|
43
|
+
expect(previewGeometry(3).opacity).toBe(0.4)
|
|
44
|
+
// ...while the curve itself would very much have moved.
|
|
45
|
+
expect(geometryAt(clip, 'video', 1.5).opacity).not.toBe(0.4)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('an overlay still animates opacity, because its render path can', () => {
|
|
49
|
+
const ov = { ...clip, type: 'overlay' as const }
|
|
50
|
+
expect(geometryAt(ov, 'overlay', 0).opacity).toBe(0)
|
|
51
|
+
expect(geometryAt(ov, 'overlay', 3).opacity).toBe(1)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('a clip with no keyframes resolves exactly as it always did', () => {
|
|
55
|
+
const plain = { id: 'v2', type: 'video' as const, src: '/tmp/b.mp4', start: 0, end: 3, scale: 0.6, offsetX: 4, offsetY: -2, opacity: 0.9 }
|
|
56
|
+
const g = { ...geometryAt(plain, 'video', 1), opacity: geometryFor(plain, 'video').opacity }
|
|
57
|
+
expect(g).toMatchObject({ scale: 0.6, offsetX: 4, offsetY: -2, opacity: 0.9 })
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
import { describe, it, expect } from 'vitest'
|
|
3
|
+
import src from '../VideoEditor.tsx?raw'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A source-level assertion rather than a render test: VideoEditor mounts the
|
|
7
|
+
* whole editor (engine, canvas timeline, workers) and is not cheaply
|
|
8
|
+
* renderable in jsdom. What matters here is only that the reporter is wired
|
|
9
|
+
* to the real clock and the real selection state — which the source shows
|
|
10
|
+
* directly, and which is exactly what a future refactor could silently break.
|
|
11
|
+
*/
|
|
12
|
+
describe('VideoEditor context reporting', () => {
|
|
13
|
+
it('calls useReportContext', () => {
|
|
14
|
+
expect(src).toMatch(/useReportContext\(/)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('passes the real clock and both selection states', () => {
|
|
18
|
+
const call = src.match(/useReportContext\(\{[\s\S]*?\}\)/)?.[0] ?? ''
|
|
19
|
+
expect(call).toMatch(/clock/)
|
|
20
|
+
expect(call).toMatch(/selectedIds/)
|
|
21
|
+
expect(call).toMatch(/selectedCaptionId/)
|
|
22
|
+
expect(call).toMatch(/adapter/)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
// D1: captions share `selectedIds` with clips/audio now, so `selectedCaptionId`
|
|
26
|
+
// must be DERIVED from that array rather than tracked as its own `useState` —
|
|
27
|
+
// a second selection model is exactly the bug a future refactor could
|
|
28
|
+
// reintroduce silently, since both compile and both satisfy the assertion
|
|
29
|
+
// above.
|
|
30
|
+
it('derives selectedCaptionId from selectedIds instead of its own useState', () => {
|
|
31
|
+
expect(src).not.toMatch(/\[selectedCaptionId, setSelectedCaptionId\]/)
|
|
32
|
+
const decl = src.match(/const selectedCaptionId = [^\n]+/)?.[0] ?? ''
|
|
33
|
+
expect(decl).toMatch(/selectedIds\.find/)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// handleSelectCaption used to keep two selection models mutually exclusive
|
|
37
|
+
// (set the caption id AND clear selectedIds). Under D1 there is one model,
|
|
38
|
+
// so selecting a caption is just replacing selectedIds.
|
|
39
|
+
it('handleSelectCaption sets selectedIds directly, not a separate caption state', () => {
|
|
40
|
+
const fn = src.match(/const handleSelectCaption = useCallback\(\([\s\S]*?\}, \[\]\)/)?.[0] ?? ''
|
|
41
|
+
expect(fn).toMatch(/setSelectedIds/)
|
|
42
|
+
expect(fn).not.toMatch(/setSelectedCaptionId/)
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
import { describe, it, expect } from 'vitest'
|
|
3
|
+
import src from '../VideoEditor.tsx?raw'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Source-level, same rationale as VideoEditor.context.test.tsx: VideoEditor
|
|
7
|
+
* mounts the whole editor (engine, canvas timeline, workers) and is not
|
|
8
|
+
* cheaply renderable in jsdom, so the closure logic here can't be exercised
|
|
9
|
+
* through a real double-click. The three things that DO need a live DOM or a
|
|
10
|
+
* plain function call — "a canvas double-click calls onEditCaption(id)", "a
|
|
11
|
+
* changed editFocusId nonce scrolls/focuses the sidebar row", and "the nonce
|
|
12
|
+
* actually climbs off the previous request, not a fixed value" — are already
|
|
13
|
+
* covered where they live: TimelineCanvas.pointer.test.tsx, CaptionListPanel's
|
|
14
|
+
* own `editFocusId` tests, and CaptionListPanel.test.tsx's `nextEditFocus`
|
|
15
|
+
* suite (the increment is a pure exported function specifically so that last
|
|
16
|
+
* one can be proven behaviourally instead of by matching source text, which
|
|
17
|
+
* only pins formatting and would miss a wrong-operator bug). What's unique to
|
|
18
|
+
* VideoEditor, and only lives here, is the bridge between them: that
|
|
19
|
+
* `handleEditCaption` actually calls both `handleSelectCaption` and
|
|
20
|
+
* `nextEditFocus` rather than reimplementing either inline.
|
|
21
|
+
*/
|
|
22
|
+
describe('VideoEditor caption edit-focus bridge (Phase 6)', () => {
|
|
23
|
+
it('imports nextEditFocus from CaptionListPanel — the same shared helper the composition check below points at', () => {
|
|
24
|
+
expect(src).toMatch(/import CaptionListPanel, \{[^}]*nextEditFocus[^}]*\} from '\.\/CaptionListPanel'/)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('holds editFocusId as its own state, shaped { id, nonce } | null', () => {
|
|
28
|
+
expect(src).toMatch(/const \[editFocusId, setEditFocusId\] = useState<CaptionEditFocusRequest \| null>\(null\)/)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('handleEditCaption both selects the caption and requests a focus', () => {
|
|
32
|
+
const fn = src.match(/const handleEditCaption = useCallback\(\([\s\S]*?\}, \[handleSelectCaption\]\)/)?.[0] ?? ''
|
|
33
|
+
expect(fn).not.toBe('')
|
|
34
|
+
expect(fn).toMatch(/handleSelectCaption\(id\)/)
|
|
35
|
+
expect(fn).toMatch(/setEditFocusId/)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// A composition check, not a match on the increment's arithmetic — that
|
|
39
|
+
// arithmetic is unit-tested behaviourally in CaptionListPanel.test.tsx's
|
|
40
|
+
// `nextEditFocus` suite (`?? 0` vs `?? 1`, `+ 1` present or dropped, etc).
|
|
41
|
+
// What this can honestly prove from source text alone is that
|
|
42
|
+
// `handleEditCaption` actually CALLS the shared helper rather than
|
|
43
|
+
// reimplementing the increment inline — a reformat of `nextEditFocus`'s
|
|
44
|
+
// body won't break this the way matching its expression verbatim would.
|
|
45
|
+
it('derives the next focus request via the shared nextEditFocus helper, not an inline reimplementation', () => {
|
|
46
|
+
const fn = src.match(/const handleEditCaption = useCallback\(\([\s\S]*?\}, \[handleSelectCaption\]\)/)?.[0] ?? ''
|
|
47
|
+
expect(fn).toMatch(/setEditFocusId\(prev => nextEditFocus\(prev, id\)\)/)
|
|
48
|
+
expect(fn).not.toMatch(/prev\?\.nonce/)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('threads onEditCaption into Timeline and editFocusId into the sidebar CaptionListPanel', () => {
|
|
52
|
+
expect(src).toMatch(/onEditCaption=\{handleEditCaption\}/)
|
|
53
|
+
expect(src).toMatch(/editFocusId=\{editFocusId\}/)
|
|
54
|
+
})
|
|
55
|
+
})
|