| name | playwright-test-generation |
| description | Generate Playwright automated test scripts for the Talosix EDC platform. Uses Page Object Model, data-testid selectors, and covers clinical trial UI patterns including forms, validation, and e-signatures. |
| allowed-tools | Read, Grep, Glob, Bash |
Playwright Test Generation
Purpose
Generate production-quality Playwright automated test scripts for the Talosix EDC platform. Tests follow the Page Object Model pattern, use data-testid selectors, and cover both happy path and error scenarios for clinical trial workflows.
Context: Talosix EDC UI Patterns
The Talosix EDC frontend presents clinical trial-specific UI patterns that require specialized testing approaches:
- CRF Forms: Multi-section forms with conditional fields, repeating groups, edit checks firing on blur/save.
- Validation Messages: Inline field errors, form-level warnings, and query-generating violations.
- E-Signature Dialogs: Modal requiring username + password + meaning selection, per 21 CFR Part 11.
- Audit Trail Viewer: Expandable history panel showing change history for each field.
- Query Panels: Side panels for viewing, responding to, and managing data queries.
- Role-Based UI: Features and actions shown/hidden based on user role and study permissions.
- Data Tables: Paginated, sortable, filterable tables for subjects, visits, queries, etc.
- Wizards: Multi-step configuration wizards for study build.
Workflow
- Analyze the feature: Read the source code, component files, and/or Jira story to understand the UI behavior.
- Identify existing page objects: Search the test directory for reusable page objects and utilities.
- Design test structure: Plan the test file with describe blocks for happy path and error scenarios.
- Generate page objects (if needed) following the existing POM pattern.
- Write test cases with proper assertions, waits, and cleanup.
- Add test data fixtures for deterministic execution.
Project Structure Convention
tests/
e2e/
pages/ # Page Object Models
login.page.ts
dashboard.page.ts
crf-form.page.ts
query-inbox.page.ts
subject-list.page.ts
...
fixtures/ # Test data and setup utilities
test-data.ts
auth.setup.ts
study.setup.ts
specs/ # Test specifications
auth/
login.spec.ts
session.spec.ts
crf/
data-entry.spec.ts
edit-checks.spec.ts
queries/
query-workflow.spec.ts
...
utils/ # Shared helpers
api-helpers.ts
date-helpers.ts
wait-helpers.ts
Before generating, use Glob and Grep to discover the actual project structure and conventions.
Page Object Model Pattern
Every page object must follow this structure:
import { type Page, type Locator, expect } from '@playwright/test';
export class CrfFormPage {
readonly page: Page;
readonly formTitle: Locator;
readonly saveButton: Locator;
readonly submitButton: Locator;
readonly fieldError: (fieldName: string) => Locator;
readonly formField: (fieldName: string) => Locator;
constructor(page: Page) {
this.page = page;
this.formTitle = page.getByTestId('crf-form-title');
this.saveButton = page.getByTestId('crf-save-btn');
this.submitButton = page.();
. =
page.();
. =
page.();
}
() {
..(
);
..({ : });
}
() {
field = .(fieldName);
field.();
field.(value);
field.();
}
() {
..();
..().({
: ,
});
}
() {
(.(fieldName)).(message);
}
() {
(.(fieldName))..();
}
}
Page Object Rules
- All selectors use
data-testid attributes. Never use CSS classes, tag names, or XPath for primary selectors.
- Use
getByTestId(), getByRole(), getByLabel(), and getByText() in order of preference.
- Encapsulate navigation, actions, and assertions as page object methods.
- Page objects do not contain test logic (no
test() blocks).
- Return
this from action methods to allow chaining where appropriate.
- Include
waitFor calls inside page object methods so tests do not need manual waits.
Test File Pattern
import { test, expect } from '@playwright/test';
import { CrfFormPage } from '../pages/crf-form.page';
import { LoginPage } from '../pages/login.page';
import { testData } from '../fixtures/test-data';
test.describe('CRF Data Entry - Lab Values', () => {
let crfForm: CrfFormPage;
test.beforeEach(async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.loginAs(testData.users.siteCoordinator);
crfForm = new CrfFormPage(page);
await crfForm.navigate(
testData.subjects.subject001,
testData.visits.visit2,
testData.forms.labResults
);
});
test.describe('Happy Path', () => {
test('should save valid lab values', async () => {
await crfForm.fillField(, );
crfForm.(, );
crfForm.();
crfForm.();
crfForm.();
});
(, ({ page }) => {
crfForm.(, );
crfForm.();
crfForm.(
testData..,
testData..,
testData..
);
(crfForm.()).();
});
});
test.(, {
(, () => {
crfForm.(, );
crfForm.(
,
);
});
(, () => {
crfForm.(, );
crfForm.();
crfForm.(, );
});
(, () => {
crfForm.(, );
crfForm.(
,
);
});
});
});
Clinical Trial UI Testing Patterns
E-Signature Testing
async signRecord(username: string, password: string, meaning: string) {
await this.page.getByTestId('sign-btn').click();
await this.page.getByTestId('esig-dialog').waitFor({ state: 'visible' });
await this.page.getByTestId('esig-username').fill(username);
await this.page.getByTestId('esig-password').fill(password);
await this.page.getByTestId('esig-meaning').selectOption(meaning);
await this.page.getByTestId('esig-confirm').click();
await this.page.getByTestId('esig-dialog').({ : });
}
Test scenarios for e-signatures:
- Successful signature with valid credentials.
- Rejected signature with wrong password.
- Signature invalidated after record modification.
- Signature meaning recorded in audit trail.
- Concurrent signature attempt by another user.
Audit Trail Verification
async verifyAuditEntry(fieldName: string, expectedValues: {
user: string;
action: string;
oldValue?: string;
newValue: string;
}) {
await this.page.getByTestId(`audit-toggle-${fieldName}`).click();
const auditPanel = this.page.getByTestId(`audit-panel-${fieldName}`);
await auditPanel.waitFor({ state: 'visible' });
const latestEntry = auditPanel.getByTestId('audit-entry').first();
await expect(latestEntry.getByTestId('audit-user')).toContainText(expectedValues.user);
await expect(latestEntry.getByTestId('audit-action')).toContainText(expectedValues.action);
await expect(latestEntry.getByTestId('audit-new-value')).toContainText(expectedValues.newValue);
(expectedValues.) {
(latestEntry.()).(expectedValues.);
}
}
Query Workflow Testing
async openQuery(fieldName: string, queryText: string) {
await this.page.getByTestId(`query-icon-${fieldName}`).click();
await this.page.getByTestId('query-panel').waitFor({ state: 'visible' });
await this.page.getByTestId('query-text-input').fill(queryText);
await this.page.getByTestId('query-submit-btn').click();
await expect(this.page.getByTestId('query-status')).toHaveText('Open');
}
Data Table Testing
async verifyTableRow(rowIndex: number, expectedData: Record<string, string>) {
const row = this.page.getByTestId('data-table-row').nth(rowIndex);
for (const [column, value] of Object.entries(expectedData)) {
await expect(row.getByTestId(`cell-${column}`)).toHaveText(value);
}
}
async sortByColumn(columnName: string) {
await this.page.getByTestId(`column-header-${columnName}`).click();
}
async filterTable(filterField: string, filterValue: string) {
await this.page.getByTestId(`filter-${filterField}`).fill(filterValue);
await ..().();
..().({ : });
}
Role-Based Access Testing
test.describe('Role-Based Access', () => {
test('site user cannot access data manager functions', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.loginAs(testData.users.siteCoordinator);
await expect(page.getByTestId('nav-db-lock')).not.toBeVisible();
await expect(page.getByTestId('nav-data-export')).not.toBeVisible();
await page.goto('/admin/database-lock');
await expect(page.getByTestId('access-denied')).toBeVisible();
});
});
Test Data Management
export const testData = {
users: {
siteCoordinator: { username: 'site_coord_01', password: 'TestPass123!', role: 'Site Coordinator' },
cra: { username: 'cra_01', password: 'TestPass123!', role: 'CRA' },
dataManager: { username: 'dm_01', password: 'TestPass123!', role: 'Data Manager' },
},
subjects: {
subject001: 'SUBJ-001',
subject002: 'SUBJ-002',
},
visits: {
screening: 'SCR',
visit1: 'V1',
visit2: 'V2',
},
forms: {
demographics: 'DM',
labResults: 'LB',
adverseEvents: 'AE',
},
} as const;
- Use deterministic test data, never random values.
- Clean up test data in
afterEach or use isolated test contexts.
- Use API calls in
beforeAll/beforeEach for fast setup instead of UI-driven setup.
- Store authentication state to avoid repeated login flows.
Selector Strategy
Priority order for selectors:
data-testid -- primary strategy, most stable.
getByRole() -- for accessible elements (buttons, links, headings).
getByLabel() -- for form fields with proper labels.
getByText() -- for static text content, use sparingly.
Never use:
- CSS class selectors (brittle, change with styling).
- XPath (hard to maintain).
- nth-child or positional selectors without testid context.
When generating tests, if a data-testid is not found in the source code, note it as a required addition and use the expected testid name following the convention: [component]-[element]-[qualifier].
Guidelines
- Use
async/await consistently. Never use .then() chains.
- Prefer
expect auto-retrying assertions over manual waitFor + assert.
- Set reasonable timeouts for clinical operations that may be slow (e.g., form save, data export).
- Group related tests in
describe blocks.
- Each test must be independent. Use
beforeEach for setup, not test order dependency.
- Include
test.describe.configure({ mode: 'serial' }) only when tests genuinely depend on shared state (rare).
- Add JSDoc comments to page object methods describing the clinical workflow context.
- Tag tests with annotations:
test('name', { tag: ['@smoke', '@regression', '@regulatory'] }, ...).