Skip to content

Commit 027c76d

Browse files
feat: implementation login screen (#39)
* feat: implementation login screen * ci: deployment for login screen * fix: tooltip addition * fix: border and ring fix for floating label field * fix: css fix * test: snapshot test fix * fix: reert border length changes * fix: ul theme link component used
1 parent f89cce9 commit 027c76d

32 files changed

+1448
-305
lines changed

.github/config/deploy_config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
default_screen_deployment_status:
77
"login-id": true
8-
8+
"login": true
99
# All other valid screens (commented out by default):
1010
# "email-identifier-challenge": true
1111
# "interstitial-captcha": true

package-lock.json

Lines changed: 161 additions & 212 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
},
1616
"dependencies": {
1717
"@auth0/auth0-acul-js": "0.1.0-beta.7",
18-
"@base-ui-components/react": "1.0.0-beta.1",
18+
"@base-ui-components/react": "^1.0.0-beta.1",
1919
"class-variance-authority": "^0.7.1",
2020
"clsx": "^2.1.1",
2121
"lucide-react": "^0.525.0",
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* @file This file provides a comprehensive mock for the Auth0 ACUL JS Login screen.
3+
* It is designed to be structurally aligned with the official SDK, enabling robust
4+
* and isolated testing of our components.
5+
*
6+
* It mocks the following core parts of the SDK:
7+
* - `Login` class: {@link https://auth0.github.io/universal-login/classes/Classes.Login.html}
8+
* - `ScreenMembersOnLogin`: {@link https://auth0.github.io/universal-login/interfaces/Classes.ScreenMembersOnLogin.html}
9+
* - `TransactionMembersOnLogin`: {@link https://auth0.github.io/universal-login/interfaces/Classes.TransactionMembersOnLogin.html}
10+
*/
11+
import type {
12+
ScreenMembersOnLogin,
13+
TransactionMembersOnLogin,
14+
} from "@auth0/auth0-acul-js";
15+
16+
/**
17+
* Defines the "contract" for our mock. It combines the methods from the main
18+
* `Login` class with the `screen` and `transaction` data structures.
19+
* This provides a single, type-safe object to control in our tests.
20+
*/
21+
export interface MockLoginInstance {
22+
login: jest.Mock;
23+
federatedLogin: jest.Mock;
24+
getError: jest.Mock;
25+
screen: ScreenMembersOnLogin;
26+
transaction: TransactionMembersOnLogin;
27+
}
28+
29+
/**
30+
* Factory function to create a new mock instance of the `Login` class.
31+
* This ensures each test gets a clean, isolated mock object that is
32+
* structurally aligned with the official SDK documentation.
33+
*/
34+
export const createMockLoginInstance = (): MockLoginInstance => ({
35+
login: jest.fn(),
36+
federatedLogin: jest.fn(),
37+
getError: jest.fn(() => []), // Returns empty array by default
38+
screen: {
39+
name: "login",
40+
texts: {
41+
title: "Mock Welcome Title",
42+
description: "Mock description text.",
43+
usernamePlaceholder: "Username",
44+
emailPlaceholder: "Email Address",
45+
usernameOrEmailPlaceholder: "Username or Email Address",
46+
passwordPlaceholder: "Password",
47+
buttonText: "Mock Continue",
48+
forgotPasswordText: "Can't log in?",
49+
separatorText: "Or",
50+
signupActionLinkText: "Sign up",
51+
captchaCodePlaceholder: "Enter the code shown above",
52+
},
53+
// Structurally correct captcha properties
54+
isCaptchaAvailable: false,
55+
captchaProvider: null,
56+
captchaSiteKey: null,
57+
captchaImage: null,
58+
captcha: null,
59+
// Use direct link properties
60+
signupLink: "/signup",
61+
resetPasswordLink: "/reset",
62+
// Base properties that must exist
63+
links: {},
64+
data: {},
65+
},
66+
transaction: {
67+
// Declarative boolean flags for UI logic
68+
isSignupEnabled: true,
69+
isForgotPasswordEnabled: true,
70+
isPasskeyEnabled: false,
71+
hasErrors: false,
72+
// Default transaction state
73+
errors: [],
74+
alternateConnections: [
75+
{
76+
name: "google-oauth2",
77+
strategy: "google",
78+
options: { displayName: "Google", showAsButton: true },
79+
},
80+
{
81+
name: "github",
82+
strategy: "github",
83+
options: { displayName: "Github", showAsButton: true },
84+
},
85+
],
86+
locale: "en",
87+
state: "g-state",
88+
currentConnection: null,
89+
connectionStrategy: null,
90+
passwordPolicy: {
91+
minLength: 8,
92+
policy: "good",
93+
},
94+
allowedIdentifiers: ["email", "username"],
95+
countryCode: null,
96+
countryPrefix: null,
97+
},
98+
});
99+
100+
// Default mock implementation
101+
const createDefaultMock = () => createMockLoginInstance();
102+
103+
export default jest.fn().mockImplementation(createDefaultMock);

src/components/ULThemeLink.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as React from "react";
2+
23
import { Link, type LinkProps } from "@/components/ui/link";
34
import { cn } from "@/lib/utils";
45
import { extractTokenValue } from "@/utils/helpers/tokenUtils";
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { useState } from "react";
2+
3+
import { Eye, EyeOff } from "lucide-react";
4+
5+
import {
6+
ULThemeFloatingLabelField,
7+
type ULThemeFloatingLabelFieldProps,
8+
} from "@/components/form/ULThemeFloatingLabelField";
9+
import { Button } from "@/components/ui/button";
10+
import {
11+
Tooltip,
12+
TooltipContent,
13+
TooltipTrigger,
14+
} from "@/components/ui/tooltip";
15+
import { cn } from "@/lib/utils";
16+
17+
export interface ULThemePasswordFieldProps
18+
extends Omit<ULThemeFloatingLabelFieldProps, "type" | "endAdornment"> {
19+
onVisibilityToggle?: (isVisible: boolean) => void;
20+
buttonClassName?: string;
21+
}
22+
23+
export const ULThemePasswordField = ({
24+
onVisibilityToggle,
25+
buttonClassName,
26+
...props
27+
}: ULThemePasswordFieldProps) => {
28+
const [showPassword, setShowPassword] = useState(false);
29+
30+
const handleToggle = () => {
31+
const newState = !showPassword;
32+
setShowPassword(newState);
33+
onVisibilityToggle?.(newState);
34+
};
35+
36+
const passwordButton = (
37+
<Tooltip>
38+
<TooltipTrigger className="w-full h-full">
39+
<Button
40+
variant="ghost"
41+
size="icon"
42+
type="button"
43+
onClick={handleToggle}
44+
className={cn(
45+
// Layout & Positioning
46+
"cursor-pointer h-full w-full min-w-[44px] mr-[-5px]",
47+
48+
// Border Radius - matches input field
49+
"theme-universal:rounded-r-input theme-universal:rounded-l-none",
50+
51+
// Colors
52+
"theme-universal:text-input-labels",
53+
"theme-universal:hover:text-input-text",
54+
55+
// Transitions
56+
"transition-colors",
57+
58+
// Focus States
59+
"theme-universal:focus:bg-base-focus/15 theme-universal:focus-visible:ring-0 theme-universal:focus-visible:ring-offset-0",
60+
61+
// Layout
62+
"flex items-center justify-center",
63+
buttonClassName
64+
)}
65+
aria-label={showPassword ? "Hide password" : "Show password"}
66+
>
67+
{showPassword ? <EyeOff /> : <Eye />}
68+
</Button>
69+
</TooltipTrigger>
70+
<TooltipContent
71+
sideOffset={0}
72+
className="bg-black text-white -mb-2 ml-0.5"
73+
>
74+
{showPassword ? "Hide password" : "Show password"}
75+
</TooltipContent>
76+
</Tooltip>
77+
);
78+
79+
return (
80+
<ULThemeFloatingLabelField
81+
{...props}
82+
type={showPassword ? "text" : "password"}
83+
endAdornment={passwordButton}
84+
/>
85+
);
86+
};
87+
88+
ULThemePasswordField.displayName = "ULThemePasswordField";

src/components/ULThemeSocialProviderButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { cn } from "@/lib/utils";
21
import { Button, type ButtonProps } from "@/components/ui/button";
2+
import { cn } from "@/lib/utils";
33

44
export interface ULThemeSocialProviderButtonProps
55
extends React.HTMLAttributes<HTMLButtonElement>,

src/components/__tests__/ULThemeLink.test.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { render, screen, fireEvent } from "@testing-library/react";
1+
import { fireEvent, render, screen } from "@testing-library/react";
2+
23
import ULThemeLink from "../ULThemeLink";
34

45
describe("ULThemeLink Component", () => {
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { useForm } from "react-hook-form";
2+
3+
import { fireEvent, render, screen } from "@testing-library/react";
4+
5+
import { Form } from "@/components/ui/form";
6+
import { ULThemePasswordField } from "@/components/ULThemePasswordField";
7+
8+
// Wrapper component for tests that need form context
9+
function TestFormWrapper({ children }: { children: React.ReactNode }) {
10+
const form = useForm({
11+
defaultValues: {
12+
password: "",
13+
},
14+
});
15+
16+
return <Form {...form}>{children}</Form>;
17+
}
18+
19+
describe("ULThemePasswordField", () => {
20+
it("renders password field with toggle button", () => {
21+
render(
22+
<TestFormWrapper>
23+
<ULThemePasswordField
24+
label="Password"
25+
name="password"
26+
placeholder="Enter password"
27+
/>
28+
</TestFormWrapper>
29+
);
30+
31+
const passwordInput = screen.getByLabelText("Password");
32+
const toggleButton = screen.getByRole("button", { name: /show password/i });
33+
34+
expect(passwordInput).toHaveAttribute("type", "password");
35+
expect(toggleButton).toBeInTheDocument();
36+
});
37+
38+
it("toggles password visibility when button is clicked", () => {
39+
render(
40+
<TestFormWrapper>
41+
<ULThemePasswordField
42+
label="Password"
43+
name="password"
44+
placeholder="Enter password"
45+
/>
46+
</TestFormWrapper>
47+
);
48+
49+
const passwordInput = screen.getByLabelText("Password");
50+
const toggleButton = screen.getByRole("button", { name: /show password/i });
51+
52+
// Initially password type
53+
expect(passwordInput).toHaveAttribute("type", "password");
54+
55+
// Click to show password
56+
fireEvent.click(toggleButton);
57+
expect(passwordInput).toHaveAttribute("type", "text");
58+
expect(
59+
screen.getByRole("button", { name: /hide password/i })
60+
).toBeInTheDocument();
61+
62+
// Click to hide password again
63+
fireEvent.click(toggleButton);
64+
expect(passwordInput).toHaveAttribute("type", "password");
65+
expect(
66+
screen.getByRole("button", { name: /show password/i })
67+
).toBeInTheDocument();
68+
});
69+
70+
it("calls onVisibilityToggle callback when toggled", () => {
71+
const handleVisibilityToggle = jest.fn();
72+
73+
render(
74+
<TestFormWrapper>
75+
<ULThemePasswordField
76+
label="Password"
77+
name="password"
78+
placeholder="Enter password"
79+
onVisibilityToggle={handleVisibilityToggle}
80+
/>
81+
</TestFormWrapper>
82+
);
83+
84+
const toggleButton = screen.getByRole("button", { name: /show password/i });
85+
86+
// Click to show password
87+
fireEvent.click(toggleButton);
88+
expect(handleVisibilityToggle).toHaveBeenCalledWith(true);
89+
90+
// Click to hide password
91+
fireEvent.click(toggleButton);
92+
expect(handleVisibilityToggle).toHaveBeenCalledWith(false);
93+
});
94+
95+
it("accepts all standard input props", () => {
96+
render(
97+
<TestFormWrapper>
98+
<ULThemePasswordField
99+
label="Password"
100+
name="password"
101+
placeholder="Enter password"
102+
disabled
103+
data-testid="password-input"
104+
/>
105+
</TestFormWrapper>
106+
);
107+
108+
const passwordInput = screen.getByLabelText("Password");
109+
expect(passwordInput).toBeDisabled();
110+
});
111+
});

src/components/__tests__/ULThemeSocialProviderButton.test.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { render, screen, fireEvent } from "@testing-library/react";
1+
import { fireEvent, render, screen } from "@testing-library/react";
2+
23
import ULThemeSocialProviderButton from "../ULThemeSocialProviderButton";
34

45
describe("ULThemeSocialProviderButton Component", () => {

0 commit comments

Comments
 (0)