Skip to content

Commit

Permalink
Finish Sign Up and Login Flow
Browse files Browse the repository at this point in the history
  • Loading branch information
ThomasCode92 authored Nov 20, 2024
1 parent 34bfb6b commit 6205a55
Show file tree
Hide file tree
Showing 25 changed files with 698 additions and 35 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ dist-ssr
# Cache
cache

# Firebase data
firebase/data

# Editor directories and files
.idea
.DS_Store
Expand Down
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"files.associations": {
"*.css": "tailwindcss"
},
"explorer.compactFolders": false,

// Language Specific Config
"[nginx]": {
Expand Down
2 changes: 1 addition & 1 deletion cspell.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"ignoreWords": ["tseslint", "tailwindcss", "testid", "svgr"],
"ignoreWords": ["tseslint", "tailwindcss", "testid", "svgr", "firestore"],
"words": ["crwn"]
}
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ services:
- "4000:4001" # ui
- "4600:4601" # logging
- "9099:9100" # auth
- "8080:8081" # firestore
volumes:
- ./firebase:/srv/firebase:rw
- ./cache:/root/.cache/:rw
Expand Down
7 changes: 6 additions & 1 deletion firebase/firestore.rules
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
rules_version = '2';
service cloud.firestore {
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@eslint/js": "^9.9.0",
"@testing-library/jest-dom": "^6.6.1",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^14.5.2",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.1",
Expand Down
6 changes: 6 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ body {
a {
color: black;
}

#root {
width: 80%;
max-width: 85rem;
margin: auto;
}
4 changes: 2 additions & 2 deletions src/components/NavigationBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ test("should have a link to the shop page", function () {
expect(shopLinkElement).toHaveAttribute("href", "/shop");
});

test("should have a link to the sign in page", function () {
test("should have a link to the authentication page", function () {
render(<NavigationBar />, { wrapper: MemoryRouter });
const signInLinkElement = screen.getByText(/sign in/i);
expect(signInLinkElement).toHaveAttribute("href", "/sign-in");
expect(signInLinkElement).toHaveAttribute("href", "/auth");
});
2 changes: 1 addition & 1 deletion src/components/NavigationBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default function NavigationBar() {
<Link to="/shop">shop</Link>
</li>
<li className="text-lg uppercase">
<Link to="/sign-in">sign in</Link>
<Link to="/auth">sign in</Link>
</li>
</ul>
</nav>
Expand Down
33 changes: 33 additions & 0 deletions src/components/UI/Button.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { render, screen } from "@testing-library/react";

import Button from "./Button";

test("should render with default styles", () => {
render(<Button>Default Button</Button>);
const buttonElement = screen.getByText("Default Button");
expect(buttonElement).toBeInTheDocument();
expect(buttonElement).toHaveClass("bg-black text-white");
});

test("should render with google-sign-in styles", () => {
render(<Button buttonType="google-sign-in">Google Sign In</Button>);
const buttonElement = screen.getByText("Google Sign In");
expect(buttonElement).toBeInTheDocument();
expect(buttonElement).toHaveClass("bg-google text-white");
});

test("should render with inverted styles", () => {
render(<Button buttonType="inverted">Inverted Button</Button>);
const buttonElement = screen.getByText("Inverted Button");
expect(buttonElement).toBeInTheDocument();
expect(buttonElement).toHaveClass(
"border-2 border-black bg-white text-black",
);
});

test("should pass other props to the button element", () => {
render(<Button disabled>Disabled Button</Button>);
const buttonElement = screen.getByText("Disabled Button");
expect(buttonElement).toBeInTheDocument();
expect(buttonElement).toBeDisabled();
});
33 changes: 33 additions & 0 deletions src/components/UI/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ButtonHTMLAttributes, ReactNode } from "react";
import tw from "../../utils/tw-identity";

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode;
buttonType?: "inverted" | "google-sign-in";
}

export default function Button({
children,
buttonType,
...otherProps
}: ButtonProps) {
let classes = tw`h-12 min-w-40 px-8 font-bold uppercase hover:border-2` + " ";

if (!buttonType) {
classes += tw`bg-black text-white hover:border-black hover:bg-white hover:text-black`;
}

if (buttonType === "google-sign-in") {
classes += tw`bg-google text-white hover:border-google hover:bg-white hover:text-google`;
}

if (buttonType === "inverted") {
classes += tw`border-2 border-black bg-white text-black hover:border-white hover:bg-black hover:text-white`;
}

return (
<button className={classes} {...otherProps}>
{children}
</button>
);
}
42 changes: 42 additions & 0 deletions src/components/UI/FormInput.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import FormInput from "./FormInput";

test("should render with the correct label", () => {
render(<FormInput label="Username" />);
const labelElement = screen.getByLabelText("Username");
expect(labelElement).toBeInTheDocument();
});

test("should apply shrink classes when input has value", () => {
render(<FormInput label="Username" value="test" />);
const labelElement = screen.getByText("Username");
expect(labelElement).toHaveClass("-top-5 text-xs text-black");
});

test("should apply default classes when input is empty", () => {
render(<FormInput label="Username" />);
const labelElement = screen.getByText("Username");
expect(labelElement).toHaveClass("top-3 text-gray-600");
});

test("should apply password classes when input type is password", () => {
render(<FormInput label="Password" type="password" />);
const inputElement = screen.getByLabelText("Password");
expect(inputElement).toHaveClass("tracking-wider");
});

test("should focus input when label is clicked", async () => {
render(<FormInput label="Username" />);
const labelElement = screen.getByText("Username");
const inputElement = screen.getByLabelText("Username");
await userEvent.click(labelElement);
expect(inputElement).toHaveFocus();
});

test("should pass other props to the input element", () => {
render(<FormInput label="Username" disabled />);
const inputElement = screen.getByLabelText("Username");
expect(inputElement).toBeDisabled();
});
29 changes: 29 additions & 0 deletions src/components/UI/FormInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { InputHTMLAttributes } from "react";

import tw from "../../utils/tw-identity";

interface FormInputProps extends InputHTMLAttributes<HTMLInputElement> {
label: string;
}

export default function FormInput({ label, ...otherProps }: FormInputProps) {
const inputId = label.toLowerCase().replace(" ", "-");

const shrinkClasses = otherProps.value?.toString().length
? tw`-top-5 text-xs text-black`
: tw`top-3 text-gray-600`;
const passwordClasses =
otherProps.type === "password" ? "tracking-wider" : "";

const inputClasses = tw`peer my-6 block w-full border-b-2 border-b-gray-600 bg-slate-100 py-2.5 pl-1 pr-2.5 text-lg text-gray-600 focus:outline-none ${passwordClasses}`;
const labelClasses = tw`pointer-events-none absolute left-1 transition-all peer-focus:-top-5 peer-focus:text-xs peer-focus:text-black ${shrinkClasses}`;

return (
<div className="relative my-12">
<input id={inputId} className={inputClasses} {...otherProps} />
<label htmlFor={inputId} className={labelClasses}>
{label}
</label>
</div>
);
}
102 changes: 102 additions & 0 deletions src/components/authentication/SignInForm.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import SignInForm from "./SignInForm";

const mockedMethods = vi.hoisted(function () {
return {
signInAuthUserFn: vi.fn(),
};
});

vi.mock("../../utils/firebase", function () {
return {
signInAuthUserWithEmailAndPassword: mockedMethods.signInAuthUserFn,
};
});

vi.spyOn(window, "alert").mockImplementation(() => {});

test("should render the correct titles", function () {
render(<SignInForm />);

expect(screen.getByRole("heading", { name: /sign in/i })).toBeInTheDocument();
expect(screen.getByText(/already have an account/i)).toBeInTheDocument();
});

test("should render the correct form fields", function () {
render(<SignInForm />);

expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
});

test("should render the correct buttons", function () {
render(<SignInForm />);

expect(
screen.getByRole("button", { name: /^sign in$/i }),
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: /google sign in/i }),
).toBeInTheDocument();
});

test("should submit the form with the correct data", async function () {
render(<SignInForm />);

const emailInput = screen.getByLabelText(/email/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole("button", { name: /^sign in$/i });

const user = userEvent.setup();

await user.type(emailInput, "[email protected]");
await user.type(passwordInput, "password");
await user.click(submitButton);

expect(mockedMethods.signInAuthUserFn).toHaveBeenCalledWith(
"[email protected]",
"password",
);
});

test("should show an alert if the password is incorrect", async function () {
mockedMethods.signInAuthUserFn.mockRejectedValue({
code: "auth/wrong-password",
});

render(<SignInForm />);

const emailInput = screen.getByLabelText(/email/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole("button", { name: /^sign in$/i });

const user = userEvent.setup();

await user.type(emailInput, "[email protected]");
await user.type(passwordInput, "password");
await user.click(submitButton);

expect(window.alert).toHaveBeenCalledWith("Wrong email or password");
});

test("should show an alert if the user is not found", async function () {
mockedMethods.signInAuthUserFn.mockRejectedValue({
code: "auth/user-not-found",
});

render(<SignInForm />);

const emailInput = screen.getByLabelText(/email/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole("button", { name: /^sign in$/i });

const user = userEvent.setup();

await user.type(emailInput, "[email protected]");
await user.type(passwordInput, "password");
await user.click(submitButton);

expect(window.alert).toHaveBeenCalledWith("Wrong email or password");
});
Loading

0 comments on commit 6205a55

Please sign in to comment.