Skip to content

Commit 3352acc

Browse files
committed
feat: implement Playwright e2e testing infrastructure
Add complete e2e testing setup using Playwright following best practices: - Add Playwright dependencies and test scripts to package.json - Configure Playwright with setup project pattern for authentication - Create test helpers for Frappe API, authentication, and workflow operations - Implement Page Object Models for Login, WorkflowList, and WorkflowEditor - Add test specifications for auth, workflow CRUD, and editor interactions - Create GitHub Actions workflow for automated UI testing Key features: - Uses Playwright's recommended "setup project" pattern for auth state reuse - Pre-authenticated tests via storageState for faster execution - Fresh state tests for authentication flow testing - Proper cleanup with try/finally blocks for test isolation - CI workflow with Frappe bench setup and Playwright browser caching Directory structure: e2e/ ├── helpers/ # Auth, Frappe API, workflow utilities ├── pages/ # Page Object Models ├── tests/ # Test specifications └── tsconfig.json
1 parent c2ee3ad commit 3352acc

17 files changed

Lines changed: 1670 additions & 2 deletions

.github/workflows/ui-tests.yml

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
name: UI Tests
2+
3+
on:
4+
push:
5+
branches:
6+
- develop
7+
pull_request:
8+
workflow_dispatch:
9+
10+
concurrency:
11+
group: ui-tests-hazelnode-${{ github.event.number || github.ref }}
12+
cancel-in-progress: true
13+
14+
jobs:
15+
ui-tests:
16+
runs-on: ubuntu-latest
17+
timeout-minutes: 60
18+
name: Playwright E2E Tests
19+
20+
services:
21+
redis-cache:
22+
image: redis:alpine
23+
ports:
24+
- 13000:6379
25+
redis-queue:
26+
image: redis:alpine
27+
ports:
28+
- 11000:6379
29+
mariadb:
30+
image: mariadb:10.6
31+
env:
32+
MYSQL_ROOT_PASSWORD: root
33+
ports:
34+
- 3306:3306
35+
options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3
36+
37+
steps:
38+
- name: Clone
39+
uses: actions/checkout@v4
40+
41+
- name: Setup Python
42+
uses: actions/setup-python@v5
43+
with:
44+
python-version: "3.14"
45+
46+
- name: Setup Node
47+
uses: actions/setup-node@v4
48+
with:
49+
node-version: 24
50+
check-latest: true
51+
52+
- name: Add to Hosts
53+
run: echo "127.0.0.1 hazelnode.test" | sudo tee -a /etc/hosts
54+
55+
- name: Cache pip
56+
uses: actions/cache@v4
57+
with:
58+
path: ~/.cache/pip
59+
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py', '**/setup.cfg') }}
60+
restore-keys: |
61+
${{ runner.os }}-pip-
62+
${{ runner.os }}-
63+
64+
- name: Get yarn cache directory path
65+
id: yarn-cache-dir-path
66+
run: 'echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT'
67+
68+
- name: Cache yarn
69+
uses: actions/cache@v4
70+
with:
71+
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
72+
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
73+
restore-keys: |
74+
${{ runner.os }}-yarn-
75+
76+
- name: Cache Playwright browsers
77+
uses: actions/cache@v4
78+
with:
79+
path: ~/.cache/ms-playwright
80+
key: ${{ runner.os }}-playwright-${{ hashFiles('**/package.json') }}
81+
restore-keys: |
82+
${{ runner.os }}-playwright-
83+
84+
- name: Install MariaDB Client
85+
run: |
86+
sudo apt update
87+
sudo apt-get install mariadb-client
88+
89+
- name: Setup Bench
90+
run: |
91+
pip install frappe-bench
92+
bench init --skip-redis-config-generation --skip-assets --python "$(which python)" ~/frappe-bench
93+
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'"
94+
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
95+
96+
- name: Install Hazelnode
97+
working-directory: /home/runner/frappe-bench
98+
run: |
99+
bench get-app hazelnode $GITHUB_WORKSPACE
100+
bench setup requirements --dev
101+
bench new-site --db-root-password root --admin-password admin hazelnode.test
102+
bench --site hazelnode.test install-app hazelnode
103+
bench build
104+
env:
105+
CI: "Yes"
106+
107+
- name: Configure Site for UI Tests
108+
working-directory: /home/runner/frappe-bench
109+
run: |
110+
bench --site hazelnode.test set-config allow_tests true
111+
bench --site hazelnode.test set-config host_name "http://hazelnode.test:8000"
112+
113+
- name: Start Frappe Server
114+
working-directory: /home/runner/frappe-bench
115+
run: |
116+
# Disable watch and schedule to reduce resource usage
117+
sed -i 's/^watch:/# watch:/g' Procfile
118+
sed -i 's/^schedule:/# schedule:/g' Procfile
119+
120+
# Start bench in background
121+
bench start &> bench_start.log &
122+
123+
# Wait for server to be ready
124+
echo "Waiting for Frappe server to start..."
125+
timeout 60 bash -c 'until curl -s http://hazelnode.test:8000 > /dev/null; do sleep 2; done'
126+
echo "Frappe server is ready!"
127+
128+
- name: Install Playwright
129+
run: |
130+
npm ci
131+
npx playwright install --with-deps chromium
132+
133+
- name: Run Playwright Tests
134+
run: npx playwright test
135+
env:
136+
BASE_URL: http://hazelnode.test:8000
137+
FRAPPE_USER: Administrator
138+
FRAPPE_PASSWORD: admin
139+
140+
- name: Upload Playwright Report
141+
uses: actions/upload-artifact@v4
142+
if: always()
143+
with:
144+
name: playwright-report
145+
path: playwright-report/
146+
retention-days: 7
147+
148+
- name: Upload Test Results
149+
uses: actions/upload-artifact@v4
150+
if: always()
151+
with:
152+
name: test-results
153+
path: test-results/
154+
retention-days: 7
155+
156+
- name: Show Bench Logs on Failure
157+
if: failure()
158+
working-directory: /home/runner/frappe-bench
159+
run: |
160+
echo "=== Bench Start Log ==="
161+
cat bench_start.log || true
162+
echo ""
163+
echo "=== Frappe Logs ==="
164+
cat logs/*.log || true

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,8 @@ __pycache__
88
hazelnode/public/frontend
99
hazelnode/www/frontend.html
1010
hazelnode/www/hazelnode.html
11+
12+
# Playwright
13+
e2e/.auth/
14+
playwright-report/
15+
test-results/

e2e/helpers/auth.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { APIRequestContext, Page, BrowserContext } from '@playwright/test';
2+
3+
const STORAGE_STATE_PATH = 'e2e/.auth/user.json';
4+
5+
/**
6+
* Login via Frappe API (faster than UI login).
7+
* Sets cookies on the request context for subsequent API calls.
8+
*/
9+
export async function loginViaAPI(
10+
request: APIRequestContext,
11+
email: string = 'Administrator',
12+
password: string = 'admin'
13+
): Promise<void> {
14+
const response = await request.post('/api/method/login', {
15+
form: {
16+
usr: email,
17+
pwd: password,
18+
},
19+
});
20+
21+
if (!response.ok()) {
22+
throw new Error(
23+
`Login failed: ${response.status()} ${await response.text()}`
24+
);
25+
}
26+
}
27+
28+
/**
29+
* Login via UI (for testing the login flow itself).
30+
*/
31+
export async function loginViaUI(
32+
page: Page,
33+
email: string = 'Administrator',
34+
password: string = 'admin'
35+
): Promise<void> {
36+
await page.goto('/login');
37+
await page.waitForLoadState('networkidle');
38+
39+
await page.fill('input[data-fieldname="email"]', email);
40+
await page.fill('input[data-fieldname="password"]', password);
41+
await page.click('button[type="submit"]');
42+
43+
// Wait for redirect to desk/app
44+
await page.waitForURL(/\/(app|desk)/, { timeout: 30000 });
45+
}
46+
47+
/**
48+
* Logout the current user.
49+
*/
50+
export async function logout(page: Page): Promise<void> {
51+
await page.goto('/api/method/logout');
52+
await page.waitForLoadState('networkidle');
53+
}
54+
55+
/**
56+
* Save authentication state for reuse across tests.
57+
*/
58+
export async function saveAuthState(context: BrowserContext): Promise<void> {
59+
await context.storageState({ path: STORAGE_STATE_PATH });
60+
}
61+
62+
/**
63+
* Get the storage state path for authenticated sessions.
64+
*/
65+
export function getStorageStatePath(): string {
66+
return STORAGE_STATE_PATH;
67+
}
68+
69+
/**
70+
* Check if user is logged in by verifying session.
71+
*/
72+
export async function isLoggedIn(
73+
request: APIRequestContext
74+
): Promise<boolean> {
75+
try {
76+
const response = await request.get(
77+
'/api/method/frappe.auth.get_logged_user'
78+
);
79+
if (!response.ok()) return false;
80+
81+
const data = await response.json();
82+
return data.message && data.message !== 'Guest';
83+
} catch {
84+
return false;
85+
}
86+
}

0 commit comments

Comments
 (0)