react-native-image-stitcher 0.15.2 → 0.16.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/CHANGELOG.md +171 -1
- package/README.md +131 -5
- package/android/src/main/cpp/image_stitcher_jni.cpp +154 -13
- package/android/src/main/cpp/keyframe_gate_jni.cpp +15 -8
- package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +223 -1
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +220 -87
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +1 -1
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +7 -36
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +14 -8
- package/android/src/main/java/io/imagestitcher/rn/ar/YuvImageConverter.kt +39 -1
- package/cpp/crop_quad.cpp +162 -0
- package/cpp/crop_quad.hpp +163 -0
- package/cpp/keyframe_gate.cpp +54 -15
- package/cpp/keyframe_gate.hpp +33 -0
- package/cpp/stitcher.cpp +1122 -132
- package/cpp/stitcher.hpp +62 -0
- package/cpp/warp_guard.hpp +212 -0
- package/dist/camera/Camera.d.ts +209 -12
- package/dist/camera/Camera.js +575 -36
- package/dist/camera/CameraView.js +35 -16
- package/dist/camera/CaptureCountdownOverlay.d.ts +70 -0
- package/dist/camera/CaptureCountdownOverlay.js +239 -0
- package/dist/camera/CaptureFrameCounterOverlay.d.ts +58 -0
- package/dist/camera/CaptureFrameCounterOverlay.js +153 -0
- package/dist/camera/CaptureMemoryPill.d.ts +24 -8
- package/dist/camera/CaptureMemoryPill.js +37 -12
- package/dist/camera/CapturePreview.js +2 -1
- package/dist/camera/CaptureStatusOverlay.d.ts +11 -4
- package/dist/camera/CaptureStatusOverlay.js +22 -5
- package/dist/camera/CaptureThumbnailStrip.js +2 -1
- package/dist/camera/LateralMotionModal.d.ts +85 -0
- package/dist/camera/LateralMotionModal.js +134 -0
- package/dist/camera/PanHowToOverlay.d.ts +76 -0
- package/dist/camera/PanHowToOverlay.js +222 -0
- package/dist/camera/PanoramaBandOverlay.d.ts +2 -1
- package/dist/camera/PanoramaBandOverlay.js +9 -3
- package/dist/camera/PanoramaSettings.d.ts +8 -6
- package/dist/camera/PanoramaSettings.js +19 -1
- package/dist/camera/PanoramaSettingsModal.js +4 -4
- package/dist/camera/RectCropPreview.d.ts +135 -0
- package/dist/camera/RectCropPreview.js +370 -0
- package/dist/camera/RotateToLandscapePrompt.d.ts +87 -0
- package/dist/camera/RotateToLandscapePrompt.js +138 -0
- package/dist/camera/buildPanoramaInitialSettings.d.ts +19 -2
- package/dist/camera/buildPanoramaInitialSettings.js +9 -0
- package/dist/camera/cameraErrorMessages.d.ts +30 -1
- package/dist/camera/cameraErrorMessages.js +26 -10
- package/dist/camera/cameraGuidanceCopy.d.ts +87 -0
- package/dist/camera/cameraGuidanceCopy.js +80 -0
- package/dist/camera/captureCountdown.d.ts +52 -0
- package/dist/camera/captureCountdown.js +76 -0
- package/dist/camera/captureWarnings.d.ts +90 -0
- package/dist/camera/captureWarnings.js +108 -0
- package/dist/camera/classifyStitchError.d.ts +30 -0
- package/dist/camera/classifyStitchError.js +42 -0
- package/dist/camera/cropGeometry.d.ts +136 -0
- package/dist/camera/cropGeometry.js +223 -0
- package/dist/camera/displayDecodeImageProps.d.ts +25 -0
- package/dist/camera/displayDecodeImageProps.js +29 -0
- package/dist/camera/guidanceGraphics.d.ts +58 -0
- package/dist/camera/guidanceGraphics.js +280 -0
- package/dist/camera/guidanceTokens.d.ts +54 -0
- package/dist/camera/guidanceTokens.js +58 -0
- package/dist/camera/panModeGate.d.ts +54 -0
- package/dist/camera/panModeGate.js +62 -0
- package/dist/camera/pickCaptureFormat.d.ts +71 -0
- package/dist/camera/pickCaptureFormat.js +85 -0
- package/dist/camera/stitchDebugInfo.d.ts +27 -0
- package/dist/camera/stitchDebugInfo.js +55 -0
- package/dist/camera/usePanMotion.d.ts +250 -0
- package/dist/camera/usePanMotion.js +451 -0
- package/dist/index.d.ts +24 -3
- package/dist/index.js +33 -2
- package/dist/stitching/computeInscribedRect.d.ts +40 -0
- package/dist/stitching/computeInscribedRect.js +55 -0
- package/dist/stitching/cropQuad.d.ts +78 -0
- package/dist/stitching/cropQuad.js +116 -0
- package/dist/stitching/incremental.d.ts +74 -0
- package/dist/stitching/useIncrementalStitcher.d.ts +7 -1
- package/dist/stitching/useIncrementalStitcher.js +7 -1
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +154 -29
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +4 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +15 -0
- package/ios/Sources/RNImageStitcher/KeyframeGate.swift +2 -2
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +48 -5
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +27 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +211 -7
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +25 -1
- package/ios/Sources/RNImageStitcher/Stitcher.swift +34 -1
- package/ios/Sources/RNImageStitcher/StitcherBridge.m +5 -0
- package/ios/Sources/RNImageStitcher/StitcherBridge.swift +56 -0
- package/package.json +5 -1
- package/src/camera/Camera.tsx +945 -47
- package/src/camera/CameraView.tsx +48 -16
- package/src/camera/CaptureCountdownOverlay.tsx +272 -0
- package/src/camera/CaptureFrameCounterOverlay.tsx +197 -0
- package/src/camera/CaptureMemoryPill.tsx +50 -12
- package/src/camera/CapturePreview.tsx +5 -0
- package/src/camera/CaptureStatusOverlay.tsx +35 -7
- package/src/camera/CaptureThumbnailStrip.tsx +4 -0
- package/src/camera/LateralMotionModal.tsx +199 -0
- package/src/camera/PanHowToOverlay.tsx +246 -0
- package/src/camera/PanoramaBandOverlay.tsx +9 -1
- package/src/camera/PanoramaSettings.ts +27 -7
- package/src/camera/PanoramaSettingsModal.tsx +4 -4
- package/src/camera/RectCropPreview.tsx +638 -0
- package/src/camera/RotateToLandscapePrompt.tsx +188 -0
- package/src/camera/buildPanoramaInitialSettings.ts +30 -1
- package/src/camera/cameraErrorMessages.ts +39 -2
- package/src/camera/cameraGuidanceCopy.ts +145 -0
- package/src/camera/captureCountdown.ts +83 -0
- package/src/camera/captureWarnings.ts +190 -0
- package/src/camera/classifyStitchError.ts +68 -0
- package/src/camera/cropGeometry.ts +268 -0
- package/src/camera/displayDecodeImageProps.ts +25 -0
- package/src/camera/guidanceGraphics.tsx +347 -0
- package/src/camera/guidanceTokens.ts +57 -0
- package/src/camera/panModeGate.ts +81 -0
- package/src/camera/pickCaptureFormat.ts +130 -0
- package/src/camera/stitchDebugInfo.ts +71 -0
- package/src/camera/usePanMotion.ts +667 -0
- package/src/index.ts +66 -3
- package/src/stitching/computeInscribedRect.ts +81 -0
- package/src/stitching/cropQuad.ts +167 -0
- package/src/stitching/incremental.ts +74 -0
- package/src/stitching/useIncrementalStitcher.ts +13 -0
- package/android/src/main/java/io/imagestitcher/rn/TransferredNV21.kt +0 -100
- package/cpp/tests/CMakeLists.txt +0 -104
- package/cpp/tests/README.md +0 -86
- package/cpp/tests/keyframe_timebudget_test.cpp +0 -65
- package/cpp/tests/pose_test.cpp +0 -74
- package/cpp/tests/stitcher_frame_data_test.cpp +0 -132
- package/cpp/tests/stubs/jsi/jsi.h +0 -33
- package/cpp/tests/stubs/react-native-worklets-core/WKTJsiWorklet.h +0 -34
- package/cpp/tests/warp_guard_test.cpp +0 -48
- package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +0 -190
- package/src/camera/__tests__/bandThumbRotation.test.ts +0 -120
- package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +0 -160
- package/src/camera/__tests__/cameraErrorMessages.test.ts +0 -76
- package/src/camera/__tests__/homeIndicatorEdge.test.ts +0 -116
- package/src/camera/__tests__/lowMemDevice.test.ts +0 -52
- package/src/camera/__tests__/selectCaptureDevice.test.ts +0 -210
- package/src/camera/__tests__/useContentRotation.test.ts +0 -89
- package/src/camera/__tests__/useOrientationDrift.test.ts +0 -169
- package/src/stitching/__tests__/subscribeIncrementalState.refine.test.ts +0 -276
- package/src/stitching/__tests__/useStitcherWorklet.test.ts +0 -202
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
/**
|
|
3
|
-
* Unit tests for the JS→native settings bridge.
|
|
4
|
-
*
|
|
5
|
-
* Scope: every adapter (panoramaSettingsToNativeConfig,
|
|
6
|
-
* slitscanSettingsToNativeConfig, hybridSettingsToNativeConfig)
|
|
7
|
-
* round-trip from a hierarchical typed input to the flat wire dict
|
|
8
|
-
* the native side reads. Asserts both:
|
|
9
|
-
*
|
|
10
|
-
* 1. Naming — JS key `registration.ncc1d.searchRadius` becomes
|
|
11
|
-
* native key `nccSearchRadius1d` (and similar mappings). Each
|
|
12
|
-
* DEFAULT_* snapshot's expected wire dict is enumerated below;
|
|
13
|
-
* drift in either direction (lib drops a key, or adds a phantom
|
|
14
|
-
* one) is caught here.
|
|
15
|
-
*
|
|
16
|
-
* 2. Presence-as-enable — undefined optional sub-objects in the
|
|
17
|
-
* typed shape (`registration.ncc1d`, `registration.ncc2d`,
|
|
18
|
-
* `registration.ncc2d.emaSmoothing`, `registration.ncc2d.panAxisLock`,
|
|
19
|
-
* `frameSelection.flow`, `advanced`) translate to explicit
|
|
20
|
-
* `enable*: false` (or the absence of all the sub-object's
|
|
21
|
-
* payload keys) on the wire. Many of these have been silent
|
|
22
|
-
* drift hazards historically — the old flat type required the
|
|
23
|
-
* consumer to set BOTH `enable1dNcc: true` AND `nccSearchRadius1d:
|
|
24
|
-
* <value>`; v0.4 makes them inseparable by collapsing into a
|
|
25
|
-
* single optional sub-object, and this file is what guarantees
|
|
26
|
-
* the wire side still gets both halves.
|
|
27
|
-
*
|
|
28
|
-
* 3. Engine-discriminated coverage — plane source variants
|
|
29
|
-
* ('Disabled' / 'ARKitDetected' / 'Virtual') gate which optional
|
|
30
|
-
* plane fields are emitted; the bridge filters those at the
|
|
31
|
-
* adapter boundary so the modal's per-source rendering doesn't
|
|
32
|
-
* get mislead by stale-but-present keys from a previous source
|
|
33
|
-
* selection.
|
|
34
|
-
*
|
|
35
|
-
* These tests are pure-TS; no React Native module import. Jest config
|
|
36
|
-
* (`jest.config.js`) routes test files in `__tests__/` through ts-jest
|
|
37
|
-
* with the `node` testEnvironment.
|
|
38
|
-
*/
|
|
39
|
-
|
|
40
|
-
import {
|
|
41
|
-
DEFAULT_FLOW_GATE_SETTINGS,
|
|
42
|
-
DEFAULT_PANORAMA_SETTINGS,
|
|
43
|
-
type PanoramaSettings,
|
|
44
|
-
} from '../PanoramaSettings';
|
|
45
|
-
import {
|
|
46
|
-
panoramaSettingsToNativeConfig,
|
|
47
|
-
} from '../PanoramaSettingsBridge';
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
// ════════════════════════════════════════════════════════════════════
|
|
51
|
-
// PANORAMA — batch-keyframe engine
|
|
52
|
-
// ════════════════════════════════════════════════════════════════════
|
|
53
|
-
|
|
54
|
-
describe('panoramaSettingsToNativeConfig', () => {
|
|
55
|
-
it('round-trips DEFAULT_PANORAMA_SETTINGS to the expected flat dict', () => {
|
|
56
|
-
const cfg = panoramaSettingsToNativeConfig(DEFAULT_PANORAMA_SETTINGS);
|
|
57
|
-
|
|
58
|
-
// Cross-cutting
|
|
59
|
-
expect(cfg.captureSource).toBe('ar');
|
|
60
|
-
|
|
61
|
-
// BatchStitcherSettings
|
|
62
|
-
expect(cfg.stitchMode).toBe('auto');
|
|
63
|
-
expect(cfg.warperType).toBe('plane');
|
|
64
|
-
expect(cfg.blenderType).toBe('multiband');
|
|
65
|
-
expect(cfg.seamFinderType).toBe('graphcut');
|
|
66
|
-
expect(cfg.enableMaxInscribedRectCrop).toBe(false);
|
|
67
|
-
|
|
68
|
-
// FrameSelectionSettings
|
|
69
|
-
expect(cfg.frameSelectionMode).toBe('flow-based');
|
|
70
|
-
expect(cfg.keyframeMaxCount).toBe(6);
|
|
71
|
-
expect(cfg.keyframeOverlapThreshold).toBe(0.2);
|
|
72
|
-
expect(cfg.maxKeyframeIntervalMs).toBe(2000);
|
|
73
|
-
|
|
74
|
-
// FlowGateSettings (flow is defined in the default)
|
|
75
|
-
expect(cfg.flowNoveltyPercentile).toBe(0.85);
|
|
76
|
-
expect(cfg.flowEvalEveryNFrames).toBe(5);
|
|
77
|
-
expect(cfg.flowMaxTranslationCm).toBe(50);
|
|
78
|
-
expect(cfg.flowMaxCorners).toBe(150);
|
|
79
|
-
expect(cfg.flowQualityLevel).toBe(0.01);
|
|
80
|
-
expect(cfg.flowMinDistance).toBe(10);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('falls back to DEFAULT_FLOW_GATE_SETTINGS when frameSelection.flow is undefined', () => {
|
|
84
|
-
// F10 Phase 2 review B1 — native compiled-in defaults disagree
|
|
85
|
-
// with the JS defaults for two flow knobs (maxTranslationCm and
|
|
86
|
-
// evalEveryNFrames). The bridge must always emit every flow key
|
|
87
|
-
// so sparse-literal hosts get the JS defaults on the wire, not
|
|
88
|
-
// the native fallbacks.
|
|
89
|
-
const noFlow: PanoramaSettings = {
|
|
90
|
-
...DEFAULT_PANORAMA_SETTINGS,
|
|
91
|
-
frameSelection: {
|
|
92
|
-
...DEFAULT_PANORAMA_SETTINGS.frameSelection,
|
|
93
|
-
flow: undefined,
|
|
94
|
-
},
|
|
95
|
-
};
|
|
96
|
-
const cfg = panoramaSettingsToNativeConfig(noFlow);
|
|
97
|
-
|
|
98
|
-
expect(cfg.frameSelectionMode).toBe('flow-based');
|
|
99
|
-
expect(cfg.keyframeMaxCount).toBe(6);
|
|
100
|
-
expect(cfg.keyframeOverlapThreshold).toBe(0.2);
|
|
101
|
-
|
|
102
|
-
// Every flow.* native key present, matching DEFAULT_FLOW_GATE_SETTINGS.
|
|
103
|
-
expect(cfg.flowNoveltyPercentile).toBe(DEFAULT_FLOW_GATE_SETTINGS.noveltyPercentile);
|
|
104
|
-
expect(cfg.flowEvalEveryNFrames).toBe(DEFAULT_FLOW_GATE_SETTINGS.evalEveryNFrames);
|
|
105
|
-
expect(cfg.flowMaxTranslationCm).toBe(DEFAULT_FLOW_GATE_SETTINGS.maxTranslationCm);
|
|
106
|
-
expect(cfg.flowMaxCorners).toBe(DEFAULT_FLOW_GATE_SETTINGS.maxCorners);
|
|
107
|
-
expect(cfg.flowQualityLevel).toBe(DEFAULT_FLOW_GATE_SETTINGS.qualityLevel);
|
|
108
|
-
expect(cfg.flowMinDistance).toBe(DEFAULT_FLOW_GATE_SETTINGS.minDistance);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('emits flow defaults to the wire when frameSelection.flow is undefined AND mode is flow-based', () => {
|
|
112
|
-
// F10 Phase 2 review N3 — the realistic user-facing case:
|
|
113
|
-
// host writes `mode: 'flow-based'` but omits the flow sub-tree.
|
|
114
|
-
// Pre-B1-fix, the gate would silently run with native fallbacks
|
|
115
|
-
// (flowMaxTranslationCm=0, flowEvalEveryNFrames=1) instead of
|
|
116
|
-
// the JS defaults (50 cm budget, 5× throttle).
|
|
117
|
-
const s: PanoramaSettings = {
|
|
118
|
-
...DEFAULT_PANORAMA_SETTINGS,
|
|
119
|
-
frameSelection: {
|
|
120
|
-
mode: 'flow-based',
|
|
121
|
-
maxKeyframes: 6,
|
|
122
|
-
overlapThreshold: 0.20,
|
|
123
|
-
maxKeyframeIntervalMs: 2000,
|
|
124
|
-
// flow omitted — legal per the optional `?` in the type
|
|
125
|
-
},
|
|
126
|
-
};
|
|
127
|
-
const cfg = panoramaSettingsToNativeConfig(s);
|
|
128
|
-
|
|
129
|
-
expect(cfg.flowMaxTranslationCm).toBe(50);
|
|
130
|
-
expect(cfg.flowEvalEveryNFrames).toBe(5);
|
|
131
|
-
expect(cfg.flowNoveltyPercentile).toBe(0.85);
|
|
132
|
-
expect(cfg.flowMaxCorners).toBe(150);
|
|
133
|
-
expect(cfg.flowQualityLevel).toBe(0.01);
|
|
134
|
-
expect(cfg.flowMinDistance).toBe(10);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('locks down the full wire-key set for DEFAULT_PANORAMA_SETTINGS', () => {
|
|
138
|
-
// F10 Phase 2 review N4 — mirror the hybrid test below. Lock
|
|
139
|
-
// down which keys leave the bridge so a future field accidentally
|
|
140
|
-
// riding along (e.g. `debug` being treated as a wire knob) fails
|
|
141
|
-
// this test immediately.
|
|
142
|
-
const cfg = panoramaSettingsToNativeConfig(DEFAULT_PANORAMA_SETTINGS);
|
|
143
|
-
expect(Object.keys(cfg).sort()).toEqual([
|
|
144
|
-
'blenderType',
|
|
145
|
-
'captureSource',
|
|
146
|
-
'enableMaxInscribedRectCrop',
|
|
147
|
-
'flowEvalEveryNFrames',
|
|
148
|
-
'flowMaxCorners',
|
|
149
|
-
'flowMaxTranslationCm',
|
|
150
|
-
'flowMinDistance',
|
|
151
|
-
'flowNoveltyPercentile',
|
|
152
|
-
'flowQualityLevel',
|
|
153
|
-
'frameSelectionMode',
|
|
154
|
-
'keyframeMaxCount',
|
|
155
|
-
'keyframeOverlapThreshold',
|
|
156
|
-
'maxKeyframeIntervalMs',
|
|
157
|
-
'seamFinderType',
|
|
158
|
-
'stitchMode',
|
|
159
|
-
'warperType',
|
|
160
|
-
]);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('honours captureSource and stitcher overrides', () => {
|
|
164
|
-
const overridden: PanoramaSettings = {
|
|
165
|
-
...DEFAULT_PANORAMA_SETTINGS,
|
|
166
|
-
captureSource: 'non-ar',
|
|
167
|
-
debug: true,
|
|
168
|
-
stitcher: {
|
|
169
|
-
stitchMode: 'scans',
|
|
170
|
-
warperType: 'spherical',
|
|
171
|
-
blenderType: 'feather',
|
|
172
|
-
seamFinderType: 'skip',
|
|
173
|
-
enableMaxInscribedRectCrop: true,
|
|
174
|
-
},
|
|
175
|
-
};
|
|
176
|
-
const cfg = panoramaSettingsToNativeConfig(overridden);
|
|
177
|
-
|
|
178
|
-
expect(cfg.captureSource).toBe('non-ar');
|
|
179
|
-
expect(cfg.stitchMode).toBe('scans');
|
|
180
|
-
expect(cfg.warperType).toBe('spherical');
|
|
181
|
-
expect(cfg.blenderType).toBe('feather');
|
|
182
|
-
expect(cfg.seamFinderType).toBe('skip');
|
|
183
|
-
expect(cfg.enableMaxInscribedRectCrop).toBe(true);
|
|
184
|
-
// Note: `debug` is intentionally NOT on the wire — it's a
|
|
185
|
-
// JS-side UI gate, not a native config knob. The bridge MUST
|
|
186
|
-
// omit it; if a future change starts emitting it, the modal's
|
|
187
|
-
// operator-facing semantics will silently drift.
|
|
188
|
-
expect(cfg).not.toHaveProperty('debug');
|
|
189
|
-
});
|
|
190
|
-
});
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
/**
|
|
3
|
-
* Unit tests for the band/tile orientation-decision functions in
|
|
4
|
-
* `PanoramaBandOverlay` — the pure logic behind the v0.13.1 EXIF
|
|
5
|
-
* double-rotation fix.
|
|
6
|
-
*
|
|
7
|
-
* Why test the pure functions, not a render: the lib's jest config is
|
|
8
|
-
* pure-TS (`ts-jest` + node env, no `@testing-library/react-native`;
|
|
9
|
-
* see jest.config.js header). The orientation contract lives entirely
|
|
10
|
-
* in `bandThumbRotation` / `tileRotation`, which the component now calls
|
|
11
|
-
* directly — so exercising them here covers the real code path.
|
|
12
|
-
*
|
|
13
|
-
* The bug these guard against:
|
|
14
|
-
* Saved `keyframe-N.jpg` files are sensor-native LANDSCAPE pixels with
|
|
15
|
-
* EXIF Orientation = 6 ("rotate 90° CW"). RN's <Image> auto-rotates
|
|
16
|
-
* them upright. v0.12 ALSO applied a JS rotate transform to the tiles
|
|
17
|
-
* → double-rotation → thumbnails 90° off in portrait-locked landscape.
|
|
18
|
-
* The fix: tiles get NO transform in the portrait-locked
|
|
19
|
-
* (vertical=false) path; the single cumulative thumb (no EXIF) still
|
|
20
|
-
* does.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
// Mock react-native so importing the SUT module doesn't pull the native
|
|
24
|
-
// StyleSheet/Image bridge (we only call the pure functions). Matches
|
|
25
|
-
// the mocking approach in useOrientationDrift.test.ts.
|
|
26
|
-
jest.mock('react-native', () => ({
|
|
27
|
-
Image: 'Image',
|
|
28
|
-
ScrollView: 'ScrollView',
|
|
29
|
-
StyleSheet: { create: (s: Record<string, unknown>) => s, absoluteFill: {} },
|
|
30
|
-
Text: 'Text',
|
|
31
|
-
View: 'View',
|
|
32
|
-
}));
|
|
33
|
-
|
|
34
|
-
import {
|
|
35
|
-
_bandThumbRotationForTests as bandThumbRotation,
|
|
36
|
-
_tileRotationForTests as tileRotation,
|
|
37
|
-
type BandCaptureOrientation,
|
|
38
|
-
} from '../PanoramaBandOverlay';
|
|
39
|
-
|
|
40
|
-
const PORTRAIT: BandCaptureOrientation = 'portrait';
|
|
41
|
-
const UPSIDE: BandCaptureOrientation = 'portrait-upside-down';
|
|
42
|
-
const LEFT: BandCaptureOrientation = 'landscape-left';
|
|
43
|
-
const RIGHT: BandCaptureOrientation = 'landscape-right';
|
|
44
|
-
|
|
45
|
-
describe('bandThumbRotation — single cumulative thumb (no EXIF source)', () => {
|
|
46
|
-
describe('vertical=false (portrait-locked UI)', () => {
|
|
47
|
-
it('does not rotate in portrait', () => {
|
|
48
|
-
expect(bandThumbRotation(PORTRAIT, false)).toBeUndefined();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('does not rotate in portrait-upside-down', () => {
|
|
52
|
-
expect(bandThumbRotation(UPSIDE, false)).toBeUndefined();
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('rotates 90° CW for landscape-left', () => {
|
|
56
|
-
expect(bandThumbRotation(LEFT, false)).toEqual([{ rotate: '90deg' }]);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('rotates 90° CCW for landscape-right (opposite sign of left)', () => {
|
|
60
|
-
expect(bandThumbRotation(RIGHT, false)).toEqual([{ rotate: '-90deg' }]);
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
describe('vertical=true (non-locked, OS-rotated framebuffer)', () => {
|
|
65
|
-
it('does not rotate in portrait', () => {
|
|
66
|
-
expect(bandThumbRotation(PORTRAIT, true)).toBeUndefined();
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('uses the OPPOSITE sign from the portrait-locked case (left)', () => {
|
|
70
|
-
// vertical=false → 90deg, so vertical=true → -90deg.
|
|
71
|
-
expect(bandThumbRotation(LEFT, true)).toEqual([{ rotate: '-90deg' }]);
|
|
72
|
-
expect(bandThumbRotation(LEFT, true)).not.toEqual(
|
|
73
|
-
bandThumbRotation(LEFT, false),
|
|
74
|
-
);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('uses the OPPOSITE sign from the portrait-locked case (right)', () => {
|
|
78
|
-
expect(bandThumbRotation(RIGHT, true)).toEqual([{ rotate: '90deg' }]);
|
|
79
|
-
expect(bandThumbRotation(RIGHT, true)).not.toEqual(
|
|
80
|
-
bandThumbRotation(RIGHT, false),
|
|
81
|
-
);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe('tileRotation — per-keyframe tiles (EXIF-6 source, the fix)', () => {
|
|
87
|
-
describe('vertical=false (portrait-locked) — the regression case', () => {
|
|
88
|
-
it.each<[BandCaptureOrientation]>([
|
|
89
|
-
[PORTRAIT],
|
|
90
|
-
[UPSIDE],
|
|
91
|
-
[LEFT],
|
|
92
|
-
[RIGHT],
|
|
93
|
-
])(
|
|
94
|
-
'applies NO transform for %s (EXIF already auto-rotates → no double-rotate)',
|
|
95
|
-
(orientation) => {
|
|
96
|
-
expect(tileRotation(orientation, false)).toBeUndefined();
|
|
97
|
-
},
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
it('specifically does NOT rotate landscape tiles (the v0.12 bug)', () => {
|
|
101
|
-
// Pre-fix this returned [{rotate:'90deg'}] / [{rotate:'-90deg'}]
|
|
102
|
-
// on top of the EXIF auto-rotate → tiles 90° off. Must be undefined.
|
|
103
|
-
expect(tileRotation(LEFT, false)).toBeUndefined();
|
|
104
|
-
expect(tileRotation(RIGHT, false)).toBeUndefined();
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
describe('vertical=true (non-locked landscape) — transform still needed', () => {
|
|
109
|
-
it('matches bandThumbRotation in the vertical path', () => {
|
|
110
|
-
// In the OS-rotated case the box is landscape JS coords, 90° off
|
|
111
|
-
// the EXIF-upright tile, so the compensation IS required.
|
|
112
|
-
expect(tileRotation(LEFT, true)).toEqual(bandThumbRotation(LEFT, true));
|
|
113
|
-
expect(tileRotation(RIGHT, true)).toEqual(bandThumbRotation(RIGHT, true));
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('does not rotate in portrait even when vertical', () => {
|
|
117
|
-
expect(tileRotation(PORTRAIT, true)).toBeUndefined();
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
});
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
/**
|
|
3
|
-
* Unit tests for buildPanoramaInitialSettings — the prop→settings-tree
|
|
4
|
-
* translation that runs once at <Camera>'s mount.
|
|
5
|
-
*
|
|
6
|
-
* Coverage:
|
|
7
|
-
*
|
|
8
|
-
* - Defaults (no prop overrides) reproduce DEFAULT_PANORAMA_SETTINGS
|
|
9
|
-
* when `isLowMemDevice=false`.
|
|
10
|
-
* - `isLowMemDevice=true` swaps blender + seamFinder defaults to the
|
|
11
|
-
* feather+skip fallback; other fields unchanged.
|
|
12
|
-
* - Every prop override routes to its hierarchical path: stitchMode →
|
|
13
|
-
* stitcher.stitchMode, defaultFlowMaxTranslationCm →
|
|
14
|
-
* frameSelection.flow.maxTranslationCm, etc.
|
|
15
|
-
* - Partial overrides leave non-overridden fields at the default.
|
|
16
|
-
*
|
|
17
|
-
* Plus a "wire-format integration" check: the produced settings tree,
|
|
18
|
-
* fed through `panoramaSettingsToNativeConfig`, lands at the expected
|
|
19
|
-
* flat dict. This is the seam where the prop translation, the
|
|
20
|
-
* hierarchical tree, and the bridge all meet — verifying it here means
|
|
21
|
-
* <Camera>'s `incremental.start({ config })` call is correctly wired
|
|
22
|
-
* end-to-end on the JS side (on-device run remains the integration
|
|
23
|
-
* check across the JS/native boundary itself).
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import {
|
|
27
|
-
DEFAULT_PANORAMA_SETTINGS,
|
|
28
|
-
type PanoramaSettings,
|
|
29
|
-
} from '../PanoramaSettings';
|
|
30
|
-
import { panoramaSettingsToNativeConfig } from '../PanoramaSettingsBridge';
|
|
31
|
-
import { buildPanoramaInitialSettings } from '../buildPanoramaInitialSettings';
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
describe('buildPanoramaInitialSettings', () => {
|
|
35
|
-
it('returns DEFAULT_PANORAMA_SETTINGS verbatim when no overrides and not low-mem', () => {
|
|
36
|
-
const s = buildPanoramaInitialSettings({}, false);
|
|
37
|
-
expect(s).toEqual(DEFAULT_PANORAMA_SETTINGS);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('swaps blender + seamFinder for the low-mem fallback', () => {
|
|
41
|
-
const s = buildPanoramaInitialSettings({}, true);
|
|
42
|
-
expect(s.stitcher.blenderType).toBe('feather');
|
|
43
|
-
expect(s.stitcher.seamFinderType).toBe('skip');
|
|
44
|
-
// Everything else stays at the static default.
|
|
45
|
-
expect(s.stitcher.stitchMode).toBe(DEFAULT_PANORAMA_SETTINGS.stitcher.stitchMode);
|
|
46
|
-
expect(s.stitcher.warperType).toBe(DEFAULT_PANORAMA_SETTINGS.stitcher.warperType);
|
|
47
|
-
expect(s.frameSelection).toEqual(DEFAULT_PANORAMA_SETTINGS.frameSelection);
|
|
48
|
-
expect(s.captureSource).toBe(DEFAULT_PANORAMA_SETTINGS.captureSource);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('routes every prop override to its hierarchical path', () => {
|
|
52
|
-
const s = buildPanoramaInitialSettings(
|
|
53
|
-
{
|
|
54
|
-
defaultCaptureSource: 'non-ar',
|
|
55
|
-
defaultStitchMode: 'scans',
|
|
56
|
-
defaultBlender: 'feather',
|
|
57
|
-
defaultSeamFinder: 'skip',
|
|
58
|
-
defaultWarper: 'cylindrical',
|
|
59
|
-
defaultFlowNoveltyPercentile: 0.70,
|
|
60
|
-
defaultFlowEvalEveryNFrames: 3,
|
|
61
|
-
defaultFlowMaxTranslationCm: 12,
|
|
62
|
-
defaultKeyframeMaxCount: 8,
|
|
63
|
-
defaultKeyframeOverlapThreshold: 0.30,
|
|
64
|
-
maxInscribedRectCrop: true,
|
|
65
|
-
},
|
|
66
|
-
false,
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
expect(s.captureSource).toBe('non-ar');
|
|
70
|
-
expect(s.stitcher.stitchMode).toBe('scans');
|
|
71
|
-
expect(s.stitcher.blenderType).toBe('feather');
|
|
72
|
-
expect(s.stitcher.seamFinderType).toBe('skip');
|
|
73
|
-
expect(s.stitcher.warperType).toBe('cylindrical');
|
|
74
|
-
expect(s.frameSelection.flow?.noveltyPercentile).toBe(0.70);
|
|
75
|
-
expect(s.frameSelection.flow?.evalEveryNFrames).toBe(3);
|
|
76
|
-
expect(s.frameSelection.flow?.maxTranslationCm).toBe(12);
|
|
77
|
-
expect(s.frameSelection.maxKeyframes).toBe(8);
|
|
78
|
-
expect(s.frameSelection.overlapThreshold).toBe(0.30);
|
|
79
|
-
expect(s.stitcher.enableMaxInscribedRectCrop).toBe(true);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('maps maxInscribedRectCrop → stitcher.enableMaxInscribedRectCrop', () => {
|
|
83
|
-
expect(
|
|
84
|
-
buildPanoramaInitialSettings({ maxInscribedRectCrop: true }, false)
|
|
85
|
-
.stitcher.enableMaxInscribedRectCrop,
|
|
86
|
-
).toBe(true);
|
|
87
|
-
expect(
|
|
88
|
-
buildPanoramaInitialSettings({ maxInscribedRectCrop: false }, false)
|
|
89
|
-
.stitcher.enableMaxInscribedRectCrop,
|
|
90
|
-
).toBe(false);
|
|
91
|
-
// Omitted ⇒ default (false — inscribed-rect crop is opt-in), and the
|
|
92
|
-
// low-mem fallback must not flip it.
|
|
93
|
-
expect(
|
|
94
|
-
buildPanoramaInitialSettings({}, false)
|
|
95
|
-
.stitcher.enableMaxInscribedRectCrop,
|
|
96
|
-
).toBe(false);
|
|
97
|
-
expect(
|
|
98
|
-
buildPanoramaInitialSettings({}, true)
|
|
99
|
-
.stitcher.enableMaxInscribedRectCrop,
|
|
100
|
-
).toBe(false);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('maps defaultMaxKeyframeIntervalMs → frameSelection.maxKeyframeIntervalMs', () => {
|
|
104
|
-
expect(
|
|
105
|
-
buildPanoramaInitialSettings({ defaultMaxKeyframeIntervalMs: 3500 }, false)
|
|
106
|
-
.frameSelection.maxKeyframeIntervalMs,
|
|
107
|
-
).toBe(3500);
|
|
108
|
-
// 0 explicitly disables the time-budget force-accept — it is NOT
|
|
109
|
-
// nullish, so `??` does not replace it with the default.
|
|
110
|
-
expect(
|
|
111
|
-
buildPanoramaInitialSettings({ defaultMaxKeyframeIntervalMs: 0 }, false)
|
|
112
|
-
.frameSelection.maxKeyframeIntervalMs,
|
|
113
|
-
).toBe(0);
|
|
114
|
-
// Omitted ⇒ the 2000 ms default.
|
|
115
|
-
expect(
|
|
116
|
-
buildPanoramaInitialSettings({}, false)
|
|
117
|
-
.frameSelection.maxKeyframeIntervalMs,
|
|
118
|
-
).toBe(2000);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('leaves non-overridden fields at the default (partial override)', () => {
|
|
122
|
-
const s = buildPanoramaInitialSettings(
|
|
123
|
-
{ defaultStitchMode: 'panorama' },
|
|
124
|
-
false,
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
// The override took effect …
|
|
128
|
-
expect(s.stitcher.stitchMode).toBe('panorama');
|
|
129
|
-
|
|
130
|
-
// … and every other field stays at the corresponding default.
|
|
131
|
-
expect(s.stitcher.warperType).toBe(DEFAULT_PANORAMA_SETTINGS.stitcher.warperType);
|
|
132
|
-
expect(s.stitcher.blenderType).toBe(DEFAULT_PANORAMA_SETTINGS.stitcher.blenderType);
|
|
133
|
-
expect(s.stitcher.seamFinderType).toBe(DEFAULT_PANORAMA_SETTINGS.stitcher.seamFinderType);
|
|
134
|
-
expect(s.frameSelection).toEqual(DEFAULT_PANORAMA_SETTINGS.frameSelection);
|
|
135
|
-
expect(s.captureSource).toBe(DEFAULT_PANORAMA_SETTINGS.captureSource);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('produces wire-format-clean output when piped through the bridge', () => {
|
|
139
|
-
// The end-to-end JS-side path: props → buildPanoramaInitialSettings →
|
|
140
|
-
// panoramaSettingsToNativeConfig. Verifying it here catches drift
|
|
141
|
-
// at any of the three layers (prop name, type-tree shape, bridge
|
|
142
|
-
// adapter) with a single assertion.
|
|
143
|
-
const overrides = {
|
|
144
|
-
defaultCaptureSource: 'non-ar' as const,
|
|
145
|
-
defaultStitchMode: 'scans' as const,
|
|
146
|
-
defaultFlowMaxTranslationCm: 25,
|
|
147
|
-
};
|
|
148
|
-
const settings: PanoramaSettings =
|
|
149
|
-
buildPanoramaInitialSettings(overrides, false);
|
|
150
|
-
const wire = panoramaSettingsToNativeConfig(settings);
|
|
151
|
-
|
|
152
|
-
expect(wire.captureSource).toBe('non-ar');
|
|
153
|
-
expect(wire.stitchMode).toBe('scans');
|
|
154
|
-
expect(wire.flowMaxTranslationCm).toBe(25);
|
|
155
|
-
// Defaulted fields still on the wire with their default value.
|
|
156
|
-
expect(wire.warperType).toBe('plane');
|
|
157
|
-
expect(wire.frameSelectionMode).toBe('flow-based');
|
|
158
|
-
expect(wire.flowNoveltyPercentile).toBe(0.85);
|
|
159
|
-
});
|
|
160
|
-
});
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
/**
|
|
3
|
-
* Unit tests for the recoverable-stitch-failure guidance map.
|
|
4
|
-
*
|
|
5
|
-
* Guarantees: (1) every recoverable code yields non-empty, plain-language
|
|
6
|
-
* copy with no raw cv::Stitcher diagnostic leaking through; (2) the
|
|
7
|
-
* cause-specific guidance actually names its corrective action; (3) every
|
|
8
|
-
* non-recoverable code returns null so the host falls back to its generic
|
|
9
|
-
* error UI.
|
|
10
|
-
*/
|
|
11
|
-
import { userFacingStitchError } from '../cameraErrorMessages';
|
|
12
|
-
import type { CameraErrorCode } from '../Camera';
|
|
13
|
-
|
|
14
|
-
describe('userFacingStitchError', () => {
|
|
15
|
-
const RECOVERABLE: CameraErrorCode[] = [
|
|
16
|
-
'STITCH_NEED_MORE_IMGS',
|
|
17
|
-
'STITCH_HOMOGRAPHY_FAIL',
|
|
18
|
-
'STITCH_CAMERA_PARAMS_FAIL',
|
|
19
|
-
'STITCH_OOM',
|
|
20
|
-
];
|
|
21
|
-
|
|
22
|
-
it.each(RECOVERABLE)(
|
|
23
|
-
'returns non-empty, jargon-free title+message for %s',
|
|
24
|
-
(code) => {
|
|
25
|
-
const r = userFacingStitchError(code);
|
|
26
|
-
expect(r).not.toBeNull();
|
|
27
|
-
expect(r!.title.length).toBeGreaterThan(0);
|
|
28
|
-
expect(r!.message.length).toBeGreaterThan(0);
|
|
29
|
-
// No raw stitcher diagnostics should ever reach the user.
|
|
30
|
-
expect(r!.message).not.toMatch(/warpRoi|cv::|OpenCV|ERR_|StsOutOfRange|estimator/i);
|
|
31
|
-
expect(r!.title).not.toMatch(/cv::|OpenCV|ERR_/i);
|
|
32
|
-
// The title is the corrective ASK (e.g. "Please pan more slowly"),
|
|
33
|
-
// not a generic failure headline.
|
|
34
|
-
expect(r!.title).not.toMatch(/couldn't|can't|error|failed|too large/i);
|
|
35
|
-
},
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
it('camera-params guidance names the 0.5x sensitivity and the 1x fix', () => {
|
|
39
|
-
const r = userFacingStitchError('STITCH_CAMERA_PARAMS_FAIL');
|
|
40
|
-
expect(r).not.toBeNull();
|
|
41
|
-
// The actual root cause (translation) + the actionable lens advice.
|
|
42
|
-
expect(r!.message).toMatch(/0\.5x|ultra-wide/i);
|
|
43
|
-
expect(r!.message).toMatch(/\b1x\b/i);
|
|
44
|
-
expect(r!.message).toMatch(/pivot|turning|one spot|moved|shifted/i);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('need-more-images guidance is about overlap', () => {
|
|
48
|
-
expect(userFacingStitchError('STITCH_NEED_MORE_IMGS')!.message).toMatch(
|
|
49
|
-
/overlap/i,
|
|
50
|
-
);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('oom guidance suggests a shorter sweep', () => {
|
|
54
|
-
expect(userFacingStitchError('STITCH_OOM')!.message).toMatch(
|
|
55
|
-
/shorter|narrower|memory/i,
|
|
56
|
-
);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const NON_RECOVERABLE: CameraErrorCode[] = [
|
|
60
|
-
'CAMERA_PERMISSION_DENIED',
|
|
61
|
-
'CAMERA_DEVICE_UNAVAILABLE',
|
|
62
|
-
'PHOTO_CAPTURE_FAILED',
|
|
63
|
-
'PANORAMA_START_FAILED',
|
|
64
|
-
'PANORAMA_FINALIZE_FAILED',
|
|
65
|
-
'OUTPUT_WRITE_FAILED',
|
|
66
|
-
'VISION_CAMERA_RUNTIME',
|
|
67
|
-
'UNKNOWN',
|
|
68
|
-
];
|
|
69
|
-
|
|
70
|
-
it.each(NON_RECOVERABLE)(
|
|
71
|
-
'returns null for non-recoverable code %s',
|
|
72
|
-
(code) => {
|
|
73
|
-
expect(userFacingStitchError(code)).toBeNull();
|
|
74
|
-
},
|
|
75
|
-
);
|
|
76
|
-
});
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
/**
|
|
3
|
-
* Unit tests for `homeIndicatorEdge` + `isSideEdge` — the pure functions
|
|
4
|
-
* that produce the `vertical` flag driving PanoramaBandOverlay and
|
|
5
|
-
* CaptureThumbnailStrip layout under non-locked hosts.
|
|
6
|
-
*
|
|
7
|
-
* vertical = isSideEdge(homeIndicatorEdge(jsLandscape, deviceOrient))
|
|
8
|
-
*
|
|
9
|
-
* Contract (v0.12 orientation-aware Camera):
|
|
10
|
-
* - Portrait JS layout (jsLandscape=false) → 'bottom' edge → NOT a
|
|
11
|
-
* side edge → vertical=false (horizontal strip, the portrait-locked
|
|
12
|
-
* case that's the recommended config).
|
|
13
|
-
* - Landscape JS layout → 'right'/'left' edge → side edge →
|
|
14
|
-
* vertical=true (the strip/band stack along the home-indicator edge).
|
|
15
|
-
*
|
|
16
|
-
* Pure-TS test per jest.config.js (no component mount). The functions
|
|
17
|
-
* are imported via Camera.tsx's `_*ForTests` handles; react-native and
|
|
18
|
-
* the heavy native deps are mocked so the import resolves in node env.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
// The SUT lives in Camera.tsx, which transitively imports the entire
|
|
22
|
-
// camera surface (vision-camera, worklets, sensors, native modules).
|
|
23
|
-
// We only call two pure functions, so stub the whole dependency tree.
|
|
24
|
-
jest.mock('react-native', () => ({
|
|
25
|
-
NativeModules: {},
|
|
26
|
-
Platform: { OS: 'ios', select: (o: Record<string, unknown>) => o.ios },
|
|
27
|
-
Pressable: 'Pressable',
|
|
28
|
-
StyleSheet: { create: (s: Record<string, unknown>) => s, absoluteFill: {} },
|
|
29
|
-
Text: 'Text',
|
|
30
|
-
View: 'View',
|
|
31
|
-
Image: 'Image',
|
|
32
|
-
ScrollView: 'ScrollView',
|
|
33
|
-
Animated: { View: 'Animated.View', Value: class {}, timing: () => ({ start: () => undefined }) },
|
|
34
|
-
Modal: 'Modal',
|
|
35
|
-
ActivityIndicator: 'ActivityIndicator',
|
|
36
|
-
useWindowDimensions: () => ({ width: 0, height: 0 }),
|
|
37
|
-
requireNativeComponent: () => 'NativeComponent',
|
|
38
|
-
UIManager: { getViewManagerConfig: () => ({}) },
|
|
39
|
-
findNodeHandle: () => 1,
|
|
40
|
-
}));
|
|
41
|
-
jest.mock('react-native-safe-area-context', () => ({
|
|
42
|
-
useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 }),
|
|
43
|
-
}));
|
|
44
|
-
jest.mock('react-native-sensors', () => ({
|
|
45
|
-
accelerometer: { subscribe: jest.fn(() => ({ unsubscribe: jest.fn() })) },
|
|
46
|
-
setUpdateIntervalForType: jest.fn(),
|
|
47
|
-
SensorTypes: { accelerometer: 'accelerometer' },
|
|
48
|
-
}));
|
|
49
|
-
jest.mock('react-native-worklets-core', () => ({ Worklets: {} }));
|
|
50
|
-
jest.mock('react-native-vision-camera', () => ({
|
|
51
|
-
Camera: 'Camera',
|
|
52
|
-
useCameraDevice: jest.fn(),
|
|
53
|
-
useCameraPermission: jest.fn(),
|
|
54
|
-
}));
|
|
55
|
-
|
|
56
|
-
import {
|
|
57
|
-
_homeIndicatorEdgeForTests as homeIndicatorEdge,
|
|
58
|
-
_isSideEdgeForTests as isSideEdge,
|
|
59
|
-
} from '../Camera';
|
|
60
|
-
import type { DeviceOrientation } from '../useDeviceOrientation';
|
|
61
|
-
|
|
62
|
-
const PORTRAIT: DeviceOrientation = 'portrait';
|
|
63
|
-
const UPSIDE: DeviceOrientation = 'portrait-upside-down';
|
|
64
|
-
const LEFT: DeviceOrientation = 'landscape-left';
|
|
65
|
-
const RIGHT: DeviceOrientation = 'landscape-right';
|
|
66
|
-
|
|
67
|
-
// The composed signal the band/strip actually consume.
|
|
68
|
-
const vertical = (jsLandscape: boolean, o: DeviceOrientation) =>
|
|
69
|
-
isSideEdge(homeIndicatorEdge(jsLandscape, o));
|
|
70
|
-
|
|
71
|
-
describe('homeIndicatorEdge', () => {
|
|
72
|
-
it('returns bottom for any portrait JS layout (jsLandscape=false)', () => {
|
|
73
|
-
// Portrait JS layout always anchors bottom regardless of the sensor
|
|
74
|
-
// value — this is the portrait-locked case (the recommended config).
|
|
75
|
-
for (const o of [PORTRAIT, UPSIDE, LEFT, RIGHT]) {
|
|
76
|
-
expect(homeIndicatorEdge(false, o)).toBe('bottom');
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('anchors RIGHT for landscape-left device in landscape JS layout', () => {
|
|
81
|
-
expect(homeIndicatorEdge(true, LEFT)).toBe('right');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('anchors LEFT for landscape-right device in landscape JS layout', () => {
|
|
85
|
-
expect(homeIndicatorEdge(true, RIGHT)).toBe('left');
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('falls through to right for non-landscape sensor + landscape JS (transient)', () => {
|
|
89
|
-
// jsLandscape=true with a portrait sensor reading only happens
|
|
90
|
-
// mid-rotation; defensive default is 'right'.
|
|
91
|
-
expect(homeIndicatorEdge(true, PORTRAIT)).toBe('right');
|
|
92
|
-
expect(homeIndicatorEdge(true, UPSIDE)).toBe('right');
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
describe('isSideEdge', () => {
|
|
97
|
-
it('is true only for left/right edges', () => {
|
|
98
|
-
expect(isSideEdge('left')).toBe(true);
|
|
99
|
-
expect(isSideEdge('right')).toBe(true);
|
|
100
|
-
expect(isSideEdge('bottom')).toBe(false);
|
|
101
|
-
expect(isSideEdge('top')).toBe(false);
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
describe('vertical flag (composed) — what the strip/band consume', () => {
|
|
106
|
-
it('is FALSE for portrait-locked layout (horizontal strip, recommended)', () => {
|
|
107
|
-
for (const o of [PORTRAIT, UPSIDE, LEFT, RIGHT]) {
|
|
108
|
-
expect(vertical(false, o)).toBe(false);
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('is TRUE for both landscape orientations under a non-locked host', () => {
|
|
113
|
-
expect(vertical(true, LEFT)).toBe(true);
|
|
114
|
-
expect(vertical(true, RIGHT)).toBe(true);
|
|
115
|
-
});
|
|
116
|
-
});
|