feat(admin): 添加用户管理相关文件
添加用户管理视图、API和状态管理文件
This commit is contained in:
@@ -0,0 +1,566 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user