feat(admin): 添加用户管理相关文件
添加用户管理视图、API和状态管理文件
This commit is contained in:
@@ -0,0 +1,414 @@
|
||||
import { Page, Locator, expect } from '@playwright/test';
|
||||
import { testConfig } from '../core/test-config';
|
||||
import { testLogger } from '../core/test-logger';
|
||||
import { ScreenshotHelper } from '../helpers/screenshot-helper';
|
||||
|
||||
export class BasePage {
|
||||
protected page: Page;
|
||||
protected screenshotHelper: ScreenshotHelper;
|
||||
protected baseURL: string;
|
||||
protected timeout: {
|
||||
default: number;
|
||||
navigation: number;
|
||||
element: number;
|
||||
network: number;
|
||||
};
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.screenshotHelper = new ScreenshotHelper(page);
|
||||
this.baseURL = testConfig.getBaseURL();
|
||||
this.timeout = testConfig.getEnvironment().timeout;
|
||||
}
|
||||
|
||||
async navigate(path: string = ''): Promise<void> {
|
||||
const url = path.startsWith('http') ? path : `${this.baseURL}${path}`;
|
||||
testLogger.info(`导航到页面: ${url}`);
|
||||
|
||||
try {
|
||||
await this.page.goto(url, {
|
||||
timeout: this.timeout.navigation,
|
||||
waitUntil: 'networkidle'
|
||||
});
|
||||
|
||||
await this.page.waitForLoadState('networkidle', {
|
||||
timeout: this.timeout.network
|
||||
});
|
||||
|
||||
testLogger.info(`页面加载完成: ${url}`);
|
||||
} catch (error) {
|
||||
testLogger.error(`页面导航失败: ${url}`, error as Error);
|
||||
await this.screenshotHelper.takeScreenshot('navigation-error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async reload(): Promise<void> {
|
||||
testLogger.info('重新加载页面');
|
||||
|
||||
try {
|
||||
await this.page.reload({
|
||||
timeout: this.timeout.navigation,
|
||||
waitUntil: 'networkidle'
|
||||
});
|
||||
|
||||
await this.page.waitForLoadState('networkidle', {
|
||||
timeout: this.timeout.network
|
||||
});
|
||||
|
||||
testLogger.info('页面重新加载完成');
|
||||
} catch (error) {
|
||||
testLogger.error('页面重新加载失败', error as Error);
|
||||
await this.screenshotHelper.takeScreenshot('reload-error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async goBack(): Promise<void> {
|
||||
testLogger.info('返回上一页');
|
||||
await this.page.goBack();
|
||||
}
|
||||
|
||||
async goForward(): Promise<void> {
|
||||
testLogger.info('前进到下一页');
|
||||
await this.page.goForward();
|
||||
}
|
||||
|
||||
async waitForLoad(timeout?: number): Promise<void> {
|
||||
const loadTimeout = timeout || this.timeout.navigation;
|
||||
testLogger.debug(`等待页面加载完成, 超时时间: ${loadTimeout}ms`);
|
||||
|
||||
try {
|
||||
await this.page.waitForLoadState('networkidle', {
|
||||
timeout: loadTimeout
|
||||
});
|
||||
|
||||
testLogger.debug('页面加载完成');
|
||||
} catch (error) {
|
||||
testLogger.error('等待页面加载超时', error as Error);
|
||||
await this.screenshotHelper.takeScreenshot('wait-load-error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async waitForURL(urlPattern: string | RegExp, timeout?: number): Promise<void> {
|
||||
const waitTimeout = timeout || this.timeout.navigation;
|
||||
testLogger.debug(`等待URL匹配: ${urlPattern}, 超时时间: ${waitTimeout}ms`);
|
||||
|
||||
try {
|
||||
await this.page.waitForURL(urlPattern, {
|
||||
timeout: waitTimeout
|
||||
});
|
||||
|
||||
testLogger.debug(`URL匹配成功: ${this.page.url()}`);
|
||||
} catch (error) {
|
||||
testLogger.error(`等待URL匹配失败: ${urlPattern}`, error as Error);
|
||||
await this.screenshotHelper.takeScreenshot('wait-url-error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async waitForElement(selector: string, timeout?: number): Promise<Locator> {
|
||||
const waitTimeout = timeout || this.timeout.element;
|
||||
testLogger.debug(`等待元素可见: ${selector}, 超时时间: ${waitTimeout}ms`);
|
||||
|
||||
try {
|
||||
const locator = this.page.locator(selector);
|
||||
await locator.waitFor({
|
||||
state: 'visible',
|
||||
timeout: waitTimeout
|
||||
});
|
||||
|
||||
testLogger.debug(`元素可见: ${selector}`);
|
||||
return locator;
|
||||
} catch (error) {
|
||||
testLogger.error(`等待元素超时: ${selector}`, error as Error);
|
||||
await this.screenshotHelper.takeScreenshot('wait-element-error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async waitForElementHidden(selector: string, timeout?: number): Promise<void> {
|
||||
const waitTimeout = timeout || this.timeout.element;
|
||||
testLogger.debug(`等待元素隐藏: ${selector}, 超时时间: ${waitTimeout}ms`);
|
||||
|
||||
try {
|
||||
const locator = this.page.locator(selector);
|
||||
await locator.waitFor({
|
||||
state: 'hidden',
|
||||
timeout: waitTimeout
|
||||
});
|
||||
|
||||
testLogger.debug(`元素已隐藏: ${selector}`);
|
||||
} catch (error) {
|
||||
testLogger.error(`等待元素隐藏超时: ${selector}`, error as Error);
|
||||
await this.screenshotHelper.takeScreenshot('wait-hidden-error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async click(selector: string, options?: { timeout?: number; force?: boolean }): Promise<void> {
|
||||
const clickTimeout = options?.timeout || this.timeout.element;
|
||||
testLogger.debug(`点击元素: ${selector}`);
|
||||
|
||||
try {
|
||||
const locator = this.page.locator(selector);
|
||||
await locator.waitFor({
|
||||
state: 'visible',
|
||||
timeout: clickTimeout
|
||||
});
|
||||
|
||||
await locator.click({
|
||||
timeout: clickTimeout,
|
||||
force: options?.force
|
||||
});
|
||||
|
||||
testLogger.debug(`元素点击成功: ${selector}`);
|
||||
} catch (error) {
|
||||
testLogger.error(`点击元素失败: ${selector}`, error as Error);
|
||||
await this.screenshotHelper.takeScreenshot('click-error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async fill(selector: string, value: string, options?: { timeout?: number }): Promise<void> {
|
||||
const fillTimeout = options?.timeout || this.timeout.element;
|
||||
testLogger.debug(`填充输入框: ${selector}, 值: ${value}`);
|
||||
|
||||
try {
|
||||
const locator = this.page.locator(selector);
|
||||
await locator.waitFor({
|
||||
state: 'visible',
|
||||
timeout: fillTimeout
|
||||
});
|
||||
|
||||
await locator.fill(value, {
|
||||
timeout: fillTimeout
|
||||
});
|
||||
|
||||
testLogger.debug(`输入框填充成功: ${selector}`);
|
||||
} catch (error) {
|
||||
testLogger.error(`填充输入框失败: ${selector}`, error as Error);
|
||||
await this.screenshotHelper.takeScreenshot('fill-error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async selectOption(selector: string, value: string | string[], options?: { timeout?: number }): Promise<void> {
|
||||
const selectTimeout = options?.timeout || this.timeout.element;
|
||||
testLogger.debug(`选择下拉选项: ${selector}, 值: ${value}`);
|
||||
|
||||
try {
|
||||
const locator = this.page.locator(selector);
|
||||
await locator.waitFor({
|
||||
state: 'visible',
|
||||
timeout: selectTimeout
|
||||
});
|
||||
|
||||
await locator.selectOption(value, {
|
||||
timeout: selectTimeout
|
||||
});
|
||||
|
||||
testLogger.debug(`下拉选项选择成功: ${selector}`);
|
||||
} catch (error) {
|
||||
testLogger.error(`选择下拉选项失败: ${selector}`, error as Error);
|
||||
await this.screenshotHelper.takeScreenshot('select-error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async check(selector: string, options?: { timeout?: number }): Promise<void> {
|
||||
const checkTimeout = options?.timeout || this.timeout.element;
|
||||
testLogger.debug(`勾选复选框: ${selector}`);
|
||||
|
||||
try {
|
||||
const locator = this.page.locator(selector);
|
||||
await locator.waitFor({
|
||||
state: 'visible',
|
||||
timeout: checkTimeout
|
||||
});
|
||||
|
||||
await locator.check({
|
||||
timeout: checkTimeout
|
||||
});
|
||||
|
||||
testLogger.debug(`复选框勾选成功: ${selector}`);
|
||||
} catch (error) {
|
||||
testLogger.error(`勾选复选框失败: ${selector}`, error as Error);
|
||||
await this.screenshotHelper.takeScreenshot('check-error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async uncheck(selector: string, options?: { timeout?: number }): Promise<void> {
|
||||
const uncheckTimeout = options?.timeout || this.timeout.element;
|
||||
testLogger.debug(`取消勾选复选框: ${selector}`);
|
||||
|
||||
try {
|
||||
const locator = this.page.locator(selector);
|
||||
await locator.waitFor({
|
||||
state: 'visible',
|
||||
timeout: uncheckTimeout
|
||||
});
|
||||
|
||||
await locator.uncheck({
|
||||
timeout: uncheckTimeout
|
||||
});
|
||||
|
||||
testLogger.debug(`复选框取消勾选成功: ${selector}`);
|
||||
} catch (error) {
|
||||
testLogger.error(`取消勾选复选框失败: ${selector}`, error as Error);
|
||||
await this.screenshotHelper.takeScreenshot('uncheck-error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getText(selector: string, options?: { timeout?: number }): Promise<string> {
|
||||
const textTimeout = options?.timeout || this.timeout.element;
|
||||
testLogger.debug(`获取元素文本: ${selector}`);
|
||||
|
||||
try {
|
||||
const locator = this.page.locator(selector);
|
||||
await locator.waitFor({
|
||||
state: 'visible',
|
||||
timeout: textTimeout
|
||||
});
|
||||
|
||||
const text = await locator.textContent();
|
||||
testLogger.debug(`元素文本: ${selector} = ${text}`);
|
||||
|
||||
return text || '';
|
||||
} catch (error) {
|
||||
testLogger.error(`获取元素文本失败: ${selector}`, error as Error);
|
||||
await this.screenshotHelper.takeScreenshot('get-text-error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getAttribute(selector: string, attributeName: string, options?: { timeout?: number }): Promise<string | null> {
|
||||
const attrTimeout = options?.timeout || this.timeout.element;
|
||||
testLogger.debug(`获取元素属性: ${selector}, 属性名: ${attributeName}`);
|
||||
|
||||
try {
|
||||
const locator = this.page.locator(selector);
|
||||
await locator.waitFor({
|
||||
state: 'visible',
|
||||
timeout: attrTimeout
|
||||
});
|
||||
|
||||
const attribute = await locator.getAttribute(attributeName);
|
||||
testLogger.debug(`元素属性: ${selector}[${attributeName}] = ${attribute}`);
|
||||
|
||||
return attribute;
|
||||
} catch (error) {
|
||||
testLogger.error(`获取元素属性失败: ${selector}`, error as Error);
|
||||
await this.screenshotHelper.takeScreenshot('get-attribute-error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async isVisible(selector: string): Promise<boolean> {
|
||||
try {
|
||||
const locator = this.page.locator(selector);
|
||||
const visible = await locator.isVisible({ timeout: 5000 });
|
||||
testLogger.debug(`元素可见性: ${selector} = ${visible}`);
|
||||
|
||||
return visible;
|
||||
} catch (error) {
|
||||
testLogger.debug(`元素不可见: ${selector}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async isEnabled(selector: string): Promise<boolean> {
|
||||
try {
|
||||
const locator = this.page.locator(selector);
|
||||
const enabled = await locator.isEnabled({ timeout: 5000 });
|
||||
testLogger.debug(`元素可用性: ${selector} = ${enabled}`);
|
||||
|
||||
return enabled;
|
||||
} catch (error) {
|
||||
testLogger.debug(`元素不可用: ${selector}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async waitForTimeout(ms: number): Promise<void> {
|
||||
testLogger.debug(`等待 ${ms}ms`);
|
||||
await this.page.waitForTimeout(ms);
|
||||
}
|
||||
|
||||
async executeScript(script: string, ...args: any[]): Promise<any> {
|
||||
testLogger.debug('执行JavaScript脚本');
|
||||
|
||||
try {
|
||||
const result = await this.page.evaluate(script, ...args);
|
||||
testLogger.debug('JavaScript脚本执行成功');
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
testLogger.error('JavaScript脚本执行失败', error as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async scrollToElement(selector: string): Promise<void> {
|
||||
testLogger.debug(`滚动到元素: ${selector}`);
|
||||
|
||||
try {
|
||||
const locator = this.page.locator(selector);
|
||||
await locator.scrollIntoViewIfNeeded();
|
||||
|
||||
testLogger.debug(`滚动到元素成功: ${selector}`);
|
||||
} catch (error) {
|
||||
testLogger.error(`滚动到元素失败: ${selector}`, error as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async takeScreenshot(name: string): Promise<string> {
|
||||
return await this.screenshotHelper.takeScreenshot(name);
|
||||
}
|
||||
|
||||
async takeElementScreenshot(selector: string, name: string): Promise<string> {
|
||||
return await this.screenshotHelper.takeElementScreenshot(selector, name);
|
||||
}
|
||||
|
||||
getCurrentURL(): string {
|
||||
return this.page.url();
|
||||
}
|
||||
|
||||
getTitle(): Promise<string> {
|
||||
return this.page.title();
|
||||
}
|
||||
|
||||
async expectVisible(selector: string, timeout?: number): Promise<void> {
|
||||
const locator = await this.waitForElement(selector, timeout);
|
||||
await expect(locator).toBeVisible();
|
||||
}
|
||||
|
||||
async expectHidden(selector: string, timeout?: number): Promise<void> {
|
||||
const locator = this.page.locator(selector);
|
||||
await expect(locator).toBeHidden({ timeout });
|
||||
}
|
||||
|
||||
async expectText(selector: string, expectedText: string, timeout?: number): Promise<void> {
|
||||
const locator = await this.waitForElement(selector, timeout);
|
||||
await expect(locator).toHaveText(expectedText);
|
||||
}
|
||||
|
||||
async expectValue(selector: string, expectedValue: string, timeout?: number): Promise<void> {
|
||||
const locator = await this.waitForElement(selector, timeout);
|
||||
await expect(locator).toHaveValue(expectedValue);
|
||||
}
|
||||
|
||||
async expectAttribute(selector: string, attributeName: string, expectedValue: string, timeout?: number): Promise<void> {
|
||||
const locator = await this.waitForElement(selector, timeout);
|
||||
await expect(locator).toHaveAttribute(attributeName, expectedValue);
|
||||
}
|
||||
|
||||
async expectCount(selector: string, expectedCount: number, timeout?: number): Promise<void> {
|
||||
const locator = this.page.locator(selector);
|
||||
await expect(locator).toHaveCount(expectedCount, { timeout });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import { Page, expect } from '@playwright/test';
|
||||
import { BasePage } from './base-page';
|
||||
import { SELECTORS, TIMEOUTS } from '../constants';
|
||||
|
||||
export class DashboardPage extends BasePage {
|
||||
private readonly selectors = {
|
||||
dashboardContainer: '.dashboard-container',
|
||||
pageTitle: '.page-title',
|
||||
statisticsCards: '.statistic-card',
|
||||
charts: '.chart-container',
|
||||
menuItems: '.ant-menu-item',
|
||||
welcomeMessage: '.welcome-message',
|
||||
quickActions: '.quick-action'
|
||||
};
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
}
|
||||
|
||||
async navigate(): Promise<void> {
|
||||
testLogger.info('导航到仪表盘页面');
|
||||
await super.navigate('/dashboard');
|
||||
}
|
||||
|
||||
async waitForLoad(): Promise<void> {
|
||||
testLogger.info('等待仪表盘页面加载');
|
||||
|
||||
try {
|
||||
await this.page.waitForSelector(this.selectors.dashboardContainer, {
|
||||
state: 'visible',
|
||||
timeout: this.timeout.default
|
||||
});
|
||||
|
||||
testLogger.info('仪表盘页面加载完成');
|
||||
} catch (error) {
|
||||
testLogger.error('仪表盘页面加载超时', error as Error);
|
||||
await this.screenshotHelper.takeScreenshot('dashboard-load-error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getPageTitle(): Promise<string> {
|
||||
testLogger.debug('获取页面标题');
|
||||
|
||||
try {
|
||||
const titleElement = this.page.locator(this.selectors.pageTitle);
|
||||
await titleElement.waitFor({ state: 'visible', timeout: this.timeout.element });
|
||||
|
||||
const title = await titleElement.textContent();
|
||||
testLogger.debug(`页面标题: ${title}`);
|
||||
|
||||
return title || '';
|
||||
} catch (error) {
|
||||
testLogger.error('获取页面标题失败', error as Error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async getWelcomeMessage(): Promise<string> {
|
||||
testLogger.debug('获取欢迎消息');
|
||||
|
||||
try {
|
||||
const welcomeElement = this.page.locator(this.selectors.welcomeMessage);
|
||||
await welcomeElement.waitFor({ state: 'visible', timeout: this.timeout.element });
|
||||
|
||||
const message = await welcomeElement.textContent();
|
||||
testLogger.debug(`欢迎消息: ${message}`);
|
||||
|
||||
return message || '';
|
||||
} catch (error) {
|
||||
testLogger.error('获取欢迎消息失败', error as Error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async getStatisticsCardCount(): Promise<number> {
|
||||
testLogger.debug('获取统计卡片数量');
|
||||
|
||||
try {
|
||||
const cards = this.page.locator(this.selectors.statisticsCards);
|
||||
const count = await cards.count();
|
||||
|
||||
testLogger.debug(`统计卡片数量: ${count}`);
|
||||
|
||||
return count;
|
||||
} catch (error) {
|
||||
testLogger.error('获取统计卡片数量失败', error as Error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
async getChartCount(): Promise<number> {
|
||||
testLogger.debug('获取图表数量');
|
||||
|
||||
try {
|
||||
const charts = this.page.locator(this.selectors.charts);
|
||||
const count = await charts.count();
|
||||
|
||||
testLogger.debug(`图表数量: ${count}`);
|
||||
|
||||
return count;
|
||||
} catch (error) {
|
||||
testLogger.error('获取图表数量失败', error as Error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
async clickMenuItem(menuName: string): Promise<void> {
|
||||
testLogger.info(`点击菜单项: ${menuName}`);
|
||||
|
||||
try {
|
||||
const menuItem = this.page.locator(this.selectors.menuItems).filter({ hasText: menuName });
|
||||
await menuItem.waitFor({ state: 'visible', timeout: this.timeout.element });
|
||||
|
||||
await menuItem.click();
|
||||
|
||||
testLogger.info(`菜单项点击成功: ${menuName}`);
|
||||
} catch (error) {
|
||||
testLogger.error(`点击菜单项失败: ${menuName}`, error as Error);
|
||||
await this.screenshotHelper.takeScreenshot(`click-menu-${menuName}-error`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async clickQuickAction(actionName: string): Promise<void> {
|
||||
testLogger.info(`点击快捷操作: ${actionName}`);
|
||||
|
||||
try {
|
||||
const quickAction = this.page.locator(this.selectors.quickActions).filter({ hasText: actionName });
|
||||
await quickAction.waitFor({ state: 'visible', timeout: this.timeout.element });
|
||||
|
||||
await quickAction.click();
|
||||
|
||||
testLogger.info(`快捷操作点击成功: ${actionName}`);
|
||||
} catch (error) {
|
||||
testLogger.error(`点击快捷操作失败: ${actionName}`, error as Error);
|
||||
await this.screenshotHelper.takeScreenshot(`click-quick-action-${actionName}-error`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async isDashboardVisible(): Promise<boolean> {
|
||||
testLogger.debug('检查仪表盘是否可见');
|
||||
|
||||
try {
|
||||
const dashboard = this.page.locator(this.selectors.dashboardContainer);
|
||||
const isVisible = await dashboard.isVisible();
|
||||
|
||||
testLogger.debug(`仪表盘可见性: ${isVisible}`);
|
||||
|
||||
return isVisible;
|
||||
} catch (error) {
|
||||
testLogger.error('检查仪表盘可见性失败', error as Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async waitForStatistics(): Promise<void> {
|
||||
testLogger.info('等待统计数据加载');
|
||||
|
||||
try {
|
||||
await this.page.waitForSelector(this.selectors.statisticsCards, {
|
||||
state: 'visible',
|
||||
timeout: this.timeout.network
|
||||
});
|
||||
|
||||
testLogger.info('统计数据加载完成');
|
||||
} catch (error) {
|
||||
testLogger.error('等待统计数据加载超时', error as Error);
|
||||
await this.screenshotHelper.takeScreenshot('statistics-load-error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async waitForCharts(): Promise<void> {
|
||||
testLogger.info('等待图表加载');
|
||||
|
||||
try {
|
||||
await this.page.waitForSelector(this.selectors.charts, {
|
||||
state: 'visible',
|
||||
timeout: this.timeout.network
|
||||
});
|
||||
|
||||
testLogger.info('图表加载完成');
|
||||
} catch (error) {
|
||||
testLogger.error('等待图表加载超时', error as Error);
|
||||
await this.screenshotHelper.takeScreenshot('charts-load-error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
import { Page, expect } from '@playwright/test';
|
||||
import { BasePage } from './base-page';
|
||||
import { testLogger } from '../core/test-logger';
|
||||
|
||||
export class LoginPage extends BasePage {
|
||||
private readonly selectors = {
|
||||
usernameInput: '[data-testid="username-input"]',
|
||||
passwordInput: '[data-testid="password-input"]',
|
||||
loginButton: '[data-testid="login-button"]',
|
||||
errorMessage: '.ant-message-error',
|
||||
successMessage: '.ant-message-success',
|
||||
loginForm: '[data-testid="login-form"]',
|
||||
rememberMeCheckbox: '[data-testid="remember-checkbox"]',
|
||||
forgotPasswordLink: 'text=忘记密码',
|
||||
registerLink: 'text=注册账号'
|
||||
};
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
}
|
||||
|
||||
async navigate(): Promise<void> {
|
||||
testLogger.info('导航到登录页面');
|
||||
await super.navigate('/login');
|
||||
}
|
||||
|
||||
async waitForLoad(): Promise<void> {
|
||||
testLogger.debug('等待登录页面加载完成');
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
this.waitForElement(this.selectors.usernameInput),
|
||||
this.waitForElement(this.selectors.passwordInput),
|
||||
this.waitForElement(this.selectors.loginButton)
|
||||
]);
|
||||
|
||||
testLogger.info('登录页面加载完成');
|
||||
} catch (error) {
|
||||
testLogger.error('登录页面加载失败', error as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getUsernameInput() {
|
||||
return this.page.locator(this.selectors.usernameInput);
|
||||
}
|
||||
|
||||
async getPasswordInput() {
|
||||
return this.page.locator(this.selectors.passwordInput);
|
||||
}
|
||||
|
||||
async getLoginButton() {
|
||||
return this.page.locator(this.selectors.loginButton);
|
||||
}
|
||||
|
||||
async getErrorMessage() {
|
||||
return this.page.locator(this.selectors.errorMessage);
|
||||
}
|
||||
|
||||
async getSuccessMessage() {
|
||||
return this.page.locator(this.selectors.successMessage);
|
||||
}
|
||||
|
||||
async fillUsername(username: string): Promise<void> {
|
||||
testLogger.info(`填写用户名: ${username}`);
|
||||
await this.fill(this.selectors.usernameInput, username);
|
||||
}
|
||||
|
||||
async fillPassword(password: string): Promise<void> {
|
||||
testLogger.info('填写密码');
|
||||
await this.fill(this.selectors.passwordInput, password);
|
||||
}
|
||||
|
||||
async clickLoginButton(): Promise<void> {
|
||||
testLogger.info('点击登录按钮');
|
||||
await this.click(this.selectors.loginButton);
|
||||
}
|
||||
|
||||
async toggleRememberMe(remember: boolean): Promise<void> {
|
||||
testLogger.info(`设置记住密码: ${remember}`);
|
||||
|
||||
if (remember) {
|
||||
await this.check(this.selectors.rememberMeCheckbox);
|
||||
} else {
|
||||
await this.uncheck(this.selectors.rememberMeCheckbox);
|
||||
}
|
||||
}
|
||||
|
||||
async login(username: string, password: string, rememberMe: boolean = false): Promise<void> {
|
||||
testLogger.startStep('用户登录');
|
||||
|
||||
try {
|
||||
await this.waitForLoad();
|
||||
await this.fillUsername(username);
|
||||
await this.fillPassword(password);
|
||||
|
||||
if (rememberMe) {
|
||||
await this.toggleRememberMe(true);
|
||||
}
|
||||
|
||||
await this.clickLoginButton();
|
||||
|
||||
testLogger.endStep('用户登录', 'passed');
|
||||
} catch (error) {
|
||||
testLogger.endStep('用户登录', 'failed', error as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async loginAndWaitForDashboard(username: string, password: string, rememberMe: boolean = false): Promise<void> {
|
||||
testLogger.startStep('登录并等待跳转到仪表盘');
|
||||
|
||||
try {
|
||||
await this.login(username, password, rememberMe);
|
||||
|
||||
await this.waitForURL(/.*dashboard/, this.timeout.navigation);
|
||||
|
||||
testLogger.endStep('登录并等待跳转到仪表盘', 'passed');
|
||||
} catch (error) {
|
||||
testLogger.endStep('登录并等待跳转到仪表盘', 'failed', error as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async expectErrorMessage(message: string): Promise<void> {
|
||||
testLogger.debug(`期望错误消息: ${message}`);
|
||||
|
||||
const errorLocator = this.getErrorMessage();
|
||||
await expect(errorLocator).toBeVisible({ timeout: 5000 });
|
||||
await expect(errorLocator).toContainText(message);
|
||||
}
|
||||
|
||||
async expectSuccessMessage(message: string): Promise<void> {
|
||||
testLogger.debug(`期望成功消息: ${message}`);
|
||||
|
||||
const successLocator = this.getSuccessMessage();
|
||||
await expect(successLocator).toBeVisible({ timeout: 5000 });
|
||||
await expect(successLocator).toContainText(message);
|
||||
}
|
||||
|
||||
async hasErrorMessage(): Promise<boolean> {
|
||||
const errorLocator = this.getErrorMessage();
|
||||
const count = await errorLocator.count();
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
async hasSuccessMessage(): Promise<boolean> {
|
||||
const successLocator = this.getSuccessMessage();
|
||||
const count = await successLocator.count();
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
async getErrorMessageText(): Promise<string> {
|
||||
const errorLocator = this.getErrorMessage();
|
||||
const text = await errorLocator.textContent();
|
||||
return text || '';
|
||||
}
|
||||
|
||||
async getSuccessMessageText(): Promise<string> {
|
||||
const successLocator = this.getSuccessMessage();
|
||||
const text = await successLocator.textContent();
|
||||
return text || '';
|
||||
}
|
||||
|
||||
async isLoginButtonEnabled(): Promise<boolean> {
|
||||
const loginButton = this.getLoginButton();
|
||||
return await loginButton.isEnabled();
|
||||
}
|
||||
|
||||
async isUsernameInputVisible(): Promise<boolean> {
|
||||
return await this.isVisible(this.selectors.usernameInput);
|
||||
}
|
||||
|
||||
async isPasswordInputVisible(): Promise<boolean> {
|
||||
return await this.isVisible(this.selectors.passwordInput);
|
||||
}
|
||||
|
||||
async isLoginFormVisible(): Promise<boolean> {
|
||||
return await this.isVisible(this.selectors.loginForm);
|
||||
}
|
||||
|
||||
async clearUsername(): Promise<void> {
|
||||
testLogger.info('清空用户名输入框');
|
||||
const usernameInput = this.getUsernameInput();
|
||||
await usernameInput.fill('');
|
||||
}
|
||||
|
||||
async clearPassword(): Promise<void> {
|
||||
testLogger.info('清空密码输入框');
|
||||
const passwordInput = this.getPasswordInput();
|
||||
await passwordInput.fill('');
|
||||
}
|
||||
|
||||
async clearAllFields(): Promise<void> {
|
||||
await this.clearUsername();
|
||||
await this.clearPassword();
|
||||
}
|
||||
|
||||
async clickForgotPassword(): Promise<void> {
|
||||
testLogger.info('点击忘记密码链接');
|
||||
await this.click(this.selectors.forgotPasswordLink);
|
||||
}
|
||||
|
||||
async clickRegister(): Promise<void> {
|
||||
testLogger.info('点击注册账号链接');
|
||||
await this.click(this.selectors.registerLink);
|
||||
}
|
||||
|
||||
async pressEnter(): Promise<void> {
|
||||
testLogger.info('按Enter键提交登录');
|
||||
await this.page.keyboard.press('Enter');
|
||||
}
|
||||
|
||||
async loginWithEnter(username: string, password: string): Promise<void> {
|
||||
testLogger.startStep('使用Enter键登录');
|
||||
|
||||
try {
|
||||
await this.waitForLoad();
|
||||
await this.fillUsername(username);
|
||||
await this.fillPassword(password);
|
||||
await this.pressEnter();
|
||||
|
||||
testLogger.endStep('使用Enter键登录', 'passed');
|
||||
} catch (error) {
|
||||
testLogger.endStep('使用Enter键登录', 'failed', error as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async takeScreenshotOnLogin(name: string = 'login-page'): Promise<string> {
|
||||
return await this.takeScreenshot(name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
import { Page } from '@playwright/test';
|
||||
import { BasePage } from './base-page';
|
||||
import { FormHelper } from '../helpers/form-helper';
|
||||
import { TableHelper } from '../helpers/table-helper';
|
||||
import { ScreenshotHelper } from '../helpers/screenshot-helper';
|
||||
import { testLogger } from '../core/test-logger';
|
||||
|
||||
export class MenuManagementPage extends BasePage {
|
||||
private formHelper: FormHelper;
|
||||
private tableHelper: TableHelper;
|
||||
private screenshotHelper: ScreenshotHelper;
|
||||
|
||||
private readonly selectors = {
|
||||
menuTree: '.menu-tree',
|
||||
menuTable: '.menu-table',
|
||||
addMenuButton: 'button:has-text("新增")',
|
||||
editButton: 'button:has-text("编辑")',
|
||||
deleteButton: 'button:has-text("删除")',
|
||||
searchInput: 'input[placeholder*="搜索"]',
|
||||
searchButton: 'button:has-text("查询")',
|
||||
resetButton: 'button:has-text("重置")',
|
||||
modal: '.ant-modal',
|
||||
modalTitle: '.ant-modal-title',
|
||||
modalConfirmButton: '.ant-modal-confirm-btn',
|
||||
modalCancelButton: '.ant-modal-cancel-btn',
|
||||
successMessage: '.ant-message-success',
|
||||
errorMessage: '.ant-message-error',
|
||||
menuForm: '.menu-form',
|
||||
menuNameInput: 'input[name="menuName"]',
|
||||
menuTypeSelect: 'select[name="menuType"]',
|
||||
menuIconInput: 'input[name="icon"]',
|
||||
orderNumInput: 'input[name="orderNum"]',
|
||||
pathInput: 'input[name="path"]',
|
||||
componentInput: 'input[name="component"]',
|
||||
statusSelect: 'select[name="status"]',
|
||||
visibleSelect: 'select[name="visible"]',
|
||||
remarkInput: 'textarea[name="remark"]',
|
||||
treeNode: '.ant-tree-node',
|
||||
treeExpandButton: '.ant-tree-switcher'
|
||||
};
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
this.formHelper = new FormHelper(page);
|
||||
this.tableHelper = new TableHelper(page);
|
||||
this.screenshotHelper = new ScreenshotHelper(page);
|
||||
}
|
||||
|
||||
async navigate(): Promise<void> {
|
||||
testLogger.info('导航到菜单管理页面');
|
||||
await super.navigate('/system/menu');
|
||||
}
|
||||
|
||||
async waitForLoad(): Promise<void> {
|
||||
testLogger.info('等待菜单管理页面加载');
|
||||
|
||||
try {
|
||||
await this.page.waitForSelector(this.selectors.menuTree, {
|
||||
state: 'visible',
|
||||
timeout: this.timeout.default
|
||||
});
|
||||
|
||||
testLogger.info('菜单管理页面加载完成');
|
||||
} catch (error) {
|
||||
testLogger.error('菜单管理页面加载超时', error as Error);
|
||||
await this.screenshotHelper.takeScreenshot('menu-management-load-error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async clickAddMenu(): Promise<void> {
|
||||
testLogger.info('点击新增菜单按钮');
|
||||
|
||||
try {
|
||||
await this.page.waitForSelector(this.selectors.addMenuButton, {
|
||||
state: 'visible',
|
||||
timeout: this.timeout.element
|
||||
});
|
||||
|
||||
await this.page.click(this.selectors.addMenuButton);
|
||||
|
||||
await this.page.waitForSelector(this.selectors.modal, {
|
||||
state: 'visible',
|
||||
timeout: this.timeout.element
|
||||
});
|
||||
|
||||
testLogger.info('新增菜单对话框已打开');
|
||||
} catch (error) {
|
||||
testLogger.error('点击新增菜单按钮失败', error as Error);
|
||||
await this.screenshotHelper.takeScreenshot('click-add-menu-error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async clickEditMenu(menuName: string): Promise<void> {
|
||||
testLogger.info(`点击编辑菜单按钮,菜单名称: ${menuName}`);
|
||||
|
||||
try {
|
||||
const editButtons = this.page.locator(this.selectors.editButton);
|
||||
await editButtons.first().waitFor({ state: 'visible', timeout: this.timeout.element });
|
||||
|
||||
await editButtons.first().click();
|
||||
|
||||
await this.page.waitForSelector(this.selectors.modal, {
|
||||
state: 'visible',
|
||||
timeout: this.timeout.element
|
||||
});
|
||||
|
||||
testLogger.info('编辑菜单对话框已打开');
|
||||
} catch (error) {
|
||||
testLogger.error(`点击编辑菜单按钮失败,菜单名称: ${menuName}`, error as Error);
|
||||
await this.screenshotHelper.takeScreenshot(`click-edit-menu-${menuName}-error`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async clickDeleteMenu(menuName: string): Promise<void> {
|
||||
testLogger.info(`点击删除菜单按钮,菜单名称: ${menuName}`);
|
||||
|
||||
try {
|
||||
const deleteButtons = this.page.locator(this.selectors.deleteButton);
|
||||
await deleteButtons.first().waitFor({ state: 'visible', timeout: this.timeout.element });
|
||||
|
||||
await deleteButtons.first().click();
|
||||
|
||||
testLogger.info('删除确认对话框已打开');
|
||||
} catch (error) {
|
||||
testLogger.error(`点击删除菜单按钮失败,菜单名称: ${menuName}`, error as Error);
|
||||
await this.screenshotHelper.takeScreenshot(`click-delete-menu-${menuName}-error`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async confirmDelete(): Promise<void> {
|
||||
testLogger.info('确认删除菜单');
|
||||
|
||||
try {
|
||||
await this.page.waitForSelector(this.selectors.modalConfirmButton, {
|
||||
state: 'visible',
|
||||
timeout: this.timeout.element
|
||||
});
|
||||
|
||||
await this.page.click(this.selectors.modalConfirmButton);
|
||||
|
||||
await this.page.waitForSelector(this.selectors.modal, {
|
||||
state: 'hidden',
|
||||
timeout: this.timeout.element
|
||||
});
|
||||
|
||||
testLogger.info('菜单删除确认成功');
|
||||
} catch (error) {
|
||||
testLogger.error('确认删除菜单失败', error as Error);
|
||||
await this.screenshotHelper.takeScreenshot('confirm-delete-error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async searchMenu(keyword: string): Promise<void> {
|
||||
testLogger.info(`搜索菜单,关键词: ${keyword}`);
|
||||
|
||||
try {
|
||||
await this.page.waitForSelector(this.selectors.searchInput, {
|
||||
state: 'visible',
|
||||
timeout: this.timeout.element
|
||||
});
|
||||
|
||||
await this.page.fill(this.selectors.searchInput, keyword);
|
||||
await this.page.click(this.selectors.searchButton);
|
||||
|
||||
await this.page.waitForLoadState('networkidle', {
|
||||
timeout: this.timeout.network
|
||||
});
|
||||
|
||||
testLogger.info('菜单搜索完成');
|
||||
} catch (error) {
|
||||
testLogger.error(`搜索菜单失败,关键词: ${keyword}`, error as Error);
|
||||
await this.screenshotHelper.takeScreenshot('search-menu-error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async expandTreeNode(nodeIndex: number): Promise<void> {
|
||||
testLogger.info(`展开树节点,索引: ${nodeIndex}`);
|
||||
|
||||
try {
|
||||
const expandButtons = this.page.locator(this.selectors.treeExpandButton);
|
||||
await expandButtons.nth(nodeIndex).waitFor({ state: 'visible', timeout: this.timeout.element });
|
||||
|
||||
await expandButtons.nth(nodeIndex).click();
|
||||
|
||||
testLogger.info(`树节点已展开,索引: ${nodeIndex}`);
|
||||
} catch (error) {
|
||||
testLogger.error(`展开树节点失败,索引: ${nodeIndex}`, error as Error);
|
||||
await this.screenshotHelper.takeScreenshot(`expand-tree-node-${nodeIndex}-error`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getSuccessMessage(): Promise<string> {
|
||||
testLogger.debug('获取成功消息');
|
||||
|
||||
try {
|
||||
const successElement = this.page.locator(this.selectors.successMessage);
|
||||
await successElement.waitFor({ state: 'visible', timeout: this.timeout.element });
|
||||
|
||||
const message = await successElement.textContent();
|
||||
testLogger.debug(`成功消息: ${message}`);
|
||||
|
||||
return message || '';
|
||||
} catch (error) {
|
||||
testLogger.error('获取成功消息失败', error as Error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async getErrorMessage(): Promise<string> {
|
||||
testLogger.debug('获取错误消息');
|
||||
|
||||
try {
|
||||
const errorElement = this.page.locator(this.selectors.errorMessage);
|
||||
await errorElement.waitFor({ state: 'visible', timeout: this.timeout.element });
|
||||
|
||||
const message = await errorElement.textContent();
|
||||
testLogger.debug(`错误消息: ${message}`);
|
||||
|
||||
return message || '';
|
||||
} catch (error) {
|
||||
testLogger.error('获取错误消息失败', error as Error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async getMenuCount(): Promise<number> {
|
||||
testLogger.debug('获取菜单数量');
|
||||
|
||||
try {
|
||||
const count = await this.tableHelper.getRowCount(this.selectors.menuTable);
|
||||
testLogger.debug(`菜单数量: ${count}`);
|
||||
|
||||
return count;
|
||||
} catch (error) {
|
||||
testLogger.error('获取菜单数量失败', error as Error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
async getTreeNodeCount(): Promise<number> {
|
||||
testLogger.debug('获取树节点数量');
|
||||
|
||||
try {
|
||||
const treeNodes = this.page.locator(this.selectors.treeNode);
|
||||
const count = await treeNodes.count();
|
||||
|
||||
testLogger.debug(`树节点数量: ${count}`);
|
||||
|
||||
return count;
|
||||
} catch (error) {
|
||||
testLogger.error('获取树节点数量失败', error as Error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
import { Page } from '@playwright/test';
|
||||
import { BasePage } from './base-page';
|
||||
import { FormHelper } from '../helpers/form-helper';
|
||||
import { TableHelper } from '../helpers/table-helper';
|
||||
import { ScreenshotHelper } from '../helpers/screenshot-helper';
|
||||
import { testLogger } from '../core/test-logger';
|
||||
|
||||
export class RoleManagementPage extends BasePage {
|
||||
private formHelper: FormHelper;
|
||||
private tableHelper: TableHelper;
|
||||
private screenshotHelper: ScreenshotHelper;
|
||||
|
||||
private readonly selectors = {
|
||||
roleTable: '.role-table',
|
||||
addRoleButton: 'button:has-text("新增")',
|
||||
editButton: 'button:has-text("编辑")',
|
||||
deleteButton: 'button:has-text("删除")',
|
||||
searchInput: 'input[placeholder*="搜索"]',
|
||||
searchButton: 'button:has-text("查询")',
|
||||
resetButton: 'button:has-text("重置")',
|
||||
modal: '.ant-modal',
|
||||
modalTitle: '.ant-modal-title',
|
||||
modalConfirmButton: '.ant-modal-confirm-btn',
|
||||
modalCancelButton: '.ant-modal-cancel-btn',
|
||||
successMessage: '.ant-message-success',
|
||||
errorMessage: '.ant-message-error',
|
||||
roleForm: '.role-form',
|
||||
roleNameInput: 'input[name="roleName"]',
|
||||
roleKeyInput: 'input[name="roleKey"]',
|
||||
roleSortInput: 'input[name="roleSort"]',
|
||||
statusSelect: 'select[name="status"]',
|
||||
remarkInput: 'textarea[name="remark"]',
|
||||
permissionTree: '.permission-tree',
|
||||
permissionCheckbox: '.ant-tree-checkbox'
|
||||
};
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
this.formHelper = new FormHelper(page);
|
||||
this.tableHelper = new TableHelper(page);
|
||||
this.screenshotHelper = new ScreenshotHelper(page);
|
||||
}
|
||||
|
||||
async navigate(): Promise<void> {
|
||||
testLogger.info('导航到角色管理页面');
|
||||
await super.navigate('/system/role');
|
||||
}
|
||||
|
||||
async waitForLoad(): Promise<void> {
|
||||
testLogger.info('等待角色管理页面加载');
|
||||
|
||||
try {
|
||||
await this.page.waitForSelector(this.selectors.roleTable, {
|
||||
state: 'visible',
|
||||
timeout: this.timeout.default
|
||||
});
|
||||
|
||||
testLogger.info('角色管理页面加载完成');
|
||||
} catch (error) {
|
||||
testLogger.error('角色管理页面加载超时', error as Error);
|
||||
await this.screenshotHelper.takeScreenshot('role-management-load-error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async clickAddRole(): Promise<void> {
|
||||
testLogger.info('点击新增角色按钮');
|
||||
|
||||
try {
|
||||
await this.page.waitForSelector(this.selectors.addRoleButton, {
|
||||
state: 'visible',
|
||||
timeout: this.timeout.element
|
||||
});
|
||||
|
||||
await this.page.click(this.selectors.addRoleButton);
|
||||
|
||||
await this.page.waitForSelector(this.selectors.modal, {
|
||||
state: 'visible',
|
||||
timeout: this.timeout.element
|
||||
});
|
||||
|
||||
testLogger.info('新增角色对话框已打开');
|
||||
} catch (error) {
|
||||
testLogger.error('点击新增角色按钮失败', error as Error);
|
||||
await this.screenshotHelper.takeScreenshot('click-add-role-error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async clickEditRole(rowIndex: number): Promise<void> {
|
||||
testLogger.info(`点击编辑角色按钮,行索引: ${rowIndex}`);
|
||||
|
||||
try {
|
||||
const editButtons = this.page.locator(this.selectors.editButton);
|
||||
await editButtons.nth(rowIndex).waitFor({ state: 'visible', timeout: this.timeout.element });
|
||||
|
||||
await editButtons.nth(rowIndex).click();
|
||||
|
||||
await this.page.waitForSelector(this.selectors.modal, {
|
||||
state: 'visible',
|
||||
timeout: this.timeout.element
|
||||
});
|
||||
|
||||
testLogger.info('编辑角色对话框已打开');
|
||||
} catch (error) {
|
||||
testLogger.error(`点击编辑角色按钮失败,行索引: ${rowIndex}`, error as Error);
|
||||
await this.screenshotHelper.takeScreenshot(`click-edit-role-${rowIndex}-error`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async clickDeleteRole(rowIndex: number): Promise<void> {
|
||||
testLogger.info(`点击删除角色按钮,行索引: ${rowIndex}`);
|
||||
|
||||
try {
|
||||
const deleteButtons = this.page.locator(this.selectors.deleteButton);
|
||||
await deleteButtons.nth(rowIndex).waitFor({ state: 'visible', timeout: this.timeout.element });
|
||||
|
||||
await deleteButtons.nth(rowIndex).click();
|
||||
|
||||
testLogger.info('删除确认对话框已打开');
|
||||
} catch (error) {
|
||||
testLogger.error(`点击删除角色按钮失败,行索引: ${rowIndex}`, error as Error);
|
||||
await this.screenshotHelper.takeScreenshot(`click-delete-role-${rowIndex}-error`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async confirmDelete(): Promise<void> {
|
||||
testLogger.info('确认删除角色');
|
||||
|
||||
try {
|
||||
await this.page.waitForSelector(this.selectors.modalConfirmButton, {
|
||||
state: 'visible',
|
||||
timeout: this.timeout.element
|
||||
});
|
||||
|
||||
await this.page.click(this.selectors.modalConfirmButton);
|
||||
|
||||
await this.page.waitForSelector(this.selectors.modal, {
|
||||
state: 'hidden',
|
||||
timeout: this.timeout.element
|
||||
});
|
||||
|
||||
testLogger.info('角色删除确认成功');
|
||||
} catch (error) {
|
||||
testLogger.error('确认删除角色失败', error as Error);
|
||||
await this.screenshotHelper.takeScreenshot('confirm-delete-error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async searchRole(keyword: string): Promise<void> {
|
||||
testLogger.info(`搜索角色,关键词: ${keyword}`);
|
||||
|
||||
try {
|
||||
await this.page.waitForSelector(this.selectors.searchInput, {
|
||||
state: 'visible',
|
||||
timeout: this.timeout.element
|
||||
});
|
||||
|
||||
await this.page.fill(this.selectors.searchInput, keyword);
|
||||
await this.page.click(this.selectors.searchButton);
|
||||
|
||||
await this.page.waitForLoadState('networkidle', {
|
||||
timeout: this.timeout.network
|
||||
});
|
||||
|
||||
testLogger.info('角色搜索完成');
|
||||
} catch (error) {
|
||||
testLogger.error(`搜索角色失败,关键词: ${keyword}`, error as Error);
|
||||
await this.screenshotHelper.takeScreenshot('search-role-error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getSuccessMessage(): Promise<string> {
|
||||
testLogger.debug('获取成功消息');
|
||||
|
||||
try {
|
||||
const successElement = this.page.locator(this.selectors.successMessage);
|
||||
await successElement.waitFor({ state: 'visible', timeout: this.timeout.element });
|
||||
|
||||
const message = await successElement.textContent();
|
||||
testLogger.debug(`成功消息: ${message}`);
|
||||
|
||||
return message || '';
|
||||
} catch (error) {
|
||||
testLogger.error('获取成功消息失败', error as Error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async getErrorMessage(): Promise<string> {
|
||||
testLogger.debug('获取错误消息');
|
||||
|
||||
try {
|
||||
const errorElement = this.page.locator(this.selectors.errorMessage);
|
||||
await errorElement.waitFor({ state: 'visible', timeout: this.timeout.element });
|
||||
|
||||
const message = await errorElement.textContent();
|
||||
testLogger.debug(`错误消息: ${message}`);
|
||||
|
||||
return message || '';
|
||||
} catch (error) {
|
||||
testLogger.error('获取错误消息失败', error as Error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async getRoleCount(): Promise<number> {
|
||||
testLogger.debug('获取角色数量');
|
||||
|
||||
try {
|
||||
const count = await this.tableHelper.getRowCount(this.selectors.roleTable);
|
||||
testLogger.debug(`角色数量: ${count}`);
|
||||
|
||||
return count;
|
||||
} catch (error) {
|
||||
testLogger.error('获取角色数量失败', error as Error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
import { Page } from '@playwright/test';
|
||||
import { BasePage } from './base-page';
|
||||
import { FormHelper } from '../helpers/form-helper';
|
||||
import { TableHelper } from '../helpers/table-helper';
|
||||
import { ScreenshotHelper } from '../helpers/screenshot-helper';
|
||||
import { testLogger } from '../core/test-logger';
|
||||
|
||||
export class UserManagementPage extends BasePage {
|
||||
private formHelper: FormHelper;
|
||||
private tableHelper: TableHelper;
|
||||
private screenshotHelper: ScreenshotHelper;
|
||||
|
||||
private readonly selectors = {
|
||||
userTable: '[data-testid="user-table"]',
|
||||
addUserButton: '[data-testid="add-user-button"]',
|
||||
editButton: 'button:has-text("编辑")',
|
||||
deleteButton: 'button:has-text("删除")',
|
||||
searchInput: '[data-testid="username-search-input"]',
|
||||
emailSearchInput: '[data-testid="email-search-input"]',
|
||||
statusSelect: '[data-testid="status-select"]',
|
||||
searchButton: '[data-testid="search-button"]',
|
||||
resetButton: '[data-testid="reset-button"]',
|
||||
modal: '.ant-modal',
|
||||
modalTitle: '.ant-modal-title',
|
||||
modalConfirmButton: '.ant-modal .ant-btn-primary',
|
||||
modalCancelButton: '.ant-modal .ant-btn-default',
|
||||
successMessage: '.ant-message-success',
|
||||
errorMessage: '.ant-message-error',
|
||||
pagination: '.ant-pagination',
|
||||
userForm: '.user-form',
|
||||
usernameInput: 'input[name="username"]',
|
||||
passwordInput: 'input[name="password"]',
|
||||
emailInput: 'input[name="email"]',
|
||||
phoneInput: 'input[name="phone"]',
|
||||
realNameInput: 'input[name="realName"]',
|
||||
statusSelect: 'select[name="status"]',
|
||||
roleSelect: 'select[name="roleIds"]'
|
||||
};
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
this.formHelper = new FormHelper(page);
|
||||
this.tableHelper = new TableHelper(page);
|
||||
this.screenshotHelper = new ScreenshotHelper(page);
|
||||
}
|
||||
|
||||
async navigate(): Promise<void> {
|
||||
testLogger.info('导航到用户管理页面');
|
||||
await super.navigate('/system/user');
|
||||
}
|
||||
|
||||
async waitForLoad(): Promise<void> {
|
||||
testLogger.info('等待用户管理页面加载');
|
||||
|
||||
try {
|
||||
await this.page.waitForSelector(this.selectors.userTable, {
|
||||
state: 'visible',
|
||||
timeout: this.timeout.default
|
||||
});
|
||||
|
||||
testLogger.info('用户管理页面加载完成');
|
||||
} catch (error) {
|
||||
testLogger.error('用户管理页面加载超时', error as Error);
|
||||
await this.screenshotHelper.takeScreenshot('user-management-load-error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async clickAddUser(): Promise<void> {
|
||||
testLogger.info('点击新增用户按钮');
|
||||
|
||||
try {
|
||||
await this.page.waitForSelector(this.selectors.addUserButton, {
|
||||
state: 'visible',
|
||||
timeout: this.timeout.element
|
||||
});
|
||||
|
||||
await this.page.click(this.selectors.addUserButton);
|
||||
|
||||
await this.page.waitForSelector(this.selectors.modal, {
|
||||
state: 'visible',
|
||||
timeout: this.timeout.element
|
||||
});
|
||||
|
||||
testLogger.info('新增用户对话框已打开');
|
||||
} catch (error) {
|
||||
testLogger.error('点击新增用户按钮失败', error as Error);
|
||||
await this.screenshotHelper.takeScreenshot('click-add-user-error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async clickEditUser(rowIndex: number): Promise<void> {
|
||||
testLogger.info(`点击编辑用户按钮,行索引: ${rowIndex}`);
|
||||
|
||||
try {
|
||||
const editButtons = this.page.locator(this.selectors.editButton);
|
||||
await editButtons.nth(rowIndex).waitFor({ state: 'visible', timeout: this.timeout.element });
|
||||
|
||||
await editButtons.nth(rowIndex).click();
|
||||
|
||||
await this.page.waitForSelector(this.selectors.modal, {
|
||||
state: 'visible',
|
||||
timeout: this.timeout.element
|
||||
});
|
||||
|
||||
testLogger.info('编辑用户对话框已打开');
|
||||
} catch (error) {
|
||||
testLogger.error(`点击编辑用户按钮失败,行索引: ${rowIndex}`, error as Error);
|
||||
await this.screenshotHelper.takeScreenshot(`click-edit-user-${rowIndex}-error`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async clickDeleteUser(rowIndex: number): Promise<void> {
|
||||
testLogger.info(`点击删除用户按钮,行索引: ${rowIndex}`);
|
||||
|
||||
try {
|
||||
const deleteButtons = this.page.locator(this.selectors.deleteButton);
|
||||
await deleteButtons.nth(rowIndex).waitFor({ state: 'visible', timeout: this.timeout.element });
|
||||
|
||||
await deleteButtons.nth(rowIndex).click();
|
||||
|
||||
testLogger.info('删除确认对话框已打开');
|
||||
} catch (error) {
|
||||
testLogger.error(`点击删除用户按钮失败,行索引: ${rowIndex}`, error as Error);
|
||||
await this.screenshotHelper.takeScreenshot(`click-delete-user-${rowIndex}-error`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async confirmDelete(): Promise<void> {
|
||||
testLogger.info('确认删除用户');
|
||||
|
||||
try {
|
||||
await this.page.waitForSelector(this.selectors.modalConfirmButton, {
|
||||
state: 'visible',
|
||||
timeout: this.timeout.element
|
||||
});
|
||||
|
||||
await this.page.click(this.selectors.modalConfirmButton);
|
||||
|
||||
await this.page.waitForSelector(this.selectors.modal, {
|
||||
state: 'hidden',
|
||||
timeout: this.timeout.element
|
||||
});
|
||||
|
||||
testLogger.info('用户删除确认成功');
|
||||
} catch (error) {
|
||||
testLogger.error('确认删除用户失败', error as Error);
|
||||
await this.screenshotHelper.takeScreenshot('confirm-delete-error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async searchUser(keyword: string): Promise<void> {
|
||||
testLogger.info(`搜索用户,关键词: ${keyword}`);
|
||||
|
||||
try {
|
||||
await this.page.waitForSelector(this.selectors.searchInput, {
|
||||
state: 'visible',
|
||||
timeout: this.timeout.element
|
||||
});
|
||||
|
||||
await this.page.fill(this.selectors.searchInput, keyword);
|
||||
await this.page.click(this.selectors.searchButton);
|
||||
|
||||
await this.page.waitForLoadState('networkidle', {
|
||||
timeout: this.timeout.network
|
||||
});
|
||||
|
||||
testLogger.info('用户搜索完成');
|
||||
} catch (error) {
|
||||
testLogger.error(`搜索用户失败,关键词: ${keyword}`, error as Error);
|
||||
await this.screenshotHelper.takeScreenshot('search-user-error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async resetSearch(): Promise<void> {
|
||||
testLogger.info('重置搜索条件');
|
||||
|
||||
try {
|
||||
await this.page.waitForSelector(this.selectors.resetButton, {
|
||||
state: 'visible',
|
||||
timeout: this.timeout.element
|
||||
});
|
||||
|
||||
await this.page.click(this.selectors.resetButton);
|
||||
|
||||
await this.page.waitForLoadState('networkidle', {
|
||||
timeout: this.timeout.network
|
||||
});
|
||||
|
||||
testLogger.info('搜索条件已重置');
|
||||
} catch (error) {
|
||||
testLogger.error('重置搜索条件失败', error as Error);
|
||||
await this.screenshotHelper.takeScreenshot('reset-search-error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getSuccessMessage(): Promise<string> {
|
||||
testLogger.debug('获取成功消息');
|
||||
|
||||
try {
|
||||
const successElement = this.page.locator(this.selectors.successMessage);
|
||||
await successElement.waitFor({ state: 'visible', timeout: this.timeout.element });
|
||||
|
||||
const message = await successElement.textContent();
|
||||
testLogger.debug(`成功消息: ${message}`);
|
||||
|
||||
return message || '';
|
||||
} catch (error) {
|
||||
testLogger.error('获取成功消息失败', error as Error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async getErrorMessage(): Promise<string> {
|
||||
testLogger.debug('获取错误消息');
|
||||
|
||||
try {
|
||||
const errorElement = this.page.locator(this.selectors.errorMessage);
|
||||
await errorElement.waitFor({ state: 'visible', timeout: this.timeout.element });
|
||||
|
||||
const message = await errorElement.textContent();
|
||||
testLogger.debug(`错误消息: ${message}`);
|
||||
|
||||
return message || '';
|
||||
} catch (error) {
|
||||
testLogger.error('获取错误消息失败', error as Error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async getUserCount(): Promise<number> {
|
||||
testLogger.debug('获取用户数量');
|
||||
|
||||
try {
|
||||
const count = await this.tableHelper.getRowCount(this.selectors.userTable);
|
||||
testLogger.debug(`用户数量: ${count}`);
|
||||
|
||||
return count;
|
||||
} catch (error) {
|
||||
testLogger.error('获取用户数量失败', error as Error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user