Initial commit
This commit is contained in:
265
skills/page-object-builder/resources/base-page.ts
Normal file
265
skills/page-object-builder/resources/base-page.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Base Page Object
|
||||
*
|
||||
* Contains common functionality shared across all page objects.
|
||||
* Extend this class for your specific page objects to avoid duplication.
|
||||
*
|
||||
* Usage:
|
||||
* ```typescript
|
||||
* export class LoginPage extends BasePage {
|
||||
* constructor(page: Page) {
|
||||
* super(page);
|
||||
* }
|
||||
*
|
||||
* async login() {
|
||||
* await this.fillInput('username', 'user@example.com');
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export class BasePage {
|
||||
readonly page: Page;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a URL
|
||||
*/
|
||||
async goto(url: string): Promise<void> {
|
||||
await this.page.goto(url);
|
||||
await this.page.waitForLoadState('domcontentloaded');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get locator by data-testid
|
||||
*/
|
||||
getByTestId(testId: string): Locator {
|
||||
return this.page.locator(`[data-testid="${testId}"]`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click element by data-testid
|
||||
*/
|
||||
async clickByTestId(testId: string): Promise<void> {
|
||||
const locator = this.getByTestId(testId);
|
||||
await locator.waitFor({ state: 'visible' });
|
||||
await locator.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill input by data-testid
|
||||
*/
|
||||
async fillInput(testId: string, value: string): Promise<void> {
|
||||
const locator = this.getByTestId(testId);
|
||||
await locator.waitFor({ state: 'visible' });
|
||||
await locator.fill(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get text content by data-testid
|
||||
*/
|
||||
async getTextByTestId(testId: string): Promise<string> {
|
||||
const locator = this.getByTestId(testId);
|
||||
const text = await locator.textContent();
|
||||
return text?.trim() || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if element is visible by data-testid
|
||||
*/
|
||||
async isVisibleByTestId(testId: string): Promise<boolean> {
|
||||
try {
|
||||
await this.getByTestId(testId).waitFor({
|
||||
state: 'visible',
|
||||
timeout: 2000,
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for element by data-testid
|
||||
*/
|
||||
async waitForElement(testId: string, timeout = 10000): Promise<Locator> {
|
||||
const locator = this.getByTestId(testId);
|
||||
await locator.waitFor({ state: 'visible', timeout });
|
||||
return locator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for element to disappear
|
||||
*/
|
||||
async waitForElementToDisappear(
|
||||
testId: string,
|
||||
timeout = 10000
|
||||
): Promise<void> {
|
||||
const locator = this.getByTestId(testId);
|
||||
await locator.waitFor({ state: 'hidden', timeout });
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill a form with multiple fields
|
||||
*/
|
||||
async fillForm(fields: Record<string, string>): Promise<void> {
|
||||
for (const [testId, value] of Object.entries(fields)) {
|
||||
await this.fillInput(testId, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click and wait for navigation
|
||||
*/
|
||||
async clickAndNavigate(
|
||||
testId: string,
|
||||
expectedUrl?: string | RegExp
|
||||
): Promise<void> {
|
||||
const locator = this.getByTestId(testId);
|
||||
await locator.waitFor({ state: 'visible' });
|
||||
|
||||
await Promise.all([
|
||||
expectedUrl
|
||||
? this.page.waitForURL(expectedUrl)
|
||||
: this.page.waitForLoadState('networkidle'),
|
||||
locator.click(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select option from dropdown
|
||||
*/
|
||||
async selectOption(testId: string, value: string): Promise<void> {
|
||||
const locator = this.getByTestId(testId);
|
||||
await locator.waitFor({ state: 'visible' });
|
||||
await locator.selectOption(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check/uncheck checkbox
|
||||
*/
|
||||
async toggleCheckbox(testId: string, checked: boolean): Promise<void> {
|
||||
const locator = this.getByTestId(testId);
|
||||
await locator.waitFor({ state: 'visible' });
|
||||
|
||||
const isChecked = await locator.isChecked();
|
||||
if (isChecked !== checked) {
|
||||
await locator.click();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload file
|
||||
*/
|
||||
async uploadFile(testId: string, filePath: string): Promise<void> {
|
||||
const locator = this.getByTestId(testId);
|
||||
await locator.waitFor({ state: 'attached' });
|
||||
await locator.setInputFiles(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all text contents from multiple elements
|
||||
*/
|
||||
async getAllTextsByTestId(testId: string): Promise<string[]> {
|
||||
const locator = this.getByTestId(testId);
|
||||
await locator.first().waitFor({ state: 'visible' });
|
||||
return await locator.allTextContents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get element count by data-testid
|
||||
*/
|
||||
async getCountByTestId(testId: string): Promise<number> {
|
||||
return await this.getByTestId(testId).count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for API response
|
||||
*/
|
||||
async waitForApiResponse<T = any>(
|
||||
urlPattern: string | RegExp,
|
||||
action: () => Promise<void>
|
||||
): Promise<T> {
|
||||
const [response] = await Promise.all([
|
||||
this.page.waitForResponse(urlPattern),
|
||||
action(),
|
||||
]);
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current URL
|
||||
*/
|
||||
getCurrentUrl(): string {
|
||||
return this.page.url();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload page
|
||||
*/
|
||||
async reload(): Promise<void> {
|
||||
await this.page.reload();
|
||||
await this.page.waitForLoadState('domcontentloaded');
|
||||
}
|
||||
|
||||
/**
|
||||
* Go back in browser history
|
||||
*/
|
||||
async goBack(): Promise<void> {
|
||||
await this.page.goBack();
|
||||
await this.page.waitForLoadState('domcontentloaded');
|
||||
}
|
||||
|
||||
/**
|
||||
* Go forward in browser history
|
||||
*/
|
||||
async goForward(): Promise<void> {
|
||||
await this.page.goForward();
|
||||
await this.page.waitForLoadState('domcontentloaded');
|
||||
}
|
||||
|
||||
/**
|
||||
* Take screenshot
|
||||
*/
|
||||
async takeScreenshot(name: string): Promise<void> {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
await this.page.screenshot({
|
||||
path: `${name}-${timestamp}.png`,
|
||||
fullPage: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Example usage:
|
||||
*
|
||||
* export class LoginPage extends BasePage {
|
||||
* readonly usernameInput: Locator;
|
||||
* readonly passwordInput: Locator;
|
||||
* readonly loginButton: Locator;
|
||||
*
|
||||
* constructor(page: Page) {
|
||||
* super(page);
|
||||
* this.usernameInput = this.getByTestId('username-input');
|
||||
* this.passwordInput = this.getByTestId('password-input');
|
||||
* this.loginButton = this.getByTestId('login-button');
|
||||
* }
|
||||
*
|
||||
* async goto(): Promise<void> {
|
||||
* await super.goto('/login');
|
||||
* }
|
||||
*
|
||||
* async login(username: string, password: string): Promise<void> {
|
||||
* await this.fillForm({
|
||||
* 'username-input': username,
|
||||
* 'password-input': password,
|
||||
* });
|
||||
* await this.clickAndNavigate('login-button', '/dashboard');
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
161
skills/page-object-builder/resources/component-template.ts
Normal file
161
skills/page-object-builder/resources/component-template.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Component Object Model for [Component Name]
|
||||
*
|
||||
* Description: Reusable component that appears on multiple pages
|
||||
* Example: Navigation bar, modal dialog, toast notification, etc.
|
||||
*
|
||||
* Usage:
|
||||
* ```typescript
|
||||
* const component = new ComponentName(page);
|
||||
* await component.performAction();
|
||||
* ```
|
||||
*/
|
||||
export class ComponentName {
|
||||
readonly page: Page;
|
||||
|
||||
// Component container
|
||||
readonly container: Locator;
|
||||
|
||||
// Component elements
|
||||
readonly element1: Locator;
|
||||
readonly element2: Locator;
|
||||
|
||||
/**
|
||||
* Constructor: Initialize component with page and optional container selector
|
||||
* @param page - Playwright Page object
|
||||
* @param containerTestId - Optional data-testid for component container
|
||||
*/
|
||||
constructor(page: Page, containerTestId = 'component-container') {
|
||||
this.page = page;
|
||||
this.container = page.locator(`[data-testid="${containerTestId}"]`);
|
||||
|
||||
// Scoped locators within the component
|
||||
this.element1 = this.container.locator('[data-testid="element-1"]');
|
||||
this.element2 = this.container.locator('[data-testid="element-2"]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for component to be visible
|
||||
*/
|
||||
async waitForComponent(): Promise<void> {
|
||||
await this.container.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if component is visible
|
||||
*/
|
||||
async isVisible(): Promise<boolean> {
|
||||
try {
|
||||
await this.container.waitFor({ state: 'visible', timeout: 2000 });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component-specific action
|
||||
*/
|
||||
async performAction(): Promise<void> {
|
||||
await this.waitForComponent();
|
||||
await this.element1.click();
|
||||
}
|
||||
|
||||
// Getters
|
||||
getContainer(): Locator {
|
||||
return this.container;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Example: Modal Dialog Component
|
||||
*/
|
||||
export class ModalDialog {
|
||||
readonly page: Page;
|
||||
readonly modal: Locator;
|
||||
readonly title: Locator;
|
||||
readonly closeButton: Locator;
|
||||
readonly confirmButton: Locator;
|
||||
readonly cancelButton: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.modal = page.locator('[data-testid="modal-dialog"]');
|
||||
this.title = this.modal.locator('[data-testid="modal-title"]');
|
||||
this.closeButton = this.modal.locator('[data-testid="modal-close"]');
|
||||
this.confirmButton = this.modal.locator('[data-testid="modal-confirm"]');
|
||||
this.cancelButton = this.modal.locator('[data-testid="modal-cancel"]');
|
||||
}
|
||||
|
||||
async waitForModal(): Promise<void> {
|
||||
await this.modal.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await this.closeButton.click();
|
||||
await this.modal.waitFor({ state: 'hidden' });
|
||||
}
|
||||
|
||||
async confirm(): Promise<void> {
|
||||
await this.confirmButton.click();
|
||||
await this.modal.waitFor({ state: 'hidden' });
|
||||
}
|
||||
|
||||
async cancel(): Promise<void> {
|
||||
await this.cancelButton.click();
|
||||
await this.modal.waitFor({ state: 'hidden' });
|
||||
}
|
||||
|
||||
async getTitle(): Promise<string> {
|
||||
const text = await this.title.textContent();
|
||||
return text?.trim() || '';
|
||||
}
|
||||
|
||||
getModal(): Locator {
|
||||
return this.modal;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Example: Navigation Bar Component
|
||||
*/
|
||||
export class NavigationBar {
|
||||
readonly page: Page;
|
||||
readonly nav: Locator;
|
||||
readonly logo: Locator;
|
||||
readonly menuItems: Locator;
|
||||
readonly userMenu: Locator;
|
||||
readonly searchBar: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.nav = page.locator('[data-testid="navbar"]');
|
||||
this.logo = this.nav.locator('[data-testid="logo"]');
|
||||
this.menuItems = this.nav.locator('[data-testid="menu-item"]');
|
||||
this.userMenu = this.nav.locator('[data-testid="user-menu"]');
|
||||
this.searchBar = this.nav.locator('[data-testid="search-bar"]');
|
||||
}
|
||||
|
||||
async clickLogo(): Promise<void> {
|
||||
await this.logo.click();
|
||||
}
|
||||
|
||||
async clickMenuItem(name: string): Promise<void> {
|
||||
await this.menuItems.filter({ hasText: name }).click();
|
||||
}
|
||||
|
||||
async openUserMenu(): Promise<void> {
|
||||
await this.userMenu.click();
|
||||
}
|
||||
|
||||
async search(query: string): Promise<void> {
|
||||
await this.searchBar.fill(query);
|
||||
await this.page.keyboard.press('Enter');
|
||||
}
|
||||
|
||||
getNav(): Locator {
|
||||
return this.nav;
|
||||
}
|
||||
}
|
||||
112
skills/page-object-builder/resources/page-template.ts
Normal file
112
skills/page-object-builder/resources/page-template.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Page Object Model for [Page Name]
|
||||
*
|
||||
* Description: [Brief description of what this page represents]
|
||||
*
|
||||
* Key functionality:
|
||||
* - [Key function 1]
|
||||
* - [Key function 2]
|
||||
* - [Key function 3]
|
||||
*/
|
||||
export class PageName {
|
||||
readonly page: Page;
|
||||
|
||||
// ============================================
|
||||
// LOCATORS
|
||||
// ============================================
|
||||
// Group related locators together with comments
|
||||
|
||||
// Section 1: [e.g., Form Elements]
|
||||
readonly elementName: Locator;
|
||||
readonly anotherElement: Locator;
|
||||
|
||||
// Section 2: [e.g., Navigation]
|
||||
readonly navElement: Locator;
|
||||
|
||||
/**
|
||||
* Constructor: Initialize page object with locators
|
||||
* @param page - Playwright Page object
|
||||
*/
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
|
||||
// Initialize all locators using data-testid
|
||||
this.elementName = page.locator('[data-testid="element-name"]');
|
||||
this.anotherElement = page.locator('[data-testid="another-element"]');
|
||||
this.navElement = page.locator('[data-testid="nav-element"]');
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// NAVIGATION
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Navigate to this page
|
||||
*/
|
||||
async goto(): Promise<void> {
|
||||
await this.page.goto('/page-url');
|
||||
await this.page.waitForLoadState('domcontentloaded');
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ACTIONS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Perform a specific action
|
||||
* @param param - Description of parameter
|
||||
*/
|
||||
async performAction(param: string): Promise<void> {
|
||||
await this.elementName.waitFor({ state: 'visible' });
|
||||
await this.elementName.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill a form with data
|
||||
* @param data - Form data object
|
||||
*/
|
||||
async fillForm(data: { field1: string; field2: string }): Promise<void> {
|
||||
await this.elementName.fill(data.field1);
|
||||
await this.anotherElement.fill(data.field2);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// GETTERS (for assertions in tests)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Get element for assertions
|
||||
*/
|
||||
getElement(): Locator {
|
||||
return this.elementName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get text content of an element
|
||||
*/
|
||||
async getTextContent(): Promise<string> {
|
||||
const text = await this.elementName.textContent();
|
||||
return text?.trim() || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if element is visible
|
||||
*/
|
||||
async isElementVisible(): Promise<boolean> {
|
||||
try {
|
||||
await this.elementName.waitFor({ state: 'visible', timeout: 2000 });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Required data-testid values:
|
||||
* - element-name: [Description of element]
|
||||
* - another-element: [Description of element]
|
||||
* - nav-element: [Description of element]
|
||||
*/
|
||||
Reference in New Issue
Block a user