@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,165 @@
|
|
|
1
|
+
/// <reference types="vitest/globals" />
|
|
2
|
+
/**
|
|
3
|
+
* SocialSafeZoneOverlay is a preview-only viewing aid (see the file-level
|
|
4
|
+
* doc comment on the component) — these tests cover exactly what the spec
|
|
5
|
+
* calls out: the chrome renders for a recognized platform, renders nothing
|
|
6
|
+
* for an absent/unrecognized one, stays pointer-events-none throughout, and
|
|
7
|
+
* scales off a measured container size rather than a fixed pixel value.
|
|
8
|
+
*
|
|
9
|
+
* jsdom does no layout, so ResizeObserver never fires on its own — each test
|
|
10
|
+
* installs a controllable stub (mirrors the pattern in
|
|
11
|
+
* `src/video/__tests__/captionPositioning.test.tsx`) that reports a size the
|
|
12
|
+
* test chooses, and can re-fire with a different size mid-test.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, expect, afterEach } from 'vitest'
|
|
15
|
+
import { render, cleanup, act } from '@testing-library/react'
|
|
16
|
+
import SocialSafeZoneOverlay from '../SocialSafeZoneOverlay'
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
cleanup()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
/** Installs a ResizeObserver stub that reports `initial` the moment
|
|
23
|
+
* `observe()` is called, and returns a `fire` helper to report a different
|
|
24
|
+
* size later — e.g. simulating a pane drag or fullscreen toggle. */
|
|
25
|
+
function installResizeObserver(initial: { width: number; height: number }) {
|
|
26
|
+
let cb: ((entries: { contentRect: { width: number; height: number } }[]) => void) | null = null
|
|
27
|
+
;(globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver = class {
|
|
28
|
+
constructor(callback: typeof cb) { cb = callback }
|
|
29
|
+
observe() { cb?.([{ contentRect: initial }]) }
|
|
30
|
+
unobserve() {}
|
|
31
|
+
disconnect() {}
|
|
32
|
+
}
|
|
33
|
+
return { fire: (width: number, height: number) => cb?.([{ contentRect: { width, height } }]) }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('SocialSafeZoneOverlay — activation', () => {
|
|
37
|
+
it('renders nothing when no platform is given', () => {
|
|
38
|
+
installResizeObserver({ width: 1080, height: 1920 })
|
|
39
|
+
const { container } = render(<SocialSafeZoneOverlay />)
|
|
40
|
+
expect(container.firstChild).toBeNull()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('renders nothing for an unrecognized platform', () => {
|
|
44
|
+
installResizeObserver({ width: 1080, height: 1920 })
|
|
45
|
+
const { container } = render(<SocialSafeZoneOverlay platform="reels" />)
|
|
46
|
+
expect(container.firstChild).toBeNull()
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('SocialSafeZoneOverlay — TikTok chrome', () => {
|
|
51
|
+
it('renders the status bar, nav row, engagement rail and bottom-left credit block', () => {
|
|
52
|
+
installResizeObserver({ width: 1080, height: 1920 })
|
|
53
|
+
const { getByText, getAllByText } = render(<SocialSafeZoneOverlay platform="tiktok" />)
|
|
54
|
+
|
|
55
|
+
// Status bar
|
|
56
|
+
expect(getByText('9:41')).toBeTruthy()
|
|
57
|
+
|
|
58
|
+
// Nav row — For You is the active tab
|
|
59
|
+
expect(getByText('Following')).toBeTruthy()
|
|
60
|
+
expect(getByText('For You')).toBeTruthy()
|
|
61
|
+
|
|
62
|
+
// Right rail — one avatar + four counted actions, each labeled 2.8M
|
|
63
|
+
expect(getAllByText('2.8M')).toHaveLength(4)
|
|
64
|
+
|
|
65
|
+
// Bottom-left credit block
|
|
66
|
+
expect(getByText('@Your name')).toBeTruthy()
|
|
67
|
+
expect(getByText('Here are some descriptions about videos')).toBeTruthy()
|
|
68
|
+
expect(getByText('Music name')).toBeTruthy()
|
|
69
|
+
|
|
70
|
+
// Bottom app nav
|
|
71
|
+
expect(getByText('Home')).toBeTruthy()
|
|
72
|
+
expect(getByText('Profile')).toBeTruthy()
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('SocialSafeZoneOverlay — YouTube Shorts chrome', () => {
|
|
77
|
+
it('renders its distinguishing chrome: channel row and Subscribe pill', () => {
|
|
78
|
+
installResizeObserver({ width: 1080, height: 1920 })
|
|
79
|
+
const { getByText, getAllByText } = render(<SocialSafeZoneOverlay platform="youtube" />)
|
|
80
|
+
|
|
81
|
+
expect(getByText('9:41')).toBeTruthy()
|
|
82
|
+
expect(getByText('@channel')).toBeTruthy()
|
|
83
|
+
expect(getByText('Subscribe')).toBeTruthy()
|
|
84
|
+
// Right rail — thumbs-up and comments are both counted; thumbs-down,
|
|
85
|
+
// share, remix and the sound thumbnail are icon-only (no count).
|
|
86
|
+
expect(getAllByText('2.8M')).toHaveLength(2)
|
|
87
|
+
|
|
88
|
+
// Bottom app nav
|
|
89
|
+
expect(getByText('Home')).toBeTruthy()
|
|
90
|
+
expect(getByText('Subscriptions')).toBeTruthy()
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe('SocialSafeZoneOverlay — Instagram Reels chrome', () => {
|
|
95
|
+
it('renders its distinguishing chrome: the Reels title and a Follow pill', () => {
|
|
96
|
+
installResizeObserver({ width: 1080, height: 1920 })
|
|
97
|
+
const { getByText, getAllByText } = render(<SocialSafeZoneOverlay platform="instagram" />)
|
|
98
|
+
|
|
99
|
+
expect(getByText('9:41')).toBeTruthy()
|
|
100
|
+
expect(getByText('Reels')).toBeTruthy()
|
|
101
|
+
expect(getByText('Follow')).toBeTruthy()
|
|
102
|
+
expect(getByText('Your name')).toBeTruthy()
|
|
103
|
+
// Right rail — likes, comments, reshare and send are each counted 2.8M.
|
|
104
|
+
expect(getAllByText('2.8M')).toHaveLength(4)
|
|
105
|
+
// Immersive reel view also draws the add-comment bar.
|
|
106
|
+
expect(getByText('Add comment...')).toBeTruthy()
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe('SocialSafeZoneOverlay — copy style', () => {
|
|
111
|
+
it('never uses an em dash, on any platform', () => {
|
|
112
|
+
installResizeObserver({ width: 1080, height: 1920 })
|
|
113
|
+
for (const platform of ['tiktok', 'youtube', 'instagram'] as const) {
|
|
114
|
+
const { getByTestId, unmount } = render(<SocialSafeZoneOverlay platform={platform} />)
|
|
115
|
+
expect(getByTestId('social-safe-zone-overlay').textContent).not.toContain('—')
|
|
116
|
+
unmount()
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
describe('SocialSafeZoneOverlay — never steals a click', () => {
|
|
122
|
+
it('the root layer is pointer-events-none, on every platform', () => {
|
|
123
|
+
installResizeObserver({ width: 1080, height: 1920 })
|
|
124
|
+
for (const platform of ['tiktok', 'youtube', 'instagram'] as const) {
|
|
125
|
+
const { getByTestId, unmount } = render(<SocialSafeZoneOverlay platform={platform} />)
|
|
126
|
+
const root = getByTestId('social-safe-zone-overlay')
|
|
127
|
+
expect(root.className).toContain('pointer-events-none')
|
|
128
|
+
unmount()
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
describe('SocialSafeZoneOverlay — scales with its container, not a fixed pixel size', () => {
|
|
134
|
+
it('a container matching the 1080×1920 design canvas scales 1:1', () => {
|
|
135
|
+
installResizeObserver({ width: 1080, height: 1920 })
|
|
136
|
+
const { getByTestId } = render(<SocialSafeZoneOverlay platform="tiktok" />)
|
|
137
|
+
expect(getByTestId('social-safe-zone-canvas').style.transform).toContain('scale(1)')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('a half-size container halves the scale, proportionally, not to a fixed px value', () => {
|
|
141
|
+
const ro = installResizeObserver({ width: 1080, height: 1920 })
|
|
142
|
+
const { getByTestId } = render(<SocialSafeZoneOverlay platform="tiktok" />)
|
|
143
|
+
const canvas = getByTestId('social-safe-zone-canvas')
|
|
144
|
+
expect(canvas.style.transform).toContain('scale(1)')
|
|
145
|
+
|
|
146
|
+
act(() => { ro.fire(540, 960) })
|
|
147
|
+
expect(canvas.style.transform).toContain('scale(0.5)')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('a landscape (non-vertical) container contain-fits instead of overflowing', () => {
|
|
151
|
+
// 16:9 landscape box: the design canvas is scaled by the SMALLER of the
|
|
152
|
+
// two axis ratios (height-bound here) so the whole 1080×1920 chrome
|
|
153
|
+
// stays inside the box on both axes rather than spilling past its
|
|
154
|
+
// narrower dimension — the "does not break or overflow" degrade for a
|
|
155
|
+
// non-vertical preview.
|
|
156
|
+
installResizeObserver({ width: 1920, height: 1080 })
|
|
157
|
+
const { getByTestId } = render(<SocialSafeZoneOverlay platform="tiktok" />)
|
|
158
|
+
const canvas = getByTestId('social-safe-zone-canvas')
|
|
159
|
+
const expectedScale = Math.min(1920 / 1080, 1080 / 1920)
|
|
160
|
+
expect(canvas.style.transform).toContain(`scale(${expectedScale})`)
|
|
161
|
+
|
|
162
|
+
const root = getByTestId('social-safe-zone-overlay')
|
|
163
|
+
expect(root.className).toContain('overflow-hidden')
|
|
164
|
+
})
|
|
165
|
+
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
1
|
+
import { describe, it, expect, afterEach, beforeEach } from 'vitest'
|
|
2
2
|
import {
|
|
3
3
|
CAPTION_DRAG_SLOP_PX,
|
|
4
4
|
CAPTION_MAX_SCALE,
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
captionDragGeometry,
|
|
7
7
|
captionDragPatch,
|
|
8
8
|
hasEscapedClickSlop,
|
|
9
|
+
measureCaptionContentRect,
|
|
9
10
|
readCaptionGeometry,
|
|
10
11
|
screenDeltaToFramePercent,
|
|
11
12
|
type CaptionDragState,
|
|
@@ -166,3 +167,121 @@ describe('captionDragPatch', () => {
|
|
|
166
167
|
.toEqual({ scale: 2 })
|
|
167
168
|
})
|
|
168
169
|
})
|
|
170
|
+
|
|
171
|
+
// ── measureCaptionContentRect ───────────────────────────────────────────────
|
|
172
|
+
//
|
|
173
|
+
// Captions have lanes, so a template can paint several captions at once. The
|
|
174
|
+
// selection box must wrap ONE of them, which is what the `segmentId` argument
|
|
175
|
+
// is for. These cover the scoping and — just as important — the fallback that
|
|
176
|
+
// keeps a template with no `data-caption-id` behaving exactly as it did before
|
|
177
|
+
// lanes existed.
|
|
178
|
+
|
|
179
|
+
/** Client rect with the fields the measurer reads. jsdom does no layout. */
|
|
180
|
+
function fixedRect(left: number, top: number, right: number, bottom: number): DOMRect {
|
|
181
|
+
return {
|
|
182
|
+
left, top, right, bottom,
|
|
183
|
+
width: right - left, height: bottom - top,
|
|
184
|
+
x: left, y: top,
|
|
185
|
+
toJSON: () => ({}),
|
|
186
|
+
} as DOMRect
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Where each caption's text "paints", keyed by the text itself. Two boxes far
|
|
190
|
+
// apart on the frame, so a measurement that accidentally unions them is
|
|
191
|
+
// obvious rather than off by a few pixels.
|
|
192
|
+
const BOXES: Record<string, [left: number, top: number, right: number, bottom: number]> = {
|
|
193
|
+
bottom: [100, 1700, 300, 1750], // lane 0 — low on the frame
|
|
194
|
+
top: [400, 200, 900, 280], // lane 1 — high on the frame
|
|
195
|
+
}
|
|
196
|
+
const BOTTOM_RECT = { left: 100, top: 1700, width: 200, height: 50 }
|
|
197
|
+
const TOP_RECT = { left: 400, top: 200, width: 500, height: 80 }
|
|
198
|
+
// What an unscoped walk sees: the union of both, spanning most of the frame.
|
|
199
|
+
const UNION_RECT = { left: 100, top: 200, width: 800, height: 1550 }
|
|
200
|
+
|
|
201
|
+
beforeEach(() => {
|
|
202
|
+
// jsdom has no Range.prototype.getBoundingClientRect at all (and Element's
|
|
203
|
+
// returns zeros), so feed the walk a rect table keyed by the text node it is
|
|
204
|
+
// measuring. Assigned directly — vi.spyOn needs a pre-existing method.
|
|
205
|
+
Range.prototype.getBoundingClientRect = function (this: Range): DOMRect {
|
|
206
|
+
const text = this.startContainer.nodeValue?.trim() ?? ''
|
|
207
|
+
const box = BOXES[text]
|
|
208
|
+
return box ? fixedRect(box[0], box[1], box[2], box[3]) : fixedRect(0, 0, 0, 0)
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
afterEach(() => {
|
|
213
|
+
delete (Range.prototype as { getBoundingClientRect?: unknown }).getBoundingClientRect
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
function mount(html: string): HTMLElement {
|
|
217
|
+
const root = document.createElement('div')
|
|
218
|
+
root.innerHTML = html
|
|
219
|
+
return root
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Two marked caption subtrees — what the templates render for two lanes. */
|
|
223
|
+
const TWO_LANES =
|
|
224
|
+
'<div data-caption-id="cap-bottom"><div><span>bottom</span></div></div>' +
|
|
225
|
+
'<div data-caption-id="cap-top"><div><span>top</span></div></div>'
|
|
226
|
+
|
|
227
|
+
/** One caption from a template that predates `data-caption-id`. */
|
|
228
|
+
const UNMARKED = '<div><div><span>bottom</span></div></div>'
|
|
229
|
+
|
|
230
|
+
describe('measureCaptionContentRect — scoping to one caption', () => {
|
|
231
|
+
it('measures only the requested subtree when two are marked', () => {
|
|
232
|
+
const root = mount(TWO_LANES)
|
|
233
|
+
expect(measureCaptionContentRect(root, 'cap-top')).toEqual(TOP_RECT)
|
|
234
|
+
expect(measureCaptionContentRect(root, 'cap-bottom')).toEqual(BOTTOM_RECT)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('unions everything under the root when no segment is named', () => {
|
|
238
|
+
// The pre-lane behaviour, kept for callers that have no target in mind.
|
|
239
|
+
expect(measureCaptionContentRect(mount(TWO_LANES))).toEqual(UNION_RECT)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('finds a marker nested below the root, not just a direct child', () => {
|
|
243
|
+
const root = mount(`<div class="wrapper">${TWO_LANES}</div>`)
|
|
244
|
+
expect(measureCaptionContentRect(root, 'cap-top')).toEqual(TOP_RECT)
|
|
245
|
+
})
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
describe('measureCaptionContentRect — fallback when the marker is absent', () => {
|
|
249
|
+
it('falls back to the whole root for a template that emits no marker', () => {
|
|
250
|
+
// Today's numbers, unchanged: a host-supplied or pre-lane template must
|
|
251
|
+
// keep its selection box rather than losing it.
|
|
252
|
+
expect(measureCaptionContentRect(mount(UNMARKED), 'cap-0')).toEqual(BOTTOM_RECT)
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('falls back to the whole root when the named segment is not the one on screen', () => {
|
|
256
|
+
expect(measureCaptionContentRect(mount(TWO_LANES), 'cap-nonexistent')).toEqual(UNION_RECT)
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('an id containing selector metacharacters cannot throw — it just misses and falls back', () => {
|
|
260
|
+
// project.json is hand-editable, so the lookup compares attributes rather
|
|
261
|
+
// than building a `[data-caption-id="…"]` selector that a quote would break.
|
|
262
|
+
expect(() => measureCaptionContentRect(mount(TWO_LANES), 'cap"]:not(*)')).not.toThrow()
|
|
263
|
+
expect(measureCaptionContentRect(mount(TWO_LANES), 'cap"]:not(*)')).toEqual(UNION_RECT)
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
describe('measureCaptionContentRect — nothing painted', () => {
|
|
268
|
+
it('returns null for an empty subtree, so the caller hides the box', () => {
|
|
269
|
+
expect(measureCaptionContentRect(mount(''))).toBeNull()
|
|
270
|
+
expect(measureCaptionContentRect(mount('<div data-caption-id="cap-0"></div>'), 'cap-0')).toBeNull()
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('ignores whitespace-only text nodes', () => {
|
|
274
|
+
expect(measureCaptionContentRect(mount('<div data-caption-id="cap-0"> </div>'), 'cap-0')).toBeNull()
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('returns null for the requested caption even while the OTHER one is painting', () => {
|
|
278
|
+
// `pop` renders no word between two word windows; the box must disappear
|
|
279
|
+
// for that caption rather than snapping onto its neighbour.
|
|
280
|
+
const root = mount(
|
|
281
|
+
'<div data-caption-id="cap-bottom"><div><span>bottom</span></div></div>' +
|
|
282
|
+
'<div data-caption-id="cap-top"></div>',
|
|
283
|
+
)
|
|
284
|
+
expect(measureCaptionContentRect(root, 'cap-top')).toBeNull()
|
|
285
|
+
expect(measureCaptionContentRect(root, 'cap-bottom')).toEqual(BOTTOM_RECT)
|
|
286
|
+
})
|
|
287
|
+
})
|