Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/simple-camera/__tests__/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Tests are split by domain. Each file tests one slice of the imperative `VisionCa
| [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 |
| [visioncamera.constraints.harness.ts](visioncamera.constraints.harness.ts) | `VisionCamera.resolveConstraints` + `onSessionConfigSelected`, FPS / HDR / stabilization / binned / pixelFormat / resolutionBias constraints |
| [visioncamera.controller.harness.ts](visioncamera.controller.harness.ts) | `CameraController` — zoom, torch, exposure bias, focus metering, low-light boost, subject area listener |
| [visioncamera.hooks.harness.tsx](visioncamera.hooks.harness.tsx) | React hook reactivity for `useCameraDevice(...)` position and physical-device filter changes |
| [visioncamera.hooks.harness.tsx](visioncamera.hooks.harness.tsx) | React hook reactivity for `useCameraDevice(...)` position and physical-device filter changes, and `useCamera(...).onUIRotationChanged` |
| [visioncamera.utils.harness.ts](visioncamera.utils.harness.ts) | Pure public utilities such as `getUIRotation(...)` across every output/interface orientation pair |
| [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 |
| [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 |
Expand Down
129 changes: 128 additions & 1 deletion apps/simple-camera/__tests__/visioncamera.hooks.harness.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect } from 'react'
import { StyleSheet } from 'react-native'
import {
beforeAll,
describe,
Expand All @@ -9,14 +10,27 @@ import {
render,
waitFor,
} from 'react-native-harness'
import {
Screen,
type ScreenOrientationTypes,
ScreenStack,
} from 'react-native-screens'
import type {
CameraDevice,
CameraDeviceFactory,
CameraOrientation,
CameraPosition,
DeviceFilter,
TargetCameraPosition,
} from 'react-native-vision-camera'
import { useCameraDevice, VisionCamera } from 'react-native-vision-camera'
import {
getUIRotation,
useCamera,
useCameraDevice,
useOrientation,
usePreviewOutput,
VisionCamera,
} from 'react-native-vision-camera'

interface DeviceSnapshot {
requestedPosition: TargetCameraPosition
Expand Down Expand Up @@ -214,4 +228,117 @@ describe('VisionCamera - Hooks', () => {
)
await expectLatestDeviceSnapshot(onSnapshot, 'back', tripleDevice)
})

it('updates onUIRotationChanged when the interface orientation changes', async () => {
const onConfigured = fn<() => void>()
const onInterfaceOrientationChanged =
fn<(orientation: CameraOrientation | undefined) => void>()
const onUIRotationChanged = fn<(rotation: number) => void>()
const onError = fn<(error: Error) => void>()

function TestCamera({
screenOrientation,
}: {
screenOrientation: ScreenOrientationTypes
}): React.ReactElement {
// CameraX requires at least one use case when configuring a session.
const previewOutput = usePreviewOutput()
const interfaceOrientation = useOrientation('interface')
useEffect(() => {
onInterfaceOrientationChanged(interfaceOrientation)
}, [interfaceOrientation])
useCamera({
isActive: false,
device: 'back',
outputs: [previewOutput],
orientationSource: 'custom',
onConfigured,
onUIRotationChanged,
onError,
})

return (
<ScreenStack style={StyleSheet.absoluteFill}>
<Screen
enabled={true}
activityState={2}
screenOrientation={screenOrientation}
style={StyleSheet.absoluteFill}
/>
</ScreenStack>
)
}

const waitForRotation = async (
allowedOrientations: readonly CameraOrientation[],
): Promise<CameraOrientation> => {
let receivedOrientation: CameraOrientation | undefined
await waitFor(
() => {
const error = onError.mock.lastCall?.[0]
if (error != null) throw error

const orientation = onInterfaceOrientationChanged.mock.lastCall?.[0]
if (orientation == null) {
throw new Error('No interface orientation was received yet.')
}
receivedOrientation = orientation
expect(allowedOrientations).toContain(orientation)
const expectedRotation = getUIRotation('up', orientation)
expect(onUIRotationChanged).toHaveBeenLastCalledWith(expectedRotation)
},
{ timeout: 10_000 },
)
if (receivedOrientation == null) {
throw new Error('No interface orientation was received.')
}
return receivedOrientation
}

const { rerender } = await render(
<TestCamera screenOrientation="portrait_up" />,
{
timeout: 10_000,
},
)
await waitFor(
() => {
const error = onError.mock.lastCall?.[0]
if (error != null) throw error
expect(onConfigured).toHaveBeenCalledTimes(1)
},
{ timeout: 10_000 },
)
await waitFor(
() => {
const error = onError.mock.lastCall?.[0]
if (error != null) throw error
const expectedRotation = getUIRotation('up', 'up')
expect(onUIRotationChanged).toHaveBeenLastCalledWith(expectedRotation)
},
{ timeout: 10_000 },
)

try {
onInterfaceOrientationChanged.mockClear()
onUIRotationChanged.mockClear()
await rerender(<TestCamera screenOrientation="landscape_left" />)
const firstLandscapeOrientation = await waitForRotation(['left', 'right'])
const oppositeLandscapeOrientation =
firstLandscapeOrientation === 'left' ? 'right' : 'left'
onInterfaceOrientationChanged.mockClear()
onUIRotationChanged.mockClear()
await rerender(<TestCamera screenOrientation="landscape_right" />)
await waitForRotation([oppositeLandscapeOrientation])

onInterfaceOrientationChanged.mockClear()
onUIRotationChanged.mockClear()
await rerender(<TestCamera screenOrientation="portrait_up" />)
await waitForRotation(['up'])
} finally {
await rerender(<TestCamera screenOrientation="portrait_up" />)
}

expect(onError).not.toHaveBeenCalled()
})
})
Loading