Skip to content

Commit 8008ade

Browse files
authored
Merge pull request #1213 from joshunrau/playwright
setup playwright
2 parents 151c002 + 6f1a2f3 commit 8008ade

File tree

10 files changed

+251
-6
lines changed

10 files changed

+251
-6
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,7 @@ storybook-static
7272

7373
# claude
7474
.claude/settings.local.json
75+
76+
# playwright
77+
testing/e2e/.output/
78+
testing/e2e/playwright/.cache/

apps/api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"build": "rm -rf dist && NODE_ENV=production libnest build -c libnest.config.ts",
99
"db:generate": "prisma generate --no-hints",
1010
"dev": "NODE_ENV=development NODE_OPTIONS='--conditions=development' env-cmd -f ../../.env libnest dev -c libnest.config.ts",
11-
"dev:test": "NODE_ENV=test env-cmd -f ../../.env libnest dev -c libnest.config.ts",
11+
"dev:test": "NODE_ENV=test env-cmd -f ../../.env libnest dev -c libnest.config.ts --no-watch",
1212
"format": "prettier --write src",
1313
"lint": "tsc && eslint --fix src",
1414
"start": "NODE_ENV=production env-cmd -f ../../.env node dist/app.js",

apps/api/src/setup/setup.controller.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { RouteAccess } from '@douglasneuroinformatics/libnest';
2-
import { Body, Controller, Get, Patch, Post } from '@nestjs/common';
2+
import { Body, Controller, Delete, Get, Patch, Post } from '@nestjs/common';
33
import { ApiOperation, ApiTags } from '@nestjs/swagger';
44
import type { SetupState } from '@opendatacapture/schemas/setup';
55

@@ -12,6 +12,12 @@ import { SetupService } from './setup.service';
1212
export class SetupController {
1313
constructor(private readonly setupService: SetupService) {}
1414

15+
@Delete()
16+
@RouteAccess({ action: 'delete', subject: 'all' })
17+
async delete(): Promise<void> {
18+
return this.setupService.delete();
19+
}
20+
1521
@ApiOperation({
1622
description: 'Return the current setup state',
1723
summary: 'Get State'

apps/api/src/setup/setup.service.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ export class SetupService {
2020
return this.usersService.create({ ...admin, basePermissionLevel: 'ADMIN', groupIds: [] });
2121
}
2222

23+
async delete(): Promise<void> {
24+
const isTest = this.configService.get('NODE_ENV') === 'test';
25+
if (!isTest) {
26+
throw new ForbiddenException('Cannot access outside of test');
27+
}
28+
await this.prismaService.dropDatabase();
29+
}
30+
2331
async getState() {
2432
const savedOptions = await this.getSavedOptions();
2533
return {

pnpm-lock.yaml

Lines changed: 57 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

testing/e2e/package.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "@opendatacapture/test",
3+
"type": "module",
4+
"version": "0.0.0",
5+
"private": true,
6+
"scripts": {
7+
"lint": "tsc && eslint --fix .",
8+
"test": "env-cmd -f ../../.env playwright test",
9+
"test:dev": "env-cmd -f ../../.env playwright test --ui"
10+
},
11+
"dependencies": {
12+
"@douglasneuroinformatics/libjs": "catalog:",
13+
"@opendatacapture/schemas": "workspace:*",
14+
"@playwright/test": "^1.51.1"
15+
}
16+
}

testing/e2e/playwright.config.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import * as path from 'node:path';
2+
3+
import { parseNumber } from '@douglasneuroinformatics/libjs';
4+
import { defineConfig, devices } from '@playwright/test';
5+
6+
const API_PORT = parseNumber(process.env.API_DEV_SERVER_PORT);
7+
const WEB_PORT = parseNumber(process.env.WEB_DEV_SERVER_PORT);
8+
9+
export default defineConfig({
10+
fullyParallel: true,
11+
outputDir: path.resolve(import.meta.dirname, '.output/results'),
12+
projects: [
13+
{
14+
name: 'Global Setup',
15+
teardown: 'Global Teardown',
16+
testMatch: '**/global/global.setup.spec.ts'
17+
},
18+
{
19+
name: 'Global Teardown',
20+
testMatch: '**/global/global.teardown.spec.ts'
21+
},
22+
{
23+
dependencies: ['Global Setup'],
24+
name: 'Desktop Chrome',
25+
testIgnore: '**/global/**',
26+
use: { ...devices['Desktop Chrome'] }
27+
},
28+
{
29+
dependencies: ['Global Setup'],
30+
name: 'Desktop Firefox',
31+
testIgnore: '**/global/**',
32+
use: { ...devices['Desktop Firefox'] }
33+
},
34+
{
35+
dependencies: ['Global Setup'],
36+
name: 'Desktop Safari',
37+
testIgnore: '**/global/**',
38+
use: { ...devices['Desktop Safari'] }
39+
}
40+
],
41+
reporter: [['html', { open: 'never', outputFolder: path.resolve(import.meta.dirname, '.output/report') }]],
42+
testDir: path.resolve(import.meta.dirname, 'src'),
43+
use: {
44+
baseURL: `http://localhost:${WEB_PORT}`,
45+
trace: 'on-first-retry'
46+
},
47+
webServer: [
48+
{
49+
command: 'pnpm dev:test',
50+
cwd: path.resolve(import.meta.dirname, '../../apps/api'),
51+
gracefulShutdown: {
52+
signal: 'SIGINT',
53+
timeout: 1000
54+
},
55+
url: `http://localhost:${API_PORT}`
56+
},
57+
{
58+
command: 'pnpm dev:test',
59+
cwd: path.resolve(import.meta.dirname, '../../apps/web'),
60+
gracefulShutdown: {
61+
signal: 'SIGINT',
62+
timeout: 1000
63+
},
64+
url: `http://localhost:${WEB_PORT}`
65+
}
66+
]
67+
});
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/* eslint-disable @typescript-eslint/no-namespace */
2+
/* eslint-disable @typescript-eslint/consistent-type-definitions */
3+
4+
import type { LoginCredentials } from '@opendatacapture/schemas/auth';
5+
import type { InitAppOptions } from '@opendatacapture/schemas/setup';
6+
import { expect, test } from '@playwright/test';
7+
8+
const initOptions = {
9+
admin: {
10+
firstName: 'Jane',
11+
lastName: 'Doe',
12+
password: 'DataCapture2025',
13+
username: 'admin'
14+
},
15+
dummySubjectCount: 10,
16+
enableExperimentalFeatures: false,
17+
initDemo: true,
18+
recordsPerSubject: 10
19+
} satisfies InitAppOptions;
20+
21+
declare global {
22+
namespace NodeJS {
23+
interface ProcessEnv {
24+
ADMIN_ACCESS_TOKEN: string;
25+
ADMIN_PASSWORD: string;
26+
ADMIN_USERNAME: string;
27+
GLOBAL_SETUP_COMPLETE?: '1';
28+
}
29+
}
30+
}
31+
32+
test.skip(() => process.env.GLOBAL_SETUP_COMPLETE === '1');
33+
34+
test.describe.serial(() => {
35+
test.describe.serial('setup', () => {
36+
test('initial setup', async ({ request }) => {
37+
const response = await request.get('/api/v1/setup');
38+
expect(response.status()).toBe(200);
39+
await expect(response.json()).resolves.toMatchObject({ isSetup: false });
40+
});
41+
test('successful setup', async ({ page }) => {
42+
await page.goto('/setup');
43+
await expect(page).toHaveURL('/setup');
44+
const setupForm = page.locator('form[data-cy="setup-form"]');
45+
await setupForm.locator('input[name="firstName"]').fill(initOptions.admin.firstName);
46+
await setupForm.locator('input[name="lastName"]').fill(initOptions.admin.lastName);
47+
await setupForm.locator('input[name="username"]').fill(initOptions.admin.username);
48+
await setupForm.locator('input[name="password"]').fill(initOptions.admin.password);
49+
await setupForm.locator('input[name="confirmPassword"]').fill(initOptions.admin.password);
50+
await setupForm.locator('#initDemo-true').click();
51+
await setupForm.locator('input[name="dummySubjectCount"]').fill(initOptions.dummySubjectCount.toString());
52+
await setupForm.locator('input[name="recordsPerSubject"]').fill(initOptions.recordsPerSubject.toString());
53+
await setupForm.getByLabel('Submit').click();
54+
await expect(page).toHaveURL('/dashboard');
55+
});
56+
test('setup state after initialization', async ({ request }) => {
57+
const response = await request.get('/api/v1/setup');
58+
expect(response.status()).toBe(200);
59+
await expect(response.json()).resolves.toMatchObject({ isSetup: true });
60+
});
61+
});
62+
test.describe.serial('auth', () => {
63+
test('login', async ({ request }) => {
64+
const { password, username } = initOptions.admin;
65+
const response = await request.post('/api/v1/auth/login', {
66+
data: { password, username } satisfies LoginCredentials
67+
});
68+
expect(response.status()).toBe(200);
69+
const { accessToken } = await response.json();
70+
expect(typeof accessToken).toBe('string');
71+
process.env.ADMIN_ACCESS_TOKEN = accessToken;
72+
process.env.ADMIN_USERNAME = username;
73+
process.env.ADMIN_PASSWORD = password;
74+
process.env.GLOBAL_SETUP_COMPLETE = '1';
75+
});
76+
});
77+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { expect, test } from '@playwright/test';
2+
3+
test('delete database', async ({ request }) => {
4+
const response = await request.delete('/api/v1/setup', {
5+
headers: {
6+
Authorization: `Bearer ${process.env.ADMIN_ACCESS_TOKEN}`
7+
}
8+
});
9+
expect(response.status()).toBe(200);
10+
});

testing/e2e/tsconfig.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": ["../../tsconfig.base.json"],
3+
"include": ["src/**/*", "playwright.config.ts"]
4+
}

0 commit comments

Comments
 (0)