-
Notifications
You must be signed in to change notification settings - Fork 2
feat(sign-in-page): implement sign-in page with email and password inputs, Google sign-in button, fix footer position, and test #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat/resource-list-api-integration
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import { render } from "@testing-library/react"; | ||
| import { describe, it, expect } from "vitest"; | ||
| import { GoogleLogo } from "./GoogleLogo"; | ||
|
|
||
| interface RenderOptions { | ||
| className?: string; | ||
| } | ||
|
|
||
| const defaultProps: RenderOptions = { | ||
| className: "w-5 h-5", | ||
| }; | ||
|
|
||
| const renderGoogleLogo = (additionalProps: RenderOptions = {}) => { | ||
| const props = { ...defaultProps, ...additionalProps }; | ||
|
|
||
| return render(<GoogleLogo className={props.className} />); | ||
| }; | ||
|
|
||
| describe("GoogleLogo", () => { | ||
| it("should render an svg element", () => { | ||
| const { container } = renderGoogleLogo(); | ||
|
|
||
| const svg = container.querySelector("svg"); | ||
|
|
||
| expect(svg).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("should apply provided className when passed", () => { | ||
| const { container } = renderGoogleLogo({ className: "w-8 h-8" }); | ||
|
|
||
| const svg = container.querySelector("svg"); | ||
|
|
||
| expect(svg).toHaveClass("w-8"); | ||
| }); | ||
|
|
||
| it("should be hidden from the accessibility tree", () => { | ||
| const { container } = renderGoogleLogo(); | ||
|
|
||
| const svg = container.querySelector("svg"); | ||
|
|
||
| expect(svg).toHaveAttribute("aria-hidden", "true"); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| type GoogleLogoProps = { | ||
| className?: string; | ||
| }; | ||
|
|
||
| export const GoogleLogo = ({ className }: GoogleLogoProps) => { | ||
| return ( | ||
| <svg | ||
| className={className ?? "w-5 h-5"} | ||
| viewBox="0 0 24 24" | ||
| xmlns="http://www.w3.org/2000/svg" | ||
| aria-hidden="true" | ||
| focusable="false" | ||
| > | ||
| <path | ||
| fill="#4285F4" | ||
| d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" | ||
| /> | ||
| <path | ||
| fill="#34A853" | ||
| d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" | ||
| /> | ||
| <path | ||
| fill="#FBBC05" | ||
| d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" | ||
| /> | ||
| <path | ||
| fill="#EA4335" | ||
| d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" | ||
| /> | ||
| </svg> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| import { render, screen } from "@testing-library/react"; | ||
| import userEvent from "@testing-library/user-event"; | ||
| import { describe, it } from "vitest"; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Severity: 🟠 Major Import import { describe, it, expect } from "vitest"; |
||
| import { BrowserRouter } from "react-router-dom"; | ||
| import { SignInPage } from "./SignInPage"; | ||
|
|
||
| const renderSignInPage = () => { | ||
| return render( | ||
| <BrowserRouter> | ||
| <SignInPage /> | ||
| </BrowserRouter>, | ||
| ); | ||
| }; | ||
|
|
||
| describe("SignInPage", () => { | ||
| it("should render the email input field", () => { | ||
| renderSignInPage(); | ||
|
|
||
| expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("should render the password input field", () => { | ||
| renderSignInPage(); | ||
|
|
||
| expect(screen.getByLabelText(/password/i)).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("should allow typing in the email input", async () => { | ||
| renderSignInPage(); | ||
| const user = userEvent.setup(); | ||
|
|
||
| const emailInput = screen.getByLabelText(/email/i); | ||
| await user.type(emailInput, "test@example.com"); | ||
|
|
||
| expect(emailInput).toHaveValue("test@example.com"); | ||
| }); | ||
|
|
||
| it("should allow typing in the password input", async () => { | ||
| renderSignInPage(); | ||
| const user = userEvent.setup(); | ||
|
|
||
| const passwordInput = screen.getByLabelText(/password/i); | ||
| await user.type(passwordInput, "password123"); | ||
|
|
||
| expect(passwordInput).toHaveValue("password123"); | ||
| }); | ||
|
|
||
| it("should render the sign in button", () => { | ||
| renderSignInPage(); | ||
|
|
||
| expect( | ||
| screen.getByRole("button", { name: /sign in/i }), | ||
| ).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("should render the continue with google button", () => { | ||
| renderSignInPage(); | ||
|
|
||
| expect( | ||
| screen.getByRole("button", { name: /continue with google/i }), | ||
| ).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("should render the sign up link", () => { | ||
| renderSignInPage(); | ||
|
|
||
| expect(screen.getByRole("link", { name: /sign up/i })).toBeInTheDocument(); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| import { useState } from "react"; | ||
| import { Link } from "react-router-dom"; | ||
| import { Button } from "../../components/button/Button"; | ||
| import { Input } from "../../components/input/Input"; | ||
| import { Card, CardBody } from "../../components/card/Card"; | ||
| import { GoogleLogo } from "../../components/googlelogo/GoogleLogo"; | ||
|
|
||
| export const SignInPage = () => { | ||
| const [email, setEmail] = useState(""); | ||
| const [password, setPassword] = useState(""); | ||
|
|
||
| const handleSubmit = (event: React.FormEvent) => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Severity: 🔴 Critical Avoid relying on a global import { useState, type FormEvent } from "react";
...
const handleSubmit = (event: FormEvent<HTMLFormElement>) => { |
||
| event.preventDefault(); | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="min-h-screen bg-linear-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4"> | ||
| <div className="w-full max-w-md"> | ||
| <Card> | ||
| <CardBody className="space-y-6"> | ||
| <div className="text-center"> | ||
| <h1 className="text-2xl font-bold text-gray-900 mb-2"> | ||
| Welcome Back | ||
| </h1> | ||
| <p className="text-gray-600"> | ||
| Sign in to access your saved resources | ||
| </p> | ||
| </div> | ||
|
|
||
| <form onSubmit={handleSubmit} className="space-y-4"> | ||
| <Input | ||
| label="Email" | ||
| type="email" | ||
| placeholder="Enter your email" | ||
| value={email} | ||
| onChange={(event) => setEmail(event.target.value)} | ||
| className="w-full" | ||
| required | ||
| autoComplete="email" | ||
| /> | ||
|
|
||
| <Input | ||
| label="Password" | ||
| type="password" | ||
| placeholder="Enter your password" | ||
| value={password} | ||
| onChange={(event) => setPassword(event.target.value)} | ||
| className="w-full" | ||
| required | ||
| autoComplete="current password" | ||
| /> | ||
|
|
||
| <Button type="submit" className="w-full"> | ||
| Sign In | ||
| </Button> | ||
| </form> | ||
|
|
||
| <div className="relative"> | ||
| <div className="absolute inset-0 flex items-center"> | ||
| <div className="w-full border-t border-gray-200" /> | ||
| </div> | ||
| <div className="relative flex justify-center text-sm"> | ||
| <span className="px-2 bg-white text-gray-500"> | ||
| Or continue with | ||
| </span> | ||
| </div> | ||
| </div> | ||
|
|
||
| <Button | ||
| variant="outline" | ||
| type="button" | ||
| className="w-full flex gap-2" | ||
| > | ||
| <GoogleLogo /> | ||
| Continue with Google | ||
| </Button> | ||
|
|
||
| <p className="text-center text-gray-600 text-sm"> | ||
| Don't have an account?{" "} | ||
| <Link | ||
| to="/signup" | ||
| className="text-blue-600 hover:text-blue-700 font-medium" | ||
| > | ||
| Sign up | ||
| </Link> | ||
| </p> | ||
| </CardBody> | ||
| </Card> | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Severity: 🟠 Major
Add a small regression test for the footer-bottom flex layout so future class refactors don’t reintroduce the floating footer.
(For example: render the layout with a minimal router + outlet, then assert the root has
flex flex-coland<main>hasflex-1.)