-
Notifications
You must be signed in to change notification settings - Fork 22
test: create end-to-end playwright suite #1648
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
37b4fd6
c166698
d6dd4bb
31472c9
db829d7
ffd1d81
4bfe5e9
843a3d2
017e0e9
541c14c
97f5149
3c7c23a
2878560
1a970c5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| # Unraid Server Configuration | ||
| UNRAID_URL=http://tower.local | ||
|
|
||
| # Authentication (if needed) | ||
| UNRAID_USERNAME=root | ||
| UNRAID_PASSWORD= | ||
|
|
||
| # Test Configuration | ||
| TEST_TIMEOUT=60000 | ||
| RETRY_COUNT=2 | ||
|
|
||
| # Browser Configuration | ||
| SLOW_MO=0 | ||
|
|
||
| # Debugging | ||
| DEBUG=false | ||
|
Comment on lines
+8
to
+16
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainConfirm these env vars are actually consumed (TEST_TIMEOUT, RETRY_COUNT, TRACE). If unused, drop them from the example to reduce confusion; if used, document them in README. 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Scanning for env var usage under e2e/"
rg -nP 'process\.env\.(UNRAID_URL|UNRAID_USERNAME|UNRAID_PASSWORD|TEST_TIMEOUT|RETRY_COUNT|SLOW_MO|DEBUG|TRACE)' e2e || trueLength of output: 880 Remove unused env vars from e2e/.env.example 🧰 Tools🪛 dotenv-linter (3.3.0)[warning] 10-10: [UnorderedKey] The RETRY_COUNT key should go before the TEST_TIMEOUT key (UnorderedKey) 🤖 Prompt for AI Agents |
||
| TRACE=false | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| node_modules/ | ||
| /test-results*/ | ||
| /playwright-report*/ | ||
| /blob-report*/ | ||
| /playwright/.cache/ | ||
| .env* | ||
| !.env.example | ||
| *.log | ||
| screenshots/ | ||
| videos/ | ||
| traces/ | ||
|
|
||
| # specific to each individual developer, not the project | ||
| auth.json |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,272 @@ | ||
| # Unraid WebGUI E2E Tests | ||
|
|
||
| End-to-end tests for Unraid WebGUI using Playwright. | ||
|
|
||
| ## Setup | ||
|
|
||
| 1. Copy `.env.example` to `.env` and configure: | ||
| ```bash | ||
| cp .env.example .env | ||
| ``` | ||
|
|
||
| 2. Install dependencies: | ||
| ```bash | ||
| pnpm install | ||
| pnpm playwright:install | ||
| ``` | ||
|
|
||
| 3. Configure your Unraid server URL in `.env`: | ||
| ``` | ||
| UNRAID_URL=http://tower.local | ||
| UNRAID_USERNAME=root | ||
| UNRAID_PASSWORD=your_password | ||
| ``` | ||
|
|
||
| ## Creating Test Profiles | ||
|
|
||
| To test against multiple Unraid servers or configurations, create additional environment files: | ||
|
|
||
| 1. Copy `.env.example` to create new profiles: | ||
| ```bash | ||
| cp .env.example .env.test-server | ||
| cp .env.example .env.dev-server-2 | ||
| cp .env.example .env.staging | ||
| ``` | ||
|
|
||
| 2. Configure each profile with different server details: | ||
| ```bash | ||
| # .env.test-server | ||
| UNRAID_URL=https://test.example.com | ||
| UNRAID_USERNAME=root | ||
| UNRAID_PASSWORD=test_password | ||
|
|
||
| # .env.dev-server-2 | ||
| UNRAID_URL=http://192.168.1.50 | ||
| UNRAID_USERNAME=admin | ||
| UNRAID_PASSWORD=dev_password | ||
| ``` | ||
|
|
||
| You can then run tests against specific profiles using dotenvx or the justfile recipes (see below). | ||
|
|
||
| ## Running Tests | ||
|
|
||
| By default, `pnpm test` commands will read environment variables from `.env`. | ||
|
|
||
| Use `dotenvx` as shown below to override. | ||
|
|
||
| ```bash | ||
| # Run all tests | ||
| pnpm test | ||
|
|
||
| # Run tests in headed mode (see browser) | ||
| pnpm test:headed | ||
|
|
||
| # Debug tests interactively | ||
| pnpm test:debug | ||
|
|
||
| # Open Playwright UI | ||
| pnpm test:ui | ||
|
|
||
| # Run specific test file | ||
| pnpm test auth.spec.ts | ||
|
|
||
| # Run tests against different server | ||
| UNRAID_URL=http://192.168.1.100 pnpm test:e2e | ||
|
|
||
| # Run tests against different "profiles" | ||
| # -> Make sure not to track your profiles in git | ||
| dotenvx run -f .env.dev-server-2 -- pnpm test | ||
| ``` | ||
|
Comment on lines
+57
to
+79
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainVerify scripts listed here exist in e2e/package.json and match names. Catches drift between docs and scripts ( 🏁 Script executed: #!/bin/bash
set -euo pipefail
pkg="e2e/package.json"
jq -r '.scripts' "$pkg"
echo "Expecting keys: test, test:e2e, test:headed, test:debug, test:ui, test:report"Length of output: 522 Add missing 🤖 Prompt for AI Agents |
||
|
|
||
| ## Justfile Usage | ||
|
|
||
| This project includes a `justfile` for advanced testing workflows. Install [just](https://github.com/casey/just) if you haven't already. | ||
|
|
||
| ### Quick Start | ||
|
|
||
| ```bash | ||
| # Install dependencies (including dotenvx if not present) | ||
| just install | ||
|
|
||
| # List available commands | ||
| just | ||
|
|
||
| # List your environment profiles | ||
| just list-envs | ||
| ``` | ||
|
|
||
| ### Running Tests with Profiles | ||
|
|
||
| ```bash | ||
| # Run tests against a specific environment file | ||
| just test-env .env.production-server | ||
|
|
||
| # Run tests against a specific environment with additional args | ||
| just test-env .env.dev-server-2 --grep "login" | ||
|
|
||
| # Run tests against ALL environment files (excludes .env.example) | ||
| just test-all-envs | ||
|
|
||
| # Run tests against all environments with specific test pattern | ||
| just test-all-envs --grep "dashboard" | ||
| ``` | ||
|
|
||
| ### Headed Mode Testing | ||
|
|
||
| ```bash | ||
| # Run tests in headed mode for debugging | ||
| just test-env-headed .env.staging | ||
|
|
||
| # Run headed tests with specific browser | ||
| just test-env-headed .env.local --project=chromium | ||
| ``` | ||
|
|
||
| ### Managing Results | ||
|
|
||
| ```bash | ||
| # View report for a specific environment | ||
| just show-report production-server | ||
|
|
||
| # Clean all test artifacts and reports | ||
| just clean | ||
| ``` | ||
|
|
||
| ### Use Cases | ||
|
|
||
| - **Multi-environment validation**: Test the same suite against development, staging, and production servers | ||
| - **Configuration testing**: Validate behavior across different Unraid configurations (different plugins, versions, etc.) | ||
| - **Regression testing**: Run focused tests against specific environments when debugging issues | ||
| - **Batch testing**: Validate changes across your entire server fleet with one command | ||
|
|
||
| The justfile automatically organizes reports by environment (e.g., `playwright-report-production-server`) so you can easily compare results across different servers. | ||
|
|
||
| ## Test Structure | ||
|
|
||
| ``` | ||
| e2e/ | ||
| ├── fixtures/ # Test fixtures and setup | ||
| ├── tests/ # Test specifications | ||
| ├── utils/ # Helper functions and page objects | ||
| │ ├── pages/ # Page object models | ||
| │ └── helpers.ts # Utility functions | ||
| └── playwright.config.ts | ||
| ``` | ||
|
|
||
| ## Writing Tests | ||
|
|
||
| ### Basic Test | ||
| ```typescript | ||
| import { test, expect } from '@playwright/test'; | ||
|
|
||
| test('should load dashboard', async ({ page }) => { | ||
| await page.goto('/Dashboard'); | ||
| await expect(page.locator('h1')).toContainText('Dashboard'); | ||
| }); | ||
| ``` | ||
|
|
||
| ### With Authentication | ||
| ```typescript | ||
| import { test, expect } from '../fixtures/auth.fixture'; | ||
|
|
||
| test('authenticated test', async ({ page, authenticatedPage }) => { | ||
| // Already logged in | ||
| await page.goto('/Settings'); | ||
| // ... test code | ||
| }); | ||
| ``` | ||
|
|
||
| ### Page Objects | ||
| ```typescript | ||
| import { DashboardPage } from '../utils/pages/dashboard.page'; | ||
|
|
||
| test('using page object', async ({ page }) => { | ||
| const dashboard = new DashboardPage(page); | ||
| await dashboard.goto(); | ||
| await dashboard.navigateTo('Docker'); | ||
| }); | ||
| ``` | ||
|
|
||
| ## CI/CD Integration | ||
|
|
||
| Tests can run in CI with: | ||
| ```yaml | ||
| - name: Run E2E Tests | ||
| env: | ||
| UNRAID_URL: ${{ secrets.UNRAID_URL }} | ||
| UNRAID_PASSWORD: ${{ secrets.UNRAID_PASSWORD }} | ||
| run: pnpm test:e2e | ||
| ``` | ||
|
|
||
| ## Logging | ||
|
|
||
| Tests automatically capture console logs and redirect them to structured log files. You don't need to import anything - just use standard console methods: | ||
|
|
||
| ```typescript | ||
| test('example test', async ({ page }) => { | ||
| console.log('Starting test'); | ||
| console.info('User navigated to dashboard'); | ||
| console.warn('Slow network detected'); | ||
| console.error('Authentication failed'); | ||
|
|
||
| // All logs are automatically captured with test context | ||
| }); | ||
| ``` | ||
|
|
||
| ### Log Files Location | ||
|
|
||
| Logs are organized by browser and test hierarchy: | ||
| ``` | ||
| test-results/logs/ | ||
| ├── chromium/ | ||
| ├── firefox/ | ||
| └── mobile-chrome/ | ||
| └── test-file/ | ||
| └── test-suite/ | ||
| └── test-name/ | ||
| └── timestamp.log | ||
| ``` | ||
|
|
||
| ### Log Format | ||
|
|
||
| Each log entry includes: | ||
| - Timestamp (millisecond precision) | ||
| - Log level (INFO, WARN, ERROR) | ||
| - Browser name | ||
| - Full test path | ||
| - Message and metadata | ||
|
|
||
| Example log entry: | ||
| ``` | ||
| [2025-09-04 15:54:14.774] [INFO] [chromium] [dashboard.spec.ts > Dashboard > should display navigation menu] Found menu item: Main | ||
| ``` | ||
|
|
||
| ### Direct Logger Access | ||
|
|
||
| For more control, you can access the logger directly: | ||
|
|
||
| ```typescript | ||
| test('advanced logging', async ({ logger }) => { | ||
| logger.info('Test started'); | ||
| logger.debug('Detailed debug info', { userId: 123 }); | ||
| logger.warn('Performance issue detected', { loadTime: 5000 }); | ||
| logger.error('Critical failure', new Error('Database connection lost')); | ||
| }); | ||
| ``` | ||
|
|
||
| ## Debugging | ||
|
|
||
| - Screenshots on failure: `test-results/` | ||
| - Videos: `test-results/` (on failure) | ||
| - Traces: `test-results/` (on retry) | ||
| - **Logs: `test-results/logs/` (organized by browser/test)** | ||
| - HTML Report: `pnpm --filter @unraid/e2e test:report` | ||
|
|
||
| ## Environment Variables | ||
|
|
||
| | Variable | Description | Default | | ||
| |----------|-------------|---------| | ||
| | UNRAID_URL | Unraid server URL | http://tower.local | | ||
| | UNRAID_USERNAME | Username for auth | root | | ||
| | UNRAID_PASSWORD | Password for auth | - | | ||
| | SLOW_MO | Slow down actions (ms) | 0 | | ||
| | DEBUG | Enable debug mode | false | | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,62 @@ | ||||||
| import { test as loggerTest, expect, LoggerFixtures } from './logger.fixture'; | ||||||
| import { LoginPage } from '../utils/pages/login.page.js'; | ||||||
|
|
||||||
| type AuthFixtures = { | ||||||
| loginPage: LoginPage; | ||||||
| authenticatedPage: void; | ||||||
| } & LoggerFixtures; | ||||||
|
|
||||||
| export const test = loggerTest.extend<AuthFixtures>({ | ||||||
| loginPage: async ({ page }, use) => { | ||||||
| const loginPage = new LoginPage(page); | ||||||
| await use(loginPage); | ||||||
| }, | ||||||
|
|
||||||
| authenticatedPage: async ({ page, logger }, use) => { | ||||||
| const username = process.env.UNRAID_USERNAME || 'root'; | ||||||
| const password = process.env.UNRAID_PASSWORD || ''; | ||||||
|
|
||||||
| if (password) { | ||||||
| logger.info('Attempting authentication'); | ||||||
| await page.goto('/'); | ||||||
|
|
||||||
| // Wait for page to fully load | ||||||
| await page.waitForLoadState('networkidle'); | ||||||
|
|
||||||
| // Check if we need to authenticate | ||||||
| const needsAuth = await page.locator('input[name="username"], input[name="password"], input#user, input#pass').count() > 0; | ||||||
|
|
||||||
| if (needsAuth) { | ||||||
| logger.debug('Authentication required, filling credentials'); | ||||||
| // Try different selector combinations for username/password fields | ||||||
| const usernameInput = page.locator('input[name="username"], input#user').first(); | ||||||
| const passwordInput = page.locator('input[name="password"], input#pass').first(); | ||||||
| const submitButton = page.locator('button[type="submit"], input[type="submit"], button:has-text("Login")').first(); | ||||||
|
|
||||||
| await usernameInput.fill(username); | ||||||
| await passwordInput.fill(password); | ||||||
| await submitButton.click(); | ||||||
|
|
||||||
|
Comment on lines
+32
to
+39
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Stabilize the login sequence (wait for visibility and assert redirect) Wait for fields, click, then assert we left the login route. This reduces flakiness across CI. - await usernameInput.fill(username);
- await passwordInput.fill(password);
- await submitButton.click();
+ await usernameInput.waitFor({ state: 'visible' });
+ await passwordInput.waitFor({ state: 'visible' });
+ await submitButton.waitFor({ state: 'attached' });
+ await usernameInput.fill(username);
+ await passwordInput.fill(password);
+ await Promise.all([
+ submitButton.click(),
+ page.waitForLoadState('domcontentloaded'),
+ ]);
@@
- const stillOnLogin = page.url().includes('login');
- if (!stillOnLogin) {
+ await expect(page).not.toHaveURL(/login/i);
+ if (!/login/i.test(page.url())) {
await page.context().storageState({ path: 'auth.json' });
}Also applies to: 38-45 🤖 Prompt for AI Agents |
||||||
| // Wait for navigation to complete | ||||||
| await page.waitForLoadState('networkidle'); | ||||||
|
|
||||||
| // Check if we successfully logged in | ||||||
| const stillOnLogin = page.url().includes('login'); | ||||||
| if (!stillOnLogin) { | ||||||
| logger.info('Authentication successful, saving storage state'); | ||||||
| await page.context().storageState({ path: 'auth.json' }); | ||||||
| } else { | ||||||
| logger.warn('Authentication may have failed - still on login page'); | ||||||
| } | ||||||
| } else { | ||||||
| logger.debug('No authentication needed'); | ||||||
| } | ||||||
| } else { | ||||||
| logger.debug('No password configured, skipping authentication'); | ||||||
| } | ||||||
|
|
||||||
| await use(); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Call use(undefined) for void fixtures TypeScript expects one arg even when the fixture type is void; omitting it can cause a compile error. - await use();
+ await use(undefined);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| }, | ||||||
| }); | ||||||
|
|
||||||
| export { expect }; | ||||||
Uh oh!
There was an error while loading. Please reload this page.