From fd0a4668ece6fbc4c60a391041ccb0b599fa61b1 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Wed, 25 Dec 2024 06:58:38 +0000 Subject: [PATCH] chore: Bump version + liniting --- biome.json | 2 +- package.json | 3 +- packages/react/package.json | 10 +- .../src/auth/useDeleteUserMutation.test.tsx | 172 +- .../react/src/auth/useDeleteUserMutation.ts | 26 +- .../react/src/auth/useReloadMutation.test.tsx | 88 +- packages/react/src/auth/useReloadMutation.ts | 16 +- .../useSendSignInLinkToEmailMutation.test.tsx | 210 +- .../auth/useSendSignInLinkToEmailMutation.ts | 32 +- .../useSignInAnonymouslyMutation.test.tsx | 198 +- .../src/auth/useSignInAnonymouslyMutation.ts | 24 +- .../useSignInWithCredentialMutation.test.tsx | 276 +-- .../auth/useSignInWithCredentialMutation.ts | 28 +- ...ignInWithEmailAndPasswordMutation.test.tsx | 244 +- .../useSignInWithEmailAndPasswordMutation.ts | 44 +- .../src/auth/useSignOutMutation.test.tsx | 172 +- packages/react/src/auth/useSignOutMutation.ts | 18 +- .../useUpdateCurrentUserMutation.test.tsx | 330 +-- .../src/auth/useUpdateCurrentUserMutation.ts | 26 +- ...seVerifyPasswordResetCodeMutation.test.tsx | 118 +- .../useVerifyPasswordResetCodeMutation.ts | 24 +- packages/react/src/auth/utils.ts | 84 +- .../react/src/data-connect/query-client.ts | 114 +- packages/react/src/data-connect/types.ts | 12 +- .../useDataConnectMutation.test.tsx | 1991 +++++++++-------- .../data-connect/useDataConnectMutation.ts | 126 +- .../data-connect/useDataConnectQuery.test.tsx | 460 ++-- .../src/data-connect/useDataConnectQuery.ts | 74 +- ...ClearIndexedDbPersistenceMutation.test.tsx | 136 +- .../useClearIndexedDbPersistenceMutation.ts | 22 +- .../src/firestore/useCollectionQuery.test.tsx | 356 +-- .../react/src/firestore/useCollectionQuery.ts | 64 +- .../useDisableNetworkMutation.test.tsx | 72 +- .../firestore/useDisableNetworkMutation.ts | 22 +- .../src/firestore/useDocumentQuery.test.tsx | 268 +-- .../react/src/firestore/useDocumentQuery.ts | 68 +- .../useGetAggregateFromServerQuery.test.tsx | 272 +-- .../useGetAggregateFromServerQuery.ts | 48 +- .../useGetCountFromServerQuery.test.tsx | 166 +- .../firestore/useGetCountFromServerQuery.ts | 60 +- .../useRunTransactionMutation.test.tsx | 186 +- .../firestore/useRunTransactionMutation.ts | 34 +- .../useWaitForPendingWritesQuery.test.tsx | 52 +- .../firestore/useWaitForPendingWritesQuery.ts | 22 +- .../useWriteBatchCommitMutation.test.tsx | 210 +- .../firestore/useWriteBatchCommitMutation.ts | 14 +- 46 files changed, 3509 insertions(+), 3485 deletions(-) diff --git a/biome.json b/biome.json index 3f0c3b5..47536fc 100644 --- a/biome.json +++ b/biome.json @@ -11,7 +11,7 @@ }, "formatter": { "enabled": true, - "indentStyle": "tab" + "indentStyle": "space" }, "organizeImports": { "enabled": true diff --git a/package.json b/package.json index 6c39c83..1456275 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "test": "vitest --dom --coverage", "serve:coverage": "npx serve coverage", "emulator": "firebase emulators:start --project demo-projectc", - "emulator:kill": "lsof -t -i:4001 -i:8080 -i:9000 -i:9099 -i:9199 -i:8085 | xargs kill -9" + "emulator:kill": "lsof -t -i:4001 -i:8080 -i:9000 -i:9099 -i:9199 -i:8085 | xargs kill -9", + "check": "pnpm biome check --write ./packages/react/src" }, "devDependencies": { "@biomejs/biome": "1.9.4", diff --git a/packages/react/package.json b/packages/react/package.json index fd18cb5..acdbbb0 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack-query-firebase/react", - "version": "1.0.1", + "version": "1.0.2", "description": "TanStack Query bindings for Firebase and React", "type": "module", "scripts": { @@ -29,6 +29,10 @@ "email": "oss@invertase.io", "url": "https://github.com/invertase/tanstack-query-firebase" }, + "files": [ + "dist", + "README.md" + ], "license": "Apache-2.0", "devDependencies": { "@testing-library/react": "^16.0.1", @@ -37,7 +41,7 @@ "@dataconnect/default-connector": "workspace:*" }, "peerDependencies": { - "firebase": "^11.1.0", - "@tanstack/react-query": "^5.55.4" + "firebase": "^11", + "@tanstack/react-query": "^5" } } diff --git a/packages/react/src/auth/useDeleteUserMutation.test.tsx b/packages/react/src/auth/useDeleteUserMutation.test.tsx index 4bea836..4d16411 100644 --- a/packages/react/src/auth/useDeleteUserMutation.test.tsx +++ b/packages/react/src/auth/useDeleteUserMutation.test.tsx @@ -1,101 +1,101 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { act, renderHook, waitFor } from "@testing-library/react"; import { type User, createUserWithEmailAndPassword } from "firebase/auth"; -import React from "react"; +import type React from "react"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { auth, wipeAuth } from "~/testing-utils"; import { useDeleteUserMutation } from "./useDeleteUserMutation"; const queryClient = new QueryClient({ - defaultOptions: { - queries: { retry: false }, - mutations: { retry: false }, - }, + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, }); const wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} + {children} ); describe("useVerifyPasswordResetCodeMutation", () => { - const email = "tqf@invertase.io"; - const password = "TanstackQueryFirebase#123"; - let user: User; - - beforeEach(async () => { - queryClient.clear(); - await wipeAuth(); - const userCredential = await createUserWithEmailAndPassword( - auth, - email, - password, - ); - user = userCredential.user; - }); - - afterEach(async () => { - vi.clearAllMocks(); - await auth.signOut(); - }); - - test("successfully verifies the reset code", async () => { - const { result } = renderHook(() => useDeleteUserMutation(auth), { - wrapper, - }); - - await act(async () => { - result.current.mutate(user); - }); - - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - - expect(result.current.data).toBeUndefined(); - }); - - test("resets mutation state correctly", async () => { - const { result } = renderHook(() => useDeleteUserMutation(auth), { - wrapper, - }); - - act(() => { - result.current.mutate(user); - }); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - act(() => { - result.current.reset(); - }); - - await waitFor(() => { - expect(result.current.isIdle).toBe(true); - expect(result.current.data).toBeUndefined(); - expect(result.current.error).toBeNull(); - }); - }); - - test("should call onSuccess when the user is successfully deleted", async () => { - const onSuccess = vi.fn(); - - const { result } = renderHook( - () => - useDeleteUserMutation(auth, { - onSuccess, - }), - { - wrapper, - }, - ); - - act(() => { - result.current.mutate(user); - }); - - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - - expect(onSuccess).toHaveBeenCalledTimes(1); - expect(result.current.data).toBeUndefined(); - }); + const email = "tqf@invertase.io"; + const password = "TanstackQueryFirebase#123"; + let user: User; + + beforeEach(async () => { + queryClient.clear(); + await wipeAuth(); + const userCredential = await createUserWithEmailAndPassword( + auth, + email, + password, + ); + user = userCredential.user; + }); + + afterEach(async () => { + vi.clearAllMocks(); + await auth.signOut(); + }); + + test("successfully verifies the reset code", async () => { + const { result } = renderHook(() => useDeleteUserMutation(auth), { + wrapper, + }); + + await act(async () => { + result.current.mutate(user); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data).toBeUndefined(); + }); + + test("resets mutation state correctly", async () => { + const { result } = renderHook(() => useDeleteUserMutation(auth), { + wrapper, + }); + + act(() => { + result.current.mutate(user); + }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + act(() => { + result.current.reset(); + }); + + await waitFor(() => { + expect(result.current.isIdle).toBe(true); + expect(result.current.data).toBeUndefined(); + expect(result.current.error).toBeNull(); + }); + }); + + test("should call onSuccess when the user is successfully deleted", async () => { + const onSuccess = vi.fn(); + + const { result } = renderHook( + () => + useDeleteUserMutation(auth, { + onSuccess, + }), + { + wrapper, + }, + ); + + act(() => { + result.current.mutate(user); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(onSuccess).toHaveBeenCalledTimes(1); + expect(result.current.data).toBeUndefined(); + }); }); diff --git a/packages/react/src/auth/useDeleteUserMutation.ts b/packages/react/src/auth/useDeleteUserMutation.ts index 29e1b98..56ee3d4 100644 --- a/packages/react/src/auth/useDeleteUserMutation.ts +++ b/packages/react/src/auth/useDeleteUserMutation.ts @@ -1,23 +1,23 @@ import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; import { - type Auth, - type AuthError, - type User, - deleteUser, + type Auth, + type AuthError, + type User, + deleteUser, } from "firebase/auth"; type AuthUMutationOptions< - TData = unknown, - TError = Error, - TVariables = void, + TData = unknown, + TError = Error, + TVariables = void, > = Omit, "mutationFn">; export function useDeleteUserMutation( - auth: Auth, - options?: AuthUMutationOptions, + auth: Auth, + options?: AuthUMutationOptions, ) { - return useMutation({ - ...options, - mutationFn: (user: User) => deleteUser(user), - }); + return useMutation({ + ...options, + mutationFn: (user: User) => deleteUser(user), + }); } diff --git a/packages/react/src/auth/useReloadMutation.test.tsx b/packages/react/src/auth/useReloadMutation.test.tsx index f0dd543..cb81c42 100644 --- a/packages/react/src/auth/useReloadMutation.test.tsx +++ b/packages/react/src/auth/useReloadMutation.test.tsx @@ -1,70 +1,70 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { act, renderHook, waitFor } from "@testing-library/react"; import { - type User, - createUserWithEmailAndPassword, - signInWithEmailAndPassword, + type User, + createUserWithEmailAndPassword, + signInWithEmailAndPassword, } from "firebase/auth"; -import React from "react"; +import type React from "react"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { auth, wipeAuth } from "~/testing-utils"; import { useReloadMutation } from "./useReloadMutation"; const queryClient = new QueryClient({ - defaultOptions: { - queries: { retry: false }, - mutations: { retry: false }, - }, + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, }); const wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} + {children} ); describe("useReloadMutation", () => { - const email = "tqf@invertase.io"; - const password = "TanstackQueryFirebase#123"; - let user: User; - beforeEach(async () => { - queryClient.clear(); - await wipeAuth(); - const userCredential = await createUserWithEmailAndPassword( - auth, - email, - password, - ); - user = userCredential.user; - }); + const email = "tqf@invertase.io"; + const password = "TanstackQueryFirebase#123"; + let user: User; + beforeEach(async () => { + queryClient.clear(); + await wipeAuth(); + const userCredential = await createUserWithEmailAndPassword( + auth, + email, + password, + ); + user = userCredential.user; + }); - afterEach(async () => { - vi.clearAllMocks(); - await auth.signOut(); - }); + afterEach(async () => { + vi.clearAllMocks(); + await auth.signOut(); + }); - test.sequential("should successfully reloads user data", async () => { - await signInWithEmailAndPassword(auth, email, password); + test.sequential("should successfully reloads user data", async () => { + await signInWithEmailAndPassword(auth, email, password); - const { result } = renderHook(() => useReloadMutation(), { wrapper }); + const { result } = renderHook(() => useReloadMutation(), { wrapper }); - act(() => result.current.mutate(user)); + act(() => result.current.mutate(user)); - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - }); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + }); - test("should handle onSuccess callback", async () => { - await signInWithEmailAndPassword(auth, email, password); + test("should handle onSuccess callback", async () => { + await signInWithEmailAndPassword(auth, email, password); - const onSuccess = vi.fn(); - const { result } = renderHook(() => useReloadMutation({ onSuccess }), { - wrapper, - }); + const onSuccess = vi.fn(); + const { result } = renderHook(() => useReloadMutation({ onSuccess }), { + wrapper, + }); - act(() => { - result.current.mutate(user); - }); + act(() => { + result.current.mutate(user); + }); - await waitFor(() => expect(result.current.isSuccess).toBe(true)); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); - expect(onSuccess).toHaveBeenCalled(); - }); + expect(onSuccess).toHaveBeenCalled(); + }); }); diff --git a/packages/react/src/auth/useReloadMutation.ts b/packages/react/src/auth/useReloadMutation.ts index 2ff858c..0f3affa 100644 --- a/packages/react/src/auth/useReloadMutation.ts +++ b/packages/react/src/auth/useReloadMutation.ts @@ -2,16 +2,16 @@ import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; import { type AuthError, type User, reload } from "firebase/auth"; type AuthMutationOptions< - TData = unknown, - TError = Error, - TVariables = void, + TData = unknown, + TError = Error, + TVariables = void, > = Omit, "mutationFn">; export function useReloadMutation( - options?: AuthMutationOptions, + options?: AuthMutationOptions, ) { - return useMutation({ - ...options, - mutationFn: (user: User) => reload(user), - }); + return useMutation({ + ...options, + mutationFn: (user: User) => reload(user), + }); } diff --git a/packages/react/src/auth/useSendSignInLinkToEmailMutation.test.tsx b/packages/react/src/auth/useSendSignInLinkToEmailMutation.test.tsx index fb2f7df..2c442ee 100644 --- a/packages/react/src/auth/useSendSignInLinkToEmailMutation.test.tsx +++ b/packages/react/src/auth/useSendSignInLinkToEmailMutation.test.tsx @@ -1,119 +1,119 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { act, renderHook, waitFor } from "@testing-library/react"; -import React from "react"; +import type React from "react"; import { beforeEach, describe, expect, test } from "vitest"; import { auth, wipeAuth } from "~/testing-utils"; import { useSendSignInLinkToEmailMutation } from "./useSendSignInLinkToEmailMutation"; const queryClient = new QueryClient({ - defaultOptions: { - queries: { retry: false }, - mutations: { retry: false }, - }, + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, }); const wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} + {children} ); describe("useSendSignInLinkToEmailMutation", () => { - const email = "tanstack-query-firebase@invertase.io"; - const actionCodeSettings = { - url: `https://invertase.io/?email=${email}`, - iOS: { - bundleId: "com.example.ios", - }, - android: { - packageName: "com.example.android", - installApp: true, - minimumVersion: "12", - }, - handleCodeInApp: true, - }; - - beforeEach(async () => { - queryClient.clear(); - await wipeAuth(); - }); - - test("resets mutation state correctly", async () => { - const { result } = renderHook( - () => useSendSignInLinkToEmailMutation(auth), - { wrapper }, - ); - - act(() => { - result.current.mutate({ email, actionCodeSettings }); - }); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - act(() => { - result.current.reset(); - }); - - await waitFor(() => { - expect(result.current.isIdle).toBe(true); - expect(result.current.data).toBeUndefined(); - expect(result.current.error).toBeNull(); - }); - }); - - test("successfully sends sign-in link to email", async () => { - const { result } = renderHook( - () => useSendSignInLinkToEmailMutation(auth), - { wrapper }, - ); - - act(() => { - result.current.mutate({ email, actionCodeSettings }); - }); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - expect(result.current.isSuccess).toBe(true); - expect(result.current.error).toBeNull(); - }); - - test("allows multiple sequential send attempts", async () => { - const { result } = renderHook( - () => useSendSignInLinkToEmailMutation(auth), - { wrapper }, - ); - - // First attempt - act(() => { - result.current.mutate({ email, actionCodeSettings }); - }); - - await waitFor(() => { - expect(result.current.isSuccess).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 attempt - act(() => { - result.current.mutate({ email, actionCodeSettings }); - }); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - expect(result.current.error).toBeNull(); - }); + const email = "tanstack-query-firebase@invertase.io"; + const actionCodeSettings = { + url: `https://invertase.io/?email=${email}`, + iOS: { + bundleId: "com.example.ios", + }, + android: { + packageName: "com.example.android", + installApp: true, + minimumVersion: "12", + }, + handleCodeInApp: true, + }; + + beforeEach(async () => { + queryClient.clear(); + await wipeAuth(); + }); + + test("resets mutation state correctly", async () => { + const { result } = renderHook( + () => useSendSignInLinkToEmailMutation(auth), + { wrapper }, + ); + + act(() => { + result.current.mutate({ email, actionCodeSettings }); + }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + act(() => { + result.current.reset(); + }); + + await waitFor(() => { + expect(result.current.isIdle).toBe(true); + expect(result.current.data).toBeUndefined(); + expect(result.current.error).toBeNull(); + }); + }); + + test("successfully sends sign-in link to email", async () => { + const { result } = renderHook( + () => useSendSignInLinkToEmailMutation(auth), + { wrapper }, + ); + + act(() => { + result.current.mutate({ email, actionCodeSettings }); + }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + expect(result.current.isSuccess).toBe(true); + expect(result.current.error).toBeNull(); + }); + + test("allows multiple sequential send attempts", async () => { + const { result } = renderHook( + () => useSendSignInLinkToEmailMutation(auth), + { wrapper }, + ); + + // First attempt + act(() => { + result.current.mutate({ email, actionCodeSettings }); + }); + + await waitFor(() => { + expect(result.current.isSuccess).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 attempt + act(() => { + result.current.mutate({ email, actionCodeSettings }); + }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + expect(result.current.error).toBeNull(); + }); }); diff --git a/packages/react/src/auth/useSendSignInLinkToEmailMutation.ts b/packages/react/src/auth/useSendSignInLinkToEmailMutation.ts index 3f17742..1177170 100644 --- a/packages/react/src/auth/useSendSignInLinkToEmailMutation.ts +++ b/packages/react/src/auth/useSendSignInLinkToEmailMutation.ts @@ -1,29 +1,29 @@ import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; import { - type ActionCodeSettings, - type Auth, - type AuthError, - sendSignInLinkToEmail, + type ActionCodeSettings, + type Auth, + type AuthError, + sendSignInLinkToEmail, } from "firebase/auth"; type SendSignInLinkParams = { - email: string; - actionCodeSettings: ActionCodeSettings; + email: string; + actionCodeSettings: ActionCodeSettings; }; type AuthUseMutationOptions< - TData = unknown, - TError = Error, - TVariables = void, + TData = unknown, + TError = Error, + TVariables = void, > = Omit, "mutationFn">; export function useSendSignInLinkToEmailMutation( - auth: Auth, - options?: AuthUseMutationOptions, + auth: Auth, + options?: AuthUseMutationOptions, ) { - return useMutation({ - ...options, - mutationFn: ({ email, actionCodeSettings }) => - sendSignInLinkToEmail(auth, email, actionCodeSettings), - }); + return useMutation({ + ...options, + mutationFn: ({ email, actionCodeSettings }) => + sendSignInLinkToEmail(auth, email, actionCodeSettings), + }); } diff --git a/packages/react/src/auth/useSignInAnonymouslyMutation.test.tsx b/packages/react/src/auth/useSignInAnonymouslyMutation.test.tsx index d55f298..99ea8d8 100644 --- a/packages/react/src/auth/useSignInAnonymouslyMutation.test.tsx +++ b/packages/react/src/auth/useSignInAnonymouslyMutation.test.tsx @@ -1,114 +1,114 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { act, renderHook, waitFor } from "@testing-library/react"; -import React from "react"; +import type React from "react"; import { - type MockInstance, - afterEach, - beforeEach, - describe, - expect, - test, - vi, + type MockInstance, + afterEach, + beforeEach, + describe, + expect, + test, + vi, } from "vitest"; import { auth, wipeAuth } from "~/testing-utils"; import { useSignInAnonymouslyMutation } from "./useSignInAnonymouslyMutation"; const queryClient = new QueryClient({ - defaultOptions: { - queries: { retry: false }, - mutations: { retry: false }, - }, + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, }); const wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} + {children} ); describe("useSignInAnonymouslyMutation", () => { - beforeEach(async () => { - queryClient.clear(); - await wipeAuth(); - }); - - afterEach(async () => { - await auth.signOut(); - }); - - test("successfully signs in anonymously", async () => { - const { result } = renderHook(() => useSignInAnonymouslyMutation(auth), { - wrapper, - }); - - act(() => { - result.current.mutate(); - }); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - expect(result.current.data?.user.isAnonymous).toBe(true); - }); - - test("resets mutation state correctly", async () => { - const { result } = renderHook(() => useSignInAnonymouslyMutation(auth), { - wrapper, - }); - - act(() => { - result.current.mutateAsync(); - }); - - await waitFor(() => { - expect(result.current.data?.user.isAnonymous).toBe(true); - expect(result.current.isSuccess).toBe(true); - }); - - act(() => { - result.current.reset(); - }); - - await waitFor(() => { - expect(result.current.isIdle).toBe(true); - expect(result.current.data).toBeUndefined(); - expect(result.current.error).toBeNull(); - }); - }); - - test("allows multiple sequential sign-ins", async () => { - const { result } = renderHook(() => useSignInAnonymouslyMutation(auth), { - wrapper, - }); - - // First sign-in - act(() => { - result.current.mutate(); - }); - - await waitFor(() => { - 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(); - }); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - expect(result.current.data?.user.isAnonymous).toBe(true); - }); - }); + beforeEach(async () => { + queryClient.clear(); + await wipeAuth(); + }); + + afterEach(async () => { + await auth.signOut(); + }); + + test("successfully signs in anonymously", async () => { + const { result } = renderHook(() => useSignInAnonymouslyMutation(auth), { + wrapper, + }); + + act(() => { + result.current.mutate(); + }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + expect(result.current.data?.user.isAnonymous).toBe(true); + }); + + test("resets mutation state correctly", async () => { + const { result } = renderHook(() => useSignInAnonymouslyMutation(auth), { + wrapper, + }); + + act(() => { + result.current.mutateAsync(); + }); + + await waitFor(() => { + expect(result.current.data?.user.isAnonymous).toBe(true); + expect(result.current.isSuccess).toBe(true); + }); + + act(() => { + result.current.reset(); + }); + + await waitFor(() => { + expect(result.current.isIdle).toBe(true); + expect(result.current.data).toBeUndefined(); + expect(result.current.error).toBeNull(); + }); + }); + + test("allows multiple sequential sign-ins", async () => { + const { result } = renderHook(() => useSignInAnonymouslyMutation(auth), { + wrapper, + }); + + // First sign-in + act(() => { + result.current.mutate(); + }); + + await waitFor(() => { + 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(); + }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + expect(result.current.data?.user.isAnonymous).toBe(true); + }); + }); }); diff --git a/packages/react/src/auth/useSignInAnonymouslyMutation.ts b/packages/react/src/auth/useSignInAnonymouslyMutation.ts index a1a1237..bf186e8 100644 --- a/packages/react/src/auth/useSignInAnonymouslyMutation.ts +++ b/packages/react/src/auth/useSignInAnonymouslyMutation.ts @@ -1,22 +1,22 @@ import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; import { - type Auth, - type AuthError, - type UserCredential, - signInAnonymously, + type Auth, + type AuthError, + type UserCredential, + signInAnonymously, } from "firebase/auth"; type SignInAnonymouslyOptions = Omit< - UseMutationOptions, - "mutationFn" + UseMutationOptions, + "mutationFn" >; export function useSignInAnonymouslyMutation( - auth: Auth, - options?: SignInAnonymouslyOptions, + auth: Auth, + options?: SignInAnonymouslyOptions, ) { - return useMutation({ - ...options, - mutationFn: () => signInAnonymously(auth), - }); + return useMutation({ + ...options, + mutationFn: () => signInAnonymously(auth), + }); } diff --git a/packages/react/src/auth/useSignInWithCredentialMutation.test.tsx b/packages/react/src/auth/useSignInWithCredentialMutation.test.tsx index 26c18f2..fe3cd49 100644 --- a/packages/react/src/auth/useSignInWithCredentialMutation.test.tsx +++ b/packages/react/src/auth/useSignInWithCredentialMutation.test.tsx @@ -2,159 +2,159 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { act, renderHook, waitFor } from "@testing-library/react"; import { GoogleAuthProvider } from "firebase/auth"; import jwt from "jsonwebtoken"; -import React from "react"; +import type React from "react"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { auth, expectFirebaseError, wipeAuth } from "~/testing-utils"; import { useSignInWithCredentialMutation } from "./useSignInWithCredentialMutation"; const secret = "something-secret"; const payload = { - email: "tanstack-query-firebase@invertase.io", - sub: "tanstack-query-firebase", + email: "tanstack-query-firebase@invertase.io", + sub: "tanstack-query-firebase", }; const mockIdToken = jwt.sign(payload, secret, { expiresIn: "1h" }); const queryClient = new QueryClient({ - defaultOptions: { - queries: { retry: false }, - mutations: { retry: false }, - }, + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, }); const wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} + {children} ); describe("useSignInWithCredentialMutation", () => { - beforeEach(async () => { - queryClient.clear(); - await wipeAuth(); - }); - - afterEach(async () => { - await auth.signOut(); - }); - - const credential = GoogleAuthProvider.credential(mockIdToken); - - test("successfully signs in with credentials", async () => { - const { result } = renderHook( - () => useSignInWithCredentialMutation(auth, credential), - { wrapper }, - ); - - act(() => result.current.mutate()); - - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - - expect(result.current.data).toHaveProperty("user"); - expect(result.current.data?.user).toHaveProperty("uid"); - expect(result.current.data?.user).toHaveProperty("email"); - expect(result.current.data?.user.email).toBe( - "tanstack-query-firebase@invertase.io", - ); - expect(result.current.data?.user.isAnonymous).toBe(false); - }); - - test("handles sign-in error with invalid credential", async () => { - const invalidCredential = GoogleAuthProvider.credential("invalid-token"); - - const { result } = renderHook( - () => useSignInWithCredentialMutation(auth, invalidCredential), - { wrapper }, - ); - - act(() => { - result.current.mutate(); - }); - - await waitFor(() => expect(result.current.isError).toBe(true)); - - expectFirebaseError(result.current.error, "auth/invalid-credential"); - }); - - test("resets mutation state correctly", async () => { - const { result } = renderHook( - () => useSignInWithCredentialMutation(auth, credential), - { wrapper }, - ); - - act(() => result.current.mutate()); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - expect(result.current.data).toBeDefined(); - }); - - act(() => { - result.current.reset(); - }); - - await waitFor(() => { - expect(result.current.isIdle).toBe(true); - expect(result.current.data).toBeUndefined(); - expect(result.current.error).toBeNull(); - }); - }); - - test("allows multiple sequential sign-ins", async () => { - const { result } = renderHook( - () => useSignInWithCredentialMutation(auth, credential), - { wrapper }, - ); - - // First sign-in - act(() => result.current.mutate()); - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - - // Reset state - act(() => result.current.reset()); - await waitFor(() => expect(result.current.isIdle).toBe(true)); - - // Second sign-in - act(() => result.current.mutate()); - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - }); - - test("handles concurrent sign-in attempts", async () => { - const { result } = renderHook( - () => useSignInWithCredentialMutation(auth, credential), - { wrapper }, - ); - - const promise1 = act(() => result.current.mutate()); - const promise2 = act(() => result.current.mutate()); - - await Promise.all([promise1, promise2]); - - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - - expect(result.current.data?.user.email).toBe( - "tanstack-query-firebase@invertase.io", - ); - }); - - test("respects custom mutation options", async () => { - const onSuccess = vi.fn(); - const onError = vi.fn(); - - const { result } = renderHook( - () => - useSignInWithCredentialMutation(auth, credential, { - onSuccess, - onError, - }), - { wrapper }, - ); - - act(() => { - result.current.mutate(); - }); - - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - - expect(onSuccess).toHaveBeenCalled(); - expect(onError).not.toHaveBeenCalled(); - }); + beforeEach(async () => { + queryClient.clear(); + await wipeAuth(); + }); + + afterEach(async () => { + await auth.signOut(); + }); + + const credential = GoogleAuthProvider.credential(mockIdToken); + + test("successfully signs in with credentials", async () => { + const { result } = renderHook( + () => useSignInWithCredentialMutation(auth, credential), + { wrapper }, + ); + + act(() => result.current.mutate()); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data).toHaveProperty("user"); + expect(result.current.data?.user).toHaveProperty("uid"); + expect(result.current.data?.user).toHaveProperty("email"); + expect(result.current.data?.user.email).toBe( + "tanstack-query-firebase@invertase.io", + ); + expect(result.current.data?.user.isAnonymous).toBe(false); + }); + + test("handles sign-in error with invalid credential", async () => { + const invalidCredential = GoogleAuthProvider.credential("invalid-token"); + + const { result } = renderHook( + () => useSignInWithCredentialMutation(auth, invalidCredential), + { wrapper }, + ); + + act(() => { + result.current.mutate(); + }); + + await waitFor(() => expect(result.current.isError).toBe(true)); + + expectFirebaseError(result.current.error, "auth/invalid-credential"); + }); + + test("resets mutation state correctly", async () => { + const { result } = renderHook( + () => useSignInWithCredentialMutation(auth, credential), + { wrapper }, + ); + + act(() => result.current.mutate()); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + expect(result.current.data).toBeDefined(); + }); + + act(() => { + result.current.reset(); + }); + + await waitFor(() => { + expect(result.current.isIdle).toBe(true); + expect(result.current.data).toBeUndefined(); + expect(result.current.error).toBeNull(); + }); + }); + + test("allows multiple sequential sign-ins", async () => { + const { result } = renderHook( + () => useSignInWithCredentialMutation(auth, credential), + { wrapper }, + ); + + // First sign-in + act(() => result.current.mutate()); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + // Reset state + act(() => result.current.reset()); + await waitFor(() => expect(result.current.isIdle).toBe(true)); + + // Second sign-in + act(() => result.current.mutate()); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + }); + + test("handles concurrent sign-in attempts", async () => { + const { result } = renderHook( + () => useSignInWithCredentialMutation(auth, credential), + { wrapper }, + ); + + const promise1 = act(() => result.current.mutate()); + const promise2 = act(() => result.current.mutate()); + + await Promise.all([promise1, promise2]); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data?.user.email).toBe( + "tanstack-query-firebase@invertase.io", + ); + }); + + test("respects custom mutation options", async () => { + const onSuccess = vi.fn(); + const onError = vi.fn(); + + const { result } = renderHook( + () => + useSignInWithCredentialMutation(auth, credential, { + onSuccess, + onError, + }), + { wrapper }, + ); + + act(() => { + result.current.mutate(); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(onSuccess).toHaveBeenCalled(); + expect(onError).not.toHaveBeenCalled(); + }); }); diff --git a/packages/react/src/auth/useSignInWithCredentialMutation.ts b/packages/react/src/auth/useSignInWithCredentialMutation.ts index 580b251..bb1253a 100644 --- a/packages/react/src/auth/useSignInWithCredentialMutation.ts +++ b/packages/react/src/auth/useSignInWithCredentialMutation.ts @@ -1,24 +1,24 @@ import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; import { - type Auth, - type AuthCredential, - type AuthError, - type UserCredential, - signInWithCredential, + type Auth, + type AuthCredential, + type AuthError, + type UserCredential, + signInWithCredential, } from "firebase/auth"; type AuthUseMutationOptions = Omit< - UseMutationOptions, - "mutationFn" + UseMutationOptions, + "mutationFn" >; export function useSignInWithCredentialMutation( - auth: Auth, - credential: AuthCredential, - options?: AuthUseMutationOptions, + auth: Auth, + credential: AuthCredential, + options?: AuthUseMutationOptions, ) { - return useMutation({ - ...options, - mutationFn: () => signInWithCredential(auth, credential), - }); + return useMutation({ + ...options, + mutationFn: () => signInWithCredential(auth, credential), + }); } diff --git a/packages/react/src/auth/useSignInWithEmailAndPasswordMutation.test.tsx b/packages/react/src/auth/useSignInWithEmailAndPasswordMutation.test.tsx index b0c845a..17ae089 100644 --- a/packages/react/src/auth/useSignInWithEmailAndPasswordMutation.test.tsx +++ b/packages/react/src/auth/useSignInWithEmailAndPasswordMutation.test.tsx @@ -1,177 +1,177 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { act, renderHook, waitFor } from "@testing-library/react"; import { createUserWithEmailAndPassword } from "firebase/auth"; -import React from "react"; +import type React from "react"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { auth, wipeAuth } from "~/testing-utils"; import { useSignInWithEmailAndPasswordMutation } from "./useSignInWithEmailAndPasswordMutation"; const queryClient = new QueryClient({ - defaultOptions: { - queries: { retry: false }, - mutations: { retry: false }, - }, + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, }); const wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} + {children} ); describe("useSignInWithEmailAndPasswordMutation", () => { - beforeEach(async () => { - queryClient.clear(); - await wipeAuth(); - }); + beforeEach(async () => { + queryClient.clear(); + await wipeAuth(); + }); - afterEach(async () => { - await auth.signOut(); - }); + afterEach(async () => { + await auth.signOut(); + }); - test("successfully signs in with email and password", async () => { - const email = "tqf@invertase.io"; - const password = "tanstackQueryFirebase#123"; - await createUserWithEmailAndPassword(auth, email, password); + test("successfully signs in with email and password", async () => { + const email = "tqf@invertase.io"; + const password = "tanstackQueryFirebase#123"; + await createUserWithEmailAndPassword(auth, email, password); - const { result } = renderHook( - () => useSignInWithEmailAndPasswordMutation(auth), - { wrapper }, - ); + const { result } = renderHook( + () => useSignInWithEmailAndPasswordMutation(auth), + { wrapper }, + ); - await act(async () => result.current.mutate({ email, password })); + await act(async () => result.current.mutate({ email, password })); - await waitFor(async () => expect(result.current.isSuccess).toBe(true)); + await waitFor(async () => expect(result.current.isSuccess).toBe(true)); - expect(result.current.data?.user.email).toBe(email); - }); + expect(result.current.data?.user.email).toBe(email); + }); - test("fails to sign in with incorrect password", async () => { - const email = "tqf@invertase.io"; - const password = "tanstackQueryFirebase#123"; - const wrongPassword = "wrongpassword"; + test("fails to sign in with incorrect password", async () => { + const email = "tqf@invertase.io"; + const password = "tanstackQueryFirebase#123"; + const wrongPassword = "wrongpassword"; - await createUserWithEmailAndPassword(auth, email, password); + await createUserWithEmailAndPassword(auth, email, password); - const { result } = renderHook( - () => useSignInWithEmailAndPasswordMutation(auth), - { wrapper }, - ); + const { result } = renderHook( + () => useSignInWithEmailAndPasswordMutation(auth), + { wrapper }, + ); - await act(async () => { - result.current.mutate({ email, password: wrongPassword }); - }); + await act(async () => { + result.current.mutate({ email, password: wrongPassword }); + }); - await waitFor(() => expect(result.current.isError).toBe(true)); + await waitFor(() => expect(result.current.isError).toBe(true)); - expect(result.current.error).toBeDefined(); - expect(result.current.isSuccess).toBe(false); - // TODO: Assert Firebase error for auth/wrong-password - }); + expect(result.current.error).toBeDefined(); + expect(result.current.isSuccess).toBe(false); + // TODO: Assert Firebase error for auth/wrong-password + }); - test("fails to sign in with non-existent email", async () => { - const email = "tqf@invertase.io"; - const password = "tanstackQueryFirebase#123"; + test("fails to sign in with non-existent email", async () => { + const email = "tqf@invertase.io"; + const password = "tanstackQueryFirebase#123"; - const { result } = renderHook( - () => useSignInWithEmailAndPasswordMutation(auth), - { wrapper }, - ); + const { result } = renderHook( + () => useSignInWithEmailAndPasswordMutation(auth), + { wrapper }, + ); - await act(async () => { - result.current.mutate({ email, password }); - }); + await act(async () => { + result.current.mutate({ email, password }); + }); - await waitFor(() => expect(result.current.isError).toBe(true)); + await waitFor(() => expect(result.current.isError).toBe(true)); - expect(result.current.error).toBeDefined(); - expect(result.current.isSuccess).toBe(false); - // TODO: Assert Firebase error for auth/user-not-found - }); + expect(result.current.error).toBeDefined(); + expect(result.current.isSuccess).toBe(false); + // TODO: Assert Firebase error for auth/user-not-found + }); - test("handles empty email input", async () => { - const email = ""; - const password = "validPassword123"; + test("handles empty email input", async () => { + const email = ""; + const password = "validPassword123"; - const { result } = renderHook( - () => useSignInWithEmailAndPasswordMutation(auth), - { wrapper }, - ); + const { result } = renderHook( + () => useSignInWithEmailAndPasswordMutation(auth), + { wrapper }, + ); - await act(async () => { - result.current.mutate({ email, password }); - }); + await act(async () => { + result.current.mutate({ email, password }); + }); - await waitFor(() => expect(result.current.isError).toBe(true)); + await waitFor(() => expect(result.current.isError).toBe(true)); - expect(result.current.error).toBeDefined(); - expect(result.current.isSuccess).toBe(false); - // TODO: Assert Firebase error for auth/invalid-email - }); + expect(result.current.error).toBeDefined(); + expect(result.current.isSuccess).toBe(false); + // TODO: Assert Firebase error for auth/invalid-email + }); - test("handles empty password input", async () => { - const email = "tqf@invertase.io"; - const password = ""; + test("handles empty password input", async () => { + const email = "tqf@invertase.io"; + const password = ""; - const { result } = renderHook( - () => useSignInWithEmailAndPasswordMutation(auth), - { wrapper }, - ); + const { result } = renderHook( + () => useSignInWithEmailAndPasswordMutation(auth), + { wrapper }, + ); - await act(async () => { - result.current.mutate({ email, password }); - }); + await act(async () => { + result.current.mutate({ email, password }); + }); - await waitFor(() => expect(result.current.isError).toBe(true)); + await waitFor(() => expect(result.current.isError).toBe(true)); - expect(result.current.error).toBeDefined(); - expect(result.current.isSuccess).toBe(false); - // TODO: Assert Firebase error for auth/missing-password - }); + expect(result.current.error).toBeDefined(); + expect(result.current.isSuccess).toBe(false); + // TODO: Assert Firebase error for auth/missing-password + }); - test("handles concurrent sign in attempts", async () => { - const email = "tqf@invertase.io"; - const password = "tanstackQueryFirebase#123"; + test("handles concurrent sign in attempts", async () => { + const email = "tqf@invertase.io"; + const password = "tanstackQueryFirebase#123"; - await createUserWithEmailAndPassword(auth, email, password); + await createUserWithEmailAndPassword(auth, email, password); - const { result } = renderHook( - () => useSignInWithEmailAndPasswordMutation(auth), - { wrapper }, - ); + const { result } = renderHook( + () => useSignInWithEmailAndPasswordMutation(auth), + { wrapper }, + ); - // Attempt multiple concurrent sign-ins - await act(async () => { - result.current.mutate({ email, password }); - result.current.mutate({ email, password }); - }); + // Attempt multiple concurrent sign-ins + await act(async () => { + result.current.mutate({ email, password }); + result.current.mutate({ email, password }); + }); - await waitFor(() => expect(result.current.isSuccess).toBe(true)); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); - expect(result.current.data?.user.email).toBe(email); - }); + expect(result.current.data?.user.email).toBe(email); + }); - test("handles sign in with custom mutation options", async () => { - const email = "tqf@invertase.io"; - const password = "tanstackQueryFirebase#123"; + test("handles sign in with custom mutation options", async () => { + const email = "tqf@invertase.io"; + const password = "tanstackQueryFirebase#123"; - const onSuccessMock = vi.fn(); + const onSuccessMock = vi.fn(); - await createUserWithEmailAndPassword(auth, email, password); + await createUserWithEmailAndPassword(auth, email, password); - const { result } = renderHook( - () => - useSignInWithEmailAndPasswordMutation(auth, { - onSuccess: onSuccessMock, - }), - { wrapper }, - ); + const { result } = renderHook( + () => + useSignInWithEmailAndPasswordMutation(auth, { + onSuccess: onSuccessMock, + }), + { wrapper }, + ); - await act(async () => { - result.current.mutate({ email, password }); - }); + await act(async () => { + result.current.mutate({ email, password }); + }); - await waitFor(() => expect(result.current.isSuccess).toBe(true)); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); - expect(onSuccessMock).toHaveBeenCalled(); - expect(result.current.data?.user.email).toBe(email); - }); + expect(onSuccessMock).toHaveBeenCalled(); + expect(result.current.data?.user.email).toBe(email); + }); }); diff --git a/packages/react/src/auth/useSignInWithEmailAndPasswordMutation.ts b/packages/react/src/auth/useSignInWithEmailAndPasswordMutation.ts index 1707be7..0a46b6a 100644 --- a/packages/react/src/auth/useSignInWithEmailAndPasswordMutation.ts +++ b/packages/react/src/auth/useSignInWithEmailAndPasswordMutation.ts @@ -1,32 +1,32 @@ import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; import { - type Auth, - type AuthError, - type UserCredential, - signInWithEmailAndPassword, + type Auth, + type AuthError, + type UserCredential, + signInWithEmailAndPassword, } from "firebase/auth"; type AuthUseMutationOptions< - TData = unknown, - TError = Error, - TVariables = void, + TData = unknown, + TError = Error, + TVariables = void, > = Omit, "mutationFn">; export function useSignInWithEmailAndPasswordMutation( - auth: Auth, - options?: AuthUseMutationOptions< - UserCredential, - AuthError, - { email: string; password: string } - >, + auth: Auth, + options?: AuthUseMutationOptions< + UserCredential, + AuthError, + { email: string; password: string } + >, ) { - return useMutation< - UserCredential, - AuthError, - { email: string; password: string } - >({ - ...options, - mutationFn: ({ email, password }) => - signInWithEmailAndPassword(auth, email, password), - }); + return useMutation< + UserCredential, + AuthError, + { email: string; password: string } + >({ + ...options, + mutationFn: ({ email, password }) => + signInWithEmailAndPassword(auth, email, password), + }); } diff --git a/packages/react/src/auth/useSignOutMutation.test.tsx b/packages/react/src/auth/useSignOutMutation.test.tsx index bbf220b..bc7063a 100644 --- a/packages/react/src/auth/useSignOutMutation.test.tsx +++ b/packages/react/src/auth/useSignOutMutation.test.tsx @@ -1,134 +1,134 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { act, renderHook, waitFor } from "@testing-library/react"; import { - createUserWithEmailAndPassword, - signInWithEmailAndPassword, + createUserWithEmailAndPassword, + signInWithEmailAndPassword, } from "firebase/auth"; -import React from "react"; +import type React from "react"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { auth, wipeAuth } from "~/testing-utils"; import { useSignOutMutation } from "./useSignOutMutation"; const queryClient = new QueryClient({ - defaultOptions: { - queries: { retry: false }, - mutations: { retry: false }, - }, + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, }); const wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} + {children} ); describe("useSignOutMutation", () => { - beforeEach(async () => { - queryClient.clear(); - await wipeAuth(); - }); + beforeEach(async () => { + queryClient.clear(); + await wipeAuth(); + }); - test("successfully signs out an authenticated user", async () => { - const email = "tqf@invertase.io"; - const password = "tanstackQueryFirebase#123"; + test("successfully signs out an authenticated user", async () => { + const email = "tqf@invertase.io"; + const password = "tanstackQueryFirebase#123"; - await createUserWithEmailAndPassword(auth, email, password); - await signInWithEmailAndPassword(auth, email, password); + await createUserWithEmailAndPassword(auth, email, password); + await signInWithEmailAndPassword(auth, email, password); - const { result } = renderHook(() => useSignOutMutation(auth), { wrapper }); + const { result } = renderHook(() => useSignOutMutation(auth), { wrapper }); - await act(async () => { - result.current.mutate(); - }); + await act(async () => { + result.current.mutate(); + }); - await waitFor(() => expect(result.current.isSuccess).toBe(true)); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); - expect(auth.currentUser).toBeNull(); - }); + expect(auth.currentUser).toBeNull(); + }); - test("handles sign out for a non-authenticated user", async () => { - const email = "tqf@invertase.io"; - const password = "tanstackQueryFirebase#123"; + test("handles sign out for a non-authenticated user", async () => { + const email = "tqf@invertase.io"; + const password = "tanstackQueryFirebase#123"; - await createUserWithEmailAndPassword(auth, email, password); - await signInWithEmailAndPassword(auth, email, password); + await createUserWithEmailAndPassword(auth, email, password); + await signInWithEmailAndPassword(auth, email, password); - await auth.signOut(); + await auth.signOut(); - const { result } = renderHook(() => useSignOutMutation(auth), { wrapper }); + const { result } = renderHook(() => useSignOutMutation(auth), { wrapper }); - await act(async () => { - result.current.mutate(); - }); + await act(async () => { + result.current.mutate(); + }); - await waitFor(() => expect(result.current.isSuccess).toBe(true)); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); - expect(auth.currentUser).toBeNull(); - }); + expect(auth.currentUser).toBeNull(); + }); - test("calls onSuccess callback after successful sign out", async () => { - const email = "tqf@invertase.io"; - const password = "tanstackQueryFirebase#123"; - const onSuccessMock = vi.fn(); + test("calls onSuccess callback after successful sign out", async () => { + const email = "tqf@invertase.io"; + const password = "tanstackQueryFirebase#123"; + const onSuccessMock = vi.fn(); - await createUserWithEmailAndPassword(auth, email, password); - await signInWithEmailAndPassword(auth, email, password); + await createUserWithEmailAndPassword(auth, email, password); + await signInWithEmailAndPassword(auth, email, password); - const { result } = renderHook( - () => useSignOutMutation(auth, { onSuccess: onSuccessMock }), - { wrapper }, - ); + const { result } = renderHook( + () => useSignOutMutation(auth, { onSuccess: onSuccessMock }), + { wrapper }, + ); - await act(async () => { - result.current.mutate(); - }); + await act(async () => { + result.current.mutate(); + }); - await waitFor(() => expect(result.current.isSuccess).toBe(true)); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); - expect(onSuccessMock).toHaveBeenCalled(); - }); + expect(onSuccessMock).toHaveBeenCalled(); + }); - test("calls onError callback on sign out failure", async () => { - const email = "tqf@invertase.io"; - const password = "tanstackQueryFirebase#123"; - const onErrorMock = vi.fn(); - const error = new Error("Sign out failed"); + test("calls onError callback on sign out failure", async () => { + const email = "tqf@invertase.io"; + const password = "tanstackQueryFirebase#123"; + const onErrorMock = vi.fn(); + const error = new Error("Sign out failed"); - await createUserWithEmailAndPassword(auth, email, password); - await signInWithEmailAndPassword(auth, email, password); + await createUserWithEmailAndPassword(auth, email, password); + await signInWithEmailAndPassword(auth, email, password); - const mockSignOut = vi.spyOn(auth, "signOut").mockRejectedValueOnce(error); + const mockSignOut = vi.spyOn(auth, "signOut").mockRejectedValueOnce(error); - const { result } = renderHook( - () => useSignOutMutation(auth, { onError: onErrorMock }), - { wrapper }, - ); + const { result } = renderHook( + () => useSignOutMutation(auth, { onError: onErrorMock }), + { wrapper }, + ); - await act(async () => result.current.mutate()); + await act(async () => result.current.mutate()); - await waitFor(() => expect(result.current.isError).toBe(true)); + await waitFor(() => expect(result.current.isError).toBe(true)); - expect(onErrorMock).toHaveBeenCalled(); - expect(result.current.error).toBe(error); - expect(result.current.isSuccess).toBe(false); - mockSignOut.mockRestore(); - }); + expect(onErrorMock).toHaveBeenCalled(); + expect(result.current.error).toBe(error); + expect(result.current.isSuccess).toBe(false); + mockSignOut.mockRestore(); + }); - test("handles concurrent sign out attempts", async () => { - const email = "tqf@invertase.io"; - const password = "tanstackQueryFirebase#123"; + test("handles concurrent sign out attempts", async () => { + const email = "tqf@invertase.io"; + const password = "tanstackQueryFirebase#123"; - await createUserWithEmailAndPassword(auth, email, password); - await signInWithEmailAndPassword(auth, email, password); + await createUserWithEmailAndPassword(auth, email, password); + await signInWithEmailAndPassword(auth, email, password); - const { result } = renderHook(() => useSignOutMutation(auth), { wrapper }); + const { result } = renderHook(() => useSignOutMutation(auth), { wrapper }); - await act(async () => { - // Attempt multiple concurrent sign-outs - result.current.mutate(); - result.current.mutate(); - }); + await act(async () => { + // Attempt multiple concurrent sign-outs + result.current.mutate(); + result.current.mutate(); + }); - await waitFor(() => expect(result.current.isSuccess).toBe(true)); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); - expect(auth.currentUser).toBeNull(); - }); + expect(auth.currentUser).toBeNull(); + }); }); diff --git a/packages/react/src/auth/useSignOutMutation.ts b/packages/react/src/auth/useSignOutMutation.ts index 448ecd7..ca10c50 100644 --- a/packages/react/src/auth/useSignOutMutation.ts +++ b/packages/react/src/auth/useSignOutMutation.ts @@ -2,17 +2,17 @@ import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; import { type Auth, signOut } from "firebase/auth"; type AuthUseMutationOptions< - TData = unknown, - TError = Error, - TVariables = void, + TData = unknown, + TError = Error, + TVariables = void, > = Omit, "mutationFn">; export function useSignOutMutation( - auth: Auth, - options?: AuthUseMutationOptions, + auth: Auth, + options?: AuthUseMutationOptions, ) { - return useMutation({ - ...options, - mutationFn: () => signOut(auth), - }); + return useMutation({ + ...options, + mutationFn: () => signOut(auth), + }); } diff --git a/packages/react/src/auth/useUpdateCurrentUserMutation.test.tsx b/packages/react/src/auth/useUpdateCurrentUserMutation.test.tsx index d77602f..d4394db 100644 --- a/packages/react/src/auth/useUpdateCurrentUserMutation.test.tsx +++ b/packages/react/src/auth/useUpdateCurrentUserMutation.test.tsx @@ -1,185 +1,185 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { act, renderHook, waitFor } from "@testing-library/react"; import { - type User, - createUserWithEmailAndPassword, - signInWithEmailAndPassword, + type User, + createUserWithEmailAndPassword, + signInWithEmailAndPassword, } from "firebase/auth"; -import React from "react"; +import type React from "react"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { auth, wipeAuth } from "~/testing-utils"; import { useUpdateCurrentUserMutation } from "./useUpdateCurrentUserMutation"; const queryClient = new QueryClient({ - defaultOptions: { - queries: { retry: false }, - mutations: { retry: false }, - }, + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, }); const wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} + {children} ); describe("useUpdateCurrentUserMutation", () => { - const currentUser: User | null = null; + const currentUser: User | null = null; - beforeEach(async () => { - queryClient.clear(); - await wipeAuth(); - }); + beforeEach(async () => { + queryClient.clear(); + await wipeAuth(); + }); - afterEach(async () => { - await auth.signOut(); - }); + afterEach(async () => { + await auth.signOut(); + }); - test("successfully updates current user", async () => { - const email = "tqf@invertase.io"; - const password = "tanstackQueryFirebase#123"; + test("successfully updates current user", async () => { + const email = "tqf@invertase.io"; + const password = "tanstackQueryFirebase#123"; - await createUserWithEmailAndPassword(auth, email, password); - const userCredential = await signInWithEmailAndPassword( - auth, - email, - password, - ); - const newUser = userCredential.user; - - const { result } = renderHook(() => useUpdateCurrentUserMutation(auth), { - wrapper, - }); - - await act(async () => { - result.current.mutate(newUser); - }); - - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - - expect(auth.currentUser?.uid).toBe(newUser.uid); - expect(auth.currentUser?.email).toBe(email); - }); - - test("successfully sets current user to null", async () => { - const email = "tqf@invertase.io"; - const password = "tanstackQueryFirebase#123"; - - await createUserWithEmailAndPassword(auth, email, password); - await signInWithEmailAndPassword(auth, email, password); - - const { result } = renderHook(() => useUpdateCurrentUserMutation(auth), { - wrapper, - }); - - await act(async () => { - result.current.mutate(null); - }); - - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - - expect(auth.currentUser).toBeNull(); - }); - - test("handles update error when user is invalid", async () => { - const invalidUser = { uid: "invalid-uid" } as User; - - const { result } = renderHook(() => useUpdateCurrentUserMutation(auth), { - wrapper, - }); - - await act(async () => { - result.current.mutate(invalidUser); - }); - - await waitFor(() => expect(result.current.isError).toBe(true)); - - expect(result.current.error).toBeDefined(); - // TODO: Assert for firebase error - }); - - test("calls onSuccess callback after successful update", async () => { - const email = "tqf@invertase.io"; - const password = "tanstackQueryFirebase#123"; - const onSuccessMock = vi.fn(); - - await createUserWithEmailAndPassword(auth, email, password); - const userCredential = await signInWithEmailAndPassword( - auth, - email, - password, - ); - const newUser = userCredential.user; - - const { result } = renderHook( - () => - useUpdateCurrentUserMutation(auth, { - onSuccess: onSuccessMock, - }), - { wrapper }, - ); - - await act(async () => { - result.current.mutate(newUser); - }); - - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - - expect(onSuccessMock).toHaveBeenCalled(); - expect(auth.currentUser?.email).toBe(email); - }); - - test("calls onError callback on update failure", async () => { - const onErrorMock = vi.fn(); - const error = new Error("Update failed"); - - // Mock updateCurrentUser to simulate error - const mockUpdateUser = vi - .spyOn(auth, "updateCurrentUser") - .mockRejectedValueOnce(error); - - const { result } = renderHook( - () => - useUpdateCurrentUserMutation(auth, { - onError: onErrorMock, - }), - { wrapper }, - ); - - await act(async () => { - result.current.mutate(null); - }); - - await waitFor(() => expect(result.current.isError).toBe(true)); - - expect(result.current.error).toBe(error); - expect(result.current.isSuccess).toBe(false); - mockUpdateUser.mockRestore(); - }); - - test("handles concurrent update attempts", async () => { - const email = "tqf@invertase.io"; - const password = "tanstackQueryFirebase#123"; - - await createUserWithEmailAndPassword(auth, email, password); - const userCredential = await signInWithEmailAndPassword( - auth, - email, - password, - ); - const newUser = userCredential.user; - - const { result } = renderHook(() => useUpdateCurrentUserMutation(auth), { - wrapper, - }); - - await act(async () => { - // Attempt multiple concurrent updates - result.current.mutate(newUser); - result.current.mutate(newUser); - }); - - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - - expect(auth.currentUser?.uid).toBe(newUser.uid); - expect(auth.currentUser?.email).toBe(email); - }); + await createUserWithEmailAndPassword(auth, email, password); + const userCredential = await signInWithEmailAndPassword( + auth, + email, + password, + ); + const newUser = userCredential.user; + + const { result } = renderHook(() => useUpdateCurrentUserMutation(auth), { + wrapper, + }); + + await act(async () => { + result.current.mutate(newUser); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(auth.currentUser?.uid).toBe(newUser.uid); + expect(auth.currentUser?.email).toBe(email); + }); + + test("successfully sets current user to null", async () => { + const email = "tqf@invertase.io"; + const password = "tanstackQueryFirebase#123"; + + await createUserWithEmailAndPassword(auth, email, password); + await signInWithEmailAndPassword(auth, email, password); + + const { result } = renderHook(() => useUpdateCurrentUserMutation(auth), { + wrapper, + }); + + await act(async () => { + result.current.mutate(null); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(auth.currentUser).toBeNull(); + }); + + test("handles update error when user is invalid", async () => { + const invalidUser = { uid: "invalid-uid" } as User; + + const { result } = renderHook(() => useUpdateCurrentUserMutation(auth), { + wrapper, + }); + + await act(async () => { + result.current.mutate(invalidUser); + }); + + await waitFor(() => expect(result.current.isError).toBe(true)); + + expect(result.current.error).toBeDefined(); + // TODO: Assert for firebase error + }); + + test("calls onSuccess callback after successful update", async () => { + const email = "tqf@invertase.io"; + const password = "tanstackQueryFirebase#123"; + const onSuccessMock = vi.fn(); + + await createUserWithEmailAndPassword(auth, email, password); + const userCredential = await signInWithEmailAndPassword( + auth, + email, + password, + ); + const newUser = userCredential.user; + + const { result } = renderHook( + () => + useUpdateCurrentUserMutation(auth, { + onSuccess: onSuccessMock, + }), + { wrapper }, + ); + + await act(async () => { + result.current.mutate(newUser); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(onSuccessMock).toHaveBeenCalled(); + expect(auth.currentUser?.email).toBe(email); + }); + + test("calls onError callback on update failure", async () => { + const onErrorMock = vi.fn(); + const error = new Error("Update failed"); + + // Mock updateCurrentUser to simulate error + const mockUpdateUser = vi + .spyOn(auth, "updateCurrentUser") + .mockRejectedValueOnce(error); + + const { result } = renderHook( + () => + useUpdateCurrentUserMutation(auth, { + onError: onErrorMock, + }), + { wrapper }, + ); + + await act(async () => { + result.current.mutate(null); + }); + + await waitFor(() => expect(result.current.isError).toBe(true)); + + expect(result.current.error).toBe(error); + expect(result.current.isSuccess).toBe(false); + mockUpdateUser.mockRestore(); + }); + + test("handles concurrent update attempts", async () => { + const email = "tqf@invertase.io"; + const password = "tanstackQueryFirebase#123"; + + await createUserWithEmailAndPassword(auth, email, password); + const userCredential = await signInWithEmailAndPassword( + auth, + email, + password, + ); + const newUser = userCredential.user; + + const { result } = renderHook(() => useUpdateCurrentUserMutation(auth), { + wrapper, + }); + + await act(async () => { + // Attempt multiple concurrent updates + result.current.mutate(newUser); + result.current.mutate(newUser); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(auth.currentUser?.uid).toBe(newUser.uid); + expect(auth.currentUser?.email).toBe(email); + }); }); diff --git a/packages/react/src/auth/useUpdateCurrentUserMutation.ts b/packages/react/src/auth/useUpdateCurrentUserMutation.ts index d6d3088..45dd995 100644 --- a/packages/react/src/auth/useUpdateCurrentUserMutation.ts +++ b/packages/react/src/auth/useUpdateCurrentUserMutation.ts @@ -1,23 +1,23 @@ import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; import { - type Auth, - type AuthError, - type User, - updateCurrentUser, + type Auth, + type AuthError, + type User, + updateCurrentUser, } from "firebase/auth"; type AuthUseMutationOptions< - TData = unknown, - TError = Error, - TVariables = void, + TData = unknown, + TError = Error, + TVariables = void, > = Omit, "mutationFn">; export function useUpdateCurrentUserMutation( - auth: Auth, - options?: AuthUseMutationOptions, + auth: Auth, + options?: AuthUseMutationOptions, ) { - return useMutation({ - ...options, - mutationFn: (user) => updateCurrentUser(auth, user), - }); + return useMutation({ + ...options, + mutationFn: (user) => updateCurrentUser(auth, user), + }); } diff --git a/packages/react/src/auth/useVerifyPasswordResetCodeMutation.test.tsx b/packages/react/src/auth/useVerifyPasswordResetCodeMutation.test.tsx index c0b68f1..bf930a4 100644 --- a/packages/react/src/auth/useVerifyPasswordResetCodeMutation.test.tsx +++ b/packages/react/src/auth/useVerifyPasswordResetCodeMutation.test.tsx @@ -1,93 +1,93 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { act, renderHook, waitFor } from "@testing-library/react"; import { - createUserWithEmailAndPassword, - sendPasswordResetEmail, + createUserWithEmailAndPassword, + sendPasswordResetEmail, } from "firebase/auth"; -import React from "react"; +import type React from "react"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { auth, wipeAuth } from "~/testing-utils"; import { useVerifyPasswordResetCodeMutation } from "./useVerifyPasswordResetCodeMutation"; import { waitForPasswordResetCode } from "./utils"; const queryClient = new QueryClient({ - defaultOptions: { - queries: { retry: false }, - mutations: { retry: false }, - }, + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, }); const wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} + {children} ); describe("useVerifyPasswordResetCodeMutation", () => { - const email = "tqf@invertase.io"; - const password = "TanstackQueryFirebase#123"; + const email = "tqf@invertase.io"; + const password = "TanstackQueryFirebase#123"; - beforeEach(async () => { - queryClient.clear(); - await wipeAuth(); - await createUserWithEmailAndPassword(auth, email, password); - await sendPasswordResetEmail(auth, email); - }); + beforeEach(async () => { + queryClient.clear(); + await wipeAuth(); + await createUserWithEmailAndPassword(auth, email, password); + await sendPasswordResetEmail(auth, email); + }); - afterEach(async () => { - vi.clearAllMocks(); - await auth.signOut(); - }); + afterEach(async () => { + vi.clearAllMocks(); + await auth.signOut(); + }); - test("successfully verifies the reset code", async () => { - const code = await waitForPasswordResetCode(email); + test("successfully verifies the reset code", async () => { + const code = await waitForPasswordResetCode(email); - const { result } = renderHook( - () => useVerifyPasswordResetCodeMutation(auth), - { - wrapper, - }, - ); + const { result } = renderHook( + () => useVerifyPasswordResetCodeMutation(auth), + { + wrapper, + }, + ); - await act(async () => { - code && (await result.current.mutateAsync(code)); - }); + await act(async () => { + code && (await result.current.mutateAsync(code)); + }); - await waitFor(() => expect(result.current.isSuccess).toBe(true)); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); - expect(result.current.data).toBe(email); - expect(result.current.variables).toBe(code); - }); + expect(result.current.data).toBe(email); + expect(result.current.variables).toBe(code); + }); - test("handles invalid reset code", async () => { - const invalidCode = "invalid-reset-code"; + test("handles invalid reset code", async () => { + const invalidCode = "invalid-reset-code"; - const { result } = renderHook( - () => useVerifyPasswordResetCodeMutation(auth), - { wrapper }, - ); + const { result } = renderHook( + () => useVerifyPasswordResetCodeMutation(auth), + { wrapper }, + ); - await act(async () => { - await result.current.mutate(invalidCode); - }); + await act(async () => { + await result.current.mutate(invalidCode); + }); - await waitFor(() => expect(result.current.isError).toBe(true)); + await waitFor(() => expect(result.current.isError).toBe(true)); - expect(result.current.error).toBeDefined(); - // TODO: Assert Firebase error for auth/invalid-action-code - }); + expect(result.current.error).toBeDefined(); + // TODO: Assert Firebase error for auth/invalid-action-code + }); - test("handles empty reset code", async () => { - const emptyCode = ""; + test("handles empty reset code", async () => { + const emptyCode = ""; - const { result } = renderHook( - () => useVerifyPasswordResetCodeMutation(auth), - { wrapper }, - ); + const { result } = renderHook( + () => useVerifyPasswordResetCodeMutation(auth), + { wrapper }, + ); - await act(async () => await result.current.mutate(emptyCode)); + await act(async () => await result.current.mutate(emptyCode)); - await waitFor(() => expect(result.current.isError).toBe(true)); + await waitFor(() => expect(result.current.isError).toBe(true)); - expect(result.current.error).toBeDefined(); - // TODO: Assert Firebase error for auth/invalid-action-code - }); + expect(result.current.error).toBeDefined(); + // TODO: Assert Firebase error for auth/invalid-action-code + }); }); diff --git a/packages/react/src/auth/useVerifyPasswordResetCodeMutation.ts b/packages/react/src/auth/useVerifyPasswordResetCodeMutation.ts index a56cd5f..4207c7a 100644 --- a/packages/react/src/auth/useVerifyPasswordResetCodeMutation.ts +++ b/packages/react/src/auth/useVerifyPasswordResetCodeMutation.ts @@ -1,22 +1,22 @@ import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; import { - type Auth, - type AuthError, - verifyPasswordResetCode, + type Auth, + type AuthError, + verifyPasswordResetCode, } from "firebase/auth"; type AuthUseMutationOptions< - TData = unknown, - TError = Error, - TVariables = void, + TData = unknown, + TError = Error, + TVariables = void, > = Omit, "mutationFn">; export function useVerifyPasswordResetCodeMutation( - auth: Auth, - options?: AuthUseMutationOptions, + auth: Auth, + options?: AuthUseMutationOptions, ) { - return useMutation({ - ...options, - mutationFn: (code: string) => verifyPasswordResetCode(auth, code), - }); + return useMutation({ + ...options, + mutationFn: (code: string) => verifyPasswordResetCode(auth, code), + }); } diff --git a/packages/react/src/auth/utils.ts b/packages/react/src/auth/utils.ts index 0e61eeb..802171f 100644 --- a/packages/react/src/auth/utils.ts +++ b/packages/react/src/auth/utils.ts @@ -7,40 +7,40 @@ import path from "node:path"; * @returns The password reset code (oobCode) or null if not found */ async function getPasswordResetCodeFromLogs( - email: string, + email: string, ): Promise { - try { - // Read the firebase-debug.log file - const logPath = path.join(process.cwd(), "firebase-debug.log"); - const logContent = await fs.promises.readFile(logPath, "utf8"); + try { + // Read the firebase-debug.log file + const logPath = path.join(process.cwd(), "firebase-debug.log"); + const logContent = await fs.promises.readFile(logPath, "utf8"); - // Find the most recent password reset link for the given email - const lines = logContent.split("\n").reverse(); - const resetLinkPattern = new RegExp( - `To reset the password for ${email.replace( - ".", - "\\.", - )}.*?http://127\\.0\\.0\\.1:9099.*`, - "i", - ); + // Find the most recent password reset link for the given email + const lines = logContent.split("\n").reverse(); + const resetLinkPattern = new RegExp( + `To reset the password for ${email.replace( + ".", + "\\.", + )}.*?http://127\\.0\\.0\\.1:9099.*`, + "i", + ); - for (const line of lines) { - const match = line.match(resetLinkPattern); - if (match) { - // Extract oobCode from the reset link - const url = match[0].match(/http:\/\/127\.0\.0\.1:9099\/.*?$/)?.[0]; - if (url) { - const oobCode = new URL(url).searchParams.get("oobCode"); - return oobCode; - } - } - } + for (const line of lines) { + const match = line.match(resetLinkPattern); + if (match) { + // Extract oobCode from the reset link + const url = match[0].match(/http:\/\/127\.0\.0\.1:9099\/.*?$/)?.[0]; + if (url) { + const oobCode = new URL(url).searchParams.get("oobCode"); + return oobCode; + } + } + } - return null; - } catch (error) { - console.error("Error reading Firebase debug log:", error); - return null; - } + return null; + } catch (error) { + console.error("Error reading Firebase debug log:", error); + return null; + } } /** @@ -51,21 +51,21 @@ async function getPasswordResetCodeFromLogs( * @returns The password reset code or null if timeout is reached */ async function waitForPasswordResetCode( - email: string, - timeout = 5000, - interval = 100, + email: string, + timeout = 5000, + interval = 100, ): Promise { - const startTime = Date.now(); + const startTime = Date.now(); - while (Date.now() - startTime < timeout) { - const code = await getPasswordResetCodeFromLogs(email); - if (code) { - return code; - } - await new Promise((resolve) => setTimeout(resolve, interval)); - } + while (Date.now() - startTime < timeout) { + const code = await getPasswordResetCodeFromLogs(email); + if (code) { + return code; + } + await new Promise((resolve) => setTimeout(resolve, interval)); + } - return null; + return null; } export { getPasswordResetCodeFromLogs, waitForPasswordResetCode }; diff --git a/packages/react/src/data-connect/query-client.ts b/packages/react/src/data-connect/query-client.ts index 519cfc2..b5d2561 100644 --- a/packages/react/src/data-connect/query-client.ts +++ b/packages/react/src/data-connect/query-client.ts @@ -1,74 +1,74 @@ import { - type FetchQueryOptions, - QueryClient, - type QueryKey, + type FetchQueryOptions, + QueryClient, + type QueryKey, } from "@tanstack/react-query"; import type { FirebaseError } from "firebase/app"; import { - type QueryRef, - type QueryResult, - executeQuery, + type QueryRef, + type QueryResult, + executeQuery, } from "firebase/data-connect"; import type { FlattenedQueryResult } from "./types"; export type DataConnectQueryOptions = Omit< - FetchQueryOptions< - FlattenedQueryResult, - FirebaseError, - FlattenedQueryResult, - QueryKey - >, - "queryFn" | "queryKey" + FetchQueryOptions< + FlattenedQueryResult, + FirebaseError, + FlattenedQueryResult, + QueryKey + >, + "queryFn" | "queryKey" > & { - queryRef: QueryRef; - queryKey?: QueryKey; + queryRef: QueryRef; + queryKey?: QueryKey; }; export class DataConnectQueryClient extends QueryClient { - prefetchDataConnectQuery, Variables>( - refOrResult: QueryRef | QueryResult, - options?: DataConnectQueryOptions, - ) { - let queryRef: QueryRef; - let initialData: FlattenedQueryResult | undefined; + prefetchDataConnectQuery, Variables>( + refOrResult: QueryRef | QueryResult, + options?: DataConnectQueryOptions, + ) { + let queryRef: QueryRef; + let initialData: FlattenedQueryResult | undefined; - if ("ref" in refOrResult) { - queryRef = refOrResult.ref; - initialData = { - ...refOrResult.data, - ref: refOrResult.ref, - source: refOrResult.source, - fetchTime: refOrResult.fetchTime, - }; - } else { - queryRef = refOrResult; - } + if ("ref" in refOrResult) { + queryRef = refOrResult.ref; + initialData = { + ...refOrResult.data, + ref: refOrResult.ref, + source: refOrResult.source, + fetchTime: refOrResult.fetchTime, + }; + } else { + queryRef = refOrResult; + } - return this.prefetchQuery< - FlattenedQueryResult, - FirebaseError, - FlattenedQueryResult, - QueryKey - >({ - ...options, - initialData, - queryKey: options?.queryKey ?? [ - queryRef.name, - queryRef.variables || null, - ], - queryFn: async () => { - const response = await executeQuery(queryRef); + return this.prefetchQuery< + FlattenedQueryResult, + FirebaseError, + FlattenedQueryResult, + QueryKey + >({ + ...options, + initialData, + queryKey: options?.queryKey ?? [ + queryRef.name, + queryRef.variables || null, + ], + queryFn: async () => { + const response = await executeQuery(queryRef); - const data = { - ...response.data, - ref: response.ref, - source: response.source, - fetchTime: response.fetchTime, - }; + const data = { + ...response.data, + ref: response.ref, + source: response.source, + fetchTime: response.fetchTime, + }; - // Ensures no serialization issues with undefined values - return JSON.parse(JSON.stringify(data)); - }, - }); - } + // Ensures no serialization issues with undefined values + return JSON.parse(JSON.stringify(data)); + }, + }); + } } diff --git a/packages/react/src/data-connect/types.ts b/packages/react/src/data-connect/types.ts index 2833b01..1abd2e4 100644 --- a/packages/react/src/data-connect/types.ts +++ b/packages/react/src/data-connect/types.ts @@ -3,13 +3,13 @@ import type { MutationResult, QueryResult } from "firebase/data-connect"; // Flattens a QueryResult data down into a single object. // This is to prevent query.data.data, and also expose additional properties. export type FlattenedQueryResult = Omit< - QueryResult, - "data" | "toJSON" + QueryResult, + "data" | "toJSON" > & - Data; + Data; export type FlattenedMutationResult = Omit< - MutationResult, - "data" | "toJSON" + MutationResult, + "data" | "toJSON" > & - Data; + Data; diff --git a/packages/react/src/data-connect/useDataConnectMutation.test.tsx b/packages/react/src/data-connect/useDataConnectMutation.test.tsx index f488fde..2d5af40 100644 --- a/packages/react/src/data-connect/useDataConnectMutation.test.tsx +++ b/packages/react/src/data-connect/useDataConnectMutation.test.tsx @@ -1,10 +1,10 @@ import { - createMovie, - createMovieRef, - deleteMovieRef, - getMovieByIdRef, - listMoviesRef, - upsertMovieRef, + createMovie, + createMovieRef, + deleteMovieRef, + getMovieByIdRef, + listMoviesRef, + upsertMovieRef, } from "@/dataconnect/default-connector"; import { act, renderHook, waitFor } from "@testing-library/react"; import { beforeEach, describe, expect, test, vi } from "vitest"; @@ -16,984 +16,1003 @@ import { useDataConnectMutation } from "./useDataConnectMutation"; firebaseApp; describe("useDataConnectMutation", () => { - const invalidateQueriesSpy = vi.spyOn(queryClient, "invalidateQueries"); - const onSuccess = vi.fn(); - - beforeEach(() => { - vi.resetAllMocks(); - queryClient.clear(); - }); - - test("returns initial state correctly for create mutation", () => { - const { result } = renderHook(() => useDataConnectMutation(createMovieRef), { - wrapper, - }); - - expect(result.current.isIdle).toBe(true); - expect(result.current.status).toBe("idle"); - }); - - test("returns initial state correctly for update mutation", () => { - const { result } = renderHook(() => useDataConnectMutation(upsertMovieRef), { - wrapper, - }); - - expect(result.current.isIdle).toBe(true); - expect(result.current.status).toBe("idle"); - }); - - test("returns initial state correctly for delete mutation", () => { - const { result } = renderHook(() => useDataConnectMutation(deleteMovieRef), { - wrapper, - }); - - expect(result.current.isIdle).toBe(true); - expect(result.current.status).toBe("idle"); - }); - - test("executes create mutation successfully thus returning flattened data including ref, source, and fetchTime", async () => { - const { result } = renderHook(() => useDataConnectMutation(createMovieRef), { - wrapper, - }); - - expect(result.current.isIdle).toBe(true); - - const movie = { - title: "TanStack Query Firebase", - genre: "library", - imageUrl: "https://test-image-url.com/", - }; - - await act(async () => { - await result.current.mutateAsync(movie); - }); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - expect(result.current.data).toBeDefined(); - expect(result.current.data).toHaveProperty("ref"); - expect(result.current.data).toHaveProperty("source"); - expect(result.current.data).toHaveProperty("fetchTime"); - expect(result.current.data).toHaveProperty("movie_insert"); - }); - }); - - test("executes update mutation successfully thus returning flattened data including ref, source, and fetchTime", async () => { - const { result: createMutationResult } = renderHook( - () => useDataConnectMutation(createMovieRef), - { - wrapper, - }, - ); - - expect(createMutationResult.current.isIdle).toBe(true); - - const movie = { - title: "TanStack Query Firebase", - genre: "library", - imageUrl: "https://test-image-url.com/", - }; - - await act(async () => { - await createMutationResult.current.mutateAsync(movie); - }); - - await waitFor(() => { - expect(createMutationResult.current.isSuccess).toBe(true); - expect(createMutationResult.current.data).toHaveProperty("movie_insert"); - }); - - const movieId = createMutationResult.current.data?.movie_insert.id!; - - const { result: upsertMutationResult } = renderHook( - () => useDataConnectMutation(upsertMovieRef), - { - wrapper, - }, - ); - - await act(async () => { - await upsertMutationResult.current.mutateAsync({ - id: movieId, - imageUrl: "https://updated-image-url.com/", - title: "TanStack Query Firebase - updated", - }); - }); - - await waitFor(() => { - expect(upsertMutationResult.current.isSuccess).toBe(true); - expect(upsertMutationResult.current.data).toBeDefined(); - expect(upsertMutationResult.current.data).toHaveProperty("ref"); - expect(upsertMutationResult.current.data).toHaveProperty("source"); - expect(upsertMutationResult.current.data).toHaveProperty("fetchTime"); - expect(upsertMutationResult.current.data).toHaveProperty("movie_upsert"); - expect(upsertMutationResult.current.data?.movie_upsert.id).toBe(movieId); - }); - }); - - test("executes delete mutation successfully thus returning flattened data including ref, source, and fetchTime", async () => { - const { result: createMutationResult } = renderHook( - () => useDataConnectMutation(createMovieRef), - { - wrapper, - }, - ); - - expect(createMutationResult.current.isIdle).toBe(true); - - const movie = { - title: "TanStack Query Firebase", - genre: "library", - imageUrl: "https://test-image-url.com/", - }; - - await act(async () => { - await createMutationResult.current.mutateAsync(movie); - }); - - await waitFor(() => { - expect(createMutationResult.current.isSuccess).toBe(true); - expect(createMutationResult.current.data).toHaveProperty("movie_insert"); - }); - - const movieId = createMutationResult.current.data?.movie_insert.id!; - - const { result: deleteMutationResult } = renderHook( - () => useDataConnectMutation(deleteMovieRef), - { - wrapper, - }, - ); - - await act(async () => { - await deleteMutationResult.current.mutateAsync({ - id: movieId, - }); - }); - - await waitFor(() => { - expect(deleteMutationResult.current.isSuccess).toBe(true); - expect(deleteMutationResult.current.data).toBeDefined(); - expect(deleteMutationResult.current.data).toHaveProperty("ref"); - expect(deleteMutationResult.current.data).toHaveProperty("source"); - expect(deleteMutationResult.current.data).toHaveProperty("fetchTime"); - expect(deleteMutationResult.current.data).toHaveProperty("movie_delete"); - expect(deleteMutationResult.current.data?.movie_delete?.id).toBe(movieId); - }); - }); - - test("handles concurrent create mutations", async () => { - const { result } = renderHook(() => useDataConnectMutation(createMovieRef), { - wrapper, - }); - - const movies = [ - { - title: "Concurrent Test 1", - genre: "concurrent_test", - imageUrl: "https://test-image-url-1.com/", - }, - { - title: "Concurrent Test 2", - genre: "concurrent_test", - imageUrl: "https://test-image-url-2.com/", - }, - { - title: "Concurrent Test 3", - genre: "concurrent_test", - imageUrl: "https://test-image-url-3.com/", - }, - ]; - - const createdMovies: { id: string }[] = []; - - await act(async () => { - await Promise.all( - movies.map(async (movie) => { - const data = await result.current.mutateAsync(movie); - createdMovies.push(data?.movie_insert); - }), - ); - }); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - - // Assert that all movies were created - expect(createdMovies).toHaveLength(3); - createdMovies.forEach((movie) => { - expect(movie).toHaveProperty("id"); - }); - - // Check if all IDs are unique - const ids = createdMovies.map((movie) => movie.id); - expect(new Set(ids).size).toBe(ids.length); - }); - }); - - test("handles concurrent upsert mutations", async () => { - const { result: createMutationResult } = renderHook( - () => useDataConnectMutation(createMovieRef), - { - wrapper, - }, - ); - - const movies = [ - { - title: "Concurrent Test 1", - genre: "concurrent_test", - imageUrl: "https://test-image-url-1.com/", - }, - { - title: "Concurrent Test 2", - genre: "concurrent_test", - imageUrl: "https://test-image-url-2.com/", - }, - { - title: "Concurrent Test 3", - genre: "concurrent_test", - imageUrl: "https://test-image-url-3.com/", - }, - ]; - - const createdMovies: { id: string }[] = []; - - await act(async () => { - await Promise.all( - movies.map(async (movie) => { - const data = await createMutationResult.current.mutateAsync(movie); - createdMovies.push(data?.movie_insert); - }), - ); - }); - - await waitFor(() => { - expect(createMutationResult.current.isSuccess).toBe(true); - }); - - const { result: upsertMutationResult } = renderHook( - () => useDataConnectMutation(upsertMovieRef), - { - wrapper, - }, - ); - - const upsertData = createdMovies.map((movie, index) => ({ - id: movie.id, - title: `Updated Test ${index + 1}`, - imageUrl: `https://updated-image-url-${index + 1}.com/`, - })); - - // concurrent upsert operations - const upsertedMovies: { id: string }[] = []; - await act(async () => { - await Promise.all( - upsertData.map(async (update) => { - const data = await upsertMutationResult.current.mutateAsync(update); - upsertedMovies.push(data?.movie_upsert); - }), - ); - }); - - await waitFor(() => { - expect(upsertMutationResult.current.isSuccess).toBe(true); - expect(upsertedMovies).toHaveLength(3); - - // Check if all upserted IDs match original IDs - const upsertedIds = upsertedMovies.map((movie) => movie.id); - expect(upsertedIds).toEqual( - expect.arrayContaining(createdMovies.map((m) => m.id)), - ); - }); - }); - - test("handles concurrent delete mutations", async () => { - const { result: createMutationResult } = renderHook( - () => useDataConnectMutation(createMovieRef), - { - wrapper, - }, - ); - - const movies = [ - { - title: "Concurrent Test 1", - genre: "concurrent_test", - imageUrl: "https://test-image-url-1.com/", - }, - { - title: "Concurrent Test 2", - genre: "concurrent_test", - imageUrl: "https://test-image-url-2.com/", - }, - { - title: "Concurrent Test 3", - genre: "concurrent_test", - imageUrl: "https://test-image-url-3.com/", - }, - ]; - - const createdMovies: { id: string }[] = []; - - await act(async () => { - await Promise.all( - movies.map(async (movie) => { - const data = await createMutationResult.current.mutateAsync(movie); - createdMovies.push(data?.movie_insert); - }), - ); - }); - - await waitFor(() => { - expect(createMutationResult.current.isSuccess).toBe(true); - }); - - const { result: deleteMutationResult } = renderHook( - () => useDataConnectMutation(deleteMovieRef), - { - wrapper, - }, - ); - - const deleteData = createdMovies.map((movie, index) => ({ - id: movie.id, - })); - - // concurrent delete operations - const deletedMovies: { id: string }[] = []; - await act(async () => { - await Promise.all( - deleteData.map(async (i) => { - const data = await deleteMutationResult.current.mutateAsync(i); - deletedMovies.push(data.movie_delete!); - }), - ); - }); - - await waitFor(() => { - expect(deleteMutationResult.current.isSuccess).toBe(true); - expect(deletedMovies).toHaveLength(3); - - // Check if all deleted IDs match original IDs - const deletedIds = deletedMovies.map((movie) => movie.id); - expect(deletedIds).toEqual( - expect.arrayContaining(createdMovies.map((m) => m.id)), - ); - }); - }); - - test("invalidates queries specified in the invalidate option for create mutations with non-variable refs", async () => { - const { result } = renderHook( - () => - useDataConnectMutation(createMovieRef, { - invalidate: [listMoviesRef()], - }), - { - wrapper, - }, - ); - const movie = { - title: "TanStack Query Firebase", - genre: "invalidate_option_test", - imageUrl: "https://test-image-url.com/", - }; - - await act(async () => { - await result.current.mutateAsync(movie); - }); - - await waitFor(() => { - expect(result.current.status).toBe("success"); - }); - - expect(invalidateQueriesSpy.mock.calls).toHaveLength(1); - expect(invalidateQueriesSpy).toHaveBeenCalledWith( - expect.objectContaining({ - queryKey: [listMoviesRef().name], - }), - ); - }); - - test("invalidates queries specified in the invalidate option for create mutations with variable refs", async () => { - const movieData = { - title: "tanstack query firebase", - genre: "library", - imageUrl: "https://invertase.io/", - }; - - const createdMovie = await createMovie(movieData); - - const movieId = createdMovie?.data?.movie_insert?.id; - - const { result } = renderHook( - () => - useDataConnectMutation(createMovieRef, { - invalidate: [getMovieByIdRef({ id: movieId })], - }), - { - wrapper, - }, - ); - const movie = { - title: "TanStack Query Firebase", - genre: "invalidate_option_test", - imageUrl: "https://test-image-url.com/", - }; - - await act(async () => { - await result.current.mutateAsync(movie); - }); - - await waitFor(() => { - expect(result.current.status).toBe("success"); - }); - - expect(invalidateQueriesSpy.mock.calls).toHaveLength(1); - expect(invalidateQueriesSpy).toHaveBeenCalledWith( - expect.objectContaining({ - queryKey: ["GetMovieById", { id: movieId }], - exact: true, - }), - ); - }); - - test("invalidates queries specified in the invalidate option for create mutations with both variable and non-variable refs", async () => { - const movieData = { - title: "tanstack query firebase", - genre: "library", - imageUrl: "https://invertase.io/", - }; - - const createdMovie = await createMovie(movieData); - - const movieId = createdMovie?.data?.movie_insert?.id; - - const { result } = renderHook( - () => - useDataConnectMutation(createMovieRef, { - invalidate: [listMoviesRef(), getMovieByIdRef({ id: movieId })], - }), - { - wrapper, - }, - ); - const movie = { - title: "TanStack Query Firebase", - genre: "invalidate_option_test", - imageUrl: "https://test-image-url.com/", - }; - - await act(async () => { - await result.current.mutateAsync(movie); - }); - - await waitFor(() => { - expect(result.current.status).toBe("success"); - }); - - expect(invalidateQueriesSpy.mock.calls).toHaveLength(2); - expect(invalidateQueriesSpy.mock.calls).toEqual( - expect.arrayContaining([ - [ - expect.objectContaining({ - queryKey: ["GetMovieById", { id: movieId }], - exact: true, - }), - ], - [ - expect.objectContaining({ - queryKey: ["ListMovies"], - }), - ], - ]), - ); - }); - - test("invalidates queries specified in the invalidate option for upsert mutations with non-variable refs", async () => { - const { result: createMutationResult } = renderHook( - () => useDataConnectMutation(createMovieRef), - { - wrapper, - }, - ); - - expect(createMutationResult.current.isIdle).toBe(true); - - const movie = { - title: "TanStack Query Firebase", - genre: "library", - imageUrl: "https://test-image-url.com/", - }; - - await act(async () => { - await createMutationResult.current.mutateAsync(movie); - }); - - await waitFor(() => { - expect(createMutationResult.current.isSuccess).toBe(true); - expect(createMutationResult.current.data).toHaveProperty("movie_insert"); - }); - - const movieId = createMutationResult.current.data?.movie_insert.id!; - - const { result: upsertMutationResult } = renderHook( - () => - useDataConnectMutation(upsertMovieRef, { invalidate: [listMoviesRef()] }), - { - wrapper, - }, - ); - - await act(async () => { - await upsertMutationResult.current.mutateAsync({ - id: movieId, - imageUrl: "https://updated-image-url.com/", - title: "TanStack Query Firebase - updated", - }); - }); - - await waitFor(() => { - expect(upsertMutationResult.current.isSuccess).toBe(true); - expect(upsertMutationResult.current.data).toHaveProperty("movie_upsert"); - expect(upsertMutationResult.current.data?.movie_upsert.id).toBe(movieId); - }); - - expect(invalidateQueriesSpy.mock.calls).toHaveLength(1); - expect(invalidateQueriesSpy).toHaveBeenCalledWith( - expect.objectContaining({ - queryKey: [listMoviesRef().name], - }), - ); - }); - - test("invalidates queries specified in the invalidate option for upsert mutations with variable refs", async () => { - const { result: createMutationResult } = renderHook( - () => useDataConnectMutation(createMovieRef), - { - wrapper, - }, - ); - - expect(createMutationResult.current.isIdle).toBe(true); - - const movie = { - title: "TanStack Query Firebase", - genre: "library", - imageUrl: "https://test-image-url.com/", - }; - - await act(async () => { - await createMutationResult.current.mutateAsync(movie); - }); - - await waitFor(() => { - expect(createMutationResult.current.isSuccess).toBe(true); - expect(createMutationResult.current.data).toHaveProperty("movie_insert"); - }); - - const movieId = createMutationResult.current.data?.movie_insert.id!; - - const { result: upsertMutationResult } = renderHook( - () => - useDataConnectMutation(upsertMovieRef, { - invalidate: [getMovieByIdRef({ id: movieId })], - }), - { - wrapper, - }, - ); - - await act(async () => { - await upsertMutationResult.current.mutateAsync({ - id: movieId, - imageUrl: "https://updated-image-url.com/", - title: "TanStack Query Firebase - updated", - }); - }); - - await waitFor(() => { - expect(upsertMutationResult.current.isSuccess).toBe(true); - expect(upsertMutationResult.current.data).toHaveProperty("movie_upsert"); - expect(upsertMutationResult.current.data?.movie_upsert.id).toBe(movieId); - }); - - expect(invalidateQueriesSpy.mock.calls).toHaveLength(1); - expect(invalidateQueriesSpy).toHaveBeenCalledWith( - expect.objectContaining({ - queryKey: ["GetMovieById", { id: movieId }], - exact: true, - }), - ); - }); - - test("invalidates queries specified in the invalidate option for upsert mutations with both variable and non-variable refs", async () => { - const { result: createMutationResult } = renderHook( - () => useDataConnectMutation(createMovieRef), - { - wrapper, - }, - ); - - expect(createMutationResult.current.isIdle).toBe(true); - - const movie = { - title: "TanStack Query Firebase", - genre: "library", - imageUrl: "https://test-image-url.com/", - }; - - await act(async () => { - await createMutationResult.current.mutateAsync(movie); - }); - - await waitFor(() => { - expect(createMutationResult.current.isSuccess).toBe(true); - expect(createMutationResult.current.data).toHaveProperty("movie_insert"); - }); - - const movieId = createMutationResult.current.data?.movie_insert.id!; - - const { result: upsertMutationResult } = renderHook( - () => - useDataConnectMutation(upsertMovieRef, { - invalidate: [listMoviesRef(), getMovieByIdRef({ id: movieId })], - }), - { - wrapper, - }, - ); - - await act(async () => { - await upsertMutationResult.current.mutateAsync({ - id: movieId, - imageUrl: "https://updated-image-url.com/", - title: "TanStack Query Firebase - updated", - }); - }); - - await waitFor(() => { - expect(upsertMutationResult.current.isSuccess).toBe(true); - expect(upsertMutationResult.current.data).toHaveProperty("movie_upsert"); - expect(upsertMutationResult.current.data?.movie_upsert.id).toBe(movieId); - }); - - expect(invalidateQueriesSpy.mock.calls).toHaveLength(2); - expect(invalidateQueriesSpy.mock.calls).toEqual( - expect.arrayContaining([ - [ - expect.objectContaining({ - queryKey: ["GetMovieById", { id: movieId }], - exact: true, - }), - ], - [ - expect.objectContaining({ - queryKey: ["ListMovies"], - }), - ], - ]), - ); - }); - - test("invalidates queries specified in the invalidate option for delete mutations with non-variable refs", async () => { - const { result: createMutationResult } = renderHook( - () => useDataConnectMutation(createMovieRef), - { - wrapper, - }, - ); - - expect(createMutationResult.current.isIdle).toBe(true); - - const movie = { - title: "TanStack Query Firebase", - genre: "library", - imageUrl: "https://test-image-url.com/", - }; - - await act(async () => { - await createMutationResult.current.mutateAsync(movie); - }); - - await waitFor(() => { - expect(createMutationResult.current.isSuccess).toBe(true); - expect(createMutationResult.current.data).toHaveProperty("movie_insert"); - }); - - const movieId = createMutationResult.current.data?.movie_insert.id!; - - const { result: deleteMutationResult } = renderHook( - () => - useDataConnectMutation(deleteMovieRef, { invalidate: [listMoviesRef()] }), - { - wrapper, - }, - ); - - await act(async () => { - await deleteMutationResult.current.mutateAsync({ - id: movieId, - }); - }); - - await waitFor(() => { - expect(deleteMutationResult.current.isSuccess).toBe(true); - expect(deleteMutationResult.current.data).toHaveProperty("movie_delete"); - expect(deleteMutationResult.current.data?.movie_delete?.id).toBe(movieId); - }); - - expect(invalidateQueriesSpy.mock.calls).toHaveLength(1); - expect(invalidateQueriesSpy).toHaveBeenCalledWith( - expect.objectContaining({ - queryKey: [listMoviesRef().name], - }), - ); - }); - - test("invalidates queries specified in the invalidate option for delete mutations with variable refs", async () => { - const { result: createMutationResult } = renderHook( - () => useDataConnectMutation(createMovieRef), - { - wrapper, - }, - ); - - expect(createMutationResult.current.isIdle).toBe(true); - - const movie = { - title: "TanStack Query Firebase", - genre: "library", - imageUrl: "https://test-image-url.com/", - }; - - await act(async () => { - await createMutationResult.current.mutateAsync(movie); - }); - - await waitFor(() => { - expect(createMutationResult.current.isSuccess).toBe(true); - expect(createMutationResult.current.data).toHaveProperty("movie_insert"); - }); - - const movieId = createMutationResult.current.data?.movie_insert.id!; - - const { result: deleteMutationResult } = renderHook( - () => - useDataConnectMutation(deleteMovieRef, { - invalidate: [getMovieByIdRef({ id: movieId })], - }), - { - wrapper, - }, - ); - - await act(async () => { - await deleteMutationResult.current.mutateAsync({ - id: movieId, - }); - }); - - await waitFor(() => { - expect(deleteMutationResult.current.isSuccess).toBe(true); - expect(deleteMutationResult.current.data).toHaveProperty("movie_delete"); - expect(deleteMutationResult.current.data?.movie_delete?.id).toBe(movieId); - }); - - expect(invalidateQueriesSpy.mock.calls).toHaveLength(1); - expect(invalidateQueriesSpy).toHaveBeenCalledWith( - expect.objectContaining({ - queryKey: ["GetMovieById", { id: movieId }], - exact: true, - }), - ); - }); - - test("invalidates queries specified in the invalidate option for delete mutations with both variable and non-variable refs", async () => { - const { result: createMutationResult } = renderHook( - () => useDataConnectMutation(createMovieRef), - { - wrapper, - }, - ); - - expect(createMutationResult.current.isIdle).toBe(true); - - const movie = { - title: "TanStack Query Firebase", - genre: "library", - imageUrl: "https://test-image-url.com/", - }; - - await act(async () => { - await createMutationResult.current.mutateAsync(movie); - }); - - await waitFor(() => { - expect(createMutationResult.current.isSuccess).toBe(true); - expect(createMutationResult.current.data).toHaveProperty("movie_insert"); - }); - - const movieId = createMutationResult.current.data?.movie_insert.id!; - - const { result: deleteMutationResult } = renderHook( - () => - useDataConnectMutation(deleteMovieRef, { - invalidate: [listMoviesRef(), getMovieByIdRef({ id: movieId })], - }), - { - wrapper, - }, - ); - - await act(async () => { - await deleteMutationResult.current.mutateAsync({ - id: movieId, - }); - }); - - await waitFor(() => { - expect(deleteMutationResult.current.isSuccess).toBe(true); - expect(deleteMutationResult.current.data).toHaveProperty("movie_delete"); - expect(deleteMutationResult.current.data?.movie_delete?.id).toBe(movieId); - }); - - expect(invalidateQueriesSpy.mock.calls).toHaveLength(2); - expect(invalidateQueriesSpy.mock.calls).toEqual( - expect.arrayContaining([ - [ - expect.objectContaining({ - queryKey: ["GetMovieById", { id: movieId }], - exact: true, - }), - ], - [ - expect.objectContaining({ - queryKey: ["ListMovies"], - }), - ], - ]), - ); - }); - - test("calls onSuccess callback after successful create mutation", async () => { - const { result } = renderHook( - () => useDataConnectMutation(createMovieRef, { onSuccess }), - { wrapper }, - ); - - const movie = { - title: "TanStack Query Firebase", - genre: "onsuccess_callback_test", - imageUrl: "https://test-image-url.com/", - }; - - await act(async () => { - await result.current.mutateAsync(movie); - }); - - await waitFor(() => { - expect(onSuccess).toHaveBeenCalled(); - expect(result.current.isSuccess).toBe(true); - expect(result.current.data).toHaveProperty("movie_insert"); - }); - }); - - test("calls onSuccess callback after successful upsert mutation", async () => { - const { result: createMutationResult } = renderHook( - () => useDataConnectMutation(createMovieRef), - { - wrapper, - }, - ); - - expect(createMutationResult.current.isIdle).toBe(true); - - const movie = { - title: "TanStack Query Firebase", - genre: "library", - imageUrl: "https://test-image-url.com/", - }; - - await act(async () => { - await createMutationResult.current.mutateAsync(movie); - }); - - await waitFor(() => { - expect(createMutationResult.current.isSuccess).toBe(true); - expect(createMutationResult.current.data).toHaveProperty("movie_insert"); - }); - - const movieId = createMutationResult.current.data?.movie_insert.id!; - - const { result: upsertMutationResult } = renderHook( - () => useDataConnectMutation(upsertMovieRef, { onSuccess }), - { - wrapper, - }, - ); - - await act(async () => { - await upsertMutationResult.current.mutateAsync({ - id: movieId, - imageUrl: "https://updated-image-url.com/", - title: "TanStack Query Firebase - updated", - }); - }); - - await waitFor(() => { - expect(upsertMutationResult.current.isSuccess).toBe(true); - expect(onSuccess).toHaveBeenCalled(); - expect(upsertMutationResult.current.data).toHaveProperty("movie_upsert"); - expect(upsertMutationResult.current.data?.movie_upsert.id).toBe(movieId); - }); - }); - - test("calls onSuccess callback after successful delete mutation", async () => { - const { result: createMutationResult } = renderHook( - () => useDataConnectMutation(createMovieRef), - { - wrapper, - }, - ); - - expect(createMutationResult.current.isIdle).toBe(true); - - const movie = { - title: "TanStack Query Firebase", - genre: "library", - imageUrl: "https://test-image-url.com/", - }; - - await act(async () => { - await createMutationResult.current.mutateAsync(movie); - }); - - await waitFor(() => { - expect(createMutationResult.current.isSuccess).toBe(true); - expect(createMutationResult.current.data).toHaveProperty("movie_insert"); - }); - - const movieId = createMutationResult.current.data?.movie_insert.id!; - - const { result: deleteMutationResult } = renderHook( - () => useDataConnectMutation(deleteMovieRef, { onSuccess }), - { - wrapper, - }, - ); - - await act(async () => { - await deleteMutationResult.current.mutateAsync({ - id: movieId, - }); - }); - - await waitFor(() => { - expect(deleteMutationResult.current.isSuccess).toBe(true); - expect(onSuccess).toHaveBeenCalled(); - expect(deleteMutationResult.current.data).toHaveProperty("movie_delete"); - expect(deleteMutationResult.current.data?.movie_delete?.id).toBe(movieId); - }); - }); + const invalidateQueriesSpy = vi.spyOn(queryClient, "invalidateQueries"); + const onSuccess = vi.fn(); + + beforeEach(() => { + vi.resetAllMocks(); + queryClient.clear(); + }); + + test("returns initial state correctly for create mutation", () => { + const { result } = renderHook( + () => useDataConnectMutation(createMovieRef), + { + wrapper, + }, + ); + + expect(result.current.isIdle).toBe(true); + expect(result.current.status).toBe("idle"); + }); + + test("returns initial state correctly for update mutation", () => { + const { result } = renderHook( + () => useDataConnectMutation(upsertMovieRef), + { + wrapper, + }, + ); + + expect(result.current.isIdle).toBe(true); + expect(result.current.status).toBe("idle"); + }); + + test("returns initial state correctly for delete mutation", () => { + const { result } = renderHook( + () => useDataConnectMutation(deleteMovieRef), + { + wrapper, + }, + ); + + expect(result.current.isIdle).toBe(true); + expect(result.current.status).toBe("idle"); + }); + + test("executes create mutation successfully thus returning flattened data including ref, source, and fetchTime", async () => { + const { result } = renderHook( + () => useDataConnectMutation(createMovieRef), + { + wrapper, + }, + ); + + expect(result.current.isIdle).toBe(true); + + const movie = { + title: "TanStack Query Firebase", + genre: "library", + imageUrl: "https://test-image-url.com/", + }; + + await act(async () => { + await result.current.mutateAsync(movie); + }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + expect(result.current.data).toBeDefined(); + expect(result.current.data).toHaveProperty("ref"); + expect(result.current.data).toHaveProperty("source"); + expect(result.current.data).toHaveProperty("fetchTime"); + expect(result.current.data).toHaveProperty("movie_insert"); + }); + }); + + test("executes update mutation successfully thus returning flattened data including ref, source, and fetchTime", async () => { + const { result: createMutationResult } = renderHook( + () => useDataConnectMutation(createMovieRef), + { + wrapper, + }, + ); + + expect(createMutationResult.current.isIdle).toBe(true); + + const movie = { + title: "TanStack Query Firebase", + genre: "library", + imageUrl: "https://test-image-url.com/", + }; + + await act(async () => { + await createMutationResult.current.mutateAsync(movie); + }); + + await waitFor(() => { + expect(createMutationResult.current.isSuccess).toBe(true); + expect(createMutationResult.current.data).toHaveProperty("movie_insert"); + }); + + const movieId = createMutationResult.current.data?.movie_insert.id!; + + const { result: upsertMutationResult } = renderHook( + () => useDataConnectMutation(upsertMovieRef), + { + wrapper, + }, + ); + + await act(async () => { + await upsertMutationResult.current.mutateAsync({ + id: movieId, + imageUrl: "https://updated-image-url.com/", + title: "TanStack Query Firebase - updated", + }); + }); + + await waitFor(() => { + expect(upsertMutationResult.current.isSuccess).toBe(true); + expect(upsertMutationResult.current.data).toBeDefined(); + expect(upsertMutationResult.current.data).toHaveProperty("ref"); + expect(upsertMutationResult.current.data).toHaveProperty("source"); + expect(upsertMutationResult.current.data).toHaveProperty("fetchTime"); + expect(upsertMutationResult.current.data).toHaveProperty("movie_upsert"); + expect(upsertMutationResult.current.data?.movie_upsert.id).toBe(movieId); + }); + }); + + test("executes delete mutation successfully thus returning flattened data including ref, source, and fetchTime", async () => { + const { result: createMutationResult } = renderHook( + () => useDataConnectMutation(createMovieRef), + { + wrapper, + }, + ); + + expect(createMutationResult.current.isIdle).toBe(true); + + const movie = { + title: "TanStack Query Firebase", + genre: "library", + imageUrl: "https://test-image-url.com/", + }; + + await act(async () => { + await createMutationResult.current.mutateAsync(movie); + }); + + await waitFor(() => { + expect(createMutationResult.current.isSuccess).toBe(true); + expect(createMutationResult.current.data).toHaveProperty("movie_insert"); + }); + + const movieId = createMutationResult.current.data?.movie_insert.id!; + + const { result: deleteMutationResult } = renderHook( + () => useDataConnectMutation(deleteMovieRef), + { + wrapper, + }, + ); + + await act(async () => { + await deleteMutationResult.current.mutateAsync({ + id: movieId, + }); + }); + + await waitFor(() => { + expect(deleteMutationResult.current.isSuccess).toBe(true); + expect(deleteMutationResult.current.data).toBeDefined(); + expect(deleteMutationResult.current.data).toHaveProperty("ref"); + expect(deleteMutationResult.current.data).toHaveProperty("source"); + expect(deleteMutationResult.current.data).toHaveProperty("fetchTime"); + expect(deleteMutationResult.current.data).toHaveProperty("movie_delete"); + expect(deleteMutationResult.current.data?.movie_delete?.id).toBe(movieId); + }); + }); + + test("handles concurrent create mutations", async () => { + const { result } = renderHook( + () => useDataConnectMutation(createMovieRef), + { + wrapper, + }, + ); + + const movies = [ + { + title: "Concurrent Test 1", + genre: "concurrent_test", + imageUrl: "https://test-image-url-1.com/", + }, + { + title: "Concurrent Test 2", + genre: "concurrent_test", + imageUrl: "https://test-image-url-2.com/", + }, + { + title: "Concurrent Test 3", + genre: "concurrent_test", + imageUrl: "https://test-image-url-3.com/", + }, + ]; + + const createdMovies: { id: string }[] = []; + + await act(async () => { + await Promise.all( + movies.map(async (movie) => { + const data = await result.current.mutateAsync(movie); + createdMovies.push(data?.movie_insert); + }), + ); + }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + + // Assert that all movies were created + expect(createdMovies).toHaveLength(3); + createdMovies.forEach((movie) => { + expect(movie).toHaveProperty("id"); + }); + + // Check if all IDs are unique + const ids = createdMovies.map((movie) => movie.id); + expect(new Set(ids).size).toBe(ids.length); + }); + }); + + test("handles concurrent upsert mutations", async () => { + const { result: createMutationResult } = renderHook( + () => useDataConnectMutation(createMovieRef), + { + wrapper, + }, + ); + + const movies = [ + { + title: "Concurrent Test 1", + genre: "concurrent_test", + imageUrl: "https://test-image-url-1.com/", + }, + { + title: "Concurrent Test 2", + genre: "concurrent_test", + imageUrl: "https://test-image-url-2.com/", + }, + { + title: "Concurrent Test 3", + genre: "concurrent_test", + imageUrl: "https://test-image-url-3.com/", + }, + ]; + + const createdMovies: { id: string }[] = []; + + await act(async () => { + await Promise.all( + movies.map(async (movie) => { + const data = await createMutationResult.current.mutateAsync(movie); + createdMovies.push(data?.movie_insert); + }), + ); + }); + + await waitFor(() => { + expect(createMutationResult.current.isSuccess).toBe(true); + }); + + const { result: upsertMutationResult } = renderHook( + () => useDataConnectMutation(upsertMovieRef), + { + wrapper, + }, + ); + + const upsertData = createdMovies.map((movie, index) => ({ + id: movie.id, + title: `Updated Test ${index + 1}`, + imageUrl: `https://updated-image-url-${index + 1}.com/`, + })); + + // concurrent upsert operations + const upsertedMovies: { id: string }[] = []; + await act(async () => { + await Promise.all( + upsertData.map(async (update) => { + const data = await upsertMutationResult.current.mutateAsync(update); + upsertedMovies.push(data?.movie_upsert); + }), + ); + }); + + await waitFor(() => { + expect(upsertMutationResult.current.isSuccess).toBe(true); + expect(upsertedMovies).toHaveLength(3); + + // Check if all upserted IDs match original IDs + const upsertedIds = upsertedMovies.map((movie) => movie.id); + expect(upsertedIds).toEqual( + expect.arrayContaining(createdMovies.map((m) => m.id)), + ); + }); + }); + + test("handles concurrent delete mutations", async () => { + const { result: createMutationResult } = renderHook( + () => useDataConnectMutation(createMovieRef), + { + wrapper, + }, + ); + + const movies = [ + { + title: "Concurrent Test 1", + genre: "concurrent_test", + imageUrl: "https://test-image-url-1.com/", + }, + { + title: "Concurrent Test 2", + genre: "concurrent_test", + imageUrl: "https://test-image-url-2.com/", + }, + { + title: "Concurrent Test 3", + genre: "concurrent_test", + imageUrl: "https://test-image-url-3.com/", + }, + ]; + + const createdMovies: { id: string }[] = []; + + await act(async () => { + await Promise.all( + movies.map(async (movie) => { + const data = await createMutationResult.current.mutateAsync(movie); + createdMovies.push(data?.movie_insert); + }), + ); + }); + + await waitFor(() => { + expect(createMutationResult.current.isSuccess).toBe(true); + }); + + const { result: deleteMutationResult } = renderHook( + () => useDataConnectMutation(deleteMovieRef), + { + wrapper, + }, + ); + + const deleteData = createdMovies.map((movie, index) => ({ + id: movie.id, + })); + + // concurrent delete operations + const deletedMovies: { id: string }[] = []; + await act(async () => { + await Promise.all( + deleteData.map(async (i) => { + const data = await deleteMutationResult.current.mutateAsync(i); + deletedMovies.push(data.movie_delete!); + }), + ); + }); + + await waitFor(() => { + expect(deleteMutationResult.current.isSuccess).toBe(true); + expect(deletedMovies).toHaveLength(3); + + // Check if all deleted IDs match original IDs + const deletedIds = deletedMovies.map((movie) => movie.id); + expect(deletedIds).toEqual( + expect.arrayContaining(createdMovies.map((m) => m.id)), + ); + }); + }); + + test("invalidates queries specified in the invalidate option for create mutations with non-variable refs", async () => { + const { result } = renderHook( + () => + useDataConnectMutation(createMovieRef, { + invalidate: [listMoviesRef()], + }), + { + wrapper, + }, + ); + const movie = { + title: "TanStack Query Firebase", + genre: "invalidate_option_test", + imageUrl: "https://test-image-url.com/", + }; + + await act(async () => { + await result.current.mutateAsync(movie); + }); + + await waitFor(() => { + expect(result.current.status).toBe("success"); + }); + + expect(invalidateQueriesSpy.mock.calls).toHaveLength(1); + expect(invalidateQueriesSpy).toHaveBeenCalledWith( + expect.objectContaining({ + queryKey: [listMoviesRef().name], + }), + ); + }); + + test("invalidates queries specified in the invalidate option for create mutations with variable refs", async () => { + const movieData = { + title: "tanstack query firebase", + genre: "library", + imageUrl: "https://invertase.io/", + }; + + const createdMovie = await createMovie(movieData); + + const movieId = createdMovie?.data?.movie_insert?.id; + + const { result } = renderHook( + () => + useDataConnectMutation(createMovieRef, { + invalidate: [getMovieByIdRef({ id: movieId })], + }), + { + wrapper, + }, + ); + const movie = { + title: "TanStack Query Firebase", + genre: "invalidate_option_test", + imageUrl: "https://test-image-url.com/", + }; + + await act(async () => { + await result.current.mutateAsync(movie); + }); + + await waitFor(() => { + expect(result.current.status).toBe("success"); + }); + + expect(invalidateQueriesSpy.mock.calls).toHaveLength(1); + expect(invalidateQueriesSpy).toHaveBeenCalledWith( + expect.objectContaining({ + queryKey: ["GetMovieById", { id: movieId }], + exact: true, + }), + ); + }); + + test("invalidates queries specified in the invalidate option for create mutations with both variable and non-variable refs", async () => { + const movieData = { + title: "tanstack query firebase", + genre: "library", + imageUrl: "https://invertase.io/", + }; + + const createdMovie = await createMovie(movieData); + + const movieId = createdMovie?.data?.movie_insert?.id; + + const { result } = renderHook( + () => + useDataConnectMutation(createMovieRef, { + invalidate: [listMoviesRef(), getMovieByIdRef({ id: movieId })], + }), + { + wrapper, + }, + ); + const movie = { + title: "TanStack Query Firebase", + genre: "invalidate_option_test", + imageUrl: "https://test-image-url.com/", + }; + + await act(async () => { + await result.current.mutateAsync(movie); + }); + + await waitFor(() => { + expect(result.current.status).toBe("success"); + }); + + expect(invalidateQueriesSpy.mock.calls).toHaveLength(2); + expect(invalidateQueriesSpy.mock.calls).toEqual( + expect.arrayContaining([ + [ + expect.objectContaining({ + queryKey: ["GetMovieById", { id: movieId }], + exact: true, + }), + ], + [ + expect.objectContaining({ + queryKey: ["ListMovies"], + }), + ], + ]), + ); + }); + + test("invalidates queries specified in the invalidate option for upsert mutations with non-variable refs", async () => { + const { result: createMutationResult } = renderHook( + () => useDataConnectMutation(createMovieRef), + { + wrapper, + }, + ); + + expect(createMutationResult.current.isIdle).toBe(true); + + const movie = { + title: "TanStack Query Firebase", + genre: "library", + imageUrl: "https://test-image-url.com/", + }; + + await act(async () => { + await createMutationResult.current.mutateAsync(movie); + }); + + await waitFor(() => { + expect(createMutationResult.current.isSuccess).toBe(true); + expect(createMutationResult.current.data).toHaveProperty("movie_insert"); + }); + + const movieId = createMutationResult.current.data?.movie_insert.id!; + + const { result: upsertMutationResult } = renderHook( + () => + useDataConnectMutation(upsertMovieRef, { + invalidate: [listMoviesRef()], + }), + { + wrapper, + }, + ); + + await act(async () => { + await upsertMutationResult.current.mutateAsync({ + id: movieId, + imageUrl: "https://updated-image-url.com/", + title: "TanStack Query Firebase - updated", + }); + }); + + await waitFor(() => { + expect(upsertMutationResult.current.isSuccess).toBe(true); + expect(upsertMutationResult.current.data).toHaveProperty("movie_upsert"); + expect(upsertMutationResult.current.data?.movie_upsert.id).toBe(movieId); + }); + + expect(invalidateQueriesSpy.mock.calls).toHaveLength(1); + expect(invalidateQueriesSpy).toHaveBeenCalledWith( + expect.objectContaining({ + queryKey: [listMoviesRef().name], + }), + ); + }); + + test("invalidates queries specified in the invalidate option for upsert mutations with variable refs", async () => { + const { result: createMutationResult } = renderHook( + () => useDataConnectMutation(createMovieRef), + { + wrapper, + }, + ); + + expect(createMutationResult.current.isIdle).toBe(true); + + const movie = { + title: "TanStack Query Firebase", + genre: "library", + imageUrl: "https://test-image-url.com/", + }; + + await act(async () => { + await createMutationResult.current.mutateAsync(movie); + }); + + await waitFor(() => { + expect(createMutationResult.current.isSuccess).toBe(true); + expect(createMutationResult.current.data).toHaveProperty("movie_insert"); + }); + + const movieId = createMutationResult.current.data?.movie_insert.id!; + + const { result: upsertMutationResult } = renderHook( + () => + useDataConnectMutation(upsertMovieRef, { + invalidate: [getMovieByIdRef({ id: movieId })], + }), + { + wrapper, + }, + ); + + await act(async () => { + await upsertMutationResult.current.mutateAsync({ + id: movieId, + imageUrl: "https://updated-image-url.com/", + title: "TanStack Query Firebase - updated", + }); + }); + + await waitFor(() => { + expect(upsertMutationResult.current.isSuccess).toBe(true); + expect(upsertMutationResult.current.data).toHaveProperty("movie_upsert"); + expect(upsertMutationResult.current.data?.movie_upsert.id).toBe(movieId); + }); + + expect(invalidateQueriesSpy.mock.calls).toHaveLength(1); + expect(invalidateQueriesSpy).toHaveBeenCalledWith( + expect.objectContaining({ + queryKey: ["GetMovieById", { id: movieId }], + exact: true, + }), + ); + }); + + test("invalidates queries specified in the invalidate option for upsert mutations with both variable and non-variable refs", async () => { + const { result: createMutationResult } = renderHook( + () => useDataConnectMutation(createMovieRef), + { + wrapper, + }, + ); + + expect(createMutationResult.current.isIdle).toBe(true); + + const movie = { + title: "TanStack Query Firebase", + genre: "library", + imageUrl: "https://test-image-url.com/", + }; + + await act(async () => { + await createMutationResult.current.mutateAsync(movie); + }); + + await waitFor(() => { + expect(createMutationResult.current.isSuccess).toBe(true); + expect(createMutationResult.current.data).toHaveProperty("movie_insert"); + }); + + const movieId = createMutationResult.current.data?.movie_insert.id!; + + const { result: upsertMutationResult } = renderHook( + () => + useDataConnectMutation(upsertMovieRef, { + invalidate: [listMoviesRef(), getMovieByIdRef({ id: movieId })], + }), + { + wrapper, + }, + ); + + await act(async () => { + await upsertMutationResult.current.mutateAsync({ + id: movieId, + imageUrl: "https://updated-image-url.com/", + title: "TanStack Query Firebase - updated", + }); + }); + + await waitFor(() => { + expect(upsertMutationResult.current.isSuccess).toBe(true); + expect(upsertMutationResult.current.data).toHaveProperty("movie_upsert"); + expect(upsertMutationResult.current.data?.movie_upsert.id).toBe(movieId); + }); + + expect(invalidateQueriesSpy.mock.calls).toHaveLength(2); + expect(invalidateQueriesSpy.mock.calls).toEqual( + expect.arrayContaining([ + [ + expect.objectContaining({ + queryKey: ["GetMovieById", { id: movieId }], + exact: true, + }), + ], + [ + expect.objectContaining({ + queryKey: ["ListMovies"], + }), + ], + ]), + ); + }); + + test("invalidates queries specified in the invalidate option for delete mutations with non-variable refs", async () => { + const { result: createMutationResult } = renderHook( + () => useDataConnectMutation(createMovieRef), + { + wrapper, + }, + ); + + expect(createMutationResult.current.isIdle).toBe(true); + + const movie = { + title: "TanStack Query Firebase", + genre: "library", + imageUrl: "https://test-image-url.com/", + }; + + await act(async () => { + await createMutationResult.current.mutateAsync(movie); + }); + + await waitFor(() => { + expect(createMutationResult.current.isSuccess).toBe(true); + expect(createMutationResult.current.data).toHaveProperty("movie_insert"); + }); + + const movieId = createMutationResult.current.data?.movie_insert.id!; + + const { result: deleteMutationResult } = renderHook( + () => + useDataConnectMutation(deleteMovieRef, { + invalidate: [listMoviesRef()], + }), + { + wrapper, + }, + ); + + await act(async () => { + await deleteMutationResult.current.mutateAsync({ + id: movieId, + }); + }); + + await waitFor(() => { + expect(deleteMutationResult.current.isSuccess).toBe(true); + expect(deleteMutationResult.current.data).toHaveProperty("movie_delete"); + expect(deleteMutationResult.current.data?.movie_delete?.id).toBe(movieId); + }); + + expect(invalidateQueriesSpy.mock.calls).toHaveLength(1); + expect(invalidateQueriesSpy).toHaveBeenCalledWith( + expect.objectContaining({ + queryKey: [listMoviesRef().name], + }), + ); + }); + + test("invalidates queries specified in the invalidate option for delete mutations with variable refs", async () => { + const { result: createMutationResult } = renderHook( + () => useDataConnectMutation(createMovieRef), + { + wrapper, + }, + ); + + expect(createMutationResult.current.isIdle).toBe(true); + + const movie = { + title: "TanStack Query Firebase", + genre: "library", + imageUrl: "https://test-image-url.com/", + }; + + await act(async () => { + await createMutationResult.current.mutateAsync(movie); + }); + + await waitFor(() => { + expect(createMutationResult.current.isSuccess).toBe(true); + expect(createMutationResult.current.data).toHaveProperty("movie_insert"); + }); + + const movieId = createMutationResult.current.data?.movie_insert.id!; + + const { result: deleteMutationResult } = renderHook( + () => + useDataConnectMutation(deleteMovieRef, { + invalidate: [getMovieByIdRef({ id: movieId })], + }), + { + wrapper, + }, + ); + + await act(async () => { + await deleteMutationResult.current.mutateAsync({ + id: movieId, + }); + }); + + await waitFor(() => { + expect(deleteMutationResult.current.isSuccess).toBe(true); + expect(deleteMutationResult.current.data).toHaveProperty("movie_delete"); + expect(deleteMutationResult.current.data?.movie_delete?.id).toBe(movieId); + }); + + expect(invalidateQueriesSpy.mock.calls).toHaveLength(1); + expect(invalidateQueriesSpy).toHaveBeenCalledWith( + expect.objectContaining({ + queryKey: ["GetMovieById", { id: movieId }], + exact: true, + }), + ); + }); + + test("invalidates queries specified in the invalidate option for delete mutations with both variable and non-variable refs", async () => { + const { result: createMutationResult } = renderHook( + () => useDataConnectMutation(createMovieRef), + { + wrapper, + }, + ); + + expect(createMutationResult.current.isIdle).toBe(true); + + const movie = { + title: "TanStack Query Firebase", + genre: "library", + imageUrl: "https://test-image-url.com/", + }; + + await act(async () => { + await createMutationResult.current.mutateAsync(movie); + }); + + await waitFor(() => { + expect(createMutationResult.current.isSuccess).toBe(true); + expect(createMutationResult.current.data).toHaveProperty("movie_insert"); + }); + + const movieId = createMutationResult.current.data?.movie_insert.id!; + + const { result: deleteMutationResult } = renderHook( + () => + useDataConnectMutation(deleteMovieRef, { + invalidate: [listMoviesRef(), getMovieByIdRef({ id: movieId })], + }), + { + wrapper, + }, + ); + + await act(async () => { + await deleteMutationResult.current.mutateAsync({ + id: movieId, + }); + }); + + await waitFor(() => { + expect(deleteMutationResult.current.isSuccess).toBe(true); + expect(deleteMutationResult.current.data).toHaveProperty("movie_delete"); + expect(deleteMutationResult.current.data?.movie_delete?.id).toBe(movieId); + }); + + expect(invalidateQueriesSpy.mock.calls).toHaveLength(2); + expect(invalidateQueriesSpy.mock.calls).toEqual( + expect.arrayContaining([ + [ + expect.objectContaining({ + queryKey: ["GetMovieById", { id: movieId }], + exact: true, + }), + ], + [ + expect.objectContaining({ + queryKey: ["ListMovies"], + }), + ], + ]), + ); + }); + + test("calls onSuccess callback after successful create mutation", async () => { + const { result } = renderHook( + () => useDataConnectMutation(createMovieRef, { onSuccess }), + { wrapper }, + ); + + const movie = { + title: "TanStack Query Firebase", + genre: "onsuccess_callback_test", + imageUrl: "https://test-image-url.com/", + }; + + await act(async () => { + await result.current.mutateAsync(movie); + }); + + await waitFor(() => { + expect(onSuccess).toHaveBeenCalled(); + expect(result.current.isSuccess).toBe(true); + expect(result.current.data).toHaveProperty("movie_insert"); + }); + }); + + test("calls onSuccess callback after successful upsert mutation", async () => { + const { result: createMutationResult } = renderHook( + () => useDataConnectMutation(createMovieRef), + { + wrapper, + }, + ); + + expect(createMutationResult.current.isIdle).toBe(true); + + const movie = { + title: "TanStack Query Firebase", + genre: "library", + imageUrl: "https://test-image-url.com/", + }; + + await act(async () => { + await createMutationResult.current.mutateAsync(movie); + }); + + await waitFor(() => { + expect(createMutationResult.current.isSuccess).toBe(true); + expect(createMutationResult.current.data).toHaveProperty("movie_insert"); + }); + + const movieId = createMutationResult.current.data?.movie_insert.id!; + + const { result: upsertMutationResult } = renderHook( + () => useDataConnectMutation(upsertMovieRef, { onSuccess }), + { + wrapper, + }, + ); + + await act(async () => { + await upsertMutationResult.current.mutateAsync({ + id: movieId, + imageUrl: "https://updated-image-url.com/", + title: "TanStack Query Firebase - updated", + }); + }); + + await waitFor(() => { + expect(upsertMutationResult.current.isSuccess).toBe(true); + expect(onSuccess).toHaveBeenCalled(); + expect(upsertMutationResult.current.data).toHaveProperty("movie_upsert"); + expect(upsertMutationResult.current.data?.movie_upsert.id).toBe(movieId); + }); + }); + + test("calls onSuccess callback after successful delete mutation", async () => { + const { result: createMutationResult } = renderHook( + () => useDataConnectMutation(createMovieRef), + { + wrapper, + }, + ); + + expect(createMutationResult.current.isIdle).toBe(true); + + const movie = { + title: "TanStack Query Firebase", + genre: "library", + imageUrl: "https://test-image-url.com/", + }; + + await act(async () => { + await createMutationResult.current.mutateAsync(movie); + }); + + await waitFor(() => { + expect(createMutationResult.current.isSuccess).toBe(true); + expect(createMutationResult.current.data).toHaveProperty("movie_insert"); + }); + + const movieId = createMutationResult.current.data?.movie_insert.id!; + + const { result: deleteMutationResult } = renderHook( + () => useDataConnectMutation(deleteMovieRef, { onSuccess }), + { + wrapper, + }, + ); + + await act(async () => { + await deleteMutationResult.current.mutateAsync({ + id: movieId, + }); + }); + + await waitFor(() => { + expect(deleteMutationResult.current.isSuccess).toBe(true); + expect(onSuccess).toHaveBeenCalled(); + expect(deleteMutationResult.current.data).toHaveProperty("movie_delete"); + expect(deleteMutationResult.current.data?.movie_delete?.id).toBe(movieId); + }); + }); }); diff --git a/packages/react/src/data-connect/useDataConnectMutation.ts b/packages/react/src/data-connect/useDataConnectMutation.ts index 4e394ef..debe3fe 100644 --- a/packages/react/src/data-connect/useDataConnectMutation.ts +++ b/packages/react/src/data-connect/useDataConnectMutation.ts @@ -1,81 +1,81 @@ import { - type UseMutationOptions, - useMutation, - useQueryClient, + type UseMutationOptions, + useMutation, + useQueryClient, } from "@tanstack/react-query"; import type { FirebaseError } from "firebase/app"; import { - type DataConnect, - type MutationRef, - type QueryRef, - executeMutation, + type DataConnect, + type MutationRef, + type QueryRef, + executeMutation, } from "firebase/data-connect"; import type { FlattenedMutationResult } from "./types"; export type useDataConnectMutationOptions< - TData = unknown, - TError = FirebaseError, - Variables = unknown, + TData = unknown, + TError = FirebaseError, + Variables = unknown, > = Omit, "mutationFn"> & { - invalidate?: Array< - QueryRef | (() => QueryRef) - >; + invalidate?: Array< + QueryRef | (() => QueryRef) + >; }; export function useDataConnectMutation< - Fn extends (...args: any[]) => MutationRef, - Data = ReturnType extends MutationRef ? D : never, - Variables = Fn extends ( - dc: DataConnect, - vars: infer V, - ) => MutationRef - ? V - : Fn extends (vars: infer V) => MutationRef - ? V - : never, + Fn extends (...args: any[]) => MutationRef, + Data = ReturnType extends MutationRef ? D : never, + Variables = Fn extends ( + dc: DataConnect, + vars: infer V, + ) => MutationRef + ? V + : Fn extends (vars: infer V) => MutationRef + ? V + : never, >( - ref: Fn, - options?: useDataConnectMutationOptions< - FlattenedMutationResult, - FirebaseError, - Variables - >, + ref: Fn, + options?: useDataConnectMutationOptions< + FlattenedMutationResult, + FirebaseError, + Variables + >, ) { - const queryClient = useQueryClient(); + const queryClient = useQueryClient(); - return useMutation< - FlattenedMutationResult, - FirebaseError, - Variables - >({ - ...options, - onSuccess(...args) { - if (options?.invalidate?.length) { - for (const ref of options.invalidate) { - if ("variables" in ref && ref.variables !== undefined) { - queryClient.invalidateQueries({ - queryKey: [ref.name, ref.variables], - exact: true, - }); - } else { - queryClient.invalidateQueries({ - queryKey: [ref.name], - }); - } - } - } + return useMutation< + FlattenedMutationResult, + FirebaseError, + Variables + >({ + ...options, + onSuccess(...args) { + if (options?.invalidate?.length) { + for (const ref of options.invalidate) { + if ("variables" in ref && ref.variables !== undefined) { + queryClient.invalidateQueries({ + queryKey: [ref.name, ref.variables], + exact: true, + }); + } else { + queryClient.invalidateQueries({ + queryKey: [ref.name], + }); + } + } + } - options?.onSuccess?.(...args); - }, - mutationFn: async (variables) => { - const response = await executeMutation(ref(variables)); + options?.onSuccess?.(...args); + }, + mutationFn: async (variables) => { + const response = await executeMutation(ref(variables)); - return { - ...response.data, - ref: response.ref, - source: response.source, - fetchTime: response.fetchTime, - }; - }, - }); + return { + ...response.data, + ref: response.ref, + source: response.source, + fetchTime: response.fetchTime, + }; + }, + }); } diff --git a/packages/react/src/data-connect/useDataConnectQuery.test.tsx b/packages/react/src/data-connect/useDataConnectQuery.test.tsx index c809d04..dcb37ac 100644 --- a/packages/react/src/data-connect/useDataConnectQuery.test.tsx +++ b/packages/react/src/data-connect/useDataConnectQuery.test.tsx @@ -1,7 +1,7 @@ import { - createMovie, - getMovieByIdRef, - listMoviesRef, + createMovie, + getMovieByIdRef, + listMoviesRef, } from "@/dataconnect/default-connector"; import { dehydrate } from "@tanstack/react-query"; import { act, renderHook, waitFor } from "@testing-library/react"; @@ -16,238 +16,238 @@ import { useDataConnectQuery } from "./useDataConnectQuery"; firebaseApp; describe("useDataConnectQuery", () => { - beforeEach(async () => { - queryClient.clear(); - }); - - test("returns pending state initially", async () => { - const { result } = renderHook(() => useDataConnectQuery(listMoviesRef()), { - wrapper, - }); - - expect(result.current.isPending).toBe(true); - expect(result.current.status).toBe("pending"); - - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - - expect(result.current.data).toBeDefined(); - }); - - test("fetches data successfully", async () => { - const { result } = renderHook(() => useDataConnectQuery(listMoviesRef()), { - wrapper, - }); - - expect(result.current.isPending).toBe(true); - - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - - expect(result.current.data).toBeDefined(); - expect(result.current.data).toHaveProperty("movies"); - expect(Array.isArray(result.current.data?.movies)).toBe(true); - expect(result.current.data?.movies.length).toBeGreaterThanOrEqual(0); - }); - - test("refetches data successfully", async () => { - const { result } = renderHook(() => useDataConnectQuery(listMoviesRef()), { - wrapper, - }); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - expect(result.current.data).toBeDefined(); - expect(result.current.data).toHaveProperty("ref"); - expect(result.current.data).toHaveProperty("source"); - expect(result.current.data).toHaveProperty("fetchTime"); - }); - - const initialFetchTime = result.current.data?.fetchTime; - - await new Promise((resolve) => setTimeout(resolve, 2000)); // 2 seconds delay before refetching - - await act(async () => { - result.current.refetch(); - }); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - expect(result.current.data).toBeDefined(); - expect(result.current.data).toHaveProperty("ref"); - expect(result.current.data).toHaveProperty("source"); - expect(result.current.data).toHaveProperty("fetchTime"); - expect(result.current.data).toHaveProperty("movies"); - expect(Array.isArray(result.current.data?.movies)).toBe(true); - expect(result.current.data?.movies.length).toBeGreaterThanOrEqual(0); - }); - - const refetchTime = result.current.data?.fetchTime; - }); - - test("returns correct data", async () => { - const { result } = renderHook(() => useDataConnectQuery(listMoviesRef()), { - wrapper, - }); - - await createMovie({ - title: "tanstack query firebase", - genre: "library", - imageUrl: "https://invertase.io/", - }); - - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - - expect(result.current.data).toBeDefined(); - expect(result.current.data?.movies).toBeDefined(); - expect(Array.isArray(result.current.data?.movies)).toBe(true); - - const movie = result.current.data?.movies.find( - (m) => m.title === "tanstack query firebase", - ); - - expect(movie).toBeDefined(); - expect(movie).toHaveProperty("title", "tanstack query firebase"); - expect(movie).toHaveProperty("genre", "library"); - expect(movie).toHaveProperty("imageUrl", "https://invertase.io/"); - }); - - test("returns the correct data properties", async () => { - const { result } = renderHook(() => useDataConnectQuery(listMoviesRef()), { - wrapper, - }); - - await createMovie({ - title: "tanstack query firebase", - genre: "library", - imageUrl: "https://invertase.io/", - }); - - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - - result.current.data?.movies.forEach((i) => { - expect(i).toHaveProperty("title"); - expect(i).toHaveProperty("genre"); - expect(i).toHaveProperty("imageUrl"); - }); - }); - - test("fetches data by unique identifier", async () => { - const movieData = { - title: "tanstack query firebase", - genre: "library", - imageUrl: "https://invertase.io/", - }; - const createdMovie = await createMovie(movieData); - - const movieId = createdMovie?.data?.movie_insert?.id; - - const { result } = renderHook( - () => useDataConnectQuery(getMovieByIdRef({ id: movieId })), - { - wrapper, - }, - ); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - expect(result.current.data?.movie?.title).toBe(movieData?.title); - expect(result.current.data?.movie?.genre).toBe(movieData?.genre); - expect(result.current.data?.movie?.imageUrl).toBe(movieData?.imageUrl); - }); - }); - - test("returns flattened data including ref, source, and fetchTime", async () => { - const { result } = renderHook(() => useDataConnectQuery(listMoviesRef()), { - wrapper, - }); - - expect(result.current.isLoading).toBe(true); - - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - - expect(result.current.data).toBeDefined(); - expect(result.current.data).toHaveProperty("ref"); - expect(result.current.data).toHaveProperty("source"); - expect(result.current.data).toHaveProperty("fetchTime"); - }); - - test("returns flattened data including ref, source, and fetchTime for queries with unique identifier", async () => { - const movieData = { - title: "tanstack query firebase", - genre: "library", - imageUrl: "https://invertase.io/", - }; - const createdMovie = await createMovie(movieData); - - const movieId = createdMovie?.data?.movie_insert?.id; - - const { result } = renderHook( - () => useDataConnectQuery(getMovieByIdRef({ id: movieId })), - { - wrapper, - }, - ); - - expect(result.current.isPending).toBe(true); - - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - - expect(result.current.data).toBeDefined(); - expect(result.current.data).toHaveProperty("ref"); - expect(result.current.data).toHaveProperty("source"); - expect(result.current.data).toHaveProperty("fetchTime"); - }); - - test("avails the data immediately when QueryResult is passed", async () => { - const queryResult = await executeQuery(listMoviesRef()); - - const { result } = renderHook(() => useDataConnectQuery(queryResult), { - wrapper, - }); - - // Should not enter a loading state - expect(result.current.isLoading).toBe(false); - expect(result.current.isPending).toBe(false); - - expect(result.current.isSuccess).toBe(true); - - expect(result.current.data).toBeDefined(); - expect(result.current.data).toHaveProperty("ref"); - expect(result.current.data).toHaveProperty("source"); - expect(result.current.data).toHaveProperty("fetchTime"); - }); - - test("a query with no variables has null as the second query key argument", async () => { - const queryClient = new DataConnectQueryClient(); - - await queryClient.prefetchDataConnectQuery(listMoviesRef()); + beforeEach(async () => { + queryClient.clear(); + }); + + test("returns pending state initially", async () => { + const { result } = renderHook(() => useDataConnectQuery(listMoviesRef()), { + wrapper, + }); + + expect(result.current.isPending).toBe(true); + expect(result.current.status).toBe("pending"); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data).toBeDefined(); + }); + + test("fetches data successfully", async () => { + const { result } = renderHook(() => useDataConnectQuery(listMoviesRef()), { + wrapper, + }); + + expect(result.current.isPending).toBe(true); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data).toBeDefined(); + expect(result.current.data).toHaveProperty("movies"); + expect(Array.isArray(result.current.data?.movies)).toBe(true); + expect(result.current.data?.movies.length).toBeGreaterThanOrEqual(0); + }); + + test("refetches data successfully", async () => { + const { result } = renderHook(() => useDataConnectQuery(listMoviesRef()), { + wrapper, + }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + expect(result.current.data).toBeDefined(); + expect(result.current.data).toHaveProperty("ref"); + expect(result.current.data).toHaveProperty("source"); + expect(result.current.data).toHaveProperty("fetchTime"); + }); + + const initialFetchTime = result.current.data?.fetchTime; + + await new Promise((resolve) => setTimeout(resolve, 2000)); // 2 seconds delay before refetching + + await act(async () => { + result.current.refetch(); + }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + expect(result.current.data).toBeDefined(); + expect(result.current.data).toHaveProperty("ref"); + expect(result.current.data).toHaveProperty("source"); + expect(result.current.data).toHaveProperty("fetchTime"); + expect(result.current.data).toHaveProperty("movies"); + expect(Array.isArray(result.current.data?.movies)).toBe(true); + expect(result.current.data?.movies.length).toBeGreaterThanOrEqual(0); + }); + + const refetchTime = result.current.data?.fetchTime; + }); + + test("returns correct data", async () => { + const { result } = renderHook(() => useDataConnectQuery(listMoviesRef()), { + wrapper, + }); + + await createMovie({ + title: "tanstack query firebase", + genre: "library", + imageUrl: "https://invertase.io/", + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data).toBeDefined(); + expect(result.current.data?.movies).toBeDefined(); + expect(Array.isArray(result.current.data?.movies)).toBe(true); + + const movie = result.current.data?.movies.find( + (m) => m.title === "tanstack query firebase", + ); + + expect(movie).toBeDefined(); + expect(movie).toHaveProperty("title", "tanstack query firebase"); + expect(movie).toHaveProperty("genre", "library"); + expect(movie).toHaveProperty("imageUrl", "https://invertase.io/"); + }); + + test("returns the correct data properties", async () => { + const { result } = renderHook(() => useDataConnectQuery(listMoviesRef()), { + wrapper, + }); + + await createMovie({ + title: "tanstack query firebase", + genre: "library", + imageUrl: "https://invertase.io/", + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + result.current.data?.movies.forEach((i) => { + expect(i).toHaveProperty("title"); + expect(i).toHaveProperty("genre"); + expect(i).toHaveProperty("imageUrl"); + }); + }); + + test("fetches data by unique identifier", async () => { + const movieData = { + title: "tanstack query firebase", + genre: "library", + imageUrl: "https://invertase.io/", + }; + const createdMovie = await createMovie(movieData); + + const movieId = createdMovie?.data?.movie_insert?.id; + + const { result } = renderHook( + () => useDataConnectQuery(getMovieByIdRef({ id: movieId })), + { + wrapper, + }, + ); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + expect(result.current.data?.movie?.title).toBe(movieData?.title); + expect(result.current.data?.movie?.genre).toBe(movieData?.genre); + expect(result.current.data?.movie?.imageUrl).toBe(movieData?.imageUrl); + }); + }); + + test("returns flattened data including ref, source, and fetchTime", async () => { + const { result } = renderHook(() => useDataConnectQuery(listMoviesRef()), { + wrapper, + }); + + expect(result.current.isLoading).toBe(true); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data).toBeDefined(); + expect(result.current.data).toHaveProperty("ref"); + expect(result.current.data).toHaveProperty("source"); + expect(result.current.data).toHaveProperty("fetchTime"); + }); + + test("returns flattened data including ref, source, and fetchTime for queries with unique identifier", async () => { + const movieData = { + title: "tanstack query firebase", + genre: "library", + imageUrl: "https://invertase.io/", + }; + const createdMovie = await createMovie(movieData); + + const movieId = createdMovie?.data?.movie_insert?.id; + + const { result } = renderHook( + () => useDataConnectQuery(getMovieByIdRef({ id: movieId })), + { + wrapper, + }, + ); + + expect(result.current.isPending).toBe(true); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data).toBeDefined(); + expect(result.current.data).toHaveProperty("ref"); + expect(result.current.data).toHaveProperty("source"); + expect(result.current.data).toHaveProperty("fetchTime"); + }); + + test("avails the data immediately when QueryResult is passed", async () => { + const queryResult = await executeQuery(listMoviesRef()); + + const { result } = renderHook(() => useDataConnectQuery(queryResult), { + wrapper, + }); + + // Should not enter a loading state + expect(result.current.isLoading).toBe(false); + expect(result.current.isPending).toBe(false); + + expect(result.current.isSuccess).toBe(true); + + expect(result.current.data).toBeDefined(); + expect(result.current.data).toHaveProperty("ref"); + expect(result.current.data).toHaveProperty("source"); + expect(result.current.data).toHaveProperty("fetchTime"); + }); + + test("a query with no variables has null as the second query key argument", async () => { + const queryClient = new DataConnectQueryClient(); + + await queryClient.prefetchDataConnectQuery(listMoviesRef()); - const dehydrated = dehydrate(queryClient); - - expect(dehydrated.queries[0].queryKey).toEqual(["ListMovies", null]); - }); - - test("a query with variables has valid query keys including the variables", async () => { - const movieData = { - title: "tanstack query firebase", - genre: "library", - imageUrl: "https://invertase.io/", - }; + const dehydrated = dehydrate(queryClient); + + expect(dehydrated.queries[0].queryKey).toEqual(["ListMovies", null]); + }); + + test("a query with variables has valid query keys including the variables", async () => { + const movieData = { + title: "tanstack query firebase", + genre: "library", + imageUrl: "https://invertase.io/", + }; - const createdMovie = await createMovie(movieData); + const createdMovie = await createMovie(movieData); - const movieId = createdMovie?.data?.movie_insert?.id; + const movieId = createdMovie?.data?.movie_insert?.id; - const queryClient = new DataConnectQueryClient(); + const queryClient = new DataConnectQueryClient(); - await queryClient.prefetchDataConnectQuery( - getMovieByIdRef({ id: movieId }), - ); + await queryClient.prefetchDataConnectQuery( + getMovieByIdRef({ id: movieId }), + ); - const dehydrated = dehydrate(queryClient); + const dehydrated = dehydrate(queryClient); - expect(dehydrated.queries[0].queryKey).toEqual([ - "GetMovieById", - { id: movieId }, - ]); - }); + expect(dehydrated.queries[0].queryKey).toEqual([ + "GetMovieById", + { id: movieId }, + ]); + }); }); diff --git a/packages/react/src/data-connect/useDataConnectQuery.ts b/packages/react/src/data-connect/useDataConnectQuery.ts index b1bfd78..31ca7bb 100644 --- a/packages/react/src/data-connect/useDataConnectQuery.ts +++ b/packages/react/src/data-connect/useDataConnectQuery.ts @@ -1,53 +1,53 @@ import { type UseQueryOptions, useQuery } from "@tanstack/react-query"; import type { FirebaseError } from "firebase/app"; import { - type QueryRef, - type QueryResult, - executeQuery, + type QueryRef, + type QueryResult, + executeQuery, } from "firebase/data-connect"; import type { PartialBy } from "../../utils"; import type { FlattenedQueryResult } from "./types"; export type useDataConnectQueryOptions< - TData = unknown, - TError = FirebaseError, + TData = unknown, + TError = FirebaseError, > = PartialBy, "queryFn">, "queryKey">; export function useDataConnectQuery( - refOrResult: QueryRef | QueryResult, - options?: useDataConnectQueryOptions< - FlattenedQueryResult, - FirebaseError - >, + refOrResult: QueryRef | QueryResult, + options?: useDataConnectQueryOptions< + FlattenedQueryResult, + FirebaseError + >, ) { - let queryRef: QueryRef; - let initialData: FlattenedQueryResult | undefined; + let queryRef: QueryRef; + let initialData: FlattenedQueryResult | undefined; - if ("ref" in refOrResult) { - queryRef = refOrResult.ref; - initialData = { - ...refOrResult.data, - ref: refOrResult.ref, - source: refOrResult.source, - fetchTime: refOrResult.fetchTime, - }; - } else { - queryRef = refOrResult; - } + if ("ref" in refOrResult) { + queryRef = refOrResult.ref; + initialData = { + ...refOrResult.data, + ref: refOrResult.ref, + source: refOrResult.source, + fetchTime: refOrResult.fetchTime, + }; + } else { + queryRef = refOrResult; + } - return useQuery, FirebaseError>({ - ...options, - initialData, - queryKey: options?.queryKey ?? [queryRef.name, queryRef.variables || null], - queryFn: async () => { - const response = await executeQuery(queryRef); + return useQuery, FirebaseError>({ + ...options, + initialData, + queryKey: options?.queryKey ?? [queryRef.name, queryRef.variables || null], + queryFn: async () => { + const response = await executeQuery(queryRef); - return { - ...response.data, - ref: response.ref, - source: response.source, - fetchTime: response.fetchTime, - }; - }, - }); + return { + ...response.data, + ref: response.ref, + source: response.source, + fetchTime: response.fetchTime, + }; + }, + }); } diff --git a/packages/react/src/firestore/useClearIndexedDbPersistenceMutation.test.tsx b/packages/react/src/firestore/useClearIndexedDbPersistenceMutation.test.tsx index d43d87d..e546a23 100644 --- a/packages/react/src/firestore/useClearIndexedDbPersistenceMutation.test.tsx +++ b/packages/react/src/firestore/useClearIndexedDbPersistenceMutation.test.tsx @@ -1,82 +1,82 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { act, renderHook, waitFor } from "@testing-library/react"; -import React from "react"; +import type React from "react"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { firestore, wipeFirestore } from "~/testing-utils"; import { useClearIndexedDbPersistenceMutation } from "./useClearIndexedDbPersistenceMutation"; const queryClient = new QueryClient({ - defaultOptions: { - queries: { retry: false }, - mutations: { retry: false }, - }, + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, }); const wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} + {children} ); describe("useClearIndexedDbPersistenceMutation", () => { - beforeEach(async () => { - queryClient.clear(); - await wipeFirestore(); - }); - - test("should successfully clear IndexedDB persistence", async () => { - const { result } = renderHook( - () => useClearIndexedDbPersistenceMutation(firestore), - { - wrapper, - }, - ); - - await act(() => result.current.mutate()); - - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - - expect(result.current.data).toBeUndefined(); - expect(result.current.error).toBeNull(); - expect(result.current.isPending).toBe(false); - }); - - test("should respect custom options passed to the hook", async () => { - const onSuccessMock = vi.fn(); - const onErrorMock = vi.fn(); - - const { result } = renderHook( - () => - useClearIndexedDbPersistenceMutation(firestore, { - onSuccess: onSuccessMock, - onError: onErrorMock, - }), - { wrapper }, - ); - - await act(() => result.current.mutate()); - - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - expect(onSuccessMock).toHaveBeenCalled(); - expect(onErrorMock).not.toHaveBeenCalled(); - }); - - test("should correctly reset mutation state after operations", async () => { - const { result } = renderHook( - () => useClearIndexedDbPersistenceMutation(firestore), - { - wrapper, - }, - ); - - await act(() => result.current.mutate()); - - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - - act(() => result.current.reset()); - - await waitFor(() => { - expect(result.current.isIdle).toBe(true); - expect(result.current.data).toBeUndefined(); - expect(result.current.error).toBeNull(); - }); - }); + beforeEach(async () => { + queryClient.clear(); + await wipeFirestore(); + }); + + test("should successfully clear IndexedDB persistence", async () => { + const { result } = renderHook( + () => useClearIndexedDbPersistenceMutation(firestore), + { + wrapper, + }, + ); + + await act(() => result.current.mutate()); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data).toBeUndefined(); + expect(result.current.error).toBeNull(); + expect(result.current.isPending).toBe(false); + }); + + test("should respect custom options passed to the hook", async () => { + const onSuccessMock = vi.fn(); + const onErrorMock = vi.fn(); + + const { result } = renderHook( + () => + useClearIndexedDbPersistenceMutation(firestore, { + onSuccess: onSuccessMock, + onError: onErrorMock, + }), + { wrapper }, + ); + + await act(() => result.current.mutate()); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + expect(onSuccessMock).toHaveBeenCalled(); + expect(onErrorMock).not.toHaveBeenCalled(); + }); + + test("should correctly reset mutation state after operations", async () => { + const { result } = renderHook( + () => useClearIndexedDbPersistenceMutation(firestore), + { + wrapper, + }, + ); + + await act(() => result.current.mutate()); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + act(() => result.current.reset()); + + await waitFor(() => { + expect(result.current.isIdle).toBe(true); + expect(result.current.data).toBeUndefined(); + expect(result.current.error).toBeNull(); + }); + }); }); diff --git a/packages/react/src/firestore/useClearIndexedDbPersistenceMutation.ts b/packages/react/src/firestore/useClearIndexedDbPersistenceMutation.ts index 2208310..232ba81 100644 --- a/packages/react/src/firestore/useClearIndexedDbPersistenceMutation.ts +++ b/packages/react/src/firestore/useClearIndexedDbPersistenceMutation.ts @@ -1,21 +1,21 @@ import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; import { - type Firestore, - type FirestoreError, - clearIndexedDbPersistence, + type Firestore, + type FirestoreError, + clearIndexedDbPersistence, } from "firebase/firestore"; type UseFirestoreMutationOptions = Omit< - UseMutationOptions, - "mutationFn" + UseMutationOptions, + "mutationFn" >; export function useClearIndexedDbPersistenceMutation( - firestore: Firestore, - options?: UseFirestoreMutationOptions, + firestore: Firestore, + options?: UseFirestoreMutationOptions, ) { - return useMutation({ - ...options, - mutationFn: () => clearIndexedDbPersistence(firestore), - }); + return useMutation({ + ...options, + mutationFn: () => clearIndexedDbPersistence(firestore), + }); } diff --git a/packages/react/src/firestore/useCollectionQuery.test.tsx b/packages/react/src/firestore/useCollectionQuery.test.tsx index 1803f8c..7880b0d 100644 --- a/packages/react/src/firestore/useCollectionQuery.test.tsx +++ b/packages/react/src/firestore/useCollectionQuery.test.tsx @@ -6,191 +6,191 @@ import { beforeEach, describe, expect, test } from "vitest"; import { useCollectionQuery } from "./useCollectionQuery"; import { - expectFirestoreError, - firestore, - wipeFirestore, + expectFirestoreError, + firestore, + wipeFirestore, } from "~/testing-utils"; const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - }, - }, + defaultOptions: { + queries: { + retry: false, + }, + }, }); const wrapper = ({ children }: { children: ReactNode }) => ( - {children} + {children} ); describe("useCollectionQuery", () => { - beforeEach(async () => { - await wipeFirestore(); - }); - - test("fetches and returns documents from Firestore collection", async () => { - const collectionRef = collection(firestore, "tests"); - - await addDoc(collectionRef, { foo: "bar1" }); - await addDoc(collectionRef, { foo: "bar2" }); - - const { result } = renderHook( - () => - useCollectionQuery(collectionRef, { - queryKey: ["some", "collection"], - }), - { wrapper }, - ); - - // in pending state before resolving - expect(result.current.isPending).toBe(true); - expect(result.current.status).toBe("pending"); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - // It should exist and have data. - expect(result.current.data).toBeDefined(); - - const snapshot = result.current.data!; - expect(snapshot.empty).toBe(false); - expect(snapshot.size).toBe(2); - expect(snapshot.docs[0].data().foo).toMatch(/bar[12]/); - expect(snapshot.docs[1].data().foo).toMatch(/bar[12]/); - }); - - test("fetches collection from server source", async () => { - const collectionRef = collection(firestore, "tests"); - - await addDoc(collectionRef, { foo: "fromServer" }); - - const { result } = renderHook( - () => - useCollectionQuery(collectionRef, { - queryKey: ["server", "collection"], - firestore: { source: "server" }, - }), - { wrapper }, - ); - - expect(result.current.isPending).toBe(true); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - // Snapshot should exist, data should be fetched from the server and should contain the correct data - const snapshot = result.current.data; - expect(snapshot?.empty).toBe(false); - expect(snapshot?.size).toBe(1); - expect(snapshot?.docs[0].data().foo).toBe("fromServer"); - }); - - test("handles restricted collections appropriately", async () => { - const restrictedCollectionRef = collection( - firestore, - "restrictedCollection", - ); - - const { result } = renderHook( - () => - useCollectionQuery(restrictedCollectionRef, { - queryKey: ["restricted", "collection"], - }), - { wrapper }, - ); - - expect(result.current.isPending).toBe(true); - - await waitFor(() => { - expect(result.current.isError).toBe(true); - }); - - expectFirestoreError(result.current.error, "permission-denied"); - }); - - test("returns pending state initially", async () => { - const collectionRef = collection(firestore, "tests"); - - await addDoc(collectionRef, { foo: "pending" }); - - const { result } = renderHook( - () => - useCollectionQuery(collectionRef, { - queryKey: ["pending", "state"], - }), - { wrapper }, - ); - - // Initially isPending should be true - expect(result.current.isPending).toBe(true); - - // Wait for the query to finish, and should have isSuccess true - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - const snapshot = result.current.data; - expect(snapshot?.empty).toBe(false); - expect(snapshot?.size).toBe(1); - expect(snapshot?.docs[0].data().foo).toBe("pending"); - }); - - test("returns correct data type", async () => { - const collectionRef = collection(firestore, "tests"); - - await addDoc(collectionRef, { foo: "bar", num: 23 }); - - const { result } = renderHook( - () => - useCollectionQuery(collectionRef, { - queryKey: ["typed", "collection"], - }), - { wrapper }, - ); - - expect(result.current.isPending).toBe(true); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - const snapshot = result.current.data; - expect(snapshot?.empty).toBe(false); - expect(snapshot?.size).toBe(1); - const doc = snapshot?.docs[0]; - expect(doc?.data().foo).toBe("bar"); - expect(doc?.data().num).toBe(23); - }); - - test("handles complex queries", async () => { - const collectionRef = collection(firestore, "tests"); - - await addDoc(collectionRef, { category: "A", value: 1 }); - await addDoc(collectionRef, { category: "B", value: 2 }); - await addDoc(collectionRef, { category: "A", value: 3 }); - - const complexQuery = query(collectionRef, where("category", "==", "A")); - - const { result } = renderHook( - () => - useCollectionQuery(complexQuery, { - queryKey: ["complex", "query"], - }), - { wrapper }, - ); - - expect(result.current.isPending).toBe(true); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - const snapshot = result.current.data; - expect(snapshot?.size).toBe(2); - snapshot?.forEach((doc) => { - expect(doc.data().category).toBe("A"); - }); - }); + beforeEach(async () => { + await wipeFirestore(); + }); + + test("fetches and returns documents from Firestore collection", async () => { + const collectionRef = collection(firestore, "tests"); + + await addDoc(collectionRef, { foo: "bar1" }); + await addDoc(collectionRef, { foo: "bar2" }); + + const { result } = renderHook( + () => + useCollectionQuery(collectionRef, { + queryKey: ["some", "collection"], + }), + { wrapper }, + ); + + // in pending state before resolving + expect(result.current.isPending).toBe(true); + expect(result.current.status).toBe("pending"); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + // It should exist and have data. + expect(result.current.data).toBeDefined(); + + const snapshot = result.current.data!; + expect(snapshot.empty).toBe(false); + expect(snapshot.size).toBe(2); + expect(snapshot.docs[0].data().foo).toMatch(/bar[12]/); + expect(snapshot.docs[1].data().foo).toMatch(/bar[12]/); + }); + + test("fetches collection from server source", async () => { + const collectionRef = collection(firestore, "tests"); + + await addDoc(collectionRef, { foo: "fromServer" }); + + const { result } = renderHook( + () => + useCollectionQuery(collectionRef, { + queryKey: ["server", "collection"], + firestore: { source: "server" }, + }), + { wrapper }, + ); + + expect(result.current.isPending).toBe(true); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + // Snapshot should exist, data should be fetched from the server and should contain the correct data + const snapshot = result.current.data; + expect(snapshot?.empty).toBe(false); + expect(snapshot?.size).toBe(1); + expect(snapshot?.docs[0].data().foo).toBe("fromServer"); + }); + + test("handles restricted collections appropriately", async () => { + const restrictedCollectionRef = collection( + firestore, + "restrictedCollection", + ); + + const { result } = renderHook( + () => + useCollectionQuery(restrictedCollectionRef, { + queryKey: ["restricted", "collection"], + }), + { wrapper }, + ); + + expect(result.current.isPending).toBe(true); + + await waitFor(() => { + expect(result.current.isError).toBe(true); + }); + + expectFirestoreError(result.current.error, "permission-denied"); + }); + + test("returns pending state initially", async () => { + const collectionRef = collection(firestore, "tests"); + + await addDoc(collectionRef, { foo: "pending" }); + + const { result } = renderHook( + () => + useCollectionQuery(collectionRef, { + queryKey: ["pending", "state"], + }), + { wrapper }, + ); + + // Initially isPending should be true + expect(result.current.isPending).toBe(true); + + // Wait for the query to finish, and should have isSuccess true + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + const snapshot = result.current.data; + expect(snapshot?.empty).toBe(false); + expect(snapshot?.size).toBe(1); + expect(snapshot?.docs[0].data().foo).toBe("pending"); + }); + + test("returns correct data type", async () => { + const collectionRef = collection(firestore, "tests"); + + await addDoc(collectionRef, { foo: "bar", num: 23 }); + + const { result } = renderHook( + () => + useCollectionQuery(collectionRef, { + queryKey: ["typed", "collection"], + }), + { wrapper }, + ); + + expect(result.current.isPending).toBe(true); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + const snapshot = result.current.data; + expect(snapshot?.empty).toBe(false); + expect(snapshot?.size).toBe(1); + const doc = snapshot?.docs[0]; + expect(doc?.data().foo).toBe("bar"); + expect(doc?.data().num).toBe(23); + }); + + test("handles complex queries", async () => { + const collectionRef = collection(firestore, "tests"); + + await addDoc(collectionRef, { category: "A", value: 1 }); + await addDoc(collectionRef, { category: "B", value: 2 }); + await addDoc(collectionRef, { category: "A", value: 3 }); + + const complexQuery = query(collectionRef, where("category", "==", "A")); + + const { result } = renderHook( + () => + useCollectionQuery(complexQuery, { + queryKey: ["complex", "query"], + }), + { wrapper }, + ); + + expect(result.current.isPending).toBe(true); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + const snapshot = result.current.data; + expect(snapshot?.size).toBe(2); + snapshot?.forEach((doc) => { + expect(doc.data().category).toBe("A"); + }); + }); }); diff --git a/packages/react/src/firestore/useCollectionQuery.ts b/packages/react/src/firestore/useCollectionQuery.ts index 71dd849..be9854d 100644 --- a/packages/react/src/firestore/useCollectionQuery.ts +++ b/packages/react/src/firestore/useCollectionQuery.ts @@ -1,47 +1,47 @@ import { type UseQueryOptions, useQuery } from "@tanstack/react-query"; import { - type DocumentData, - type FirestoreError, - type Query, - type QuerySnapshot, - getDocs, - getDocsFromCache, - getDocsFromServer, + type DocumentData, + type FirestoreError, + type Query, + type QuerySnapshot, + getDocs, + getDocsFromCache, + getDocsFromServer, } from "firebase/firestore"; type FirestoreUseQueryOptions = Omit< - UseQueryOptions, - "queryFn" + UseQueryOptions, + "queryFn" > & { - firestore?: { - source?: "server" | "cache"; - }; + firestore?: { + source?: "server" | "cache"; + }; }; export function useCollectionQuery< - FromFirestore extends DocumentData = DocumentData, - ToFirestore extends DocumentData = DocumentData, + FromFirestore extends DocumentData = DocumentData, + ToFirestore extends DocumentData = DocumentData, >( - query: Query, - options: FirestoreUseQueryOptions< - QuerySnapshot, - FirestoreError - >, + query: Query, + options: FirestoreUseQueryOptions< + QuerySnapshot, + FirestoreError + >, ) { - const { firestore, ...queryOptions } = options; + const { firestore, ...queryOptions } = options; - return useQuery, FirestoreError>({ - ...queryOptions, - queryFn: async () => { - if (firestore?.source === "server") { - return await getDocsFromServer(query); - } + return useQuery, FirestoreError>({ + ...queryOptions, + queryFn: async () => { + if (firestore?.source === "server") { + return await getDocsFromServer(query); + } - if (firestore?.source === "cache") { - return await getDocsFromCache(query); - } + if (firestore?.source === "cache") { + return await getDocsFromCache(query); + } - return await getDocs(query); - }, - }); + return await getDocs(query); + }, + }); } diff --git a/packages/react/src/firestore/useDisableNetworkMutation.test.tsx b/packages/react/src/firestore/useDisableNetworkMutation.test.tsx index 49e6959..d402027 100644 --- a/packages/react/src/firestore/useDisableNetworkMutation.test.tsx +++ b/packages/react/src/firestore/useDisableNetworkMutation.test.tsx @@ -1,52 +1,52 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { act, renderHook, waitFor } from "@testing-library/react"; import { doc, enableNetwork, getDocFromServer } from "firebase/firestore"; -import React from "react"; +import type React from "react"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { - expectFirestoreError, - firestore, - wipeFirestore, + expectFirestoreError, + firestore, + wipeFirestore, } from "~/testing-utils"; import { useDisableNetworkMutation } from "./useDisableNetworkMutation"; const queryClient = new QueryClient({ - defaultOptions: { - queries: { retry: false }, - mutations: { retry: false }, - }, + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, }); const wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} + {children} ); describe("useDisableNetworkMutation", () => { - beforeEach(async () => { - queryClient.clear(); - await enableNetwork(firestore); - await wipeFirestore(); - }); - - test("should successfully disable the Firestore network", async () => { - const { result } = renderHook(() => useDisableNetworkMutation(firestore), { - wrapper, - }); - - await act(() => result.current.mutate()); - - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - - // Verify that network operations fail - const docRef = doc(firestore, "tests", "someDoc"); - - try { - await getDocFromServer(docRef); - throw new Error( - "Expected the network to be disabled, but Firestore operation succeeded.", - ); - } catch (error) { - expectFirestoreError(error, "unavailable"); - } - }); + beforeEach(async () => { + queryClient.clear(); + await enableNetwork(firestore); + await wipeFirestore(); + }); + + test("should successfully disable the Firestore network", async () => { + const { result } = renderHook(() => useDisableNetworkMutation(firestore), { + wrapper, + }); + + await act(() => result.current.mutate()); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + // Verify that network operations fail + const docRef = doc(firestore, "tests", "someDoc"); + + try { + await getDocFromServer(docRef); + throw new Error( + "Expected the network to be disabled, but Firestore operation succeeded.", + ); + } catch (error) { + expectFirestoreError(error, "unavailable"); + } + }); }); diff --git a/packages/react/src/firestore/useDisableNetworkMutation.ts b/packages/react/src/firestore/useDisableNetworkMutation.ts index 078d518..795766d 100644 --- a/packages/react/src/firestore/useDisableNetworkMutation.ts +++ b/packages/react/src/firestore/useDisableNetworkMutation.ts @@ -1,21 +1,21 @@ import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; import { - type Firestore, - type FirestoreError, - disableNetwork, + type Firestore, + type FirestoreError, + disableNetwork, } from "firebase/firestore"; type FirestoreUseMutationOptions = Omit< - UseMutationOptions, - "mutationFn" + UseMutationOptions, + "mutationFn" >; export function useDisableNetworkMutation( - firestore: Firestore, - options?: FirestoreUseMutationOptions, + firestore: Firestore, + options?: FirestoreUseMutationOptions, ) { - return useMutation({ - ...options, - mutationFn: () => disableNetwork(firestore), - }); + return useMutation({ + ...options, + mutationFn: () => disableNetwork(firestore), + }); } diff --git a/packages/react/src/firestore/useDocumentQuery.test.tsx b/packages/react/src/firestore/useDocumentQuery.test.tsx index db4709a..e21714b 100644 --- a/packages/react/src/firestore/useDocumentQuery.test.tsx +++ b/packages/react/src/firestore/useDocumentQuery.test.tsx @@ -6,147 +6,147 @@ import { beforeEach, describe, expect, test } from "vitest"; import { useDocumentQuery } from "./useDocumentQuery"; import { - expectFirestoreError, - firestore, - wipeFirestore, + expectFirestoreError, + firestore, + wipeFirestore, } from "~/testing-utils"; const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - }, - }, + defaultOptions: { + queries: { + retry: false, + }, + }, }); const wrapper = ({ children }: { children: ReactNode }) => ( - {children} + {children} ); describe("useDocumentQuery", () => { - beforeEach(async () => { - await wipeFirestore(); - }); - - test("it works", async () => { - const ref = doc(firestore, "tests", "useDocumentQuery"); - - // Set some data - await setDoc(ref, { foo: "bar" }); - - // Test the hook - const { result } = renderHook( - () => - useDocumentQuery(ref, { - queryKey: ["some", "doc"], - }), - { wrapper }, - ); - - // Wait for the query to finish - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - // It shoiuld exist and have data. - expect(result.current.data).toBeDefined(); - - const snapshot = result.current.data!; - expect(snapshot.exists()).toBe(true); - expect(snapshot.data()?.foo).toBe("bar"); - }); - - test("fetches document from server source", async () => { - const ref = doc(firestore, "tests", "serverSource"); - - // set data - await setDoc(ref, { foo: "fromServer" }); - - //test the hook - const { result } = renderHook( - () => - useDocumentQuery(ref, { - queryKey: ["server", "doc"], - firestore: { source: "server" }, - }), - { wrapper }, - ); - - // await the query - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - // snapshot should exist, data should be fetched from the server and should contain the correct data - const snapshot = result.current.data; - expect(snapshot?.exists()).toBe(true); - expect(snapshot?.data()?.foo).toBe("fromServer"); - }); - - test("handles restricted collections appropriately", async () => { - const ref = doc(firestore, "restrictedCollection", "someDoc"); - - const { result } = renderHook( - () => - useDocumentQuery(ref, { - queryKey: ["restricted", "doc"], - }), - { wrapper }, - ); - - await waitFor(() => { - expect(result.current.isError).toBe(true); - }); - - expectFirestoreError(result.current.error, "permission-denied"); - }); - - test("returns pending state initially", async () => { - const ref = doc(firestore, "tests", "pendingState"); - - setDoc(ref, { foo: "pending" }); - - const { result } = renderHook( - () => - useDocumentQuery(ref, { - queryKey: ["pending", "state"], - }), - { wrapper }, - ); - - // initially isPending should be true - expect(result.current.isPending).toBe(true); - - // wait for the query to finish, and should have isSuccess true - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - const snapshot = result.current.data; - expect(snapshot?.exists()).toBe(true); - expect(snapshot?.data()?.foo).toBe("pending"); - }); - - test("returns correct data type", async () => { - const ref = doc(firestore, "tests", "typedDoc"); - - setDoc(ref, { foo: "bar", num: 23 } as { foo: string; num: number }); - - const { result } = renderHook( - () => - useDocumentQuery(ref, { - queryKey: ["typed", "doc"], - }), - { wrapper }, - ); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - const snapshot = result.current.data; - expect(snapshot?.exists()).toBe(true); - expect(snapshot?.data()?.foo).toBe("bar"); - expect(snapshot?.data()?.num).toBe(23); - }); + beforeEach(async () => { + await wipeFirestore(); + }); + + test("it works", async () => { + const ref = doc(firestore, "tests", "useDocumentQuery"); + + // Set some data + await setDoc(ref, { foo: "bar" }); + + // Test the hook + const { result } = renderHook( + () => + useDocumentQuery(ref, { + queryKey: ["some", "doc"], + }), + { wrapper }, + ); + + // Wait for the query to finish + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + // It shoiuld exist and have data. + expect(result.current.data).toBeDefined(); + + const snapshot = result.current.data!; + expect(snapshot.exists()).toBe(true); + expect(snapshot.data()?.foo).toBe("bar"); + }); + + test("fetches document from server source", async () => { + const ref = doc(firestore, "tests", "serverSource"); + + // set data + await setDoc(ref, { foo: "fromServer" }); + + //test the hook + const { result } = renderHook( + () => + useDocumentQuery(ref, { + queryKey: ["server", "doc"], + firestore: { source: "server" }, + }), + { wrapper }, + ); + + // await the query + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + // snapshot should exist, data should be fetched from the server and should contain the correct data + const snapshot = result.current.data; + expect(snapshot?.exists()).toBe(true); + expect(snapshot?.data()?.foo).toBe("fromServer"); + }); + + test("handles restricted collections appropriately", async () => { + const ref = doc(firestore, "restrictedCollection", "someDoc"); + + const { result } = renderHook( + () => + useDocumentQuery(ref, { + queryKey: ["restricted", "doc"], + }), + { wrapper }, + ); + + await waitFor(() => { + expect(result.current.isError).toBe(true); + }); + + expectFirestoreError(result.current.error, "permission-denied"); + }); + + test("returns pending state initially", async () => { + const ref = doc(firestore, "tests", "pendingState"); + + setDoc(ref, { foo: "pending" }); + + const { result } = renderHook( + () => + useDocumentQuery(ref, { + queryKey: ["pending", "state"], + }), + { wrapper }, + ); + + // initially isPending should be true + expect(result.current.isPending).toBe(true); + + // wait for the query to finish, and should have isSuccess true + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + const snapshot = result.current.data; + expect(snapshot?.exists()).toBe(true); + expect(snapshot?.data()?.foo).toBe("pending"); + }); + + test("returns correct data type", async () => { + const ref = doc(firestore, "tests", "typedDoc"); + + setDoc(ref, { foo: "bar", num: 23 } as { foo: string; num: number }); + + const { result } = renderHook( + () => + useDocumentQuery(ref, { + queryKey: ["typed", "doc"], + }), + { wrapper }, + ); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + const snapshot = result.current.data; + expect(snapshot?.exists()).toBe(true); + expect(snapshot?.data()?.foo).toBe("bar"); + expect(snapshot?.data()?.num).toBe(23); + }); }); diff --git a/packages/react/src/firestore/useDocumentQuery.ts b/packages/react/src/firestore/useDocumentQuery.ts index 956d938..4206874 100644 --- a/packages/react/src/firestore/useDocumentQuery.ts +++ b/packages/react/src/firestore/useDocumentQuery.ts @@ -1,49 +1,49 @@ import { type UseQueryOptions, useQuery } from "@tanstack/react-query"; import { - type DocumentData, - type DocumentReference, - type DocumentSnapshot, - type FirestoreError, - getDoc, - getDocFromCache, - getDocFromServer, + type DocumentData, + type DocumentReference, + type DocumentSnapshot, + type FirestoreError, + getDoc, + getDocFromCache, + getDocFromServer, } from "firebase/firestore"; type FirestoreUseQueryOptions = Omit< - UseQueryOptions, - "queryFn" + UseQueryOptions, + "queryFn" > & { - firestore?: { - source?: "server" | "cache"; - }; + firestore?: { + source?: "server" | "cache"; + }; }; export function useDocumentQuery< - FromFirestore extends DocumentData = DocumentData, - ToFirestore extends DocumentData = DocumentData, + FromFirestore extends DocumentData = DocumentData, + ToFirestore extends DocumentData = DocumentData, >( - documentRef: DocumentReference, - options: FirestoreUseQueryOptions< - DocumentSnapshot, - FirestoreError - >, + documentRef: DocumentReference, + options: FirestoreUseQueryOptions< + DocumentSnapshot, + FirestoreError + >, ) { - const { firestore, ...queryOptions } = options; + const { firestore, ...queryOptions } = options; - return useQuery, FirestoreError>( - { - ...queryOptions, - queryFn: async () => { - if (firestore?.source === "server") { - return await getDocFromServer(documentRef); - } + return useQuery, FirestoreError>( + { + ...queryOptions, + queryFn: async () => { + if (firestore?.source === "server") { + return await getDocFromServer(documentRef); + } - if (firestore?.source === "cache") { - return await getDocFromCache(documentRef); - } + if (firestore?.source === "cache") { + return await getDocFromCache(documentRef); + } - return await getDoc(documentRef); - }, - }, - ); + return await getDoc(documentRef); + }, + }, + ); } diff --git a/packages/react/src/firestore/useGetAggregateFromServerQuery.test.tsx b/packages/react/src/firestore/useGetAggregateFromServerQuery.test.tsx index 36faef7..1c3c3ee 100644 --- a/packages/react/src/firestore/useGetAggregateFromServerQuery.test.tsx +++ b/packages/react/src/firestore/useGetAggregateFromServerQuery.test.tsx @@ -1,154 +1,154 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { renderHook, waitFor } from "@testing-library/react"; import { - addDoc, - average, - collection, - count, - query, - sum, - where, + addDoc, + average, + collection, + count, + query, + sum, + where, } from "firebase/firestore"; import React, { type ReactNode } from "react"; import { beforeEach, describe, expect, test } from "vitest"; import { - expectFirestoreError, - firestore, - wipeFirestore, + expectFirestoreError, + firestore, + wipeFirestore, } from "~/testing-utils"; import { useGetAggregateFromServerQuery } from "./useGetAggregateFromServerQuery"; const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - }, - }, + defaultOptions: { + queries: { + retry: false, + }, + }, }); const wrapper = ({ children }: { children: ReactNode }) => ( - {children} + {children} ); describe("useGetAggregateFromServerQuery", () => { - beforeEach(async () => await wipeFirestore()); - - test("returns correct count for empty collection", async () => { - const collectionRef = collection(firestore, "tests"); - - const { result } = renderHook( - () => - useGetAggregateFromServerQuery( - collectionRef, - { countOfDocs: count() }, - { queryKey: ["aggregate", "empty"] }, - ), - { wrapper }, - ); - - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - - expect(result.current.data?.data().countOfDocs).toBe(0); - }); - - test("returns correct aggregate values for non-empty collection", async () => { - const collectionRef = collection(firestore, "tests"); - - await addDoc(collectionRef, { value: 10 }); - await addDoc(collectionRef, { value: 20 }); - await addDoc(collectionRef, { value: 30 }); - - const { result } = renderHook( - () => - useGetAggregateFromServerQuery( - collectionRef, - { - countOfDocs: count(), - averageValue: average("value"), - totalValue: sum("value"), - }, - { queryKey: ["aggregate", "non-empty"] }, - ), - { wrapper }, - ); - - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - - expect(result.current.data?.data().averageValue).toBe(20); - expect(result.current.data?.data().totalValue).toBe(60); - expect(result.current.data?.data().countOfDocs).toBe(3); - }); - - test("handles complex queries", async () => { - const collectionRef = collection(firestore, "tests"); - - await addDoc(collectionRef, { category: "A", books: 10 }); - await addDoc(collectionRef, { category: "B", books: 20 }); - await addDoc(collectionRef, { category: "A", books: 30 }); - await addDoc(collectionRef, { category: "C", books: 40 }); - - const complexQuery = query(collectionRef, where("category", "==", "A")); - - const { result } = renderHook( - () => - useGetAggregateFromServerQuery( - complexQuery, - { - countOfDocs: count(), - averageNumberOfBooks: average("books"), - totalNumberOfBooks: sum("books"), - }, - { queryKey: ["aggregate", "complex"] }, - ), - { - wrapper, - }, - ); - - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - - expect(result.current.data?.data().averageNumberOfBooks).toBe(20); - expect(result.current.data?.data().totalNumberOfBooks).toBe(40); - expect(result.current.data?.data().countOfDocs).toBe(2); - }); - - test("handles restricted collection appropriately", async () => { - const collectionRef = collection(firestore, "restrictedCollection"); - - const { result } = renderHook( - () => - useGetAggregateFromServerQuery( - collectionRef, - { count: count() }, - { queryKey: ["aggregate", "restricted"] }, - ), - { wrapper }, - ); - - await waitFor(() => expect(result.current.isError).toBe(true)); - - expectFirestoreError(result.current.error, "permission-denied"); - }); - - test("returns pending state initially", async () => { - const collectionRef = collection(firestore, "tests"); - - await addDoc(collectionRef, { value: 10 }); - - const { result } = renderHook( - () => - useGetAggregateFromServerQuery( - collectionRef, - { count: count() }, - { queryKey: ["aggregate", "pending"] }, - ), - { wrapper }, - ); - - expect(result.current.isPending).toBe(true); - - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - - expect(result.current.data?.data().count).toBe(1); - }); + beforeEach(async () => await wipeFirestore()); + + test("returns correct count for empty collection", async () => { + const collectionRef = collection(firestore, "tests"); + + const { result } = renderHook( + () => + useGetAggregateFromServerQuery( + collectionRef, + { countOfDocs: count() }, + { queryKey: ["aggregate", "empty"] }, + ), + { wrapper }, + ); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data?.data().countOfDocs).toBe(0); + }); + + test("returns correct aggregate values for non-empty collection", async () => { + const collectionRef = collection(firestore, "tests"); + + await addDoc(collectionRef, { value: 10 }); + await addDoc(collectionRef, { value: 20 }); + await addDoc(collectionRef, { value: 30 }); + + const { result } = renderHook( + () => + useGetAggregateFromServerQuery( + collectionRef, + { + countOfDocs: count(), + averageValue: average("value"), + totalValue: sum("value"), + }, + { queryKey: ["aggregate", "non-empty"] }, + ), + { wrapper }, + ); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data?.data().averageValue).toBe(20); + expect(result.current.data?.data().totalValue).toBe(60); + expect(result.current.data?.data().countOfDocs).toBe(3); + }); + + test("handles complex queries", async () => { + const collectionRef = collection(firestore, "tests"); + + await addDoc(collectionRef, { category: "A", books: 10 }); + await addDoc(collectionRef, { category: "B", books: 20 }); + await addDoc(collectionRef, { category: "A", books: 30 }); + await addDoc(collectionRef, { category: "C", books: 40 }); + + const complexQuery = query(collectionRef, where("category", "==", "A")); + + const { result } = renderHook( + () => + useGetAggregateFromServerQuery( + complexQuery, + { + countOfDocs: count(), + averageNumberOfBooks: average("books"), + totalNumberOfBooks: sum("books"), + }, + { queryKey: ["aggregate", "complex"] }, + ), + { + wrapper, + }, + ); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data?.data().averageNumberOfBooks).toBe(20); + expect(result.current.data?.data().totalNumberOfBooks).toBe(40); + expect(result.current.data?.data().countOfDocs).toBe(2); + }); + + test("handles restricted collection appropriately", async () => { + const collectionRef = collection(firestore, "restrictedCollection"); + + const { result } = renderHook( + () => + useGetAggregateFromServerQuery( + collectionRef, + { count: count() }, + { queryKey: ["aggregate", "restricted"] }, + ), + { wrapper }, + ); + + await waitFor(() => expect(result.current.isError).toBe(true)); + + expectFirestoreError(result.current.error, "permission-denied"); + }); + + test("returns pending state initially", async () => { + const collectionRef = collection(firestore, "tests"); + + await addDoc(collectionRef, { value: 10 }); + + const { result } = renderHook( + () => + useGetAggregateFromServerQuery( + collectionRef, + { count: count() }, + { queryKey: ["aggregate", "pending"] }, + ), + { wrapper }, + ); + + expect(result.current.isPending).toBe(true); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data?.data().count).toBe(1); + }); }); diff --git a/packages/react/src/firestore/useGetAggregateFromServerQuery.ts b/packages/react/src/firestore/useGetAggregateFromServerQuery.ts index 478a10b..449a300 100644 --- a/packages/react/src/firestore/useGetAggregateFromServerQuery.ts +++ b/packages/react/src/firestore/useGetAggregateFromServerQuery.ts @@ -1,35 +1,35 @@ import { type UseQueryOptions, useQuery } from "@tanstack/react-query"; import { - type AggregateQuerySnapshot, - type AggregateSpec, - type DocumentData, - type FirestoreError, - type Query, - getAggregateFromServer, + type AggregateQuerySnapshot, + type AggregateSpec, + type DocumentData, + type FirestoreError, + type Query, + getAggregateFromServer, } from "firebase/firestore"; type FirestoreUseQueryOptions = Omit< - UseQueryOptions, - "queryFn" + UseQueryOptions, + "queryFn" >; export function useGetAggregateFromServerQuery< - T extends AggregateSpec, - AppModelType = DocumentData, - DbModelType extends DocumentData = DocumentData, + T extends AggregateSpec, + AppModelType = DocumentData, + DbModelType extends DocumentData = DocumentData, >( - query: Query, - aggregateSpec: T, - options: FirestoreUseQueryOptions< - AggregateQuerySnapshot, - FirestoreError - >, + query: Query, + aggregateSpec: T, + options: FirestoreUseQueryOptions< + AggregateQuerySnapshot, + FirestoreError + >, ) { - return useQuery< - AggregateQuerySnapshot, - FirestoreError - >({ - ...options, - queryFn: () => getAggregateFromServer(query, aggregateSpec), - }); + return useQuery< + AggregateQuerySnapshot, + FirestoreError + >({ + ...options, + queryFn: () => getAggregateFromServer(query, aggregateSpec), + }); } diff --git a/packages/react/src/firestore/useGetCountFromServerQuery.test.tsx b/packages/react/src/firestore/useGetCountFromServerQuery.test.tsx index 55ba5cd..072e031 100644 --- a/packages/react/src/firestore/useGetCountFromServerQuery.test.tsx +++ b/packages/react/src/firestore/useGetCountFromServerQuery.test.tsx @@ -4,121 +4,121 @@ import { addDoc, collection, query, where } from "firebase/firestore"; import React, { type ReactNode } from "react"; import { beforeEach, describe, expect, test } from "vitest"; import { - expectFirestoreError, - firestore, - wipeFirestore, + expectFirestoreError, + firestore, + wipeFirestore, } from "~/testing-utils"; import { useGetCountFromServerQuery } from "./useGetCountFromServerQuery"; const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - }, - }, + defaultOptions: { + queries: { + retry: false, + }, + }, }); const wrapper = ({ children }: { children: ReactNode }) => ( - {children} + {children} ); describe("useGetCountFromServerQuery", () => { - beforeEach(async () => await wipeFirestore()); + beforeEach(async () => await wipeFirestore()); - test("returns correct count for empty collection", async () => { - const collectionRef = collection(firestore, "tests"); + test("returns correct count for empty collection", async () => { + const collectionRef = collection(firestore, "tests"); - const { result } = renderHook( - () => - useGetCountFromServerQuery(collectionRef, { - queryKey: ["count", "empty"], - }), - { wrapper }, - ); + const { result } = renderHook( + () => + useGetCountFromServerQuery(collectionRef, { + queryKey: ["count", "empty"], + }), + { wrapper }, + ); - await waitFor(() => expect(result.current.isSuccess).toBe(true)); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); - expect(result.current.data?.data().count).toBe(0); - }); + expect(result.current.data?.data().count).toBe(0); + }); - test("returns correct count for non-empty collection", async () => { - const collectionRef = collection(firestore, "tests"); + test("returns correct count for non-empty collection", async () => { + const collectionRef = collection(firestore, "tests"); - await addDoc(collectionRef, { foo: "bar1" }); - await addDoc(collectionRef, { foo: "bar2" }); - await addDoc(collectionRef, { foo: "bar3" }); + await addDoc(collectionRef, { foo: "bar1" }); + await addDoc(collectionRef, { foo: "bar2" }); + await addDoc(collectionRef, { foo: "bar3" }); - const { result } = renderHook( - () => - useGetCountFromServerQuery(collectionRef, { - queryKey: ["count", "non-empty"], - }), - { wrapper }, - ); + const { result } = renderHook( + () => + useGetCountFromServerQuery(collectionRef, { + queryKey: ["count", "non-empty"], + }), + { wrapper }, + ); - await waitFor(() => expect(result.current.isSuccess).toBe(true)); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); - expect(result.current.data?.data().count).toBe(3); - }); + expect(result.current.data?.data().count).toBe(3); + }); - test("handles complex queries", async () => { - const collectionRef = collection(firestore, "tests"); + test("handles complex queries", async () => { + const collectionRef = collection(firestore, "tests"); - await addDoc(collectionRef, { category: "A", value: 1 }); - await addDoc(collectionRef, { category: "B", value: 2 }); - await addDoc(collectionRef, { category: "A", value: 3 }); - await addDoc(collectionRef, { category: "C", value: 4 }); + await addDoc(collectionRef, { category: "A", value: 1 }); + await addDoc(collectionRef, { category: "B", value: 2 }); + await addDoc(collectionRef, { category: "A", value: 3 }); + await addDoc(collectionRef, { category: "C", value: 4 }); - const complexQuery = query(collectionRef, where("category", "==", "A")); + const complexQuery = query(collectionRef, where("category", "==", "A")); - const { result } = renderHook( - () => - useGetCountFromServerQuery(complexQuery, { - queryKey: ["count", "complex"], - }), - { wrapper }, - ); + const { result } = renderHook( + () => + useGetCountFromServerQuery(complexQuery, { + queryKey: ["count", "complex"], + }), + { wrapper }, + ); - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - expect(result.current.data?.data().count).toBe(2); - }); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + expect(result.current.data?.data().count).toBe(2); + }); - test("handles restricted collections appropriately", async () => { - const collectionRef = collection(firestore, "restrictedCollection"); + test("handles restricted collections appropriately", async () => { + const collectionRef = collection(firestore, "restrictedCollection"); - const { result } = renderHook( - () => - useGetCountFromServerQuery(collectionRef, { - queryKey: ["count", "restricted"], - }), - { wrapper }, - ); + const { result } = renderHook( + () => + useGetCountFromServerQuery(collectionRef, { + queryKey: ["count", "restricted"], + }), + { wrapper }, + ); - await waitFor(() => expect(result.current.isError).toBe(true)); + await waitFor(() => expect(result.current.isError).toBe(true)); - expectFirestoreError(result.current.error, "permission-denied"); - }); + expectFirestoreError(result.current.error, "permission-denied"); + }); - test("returns pending state initially", async () => { - const collectionRef = collection(firestore, "tests"); + test("returns pending state initially", async () => { + const collectionRef = collection(firestore, "tests"); - await addDoc(collectionRef, { foo: "bar" }); + await addDoc(collectionRef, { foo: "bar" }); - const { result } = renderHook( - () => - useGetCountFromServerQuery(collectionRef, { - queryKey: ["count", "pending"], - }), - { wrapper }, - ); + const { result } = renderHook( + () => + useGetCountFromServerQuery(collectionRef, { + queryKey: ["count", "pending"], + }), + { wrapper }, + ); - // Initially isPending should be true - expect(result.current.isPending).toBe(true); + // Initially isPending should be true + expect(result.current.isPending).toBe(true); - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); - expect(result.current.data?.data().count).toBe(1); - }); + expect(result.current.data?.data().count).toBe(1); + }); }); diff --git a/packages/react/src/firestore/useGetCountFromServerQuery.ts b/packages/react/src/firestore/useGetCountFromServerQuery.ts index a95dc40..73ee67a 100644 --- a/packages/react/src/firestore/useGetCountFromServerQuery.ts +++ b/packages/react/src/firestore/useGetCountFromServerQuery.ts @@ -1,41 +1,41 @@ import { type UseQueryOptions, useQuery } from "@tanstack/react-query"; import { - type AggregateField, - type AggregateQuerySnapshot, - type DocumentData, - type FirestoreError, - type Query, - getCountFromServer, + type AggregateField, + type AggregateQuerySnapshot, + type DocumentData, + type FirestoreError, + type Query, + getCountFromServer, } from "firebase/firestore"; type FirestoreUseQueryOptions = Omit< - UseQueryOptions, - "queryFn" + UseQueryOptions, + "queryFn" >; export function useGetCountFromServerQuery< - AppModelType = DocumentData, - DbModelType extends DocumentData = DocumentData, + AppModelType = DocumentData, + DbModelType extends DocumentData = DocumentData, >( - query: Query, - options: FirestoreUseQueryOptions< - AggregateQuerySnapshot< - { count: AggregateField }, - AppModelType, - DbModelType - >, - FirestoreError - >, + query: Query, + options: FirestoreUseQueryOptions< + AggregateQuerySnapshot< + { count: AggregateField }, + AppModelType, + DbModelType + >, + FirestoreError + >, ) { - return useQuery< - AggregateQuerySnapshot< - { count: AggregateField }, - AppModelType, - DbModelType - >, - FirestoreError - >({ - ...options, - queryFn: () => getCountFromServer(query), - }); + return useQuery< + AggregateQuerySnapshot< + { count: AggregateField }, + AppModelType, + DbModelType + >, + FirestoreError + >({ + ...options, + queryFn: () => getCountFromServer(query), + }); } diff --git a/packages/react/src/firestore/useRunTransactionMutation.test.tsx b/packages/react/src/firestore/useRunTransactionMutation.test.tsx index 4a48659..71d9b1a 100644 --- a/packages/react/src/firestore/useRunTransactionMutation.test.tsx +++ b/packages/react/src/firestore/useRunTransactionMutation.test.tsx @@ -1,138 +1,138 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { act, renderHook, waitFor } from "@testing-library/react"; import { type Transaction, doc, getDoc, setDoc } from "firebase/firestore"; -import React from "react"; +import type React from "react"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { firestore, wipeFirestore } from "~/testing-utils"; import { useRunTransactionMutation } from "./useRunTransactionMutation"; const queryClient = new QueryClient({ - defaultOptions: { - queries: { retry: false }, - mutations: { retry: false }, - }, + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, }); const wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} + {children} ); describe("useRunTransactionMutation", () => { - beforeEach(async () => { - queryClient.clear(); - await wipeFirestore(); - }); + beforeEach(async () => { + queryClient.clear(); + await wipeFirestore(); + }); - test("should successfully perform a transaction and update a Firestore document", async () => { - const docRef = doc(firestore, "tests", "transactionDoc"); - await setDoc(docRef, { foo: "bar" }); + test("should successfully perform a transaction and update a Firestore document", async () => { + const docRef = doc(firestore, "tests", "transactionDoc"); + await setDoc(docRef, { foo: "bar" }); - const updateFunction = async (transaction: Transaction) => { - transaction.set(docRef, { foo: "updatedDoc" }); - }; + const updateFunction = async (transaction: Transaction) => { + transaction.set(docRef, { foo: "updatedDoc" }); + }; - const { result } = renderHook( - () => useRunTransactionMutation(firestore, updateFunction), - { wrapper }, - ); + const { result } = renderHook( + () => useRunTransactionMutation(firestore, updateFunction), + { wrapper }, + ); - await act(() => result.current.mutate()); + await act(() => result.current.mutate()); - await waitFor(() => expect(result.current.isSuccess).toBe(true)); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); - // Verify the document was actually updated - const docSnapshot = await getDoc(docRef); - expect(docSnapshot.exists()).toBe(true); - expect(docSnapshot.data()).toEqual({ foo: "updatedDoc" }); - }); + // Verify the document was actually updated + const docSnapshot = await getDoc(docRef); + expect(docSnapshot.exists()).toBe(true); + expect(docSnapshot.data()).toEqual({ foo: "updatedDoc" }); + }); - test("should perform a transaction with options and update a Firestore document", async () => { - const docRef = doc(firestore, "tests", "transactionDoc"); + test("should perform a transaction with options and update a Firestore document", async () => { + const docRef = doc(firestore, "tests", "transactionDoc"); - await setDoc(docRef, { foo: "bar" }); + await setDoc(docRef, { foo: "bar" }); - const updateFunction = async (transaction: Transaction) => { - transaction.set(docRef, { foo: "updatedWithOptions" }); - }; + const updateFunction = async (transaction: Transaction) => { + transaction.set(docRef, { foo: "updatedWithOptions" }); + }; - const { result } = renderHook( - () => - useRunTransactionMutation(firestore, updateFunction, { - firestore: { maxAttempts: 1 }, - }), - { wrapper }, - ); + const { result } = renderHook( + () => + useRunTransactionMutation(firestore, updateFunction, { + firestore: { maxAttempts: 1 }, + }), + { wrapper }, + ); - await act(() => result.current.mutate()); + await act(() => result.current.mutate()); - await waitFor(() => expect(result.current.isSuccess).toBe(true)); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); - const docSnapshot = await getDoc(docRef); - expect(docSnapshot.exists()).toBe(true); - expect(docSnapshot.data()).toEqual({ foo: "updatedWithOptions" }); - }); + const docSnapshot = await getDoc(docRef); + expect(docSnapshot.exists()).toBe(true); + expect(docSnapshot.data()).toEqual({ foo: "updatedWithOptions" }); + }); - test("should handle transaction errors correctly", async () => { - const updateFunction = async () => { - throw new Error("Transaction failed"); - }; + test("should handle transaction errors correctly", async () => { + const updateFunction = async () => { + throw new Error("Transaction failed"); + }; - const { result } = renderHook( - () => useRunTransactionMutation(firestore, updateFunction), - { wrapper }, - ); + const { result } = renderHook( + () => useRunTransactionMutation(firestore, updateFunction), + { wrapper }, + ); - await act(() => result.current.mutate()); + await act(() => result.current.mutate()); - await waitFor(() => expect(result.current.isError).toBe(true)); + await waitFor(() => expect(result.current.isError).toBe(true)); - expect(result.current.isError).toBe(true); - expect(result.current.error?.message).toBe("Transaction failed"); - }); + expect(result.current.isError).toBe(true); + expect(result.current.error?.message).toBe("Transaction failed"); + }); - test("should call onSuccess callback when transaction is successful", async () => { - const updateFunction = async (transaction: Transaction) => { - const docRef = doc(firestore, "tests", "transactionDoc"); - transaction.set(docRef, { foo: "onSuccessTest" }); - return "Success"; - }; + test("should call onSuccess callback when transaction is successful", async () => { + const updateFunction = async (transaction: Transaction) => { + const docRef = doc(firestore, "tests", "transactionDoc"); + transaction.set(docRef, { foo: "onSuccessTest" }); + return "Success"; + }; - const onSuccessMock = vi.fn(); + const onSuccessMock = vi.fn(); - const { result } = renderHook( - () => - useRunTransactionMutation(firestore, updateFunction, { - onSuccess: onSuccessMock, - }), - { wrapper }, - ); + const { result } = renderHook( + () => + useRunTransactionMutation(firestore, updateFunction, { + onSuccess: onSuccessMock, + }), + { wrapper }, + ); - await act(() => result.current.mutate()); + await act(() => result.current.mutate()); - await waitFor(() => expect(result.current.isSuccess).toBe(true)); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); - expect(onSuccessMock).toHaveBeenCalled(); - }); + expect(onSuccessMock).toHaveBeenCalled(); + }); - test("should call onError callback when transaction fails", async () => { - const updateFunction = async () => { - throw new Error("Transaction failed"); - }; + test("should call onError callback when transaction fails", async () => { + const updateFunction = async () => { + throw new Error("Transaction failed"); + }; - const onErrorMock = vi.fn(); + const onErrorMock = vi.fn(); - const { result } = renderHook( - () => - useRunTransactionMutation(firestore, updateFunction, { - onError: onErrorMock, - }), - { wrapper }, - ); + const { result } = renderHook( + () => + useRunTransactionMutation(firestore, updateFunction, { + onError: onErrorMock, + }), + { wrapper }, + ); - await act(() => result.current.mutate()); + await act(() => result.current.mutate()); - await waitFor(() => expect(result.current.isError).toBe(true)); + await waitFor(() => expect(result.current.isError).toBe(true)); - expect(onErrorMock).toHaveBeenCalled(); - }); + expect(onErrorMock).toHaveBeenCalled(); + }); }); diff --git a/packages/react/src/firestore/useRunTransactionMutation.ts b/packages/react/src/firestore/useRunTransactionMutation.ts index b4833c9..013e82e 100644 --- a/packages/react/src/firestore/useRunTransactionMutation.ts +++ b/packages/react/src/firestore/useRunTransactionMutation.ts @@ -1,31 +1,31 @@ import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; import { - type Firestore, - type FirestoreError, - type Transaction, - type TransactionOptions, - runTransaction, + type Firestore, + type FirestoreError, + type Transaction, + type TransactionOptions, + runTransaction, } from "firebase/firestore"; type RunTransactionFunction = (transaction: Transaction) => Promise; type FirestoreUseMutationOptions = Omit< - UseMutationOptions, - "mutationFn" + UseMutationOptions, + "mutationFn" > & { - firestore?: TransactionOptions; + firestore?: TransactionOptions; }; export function useRunTransactionMutation( - firestore: Firestore, - updateFunction: RunTransactionFunction, - options?: FirestoreUseMutationOptions, + firestore: Firestore, + updateFunction: RunTransactionFunction, + options?: FirestoreUseMutationOptions, ) { - const { firestore: firestoreOptions, ...queryOptions } = options ?? {}; + const { firestore: firestoreOptions, ...queryOptions } = options ?? {}; - return useMutation({ - ...queryOptions, - mutationFn: () => - runTransaction(firestore, updateFunction, firestoreOptions), - }); + return useMutation({ + ...queryOptions, + mutationFn: () => + runTransaction(firestore, updateFunction, firestoreOptions), + }); } diff --git a/packages/react/src/firestore/useWaitForPendingWritesQuery.test.tsx b/packages/react/src/firestore/useWaitForPendingWritesQuery.test.tsx index 6efef51..5c37874 100644 --- a/packages/react/src/firestore/useWaitForPendingWritesQuery.test.tsx +++ b/packages/react/src/firestore/useWaitForPendingWritesQuery.test.tsx @@ -1,46 +1,46 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { act, renderHook, waitFor } from "@testing-library/react"; import { doc, setDoc } from "firebase/firestore"; -import React from "react"; +import type React from "react"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { - expectFirestoreError, - firestore, - wipeFirestore, + expectFirestoreError, + firestore, + wipeFirestore, } from "~/testing-utils"; import { useWaitForPendingWritesQuery } from "./useWaitForPendingWritesQuery"; const queryClient = new QueryClient({ - defaultOptions: { - queries: { retry: false }, - mutations: { retry: false }, - }, + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, }); const wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} + {children} ); describe("useWaitForPendingWritesQuery", () => { - beforeEach(async () => { - queryClient.clear(); - await wipeFirestore(); - }); + beforeEach(async () => { + queryClient.clear(); + await wipeFirestore(); + }); - test("enters loading state when pending writes are in progress", async () => { - const docRef = doc(firestore, "tests", "loadingStateDoc"); + test("enters loading state when pending writes are in progress", async () => { + const docRef = doc(firestore, "tests", "loadingStateDoc"); - const { result } = renderHook( - () => - useWaitForPendingWritesQuery(firestore, { - queryKey: ["pending", "write", "loading"], - }), - { wrapper }, - ); + const { result } = renderHook( + () => + useWaitForPendingWritesQuery(firestore, { + queryKey: ["pending", "write", "loading"], + }), + { wrapper }, + ); - // Initiate a write without an await - setDoc(docRef, { value: "loading-test" }); + // Initiate a write without an await + setDoc(docRef, { value: "loading-test" }); - expect(result.current.isPending).toBe(true); - }); + expect(result.current.isPending).toBe(true); + }); }); diff --git a/packages/react/src/firestore/useWaitForPendingWritesQuery.ts b/packages/react/src/firestore/useWaitForPendingWritesQuery.ts index f7034b3..2c9b0bd 100644 --- a/packages/react/src/firestore/useWaitForPendingWritesQuery.ts +++ b/packages/react/src/firestore/useWaitForPendingWritesQuery.ts @@ -1,21 +1,21 @@ import { type UseQueryOptions, useQuery } from "@tanstack/react-query"; import { - type Firestore, - type FirestoreError, - waitForPendingWrites, + type Firestore, + type FirestoreError, + waitForPendingWrites, } from "firebase/firestore"; type FirestoreUseQueryOptions = Omit< - UseQueryOptions, - "queryFn" + UseQueryOptions, + "queryFn" >; export function useWaitForPendingWritesQuery( - firestore: Firestore, - options: FirestoreUseQueryOptions, + firestore: Firestore, + options: FirestoreUseQueryOptions, ) { - return useQuery({ - ...options, - queryFn: () => waitForPendingWrites(firestore), - }); + return useQuery({ + ...options, + queryFn: () => waitForPendingWrites(firestore), + }); } diff --git a/packages/react/src/firestore/useWriteBatchCommitMutation.test.tsx b/packages/react/src/firestore/useWriteBatchCommitMutation.test.tsx index bc994eb..9add229 100644 --- a/packages/react/src/firestore/useWriteBatchCommitMutation.test.tsx +++ b/packages/react/src/firestore/useWriteBatchCommitMutation.test.tsx @@ -1,120 +1,120 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { act, renderHook, waitFor } from "@testing-library/react"; import { doc, getDoc, setDoc, writeBatch } from "firebase/firestore"; -import React from "react"; +import type React from "react"; import { beforeEach, describe, expect, test } from "vitest"; import { firestore, wipeFirestore } from "~/testing-utils"; import { useWriteBatchCommitMutation } from "./useWriteBatchCommitMutation"; const queryClient = new QueryClient({ - defaultOptions: { - queries: { retry: false }, - mutations: { retry: false }, - }, + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, }); const wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} + {children} ); describe("useWriteBatchCommitMutation", () => { - beforeEach(async () => { - queryClient.clear(); - await wipeFirestore(); - }); - - test("successfully creates and commits a write batch", async () => { - const docRef1 = doc(firestore, "tests", "doc1"); - const docRef2 = doc(firestore, "tests", "doc2"); - - const { result } = renderHook(() => useWriteBatchCommitMutation(), { - wrapper, - }); - - await act(async () => { - const batch = writeBatch(firestore); - batch.set(docRef1, { value: "test1" }); - batch.set(docRef2, { value: "test2" }); - await result.current.mutate(batch); - }); - - const doc1Snapshot = await getDoc(docRef1); - const doc2Snapshot = await getDoc(docRef2); - - await waitFor(async () => { - expect(doc1Snapshot.exists()).toBe(true); - expect(doc2Snapshot.exists()).toBe(true); - - expect(doc1Snapshot.data()).toEqual({ value: "test1" }); - expect(doc2Snapshot.data()).toEqual({ value: "test2" }); - }); - }); - - test("handles multiple operations in a single batch", async () => { - const docRef1 = doc(firestore, "tests", "doc1"); - const docRef2 = doc(firestore, "tests", "doc2"); - const docRef3 = doc(firestore, "tests", "doc3"); - - const { result } = renderHook(() => useWriteBatchCommitMutation(), { - wrapper, - }); - - await setDoc(docRef1, { value: "initial1" }); - await setDoc(docRef2, { value: "initial2" }); - - await act(async () => { - const batch = writeBatch(firestore); - batch.update(docRef1, { value: "updated1" }); - batch.update(docRef1, { value: "updated1" }); - batch.delete(docRef2); - batch.set(docRef3, { value: "new3" }); - await result.current.mutate(batch); - }); - - const doc1Snapshot = await getDoc(docRef1); - const doc2Snapshot = await getDoc(docRef2); - const doc3Snapshot = await getDoc(docRef3); - - await waitFor(async () => { - expect(doc1Snapshot.data()).toEqual({ value: "updated1" }); - expect(doc2Snapshot.exists()).toBe(false); - expect(doc3Snapshot.data()).toEqual({ value: "new3" }); - }); - }); - - test("successfully creates and commits a write batch with nested fields", async () => { - const docRef1 = doc(firestore, "tests", "doc1"); - const docRef2 = doc(firestore, "tests", "doc2"); - - await setDoc(docRef1, { - fieldToUpdate: { nestedField: "value" }, - }); - - const { result } = renderHook(() => useWriteBatchCommitMutation(), { - wrapper, - }); - - await act(async () => { - const batch = writeBatch(firestore); - - batch.set(docRef2, { value: "test2" }); - batch.update(docRef1, { "fieldToUpdate.nestedField": "newValue" }); - await result.current.mutate(batch); - }); - - const doc1Snapshot = await getDoc(docRef1); - const doc2Snapshot = await getDoc(docRef2); - - await waitFor(async () => { - expect(doc1Snapshot.exists()).toBe(true); - expect(doc2Snapshot.exists()).toBe(true); - - expect(doc1Snapshot.data()).toEqual({ - fieldToUpdate: { nestedField: "newValue" }, - }); - expect(doc2Snapshot.data()).toEqual({ - value: "test2", - }); - }); - }); + beforeEach(async () => { + queryClient.clear(); + await wipeFirestore(); + }); + + test("successfully creates and commits a write batch", async () => { + const docRef1 = doc(firestore, "tests", "doc1"); + const docRef2 = doc(firestore, "tests", "doc2"); + + const { result } = renderHook(() => useWriteBatchCommitMutation(), { + wrapper, + }); + + await act(async () => { + const batch = writeBatch(firestore); + batch.set(docRef1, { value: "test1" }); + batch.set(docRef2, { value: "test2" }); + await result.current.mutate(batch); + }); + + const doc1Snapshot = await getDoc(docRef1); + const doc2Snapshot = await getDoc(docRef2); + + await waitFor(async () => { + expect(doc1Snapshot.exists()).toBe(true); + expect(doc2Snapshot.exists()).toBe(true); + + expect(doc1Snapshot.data()).toEqual({ value: "test1" }); + expect(doc2Snapshot.data()).toEqual({ value: "test2" }); + }); + }); + + test("handles multiple operations in a single batch", async () => { + const docRef1 = doc(firestore, "tests", "doc1"); + const docRef2 = doc(firestore, "tests", "doc2"); + const docRef3 = doc(firestore, "tests", "doc3"); + + const { result } = renderHook(() => useWriteBatchCommitMutation(), { + wrapper, + }); + + await setDoc(docRef1, { value: "initial1" }); + await setDoc(docRef2, { value: "initial2" }); + + await act(async () => { + const batch = writeBatch(firestore); + batch.update(docRef1, { value: "updated1" }); + batch.update(docRef1, { value: "updated1" }); + batch.delete(docRef2); + batch.set(docRef3, { value: "new3" }); + await result.current.mutate(batch); + }); + + const doc1Snapshot = await getDoc(docRef1); + const doc2Snapshot = await getDoc(docRef2); + const doc3Snapshot = await getDoc(docRef3); + + await waitFor(async () => { + expect(doc1Snapshot.data()).toEqual({ value: "updated1" }); + expect(doc2Snapshot.exists()).toBe(false); + expect(doc3Snapshot.data()).toEqual({ value: "new3" }); + }); + }); + + test("successfully creates and commits a write batch with nested fields", async () => { + const docRef1 = doc(firestore, "tests", "doc1"); + const docRef2 = doc(firestore, "tests", "doc2"); + + await setDoc(docRef1, { + fieldToUpdate: { nestedField: "value" }, + }); + + const { result } = renderHook(() => useWriteBatchCommitMutation(), { + wrapper, + }); + + await act(async () => { + const batch = writeBatch(firestore); + + batch.set(docRef2, { value: "test2" }); + batch.update(docRef1, { "fieldToUpdate.nestedField": "newValue" }); + await result.current.mutate(batch); + }); + + const doc1Snapshot = await getDoc(docRef1); + const doc2Snapshot = await getDoc(docRef2); + + await waitFor(async () => { + expect(doc1Snapshot.exists()).toBe(true); + expect(doc2Snapshot.exists()).toBe(true); + + expect(doc1Snapshot.data()).toEqual({ + fieldToUpdate: { nestedField: "newValue" }, + }); + expect(doc2Snapshot.data()).toEqual({ + value: "test2", + }); + }); + }); }); diff --git a/packages/react/src/firestore/useWriteBatchCommitMutation.ts b/packages/react/src/firestore/useWriteBatchCommitMutation.ts index 37a1022..50671f9 100644 --- a/packages/react/src/firestore/useWriteBatchCommitMutation.ts +++ b/packages/react/src/firestore/useWriteBatchCommitMutation.ts @@ -2,15 +2,15 @@ import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; import type { FirestoreError, WriteBatch } from "firebase/firestore"; type FirestoreUseMutationOptions = Omit< - UseMutationOptions, - "mutationFn" + UseMutationOptions, + "mutationFn" >; export function useWriteBatchCommitMutation( - options?: FirestoreUseMutationOptions, + options?: FirestoreUseMutationOptions, ) { - return useMutation({ - ...options, - mutationFn: (batch: WriteBatch) => batch.commit(), - }); + return useMutation({ + ...options, + mutationFn: (batch: WriteBatch) => batch.commit(), + }); }