Add Playwright configuration and update codebase

The commit introduces Playwright configuration for End-to-End testing and modifies several files to optimize the project's structure. It also modifies the middleware to interact with Next.js and fix URL creation. Changes in database types were made to refine their structure.
This commit is contained in:
giancarlo
2024-04-11 15:59:08 +08:00
parent 2c7478abff
commit bf716b5dd6
13 changed files with 2249 additions and 1873 deletions

41
.github/workflows/workflow.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: Workflow
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
typescript:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm install -g pnpm && pnpm install
- name: Typecheck
run: pnpm run typecheck
- name: Lint
run: pnpm run lint
test:
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 install -g pnpm && pnpm install
- name: Install Playwright Browsers
run: pnpm exec playwright install --with-deps
- name: Run Playwright tests
run: pnpm run test
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30

5
apps/e2e/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

19
apps/e2e/package.json Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "web-e2e",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"report": "playwright show-report",
"test": "playwright test",
"test:ui": "playwright test --ui"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.43.0",
"@types/node": "^20.12.7",
"node-html-parser": "^6.1.13"
}
}

View File

@@ -0,0 +1,66 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
});

View File

@@ -0,0 +1,54 @@
import { Page } from '@playwright/test';
import { Mailbox } from '../utils/mailbox';
export class AuthPageObject {
private readonly page: Page;
private readonly mailbox: Mailbox;
constructor(page: Page) {
this.page = page;
this.mailbox = new Mailbox(page);
}
goToSignIn() {
return this.page.goto('/auth/sign-in');
}
goToSignUp() {
return this.page.goto('/auth/sign-up');
}
async signIn(params: {
email: string,
password: string
}) {
await this.page.locator('input[name="email"]').clear();
await this.page.fill('input[name="email"]', params.email);
await this.page.fill('input[name="password"]', params.password);
await this.page.click('button[type="submit"]');
}
async signUp(params: {
email: string,
password: string,
repeatPassword: string
}) {
await this.page.fill('input[name="email"]', params.email);
await this.page.fill('input[name="password"]', params.password);
await this.page.fill('input[name="repeatPassword"]', params.repeatPassword);
await this.page.click('button[type="submit"]');
}
async visitConfirmEmailLink(email: string) {
await this.page.waitForTimeout(300);
return this.mailbox.visitMailbox(email);
}
createRandomEmail() {
const value = Math.random() * 1000;
return `${value.toFixed(0)}@makerkit.dev`;
}
}

View File

@@ -0,0 +1,57 @@
import { test, expect } from '@playwright/test';
import { AuthPageObject } from './auth.po';
test.describe('Auth flow', () => {
test.describe.configure({ mode: 'serial' });
let email: string;
test('will sign-up and redirect to the home page', async ({ page }) => {
const auth = new AuthPageObject(page);
await auth.goToSignUp();
email = auth.createRandomEmail();
console.log(`Signing up with email ${email} ...`);
await auth.signUp({
email,
password: 'password',
repeatPassword: 'password',
});
await auth.visitConfirmEmailLink(email);
expect(page.url()).toContain('http://localhost:3000/home');
});
test('will sign-in with the correct credentials', async ({ page }) => {
const auth = new AuthPageObject(page);
await auth.goToSignIn();
console.log(`Signing in with email ${email} ...`);
await auth.signIn({
email,
password: 'password',
});
await page.waitForURL('http://localhost:3000/home');
expect(page.url()).toContain('http://localhost:3000/home');
});
});
test.describe('Protected routes', () => {
test('will redirect to the sign-in page if not authenticated', async ({ page }) => {
await page.goto('/home/settings');
expect(page.url()).toContain('/auth/sign-in?next=/home/settings');
});
test('will return a 404 for the admin page', async ({ page }) => {
await page.goto('/admin')
expect(page.url()).toContain('/auth/sign-in');
});
})

View File

@@ -0,0 +1,68 @@
import { Page } from '@playwright/test';
import { parse } from 'node-html-parser';
export class Mailbox {
constructor(
private readonly page: Page
) {
}
async visitMailbox(email: string) {
const mailbox = email.split('@')[0];
console.log(`Visiting mailbox ${mailbox} ...`)
if (!mailbox) {
throw new Error('Invalid email');
}
const json = await this.getInviteEmail(mailbox);
const html = (json.body as { html: string }).html;
const el = parse(html);
const linkHref = el.querySelector('a')?.getAttribute('href');
if (!linkHref) {
throw new Error('No link found in email');
}
console.log(`Visiting ${linkHref} ...`);
return this.page.goto(linkHref);
}
async getInviteEmail(
mailbox: string,
params = {
deleteAfter: true
}
) {
const url = `http://localhost:54324/api/v1/mailbox/${mailbox}`;
const response = await fetch(url);
const json = (await response.json()) as Array<{ id: string }>;
if (!json || !json.length) {
return;
}
const messageId = json[0]?.id;
const messageUrl = `${url}/${messageId}`;
const messageResponse = await fetch(messageUrl);
if (!messageResponse.ok) {
throw new Error(`Failed to fetch email: ${messageResponse.statusText}`);
}
// delete message
if (params.deleteAfter) {
await fetch(messageUrl, {
method: 'DELETE'
});
}
return await messageResponse.json();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -86,19 +86,20 @@ async function adminMiddleware(request: NextRequest, response: NextResponse) {
const supabase = createMiddlewareClient(request, response);
const { data, error } = await supabase.auth.getUser();
const host = request.nextUrl.host;
// If user is not logged in, redirect to sign in page.
// This should never happen, but just in case.
if (!data.user || error) {
return NextResponse.redirect(`${host}/auth/sign-in`);
return NextResponse.redirect(
new URL(pathsConfig.auth.signIn, request.nextUrl.origin).href,
);
}
const role = data.user?.app_metadata.role;
// If user is not an admin, redirect to 404 page.
if (!role || role !== 'super-admin') {
return NextResponse.redirect(`${host}/404`);
return NextResponse.redirect(new URL('/404', request.nextUrl.origin).href);
}
// in all other cases, return the response

View File

@@ -19,6 +19,7 @@
"lint": "turbo lint --continue -- --cache --cache-location 'node_modules/.cache/.eslintcache' && manypkg check",
"lint:fix": "turbo lint --continue -- --fix --cache --cache-location 'node_modules/.cache/.eslintcache' && manypkg fix",
"typecheck": "turbo typecheck",
"test": "turbo test",
"stripe:listen": "pnpm --filter '@kit/stripe' start"
},
"prettier": "@kit/prettier-config",

File diff suppressed because it is too large Load Diff

90
pnpm-lock.yaml generated
View File

@@ -33,6 +33,21 @@ importers:
specifier: ^1.22.22
version: 1.22.22
apps/e2e:
devDependencies:
'@playwright/test':
specifier: ^1.43.0
version: 1.43.0
'@types/node':
specifier: ^20.12.7
version: 20.12.7
nanoid:
specifier: ^5.0.7
version: 5.0.7
node-html-parser:
specifier: ^6.1.13
version: 6.1.13
apps/web:
dependencies:
'@epic-web/invariant':
@@ -3385,6 +3400,14 @@ packages:
requiresBuild: true
optional: true
/@playwright/test@1.43.0:
resolution: {integrity: sha512-Ebw0+MCqoYflop7wVKj711ccbNlrwTBCtjY5rlbiY9kHL2bCYxq+qltK6uPsVBGGAOb033H2VO0YobcQVxoW7Q==}
engines: {node: '>=16'}
hasBin: true
dependencies:
playwright: 1.43.0
dev: true
/@pnpm/config.env-replace@1.1.0:
resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==}
engines: {node: '>=12.22.0'}
@@ -7694,6 +7717,10 @@ packages:
readable-stream: 3.6.2
dev: false
/boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
dev: true
/brace-expansion@1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
dependencies:
@@ -8143,6 +8170,21 @@ packages:
shebang-command: 2.0.0
which: 2.0.2
/css-select@5.1.0:
resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
dependencies:
boolbase: 1.0.0
css-what: 6.1.0
domhandler: 5.0.3
domutils: 3.1.0
nth-check: 2.1.1
dev: true
/css-what@6.1.0:
resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
engines: {node: '>= 6'}
dev: true
/cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
@@ -8453,18 +8495,15 @@ packages:
domelementtype: 2.3.0
domhandler: 5.0.3
entities: 4.5.0
dev: false
/domelementtype@2.3.0:
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
dev: false
/domhandler@5.0.3:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
dependencies:
domelementtype: 2.3.0
dev: false
/domutils@3.1.0:
resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
@@ -8472,7 +8511,6 @@ packages:
dom-serializer: 2.0.0
domelementtype: 2.3.0
domhandler: 5.0.3
dev: false
/dot-case@2.1.1:
resolution: {integrity: sha512-HnM6ZlFqcajLsyudHq7LeeLDr2rFAVYtDv/hV5qchQEidSck8j9OPUsXY9KwJv/lHMtYlX4DjRQqwFYa+0r8Ug==}
@@ -8594,7 +8632,6 @@ packages:
/entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
dev: false
/error-ex@1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
@@ -9316,6 +9353,14 @@ packages:
/fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
/fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
requiresBuild: true
dev: true
optional: true
/fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -9614,6 +9659,11 @@ packages:
dependencies:
function-bind: 1.1.2
/he@1.2.0:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
dev: true
/header-case@1.0.1:
resolution: {integrity: sha512-i0q9mkOeSuhXw6bGgiQCCBgY/jlZuV/7dZXyZ9c6LcBrqwvT8eT719E9uxE5LiZftdl+z81Ugbg/VvXV4OJOeQ==}
dependencies:
@@ -11157,7 +11207,6 @@ packages:
resolution: {integrity: sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==}
engines: {node: ^18 || >=20}
hasBin: true
dev: false
/natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
@@ -11351,6 +11400,13 @@ packages:
formdata-polyfill: 4.0.10
dev: true
/node-html-parser@6.1.13:
resolution: {integrity: sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==}
dependencies:
css-select: 5.1.0
he: 1.2.0
dev: true
/node-plop@0.26.3:
resolution: {integrity: sha512-Cov028YhBZ5aB7MdMWJEmwyBig43aGL5WT4vdoB28Oitau1zZAcHUn8Sgfk9HM33TqhtLJ9PlM/O0Mv+QpV/4Q==}
engines: {node: '>=8.9.4'}
@@ -11409,6 +11465,12 @@ packages:
path-key: 3.1.1
dev: false
/nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
dependencies:
boolbase: 1.0.0
dev: true
/object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
@@ -11776,6 +11838,22 @@ packages:
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
engines: {node: '>= 6'}
/playwright-core@1.43.0:
resolution: {integrity: sha512-iWFjyBUH97+pUFiyTqSLd8cDMMOS0r2ZYz2qEsPjH8/bX++sbIJT35MSwKnp1r/OQBAqC5XO99xFbJ9XClhf4w==}
engines: {node: '>=16'}
hasBin: true
dev: true
/playwright@1.43.0:
resolution: {integrity: sha512-SiOKHbVjTSf6wHuGCbqrEyzlm6qvXcv7mENP+OZon1I07brfZLGdfWV0l/efAzVx7TF3Z45ov1gPEkku9q25YQ==}
engines: {node: '>=16'}
hasBin: true
dependencies:
playwright-core: 1.43.0
optionalDependencies:
fsevents: 2.3.2
dev: true
/pnpm@8.15.6:
resolution: {integrity: sha512-d7iem+d6Kwatj0A6Gcrl4il29hAj+YrTI9XDAZSVjrwC7gpq5dE+5FT2E05OjK8poF8LGg4dKxe8prah8RWfhg==}
engines: {node: '>=16.14'}

View File

@@ -45,6 +45,14 @@
"node_modules/.cache/tsbuildinfo.json"
]
},
"test": {
"dependsOn": [
"^topo"
],
"outputs": [
"node_modules/.cache/tsbuildinfo.json"
]
},
"clean": {
"cache": false
},