Skip to content
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

Add playwright testing library + intial integration tests for the Join Form #58

Merged
merged 14 commits into from
Aug 1, 2024
37 changes: 37 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Playwright Tests
Fixed Show fixed Hide fixed
permissions: read-all
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
name: Run Playwright Tests
environment: dev3
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
env:
NEXT_PUBLIC_MESHDB_URL: ${{ secrets.BETA_MESHDB_URL }}
S3_BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME }}
S3_BASE_NAME: ${{ secrets.TEST_S3_BASE_NAME }}
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
JOIN_FORM_LOG: /tmp/join.csv
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,8 @@ yarn-error.log*
*.tsbuildinfo

# IDEs
.idea/
.idea/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,38 @@ npm install
npm run dev
```

Finally, open `http://127.0.0.1:3000` in your web browser to interact with your copy of the application
Finally, open `http://127.0.0.1:3000` in your web browser to interact with your copy of the application

# Testing

We use `playwright` to do integration tests. You can run them with the following instructions:

1. Setup a dev instance of [meshdb](https://github.com/nycmeshnet/meshdb)

2. Copy `.env.sample` into `.env.local` and fill it out

3. Run the integration tests with `npx playwright test`

You can see what playwright is doing with `--headed`, and you can pause a test to
examine the browser by inserting `page.pause()` in your test.

To run a specific test, you can use `-g`:

`npx playwright test -g 'missing name'`

See the [docs](https://playwright.dev/docs/running-tests) for more information about playwright.

If you get an error like this one:

```
[WebServer] [Error: ENOENT: no such file or directory, open '/home/wilnil/Code/nycmesh/meshforms/.next/BUILD_ID'] {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: '/home/wilnil/Code/nycmesh/meshforms/.next/BUILD_ID'
}

Error: Process from config.webServer was not able to start. Exit code: 1
```

Try running `npm run build`
73 changes: 1 addition & 72 deletions app/api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { JoinFormInput, JoinFormResponse, NNAssignFormInput, NNAssignFormResponse, QueryFormInput, QueryFormResponse } from "@/app/io";
import { z } from "zod";

if (process.env.NEXT_PUBLIC_MESHDB_URL === undefined) {
Expand All @@ -10,78 +11,6 @@ if (process.env.NEXT_PUBLIC_MESHDB_URL === undefined) {

const API_BASE = new URL(process.env.NEXT_PUBLIC_MESHDB_URL as string + "/api/v1/");

export const JoinFormInput = z.object({
first_name: z.string(),
last_name: z.string(),
email: z.string(),
phone: z.string(),
street_address: z.string(),
apartment: z.string(),
city: z.string(),
state: z.string(),
zip: z.number(),
roof_access: z.boolean(),
referral: z.string(),
ncl: z.boolean(),
})
export type JoinFormInput = z.infer<typeof JoinFormInput>

export const JoinFormResponse = z.object({
message: z.string().optional(),
building_id: z.number(),
member_id: z.number(),
install_number: z.number(),
member_exists: z.boolean(),
})
export type JoinFormResponse = z.infer<typeof JoinFormResponse>

export const NNAssignFormInput = z.object({
install_number: z.number(),
password: z.string(), // TODO: Salt/hash/whatever this
})
export type NNAssignFormInput = z.infer<typeof NNAssignFormInput>

export const NNAssignFormResponse = z.object({
message: z.string().optional(),
building_id: z.number(),
install_number: z.number(),
network_number: z.number(),
created: z.boolean(),
})
export type NNAssignFormResponse = z.infer<typeof NNAssignFormResponse>

export const QueryFormInput = z.object({
//route: z.string(),
legacy: z.string().optional(),
query_type: z.string(),
data: z.string(),
password: z.string(), // TODO: Salt/hash/whatever this
})
export type QueryFormInput = z.infer<typeof QueryFormInput>

export const QueryFormResponse = z.object({
count: z.number(),
next: z.string().nullable(),
previous: z.string().nullable(),
results: z.array(z.object({
install_number: z.number(),
street_address: z.string().nullable(),
unit: z.string(),
city: z.string(),
state: z.string(),
zip_code: z.string(),
name: z.string(),
phone_number: z.string().nullable(),
primary_email_address: z.string().nullable(),
stripe_email_address: z.string().nullable(),
additional_email_addresses: z.array(z.string()).nullable(),
notes: z.string(),
network_number: z.number().nullable(),
status: z.string(),
}))
})
export type QueryFormResponse = z.infer<typeof QueryFormResponse>

const get = async <S extends z.Schema>(url: string, schema: S, auth?: string, nextOptions?: NextFetchRequestConfig): Promise<ReturnType<S['parse']>> => {
const res = await fetch(new URL(url, API_BASE), {
headers: {
Expand Down
34 changes: 12 additions & 22 deletions app/data.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
'use server'
import { access, constants, appendFileSync, readFile } from 'node:fs';
import { JoinFormInput } from "@/app/api";
import { JoinFormInput } from "@/app/io";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
const JOIN_FORM_LOG = process.env.JOIN_FORM_LOG as string;

const S3_REGION = process.env.S3_REGION as string;
const S3_ENDPOINT = process.env.S3_ENDPOINT as string;
const S3_BUCKET_NAME = process.env.S3_BUCKET_NAME as string;
const S3_BASE_NAME = process.env.S3_BASE_NAME as string;
const S3_ACCESS_KEY = process.env.S3_ACCESS_KEY as string;
const S3_SECRET_KEY = process.env.S3_SECRET_KEY as string;

Expand All @@ -29,33 +30,21 @@ export async function recordJoinFormSubmissionToCSV(submission: JoinFormInput) {

// Records the submission we just got as a JSON object in an S3 bucket.
export async function recordJoinFormSubmissionToS3(submission: JoinFormInput) {
// Don't define endpoint if one is not passed
let s3Client;
if (S3_ENDPOINT != "" && S3_ENDPOINT != null) {
s3Client = new S3Client({
region: S3_REGION,
endpoint: S3_ENDPOINT,
credentials: {
accessKeyId: S3_ACCESS_KEY,
secretAccessKey: S3_SECRET_KEY,
},
});
} else {
s3Client = new S3Client({
region: S3_REGION,
credentials: {
accessKeyId: S3_ACCESS_KEY,
secretAccessKey: S3_SECRET_KEY,
},
});
}
const s3Client = new S3Client({
region: S3_REGION != undefined ? S3_REGION : "us-east-1",
endpoint: S3_ENDPOINT != undefined ? S3_ENDPOINT : "https://s3.us-east-1.amazonaws.com",
credentials: {
accessKeyId: S3_ACCESS_KEY,
secretAccessKey: S3_SECRET_KEY,
},
});

// Thanks ChatGPT, eww...
const submissionKey = new Date().toISOString().replace(/[-:T]/g, '/').slice(0, 19);

const command = new PutObjectCommand({
Bucket: S3_BUCKET_NAME,
Key: `join-form-submissions/${submissionKey}.json`,
Key: `${S3_BASE_NAME}/${submissionKey}.json`,
Body: JSON.stringify(submission),
});

Expand All @@ -66,6 +55,7 @@ export async function recordJoinFormSubmissionToS3(submission: JoinFormInput) {
console.error(err);
// Record the submission to a local CSV file *just in case*
recordJoinFormSubmissionToCSV(submission);
throw err;
}

}
73 changes: 73 additions & 0 deletions app/io.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { z } from "zod";

export const JoinFormInput = z.object({
first_name: z.string(),
last_name: z.string(),
email: z.string(),
phone: z.string(),
street_address: z.string(),
apartment: z.string(),
city: z.string(),
state: z.string(),
zip: z.number(),
roof_access: z.boolean(),
referral: z.string(),
ncl: z.boolean(),
})
export type JoinFormInput = z.infer<typeof JoinFormInput>

export const JoinFormResponse = z.object({
message: z.string().optional(),
building_id: z.number(),
member_id: z.number(),
install_number: z.number(),
member_exists: z.boolean(),
})
export type JoinFormResponse = z.infer<typeof JoinFormResponse>

export const NNAssignFormInput = z.object({
install_number: z.number(),
password: z.string(), // TODO: Salt/hash/whatever this
})
export type NNAssignFormInput = z.infer<typeof NNAssignFormInput>

export const NNAssignFormResponse = z.object({
message: z.string().optional(),
building_id: z.number(),
install_number: z.number(),
network_number: z.number(),
created: z.boolean(),
})
export type NNAssignFormResponse = z.infer<typeof NNAssignFormResponse>

export const QueryFormInput = z.object({
//route: z.string(),
legacy: z.string().optional(),
query_type: z.string(),
data: z.string(),
password: z.string(), // TODO: Salt/hash/whatever this
})
export type QueryFormInput = z.infer<typeof QueryFormInput>

export const QueryFormResponse = z.object({
count: z.number(),
next: z.string().nullable(),
previous: z.string().nullable(),
results: z.array(z.object({
install_number: z.number(),
street_address: z.string().nullable(),
unit: z.string(),
city: z.string(),
state: z.string(),
zip_code: z.string(),
name: z.string(),
phone_number: z.string().nullable(),
primary_email_address: z.string().nullable(),
stripe_email_address: z.string().nullable(),
additional_email_addresses: z.array(z.string()).nullable(),
notes: z.string(),
network_number: z.number().nullable(),
status: z.string(),
}))
})
export type QueryFormResponse = z.infer<typeof QueryFormResponse>
8 changes: 5 additions & 3 deletions app/join/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Footer } from "@/components/Footer/Footer";
import { Header } from "@/components/Header/Header";
import JoinForm from "@/components/JoinForm/JoinForm";
import Head from 'next/head';

// TODO:
// https://www.npmjs.com/package/react-phone-number-input
// https://www.npmjs.com/package/react-error-boundary
export const metadata = {
title: "Join Our Community Network!",
description: "Use this form to sign up for NYC Mesh",
};

export default async function Join() {
return <>
Expand Down
6 changes: 5 additions & 1 deletion components/JoinForm/JoinForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { JoinFormInput, submitJoinForm } from "@/app/api";
import { submitJoinForm } from "@/app/api";
import { JoinFormInput } from "@/app/io";
import { recordJoinFormSubmissionToS3 } from "@/app/data";
import 'react-phone-number-input/style.css';
import PhoneInput from 'react-phone-number-input/input';
Expand Down Expand Up @@ -143,13 +144,16 @@ const JoinForm = () => {
variant="contained"
size="large"
sx={{ width: "12rem", fontSize: "1rem", m:"1rem"}}
name="submit_join_form"
>
{ isLoading ? "Loading..." : (submitted ? "Thanks!" : "Submit") }
</Button>
</div>
</form>
</div>
<div className="toasty">
<ToastContainer />
</div>
</>
}

Expand Down
6 changes: 2 additions & 4 deletions components/NNAssignForm/NNAssignForm.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
"use client";

import Button from "@mui/material/Button";
import { useFormState } from "react-dom";
import { useFormStatus } from "react-dom";
import { NNAssignFormInput, submitNNAssignForm } from "@/app/api";
import { NNAssignFormInput } from "@/app/io";
import { submitNNAssignForm } from "@/app/api";
import { useRouter } from 'next/navigation'
import { ErrorBoundary } from "react-error-boundary";
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';

Expand Down
3 changes: 2 additions & 1 deletion components/QueryForm/QueryForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client'

import { QueryFormInput, QueryFormResponse, submitQueryForm } from "@/app/api";
import { submitQueryForm } from "@/app/api";
import { QueryFormInput, QueryFormResponse } from "@/app/io";
import Button from "@mui/material/Button";
import { toastErrorMessage } from "@/app/utils/toastErrorMessage";
import { ToastContainer, toast } from 'react-toastify';
Expand Down
Loading
Loading