diff --git a/package.json b/package.json index 66181155..4eaed52c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "scripts": { "test": "vitest --dom --coverage", "serve:coverage": "npx serve coverage", - "emulator": "firebase emulators:start --project test-project --only firestore" + "emulators:start": "firebase emulators:start --project test-project" }, "devDependencies": { "@tanstack/react-query": "^5.55.4", diff --git a/packages/react/src/auth/useSignInAnonymouslyMutation.test.tsx b/packages/react/src/auth/useSignInAnonymouslyMutation.test.tsx index f059d85e..f52b97f2 100644 --- a/packages/react/src/auth/useSignInAnonymouslyMutation.test.tsx +++ b/packages/react/src/auth/useSignInAnonymouslyMutation.test.tsx @@ -1,15 +1,22 @@ import React from "react"; -import { describe, expect, test, beforeEach, vi } from "vitest"; +import { + describe, + expect, + test, + beforeEach, + afterEach, + vi, + type MockInstance, +} from "vitest"; import { renderHook, act, waitFor } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { type Auth, type UserCredential } from "firebase/auth"; import { useSignInAnonymouslyMutation } from "./useSignInAnonymouslyMutation"; +import { auth, wipeAuth } from "~/testing-utils"; const queryClient = new QueryClient({ defaultOptions: { - queries: { - retry: false, - }, + queries: { retry: false }, + mutations: { retry: false }, }, }); @@ -17,10 +24,14 @@ const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); -describe("useSignInAnonymously", () => { - let auth: Auth; +describe("useSignInAnonymouslyMutation", () => { + beforeEach(async () => { + queryClient.clear(); + await wipeAuth(); + }); - beforeEach(() => { + afterEach(async () => { + await auth.signOut(); }); test("successfully signs in anonymously", async () => { @@ -28,7 +39,7 @@ describe("useSignInAnonymously", () => { wrapper, }); - await act(async () => { + act(() => { result.current.mutate(); }); @@ -39,42 +50,65 @@ describe("useSignInAnonymously", () => { expect(result.current.data?.user.isAnonymous).toBe(true); }); - test("handles auth error", async () => { + test("resets mutation state correctly", async () => { const { result } = renderHook(() => useSignInAnonymouslyMutation(auth), { wrapper, }); - await act(async () => { - result.current.mutate(); + act(() => { + result.current.mutateAsync(); }); await waitFor(() => { - expect(result.current.isError).toBe(true); + expect(result.current.data?.user.isAnonymous).toBe(true); + expect(result.current.isSuccess).toBe(true); }); - // expect(result.current.error).toEqual(mockError); + act(() => { + result.current.reset(); + }); + + await waitFor(() => { + expect(result.current.isIdle).toBe(true); + expect(result.current.data).toBeUndefined(); + expect(result.current.error).toBeNull(); + }); }); - test("returns pending state initially", async () => { + test("allows multiple sequential sign-ins", async () => { const { result } = renderHook(() => useSignInAnonymouslyMutation(auth), { wrapper, }); - // Initially, it should be idle - expect(result.current.isIdle).toBe(true); - + // First sign-in act(() => { result.current.mutate(); }); - // After mutate is called, it should be loading await waitFor(() => { - expect(result.current.isPending).toBe(true); + expect(result.current.isSuccess).toBe(true); + expect(result.current.data?.user.isAnonymous).toBe(true); + }); + + // Reset state + act(() => { + result.current.reset(); + }); + + await waitFor(() => { + expect(result.current.isIdle).toBe(true); + expect(result.current.data).toBeUndefined(); + expect(result.current.error).toBeNull(); + }); + + // Second sign-in + act(() => { + result.current.mutate(); }); - // Once the request is resolved, it should be successful await waitFor(() => { expect(result.current.isSuccess).toBe(true); + expect(result.current.data?.user.isAnonymous).toBe(true); }); }); }); diff --git a/vitest/utils.ts b/vitest/utils.ts index f10031bc..d42d3996 100644 --- a/vitest/utils.ts +++ b/vitest/utils.ts @@ -1,4 +1,5 @@ import { type FirebaseApp, FirebaseError, initializeApp } from "firebase/app"; +import { getAuth, connectAuthEmulator, type Auth } from "firebase/auth"; import { getFirestore, connectFirestoreEmulator, @@ -8,16 +9,20 @@ import { expect } from "vitest"; const firebaseTestingOptions = { projectId: "test-project", + apiKey: "test-api-key", }; let app: FirebaseApp | undefined; let firestore: Firestore; +let auth: Auth; if (!app) { app = initializeApp(firebaseTestingOptions); firestore = getFirestore(app); + auth = getAuth(app); connectFirestoreEmulator(firestore, "localhost", 8080); + connectAuthEmulator(auth, "http://localhost:9099"); } async function wipeFirestore() { @@ -33,6 +38,19 @@ async function wipeFirestore() { } } +async function wipeAuth() { + const response = await fetch( + "http://localhost:9099/emulator/v1/projects/test-project/accounts", + { + method: "DELETE", + } + ); + + if (!response.ok) { + throw new Error("Failed to wipe auth"); + } +} + function expectFirestoreError(error: unknown, expectedCode: string) { if (error instanceof FirebaseError) { expect(error).toBeDefined(); @@ -45,4 +63,11 @@ function expectFirestoreError(error: unknown, expectedCode: string) { } } -export { firestore, wipeFirestore, expectFirestoreError }; +export { + firestore, + wipeFirestore, + expectFirestoreError, + firebaseTestingOptions, + auth, + wipeAuth, +};