Skip to content

Commit cfbbd10

Browse files
committed
Merge branch '@invertase/align-core' of https://github.com/firebase/firebaseui-web into @invertase/align-react
2 parents 0ff79ea + d42c8d9 commit cfbbd10

21 files changed

+1897
-1909
lines changed

packages/core/package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@
2727
"format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
2828
"format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"",
2929
"clean": "rimraf dist",
30-
"test:unit": "vitest run tests/unit",
31-
"test:unit:watch": "vitest tests/unit",
32-
"test:integration": "vitest run tests/integration",
33-
"test:integration:watch": "vitest tests/integration",
30+
"test:unit": "vitest run src",
31+
"test:unit:watch": "vitest tests",
32+
"test:integration": "vitest run tests",
33+
"test:integration:watch": "vitest integration",
3434
"test": "vitest run",
3535
"publish:tags": "sh -c 'TAG=\"${npm_package_name}@${npm_package_version}\"; git tag --list \"$TAG\" | grep . || git tag \"$TAG\"; git push origin \"$TAG\"'",
3636
"release": "pnpm run build && pnpm pack --pack-destination --pack-destination ../../releases/"
@@ -60,6 +60,7 @@
6060
"tsup": "catalog:",
6161
"typescript": "catalog:",
6262
"vite": "catalog:",
63+
"vitest-tsconfig-paths": "catalog:",
6364
"vitest": "catalog:"
6465
}
6566
}

packages/core/src/auth.test.ts

Lines changed: 802 additions & 0 deletions
Large diffs are not rendered by default.

packages/core/src/auth.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export async function signInWithEmailAndPassword(
6363

6464
if (hasBehavior(ui, "autoUpgradeAnonymousCredential")) {
6565
const result = await getBehavior(ui, "autoUpgradeAnonymousCredential")(ui, credential);
66-
66+
6767
if (result) {
6868
return handlePendingCredential(ui, result);
6969
}
@@ -168,6 +168,7 @@ export async function sendSignInLinkToEmail(ui: FirebaseUIConfiguration, email:
168168

169169
ui.setState("pending");
170170
await _sendSignInLinkToEmail(ui.auth, email, actionCodeSettings);
171+
// TODO: Should this be a behavior ("storageStrategy")?
171172
window.localStorage.setItem("emailForSignIn", email);
172173
} catch (error) {
173174
handleFirebaseError(ui, error);
@@ -248,7 +249,7 @@ export async function completeEmailLinkSignIn(
248249

249250
ui.setState("pending");
250251
const result = await signInWithEmailLink(ui, email, currentUrl);
251-
ui.setState("idle");
252+
ui.setState("idle"); // TODO(ehesp): Do we need this here?
252253
return handlePendingCredential(ui, result);
253254
} catch (error) {
254255
handleFirebaseError(ui, error);
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest";
2+
import { createMockUI } from "~/tests/utils";
3+
import { autoAnonymousLogin, autoUpgradeAnonymousUsers, getBehavior, hasBehavior } from "./behaviors";
4+
import { Auth, signInAnonymously, User, UserCredential, linkWithCredential, linkWithRedirect, AuthCredential, AuthProvider } from "firebase/auth";
5+
6+
vi.mock("firebase/auth", () => ({
7+
signInAnonymously: vi.fn(),
8+
linkWithCredential: vi.fn(),
9+
linkWithRedirect: vi.fn(),
10+
}));
11+
12+
describe("hasBehavior", () => {
13+
beforeEach(() => {
14+
vi.clearAllMocks();
15+
});
16+
17+
it("should return true if the behavior is enabled, but not call it", () => {
18+
const mockBehavior = vi.fn();
19+
const ui = createMockUI({
20+
behaviors: {
21+
autoAnonymousLogin: mockBehavior,
22+
},
23+
});
24+
25+
expect(hasBehavior(ui, "autoAnonymousLogin")).toBe(true);
26+
expect(mockBehavior).not.toHaveBeenCalled();
27+
});
28+
29+
it("should return false if the behavior is not enabled", () => {
30+
const ui = createMockUI();
31+
expect(hasBehavior(ui, "autoAnonymousLogin")).toBe(false);
32+
});
33+
});
34+
35+
describe("getBehavior", () => {
36+
beforeEach(() => {
37+
vi.clearAllMocks();
38+
});
39+
40+
it("should throw if the behavior is not enabled", () => {
41+
const ui = createMockUI();
42+
expect(() => getBehavior(ui, "autoAnonymousLogin")).toThrow();
43+
});
44+
45+
it("should call the behavior if it is enabled", () => {
46+
const mockBehavior = vi.fn();
47+
const ui = createMockUI({
48+
behaviors: {
49+
autoAnonymousLogin: mockBehavior,
50+
},
51+
});
52+
53+
expect(hasBehavior(ui, "autoAnonymousLogin")).toBe(true);
54+
expect(getBehavior(ui, "autoAnonymousLogin")).toBe(mockBehavior);
55+
});
56+
});
57+
58+
describe("autoAnonymousLogin", () => {
59+
beforeEach(() => {
60+
vi.clearAllMocks();
61+
});
62+
63+
it('should sign the user in anonymously if they are not signed in', async () => {
64+
const mockAuthStateReady = vi.fn().mockResolvedValue(undefined);
65+
const mockUI = createMockUI({
66+
auth: {
67+
currentUser: null,
68+
authStateReady: mockAuthStateReady,
69+
} as unknown as Auth,
70+
});
71+
72+
vi.mocked(signInAnonymously).mockResolvedValue({} as UserCredential);
73+
74+
await autoAnonymousLogin().autoAnonymousLogin(mockUI);
75+
76+
expect(mockAuthStateReady).toHaveBeenCalled();
77+
expect(signInAnonymously).toHaveBeenCalledWith(mockUI.auth);
78+
79+
expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["loading"], ["idle"]]);
80+
});
81+
82+
it('should not attempt to sign in anonymously if the user is already signed in', async () => {
83+
const mockAuthStateReady = vi.fn().mockResolvedValue(undefined);
84+
const mockUI = createMockUI({
85+
auth: {
86+
currentUser: { uid: "test-user" } as User,
87+
authStateReady: mockAuthStateReady,
88+
} as unknown as Auth,
89+
});
90+
91+
vi.mocked(signInAnonymously).mockResolvedValue({} as UserCredential);
92+
93+
await autoAnonymousLogin().autoAnonymousLogin(mockUI);
94+
95+
expect(mockAuthStateReady).toHaveBeenCalled();
96+
expect(signInAnonymously).not.toHaveBeenCalled();
97+
98+
expect(vi.mocked(mockUI.setState).mock.calls).toEqual([ ["idle"]]);
99+
});
100+
101+
it("should return noop behavior in SSR mode", async () => {
102+
// Mock window as undefined to simulate SSR
103+
const originalWindow = global.window;
104+
// @ts-ignore
105+
delete global.window;
106+
107+
const behavior = autoAnonymousLogin();
108+
const mockUI = createMockUI();
109+
110+
const result = await behavior.autoAnonymousLogin(mockUI);
111+
112+
expect(result).toEqual({ uid: "server-placeholder" });
113+
expect(signInAnonymously).not.toHaveBeenCalled();
114+
115+
// Restore window
116+
global.window = originalWindow;
117+
});
118+
});
119+
120+
describe("autoUpgradeAnonymousUsers", () => {
121+
beforeEach(() => {
122+
vi.clearAllMocks();
123+
});
124+
125+
describe("autoUpgradeAnonymousCredential", () => {
126+
it("should upgrade anonymous user with credential", async () => {
127+
const mockCredential = { providerId: "password" } as AuthCredential;
128+
const mockUserCredential = { user: { uid: "test-user" } } as UserCredential;
129+
const mockAnonymousUser = { uid: "anonymous-user", isAnonymous: true } as User;
130+
131+
const mockUI = createMockUI({
132+
auth: {
133+
currentUser: mockAnonymousUser,
134+
} as unknown as Auth,
135+
});
136+
137+
vi.mocked(linkWithCredential).mockResolvedValue(mockUserCredential);
138+
139+
const behavior = autoUpgradeAnonymousUsers();
140+
const result = await behavior.autoUpgradeAnonymousCredential(mockUI, mockCredential);
141+
142+
expect(linkWithCredential).toHaveBeenCalledWith(mockAnonymousUser, mockCredential);
143+
expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["idle"]]);
144+
expect(result).toBe(mockUserCredential);
145+
});
146+
147+
it("should return undefined if user is not anonymous", async () => {
148+
const mockCredential = { providerId: "password" } as AuthCredential;
149+
const mockRegularUser = { uid: "regular-user", isAnonymous: false } as User;
150+
151+
const mockUI = createMockUI({
152+
auth: {
153+
currentUser: mockRegularUser,
154+
} as unknown as Auth,
155+
});
156+
157+
const behavior = autoUpgradeAnonymousUsers();
158+
const result = await behavior.autoUpgradeAnonymousCredential(mockUI, mockCredential);
159+
160+
expect(linkWithCredential).not.toHaveBeenCalled();
161+
expect(vi.mocked(mockUI.setState).mock.calls).toEqual([]);
162+
expect(result).toBeUndefined();
163+
});
164+
165+
it("should return undefined if no current user", async () => {
166+
const mockCredential = { providerId: "password" } as AuthCredential;
167+
168+
const mockUI = createMockUI({
169+
auth: {
170+
currentUser: null,
171+
} as unknown as Auth,
172+
});
173+
174+
const behavior = autoUpgradeAnonymousUsers();
175+
const result = await behavior.autoUpgradeAnonymousCredential(mockUI, mockCredential);
176+
177+
expect(linkWithCredential).not.toHaveBeenCalled();
178+
expect(vi.mocked(mockUI.setState).mock.calls).toEqual([]);
179+
expect(result).toBeUndefined();
180+
});
181+
});
182+
183+
describe("autoUpgradeAnonymousProvider", () => {
184+
it("should upgrade anonymous user with provider", async () => {
185+
const mockProvider = { providerId: "google.com" } as AuthProvider;
186+
const mockAnonymousUser = { uid: "anonymous-user", isAnonymous: true } as User;
187+
188+
const mockUI = createMockUI({
189+
auth: {
190+
currentUser: mockAnonymousUser,
191+
} as unknown as Auth,
192+
});
193+
194+
vi.mocked(linkWithRedirect).mockResolvedValue(undefined as never);
195+
196+
const behavior = autoUpgradeAnonymousUsers();
197+
await behavior.autoUpgradeAnonymousProvider(mockUI, mockProvider);
198+
199+
expect(linkWithRedirect).toHaveBeenCalledWith(mockAnonymousUser, mockProvider);
200+
expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"]]);
201+
});
202+
203+
it("should return early if user is not anonymous", async () => {
204+
const mockProvider = { providerId: "google.com" } as AuthProvider;
205+
const mockRegularUser = { uid: "regular-user", isAnonymous: false } as User;
206+
207+
const mockUI = createMockUI({
208+
auth: {
209+
currentUser: mockRegularUser,
210+
} as unknown as Auth,
211+
});
212+
213+
const behavior = autoUpgradeAnonymousUsers();
214+
await behavior.autoUpgradeAnonymousProvider(mockUI, mockProvider);
215+
216+
expect(linkWithRedirect).not.toHaveBeenCalled();
217+
expect(vi.mocked(mockUI.setState).mock.calls).toEqual([]);
218+
});
219+
});
220+
});

packages/core/src/config.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { FirebaseApp } from "firebase/app";
2+
import { Auth } from "firebase/auth";
3+
import { describe, it, expect } from "vitest";
4+
import { initializeUI } from "./config";
5+
import { enUs, registerLocale } from "@firebase-ui/translations";
6+
import { autoUpgradeAnonymousUsers } from "./behaviors";
7+
8+
describe('initializeUI', () => {
9+
it('should return a valid deep store with default values', () => {
10+
const config = {
11+
app: {} as FirebaseApp,
12+
auth: {} as Auth,
13+
};
14+
15+
const ui = initializeUI(config);
16+
expect(ui).toBeDefined();
17+
expect(ui.get()).toBeDefined();
18+
expect(ui.get().app).toBe(config.app);
19+
expect(ui.get().auth).toBe(config.auth);
20+
expect(ui.get().behaviors).toEqual({});
21+
expect(ui.get().state).toEqual("idle");
22+
expect(ui.get().locale).toEqual(enUs);
23+
});
24+
25+
it('should merge behaviors', () => {
26+
const config = {
27+
app: {} as FirebaseApp,
28+
auth: {} as Auth,
29+
behaviors: [autoUpgradeAnonymousUsers()],
30+
};
31+
32+
const ui = initializeUI(config);
33+
expect(ui).toBeDefined();
34+
expect(ui.get()).toBeDefined();
35+
expect(ui.get().behaviors).toHaveProperty("autoUpgradeAnonymousCredential");
36+
expect(ui.get().behaviors).toHaveProperty("autoUpgradeAnonymousProvider");
37+
});
38+
39+
it('should set state and update state when called', () => {
40+
const config = {
41+
app: {} as FirebaseApp,
42+
auth: {} as Auth,
43+
};
44+
45+
const ui = initializeUI(config);
46+
expect(ui.get().state).toEqual("idle");
47+
ui.get().setState("loading");
48+
expect(ui.get().state).toEqual("loading");
49+
ui.get().setState("idle");
50+
expect(ui.get().state).toEqual("idle");
51+
});
52+
53+
it('should set state and update locale when called', () => {
54+
const testLocale1 = registerLocale('test1', {});
55+
const testLocale2 = registerLocale('test2', {});
56+
57+
const config = {
58+
app: {} as FirebaseApp,
59+
auth: {} as Auth,
60+
};
61+
62+
const ui = initializeUI(config);
63+
expect(ui.get().locale.locale).toEqual('en-US');
64+
ui.get().setLocale(testLocale1);
65+
expect(ui.get().locale.locale).toEqual('test1');
66+
ui.get().setLocale(testLocale2);
67+
expect(ui.get().locale.locale).toEqual('test2');
68+
});
69+
});
70+

0 commit comments

Comments
 (0)