Finished testing implementation

This commit is contained in:
Christopher Sanden
2026-03-18 16:38:55 +01:00
parent c73363efb9
commit 7aad9dc34d
15 changed files with 3177 additions and 33 deletions

View File

@@ -0,0 +1,54 @@
import { renderRouter, screen, waitFor } from "expo-router/testing-library"
import React from "react"
jest.mock("@/providers/auth-provider", () => {
const React = require("react")
const { AuthContext } = require("@/hooks/use-auth-context")
return {
__esModule: true,
default: ({ children }: React.PropsWithChildren) =>
React.createElement(
AuthContext.Provider,
{
value: {
claims: null,
profile: null,
isLoading: false,
isLoggedIn: false,
},
},
children
),
}
})
jest.mock("@/libs/supabase", () => ({
supabase: {
auth: {
signInWithPassword: jest.fn(),
signUp: jest.fn(),
signOut: jest.fn(),
getUser: jest.fn(),
},
from: jest.fn(),
},
supabaseUrl: "https://example.supabase.co",
supabaseAnonKey: "test-anon-key",
}))
describe("Auth guard", () => {
it("redirects logged-out users to the login screen instead of protected content", async () => {
const routerScreen = renderRouter("./app", {
initialUrl: "/",
})
await waitFor(() => {
expect(routerScreen.getPathname()).toBe("/login")
})
expect(screen.getByText("Login")).toBeTruthy()
expect(screen.getByText("Log in")).toBeTruthy()
expect(screen.queryByText("FastNotes")).toBeNull()
})
})

View File

@@ -0,0 +1,178 @@
import { act, render, screen, waitFor } from "@testing-library/react-native"
import React, { PropsWithChildren } from "react"
import DetailScreen from "@/app/detail"
import { AuthContext, AuthData } from "@/hooks/use-auth-context"
import { supabase } from "@/libs/supabase"
import { NotesProvider } from "@/src/notes/NotesContext"
import { AppThemeProvider } from "@/src/theme/AppThemeProvider"
import { useLocalSearchParams } from "expo-router"
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 }
}
jest.mock("expo-router", () => ({
router: {
replace: jest.fn(),
back: jest.fn(),
canGoBack: jest.fn(),
},
useLocalSearchParams: jest.fn(),
}))
jest.mock("@/libs/supabase", () => ({
supabase: {
from: jest.fn(),
},
supabaseUrl: "https://example.supabase.co",
supabaseAnonKey: "test-anon-key",
}))
jest.mock("@/components/note-image-panel", () => ({
__esModule: true,
default: () => null,
}))
describe("DetailScreen", () => {
const mockUseLocalSearchParams = useLocalSearchParams as jest.MockedFunction<typeof useLocalSearchParams>
const mockSupabase = supabase as unknown as {
from: jest.Mock
}
function TestWrapper({ children }: PropsWithChildren) {
const authValue: AuthData = {
claims: {
sub: "user-1",
email: "user-1@example.com",
},
profile: {
id: "user-1",
email: "user-1@example.com",
username: null,
full_name: null,
},
isLoading: false,
isLoggedIn: true,
}
return (
<AppThemeProvider>
<AuthContext.Provider value={authValue}>
<NotesProvider>{children}</NotesProvider>
</AuthContext.Provider>
</AppThemeProvider>
)
}
beforeEach(() => {
mockUseLocalSearchParams.mockReturnValue({ id: "42" })
})
it("shows a loader while fetching a note, then renders the loaded content", async () => {
const deferredNote = createDeferred<{
data: {
id: number
created_by: string
title: string
content: string
created_at: string
updated_at: string
image_url: null
image_path: null
image_mime_type: null
image_size_bytes: null
} | null
error: null
}>()
const notesQuery = {
order: jest.fn(),
eq: jest.fn(),
maybeSingle: jest.fn(() => deferredNote.promise),
}
notesQuery.order
.mockImplementationOnce(() => notesQuery)
.mockImplementationOnce(() => Promise.resolve({ data: [], error: null }))
notesQuery.eq.mockReturnValue(notesQuery)
mockSupabase.from.mockImplementation((table: string) => {
if (table === "Notes") {
return {
select: jest.fn(() => notesQuery),
}
}
if (table === "profiles") {
return {
select: jest.fn(() => ({
in: jest.fn(() =>
Promise.resolve({
data: [
{
id: "user-1",
email: "user-1@example.com",
username: null,
full_name: "Exam User",
},
],
error: null,
})
),
})),
}
}
throw new Error(`Unexpected table requested in test: ${table}`)
})
render(<DetailScreen />, {
wrapper: TestWrapper,
})
await waitFor(() => {
expect(screen.getByTestId("note-detail-loader")).toBeTruthy()
expect(screen.getByText("Loading note...")).toBeTruthy()
})
await act(async () => {
deferredNote.resolve({
data: {
id: 42,
created_by: "user-1",
title: "Fetched note",
content: "Loaded from Supabase for the integration test",
created_at: "2026-03-18T10:00:00.000Z",
updated_at: "2026-03-18T10:05:00.000Z",
image_url: null,
image_path: null,
image_mime_type: null,
image_size_bytes: null,
},
error: null,
})
await deferredNote.promise
})
await waitFor(() => {
expect(screen.queryByTestId("note-detail-loader")).toBeNull()
expect(screen.getByDisplayValue("Fetched note")).toBeTruthy()
expect(screen.getByDisplayValue("Loaded from Supabase for the integration test")).toBeTruthy()
})
})
})

View File

@@ -0,0 +1,77 @@
import { fireEvent, screen, waitFor } from "@testing-library/react-native"
import React from "react"
import NewNoteScreen from "@/app/newNote"
import { useNotes } from "@/src/notes/NotesContext"
import { renderWithTheme } from "@/test-utils/renderWithTheme"
import { router } from "expo-router"
const mockAddNote = jest.fn()
jest.mock("expo-router", () => ({
router: {
canGoBack: jest.fn(),
back: jest.fn(),
replace: jest.fn(),
},
}))
jest.mock("@/src/notes/NotesContext", () => ({
useNotes: jest.fn(),
}))
jest.mock("@/components/note-image-panel", () => ({
__esModule: true,
default: () => null,
}))
describe("NewNoteScreen", () => {
const mockUseNotes = useNotes as jest.MockedFunction<typeof useNotes>
const mockRouter = router as unknown as {
canGoBack: jest.Mock
back: jest.Mock
replace: jest.Mock
}
beforeEach(() => {
mockAddNote.mockResolvedValue(true)
mockUseNotes.mockReturnValue({
notes: [],
isLoading: false,
refreshNotes: jest.fn(),
fetchNoteById: jest.fn(),
addNote: mockAddNote,
updateNote: jest.fn(),
deleteNote: jest.fn(),
errorMessage: null,
})
mockRouter.canGoBack.mockReturnValue(true)
})
it("submits a valid note and navigates back to the main screen", async () => {
renderWithTheme(<NewNoteScreen />)
fireEvent.changeText(screen.getByPlaceholderText("Give it a title..."), "Exam note")
fireEvent.changeText(screen.getByPlaceholderText("Write your note..."), "Testing the final assignment flow")
fireEvent.press(screen.getByText("Save note"))
await waitFor(() => {
expect(mockAddNote).toHaveBeenCalledWith(
"Exam note",
"Testing the final assignment flow",
null,
expect.objectContaining({
onImageUploadProgress: expect.any(Function),
})
)
})
await waitFor(() => {
expect(mockRouter.back).toHaveBeenCalledTimes(1)
})
expect(mockRouter.replace).not.toHaveBeenCalled()
})
})