Skip to content

Commit acf8fd7

Browse files
authored
test: make playwright tests end to end (#34)
1 parent 74be17c commit acf8fd7

37 files changed

Lines changed: 1368 additions & 1424 deletions

.github/workflows/frontend-ci.yml

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,21 @@ on:
66
- main
77
paths:
88
- 'frontend/**'
9+
- 'scripts/run_e2e.py'
910
- '.github/workflows/frontend-ci.yml'
1011
pull_request:
1112
branches:
1213
- main
1314
paths:
1415
- 'frontend/**'
16+
- 'scripts/run_e2e.py'
1517
- '.github/workflows/frontend-ci.yml'
1618
workflow_dispatch:
1719

1820
jobs:
1921
test:
2022
name: Run Frontend Tests
2123
runs-on: ubuntu-latest
22-
defaults:
23-
run:
24-
working-directory: frontend
2524

2625
steps:
2726
- name: Checkout code
@@ -34,17 +33,27 @@ jobs:
3433
cache: 'npm'
3534
cache-dependency-path: frontend/package-lock.json
3635

36+
- name: Set up Python 3.11
37+
uses: actions/setup-python@v5
38+
with:
39+
python-version: '3.11'
40+
cache: 'pip'
41+
cache-dependency-path: backend/requirements.txt
42+
3743
- name: Install dependencies
44+
working-directory: frontend
3845
run: npm ci
3946

4047
- name: Run Unit Tests
48+
working-directory: frontend
4149
run: npm run test
4250

43-
- name: Install Playwright Browsers
44-
run: npx playwright install --with-deps chromium
45-
4651
- name: Run UI Tests
47-
run: npm run test:ui
52+
run: python3 scripts/run_e2e.py
53+
env:
54+
E2E_INSTALL_FRONTEND_DEPS: "0"
55+
E2E_INSTALL_BACKEND_DEPS: "1"
56+
E2E_INSTALL_PLAYWRIGHT: "1"
4857

4958
- name: Upload Playwright Report
5059
uses: actions/upload-artifact@v4

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ routeTree.gen.ts
1818
test-results/
1919
playwright-report/
2020
playwright/.cache/
21+
.tmp/
2122

2223
# Backend
2324
venv

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,24 @@ To initialize the system, a default administrator account is created automatical
8787

8888
> **⚠️ Security Warning:** Please log in and change the password immediately after the first deployment.
8989

90+
### 🧪 End-to-End UI Tests
91+
92+
Run the E2E suite (local and CI use the same entrypoint):
93+
94+
```bash
95+
python3 scripts/run_e2e.py
96+
```
97+
98+
The runner will:
99+
- set up a disposable PostgreSQL container
100+
- start the backend against that database
101+
- run frontend Playwright E2E tests
102+
- clean up started services when tests finish
103+
104+
Optional environment variables:
105+
- `E2E_INSTALL_FRONTEND_DEPS=0|1|auto` (default: `auto`)
106+
- `E2E_INSTALL_BACKEND_DEPS=0|1|auto` (default: `auto`)
107+
- `E2E_INSTALL_PLAYWRIGHT=0|1|auto` (default: `auto`)
108+
- `E2E_FRONTEND_HOST` / `E2E_FRONTEND_PORT` (defaults: `localhost` / `3000`)
109+
- `E2E_BACKEND_HOST` / `E2E_BACKEND_PORT` (defaults: `127.0.0.1` / `8000`)
110+
- `E2E_KEEP_STACK=1` (keep DB container running after test run)
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import {expect, test, type Page} from "playwright/test";
2+
import {
3+
DEFAULT_TEST_PASSWORD,
4+
createUserViaUserManagement,
5+
gotoSettings,
6+
gotoUserManagement,
7+
login,
8+
loginAsAdmin,
9+
logout,
10+
uniqueUsername,
11+
} from "./helpers";
12+
13+
async function createRegularUserAndLogin(page: Page): Promise<{
14+
username: string;
15+
password: string;
16+
}> {
17+
const username = uniqueUsername("e2e_account");
18+
const password = DEFAULT_TEST_PASSWORD;
19+
20+
await loginAsAdmin(page);
21+
await gotoUserManagement(page);
22+
await createUserViaUserManagement(page, username, password);
23+
await logout(page);
24+
await login(page, username, password);
25+
26+
return {username, password};
27+
}
28+
29+
async function submitLoginForm(
30+
page: Page,
31+
username: string,
32+
password: string,
33+
): Promise<void> {
34+
await page.goto("/login");
35+
36+
const loginForm = page.locator("form").first();
37+
await loginForm.locator('input[autocomplete="username"]').fill(username);
38+
await loginForm
39+
.locator('input[autocomplete="current-password"]')
40+
.fill(password);
41+
await loginForm.getByRole("button", {name: "Login"}).click();
42+
}
43+
44+
async function expectLoginFailure(
45+
page: Page,
46+
username: string,
47+
password: string,
48+
): Promise<void> {
49+
await submitLoginForm(page, username, password);
50+
await expect(page.getByText("Invalid username or password")).toBeVisible();
51+
}
52+
53+
test.describe("account settings", () => {
54+
test("renames username and allows login with the new name", async ({page}) => {
55+
const {password} = await createRegularUserAndLogin(page);
56+
const renamedUsername = uniqueUsername("e2e_account_renamed");
57+
58+
await gotoSettings(page);
59+
60+
const usernameInput = page.getByPlaceholder("Your username...");
61+
await usernameInput.fill(renamedUsername);
62+
await page.getByRole("button", {name: "Save Changes"}).click();
63+
await expect(
64+
page.getByText("Username successfully updated!"),
65+
).toBeVisible({timeout: 15_000});
66+
67+
await expect(usernameInput).toHaveValue(renamedUsername);
68+
69+
await logout(page);
70+
await login(page, renamedUsername, password);
71+
});
72+
73+
test("changes password end-to-end", async ({page}) => {
74+
const {username, password} = await createRegularUserAndLogin(page);
75+
const newPassword = "Password123!New";
76+
77+
await gotoSettings(page);
78+
79+
await page.getByPlaceholder("Your current password...").fill(password);
80+
await page.getByPlaceholder("Your new password...").fill(newPassword);
81+
await page
82+
.getByPlaceholder("Your new password again...")
83+
.fill(newPassword);
84+
85+
await page.getByRole("button", {name: "Update Password"}).click();
86+
await expect(
87+
page.getByText("Password successfully updated!"),
88+
).toBeVisible({timeout: 15_000});
89+
90+
await logout(page);
91+
92+
await expectLoginFailure(page, username, password);
93+
await login(page, username, newPassword);
94+
});
95+
96+
test("deactivates account end-to-end", async ({page}) => {
97+
const {username, password} = await createRegularUserAndLogin(page);
98+
99+
await gotoSettings(page);
100+
101+
await page.getByRole("button", {name: "Deactivate this account"}).click();
102+
103+
const dialog = page.getByRole("alertdialog").first();
104+
await dialog
105+
.getByRole("button", {name: "Yes, deactivate my account"})
106+
.click();
107+
108+
await expect(page).toHaveURL(/\/login$/, {timeout: 15_000});
109+
await expect(page.getByText("Welcome to Triage!")).toBeVisible({
110+
timeout: 15_000,
111+
});
112+
113+
await expectLoginFailure(page, username, password);
114+
});
115+
116+
test("shows password mismatch validation", async ({page}) => {
117+
const {password} = await createRegularUserAndLogin(page);
118+
119+
await gotoSettings(page);
120+
121+
await page.getByPlaceholder("Your current password...").fill(password);
122+
await page.getByPlaceholder("Your new password...").fill("Password123!A");
123+
await page
124+
.getByPlaceholder("Your new password again...")
125+
.fill("DifferentPassword123!");
126+
127+
await page.getByRole("button", {name: "Update Password"}).click();
128+
129+
await expect(
130+
page.getByText(
131+
"Passwords do not match. Please ensure both password fields are identical.",
132+
),
133+
).toBeVisible();
134+
});
135+
136+
test("rejects password change when current password is wrong", async ({page}) => {
137+
const {username, password} = await createRegularUserAndLogin(page);
138+
const attemptedNewPassword = "Password123!Changed";
139+
140+
await gotoSettings(page);
141+
142+
await page
143+
.getByPlaceholder("Your current password...")
144+
.fill("wrong-current-password");
145+
await page
146+
.getByPlaceholder("Your new password...")
147+
.fill(attemptedNewPassword);
148+
await page
149+
.getByPlaceholder("Your new password again...")
150+
.fill(attemptedNewPassword);
151+
152+
await page.getByRole("button", {name: "Update Password"}).click();
153+
154+
await logout(page);
155+
156+
await expectLoginFailure(page, username, attemptedNewPassword);
157+
await login(page, username, password);
158+
});
159+
160+
test("prevents changing username to an existing one", async ({page}) => {
161+
await createRegularUserAndLogin(page);
162+
163+
await gotoSettings(page);
164+
165+
await page.getByPlaceholder("Your username...").fill("admin");
166+
await page.getByRole("button", {name: "Save Changes"}).click();
167+
168+
await expect(
169+
page.getByText(
170+
"A user with this name already exists. Please choose a different username.",
171+
),
172+
).toBeVisible();
173+
});
174+
});

0 commit comments

Comments
 (0)