Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,7 @@ storybook-static

# claude
.claude/settings.local.json

# playwright
testing/e2e/.output/
testing/e2e/playwright/.cache/
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"build": "rm -rf dist && NODE_ENV=production libnest build -c libnest.config.ts",
"db:generate": "prisma generate --no-hints",
"dev": "NODE_ENV=development NODE_OPTIONS='--conditions=development' env-cmd -f ../../.env libnest dev -c libnest.config.ts",
"dev:test": "NODE_ENV=test env-cmd -f ../../.env libnest dev -c libnest.config.ts",
"dev:test": "NODE_ENV=test env-cmd -f ../../.env libnest dev -c libnest.config.ts --no-watch",
"format": "prettier --write src",
"lint": "tsc && eslint --fix src",
"start": "NODE_ENV=production env-cmd -f ../../.env node dist/app.js",
Expand Down
8 changes: 7 additions & 1 deletion apps/api/src/setup/setup.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { RouteAccess } from '@douglasneuroinformatics/libnest';
import { Body, Controller, Get, Patch, Post } from '@nestjs/common';
import { Body, Controller, Delete, Get, Patch, Post } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import type { SetupState } from '@opendatacapture/schemas/setup';

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

@Delete()
@RouteAccess({ action: 'delete', subject: 'all' })
async delete(): Promise<void> {
return this.setupService.delete();
}

@ApiOperation({
description: 'Return the current setup state',
summary: 'Get State'
Expand Down
8 changes: 8 additions & 0 deletions apps/api/src/setup/setup.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ export class SetupService {
return this.usersService.create({ ...admin, basePermissionLevel: 'ADMIN', groupIds: [] });
}

async delete(): Promise<void> {
const isTest = this.configService.get('NODE_ENV') === 'test';
if (!isTest) {
throw new ForbiddenException('Cannot access outside of test');
}
await this.prismaService.dropDatabase();
}

async getState() {
const savedOptions = await this.getSavedOptions();
return {
Expand Down
61 changes: 57 additions & 4 deletions pnpm-lock.yaml

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

16 changes: 16 additions & 0 deletions testing/e2e/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "@opendatacapture/test",
"type": "module",
"version": "0.0.0",
"private": true,
"scripts": {
"lint": "tsc && eslint --fix .",
"test": "env-cmd -f ../../.env playwright test",
"test:dev": "env-cmd -f ../../.env playwright test --ui"
},
"dependencies": {
"@douglasneuroinformatics/libjs": "catalog:",
"@opendatacapture/schemas": "workspace:*",
"@playwright/test": "^1.51.1"
}
}
67 changes: 67 additions & 0 deletions testing/e2e/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as path from 'node:path';

import { parseNumber } from '@douglasneuroinformatics/libjs';
import { defineConfig, devices } from '@playwright/test';

const API_PORT = parseNumber(process.env.API_DEV_SERVER_PORT);
const WEB_PORT = parseNumber(process.env.WEB_DEV_SERVER_PORT);

export default defineConfig({
fullyParallel: true,
outputDir: path.resolve(import.meta.dirname, '.output/results'),
projects: [
{
name: 'Global Setup',
teardown: 'Global Teardown',
testMatch: '**/global/global.setup.spec.ts'
},
{
name: 'Global Teardown',
testMatch: '**/global/global.teardown.spec.ts'
},
{
dependencies: ['Global Setup'],
name: 'Desktop Chrome',
testIgnore: '**/global/**',
use: { ...devices['Desktop Chrome'] }
},
{
dependencies: ['Global Setup'],
name: 'Desktop Firefox',
testIgnore: '**/global/**',
use: { ...devices['Desktop Firefox'] }
},
{
dependencies: ['Global Setup'],
name: 'Desktop Safari',
testIgnore: '**/global/**',
use: { ...devices['Desktop Safari'] }
}
],
reporter: [['html', { open: 'never', outputFolder: path.resolve(import.meta.dirname, '.output/report') }]],
testDir: path.resolve(import.meta.dirname, 'src'),
use: {
baseURL: `http://localhost:${WEB_PORT}`,
trace: 'on-first-retry'
},
webServer: [
{
command: 'pnpm dev:test',
cwd: path.resolve(import.meta.dirname, '../../apps/api'),
gracefulShutdown: {
signal: 'SIGINT',
timeout: 1000
},
url: `http://localhost:${API_PORT}`
},
{
command: 'pnpm dev:test',
cwd: path.resolve(import.meta.dirname, '../../apps/web'),
gracefulShutdown: {
signal: 'SIGINT',
timeout: 1000
},
url: `http://localhost:${WEB_PORT}`
}
]
});
77 changes: 77 additions & 0 deletions testing/e2e/src/global/global.setup.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/* eslint-disable @typescript-eslint/no-namespace */
/* eslint-disable @typescript-eslint/consistent-type-definitions */

import type { LoginCredentials } from '@opendatacapture/schemas/auth';
import type { InitAppOptions } from '@opendatacapture/schemas/setup';
import { expect, test } from '@playwright/test';

const initOptions = {
admin: {
firstName: 'Jane',
lastName: 'Doe',
password: 'DataCapture2025',
username: 'admin'
},
dummySubjectCount: 10,
enableExperimentalFeatures: false,
initDemo: true,
recordsPerSubject: 10
} satisfies InitAppOptions;

declare global {
namespace NodeJS {
interface ProcessEnv {
ADMIN_ACCESS_TOKEN: string;
ADMIN_PASSWORD: string;
ADMIN_USERNAME: string;
GLOBAL_SETUP_COMPLETE?: '1';
}
}
}

test.skip(() => process.env.GLOBAL_SETUP_COMPLETE === '1');

test.describe.serial(() => {
test.describe.serial('setup', () => {
test('initial setup', async ({ request }) => {
const response = await request.get('/api/v1/setup');
expect(response.status()).toBe(200);
await expect(response.json()).resolves.toMatchObject({ isSetup: false });
});
test('successful setup', async ({ page }) => {
await page.goto('/setup');
await expect(page).toHaveURL('/setup');
const setupForm = page.locator('form[data-cy="setup-form"]');
await setupForm.locator('input[name="firstName"]').fill(initOptions.admin.firstName);
await setupForm.locator('input[name="lastName"]').fill(initOptions.admin.lastName);
await setupForm.locator('input[name="username"]').fill(initOptions.admin.username);
await setupForm.locator('input[name="password"]').fill(initOptions.admin.password);
await setupForm.locator('input[name="confirmPassword"]').fill(initOptions.admin.password);
await setupForm.locator('#initDemo-true').click();
await setupForm.locator('input[name="dummySubjectCount"]').fill(initOptions.dummySubjectCount.toString());
await setupForm.locator('input[name="recordsPerSubject"]').fill(initOptions.recordsPerSubject.toString());
await setupForm.getByLabel('Submit').click();
await expect(page).toHaveURL('/dashboard');
});
test('setup state after initialization', async ({ request }) => {
const response = await request.get('/api/v1/setup');
expect(response.status()).toBe(200);
await expect(response.json()).resolves.toMatchObject({ isSetup: true });
});
});
test.describe.serial('auth', () => {
test('login', async ({ request }) => {
const { password, username } = initOptions.admin;
const response = await request.post('/api/v1/auth/login', {
data: { password, username } satisfies LoginCredentials
});
expect(response.status()).toBe(200);
const { accessToken } = await response.json();
expect(typeof accessToken).toBe('string');
process.env.ADMIN_ACCESS_TOKEN = accessToken;
process.env.ADMIN_USERNAME = username;
process.env.ADMIN_PASSWORD = password;
process.env.GLOBAL_SETUP_COMPLETE = '1';
});
});
});
10 changes: 10 additions & 0 deletions testing/e2e/src/global/global.teardown.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { expect, test } from '@playwright/test';

test('delete database', async ({ request }) => {
const response = await request.delete('/api/v1/setup', {
headers: {
Authorization: `Bearer ${process.env.ADMIN_ACCESS_TOKEN}`
}
});
expect(response.status()).toBe(200);
});
4 changes: 4 additions & 0 deletions testing/e2e/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": ["../../tsconfig.base.json"],
"include": ["src/**/*", "playwright.config.ts"]
}