Skip to content

test: create end-to-end playwright suite#1648

Closed
pujitm wants to merge 14 commits into
mainfrom
test/notif-styles
Closed

test: create end-to-end playwright suite#1648
pujitm wants to merge 14 commits into
mainfrom
test/notif-styles

Conversation

@pujitm

@pujitm pujitm commented Sep 3, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features
    • Added a full Playwright E2E suite with multi-browser projects, auth fixtures, page objects, runtime logging, reporting, toast/accessibility helpers, and Justfile workflows.
  • Tests
    • New auth, dashboard, docker, theme/toast (contrast/accessibility), responsiveness, and session-preservation test suites.
  • Documentation
    • E2E README with setup, run/debug, CI snippet, and environment guidance.
  • Chores
    • Workspace entry, package scripts, .env example, gitignore, Playwright/TypeScript configs, and test utilities added.

@coderabbitai

coderabbitai Bot commented Sep 3, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds a Playwright-based end-to-end test workspace under e2e/, including project config, fixtures (auth/logger), page objects, helpers, toast/reporting utilities, multiple test specs, workspace scripts, sample env/gitignore, documentation, and orchestration (justfile).

Changes

Cohort / File(s) Summary
Workspace & tooling
e2e/package.json, e2e/playwright.config.ts, e2e/tsconfig.json, pnpm-workspace.yaml, package.json
Adds an @unraid/e2e package manifest, Playwright config (env-driven, CI tweaks, reporters, projects), TypeScript config with path aliases, registers ./e2e in pnpm workspace, and adds root scripts to run/install the e2e package.
Environment & docs
e2e/.env.example, e2e/.gitignore, e2e/README.md
Adds sample env file, e2e-specific .gitignore (unignore .env.example), and README describing setup, test commands, structure, CI snippets, and debugging artifacts.
Fixtures & logging
e2e/fixtures/auth.fixture.ts, e2e/fixtures/logger.fixture.ts, e2e/utils/logger.ts
Introduces auth and logger Playwright fixtures: AuthFixtures (loginPage, authenticatedPage) with env-driven login and storageState persistence; LoggerFixtures providing TestLogger per test, console interception, and log attachments; TestLogger implementation using winston.
Page objects
e2e/utils/pages/login.page.ts, e2e/utils/pages/dashboard.page.ts, e2e/utils/pages/settings.page.ts
Adds LoginPage, DashboardPage, SettingsPage classes with locators and async helpers (navigation, login checks, counts, theme switching).
Utilities & reporting
e2e/utils/helpers.ts, e2e/utils/reporting.ts, e2e/utils/toast-helpers.ts
Adds generic Playwright helpers (page load, screenshots, storage, waits), reporting utilities (report, reportAndAnnotate), and toast helpers (trigger, style extraction, contrast/luminance math).
Test suites
e2e/tests/*.spec.ts
e2e/tests/auth.spec.ts, e2e/tests/dashboard.spec.ts, e2e/tests/docker.spec.ts, e2e/tests/theme-toast.spec.ts
Adds Playwright test specs for authentication, dashboard navigation/responsiveness, Docker UI smoke checks, and theme/toast styling & contrast tests.
Orchestration
e2e/justfile
Adds Justfile recipes to run tests across multiple .env.* files, show reports, clean artifacts, and install dependencies.
TypeScript & tooling metadata
e2e/tsconfig.json
TypeScript config for e2e tests (ESNext, DOM, strict, path aliases).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Dev as Developer
  participant CLI as pnpm (e2e)
  participant Playwright as Playwright Runner
  participant LoggerFx as Logger Fixture
  participant AuthFx as Auth Fixture
  participant LoginPO as LoginPage
  participant Server as Unraid Server

  Dev->>CLI: pnpm --filter @unraid/e2e test
  CLI->>Playwright: start tests
  Playwright->>LoggerFx: init logger (attachToTest)
  Playwright->>AuthFx: setup auth fixture
  AuthFx->>Server: GET /
  alt login form present
    AuthFx->>LoginPO: fill credentials & submit
    LoginPO->>Server: POST /login
    Server-->>AuthFx: redirect/ok
    AuthFx->>Playwright: save storageState (auth.json)
  else no login needed
    AuthFx-->>Playwright: proceed without login
  end
  Playwright->>Server: execute test actions via POs/helpers
  Playwright->>LoggerFx: detach logger, attach logs to test results
  Playwright-->>Dev: test results and reports
Loading
sequenceDiagram
  autonumber
  participant Test as Playwright Test
  participant SettingsPO as SettingsPage
  participant UI as Unraid WebGUI
  participant ToastH as Toast Helpers
  participant Reporter as reporting.ts

  Test->>SettingsPO: goto /Settings
  SettingsPO->>UI: request page
  UI-->>SettingsPO: settings rendered
  Test->>SettingsPO: setTheme(light/dark)
  SettingsPO->>UI: apply theme change
  UI-->>Test: UI updated
  Test->>ToastH: triggerToast(type, message)
  ToastH->>UI: window.toast(...)
  UI-->>ToastH: toast element appears
  Test->>ToastH: getToastStyles()
  ToastH-->>Test: bg/text rgb + luminance
  Test->>Reporter: report/annotate contrast metrics
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

A rabbit hops through test and log,
Builds themes and toasts from under the fog.
It saves sessions snug, clicks menus bright,
Reports the contrasts, day and night.
Hop on — the e2e burrow's ready to jog. 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch test/notif-styles

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@claude

claude Bot commented Sep 3, 2025

Copy link
Copy Markdown

Claude finished @pujitm's task —— View job


Code Review for E2E Playwright Suite

Reviewed the diff for critical issues in the newly added E2E test suite.

Todo List:

  • Analyze changed files for critical bugs
  • Check for security vulnerabilities
  • Look for breaking changes
  • Identify data loss risks
  • Provide final review summary

Review Results

No critical issues found in changes

The E2E test suite addition contains well-structured Playwright test code with the following observations:

Security:

  • No hardcoded credentials (uses environment variables appropriately)
  • Authentication state properly managed through fixtures
  • No SQL injection or XSS vulnerabilities in test code

Bugs/Logic:

  • No infinite loops, race conditions, or null pointer issues
  • Proper error handling and timeout management
  • Appropriate use of Playwright APIs

Breaking Changes:

  • New addition only - no existing APIs modified
  • Root package.json scripts added safely without conflicts

Data Loss Risks:

  • Tests are read-only operations against external Unraid server
  • No destructive operations or data modifications
  • Proper test isolation through fixtures

The implementation follows Playwright best practices with page object models, proper fixtures, and comprehensive test coverage for authentication, navigation, and UI functionality.


@codecov

codecov Bot commented Sep 3, 2025

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 54.58%. Comparing base (7c3aee8) to head (1a970c5).
⚠️ Report is 20 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1648      +/-   ##
==========================================
+ Coverage   53.85%   54.58%   +0.72%     
==========================================
  Files         797      809      +12     
  Lines       44310    45445    +1135     
  Branches     4493     4675     +182     
==========================================
+ Hits        23864    24804     +940     
- Misses      20410    20604     +194     
- Partials       36       37       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pujitm pujitm requested a review from elibosley September 3, 2025 19:11

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 27

♻️ Duplicate comments (1)
e2e/tests/auth.spec.ts (1)

1-3: Same import-extension concern as dashboard.spec.ts

Ensure tsconfig NodeNext/Bundler resolution is configured so .js extensions in TS imports compile cleanly.

Reuse the script from the dashboard.spec.ts comment.

🧹 Nitpick comments (35)
e2e/.gitignore (1)

1-14: Broaden ignores for Playwright cache and env variants.

Consider ignoring the actual Playwright cache path and common env variants to avoid churn.

 node_modules/
 /test-results/
 /playwright-report/
 /blob-report/
-/playwright/.cache/
+/node_modules/.cache/ms-playwright/
 .env
 .env.local
+.env.*
 *.log
 screenshots/
 videos/
 traces/
 
 # specific to each individual developer, not the project
 auth.json
+.DS_Store
+*.trace.zip
+junit*.xml
e2e/.env.example (1)

1-18: Address dotenv lint nits and clarify credential handling.

Minor ordering/formatting nits and a safety note.

+# Do NOT commit real credentials. Copy to .env and keep it local.
 # Unraid Server Configuration
 UNRAID_URL=http://tower.local
 
 # Authentication (if needed)
-UNRAID_USERNAME=root
-UNRAID_PASSWORD=
+UNRAID_PASSWORD=
+UNRAID_USERNAME=root
 
 # Test Configuration
-TEST_TIMEOUT=60000
-RETRY_COUNT=2
+RETRY_COUNT=2
+TEST_TIMEOUT=60000
 
 # Browser Configuration
 HEADLESS=true
 SLOW_MO=0
 
 # Debugging
 DEBUG=false
-TRACE=false
+TRACE=false
+
e2e/tsconfig.json (1)

1-24: TS config is solid for Playwright; optional: use Bundler resolution to align with ESM.

Current config will work. If you hit ESM/extension issues, switch moduleResolution to "Bundler" and enable verbatimModuleSyntax.

   "compilerOptions": {
     "target": "ESNext",
     "module": "ESNext",
-    "moduleResolution": "node",
+    "moduleResolution": "Bundler",
     "strict": true,
+    "verbatimModuleSyntax": true,
     "esModuleInterop": true,
     "skipLibCheck": true,
     "forceConsistentCasingInFileNames": true,
     "resolveJsonModule": true,
     "allowSyntheticDefaultImports": true,
     "lib": ["ESNext", "DOM"],
     "types": ["@playwright/test", "node"],
e2e/README.md (6)

7-16: Add language to fenced code blocks (markdownlint MD040).

Specify bash for shell snippets.

-   ```bash
+   ```bash
    cp .env.example .env
    ```

18-23: Mark env snippet as properties and mask password placeholder.

Improves readability and avoids bare credential examples.

-   ```
-   UNRAID_URL=http://tower.local
-   UNRAID_USERNAME=root
-   UNRAID_PASSWORD=your_password
-   ```
+   ```properties
+   UNRAID_URL=http://tower.local
+   UNRAID_USERNAME=root
+   UNRAID_PASSWORD=********
+   ```

27-45: Ensure workspace commands are unambiguous.

Use filter when running from repo root; retain local commands when cd e2e/.

-# Run all tests
-pnpm test
+# Run all tests (from e2e/): pnpm test
+# Or from repo root:
+pnpm --filter @unraid/e2e test
-UNRAID_URL=http://192.168.1.100 pnpm test:e2e
+UNRAID_URL=http://192.168.1.100 pnpm --filter @unraid/e2e test

49-57: Add language to tree snippet (markdownlint MD040).

-```
+```text
 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

---

`95-102`: **CI snippet: ensure correct script name and workspace filter.**

Aligns with workspace usage.


```diff
   run: pnpm test:e2e
+  # or
+  # run: pnpm --filter @unraid/e2e test

111-120: Expand env table and wrap default URL in code formatting (MD034).

Document all supported vars for parity with .env.example.

-| UNRAID_URL | Unraid server URL | http://tower.local |
+| UNRAID_URL | Unraid server URL | `http://tower.local` |
 | UNRAID_USERNAME | Username for auth | root |
 | UNRAID_PASSWORD | Password for auth | - |
 | HEADLESS | Run in headless mode | true |
 | SLOW_MO | Slow down actions (ms) | 0 |
 | DEBUG | Enable debug mode | false |
+| TRACE | Record traces on retry | false |
+| TEST_TIMEOUT | Per-test timeout (ms) | 60000 |
+| RETRY_COUNT | Retries per test | 2 |
package.json (1)

12-16: Prevent e2e from running with the default monorepo test command

Because the root "test" runs "pnpm -r test", adding a "test" script in @unraid/e2e means CI/devs invoking just "pnpm test" will now execute heavy Playwright runs. Recommend gating e2e behind an explicit opt-in or separate filter to avoid surprising slow pipelines.

Proposed approaches (pick one):

  • Keep @unraid/e2e "test" as-is but change root "test" to exclude e2e by default:
- "test": "pnpm -r test",
+ "test": "pnpm -r --filter '...[!@unraid/e2e]' test",
  • Or keep root "test" unchanged, and in e2e/package.json change "test" to a no-op with guidance, and rely on the new root wrappers to actually run e2e:
- "test": "playwright test",
+ "test": "echo \"Skipping by default. Use: pnpm test:e2e\"",

Also consider adding a CI-friendly installer:

+ "playwright:install:ci": "pnpm --filter @unraid/e2e exec playwright install --with-deps"
e2e/playwright.config.ts (2)

31-37: Consider aligning global timeout/expect timeouts with action/navigation

Global timeout is 60s while action/navigation is 30s and expect is 20s; this may mask slowness by failing at different layers inconsistently. Optional: normalize to a single budget (e.g., 30s) unless you have a specific reason to diverge.


55-58: Mobile project naming consistency

Name projects consistently (e.g., "mobile-chromium") to simplify shard filters like "--project mobile-chromium".

-      name: 'Mobile Chrome',
+      name: 'mobile-chromium',
e2e/utils/reporting.ts (1)

13-16: Annotations: consider attaching structured data separately

Pushing the entire message (including data) is fine structurally, but Playwright only renders "type" and "description". If you need rich data, attach it for later inspection.

Example:

if (message.data) {
  await testInfo.attach('report-data.json', {
    contentType: 'application/json',
    body: Buffer.from(JSON.stringify(message.data, null, 2)),
  });
}
testInfo.annotations.push({ type: message.type, description: message.description });
e2e/package.json (1)

1-21: Add CI-friendly install and an explicit "clean"

Playwright on Linux often needs system deps; provide a CI install script and a clean target for local runs.

Suggested additions:

   "scripts": {
-    "test": "playwright test",
+    "test": "playwright test",
     "test:headed": "playwright test --headed",
     "test:debug": "playwright test --debug",
     "test:ui": "playwright test --ui",
     "test:report": "playwright show-report",
-    "playwright:install": "playwright install",
+    "playwright:install": "playwright install",
+    "playwright:install:ci": "playwright install --with-deps",
+    "clean": "rimraf playwright-report test-results || true",
     "codegen": "playwright codegen"
   },

If you prefer to avoid adding rimraf as a dep, replace "clean" with a portable Node one-liner.

e2e/utils/pages/settings.page.ts (2)

20-29: Harden navigation fallback with visibility checks

If the Display Settings link isn’t visible, the click will flake. Guard with visibility and a more deterministic URL check.

-    const currentUrl = this.page.url();
-    if (!currentUrl.includes('DisplaySettings')) {
+    if (!/\/Settings\/DisplaySettings\b/.test(this.page.url())) {
       await this.goto();
-      await this.displaySettingsLink.click();
+      await this.displaySettingsLink.waitFor({ state: 'visible', timeout: 10_000 });
+      await this.displaySettingsLink.click();
       await this.page.waitForLoadState('networkidle');
     }

11-13: Stabilize selectors with data attributes if available

Text-based selectors are brittle. If the WebGUI can expose data-testid/data-qa, switch to those for the theme select, Apply button, and Display Settings link.

e2e/utils/helpers.ts (2)

51-53: Use slice instead of deprecated substr

Minor cleanup; slice is preferred and avoids deprecation.

-export function generateTestId(): string {
-  return `test-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
-}
+export function generateTestId(): string {
+  return `test-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
+}

3-5: Optional: avoid networkidle as the default wait

networkidle can be brittle with long-lived connections. Consider domcontentloaded or a specific UI-ready locator in call sites.

e2e/utils/pages/login.page.ts (1)

10-16: Prefer role-based selectors for resilience

Where possible, use getByRole/getByLabel over CSS. Improves accessibility alignment and reduces selector churn.

e2e/utils/toast-helpers.ts (2)

25-33: Use role-based fallback for toasts

Add ARIA roles commonly used for toasts to reduce selector brittleness.

 export async function waitForToast(page: Page) {
   // Look for toast elements in the Unraid toaster structure
-  const toastLocator = page.locator('ol.toaster li').first();
+  const toastLocator = page.locator('ol.toaster li, [role="status"], [role="alert"]').first();

53-58: Unify luminance computation to a single, correct routine

You compute “luminance” two different ways. Reuse the WCAG relative luminance function for consistency.

-export function isLightBackground(rgb: number[]): boolean {
-  if (rgb.length < 3) return true;
-  // Calculate luminance
-  const luminance = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255;
-  return luminance > 0.5;
-}
+export function isLightBackground(rgb: number[]): boolean {
+  if (rgb.length < 3) return true;
+  return getLuminance(rgb) > 0.5;
+}
@@
-export function getLuminance(rgb: number[]): number {
-  if (rgb.length < 3) return 0.5;
-  // Calculate luminance (0 = dark, 1 = light)
-  return (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255;
-}
+export function getLuminance(rgb: number[]): number {
+  if (rgb.length < 3) return 0.5;
+  const toLinear = (v: number) => {
+    const s = v / 255;
+    return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
+  };
+  const [r, g, b] = rgb.map(toLinear);
+  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
+}

Also applies to: 94-97

e2e/utils/pages/dashboard.page.ts (2)

22-28: Stabilize post-navigation state

After goto, wait for a visible, page-specific marker to reduce flakiness.

   async goto() {
     await this.page.goto('/Dashboard');
+    await this.mainContent.waitFor({ state: 'visible' });
   }

44-46: Prefer role-based navigation for resilience

getByRole('link', { name: menuItem }) tends to be more stable than text CSS. Consider switching if the DOM exposes accessible names.

-    await this.navigationMenu.locator(`a:has-text("${menuItem}")`).first().click();
+    await this.navigationMenu.getByRole('link', { name: menuItem }).first().click();
e2e/tests/docker.spec.ts (1)

26-27: Prefer test.info().log over console.log in tests

Playwright surfaces test.info logs in reports and traces; replace console.log for better CI visibility.

Also applies to: 32-33, 46-47, 56-57, 62-63, 76-77, 92-93, 115-116

e2e/tests/dashboard.spec.ts (1)

95-101: Drop fixed waits after viewport changes

The explicit 500ms sleep can be flaky and slow. Rely on visibility assertion with default timeout.

-      await page.waitForTimeout(500);
-      
       await expect(dashboard.mainContent).toBeVisible();
e2e/tests/auth.spec.ts (2)

24-27: Prefer declarative skip over imperative return

Minor: you can gate the test with a single call and drop the manual return.

-    if (!password) {
-      test.skip();
-      return;
-    }
+    test.skip(!password, 'Set UNRAID_PASSWORD to run authenticated login tests');

67-77: Speed up authenticated tests by reusing storage state

You already persist auth.json in the fixture. Consider using it at project/test level to avoid logging in every test.

Example (playwright.config):

use: {
  baseURL: process.env.BASE_URL || 'http://localhost:3000',
  storageState: process.env.UNRAID_PASSWORD ? 'auth.json' : undefined,
}
e2e/tests/theme-toast.spec.ts (2)

46-50: Avoid double-waiting for the same toast

getToastStyles already waits for a toast. The explicit waitForToast + visible check is redundant.

-          const toastElement = await waitForToast(page);
-          await expect(toastElement).toBeVisible();
-
-          const styles = await getToastStyles(page);
+          const styles = await getToastStyles(page);

If you prefer to reuse the located element, add a helper overload (in toast-helpers.ts):

export async function getToastStylesFrom(toast: Locator) { /* same body, using provided element */ }

Also applies to: 121-125


100-101: Replace fixed sleeps with state-based waits

Use locator counts/visibility instead of arbitrary timeouts between toasts.

-          await page.waitForTimeout(1000);
+          await expect(page.locator('ol.toaster li')).toHaveCount(0);
-      await page.waitForTimeout(2000);
+      await expect(page.locator('ol.toaster li')).toHaveCount(0);

Also applies to: 173-174, 199-201

e2e/fixtures/auth.fixture.ts (6)

16-17: Prefer nullish coalescing for env defaults

This avoids treating empty strings as “unset.”

-const username = process.env.UNRAID_USERNAME || 'root';
-const password = process.env.UNRAID_PASSWORD || '';
+const username = process.env.UNRAID_USERNAME ?? 'root';
+const password = process.env.UNRAID_PASSWORD ?? '';

23-23: Avoid networkidle; it’s flaky with long‑polling/WS

Use a lighter wait and then wait on concrete UI conditions.

-      await page.waitForLoadState('networkidle');
+      await page.waitForLoadState('domcontentloaded');
@@
-        await page.waitForLoadState('networkidle');
+        await page.waitForLoadState('domcontentloaded');

Also applies to: 39-39


29-33: Reuse the LoginPage POM to avoid selector drift

Use the page object you already have for filling and login state checks.

-        // 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();
+        const lp = new LoginPage(page);
+        await lp.login(username, password);
@@
-        // Check if we successfully logged in
-        const stillOnLogin = page.url().includes('login');
-        if (!stillOnLogin) {
+        const loggedIn = await lp.isLoggedIn();
+        if (loggedIn) {
           await page.context().storageState({ path: 'auth.json' });
         }

Also applies to: 41-45


41-45: Fail fast with a clear error when login fails

Surface the UI error so the test suite exits early with context.

         // Check if we successfully logged in
-        const stillOnLogin = page.url().includes('login');
+        const loginError = page.locator('.error, .alert-error, [role="alert"]');
+        if (await loginError.isVisible()) {
+          throw new Error(`Login failed: ${await loginError.textContent()}`);
+        }
+        const stillOnLogin = page.url().includes('login');

44-44: Persist auth state in a repo-local path and wire it in config

Store under e2e/.auth and reference via test.use({ storageState }) so other tests can reuse it.

-          await page.context().storageState({ path: 'auth.json' });
+          await page.context().storageState({ path: 'e2e/.auth/auth.json' });

Outside this file (playwright.config.ts):

import { defineConfig } from '@playwright/test';

export default defineConfig({
  // ...
  use: {
    baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
  },
  projects: [
    {
      name: 'chromium',
      use: { storageState: 'e2e/.auth/auth.json' },
    },
    // other browsers...
  ],
});

6-6: Fixture naming nit

authenticatedPage does not provide a Page; consider login or authenticated to avoid confusion in test code completion.

-  authenticatedPage: void;
+  authenticated: void;
@@
-  authenticatedPage: async ({ page }, use) => {
+  authenticated: async ({ page }, use) => {

Also applies to: 15-15

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7c3aee8 and d6dd4bb.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (19)
  • e2e/.env.example (1 hunks)
  • e2e/.gitignore (1 hunks)
  • e2e/README.md (1 hunks)
  • e2e/fixtures/auth.fixture.ts (1 hunks)
  • e2e/package.json (1 hunks)
  • e2e/playwright.config.ts (1 hunks)
  • e2e/tests/auth.spec.ts (1 hunks)
  • e2e/tests/dashboard.spec.ts (1 hunks)
  • e2e/tests/docker.spec.ts (1 hunks)
  • e2e/tests/theme-toast.spec.ts (1 hunks)
  • e2e/tsconfig.json (1 hunks)
  • e2e/utils/helpers.ts (1 hunks)
  • e2e/utils/pages/dashboard.page.ts (1 hunks)
  • e2e/utils/pages/login.page.ts (1 hunks)
  • e2e/utils/pages/settings.page.ts (1 hunks)
  • e2e/utils/reporting.ts (1 hunks)
  • e2e/utils/toast-helpers.ts (1 hunks)
  • package.json (1 hunks)
  • pnpm-workspace.yaml (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use the any type; prefer proper typing
Avoid using type assertions/casting; prefer proper typing from the start

Files:

  • e2e/fixtures/auth.fixture.ts
  • e2e/utils/reporting.ts
  • e2e/tests/dashboard.spec.ts
  • e2e/tests/theme-toast.spec.ts
  • e2e/tests/docker.spec.ts
  • e2e/utils/pages/login.page.ts
  • e2e/utils/pages/settings.page.ts
  • e2e/tests/auth.spec.ts
  • e2e/playwright.config.ts
  • e2e/utils/pages/dashboard.page.ts
  • e2e/utils/helpers.ts
  • e2e/utils/toast-helpers.ts
🧠 Learnings (19)
📓 Common learnings
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-02T14:41:56.235Z
Learning: Applies to web/__test__/**/*.@(ts|tsx|js|jsx) : Web tests are located under web/__test__
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-02T14:41:56.235Z
Learning: Applies to web/**/*.{spec,test}.@(ts|tsx|js|jsx) : Test component behavior and rendered output, not internal implementation details
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Run terminal commands with pnpm within the web directory (e.g., pnpm test)

Applied to files:

  • package.json
📚 Learning: 2025-09-02T14:41:56.235Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-02T14:41:56.235Z
Learning: Run API tests with: pnpm --filter ./api test

Applied to files:

  • package.json
📚 Learning: 2025-08-11T15:07:39.222Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/api-rules.mdc:0-0
Timestamp: 2025-08-11T15:07:39.222Z
Learning: Run tests with pnpm --filter ./api test

Applied to files:

  • package.json
📚 Learning: 2025-08-11T15:07:39.222Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/api-rules.mdc:0-0
Timestamp: 2025-08-11T15:07:39.222Z
Learning: Use pnpm only (do not use npm/yarn)

Applied to files:

  • package.json
📚 Learning: 2025-02-05T14:43:48.568Z
Learnt from: elibosley
PR: unraid/api#1120
File: plugin/package.json:0-0
Timestamp: 2025-02-05T14:43:48.568Z
Learning: In Node.js projects, npm scripts should be organized with clear namespacing (e.g., build:*, docker:*, env:*) and include proper environment validation and error handling. Each script should follow the single responsibility principle.

Applied to files:

  • package.json
📚 Learning: 2025-09-02T14:41:56.235Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-02T14:41:56.235Z
Learning: Use pnpm only for package management

Applied to files:

  • package.json
📚 Learning: 2025-09-02T14:41:56.235Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-02T14:41:56.235Z
Learning: Always run scripts from api/package.json unless requested otherwise

Applied to files:

  • package.json
📚 Learning: 2025-02-06T17:25:45.397Z
Learnt from: mdatelle
PR: unraid/api#1122
File: web/package.json:16-16
Timestamp: 2025-02-06T17:25:45.397Z
Learning: The build script in web/package.json should include type-check command (`npm run type-check`) even when NODE_ENV is production, as it provides value for local builds by catching type errors before pushing to CI/CD.

Applied to files:

  • package.json
📚 Learning: 2025-09-02T14:41:56.235Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-02T14:41:56.235Z
Learning: Applies to web/__test__/**/*.@(ts|tsx|js|jsx) : Web tests are located under web/__test__

Applied to files:

  • e2e/tsconfig.json
  • pnpm-workspace.yaml
📚 Learning: 2025-05-23T21:59:29.632Z
Learnt from: elibosley
PR: unraid/api#1408
File: web/components/ApiKey/PermissionCounter.vue:6-6
Timestamp: 2025-05-23T21:59:29.632Z
Learning: This codebase uses ESM (ECMAScript Modules) and requires .js extensions in import statements, even when importing from TypeScript files, as the imports refer to the compiled JavaScript output.

Applied to files:

  • e2e/tsconfig.json
📚 Learning: 2025-09-02T14:41:56.235Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-02T14:41:56.235Z
Learning: Applies to api/src/**/*.ts : In API TypeScript, use .js extensions in import specifiers for ESM compatibility

Applied to files:

  • e2e/tsconfig.json
📚 Learning: 2025-09-02T14:41:56.235Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-02T14:41:56.235Z
Learning: Applies to {api,web}/**/*.{spec,test}.@(ts|tsx|js|jsx) : Avoid brittle tests that depend on non-essential details (e.g., log formats)

Applied to files:

  • e2e/tsconfig.json
  • e2e/tests/theme-toast.spec.ts
  • e2e/tests/auth.spec.ts
📚 Learning: 2025-09-02T14:41:56.235Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-02T14:41:56.235Z
Learning: Applies to api/**/vitest.config.@(js|ts|mjs|cjs) : API test runner is Vitest

Applied to files:

  • e2e/tsconfig.json
  • e2e/playwright.config.ts
📚 Learning: 2025-09-02T14:41:56.235Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-02T14:41:56.235Z
Learning: Applies to web/**/{store,stores}/**/*.{spec,test}.@(ts|js) : Use factory functions for module mocks to avoid hoisting issues

Applied to files:

  • e2e/fixtures/auth.fixture.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()

Applied to files:

  • e2e/fixtures/auth.fixture.ts
  • e2e/utils/helpers.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use await nextTick() for DOM update assertions and flushPromises() for complex async chains; always await async operations before asserting

Applied to files:

  • e2e/fixtures/auth.fixture.ts
  • e2e/utils/helpers.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)

Applied to files:

  • e2e/tests/theme-toast.spec.ts
📚 Learning: 2025-09-02T14:41:56.235Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-02T14:41:56.235Z
Learning: Applies to web/**/*.{spec,test}.@(ts|tsx|js|jsx) : Use await nextTick() for DOM updates before assertions

Applied to files:

  • e2e/utils/helpers.ts
🧬 Code graph analysis (6)
e2e/fixtures/auth.fixture.ts (1)
e2e/utils/pages/login.page.ts (1)
  • LoginPage (3-36)
e2e/tests/dashboard.spec.ts (3)
e2e/fixtures/auth.fixture.ts (1)
  • test (9-51)
e2e/utils/pages/dashboard.page.ts (1)
  • DashboardPage (3-47)
e2e/utils/helpers.ts (1)
  • waitForPageLoad (3-5)
e2e/tests/theme-toast.spec.ts (4)
e2e/fixtures/auth.fixture.ts (1)
  • test (9-51)
e2e/utils/pages/settings.page.ts (1)
  • SettingsPage (3-44)
e2e/utils/toast-helpers.ts (8)
  • triggerToast (3-23)
  • waitForToast (25-33)
  • getToastStyles (35-51)
  • getLuminance (94-98)
  • getContrastRatio (64-87)
  • isLightBackground (53-58)
  • hasGoodContrast (89-92)
  • isDarkBackground (60-62)
e2e/utils/reporting.ts (2)
  • report (9-11)
  • reportAndAnnotate (13-16)
e2e/tests/docker.spec.ts (2)
e2e/fixtures/auth.fixture.ts (2)
  • test (9-51)
  • expect (53-53)
e2e/utils/helpers.ts (2)
  • waitForPageLoad (3-5)
  • isElementVisible (34-36)
e2e/utils/pages/settings.page.ts (1)
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (1)
  • theme (58-60)
e2e/tests/auth.spec.ts (2)
e2e/fixtures/auth.fixture.ts (1)
  • test (9-51)
e2e/utils/pages/login.page.ts (2)
  • LoginPage (3-36)
  • isLoggedIn (28-31)
🪛 LanguageTool
e2e/README.md

[grammar] ~107-~107: There might be a mistake here.
Context: ...- Videos:test-results/(on failure) - Traces:test-results/` (on retry) - HTM...

(QB_NEW_EN)


[grammar] ~108-~108: There might be a mistake here.
Context: ...re) - Traces: test-results/ (on retry) - HTML Report: `pnpm --filter @unraid/e2e ...

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
e2e/README.md

19-19: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


49-49: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


115-115: Bare URL used

(MD034, no-bare-urls)

🪛 dotenv-linter (3.3.0)
e2e/.env.example

[warning] 6-6: [UnorderedKey] The UNRAID_PASSWORD key should go before the UNRAID_USERNAME key

(UnorderedKey)


[warning] 10-10: [UnorderedKey] The RETRY_COUNT key should go before the TEST_TIMEOUT key

(UnorderedKey)


[warning] 18-18: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)

🔇 Additional comments (7)
pnpm-workspace.yaml (1)

8-8: e2e workspace configuration verified e2e/package.json has "private": true, and root-level scripts (test:e2e & playwright:install) correctly reference @unraid/e2e.

e2e/utils/pages/settings.page.ts (1)

31-31: Verify theme option values match the app

Confirm the select options are literally "white" and "black". If the app uses "light"/"dark" or localized values, this will fail. Consider deriving valid values at runtime.

Example:

const options = await this.themeSelector.locator('option').allTextContents();
e2e/utils/pages/login.page.ts (1)

28-31: Login-state check is fragile

Inferring “logged in” from the absence of username/password inputs may be false-positive on other forms. Prefer a positive signal (e.g., presence of user menu, dashboard header) or a URL predicate.

Do you have a stable selector (e.g., a user menu or dashboard breadcrumb) we can key off? If yes, I can update isLoggedIn() accordingly.

e2e/utils/pages/dashboard.page.ts (1)

3-20: Page Object structure looks good

Clear, composable locators and narrow-scoped helpers.

e2e/tests/theme-toast.spec.ts (1)

30-76: Nice accessibility telemetry; keep assertions minimal and actionable

The reporting plus soft expectations are a good balance for visual styling checks.

Also applies to: 106-151

e2e/fixtures/auth.fixture.ts (2)

10-13: loginPage fixture looks good

Creates and exposes the page object cleanly.


2-2: Verify ESM import specifier for TS

Importing a TS module with a .js suffix requires tsconfig moduleResolution NodeNext/bundler. If not configured, this will fail. Consider extensionless import.

-import { LoginPage } from '../utils/pages/login.page.js';
+import { LoginPage } from '../utils/pages/login.page';

Comment on lines +19 to +21
if (password) {
await page.goto('/');

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

Don’t skip login when password is empty

Gating the whole flow on truthy password breaks default “root with blank password” setups and will silently skip auth.

-    if (password) {
-      await page.goto('/');
+    await page.goto('/');
@@
-      }
-    }
+      }

Also applies to: 46-47

🤖 Prompt for AI Agents
In e2e/fixtures/auth.fixture.ts around lines 19-21 (and also update the similar
check at lines 46-47): the current conditional uses a truthy check (if
(password)) which skips the login when password is an empty string; change the
condition to explicitly check for undefined (e.g., if (password !== undefined)
or if (typeof password !== 'undefined') ) so the login flow runs when password
is an empty string (blank root password) but is skipped only when password is
truly undefined.

Comment on lines +30 to +37
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();

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.

}
}

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.

Comment thread e2e/playwright.config.ts
Comment on lines +12 to +16
reporter: [
['html'],
['list'],
process.env.CI ? ['github'] : null,
].filter(Boolean) as any,

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

Remove "as any" on reporters; use a type-safe conditional instead

The cast hides type issues and violates the TS guideline to avoid assertions. You can avoid filter(Boolean) and keep types intact by building the reporters array conditionally.

Apply:

-  reporter: [
-    ['html'],
-    ['list'],
-    process.env.CI ? ['github'] : null,
-  ].filter(Boolean) as any,
+  reporter: process.env.CI
+    ? [['html'], ['list'], ['github']]
+    : [['html'], ['list']],
📝 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
reporter: [
['html'],
['list'],
process.env.CI ? ['github'] : null,
].filter(Boolean) as any,
reporter: process.env.CI
? [['html'], ['list'], ['github']]
: [['html'], ['list']],
🤖 Prompt for AI Agents
In e2e/playwright.config.ts around lines 12 to 16, remove the "as any" cast and
the filter(Boolean) call by constructing the reporters array with proper types:
declare the reporters variable as the correct Playwright ReporterDescription[]
type, include the mandatory ['html'] and ['list'] entries, and conditionally
push ['github'] only when process.env.CI is truthy; then assign that typed
reporters array to the config. This preserves type safety and eliminates the
need for assertions or filtering.

Comment thread e2e/playwright.config.ts
Comment on lines +17 to +19
use: {
baseURL: process.env.UNRAID_URL || 'http://tower.local',
trace: 'on-first-retry',

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

Unify baseURL and remove unsupported webServer option

  • Define baseURL once and reuse it in both "use" and "webServer" to avoid divergence.
  • webServer.ignoreHTTPSErrors is not a supported field on the webServer config; HTTPS error handling belongs under "use.ignoreHTTPSErrors" (already present).

Apply:

+const baseURL = process.env.UNRAID_URL || 'http://tower.local';
 
 export default defineConfig({
   ...
   use: {
-    baseURL: process.env.UNRAID_URL || 'http://tower.local',
+    baseURL,
     ignoreHTTPSErrors: true,
     ...
   },
   ...
-  webServer: process.env.NO_WEB_SERVER ? undefined : {
-    command: 'echo "Using external Unraid server"',
-    url: process.env.UNRAID_URL || 'http://tower.local',
-    reuseExistingServer: true,
-    ignoreHTTPSErrors: true,
-  },
+  webServer: process.env.NO_WEB_SERVER ? undefined : {
+    // External server expected; we just validate it's reachable.
+    command: 'node -e "console.log(\\"Using external Unraid server\\")"',
+    url: baseURL,
+    reuseExistingServer: true
+  },

Also applies to: 66-71

🤖 Prompt for AI Agents
In e2e/playwright.config.ts around lines 17-19 (and also apply to 66-71), the
baseURL is set inline in use but duplicated for webServer and webServer contains
an unsupported ignoreHTTPSErrors field; define a single const baseURL =
process.env.UNRAID_URL || 'http://tower.local' near the top and reference that
const in use.baseURL and in webServer.url (or webServer.launchOptions.args/url
as appropriate), and remove webServer.ignoreHTTPSErrors so ignoreHTTPSErrors
remains only under use.

Comment on lines +22 to +26
async login(username: string, password: string) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.submitButton.click();
}

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

Await post-submit readiness to avoid race conditions

After clicking submit, explicitly wait for navigation or a logged-in UI marker to stabilize tests.

   async login(username: string, password: string) {
     await this.usernameInput.fill(username);
     await this.passwordInput.fill(password);
-    await this.submitButton.click();
+    await Promise.all([
+      this.page.waitForNavigation({ waitUntil: 'domcontentloaded' }).catch(() => this.page.waitForURL(/^(?!.*login)/)),
+      this.submitButton.click(),
+    ]);
   }
📝 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
async login(username: string, password: string) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async login(username: string, password: string) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await Promise.all([
this.page.waitForNavigation({ waitUntil: 'domcontentloaded' }).catch(() => this.page.waitForURL(/^(?!.*login)/)),
this.submitButton.click(),
]);
}
🤖 Prompt for AI Agents
In e2e/utils/pages/login.page.ts around lines 22 to 26, the login method clicks
submit but doesn't wait for post-submit readiness which can cause race
conditions; update the method to await a stable post-login signal (for example,
await Promise.all([page.waitForNavigation({ waitUntil: 'networkidle' }),
this.submitButton.click()]) or await a visible logged-in UI marker like
page.waitForSelector('selector-for-profile-or-dashboard')) so tests only proceed
once navigation or the logged-in UI has stabilized.

Comment on lines +31 to +38
async setTheme(theme: 'white' | 'black') {
await this.goToDisplaySettings();
await this.themeSelector.selectOption(theme);
await this.applyButton.click();
await this.page.waitForLoadState('networkidle');
// Wait a bit for theme to apply
await this.page.waitForTimeout(1_000);
}

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

Avoid fixed sleeps; wait for the theme change deterministically

A 1s timeout is flaky across environments. Assert the selected value or wait for a theme-specific DOM signal.

   async setTheme(theme: 'white' | 'black') {
     await this.goToDisplaySettings();
     await this.themeSelector.selectOption(theme);
     await this.applyButton.click();
     await this.page.waitForLoadState('networkidle');
-    // Wait a bit for theme to apply
-    await this.page.waitForTimeout(1_000);
+    // Wait until the select reflects the chosen theme
+    await this.page.waitForFunction(
+      (sel, value) => (document.querySelector(sel) as HTMLSelectElement)?.value === value,
+      ['select[name="theme"]', theme],
+      { timeout: 10_000 }
+    );

If the app toggles a DOM attribute/class (e.g., html[data-theme]), prefer waiting on that instead.

📝 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
async setTheme(theme: 'white' | 'black') {
await this.goToDisplaySettings();
await this.themeSelector.selectOption(theme);
await this.applyButton.click();
await this.page.waitForLoadState('networkidle');
// Wait a bit for theme to apply
await this.page.waitForTimeout(1_000);
}
async setTheme(theme: 'white' | 'black') {
await this.goToDisplaySettings();
await this.themeSelector.selectOption(theme);
await this.applyButton.click();
await this.page.waitForLoadState('networkidle');
// Wait until the select reflects the chosen theme
await this.page.waitForFunction(
(sel, value) => (document.querySelector(sel) as HTMLSelectElement)?.value === value,
['select[name="theme"]', theme],
{ timeout: 10_000 }
);
}
🤖 Prompt for AI Agents
In e2e/utils/pages/settings.page.ts around lines 31 to 38, replace the fixed 1s
wait with a deterministic wait that verifies the theme actually applied: after
selecting the option and clicking apply, wait for a reliable DOM signal (for
example waitForSelector or waitForFunction that checks
html[data-theme="white"|"black"] or a specific body/class change) or assert the
themeSelector's selected value, then proceed; remove the
page.waitForTimeout(1_000) and use the explicit wait for the
attribute/class/value instead.

Comment thread e2e/utils/reporting.ts
Comment on lines +7 to +19
await page.evaluate(([msg, toastType]) => {
if ((window as any).toast) {
if (toastType === 'default') {
// Simple toast call
(window as any).toast(msg, { duration: 5000 });
} else {
// Typed toast call (success, info, warning, error)
(window as any).toast[toastType](msg, { duration: 5000 });
}
} else {
console.error('Toast function not available');
}
}, [toastMessage, type]);

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

Remove any and add proper typing for window.toast

Avoid (window as any). Define the toast API type and rely on optional chaining. Also return a boolean to let callers assert availability instead of logging inside the browser context.

-export async function triggerToast(page: Page, type: 'success' | 'info' | 'warning' | 'error' | 'default' = 'default', message?: string) {
+type ToastFn = (msg: string, opts?: { duration?: number }) => void;
+type ToastApi = ToastFn & { success: ToastFn; info: ToastFn; warning: ToastFn; error: ToastFn };
+
+export async function triggerToast(
+  page: Page,
+  type: 'success' | 'info' | 'warning' | 'error' | 'default' = 'default',
+  message?: string
+) {
   const toastMessage = message || `Test ${type} notification`;
   
-  // Execute JavaScript to trigger a toast using the Unraid toast system
-  await page.evaluate(([msg, toastType]) => {
-    if ((window as any).toast) {
-      if (toastType === 'default') {
-        // Simple toast call
-        (window as any).toast(msg, { duration: 5000 });
-      } else {
-        // Typed toast call (success, info, warning, error)
-        (window as any).toast[toastType](msg, { duration: 5000 });
-      }
-    } else {
-      console.error('Toast function not available');
-    }
-  }, [toastMessage, type]);
+  // Trigger toast via the Unraid toast system
+  const ok = await page.evaluate(
+    ({ msg, toastType }) => {
+      const t = (window as unknown as { toast?: ToastApi }).toast;
+      if (!t) return false;
+      if (toastType === 'default') t(msg, { duration: 5000 });
+      else t[toastType](msg, { duration: 5000 });
+      return true;
+    },
+    { msg: toastMessage, toastType: type }
+  );
+  if (!ok) {
+    throw new Error('window.toast not available');
+  }
   
   // Give the toast a moment to appear
   await page.waitForTimeout(500);
 }

If you prefer zero assertions in helpers, we can drop the throw and let tests decide.

📝 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 page.evaluate(([msg, toastType]) => {
if ((window as any).toast) {
if (toastType === 'default') {
// Simple toast call
(window as any).toast(msg, { duration: 5000 });
} else {
// Typed toast call (success, info, warning, error)
(window as any).toast[toastType](msg, { duration: 5000 });
}
} else {
console.error('Toast function not available');
}
}, [toastMessage, type]);
type ToastFn = (msg: string, opts?: { duration?: number }) => void;
type ToastApi = ToastFn & { success: ToastFn; info: ToastFn; warning: ToastFn; error: ToastFn };
export async function triggerToast(
page: Page,
type: 'success' | 'info' | 'warning' | 'error' | 'default' = 'default',
message?: string
) {
const toastMessage = message || `Test ${type} notification`;
// Trigger toast via the Unraid toast system
const ok = await page.evaluate(
({ msg, toastType }) => {
const t = (window as unknown as { toast?: ToastApi }).toast;
if (!t) return false;
if (toastType === 'default') t(msg, { duration: 5000 });
else t[toastType](msg, { duration: 5000 });
return true;
},
{ msg: toastMessage, toastType: type }
);
if (!ok) {
throw new Error('window.toast not available');
}
// Give the toast a moment to appear
await page.waitForTimeout(500);
}
🤖 Prompt for AI Agents
In e2e/utils/toast-helpers.ts around lines 7 to 19, the code uses (window as
any) and logs to console inside the browser context; replace this by declaring a
proper Toast API type and augmenting the Window type (e.g., interface ToastAPI {
(msg: string, opts?: any): void; success?: Function; info?: Function; warning?:
Function; error?: Function } and declare global { interface Window { toast?:
ToastAPI } }), then inside page.evaluate use optional chaining
(window.toast?.(...) or window.toast?.[toastType]?.(...)) instead of any, and
return a boolean indicating whether a toast was invoked (true) or not available
(false) so callers can assert availability instead of logging in the browser
context.

Comment on lines +39 to +51
const styles = await toast.evaluate((el) => {
const computed = window.getComputedStyle(el);
return {
backgroundColor: computed.backgroundColor,
color: computed.color,
// Convert rgb to a more readable format
backgroundRgb: computed.backgroundColor.match(/\d+/g)?.map(Number) || [],
textRgb: computed.color.match(/\d+/g)?.map(Number) || []
};
});

return styles;
}

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

Strongly type getToastStyles result and normalize rgb parsing

Add a return type and ensure we only take the first three channels, ignoring alpha if present.

-export async function getToastStyles(page: Page) {
+export interface ToastStyles {
+  backgroundColor: string;
+  color: string;
+  backgroundRgb: number[];
+  textRgb: number[];
+}
+
+export async function getToastStyles(page: Page): Promise<ToastStyles> {
   const toast = await waitForToast(page);
   
   // Get computed styles of the toast
-  const styles = await toast.evaluate((el) => {
+  const styles = await toast.evaluate<ToastStyles>((el) => {
     const computed = window.getComputedStyle(el);
+    const toRgb = (css: string) => (css.match(/\d+/g) || []).slice(0, 3).map(Number);
     return {
       backgroundColor: computed.backgroundColor,
       color: computed.color,
-      // Convert rgb to a more readable format
-      backgroundRgb: computed.backgroundColor.match(/\d+/g)?.map(Number) || [],
-      textRgb: computed.color.match(/\d+/g)?.map(Number) || []
+      backgroundRgb: toRgb(computed.backgroundColor),
+      textRgb: toRgb(computed.color),
     };
   });
📝 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
const styles = await toast.evaluate((el) => {
const computed = window.getComputedStyle(el);
return {
backgroundColor: computed.backgroundColor,
color: computed.color,
// Convert rgb to a more readable format
backgroundRgb: computed.backgroundColor.match(/\d+/g)?.map(Number) || [],
textRgb: computed.color.match(/\d+/g)?.map(Number) || []
};
});
return styles;
}
export interface ToastStyles {
backgroundColor: string;
color: string;
backgroundRgb: number[];
textRgb: number[];
}
export async function getToastStyles(page: Page): Promise<ToastStyles> {
const toast = await waitForToast(page);
// Get computed styles of the toast
const styles = await toast.evaluate<ToastStyles>((el) => {
const computed = window.getComputedStyle(el);
const toRgb = (css: string) => (css.match(/\d+/g) || []).slice(0, 3).map(Number);
return {
backgroundColor: computed.backgroundColor,
color: computed.color,
backgroundRgb: toRgb(computed.backgroundColor),
textRgb: toRgb(computed.color),
};
});
return styles;
}
🤖 Prompt for AI Agents
In e2e/utils/toast-helpers.ts around lines 39 to 51, the anonymous evaluate
return is untyped and the rgb extraction can include an alpha channel; add a
concrete return interface (e.g., { backgroundColor: string; color: string;
backgroundRgb: number[]; textRgb: number[] }) as the function's return type and
update the rgb parsing to take only the first three channels by using
match(/\d+/g)?.slice(0,3).map(Number) || [], ensuring alpha is ignored and the
arrays are strictly numeric arrays of up to three elements.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 0

🧹 Nitpick comments (9)
e2e/.env.example (4)

12-14: Add HEADLESS to make CI/headed toggles explicit

Playwright defaults can differ locally vs CI. Add a HEADLESS flag so users don’t rely on implicit behavior.

 # Browser Configuration
 SLOW_MO=0
+HEADLESS=true

8-11: Confirm these vars are actually consumed in playwright.config.ts

If TEST_TIMEOUT and RETRY_COUNT aren’t read and parsed (ms vs s, Number vs string), they’ll be confusing no-ops.

Would you like me to wire them into use: { actionTimeout, navigationTimeout }, and retries in the config?


4-6: Nit: satisfy dotenv-linter ordering or disable it

The linter is flagging key order and it’ll keep failing in CI if enabled. Either reorder or ignore.

 # Authentication (if needed)
-UNRAID_USERNAME=root
-UNRAID_PASSWORD=
+UNRAID_PASSWORD=
+UNRAID_USERNAME=root

Also add a trailing newline at EOF.


16-17: Clarify TRACE/DEBUG behavior

If TRACE controls Playwright tracing (on/retain-on-failure/off), document accepted values; boolean-only may be too coarse. Consider TRACE=on-first-retry to match docs, with config mapping.

e2e/README.md (5)

19-23: Add language to fenced block and backticks for values

Fix MD040 and improve readability by marking this as dotenv and using backticks for literals.

-```
-UNRAID_URL=http://tower.local
-UNRAID_USERNAME=root
-UNRAID_PASSWORD=your_password
-```
+```dotenv
+UNRAID_URL=http://tower.local
+UNRAID_USERNAME=root
+UNRAID_PASSWORD=your_password
+```

53-61: Add language to directory tree block

Satisfy MD040; use text for the tree.

-```
+```text
 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

---

`117-123`: **Align env table with .env.example (add TEST_TIMEOUT, RETRY_COUNT, TRACE, HEADLESS; avoid bare URL)**

Keep docs consistent and fix MD034 by wrapping URL.


```diff
 | Variable | Description | Default |
 |----------|-------------|---------|
-| UNRAID_URL | Unraid server URL | http://tower.local |
+| UNRAID_URL | Unraid server URL | `http://tower.local` |
 | UNRAID_USERNAME | Username for auth | root |
 | UNRAID_PASSWORD | Password for auth | - |
+| TEST_TIMEOUT | Per-test timeout (ms) | 60000 |
+| RETRY_COUNT | Playwright retries | 2 |
 | SLOW_MO | Slow down actions (ms) | 0 |
 | DEBUG | Enable debug mode | false |
+| TRACE | Playwright trace mode (e.g., `off`, `on-first-retry`, `on`) | false |
+| HEADLESS | Run browser headless | true |

41-42: Clarify single-file invocation

Playwright expects a path; passing a bare filename may fail depending on cwd and scripts. Suggest forwarding args with “--”.

-pnpm test auth.spec.ts
+pnpm test -- e2e/tests/auth.spec.ts

101-106: Include UNRAID_USERNAME in CI sample or note default

Avoid surprises if username isn’t root on target environments.

   env:
     UNRAID_URL: ${{ secrets.UNRAID_URL }}
+    UNRAID_USERNAME: ${{ secrets.UNRAID_USERNAME }}
     UNRAID_PASSWORD: ${{ secrets.UNRAID_PASSWORD }}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d6dd4bb and db829d7.

📒 Files selected for processing (3)
  • e2e/.env.example (1 hunks)
  • e2e/.gitignore (1 hunks)
  • e2e/README.md (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • e2e/.gitignore
🧰 Additional context used
🪛 dotenv-linter (3.3.0)
e2e/.env.example

[warning] 6-6: [UnorderedKey] The UNRAID_PASSWORD key should go before the UNRAID_USERNAME key

(UnorderedKey)


[warning] 10-10: [UnorderedKey] The RETRY_COUNT key should go before the TEST_TIMEOUT key

(UnorderedKey)


[warning] 17-17: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)

🪛 LanguageTool
e2e/README.md

[grammar] ~111-~111: There might be a mistake here.
Context: ...- Videos:test-results/(on failure) - Traces:test-results/` (on retry) - HTM...

(QB_NEW_EN)


[grammar] ~112-~112: There might be a mistake here.
Context: ...re) - Traces: test-results/ (on retry) - HTML Report: `pnpm --filter @unraid/e2e ...

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
e2e/README.md

19-19: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


53-53: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


119-119: Bare URL used

(MD034, no-bare-urls)

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🧹 Nitpick comments (8)
e2e/.env.example (2)

5-6: Quiet dotenv-linter: order keys and keep sections tidy.

Reordering avoids the warnings and keeps the example clean.

-UNRAID_USERNAME=root
-UNRAID_PASSWORD=
+UNRAID_PASSWORD=
+UNRAID_USERNAME=root
@@
-TEST_TIMEOUT=60000
-RETRY_COUNT=2
+RETRY_COUNT=2
+TEST_TIMEOUT=60000

Also applies to: 9-10


17-17: Add trailing newline at EOF.

Prevents minor lint diffs across tools.

-TRACE=false
+TRACE=false
+
e2e/README.md (6)

19-23: Add a language to the fenced block showing .env contents.

Satisfies MD040 and improves rendering.

-   ```
+   ```dotenv
    UNRAID_URL=http://tower.local
    UNRAID_USERNAME=root
    UNRAID_PASSWORD=your_password
    ```

53-61: Specify a language for the directory tree block.

Silences MD040.

-```
+```text
 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

---

`117-123`: **Avoid a bare URL in the table (MD034) and document all envs used.**

Wrap the default URL and consider adding any missing vars (TEST_TIMEOUT, RETRY_COUNT, TRACE) if they are actually used.

```diff
-| UNRAID_URL | Unraid server URL | http://tower.local |
+| UNRAID_URL | Unraid server URL | `http://tower.local` |
@@
 | DEBUG | Enable debug mode | false |
+| TEST_TIMEOUT | Per-test timeout (ms) | `60000` |
+| RETRY_COUNT | Retry failed tests | `2` |
+| TRACE | Enable trace on retry | `false` |

101-106: Include UNRAID_USERNAME in CI or note the default.

Prevents surprises if the default changes.

   env:
     UNRAID_URL: ${{ secrets.UNRAID_URL }}
+    UNRAID_USERNAME: ${{ secrets.UNRAID_USERNAME }} # optional; defaults to 'root'
     UNRAID_PASSWORD: ${{ secrets.UNRAID_PASSWORD }}
   run: pnpm test:e2e

46-49: Prefer npx for dotenvx to avoid a global install requirement.

Keeps setup simpler on CI and local.

-dotenvx run -f .env.dev-server-2 -- pnpm test
+npx dotenvx run -f .env.dev-server-2 -- pnpm test

1-114: Minor: add trailing newline at EOF.

Keeps diffs clean across tools.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d6dd4bb and db829d7.

📒 Files selected for processing (3)
  • e2e/.env.example (1 hunks)
  • e2e/.gitignore (1 hunks)
  • e2e/README.md (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • e2e/.gitignore
🧰 Additional context used
🪛 LanguageTool
e2e/README.md

[grammar] ~111-~111: There might be a mistake here.
Context: ...- Videos:test-results/(on failure) - Traces:test-results/` (on retry) - HTM...

(QB_NEW_EN)


[grammar] ~112-~112: There might be a mistake here.
Context: ...re) - Traces: test-results/ (on retry) - HTML Report: `pnpm --filter @unraid/e2e ...

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
e2e/README.md

19-19: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


53-53: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


119-119: Bare URL used

(MD034, no-bare-urls)

🪛 dotenv-linter (3.3.0)
e2e/.env.example

[warning] 6-6: [UnorderedKey] The UNRAID_PASSWORD key should go before the UNRAID_USERNAME key

(UnorderedKey)


[warning] 10-10: [UnorderedKey] The RETRY_COUNT key should go before the TEST_TIMEOUT key

(UnorderedKey)


[warning] 17-17: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)

Comment thread e2e/.env.example
Comment thread e2e/.env.example
Comment on lines +8 to +16
# Test Configuration
TEST_TIMEOUT=60000
RETRY_COUNT=2

# Browser Configuration
SLOW_MO=0

# Debugging
DEBUG=false

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.

Comment thread e2e/README.md
Comment on lines +27 to +49
```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
```

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.

@@ -0,0 +1,36 @@
import { test as base, TestInfo } from '@playwright/test';

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused import TestInfo.

Copilot Autofix

AI 10 months ago

The best way to fix this unused import is to simply remove TestInfo from the import statement on line 1 of e2e/fixtures/logger.fixture.ts, leaving only the required test as base import. No other changes are necessary, as the rest of the code does not rely on TestInfo directly or indirectly.

Suggested changeset 1
e2e/fixtures/logger.fixture.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/e2e/fixtures/logger.fixture.ts b/e2e/fixtures/logger.fixture.ts
--- a/e2e/fixtures/logger.fixture.ts
+++ b/e2e/fixtures/logger.fixture.ts
@@ -1,4 +1,4 @@
-import { test as base, TestInfo } from '@playwright/test';
+import { test as base } from '@playwright/test';
 import { TestLogger, interceptConsole } from '../utils/logger';
 
 export type LoggerFixtures = {
EOF
@@ -1,4 +1,4 @@
import { test as base, TestInfo } from '@playwright/test';
import { test as base } from '@playwright/test';
import { TestLogger, interceptConsole } from '../utils/logger';

export type LoggerFixtures = {
Copilot is powered by AI and may make mistakes. Always verify output.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 4

♻️ Duplicate comments (5)
e2e/playwright.config.ts (1)

21-23: Unify baseURL and remove unsupported webServer.ignoreHTTPSErrors

Avoid duplication and fix an invalid field (ignoreHTTPSErrors is not supported on webServer; keep it under use only).

 dotenvConfig();
 
+const baseURL = process.env.UNRAID_URL || 'http://tower.local';
+
 export default defineConfig({
@@
-  use: {
-    baseURL: process.env.UNRAID_URL || 'http://tower.local',
+  use: {
+    baseURL,
@@
-  webServer: process.env.NO_WEB_SERVER ? undefined : {
-    command: 'echo "Using external Unraid server"',
-    url: process.env.UNRAID_URL || 'http://tower.local',
-    reuseExistingServer: true,
-    ignoreHTTPSErrors: true,
-  },
+  webServer: process.env.NO_WEB_SERVER ? undefined : {
+    // With reuseExistingServer=true, Playwright will only run command if URL isn't reachable.
+    command: 'node -e "console.log(\\"Using external Unraid server\\")"',
+    url: baseURL,
+    reuseExistingServer: true
+  },

Also applies to: 70-75

e2e/README.md (1)

73-75: Fix script name: README references pnpm test:e2e which may not exist

Docs show pnpm test:e2e, but earlier reviews flagged this script missing. Align docs or add the script.

Option A (update README):

-UNRAID_URL=http://192.168.1.100 pnpm test:e2e
+UNRAID_URL=http://192.168.1.100 pnpm test
@@
-  run: pnpm test:e2e
+  run: pnpm test

Option B (add script):

  • In e2e/package.json add: "test:e2e": "playwright test"

Also applies to: 191-198

e2e/utils/reporting.ts (1)

4-8: Avoid any in Report.data

Per TS guidelines in this repo, replace any with unknown (or a concrete shape).

 type Report = {
   type: "info" | "warning" | "error";
   description: string;
-  data?: Record<string, any>;
+  data?: Record<string, unknown>;
 };
e2e/fixtures/logger.fixture.ts (1)

1-1: Remove unused TestInfo import

Keeps the file clean and resolves the CodeQL notice.

-import { test as base, TestInfo } from '@playwright/test';
+import { test as base } from '@playwright/test';
e2e/fixtures/auth.fixture.ts (1)

15-56: Don’t gate login on password truthiness; stabilize flow and avoid auth.json write races.

Current if (password) skips auth for blank passwords (e.g., default root), and the click → idle sequence is flaky. Also, writing to a fixed auth.json is racy under parallel workers.

-  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();
-        
-        // 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();
-  },
+  authenticatedPage: async ({ page, logger }, use, testInfo) => {
+    const username = process.env.UNRAID_USERNAME ?? 'root';
+    const password = process.env.UNRAID_PASSWORD ?? ''; // allow blank password
+
+    await page.goto('/');
+    await page.waitForLoadState('domcontentloaded');
+
+    const needsAuth =
+      (await page.locator('input[name="username"], input[name="password"], input#user, input#pass').count()) > 0;
+
+    if (needsAuth) {
+      logger.info('Authentication required, filling credentials');
+      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.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'),
+      ]);
+
+      await expect(page).not.toHaveURL(/login/i);
+      logger.info('Authentication successful, saving storage state');
+      await page.context().storageState({ path: testInfo.outputPath('auth.json') });
+    } else {
+      logger.debug('No authentication needed');
+    }
+
+    await use(undefined);
+  },
🧹 Nitpick comments (12)
e2e/playwright.config.ts (1)

8-11: Parallelism vs single shared server: confirm no state collisions

Locally this will parallelize across many workers; against one external server that can cause flakiness/state bleed. Keep CI at 1 worker (good) but consider gating fullyParallel behind an env or defaulting to false.

e2e/README.md (2)

19-23: Add languages to fenced code blocks (markdownlint MD040)

-```
+```ini
 UNRAID_URL=http://tower.local
@@
-```
+```
-```
+```text
 e2e/
 ├── fixtures/        # Test fixtures and setup
@@
 └── playwright.config.ts
-```
+```
-```
+```text
 test-results/logs/
 ├── chromium/
@@
                 └── timestamp.log
-```
+```
-```
+```text
 [2025-09-04 15:54:14.774] [INFO] [chromium] [dashboard.spec.ts > Dashboard > should display navigation menu] Found menu item: Main
-```
+```

Also applies to: 145-154, 218-227, 239-241


266-272: Avoid bare URL in table (markdownlint MD034)

-| UNRAID_URL | Unraid server URL | http://tower.local |
+| UNRAID_URL | Unraid server URL | `http://tower.local` |
e2e/justfile (2)

12-22: Handle env filenames with spaces/newlines robustly in test-all-envs loop

Using word-splitting on env_files can break with odd filenames. Prefer null-delimited find + while-read.

-    env_files=$(find . -maxdepth 1 -name ".env.*" -type f | grep -v ".env.example" | sort)
-    ...
-    for env_file in $env_files; do
+    while IFS= read -r -d '' env_file; do
         # Extract environment name from filename
         env_name=$(basename "$env_file" | sed 's/^\.env\.//')
@@
-    done
+    done < <(find . -maxdepth 1 -name ".env.*" -type f ! -name ".env.example" -print0 | sort -z)

Also applies to: 24-46


71-76: Propagate NO_WEB_SERVER when running tests via dotenvx (keep external server)

-    TEST_RESULTS_DIR="test-results-$env_name" \
-    dotenvx run --env-file="{{env_file}}" -- pnpm test {{args}}
+    TEST_RESULTS_DIR="test-results-$env_name" \
+    NO_WEB_SERVER=1 \
+    dotenvx run --env-file="{{env_file}}" -- pnpm test {{args}}
@@
-    TEST_RESULTS_DIR="test-results-$env_name" \
-    dotenvx run --env-file="{{env_file}}" -- pnpm test:headed {{args}}
+    TEST_RESULTS_DIR="test-results-$env_name" \
+    NO_WEB_SERVER=1 \
+    dotenvx run --env-file="{{env_file}}" -- pnpm test:headed {{args}}

Also applies to: 108-113

e2e/utils/logger.ts (2)

41-69: Fix timestamp filename formatting and avoid shadowing imported timestamp

Your current slice(0, -5) trims too much and leaves a trailing dash in filenames (e.g., ...12-.log). Also, the local timestamp identifier shadows the imported formatter. Use a clearer timeTag.

-    const browserName = testInfo.project.name.replace(/[^a-z0-9]/gi, '-').toLowerCase();
-    const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
+    const browserSegment = testInfo.project.name.replace(/[^a-z0-9]/gi, '-').toLowerCase();
+    const timeTag = new Date().toISOString().replace(/[:.]/g, '-').replace('Z', '');
@@
-    const pathParts = [outputDir, 'logs', browserName];
+    const pathParts = [outputDir, 'logs', browserSegment];
@@
-    return path.join(logDir, `${timestamp}.log`);
+    return path.join(logDir, `${timeTag}.log`);

89-94: Prefer testInfo.attach() over mutating attachments array

Playwright exposes testInfo.attach(); pushing into attachments works today but is less future-proof. Making this method async keeps API alignment.

-  attachToTest(testInfo: TestInfo): void {
+  async attachToTest(testInfo: TestInfo): Promise<void> {
@@
-    testInfo.attachments.push({
-      name: 'test-logs',
-      path: this.logFilePath,
-      contentType: 'text/plain'
-    });
+    await testInfo.attach('test-logs', {
+      path: this.logFilePath!,
+      contentType: 'text/plain'
+    });

And call site change in the fixture (see e2e/fixtures/logger.fixture.ts Lines 10-13):

-    logger.attachToTest(testInfo);
+    await logger.attachToTest(testInfo);

Also applies to: 71-74

e2e/utils/reporting.ts (1)

10-29: Mark unused testInfo param to satisfy noUnusedParameters

report doesn’t use testInfo. Prefix with _ to avoid lints without changing the signature.

-export function report(testInfo: TestInfo, message: Report, logger?: TestLogger) {
+export function report(_testInfo: TestInfo, message: Report, logger?: TestLogger) {
e2e/fixtures/logger.fixture.ts (2)

9-16: If attachToTest becomes async, await it here

Only needed if you adopt the optional refactor in the logger to use testInfo.attach().

-    const logger = new TestLogger();
-    logger.attachToTest(testInfo);
+    const logger = new TestLogger();
+    await logger.attachToTest(testInfo);

23-29: Log errors with richer context

Consider logging each error individually to preserve stacks in CI logs.

-    if (testInfo.errors.length > 0) {
-      logger.error('Test errors:', { errors: testInfo.errors });
-    }
+    for (const err of testInfo.errors) {
+      logger.error(err);
+    }
e2e/fixtures/auth.fixture.ts (2)

1-1: Confirm expect export source (or import directly from Playwright).

If logger.fixture doesn’t re-export expect, this import will fail. Prefer importing expect straight from Playwright to decouple fixtures.

-import { test as loggerTest, expect, LoggerFixtures } from './logger.fixture';
+import { test as loggerTest, LoggerFixtures } from './logger.fixture';
+import { expect } from '@playwright/test';

4-7: Minor: prefer interface extension over intersection for fixtures.

Interface extending LoggerFixtures is a bit clearer and avoids intersections.

-type AuthFixtures = {
-  loginPage: LoginPage;
-  authenticatedPage: void;
-} & LoggerFixtures;
+interface AuthFixtures extends LoggerFixtures {
+  loginPage: LoginPage;
+  authenticatedPage: void;
+}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between db829d7 and 2878560.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • e2e/.gitignore (1 hunks)
  • e2e/README.md (1 hunks)
  • e2e/fixtures/auth.fixture.ts (1 hunks)
  • e2e/fixtures/logger.fixture.ts (1 hunks)
  • e2e/justfile (1 hunks)
  • e2e/package.json (1 hunks)
  • e2e/playwright.config.ts (1 hunks)
  • e2e/utils/logger.ts (1 hunks)
  • e2e/utils/reporting.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • e2e/.gitignore
  • e2e/package.json
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use the any type; prefer proper typing
Avoid using type assertions/casting; prefer proper typing from the start

Files:

  • e2e/playwright.config.ts
  • e2e/fixtures/logger.fixture.ts
  • e2e/utils/logger.ts
  • e2e/utils/reporting.ts
  • e2e/fixtures/auth.fixture.ts
🧠 Learnings (5)
📚 Learning: 2025-02-03T18:57:28.442Z
Learnt from: elibosley
PR: unraid/api#1101
File: api/src/unraid-api/unraid-file-modifier/modifications/__test__/generic-modification.spec.ts:55-56
Timestamp: 2025-02-03T18:57:28.442Z
Learning: Using console.log is acceptable in test files, especially for debugging test execution. The logger instances in test files are often used as mocks/dependencies for the classes being tested, not for actual logging.

Applied to files:

  • e2e/fixtures/logger.fixture.ts
  • e2e/utils/logger.ts
  • e2e/fixtures/auth.fixture.ts
📚 Learning: 2025-02-06T17:25:45.397Z
Learnt from: mdatelle
PR: unraid/api#1122
File: web/package.json:16-16
Timestamp: 2025-02-06T17:25:45.397Z
Learning: The build script in web/package.json should include type-check command (`npm run type-check`) even when NODE_ENV is production, as it provides value for local builds by catching type errors before pushing to CI/CD.

Applied to files:

  • e2e/README.md
📚 Learning: 2025-03-07T19:29:12.540Z
Learnt from: pujitm
PR: unraid/api#1219
File: api/src/unraid-api/main.ts:18-25
Timestamp: 2025-03-07T19:29:12.540Z
Learning: When handling type mismatches between dependencies in TypeScript (such as between NestJS and Fastify), prefer using `ts-expect-error` with a detailed explanation comment instead of `as any` type assertions. This better documents the reason for the type bypass and makes the issue more visible for future maintenance.

Applied to files:

  • e2e/utils/reporting.ts
📚 Learning: 2025-09-02T14:41:56.235Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-02T14:41:56.235Z
Learning: Applies to web/**/{store,stores}/**/*.{spec,test}.@(ts|js) : Use factory functions for module mocks to avoid hoisting issues

Applied to files:

  • e2e/fixtures/auth.fixture.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/store/**/*.ts : Thoroughly test async store actions: assert intermediate loading state, await completion, and check final state

Applied to files:

  • e2e/fixtures/auth.fixture.ts
🧬 Code graph analysis (3)
e2e/fixtures/logger.fixture.ts (2)
e2e/utils/logger.ts (2)
  • TestLogger (8-143)
  • interceptConsole (154-205)
e2e/fixtures/auth.fixture.ts (1)
  • test (9-60)
e2e/utils/reporting.ts (1)
e2e/utils/logger.ts (1)
  • TestLogger (8-143)
e2e/fixtures/auth.fixture.ts (2)
e2e/utils/pages/login.page.ts (1)
  • LoginPage (3-36)
e2e/fixtures/logger.fixture.ts (2)
  • LoggerFixtures (4-6)
  • test (8-34)
🪛 GitHub Check: CodeQL
e2e/fixtures/logger.fixture.ts

[notice] 1-1: Unused variable, import, function or class
Unused import TestInfo.

🪛 LanguageTool
e2e/README.md

[grammar] ~231-~231: There might be a mistake here.
Context: ...### Log Format Each log entry includes: - Timestamp (millisecond precision) - Log ...

(QB_NEW_EN)


[grammar] ~232-~232: There might be a mistake here.
Context: ...des: - Timestamp (millisecond precision) - Log level (INFO, WARN, ERROR) - Browser ...

(QB_NEW_EN)


[grammar] ~233-~233: There might be a mistake here.
Context: ...ecision) - Log level (INFO, WARN, ERROR) - Browser name - Full test path - Message ...

(QB_NEW_EN)


[grammar] ~234-~234: There might be a mistake here.
Context: ...level (INFO, WARN, ERROR) - Browser name - Full test path - Message and metadata E...

(QB_NEW_EN)


[grammar] ~235-~235: There might be a mistake here.
Context: ..., ERROR) - Browser name - Full test path - Message and metadata Example log entry:...

(QB_NEW_EN)


[grammar] ~259-~259: There might be a mistake here.
Context: ...- Videos:test-results/(on failure) - Traces:test-results/` (on retry) - **L...

(QB_NEW_EN)


[grammar] ~260-~260: There might be a mistake here.
Context: ...re) - Traces: test-results/ (on retry) - **Logs: test-results/logs/ (organized by...

(QB_NEW_EN)


[grammar] ~261-~261: There might be a mistake here.
Context: ...sults/logs/(organized by browser/test)** - HTML Report:pnpm --filter @unraid/e2e ...

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
e2e/README.md

19-19: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


145-145: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


218-218: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


239-239: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


268-268: Bare URL used

(MD034, no-bare-urls)

🔇 Additional comments (3)
e2e/utils/logger.ts (1)

27-37: Confirm console transport level is intentionally limited to error

Only errors reach stdout/stderr, which hides info/warn during local debugging. If unintentional, bump to info with level-controlled gating via LOG_LEVEL.

e2e/fixtures/auth.fixture.ts (2)

2-2: Verify ESM/TS resolution for .js extension import.

login.page.ts is TypeScript; importing with .js requires NodeNext/Bundler module resolution. If not configured, switch to extensionless import.

-import { LoginPage } from '../utils/pages/login.page.js';
+import { LoginPage } from '../utils/pages/login.page';

10-13: LGTM: page object fixture is clean and composable.

No issues; good separation of concerns.

Comment thread e2e/playwright.config.ts
Comment on lines +12 to +19
reporter: [
['html', {
outputFolder: process.env.PLAYWRIGHT_HTML_OUTPUT_DIR || 'playwright-report',
open: process.env.PLAYWRIGHT_HTML_OPEN as 'always' | 'never' | 'on-failure' || 'on-failure'
}],
['line'],
process.env.CI ? ['github'] : null,
].filter(Boolean) as any,

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

Remove "as any" and avoid assertions; build a typed reporters array

This violates the TS guideline to avoid any/type assertions. Construct reporters with proper types and a validated open value.

-import { defineConfig, devices } from '@playwright/test';
+import { defineConfig, devices, type ReporterDescription } from '@playwright/test';
@@
-export default defineConfig({
+const isHtmlOpen = (v: string): v is 'always' | 'never' | 'on-failure' =>
+  v === 'always' || v === 'never' || v === 'on-failure';
+const htmlOpenEnv = (process.env.PLAYWRIGHT_HTML_OPEN ?? 'on-failure').toLowerCase();
+const htmlOpen: 'always' | 'never' | 'on-failure' = isHtmlOpen(htmlOpenEnv) ? htmlOpenEnv : 'on-failure';
+
+const reporters: ReporterDescription[] = process.env.CI
+  ? [['html', { outputFolder: process.env.PLAYWRIGHT_HTML_OUTPUT_DIR || 'playwright-report', open: htmlOpen }], ['line'], ['github']]
+  : [['html', { outputFolder: process.env.PLAYWRIGHT_HTML_OUTPUT_DIR || 'playwright-report', open: htmlOpen }], ['line']];
+
+export default defineConfig({
@@
-  reporter: [
-    ['html', { 
-      outputFolder: process.env.PLAYWRIGHT_HTML_OUTPUT_DIR || 'playwright-report',
-      open: process.env.PLAYWRIGHT_HTML_OPEN as 'always' | 'never' | 'on-failure' || 'on-failure'
-    }],
-    ['line'],
-    process.env.CI ? ['github'] : null,
-  ].filter(Boolean) as any,
+  reporter: reporters,
📝 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
reporter: [
['html', {
outputFolder: process.env.PLAYWRIGHT_HTML_OUTPUT_DIR || 'playwright-report',
open: process.env.PLAYWRIGHT_HTML_OPEN as 'always' | 'never' | 'on-failure' || 'on-failure'
}],
['line'],
process.env.CI ? ['github'] : null,
].filter(Boolean) as any,
// e2e/playwright.config.ts
import { defineConfig, devices, type ReporterDescription } from '@playwright/test';
// Ensure the 'open' option is one of the allowed values
const isHtmlOpen = (v: string): v is 'always' | 'never' | 'on-failure' =>
v === 'always' || v === 'never' || v === 'on-failure';
const htmlOpenEnv = (process.env.PLAYWRIGHT_HTML_OPEN ?? 'on-failure').toLowerCase();
const htmlOpen: 'always' | 'never' | 'on-failure' =
isHtmlOpen(htmlOpenEnv) ? htmlOpenEnv : 'on-failure';
// Build a properly typed reporters array instead of using `filter(Boolean) as any`
const reporters: ReporterDescription[] = process.env.CI
? [
['html', {
outputFolder: process.env.PLAYWRIGHT_HTML_OUTPUT_DIR || 'playwright-report',
open: htmlOpen,
}],
['line'],
['github'],
]
: [
['html', {
outputFolder: process.env.PLAYWRIGHT_HTML_OUTPUT_DIR || 'playwright-report',
open: htmlOpen,
}],
['line'],
];
export default defineConfig({
- reporter: [
- ['html', {
- outputFolder: process.env.PLAYWRIGHT_HTML_OUTPUT_DIR || 'playwright-report',
- open: process.env.PLAYWRIGHT_HTML_OPEN as 'always' | 'never' | 'on-failure' || 'on-failure'
- }],
- ['line'],
- process.env.CI ? ['github'] : null,
reporter: reporters,
// ... the rest of your Playwright configuration
});

Comment thread e2e/utils/logger.ts
Comment on lines +114 to +142
log(message: string, meta?: any): void {
this.logger.info(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}

info(message: string, meta?: any): void {
this.logger.info(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}

warn(message: string, meta?: any): void {
this.logger.warn(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}

error(message: string | Error, meta?: any): void {
const msg = message instanceof Error ? message.message : message;
const errorMeta = message instanceof Error ? { stack: message.stack, ...meta } : meta;
this.logger.error(msg, { testPath: this.getTestPath(), browser: this.browserName, ...errorMeta });
}

debug(message: string, meta?: any): void {
this.logger.debug(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}

verbose(message: string, meta?: any): void {
this.logger.verbose(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}

http(message: string, meta?: any): void {
this.logger.http(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}

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

Replace unsafe any with a safer meta type per TS guidelines

Follow the repo rule “Never use any” for TS/TSX. Narrow meta to a record.

-  log(message: string, meta?: any): void {
+  log(message: string, meta?: Record<string, unknown>): void {
@@
-  info(message: string, meta?: any): void {
+  info(message: string, meta?: Record<string, unknown>): void {
@@
-  warn(message: string, meta?: any): void {
+  warn(message: string, meta?: Record<string, unknown>): void {
@@
-  error(message: string | Error, meta?: any): void {
+  error(message: string | Error, meta?: Record<string, unknown>): void {
@@
-  debug(message: string, meta?: any): void {
+  debug(message: string, meta?: Record<string, unknown>): void {
@@
-  verbose(message: string, meta?: any): void {
+  verbose(message: string, meta?: Record<string, unknown>): void {
@@
-  http(message: string, meta?: any): void {
+  http(message: string, meta?: Record<string, unknown>): void {
📝 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
log(message: string, meta?: any): void {
this.logger.info(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}
info(message: string, meta?: any): void {
this.logger.info(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}
warn(message: string, meta?: any): void {
this.logger.warn(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}
error(message: string | Error, meta?: any): void {
const msg = message instanceof Error ? message.message : message;
const errorMeta = message instanceof Error ? { stack: message.stack, ...meta } : meta;
this.logger.error(msg, { testPath: this.getTestPath(), browser: this.browserName, ...errorMeta });
}
debug(message: string, meta?: any): void {
this.logger.debug(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}
verbose(message: string, meta?: any): void {
this.logger.verbose(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}
http(message: string, meta?: any): void {
this.logger.http(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}
log(message: string, meta?: Record<string, unknown>): void {
this.logger.info(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}
info(message: string, meta?: Record<string, unknown>): void {
this.logger.info(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}
warn(message: string, meta?: Record<string, unknown>): void {
this.logger.warn(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}
error(message: string | Error, meta?: Record<string, unknown>): void {
const msg = message instanceof Error ? message.message : message;
const errorMeta = message instanceof Error ? { stack: message.stack, ...meta } : meta;
this.logger.error(msg, { testPath: this.getTestPath(), browser: this.browserName, ...errorMeta });
}
debug(message: string, meta?: Record<string, unknown>): void {
this.logger.debug(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}
verbose(message: string, meta?: Record<string, unknown>): void {
this.logger.verbose(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}
http(message: string, meta?: Record<string, unknown>): void {
this.logger.http(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}
🤖 Prompt for AI Agents
In e2e/utils/logger.ts around lines 114 to 142, the method signatures currently
use unsafe any for meta; change all occurrences of meta?: any to meta?:
Record<string, unknown> (or a specific interface if you have known keys) and
update local variables accordingly (e.g. ensure errorMeta is typed as
Record<string, unknown> | undefined and use object spreads safely). Adjust the
error method so the stack and other properties are merged into a Record<string,
unknown> (casting when necessary) to satisfy TypeScript without any, and run
type checks to fix any resulting type errors.

Comment thread e2e/utils/logger.ts
Comment on lines +154 to +205
export function interceptConsole(logger: TestLogger): () => void {
const originalConsole = {
log: console.log,
info: console.info,
warn: console.warn,
error: console.error,
debug: console.debug,
};

console.log = (...args: any[]) => {
const message = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
).join(' ');
logger.log(message);
};

console.info = (...args: any[]) => {
const message = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
).join(' ');
logger.info(message);
};

console.warn = (...args: any[]) => {
const message = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
).join(' ');
logger.warn(message);
};

console.error = (...args: any[]) => {
const message = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
).join(' ');
logger.error(message);
};

console.debug = (...args: any[]) => {
const message = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
).join(' ');
logger.debug(message);
};

return () => {
console.log = originalConsole.log;
console.info = originalConsole.info;
console.warn = originalConsole.warn;
console.error = originalConsole.error;
console.debug = originalConsole.debug;
};
} No newline at end of file

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

Console interception: keep Error stacks, handle BigInt/circulars, and drop any

Right now console.error(new Error(...)) loses the stack (stringified to {}). Also JSON.stringify on bigint throws. Tighten typing and formatting.

-export function interceptConsole(logger: TestLogger): () => void {
+export function interceptConsole(logger: TestLogger): () => void {
@@
-  console.log = (...args: any[]) => {
-    const message = args.map(arg => 
-      typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
-    ).join(' ');
+  const toStr = (arg: unknown): string => {
+    if (typeof arg === 'bigint') return `${arg}n`;
+    if (arg instanceof Error) return arg.stack || arg.message || String(arg);
+    if (typeof arg === 'object' && arg !== null) {
+      try { return JSON.stringify(arg, null, 2); } catch { return '[Unserializable Object]'; }
+    }
+    return String(arg);
+  };
+
+  console.log = (...args: unknown[]) => {
+    const message = args.map(toStr).join(' ');
     logger.log(message);
   };
 
-  console.info = (...args: any[]) => {
-    const message = args.map(arg => 
-      typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
-    ).join(' ');
+  console.info = (...args: unknown[]) => {
+    const message = args.map(toStr).join(' ');
     logger.info(message);
   };
 
-  console.warn = (...args: any[]) => {
-    const message = args.map(arg => 
-      typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
-    ).join(' ');
+  console.warn = (...args: unknown[]) => {
+    const message = args.map(toStr).join(' ');
     logger.warn(message);
   };
 
-  console.error = (...args: any[]) => {
-    const message = args.map(arg => 
-      typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
-    ).join(' ');
-    logger.error(message);
+  console.error = (...args: unknown[]) => {
+    if (args.length === 1 && args[0] instanceof Error) {
+      logger.error(args[0]);
+      return;
+    }
+    const message = args.map(toStr).join(' ');
+    logger.error(message);
   };
 
-  console.debug = (...args: any[]) => {
-    const message = args.map(arg => 
-      typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
-    ).join(' ');
+  console.debug = (...args: unknown[]) => {
+    const message = args.map(toStr).join(' ');
     logger.debug(message);
   };
📝 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
export function interceptConsole(logger: TestLogger): () => void {
const originalConsole = {
log: console.log,
info: console.info,
warn: console.warn,
error: console.error,
debug: console.debug,
};
console.log = (...args: any[]) => {
const message = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
).join(' ');
logger.log(message);
};
console.info = (...args: any[]) => {
const message = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
).join(' ');
logger.info(message);
};
console.warn = (...args: any[]) => {
const message = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
).join(' ');
logger.warn(message);
};
console.error = (...args: any[]) => {
const message = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
).join(' ');
logger.error(message);
};
console.debug = (...args: any[]) => {
const message = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
).join(' ');
logger.debug(message);
};
return () => {
console.log = originalConsole.log;
console.info = originalConsole.info;
console.warn = originalConsole.warn;
console.error = originalConsole.error;
console.debug = originalConsole.debug;
};
}
export function interceptConsole(logger: TestLogger): () => void {
const originalConsole = {
log: console.log,
info: console.info,
warn: console.warn,
error: console.error,
debug: console.debug,
};
// Unified serializer: preserves Error stacks, handles bigint,
// and safely falls back on circular/unserializable objects
const toStr = (arg: unknown): string => {
if (typeof arg === 'bigint') {
return `${arg}n`;
}
if (arg instanceof Error) {
return arg.stack || arg.message || String(arg);
}
if (typeof arg === 'object' && arg !== null) {
try {
return JSON.stringify(arg, null, 2);
} catch {
return '[Unserializable Object]';
}
}
return String(arg);
};
console.log = (...args: unknown[]) => {
const message = args.map(toStr).join(' ');
logger.log(message);
};
console.info = (...args: unknown[]) => {
const message = args.map(toStr).join(' ');
logger.info(message);
};
console.warn = (...args: unknown[]) => {
const message = args.map(toStr).join(' ');
logger.warn(message);
};
console.error = (...args: unknown[]) => {
// If it’s a lone Error, preserve its full object (stack, etc.)
if (args.length === 1 && args[0] instanceof Error) {
logger.error(args[0]);
return;
}
const message = args.map(toStr).join(' ');
logger.error(message);
};
console.debug = (...args: unknown[]) => {
const message = args.map(toStr).join(' ');
logger.debug(message);
};
return () => {
console.log = originalConsole.log;
console.info = originalConsole.info;
console.warn = originalConsole.warn;
console.error = originalConsole.error;
console.debug = originalConsole.debug;
};
}

Comment thread e2e/utils/reporting.ts
Comment on lines +31 to +34
export function reportAndAnnotate(testInfo: TestInfo, message: Report, logger?: TestLogger) {
report(testInfo, message, logger);
testInfo.annotations.push(message);
}

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

Push a Playwright-compatible annotation shape

TestInfo.annotations expects { type: string; description?: string }. Pushing the whole Report (with data) may not conform to types in some versions.

-export function reportAndAnnotate(testInfo: TestInfo, message: Report, logger?: TestLogger) {
+export function reportAndAnnotate(testInfo: TestInfo, message: Report, logger?: TestLogger) {
   report(testInfo, message, logger);
-  testInfo.annotations.push(message);
+  testInfo.annotations.push({ type: message.type, description: message.description });
 }
📝 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
export function reportAndAnnotate(testInfo: TestInfo, message: Report, logger?: TestLogger) {
report(testInfo, message, logger);
testInfo.annotations.push(message);
}
export function reportAndAnnotate(testInfo: TestInfo, message: Report, logger?: TestLogger) {
report(testInfo, message, logger);
testInfo.annotations.push({ type: message.type, description: message.description });
}
🤖 Prompt for AI Agents
In e2e/utils/reporting.ts around lines 31 to 34, the code pushes the entire
Report object into testInfo.annotations which can include a data field and not
match Playwright's expected annotation shape ({ type: string; description?:
string }); change the push to add only an object with type: message.type and
description: a string (for example message.message or
JSON.stringify(message.data) or a combined string) so the annotation conforms to
Playwright types and retains useful info.

@github-actions

github-actions Bot commented Sep 4, 2025

Copy link
Copy Markdown
Contributor

This plugin has been deployed to Cloudflare R2 and is available for testing.
Download it at this URL:

https://preview.dl.unraid.net/unraid-api/tag/PR1648/dynamix.unraid.net.plg

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 6

♻️ Duplicate comments (5)
e2e/playwright.config.ts (2)

12-19: Remove “as any” and filter(Boolean); build a typed reporters array

This violates our TS guideline to avoid assertions and using any. Construct a ReporterDescription[] conditionally instead. (Using retrieved learning: avoid type assertions/casting in TS.)

Apply:

-  reporter: [
-    ['html', { 
-      outputFolder: process.env.PLAYWRIGHT_HTML_OUTPUT_DIR || 'playwright-report',
-      open: process.env.PLAYWRIGHT_HTML_OPEN as 'always' | 'never' | 'on-failure' || 'on-failure'
-    }],
-    ['line'],
-    process.env.CI ? ['github'] : null,
-  ].filter(Boolean) as any,
+  reporter: reporters,

Add (outside the selection, near imports/top):

import { defineConfig, devices, type ReporterDescription } from '@playwright/test';

const htmlOpenEnv = process.env.PLAYWRIGHT_HTML_OPEN;
const htmlOpen: 'always' | 'never' | 'on-failure' =
  htmlOpenEnv === 'always' || htmlOpenEnv === 'never' || htmlOpenEnv === 'on-failure'
    ? htmlOpenEnv
    : 'on-failure';

const reporters: ReporterDescription[] = [
  ['html', {
    outputFolder: process.env.PLAYWRIGHT_HTML_OUTPUT_DIR || 'playwright-report',
    open: htmlOpen,
  }],
  ['line'],
];

if (process.env.CI) reporters.push(['github']);

21-23: Unify baseURL and remove unsupported webServer.ignoreHTTPSErrors

  • Define baseURL once and reuse.
  • webServer.ignoreHTTPSErrors is not a supported option and will type-error.

Apply:

+const baseURL = process.env.UNRAID_URL || 'http://tower.local';
 
 export default defineConfig({
   ...
   use: {
-    baseURL: process.env.UNRAID_URL || 'http://tower.local',
+    baseURL,
     trace: 'on-first-retry',
     screenshot: 'only-on-failure',
     video: 'retain-on-failure',
-  webServer: process.env.NO_WEB_SERVER ? undefined : {
-    command: 'echo "Using external Unraid server"',
-    url: process.env.UNRAID_URL || 'http://tower.local',
-    reuseExistingServer: true,
-    ignoreHTTPSErrors: true,
-  },
+  webServer: process.env.NO_WEB_SERVER ? undefined : {
+    // External Unraid server expected; just emit a message.
+    command: 'node -e "console.log(\\"Using external Unraid server\\")"',
+    url: baseURL,
+    reuseExistingServer: true,
+  },

Also applies to: 70-75

e2e/fixtures/logger.fixture.ts (1)

1-1: Remove unused TestInfo import

Avoid unused imports to keep the fixture clean.

Apply this diff:

-import { test as base, TestInfo } from '@playwright/test';
+import { test as base } from '@playwright/test';
e2e/fixtures/auth.fixture.ts (2)

58-59: Call use(undefined) for void fixture

Matches Playwright’s types for a void fixture.

Apply this diff:

-    await use();
+    await use(undefined);

16-18: Don’t skip login when password is blank; stabilize flow and assert redirect

Handle blank root password, wait for visibility, and assert we left the login route. Save storage state into the test’s output dir.

Apply this diff:

-  authenticatedPage: async ({ page, logger }, use) => {
-    const username = process.env.UNRAID_USERNAME || 'root';
-    const password = process.env.UNRAID_PASSWORD || '';
+  authenticatedPage: async ({ page, logger }, use, testInfo) => {
+    const username = process.env.UNRAID_USERNAME ?? 'root';
+    const password = process.env.UNRAID_PASSWORD ?? '';
@@
-    if (password) {
+    if (typeof password !== 'undefined') {
       logger.info('Attempting authentication');
-      await page.goto('/');
+      await page.goto('/', { waitUntil: 'domcontentloaded' });
@@
-      await page.waitForLoadState('networkidle');
+      await page.waitForLoadState('domcontentloaded');
@@
-        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'),
+        ]);
@@
-        // Check if we successfully logged in
-        const stillOnLogin = page.url().includes('login');
-        if (!stillOnLogin) {
+        // Check if we successfully logged in
+        await expect(page).not.toHaveURL(/login/i);
+        if (!/login/i.test(page.url())) {
           logger.info('Authentication successful, saving storage state');
-          await page.context().storageState({ path: 'auth.json' });
+          const authPath = testInfo.outputPath('auth.json');
+          await page.context().storageState({ path: authPath });
         } else {
           logger.warn('Authentication may have failed - still on login page');
         }

Also applies to: 19-56

🧹 Nitpick comments (8)
e2e/playwright.config.ts (1)

21-35: Optional: wire SLOW_MO env to launch options

Makes local debugging easier while keeping CI fast.

Apply:

   use: {
     baseURL,
     trace: 'on-first-retry',
     screenshot: 'only-on-failure',
     video: 'retain-on-failure',
+    launchOptions: {
+      slowMo: Number(process.env.SLOW_MO || 0),
+    },
e2e/README.md (2)

19-23: Add languages to fenced code blocks (markdownlint MD040)

Improves readability and fixes lint errors.

Apply:

-```
+```dotenv
 UNRAID_URL=http://tower.local
 UNRAID_USERNAME=root
 UNRAID_PASSWORD=your_password

```diff
-```
+```text
 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

```diff
-```
+```text
 test-results/logs/
 ├── chromium/
 ├── firefox/
 └── mobile-chrome/
     └── test-file/
         └── test-suite/
             └── test-name/
                 └── timestamp.log

```diff
-```
+```text
 [2025-09-04 15:54:14.774] [INFO] [chromium] [dashboard.spec.ts > Dashboard > should display navigation menu] Found menu item: Main


Also applies to: 145-153, 218-227, 239-241

---

`266-272`: **Wrap bare URL and document NO_WEB_SERVER**

Fix MD034 (bare URL) and surface the env used by the config.




Apply:
```diff
-| UNRAID_URL | Unraid server URL | http://tower.local |
+| 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 |
+| NO_WEB_SERVER | Skip starting Playwright webServer (use external Unraid) | false |
e2e/justfile (2)

12-14: Use find’s negation instead of piping to grep

Slightly simpler and avoids a subshell.

Apply:

-    env_files=$(find . -maxdepth 1 -name ".env.*" -type f | grep -v ".env.example" | sort)
+    env_files=$(find . -maxdepth 1 -name ".env.*" -type f ! -name ".env.example" | sort)

85-87: Same simplification for listing env files

Keep style consistent.

Apply:

-    @find . -maxdepth 1 -name ".env.*" -type f | grep -v ".env.example" | sort
+    @find . -maxdepth 1 -name ".env.*" -type f ! -name ".env.example" | sort
e2e/fixtures/logger.fixture.ts (2)

23-29: Be cautious logging raw testInfo.errors

Errors can include sensitive data. Prefer summarized/serialized errors after redaction (see redaction format in logger.ts).

Apply this diff once redaction is added:

-    if (testInfo.errors.length > 0) {
-      logger.error('Test errors:', { errors: testInfo.errors });
-    }
+    if (testInfo.errors.length > 0) {
+      logger.error('Test errors', { errors: testInfo.errors.map(e => ({ message: e.message })) });
+    }

31-33: Optional: guard console interception behind an env toggle

Let CI opt-out if needed.

Apply this diff:

-    restoreConsole();
+    restoreConsole();
     logger.detachFromTest();

And wrap interception above:

-    const restoreConsole = interceptConsole(logger);
+    const restoreConsole = process.env.CAPTURE_CONSOLE === 'false' ? () => {} : interceptConsole(logger);
e2e/utils/logger.ts (1)

1-7: Alias timestamp formatter and prep for redaction

Avoid name shadowing and set up a redaction transform.

Apply this diff:

-import winston from 'winston';
+import winston from 'winston';
 import * as path from 'path';
 import * as fs from 'fs';
 import { TestInfo } from '@playwright/test';
+import { inspect } from 'util';
 
-const { combine, timestamp, printf, colorize } = winston.format;
+const { combine, timestamp: fmtTimestamp, printf, colorize } = winston.format;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between db829d7 and 1a970c5.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • e2e/.gitignore (1 hunks)
  • e2e/README.md (1 hunks)
  • e2e/fixtures/auth.fixture.ts (1 hunks)
  • e2e/fixtures/logger.fixture.ts (1 hunks)
  • e2e/justfile (1 hunks)
  • e2e/package.json (1 hunks)
  • e2e/playwright.config.ts (1 hunks)
  • e2e/utils/logger.ts (1 hunks)
  • e2e/utils/reporting.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • e2e/utils/reporting.ts
  • e2e/.gitignore
  • e2e/package.json
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use the any type; prefer proper typing
Avoid using type assertions/casting; prefer proper typing from the start

Files:

  • e2e/utils/logger.ts
  • e2e/fixtures/logger.fixture.ts
  • e2e/playwright.config.ts
  • e2e/fixtures/auth.fixture.ts
🧠 Learnings (5)
📚 Learning: 2025-02-03T18:57:28.442Z
Learnt from: elibosley
PR: unraid/api#1101
File: api/src/unraid-api/unraid-file-modifier/modifications/__test__/generic-modification.spec.ts:55-56
Timestamp: 2025-02-03T18:57:28.442Z
Learning: Using console.log is acceptable in test files, especially for debugging test execution. The logger instances in test files are often used as mocks/dependencies for the classes being tested, not for actual logging.

Applied to files:

  • e2e/utils/logger.ts
  • e2e/fixtures/logger.fixture.ts
  • e2e/fixtures/auth.fixture.ts
📚 Learning: 2025-09-02T14:41:56.235Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-02T14:41:56.235Z
Learning: Applies to **/*.{ts,tsx} : Avoid using type assertions/casting; prefer proper typing from the start

Applied to files:

  • e2e/playwright.config.ts
📚 Learning: 2025-09-02T14:41:56.235Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-02T14:41:56.235Z
Learning: Applies to web/**/{store,stores}/**/*.{spec,test}.@(ts|js) : Use factory functions for module mocks to avoid hoisting issues

Applied to files:

  • e2e/fixtures/auth.fixture.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/store/**/*.ts : Thoroughly test async store actions: assert intermediate loading state, await completion, and check final state

Applied to files:

  • e2e/fixtures/auth.fixture.ts
📚 Learning: 2025-02-06T17:25:45.397Z
Learnt from: mdatelle
PR: unraid/api#1122
File: web/package.json:16-16
Timestamp: 2025-02-06T17:25:45.397Z
Learning: The build script in web/package.json should include type-check command (`npm run type-check`) even when NODE_ENV is production, as it provides value for local builds by catching type errors before pushing to CI/CD.

Applied to files:

  • e2e/README.md
🧬 Code graph analysis (1)
e2e/utils/logger.ts (1)
api/src/core/log.ts (1)
  • logger (53-83)
🪛 GitHub Check: CodeQL
e2e/fixtures/logger.fixture.ts

[notice] 1-1: Unused variable, import, function or class
Unused import TestInfo.

🪛 LanguageTool
e2e/README.md

[grammar] ~231-~231: There might be a mistake here.
Context: ...### Log Format Each log entry includes: - Timestamp (millisecond precision) - Log ...

(QB_NEW_EN)


[grammar] ~232-~232: There might be a mistake here.
Context: ...des: - Timestamp (millisecond precision) - Log level (INFO, WARN, ERROR) - Browser ...

(QB_NEW_EN)


[grammar] ~233-~233: There might be a mistake here.
Context: ...ecision) - Log level (INFO, WARN, ERROR) - Browser name - Full test path - Message ...

(QB_NEW_EN)


[grammar] ~234-~234: There might be a mistake here.
Context: ...level (INFO, WARN, ERROR) - Browser name - Full test path - Message and metadata E...

(QB_NEW_EN)


[grammar] ~235-~235: There might be a mistake here.
Context: ..., ERROR) - Browser name - Full test path - Message and metadata Example log entry:...

(QB_NEW_EN)


[grammar] ~259-~259: There might be a mistake here.
Context: ...- Videos:test-results/(on failure) - Traces:test-results/` (on retry) - **L...

(QB_NEW_EN)


[grammar] ~260-~260: There might be a mistake here.
Context: ...re) - Traces: test-results/ (on retry) - **Logs: test-results/logs/ (organized by...

(QB_NEW_EN)


[grammar] ~261-~261: There might be a mistake here.
Context: ...sults/logs/(organized by browser/test)** - HTML Report:pnpm --filter @unraid/e2e ...

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
e2e/README.md

19-19: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


145-145: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


218-218: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


239-239: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


268-268: Bare URL used

(MD034, no-bare-urls)

🔇 Additional comments (3)
e2e/README.md (1)

73-75: e2e docs correctpnpm test:e2e is defined in root package.json (filters @unraid/e2e), so the example is valid.

e2e/justfile (1)

56-76: No changes required: scripts exist Verified that test, test:headed, and playwright:install are defined in e2e/package.json, matching the justfile commands.

e2e/fixtures/auth.fixture.ts (1)

2-2: No changes required
Imports in E2E fixtures must use .js extensions for ESM compatibility and match existing imports in e2e/tests/*.ts.

Likely an incorrect or invalid review comment.

Comment on lines +9 to +13
logger: [async ({ browserName }, use, testInfo) => {
const logger = new TestLogger();
logger.attachToTest(testInfo);

const restoreConsole = interceptConsole(logger);

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

Make attachToTest async and await it

Switch to testInfo.attach(...) in TestLogger (see logger.ts comment) requires attachToTest to be async; await it here to avoid lost attachment writes.

Apply this diff (paired with logger.ts changes):

-  logger: [async ({ browserName }, use, testInfo) => {
+  logger: [async ({ browserName }, use, testInfo) => {
     const logger = new TestLogger();
-    logger.attachToTest(testInfo);
+    await logger.attachToTest(testInfo);
📝 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
logger: [async ({ browserName }, use, testInfo) => {
const logger = new TestLogger();
logger.attachToTest(testInfo);
const restoreConsole = interceptConsole(logger);
logger: [async ({ browserName }, use, testInfo) => {
const logger = new TestLogger();
await logger.attachToTest(testInfo);
const restoreConsole = interceptConsole(logger);
🤖 Prompt for AI Agents
In e2e/fixtures/logger.fixture.ts around lines 9-13, attachToTest was changed to
be asynchronous; update this fixture to await the call so attachment writes
aren't lost: mark the lambda as async (already async), call await
logger.attachToTest(testInfo) instead of the current synchronous call, and
ensure any teardown uses the restored console only after the awaited attach
completes; this change should be applied together with the corresponding async
change in logger.ts.

Comment thread e2e/utils/logger.ts
Comment on lines +14 to +38
constructor() {
this.logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: combine(
timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
printf(({ level, message, timestamp, testPath, browser, ...meta }) => {
const testContext = testPath ? `[${testPath}]` : '';
const browserContext = browser ? `[${browser}]` : '';
const metaStr = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : '';
return `[${timestamp}] [${level.toUpperCase()}] ${browserContext} ${testContext} ${message}${metaStr}`;
})
),
transports: [
new winston.transports.Console({
level: 'error',
format: combine(
colorize(),
printf(({ message, browser }) => {
const browserPrefix = browser ? `[${browser}]` : '';
return `[TEST] ${browserPrefix} ${message}`;
})
)
})
]
});

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

Add secret redaction, stronger console format, and types

Replace any with typed metadata, add redaction to prevent secrets leakage in CI artifacts, and improve console formatting.

Apply this diff:

   constructor() {
     this.logger = winston.createLogger({
       level: process.env.LOG_LEVEL || 'info',
-      format: combine(
-        timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
-        printf(({ level, message, timestamp, testPath, browser, ...meta }) => {
-          const testContext = testPath ? `[${testPath}]` : '';
-          const browserContext = browser ? `[${browser}]` : '';
-          const metaStr = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : '';
-          return `[${timestamp}] [${level.toUpperCase()}] ${browserContext} ${testContext} ${message}${metaStr}`;
-        })
-      ),
+      format: combine(
+        fmtTimestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
+        // redact sensitive keys in all metadata
+        winston.format((info) => {
+          const redact = (obj: unknown): unknown => {
+            if (!obj || typeof obj !== 'object') return obj;
+            const censor = '***REDACTED***';
+            const keys = ['password','pass','secret','token','key','apikey','accesstoken','idtoken','refreshtoken','localApiKey'];
+            const out: Record<string, unknown> = {};
+            for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
+              out[k] = keys.some(s => k.toLowerCase().includes(s)) ? censor : (typeof v === 'object' ? redact(v) : v);
+            }
+            return out;
+          };
+          const { testPath, browser, ...meta } = info as Record<string, unknown>;
+          const safeMeta = redact(meta);
+          Object.assign(info, { testPath, browser, meta: safeMeta });
+          return info;
+        })(),
+        printf(({ level, message, timestamp, testPath, browser, meta }) => {
+          const testContext = testPath ? `[${testPath}]` : '';
+          const browserContext = browser ? `[${browser}]` : '';
+          const metaStr = meta && Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : '';
+          return `[${timestamp}] [${String(level).toUpperCase()}] ${browserContext} ${testContext} ${message}${metaStr}`;
+        })
+      ),
       transports: [
         new winston.transports.Console({
           level: 'error',
           format: combine(
             colorize(),
-            printf(({ message, browser }) => {
+            printf(({ message, browser }) => {
               const browserPrefix = browser ? `[${browser}]` : '';
               return `[TEST] ${browserPrefix} ${message}`;
             })
           )
         })
       ]
     });
   }

Committable suggestion skipped: line range outside the PR's diff.

Comment thread e2e/utils/logger.ts
Comment on lines +41 to +69
private ensureLogDirectory(testInfo: TestInfo): string {
const outputDir = process.env.TEST_RESULTS_DIR || 'test-results';
const browserName = testInfo.project.name.replace(/[^a-z0-9]/gi, '-').toLowerCase();
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);

// Build simple hierarchy: logs/browser/test-file/suite/test-name/
const pathParts = [outputDir, 'logs', browserName];

// Add test file name
const testFileName = path.basename(testInfo.file, '.spec.ts')
.replace(/[^a-z0-9]/gi, '-').toLowerCase();
pathParts.push(testFileName);

// Add test suite hierarchy and test name
const titleParts = testInfo.titlePath.map(part =>
part.replace(/[^a-z0-9]/gi, '-').toLowerCase()
);
pathParts.push(...titleParts);

const logDir = path.join(...pathParts);

if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}

this.browserName = testInfo.project.name;

return path.join(logDir, `${timestamp}.log`);
}

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

Use testInfo.outputPath and avoid shadowing; create per-test file under Playwright’s output

This ensures artifacts are colocated with the test run and avoids race conditions across workers.

Apply this diff:

-  private ensureLogDirectory(testInfo: TestInfo): string {
-    const outputDir = process.env.TEST_RESULTS_DIR || 'test-results';
-    const browserName = testInfo.project.name.replace(/[^a-z0-9]/gi, '-').toLowerCase();
-    const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
+  private ensureLogFile(testInfo: TestInfo): string {
+    const browserName = testInfo.project.name.replace(/[^a-z0-9]/gi, '-').toLowerCase();
+    const timeStr = new Date().toISOString().replace(/[:.]/g, '-');
 
-    // Build simple hierarchy: logs/browser/test-file/suite/test-name/
-    const pathParts = [outputDir, 'logs', browserName];
+    // logs/<browser>/<test-file>/<suite>/<test-name>/<timestamp>.log
+    const pathParts = ['logs', browserName];
 
     // Add test file name
-    const testFileName = path.basename(testInfo.file, '.spec.ts')
+    const testFileName = path.parse(testInfo.file).name
       .replace(/[^a-z0-9]/gi, '-').toLowerCase();
     pathParts.push(testFileName);
 
     // Add test suite hierarchy and test name
     const titleParts = testInfo.titlePath.map(part => 
       part.replace(/[^a-z0-9]/gi, '-').toLowerCase()
     );
     pathParts.push(...titleParts);
 
-    const logDir = path.join(...pathParts);
-
-    if (!fs.existsSync(logDir)) {
-      fs.mkdirSync(logDir, { recursive: true });
-    }
-
     this.browserName = testInfo.project.name;
-
-    return path.join(logDir, `${timestamp}.log`);
+    const rel = path.join(...pathParts, `${timeStr}.log`);
+    const full = testInfo.outputPath(rel);
+    const dir = path.dirname(full);
+    if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
+    return full;
   }
📝 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
private ensureLogDirectory(testInfo: TestInfo): string {
const outputDir = process.env.TEST_RESULTS_DIR || 'test-results';
const browserName = testInfo.project.name.replace(/[^a-z0-9]/gi, '-').toLowerCase();
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
// Build simple hierarchy: logs/browser/test-file/suite/test-name/
const pathParts = [outputDir, 'logs', browserName];
// Add test file name
const testFileName = path.basename(testInfo.file, '.spec.ts')
.replace(/[^a-z0-9]/gi, '-').toLowerCase();
pathParts.push(testFileName);
// Add test suite hierarchy and test name
const titleParts = testInfo.titlePath.map(part =>
part.replace(/[^a-z0-9]/gi, '-').toLowerCase()
);
pathParts.push(...titleParts);
const logDir = path.join(...pathParts);
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
this.browserName = testInfo.project.name;
return path.join(logDir, `${timestamp}.log`);
}
private ensureLogFile(testInfo: TestInfo): string {
const browserName = testInfo.project.name.replace(/[^a-z0-9]/gi, '-').toLowerCase();
const timeStr = new Date().toISOString().replace(/[:.]/g, '-');
// logs/<browser>/<test-file>/<suite>/<test-name>/<timestamp>.log
const pathParts = ['logs', browserName];
// Add test file name
const testFileName = path.parse(testInfo.file).name
.replace(/[^a-z0-9]/gi, '-').toLowerCase();
pathParts.push(testFileName);
// Add test suite hierarchy and test name
const titleParts = testInfo.titlePath.map(part =>
part.replace(/[^a-z0-9]/gi, '-').toLowerCase()
);
pathParts.push(...titleParts);
const rel = path.join(...pathParts, `${timeStr}.log`);
const full = testInfo.outputPath(rel);
const dir = path.dirname(full);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
this.browserName = testInfo.project.name;
return full;
}
🤖 Prompt for AI Agents
In e2e/utils/logger.ts around lines 41-69, the current ensureLogDirectory builds
its own TEST_RESULTS_DIR, shadows Playwright output handling and risks races;
change it to use testInfo.outputPath as the base output directory (do not read
process.env.TEST_RESULTS_DIR), keep building the
logs/browser/test-file/suite/name hierarchy under that path, avoid reusing
variable names that shadow external state (e.g., do not overwrite testInfo or
other globals), ensure the directory is created with fs.mkdirSync(..., {
recursive: true }) and set this.browserName without clobbering testInfo, then
return the full per-test log file path inside testInfo.outputPath (e.g.,
path.join(testInfo.outputPath, 'logs', browserName, ..., `${timestamp}.log`)).

Comment thread e2e/utils/logger.ts
Comment on lines +71 to +94
attachToTest(testInfo: TestInfo): void {
this.testInfo = testInfo;
this.logFilePath = this.ensureLogDirectory(testInfo);

const existingFileTransport = this.logger.transports.find(
t => t instanceof winston.transports.File
);
if (existingFileTransport) {
this.logger.remove(existingFileTransport);
}

this.logger.add(
new winston.transports.File({
filename: this.logFilePath,
format: this.logger.format
})
);

testInfo.attachments.push({
name: 'test-logs',
path: this.logFilePath,
contentType: 'text/plain'
});
}

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

Use testInfo.attach and make attachToTest async

Directly mutating attachments is brittle; use the API method.

Apply this diff:

-  attachToTest(testInfo: TestInfo): void {
+  async attachToTest(testInfo: TestInfo): Promise<void> {
     this.testInfo = testInfo;
-    this.logFilePath = this.ensureLogDirectory(testInfo);
+    this.logFilePath = this.ensureLogFile(testInfo);
@@
-    testInfo.attachments.push({
-      name: 'test-logs',
-      path: this.logFilePath,
-      contentType: 'text/plain'
-    });
+    await testInfo.attach('test-logs', {
+      path: this.logFilePath!,
+      contentType: 'text/plain',
+    });

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In e2e/utils/logger.ts around lines 71 to 94, attachToTest currently mutates
testInfo.attachments directly and is synchronous; change it to async and use the
TestInfo.attach API instead. Keep the existing logic to remove any existing
winston File transport and add a new File transport pointing to the created
logFilePath, then call await testInfo.attach('test-logs', { path:
this.logFilePath, contentType: 'text/plain' }) (or use a stream/buffer as
supported) rather than pushing into testInfo.attachments; ensure the function
signature becomes async and any callers are updated/await the call.

Comment thread e2e/utils/logger.ts
Comment on lines +114 to +142
log(message: string, meta?: any): void {
this.logger.info(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}

info(message: string, meta?: any): void {
this.logger.info(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}

warn(message: string, meta?: any): void {
this.logger.warn(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}

error(message: string | Error, meta?: any): void {
const msg = message instanceof Error ? message.message : message;
const errorMeta = message instanceof Error ? { stack: message.stack, ...meta } : meta;
this.logger.error(msg, { testPath: this.getTestPath(), browser: this.browserName, ...errorMeta });
}

debug(message: string, meta?: any): void {
this.logger.debug(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}

verbose(message: string, meta?: any): void {
this.logger.verbose(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}

http(message: string, meta?: any): void {
this.logger.http(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}

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

Replace any with typed metadata

Comply with our TS guideline to avoid any.

Apply this diff:

-  log(message: string, meta?: any): void {
+  log(message: string, meta?: Record<string, unknown>): void {
     this.logger.info(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
   }
 
-  info(message: string, meta?: any): void {
+  info(message: string, meta?: Record<string, unknown>): void {
     this.logger.info(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
   }
 
-  warn(message: string, meta?: any): void {
+  warn(message: string, meta?: Record<string, unknown>): void {
     this.logger.warn(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
   }
 
-  error(message: string | Error, meta?: any): void {
+  error(message: string | Error, meta?: Record<string, unknown>): void {
     const msg = message instanceof Error ? message.message : message;
     const errorMeta = message instanceof Error ? { stack: message.stack, ...meta } : meta;
     this.logger.error(msg, { testPath: this.getTestPath(), browser: this.browserName, ...errorMeta });
   }
 
-  debug(message: string, meta?: any): void {
+  debug(message: string, meta?: Record<string, unknown>): void {
     this.logger.debug(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
   }
 
-  verbose(message: string, meta?: any): void {
+  verbose(message: string, meta?: Record<string, unknown>): void {
     this.logger.verbose(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
   }
 
-  http(message: string, meta?: any): void {
+  http(message: string, meta?: Record<string, unknown>): void {
     this.logger.http(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
   }
📝 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
log(message: string, meta?: any): void {
this.logger.info(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}
info(message: string, meta?: any): void {
this.logger.info(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}
warn(message: string, meta?: any): void {
this.logger.warn(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}
error(message: string | Error, meta?: any): void {
const msg = message instanceof Error ? message.message : message;
const errorMeta = message instanceof Error ? { stack: message.stack, ...meta } : meta;
this.logger.error(msg, { testPath: this.getTestPath(), browser: this.browserName, ...errorMeta });
}
debug(message: string, meta?: any): void {
this.logger.debug(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}
verbose(message: string, meta?: any): void {
this.logger.verbose(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}
http(message: string, meta?: any): void {
this.logger.http(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}
log(message: string, meta?: Record<string, unknown>): void {
this.logger.info(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}
info(message: string, meta?: Record<string, unknown>): void {
this.logger.info(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}
warn(message: string, meta?: Record<string, unknown>): void {
this.logger.warn(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}
error(message: string | Error, meta?: Record<string, unknown>): void {
const msg = message instanceof Error ? message.message : message;
const errorMeta = message instanceof Error ? { stack: message.stack, ...meta } : meta;
this.logger.error(msg, { testPath: this.getTestPath(), browser: this.browserName, ...errorMeta });
}
debug(message: string, meta?: Record<string, unknown>): void {
this.logger.debug(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}
verbose(message: string, meta?: Record<string, unknown>): void {
this.logger.verbose(message, { testPath: this.getTestTestPath(), browser: this.browserName, ...meta });
}
http(message: string, meta?: Record<string, unknown>): void {
this.logger.http(message, { testPath: this.getTestPath(), browser: this.browserName, ...meta });
}

Comment thread e2e/utils/logger.ts
Comment on lines +154 to +205
export function interceptConsole(logger: TestLogger): () => void {
const originalConsole = {
log: console.log,
info: console.info,
warn: console.warn,
error: console.error,
debug: console.debug,
};

console.log = (...args: any[]) => {
const message = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
).join(' ');
logger.log(message);
};

console.info = (...args: any[]) => {
const message = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
).join(' ');
logger.info(message);
};

console.warn = (...args: any[]) => {
const message = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
).join(' ');
logger.warn(message);
};

console.error = (...args: any[]) => {
const message = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
).join(' ');
logger.error(message);
};

console.debug = (...args: any[]) => {
const message = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
).join(' ');
logger.debug(message);
};

return () => {
console.log = originalConsole.log;
console.info = originalConsole.info;
console.warn = originalConsole.warn;
console.error = originalConsole.error;
console.debug = originalConsole.debug;
};
} No newline at end of file

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

Type-safe console interception and robust serialization

Avoid any, handle circular structures with inspect.

Apply this diff:

-export function interceptConsole(logger: TestLogger): () => void {
+export function interceptConsole(logger: TestLogger): () => void {
   const originalConsole = {
@@
-  console.log = (...args: any[]) => {
-    const message = args.map(arg => 
-      typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
-    ).join(' ');
+  const fmt = (args: unknown[]) =>
+    args.map(arg => typeof arg === 'string' ? arg : inspect(arg, { depth: 5, colors: false, compact: false })).join(' ');
+
+  console.log = (...args: unknown[]) => {
+    const message = fmt(args);
     logger.log(message);
   };
 
-  console.info = (...args: any[]) => {
-    const message = args.map(arg => 
-      typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
-    ).join(' ');
+  console.info = (...args: unknown[]) => {
+    const message = fmt(args);
     logger.info(message);
   };
 
-  console.warn = (...args: any[]) => {
-    const message = args.map(arg => 
-      typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
-    ).join(' ');
+  console.warn = (...args: unknown[]) => {
+    const message = fmt(args);
     logger.warn(message);
   };
 
-  console.error = (...args: any[]) => {
-    const message = args.map(arg => 
-      typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
-    ).join(' ');
+  console.error = (...args: unknown[]) => {
+    const message = fmt(args);
     logger.error(message);
   };
 
-  console.debug = (...args: any[]) => {
-    const message = args.map(arg => 
-      typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
-    ).join(' ');
+  console.debug = (...args: unknown[]) => {
+    const message = fmt(args);
     logger.debug(message);
   };

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In e2e/utils/logger.ts around lines 154 to 205, the console interception uses
`any` and JSON.stringify which can throw on circular structures; replace `any`
with appropriate tuple typing (e.g., (...args: unknown[]) => void) and use
Node's util.inspect to serialize arguments (inspect(arg, { depth: null, colors:
false })) for each arg, joining them with spaces, so circular references are
handled safely; keep the originalConsole capture and restore logic, and ensure
the logger methods are called with the inspected string.

@pujitm

pujitm commented Sep 8, 2025

Copy link
Copy Markdown
Member Author

Moving to another repo

@pujitm pujitm closed this Sep 8, 2025
@pujitm pujitm deleted the test/notif-styles branch September 8, 2025 13:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants