-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
24 changed files
with
925 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,10 @@ import "@testing-library/jest-dom"; | |
import { createElement } from "react"; | ||
import type { ImageProps } from "next/image"; | ||
|
||
process.env.SMTP_USER = "mockUser"; | ||
process.env.SMTP_PASSWORD = "mockPassword"; | ||
process.env.SMTP_RECEIVER = "[email protected]"; | ||
|
||
function MockImage({ priority, ...props }: ImageProps) { | ||
return createElement("img", { | ||
...props, | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
import { render, screen, fireEvent, waitFor } from "@testing-library/react"; | ||
import { Contact } from "../models"; | ||
import { ContactForm } from "./ContactForm"; | ||
|
||
global.fetch = jest.fn(() => Promise.resolve()) as unknown as typeof fetch; | ||
|
||
const push = jest.fn(); | ||
|
||
jest.mock("next/navigation", () => ({ | ||
useRouter: () => ({ | ||
push, | ||
}), | ||
})); | ||
|
||
const getSubmitButton = () => screen.getByRole("button", { name: /submit/i }); | ||
|
||
const contact: Record< | ||
keyof Contact, | ||
{ | ||
name: string; | ||
value: string; | ||
} | ||
> = { | ||
name: { name: "Name", value: "John Doe" }, | ||
email: { name: "Email", value: "[email protected]" }, | ||
subject: { name: "Subject", value: "Test Subject" }, | ||
text: { name: "Text", value: "Test Message" }, | ||
}; | ||
|
||
const getFormFields = () => | ||
Object.values(contact).map(({ name }) => | ||
screen.getByRole("textbox", { name }), | ||
); | ||
|
||
const getFormComponents = () => { | ||
return [...getFormFields(), getSubmitButton()]; | ||
}; | ||
|
||
const expectFormToBeEnabled = () => { | ||
getFormComponents().forEach((field) => { | ||
expect(field).toBeEnabled(); | ||
}); | ||
}; | ||
|
||
const expectFormToBeDisabled = () => { | ||
getFormComponents().forEach((field) => { | ||
expect(field).toBeDisabled(); | ||
}); | ||
}; | ||
|
||
describe("ContactForm", () => { | ||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it("should renders form fields and submit button correctly", () => { | ||
render(<ContactForm />); | ||
getFormComponents().forEach((field) => { | ||
expect(field).toBeInTheDocument(); | ||
}); | ||
}); | ||
|
||
it("should submit form data", async () => { | ||
render(<ContactForm />); | ||
|
||
Object.values(contact).forEach(({ name, value }) => | ||
fireEvent.change(screen.getByRole("textbox", { name }), { | ||
target: { value }, | ||
}), | ||
); | ||
|
||
fireEvent.submit(getSubmitButton()); | ||
|
||
const body = new FormData(); | ||
Object.entries(contact).forEach(([key, { value }]) => | ||
body.append(key, value), | ||
); | ||
|
||
await waitFor(() => { | ||
expect(fetch).toHaveBeenCalledWith("/contact/submit", { | ||
method: "POST", | ||
body, | ||
}); | ||
}); | ||
|
||
expect(push).toHaveBeenCalledWith("/contact/success"); | ||
}); | ||
|
||
it("should disable form fields while submitting", async () => { | ||
render(<ContactForm />); | ||
|
||
expectFormToBeEnabled(); | ||
|
||
fireEvent.submit(getSubmitButton()); | ||
|
||
await waitFor(() => { | ||
expectFormToBeDisabled(); | ||
}); | ||
|
||
await waitFor(() => { | ||
expectFormToBeEnabled(); | ||
}); | ||
}); | ||
|
||
it("should show error message if submission fails", async () => { | ||
(fetch as jest.Mock).mockRejectedValue(new Error()); | ||
|
||
render(<ContactForm />); | ||
|
||
fireEvent.submit(getSubmitButton()); | ||
|
||
await waitFor(() => { | ||
expectFormToBeDisabled(); | ||
}); | ||
|
||
await waitFor(() => { | ||
expect(screen.getByText(/error/i)).toBeInTheDocument(); | ||
}); | ||
|
||
expect(push).not.toHaveBeenCalled(); | ||
|
||
await waitFor(() => { | ||
expectFormToBeEnabled(); | ||
}); | ||
}); | ||
|
||
it("should hide error message when form is changed", async () => { | ||
(fetch as jest.Mock).mockRejectedValue(new Error()); | ||
|
||
render(<ContactForm />); | ||
|
||
fireEvent.submit(getSubmitButton()); | ||
|
||
await waitFor(() => { | ||
expect(screen.getByText(/error/i)).toBeInTheDocument(); | ||
}); | ||
|
||
fireEvent.change(screen.getByRole("textbox", { name: "Name" }), { | ||
target: { value: "John Doe" }, | ||
}); | ||
|
||
await waitFor(() => { | ||
expect(screen.queryByText(/error/i)).not.toBeInTheDocument(); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
"use client"; | ||
import { useState } from "react"; | ||
import { BiMailSend } from "react-icons/bi"; | ||
import { useRouter } from "next/navigation"; | ||
import { Input } from "../../forms/components/Input"; | ||
import { Submit } from "../../forms/components/Submit"; | ||
import { Field } from "../../forms/models"; | ||
import { Contact } from "../models"; | ||
import { fields } from "../utils"; | ||
|
||
export function ContactForm() { | ||
const router = useRouter(); | ||
const [isSending, setIsSending] = useState(false); | ||
const [isError, setIsError] = useState(false); | ||
|
||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) { | ||
try { | ||
e.preventDefault(); | ||
setIsSending(true); | ||
const body = new FormData(e.currentTarget); | ||
await fetch("/contact/submit", { method: "POST", body }); | ||
router.push("/contact/success"); | ||
} catch (error) { | ||
setIsError(true); | ||
} finally { | ||
setIsSending(false); | ||
} | ||
} | ||
|
||
function getField(field: Field<keyof Contact>): Field<keyof Contact> { | ||
return { | ||
...field, | ||
attributes: { | ||
...field.attributes, | ||
disabled: isSending, | ||
}, | ||
}; | ||
} | ||
|
||
return ( | ||
<form | ||
onSubmit={handleSubmit} | ||
onChange={() => setIsError(false)} | ||
className="flex flex-col gap-6" | ||
> | ||
<div className="flex flex-col gap-4"> | ||
<Input field={getField(fields.name)} /> | ||
<Input field={getField(fields.email)} /> | ||
<Input field={getField(fields.subject)} /> | ||
<Input field={getField(fields.text)} /> | ||
</div> | ||
<div className="text-center"> | ||
<Submit | ||
isSubmitting={isSending} | ||
icon={<BiMailSend className="text-2xl" />} | ||
/> | ||
{isError && ( | ||
<p className="mt-4 text-red-500"> | ||
There was an error sending your message. Please try again later. | ||
</p> | ||
)} | ||
</div> | ||
</form> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { render, screen } from "@testing-library/react"; | ||
import ContactLayout, { metadata } from "./layout"; | ||
|
||
describe("ContactLayout", () => { | ||
it("should contain metadata", () => { | ||
expect(metadata.title).toBeTruthy(); | ||
}); | ||
|
||
it("should render title", () => { | ||
render(<ContactLayout> </ContactLayout>); | ||
|
||
expect( | ||
screen.getByRole("heading", { name: "Contact" }), | ||
).toBeInTheDocument(); | ||
}); | ||
|
||
it("should render children", () => { | ||
const testId = "test-children"; | ||
render( | ||
<ContactLayout> | ||
<div data-testid={testId}></div> | ||
</ContactLayout>, | ||
); | ||
|
||
expect(screen.getByTestId(testId)).toBeInTheDocument(); | ||
expect(screen.queryAllByTestId(testId)).toHaveLength(1); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import type { Metadata } from "next"; | ||
|
||
export const metadata: Metadata = { | ||
title: "Contact", | ||
}; | ||
|
||
export default function ContactLayout({ | ||
children, | ||
}: { | ||
children: React.ReactNode; | ||
}) { | ||
return ( | ||
<div className="mt-12 sm:mt-6"> | ||
<section className="max-w-[600px] mx-auto"> | ||
<h1 className="text-3xl text-primary text-center">Contact</h1> | ||
<div className="mt-4">{children}</div> | ||
</section> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export interface Contact { | ||
name: string; | ||
email: string; | ||
subject: string; | ||
text: string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { render, screen } from "@testing-library/react"; | ||
import ContactPage from "./page"; | ||
|
||
const ContactForm = "ContactForm"; | ||
|
||
jest.mock("./components/ContactForm", () => ({ | ||
ContactForm: () => <div data-testid={ContactForm}></div>, | ||
})); | ||
|
||
describe("Contact", () => { | ||
it("should render ContactForm", () => { | ||
render(<ContactPage />); | ||
|
||
expect(screen.getByTestId(ContactForm)).toBeInTheDocument(); | ||
expect(screen.queryAllByTestId(ContactForm)).toHaveLength(1); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { ContactForm } from "./components/ContactForm"; | ||
|
||
export default function ContactPage() { | ||
return <ContactForm />; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { createTransport } from "nodemailer"; | ||
import { sendMail } from "./mailer"; | ||
|
||
jest.mock("nodemailer", () => ({ | ||
createTransport: jest.fn(() => ({ | ||
sendMail: jest.fn(), | ||
})), | ||
})); | ||
|
||
describe("sendMail", () => { | ||
it("should create transporter correctly", async () => { | ||
expect(createTransport).toHaveBeenCalledWith({ | ||
service: "gmail", | ||
auth: { | ||
user: "mockUser", | ||
pass: "mockPassword", | ||
}, | ||
}); | ||
}); | ||
|
||
it("should send email correctly", async () => { | ||
const mail = { | ||
subject: "Test Subject", | ||
html: "<h1>Hello World</h1>", | ||
}; | ||
|
||
await sendMail(mail); | ||
|
||
const sendMailMocked = (createTransport as jest.Mock).mock.results[0].value | ||
.sendMail; | ||
|
||
expect(sendMailMocked).toHaveBeenCalledWith({ | ||
...mail, | ||
from: "Personal Portfolio <mockUser>", | ||
to: "[email protected]", | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { SentMessageInfo, createTransport } from "nodemailer"; | ||
|
||
const { SMTP_USER: user, SMTP_PASSWORD: pass, SMTP_RECEIVER: to } = process.env; | ||
|
||
const transporter = createTransport({ | ||
service: "gmail", | ||
auth: { | ||
user, | ||
pass, | ||
}, | ||
}); | ||
|
||
interface Mail { | ||
subject: string; | ||
html: string; | ||
} | ||
|
||
export function sendMail(mail: Mail): Promise<SentMessageInfo> { | ||
return transporter.sendMail({ | ||
...mail, | ||
from: `Personal Portfolio <${user}>`, | ||
to, | ||
}); | ||
} |
Oops, something went wrong.