Skip to content

Commit 9d76730

Browse files
committed
test: Add integration tests for Firebase authentication flows
1 parent 016ef82 commit 9d76730

File tree

9 files changed

+965
-108
lines changed

9 files changed

+965
-108
lines changed

packages/firebaseui-react/package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@
2323
"lint": "tsc --noEmit",
2424
"format": "prettier --write \"src/**/*.ts\"",
2525
"clean": "rimraf dist",
26-
"test:unit": "vitest run tests/unit",
27-
"test:unit:watch": "vitest tests/unit",
28-
"test": "vitest run"
26+
"test:unit": "TEST_TYPE=unit vitest run tests/unit",
27+
"test:unit:watch": "TEST_TYPE=unit vitest tests/unit",
28+
"test:integration": "TEST_TYPE=integration vitest run tests/integration",
29+
"test:integration:watch": "TEST_TYPE=integration vitest tests/integration"
2930
},
3031
"peerDependencies": {
3132
"@firebase-ui/core": "workspace:*",
@@ -54,7 +55,7 @@
5455
"tsup": "^8.3.6",
5556
"typescript": "~5.6.2",
5657
"vite": "^6.0.5",
57-
"vitest": "^3.0.7",
58+
"vitest": "^3.0.8",
5859
"vitest-tsconfig-paths": "^3.4.1"
5960
}
6061
}
Lines changed: 37 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,37 @@
1-
import { vi } from "vitest";
2-
3-
// Define TranslationStrings type to match core package
4-
export type TranslationStrings = Record<string, string>;
5-
6-
// Implement FirebaseUIError with the same interface as the real implementation
7-
export class FirebaseUIError extends Error {
8-
code: string;
9-
constructor(
10-
error: any,
11-
_translations?: Partial<Record<string, Partial<TranslationStrings>>>,
12-
_language?: string
13-
) {
14-
// Extract error code from the error object
15-
const errorCode =
16-
typeof error === "string" ? error : error?.code || "unknown";
17-
18-
// For simplicity in tests, we'll use a direct message if provided as a string
19-
// or extract from translations if provided
20-
let errorMessage = `Error: ${errorCode}`;
21-
22-
if (
23-
typeof error === "string" &&
24-
arguments.length > 1 &&
25-
typeof arguments[1] === "string"
26-
) {
27-
// Handle case where first arg is code and second is message (for test convenience)
28-
errorMessage = arguments[1];
29-
}
30-
31-
super(errorMessage);
32-
this.name = "FirebaseUIError";
33-
this.code = errorCode;
34-
}
35-
}
36-
37-
// Authentication functions
38-
export const fuiSignInWithEmailAndPassword = vi.fn();
39-
export const fuiSignInWithEmailLink = vi.fn();
40-
export const fuiSignInWithPhone = vi.fn();
41-
export const fuiSignInWithOAuth = vi.fn();
42-
export const fuiResetPassword = vi.fn();
43-
export const fuiCreateUserWithEmailAndPassword = vi.fn();
44-
45-
// Country data for phone authentication
46-
export const countryData = [
47-
{ code: "US", name: "United States", dialCode: "+1", emoji: "🇺🇸" },
48-
{ code: "GB", name: "United Kingdom", dialCode: "+44", emoji: "🇬🇧" },
49-
{ code: "DE", name: "Germany", dialCode: "+49", emoji: "🇩🇪" },
50-
{ code: "FR", name: "France", dialCode: "+33", emoji: "🇫🇷" },
51-
{ code: "JP", name: "Japan", dialCode: "+81", emoji: "🇯🇵" },
52-
];
53-
54-
// Translation helpers
55-
export const getTranslation = vi.fn((section, key) => `${section}.${key}`);
56-
export const populateTranslation = vi.fn((text, data) => {
57-
if (!data) return text;
58-
let result = text;
59-
Object.entries(data).forEach(([key, value]) => {
60-
result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
61-
});
62-
return result;
63-
});
1+
/**
2+
* This is the automatic module mock for @firebase-ui/core
3+
* It re-exports the mock implementations from utils/mocks.ts to avoid duplication
4+
*/
5+
import {
6+
FirebaseUIError,
7+
TranslationStrings,
8+
mockCountryData,
9+
createCoreMocks,
10+
} from "../../utils/mocks";
11+
12+
// Create a plain object with all the mocks
13+
const coreMocks = createCoreMocks();
14+
15+
// Export types
16+
export type { TranslationStrings };
17+
18+
// Export the error class
19+
export { FirebaseUIError };
20+
21+
// Export other values
22+
export const countryData = mockCountryData;
23+
24+
// Export mock functions directly from coreMocks
25+
export const fuiSignInWithEmailAndPassword =
26+
coreMocks.fuiSignInWithEmailAndPassword;
27+
export const fuiSignInWithEmailLink = coreMocks.fuiSignInWithEmailLink;
28+
export const fuiSendSignInLinkToEmail = coreMocks.fuiSendSignInLinkToEmail;
29+
export const fuiCompleteEmailLinkSignIn = coreMocks.fuiCompleteEmailLinkSignIn;
30+
export const fuiSignInWithPhone = coreMocks.fuiSignInWithPhone;
31+
export const fuiSignInWithOAuth = coreMocks.fuiSignInWithOAuth;
32+
export const fuiResetPassword = coreMocks.fuiResetPassword;
33+
export const fuiSendPasswordResetEmail = coreMocks.fuiSendPasswordResetEmail;
34+
export const fuiCreateUserWithEmailAndPassword =
35+
coreMocks.fuiCreateUserWithEmailAndPassword;
36+
export const getTranslation = coreMocks.getTranslation;
37+
export const populateTranslation = coreMocks.populateTranslation;
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { describe, it, expect, afterAll } from "vitest";
2+
import { screen, fireEvent, waitFor, act } from "@testing-library/react";
3+
import { EmailLinkForm } from "../../../src/auth/forms/email-link-form";
4+
import { initializeApp } from "firebase/app";
5+
import { getAuth, connectAuthEmulator, deleteUser } from "firebase/auth";
6+
import { renderWithProviders } from "../../utils/mocks";
7+
import { getTranslation } from "@firebase-ui/core";
8+
9+
// Prepare the test environment
10+
const firebaseConfig = {
11+
apiKey: "demo-api-key",
12+
authDomain: "demo-firebaseui.firebaseapp.com",
13+
projectId: "demo-firebaseui",
14+
};
15+
16+
// Initialize app once for all tests
17+
const app = initializeApp(firebaseConfig);
18+
const auth = getAuth(app);
19+
20+
// Connect to the auth emulator
21+
connectAuthEmulator(auth, "http://localhost:9099", { disableWarnings: true });
22+
23+
describe("Email Link Authentication Integration", () => {
24+
const testEmail = `test-${Date.now()}@example.com`;
25+
26+
// Clean up after tests
27+
afterAll(async () => {
28+
try {
29+
const currentUser = auth.currentUser;
30+
if (currentUser) {
31+
await deleteUser(currentUser);
32+
}
33+
} catch (error) {
34+
console.error("Error cleaning up test user:", error);
35+
}
36+
});
37+
38+
it("should successfully initiate email link sign in", async () => {
39+
const { container } = renderWithProviders(<EmailLinkForm />);
40+
41+
const emailInput = container.querySelector('input[type="email"]');
42+
expect(emailInput).not.toBeNull();
43+
44+
await act(async () => {
45+
if (emailInput) {
46+
fireEvent.change(emailInput, { target: { value: testEmail } });
47+
}
48+
});
49+
50+
const submitButton = screen.getByRole("button", {
51+
name: getTranslation("labels", "sendSignInLink", { en: {} }, "en"),
52+
});
53+
54+
await act(async () => {
55+
fireEvent.click(submitButton);
56+
});
57+
58+
await waitFor(
59+
() => {
60+
// Should show success message from translations
61+
expect(
62+
screen.queryByText(
63+
getTranslation("messages", "signInLinkSent", { en: {} }, "en")
64+
)
65+
).not.toBeNull();
66+
},
67+
{ timeout: 5000 }
68+
);
69+
});
70+
71+
it("should handle invalid email format", async () => {
72+
const { container } = renderWithProviders(<EmailLinkForm />);
73+
74+
const emailInput = container.querySelector('input[type="email"]');
75+
expect(emailInput).not.toBeNull();
76+
77+
await act(async () => {
78+
if (emailInput) {
79+
fireEvent.change(emailInput, { target: { value: "invalid-email" } });
80+
// Trigger blur to show validation error
81+
fireEvent.blur(emailInput);
82+
}
83+
});
84+
85+
const submitButton = screen.getByRole("button", {
86+
name: getTranslation("labels", "sendSignInLink", { en: {} }, "en"),
87+
});
88+
89+
await act(async () => {
90+
fireEvent.click(submitButton);
91+
});
92+
93+
await waitFor(() => {
94+
expect(container.querySelector(".fui-form__error")).not.toBeNull();
95+
});
96+
});
97+
});
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
2+
import { screen, fireEvent, waitFor, act } from "@testing-library/react";
3+
import { EmailPasswordForm } from "../../../src/auth/forms/email-password-form";
4+
import { initializeApp } from "firebase/app";
5+
import {
6+
getAuth,
7+
connectAuthEmulator,
8+
signInWithEmailAndPassword,
9+
createUserWithEmailAndPassword,
10+
deleteUser,
11+
} from "firebase/auth";
12+
import { renderWithProviders } from "../../utils/mocks";
13+
14+
// Prepare the test environment
15+
const firebaseConfig = {
16+
apiKey: "test-api-key",
17+
authDomain: "test-project.firebaseapp.com",
18+
projectId: "test-project",
19+
};
20+
21+
// Initialize app once for all tests
22+
const app = initializeApp(firebaseConfig);
23+
const auth = getAuth(app);
24+
25+
// Connect to the auth emulator
26+
connectAuthEmulator(auth, "http://localhost:9099", { disableWarnings: true });
27+
28+
describe("Email Password Authentication Integration", () => {
29+
// Test user we'll create for our tests
30+
const testEmail = `test-${Date.now()}@example.com`;
31+
const testPassword = "Test123!";
32+
33+
// Set up a test user before tests
34+
beforeAll(async () => {
35+
try {
36+
await createUserWithEmailAndPassword(auth, testEmail, testPassword);
37+
} catch (error) {
38+
console.error("Error setting up test user:", error);
39+
}
40+
});
41+
42+
// Clean up after tests
43+
afterAll(async () => {
44+
try {
45+
const userCredential = await signInWithEmailAndPassword(
46+
auth,
47+
testEmail,
48+
testPassword
49+
);
50+
await deleteUser(userCredential.user);
51+
} catch (error) {
52+
console.error("Error cleaning up test user:", error);
53+
}
54+
});
55+
56+
it("should successfully sign in with email and password using actual Firebase Auth", async () => {
57+
const { container } = renderWithProviders(<EmailPasswordForm />);
58+
59+
const emailInput = container.querySelector('input[type="email"]');
60+
const passwordInput = container.querySelector('input[type="password"]');
61+
62+
expect(emailInput).not.toBeNull();
63+
expect(passwordInput).not.toBeNull();
64+
65+
await act(async () => {
66+
if (emailInput && passwordInput) {
67+
fireEvent.change(emailInput, { target: { value: testEmail } });
68+
fireEvent.blur(emailInput);
69+
fireEvent.change(passwordInput, { target: { value: testPassword } });
70+
fireEvent.blur(passwordInput);
71+
}
72+
});
73+
74+
const submitButton = await screen.findByRole("button", {
75+
name: /sign in/i,
76+
});
77+
78+
await act(async () => {
79+
fireEvent.click(submitButton);
80+
});
81+
82+
await waitFor(
83+
() => {
84+
expect(screen.queryByText(/invalid credentials/i)).toBeNull();
85+
},
86+
{ timeout: 5000 }
87+
);
88+
});
89+
90+
it("should fail when using invalid credentials", async () => {
91+
const { container } = renderWithProviders(<EmailPasswordForm />);
92+
93+
const emailInput = container.querySelector('input[type="email"]');
94+
const passwordInput = container.querySelector('input[type="password"]');
95+
96+
expect(emailInput).not.toBeNull();
97+
expect(passwordInput).not.toBeNull();
98+
99+
await act(async () => {
100+
if (emailInput && passwordInput) {
101+
fireEvent.change(emailInput, { target: { value: testEmail } });
102+
fireEvent.blur(emailInput);
103+
fireEvent.change(passwordInput, { target: { value: "wrongpassword" } });
104+
fireEvent.blur(passwordInput);
105+
}
106+
});
107+
108+
const submitButton = await screen.findByRole("button", {
109+
name: /sign in/i,
110+
});
111+
112+
await act(async () => {
113+
fireEvent.click(submitButton);
114+
});
115+
116+
await waitFor(
117+
() => {
118+
expect(container.querySelector(".fui-form__error")).not.toBeNull();
119+
},
120+
{ timeout: 5000 }
121+
);
122+
});
123+
124+
it("should show an error message for invalid credentials", async () => {
125+
const { container } = renderWithProviders(<EmailPasswordForm />);
126+
127+
const emailInput = container.querySelector('input[type="email"]');
128+
const passwordInput = container.querySelector('input[type="password"]');
129+
130+
expect(emailInput).not.toBeNull();
131+
expect(passwordInput).not.toBeNull();
132+
133+
await act(async () => {
134+
if (emailInput && passwordInput) {
135+
fireEvent.change(emailInput, { target: { value: testEmail } });
136+
fireEvent.blur(emailInput);
137+
fireEvent.change(passwordInput, { target: { value: "wrongpassword" } });
138+
fireEvent.blur(passwordInput);
139+
}
140+
});
141+
142+
const submitButton = await screen.findByRole("button", {
143+
name: /sign in/i,
144+
});
145+
146+
await act(async () => {
147+
fireEvent.click(submitButton);
148+
});
149+
150+
await waitFor(
151+
() => {
152+
expect(container.querySelector(".fui-form__error")).not.toBeNull();
153+
},
154+
{ timeout: 5000 }
155+
);
156+
});
157+
});

0 commit comments

Comments
 (0)