+vi.mock("~/auth/forms/forgot-password-auth-form", () => ({
+ ForgotPasswordAuthForm: ({ onBackToSignInClick }: { onBackToSignInClick?: () => void }) => (
+
@@ -47,37 +29,67 @@ vi.mock("~/auth/forms/forgot-password-form", () => ({
),
}));
-describe("PasswordResetScreen", () => {
- const mockOnBackToSignInClick = vi.fn();
+describe("
", () => {
+ afterEach(() => {
+ cleanup();
+ });
afterEach(() => {
vi.clearAllMocks();
});
it("renders with correct title and subtitle", () => {
- const { getByText } = render(
);
+ const ui = createMockUI({
+ locale: registerLocale("test", {
+ labels: {
+ resetPassword: "resetPassword",
+ },
+ prompts: {
+ enterEmailToReset: "enterEmailToReset",
+ },
+ }),
+ });
- expect(getByText("Reset Password")).toBeInTheDocument();
- expect(getByText("Enter your email to reset your password")).toBeInTheDocument();
- });
+ render(
+
+
+
+ );
- it("calls useUI to get the locale", () => {
- render(
);
+ const title = screen.getByText("resetPassword");
+ expect(title).toBeDefined();
+ expect(title.className).toContain("fui-card__title");
- expect(hooks.useUI).toHaveBeenCalled();
+ const subtitle = screen.getByText("enterEmailToReset");
+ expect(subtitle).toBeDefined();
+ expect(subtitle.className).toContain("fui-card__subtitle");
});
- it("includes the ForgotPasswordForm component", () => {
- const { getByTestId } = render(
);
+ it("renders the
component", () => {
+ const ui = createMockUI();
- expect(getByTestId("forgot-password-form")).toBeInTheDocument();
+ render(
+
+
+
+ );
+
+ // Mocked so only has as test id
+ expect(screen.getByTestId("forgot-password-auth-form")).toBeDefined();
});
- it("passes onBackToSignInClick to ForgotPasswordForm", () => {
- const { getByTestId } = render(
);
+ it("passes onBackToSignInClick to ForgotPasswordAuthForm", () => {
+ const mockOnBackToSignInClick = vi.fn();
+ const ui = createMockUI();
+
+ render(
+
+
+
+ );
// Click the back button in the mocked form
- fireEvent.click(getByTestId("back-button"));
+ fireEvent.click(screen.getByTestId("back-button"));
// Verify the callback was called
expect(mockOnBackToSignInClick).toHaveBeenCalledTimes(1);
diff --git a/packages/react/src/auth/screens/oauth-screen.test.tsx b/packages/react/src/auth/screens/oauth-screen.test.tsx
index 0973269b..2ddae387 100644
--- a/packages/react/src/auth/screens/oauth-screen.test.tsx
+++ b/packages/react/src/auth/screens/oauth-screen.test.tsx
@@ -4,7 +4,6 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
@@ -14,76 +13,120 @@
* limitations under the License.
*/
-import { describe, it, expect, vi } from "vitest";
-import { render } from "@testing-library/react";
+import { describe, it, expect, vi, afterEach } from "vitest";
+import { render, screen, cleanup } from "@testing-library/react";
import { OAuthScreen } from "~/auth/screens/oauth-screen";
+import { CreateFirebaseUIProvider, createMockUI } from "~/tests/utils";
+import { registerLocale } from "@firebase-ui/translations";
+
+vi.mock("~/components/policies", async (originalModule) => {
+ const module = await originalModule();
+ return {
+ ...(module as object),
+ Policies: () =>
Policies
,
+ };
+});
+
+afterEach(() => {
+ cleanup();
+});
+
+describe("
", () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
-// Mock hooks
-vi.mock("~/hooks", () => ({
- useUI: () => ({
- locale: "en-US",
- translations: {
- "en-US": {
+ it("renders with correct title and subtitle", () => {
+ const ui = createMockUI({
+ locale: registerLocale("test", {
labels: {
- signIn: "Sign In",
- signInToAccount: "Sign in to your account",
+ signIn: "signIn",
},
- },
- },
- }),
-}));
-
-// Mock getTranslation
-// vi.mock("@firebase-ui/core", () => ({
-// getTranslation: vi.fn((category, key) => {
-// if (category === "labels" && key === "signIn") return "Sign In";
-// if (category === "prompts" && key === "signInToAccount")
-// return "Sign in to your account";
-// return key;
-// }),
-// }));
-
-// Mock TermsAndPrivacy component
-vi.mock("../../../../src/components/policies", () => ({
- Policies: () =>
Policies
,
-}));
-
-describe("OAuthScreen", () => {
- it("renders with correct title and subtitle", () => {
- const { getByText } = render(
OAuth Provider);
+ prompts: {
+ signInToAccount: "signInToAccount",
+ },
+ }),
+ });
- expect(getByText("Sign In")).toBeInTheDocument();
- expect(getByText("Sign in to your account")).toBeInTheDocument();
- });
+ render(
+
+ OAuth Provider
+
+ );
- it("calls useConfig to get the language", () => {
- render(
OAuth Provider);
+ const title = screen.getByText("signIn");
+ expect(title).toBeDefined();
+ expect(title.className).toContain("fui-card__title");
- // This test implicitly tests that useConfig is called through the mock
- // If it hadn't been called, the title and subtitle wouldn't render correctly
+ const subtitle = screen.getByText("signInToAccount");
+ expect(subtitle).toBeDefined();
+ expect(subtitle.className).toContain("fui-card__subtitle");
});
it("renders children", () => {
- const { getByText } = render(
OAuth Provider);
+ const ui = createMockUI();
+
+ render(
+
+ OAuth Provider
+
+ );
- expect(getByText("OAuth Provider")).toBeInTheDocument();
+ expect(screen.getByText("OAuth Provider")).toBeDefined();
});
it("renders multiple children when provided", () => {
- const { getByText } = render(
-
- Provider 1
- Provider 2
-
+ const ui = createMockUI();
+
+ render(
+
+
+ Provider 1
+ Provider 2
+
+
);
- expect(getByText("Provider 1")).toBeInTheDocument();
- expect(getByText("Provider 2")).toBeInTheDocument();
+ expect(screen.getByText("Provider 1")).toBeDefined();
+ expect(screen.getByText("Provider 2")).toBeDefined();
});
it("includes the Policies component", () => {
- const { getByTestId } = render(
OAuth Provider);
+ const ui = createMockUI();
+
+ render(
+
+ OAuth Provider
+
+ );
- expect(getByTestId("policies")).toBeInTheDocument();
+ expect(screen.getByTestId("policies")).toBeDefined();
});
-});
+
+ it("renders children before the Policies component", () => {
+ const ui = createMockUI();
+
+ render(
+
+
+ OAuth Provider
+
+
+ );
+
+ const oauthProvider = screen.getByTestId("oauth-provider");
+ const policies = screen.getByTestId("policies");
+
+ // Both should be present
+ expect(oauthProvider).toBeDefined();
+ expect(policies).toBeDefined();
+
+ // OAuth provider should come before policies in the DOM
+ const cardContent = oauthProvider.parentElement;
+ const children = Array.from(cardContent?.children || []);
+ const oauthIndex = children.indexOf(oauthProvider);
+ const policiesIndex = children.indexOf(policies);
+
+ expect(oauthIndex).toBeLessThan(policiesIndex);
+ });
+});
\ No newline at end of file
diff --git a/packages/react/src/auth/screens/phone-auth-screen.test.tsx b/packages/react/src/auth/screens/phone-auth-screen.test.tsx
index 18f18aa4..b257ae71 100644
--- a/packages/react/src/auth/screens/phone-auth-screen.test.tsx
+++ b/packages/react/src/auth/screens/phone-auth-screen.test.tsx
@@ -4,7 +4,6 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
@@ -14,73 +13,147 @@
* limitations under the License.
*/
-import { describe, it, expect, vi } from "vitest";
-import { render } from "@testing-library/react";
+import { describe, it, expect, vi, afterEach } from "vitest";
+import { render, screen, cleanup } from "@testing-library/react";
import { PhoneAuthScreen } from "~/auth/screens/phone-auth-screen";
+import { CreateFirebaseUIProvider, createMockUI } from "~/tests/utils";
+import { registerLocale } from "@firebase-ui/translations";
+
+vi.mock("~/auth/forms/phone-auth-form", () => ({
+ PhoneAuthForm: ({ resendDelay }: { resendDelay?: number }) => (
+
+ Phone Auth Form
+
+ ),
+}));
+
+vi.mock("~/components/divider", async (originalModule) => {
+ const module = await originalModule();
+ return {
+ ...(module as object),
+ Divider: ({ children }: { children: React.ReactNode }) => (
+
{children}
+ ),
+ };
+});
+
+afterEach(() => {
+ cleanup();
+});
+
+describe("
", () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
-// Mock the hooks
-vi.mock("~/hooks", () => ({
- useUI: () => ({
- locale: "en-US",
- translations: {
- "en-US": {
+ it("renders with correct title and subtitle", () => {
+ const ui = createMockUI({
+ locale: registerLocale("test", {
labels: {
- signIn: "Sign in",
- dividerOr: "or",
+ signIn: "signIn",
},
prompts: {
- signInToAccount: "Sign in to your account",
+ signInToAccount: "signInToAccount",
},
- },
- },
- }),
-}));
+ }),
+ });
-// Mock the PhoneForm component
-vi.mock("~/auth/forms/phone-form", () => ({
- PhoneForm: ({ resendDelay }: { resendDelay?: number }) => (
-
- Phone Form
-
- ),
-}));
+ render(
+
+
+
+ );
-describe("PhoneAuthScreen", () => {
- it("displays the correct title and subtitle", () => {
- const { getByText } = render(
);
+ const title = screen.getByText("signIn");
+ expect(title).toBeDefined();
+ expect(title.className).toContain("fui-card__title");
- expect(getByText("Sign in")).toBeInTheDocument();
- expect(getByText("Sign in to your account")).toBeInTheDocument();
+ const subtitle = screen.getByText("signInToAccount");
+ expect(subtitle).toBeDefined();
+ expect(subtitle.className).toContain("fui-card__subtitle");
});
- it("calls useConfig to retrieve the language", () => {
- const { getByText } = render(
);
+ it("renders the
component", () => {
+ const ui = createMockUI();
- expect(getByText("Sign in")).toBeInTheDocument();
+ render(
+
+
+
+ );
+
+ // Mocked so only has as test id
+ expect(screen.getByTestId("phone-auth-form")).toBeDefined();
});
- it("includes the PhoneForm with the correct resendDelay prop", () => {
- const { getByTestId } = render(
);
+ it("passes resendDelay prop to PhoneAuthForm", () => {
+ const ui = createMockUI();
+
+ render(
+
+
+
+ );
- const phoneForm = getByTestId("phone-form");
- expect(phoneForm).toBeInTheDocument();
+ const phoneForm = screen.getByTestId("phone-auth-form");
+ expect(phoneForm).toBeDefined();
expect(phoneForm.getAttribute("data-resend-delay")).toBe("60");
});
- it("renders children when provided", () => {
- const { getByText, getByTestId } = render(
-
-
-
+ it("renders a divider with children when present", () => {
+ const ui = createMockUI({
+ locale: registerLocale("test", {
+ messages: {
+ dividerOr: "dividerOr",
+ },
+ }),
+ });
+
+ render(
+
+
+ Test Child
+
+
);
- expect(getByTestId("test-button")).toBeInTheDocument();
- expect(getByText("or")).toBeInTheDocument();
+ expect(screen.getByTestId("divider")).toBeDefined();
+ expect(screen.getByText("dividerOr")).toBeDefined();
+ expect(screen.getByTestId("test-child")).toBeDefined();
});
- it("does not render children or divider when not provided", () => {
- const { queryByText } = render(
);
+ it("does not render divider and children when no children are provided", () => {
+ const ui = createMockUI();
+
+ render(
+
+
+
+ );
- expect(queryByText("or")).not.toBeInTheDocument();
+ expect(screen.queryByTestId("divider")).toBeNull();
});
-});
+
+ it("renders multiple children when provided", () => {
+ const ui = createMockUI({
+ locale: registerLocale("test", {
+ messages: {
+ dividerOr: "dividerOr",
+ },
+ }),
+ });
+
+ render(
+
+
+ Child 1
+ Child 2
+
+
+ );
+
+ expect(screen.getByTestId("divider")).toBeDefined();
+ expect(screen.getByTestId("child-1")).toBeDefined();
+ expect(screen.getByTestId("child-2")).toBeDefined();
+ });
+});
\ No newline at end of file
diff --git a/packages/react/src/auth/screens/sign-in-auth-screen.test.tsx b/packages/react/src/auth/screens/sign-in-auth-screen.test.tsx
index 47a486ab..2ab83a2c 100644
--- a/packages/react/src/auth/screens/sign-in-auth-screen.test.tsx
+++ b/packages/react/src/auth/screens/sign-in-auth-screen.test.tsx
@@ -4,7 +4,6 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
@@ -14,38 +13,21 @@
* limitations under the License.
*/
-import { describe, it, expect, vi } from "vitest";
-import { render, fireEvent } from "@testing-library/react";
+import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
+import { render, screen, fireEvent, cleanup } from "@testing-library/react";
import { SignInAuthScreen } from "~/auth/screens/sign-in-auth-screen";
+import { CreateFirebaseUIProvider, createMockUI } from "~/tests/utils";
+import { registerLocale } from "@firebase-ui/translations";
-// Mock the hooks
-vi.mock("~/hooks", () => ({
- useUI: () => ({
- locale: "en-US",
- translations: {
- "en-US": {
- labels: {
- signIn: "Sign in",
- dividerOr: "or",
- },
- prompts: {
- signInToAccount: "Sign in to your account",
- },
- },
- },
- }),
-}));
-
-// Mock the EmailPasswordForm component
-vi.mock("~/auth/forms/email-password-form", () => ({
- EmailPasswordForm: ({
+vi.mock("~/auth/forms/sign-in-auth-form", () => ({
+ SignInAuthForm: ({
onForgotPasswordClick,
onRegisterClick,
}: {
onForgotPasswordClick?: () => void;
onRegisterClick?: () => void;
}) => (
-
+
@@ -56,60 +38,151 @@ vi.mock("~/auth/forms/email-password-form", () => ({
),
}));
-describe("SignInAuthScreen", () => {
- it("displays the correct title and subtitle", () => {
- const { getByText } = render(
);
+vi.mock("~/components/divider", async (originalModule) => {
+ const module = await originalModule();
+ return {
+ ...(module as object),
+ Divider: ({ children }: { children: React.ReactNode }) => (
+
{children}
+ ),
+ };
+});
+
+describe("
", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
- expect(getByText("Sign in")).toBeInTheDocument();
- expect(getByText("Sign in to your account")).toBeInTheDocument();
+ afterEach(() => {
+ cleanup();
});
- it("calls useConfig to retrieve the language", () => {
- const { getByText } = render(
);
+ it("renders with correct title and subtitle", () => {
+ const ui = createMockUI({
+ locale: registerLocale("test", {
+ labels: {
+ signIn: "signIn",
+ },
+ prompts: {
+ signInToAccount: "signInToAccount",
+ },
+ }),
+ });
+
+ render(
+
+
+
+ );
+
+ const title = screen.getByText("signIn");
+ expect(title).toBeDefined();
+ expect(title.className).toContain("fui-card__title");
- expect(getByText("Sign in")).toBeInTheDocument();
+ const subtitle = screen.getByText("signInToAccount");
+ expect(subtitle).toBeDefined();
+ expect(subtitle.className).toContain("fui-card__subtitle");
});
- it("includes the EmailPasswordForm component", () => {
- const { getByTestId } = render(
);
+ it("renders the
component", () => {
+ const ui = createMockUI();
+
+ render(
+
+
+
+ );
- expect(getByTestId("email-password-form")).toBeInTheDocument();
+ // Mocked so only has as test id
+ expect(screen.getByTestId("sign-in-auth-form")).toBeDefined();
});
- it("passes onForgotPasswordClick to EmailPasswordForm", () => {
+ it("passes onForgotPasswordClick to SignInAuthForm", () => {
const mockOnForgotPasswordClick = vi.fn();
- const { getByTestId } = render(
);
+ const ui = createMockUI();
+
+ render(
+
+
+
+ );
- const forgotPasswordButton = getByTestId("forgot-password-button");
+ const forgotPasswordButton = screen.getByTestId("forgot-password-button");
fireEvent.click(forgotPasswordButton);
expect(mockOnForgotPasswordClick).toHaveBeenCalledTimes(1);
});
- it("passes onRegisterClick to EmailPasswordForm", () => {
+ it("passes onRegisterClick to SignInAuthForm", () => {
const mockOnRegisterClick = vi.fn();
- const { getByTestId } = render(
);
+ const ui = createMockUI();
+
+ render(
+
+
+
+ );
- const registerButton = getByTestId("register-button");
+ const registerButton = screen.getByTestId("register-button");
fireEvent.click(registerButton);
expect(mockOnRegisterClick).toHaveBeenCalledTimes(1);
});
- it("renders children when provided", () => {
- const { getByText, getByTestId } = render(
-
-
-
+ it("renders a divider with children when present", () => {
+ const ui = createMockUI({
+ locale: registerLocale("test", {
+ messages: {
+ dividerOr: "dividerOr",
+ },
+ }),
+ });
+
+ render(
+
+
+ Test Child
+
+
+ );
+
+ expect(screen.getByTestId("divider")).toBeDefined();
+ expect(screen.getByText("dividerOr")).toBeDefined();
+ expect(screen.getByTestId("test-child")).toBeDefined();
+ });
+
+ it("does not render divider and children when no children are provided", () => {
+ const ui = createMockUI();
+
+ render(
+
+
+
);
- expect(getByTestId("test-button")).toBeInTheDocument();
- expect(getByText("or")).toBeInTheDocument();
+ expect(screen.queryByTestId("divider")).toBeNull();
});
- it("does not render children or divider when not provided", () => {
- const { queryByText } = render(
);
+ it("renders multiple children when provided", () => {
+ const ui = createMockUI({
+ locale: registerLocale("test", {
+ messages: {
+ dividerOr: "dividerOr",
+ },
+ }),
+ });
+
+ render(
+
+
+ Child 1
+ Child 2
+
+
+ );
- expect(queryByText("or")).not.toBeInTheDocument();
+ expect(screen.getByTestId("divider")).toBeDefined();
+ expect(screen.getByTestId("child-1")).toBeDefined();
+ expect(screen.getByTestId("child-2")).toBeDefined();
});
-});
+});
\ No newline at end of file
diff --git a/packages/react/src/auth/screens/sign-up-auth-screen.test.tsx b/packages/react/src/auth/screens/sign-up-auth-screen.test.tsx
index e7a4b2e6..9fcb4679 100644
--- a/packages/react/src/auth/screens/sign-up-auth-screen.test.tsx
+++ b/packages/react/src/auth/screens/sign-up-auth-screen.test.tsx
@@ -4,7 +4,6 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
@@ -14,43 +13,15 @@
* limitations under the License.
*/
-import { describe, expect, it, vi } from "vitest";
-import { render, screen } from "@testing-library/react";
+import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
+import { render, screen, fireEvent, cleanup } from "@testing-library/react";
import { SignUpAuthScreen } from "~/auth/screens/sign-up-auth-screen";
+import { CreateFirebaseUIProvider, createMockUI } from "~/tests/utils";
+import { registerLocale } from "@firebase-ui/translations";
-// Mock hooks
-vi.mock("~/hooks", () => ({
- useUI: () => ({
- locale: "en-US",
- translations: {
- "en-US": {
- labels: {
- register: "Create Account",
- dividerOr: "OR",
- },
- prompts: {
- enterDetailsToCreate: "Enter your details to create an account",
- },
- },
- },
- }),
-}));
-
-// Mock translations
-// vi.mock("@firebase-ui/core", () => ({
-// getTranslation: vi.fn((category, key) => {
-// if (category === "labels" && key === "register") return "Create Account";
-// if (category === "prompts" && key === "enterDetailsToCreate")
-// return "Enter your details to create an account";
-// if (category === "messages" && key === "dividerOr") return "OR";
-// return `${category}.${key}`;
-// }),
-// }));
-
-// Mock RegisterForm component
-vi.mock("~/auth/forms/register-form", () => ({
- RegisterForm: ({ onBackToSignInClick }: { onBackToSignInClick?: () => void }) => (
-
+vi.mock("~/auth/forms/sign-up-auth-form", () => ({
+ SignUpAuthForm: ({ onBackToSignInClick }: { onBackToSignInClick?: () => void }) => (
+
@@ -58,44 +29,135 @@ vi.mock("~/auth/forms/register-form", () => ({
),
}));
-describe("SignUpAuthScreen", () => {
- it("renders the correct title and subtitle", () => {
- render(
);
+vi.mock("~/components/divider", async (originalModule) => {
+ const module = await originalModule();
+ return {
+ ...(module as object),
+ Divider: ({ children }: { children: React.ReactNode }) => (
+
{children}
+ ),
+ };
+});
+
+describe("
", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ afterEach(() => {
+ cleanup();
+ });
+
+ it("renders with correct title and subtitle", () => {
+ const ui = createMockUI({
+ locale: registerLocale("test", {
+ labels: {
+ register: "register",
+ },
+ prompts: {
+ enterDetailsToCreate: "enterDetailsToCreate",
+ },
+ }),
+ });
+
+ render(
+
+
+
+ );
+
+ const title = screen.getByText("register");
+ expect(title).toBeDefined();
+ expect(title.className).toContain("fui-card__title");
- expect(screen.getByText("Create Account")).toBeInTheDocument();
- expect(screen.getByText("Enter your details to create an account")).toBeInTheDocument();
+ const subtitle = screen.getByText("enterDetailsToCreate");
+ expect(subtitle).toBeDefined();
+ expect(subtitle.className).toContain("fui-card__subtitle");
});
- it("includes the RegisterForm component", () => {
- render(
);
+ it("renders the
component", () => {
+ const ui = createMockUI();
- expect(screen.getByTestId("register-form")).toBeInTheDocument();
+ render(
+
+
+
+ );
+
+ // Mocked so only has as test id
+ expect(screen.getByTestId("sign-up-auth-form")).toBeDefined();
});
- it("passes the onBackToSignInClick prop to the RegisterForm", async () => {
- const onBackToSignInClick = vi.fn();
- render(
);
+ it("passes onBackToSignInClick to SignUpAuthForm", () => {
+ const mockOnBackToSignInClick = vi.fn();
+ const ui = createMockUI();
+
+ render(
+
+
+
+ );
const backButton = screen.getByTestId("back-to-sign-in-button");
- backButton.click();
+ fireEvent.click(backButton);
+
+ expect(mockOnBackToSignInClick).toHaveBeenCalledTimes(1);
+ });
+
+ it("renders a divider with children when present", () => {
+ const ui = createMockUI({
+ locale: registerLocale("test", {
+ messages: {
+ dividerOr: "dividerOr",
+ },
+ }),
+ });
- expect(onBackToSignInClick).toHaveBeenCalled();
+ render(
+
+
+ Test Child
+
+
+ );
+
+ expect(screen.getByTestId("divider")).toBeDefined();
+ expect(screen.getByText("dividerOr")).toBeDefined();
+ expect(screen.getByTestId("test-child")).toBeDefined();
});
- it("renders children when provided", () => {
+ it("does not render divider and children when no children are provided", () => {
+ const ui = createMockUI();
+
render(
-
- Child element
-
+
+
+
);
- expect(screen.getByTestId("test-child")).toBeInTheDocument();
- expect(screen.getByText("or")).toBeInTheDocument();
+ expect(screen.queryByTestId("divider")).toBeNull();
});
- it("does not render divider or children container when no children are provided", () => {
- render(
);
+ it("renders multiple children when provided", () => {
+ const ui = createMockUI({
+ locale: registerLocale("test", {
+ messages: {
+ dividerOr: "dividerOr",
+ },
+ }),
+ });
- expect(screen.queryByText("or")).not.toBeInTheDocument();
+ render(
+
+
+ Child 1
+ Child 2
+
+
+ );
+
+ expect(screen.getByTestId("divider")).toBeDefined();
+ expect(screen.getByTestId("child-1")).toBeDefined();
+ expect(screen.getByTestId("child-2")).toBeDefined();
});
-});
+});
\ No newline at end of file
diff --git a/packages/react/src/components/button.test.tsx b/packages/react/src/components/button.test.tsx
index 24a0db56..4ffafad2 100644
--- a/packages/react/src/components/button.test.tsx
+++ b/packages/react/src/components/button.test.tsx
@@ -14,16 +14,19 @@
* limitations under the License.
*/
-import { describe, it, expect, vi } from "vitest";
-import { render, screen, fireEvent } from "@testing-library/react";
-import "@testing-library/jest-dom";
+import { describe, it, expect, vi, afterEach } from "vitest";
+import { render, screen, fireEvent, cleanup } from "@testing-library/react";
import { Button } from "./button";
-describe("Button Component", () => {
+afterEach(() => {
+ cleanup();
+});
+
+describe("
", () => {
it("renders with default variant (primary)", () => {
render(
);
const button = screen.getByRole("button", { name: /click me/i });
- expect(button).toBeInTheDocument();
+ expect(button).toBeDefined();
expect(button).toHaveClass("fui-button");
expect(button).not.toHaveClass("fui-button--secondary");
});
@@ -60,7 +63,7 @@ describe("Button Component", () => {
);
const button = screen.getByTestId("test-button");
- expect(button).toBeDisabled();
+ expect(button).toHaveAttribute("disabled");
});
it("renders as a Slot component when asChild is true", () => {
@@ -71,7 +74,7 @@ describe("Button Component", () => {
);
const link = screen.getByRole("link", { name: /link button/i });
- expect(link).toBeInTheDocument();
+ expect(link).toBeDefined();
expect(link).toHaveClass("fui-button");
expect(link.tagName).toBe("A");
expect(link).toHaveAttribute("href", "/test");
diff --git a/packages/react/src/components/card.test.tsx b/packages/react/src/components/card.test.tsx
index 5053e278..44f00574 100644
--- a/packages/react/src/components/card.test.tsx
+++ b/packages/react/src/components/card.test.tsx
@@ -14,55 +14,89 @@
* limitations under the License.
*/
-import { describe, it, expect } from "vitest";
-import { render, screen } from "@testing-library/react";
-import "@testing-library/jest-dom";
-import { Card, CardHeader, CardTitle, CardSubtitle } from "./card";
+import { describe, it, expect, afterEach } from "vitest";
+import { render, screen, cleanup } from "@testing-library/react";
+import { Card, CardHeader, CardTitle, CardSubtitle, CardContent } from "./card";
-describe("Card Components", () => {
- describe("Card", () => {
- it("renders a card with children", () => {
- render(
Card content);
- const card = screen.getByTestId("test-card");
+afterEach(() => {
+ cleanup();
+});
- expect(card).toHaveClass("fui-card");
- expect(card).toHaveTextContent("Card content");
- });
+describe("
", () => {
+ it("renders a card with children", () => {
+ render(
Card content);
+ const card = screen.getByTestId("test-card");
- it("applies custom className", () => {
- render(
-
- Card content
-
- );
- const card = screen.getByTestId("test-card");
+ expect(card).toHaveClass("fui-card");
+ expect(card).toHaveTextContent("Card content");
+ });
- expect(card).toHaveClass("fui-card");
- expect(card).toHaveClass("custom-class");
- });
+ it("applies custom className", () => {
+ render(
+
+ Card content
+
+ );
+ const card = screen.getByTestId("test-card");
- it("passes other props to the div element", () => {
- render(
-
- Card content
-
- );
- const card = screen.getByTestId("test-card");
+ expect(card).toHaveClass("fui-card");
+ expect(card).toHaveClass("custom-class");
+ });
- expect(card).toHaveClass("fui-card");
- expect(card).toHaveAttribute("aria-label", "card");
- });
+ it("passes other props to the div element", () => {
+ render(
+
+ Card content
+
+ );
+ const card = screen.getByTestId("test-card");
+
+ expect(card).toHaveClass("fui-card");
+ expect(card).toHaveAttribute("aria-label", "card");
});
- describe("CardHeader", () => {
+ it("renders a complete card with all subcomponents", () => {
+ render(
+
+
+ Card Title
+ Card Subtitle
+
+
+ Card Body Content
+
+
+ );
+
+ const card = screen.getByTestId("complete-card");
+ const header = screen.getByTestId("complete-header");
+ const title = screen.getByRole("heading", { name: "Card Title" });
+ const subtitle = screen.getByText("Card Subtitle");
+ const content = screen.getByText("Card Body Content");
+
+ expect(card).toHaveClass("fui-card");
+ expect(title).toHaveClass("fui-card__title");
+ expect(subtitle).toHaveClass("fui-card__subtitle");
+ expect(header).toHaveClass("fui-card__header");
+ expect(content).toBeInTheDocument();
+
+ // Check structure
+ expect(header).toContainElement(title);
+ expect(header).toContainElement(subtitle);
+ expect(card).toContainElement(header);
+ expect(card).toContainElement(content);
+ });
+
+
+ describe("
", () => {
it("renders a card header with children", () => {
render(
Header content);
const header = screen.getByTestId("test-header");
-
+
expect(header).toHaveClass("fui-card__header");
expect(header).toHaveTextContent("Header content");
});
-
+
it("applies custom className", () => {
render(
@@ -70,75 +104,63 @@ describe("Card Components", () => {
);
const header = screen.getByTestId("test-header");
-
+
expect(header).toHaveClass("fui-card__header");
expect(header).toHaveClass("custom-header");
});
});
-
- describe("CardTitle", () => {
+
+ describe("
", () => {
it("renders a card title with children", () => {
render(
Title content);
const title = screen.getByRole("heading", { name: "Title content" });
-
- expect(title).toHaveClass("fui-card__title");
+
+ expect(title.className).toContain("fui-card__title");
expect(title.tagName).toBe("H2");
});
-
+
it("applies custom className", () => {
render(
Title content);
const title = screen.getByRole("heading", { name: "Title content" });
-
+
expect(title).toHaveClass("fui-card__title");
expect(title).toHaveClass("custom-title");
});
});
-
- describe("CardSubtitle", () => {
+
+ describe("
", () => {
it("renders a card subtitle with children", () => {
render(
Subtitle content);
const subtitle = screen.getByText("Subtitle content");
-
+
expect(subtitle).toHaveClass("fui-card__subtitle");
expect(subtitle.tagName).toBe("P");
});
-
+
it("applies custom className", () => {
render(
Subtitle content);
const subtitle = screen.getByText("Subtitle content");
-
+
expect(subtitle).toHaveClass("fui-card__subtitle");
expect(subtitle).toHaveClass("custom-subtitle");
});
});
-
- it("renders a complete card with all subcomponents", () => {
- render(
-
-
- Card Title
- Card Subtitle
-
- Card Body Content
-
- );
-
- const card = screen.getByTestId("complete-card");
- const header = screen.getByTestId("complete-header");
- const title = screen.getByRole("heading", { name: "Card Title" });
- const subtitle = screen.getByText("Card Subtitle");
- const content = screen.getByText("Card Body Content");
-
- expect(card).toHaveClass("fui-card");
- expect(title).toHaveClass("fui-card__title");
- expect(subtitle).toHaveClass("fui-card__subtitle");
- expect(header).toHaveClass("fui-card__header");
- expect(content).toBeInTheDocument();
-
- // Check structure
- expect(header).toContainElement(title);
- expect(header).toContainElement(subtitle);
- expect(card).toContainElement(header);
- expect(card).toContainElement(content);
+
+ describe("
", () => {
+ it("renders a card content with children", () => {
+ render(
Content content);
+ const content = screen.getByText("Content content");
+
+ expect(content).toHaveClass("fui-card__content");
+ expect(content.tagName).toBe("DIV");
+ });
+
+ it("applies custom className", () => {
+ render(
Content);
+ const content = screen.getByText("Content");
+
+ expect(content).toHaveClass("fui-card__content");
+ expect(content).toHaveClass("custom-content");
+ });
});
});
diff --git a/packages/react/src/components/card.tsx b/packages/react/src/components/card.tsx
index 981abfae..c382af90 100644
--- a/packages/react/src/components/card.tsx
+++ b/packages/react/src/components/card.tsx
@@ -53,7 +53,7 @@ export function CardSubtitle({ children, className, ...props }: ComponentProps<"
export function CardContent({ children, className, ...props }: ComponentProps<"div">) {
return (
-
+
{children}
);
diff --git a/packages/react/src/components/country-selector.test.tsx b/packages/react/src/components/country-selector.test.tsx
index 7eea3647..dffe2ae3 100644
--- a/packages/react/src/components/country-selector.test.tsx
+++ b/packages/react/src/components/country-selector.test.tsx
@@ -14,81 +14,70 @@
* limitations under the License.
*/
-import { describe, it, expect, vi, beforeEach } from "vitest";
-import { render, screen, fireEvent } from "@testing-library/react";
-import "@testing-library/jest-dom";
-import { CountrySelector } from "./country-selector";
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { render, screen, fireEvent, cleanup } from "@testing-library/react";
import { countryData } from "@firebase-ui/core";
+import { CountrySelector } from "./country-selector";
-describe("CountrySelector Component", () => {
- const mockOnChange = vi.fn();
- const defaultCountry = countryData[0]; // First country in the list
-
+describe("
", () => {
beforeEach(() => {
- mockOnChange.mockClear();
+ vi.clearAllMocks();
+ });
+
+ afterEach(() => {
+ cleanup();
});
it("renders with the selected country", () => {
- render(
);
+ const country = countryData[0];
- // Check if the country flag emoji is displayed
- expect(screen.getByText(defaultCountry.emoji)).toBeInTheDocument();
+ render(
{}} />);
- // Check if the dial code is displayed
- expect(screen.getByText(defaultCountry.dialCode)).toBeInTheDocument();
+ expect(screen.getByText(country.emoji)).toBeInTheDocument();
+ expect(screen.getByText(country.dialCode)).toBeInTheDocument();
- // Check if the select has the correct value
const select = screen.getByRole("combobox");
- expect(select).toHaveValue(defaultCountry.code);
+ expect(select).toHaveValue(country.code);
});
it("applies custom className", () => {
- render();
+ const country = countryData[0];
+ render( {}} className="custom-class" />);
- const selector = screen.getByRole("combobox").closest(".fui-country-selector");
- expect(selector).toHaveClass("fui-country-selector");
- expect(selector).toHaveClass("custom-class");
+ const rootDiv = screen.getByRole("combobox").closest("div.fui-country-selector");
+ expect(rootDiv).toHaveClass("custom-class");
});
it("calls onChange when a different country is selected", () => {
- render();
+ const country = countryData[0];
+ const onChangeMock = vi.fn();
+
+ render();
const select = screen.getByRole("combobox");
// Find a different country to select
- const newCountry = countryData.find((country) => country.code !== defaultCountry.code);
+ const newCountry = countryData.find(($) => $.code !== country.code);
- if (newCountry) {
- // Change the selection
- fireEvent.change(select, { target: { value: newCountry.code } });
-
- // Check if onChange was called with the new country
- expect(mockOnChange).toHaveBeenCalledTimes(1);
- expect(mockOnChange).toHaveBeenCalledWith(newCountry);
- } else {
- // Fail the test if no different country is found
+ if (!newCountry) {
expect.fail("No different country found in countryData. Test cannot proceed.");
}
+
+ // Change the selection
+ fireEvent.change(select, { target: { value: newCountry.code } });
+
+ // Check if onChange was called with the new country
+ expect(onChangeMock).toHaveBeenCalledTimes(1);
+ expect(onChangeMock).toHaveBeenCalledWith(newCountry.code);
});
it("renders all countries in the dropdown", () => {
- render();
+ const country = countryData[0];
+ render( {}} />);
const select = screen.getByRole("combobox");
const options = select.querySelectorAll("option");
- // Check if all countries are in the dropdown
expect(options.length).toBe(countryData.length);
-
- // Check if a specific country exists in the dropdown
- const usCountry = countryData.find((country) => country.code === "US");
- if (usCountry) {
- const usOption = Array.from(options).find((option) => option.value === usCountry.code);
- expect(usOption).toBeInTheDocument();
- expect(usOption?.textContent).toBe(`${usCountry.dialCode} (${usCountry.name})`);
- } else {
- // Fail the test if US country is not found
- expect.fail("US country not found in countryData. Test cannot proceed.");
- }
});
});
diff --git a/packages/react/src/components/divider.test.tsx b/packages/react/src/components/divider.test.tsx
index 92d041ee..4e744244 100644
--- a/packages/react/src/components/divider.test.tsx
+++ b/packages/react/src/components/divider.test.tsx
@@ -16,10 +16,9 @@
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
-import "@testing-library/jest-dom";
import { Divider } from "./divider";
-describe("Divider Component", () => {
+describe("", () => {
it("renders a divider with no text", () => {
render();
const divider = screen.getByTestId("divider-no-text");
diff --git a/packages/react/src/components/field-info.test.tsx b/packages/react/src/components/field-info.test.tsx
deleted file mode 100644
index 5cb72a01..00000000
--- a/packages/react/src/components/field-info.test.tsx
+++ /dev/null
@@ -1,121 +0,0 @@
-/**
- * Copyright 2025 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { describe, it, expect } from "vitest";
-import { render, screen } from "@testing-library/react";
-import "@testing-library/jest-dom";
-import { FieldInfo } from "./field-info";
-import { FieldApi } from "@tanstack/react-form";
-
-describe("FieldInfo Component", () => {
- // Create a mock FieldApi with errors
- const createMockFieldWithErrors = (errors: string[]) => {
- return {
- state: {
- meta: {
- isTouched: true,
- errors,
- },
- },
- } as unknown as FieldApi;
- };
-
- // Create a mock FieldApi without errors
- const createMockFieldWithoutErrors = () => {
- return {
- state: {
- meta: {
- isTouched: true,
- errors: [],
- },
- },
- } as unknown as FieldApi;
- };
-
- // Create a mock FieldApi that's not touched
- const createMockFieldNotTouched = () => {
- return {
- state: {
- meta: {
- isTouched: false,
- errors: ["This field is required"],
- },
- },
- } as unknown as FieldApi;
- };
-
- it("renders error message when field is touched and has errors", () => {
- const errorMessage = "This field is required";
- const field = createMockFieldWithErrors([errorMessage]);
-
- render();
-
- const errorElement = screen.getByRole("alert");
- expect(errorElement).toBeInTheDocument();
- expect(errorElement).toHaveClass("fui-form__error");
- expect(errorElement).toHaveTextContent(errorMessage);
- });
-
- it("renders nothing when field is touched but has no errors", () => {
- const field = createMockFieldWithoutErrors();
-
- const { container } = render();
-
- // The component should render nothing
- expect(container).toBeEmptyDOMElement();
- });
-
- it("renders nothing when field is not touched, even with errors", () => {
- const field = createMockFieldNotTouched();
-
- const { container } = render();
-
- // The component should render nothing
- expect(container).toBeEmptyDOMElement();
- });
-
- it("applies custom className to the error message", () => {
- const errorMessage = "This field is required";
- const field = createMockFieldWithErrors([errorMessage]);
-
- render();
-
- const errorElement = screen.getByRole("alert");
- expect(errorElement).toHaveClass("fui-form__error");
- expect(errorElement).toHaveClass("custom-error");
- });
-
- it("accepts and passes through additional props", () => {
- const errorMessage = "This field is required";
- const field = createMockFieldWithErrors([errorMessage]);
-
- render();
-
- const errorElement = screen.getByTestId("error-message");
- expect(errorElement).toHaveAttribute("aria-labelledby", "form-field");
- });
-
- it("displays only the first error when multiple errors exist", () => {
- const errors = ["First error", "Second error"];
- const field = createMockFieldWithErrors(errors);
-
- render();
-
- const errorElement = screen.getByRole("alert");
- expect(errorElement).toHaveTextContent(errors[0]);
- expect(errorElement).not.toHaveTextContent(errors[1]);
- });
-});
diff --git a/packages/react/src/components/field-info.tsx b/packages/react/src/components/field-info.tsx
deleted file mode 100644
index 024b282d..00000000
--- a/packages/react/src/components/field-info.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-/**
- * Copyright 2025 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import type { FieldApi } from "@tanstack/react-form";
-import { ComponentProps } from "react";
-import { cn } from "~/utils/cn";
-
-export type FieldInfoProps = ComponentProps<"div"> & {
- field: FieldApi;
-};
-
-export function FieldInfo({ field, className, ...props }: FieldInfoProps) {
- return (
- <>
- {field.state.meta.isTouched && field.state.meta.errors.length ? (
-
- {field.state.meta.errors[0]}
-
- ) : null}
- >
- );
-}
diff --git a/packages/react/src/components/form.test.tsx b/packages/react/src/components/form.test.tsx
new file mode 100644
index 00000000..53b9b380
--- /dev/null
+++ b/packages/react/src/components/form.test.tsx
@@ -0,0 +1,235 @@
+import { describe, it, expect, afterEach, vi, beforeEach } from "vitest";
+import { render, screen, cleanup, renderHook, act, waitFor } from "@testing-library/react";
+import { form } from "./form";
+import { ComponentProps } from "react";
+
+vi.mock("~/components/button", () => {
+ return {
+ Button: (props: ComponentProps<"button">) => ,
+ };
+});
+
+describe("form export", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ afterEach(() => {
+ cleanup();
+ });
+
+ it("should allow rendering of all composed components", () => {
+ const { result } = renderHook(() => {
+ return form.useAppForm({
+ defaultValues: { foo: "bar" },
+ });
+ });
+
+ const hook = result.current;
+
+ render(
+
+ } />
+
+ Submit
+ Action
+
+ );
+
+ expect(screen.getByRole("textbox", { name: "Foo" })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "Submit" })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "Action" })).toBeInTheDocument();
+ expect(screen.getByText("Submit")).toBeInTheDocument();
+ expect(screen.getByText("Action")).toBeInTheDocument();
+ });
+
+ describe("", () => {
+ it("should render the Input component", () => {
+ const { result } = renderHook(() => {
+ return form.useAppForm({
+ defaultValues: { foo: "bar" },
+ });
+ });
+
+ const hook = result.current;
+
+ const { container } = render(
+
+ } />
+
+ );
+
+ expect(container.querySelector('label[for="foo"]')).toBeInTheDocument();
+ expect(container.querySelector('label[for="foo"]')).toHaveTextContent("Foo");
+ expect(container.querySelector('input[name="foo"]')).toBeInTheDocument();
+ expect(container.querySelector('input[name="foo"]')).toHaveValue("bar");
+ expect(container.querySelector('input[name="foo"]')).toHaveAttribute("aria-invalid", "false");
+ });
+
+ it("should render the Input children when provided", () => {
+ const { result } = renderHook(() => {
+ return form.useAppForm({
+ defaultValues: { foo: "bar" },
+ });
+ });
+
+ const hook = result.current;
+
+ render(
+
+ (
+
+ Test Child
+
+ )}
+ />
+
+ );
+
+ expect(screen.getByTestId("test-child")).toBeInTheDocument();
+ });
+
+ it("should render the Input metadata when available", async () => {
+ const { result } = renderHook(() => {
+ return form.useAppForm({
+ defaultValues: { foo: "" },
+ });
+ });
+
+ const hook = result.current;
+
+ render(
+
+ {
+ return "error!";
+ },
+ }}
+ name="foo"
+ children={(field) => }
+ />
+
+ );
+
+ await act(async () => {
+ await hook.handleSubmit();
+ });
+
+ const error = screen.getByRole("alert");
+ expect(error).toBeInTheDocument();
+ expect(error).toHaveClass("fui-form__error");
+ });
+ });
+
+ describe("", () => {
+ it("should render the Action component", () => {
+ const { result } = renderHook(() => {
+ return form.useAppForm({});
+ });
+
+ const hook = result.current;
+
+ render(
+
+ Action
+
+ );
+
+ expect(screen.getByRole("button", { name: "Action" })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "Action" })).toHaveClass("fui-form__action");
+ expect(screen.getByRole("button", { name: "Action" })).toHaveTextContent("Action");
+ expect(screen.getByRole("button", { name: "Action" })).toHaveAttribute("type", "button");
+ });
+ });
+
+ describe("", () => {
+ it("should render the SubmitButton component", () => {
+ const { result } = renderHook(() => {
+ return form.useAppForm({});
+ });
+
+ const hook = result.current;
+
+ render(
+
+ Submit
+
+ );
+
+ expect(screen.getByRole("button", { name: "Submit" })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "Submit" })).toHaveTextContent("Submit");
+ expect(screen.getByRole("button", { name: "Submit" })).toHaveAttribute("type", "submit");
+ expect(screen.getByTestId("submit-button")).toBeInTheDocument();
+ });
+
+ it("should subscribe to the isSubmitting state", async () => {
+ const { result } = renderHook(() => {
+ return form.useAppForm({
+ validators: {
+ onSubmitAsync: async () => {
+ // Simulate a slow async operation
+ await new Promise((resolve) => setTimeout(resolve, 100));
+ return undefined;
+ },
+ },
+ });
+ });
+
+ const hook = result.current;
+
+ render(
+
+ Submit
+
+ );
+
+ const submitButton = screen.getByTestId("submit-button");
+
+ expect(submitButton).toBeInTheDocument();
+ expect(submitButton).not.toHaveAttribute("disabled");
+
+ act(() => {
+ hook.handleSubmit();
+ });
+
+ await waitFor(() => {
+ expect(submitButton).toHaveAttribute("disabled");
+ });
+ });
+ });
+
+ describe("", () => {
+ it.only("should render the ErrorMessage if the onSubmit error is set", async () => {
+ const { result } = renderHook(() => {
+ return form.useAppForm({
+ validators: {
+ onSubmitAsync: async () => {
+ return "error!";
+ },
+ },
+ });
+ });
+
+ const hook = result.current;
+
+ const { container } = render(
+
+
+
+ );
+
+ act(async () => {
+ await hook.handleSubmit();
+ });
+
+ await waitFor(() => {
+ const error = container.querySelector(".fui-form__error");
+ expect(error).toBeInTheDocument();
+ expect(error).toHaveTextContent("error!");
+ });
+ });
+ });
+});
diff --git a/packages/react/src/components/form.tsx b/packages/react/src/components/form.tsx
new file mode 100644
index 00000000..b1101f30
--- /dev/null
+++ b/packages/react/src/components/form.tsx
@@ -0,0 +1,87 @@
+import { ComponentProps, PropsWithChildren } from "react";
+import { AnyFieldApi, createFormHook, createFormHookContexts } from "@tanstack/react-form";
+import { Button } from "./button";
+import { cn } from "~/utils/cn";
+
+const { fieldContext, useFieldContext, formContext, useFormContext } = createFormHookContexts();
+
+function FieldMetadata({ className, ...props }: ComponentProps<"div"> & { field: AnyFieldApi }) {
+ if (!props.field.state.meta.isTouched || !props.field.state.meta.errors.length) {
+ return null;
+ }
+
+ return (
+
+
+ {props.field.state.meta.errors.map((error) => error.message).join(", ")}
+
+
+ );
+}
+
+function Input(props: PropsWithChildren & { label: string }>) {
+ const field = useFieldContext();
+
+ return (
+
+ );
+}
+
+function Action({ className, ...props }: ComponentProps<"button">) {
+ return ;
+}
+
+function SubmitButton(props: ComponentProps<"button">) {
+ const form = useFormContext();
+
+ return (
+ state.isSubmitting}>
+ {(isSubmitting) => }
+
+ );
+}
+
+function ErrorMessage() {
+ const form = useFormContext();
+
+ return (
+ [state.errorMap]}>
+ {([errorMap]) => {
+ if (errorMap?.onSubmit) {
+ return {String(errorMap.onSubmit)}
;
+ }
+
+ return null;
+ }}
+
+ );
+}
+
+export const form = createFormHook({
+ fieldComponents: {
+ Input,
+ },
+ formComponents: {
+ SubmitButton,
+ ErrorMessage,
+ Action,
+ },
+ fieldContext,
+ formContext,
+});
diff --git a/packages/react/src/components/index.tsx b/packages/react/src/components/index.tsx
index fa866f4b..5f625b66 100644
--- a/packages/react/src/components/index.tsx
+++ b/packages/react/src/components/index.tsx
@@ -17,6 +17,6 @@
export { Button, type ButtonProps } from "./button";
export { Card, CardHeader, CardTitle, CardSubtitle, CardContent, type CardProps } from "./card";
export { CountrySelector, type CountrySelectorProps } from "./country-selector";
-export { FieldInfo, type FieldInfoProps } from "./field-info";
export { Policies, type PolicyProps, type PolicyURL } from "./policies";
export { Divider, type DividerProps } from "./divider";
+export { form } from './form';
diff --git a/packages/react/src/components/policies.test.tsx b/packages/react/src/components/policies.test.tsx
index 24c43cff..5d9b8800 100644
--- a/packages/react/src/components/policies.test.tsx
+++ b/packages/react/src/components/policies.test.tsx
@@ -16,42 +16,26 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
-import "@testing-library/jest-dom";
-import { Policies, PolicyProvider } from "./policies";
+import { Policies } from "./policies";
+import { FirebaseUIProvider } from "~/context";
+import { createMockUI } from "~/tests/utils";
-// Mock useUI hook
-vi.mock("~/hooks", () => ({
- useUI: vi.fn(() => ({
- locale: "en-US",
- translations: {
- "en-US": {
- labels: {
- termsOfService: "Terms of Service",
- privacyPolicy: "Privacy Policy",
- },
- messages: {
- termsAndPrivacy: "By continuing, you agree to our {tos} and {privacy}",
- },
- },
- },
- })),
-}));
-
-describe("TermsAndPrivacy Component", () => {
+describe("", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders component with terms and privacy links", () => {
render(
-
-
-
+
+
);
// Check that the text and links are rendered
@@ -72,9 +56,9 @@ describe("TermsAndPrivacy Component", () => {
it("returns null when both tosUrl and privacyPolicyUrl are not provided", () => {
const { container } = render(
-
-
-
+
+
+
);
expect(container).toBeEmptyDOMElement();
});
diff --git a/packages/react/src/context.test.tsx b/packages/react/src/context.test.tsx
index fe327083..127f07b7 100644
--- a/packages/react/src/context.test.tsx
+++ b/packages/react/src/context.test.tsx
@@ -4,7 +4,6 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
@@ -14,56 +13,160 @@
* limitations under the License.
*/
-import { describe, it, expect } from "vitest";
-import { render } from "@testing-library/react";
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { render, act, cleanup } from "@testing-library/react";
import { FirebaseUIProvider, FirebaseUIContext } from "./context";
-import { map } from "nanostores";
+import { createMockUI } from "~/tests/utils";
import { useContext } from "react";
-import { FirebaseUI, FirebaseUIConfiguration } from "@firebase-ui/core";
// Mock component to test context value
function TestConsumer() {
const config = useContext(FirebaseUIContext);
- return {config.locale || "no-value"}
;
+ return {config.state || "no-value"}
;
+}
+
+// Mock component to test policy context
+function PolicyTestConsumer() {
+ const config = useContext(FirebaseUIContext);
+ return {config.state || "no-policy"}
;
}
-describe("ConfigProvider", () => {
+beforeEach(() => {
+ vi.clearAllMocks();
+});
+
+afterEach(() => {
+ cleanup();
+});
+
+describe("FirebaseUIProvider", () => {
it("provides the config value to children", () => {
- // Create a mock config store with the correct FUIConfig properties
- const mockConfig = map>({
- locale: "en-US",
- }) as FirebaseUI;
+ const mockUI = createMockUI();
const { getByTestId } = render(
-
+
);
- expect(getByTestId("test-value").textContent).toBe("en-US");
+ expect(getByTestId("test-value").textContent).toBe("idle");
});
- // TODO(ehesp): This test is not working
- it.skip("updates when the config store changes", () => {
- // // Create a mock config store
- // const mockConfig = map>({
- // locale: "en-US",
- // }) as FirebaseUI;
+ it("updates when the nanostore changes", () => {
+ const mockUI = createMockUI();
- // const { getByTestId } = render(
- //
- //
- //
- // );
+ const { getByTestId } = render(
+
+
+
+ );
- // expect(getByTestId("test-value").textContent).toBe("en-US");
+ expect(getByTestId("test-value").textContent).toBe("idle");
- // // Update the config store inside act()
- // act(() => {
- // mockConfig.setKey("locale", "fr-FR");
- // });
+ // Update the nanostore directly
+ act(() => {
+ mockUI.setKey("state", "pending");
+ });
- // // Check that the context value was updated
- // expect(getByTestId("test-value").textContent).toBe("fr-FR");
+ // Check that the context value was updated
+ expect(getByTestId("test-value").textContent).toBe("pending");
});
-});
+
+ it("provides stable references when nanostore value hasn't changed", () => {
+ const mockUI = createMockUI();
+ let providerRenderCount = 0;
+ const contextValues: any[] = [];
+
+ const TestConsumer = () => {
+ const contextValue = useContext(FirebaseUIContext);
+ contextValues.push(contextValue);
+ return {contextValue.state}
;
+ };
+
+ const TestProvider = ({ children }: { children: React.ReactNode }) => {
+ providerRenderCount++;
+ return {children};
+ };
+
+ const { rerender } = render(
+
+
+
+ );
+
+ // Initial render
+ expect(providerRenderCount).toBe(1);
+ expect(contextValues).toHaveLength(1);
+
+ // Re-render the provider without changing nanostore
+ rerender(
+
+
+
+ );
+
+ // Provider should render again, but nanostores should provide stable reference
+ expect(providerRenderCount).toBe(2);
+ expect(contextValues).toHaveLength(2);
+
+ // The context values should be the same reference (nanostores handles this)
+ expect(contextValues[0]).toBe(contextValues[1]);
+ expect(contextValues[0].state).toBe(contextValues[1].state);
+ });
+
+ it("passes policies to PolicyProvider", () => {
+ const mockUI = createMockUI();
+ const mockPolicies = {
+ privacyPolicyUrl: "https://example.com/privacy",
+ termsOfServiceUrl: "https://example.com/terms"
+ };
+
+ const { getByTestId } = render(
+
+
+
+ );
+
+ // The component should render successfully with policies
+ expect(getByTestId("policy-test").textContent).toBe("idle");
+ });
+
+ it("works without policies", () => {
+ const mockUI = createMockUI();
+
+ const { getByTestId } = render(
+
+
+
+ );
+
+ expect(getByTestId("test-value").textContent).toBe("idle");
+ });
+
+ it("handles multiple state changes correctly", () => {
+ const mockUI = createMockUI();
+
+ const { getByTestId } = render(
+
+
+
+ );
+
+ expect(getByTestId("test-value").textContent).toBe("idle");
+
+ act(() => {
+ mockUI.setKey("state", "pending");
+ });
+ expect(getByTestId("test-value").textContent).toBe("pending");
+
+ act(() => {
+ mockUI.setKey("state", "loading");
+ });
+ expect(getByTestId("test-value").textContent).toBe("loading");
+
+ act(() => {
+ mockUI.setKey("state", "idle");
+ });
+ expect(getByTestId("test-value").textContent).toBe("idle");
+ });
+});
\ No newline at end of file
diff --git a/packages/react/src/hooks.test.tsx b/packages/react/src/hooks.test.tsx
index 028e545c..368e6eb4 100644
--- a/packages/react/src/hooks.test.tsx
+++ b/packages/react/src/hooks.test.tsx
@@ -15,55 +15,570 @@
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
-import { renderHook } from "@testing-library/react";
-import { useUI } from "./hooks";
-import { getAuth } from "firebase/auth";
-import { FirebaseUIContext } from "./context";
-
-// Mock Firebase
-vi.mock("firebase/auth", () => ({
- getAuth: vi.fn(() => ({
- currentUser: null,
- /* other auth properties */
- })),
-}));
-
-describe("Hooks", () => {
- const mockApp = { name: "test-app" } as any;
- const mockTranslations = {
- en: {
- labels: {
- signIn: "Sign In",
- email: "Email",
+import { renderHook, act, cleanup } from "@testing-library/react";
+import { useUI, useSignInAuthFormSchema, useSignUpAuthFormSchema, useForgotPasswordAuthFormSchema, useEmailLinkAuthFormSchema, usePhoneAuthFormSchema } from "./hooks";
+import { createFirebaseUIProvider, createMockUI } from "~/tests/utils";
+import { registerLocale } from "@firebase-ui/translations";
+
+beforeEach(() => {
+ vi.clearAllMocks();
+});
+
+describe("useUI", () => {
+ it("returns the config from context", () => {
+ const mockUI = createMockUI();
+
+ const { result } = renderHook(() => useUI(), {
+ wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI })
+ });
+
+ expect(result.current).toEqual(mockUI.get());
+ });
+
+ // TODO(ehesp): This test is not working as expected.
+ it.skip("throws an error if no context is found", () => {
+ expect(() => {
+ renderHook(() => useUI());
+ }).toThrow("No FirebaseUI context found. Your application must be wrapped in a component.");
+ });
+
+ it("returns updated values when nanostore state changes via setState", () => {
+ const ui = createMockUI();
+
+ const { result } = renderHook(() => useUI(), {
+ wrapper: ({ children }) => createFirebaseUIProvider({ children, ui })
+ });
+
+ expect(result.current.state).toBeDefined();
+
+ act(() => {
+ result.current.setState("pending");
+ });
+
+ expect(result.current.state).toBe("pending");
+
+ act(() => {
+ result.current.setState("loading");
+ });
+
+ expect(result.current.state).toBe("loading");
+ });
+
+ it("returns stable reference when nanostore value hasn't changed", () => {
+ const ui = createMockUI();
+ let hookCallCount = 0;
+ const results: any[] = [];
+
+ const TestHook = () => {
+ hookCallCount++;
+ const result = useUI();
+ results.push(result);
+ return result;
+ };
+
+ const { rerender } = renderHook(() => TestHook(), {
+ wrapper: ({ children }) => createFirebaseUIProvider({ children, ui })
+ });
+
+ expect(hookCallCount).toBe(1);
+ expect(results).toHaveLength(1);
+
+ rerender();
+
+ expect(hookCallCount).toBe(2);
+ expect(results).toHaveLength(2);
+
+ expect(results[0]).toBe(results[1]);
+ expect(results[0].state).toBe(results[1].state);
+ });
+});
+
+describe("useSignInAuthFormSchema", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ cleanup();
+ });
+
+
+ it("returns schema with default English error messages", () => {
+ const mockUI = createMockUI();
+
+ const { result } = renderHook(() => useSignInAuthFormSchema(), {
+ wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI })
+ });
+
+ const schema = result.current;
+
+ const emailResult = schema.safeParse({ email: "invalid-email", password: "validpassword123" });
+ expect(emailResult.success).toBe(false);
+ if (!emailResult.success) {
+ expect(emailResult.error.issues[0].message).toBe("Please enter a valid email address");
+ }
+
+ const passwordResult = schema.safeParse({ email: "test@example.com", password: "123" });
+ expect(passwordResult.success).toBe(false);
+ if (!passwordResult.success) {
+ expect(passwordResult.error.issues[0].message).toBe("Password should be at least 8 characters");
+ }
+ });
+
+ it("returns schema with custom error messages when locale changes", () => {
+ const customTranslations = {
+ errors: {
+ invalidEmail: "Por favor ingresa un email válido",
+ weakPassword: "La contraseña debe tener al menos 8 caracteres",
+ },
+ };
+
+ const customLocale = registerLocale("es-ES", customTranslations);
+ const mockUI = createMockUI({ locale: customLocale });
+
+ const { result } = renderHook(() => useSignInAuthFormSchema(), {
+ wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI })
+ });
+
+ const schema = result.current;
+
+ const emailResult = schema.safeParse({ email: "invalid-email", password: "validpassword123" });
+ expect(emailResult.success).toBe(false);
+ if (!emailResult.success) {
+ expect(emailResult.error.issues[0].message).toBe("Por favor ingresa un email válido");
+ }
+
+ const passwordResult = schema.safeParse({ email: "test@example.com", password: "123" });
+ expect(passwordResult.success).toBe(false);
+ if (!passwordResult.success) {
+ expect(passwordResult.error.issues[0].message).toBe("La contraseña debe tener al menos 8 caracteres");
+ }
+ });
+
+
+ it("returns stable reference when UI hasn't changed", () => {
+ const mockUI = createMockUI();
+
+ const { result, rerender } = renderHook(() => useSignInAuthFormSchema(), {
+ wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI })
+ });
+
+ const initialSchema = result.current;
+
+ rerender();
+
+ expect(result.current).toBe(initialSchema);
+ });
+
+ it("returns new schema when locale changes", () => {
+ const mockUI = createMockUI();
+
+ const { result, rerender } = renderHook(() => useSignInAuthFormSchema(), {
+ wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI })
+ });
+
+ const initialSchema = result.current;
+
+ const customTranslations = {
+ errors: {
+ invalidEmail: "Custom email error",
+ weakPassword: "Custom password error",
+ },
+ };
+ const customLocale = registerLocale("fr-FR", customTranslations);
+
+ act(() => {
+ mockUI.setKey("locale", customLocale);
+ });
+
+ rerender();
+
+ expect(result.current).not.toBe(initialSchema);
+
+ const emailResult = result.current.safeParse({ email: "invalid-email", password: "validpassword123" });
+ expect(emailResult.success).toBe(false);
+
+ if (!emailResult.success) {
+ expect(emailResult.error.issues[0].message).toBe("Custom email error");
+ }
+ });
+});
+
+describe("useSignUpAuthFormSchema", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ cleanup();
+ });
+
+ it("returns schema with default English error messages", () => {
+ const mockUI = createMockUI();
+
+ const { result } = renderHook(() => useSignUpAuthFormSchema(), {
+ wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI })
+ });
+
+ const schema = result.current;
+
+ const emailResult = schema.safeParse({ email: "invalid-email", password: "validpassword123", confirmPassword: "validpassword123" });
+ expect(emailResult.success).toBe(false);
+ if (!emailResult.success) {
+ expect(emailResult.error.issues[0].message).toBe("Please enter a valid email address");
+ }
+
+ const passwordResult = schema.safeParse({ email: "test@example.com", password: "123", confirmPassword: "123" });
+ expect(passwordResult.success).toBe(false);
+ if (!passwordResult.success) {
+ expect(passwordResult.error.issues[0].message).toBe("Password should be at least 8 characters");
+ }
+ });
+
+ it("returns schema with custom error messages when locale changes", () => {
+ const customTranslations = {
+ errors: {
+ invalidEmail: "Por favor ingresa un email válido",
+ weakPassword: "La contraseña debe tener al menos 8 caracteres",
+ },
+ };
+
+ const customLocale = registerLocale("es-ES", customTranslations);
+ const mockUI = createMockUI({ locale: customLocale });
+
+ const { result } = renderHook(() => useSignUpAuthFormSchema(), {
+ wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI })
+ });
+
+ const schema = result.current;
+
+ const emailResult = schema.safeParse({ email: "invalid-email", password: "validpassword123", confirmPassword: "validpassword123" });
+ expect(emailResult.success).toBe(false);
+ if (!emailResult.success) {
+ expect(emailResult.error.issues[0].message).toBe("Por favor ingresa un email válido");
+ }
+
+ const passwordResult = schema.safeParse({ email: "test@example.com", password: "123", confirmPassword: "123" });
+ expect(passwordResult.success).toBe(false);
+ if (!passwordResult.success) {
+ expect(passwordResult.error.issues[0].message).toBe("La contraseña debe tener al menos 8 caracteres");
+ }
+ });
+
+ it("returns stable reference when UI hasn't changed", () => {
+ const mockUI = createMockUI();
+
+ const { result, rerender } = renderHook(() => useSignUpAuthFormSchema(), {
+ wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI })
+ });
+
+ const initialSchema = result.current;
+
+ rerender();
+
+ expect(result.current).toBe(initialSchema);
+ });
+
+ it("returns new schema when locale changes", () => {
+ const mockUI = createMockUI();
+
+ const { result, rerender } = renderHook(() => useSignUpAuthFormSchema(), {
+ wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI })
+ });
+
+ const initialSchema = result.current;
+
+ const customTranslations = {
+ errors: {
+ invalidEmail: "Custom email error",
+ weakPassword: "Custom password error",
+ },
+ };
+ const customLocale = registerLocale("fr-FR", customTranslations);
+
+ act(() => {
+ mockUI.setKey("locale", customLocale);
+ });
+
+ rerender();
+
+ expect(result.current).not.toBe(initialSchema);
+
+ const emailResult = result.current.safeParse({ email: "invalid-email", password: "validpassword123", confirmPassword: "validpassword123" });
+ expect(emailResult.success).toBe(false);
+
+ if (!emailResult.success) {
+ expect(emailResult.error.issues[0].message).toBe("Custom email error");
+ }
+ });
+});
+
+describe("useForgotPasswordAuthFormSchema", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ cleanup();
+ });
+
+ it("returns schema with default English error messages", () => {
+ const mockUI = createMockUI();
+
+ const { result } = renderHook(() => useForgotPasswordAuthFormSchema(), {
+ wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI })
+ });
+
+ const schema = result.current;
+
+ const emailResult = schema.safeParse({ email: "invalid-email" });
+ expect(emailResult.success).toBe(false);
+ if (!emailResult.success) {
+ expect(emailResult.error.issues[0].message).toBe("Please enter a valid email address");
+ }
+ });
+
+ it("returns schema with custom error messages when locale changes", () => {
+ const customTranslations = {
+ errors: {
+ invalidEmail: "Por favor ingresa un email válido",
+ },
+ };
+
+ const customLocale = registerLocale("es-ES", customTranslations);
+ const mockUI = createMockUI({ locale: customLocale });
+
+ const { result } = renderHook(() => useForgotPasswordAuthFormSchema(), {
+ wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI })
+ });
+
+ const schema = result.current;
+
+ const emailResult = schema.safeParse({ email: "invalid-email" });
+ expect(emailResult.success).toBe(false);
+ if (!emailResult.success) {
+ expect(emailResult.error.issues[0].message).toBe("Por favor ingresa un email válido");
+ }
+ });
+
+ it("returns stable reference when UI hasn't changed", () => {
+ const mockUI = createMockUI();
+
+ const { result, rerender } = renderHook(() => useForgotPasswordAuthFormSchema(), {
+ wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI })
+ });
+
+ const initialSchema = result.current;
+
+ rerender();
+
+ expect(result.current).toBe(initialSchema);
+ });
+
+ it("returns new schema when locale changes", () => {
+ const mockUI = createMockUI();
+
+ const { result, rerender } = renderHook(() => useForgotPasswordAuthFormSchema(), {
+ wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI })
+ });
+
+ const initialSchema = result.current;
+
+ const customTranslations = {
+ errors: {
+ invalidEmail: "Custom email error",
},
- },
- };
-
- const mockConfig = {
- app: mockApp,
- getAuth: vi.fn(),
- setLocale: vi.fn(),
- state: "idle",
- setState: vi.fn(),
- locale: "en",
- translations: mockTranslations,
- behaviors: {},
- recaptchaMode: "normal",
- };
-
- const wrapper = ({ children }: { children: React.ReactNode }) => (
- {children}
- );
+ };
+ const customLocale = registerLocale("fr-FR", customTranslations);
+
+ act(() => {
+ mockUI.setKey("locale", customLocale);
+ });
+
+ rerender();
+
+ expect(result.current).not.toBe(initialSchema);
+
+ const emailResult = result.current.safeParse({ email: "invalid-email" });
+ expect(emailResult.success).toBe(false);
+
+ if (!emailResult.success) {
+ expect(emailResult.error.issues[0].message).toBe("Custom email error");
+ }
+ });
+});
+describe("useEmailLinkAuthFormSchema", () => {
beforeEach(() => {
vi.clearAllMocks();
+ cleanup();
});
- describe("useUI", () => {
- it("returns the config from context", () => {
- const { result } = renderHook(() => useUI(), { wrapper });
+ it("returns schema with default English error messages", () => {
+ const mockUI = createMockUI();
- expect(result.current).toEqual(mockConfig);
+ const { result } = renderHook(() => useEmailLinkAuthFormSchema(), {
+ wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI })
});
+
+ const schema = result.current;
+
+ const emailResult = schema.safeParse({ email: "invalid-email" });
+ expect(emailResult.success).toBe(false);
+ if (!emailResult.success) {
+ expect(emailResult.error.issues[0].message).toBe("Please enter a valid email address");
+ }
+ });
+
+ it("returns schema with custom error messages when locale changes", () => {
+ const customTranslations = {
+ errors: {
+ invalidEmail: "Por favor ingresa un email válido",
+ },
+ };
+
+ const customLocale = registerLocale("es-ES", customTranslations);
+ const mockUI = createMockUI({ locale: customLocale });
+
+ const { result } = renderHook(() => useEmailLinkAuthFormSchema(), {
+ wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI })
+ });
+
+ const schema = result.current;
+
+ const emailResult = schema.safeParse({ email: "invalid-email" });
+ expect(emailResult.success).toBe(false);
+ if (!emailResult.success) {
+ expect(emailResult.error.issues[0].message).toBe("Por favor ingresa un email válido");
+ }
+ });
+
+ it("returns stable reference when UI hasn't changed", () => {
+ const mockUI = createMockUI();
+
+ const { result, rerender } = renderHook(() => useEmailLinkAuthFormSchema(), {
+ wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI })
+ });
+
+ const initialSchema = result.current;
+
+ rerender();
+
+ expect(result.current).toBe(initialSchema);
+ });
+
+ it("returns new schema when locale changes", () => {
+ const mockUI = createMockUI();
+
+ const { result, rerender } = renderHook(() => useEmailLinkAuthFormSchema(), {
+ wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI })
+ });
+
+ const initialSchema = result.current;
+
+ const customTranslations = {
+ errors: {
+ invalidEmail: "Custom email error",
+ },
+ };
+ const customLocale = registerLocale("fr-FR", customTranslations);
+
+ act(() => {
+ mockUI.setKey("locale", customLocale);
+ });
+
+ rerender();
+
+ expect(result.current).not.toBe(initialSchema);
+
+ const emailResult = result.current.safeParse({ email: "invalid-email" });
+ expect(emailResult.success).toBe(false);
+
+ if (!emailResult.success) {
+ expect(emailResult.error.issues[0].message).toBe("Custom email error");
+ }
+ });
+});
+
+describe("usePhoneAuthFormSchema", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ cleanup();
+ });
+
+ it("returns schema with default English error messages", () => {
+ const mockUI = createMockUI();
+
+ const { result } = renderHook(() => usePhoneAuthFormSchema(), {
+ wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI })
+ });
+
+ const schema = result.current;
+
+ const phoneResult = schema.safeParse({ phoneNumber: "invalid-phone" });
+ expect(phoneResult.success).toBe(false);
+ if (!phoneResult.success) {
+ expect(phoneResult.error.issues[0].message).toBe("Please enter a valid phone number");
+ }
+ });
+
+ it("returns schema with custom error messages when locale changes", () => {
+ const customTranslations = {
+ errors: {
+ invalidPhoneNumber: "Por favor ingresa un número de teléfono válido",
+ },
+ };
+
+ const customLocale = registerLocale("es-ES", customTranslations);
+ const mockUI = createMockUI({ locale: customLocale });
+
+ const { result } = renderHook(() => usePhoneAuthFormSchema(), {
+ wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI })
+ });
+
+ const schema = result.current;
+
+ const phoneResult = schema.safeParse({ phoneNumber: "invalid-phone" });
+ expect(phoneResult.success).toBe(false);
+ if (!phoneResult.success) {
+ expect(phoneResult.error.issues[0].message).toBe("Por favor ingresa un número de teléfono válido");
+ }
+ });
+
+ it("returns stable reference when UI hasn't changed", () => {
+ const mockUI = createMockUI();
+
+ const { result, rerender } = renderHook(() => usePhoneAuthFormSchema(), {
+ wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI })
+ });
+
+ const initialSchema = result.current;
+
+ rerender();
+
+ expect(result.current).toBe(initialSchema);
+ });
+
+ it("returns new schema when locale changes", () => {
+ const mockUI = createMockUI();
+
+ const { result, rerender } = renderHook(() => usePhoneAuthFormSchema(), {
+ wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI })
+ });
+
+ const initialSchema = result.current;
+
+ const customTranslations = {
+ errors: {
+ invalidPhoneNumber: "Custom phone error",
+ },
+ };
+ const customLocale = registerLocale("fr-FR", customTranslations);
+
+ act(() => {
+ mockUI.setKey("locale", customLocale);
+ });
+
+ rerender();
+
+ expect(result.current).not.toBe(initialSchema);
+
+ const phoneResult = result.current.safeParse({ phoneNumber: "invalid-phone" });
+ expect(phoneResult.success).toBe(false);
+
+ if (!phoneResult.success) {
+ expect(phoneResult.error.issues[0].message).toBe("Custom phone error");
+ }
});
});
diff --git a/packages/react/src/hooks.ts b/packages/react/src/hooks.ts
index 3575142a..fa1a8966 100644
--- a/packages/react/src/hooks.ts
+++ b/packages/react/src/hooks.ts
@@ -14,8 +14,9 @@
* limitations under the License.
*/
-import { useContext } from "react";
+import { useContext, useMemo } from "react";
import { FirebaseUIContext } from "./context";
+import { createEmailLinkAuthFormSchema, createForgotPasswordAuthFormSchema, createPhoneAuthFormSchema, createSignInAuthFormSchema, createSignUpAuthFormSchema } from "@firebase-ui/core";
/**
* Get the UI configuration from the context.
@@ -29,3 +30,28 @@ export function useUI() {
return ui;
}
+
+export function useSignInAuthFormSchema() {
+ const ui = useUI();
+ return useMemo(() => createSignInAuthFormSchema(ui), [ui]);
+}
+
+export function useSignUpAuthFormSchema() {
+ const ui = useUI();
+ return useMemo(() => createSignUpAuthFormSchema(ui), [ui]);
+}
+
+export function useForgotPasswordAuthFormSchema() {
+ const ui = useUI();
+ return useMemo(() => createForgotPasswordAuthFormSchema(ui), [ui]);
+}
+
+export function useEmailLinkAuthFormSchema() {
+ const ui = useUI();
+ return useMemo(() => createEmailLinkAuthFormSchema(ui), [ui]);
+}
+
+export function usePhoneAuthFormSchema() {
+ const ui = useUI();
+ return useMemo(() => createPhoneAuthFormSchema(ui), [ui]);
+}
\ No newline at end of file
diff --git a/packages/react/tests/tsconfig.json b/packages/react/tests/tsconfig.json
deleted file mode 100644
index fa83382e..00000000
--- a/packages/react/tests/tsconfig.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "extends": "../tsconfig.test.json",
- "include": ["./**/*.test.tsx", "./**/*.test.ts"],
- "compilerOptions": {
- "jsx": "react-jsx",
- "esModuleInterop": true,
- "types": ["vitest/globals", "node", "@testing-library/jest-dom"],
- "baseUrl": "..",
- "paths": {
- "@firebase-ui/core": ["../core/src/index.ts"],
- "~/*": ["src/*"]
- }
- }
-}
diff --git a/packages/react/tests/utils.tsx b/packages/react/tests/utils.tsx
new file mode 100644
index 00000000..12af93d0
--- /dev/null
+++ b/packages/react/tests/utils.tsx
@@ -0,0 +1,23 @@
+import type { FirebaseApp } from "firebase/app";
+import type { Auth } from "firebase/auth";
+import { enUs } from "@firebase-ui/translations";
+import { BehaviorHandlers, Behavior, FirebaseUI, FirebaseUIConfigurationOptions, initializeUI } from "@firebase-ui/core";
+import { FirebaseUIProvider } from "../src/context";
+
+export function createMockUI(overrides?: Partial): FirebaseUI {
+ return initializeUI({
+ app: {} as FirebaseApp,
+ auth: {} as Auth,
+ locale: enUs,
+ behaviors: [] as Partial>[],
+ ...overrides,
+ });
+}
+
+export const createFirebaseUIProvider = ({ children, ui }: { children: React.ReactNode, ui: FirebaseUI }) => (
+ {children}
+);
+
+export function CreateFirebaseUIProvider({ children, ui }: { children: React.ReactNode, ui: FirebaseUI }) {
+ return {children};
+}
\ No newline at end of file
diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json
index ea9d0cd8..a9786808 100644
--- a/packages/react/tsconfig.json
+++ b/packages/react/tsconfig.json
@@ -1,11 +1,14 @@
{
- "files": [],
- "references": [
- {
- "path": "./tsconfig.app.json"
- },
- {
- "path": "./tsconfig.node.json"
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "moduleResolution": "Bundler",
+ "paths": {
+ "~/*": ["./src/*"],
+ "~/tests/*": ["./tests/*"],
+ "@firebase-ui/core": ["../core/src/index.ts"],
+ "@firebase-ui/styles": ["../styles/src/index.ts"]
}
- ]
+ },
+ "include": ["src", "eslint.config.js", "vite.config.ts", "setup-test.ts"]
}
diff --git a/packages/react/tsconfig.node.json b/packages/react/tsconfig.node.json
deleted file mode 100644
index 9719b1e7..00000000
--- a/packages/react/tsconfig.node.json
+++ /dev/null
@@ -1,30 +0,0 @@
-{
- "compilerOptions": {
- "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
- "target": "ES2022",
- "lib": ["ES2023"],
- "module": "ESNext",
- "skipLibCheck": true,
-
- /* Bundler mode */
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "isolatedModules": true,
- "moduleDetection": "force",
- "noEmit": true,
-
- /* Linting */
- "strict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "noFallthroughCasesInSwitch": true,
- "noUncheckedSideEffectImports": true,
- "baseUrl": ".",
- "paths": {
- "~/*": ["./src/*"],
- "@firebase-ui/core": ["../core/src/index.ts"],
- "@firebase-ui/styles": ["../styles/src/index.ts"]
- }
- },
- "include": ["vite.config.ts"]
-}
diff --git a/packages/react/tsconfig.test.json b/packages/react/tsconfig.test.json
deleted file mode 100644
index a068a888..00000000
--- a/packages/react/tsconfig.test.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "extends": "./tsconfig.app.json",
- "compilerOptions": {
- "jsx": "react-jsx",
- "esModuleInterop": true,
- "types": ["vitest/importMeta", "node", "@testing-library/jest-dom"],
- "baseUrl": ".",
- "paths": {
- "~/*": ["./src/*"],
- "@firebase-ui/core": ["../firebaseui-core/src/index.ts"]
- }
- },
- "include": ["src", "tests"]
-}
diff --git a/packages/react/tsup.config.ts b/packages/react/tsup.config.ts
index a84245c3..f7f20af3 100644
--- a/packages/react/tsup.config.ts
+++ b/packages/react/tsup.config.ts
@@ -29,5 +29,4 @@ export default defineConfig({
".tsx": "tsx",
".jsx": "jsx",
},
- tsconfig: "./tsconfig.app.json",
});
diff --git a/packages/react/vite.config.ts b/packages/react/vite.config.ts
index b1a73ecf..2e7481aa 100644
--- a/packages/react/vite.config.ts
+++ b/packages/react/vite.config.ts
@@ -24,7 +24,9 @@ export default defineConfig({
plugins: [react()],
resolve: {
alias: {
- "@firebase-ui/core": path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../firebaseui-core/src"),
+ "@firebase-ui/core": path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../core/src"),
+ "@firebase-ui/styles": path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../styles/src"),
+ "~/tests": path.resolve(path.dirname(fileURLToPath(import.meta.url)), "./tests"),
"~": path.resolve(path.dirname(fileURLToPath(import.meta.url)), "./src"),
},
},
diff --git a/packages/react/vitest.config.ts b/packages/react/vitest.config.ts
index 322eefdb..9b7fbe6d 100644
--- a/packages/react/vitest.config.ts
+++ b/packages/react/vitest.config.ts
@@ -14,36 +14,20 @@
* limitations under the License.
*/
-import { defineConfig } from "vitest/config";
-import { resolve } from "path";
-import { fileURLToPath } from "node:url";
+import { mergeConfig } from 'vitest/config'
+import viteConfig from "./vite.config";
-export default defineConfig({
+export default mergeConfig(viteConfig, {
test: {
+ name: "@firebase-ui/react",
// Use the same environment as the package
environment: "jsdom",
// Include TypeScript files
include: ["**/*.{test,spec}.{js,ts,jsx,tsx}"],
// Exclude build output and node_modules
exclude: ["node_modules/**/*", "dist/**/*"],
- // Enable globals for jest-dom to work correctly
- globals: true,
- // Use the setup file for all tests
setupFiles: ["./setup-test.ts"],
- // Mock modules
- mockReset: false,
- // Use tsconfig.test.json for TypeScript
- typecheck: {
- enabled: true,
- tsconfig: "./tsconfig.test.json",
- include: ["**/*.{ts,tsx}"],
- },
- // Increase test timeout for Firebase operations
- testTimeout: 15000,
- },
- resolve: {
- alias: {
- "~": resolve(resolve(fileURLToPath(import.meta.url), ".."), "./src"),
- },
+ // Only see logs from failing tests.
+ silent: 'passed-only',
},
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index eba5d045..75048db8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -566,8 +566,8 @@ importers:
specifier: ^1.2.3
version: 1.2.3(@types/react@19.1.16)(react@19.1.1)
'@tanstack/react-form':
- specifier: ^0.41.3
- version: 0.41.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2)
+ specifier: ^1.20.0
+ version: 1.23.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
clsx:
specifier: ^2.1.1
version: 2.1.1
@@ -2922,44 +2922,6 @@ packages:
'@types/react':
optional: true
- '@remix-run/node@2.17.1':
- resolution: {integrity: sha512-pHmHTuLE1Lwazulx3gjrHobgBCsa+Xiq8WUO0ruLeDfEw2DU0c0SNSiyNkugu3rIZautroBwRaOoy7CWJL9xhQ==}
- engines: {node: '>=18.0.0'}
- peerDependencies:
- typescript: ^5.1.0
- peerDependenciesMeta:
- typescript:
- optional: true
-
- '@remix-run/router@1.23.0':
- resolution: {integrity: sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==}
- engines: {node: '>=14.0.0'}
-
- '@remix-run/server-runtime@2.17.1':
- resolution: {integrity: sha512-d1Vp9FxX4KafB111vP2E5C1fmWzPI+gHZ674L1drq+N8Bp9U6FBspi7GAZSU5K5Kxa4T6UF+aE1gK6pVi9R8sw==}
- engines: {node: '>=18.0.0'}
- peerDependencies:
- typescript: ^5.1.0
- peerDependenciesMeta:
- typescript:
- optional: true
-
- '@remix-run/web-blob@3.1.0':
- resolution: {integrity: sha512-owGzFLbqPH9PlKb8KvpNJ0NO74HWE2euAn61eEiyCXX/oteoVzTVSN8mpLgDjaxBf2btj5/nUllSUgpyd6IH6g==}
-
- '@remix-run/web-fetch@4.4.2':
- resolution: {integrity: sha512-jgKfzA713/4kAW/oZ4bC3MoLWyjModOVDjFPNseVqcJKSafgIscrYL9G50SurEYLswPuoU3HzSbO0jQCMYWHhA==}
- engines: {node: ^10.17 || >=12.3}
-
- '@remix-run/web-file@3.1.0':
- resolution: {integrity: sha512-dW2MNGwoiEYhlspOAXFBasmLeYshyAyhIdrlXBi06Duex5tDr3ut2LFKVj7tyHLmn8nnNwFf1BjNbkQpygC2aQ==}
-
- '@remix-run/web-form-data@3.1.0':
- resolution: {integrity: sha512-NdeohLMdrb+pHxMQ/Geuzdp0eqPbea+Ieo8M8Jx2lGC6TBHsgHzYcBvr0LyPdPVycNRDEpWpiDdCOdCryo3f9A==}
-
- '@remix-run/web-stream@1.1.0':
- resolution: {integrity: sha512-KRJtwrjRV5Bb+pM7zxcTJkhIqWWSy+MYsIxHK+0m5atcznsf15YwUBWHWulZerV2+vvHH1Lp1DD7pw6qKW8SgA==}
-
'@rolldown/binding-android-arm64@1.0.0-beta.32':
resolution: {integrity: sha512-Gs+313LfR4Ka3hvifdag9r44WrdKQaohya7ZXUXzARF7yx0atzFlVZjsvxtKAw1Vmtr4hB/RjUD1jf73SW7zDw==}
cpu: [arm64]
@@ -3351,22 +3313,22 @@ packages:
resolution: {integrity: sha512-gkvph/YMCFUfAca75EsJBJnhbKitDGix7vdEcT/3lAV+eyGSv+uECYG43apVQN4yLJKnV6mzcNvGzOhDhb72gg==}
engines: {node: '>=18'}
- '@tanstack/form-core@0.41.4':
- resolution: {integrity: sha512-XZJtN7mWJmi3apsc2J+GpWbcsXbv0pWBkZKP47ZW1QD/2Tj1UWsM6JjcaAkzIlrBdaoEFYmrHToLKr/Ddk8BVg==}
-
'@tanstack/form-core@0.42.1':
resolution: {integrity: sha512-jTU0jyHqFceujdtPNv3jPVej1dTqBwa8TYdIyWB5BCwRVUBZEp1PiYEBkC9r92xu5fMpBiKc+JKud3eeVjuMiA==}
'@tanstack/form-core@1.24.0':
resolution: {integrity: sha512-bMxl7cwBt6WYiajImYN/0fjYRuiTAW7+3C/zPHNyxLTc6U5voPGr1lF1RGzf3U5Q8qtvHyGF0dVuISMLqb31yQ==}
- '@tanstack/react-form@0.41.4':
- resolution: {integrity: sha512-uIfIDZJNqR1dLW03TNByK/woyKd2jfXIrEBq6DPJbqupqyfYXTDo5TMd/7koTYLO4dgTM5wd+2v3uBX3M2bRaA==}
+ '@tanstack/form-core@1.24.1':
+ resolution: {integrity: sha512-nI9Ad5JFHQjADkUQQtl7ajb7Lsd4sQlR0b5BmKDpJfZSPH/BF7+uUC3m91dM1bucOqIDzTTXr0QROZODnY+www==}
+
+ '@tanstack/react-form@1.23.5':
+ resolution: {integrity: sha512-WygrvTK7Fn3RmSBG5V3FIzH2Z+3FDbau7kaqa/opLiGsX2sx98ZAPvF5umXCe3fmppfJe+nBFYuqY+gaUrSf7w==}
peerDependencies:
- '@tanstack/start': ^1.43.13
+ '@tanstack/react-start': ^1.130.10
react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
- '@tanstack/start':
+ '@tanstack/react-start':
optional: true
'@tanstack/react-store@0.7.7':
@@ -3448,9 +3410,6 @@ packages:
'@types/connect@3.4.38':
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
- '@types/cookie@0.6.0':
- resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
-
'@types/cors@2.8.19':
resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==}
@@ -3821,9 +3780,6 @@ packages:
'@vue/shared@3.5.21':
resolution: {integrity: sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==}
- '@web3-storage/multipart-parser@1.0.0':
- resolution: {integrity: sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==}
-
'@webassemblyjs/ast@1.14.1':
resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==}
@@ -3878,17 +3834,10 @@ packages:
'@yarnpkg/lockfile@1.1.0':
resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==}
- '@zxing/text-encoding@0.9.0':
- resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==}
-
abbrev@3.0.1:
resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==}
engines: {node: ^18.17.0 || >=20.5.0}
- abort-controller@3.0.0:
- resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
- engines: {node: '>=6.5'}
-
accepts@1.3.8:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
@@ -4482,10 +4431,6 @@ packages:
damerau-levenshtein@1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
- data-uri-to-buffer@3.0.1:
- resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==}
- engines: {node: '>= 6'}
-
data-urls@5.0.0:
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
engines: {node: '>=18'}
@@ -4546,8 +4491,8 @@ packages:
decimal.js@10.6.0:
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
- decode-formdata@0.8.0:
- resolution: {integrity: sha512-iUzDgnWsw5ToSkFY7VPFA5Gfph6ROoOxOB7Ybna4miUSzLZ4KaSJk6IAB2AdW6+C9vCVWhjjNA4gjT6wF3eZHQ==}
+ decode-formdata@0.9.0:
+ resolution: {integrity: sha512-q5uwOjR3Um5YD+ZWPOF/1sGHVW9A5rCrRwITQChRXlmPkxDFBqCm4jNTIVdGHNH9OnR+V9MoZVgRhsFb+ARbUw==}
deep-eql@5.0.2:
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
@@ -4612,6 +4557,9 @@ packages:
detect-node@2.1.0:
resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==}
+ devalue@5.3.2:
+ resolution: {integrity: sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw==}
+
di@0.0.1:
resolution: {integrity: sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==}
@@ -4944,10 +4892,6 @@ packages:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
- event-target-shim@5.0.1:
- resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
- engines: {node: '>=6'}
-
eventemitter3@4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
@@ -5430,10 +5374,6 @@ packages:
resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==}
engines: {node: '>= 10'}
- is-arguments@1.2.0:
- resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==}
- engines: {node: '>= 0.4'}
-
is-array-buffer@3.0.5:
resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
engines: {node: '>= 0.4'}
@@ -6158,10 +6098,6 @@ packages:
mlly@1.8.0:
resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
- mrmime@1.0.1:
- resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==}
- engines: {node: '>=10'}
-
mrmime@2.0.1:
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
engines: {node: '>=10'}
@@ -7204,9 +7140,6 @@ packages:
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
engines: {node: '>= 0.4'}
- stream-slice@0.1.2:
- resolution: {integrity: sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==}
-
streamroller@3.1.5:
resolution: {integrity: sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==}
engines: {node: '>=8.0'}
@@ -7508,9 +7441,6 @@ packages:
resolution: {integrity: sha512-3T3T04WzowbwV2FDiGXBbr81t64g1MUGGJRgT4x5o97N+8ArdhVCAF9IxFrxuSJmM3E5Asn7nKHkao0ibcZXAg==}
engines: {node: ^18.17.0 || >=20.5.0}
- turbo-stream@2.4.1:
- resolution: {integrity: sha512-v8kOJXpG3WoTN/+at8vK7erSzo6nW6CIaeOvNOkHQVDajfz1ZVeSxCbc6tOH4hrGZW7VUCV0TOXd8CPzYnYkrw==}
-
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@@ -7573,10 +7503,6 @@ packages:
undici-types@7.10.0:
resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==}
- undici@6.21.3:
- resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==}
- engines: {node: '>=18.17'}
-
unicode-canonical-property-names-ecmascript@2.0.1:
resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==}
engines: {node: '>=4'}
@@ -7633,9 +7559,6 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
- util@0.12.5:
- resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==}
-
utils-merge@1.0.1:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
@@ -7910,13 +7833,6 @@ packages:
weak-lru-cache@1.2.2:
resolution: {integrity: sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==}
- web-encoding@1.1.5:
- resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==}
-
- web-streams-polyfill@3.3.3:
- resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
- engines: {node: '>= 8'}
-
web-vitals@4.2.4:
resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==}
@@ -10866,60 +10782,6 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.16
- '@remix-run/node@2.17.1(typescript@5.9.2)':
- dependencies:
- '@remix-run/server-runtime': 2.17.1(typescript@5.9.2)
- '@remix-run/web-fetch': 4.4.2
- '@web3-storage/multipart-parser': 1.0.0
- cookie-signature: 1.2.2
- source-map-support: 0.5.21
- stream-slice: 0.1.2
- undici: 6.21.3
- optionalDependencies:
- typescript: 5.9.2
-
- '@remix-run/router@1.23.0': {}
-
- '@remix-run/server-runtime@2.17.1(typescript@5.9.2)':
- dependencies:
- '@remix-run/router': 1.23.0
- '@types/cookie': 0.6.0
- '@web3-storage/multipart-parser': 1.0.0
- cookie: 0.7.2
- set-cookie-parser: 2.7.1
- source-map: 0.7.6
- turbo-stream: 2.4.1
- optionalDependencies:
- typescript: 5.9.2
-
- '@remix-run/web-blob@3.1.0':
- dependencies:
- '@remix-run/web-stream': 1.1.0
- web-encoding: 1.1.5
-
- '@remix-run/web-fetch@4.4.2':
- dependencies:
- '@remix-run/web-blob': 3.1.0
- '@remix-run/web-file': 3.1.0
- '@remix-run/web-form-data': 3.1.0
- '@remix-run/web-stream': 1.1.0
- '@web3-storage/multipart-parser': 1.0.0
- abort-controller: 3.0.0
- data-uri-to-buffer: 3.0.1
- mrmime: 1.0.1
-
- '@remix-run/web-file@3.1.0':
- dependencies:
- '@remix-run/web-blob': 3.1.0
-
- '@remix-run/web-form-data@3.1.0':
- dependencies:
- web-encoding: 1.1.5
-
- '@remix-run/web-stream@1.1.0':
- dependencies:
- web-streams-polyfill: 3.3.3
-
'@rolldown/binding-android-arm64@1.0.0-beta.32':
optional: true
@@ -11253,29 +11115,29 @@ snapshots:
'@tanstack/devtools-event-client@0.3.2': {}
- '@tanstack/form-core@0.41.4':
+ '@tanstack/form-core@0.42.1':
dependencies:
'@tanstack/store': 0.7.7
- '@tanstack/form-core@0.42.1':
+ '@tanstack/form-core@1.24.0':
dependencies:
+ '@tanstack/devtools-event-client': 0.3.2
'@tanstack/store': 0.7.7
- '@tanstack/form-core@1.24.0':
+ '@tanstack/form-core@1.24.1':
dependencies:
'@tanstack/devtools-event-client': 0.3.2
'@tanstack/store': 0.7.7
- '@tanstack/react-form@0.41.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2)':
+ '@tanstack/react-form@1.23.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
dependencies:
- '@remix-run/node': 2.17.1(typescript@5.9.2)
- '@tanstack/form-core': 0.41.4
+ '@tanstack/form-core': 1.24.1
'@tanstack/react-store': 0.7.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
- decode-formdata: 0.8.0
+ decode-formdata: 0.9.0
+ devalue: 5.3.2
react: 19.1.1
transitivePeerDependencies:
- react-dom
- - typescript
'@tanstack/react-store@0.7.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
dependencies:
@@ -11377,8 +11239,6 @@ snapshots:
dependencies:
'@types/node': 20.19.13
- '@types/cookie@0.6.0': {}
-
'@types/cors@2.8.19':
dependencies:
'@types/node': 20.19.13
@@ -11859,8 +11719,6 @@ snapshots:
'@vue/shared@3.5.21': {}
- '@web3-storage/multipart-parser@1.0.0': {}
-
'@webassemblyjs/ast@1.14.1':
dependencies:
'@webassemblyjs/helper-numbers': 1.13.2
@@ -11943,15 +11801,8 @@ snapshots:
'@yarnpkg/lockfile@1.1.0': {}
- '@zxing/text-encoding@0.9.0':
- optional: true
-
abbrev@3.0.1: {}
- abort-controller@3.0.0:
- dependencies:
- event-target-shim: 5.0.1
-
accepts@1.3.8:
dependencies:
mime-types: 2.1.35
@@ -12619,8 +12470,6 @@ snapshots:
damerau-levenshtein@1.0.8: {}
- data-uri-to-buffer@3.0.1: {}
-
data-urls@5.0.0:
dependencies:
whatwg-mimetype: 4.0.0
@@ -12668,7 +12517,7 @@ snapshots:
decimal.js@10.6.0: {}
- decode-formdata@0.8.0: {}
+ decode-formdata@0.9.0: {}
deep-eql@5.0.2: {}
@@ -12714,6 +12563,8 @@ snapshots:
detect-node@2.1.0: {}
+ devalue@5.3.2: {}
+
di@0.0.1:
optional: true
@@ -13232,8 +13083,6 @@ snapshots:
etag@1.8.1: {}
- event-target-shim@5.0.1: {}
-
eventemitter3@4.0.7: {}
eventemitter3@5.0.1: {}
@@ -13827,11 +13676,6 @@ snapshots:
ipaddr.js@2.2.0: {}
- is-arguments@1.2.0:
- dependencies:
- call-bound: 1.0.4
- has-tostringtag: 1.0.2
-
is-array-buffer@3.0.5:
dependencies:
call-bind: 1.0.8
@@ -14600,8 +14444,6 @@ snapshots:
pkg-types: 1.3.1
ufo: 1.6.1
- mrmime@1.0.1: {}
-
mrmime@2.0.1: {}
ms@2.0.0: {}
@@ -15865,8 +15707,6 @@ snapshots:
es-errors: 1.3.0
internal-slot: 1.1.0
- stream-slice@0.1.2: {}
-
streamroller@3.1.5:
dependencies:
date-format: 4.0.14
@@ -16186,8 +16026,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
- turbo-stream@2.4.1: {}
-
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
@@ -16260,8 +16098,6 @@ snapshots:
undici-types@7.10.0: {}
- undici@6.21.3: {}
-
unicode-canonical-property-names-ecmascript@2.0.1: {}
unicode-match-property-ecmascript@2.0.0:
@@ -16328,14 +16164,6 @@ snapshots:
util-deprecate@1.0.2: {}
- util@0.12.5:
- dependencies:
- inherits: 2.0.4
- is-arguments: 1.2.0
- is-generator-function: 1.1.0
- is-typed-array: 1.1.15
- which-typed-array: 1.1.19
-
utils-merge@1.0.1: {}
uuid@8.3.2: {}
@@ -16690,14 +16518,6 @@ snapshots:
weak-lru-cache@1.2.2:
optional: true
- web-encoding@1.1.5:
- dependencies:
- util: 0.12.5
- optionalDependencies:
- '@zxing/text-encoding': 0.9.0
-
- web-streams-polyfill@3.3.3: {}
-
web-vitals@4.2.4: {}
webidl-conversions@4.0.2: {}