Skip to content

Commit c8a7147

Browse files
authored
Merge pull request #1263 from joshunrau/add-test
update e2e tests
2 parents 92a8405 + 1ae43bf commit c8a7147

18 files changed

+249
-20
lines changed

apps/web/src/vite-env.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
/* eslint-disable @typescript-eslint/consistent-type-definitions */
22

3+
/// <reference types="vite/client" />
4+
35
// All of these should be undefined in production
46
interface ImportMetaDevEnv {
57
readonly VITE_DEV_BYPASS_AUTH?: string;

apps/web/tsconfig.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
"lib": ["DOM", "DOM.Iterable", "ESNext"],
66
"paths": {
77
"@/*": ["./src/*"]
8-
},
9-
"types": ["vite/client"]
8+
}
109
},
1110
"include": ["src/**/*", "vite.config.ts", "vitest.config.ts"]
1211
}

pnpm-lock.yaml

Lines changed: 3 additions & 0 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: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
"scripts": {
77
"lint": "tsc && eslint --fix .",
88
"test": "env-cmd -f ../../.env playwright test",
9+
"test:chrome": "env-cmd -f ../../.env playwright test --project='*Desktop Chrome'",
910
"test:dev": "env-cmd -f ../../.env playwright test --ui"
1011
},
1112
"dependencies": {
1213
"@douglasneuroinformatics/libjs": "catalog:",
1314
"@opendatacapture/schemas": "workspace:*",
14-
"@playwright/test": "^1.51.1"
15+
"@playwright/test": "^1.51.1",
16+
"type-fest": "workspace:type-fest__4.x@*"
1517
}
1618
}

testing/e2e/playwright.config.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ const baseURL = `http://localhost:${webPort}`;
2222

2323
const browsers: { target: BrowserTarget; use: Project['use'] }[] = [
2424
{ target: 'Desktop Chrome', use: { ...devices['Desktop Chrome'], channel: 'chromium', headless: true } },
25-
{ target: 'Desktop Firefox', use: { ...devices['Desktop Firefox'], headless: true } },
26-
{ target: 'Desktop Safari', use: devices['Desktop Safari'] }
25+
{ target: 'Desktop Firefox', use: { ...devices['Desktop Firefox'], headless: true } }
2726
] as const;
2827

2928
export default defineConfig({

testing/e2e/src/1.1-auth.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { expect, test } from './helpers/fixtures';
2+
3+
// no need to test the actual login here as it is tested on every other page
4+
5+
test.describe('redirects', () => {
6+
test('should redirect to login page from the index page', async ({ page }) => {
7+
await page.goto('/');
8+
await expect(page).toHaveURL('/auth/login');
9+
});
10+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { expect, test } from './helpers/fixtures';
2+
3+
test.describe('dashboard', () => {
4+
test.beforeEach(async ({ login, page }) => {
5+
await login();
6+
await expect(page).toHaveURL('/dashboard');
7+
});
8+
9+
test('should display the dashboard header', async ({ getPageModel }) => {
10+
const dashboardPage = getPageModel('/dashboard');
11+
await expect(dashboardPage.pageHeader).toBeVisible();
12+
await expect(dashboardPage.pageHeader).toContainText('Dashboard');
13+
});
14+
});

testing/e2e/src/global/global.setup.spec.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import type { $LoginCredentials } from '@opendatacapture/schemas/auth';
22

3-
import { initAppOptions } from '../helpers/data';
3+
import { groups, initAppOptions, users } from '../helpers/data';
44
import { expect, test } from '../helpers/fixtures';
55

6+
import type { TestDataMap } from '../helpers/types';
7+
68
test.describe.serial(() => {
79
test.describe.serial('setup', () => {
810
test('should initially not be setup', async ({ request }) => {
911
const response = await request.get('/api/v1/setup');
1012
expect(response.status()).toBe(200);
1113
await expect(response.json()).resolves.toMatchObject({ isSetup: false });
1214
});
13-
test('should successfully setup', async ({ setupPage }) => {
15+
test('should successfully setup', async ({ getPageModel }) => {
16+
const setupPage = getPageModel('/setup');
17+
await setupPage.goto('/setup');
1418
await setupPage.fillSetupForm(initAppOptions);
1519
await setupPage.expect.toHaveURL('/auth/login');
1620
});
@@ -44,4 +48,35 @@ test.describe.serial(() => {
4448
process.env.ADMIN_PASSWORD = password;
4549
});
4650
});
51+
52+
test.describe.serial('creating groups', () => {
53+
test('creating groups', async ({ request }) => {
54+
const createdGroupIds = {} as TestDataMap<string>;
55+
for (const browser in groups) {
56+
const response = await request.post('/api/v1/groups', {
57+
data: groups[browser as keyof typeof groups],
58+
headers: {
59+
Authorization: `Bearer ${process.env.ADMIN_ACCESS_TOKEN}`
60+
}
61+
});
62+
expect(response.status()).toBe(201);
63+
const data = await response.json();
64+
expect(data).toMatchObject({ id: expect.any(String) });
65+
createdGroupIds[browser as keyof typeof groups] = data.id;
66+
}
67+
68+
for (const key in users) {
69+
const response = await request.post('/api/v1/users', {
70+
data: {
71+
...users[key as keyof typeof groups],
72+
groupIds: [createdGroupIds[key as keyof typeof users]]
73+
},
74+
headers: {
75+
Authorization: `Bearer ${process.env.ADMIN_ACCESS_TOKEN}`
76+
}
77+
});
78+
expect(response.status()).toBe(201);
79+
}
80+
});
81+
});
4782
});

testing/e2e/src/helpers/data.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
import type { CreateGroupData } from '@opendatacapture/schemas/group';
12
import type { InitAppOptions } from '@opendatacapture/schemas/setup';
3+
import type { CreateUserData } from '@opendatacapture/schemas/user';
4+
5+
import type { TestDataMap } from './types';
26

37
export const initAppOptions = {
48
admin: {
@@ -12,3 +16,31 @@ export const initAppOptions = {
1216
initDemo: true,
1317
recordsPerSubject: 10
1418
} satisfies InitAppOptions;
19+
20+
export const groups: TestDataMap<CreateGroupData> = {
21+
'Desktop Chrome': {
22+
name: 'Clinical Group',
23+
type: 'CLINICAL'
24+
},
25+
'Desktop Firefox': {
26+
name: 'Research Group',
27+
type: 'RESEARCH'
28+
}
29+
};
30+
31+
export const users: TestDataMap<Omit<CreateUserData, 'groupIds'>> = {
32+
'Desktop Chrome': {
33+
basePermissionLevel: 'GROUP_MANAGER',
34+
firstName: 'John',
35+
lastName: 'Smith',
36+
password: 'DataCapture2026_User1',
37+
username: 'john_smith'
38+
},
39+
'Desktop Firefox': {
40+
basePermissionLevel: 'GROUP_MANAGER',
41+
firstName: 'Jane',
42+
lastName: 'Doe',
43+
password: 'DataCapture2026_User2',
44+
username: 'jane_doe'
45+
}
46+
};

testing/e2e/src/helpers/fixtures.ts

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,57 @@
1-
/* eslint-disable @typescript-eslint/no-empty-object-type */
1+
/* eslint-disable no-empty-pattern */
22

33
import { test as base, expect } from '@playwright/test';
44

5+
import { LoginPage } from '../pages/auth/login.page';
6+
import { DashboardPage } from '../pages/dashboard.page';
7+
import { SubjectDataTablePage } from '../pages/datahub/subject-data-table.page';
58
import { SetupPage } from '../pages/setup.page';
9+
import { users } from './data';
10+
11+
import type { ProjectMetadata, RouteTo } from './types';
12+
13+
type PageModels = typeof pageModels;
614

715
type TestArgs = {
8-
setupPage: SetupPage;
16+
getPageModel: <TKey extends Extract<keyof PageModels, RouteTo>>(key: TKey) => InstanceType<PageModels[TKey]>;
17+
login: () => Promise<void>;
918
};
1019

11-
export const test = base.extend<TestArgs, {}>({
12-
setupPage: async ({ page }, use) => {
13-
const setupPage = new SetupPage(page);
14-
await setupPage.goto();
15-
return use(setupPage);
20+
type WorkerArgs = {
21+
getProjectMetadata: <TKey extends Extract<keyof ProjectMetadata, string>>(key: TKey) => ProjectMetadata[TKey];
22+
};
23+
24+
const pageModels = {
25+
'/auth/login': LoginPage,
26+
'/dashboard': DashboardPage,
27+
'/datahub/$subjectId/table': SubjectDataTablePage,
28+
'/setup': SetupPage
29+
} satisfies { [K in RouteTo]?: any };
30+
31+
export const test = base.extend<TestArgs, WorkerArgs>({
32+
getPageModel: ({ page }, use) => {
33+
return use(<TKey extends Extract<keyof PageModels, RouteTo>>(key: TKey) => {
34+
const pageModel = new pageModels[key](page) as InstanceType<PageModels[TKey]>;
35+
return pageModel;
36+
});
37+
},
38+
getProjectMetadata: [
39+
async ({}, use, workerInfo) => {
40+
return use((key) => {
41+
return (workerInfo.project.metadata as ProjectMetadata)[key];
42+
});
43+
},
44+
{ scope: 'worker' }
45+
],
46+
login: ({ getPageModel, getProjectMetadata }, use) => {
47+
return use(async () => {
48+
const loginPage = getPageModel('/auth/login');
49+
await loginPage.goto('/auth/login');
50+
const target = getProjectMetadata('browserTarget');
51+
const credentials = users[target];
52+
await loginPage.fillLoginForm(credentials);
53+
await loginPage.expect.toHaveURL('/dashboard');
54+
});
1655
}
1756
});
1857

0 commit comments

Comments
 (0)