hardened camera mounting handling
This commit is contained in:
@@ -1,10 +1,30 @@
|
||||
import { fireEvent, screen, waitFor } from "@testing-library/react-native"
|
||||
import { act, fireEvent, screen, waitFor } from "@testing-library/react-native"
|
||||
import React from "react"
|
||||
|
||||
import NewNoteScreen from "@/app/newNote"
|
||||
import { useNotes } from "@/src/notes/NotesContext"
|
||||
import { pickImageFromCamera, pickImageFromLibrary } from "@/src/notes/native-image-picker"
|
||||
import { renderWithTheme } from "@/test-utils/renderWithTheme"
|
||||
import { router } from "expo-router"
|
||||
import { useIsFocused } from "@react-navigation/native"
|
||||
|
||||
type Deferred<T> = {
|
||||
promise: Promise<T>
|
||||
resolve: (value: T) => void
|
||||
reject: (reason?: unknown) => void
|
||||
}
|
||||
|
||||
function createDeferred<T>(): Deferred<T> {
|
||||
let resolve!: (value: T) => void
|
||||
let reject!: (reason?: unknown) => void
|
||||
|
||||
const promise = new Promise<T>((innerResolve, innerReject) => {
|
||||
resolve = innerResolve
|
||||
reject = innerReject
|
||||
})
|
||||
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
|
||||
const mockAddNote = jest.fn()
|
||||
|
||||
@@ -20,9 +40,42 @@ jest.mock("@/src/notes/NotesContext", () => ({
|
||||
useNotes: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock("@react-navigation/native", () => {
|
||||
const actual = jest.requireActual("@react-navigation/native")
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useIsFocused: jest.fn(() => true),
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock("@/src/notes/native-image-picker", () => ({
|
||||
pickImageFromCamera: jest.fn(),
|
||||
pickImageFromLibrary: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock("@/components/note-image-panel", () => ({
|
||||
__esModule: true,
|
||||
default: () => null,
|
||||
default: (props: {
|
||||
onChooseFromLibrary?: () => void
|
||||
onTakePhoto?: () => void
|
||||
stagedImage?: { fileName?: string | null } | null
|
||||
}) => {
|
||||
const React = require("react")
|
||||
const { Pressable, Text, View } = require("react-native")
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Pressable onPress={props.onTakePhoto}>
|
||||
<Text>Take photo</Text>
|
||||
</Pressable>
|
||||
<Pressable onPress={props.onChooseFromLibrary}>
|
||||
<Text>Choose from gallery</Text>
|
||||
</Pressable>
|
||||
{props.stagedImage?.fileName ? <Text>{props.stagedImage.fileName}</Text> : null}
|
||||
</View>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
describe("NewNoteScreen", () => {
|
||||
@@ -32,9 +85,15 @@ describe("NewNoteScreen", () => {
|
||||
back: jest.Mock
|
||||
replace: jest.Mock
|
||||
}
|
||||
const mockPickImageFromCamera = pickImageFromCamera as jest.MockedFunction<typeof pickImageFromCamera>
|
||||
const mockPickImageFromLibrary = pickImageFromLibrary as jest.MockedFunction<typeof pickImageFromLibrary>
|
||||
const mockUseIsFocused = useIsFocused as jest.MockedFunction<typeof useIsFocused>
|
||||
|
||||
beforeEach(() => {
|
||||
mockAddNote.mockResolvedValue(true)
|
||||
mockPickImageFromCamera.mockResolvedValue(null)
|
||||
mockPickImageFromLibrary.mockResolvedValue(null)
|
||||
mockUseIsFocused.mockReturnValue(true)
|
||||
|
||||
mockUseNotes.mockReturnValue({
|
||||
notes: [],
|
||||
@@ -74,4 +133,103 @@ describe("NewNoteScreen", () => {
|
||||
|
||||
expect(mockRouter.replace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("only launches the camera once while a picker request is active", async () => {
|
||||
const deferredCamera = createDeferred<Awaited<ReturnType<typeof pickImageFromCamera>>>()
|
||||
mockPickImageFromCamera.mockReturnValue(deferredCamera.promise)
|
||||
|
||||
renderWithTheme(<NewNoteScreen />)
|
||||
|
||||
fireEvent.press(screen.getByText("Take photo"))
|
||||
fireEvent.press(screen.getByText("Take photo"))
|
||||
|
||||
expect(mockPickImageFromCamera).toHaveBeenCalledTimes(1)
|
||||
|
||||
await act(async () => {
|
||||
deferredCamera.resolve({
|
||||
fileName: "captured-note.jpg",
|
||||
fileSize: 1024,
|
||||
height: 200,
|
||||
mimeType: "image/jpeg",
|
||||
uri: "file:///captured-note.jpg",
|
||||
width: 100,
|
||||
})
|
||||
|
||||
await deferredCamera.promise
|
||||
})
|
||||
|
||||
expect(screen.getByText("captured-note.jpg")).toBeTruthy()
|
||||
})
|
||||
|
||||
it("applies the selected camera image while the screen is still active", async () => {
|
||||
mockPickImageFromCamera.mockResolvedValue({
|
||||
fileName: "camera-note.jpg",
|
||||
fileSize: 512,
|
||||
height: 200,
|
||||
mimeType: "image/jpeg",
|
||||
uri: "file:///camera-note.jpg",
|
||||
width: 100,
|
||||
})
|
||||
|
||||
renderWithTheme(<NewNoteScreen />)
|
||||
|
||||
fireEvent.press(screen.getByText("Take photo"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("camera-note.jpg")).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
it("keeps the staged image unchanged when the gallery picker is canceled", async () => {
|
||||
mockPickImageFromLibrary.mockResolvedValue(null)
|
||||
|
||||
renderWithTheme(<NewNoteScreen />)
|
||||
|
||||
fireEvent.press(screen.getByText("Choose from gallery"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPickImageFromLibrary).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
expect(screen.queryByText(/\.jpg$/)).toBeNull()
|
||||
})
|
||||
|
||||
it("shows the camera error when the native picker fails", async () => {
|
||||
mockPickImageFromCamera.mockRejectedValue(new Error("Camera access is required to take a photo."))
|
||||
|
||||
renderWithTheme(<NewNoteScreen />)
|
||||
|
||||
fireEvent.press(screen.getByText("Take photo"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Camera access is required to take a photo.")).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
it("ignores a late camera result after the screen unmounts", async () => {
|
||||
const deferredCamera = createDeferred<Awaited<ReturnType<typeof pickImageFromCamera>>>()
|
||||
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
|
||||
mockPickImageFromCamera.mockReturnValue(deferredCamera.promise)
|
||||
|
||||
const screenRender = renderWithTheme(<NewNoteScreen />)
|
||||
|
||||
fireEvent.press(screen.getByText("Take photo"))
|
||||
screenRender.unmount()
|
||||
|
||||
await act(async () => {
|
||||
deferredCamera.resolve({
|
||||
fileName: "late-camera.jpg",
|
||||
fileSize: 1024,
|
||||
height: 200,
|
||||
mimeType: "image/jpeg",
|
||||
uri: "file:///late-camera.jpg",
|
||||
width: 100,
|
||||
})
|
||||
|
||||
await deferredCamera.promise
|
||||
})
|
||||
|
||||
expect(consoleErrorSpy).not.toHaveBeenCalled()
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user