@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,397 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
/**
|
|
3
|
+
* CaptionListPanel — the text-styling block inside the "Caption style"
|
|
4
|
+
* subsection (font family, case, letter spacing, line height, alignment).
|
|
5
|
+
*
|
|
6
|
+
* Sits in `__tests__/` while the panel's other tests sit beside the component
|
|
7
|
+
* (`../CaptionListPanel.test.tsx`); the render helpers and fixtures below
|
|
8
|
+
* mirror that file's shape rather than importing from it, so neither test file
|
|
9
|
+
* can silently change the other's setup.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
12
|
+
import { render, screen, fireEvent, cleanup } from '@testing-library/react'
|
|
13
|
+
import type { Project } from '../../types'
|
|
14
|
+
import type { Captions, CaptionSegment } from '../../schema'
|
|
15
|
+
import type { PlaybackClock } from '../playback-clock'
|
|
16
|
+
import CaptionListPanel from '../CaptionListPanel'
|
|
17
|
+
|
|
18
|
+
// The panel's active sub-tab persists to localStorage (usePersistentState).
|
|
19
|
+
// Clear it, then seed 'captions' for consistency with the sibling suites;
|
|
20
|
+
// every test here flips to the Format tab via `expandStyle()` anyway, since
|
|
21
|
+
// the font and text-styling controls live there now (moved off the retired
|
|
22
|
+
// "Style" tab, which split into "Styles" — a preset gallery, see
|
|
23
|
+
// CaptionStyleGallery.test.tsx — and "Format" — these fine controls).
|
|
24
|
+
//
|
|
25
|
+
// No local ResizeObserver stub: the specimen strip observes its own width and
|
|
26
|
+
// jsdom has no ResizeObserver, but src/test-setup.ts now installs a no-op one
|
|
27
|
+
// globally. That also means these tests run the specimen's "measurement never
|
|
28
|
+
// happens" floor-clamp path, which is why the size assertions below read the
|
|
29
|
+
// reported figure rather than a computed px.
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
window.localStorage.clear()
|
|
32
|
+
window.localStorage.setItem('montaj.editor.captionPanelTab', JSON.stringify('captions'))
|
|
33
|
+
})
|
|
34
|
+
afterEach(() => cleanup())
|
|
35
|
+
|
|
36
|
+
Element.prototype.scrollIntoView = vi.fn()
|
|
37
|
+
|
|
38
|
+
// The exact strings FONT_OPTIONS carries for these two entries
|
|
39
|
+
// (src/text/FontPicker.tsx). Hard-coded rather than imported: the whole point
|
|
40
|
+
// of the one-patch assertion below is that the persisted `fontFamily` and
|
|
41
|
+
// `googleFonts` are the REAL pair a template needs, so reading them back out
|
|
42
|
+
// of the same table the component reads would make the test agree with itself.
|
|
43
|
+
const BALOO_2_VALUE = '"Baloo 2", system-ui, sans-serif'
|
|
44
|
+
const BALOO_2_SPEC = 'Baloo+2:wght@400;500;600;700;800'
|
|
45
|
+
const SYSTEM_VALUE = 'system-ui, -apple-system, "Helvetica Neue", sans-serif'
|
|
46
|
+
|
|
47
|
+
const SEGS: CaptionSegment[] = [
|
|
48
|
+
{
|
|
49
|
+
id: 'cap-0',
|
|
50
|
+
text: 'hello world',
|
|
51
|
+
start: 0,
|
|
52
|
+
end: 2,
|
|
53
|
+
words: [
|
|
54
|
+
{ word: 'hello', start: 0, end: 1 },
|
|
55
|
+
{ word: 'world', start: 1, end: 2 },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{ id: 'cap-1', text: 'goodbye now', start: 2, end: 4 },
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
function makeClock(): PlaybackClock {
|
|
62
|
+
return { get: () => 0, set: vi.fn(), subscribe: () => () => {} }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function renderPanel(extra: Partial<Captions> = {}, currentTime = 0, selectedIds: string[] = []) {
|
|
66
|
+
const project = {
|
|
67
|
+
id: 'p1',
|
|
68
|
+
captions: { style: 'pop', fontsize: 46, segments: SEGS, ...extra },
|
|
69
|
+
} as unknown as Project
|
|
70
|
+
const onCaptionEdit = vi.fn()
|
|
71
|
+
const onProjectChange = vi.fn()
|
|
72
|
+
|
|
73
|
+
const view = render(
|
|
74
|
+
<CaptionListPanel
|
|
75
|
+
captionTrack={project.captions}
|
|
76
|
+
project={project}
|
|
77
|
+
currentTime={currentTime}
|
|
78
|
+
selectedIds={selectedIds}
|
|
79
|
+
onSelectCaption={vi.fn()}
|
|
80
|
+
onCaptionSegmentChange={vi.fn()}
|
|
81
|
+
onCaptionEdit={onCaptionEdit}
|
|
82
|
+
onProjectChange={onProjectChange}
|
|
83
|
+
onCaptionSegmentDelete={vi.fn()}
|
|
84
|
+
fps={30}
|
|
85
|
+
clock={makeClock()}
|
|
86
|
+
editFocusId={null}
|
|
87
|
+
/>,
|
|
88
|
+
)
|
|
89
|
+
return { ...view, project, onCaptionEdit, onProjectChange }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function expandStyle() {
|
|
93
|
+
// Font/text-styling controls moved from a collapse toggle, then from a
|
|
94
|
+
// single "Style" tab, onto the "Format" tab; these suites seed 'captions',
|
|
95
|
+
// so this click flips there.
|
|
96
|
+
fireEvent.click(screen.getByRole('button', { name: 'Format' }))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function pickFont(label: string) {
|
|
100
|
+
fireEvent.click(screen.getByLabelText('Font family'))
|
|
101
|
+
fireEvent.click(screen.getByRole('option', { name: label }))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
describe('CaptionListPanel font family', () => {
|
|
105
|
+
// THE assertion this whole control exists for. A family whose font file is
|
|
106
|
+
// never fetched renders as the fallback face in BOTH the editor preview and
|
|
107
|
+
// the export — silently, with no error anywhere — so `fontFamily` and
|
|
108
|
+
// `googleFonts` have to travel together. And in ONE patch, not two: two
|
|
109
|
+
// commits would stack two undo entries and could interleave with another
|
|
110
|
+
// edit landing between them, leaving the pair split. A test that checked
|
|
111
|
+
// only `fontFamily` would pass with the feature fully broken.
|
|
112
|
+
it('writes fontFamily AND googleFonts in a single commit', () => {
|
|
113
|
+
const { onCaptionEdit, onProjectChange } = renderPanel()
|
|
114
|
+
expandStyle()
|
|
115
|
+
pickFont('Baloo 2')
|
|
116
|
+
|
|
117
|
+
expect(onCaptionEdit).toHaveBeenCalledTimes(1)
|
|
118
|
+
const captions = onCaptionEdit.mock.calls[0][0].captions
|
|
119
|
+
expect(captions.fontFamily).toBe(BALOO_2_VALUE)
|
|
120
|
+
expect(captions.googleFonts).toEqual([BALOO_2_SPEC])
|
|
121
|
+
// A font pick is a settled choice, not a drag — no live-preview channel.
|
|
122
|
+
expect(onProjectChange).not.toHaveBeenCalled()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// The System option is the one entry with no `spec`. `[]` is the correct
|
|
126
|
+
// write, not "leave googleFonts alone": a previously picked family's spec
|
|
127
|
+
// would otherwise linger in the project and keep being fetched for a
|
|
128
|
+
// typeface nothing renders in any more.
|
|
129
|
+
it('picking System writes an explicitly empty googleFonts, clearing the previous family spec', () => {
|
|
130
|
+
const { onCaptionEdit } = renderPanel({ fontFamily: BALOO_2_VALUE, googleFonts: [BALOO_2_SPEC] })
|
|
131
|
+
expandStyle()
|
|
132
|
+
pickFont('System')
|
|
133
|
+
|
|
134
|
+
expect(onCaptionEdit).toHaveBeenCalledTimes(1)
|
|
135
|
+
const captions = onCaptionEdit.mock.calls[0][0].captions
|
|
136
|
+
expect(captions.fontFamily).toBe(SYSTEM_VALUE)
|
|
137
|
+
expect(captions.googleFonts).toEqual([])
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('the picker reflects the family already stored on the track', () => {
|
|
141
|
+
renderPanel({ fontFamily: BALOO_2_VALUE })
|
|
142
|
+
expandStyle()
|
|
143
|
+
expect(screen.getByLabelText('Font family').textContent).toContain('Baloo 2')
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe('CaptionListPanel bold toggle', () => {
|
|
148
|
+
it('reads on when fontWeight is absent', () => {
|
|
149
|
+
renderPanel()
|
|
150
|
+
expandStyle()
|
|
151
|
+
expect(screen.getByLabelText('Bold')).toHaveAttribute('aria-pressed', 'true')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('reads off for the explicit off value 400', () => {
|
|
155
|
+
renderPanel({ fontWeight: 400 })
|
|
156
|
+
expandStyle()
|
|
157
|
+
expect(screen.getByLabelText('Bold')).toHaveAttribute('aria-pressed', 'false')
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('reads on for a numeric weight of 600 or more', () => {
|
|
161
|
+
renderPanel({ fontWeight: 700 })
|
|
162
|
+
expandStyle()
|
|
163
|
+
expect(screen.getByLabelText('Bold')).toHaveAttribute('aria-pressed', 'true')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
// schema.ts's `fontWeight` is `number | string`, and CSS accepts the
|
|
167
|
+
// string keywords 'bold'/'bolder' as legal font-weight values.
|
|
168
|
+
// `Number('bold')` is `NaN`, which used to fail the `>= 600` check and show
|
|
169
|
+
// Bold OFF for a project that actually renders bold.
|
|
170
|
+
it.each(['bold', 'bolder', 'Bold'])('reads on for the CSS keyword %s', (kw) => {
|
|
171
|
+
renderPanel({ fontWeight: kw })
|
|
172
|
+
expandStyle()
|
|
173
|
+
expect(screen.getByLabelText('Bold')).toHaveAttribute('aria-pressed', 'true')
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
describe('CaptionListPanel text case', () => {
|
|
178
|
+
it.each([
|
|
179
|
+
['Uppercase', 'uppercase'],
|
|
180
|
+
['Lowercase', 'lowercase'],
|
|
181
|
+
['Capitalize', 'capitalize'],
|
|
182
|
+
])('%s writes captions.textTransform = %s', (label, value) => {
|
|
183
|
+
const { onCaptionEdit } = renderPanel()
|
|
184
|
+
expandStyle()
|
|
185
|
+
fireEvent.click(screen.getByLabelText(label))
|
|
186
|
+
expect(onCaptionEdit).toHaveBeenCalledTimes(1)
|
|
187
|
+
expect(onCaptionEdit.mock.calls[0][0].captions.textTransform).toBe(value)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
// 'none', not an absent key: the patch is spread over the saved captions, so
|
|
191
|
+
// dropping the field would leave the stored 'uppercase' in place and the
|
|
192
|
+
// button would appear to do nothing.
|
|
193
|
+
it('clicking the ACTIVE case again clears it to none', () => {
|
|
194
|
+
const { onCaptionEdit } = renderPanel({ textTransform: 'uppercase' })
|
|
195
|
+
expandStyle()
|
|
196
|
+
const btn = screen.getByLabelText('Uppercase')
|
|
197
|
+
expect(btn).toHaveAttribute('aria-pressed', 'true')
|
|
198
|
+
|
|
199
|
+
fireEvent.click(btn)
|
|
200
|
+
expect(onCaptionEdit).toHaveBeenCalledTimes(1)
|
|
201
|
+
expect(onCaptionEdit.mock.calls[0][0].captions.textTransform).toBe('none')
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('only the stored case reads as pressed', () => {
|
|
205
|
+
renderPanel({ textTransform: 'lowercase' })
|
|
206
|
+
expandStyle()
|
|
207
|
+
expect(screen.getByLabelText('Lowercase')).toHaveAttribute('aria-pressed', 'true')
|
|
208
|
+
expect(screen.getByLabelText('Uppercase')).toHaveAttribute('aria-pressed', 'false')
|
|
209
|
+
expect(screen.getByLabelText('Capitalize')).toHaveAttribute('aria-pressed', 'false')
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
// `outline` renders all-caps by default (its stencil look) even with no
|
|
213
|
+
// stored textTransform at all — the chip has to seed from that default the
|
|
214
|
+
// same way the Bold toggle seeds from an absent fontWeight, or it lies
|
|
215
|
+
// about the caption's actual on-screen case.
|
|
216
|
+
it("a fresh outline caption (no stored textTransform) seeds the Uppercase chip pressed from the style's default", () => {
|
|
217
|
+
renderPanel({ style: 'outline' })
|
|
218
|
+
expandStyle()
|
|
219
|
+
expect(screen.getByLabelText('Uppercase')).toHaveAttribute('aria-pressed', 'true')
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
// An explicit 'none' (written when the operator turns a case back off, see
|
|
223
|
+
// the test above) is not "absent" — it must NOT fall back to the style
|
|
224
|
+
// default.
|
|
225
|
+
it('an explicit "none" on outline overrides the style default — no chip reads pressed', () => {
|
|
226
|
+
renderPanel({ style: 'outline', textTransform: 'none' })
|
|
227
|
+
expandStyle()
|
|
228
|
+
expect(screen.getByLabelText('Uppercase')).toHaveAttribute('aria-pressed', 'false')
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('a style with no default case (pop) reads no case pressed when textTransform is absent', () => {
|
|
232
|
+
renderPanel({ style: 'pop' })
|
|
233
|
+
expandStyle()
|
|
234
|
+
expect(screen.getByLabelText('Uppercase')).toHaveAttribute('aria-pressed', 'false')
|
|
235
|
+
expect(screen.getByLabelText('Lowercase')).toHaveAttribute('aria-pressed', 'false')
|
|
236
|
+
expect(screen.getByLabelText('Capitalize')).toHaveAttribute('aria-pressed', 'false')
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
describe('CaptionListPanel alignment', () => {
|
|
241
|
+
it.each([
|
|
242
|
+
['Align left', 'left'],
|
|
243
|
+
['Align center', 'center'],
|
|
244
|
+
['Align right', 'right'],
|
|
245
|
+
])('%s writes captions.textAlign = %s', (label, value) => {
|
|
246
|
+
const { onCaptionEdit } = renderPanel()
|
|
247
|
+
expandStyle()
|
|
248
|
+
fireEvent.click(screen.getByLabelText(label))
|
|
249
|
+
expect(onCaptionEdit).toHaveBeenCalledTimes(1)
|
|
250
|
+
expect(onCaptionEdit.mock.calls[0][0].captions.textAlign).toBe(value)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('reflects the stored alignment as pressed', () => {
|
|
254
|
+
renderPanel({ textAlign: 'right' })
|
|
255
|
+
expandStyle()
|
|
256
|
+
expect(screen.getByLabelText('Align right')).toHaveAttribute('aria-pressed', 'true')
|
|
257
|
+
expect(screen.getByLabelText('Align left')).toHaveAttribute('aria-pressed', 'false')
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
describe('CaptionListPanel letter spacing and line height steppers', () => {
|
|
262
|
+
// The stored shapes differ on purpose and are what the caption templates
|
|
263
|
+
// read: letter-spacing is a CSS length (unitless is invalid and the template
|
|
264
|
+
// falls back), line-height is a unitless multiple of the font size. The
|
|
265
|
+
// operator types a bare number for both — the `em` is appended here, the
|
|
266
|
+
// same contract FontSizePicker has with `px`.
|
|
267
|
+
it('letter spacing previews live as an em string and commits once on blur', () => {
|
|
268
|
+
const { onProjectChange, onCaptionEdit } = renderPanel()
|
|
269
|
+
expandStyle()
|
|
270
|
+
const input = screen.getByLabelText('Caption letter spacing')
|
|
271
|
+
|
|
272
|
+
fireEvent.change(input, { target: { value: '0.05' } })
|
|
273
|
+
expect(onProjectChange).toHaveBeenCalledTimes(1)
|
|
274
|
+
expect(onProjectChange.mock.calls[0][0].captions.letterSpacing).toBe('0.05em')
|
|
275
|
+
expect(onCaptionEdit).not.toHaveBeenCalled()
|
|
276
|
+
|
|
277
|
+
fireEvent.blur(input)
|
|
278
|
+
expect(onCaptionEdit).toHaveBeenCalledTimes(1)
|
|
279
|
+
expect(onCaptionEdit.mock.calls[0][0].captions.letterSpacing).toBe('0.05em')
|
|
280
|
+
// The commit did not also fire another live preview.
|
|
281
|
+
expect(onProjectChange).toHaveBeenCalledTimes(1)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('letter spacing accepts negatives and shows a stored value without its unit', () => {
|
|
285
|
+
const { onCaptionEdit } = renderPanel({ letterSpacing: '-0.02em' })
|
|
286
|
+
expandStyle()
|
|
287
|
+
const input = screen.getByLabelText('Caption letter spacing')
|
|
288
|
+
expect(input).toHaveValue(-0.02)
|
|
289
|
+
|
|
290
|
+
fireEvent.change(input, { target: { value: '-0.06' } })
|
|
291
|
+
fireEvent.blur(input)
|
|
292
|
+
expect(onCaptionEdit.mock.calls[0][0].captions.letterSpacing).toBe('-0.06em')
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('line height previews live as a unitless NUMBER and commits once on blur', () => {
|
|
296
|
+
const { onProjectChange, onCaptionEdit } = renderPanel()
|
|
297
|
+
expandStyle()
|
|
298
|
+
const input = screen.getByLabelText('Caption line height')
|
|
299
|
+
|
|
300
|
+
fireEvent.change(input, { target: { value: '1.4' } })
|
|
301
|
+
expect(onProjectChange).toHaveBeenCalledTimes(1)
|
|
302
|
+
expect(onProjectChange.mock.calls[0][0].captions.lineHeight).toBe(1.4)
|
|
303
|
+
expect(onCaptionEdit).not.toHaveBeenCalled()
|
|
304
|
+
|
|
305
|
+
fireEvent.blur(input)
|
|
306
|
+
expect(onCaptionEdit).toHaveBeenCalledTimes(1)
|
|
307
|
+
const committed = onCaptionEdit.mock.calls[0][0].captions.lineHeight
|
|
308
|
+
expect(committed).toBe(1.4)
|
|
309
|
+
expect(typeof committed).toBe('number')
|
|
310
|
+
expect(onProjectChange).toHaveBeenCalledTimes(1)
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
// min/max on a number input are a spinner hint only — a typed or pasted
|
|
314
|
+
// value sails straight past them — so the clamp lives on the write path.
|
|
315
|
+
it('clamps a typed value to the control range', () => {
|
|
316
|
+
const { onProjectChange } = renderPanel()
|
|
317
|
+
expandStyle()
|
|
318
|
+
fireEvent.change(screen.getByLabelText('Caption line height'), { target: { value: '9' } })
|
|
319
|
+
expect(onProjectChange.mock.calls[0][0].captions.lineHeight).toBe(2.5)
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
// Focusing a field and leaving it untouched must not write anything: that
|
|
323
|
+
// would be a pointless PUT and an undo entry for a no-op.
|
|
324
|
+
it('a blur with no edit commits nothing', () => {
|
|
325
|
+
const { onProjectChange, onCaptionEdit } = renderPanel({ lineHeight: 1.2 })
|
|
326
|
+
expandStyle()
|
|
327
|
+
const input = screen.getByLabelText('Caption line height')
|
|
328
|
+
fireEvent.focus(input)
|
|
329
|
+
fireEvent.blur(input)
|
|
330
|
+
expect(onCaptionEdit).not.toHaveBeenCalled()
|
|
331
|
+
expect(onProjectChange).not.toHaveBeenCalled()
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
// Both fields read BLANK on a fresh caption track even though the template
|
|
335
|
+
// renders a real, non-default value — the placeholder is what makes the
|
|
336
|
+
// field honest about what's actually on screen, without writing that
|
|
337
|
+
// default into the project as an explicit value.
|
|
338
|
+
it("the letter-spacing field reports the active style's own default as a placeholder, not a stored value", () => {
|
|
339
|
+
renderPanel() // default renderPanel style is 'pop': letterSpacing defaults to -0.02em
|
|
340
|
+
expandStyle()
|
|
341
|
+
const input = screen.getByLabelText('Caption letter spacing') as HTMLInputElement
|
|
342
|
+
expect(input.value).toBe('')
|
|
343
|
+
expect(input).toHaveAttribute('placeholder', '-0.02')
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
it("the line-height field reports the active style's own default as a placeholder, not a stored value", () => {
|
|
347
|
+
renderPanel({ style: 'clean' }) // clean's lineHeight default is 1.3
|
|
348
|
+
expandStyle()
|
|
349
|
+
const input = screen.getByLabelText('Caption line height') as HTMLInputElement
|
|
350
|
+
expect(input.value).toBe('')
|
|
351
|
+
expect(input).toHaveAttribute('placeholder', '1.3')
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it('an explicit stored value is shown as the value, not overridden by the placeholder', () => {
|
|
355
|
+
renderPanel({ letterSpacing: '0.08em' })
|
|
356
|
+
expandStyle()
|
|
357
|
+
const input = screen.getByLabelText('Caption letter spacing') as HTMLInputElement
|
|
358
|
+
expect(input.value).toBe('0.08')
|
|
359
|
+
// The placeholder is still there for when the value is cleared — it just
|
|
360
|
+
// isn't what's shown while a real value is present.
|
|
361
|
+
expect(input).toHaveAttribute('placeholder', '-0.02')
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
it('a style with no line-height default (karaoke) shows an empty placeholder', () => {
|
|
365
|
+
renderPanel({ style: 'karaoke' })
|
|
366
|
+
expandStyle()
|
|
367
|
+
expect(screen.getByLabelText('Caption line height')).toHaveAttribute('placeholder', '')
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
// The stepper's `onStep` closure nudges off whatever is ACTUALLY in force —
|
|
371
|
+
// see the comment on that closure in CaptionListPanel.tsx. With no stored
|
|
372
|
+
// letterSpacing, "in force" is the active style's own default, read via the
|
|
373
|
+
// `parseEmValue(stored) || parseEmValue(styleDefault) || 0` fallback chain.
|
|
374
|
+
// This is the one test that would catch that chain regressing to nudge off
|
|
375
|
+
// a bare 0 instead: default renderPanel() style is 'pop', whose default is
|
|
376
|
+
// '-0.02em', so one click has to land on -0.01em, not on 0.01em.
|
|
377
|
+
it('letter-spacing stepper nudges off the style default, not a bare 0, when nothing is stored', () => {
|
|
378
|
+
const { onCaptionEdit } = renderPanel()
|
|
379
|
+
expandStyle()
|
|
380
|
+
fireEvent.click(screen.getByRole('button', { name: 'Increase Caption letter spacing' }))
|
|
381
|
+
|
|
382
|
+
expect(onCaptionEdit).toHaveBeenCalledTimes(1)
|
|
383
|
+
expect(onCaptionEdit.mock.calls[0][0].captions.letterSpacing).toBe('-0.01em')
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
// A stepper click is a DISCRETE edit — one final change, one undo entry —
|
|
387
|
+
// never a preview/commit pair, on this headline field exactly like every
|
|
388
|
+
// other adopter of the shared stepper contract.
|
|
389
|
+
it('font-size stepper is a single discrete edit: one onCaptionEdit, no onProjectChange', () => {
|
|
390
|
+
const { onCaptionEdit, onProjectChange } = renderPanel()
|
|
391
|
+
expandStyle()
|
|
392
|
+
fireEvent.click(screen.getByRole('button', { name: 'Increase Caption font size' }))
|
|
393
|
+
|
|
394
|
+
expect(onCaptionEdit).toHaveBeenCalledTimes(1)
|
|
395
|
+
expect(onProjectChange).not.toHaveBeenCalled()
|
|
396
|
+
})
|
|
397
|
+
})
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
/**
|
|
3
|
+
* CaptionListPanel — the Bold toggle and the manual generate/regenerate
|
|
4
|
+
* trigger (including its empty state).
|
|
5
|
+
*
|
|
6
|
+
* Sits in `__tests__/` alongside `CaptionListPanel.font.test.tsx` rather than
|
|
7
|
+
* beside the component, and does not modify or import from the sibling
|
|
8
|
+
* `../CaptionListPanel.test.tsx` — its own render helpers and fixtures mirror
|
|
9
|
+
* that file's shape independently, per the same convention.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
12
|
+
import { render, screen, fireEvent, cleanup } from '@testing-library/react'
|
|
13
|
+
import type { Project } from '../../types'
|
|
14
|
+
import type { Captions, CaptionSegment } from '../../schema'
|
|
15
|
+
import type { PlaybackClock } from '../playback-clock'
|
|
16
|
+
import CaptionListPanel from '../CaptionListPanel'
|
|
17
|
+
|
|
18
|
+
// The panel's active sub-tab persists to localStorage (usePersistentState).
|
|
19
|
+
// Clear it, then seed 'captions' so the regenerate-trigger assertions render
|
|
20
|
+
// on the tab the footer lives on; the Bold tests flip to the Format tab via
|
|
21
|
+
// `expandStyle()`.
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
window.localStorage.clear()
|
|
24
|
+
window.localStorage.setItem('montaj.editor.captionPanelTab', JSON.stringify('captions'))
|
|
25
|
+
})
|
|
26
|
+
afterEach(() => cleanup())
|
|
27
|
+
|
|
28
|
+
Element.prototype.scrollIntoView = vi.fn()
|
|
29
|
+
|
|
30
|
+
const SEGS: CaptionSegment[] = [
|
|
31
|
+
{ id: 'cap-0', text: 'hello world', start: 0, end: 2 },
|
|
32
|
+
{ id: 'cap-1', text: 'goodbye now', start: 2, end: 4 },
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
function makeClock(): PlaybackClock {
|
|
36
|
+
return { get: () => 0, set: vi.fn(), subscribe: () => () => {} }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function renderPanel(opts: {
|
|
40
|
+
captions?: Captions | undefined
|
|
41
|
+
withRegenerate?: boolean
|
|
42
|
+
captionsGenerating?: boolean
|
|
43
|
+
} = {}) {
|
|
44
|
+
// `'captions' in opts`, not a destructured default: a destructured default
|
|
45
|
+
// also kicks in for an explicitly-passed `captions: undefined`, which is
|
|
46
|
+
// exactly the "no captionTrack at all" case several tests below need to
|
|
47
|
+
// construct on purpose.
|
|
48
|
+
const captions = 'captions' in opts ? opts.captions : { style: 'pop', fontsize: 46, segments: SEGS }
|
|
49
|
+
const { withRegenerate = true, captionsGenerating } = opts
|
|
50
|
+
|
|
51
|
+
const project = { id: 'p1', captions } as unknown as Project
|
|
52
|
+
const onCaptionEdit = vi.fn()
|
|
53
|
+
const onProjectChange = vi.fn()
|
|
54
|
+
const onRegenerateCaptions = withRegenerate ? vi.fn() : undefined
|
|
55
|
+
|
|
56
|
+
const view = render(
|
|
57
|
+
<CaptionListPanel
|
|
58
|
+
captionTrack={project.captions}
|
|
59
|
+
project={project}
|
|
60
|
+
currentTime={0}
|
|
61
|
+
selectedIds={[]}
|
|
62
|
+
onSelectCaption={vi.fn()}
|
|
63
|
+
onCaptionSegmentChange={vi.fn()}
|
|
64
|
+
onCaptionEdit={onCaptionEdit}
|
|
65
|
+
onProjectChange={onProjectChange}
|
|
66
|
+
onCaptionSegmentDelete={vi.fn()}
|
|
67
|
+
onRegenerateCaptions={onRegenerateCaptions}
|
|
68
|
+
captionsGenerating={captionsGenerating}
|
|
69
|
+
fps={30}
|
|
70
|
+
clock={makeClock()}
|
|
71
|
+
editFocusId={null}
|
|
72
|
+
/>,
|
|
73
|
+
)
|
|
74
|
+
return { ...view, project, onCaptionEdit, onProjectChange, onRegenerateCaptions }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function expandStyle() {
|
|
78
|
+
// Bold moved from a collapse toggle, then from a single "Style" tab, onto
|
|
79
|
+
// the "Format" tab; these suites seed 'captions', so this click flips there.
|
|
80
|
+
fireEvent.click(screen.getByRole('button', { name: 'Format' }))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
describe('CaptionListPanel Bold toggle', () => {
|
|
84
|
+
it('reads as pressed when fontWeight is absent', () => {
|
|
85
|
+
renderPanel({ captions: { style: 'pop', fontsize: 46, segments: SEGS } })
|
|
86
|
+
expandStyle()
|
|
87
|
+
expect(screen.getByLabelText('Bold')).toHaveAttribute('aria-pressed', 'true')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('clicking while pressed commits fontWeight: 400', () => {
|
|
91
|
+
const { onCaptionEdit } = renderPanel({ captions: { style: 'pop', fontsize: 46, segments: SEGS } })
|
|
92
|
+
expandStyle()
|
|
93
|
+
fireEvent.click(screen.getByLabelText('Bold'))
|
|
94
|
+
|
|
95
|
+
expect(onCaptionEdit).toHaveBeenCalledTimes(1)
|
|
96
|
+
expect(onCaptionEdit.mock.calls[0][0].captions.fontWeight).toBe(400)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// THE assertion this toggle exists for: turning bold back on has to DELETE
|
|
100
|
+
// the key, not write a flat 700 back — a single shared weight would flatten
|
|
101
|
+
// all seven caption styles onto one look. A lazy `fontWeight: undefined`
|
|
102
|
+
// write would pass a `toBeUndefined()` check but still leave the key IN the
|
|
103
|
+
// object (and JSON.stringify would only drop it later, silently, on the
|
|
104
|
+
// trip to the server) — so this checks key ABSENCE directly.
|
|
105
|
+
it('clicking while off (fontWeight: 400) commits captions with no fontWeight key at all', () => {
|
|
106
|
+
const { onCaptionEdit } = renderPanel({ captions: { style: 'pop', fontsize: 46, segments: SEGS, fontWeight: 400 } })
|
|
107
|
+
expandStyle()
|
|
108
|
+
expect(screen.getByLabelText('Bold')).toHaveAttribute('aria-pressed', 'false')
|
|
109
|
+
|
|
110
|
+
fireEvent.click(screen.getByLabelText('Bold'))
|
|
111
|
+
|
|
112
|
+
expect(onCaptionEdit).toHaveBeenCalledTimes(1)
|
|
113
|
+
const captions = onCaptionEdit.mock.calls[0][0].captions
|
|
114
|
+
expect('fontWeight' in captions).toBe(false)
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe('CaptionListPanel generate/regenerate trigger', () => {
|
|
119
|
+
it('with no captions and a host that supports generation, shows the empty state and its button fires onRegenerateCaptions', () => {
|
|
120
|
+
const { onRegenerateCaptions } = renderPanel({ captions: undefined, withRegenerate: true })
|
|
121
|
+
|
|
122
|
+
const btn = screen.getByRole('button', { name: 'Generate captions' })
|
|
123
|
+
fireEvent.click(btn)
|
|
124
|
+
expect(onRegenerateCaptions).toHaveBeenCalledTimes(1)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('with captions present, the trigger reads "Regenerate captions"', () => {
|
|
128
|
+
renderPanel({ captions: { style: 'pop', fontsize: 46, segments: SEGS } })
|
|
129
|
+
expect(screen.getByRole('button', { name: 'Regenerate captions' })).toBeTruthy()
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('with no captions and a host that does NOT support generation, shows explanatory text and no button', () => {
|
|
133
|
+
// A captions object with zero segments (not `undefined`) — the panel's
|
|
134
|
+
// own "nothing to show and nothing to offer" guard hides the WHOLE panel
|
|
135
|
+
// when both captionTrack and onRegenerateCaptions are absent, which is a
|
|
136
|
+
// different (already-covered) case from this one: an existing project
|
|
137
|
+
// that has a captions track but happens to have no rows left, on a host
|
|
138
|
+
// (Hub/LP) with no generation capability at all.
|
|
139
|
+
renderPanel({ captions: { style: 'pop', fontsize: 46, segments: [] }, withRegenerate: false })
|
|
140
|
+
|
|
141
|
+
expect(screen.getByText(/no captions yet/i)).toBeTruthy()
|
|
142
|
+
// With zero segments the panel shows a plain empty-state placeholder — no
|
|
143
|
+
// transcript list, no tab bar (there's nothing to tab between), and, with
|
|
144
|
+
// no generation capability, no button anywhere at all.
|
|
145
|
+
expect(screen.queryByRole('list', { name: 'Caption segments' })).toBeNull()
|
|
146
|
+
expect(screen.queryByRole('button')).toBeNull()
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('disables the trigger while a job is running', () => {
|
|
150
|
+
renderPanel({ captions: { style: 'pop', fontsize: 46, segments: SEGS }, captionsGenerating: true })
|
|
151
|
+
expect(screen.getByRole('button', { name: 'Regenerate captions' })).toBeDisabled()
|
|
152
|
+
})
|
|
153
|
+
})
|
|
@@ -38,6 +38,7 @@ describe('CaptionRegenModal', () => {
|
|
|
38
38
|
<CaptionRegenModal
|
|
39
39
|
adapter={makeAdapter()}
|
|
40
40
|
projectId="vid-1"
|
|
41
|
+
existingRowCount={1}
|
|
41
42
|
onDone={onDone}
|
|
42
43
|
onClose={onClose}
|
|
43
44
|
/>,
|
|
@@ -56,10 +57,38 @@ describe('CaptionRegenModal', () => {
|
|
|
56
57
|
<CaptionRegenModal
|
|
57
58
|
adapter={errorAdapter}
|
|
58
59
|
projectId="vid-1"
|
|
60
|
+
existingRowCount={1}
|
|
59
61
|
onDone={vi.fn()}
|
|
60
62
|
onClose={vi.fn()}
|
|
61
63
|
/>,
|
|
62
64
|
)
|
|
63
65
|
await waitFor(() => expect(screen.getByText('multi_source')).toBeTruthy())
|
|
64
66
|
})
|
|
67
|
+
|
|
68
|
+
it('warns how many rows will be discarded when the project has more than one', async () => {
|
|
69
|
+
render(
|
|
70
|
+
<CaptionRegenModal
|
|
71
|
+
adapter={makeAdapter()}
|
|
72
|
+
projectId="vid-1"
|
|
73
|
+
existingRowCount={3}
|
|
74
|
+
onDone={vi.fn()}
|
|
75
|
+
onClose={vi.fn()}
|
|
76
|
+
/>,
|
|
77
|
+
)
|
|
78
|
+
await waitFor(() => expect(screen.getByText(/replace all 3 caption rows with a single new row/i)).toBeTruthy())
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('shows no discard warning for a single-row (or empty) project', async () => {
|
|
82
|
+
render(
|
|
83
|
+
<CaptionRegenModal
|
|
84
|
+
adapter={makeAdapter()}
|
|
85
|
+
projectId="vid-1"
|
|
86
|
+
existingRowCount={1}
|
|
87
|
+
onDone={vi.fn()}
|
|
88
|
+
onClose={vi.fn()}
|
|
89
|
+
/>,
|
|
90
|
+
)
|
|
91
|
+
await waitFor(() => expect(screen.getByText(/transcribing audio/i)).toBeTruthy())
|
|
92
|
+
expect(screen.queryByText(/caption rows/i)).toBeNull()
|
|
93
|
+
})
|
|
65
94
|
})
|