- Configure workspace Git repository with comprehensive .gitignore - Add Odoo as submodule for ERP reference code - Include documentation: SETUP.md, GIT-STRUCTURE.md - Add gitignore templates for projects (backend, frontend, database) - Structure supports independent repos per project/subproject level Workspace includes: - core/ - Reusable patterns, modules, orchestration system - projects/ - Active projects (erp-suite, gamilit, trading-platform, etc.) - knowledge-base/ - Reference code and patterns (includes Odoo submodule) - devtools/ - Development tools and templates - customers/ - Client implementations template 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
176 lines
4.5 KiB
TypeScript
176 lines
4.5 KiB
TypeScript
import { Page, expect } from '@playwright/test';
|
|
|
|
/**
|
|
* Page Helper Functions
|
|
*
|
|
* Reusable functions for common page interactions
|
|
*/
|
|
|
|
/**
|
|
* Wait for page to load completely
|
|
*/
|
|
export async function waitForPageLoad(page: Page) {
|
|
await page.waitForLoadState('networkidle');
|
|
await page.waitForLoadState('domcontentloaded');
|
|
}
|
|
|
|
/**
|
|
* Wait for element with retry
|
|
*/
|
|
export async function waitForElement(
|
|
page: Page,
|
|
selector: string,
|
|
options?: { timeout?: number; state?: 'visible' | 'attached' | 'hidden' }
|
|
) {
|
|
await page.waitForSelector(selector, {
|
|
timeout: options?.timeout || 10000,
|
|
state: options?.state || 'visible',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Click and wait for navigation
|
|
*/
|
|
export async function clickAndWaitForNavigation(page: Page, selector: string) {
|
|
await Promise.all([page.waitForNavigation(), page.click(selector)]);
|
|
}
|
|
|
|
/**
|
|
* Fill form field by label
|
|
*/
|
|
export async function fillField(page: Page, labelText: string | RegExp, value: string) {
|
|
await page.getByLabel(labelText).fill(value);
|
|
}
|
|
|
|
/**
|
|
* Click button by text
|
|
*/
|
|
export async function clickButton(page: Page, buttonText: string | RegExp) {
|
|
await page.getByRole('button', { name: buttonText }).click();
|
|
}
|
|
|
|
/**
|
|
* Wait for toast/notification
|
|
*/
|
|
export async function waitForToast(page: Page, expectedText?: string | RegExp) {
|
|
const toast = page.locator('[role="alert"], .toast, [class*="toast"]').first();
|
|
await expect(toast).toBeVisible({ timeout: 5000 });
|
|
|
|
if (expectedText) {
|
|
await expect(toast).toContainText(expectedText);
|
|
}
|
|
|
|
return toast;
|
|
}
|
|
|
|
/**
|
|
* Wait for modal to open
|
|
*/
|
|
export async function waitForModal(page: Page) {
|
|
const modal = page.locator('[role="dialog"], .modal, [class*="modal"]').first();
|
|
await expect(modal).toBeVisible({ timeout: 5000 });
|
|
return modal;
|
|
}
|
|
|
|
/**
|
|
* Close modal
|
|
*/
|
|
export async function closeModal(page: Page) {
|
|
// Try different ways to close modal
|
|
const closeButton = page.locator(
|
|
'button[aria-label*="cerrar"], button[aria-label*="close"], button.modal-close, [data-testid="close-modal"]'
|
|
);
|
|
|
|
if (await closeButton.isVisible()) {
|
|
await closeButton.first().click();
|
|
} else {
|
|
// Try pressing Escape
|
|
await page.keyboard.press('Escape');
|
|
}
|
|
|
|
// Wait for modal to disappear
|
|
await expect(page.locator('[role="dialog"]').first()).not.toBeVisible({ timeout: 3000 });
|
|
}
|
|
|
|
/**
|
|
* Take screenshot with timestamp
|
|
*/
|
|
export async function takeScreenshot(page: Page, name: string) {
|
|
const timestamp = new Date().toISOString().replace(/:/g, '-');
|
|
await page.screenshot({ path: `test-results/${name}-${timestamp}.png`, fullPage: true });
|
|
}
|
|
|
|
/**
|
|
* Scroll to element
|
|
*/
|
|
export async function scrollToElement(page: Page, selector: string) {
|
|
await page.locator(selector).scrollIntoViewIfNeeded();
|
|
}
|
|
|
|
/**
|
|
* Wait for loading spinner to disappear
|
|
*/
|
|
export async function waitForLoadingToFinish(page: Page) {
|
|
const spinner = page.locator('[role="progressbar"], .spinner, .loading, [class*="loading"]');
|
|
|
|
// Wait for spinner to appear (if it will)
|
|
try {
|
|
await spinner.first().waitFor({ state: 'visible', timeout: 1000 });
|
|
} catch {
|
|
// Spinner might not appear, that's ok
|
|
return;
|
|
}
|
|
|
|
// Wait for all spinners to disappear
|
|
await spinner.first().waitFor({ state: 'hidden', timeout: 10000 });
|
|
}
|
|
|
|
/**
|
|
* Get data-testid element
|
|
*/
|
|
export function getByTestId(page: Page, testId: string) {
|
|
return page.locator(`[data-testid="${testId}"]`);
|
|
}
|
|
|
|
/**
|
|
* Check if element exists
|
|
*/
|
|
export async function elementExists(page: Page, selector: string): Promise<boolean> {
|
|
const count = await page.locator(selector).count();
|
|
return count > 0;
|
|
}
|
|
|
|
/**
|
|
* Wait for API response
|
|
*/
|
|
export async function waitForApiResponse(
|
|
page: Page,
|
|
urlPattern: string | RegExp,
|
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET'
|
|
) {
|
|
return await page.waitForResponse(
|
|
(response) => {
|
|
const url = response.url();
|
|
const matchesUrl =
|
|
typeof urlPattern === 'string' ? url.includes(urlPattern) : urlPattern.test(url);
|
|
return matchesUrl && response.request().method() === method;
|
|
},
|
|
{ timeout: 10000 }
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Fill and submit form
|
|
*/
|
|
export async function fillAndSubmitForm(
|
|
page: Page,
|
|
fields: Record<string, string>,
|
|
submitButtonText: string | RegExp
|
|
) {
|
|
for (const [label, value] of Object.entries(fields)) {
|
|
await fillField(page, new RegExp(label, 'i'), value);
|
|
}
|
|
|
|
await clickButton(page, submitButtonText);
|
|
}
|