mirror of
https://github.com/AppFlowy-IO/AppFlowy-Web.git
synced 2025-11-29 02:38:00 +08:00
246 lines
6.4 KiB
TypeScript
246 lines
6.4 KiB
TypeScript
/**
|
|
* General test helper utilities
|
|
* Common functions used across multiple E2E tests
|
|
*
|
|
* Usage:
|
|
* ```typescript
|
|
* import { closeModalsIfOpen, testLog, waitForReactUpdate } from '@/cypress/support/test-helpers';
|
|
*
|
|
* closeModalsIfOpen();
|
|
* testLog.step(1, 'Login user');
|
|
* testLog.success('Login completed');
|
|
* waitForReactUpdate(500);
|
|
* ```
|
|
*/
|
|
|
|
/**
|
|
* Closes any open modals or dialogs by pressing ESC
|
|
* Safe to call even if no modals are open
|
|
*/
|
|
export const closeModalsIfOpen = () => {
|
|
cy.get('body').then(($body) => {
|
|
const hasModal =
|
|
$body.find('[role="dialog"], .MuiDialog-container, [data-testid*="modal"]').length > 0;
|
|
|
|
if (hasModal) {
|
|
cy.task('log', 'Closing open modal dialog');
|
|
cy.get('body').type('{esc}');
|
|
cy.wait(1000);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Standardized logging utilities for test output
|
|
* Provides consistent formatting for test logs
|
|
*/
|
|
export const testLog = {
|
|
/**
|
|
* Log a test step with number
|
|
* @example testLog.step(1, 'Login user');
|
|
*/
|
|
step: (num: number, msg: string) => cy.task('log', `=== Step ${num}: ${msg} ===`),
|
|
|
|
/**
|
|
* Log general information
|
|
* @example testLog.info('Navigating to page');
|
|
*/
|
|
info: (msg: string) => cy.task('log', msg),
|
|
|
|
/**
|
|
* Log success message with checkmark
|
|
* @example testLog.success('User logged in');
|
|
*/
|
|
success: (msg: string) => cy.task('log', `✓ ${msg}`),
|
|
|
|
/**
|
|
* Log error message with X mark
|
|
* @example testLog.error('Login failed');
|
|
*/
|
|
error: (msg: string) => cy.task('log', `✗ ${msg}`),
|
|
|
|
/**
|
|
* Log warning message
|
|
* @example testLog.warn('Retrying operation');
|
|
*/
|
|
warn: (msg: string) => cy.task('log', `⚠ ${msg}`),
|
|
|
|
/**
|
|
* Log data in JSON format
|
|
* @example testLog.data('User info', { email, id });
|
|
*/
|
|
data: (label: string, value: unknown) =>
|
|
cy.task('log', `${label}: ${JSON.stringify(value, null, 2)}`),
|
|
|
|
/**
|
|
* Log test start with separator
|
|
* @example testLog.testStart('OAuth Login Flow');
|
|
*/
|
|
testStart: (testName: string) =>
|
|
cy.task(
|
|
'log',
|
|
`
|
|
╔════════════════════════════════════════════════════════════════╗
|
|
║ TEST: ${testName.padEnd(55)}║
|
|
╚════════════════════════════════════════════════════════════════╝`
|
|
),
|
|
|
|
/**
|
|
* Log test end with separator
|
|
* @example testLog.testEnd('OAuth Login Flow');
|
|
*/
|
|
testEnd: (testName: string) =>
|
|
cy.task('log', `\n✅ TEST COMPLETED: ${testName}\n`),
|
|
};
|
|
|
|
/**
|
|
* Wait for React updates to complete
|
|
* Useful after DOM mutations or state changes
|
|
*
|
|
* @param ms - Milliseconds to wait (default: 500)
|
|
*/
|
|
export const waitForReactUpdate = (ms = 500) => {
|
|
cy.wait(ms);
|
|
};
|
|
|
|
/**
|
|
* Wait for an element to exist and be stable
|
|
* Retries if element is not found
|
|
*
|
|
* @param selector - CSS selector or test ID
|
|
* @param timeout - Max time to wait in ms (default: 10000)
|
|
*/
|
|
export const waitForElement = (selector: string, timeout = 10000) => {
|
|
cy.get(selector, { timeout }).should('exist');
|
|
waitForReactUpdate(300);
|
|
};
|
|
|
|
/**
|
|
* Clear all form inputs within a container
|
|
* @param containerSelector - Optional container selector (defaults to body)
|
|
*/
|
|
export const clearAllInputs = (containerSelector = 'body') => {
|
|
cy.get(containerSelector)
|
|
.find('input, textarea')
|
|
.each(($el) => {
|
|
cy.wrap($el).clear();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Type text slowly to simulate real user input
|
|
* Useful for inputs with validation or autocomplete
|
|
*
|
|
* @param selector - Element selector
|
|
* @param text - Text to type
|
|
* @param delayMs - Delay between keystrokes in ms (default: 50)
|
|
*/
|
|
export const typeSlowly = (selector: string, text: string, delayMs = 50) => {
|
|
cy.get(selector).type(text, { delay: delayMs });
|
|
};
|
|
|
|
/**
|
|
* Scroll element into view and click
|
|
* Useful for elements that might be off-screen
|
|
*
|
|
* @param selector - Element selector
|
|
*/
|
|
export const scrollAndClick = (selector: string) => {
|
|
cy.get(selector).scrollIntoView().should('be.visible').click();
|
|
};
|
|
|
|
/**
|
|
* Assert that no error messages are visible on the page
|
|
* Checks for common error indicators
|
|
*/
|
|
export const assertNoErrors = () => {
|
|
cy.get('body').then(($body) => {
|
|
const hasError =
|
|
$body.text().includes('Error') ||
|
|
$body.text().includes('Failed') ||
|
|
$body.find('[role="alert"][data-severity="error"]').length > 0 ||
|
|
$body.find('[class*="error"]').length > 0;
|
|
|
|
if (hasError) {
|
|
testLog.warn('Error indicators detected on page');
|
|
}
|
|
|
|
expect(hasError).to.be.false;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Wait for network requests to complete
|
|
* Useful after actions that trigger API calls
|
|
*
|
|
* @param aliasName - Cypress intercept alias (without @)
|
|
* @param timeout - Max time to wait in ms (default: 10000)
|
|
*/
|
|
export const waitForRequest = (aliasName: string, timeout = 10000) => {
|
|
cy.wait(`@${aliasName}`, { timeout });
|
|
};
|
|
|
|
/**
|
|
* Retry an action until it succeeds or times out
|
|
* Useful for flaky operations
|
|
*
|
|
* @param action - Function containing the action to retry
|
|
* @param maxAttempts - Maximum number of retry attempts (default: 3)
|
|
* @param delayMs - Delay between attempts in ms (default: 1000)
|
|
*/
|
|
export const retryAction = (
|
|
action: () => void,
|
|
maxAttempts = 3,
|
|
delayMs = 1000
|
|
) => {
|
|
let attempts = 0;
|
|
|
|
const tryAction = () => {
|
|
attempts++;
|
|
try {
|
|
action();
|
|
} catch (error) {
|
|
if (attempts < maxAttempts) {
|
|
testLog.warn(`Action failed, retrying... (${attempts}/${maxAttempts})`);
|
|
cy.wait(delayMs);
|
|
tryAction();
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
};
|
|
|
|
tryAction();
|
|
};
|
|
|
|
/**
|
|
* Check if element exists without failing test
|
|
* Returns a boolean via then() callback
|
|
*
|
|
* @param selector - Element selector
|
|
*/
|
|
export const elementExists = (selector: string) => {
|
|
return cy.get('body').then(($body) => {
|
|
return $body.find(selector).length > 0;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Generate a random string for test data
|
|
* @param length - Length of string (default: 8)
|
|
*/
|
|
export const randomString = (length = 8) => {
|
|
return Math.random()
|
|
.toString(36)
|
|
.substring(2, 2 + length);
|
|
};
|
|
|
|
/**
|
|
* Take a screenshot with a descriptive name
|
|
* @param name - Screenshot name (test name will be prepended)
|
|
*/
|
|
export const takeScreenshot = (name: string) => {
|
|
const timestamp = Date.now();
|
|
cy.screenshot(`${timestamp}-${name}`, { capture: 'viewport' });
|
|
};
|