Files
SkinbaseNova/tests/helpers/crudHelper.ts
2026-03-12 07:22:38 +01:00

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;
}