Skip to content

Commit b047e10

Browse files
committed
test: shard CameraController Harness tests
1 parent 3106137 commit b047e10

6 files changed

Lines changed: 765 additions & 699 deletions

apps/simple-camera/__tests__/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ Tests are split by domain. Each file tests one slice of the imperative `VisionCa
2626
| [visioncamera.frame.harness.ts](visioncamera.frame.harness.ts) | `createFrameOutput`, worklet install via `react-native-vision-camera-worklets`, YUV / RGB / native pixel formats, `scheduleOnRN`, `createSynchronizable`, `setOnFrameDroppedCallback`, `enablePreviewSizedOutputBuffers` |
2727
| [visioncamera.multi-output.harness.ts](visioncamera.multi-output.harness.ts) | Multi-output sessions that combine photo, video, and frame outputs, output replacement while other outputs stay attached, persistent recording across session restarts |
2828
| [visioncamera.constraints.harness.ts](visioncamera.constraints.harness.ts) | `VisionCamera.resolveConstraints` + `onSessionConfigSelected`, FPS / HDR / stabilization / binned / pixelFormat / resolutionBias constraints |
29-
| [visioncamera.controller.harness.ts](visioncamera.controller.harness.ts) | `CameraController` — zoom, torch, exposure bias, focus metering, low-light boost, subject area listener |
29+
| [visioncamera.controller.zoom.harness.ts](visioncamera.controller.zoom.harness.ts) | `CameraController` zoom |
30+
| [visioncamera.controller.torch.harness.ts](visioncamera.controller.torch.harness.ts) | `CameraController` torch and torch strength |
31+
| [visioncamera.controller.exposure.harness.ts](visioncamera.controller.exposure.harness.ts) | `CameraController` exposure bias and low-light boost |
32+
| [visioncamera.controller.metering.harness.ts](visioncamera.controller.metering.harness.ts) | `CameraController` focus metering, focus / exposure / white-balance locking, subject area listener |
3033
| [visioncamera.coordinates.harness.ts](visioncamera.coordinates.harness.ts) | `Frame.convertFramePointToCameraPoint` / `convertCameraPointToFramePoint`, `PreviewView.convertViewPointToCameraPoint` / `convertCameraPointToViewPoint`, `PreviewView.createMeteringPoint`, `convertScannedObjectCoordinatesToViewCoordinates`, end-to-end Frame → Camera → View round-trip |
3134
| [visioncamera.nativepreviewview.harness.tsx](visioncamera.nativepreviewview.harness.tsx) | Bare `NativePreviewView` lifecycle, layout-sensitive preview regression coverage, `resizeMode`, Android `implementationMode`, gesture controllers, multi-preview mounting, `PreviewView` ref methods, Android `takeSnapshot()` dimensions |
3235
| [visioncamera.camera-view.harness.tsx](visioncamera.camera-view.harness.tsx) | High-level `<Camera>` preview lifecycle, photo output integration, controller props, native gestures, `CameraRef` methods, `isActive`, mount / unmount / replacement behavior |
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { beforeAll, describe, expect, it } from 'react-native-harness'
2+
import type {
3+
CameraDevice,
4+
CameraDeviceFactory,
5+
} from 'react-native-vision-camera'
6+
import { CommonResolutions, VisionCamera } from 'react-native-vision-camera'
7+
8+
describe('VisionCamera - Controller Exposure', () => {
9+
let factory: CameraDeviceFactory
10+
let backDevice: CameraDevice
11+
12+
beforeAll(async () => {
13+
await VisionCamera.requestCameraPermission()
14+
expect(VisionCamera.cameraPermissionStatus).toBe('authorized')
15+
factory = await VisionCamera.createDeviceFactory()
16+
const back = factory.getDefaultCamera('back')
17+
expect(back).toBeDefined()
18+
if (back == null) throw new Error('no back camera')
19+
backDevice = back
20+
})
21+
22+
it('sets exposure bias to min/max when the device supports it', async (context) => {
23+
if (!backDevice.supportsExposureBias) {
24+
return context.skip('exposureBias: not supported on this device')
25+
}
26+
const session = await VisionCamera.createCameraSession(false)
27+
const photoOutput = VisionCamera.createPhotoOutput({
28+
targetResolution: CommonResolutions.HD_4_3,
29+
containerFormat: 'jpeg',
30+
quality: 0.8,
31+
qualityPrioritization: 'balanced',
32+
})
33+
const [controller] = await session.configure([
34+
{
35+
input: backDevice,
36+
outputs: [{ output: photoOutput, mirrorMode: 'auto' }],
37+
constraints: [],
38+
},
39+
])
40+
if (controller == null) throw new Error('no controller')
41+
await session.start()
42+
43+
try {
44+
// AVCaptureDevice quantizes the requested bias to a discrete rational
45+
// step, so the readback may differ from the request by a tiny epsilon.
46+
await controller.setExposureBias(backDevice.maxExposureBias)
47+
expect(controller.exposureBias).toBeCloseTo(backDevice.maxExposureBias, 4)
48+
49+
await controller.setExposureBias(backDevice.minExposureBias)
50+
expect(controller.exposureBias).toBeCloseTo(backDevice.minExposureBias, 4)
51+
52+
await controller.setExposureBias(0)
53+
expect(controller.exposureBias).toBeCloseTo(0, 4)
54+
} finally {
55+
await session.stop()
56+
}
57+
})
58+
59+
it('rejects setExposureBias outside the device range', async (context) => {
60+
if (!backDevice.supportsExposureBias) {
61+
return context.skip(
62+
'exposureBias out-of-range: not supported on this device',
63+
)
64+
}
65+
const session = await VisionCamera.createCameraSession(false)
66+
const photoOutput = VisionCamera.createPhotoOutput({
67+
targetResolution: CommonResolutions.HD_4_3,
68+
containerFormat: 'jpeg',
69+
quality: 0.8,
70+
qualityPrioritization: 'balanced',
71+
})
72+
const [controller] = await session.configure([
73+
{
74+
input: backDevice,
75+
outputs: [{ output: photoOutput, mirrorMode: 'auto' }],
76+
constraints: [],
77+
},
78+
])
79+
if (controller == null) throw new Error('no controller')
80+
await session.start()
81+
82+
try {
83+
const tooHighExposureBias = backDevice.maxExposureBias + 1
84+
const tooLowExposureBias = backDevice.minExposureBias - 1
85+
await expect(
86+
controller.setExposureBias(tooHighExposureBias),
87+
).rejects.toThrow()
88+
await expect(
89+
controller.setExposureBias(tooLowExposureBias),
90+
).rejects.toThrow()
91+
} finally {
92+
await session.stop()
93+
}
94+
})
95+
96+
// TODO(Android): Re-enable once initial CameraX control config is applied after the camera is active.
97+
it.skip('honors initialExposureBias passed to configure', async (context) => {
98+
if (!backDevice.supportsExposureBias) {
99+
return context.skip(
100+
'initialExposureBias: device does not support exposure bias',
101+
)
102+
}
103+
const session = await VisionCamera.createCameraSession(false)
104+
const photoOutput = VisionCamera.createPhotoOutput({
105+
targetResolution: CommonResolutions.HD_4_3,
106+
containerFormat: 'jpeg',
107+
quality: 0.8,
108+
qualityPrioritization: 'balanced',
109+
})
110+
const initial = backDevice.maxExposureBias
111+
const [controller] = await session.configure([
112+
{
113+
input: backDevice,
114+
outputs: [{ output: photoOutput, mirrorMode: 'auto' }],
115+
constraints: [],
116+
initialExposureBias: initial,
117+
},
118+
])
119+
if (controller == null) throw new Error('no controller')
120+
await session.start()
121+
122+
try {
123+
expect(controller.exposureBias).toBeCloseTo(initial, 4)
124+
} finally {
125+
await session.stop()
126+
}
127+
})
128+
129+
it('enables low-light boost via CameraController.configure when supported', async (context) => {
130+
const lowLightDevice = factory.cameraDevices.find(
131+
(d) => d.supportsLowLightBoost,
132+
)
133+
if (lowLightDevice == null) {
134+
return context.skip(
135+
'low-light boost: no device on this system supports it',
136+
)
137+
}
138+
const session = await VisionCamera.createCameraSession(false)
139+
const photoOutput = VisionCamera.createPhotoOutput({
140+
targetResolution: CommonResolutions.HD_4_3,
141+
containerFormat: 'jpeg',
142+
quality: 0.8,
143+
qualityPrioritization: 'balanced',
144+
})
145+
const [controller] = await session.configure([
146+
{
147+
input: lowLightDevice,
148+
outputs: [{ output: photoOutput, mirrorMode: 'auto' }],
149+
constraints: [],
150+
},
151+
])
152+
if (controller == null) throw new Error('no controller')
153+
await session.start()
154+
155+
try {
156+
await controller.configure({ enableLowLightBoost: true })
157+
expect(controller.isLowLightBoostEnabled).toBe(true)
158+
await controller.configure({ enableLowLightBoost: false })
159+
expect(controller.isLowLightBoostEnabled).toBe(false)
160+
} finally {
161+
await session.stop()
162+
}
163+
})
164+
})

0 commit comments

Comments
 (0)