Skip to content

Commit e7e91d5

Browse files
committed
test(react): OAuthButton tests
1 parent 9a2a289 commit e7e91d5

File tree

3 files changed

+189
-62
lines changed

3 files changed

+189
-62
lines changed

packages/react/src/auth/oauth/oauth-button.test.tsx

Lines changed: 185 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
66
* You may obtain a copy of the License at
7-
*
87
* http://www.apache.org/licenses/LICENSE-2.0
98
*
109
* Unless required by applicable law or agreed to in writing, software
@@ -14,99 +13,226 @@
1413
* limitations under the License.
1514
*/
1615

17-
import { describe, it, expect, vi, beforeEach } from "vitest";
18-
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
19-
import "@testing-library/jest-dom";
20-
import { OAuthButton } from "../../../../src/auth/oauth/oauth-button";
16+
import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
17+
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
18+
import { OAuthButton } from "./oauth-button";
19+
import { CreateFirebaseUIProvider, createMockUI } from "~/tests/utils";
20+
import { registerLocale } from "@firebase-ui/translations";
2121
import type { AuthProvider } from "firebase/auth";
22-
import { signInWithOAuth } from "@firebase-ui/core";
22+
import { ComponentProps } from "react";
23+
24+
import { signInWithProvider } from "@firebase-ui/core";
2325

24-
// Mock signInWithOAuth function
25-
vi.mock("@firebase-ui/core", async (importOriginal) => {
26-
const mod = await importOriginal<typeof import("@firebase-ui/core")>();
26+
vi.mock('@firebase-ui/core', async (importOriginal) => {
27+
const mod = await importOriginal();
2728
return {
28-
...mod,
29-
signInWithOAuth: vi.fn(),
29+
...(mod as object),
30+
signInWithProvider: vi.fn(),
31+
FirebaseUIError: class FirebaseUIError extends Error {
32+
code: string;
33+
constructor(error: any, _ui: any) {
34+
const errorCode = error?.code || "unknown";
35+
const message = errorCode === "auth/user-not-found"
36+
? "No account found with this email address"
37+
: errorCode === "auth/wrong-password"
38+
? "The password is invalid or the user does not have a password"
39+
: "An unexpected error occurred";
40+
super(message);
41+
this.name = "FirebaseUIError";
42+
this.code = errorCode;
43+
}
44+
},
3045
};
3146
});
3247

33-
// Create a mock provider that matches the AuthProvider interface
34-
const mockGoogleProvider = { providerId: "google.com" } as AuthProvider;
35-
36-
// Mock React hooks from the package
37-
const useAuthMock = vi.fn();
48+
vi.mock("~/components/button", async (importOriginal) => {
49+
const mod = await importOriginal<typeof import("~/components/button")>();
50+
return {
51+
...mod,
52+
Button: (props: ComponentProps<"button">) => (
53+
<mod.Button data-testid="oauth-button" {...props} />
54+
),
55+
}
56+
});
3857

39-
vi.mock("../../../../src/hooks", () => ({
40-
useAuth: () => useAuthMock(),
41-
useUI: () => vi.fn(),
42-
}));
58+
afterEach(() => {
59+
cleanup();
60+
});
4361

44-
// Mock the Button component
45-
vi.mock("../../../../src/components/button", () => ({
46-
Button: ({ children, onClick, disabled }: any) => (
47-
<button onClick={onClick} disabled={disabled} data-testid="oauth-button">
48-
{children}
49-
</button>
50-
),
51-
}));
62+
describe("<OAuthButton />", () => {
63+
const mockGoogleProvider = { providerId: "google.com" } as AuthProvider;
5264

53-
describe("OAuthButton Component", () => {
5465
beforeEach(() => {
5566
vi.clearAllMocks();
5667
});
5768

5869
it("renders a button with the provided children", () => {
59-
render(<OAuthButton provider={mockGoogleProvider}>Sign in with Google</OAuthButton>);
70+
const ui = createMockUI();
71+
72+
render(
73+
<CreateFirebaseUIProvider ui={ui}>
74+
<OAuthButton provider={mockGoogleProvider}>Sign in with Google</OAuthButton>
75+
</CreateFirebaseUIProvider>
76+
);
77+
78+
const button = screen.getByTestId("oauth-button");
79+
expect(button).toBeDefined();
80+
expect(button.textContent).toBe("Sign in with Google");
81+
});
82+
83+
it("applies correct CSS classes", () => {
84+
const ui = createMockUI();
85+
86+
render(
87+
<CreateFirebaseUIProvider ui={ui}>
88+
<OAuthButton provider={mockGoogleProvider}>Sign in with Google</OAuthButton>
89+
</CreateFirebaseUIProvider>
90+
);
6091

6192
const button = screen.getByTestId("oauth-button");
62-
expect(button).toBeInTheDocument();
63-
expect(button).toHaveTextContent("Sign in with Google");
93+
expect(button.className).toContain("fui-provider__button");
94+
expect(button.getAttribute("type")).toBe("button");
6495
});
6596

66-
// TODO: Fix this test
67-
it.skip("calls signInWithOAuth when clicked", async () => {
68-
// Mock the signInWithOAuth to resolve immediately
69-
vi.mocked(signInWithOAuth).mockResolvedValueOnce(undefined);
97+
it("is disabled when UI state is not idle", () => {
98+
const ui = createMockUI();
99+
ui.setKey("state", "pending");
70100

71-
render(<OAuthButton provider={mockGoogleProvider}>Sign in with Google</OAuthButton>);
101+
render(
102+
<CreateFirebaseUIProvider ui={ui}>
103+
<OAuthButton provider={mockGoogleProvider}>Sign in with Google</OAuthButton>
104+
</CreateFirebaseUIProvider>
105+
);
106+
107+
const button = screen.getByTestId("oauth-button");
108+
expect(button).toHaveAttribute("disabled");
109+
});
110+
111+
it("is enabled when UI state is idle", () => {
112+
const ui = createMockUI();
113+
114+
render(
115+
<CreateFirebaseUIProvider ui={ui}>
116+
<OAuthButton provider={mockGoogleProvider}>Sign in with Google</OAuthButton>
117+
</CreateFirebaseUIProvider>
118+
);
119+
120+
const button = screen.getByTestId("oauth-button");
121+
expect(button).not.toHaveAttribute("disabled");
122+
});
123+
124+
it("calls signInWithProvider when clicked", async () => {
125+
const mockSignInWithProvider = vi.mocked(signInWithProvider);
126+
mockSignInWithProvider.mockResolvedValue(undefined);
127+
128+
const ui = createMockUI();
129+
130+
render(
131+
<CreateFirebaseUIProvider ui={ui}>
132+
<OAuthButton provider={mockGoogleProvider}>Sign in with Google</OAuthButton>
133+
</CreateFirebaseUIProvider>
134+
);
72135

73136
const button = screen.getByTestId("oauth-button");
74137
fireEvent.click(button);
75138

76-
await waitFor(() => {
77-
expect(signInWithOAuth).toHaveBeenCalledTimes(1);
78-
expect(signInWithOAuth).toHaveBeenCalledWith(expect.anything(), mockGoogleProvider);
79-
});
139+
expect(mockSignInWithProvider).toHaveBeenCalledTimes(1);
140+
expect(mockSignInWithProvider).toHaveBeenCalledWith(expect.anything(), mockGoogleProvider);
80141
});
81142

82-
// TODO: Fix this test
83-
it.skip("displays error message when non-Firebase error occurs", async () => {
143+
it("displays FirebaseUIError message when FirebaseUIError occurs", async () => {
144+
const { FirebaseUIError } = await import("@firebase-ui/core");
145+
const mockSignInWithProvider = vi.mocked(signInWithProvider);
146+
const ui = createMockUI();
147+
const mockError = new FirebaseUIError({ code: "auth/user-not-found" }, ui.get());
148+
mockSignInWithProvider.mockRejectedValue(mockError);
149+
150+
render(
151+
<CreateFirebaseUIProvider ui={ui}>
152+
<OAuthButton provider={mockGoogleProvider}>Sign in with Google</OAuthButton>
153+
</CreateFirebaseUIProvider>
154+
);
155+
156+
const button = screen.getByTestId("oauth-button");
157+
fireEvent.click(button);
158+
159+
// Wait for error to appear
160+
await new Promise(resolve => setTimeout(resolve, 0));
161+
162+
// The error message will be the translated message for auth/user-not-found
163+
const errorMessage = screen.getByText("No account found with this email address");
164+
expect(errorMessage).toBeDefined();
165+
expect(errorMessage.className).toContain("fui-form__error");
166+
});
167+
168+
it("displays unknown error message when non-Firebase error occurs", async () => {
169+
const mockSignInWithProvider = vi.mocked(signInWithProvider);
170+
const regularError = new Error("Regular error");
171+
mockSignInWithProvider.mockRejectedValue(regularError);
172+
84173
// Mock console.error to prevent test output noise
85174
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
86175

87-
// Mock a non-Firebase error to trigger console.error
88-
const regularError = new Error("Regular error");
89-
vi.mocked(signInWithOAuth).mockRejectedValueOnce(regularError);
176+
const ui = createMockUI({
177+
locale: registerLocale("test", {
178+
errors: {
179+
unknownError: "unknownError",
180+
},
181+
}),
182+
});
90183

91-
render(<OAuthButton provider={mockGoogleProvider}>Sign in with Google</OAuthButton>);
184+
render(
185+
<CreateFirebaseUIProvider ui={ui}>
186+
<OAuthButton provider={mockGoogleProvider}>Sign in with Google</OAuthButton>
187+
</CreateFirebaseUIProvider>
188+
);
92189

93190
const button = screen.getByTestId("oauth-button");
94-
95-
// Click the button to trigger the error
96191
fireEvent.click(button);
97192

98-
// Wait for the error message to be displayed
99-
await waitFor(() => {
100-
// Verify console.error was called with the regular error
101-
expect(consoleErrorSpy).toHaveBeenCalledWith(regularError);
193+
// Wait for error to appear
194+
await new Promise(resolve => setTimeout(resolve, 0));
102195

103-
// Verify the error message is displayed
104-
const errorMessage = screen.getByText("An unknown error occurred");
105-
expect(errorMessage).toBeInTheDocument();
106-
expect(errorMessage).toHaveClass("fui-form__error");
107-
});
196+
expect(consoleErrorSpy).toHaveBeenCalledWith(regularError);
197+
198+
const errorMessage = screen.getByText("unknownError");
199+
expect(errorMessage).toBeDefined();
200+
expect(errorMessage.className).toContain("fui-form__error");
108201

109202
// Restore console.error
110203
consoleErrorSpy.mockRestore();
111204
});
112-
});
205+
206+
it("clears error when button is clicked again", async () => {
207+
const { FirebaseUIError } = await import("@firebase-ui/core");
208+
const mockSignInWithProvider = vi.mocked(signInWithProvider);
209+
const ui = createMockUI();
210+
211+
// First call fails, second call succeeds
212+
mockSignInWithProvider
213+
.mockRejectedValueOnce(new FirebaseUIError({ code: "auth/wrong-password" }, ui.get()))
214+
.mockResolvedValueOnce(undefined);
215+
216+
render(
217+
<CreateFirebaseUIProvider ui={ui}>
218+
<OAuthButton provider={mockGoogleProvider}>Sign in with Google</OAuthButton>
219+
</CreateFirebaseUIProvider>
220+
);
221+
222+
const button = screen.getByTestId("oauth-button");
223+
224+
// First click - should show error
225+
fireEvent.click(button);
226+
await new Promise(resolve => setTimeout(resolve, 0));
227+
228+
// The error message will be the translated message for auth/wrong-password
229+
const errorMessage = screen.getByText("The password is invalid or the user does not have a password");
230+
expect(errorMessage).toBeDefined();
231+
232+
// Second click - should clear error
233+
fireEvent.click(button);
234+
await new Promise(resolve => setTimeout(resolve, 0));
235+
236+
expect(screen.queryByText("The password is invalid or the user does not have a password")).toBeNull();
237+
});
238+
});

packages/react/src/auth/oauth/oauth-button.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
"use client";
1818

19-
import { FirebaseUIError, getTranslation, signInWithOAuth } from "@firebase-ui/core";
19+
import { FirebaseUIError, getTranslation, signInWithProvider } from "@firebase-ui/core";
2020
import type { AuthProvider } from "firebase/auth";
2121
import type { PropsWithChildren } from "react";
2222
import { useState } from "react";
@@ -35,7 +35,7 @@ export function OAuthButton({ provider, children }: OAuthButtonProps) {
3535
const handleOAuthSignIn = async () => {
3636
setError(null);
3737
try {
38-
await signInWithOAuth(ui, provider);
38+
await signInWithProvider(ui, provider);
3939
} catch (error) {
4040
if (error instanceof FirebaseUIError) {
4141
setError(error.message);

packages/react/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"lib": ["ES2020", "DOM", "DOM.Iterable"],
66
"module": "ESNext",
77
"skipLibCheck": true,
8+
"types": ["@testing-library/jest-dom"],
89

910
/* Bundler mode */
1011
"moduleResolution": "bundler",
@@ -28,5 +29,5 @@
2829
"@firebase-ui/styles": ["../styles/src/index.ts"]
2930
}
3031
},
31-
"include": ["src", "vite.config.ts", "tests"]
32+
"include": ["src", "vite.config.ts", "tests", "./setup-test.ts"]
3233
}

0 commit comments

Comments
 (0)