Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
gilhanan committed Oct 24, 2023
1 parent b57dc3d commit 4ba7d68
Show file tree
Hide file tree
Showing 13 changed files with 177 additions and 52 deletions.
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
35 changes: 35 additions & 0 deletions e2e/specs/contact-page.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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.getByLabel("Name").fill("Test User");
await page.getByLabel("Email").fill("[email protected]");
await page.getByLabel("Subject").fill("Test Subject");
await page.getByLabel("Text").fill("Test description");
await page
.frameLocator('[title="reCAPTCHA"]')
.getByRole("checkbox", { name: "I'm not a robot" })
.click();

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();
});
76 changes: 76 additions & 0 deletions package-lock.json

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

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@types/nodemailer": "6.4.13",
"@types/react": "18.2.17",
"@types/react-dom": "18.2.7",
"@types/react-google-recaptcha": "2.1.7",
"@typescript-eslint/eslint-plugin": "5.62.0",
"autoprefixer": "10.4.14",
"eslint": "8.45.0",
Expand All @@ -45,6 +46,7 @@
"prettier": "3.0.3",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-google-recaptcha": "3.1.0",
"react-icons": "4.11.0",
"tailwindcss": "3.3.3",
"ts-node": "10.9.1",
Expand Down
4 changes: 4 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export default defineConfig({
command: "npm start",
url: baseURL,
reuseExistingServer: !process.env.CI,
env: {
RECAPTCHA_SITE_KEY: "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI",
RECAPTCHA_SECRET_KEY: "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe",
},
},
use: {
baseURL,
Expand Down
45 changes: 14 additions & 31 deletions src/app/contact/components/ContactForm.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"use client";
import { useState } from "react";
import { useContext, useState } from "react";
import { BiMailSend } from "react-icons/bi";
import { useRouter } from "next/navigation";
import ReCAPTCHA from "react-google-recaptcha";
import { reCaptchaHeaderKey } from "@shared/constants";
import { getReCaptchaToken } from "@shared/reCaptcha";
import { ThemeContext } from "@contexts/ThemeContext";
import { Input } from "@forms/components/Input";
import { Submit } from "@forms/components/Submit";
import { Field } from "@forms/models";
Expand All @@ -14,34 +15,12 @@ interface ContactFormProps {
reCaptchaSiteKey: string;
}

function ReCaptchaBranding() {
return (
<div className="text-sm text-secondary">
This site is protected by reCAPTCHA and the Google{" "}
<a
href="https://policies.google.com/privacy"
className="text-sky-500 dark:text-sky-400"
aria-label="Google reCAPTCHA Privacy Policy"
>
Privacy Policy
</a>{" "}
and{" "}
<a
href="https://policies.google.com/terms"
className="text-sky-500 dark:text-sky-400"
aria-label="Google reCAPTCHA Terms of Service"
>
Terms of Service
</a>{" "}
apply.
</div>
);
}

export function ContactForm({ reCaptchaSiteKey }: ContactFormProps) {
const { theme } = useContext(ThemeContext);
const router = useRouter();
const [isSending, setIsSending] = useState(false);
const [isError, setIsError] = useState(false);
const [recaptchaValue, setRecaptchaValue] = useState<string | null>();

async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
try {
Expand All @@ -50,12 +29,10 @@ export function ContactForm({ reCaptchaSiteKey }: ContactFormProps) {

const body = new FormData(e.currentTarget);

const token = await getReCaptchaToken({ reCaptchaSiteKey });

const { status } = await fetch("/api/contact", {
method: "POST",
body,
headers: { [reCaptchaHeaderKey]: token },
headers: { [reCaptchaHeaderKey]: recaptchaValue as string },
});

if (status !== 200) {
Expand Down Expand Up @@ -91,10 +68,16 @@ export function ContactForm({ reCaptchaSiteKey }: ContactFormProps) {
<Input field={getField(fields.email)} />
<Input field={getField(fields.subject)} />
<Input field={getField(fields.text)} />
<ReCaptchaBranding />
</div>
<div className="text-center">
<ReCAPTCHA
sitekey={reCaptchaSiteKey}
onChange={setRecaptchaValue}
theme={theme}
className="flex justify-center"
/>
<div className="flex justify-center">
<Submit
disabled={!recaptchaValue}
isSubmitting={isSending}
icon={<BiMailSend className="text-2xl" />}
/>
Expand Down
22 changes: 17 additions & 5 deletions src/app/forms/components/Submit.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,42 @@ function getSubmitButton() {

describe("Submit", () => {
it("should render the submit button", () => {
render(<Submit isSubmitting={false} icon={null} />);
render(<Submit disabled={false} isSubmitting={false} icon={null} />);
expect(getSubmitButton()).toBeInTheDocument();
});

it("should disable the submit button when isSubmitting is true", () => {
render(<Submit isSubmitting={true} icon={null} />);
render(<Submit disabled={false} isSubmitting={true} icon={null} />);
expect(getSubmitButton()).toBeDisabled();
});

it("should show the icon when isSubmitting is true", () => {
render(<Submit isSubmitting={true} icon={<div data-testid="icon" />} />);
render(
<Submit
disabled={false}
isSubmitting={true}
icon={<div data-testid="icon" />}
/>,
);
expect(screen.getByTestId("icon")).toBeInTheDocument();
});

it("should not show the icon when isSubmitting is false", () => {
render(<Submit isSubmitting={false} icon={<div data-testid="icon" />} />);
render(
<Submit
disabled={false}
isSubmitting={false}
icon={<div data-testid="icon" />}
/>,
);
expect(screen.queryByTestId("icon")).not.toBeInTheDocument();
});

it("should call the onSubmit function when the button is clicked", () => {
const onSubmit = jest.fn();
render(
<form onSubmit={onSubmit}>
<Submit isSubmitting={false} icon={null} />
<Submit disabled={false} isSubmitting={false} icon={null} />
</form>,
);
expect(onSubmit).not.toHaveBeenCalled();
Expand Down
Loading

0 comments on commit 4ba7d68

Please sign in to comment.