@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,351 @@
|
|
|
1
|
+
/// <reference types="vitest/globals" />
|
|
2
|
+
/**
|
|
3
|
+
* Subcut-regenerate on the CANVAS timeline (the port off the DOM clip rows).
|
|
4
|
+
*
|
|
5
|
+
* The affordance is a Scissors button that toggles Timeline's `subcutClipId`,
|
|
6
|
+
* which in turn opens the host-rendered `renderSubcutRegen` tool. It lived on
|
|
7
|
+
* `VisualTrackRow` and had zero tests; the canvas draws clips as pixels, so
|
|
8
|
+
* the port is an absolutely-positioned HTML button placed by the same layout +
|
|
9
|
+
* viewport math the painter uses (`CanvasClipChrome` in Timeline.tsx).
|
|
10
|
+
*
|
|
11
|
+
* Two things these tests exist to pin down, because both were lost or
|
|
12
|
+
* simplified in earlier attempts at this port:
|
|
13
|
+
*
|
|
14
|
+
* 1. The gate has FOUR independent conditions — selected, host-enabled,
|
|
15
|
+
* generation provenance present, and a 3-SECOND MINIMUM. Each gets its own
|
|
16
|
+
* test (and the duration gets its boundary tested both ways) so dropping
|
|
17
|
+
* any one of them fails here rather than shipping.
|
|
18
|
+
* 2. "Queued" is a BADGE, not a disable. A queued clip's button stays
|
|
19
|
+
* clickable, and the badge's own visibility does not depend on the button's
|
|
20
|
+
* gate at all.
|
|
21
|
+
*
|
|
22
|
+
* Plus the hazard the structure exists to avoid: `TimelineCanvas` binds
|
|
23
|
+
* `mousedown` on its own container and a mousedown there STARTS A GESTURE, so
|
|
24
|
+
* a button nested inside it would scrub or drag underneath its own click. The
|
|
25
|
+
* chrome is rendered as a SIBLING of the surface instead — the last test here
|
|
26
|
+
* is what proves the canvas never sees the press.
|
|
27
|
+
*/
|
|
28
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
29
|
+
import { render, cleanup, fireEvent, screen, act } from '@testing-library/react'
|
|
30
|
+
import type { Project } from '../../../types'
|
|
31
|
+
import type { VisualItem } from '../../../schema'
|
|
32
|
+
import { createPlaybackClock } from '../../playback-clock'
|
|
33
|
+
import Timeline, { CLIP_CHROME_RIGHT_PAD_PX } from '../Timeline'
|
|
34
|
+
import { computeDerivedTiming } from '../timeline-model'
|
|
35
|
+
import { computeTimelineLayout } from '../canvas/draw'
|
|
36
|
+
import { ZOOM_BUTTON_FACTOR, timeToX, zoomAtPivot } from '../canvas/viewport'
|
|
37
|
+
import {
|
|
38
|
+
SURFACE_WIDTH,
|
|
39
|
+
canvasItemPoint,
|
|
40
|
+
canvasSurface,
|
|
41
|
+
canvasViewport,
|
|
42
|
+
installCanvasHarness,
|
|
43
|
+
} from './_canvasSelect'
|
|
44
|
+
|
|
45
|
+
let uninstallCanvasHarness: () => void
|
|
46
|
+
beforeEach(() => { uninstallCanvasHarness = installCanvasHarness() })
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
cleanup()
|
|
49
|
+
uninstallCanvasHarness()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
/** A 10s ai_video clip on track 0, carrying the frozen `generation` provenance
|
|
53
|
+
* the gate reads. Track 0 on purpose: Timeline's `renderSubcutRegen` guard
|
|
54
|
+
* resolves `subcutClipId` against the first track, so a fixture anywhere else
|
|
55
|
+
* would set the state and render nothing. */
|
|
56
|
+
function makeProject(overrides: Partial<VisualItem> = {}): Project {
|
|
57
|
+
return {
|
|
58
|
+
id: 'p1',
|
|
59
|
+
status: 'draft',
|
|
60
|
+
settings: { resolution: [1080, 1920], fps: 30 },
|
|
61
|
+
tracks: [{
|
|
62
|
+
id: 'trk-0',
|
|
63
|
+
items: [{
|
|
64
|
+
id: 'clip-0', type: 'video', src: 'a.mp4',
|
|
65
|
+
start: 0, end: 10, inPoint: 0, outPoint: 10,
|
|
66
|
+
generation: { sceneId: 'sc-1', prompt: 'a wide shot', provider: 'kling' },
|
|
67
|
+
...overrides,
|
|
68
|
+
}],
|
|
69
|
+
}],
|
|
70
|
+
} as unknown as Project
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface MountOptions {
|
|
74
|
+
selectedIds?: string[]
|
|
75
|
+
regenEnabled?: boolean
|
|
76
|
+
isClipQueued?: (id: string) => boolean
|
|
77
|
+
withSubcutTool?: boolean
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function mount(project: Project, opts: MountOptions = {}) {
|
|
81
|
+
const clock = createPlaybackClock(0)
|
|
82
|
+
const onSelectIds = vi.fn()
|
|
83
|
+
const renderSubcutRegen = vi.fn(({ clipId }: { clipId: string }) => (
|
|
84
|
+
<div data-testid="subcut-tool">{clipId}</div>
|
|
85
|
+
))
|
|
86
|
+
const utils = render(
|
|
87
|
+
<Timeline
|
|
88
|
+
project={project}
|
|
89
|
+
clock={clock}
|
|
90
|
+
selectedIds={opts.selectedIds ?? ['clip-0']}
|
|
91
|
+
onSelectIds={onSelectIds}
|
|
92
|
+
regenEnabled={opts.regenEnabled ?? true}
|
|
93
|
+
isClipQueued={opts.isClipQueued}
|
|
94
|
+
renderSubcutRegen={opts.withSubcutTool === false ? undefined : renderSubcutRegen}
|
|
95
|
+
/>,
|
|
96
|
+
)
|
|
97
|
+
return { clock, onSelectIds, renderSubcutRegen, ...utils }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The button, by the accessible name it carries in both the DOM and canvas
|
|
101
|
+
* implementations. */
|
|
102
|
+
function subcutButton(): HTMLElement | null {
|
|
103
|
+
return screen.queryByRole('button', { name: 'Subcut regenerate' })
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
describe('Timeline — subcut regenerate on the canvas timeline', () => {
|
|
107
|
+
// ── The gate, one condition at a time ──────────────────────────────────
|
|
108
|
+
|
|
109
|
+
it('shows the button when the clip is selected, regen is enabled, it has generation provenance, and it is at least 3s', () => {
|
|
110
|
+
mount(makeProject())
|
|
111
|
+
expect(subcutButton()).toBeInTheDocument()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('hides it when the clip is not selected', () => {
|
|
115
|
+
mount(makeProject(), { selectedIds: [] })
|
|
116
|
+
expect(subcutButton()).not.toBeInTheDocument()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('hides it when the host has not enabled regeneration', () => {
|
|
120
|
+
mount(makeProject(), { regenEnabled: false })
|
|
121
|
+
expect(subcutButton()).not.toBeInTheDocument()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('hides it on a clip with no generation provenance', () => {
|
|
125
|
+
mount(makeProject({ generation: undefined }))
|
|
126
|
+
expect(subcutButton()).not.toBeInTheDocument()
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// The 3s floor is a real product rule (there is nothing to sub-cut in a
|
|
130
|
+
// shorter clip), so both sides of the boundary are pinned: a simplification
|
|
131
|
+
// that dropped it, or one that used `> 3` instead of `>= 3`, fails here.
|
|
132
|
+
it('hides it just under the 3s floor', () => {
|
|
133
|
+
mount(makeProject({ end: 2.9, outPoint: 2.9 }))
|
|
134
|
+
expect(subcutButton()).not.toBeInTheDocument()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('shows it at exactly 3s', () => {
|
|
138
|
+
mount(makeProject({ end: 3, outPoint: 3 }))
|
|
139
|
+
expect(subcutButton()).toBeInTheDocument()
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// ── The trigger ────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
it('clicking it opens the host subcut tool for that clip', () => {
|
|
145
|
+
const { renderSubcutRegen } = mount(makeProject())
|
|
146
|
+
expect(screen.queryByTestId('subcut-tool')).not.toBeInTheDocument()
|
|
147
|
+
|
|
148
|
+
fireEvent.click(subcutButton()!)
|
|
149
|
+
|
|
150
|
+
expect(renderSubcutRegen).toHaveBeenCalledWith(expect.objectContaining({ clipId: 'clip-0' }))
|
|
151
|
+
expect(screen.getByTestId('subcut-tool')).toHaveTextContent('clip-0')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('clicking it again toggles the tool back off', () => {
|
|
155
|
+
mount(makeProject())
|
|
156
|
+
fireEvent.click(subcutButton()!)
|
|
157
|
+
expect(screen.getByTestId('subcut-tool')).toBeInTheDocument()
|
|
158
|
+
|
|
159
|
+
fireEvent.click(subcutButton()!)
|
|
160
|
+
expect(screen.queryByTestId('subcut-tool')).not.toBeInTheDocument()
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
// ── Queued is a badge, not a disable ───────────────────────────────────
|
|
164
|
+
|
|
165
|
+
it('renders the "queued" badge and leaves the button clickable while queued', () => {
|
|
166
|
+
mount(makeProject(), { isClipQueued: id => id === 'clip-0' })
|
|
167
|
+
|
|
168
|
+
expect(screen.getByText('queued')).toBeInTheDocument()
|
|
169
|
+
const button = subcutButton()!
|
|
170
|
+
expect(button).not.toBeDisabled()
|
|
171
|
+
|
|
172
|
+
// Still opens the tool — a queued clip can be re-opened and re-submitted.
|
|
173
|
+
fireEvent.click(button)
|
|
174
|
+
expect(screen.getByTestId('subcut-tool')).toHaveTextContent('clip-0')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('does not render the badge for a clip the host does not report as queued', () => {
|
|
178
|
+
mount(makeProject(), { isClipQueued: () => false })
|
|
179
|
+
expect(screen.queryByText('queued')).not.toBeInTheDocument()
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
// The badge answers "is this clip in the regen queue?", which is true (and
|
|
183
|
+
// worth showing) whether or not the clip happens to be selected. Its gate is
|
|
184
|
+
// independent of the button's on the DOM rows; keep it that way.
|
|
185
|
+
it('renders the badge independently of the button gate — unselected, regen disabled', () => {
|
|
186
|
+
mount(makeProject(), { selectedIds: [], regenEnabled: false, isClipQueued: () => true })
|
|
187
|
+
expect(screen.getByText('queued')).toBeInTheDocument()
|
|
188
|
+
expect(subcutButton()).not.toBeInTheDocument()
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
// ── Stays pinned to its clip ───────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
// The chrome is a DOM node over a surface that PANS AND RESCALES without
|
|
194
|
+
// re-rendering React (the canvas redraws from an external viewport store).
|
|
195
|
+
// Timeline's existing `useViewportValue` subscription is what re-lays this
|
|
196
|
+
// out; without it the button detaches from its clip on the first zoom.
|
|
197
|
+
it('re-pins the button to the clip after a zoom change', () => {
|
|
198
|
+
const project = makeProject()
|
|
199
|
+
mount(project)
|
|
200
|
+
|
|
201
|
+
const rightBefore = parseFloat(subcutButton()!.parentElement!.style.right)
|
|
202
|
+
// Sanity: the fitted position is the clip's own right edge, inset.
|
|
203
|
+
const fitted = canvasViewport(project)
|
|
204
|
+
expect(rightBefore).toBeCloseTo(
|
|
205
|
+
SURFACE_WIDTH - timeToX(10, fitted) + CLIP_CHROME_RIGHT_PAD_PX, 4,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
fireEvent.click(screen.getByRole('button', { name: 'Zoom in' }))
|
|
209
|
+
|
|
210
|
+
// Recomputed through the production math the zoom button itself uses, so
|
|
211
|
+
// this asserts "follows the viewport", not a hardcoded pixel.
|
|
212
|
+
const { totalDuration } = computeDerivedTiming(project)
|
|
213
|
+
const zoomed = zoomAtPivot(fitted, ZOOM_BUTTON_FACTOR, SURFACE_WIDTH / 2, totalDuration)
|
|
214
|
+
const rightAfter = parseFloat(subcutButton()!.parentElement!.style.right)
|
|
215
|
+
|
|
216
|
+
expect(zoomed.pxPerSecond).toBeGreaterThan(fitted.pxPerSecond) // the zoom really moved
|
|
217
|
+
expect(rightAfter).not.toBeCloseTo(rightBefore, 2)
|
|
218
|
+
expect(rightAfter).toBeCloseTo(
|
|
219
|
+
Math.max(CLIP_CHROME_RIGHT_PAD_PX, zoomed.widthPx - timeToX(10, zoomed) + CLIP_CHROME_RIGHT_PAD_PX), 4,
|
|
220
|
+
)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
// The horizontal pin above only proves `right`/`row.y` line up on ONE axis.
|
|
224
|
+
// `top`/`height` track `row.y`/`row.height` independently — this is the axis
|
|
225
|
+
// that breaks if `layout.rows` ordering ever changes, or if the positioning-
|
|
226
|
+
// context assumption (the chrome's parent IS the surface's box) stops
|
|
227
|
+
// holding — so it gets its own pin against the same production layout the
|
|
228
|
+
// painter draws from.
|
|
229
|
+
it('pins the chrome vertically to its row', () => {
|
|
230
|
+
const project = makeProject()
|
|
231
|
+
mount(project)
|
|
232
|
+
|
|
233
|
+
const row = computeTimelineLayout(project).rows[0]
|
|
234
|
+
const chrome = subcutButton()!.parentElement!
|
|
235
|
+
expect(parseFloat(chrome.style.top)).toBeCloseTo(row.y, 4)
|
|
236
|
+
expect(parseFloat(chrome.style.height)).toBeCloseTo(row.height, 4)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
// The right-anchored box also carries a LEFT bound (`Math.max(0, x0)`) so
|
|
240
|
+
// `overflow-hidden` clips it to the clip's own span — without it, a narrow
|
|
241
|
+
// clip's right-anchored chrome overhangs leftward past the clip's start and
|
|
242
|
+
// reads as the neighbouring clip's badge (see the comment on `left` in
|
|
243
|
+
// Timeline.tsx). Zooming in centered on this clip's own middle pushes its
|
|
244
|
+
// start well past the surface's left edge while its end stays on-screen,
|
|
245
|
+
// which is exactly the shape that exercises the clamp.
|
|
246
|
+
it('clamps the left edge to the surface when the clip starts off-screen to the left', () => {
|
|
247
|
+
const project = makeProject()
|
|
248
|
+
mount(project)
|
|
249
|
+
fireEvent.click(screen.getByRole('button', { name: 'Zoom in' }))
|
|
250
|
+
|
|
251
|
+
// Sanity: production math agrees the clip's UN-clamped left edge is
|
|
252
|
+
// actually negative here — otherwise this assertion would pass trivially.
|
|
253
|
+
const fitted = canvasViewport(project)
|
|
254
|
+
const { totalDuration } = computeDerivedTiming(project)
|
|
255
|
+
const zoomed = zoomAtPivot(fitted, ZOOM_BUTTON_FACTOR, SURFACE_WIDTH / 2, totalDuration)
|
|
256
|
+
expect(timeToX(0, zoomed)).toBeLessThan(0)
|
|
257
|
+
|
|
258
|
+
const chrome = subcutButton()!.parentElement!
|
|
259
|
+
expect(parseFloat(chrome.style.left)).toBe(0)
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
// A clip panned entirely off the surface (`x1 <= 0 || x0 >= widthPx`) should
|
|
263
|
+
// render no chrome at all — not a box clamped to an edge, which is the
|
|
264
|
+
// previous test's case. A second clip, far enough away that zooming in on
|
|
265
|
+
// the first one's own middle pushes the FIRST clip's whole span behind the
|
|
266
|
+
// surface's left edge, is what gives this a real subject: a single isolated
|
|
267
|
+
// clip always straddles a pivot centered on itself, however far you zoom.
|
|
268
|
+
it('does not render chrome for a clip scrolled entirely off the surface', () => {
|
|
269
|
+
const project: Project = {
|
|
270
|
+
id: 'p1',
|
|
271
|
+
status: 'draft',
|
|
272
|
+
settings: { resolution: [1080, 1920], fps: 30 },
|
|
273
|
+
tracks: [{
|
|
274
|
+
id: 'trk-0',
|
|
275
|
+
items: [
|
|
276
|
+
{
|
|
277
|
+
id: 'clip-0', type: 'video', src: 'a.mp4',
|
|
278
|
+
start: 0, end: 10, inPoint: 0, outPoint: 10,
|
|
279
|
+
generation: { sceneId: 'sc-1', prompt: 'a wide shot', provider: 'kling' },
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
id: 'clip-1', type: 'video', src: 'b.mp4',
|
|
283
|
+
start: 500, end: 510, inPoint: 0, outPoint: 10,
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
}],
|
|
287
|
+
} as unknown as Project
|
|
288
|
+
|
|
289
|
+
mount(project, { selectedIds: ['clip-0'] })
|
|
290
|
+
expect(subcutButton()).toBeInTheDocument() // on-screen before any zoom
|
|
291
|
+
|
|
292
|
+
fireEvent.click(screen.getByRole('button', { name: 'Zoom in' }))
|
|
293
|
+
|
|
294
|
+
// Sanity: production math agrees clip-0's span is now entirely left of
|
|
295
|
+
// the surface — this is what gives the cull a real subject to remove.
|
|
296
|
+
const fitted = canvasViewport(project)
|
|
297
|
+
const { totalDuration } = computeDerivedTiming(project)
|
|
298
|
+
const zoomed = zoomAtPivot(fitted, ZOOM_BUTTON_FACTOR, SURFACE_WIDTH / 2, totalDuration)
|
|
299
|
+
expect(timeToX(10, zoomed)).toBeLessThanOrEqual(0)
|
|
300
|
+
|
|
301
|
+
expect(subcutButton()).not.toBeInTheDocument()
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
// ── The event hazard ───────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* `TimelineCanvas` binds `mousedown` on its container element and treats it
|
|
308
|
+
* as the start of a gesture: it focuses the surface, and it attaches
|
|
309
|
+
* document-level `mousemove`/`mouseup` for the life of the press — the
|
|
310
|
+
* `mouseup` being what selects a clip. So "did a gesture start?" is
|
|
311
|
+
* observable three ways, and this asserts all three.
|
|
312
|
+
*
|
|
313
|
+
* Note a React `onMouseDown={e => e.stopPropagation()}` on the button could
|
|
314
|
+
* NOT have prevented this: React delegates synthetic events to its root
|
|
315
|
+
* container, which sits ABOVE the canvas' own native listener on the
|
|
316
|
+
* propagation path, so the canvas would already have run. The fix is
|
|
317
|
+
* structural — the chrome is not a descendant of `[data-timeline-canvas]` —
|
|
318
|
+
* and this test is what holds that structure in place.
|
|
319
|
+
*/
|
|
320
|
+
it('a mousedown on the button does not start a canvas gesture', () => {
|
|
321
|
+
const project = makeProject()
|
|
322
|
+
const { container, clock, onSelectIds } = mount(project)
|
|
323
|
+
const point = canvasItemPoint(project, { id: 'clip-0' })
|
|
324
|
+
const init = { ...point, button: 0, bubbles: true }
|
|
325
|
+
|
|
326
|
+
fireEvent.mouseDown(subcutButton()!, init)
|
|
327
|
+
act(() => { document.dispatchEvent(new MouseEvent('mouseup', init)) })
|
|
328
|
+
|
|
329
|
+
expect(document.activeElement).not.toBe(canvasSurface(container))
|
|
330
|
+
expect(onSelectIds).not.toHaveBeenCalled()
|
|
331
|
+
expect(clock.get()).toBe(0)
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
// Positive control for the test above: the very same press/release, with the
|
|
335
|
+
// very same props and the very same coordinates, dispatched on the SURFACE
|
|
336
|
+
// instead of on the button. It focuses the surface and it reaches the
|
|
337
|
+
// machine's select. Without this the assertion above would also pass if the
|
|
338
|
+
// coordinates were simply dead, or if the canvas had stopped listening.
|
|
339
|
+
it('the same press on the surface itself does start a gesture', () => {
|
|
340
|
+
const project = makeProject()
|
|
341
|
+
const { container, onSelectIds } = mount(project)
|
|
342
|
+
const point = canvasItemPoint(project, { id: 'clip-0' })
|
|
343
|
+
const init = { ...point, button: 0, bubbles: true }
|
|
344
|
+
|
|
345
|
+
fireEvent.mouseDown(canvasSurface(container), init)
|
|
346
|
+
act(() => { document.dispatchEvent(new MouseEvent('mouseup', init)) })
|
|
347
|
+
|
|
348
|
+
expect(document.activeElement).toBe(canvasSurface(container))
|
|
349
|
+
expect(onSelectIds).toHaveBeenCalledTimes(1)
|
|
350
|
+
})
|
|
351
|
+
})
|