148 lines
4.9 KiB
TypeScript
148 lines
4.9 KiB
TypeScript
/**
|
|
* CRUD Helper for cPad module tests.
|
|
*
|
|
* Provides reusable functions that detect and execute common CRUD operations
|
|
* (Create, Edit, Delete) through the cPad UI without hard-coding selectors
|
|
* for every individual module.
|
|
*
|
|
* Strategy:
|
|
* • "Create" — look for button text: Create / Add / New
|
|
* • "Edit" — look for button/link text: Edit, or table action icons
|
|
* • "Delete" — look for button/link text: Delete / Remove (with confirmation handling)
|
|
*
|
|
* Each function returns false if the appropriate trigger could not be found,
|
|
* allowing callers to skip gracefully when a module lacks a given operation.
|
|
*/
|
|
|
|
import { type Page, expect } from '@playwright/test';
|
|
import { fillForm } from './formFiller';
|
|
|
|
// Selectors used to locate CRUD triggers (order = priority)
|
|
const CREATE_SELECTORS = [
|
|
'a:has-text("Create")',
|
|
'a:has-text("Add")',
|
|
'a:has-text("New")',
|
|
'button:has-text("Create")',
|
|
'button:has-text("Add")',
|
|
'button:has-text("New")',
|
|
'[data-testid="btn-create"]',
|
|
'[data-testid="btn-add"]',
|
|
];
|
|
|
|
const EDIT_SELECTORS = [
|
|
'a:has-text("Edit")',
|
|
'button:has-text("Edit")',
|
|
'td a[href*="/edit/"]',
|
|
'[data-testid="btn-edit"]',
|
|
];
|
|
|
|
const DELETE_SELECTORS = [
|
|
'a:has-text("Delete")',
|
|
'button:has-text("Delete")',
|
|
'a:has-text("Remove")',
|
|
'button:has-text("Remove")',
|
|
'[data-testid="btn-delete"]',
|
|
];
|
|
|
|
/**
|
|
* Try to find and click a Create/Add/New button, fill the resulting form,
|
|
* submit it, and verify the page does not show a 500 error.
|
|
*
|
|
* @returns true if a create button was found and the flow completed without crashing.
|
|
*/
|
|
export async function tryCrudCreate(page: Page): Promise<boolean> {
|
|
for (const selector of CREATE_SELECTORS) {
|
|
const btn = page.locator(selector).first();
|
|
if (await btn.count() > 0 && await btn.isVisible()) {
|
|
await btn.click();
|
|
|
|
// Wait for either a form or a new page section to appear
|
|
await page.waitForLoadState('networkidle', { timeout: 10_000 }).catch(() => null);
|
|
|
|
// Fill any visible form
|
|
await fillForm(page);
|
|
|
|
// Look for a submit button
|
|
const submit = page.locator('button[type=submit], input[type=submit]').first();
|
|
if (await submit.count() > 0 && await submit.isVisible()) {
|
|
await submit.click();
|
|
await page.waitForLoadState('networkidle', { timeout: 10_000 }).catch(() => null);
|
|
|
|
// Fail if a Laravel/server error page appeared
|
|
const body = await page.locator('body').textContent() ?? '';
|
|
const hasServerError = /Whoops|Server Error|500|SQLSTATE|Call to undefined/i.test(body);
|
|
expect(hasServerError, `Server error after create on ${page.url()}`).toBe(false);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Try to find and click the first Edit button/link on the page, fill the form,
|
|
* and submit.
|
|
*
|
|
* @returns true if an edit trigger was found.
|
|
*/
|
|
export async function tryCrudEdit(page: Page): Promise<boolean> {
|
|
for (const selector of EDIT_SELECTORS) {
|
|
const btn = page.locator(selector).first();
|
|
if (await btn.count() > 0 && await btn.isVisible()) {
|
|
await btn.click();
|
|
await page.waitForLoadState('networkidle', { timeout: 10_000 }).catch(() => null);
|
|
|
|
await fillForm(page);
|
|
|
|
const submit = page.locator('button[type=submit], input[type=submit]').first();
|
|
if (await submit.count() > 0 && await submit.isVisible()) {
|
|
await submit.click();
|
|
await page.waitForLoadState('networkidle', { timeout: 10_000 }).catch(() => null);
|
|
|
|
const body = await page.locator('body').textContent() ?? '';
|
|
const hasServerError = /Whoops|Server Error|500|SQLSTATE|Call to undefined/i.test(body);
|
|
expect(hasServerError, `Server error after edit on ${page.url()}`).toBe(false);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Try to click the first delete trigger, handle a confirmation dialog if one
|
|
* appears, and assert no server error is displayed.
|
|
*
|
|
* @returns true if a delete trigger was found.
|
|
*/
|
|
export async function tryCrudDelete(page: Page): Promise<boolean> {
|
|
for (const selector of DELETE_SELECTORS) {
|
|
const btn = page.locator(selector).first();
|
|
if (await btn.count() > 0 && await btn.isVisible()) {
|
|
// Some delete links pop a browser confirm() dialog
|
|
page.once('dialog', (dialog) => dialog.accept());
|
|
|
|
await btn.click();
|
|
await page.waitForLoadState('networkidle', { timeout: 10_000 }).catch(() => null);
|
|
|
|
const body = await page.locator('body').textContent() ?? '';
|
|
const hasServerError = /Whoops|Server Error|500|SQLSTATE|Call to undefined/i.test(body);
|
|
expect(hasServerError, `Server error after delete on ${page.url()}`).toBe(false);
|
|
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Quick check whether the page contains any list/table data (i.e. the module
|
|
* has records to operate on).
|
|
*/
|
|
export async function hasListData(page: Page): Promise<boolean> {
|
|
const tableRows = await page.locator('table tbody tr').count();
|
|
return tableRows > 0;
|
|
}
|