| 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:
<platform>-parallel runs first (with up to N workers)
<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
npx playwright test --project=chromium-parallel --project=chromium-serial
npx playwright test --project=electron-parallel --project=electron-serial
npx playwright test --project=chromium-parallel
npx playwright test --project=chromium-serial --no-deps
npx playwright test
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
- Create folder under
tests/ (e.g., tests/my-feature/)
- Add project configuration in
playwright.config.ts:
{
name: 'my-feature',
testDir: './tests/my-feature',
fullyParallel: false,
workers: 1,
timeout: 120000,
}
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.
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');
}
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:
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;
test.beforeAll(async ({ apiHelper }) => {
database = await apiHelper.createDatabase({
name: 'test-feature-db',
host: standaloneConfig.host,
port: standaloneConfig.port,
});
});
test.afterAll(async ({ apiHelper }) => {
await apiHelper.deleteDatabase(database.id);
});
test.describe('Sub-feature', () => {
test.beforeEach(async ({ featurePage }) => {
await featurePage.goto(database.id);
});
test('should do something', async ({ featurePage }) => {
await featurePage.doAction();
await expect(featurePage.result).toBeVisible();
});
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
beforeAll - Create database/test data via API (runs once)
afterAll - Clean up database/test data via API (runs once)
beforeEach - Navigate to page via UI using goto(databaseId) (runs before each test)
- Individual tests - Receive page fixtures they need in the signature
- Page objects are stateless - Don't store database objects in pages, pass IDs to methods
Avoid These Anti-Patterns
const browserPage = createBrowserPage(database);
await browserPage.goto(database.id);
test('should work', async () => {
await browserPage.doSomething();
});
test('should work', async ({ browserPage }) => {
await browserPage.doSomething();
});
test('should work', async ({ browserPage }) => {
await browserPage.goto(database.id);
});
test.describe.serial('Feature', () => {
});
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,
}));
const config = ConfigFactory.build();
const config = ConfigFactory.build({ name: 'custom-name' });
Cleanup Pattern
Always prefix test data with test- for easy cleanup:
async deleteTestData(): Promise<number> {
return this.deleteByPattern(new RegExp(`^${TEST_PREFIX}`));
}
Fixtures
Add New Fixtures to 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
- Navigate to the page:
browser_navigate_Playwright to target URL
- Take snapshot:
browser_snapshot_Playwright to see element tree
- Interact with elements:
browser_click_Playwright to trigger dialogs, dropdowns, etc.
- Wait for async content:
browser_wait_for_Playwright for dynamic content
- 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:
this.addButton = page.getByTestId('btn-add-key');
this.submitButton = page.getByRole('button', { name: 'Submit' });
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();
await this.gotoDatabase(dbId);
Each page owns its navigation via its goto() method:
await settingsPage.goto();
await browserPage.goto(dbId);
await workbenchPage.goto(dbId);
await analyticsPage.goto(dbId);
await pubSubPage.goto(dbId);
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
test.beforeEach(async ({ browserPage }) => {
await browserPage.goto(database.id);
});
await browserPage.navigationTabs.gotoWorkbench();
❌ Incorrect Navigation Pattern
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
npx playwright test --project=chromium-parallel
npx playwright test --project=chromium-serial --no-deps
npx playwright test --project=electron-parallel
npx playwright test --project=electron-serial --no-deps
ENV=ci npx playwright test
ENV=staging npx playwright test
Code Quality (IMPORTANT)
Always run linter and type checker after making changes:
npm run lint
npm run 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);
});
});
2. Use Serial Only When Tests Truly Depend on Each Other
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)}`;
});
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.).