Skip to content
Closed
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
17 changes: 17 additions & 0 deletions e2e/.env.example
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=

Comment thread
pujitm marked this conversation as resolved.
# Test Configuration
TEST_TIMEOUT=60000
RETRY_COUNT=2

# Browser Configuration
SLOW_MO=0

# Debugging
DEBUG=false
Comment on lines +8 to +16

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Confirm 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 || true

Length of output: 880


Remove unused env vars from e2e/.env.example
The following variables aren’t referenced anywhere under e2e/ (TEST_TIMEOUT, RETRY_COUNT, SLOW_MO, DEBUG, TRACE); drop them to keep the example in sync with actual usage.

🧰 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
In e2e/.env.example around lines 8 to 16, remove the unused environment
variables TEST_TIMEOUT, RETRY_COUNT, SLOW_MO, DEBUG and TRACE so the example
matches actual e2e usage; delete those variable lines and any related comments
or empty rows, leaving only environment keys that are referenced under e2e/ and
keep the file minimal and in sync.

TRACE=false
14 changes: 14 additions & 0 deletions e2e/.gitignore
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
272 changes: 272 additions & 0 deletions e2e/README.md
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify scripts listed here exist in e2e/package.json and match names.

Catches drift between docs and scripts (test, test:e2e, test:headed, test:debug, test:ui, test:report).


🏁 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 test:e2e script to e2e/package.json or update the README accordingly
The README references pnpm test:e2e, but there is no "test:e2e" entry in e2e/package.json.

🤖 Prompt for AI Agents
In e2e/README.md around lines 27-49, the README mentions a pnpm test:e2e command
that does not exist in e2e/package.json; either add a "test:e2e" script to
e2e/package.json that invokes the project’s Playwright test command (matching
how other scripts like test:test:headed/debug/ui are implemented) or update the
README to reference the actual existing script name (e.g., pnpm test or pnpm
test:e2e:whatever) and include any required env usage; ensure the package.json
script name and README example are consistent and that the script runs the
intended Playwright test runner.


## 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 |
62 changes: 62 additions & 0 deletions e2e/fixtures/auth.fixture.ts
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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
In e2e/fixtures/auth.fixture.ts around lines 30-37 (and similarly apply to
38-45), the login sequence should wait for elements to be visible before
interacting and assert we left the login route after submit to reduce flakiness;
modify the flow to waitFor (or expect.toBeVisible) on usernameInput,
passwordInput and submitButton before fill/click, perform the click, then wait
for navigation or assert the page.url() is not the login path (or
waitForSelector unique to the post-login page) to confirm redirect completed.

// 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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await use();
await use(undefined);
🤖 Prompt for AI Agents
In e2e/fixtures/auth.fixture.ts around line 49, the fixture teardown invokes
use() with no arguments but TypeScript expects an argument for fixtures typed as
void; change the call to use(undefined) so the fixture is called with an
explicit undefined value to satisfy the TypeScript signature and prevent compile
errors.

},
});

export { expect };
Loading
Loading