| name | action-utilities |
| description | UIActions pattern for centralized Playwright interactions. Use when implementing clean page object interactions, creating reusable action classes for buttons, inputs, dropdowns, checkboxes, or building a centralized interaction gateway.
|
Action Utilities Skill
A comprehensive guide to implementing centralized action utilities (UIActions pattern) in Playwright for cleaner, more maintainable test automation.
What is the UIActions Pattern?
The UIActions pattern creates a single interaction gateway between your Page Objects and Playwright. Instead of scattering low-level Playwright calls (locator.click(), locator.fill()) throughout your codebase, all interactions flow through one unified, expressive interface.
Why Use UIActions?
| Problem | Solution with UIActions |
|---|
| Duplicated wait logic across tests | Centralized auto-wait handling |
| Inconsistent error handling | Unified error messages with context |
| Scattered retry logic | Single place for retry configuration |
| Hard to add logging/screenshots | One place to add cross-cutting concerns |
| Page Objects become bloated | Page Objects focus on "what", UIActions handles "how" |
Core Architecture
┌─────────────────────────────────────────────────────────┐
│ Test Files │
│ (describe what user does) │
└─────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Page Objects │
│ (map UI elements, define page actions) │
└─────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ UIActions │
│ (centralized interaction gateway - THE ONLY WAY │
│ Page Objects talk to Playwright) │
└─────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Specialized Action Classes │
│ EditBoxActions │ ButtonActions │ DropDownActions │ etc │
└─────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Playwright API │
└─────────────────────────────────────────────────────────┘
Implementation
1. Base Action Class
import { Page, Locator } from '@playwright/test';
export abstract class BaseAction {
protected page: Page;
protected defaultTimeout: number;
constructor(page: Page, timeout: number = 30000) {
this.page = page;
this.defaultTimeout = timeout;
}
protected async waitForVisible(locator: Locator, timeout?: number): Promise<void> {
await locator.waitFor({
state: 'visible',
timeout: timeout ?? this.defaultTimeout,
});
}
protected async waitForEnabled(locator: Locator, timeout?: number): Promise<void> {
await locator.waitFor({
state: 'attached',
timeout: timeout ?? this.defaultTimeout,
});
const isDisabled = await locator.isDisabled();
if (isDisabled) {
throw new Error(`Element is disabled: ${locator}`);
}
}
protected async scrollIntoView(locator: Locator): Promise<void> {
await locator.scrollIntoViewIfNeeded();
}
protected async highlight(locator: Locator): Promise<void> {
if (process.env.DEBUG_MODE === 'true') {
await locator.evaluate((el) => {
el.style.border = '3px solid red';
setTimeout(() => (el.style.border = ''), 2000);
});
}
}
protected log(action: string, details?: string): void {
if (process.env.DEBUG_MODE === 'true') {
console.log(`[UIAction] ${action}${details ? `: ${details}` : ''}`);
}
}
}
2. Specialized Action Classes
EditBoxActions (Text Inputs)
import { Page, Locator } from '@playwright/test';
import { BaseAction } from './BaseAction';
export class EditBoxActions extends BaseAction {
constructor(page: Page) {
super(page);
}
async fill(locator: Locator, value: string): Promise<void> {
this.log('Fill', `value: "${value}"`);
await this.waitForVisible(locator);
await this.scrollIntoView(locator);
await locator.fill(value);
}
async type(locator: Locator, value: string, delay: number = 50): Promise<void> {
this.log('Type', `value: "${value}", delay: ${delay}ms`);
await this.waitForVisible(locator);
await this.scrollIntoView(locator);
await locator.pressSequentially(value, { delay });
}
async clearAndFill(locator: Locator, value: string): Promise<void> {
this.log('ClearAndFill', `value: "${value}"`);
await this.waitForVisible(locator);
await this.scrollIntoView(locator);
await locator.clear();
await locator.fill(value);
}
async clear(locator: Locator): Promise<void> {
this.log('Clear');
await this.waitForVisible(locator);
await locator.clear();
}
async getValue(locator: Locator): Promise<string> {
await this.waitForVisible(locator);
return locator.inputValue();
}
async isEmpty(locator: Locator): Promise<boolean> {
const value = await this.getValue(locator);
return value.trim() === '';
}
async fillSensitive(locator: Locator, value: string): Promise<void> {
this.log('Fill', 'value: [MASKED]');
await this.waitForVisible(locator);
await this.scrollIntoView(locator);
await locator.fill(value);
}
}
ButtonActions (Clickable Elements)
import { Page, Locator } from '@playwright/test';
import { BaseAction } from './BaseAction';
export class ButtonActions extends BaseAction {
constructor(page: Page) {
super(page);
}
async click(locator: Locator): Promise<void> {
this.log('Click');
await this.waitForVisible(locator);
await this.waitForEnabled(locator);
await this.scrollIntoView(locator);
await locator.click();
}
async doubleClick(locator: Locator): Promise<void> {
this.log('DoubleClick');
await this.waitForVisible(locator);
await this.scrollIntoView(locator);
await locator.dblclick();
}
async rightClick(locator: Locator): Promise<void> {
this.log('RightClick');
await this.waitForVisible(locator);
await this.scrollIntoView(locator);
await locator.click({ button: 'right' });
}
async clickAndWaitForNavigation(locator: Locator): Promise<void> {
this.log('ClickAndWaitForNavigation');
await this.waitForVisible(locator);
await this.scrollIntoView(locator);
await Promise.all([
this.page.waitForNavigation(),
locator.click(),
]);
}
async clickAndWaitForNetworkIdle(locator: Locator): Promise<void> {
this.log('ClickAndWaitForNetworkIdle');
await this.waitForVisible(locator);
await locator.click();
await this.page.waitForLoadState('networkidle');
}
async forceClick(locator: Locator): Promise<void> {
this.log('ForceClick');
await locator.click({ force: true });
}
async hover(locator: Locator): Promise<void> {
this.log('Hover');
await this.waitForVisible(locator);
await locator.hover();
}
async isClickable(locator: Locator): Promise<boolean> {
try {
await locator.waitFor({ state: 'visible', timeout: 1000 });
const isEnabled = await locator.isEnabled();
return isEnabled;
} catch {
return false;
}
}
}
CheckboxActions
import { Page, Locator } from '@playwright/test';
import { BaseAction } from './BaseAction';
export class CheckboxActions extends BaseAction {
constructor(page: Page) {
super(page);
}
async check(locator: Locator): Promise<void> {
this.log('Check');
await this.waitForVisible(locator);
await this.scrollIntoView(locator);
await locator.check();
}
async uncheck(locator: Locator): Promise<void> {
this.log('Uncheck');
await this.waitForVisible(locator);
await this.scrollIntoView(locator);
await locator.uncheck();
}
async setChecked(locator: Locator, checked: boolean): Promise<void> {
this.log('SetChecked', `checked: ${checked}`);
await this.waitForVisible(locator);
await this.scrollIntoView(locator);
await locator.setChecked(checked);
}
async toggle(locator: Locator): Promise<void> {
this.log('Toggle');
const isChecked = await this.isChecked(locator);
await this.setChecked(locator, !isChecked);
}
async isChecked(locator: Locator): Promise<boolean> {
await this.waitForVisible(locator);
return locator.isChecked();
}
}
DropdownActions
import { Page, Locator } from '@playwright/test';
import { BaseAction } from './BaseAction';
export class DropdownActions extends BaseAction {
constructor(page: Page) {
super(page);
}
async selectByText(locator: Locator, text: string): Promise<void> {
this.log('SelectByText', `text: "${text}"`);
await this.waitForVisible(locator);
await this.scrollIntoView(locator);
await locator.selectOption({ label: text });
}
async selectByValue(locator: Locator, value: string): Promise<void> {
this.log('SelectByValue', `value: "${value}"`);
await this.waitForVisible(locator);
await this.scrollIntoView(locator);
await locator.selectOption({ value });
}
async selectByIndex(locator: Locator, index: number): Promise<void> {
this.log('SelectByIndex', `index: ${index}`);
await this.waitForVisible(locator);
await this.scrollIntoView(locator);
await locator.selectOption({ index });
}
async selectMultiple(locator: Locator, values: string[]): Promise<void> {
this.log('SelectMultiple', `values: ${values.join(', ')}`);
await this.waitForVisible(locator);
await locator.selectOption(values);
}
async getSelectedText(locator: Locator): Promise<string> {
await this.waitForVisible(locator);
return locator.evaluate((select: HTMLSelectElement) => {
return select.options[select.selectedIndex]?.text ?? '';
});
}
async getSelectedValue(locator: Locator): Promise<string> {
await this.waitForVisible(locator);
return locator.inputValue();
}
async getAllOptions(locator: Locator): Promise<string[]> {
await this.waitForVisible(locator);
return locator.evaluate((select: HTMLSelectElement) => {
return Array.from(select.options).map(option => option.text);
});
}
}
UIElementActions (Generic Elements)
import { Page, Locator } from '@playwright/test';
import { BaseAction } from './BaseAction';
export class UIElementActions extends BaseAction {
constructor(page: Page) {
super(page);
}
async getText(locator: Locator): Promise<string> {
await this.waitForVisible(locator);
return (await locator.textContent()) ?? '';
}
async getInnerText(locator: Locator): Promise<string> {
await this.waitForVisible(locator);
return locator.innerText();
}
async getAttribute(locator: Locator, attribute: string): Promise<string | null> {
await this.waitForVisible(locator);
return locator.getAttribute(attribute);
}
async isVisible(locator: Locator, timeout?: number): Promise<boolean> {
try {
await locator.waitFor({ state: 'visible', timeout: timeout ?? 5000 });
return true;
} catch {
return false;
}
}
async isHidden(locator: Locator): Promise<boolean> {
return locator.isHidden();
}
async isEnabled(locator: Locator): Promise<boolean> {
return locator.isEnabled();
}
async waitForElement(locator: Locator, timeout?: number): Promise<void> {
await locator.waitFor({
state: 'visible',
timeout: timeout ?? this.defaultTimeout,
});
}
async waitForElementToDisappear(locator: Locator, timeout?: number): Promise<void> {
await locator.waitFor({
state: 'hidden',
timeout: timeout ?? this.defaultTimeout,
});
}
async count(locator: Locator): Promise<number> {
return locator.count();
}
async getAll(locator: Locator): Promise<Locator[]> {
return locator.all();
}
async getCssValue(locator: Locator, property: string): Promise<string> {
await this.waitForVisible(locator);
return locator.evaluate((el, prop) => {
return window.getComputedStyle(el).getPropertyValue(prop);
}, property);
}
async hasClass(locator: Locator, className: string): Promise<boolean> {
const classAttr = await this.getAttribute(locator, 'class');
return classAttr?.includes(className) ?? false;
}
}
PageActions (Page-Level Operations)
import { Page } from '@playwright/test';
import { BaseAction } from './BaseAction';
export class PageActions extends BaseAction {
constructor(page: Page) {
super(page);
}
async navigate(url: string): Promise<void> {
this.log('Navigate', url);
await this.page.goto(url);
}
async navigateAndWait(url: string): Promise<void> {
this.log('NavigateAndWait', url);
await this.page.goto(url, { waitUntil: 'networkidle' });
}
async reload(): Promise<void> {
this.log('Reload');
await this.page.reload();
}
async goBack(): Promise<void> {
this.log('GoBack');
await this.page.goBack();
}
async goForward(): Promise<void> {
this.log('GoForward');
await this.page.goForward();
}
getCurrentUrl(): string {
return this.page.url();
}
async getTitle(): Promise<string> {
return this.page.title();
}
async waitForLoad(): Promise<void> {
await this.page.waitForLoadState('load');
}
async waitForNetworkIdle(): Promise<void> {
await this.page.waitForLoadState('networkidle');
}
async waitForDOMContentLoaded(): Promise<void> {
await this.page.waitForLoadState('domcontentloaded');
}
async takeScreenshot(name: string): Promise<void> {
this.log('Screenshot', name);
await this.page.screenshot({
path: `screenshots/${name}-${Date.now()}.png`,
fullPage: true,
});
}
async acceptDialog(): Promise<void> {
this.page.once('dialog', dialog => dialog.accept());
}
async dismissDialog(): Promise<void> {
this.page.once('dialog', dialog => dialog.dismiss());
}
async pressKey(key: string): Promise<void> {
this.log('PressKey', key);
await this.page.keyboard.press(key);
}
async scrollToTop(): Promise<void> {
await this.page.evaluate(() => window.scrollTo(0, 0));
}
async scrollToBottom(): Promise<void> {
await this.page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
}
}
3. Main UIActions Class (The Gateway)
import { Page } from '@playwright/test';
import { EditBoxActions } from './EditBoxActions';
import { ButtonActions } from './ButtonActions';
import { CheckboxActions } from './CheckboxActions';
import { DropdownActions } from './DropdownActions';
import { UIElementActions } from './UIElementActions';
import { PageActions } from './PageActions';
export class UIActions {
private page: Page;
private _editBox: EditBoxActions;
private _button: ButtonActions;
private _checkbox: CheckboxActions;
private _dropdown: DropdownActions;
private _element: UIElementActions;
private _pageActions: PageActions;
constructor(page: Page) {
this.page = page;
this._editBox = new EditBoxActions(page);
this._button = new ButtonActions(page);
this._checkbox = new CheckboxActions(page);
this._dropdown = new DropdownActions(page);
this._element = new UIElementActions(page);
this._pageActions = new PageActions(page);
}
editBox(): EditBoxActions {
return this._editBox;
}
button(): ButtonActions {
return this._button;
}
checkbox(): CheckboxActions {
return this._checkbox;
}
dropdown(): DropdownActions {
return this._dropdown;
}
element(): UIElementActions {
return this._element;
}
pageAction(): PageActions {
return this._pageActions;
}
getPage(): Page {
return this.page;
}
}
Using UIActions with Page Objects
Before (Without UIActions)
class LoginPage {
constructor(private page: Page) {}
readonly emailInput = this.page.getByLabel('Email');
readonly passwordInput = this.page.getByLabel('Password');
readonly loginButton = this.page.getByRole('button', { name: 'Sign in' });
async login(email: string, password: string) {
await this.emailInput.waitFor({ state: 'visible' });
await this.emailInput.fill(email);
await this.passwordInput.waitFor({ state: 'visible' });
await this.passwordInput.fill(password);
await this.loginButton.waitFor({ state: 'visible' });
await this.loginButton.click();
await this.page.waitForLoadState('networkidle');
}
}
After (With UIActions)
class LoginPage {
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
constructor(private page: Page, private ui: UIActions) {
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.loginButton = page.getByRole('button', { name: 'Sign in' });
}
async login(email: string, password: string) {
await this.ui.editBox().fill(this.emailInput, email);
await this.ui.editBox().fillSensitive(this.passwordInput, password);
await this.ui.button().clickAndWaitForNetworkIdle(this.loginButton);
}
async getEmailValue(): Promise<string> {
return this.ui.editBox().getValue(this.emailInput);
}
}
Setting Up UIActions with Fixtures
import { test as base } from '@playwright/test';
import { UIActions } from '../actions/UIActions';
type UIFixtures = {
ui: UIActions;
};
export const test = base.extend<UIFixtures>({
ui: async ({ page }, use) => {
const ui = new UIActions(page);
await use(ui);
},
});
export { expect } from '@playwright/test';
import { test, expect } from '../fixtures/ui.fixture';
import { LoginPage } from '../pages/LoginPage';
test('user can login', async ({ page, ui }) => {
const loginPage = new LoginPage(page, ui);
await ui.pageAction().navigate('/login');
await loginPage.login('user@example.com', 'password');
await expect(page).toHaveURL('/dashboard');
});
Folder Structure
your-project/
├── actions/
│ ├── BaseAction.ts # Abstract base class
│ ├── EditBoxActions.ts # Text input actions
│ ├── ButtonActions.ts # Click actions
│ ├── CheckboxActions.ts # Checkbox/radio actions
│ ├── DropdownActions.ts # Select actions
│ ├── UIElementActions.ts # Generic element actions
│ ├── PageActions.ts # Page-level actions
│ ├── UIActions.ts # Main gateway class
│ └── index.ts # Barrel exports
├── pages/
│ ├── BasePage.ts
│ ├── LoginPage.ts
│ └── ...
├── fixtures/
│ └── ui.fixture.ts
└── tests/
└── ...
Best Practices
Do's
Don'ts
Rules for Page Objects (When Using UIActions)
- Never write raw Playwright code - No
page.locator(), locator.click(), expect() in Page Objects
- Never write inline selectors - Define all locators as class properties
- Never hardcode values - Use parameters or test data
- All interactions through UIActions - No exceptions
Related Resources