@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,316 @@
|
|
|
1
|
+
/// <reference types="vitest/globals" />
|
|
2
|
+
/**
|
|
3
|
+
* Pending-import ghosts, at the COMPONENT level — the seam between the host's
|
|
4
|
+
* `PendingDrop[]` and the pixels.
|
|
5
|
+
*
|
|
6
|
+
* `pending-drop.test.ts` proves the painter given an already-resolved
|
|
7
|
+
* `PendingDropBand`. What it cannot reach is the half that lives in
|
|
8
|
+
* `TimelineCanvas`: turning a host's `trackIndex` into a row rectangle
|
|
9
|
+
* (`pendingDropBands`), and repainting the overlay when the prop changes. Both
|
|
10
|
+
* are exactly the join a unit test of either side alone would miss — a ghost
|
|
11
|
+
* resolved against the wrong row, or a correct ghost that never gets painted
|
|
12
|
+
* because nothing marked the layer dirty.
|
|
13
|
+
*
|
|
14
|
+
* The redraw case is the one worth being careful about: the overlay layer is
|
|
15
|
+
* repainted only by the clock, the viewport or a gesture, none of which fire
|
|
16
|
+
* here. So the assertion is deliberately before/after — nothing painted on the
|
|
17
|
+
* first pass, a ghost painted after the prop change — because a test that only
|
|
18
|
+
* looked at the first render would pass with the redraw effect deleted.
|
|
19
|
+
*
|
|
20
|
+
* jsdom has no 2D canvas, so `getContext` hands back a RECORDING proxy, one per
|
|
21
|
+
* canvas element: this surface stacks two (content below, overlay above), and a
|
|
22
|
+
* single shared recorder could not tell which layer a ghost landed on.
|
|
23
|
+
*/
|
|
24
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
25
|
+
import { render, act, cleanup } from '@testing-library/react'
|
|
26
|
+
import { createPlaybackClock } from '../../../playback-clock'
|
|
27
|
+
import type { Project, PendingDrop } from '../../../../types'
|
|
28
|
+
import TimelineCanvas from '../TimelineCanvas'
|
|
29
|
+
import { createViewportStore } from '../viewport'
|
|
30
|
+
import { timeToX } from '../viewport'
|
|
31
|
+
import {
|
|
32
|
+
PENDING_DROP_INSET_PX,
|
|
33
|
+
PENDING_DROP_RADIUS_PX,
|
|
34
|
+
RULER_HEIGHT_PX,
|
|
35
|
+
computeTimelineLayout,
|
|
36
|
+
} from '../draw'
|
|
37
|
+
|
|
38
|
+
// ── Recording context, one per canvas element ────────────────────────────
|
|
39
|
+
|
|
40
|
+
interface RecordedCall { method: string; args: unknown[] }
|
|
41
|
+
|
|
42
|
+
interface Recorder {
|
|
43
|
+
calls: RecordedCall[]
|
|
44
|
+
of: (method: string) => RecordedCall[]
|
|
45
|
+
reset: () => void
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const recorders = new Map<HTMLCanvasElement, Recorder>()
|
|
49
|
+
|
|
50
|
+
function recorderFor(canvas: HTMLCanvasElement) {
|
|
51
|
+
let rec = recorders.get(canvas)
|
|
52
|
+
if (!rec) {
|
|
53
|
+
const calls: RecordedCall[] = []
|
|
54
|
+
rec = {
|
|
55
|
+
calls,
|
|
56
|
+
of: (method: string) => calls.filter(c => c.method === method),
|
|
57
|
+
reset: () => { calls.length = 0 },
|
|
58
|
+
}
|
|
59
|
+
recorders.set(canvas, rec)
|
|
60
|
+
}
|
|
61
|
+
return rec
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function contextFor(canvas: HTMLCanvasElement) {
|
|
65
|
+
const rec = recorderFor(canvas)
|
|
66
|
+
const props: Record<string, unknown> = {}
|
|
67
|
+
return new Proxy({}, {
|
|
68
|
+
get(_t, prop: string) {
|
|
69
|
+
if (prop in props) return props[prop]
|
|
70
|
+
if (prop === 'createLinearGradient') return () => ({ addColorStop: () => {} })
|
|
71
|
+
return (...args: unknown[]) => { rec.calls.push({ method: prop, args }) }
|
|
72
|
+
},
|
|
73
|
+
set(_t, prop: string, value: unknown) {
|
|
74
|
+
props[prop] = value
|
|
75
|
+
rec.calls.push({ method: `set:${prop}`, args: [value] })
|
|
76
|
+
return true
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let realGetContext: typeof HTMLCanvasElement.prototype.getContext
|
|
82
|
+
let realGetRect: typeof Element.prototype.getBoundingClientRect
|
|
83
|
+
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
recorders.clear()
|
|
86
|
+
realGetContext = HTMLCanvasElement.prototype.getContext
|
|
87
|
+
realGetRect = Element.prototype.getBoundingClientRect
|
|
88
|
+
HTMLCanvasElement.prototype.getContext = function (this: HTMLCanvasElement) {
|
|
89
|
+
return contextFor(this)
|
|
90
|
+
} as unknown as typeof HTMLCanvasElement.prototype.getContext
|
|
91
|
+
Element.prototype.getBoundingClientRect = function (this: Element) {
|
|
92
|
+
return { x: 0, y: 0, top: 0, left: 0, right: 1000, bottom: 100, width: 1000, height: 100, toJSON: () => ({}) } as DOMRect
|
|
93
|
+
}
|
|
94
|
+
vi.useFakeTimers()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
afterEach(() => {
|
|
98
|
+
cleanup()
|
|
99
|
+
vi.useRealTimers()
|
|
100
|
+
HTMLCanvasElement.prototype.getContext = realGetContext
|
|
101
|
+
Element.prototype.getBoundingClientRect = realGetRect
|
|
102
|
+
recorders.clear()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// Two video tracks, so a `trackIndex` actually chooses between rows that sit at
|
|
106
|
+
// DIFFERENT y — with one row every assertion below would pass by accident.
|
|
107
|
+
const project = {
|
|
108
|
+
id: 'p',
|
|
109
|
+
tracks: [
|
|
110
|
+
{ id: 'trk-0', items: [{ id: 'a0', type: 'video', src: 'a.mp4', start: 0, end: 10, inPoint: 0, outPoint: 10, sourceDuration: 20 }] },
|
|
111
|
+
{ id: 'trk-1', items: [{ id: 'b0', type: 'video', src: 'b.mp4', start: 0, end: 2, inPoint: 0, outPoint: 2, sourceDuration: 20 }] },
|
|
112
|
+
],
|
|
113
|
+
audio: { tracks: [] },
|
|
114
|
+
} as unknown as Project
|
|
115
|
+
|
|
116
|
+
const TOTAL_DURATION = 20
|
|
117
|
+
|
|
118
|
+
function drop(over: Partial<PendingDrop> = {}): PendingDrop {
|
|
119
|
+
return { id: 'pd-1', atTime: 3, durationSec: 2, trackIndex: 0, label: 'C0042.MP4', ...over }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function mount(pendingDrops?: readonly PendingDrop[]) {
|
|
123
|
+
const store = createViewportStore()
|
|
124
|
+
const clock = createPlaybackClock()
|
|
125
|
+
|
|
126
|
+
const props = {
|
|
127
|
+
project,
|
|
128
|
+
clock,
|
|
129
|
+
store,
|
|
130
|
+
totalDuration: TOTAL_DURATION,
|
|
131
|
+
fps: 30,
|
|
132
|
+
selectedIds: [] as string[],
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const utils = render(<TimelineCanvas {...props} pendingDrops={pendingDrops} />)
|
|
136
|
+
act(() => { vi.advanceTimersByTime(32) })
|
|
137
|
+
|
|
138
|
+
const canvases = utils.container.querySelectorAll('canvas')
|
|
139
|
+
const overlay = recorderFor(canvases[1] as HTMLCanvasElement)
|
|
140
|
+
const content = recorderFor(canvases[0] as HTMLCanvasElement)
|
|
141
|
+
|
|
142
|
+
// Pin the scale so x is readable: x = t × 100. Until this lands the surface
|
|
143
|
+
// sits at its own fit scale (1000px / 20s = 50px/s), and the paint that
|
|
144
|
+
// already happened at THAT scale is still in the recorders — so they are
|
|
145
|
+
// cleared between marking the layers dirty and flushing the frame, leaving
|
|
146
|
+
// exactly one settled paint to assert on.
|
|
147
|
+
act(() => { store.set({ pxPerSecond: 100, scrollSeconds: 0, widthPx: 1000 }) })
|
|
148
|
+
overlay.reset()
|
|
149
|
+
content.reset()
|
|
150
|
+
act(() => { vi.advanceTimersByTime(32) })
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
...utils,
|
|
154
|
+
store,
|
|
155
|
+
// The surface stacks content first, overlay second (see TimelineCanvas's
|
|
156
|
+
// JSX) — ghosts belong to the overlay.
|
|
157
|
+
overlay,
|
|
158
|
+
content,
|
|
159
|
+
setPendingDrops(next?: readonly PendingDrop[]) {
|
|
160
|
+
utils.rerender(<TimelineCanvas {...props} pendingDrops={next} />)
|
|
161
|
+
act(() => { vi.advanceTimersByTime(32) })
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Where the painter should start the ghost's rounded path, derived from the
|
|
167
|
+
* layout the canvas itself draws from — never a hard-coded pixel. The fill
|
|
168
|
+
* path opens at `(x + radius, y)`; see `roundRectPath`. */
|
|
169
|
+
function expectedGhostOrigin(trackIdx: number, atTime: number, store: ReturnType<typeof createViewportStore>) {
|
|
170
|
+
const row = computeTimelineLayout(project).rows.find(r => r.trackIdx === trackIdx)
|
|
171
|
+
if (!row) throw new Error(`no row for trackIdx ${trackIdx}`)
|
|
172
|
+
return [
|
|
173
|
+
timeToX(atTime, store.get()) + PENDING_DROP_RADIUS_PX,
|
|
174
|
+
row.y + PENDING_DROP_INSET_PX,
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The first path the ghost lays down. `setLineDash` is unique to the ghost on
|
|
179
|
+
* this layer, so its presence is what proves one was drawn at all. */
|
|
180
|
+
function ghostOrigin(overlay: Recorder): unknown[] | null {
|
|
181
|
+
if (overlay.of('setLineDash').length === 0) return null
|
|
182
|
+
return overlay.of('moveTo')[0]?.args ?? null
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
describe('TimelineCanvas — pending-drop ghosts', () => {
|
|
186
|
+
it('resolves a trackIndex naming a real row to THAT row\'s rectangle', () => {
|
|
187
|
+
// trackIdx 1 is drawn ABOVE trackIdx 0 (rows descend from the top), so
|
|
188
|
+
// this y is nothing like the base row's — the two cases can't be confused.
|
|
189
|
+
const { overlay, store } = mount([drop({ trackIndex: 1 })])
|
|
190
|
+
expect(ghostOrigin(overlay)).toEqual(expectedGhostOrigin(1, 3, store))
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('resolves the other row to ITS rectangle, at a different y', () => {
|
|
194
|
+
const { overlay, store } = mount([drop({ trackIndex: 0 })])
|
|
195
|
+
expect(ghostOrigin(overlay)).toEqual(expectedGhostOrigin(0, 3, store))
|
|
196
|
+
// Guard against the two expectations having collapsed into one value.
|
|
197
|
+
expect(expectedGhostOrigin(0, 3, store)).not.toEqual(expectedGhostOrigin(1, 3, store))
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('falls back to the base video row for trackIndex -1', () => {
|
|
201
|
+
// -1 is "no preference" — the base video row is the LOWEST trackIdx that
|
|
202
|
+
// holds video, which here is trkIdx 0, drawn at the bottom.
|
|
203
|
+
const { overlay, store } = mount([drop({ trackIndex: -1 })])
|
|
204
|
+
expect(ghostOrigin(overlay)).toEqual(expectedGhostOrigin(0, 3, store))
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('falls back to the base video row for a trackIndex past the end, without throwing', () => {
|
|
208
|
+
// The host's list is asynchronous: the project can lose a track between the
|
|
209
|
+
// drop and the ghost being drawn, leaving a trackIndex that names nothing.
|
|
210
|
+
expect(() => {
|
|
211
|
+
const { overlay, store } = mount([drop({ trackIndex: 99 })])
|
|
212
|
+
expect(ghostOrigin(overlay)).toEqual(expectedGhostOrigin(0, 3, store))
|
|
213
|
+
}).not.toThrow()
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('draws a newTrack ghost on a FRESH row — never the base row, never into the ruler', () => {
|
|
217
|
+
// `trackIndex` here is deliberately a value that names no real row (the
|
|
218
|
+
// sentinel `resolveDropTrackIndex` returns for "mint a new track") — the
|
|
219
|
+
// point of `newTrack` is that the renderer must not need `trackIndex` to
|
|
220
|
+
// resolve this at all.
|
|
221
|
+
const { overlay } = mount([drop({ trackIndex: 2, newTrack: true })])
|
|
222
|
+
const origin = ghostOrigin(overlay)
|
|
223
|
+
expect(origin).not.toBeNull()
|
|
224
|
+
const [, y] = origin as [number, number]
|
|
225
|
+
|
|
226
|
+
// Not the base row (trackIdx 0, at the bottom of the video block) — the
|
|
227
|
+
// exact bug this feature fixes: a new-track ghost sitting on top of
|
|
228
|
+
// whatever footage already occupies the base row.
|
|
229
|
+
const baseRowY = computeTimelineLayout(project).rows.find(r => r.trackIdx === 0)!.y + PENDING_DROP_INSET_PX
|
|
230
|
+
expect(y).not.toEqual(baseRowY)
|
|
231
|
+
// Not even the CURRENT top-of-video-block row (trackIdx 1) — this is a
|
|
232
|
+
// genuinely fresh row, one slot further toward the ruler than any row
|
|
233
|
+
// that exists today.
|
|
234
|
+
const topVideoRowY = computeTimelineLayout(project).rows.find(r => r.trackIdx === 1)!.y + PENDING_DROP_INSET_PX
|
|
235
|
+
expect(y).not.toEqual(topVideoRowY)
|
|
236
|
+
// Never intrudes into the ruler strip.
|
|
237
|
+
expect(y).toBeGreaterThanOrEqual(RULER_HEIGHT_PX)
|
|
238
|
+
// This fixture has no overlay/caption rows above the video block, so
|
|
239
|
+
// there is no room for a full extra row without the surface actually
|
|
240
|
+
// growing by one — the phantom clamps flush against the ruler's bottom
|
|
241
|
+
// edge. Still a row of its own, and still never past the ruler.
|
|
242
|
+
expect(y).toEqual(RULER_HEIGHT_PX + PENDING_DROP_INSET_PX)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('ignores newTrack on a drop that already names a real row', () => {
|
|
246
|
+
// `newTrack` absent (or false) is the ordinary case — unchanged from
|
|
247
|
+
// before this field existed.
|
|
248
|
+
const { overlay, store } = mount([drop({ trackIndex: 1 })])
|
|
249
|
+
expect(ghostOrigin(overlay)).toEqual(expectedGhostOrigin(1, 3, store))
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('resolves each drop in a list independently', () => {
|
|
253
|
+
const { overlay, store } = mount([
|
|
254
|
+
drop({ id: 'pd-1', trackIndex: 1, atTime: 3 }),
|
|
255
|
+
drop({ id: 'pd-2', trackIndex: 0, atTime: 6 }),
|
|
256
|
+
])
|
|
257
|
+
// Each band lays two paths (fill, then the dashed outline inset by half the
|
|
258
|
+
// stroke), so the fill origins are the even entries.
|
|
259
|
+
const origins = overlay.of('moveTo').filter((_, i) => i % 2 === 0).map(c => c.args)
|
|
260
|
+
expect(origins).toEqual([
|
|
261
|
+
expectedGhostOrigin(1, 3, store),
|
|
262
|
+
expectedGhostOrigin(0, 6, store),
|
|
263
|
+
])
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('paints no ghost when pendingDrops is absent or empty', () => {
|
|
267
|
+
for (const value of [undefined, [] as PendingDrop[]]) {
|
|
268
|
+
const { overlay } = mount(value)
|
|
269
|
+
expect(overlay.of('setLineDash')).toHaveLength(0)
|
|
270
|
+
cleanup()
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
// ── The redraw wiring ──
|
|
275
|
+
//
|
|
276
|
+
// Nothing else repaints this layer here: the clock never ticks, the viewport
|
|
277
|
+
// is already settled, no gesture runs. So if the prop change did not mark the
|
|
278
|
+
// overlay dirty, the "after" assertion below would still see an empty
|
|
279
|
+
// recorder — which is exactly what makes this a test of the wiring rather
|
|
280
|
+
// than of the first render.
|
|
281
|
+
|
|
282
|
+
it('repaints the overlay when pendingDrops APPEARS on a mounted surface', () => {
|
|
283
|
+
const { overlay, store, setPendingDrops } = mount()
|
|
284
|
+
expect(ghostOrigin(overlay)).toBeNull()
|
|
285
|
+
|
|
286
|
+
overlay.reset()
|
|
287
|
+
setPendingDrops([drop({ trackIndex: 1 })])
|
|
288
|
+
expect(ghostOrigin(overlay)).toEqual(expectedGhostOrigin(1, 3, store))
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('repaints when a ghost MOVES, and the new band is at the new row', () => {
|
|
292
|
+
const { overlay, store, setPendingDrops } = mount([drop({ trackIndex: 1 })])
|
|
293
|
+
expect(ghostOrigin(overlay)).toEqual(expectedGhostOrigin(1, 3, store))
|
|
294
|
+
|
|
295
|
+
overlay.reset()
|
|
296
|
+
setPendingDrops([drop({ trackIndex: 0, atTime: 6 })])
|
|
297
|
+
expect(ghostOrigin(overlay)).toEqual(expectedGhostOrigin(0, 6, store))
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('repaints with the ghost GONE when the host retracts it', () => {
|
|
301
|
+
// The retraction is the half that actually matters in production: a ghost
|
|
302
|
+
// that never comes down outlives the import it stood for.
|
|
303
|
+
const { overlay, setPendingDrops } = mount([drop()])
|
|
304
|
+
expect(ghostOrigin(overlay)).not.toBeNull()
|
|
305
|
+
|
|
306
|
+
overlay.reset()
|
|
307
|
+
setPendingDrops([])
|
|
308
|
+
expect(overlay.of('clearRect').length).toBeGreaterThan(0)
|
|
309
|
+
expect(overlay.of('setLineDash')).toHaveLength(0)
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('keeps ghosts off the CONTENT layer, which clips and filmstrips own', () => {
|
|
313
|
+
const { content } = mount([drop()])
|
|
314
|
+
expect(content.of('setLineDash')).toHaveLength(0)
|
|
315
|
+
})
|
|
316
|
+
})
|