e2e-test/ui/playwright/README.md
Playwright-based end-to-end tests for DataHub, targeting Chromium only.
e2e-test/ui/playwright/
├── playwright.config.ts # Playwright configuration
├── fixtures/ # Modular test fixtures (composed via mergeTests)
│ ├── base-test.ts # Import for authenticated tests (most tests)
│ ├── login-test.ts # Import for login UI tests (unauthenticated)
│ ├── login.fixture.ts # Per-worker auth state management
│ ├── logger.fixture.ts # Winston logger (auto-injected into every test)
│ ├── mocking.fixture.ts # apiMock fixture for route interception
│ ├── seeding.fixture.ts # Per-worker test data injection (seedingFixture)
│ └── login.ts # Auth file path helpers + GMS token reader
├── pages/ # Page Object Models
│ ├── base.page.ts # Base class: screenshot helper, logger/logDir
│ ├── login.page.ts
│ ├── search.page.ts
│ ├── dataset.page.ts
│ ├── business-attribute.page.ts
│ ├── welcome-modal.page.ts
│ ├── incidents.page.ts
│ └── common/
│ ├── searchbar-component.ts
│ └── sidebar-component.ts
├── tests/ # Test specs, organised by feature
│ ├── auth/
│ ├── login-v2/
│ ├── onboarding/
│ ├── search/
│ ├── business-attributes/
│ └── incidents-v2/
├── helpers/ # Standalone utility classes
│ ├── cleanup-helper.ts # CleanupHelper / GlobalCleanupHelper
│ ├── graphql-helper.ts # GraphQL request helpers (executeQuery, waitForGraphQLResponse)
│ ├── seeder-utils.ts # Shared extractUrn + waitForSync utilities
│ ├── wait-helper.ts # Wait strategies
│ └── seeders/ # Seeder implementations (see seeders/README.md)
│ ├── cli-seeder.ts # Seed via DataHub CLI (requires datahub CLI on $PATH)
│ ├── graphql-seeder.ts # Seed via GraphQL API
│ └── rest-seeder.ts # Seed via GMS REST API
├── factories/ # Data generation
│ ├── test-data-factory.ts # URN builders + timestamped name generators
│ ├── mock-response-factory.ts # Re-exports from mock-responses/ (backward compat)
│ └── mock-responses/ # Per-API GraphQL mock response builders
│ ├── common.ts # Shared types + generic error/success builders
│ ├── search.ts # Search response builders
│ ├── dataset.ts # Dataset response builders
│ └── index.ts # Barrel re-export
├── data/
│ └── users.ts # Test user definitions (users)
└── utils/ # Shared constants and utilities
├── constants.ts # Timeouts, routes, entity types, data sources
├── cleanup.ts # ScopedCleanup + deleteEntities helpers
├── test-data.ts # RestFeatureDataLoader for on-demand data injection
├── logger.ts # createLogger factory (Winston)
├── api-mock.ts # PageApiMocker class
└── random.ts # withTimestamp, withRandomSuffix helpers
http://localhost:9002 (or set BASE_URL)cd e2e-test/ui/playwright
yarn install
yarn playwright install chromium
# All tests
yarn playwright test
# Headed (see browser)
yarn playwright test --headed
# Interactive UI mode
yarn playwright test --ui
# Debug mode (step through)
yarn playwright test --debug
# Specific suite
yarn playwright test tests/search/
yarn playwright test tests/business-attributes/
# Specific test file
yarn playwright test tests/search/search-filters.spec.ts
# Specific test by name
yarn playwright test --grep "should login successfully"
# View HTML report after a run
yarn playwright show-report
| Test type | Import |
|---|---|
| Needs an authenticated session (search, entities, business-attributes, etc.) | ../../fixtures/base-test |
| Tests the login UI itself (unauthenticated context) | ../../fixtures/login-test |
Never import directly from @playwright/test in spec files.
Authentication is handled automatically per worker by loginFixture.
No explicit login step is needed in beforeEach.
import { test, expect } from '../../fixtures/base-test';
import { SearchPage } from '../../pages/search-page';
test.describe('Search', () => {
let searchPage: SearchPage;
test.beforeEach(async ({ page, logger, logDir }) => {
searchPage = new SearchPage(page, logger, logDir);
await searchPage.navigateToHome();
});
test('should return results for wildcard query', async () => {
await searchPage.searchAndWait('*', 3000);
await searchPage.expectHasResults();
});
});
import { test, expect } from '../../fixtures/login-test';
test.describe('Login', () => {
test('should reject invalid credentials', async ({ loginPage }) => {
await loginPage.navigateToLogin();
await loginPage.login('bad', 'creds');
await loginPage.expectLoginError();
});
});
Destructure apiMock to activate route interception for a test:
import { test, expect } from '../../fixtures/base-test';
test('renders with feature flag enabled', async ({ page, apiMock }) => {
await apiMock.setFeatureFlags({ themeV2Enabled: true });
await page.goto('/');
// ...
});
base-test defaults to the admin user. Override per suite:
import { test } from '../../fixtures/base-test';
import { users } from '../../fixtures/users';
test.use({ user: users.reader });
test('reader cannot edit', async ({ page }) => {
/* ... */
});
Page objects live in pages/ and extend BasePage. All constructors accept
optional logger and logDir for structured logging and screenshots.
import { Page, Locator } from '@playwright/test';
import { BasePage } from './base-page';
import { DataHubLogger } from '../utils/logger';
export class FeaturePage extends BasePage {
readonly submitButton: Locator;
constructor(page: Page, logger?: DataHubLogger, logDir?: string) {
super(page, logger, logDir);
this.submitButton = page.locator('[data-testid="feature-submit"]');
}
async submit(): Promise<void> {
await this.submitButton.click();
}
}
Rules:
readonly properties, never inline in spec files.beforeEach, not registered as fixtures.data-testid attributes first; role selectors second; text selectors as a last resort.Test data is injected via seedingFixture, which mirrors the loginFixture pattern — state is tracked in a flag file on disk so seeding only happens once per worker per feature per run.
featureName at the describe level..seeded/{featureName}.json. If present → skip.tests/{featureName}/fixtures/data.json, POSTs each MCP to the GMS REST API, writes the state file.import { test, expect } from '../../fixtures/base-test';
// Set at file/describe level — seeds from tests/search/fixtures/data.json
test.use({ featureName: 'search' });
test.describe('Search', () => {
test('results exist', async ({ page }) => {
// Data is guaranteed to be present
});
});
tests/{featureName}/fixtures/data.json ← MCP array (proposedSnapshot format)
.seeded/{featureName}.json ← auto-generated state file (gitignored)
PW_NO_SEED=1 npx playwright test
rm -rf .seeded/
loginFixture (in fixtures/login.fixture.ts) overrides Playwright's built-in
context fixture. On first run for a worker it performs a full UI login and
saves the session to .auth/{username}.json. On subsequent runs it reuses the
saved file — no round-trip to the server.
base-test composes loginFixture via mergeTests, so every test that
imports from base-test gets an authenticated page automatically.
login-test composes the logger and mocking fixtures but intentionally does
not include loginFixture, giving tests a clean unauthenticated context.
logger and logDir fixtures are auto-injected into every test (they use
auto: true). In CI, logs are written to per-test files under
test-results/<test>/logs/. Page objects receive logger and logDir via their
constructor and use them for structured output and screenshots.
Use TestDataFactory to build URNs and withTimestamp / withRandomSuffix
from utils/random.ts for unique, time-sortable test data names:
import { TestDataFactory } from '../../factories/test-data-factory';
const name = TestDataFactory.generateTestDatasetName();
// → 'test_dataset_20260409_143022'
const urn = TestDataFactory.createDatasetUrn('hive', name);
// → 'urn:li:dataset:(urn:li:dataPlatform:hive,test_dataset_20260409_143022,PROD)'
Typed GraphQL response builders live in factories/mock-responses/. Import
the named functions directly, or from the barrel index.ts:
import { createSearchResponse } from '../../factories/mock-responses/search';
import { createErrorResponse } from '../../factories/mock-responses/common';
const ok = createSearchResponse([], 0);
// → { status: 200, body: { data: { search: { searchResults: [], total: 0 } } } }
const err = createErrorResponse('not found', 'NOT_FOUND');
// → { status: 200, body: { errors: [{ message: "not found", ... }] } }
Add new per-API builders in the appropriate sub-module under factories/mock-responses/.
For test-scoped cleanup, use CleanupHelper. For broad pre-run cleanup of all
test entities, use GlobalCleanupHelper (setup scripts only):
import { CleanupHelper } from '../../helpers/cleanup-helper';
test.afterAll(async ({ page }) => {
const cleanup = new CleanupHelper(page);
await cleanup.deleteEntities(createdUrns);
});
namePattern is mandatory in deleteEntitiesByType to prevent accidental
deletion of all entities of a type.
test-results/junit.xml.workers: 1) in CI for stable ordering.