Skip to content

Commit

Permalink
Contact page
Browse files Browse the repository at this point in the history
  • Loading branch information
gilhanan committed Oct 29, 2023
1 parent 0f0749e commit 2082dd2
Show file tree
Hide file tree
Showing 39 changed files with 1,619 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ env:
AZURE_RESOURCE_GROUP: "gh-personal-portfolio"
AZURE_STORAGE_ACCOUNT: "ghpersonalportfolio"
LOCATION: "West Europe"
RECAPTCHA_SITE_KEY: ${{ secrets.RECAPTCHA_SITE_KEY }}
RECAPTCHA_SECRET_KEY: ${{ secrets.RECAPTCHA_SECRET_KEY }}
SMTP_USER: ${{ secrets.SMTP_USER }}
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
SMTP_RECEIVER: ${{ secrets.SMTP_RECEIVER }}

jobs:
setup-azure-resource:
Expand Down
5 changes: 5 additions & 0 deletions e2e/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { test as base } from "@playwright/test";
import { HomePage } from "./pages/home-page";
import { ProjectsPage } from "./pages/projects-page";
import { AboutPage } from "./pages/about-page";
import { ContactPage } from "./pages/contact-page";
import { Projects } from "./pages/projects";

type Fixtures = {
homePage: HomePage;
projectsPage: ProjectsPage;
aboutPage: AboutPage;
contactPage: ContactPage;
projects: Projects;
};

Expand All @@ -21,6 +23,9 @@ export const test = base.extend<Fixtures>({
aboutPage: async ({ page, baseURL }, use) => {
await use(new AboutPage(page, baseURL as string));
},
contactPage: async ({ page, baseURL }, use) => {
await use(new ContactPage(page, baseURL as string));
},
projects: async ({ page, baseURL }, use) => {
await use(new Projects(page, baseURL as string));
},
Expand Down
12 changes: 12 additions & 0 deletions e2e/pages/contact-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Page } from "@playwright/test";
import { SharedPage } from "./shared-page";

export class ContactPage extends SharedPage {
constructor(page: Page, baseURL: string) {
super(page, baseURL);
}

async goto() {
await super.goto("/contact");
}
}
2 changes: 1 addition & 1 deletion e2e/pages/shared-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export abstract class SharedPage {
private readonly baseURL: string,
) {}

async goto(path: "" | "/projects" | "/about" = ""): Promise<void> {
async goto(path: "" | "/projects" | "/about" | "/contact" = ""): Promise<void> {
await this.page.goto(this.baseURL + path);
}

Expand Down
33 changes: 33 additions & 0 deletions e2e/specs/contact-page.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { test, expect } from "../fixtures";

test.beforeEach(async ({ contactPage }) => {
await contactPage.goto();
});

test("has title", async ({ page }) => {
await expect(page).toHaveTitle("Gil Hanan | Contact");
});

test("can submit form", async ({ page }) => {
await page.waitForSelector(".grecaptcha-badge");

await page.getByLabel("Name").fill("Test User");
await page.getByLabel("Email").fill("test@example.com");
await page.getByLabel("Subject").fill("Test Subject");
await page.getByLabel("Text").fill("Test description");

const responsePromise$ = page.waitForResponse(
(resp) => resp.url().includes("/api/contact") && resp.status() === 200,
);

await page
.getByRole("button", {
name: "Submit",
})
.click();

await responsePromise$;

await page.waitForURL("/contact/success");
await expect(page.getByText(/Thank you for your message!/i)).toBeVisible();
});
7 changes: 7 additions & 0 deletions globals.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
declare global {
interface Window {
reCaptchaCallback?: () => void;
}
}

export {};
5 changes: 5 additions & 0 deletions jest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ 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 = "mockUser@example.com";
process.env.RECAPTCHA_SECRET_KEY = "mockSecretKey";

function MockImage({ priority, ...props }: ImageProps) {
return createElement("img", {
...props,
Expand Down
43 changes: 43 additions & 0 deletions package-lock.json

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

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
"@tailwindcss/forms": "0.5.6",
"@testing-library/jest-dom": "6.1.3",
"@testing-library/react": "14.0.0",
"@types/grecaptcha": "3.0.6",
"@types/jest": "29.5.5",
"@types/node": "20.4.5",
"@types/nodemailer": "6.4.13",
"@types/react": "18.2.17",
"@types/react-dom": "18.2.7",
"@typescript-eslint/eslint-plugin": "5.62.0",
Expand All @@ -35,8 +37,10 @@
"husky": "8.0.3",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"jest-environment-node": "29.7.0",
"lint-staged": "14.0.1",
"next": "13.5.6",
"nodemailer": "6.9.6",
"postcss": "8.4.27",
"prettier": "3.0.3",
"react": "18.2.0",
Expand Down
77 changes: 77 additions & 0 deletions src/app/api/contact/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* @jest-environment node
*/

import { NextRequest } from "next/server";
import { sendMail } from "@shared/mailer";
import { isContact } from "@contact/utils";
import { Contact } from "@contact/models";
import { POST } from "@api/contact/route";

jest.mock("../../contact/utils");
jest.mock("../../shared/mailer");

const contact: Contact = {
name: "John Doe",
email: "johndoe@example.com",
subject: "Test Subject",
text: "Test Message",
};

const { name, email, subject, text } = contact;

describe("POST", () => {
let body: FormData;
let request: NextRequest;

beforeEach(() => {
jest.clearAllMocks();

body = new FormData();
Object.entries(contact).forEach(([key, value]) => body.append(key, value));

request = new NextRequest("https://example.com", {
method: "POST",
body,
});
});

it("should send email and return 200 if form data is valid", async () => {
(isContact as unknown as jest.Mock).mockReturnValue(true);

const response = await POST(request);

expect(response.status).toBe(200);
expect(await response.json()).toEqual({
message: `Successfully sent email to ${email}`,
});
expect(sendMail).toHaveBeenCalledWith({
subject,
html: `${name} &lt;${email}&gt;<br><br>${text}`,
});
});

it("should return 400 if form data is invalid", async () => {
(isContact as unknown as jest.Mock).mockReturnValue(false);

const response = await POST(request);

expect(response.status).toBe(400);
expect(await response.json()).toEqual({ message: "Invalid form data" });
expect(sendMail).not.toHaveBeenCalled();
});

it("should return 500 if sending email fails", async () => {
(isContact as unknown as jest.Mock).mockReturnValue(true);
(sendMail as jest.Mock).mockRejectedValue(new Error());

const response = await POST(request);

expect(response.status).toBe(500);
expect(await response.json()).toEqual({ message: "Failed to send email" });
expect(sendMail).toHaveBeenCalledWith({
subject,
html: `${name} &lt;${email}&gt;<br><br>${text}`,
});
});
});
33 changes: 33 additions & 0 deletions src/app/api/contact/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { NextRequest, NextResponse } from "next/server";
import { sendMail } from "@shared/mailer";
import { isContact } from "@contact/utils";

export async function POST(request: NextRequest): Promise<
NextResponse<{
message: string;
}>
> {
const form = Object.fromEntries(await request.formData());

if (!isContact(form)) {
return NextResponse.json({ message: "Invalid form data" }, { status: 400 });
}

const { name, email, subject, text } = form;

try {
await sendMail({
subject,
html: `${name} &lt;${email}&gt;<br><br>${text}`,
});

return NextResponse.json({
message: `Successfully sent email to ${form.email}`,
});
} catch (error) {
return NextResponse.json(
{ message: "Failed to send email" },
{ status: 500 },
);
}
}

0 comments on commit 2082dd2

Please sign in to comment.