mirror of
https://github.com/strapi/strapi.git
synced 2025-08-09 09:14:49 +00:00
153 lines
6.0 KiB
TypeScript
153 lines
6.0 KiB
TypeScript
import { test, expect, type Page, type Locator } from '@playwright/test';
|
|
|
|
type NavItem = string | [string, string] | Locator;
|
|
|
|
/**
|
|
* Execute a test suite only if the condition is true
|
|
*/
|
|
export const describeOnCondition = (shouldDescribe: boolean) =>
|
|
shouldDescribe ? test.describe : test.describe.skip;
|
|
|
|
/**
|
|
* Find an element in the dom after the previous element
|
|
* Useful for narrowing down which link to click when there are multiple with the same name
|
|
*/
|
|
// TODO: instead of siblingText + linkText, accept an array of any number items
|
|
export const locateFirstAfter = async (page: Page, firstText: string, secondText: string) => {
|
|
// It first searches for text containing "firstText" then uses xpath `following` to find "secondText" after it.
|
|
// `translate` is used to make the search case-insensitive
|
|
const item = page
|
|
.locator(
|
|
`xpath=//text()[contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), "${firstText.toLowerCase()}")]/following::a[starts-with(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), "${secondText.toLowerCase()}")]`
|
|
)
|
|
.first();
|
|
|
|
return item;
|
|
};
|
|
|
|
/**
|
|
* Navigate to a page and confirm the header, awaiting each step
|
|
*/
|
|
export const navToHeader = async (page: Page, navItems: NavItem[], headerText: string) => {
|
|
for (const navItem of navItems) {
|
|
// This handles some common issues
|
|
// 1. Uses name^= to only ensure starts with, because for example badge notifications cause "Settings" to really be "Settings 1"
|
|
// 2. To avoid duplicates, we accept a locator
|
|
// 3. To avoid duplicates and writing complex locators, we accept an array to pass to locateFirstAfter, which matches item0 then finds the next item1 in the dom
|
|
let item;
|
|
if (typeof navItem === 'string') {
|
|
item = page.locator(`role=link[name^="${navItem}"]`).last();
|
|
} else if (Array.isArray(navItem)) {
|
|
item = await locateFirstAfter(page, navItem[0], navItem[1]);
|
|
} else {
|
|
// it's a Locator
|
|
item = navItem;
|
|
}
|
|
|
|
await expect(item).toBeVisible();
|
|
await item.click();
|
|
}
|
|
|
|
// Verify header is correct
|
|
const header = page.getByRole('heading', { name: headerText, exact: true });
|
|
await expect(header).toBeVisible();
|
|
return header;
|
|
};
|
|
|
|
/**
|
|
* Skip the tour if the modal is visible
|
|
*/
|
|
export const skipCtbTour = async (page: Page) => {
|
|
try {
|
|
const modal = await page.getByRole('button', { name: 'Skip the tour' });
|
|
|
|
if (await modal.isVisible()) {
|
|
await modal.click();
|
|
await expect(modal).not.toBeVisible();
|
|
}
|
|
} catch (e) {
|
|
// The modal did not appear, continue with the test
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Clicks on a link and waits for the page to load completely.
|
|
*
|
|
* NOTE: this util is used to avoid inconsistent behaviour on webkit
|
|
*
|
|
*/
|
|
export const clickAndWait = async (page: Page, locator: Locator) => {
|
|
await locator.click();
|
|
await page.waitForLoadState('networkidle');
|
|
};
|
|
|
|
/**
|
|
* Look for an element containing text, and then click a sibling close button
|
|
*/
|
|
export const findAndClose = async (
|
|
page: Page,
|
|
text: string,
|
|
role: string = 'status',
|
|
closeLabel: string = 'Close'
|
|
) => {
|
|
// Verify the popup text is visible.
|
|
const elements = page.locator(`:has-text("${text}")[role="${role}"]`);
|
|
await expect(elements.first()).toBeVisible(); // expect at least one element
|
|
|
|
// Find all 'Close' buttons that are siblings of the elements containing the specified text.
|
|
const closeBtns = page.locator(
|
|
`:has-text("${text}")[role="${role}"] ~ button:has-text("${closeLabel}")`
|
|
);
|
|
|
|
// Click all 'Close' buttons.
|
|
const count = await closeBtns.count();
|
|
for (let i = 0; i < count; i++) {
|
|
await closeBtns.nth(i).click();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Finds a specific cell in a table by matching both the row text and the column header text.
|
|
*
|
|
* This function performs the following steps:
|
|
* 1. Finds a row in the table that contains the specified `rowText` (case-insensitive).
|
|
* 2. Finds the column header in the table that contains the specified `columnText` (case-insensitive).
|
|
* 3. Identifies the cell in the located row that corresponds to the column where the header matches the `columnText`.
|
|
* 4. Returns the found cell for further interactions or assertions.
|
|
*
|
|
* @param {Page} page - The Playwright `Page` object representing the browser page.
|
|
* @param {string} rowText - The text to match in the row (case-insensitive).
|
|
* @param {string} columnText - The text to match in the column header (case-insensitive).
|
|
*
|
|
* @returns {Locator} - A Playwright Locator object representing the intersecting cell.
|
|
*
|
|
* @throws Will throw an error if the row or column header is not found, or if the cell is not visible.
|
|
*
|
|
* @warning This function assumes a standard table structure where each row has an equal number of cells,
|
|
* and no cells are merged (`colspan` or `rowspan`). If the table contains merged cells,
|
|
* this method may return incorrect results or fail to locate the correct cell.
|
|
* Matches the header exactly (cell contains only exact text)
|
|
* Matches the row loosely (finds a row containing that text somewhere)
|
|
*/
|
|
export const findByRowColumn = async (page: Page, rowText: string, columnText: string) => {
|
|
// Locate the row that contains the rowText
|
|
// This just looks for the text in a row, so ensure that it is specific enough
|
|
const row = page.locator('tr').filter({ hasText: new RegExp(`${rowText}`) });
|
|
await expect(row).toBeVisible();
|
|
|
|
// Locate the column header that matches the columnText
|
|
// This assumes that header is exact (cell only contains that text and nothing else)
|
|
const header = page.locator('thead th').filter({ hasText: new RegExp(`^${columnText}$`, 'i') });
|
|
await expect(header).toBeVisible();
|
|
|
|
// Find the index of the matching column header
|
|
const columnIndex = await header.evaluate((el) => Array.from(el.parentNode.children).indexOf(el));
|
|
|
|
// Find the cell in the located row that corresponds to the matching column index
|
|
const cell = row.locator(`td:nth-child(${columnIndex + 1})`);
|
|
await expect(cell).toBeVisible();
|
|
|
|
// Return the found cell
|
|
return cell;
|
|
};
|