e2e-testing

star 8.5k

Playwright end-to-end testing standards for RedisInsight: page object models, test structure, fixtures, navigation patterns, and flaky-test prevention. Use when editing files under tests/e2e-playwright/**, writing Playwright tests, adding page objects, or when the user mentions Playwright, E2E tests, page objects, or end-to-end testing.

redis By redis schedule Updated 6/2/2026

name: e2e-testing description: >- Playwright end-to-end testing standards for RedisInsight: page object models, test structure, fixtures, navigation patterns, and flaky-test prevention. Use when editing files under tests/e2e-playwright/**, writing Playwright tests, adding page objects, or when the user mentions Playwright, E2E tests, page objects, or end-to-end testing.

E2E Testing Standards (Playwright)

Location

All E2E tests are in tests/e2e-playwright/. This is a standalone package - no imports from redisinsight/ui/ or redisinsight/api/.

Test Plan

Always refer to tests/e2e-playwright/TEST_PLAN.md for:

  • Test coverage status (✅ implemented, 🔲 not implemented)
  • Feature implementation order
  • Test data requirements

After implementing tests, update TEST_PLAN.md to mark tests as ✅.

Project Structure

tests/e2e-playwright/
├── TEST_PLAN.md         # Master test plan with coverage status
├── config/              # Configuration (env, databases)
│   └── databases/       # Database configs by type
├── fixtures/            # Playwright fixtures
├── helpers/             # API helpers for setup/teardown
├── pages/               # Page Object Models
│   ├── BasePage.ts      # Base class for all pages
│   ├── InstancePage.ts  # Base class for database instance pages
│   ├── components/      # Shared components (InstanceHeader, NavigationTabs, BottomPanel)
│   └── {feature}/       # Feature-specific pages (browser/, cli/, etc.)
├── test-data/           # Test data factories
├── tests/               # Test specs organized by project
│   ├── main/            # Default parallel tests
│   │   └── {feature}/
│   │       └── {action}/
│   ├── auto-update/     # Serial tests with special setup
│   └── electron/        # Electron-specific tests
└── types/               # TypeScript types

Playwright Projects

The folder a test lives in determines its execution mode. Each browser platform has a parallel project and a serial project:

Project Folder Parallelism Use Case
chromium-parallel tests/parallel/ Parallel (4 workers) Standard chromium tests
chromium-serial tests/serial/ Serial (1 worker) Sequential chromium tests
electron-parallel tests/parallel/ Serial (1 worker)* Standard electron tests
electron-serial tests/serial/ Serial (1 worker) Sequential electron tests

* Electron uses one worker today because there is a single app instance.

Execution order

For each platform, the serial project depends on the parallel project via dependencies: ['<platform>-parallel'] in playwright.config.ts, so the order is always:

  1. <platform>-parallel runs first (with up to N workers)
  2. <platform>-serial runs after, one worker at a time

Serial tests perform destructive operations on the shared RTE Redis (FLUSHDB, broad deleteAllIndexes, dangerous commands) so they can't safely run alongside parallel tests on the same RTE. Electron also can't run its two projects concurrently because the desktop app binds its embedded API on a fixed port (5530).

If serial grows large enough to become a CI bottleneck, the next step is to give serial tests a dedicated Redis instance (or split into a separate CI job) — not to revert the ordering.

Running Projects

# Full platform run (parallel + serial)
npx playwright test --project=chromium-parallel --project=chromium-serial
npx playwright test --project=electron-parallel --project=electron-serial

# Just parallel
npx playwright test --project=chromium-parallel

# Just serial — needs --no-deps, otherwise parallel runs first (it's a
# project dependency). --no-deps also skips browser-setup.
npx playwright test --project=chromium-serial --no-deps

npx playwright test                           # All projects

When to put a test in tests/serial/

Put a test in tests/serial/ when it:

  • Shares database state across tests via beforeAll
  • Runs dangerous commands or mutates global app state
  • Cannot tolerate concurrent execution with other tests
  • Would cause flakiness when run with other tests
  • Require special environment configuration

Adding a New Project

  1. Create folder under tests/ (e.g., tests/my-feature/)
  2. Add project configuration in playwright.config.ts:
{
  name: 'my-feature',
  testDir: './tests/my-feature',
  fullyParallel: false, // or true
  workers: 1,
  timeout: 120000,
  // Optional: different setup
  // globalSetup: './my-feature-setup.ts',
}

Page Objects

Page Object Hierarchy

BasePage (abstract)
  ├── DatabasesPage           # Databases list page
  ├── SettingsPage            # Settings page
  └── InstancePage (abstract) # Base for all database instance pages
        ├── instanceHeader    # Database name, stats, breadcrumb
        ├── navigationTabs    # Browse, Workbench, Analyze, Pub/Sub
        ├── bottomPanel       # CLI, Command Helper, Profiler
        └── BrowserPage       # Browser-specific (extends InstancePage)
              └── WorkbenchPage (future)
              └── AnalyzePage (future)
              └── PubSubPage (future)

Extend the Appropriate Base Class

  • BasePage - For standalone pages (DatabasesPage, SettingsPage)
  • InstancePage - For pages within a connected database (BrowserPage, WorkbenchPage, etc.)

Page objects are stateless - they don't store database objects. Pass databaseId to navigation methods.

// For database instance pages - extend InstancePage
import { Page, Locator } from '@playwright/test';
import { InstancePage } from '../InstancePage';

export class WorkbenchPage extends InstancePage {
  readonly editor: Locator;

  constructor(page: Page) {
    super(page);
    this.editor = page.getByTestId('workbench-editor');
  }

  // InstancePage provides: instanceHeader, navigationTabs, bottomPanel
  // Plus navigation methods: navigateToBrowser(), openCli(), etc.

  async goto(databaseId: string): Promise<void> {
    await this.gotoDatabase(databaseId);
    await this.navigationTabs.gotoWorkbench();
    await this.waitForLoad();
  }
}

Component-Based Structure

Break large pages into components:

// pages/feature/FeaturePage.ts
export class FeaturePage extends InstancePage {
  readonly dialog: FeatureDialog;
  readonly list: FeatureList;

  constructor(page: Page) {
    super(page);
    this.dialog = new FeatureDialog(page);
    this.list = new FeatureList(page);
  }
}

Test Structure

File Organization

tests/
├── main/                # Default parallel tests (most tests go here)
│   └── {feature}/       # e.g., databases, browser, workbench
│       └── {action}/    # e.g., add, edit, delete
│           ├── standalone.spec.ts
│           └── cluster.spec.ts
├── auto-update/         # Serial tests with special setup
└── electron/            # Electron-specific tests

Test Setup Pattern

Use simple, explicit setup with clear separation of concerns. Page objects are fixtures - they don't store database state. Pass databaseId to goto() methods.

import { test, expect } from '../../../fixtures/base';
import { standaloneConfig } from '../../../config/databases/standalone';
import { DatabaseInstance } from '../../../types';

test.describe('Feature > Action', () => {
  let database: DatabaseInstance;

  // Setup: Create database once for all tests
  test.beforeAll(async ({ apiHelper }) => {
    database = await apiHelper.createDatabase({
      name: 'test-feature-db',
      host: standaloneConfig.host,
      port: standaloneConfig.port,
    });
  });

  // Teardown: Clean up database after all tests
  test.afterAll(async ({ apiHelper }) => {
    await apiHelper.deleteDatabase(database.id);
  });

  test.describe('Sub-feature', () => {
    // Navigation: Pass databaseId to goto() - page is a fixture
    test.beforeEach(async ({ featurePage }) => {
      await featurePage.goto(database.id);
    });

    // Tests receive page fixtures they need
    test('should do something', async ({ featurePage }) => {
      await featurePage.doAction();
      await expect(featurePage.result).toBeVisible();
    });

    // Tests that need both page and apiHelper
    test('should create and verify', async ({ featurePage, apiHelper }) => {
      await apiHelper.createKey(database.id, 'test-key', 'value');
      await featurePage.refresh();
      await expect(featurePage.keyList).toContainText('test-key');
    });
  });
});

Key Principles

  1. beforeAll - Create database/test data via API (runs once)
  2. afterAll - Clean up database/test data via API (runs once)
  3. beforeEach - Navigate to page via UI using goto(databaseId) (runs before each test)
  4. Individual tests - Receive page fixtures they need in the signature
  5. Page objects are stateless - Don't store database objects in pages, pass IDs to methods

Avoid These Anti-Patterns

// ❌ BAD: Storing database in page object
const browserPage = createBrowserPage(database);  // OLD pattern - don't use

// ✅ GOOD: Pass databaseId to goto()
await browserPage.goto(database.id);

// ❌ BAD: Using page fixture without declaring it in test signature
test('should work', async () => {
  await browserPage.doSomething();  // browserPage is undefined!
});

// ✅ GOOD: Declare fixtures in test signature
test('should work', async ({ browserPage }) => {
  await browserPage.doSomething();
});

// ❌ BAD: Navigation inside each test
test('should work', async ({ browserPage }) => {
  await browserPage.goto(database.id);  // Should be in beforeEach
  // ...
});

// ❌ BAD: Using test.describe.serial when not needed
test.describe.serial('Feature', () => { // Use regular describe unless tests truly depend on each other
  // ...
});

Test Data

Use Fishery Factories with Faker

Use the fishery library for test data factories:

import { Factory } from 'fishery';
import { faker } from '@faker-js/faker';

export const TEST_PREFIX = 'test-';

export const ConfigFactory = Factory.define<Config>(() => ({
  name: `${TEST_PREFIX}${faker.string.alphanumeric(8)}`,
  host: '127.0.0.1',
  port: 6379,
}));

// Usage in tests
const config = ConfigFactory.build();
const config = ConfigFactory.build({ name: 'custom-name' });

Cleanup Pattern

Always prefix test data with test- for easy cleanup:

// In apiHelper
async deleteTestData(): Promise<number> {
  return this.deleteByPattern(new RegExp(`^${TEST_PREFIX}`));
}

Fixtures

Add New Fixtures to base.ts

// fixtures/base.ts
type Fixtures = {
  myPage: MyPage;
  apiHelper: ApiHelper;
};

export const test = base.extend<Fixtures>({
  myPage: async ({ page }, use) => {
    await use(new MyPage(page));
  },
  apiHelper: async ({}, use) => {
    const helper = new ApiHelper();
    await use(helper);
    await helper.dispose();
  },
});

UI Exploration with Playwright MCP

Before writing tests, ALWAYS use Playwright MCP to explore the UI:

Why Explore First?

  • Discover actual data-testid attributes used in the application
  • Understand element roles and accessible names for getByRole()
  • See page structure and component hierarchy
  • Avoid trial-and-error test writing

Exploration Workflow

  1. Navigate to the page: browser_navigate_Playwright to target URL
  2. Take snapshot: browser_snapshot_Playwright to see element tree
  3. Interact with elements: browser_click_Playwright to trigger dialogs, dropdowns, etc.
  4. Wait for async content: browser_wait_for_Playwright for dynamic content
  5. Document findings: Add discovered UI patterns to TEST_PLAN.md under the feature section

What to Look For

  • data-testid attributes → use with page.getByTestId()
  • Element roles (button, combobox, grid, treeitem) → use with page.getByRole()
  • Accessible names → use with { name: 'text' } option
  • Form field placeholders → use with page.getByPlaceholder()
  • Text content patterns → use with page.getByText()

Use Discovered Patterns in Page Objects

After exploring, use discovered patterns directly in Page Object locators:

// Use data-testid when available
this.addButton = page.getByTestId('btn-add-key');

// Use role + name for accessible elements
this.submitButton = page.getByRole('button', { name: 'Submit' });

// Use placeholder for form fields
this.searchInput = page.getByPlaceholder('Search...');

Note: Keep TEST_PLAN.md as a simple visual list of test cases. Document UI patterns in Page Object comments if needed.

Best Practices

✅ DO

  • Explore UI with Playwright MCP before writing tests
  • Use Page Object navigation methods (e.g., browserPage.goto(), workbenchPage.goto())
  • Use data-testid attributes for stable selectors
  • Use getByRole, getByLabel for accessible elements
  • Wait for elements with waitFor({ state: 'visible' })
  • Clean up test data in afterEach
  • Use API for setup, UI for assertions
  • Handle both List view and Tree view in key assertions

❌ DON'T

  • NEVER use page.goto() directly - tests must work in both browser and Electron
  • Write tests without exploring the actual UI first
  • Use fixed timeouts (waitForTimeout)
  • Use CSS selectors for dynamic content
  • Leave test data after tests
  • Import from redisinsight/ui/ or redisinsight/api/
  • Hardcode test data (use faker)
  • Assume element structure without verification

Navigation (IMPORTANT)

All navigation must use UI-based methods, NOT URL navigation.

Tests must work in both browser mode (http://localhost:8080) and Electron mode (no baseURL). Direct page.goto() calls fail in Electron because there's no baseURL.

Navigation Architecture

BasePage provides only fundamental navigation:

await this.gotoHome();              // Click Redis logo → databases list
await this.gotoDatabase(dbId);      // Click database → Browser page (default)

Each page owns its navigation via its goto() method:

await settingsPage.goto();           // Settings page
await browserPage.goto(dbId);        // Browser page for database
await workbenchPage.goto(dbId);      // Workbench page for database
await analyticsPage.goto(dbId);      // Analytics page for database
await pubSubPage.goto(dbId);         // Pub/Sub page for database

NavigationTabs component handles tab switching within a connected database:

await browserPage.navigationTabs.gotoBrowser();
await browserPage.navigationTabs.gotoWorkbench();
await browserPage.navigationTabs.gotoAnalyze();
await browserPage.navigationTabs.gotoPubSub();

✅ Correct Navigation Pattern

// Use Page Object's goto() method in beforeEach
test.beforeEach(async ({ browserPage }) => {
  await browserPage.goto(database.id);  // Navigates and waits for page load
});

// Switch tabs when already connected
await browserPage.navigationTabs.gotoWorkbench();

❌ Incorrect Navigation Pattern

// NEVER do this - fails in Electron
await page.goto(`/${database.id}/browser`);
await page.goto('/settings');
await page.goto('/');

Running Tests

Run these commands from the E2E package directory:

cd tests/e2e-playwright

npx playwright test                                              # All Playwright projects
npx playwright test --project=chromium-parallel                  # Chromium parallel tests
npx playwright test --project=chromium-serial --no-deps          # Chromium serial only (skips parallel + setup)
npx playwright test --project=electron-parallel                  # Electron parallel tests
npx playwright test --project=electron-serial --no-deps          # Electron serial only (skips parallel + setup)
ENV=ci npx playwright test                             # CI environment
ENV=staging npx playwright test                        # Staging environment

Code Quality (IMPORTANT)

Always run linter and type checker after making changes:

npm run lint                # ESLint check
npm run type-check          # TypeScript type check

Both must pass before committing. Common issues:

  • Unused variables/imports
  • Missing return types
  • any types (avoid when possible)
  • Null/undefined handling (use proper types like Promise<string | null>)

Test Isolation (IMPORTANT)

Tests should be isolated and not depend on execution order:

1. Shared Database with beforeAll/afterAll

test.describe('Feature Name', () => {
  let database: DatabaseInstance;

  test.beforeAll(async ({ apiHelper }) => {
    database = await apiHelper.createDatabase({ name: 'test-feature-db', ... });
  });

  test.afterAll(async ({ apiHelper }) => {
    await apiHelper.deleteDatabase(database.id);
  });

  // Tests can run in parallel - they share the database but don't modify shared state
});

2. Use Serial Only When Tests Truly Depend on Each Other

// Only use .serial when tests modify state that subsequent tests depend on
test.describe.serial('Workflow that modifies state', () => {
  test('step 1: create item', ...);
  test('step 2: modify item created in step 1', ...);
  test('step 3: delete item', ...);
});

3. Unique Test Data Per Test (when needed)

test('should create unique item', async ({ apiHelper }) => {
  const uniqueName = `test-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
  // Use uniqueName for this test's data
});

Feature-to-Path Mapping

Follow this naming convention for test and page object paths:

Feature Test Path Page Object Path
Database List tests/parallel/databases/list/ pages/databases/
Add Database tests/parallel/databases/add/ pages/databases/
Import Database tests/parallel/databases/import/ pages/databases/
Browser - Key List tests/parallel/browser/key-list/ pages/browser/
Browser - Add Key tests/parallel/browser/add-key/ pages/browser/
Browser - Key Details tests/parallel/browser/key-details/ pages/browser/
Workbench tests/parallel/workbench/ pages/workbench/
CLI tests/parallel/cli/ pages/cli/
Pub/Sub tests/parallel/pubsub/ pages/pubsub/
Slow Log tests/parallel/analytics/slow-log/ pages/analytics/
DB Analysis tests/parallel/analytics/analysis/ pages/analytics/
Settings tests/parallel/settings/ pages/settings/
Navigation tests/parallel/navigation/ pages/navigation/
Auto-Update tests/auto-update/ pages/ (shared)
Deep Links tests/electron/deep-links/ pages/ (shared)

Note: Most tests go in tests/parallel/. Only use other project folders for tests with special requirements (serial execution, different setup, etc.).

Install via CLI
npx skills add https://github.com/redis/RedisInsight --skill e2e-testing
Repository Details
star Stars 8,541
call_split Forks 467
navigation Branch main
article Path SKILL.md
More from Creator