08ea5fbe98
添加用户管理视图、API和状态管理文件
567 lines
16 KiB
TypeScript
567 lines
16 KiB
TypeScript
import { Page, expect } from '@playwright/test';
|
|
import { testLogger } from '../core/test-logger';
|
|
|
|
export async function waitForElement(page: Page, selector: string, timeout: number = 10000): Promise<void> {
|
|
testLogger.debug(`等待元素: ${selector}, 超时: ${timeout}ms`);
|
|
|
|
try {
|
|
await page.waitForSelector(selector, {
|
|
state: 'visible',
|
|
timeout
|
|
});
|
|
|
|
testLogger.debug(`元素已可见: ${selector}`);
|
|
} catch (error) {
|
|
testLogger.error(`等待元素超时: ${selector}`, error as Error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function waitForElementHidden(page: Page, selector: string, timeout: number = 10000): Promise<void> {
|
|
testLogger.debug(`等待元素隐藏: ${selector}, 超时: ${timeout}ms`);
|
|
|
|
try {
|
|
await page.waitForSelector(selector, {
|
|
state: 'hidden',
|
|
timeout
|
|
});
|
|
|
|
testLogger.debug(`元素已隐藏: ${selector}`);
|
|
} catch (error) {
|
|
testLogger.error(`等待元素隐藏超时: ${selector}`, error as Error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function waitForText(page: Page, selector: string, text: string, timeout: number = 10000): Promise<void> {
|
|
testLogger.debug(`等待文本: ${selector} 包含 "${text}", 超时: ${timeout}ms`);
|
|
|
|
try {
|
|
const locator = page.locator(selector);
|
|
await expect(locator).toHaveText(text, { timeout });
|
|
|
|
testLogger.debug(`文本已出现: ${selector} 包含 "${text}"`);
|
|
} catch (error) {
|
|
testLogger.error(`等待文本超时: ${selector} 包含 "${text}"`, error as Error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function waitForURL(page: Page, urlPattern: string | RegExp, timeout: number = 10000): Promise<void> {
|
|
testLogger.debug(`等待URL匹配: ${urlPattern}, 超时: ${timeout}ms`);
|
|
|
|
try {
|
|
await page.waitForURL(urlPattern, { timeout });
|
|
|
|
testLogger.debug(`URL已匹配: ${page.url()}`);
|
|
} catch (error) {
|
|
testLogger.error(`等待URL超时: ${urlPattern}`, error as Error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function clickElement(page: Page, selector: string, options?: { timeout?: number; force?: boolean }): Promise<void> {
|
|
testLogger.debug(`点击元素: ${selector}`);
|
|
|
|
try {
|
|
const locator = page.locator(selector);
|
|
await locator.click(options);
|
|
|
|
testLogger.debug(`元素点击成功: ${selector}`);
|
|
} catch (error) {
|
|
testLogger.error(`点击元素失败: ${selector}`, error as Error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function fillInput(page: Page, selector: string, value: string, options?: { timeout?: number }): Promise<void> {
|
|
testLogger.debug(`填充输入框: ${selector}, 值: ${value}`);
|
|
|
|
try {
|
|
const locator = page.locator(selector);
|
|
await locator.fill(value, options);
|
|
|
|
testLogger.debug(`输入框填充成功: ${selector}`);
|
|
} catch (error) {
|
|
testLogger.error(`填充输入框失败: ${selector}`, error as Error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function selectOption(page: Page, selector: string, value: string | string[]): Promise<void> {
|
|
testLogger.debug(`选择下拉选项: ${selector}, 值: ${value}`);
|
|
|
|
try {
|
|
const locator = page.locator(selector);
|
|
await locator.selectOption(value);
|
|
|
|
testLogger.debug(`下拉选项选择成功: ${selector}`);
|
|
} catch (error) {
|
|
testLogger.error(`选择下拉选项失败: ${selector}`, error as Error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function checkCheckbox(page: Page, selector: string): Promise<void> {
|
|
testLogger.debug(`勾选复选框: ${selector}`);
|
|
|
|
try {
|
|
const locator = page.locator(selector);
|
|
await locator.check();
|
|
|
|
testLogger.debug(`复选框勾选成功: ${selector}`);
|
|
} catch (error) {
|
|
testLogger.error(`勾选复选框失败: ${selector}`, error as Error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function uncheckCheckbox(page: Page, selector: string): Promise<void> {
|
|
testLogger.debug(`取消勾选复选框: ${selector}`);
|
|
|
|
try {
|
|
const locator = page.locator(selector);
|
|
await locator.uncheck();
|
|
|
|
testLogger.debug(`复选框取消勾选成功: ${selector}`);
|
|
} catch (error) {
|
|
testLogger.error(`取消勾选复选框失败: ${selector}`, error as Error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function getText(page: Page, selector: string): Promise<string> {
|
|
testLogger.debug(`获取元素文本: ${selector}`);
|
|
|
|
try {
|
|
const locator = page.locator(selector);
|
|
const text = await locator.textContent();
|
|
|
|
testLogger.debug(`元素文本: ${selector} = ${text}`);
|
|
|
|
return text || '';
|
|
} catch (error) {
|
|
testLogger.error(`获取元素文本失败: ${selector}`, error as Error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function getAttribute(page: Page, selector: string, attributeName: string): Promise<string | null> {
|
|
testLogger.debug(`获取元素属性: ${selector}, 属性名: ${attributeName}`);
|
|
|
|
try {
|
|
const locator = page.locator(selector);
|
|
const attribute = await locator.getAttribute(attributeName);
|
|
|
|
testLogger.debug(`元素属性: ${selector}[${attributeName}] = ${attribute}`);
|
|
|
|
return attribute;
|
|
} catch (error) {
|
|
testLogger.error(`获取元素属性失败: ${selector}`, error as Error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function isVisible(page: Page, selector: string): Promise<boolean> {
|
|
try {
|
|
const locator = page.locator(selector);
|
|
return await locator.isVisible({ timeout: 5000 });
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export async function isEnabled(page: Page, selector: string): Promise<boolean> {
|
|
try {
|
|
const locator = page.locator(selector);
|
|
return await locator.isEnabled({ timeout: 5000 });
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export async function isHidden(page: Page, selector: string): Promise<boolean> {
|
|
try {
|
|
const locator = page.locator(selector);
|
|
return await locator.isHidden({ timeout: 5000 });
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export async function isDisabled(page: Page, selector: string): Promise<boolean> {
|
|
try {
|
|
const locator = page.locator(selector);
|
|
return await locator.isDisabled({ timeout: 5000 });
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export async function scrollToElement(page: Page, selector: string): Promise<void> {
|
|
testLogger.debug(`滚动到元素: ${selector}`);
|
|
|
|
try {
|
|
const locator = page.locator(selector);
|
|
await locator.scrollIntoViewIfNeeded();
|
|
|
|
testLogger.debug(`滚动到元素成功: ${selector}`);
|
|
} catch (error) {
|
|
testLogger.error(`滚动到元素失败: ${selector}`, error as Error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function hoverElement(page: Page, selector: string): Promise<void> {
|
|
testLogger.debug(`悬停在元素上: ${selector}`);
|
|
|
|
try {
|
|
const locator = page.locator(selector);
|
|
await locator.hover();
|
|
|
|
testLogger.debug(`悬停成功: ${selector}`);
|
|
} catch (error) {
|
|
testLogger.error(`悬停失败: ${selector}`, error as Error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function doubleClickElement(page: Page, selector: string): Promise<void> {
|
|
testLogger.debug(`双击元素: ${selector}`);
|
|
|
|
try {
|
|
const locator = page.locator(selector);
|
|
await locator.dblclick();
|
|
|
|
testLogger.debug(`双击成功: ${selector}`);
|
|
} catch (error) {
|
|
testLogger.error(`双击失败: ${selector}`, error as Error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function rightClickElement(page: Page, selector: string): Promise<void> {
|
|
testLogger.debug(`右键点击元素: ${selector}`);
|
|
|
|
try {
|
|
const locator = page.locator(selector);
|
|
await locator.click({ button: 'right' });
|
|
|
|
testLogger.debug(`右键点击成功: ${selector}`);
|
|
} catch (error) {
|
|
testLogger.error(`右键点击失败: ${selector}`, error as Error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function uploadFile(page: Page, selector: string, filePath: string): Promise<void> {
|
|
testLogger.debug(`上传文件: ${selector}, 路径: ${filePath}`);
|
|
|
|
try {
|
|
const locator = page.locator(selector);
|
|
await locator.setInputFiles(filePath);
|
|
|
|
testLogger.debug(`文件上传成功: ${filePath}`);
|
|
} catch (error) {
|
|
testLogger.error(`文件上传失败: ${filePath}`, error as Error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function clearInput(page: Page, selector: string): Promise<void> {
|
|
testLogger.debug(`清空输入框: ${selector}`);
|
|
|
|
try {
|
|
const locator = page.locator(selector);
|
|
await locator.clear();
|
|
|
|
testLogger.debug(`输入框已清空: ${selector}`);
|
|
} catch (error) {
|
|
testLogger.error(`清空输入框失败: ${selector}`, error as Error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function pressKey(page: Page, key: string): Promise<void> {
|
|
testLogger.debug(`按键: ${key}`);
|
|
|
|
try {
|
|
await page.keyboard.press(key);
|
|
|
|
testLogger.debug(`按键成功: ${key}`);
|
|
} catch (error) {
|
|
testLogger.error(`按键失败: ${key}`, error as Error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function typeText(page: Page, selector: string, text: string, delay?: number): Promise<void> {
|
|
testLogger.debug(`输入文本: ${selector}, 文本: ${text}`);
|
|
|
|
try {
|
|
const locator = page.locator(selector);
|
|
await locator.type(text, { delay });
|
|
|
|
testLogger.debug(`文本输入成功: ${selector}`);
|
|
} catch (error) {
|
|
testLogger.error(`文本输入失败: ${selector}`, error as Error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function waitForNetworkIdle(page: Page, timeout: number = 30000): Promise<void> {
|
|
testLogger.debug(`等待网络空闲, 超时: ${timeout}ms`);
|
|
|
|
try {
|
|
await page.waitForLoadState('networkidle', { timeout });
|
|
|
|
testLogger.debug('网络已空闲');
|
|
} catch (error) {
|
|
testLogger.error('等待网络空闲超时', error as Error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function waitForLoadState(page: Page, state: 'load' | 'domcontentloaded' | 'networkidle' = 'load', timeout: number = 30000): Promise<void> {
|
|
testLogger.debug(`等待加载状态: ${state}, 超时: ${timeout}ms`);
|
|
|
|
try {
|
|
await page.waitForLoadState(state, { timeout });
|
|
|
|
testLogger.debug(`加载状态已达到: ${state}`);
|
|
} catch (error) {
|
|
testLogger.error(`等待加载状态超时: ${state}`, error as Error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function executeScript(page: Page, script: string, ...args: any[]): Promise<any> {
|
|
testLogger.debug('执行JavaScript脚本');
|
|
|
|
try {
|
|
const result = await page.evaluate(script, ...args);
|
|
|
|
testLogger.debug('JavaScript脚本执行成功');
|
|
|
|
return result;
|
|
} catch (error) {
|
|
testLogger.error('JavaScript脚本执行失败', error as Error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function takeScreenshot(page: Page, name: string, fullPage: boolean = false): Promise<string> {
|
|
testLogger.debug(`截图: ${name}, 全页: ${fullPage}`);
|
|
|
|
try {
|
|
const path = `test-results/screenshots/${name}-${Date.now()}.png`;
|
|
await page.screenshot({ path, fullPage });
|
|
|
|
testLogger.debug(`截图已保存: ${path}`);
|
|
|
|
return path;
|
|
} catch (error) {
|
|
testLogger.error(`截图失败: ${name}`, error as Error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function waitForTimeout(ms: number): Promise<void> {
|
|
testLogger.debug(`等待 ${ms}ms`);
|
|
await new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
export async function retryOperation<T>(
|
|
operation: () => Promise<T>,
|
|
maxRetries: number = 3,
|
|
delay: number = 1000,
|
|
description: string = '操作'
|
|
): Promise<T> {
|
|
let lastError: Error | null = null;
|
|
|
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
try {
|
|
testLogger.debug(`${description} 尝试 ${attempt}/${maxRetries}`);
|
|
const result = await operation();
|
|
|
|
if (attempt > 1) {
|
|
testLogger.info(`${description} 在第 ${attempt} 次尝试后成功`);
|
|
}
|
|
|
|
return result;
|
|
} catch (error) {
|
|
lastError = error as Error;
|
|
testLogger.warn(`${description} 第 ${attempt} 次尝试失败: ${error}`);
|
|
|
|
if (attempt < maxRetries) {
|
|
await waitForTimeout(delay);
|
|
}
|
|
}
|
|
}
|
|
|
|
throw lastError || new Error(`${description} 在 ${maxRetries} 次尝试后仍然失败`);
|
|
}
|
|
|
|
export async function waitUntil(
|
|
condition: () => boolean | Promise<boolean>,
|
|
timeout: number = 10000,
|
|
interval: number = 100,
|
|
description: string = '条件'
|
|
): Promise<void> {
|
|
const startTime = Date.now();
|
|
|
|
while (Date.now() - startTime < timeout) {
|
|
const result = await condition();
|
|
|
|
if (result) {
|
|
testLogger.debug(`${description} 已满足`);
|
|
return;
|
|
}
|
|
|
|
await waitForTimeout(interval);
|
|
}
|
|
|
|
throw new Error(`${description} 在 ${timeout}ms 内未满足`);
|
|
}
|
|
|
|
export function generateRandomString(length: number = 10): string {
|
|
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
let result = '';
|
|
|
|
for (let i = 0; i < length; i++) {
|
|
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
export function generateRandomEmail(): string {
|
|
const username = generateRandomString(8).toLowerCase();
|
|
const domains = ['example.com', 'test.com', 'demo.com'];
|
|
const domain = domains[Math.floor(Math.random() * domains.length)];
|
|
|
|
return `${username}@${domain}`;
|
|
}
|
|
|
|
export function generateRandomPhoneNumber(): string {
|
|
const prefix = ['138', '139', '150', '151', '186', '188'];
|
|
const selectedPrefix = prefix[Math.floor(Math.random() * prefix.length)];
|
|
const suffix = Math.floor(Math.random() * 100000000).toString().padStart(8, '0');
|
|
|
|
return `${selectedPrefix}${suffix}`;
|
|
}
|
|
|
|
export function generateRandomId(): string {
|
|
return `${Date.now()}-${generateRandomString(6)}`;
|
|
}
|
|
|
|
export function formatDate(date: Date, format: string = 'YYYY-MM-DD'): string {
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
const hours = String(date.getHours()).padStart(2, '0');
|
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
|
|
return format
|
|
.replace('YYYY', year.toString())
|
|
.replace('MM', month)
|
|
.replace('DD', day)
|
|
.replace('HH', hours)
|
|
.replace('mm', minutes)
|
|
.replace('ss', seconds);
|
|
}
|
|
|
|
export function parseDate(dateString: string, format: string = 'YYYY-MM-DD'): Date {
|
|
const parts = dateString.match(/(\d+)/g);
|
|
|
|
if (!parts) {
|
|
throw new Error(`无效的日期格式: ${dateString}`);
|
|
}
|
|
|
|
const year = parseInt(parts[0], 10);
|
|
const month = parseInt(parts[1], 10) - 1;
|
|
const day = parseInt(parts[2], 10);
|
|
|
|
return new Date(year, month, day);
|
|
}
|
|
|
|
export function sleep(ms: number): Promise<void> {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
export function debounce(func: Function, wait: number): Function {
|
|
let timeout: NodeJS.Timeout | null = null;
|
|
|
|
return function(...args: any[]) {
|
|
if (timeout) {
|
|
clearTimeout(timeout);
|
|
}
|
|
|
|
timeout = setTimeout(() => {
|
|
func.apply(this, args);
|
|
}, wait);
|
|
};
|
|
}
|
|
|
|
export function throttle(func: Function, limit: number): Function {
|
|
let inThrottle: boolean = false;
|
|
|
|
return function(...args: any[]) {
|
|
if (!inThrottle) {
|
|
func.apply(this, args);
|
|
inThrottle = true;
|
|
|
|
setTimeout(() => {
|
|
inThrottle = false;
|
|
}, limit);
|
|
}
|
|
};
|
|
}
|
|
|
|
export function deepClone<T>(obj: T): T {
|
|
return JSON.parse(JSON.stringify(obj));
|
|
}
|
|
|
|
export function isEmpty(value: any): boolean {
|
|
if (value === null || value === undefined) {
|
|
return true;
|
|
}
|
|
|
|
if (typeof value === 'string' || Array.isArray(value)) {
|
|
return value.length === 0;
|
|
}
|
|
|
|
if (typeof value === 'object') {
|
|
return Object.keys(value).length === 0;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
export function isNotEmpty(value: any): boolean {
|
|
return !isEmpty(value);
|
|
}
|
|
|
|
export function pick<T extends Record<string, any>, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
|
|
const result = {} as Pick<T, K>;
|
|
|
|
for (const key of keys) {
|
|
if (key in obj) {
|
|
result[key] = obj[key];
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
export function omit<T extends Record<string, any>, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {
|
|
const result = { ...obj };
|
|
|
|
for (const key of keys) {
|
|
delete result[key];
|
|
}
|
|
|
|
return result;
|
|
}
|