feat(admin): 添加用户管理相关文件
添加用户管理视图、API和状态管理文件
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import { Page, Locator } from '@playwright/test'
|
||||
|
||||
export class FormHelper {
|
||||
private page: Page
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page
|
||||
}
|
||||
|
||||
async fillInput(selector: string, value: string, options?: { clear?: boolean; delay?: number }) {
|
||||
const input = this.page.locator(selector)
|
||||
|
||||
if (options?.clear) {
|
||||
await input.clear()
|
||||
}
|
||||
|
||||
await input.fill(value, { delay: options?.delay })
|
||||
}
|
||||
|
||||
async selectOption(selector: string, value: string) {
|
||||
const select = this.page.locator(selector)
|
||||
await select.selectOption(value)
|
||||
}
|
||||
|
||||
async checkCheckbox(selector: string) {
|
||||
const checkbox = this.page.locator(selector)
|
||||
await checkbox.check()
|
||||
}
|
||||
|
||||
async uncheckCheckbox(selector: string) {
|
||||
const checkbox = this.page.locator(selector)
|
||||
await checkbox.uncheck()
|
||||
}
|
||||
|
||||
async selectRadio(selector: string, value: string) {
|
||||
const radio = this.page.locator(`${selector}[value="${value}"]`)
|
||||
await radio.check()
|
||||
}
|
||||
|
||||
async uploadFile(selector: string, filePath: string) {
|
||||
const input = this.page.locator(selector)
|
||||
await input.setInputFiles(filePath)
|
||||
}
|
||||
|
||||
async submitForm(selector: string) {
|
||||
const form = this.page.locator(selector)
|
||||
await form.evaluate((form: HTMLFormElement) => form.submit())
|
||||
}
|
||||
|
||||
async resetForm(selector: string) {
|
||||
const form = this.page.locator(selector)
|
||||
await form.evaluate((form: HTMLFormElement) => form.reset())
|
||||
}
|
||||
|
||||
async getFieldValue(selector: string): Promise<string> {
|
||||
const field = this.page.locator(selector)
|
||||
return await field.inputValue()
|
||||
}
|
||||
|
||||
async isFieldValid(selector: string): Promise<boolean> {
|
||||
const field = this.page.locator(selector)
|
||||
const isValid = await field.evaluate((el: HTMLInputElement) => el.checkValidity())
|
||||
return isValid
|
||||
}
|
||||
|
||||
async getValidationMessage(selector: string): Promise<string> {
|
||||
const field = this.page.locator(selector)
|
||||
return await field.evaluate((el: HTMLInputElement) => el.validationMessage)
|
||||
}
|
||||
|
||||
async waitForFormToBeReady(selector: string, timeout: number = 5000) {
|
||||
await this.page.waitForSelector(selector, { state: 'visible', timeout })
|
||||
await this.page.waitForLoadState('networkidle', { timeout })
|
||||
}
|
||||
|
||||
async fillForm(fields: Array<{ selector: string; value: string; type?: 'input' | 'select' | 'checkbox' }>) {
|
||||
for (const field of fields) {
|
||||
switch (field.type) {
|
||||
case 'select':
|
||||
await this.selectOption(field.selector, field.value)
|
||||
break
|
||||
case 'checkbox':
|
||||
if (field.value === 'true' || field.value === 'checked') {
|
||||
await this.checkCheckbox(field.selector)
|
||||
} else {
|
||||
await this.uncheckCheckbox(field.selector)
|
||||
}
|
||||
break
|
||||
default:
|
||||
await this.fillInput(field.selector, field.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default FormHelper
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Page } from '@playwright/test'
|
||||
|
||||
export class PerformanceMetrics {
|
||||
private metrics: Map<string, number[]> = new Map()
|
||||
|
||||
recordMetric(name: string, value: number) {
|
||||
if (!this.metrics.has(name)) {
|
||||
this.metrics.set(name, [])
|
||||
}
|
||||
this.metrics.get(name)!.push(value)
|
||||
}
|
||||
|
||||
getAverage(name: string): number {
|
||||
const values = this.metrics.get(name) || []
|
||||
if (values.length === 0) return 0
|
||||
const sum = values.reduce((a, b) => a + b, 0)
|
||||
return sum / values.length
|
||||
}
|
||||
|
||||
getP95(name: string): number {
|
||||
const values = this.metrics.get(name) || []
|
||||
if (values.length === 0) return 0
|
||||
const sorted = [...values].sort((a, b) => a - b)
|
||||
const index = Math.floor(sorted.length * 0.95)
|
||||
return sorted[index]
|
||||
}
|
||||
|
||||
getP99(name: string): number {
|
||||
const values = this.metrics.get(name) || []
|
||||
if (values.length === 0) return 0
|
||||
const sorted = [...values].sort((a, b) => a - b)
|
||||
const index = Math.floor(sorted.length * 0.99)
|
||||
return sorted[index]
|
||||
}
|
||||
|
||||
getMax(name: string): number {
|
||||
const values = this.metrics.get(name) || []
|
||||
if (values.length === 0) return 0
|
||||
return Math.max(...values)
|
||||
}
|
||||
|
||||
getMin(name: string): number {
|
||||
const values = this.metrics.get(name) || []
|
||||
if (values.length === 0) return 0
|
||||
return Math.min(...values)
|
||||
}
|
||||
|
||||
printReport() {
|
||||
console.log('\n=== 性能测试报告 ===')
|
||||
for (const [name, values] of this.metrics.entries()) {
|
||||
console.log(`\n${name}:`)
|
||||
console.log(` 平均值: ${this.getAverage(name).toFixed(2)}ms`)
|
||||
console.log(` P95: ${this.getP95(name).toFixed(2)}ms`)
|
||||
console.log(` P99: ${this.getP99(name).toFixed(2)}ms`)
|
||||
console.log(` 最大值: ${this.getMax(name).toFixed(2)}ms`)
|
||||
console.log(` 最小值: ${this.getMin(name).toFixed(2)}ms`)
|
||||
console.log(` 样本数: ${values.length}`)
|
||||
}
|
||||
console.log('\n====================\n')
|
||||
}
|
||||
}
|
||||
|
||||
export class PerformanceTestHelper {
|
||||
async clearCacheAndCookies(page: Page) {
|
||||
const context = page.context()
|
||||
await context.clearCookies()
|
||||
await page.evaluate(() => {
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
})
|
||||
}
|
||||
|
||||
async measurePageLoad(page: Page, url: string): Promise<number> {
|
||||
const startTime = Date.now()
|
||||
await page.goto(url, { waitUntil: 'networkidle' })
|
||||
const endTime = Date.now()
|
||||
return endTime - startTime
|
||||
}
|
||||
|
||||
async measureApiCall(page: Page, apiCall: () => Promise<void>): Promise<number> {
|
||||
const startTime = Date.now()
|
||||
await apiCall()
|
||||
const endTime = Date.now()
|
||||
return endTime - startTime
|
||||
}
|
||||
|
||||
async measureElementInteraction(
|
||||
page: Page,
|
||||
selector: string,
|
||||
action: () => Promise<void>
|
||||
): Promise<number> {
|
||||
await page.waitForSelector(selector, { state: 'visible' })
|
||||
const startTime = Date.now()
|
||||
await action()
|
||||
const endTime = Date.now()
|
||||
return endTime - startTime
|
||||
}
|
||||
|
||||
async measurePageNavigation(
|
||||
page: Page,
|
||||
fromUrl: string,
|
||||
toUrl: string
|
||||
): Promise<number> {
|
||||
await page.goto(fromUrl, { waitUntil: 'networkidle' })
|
||||
const startTime = Date.now()
|
||||
await page.goto(toUrl, { waitUntil: 'networkidle' })
|
||||
const endTime = Date.now()
|
||||
return endTime - startTime
|
||||
}
|
||||
}
|
||||
|
||||
export default PerformanceTestHelper
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Page } from '@playwright/test'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
|
||||
export class ScreenshotHelper {
|
||||
private page: Page
|
||||
private screenshotDir: string
|
||||
|
||||
constructor(page: Page, screenshotDir: string = './test-results/screenshots') {
|
||||
this.page = page
|
||||
this.screenshotDir = screenshotDir
|
||||
this.ensureDirectoryExists(screenshotDir)
|
||||
}
|
||||
|
||||
private ensureDirectoryExists(dir: string) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
async takeScreenshot(name: string): Promise<string> {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
||||
const filename = `${timestamp}-${name}.png`
|
||||
const filepath = path.join(this.screenshotDir, filename)
|
||||
|
||||
await this.page.screenshot({
|
||||
path: filepath,
|
||||
fullPage: true
|
||||
})
|
||||
|
||||
return filepath
|
||||
}
|
||||
|
||||
async takeElementScreenshot(selector: string, name: string): Promise<string> {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
||||
const filename = `${timestamp}-${name}.png`
|
||||
const filepath = path.join(this.screenshotDir, filename)
|
||||
|
||||
const element = await this.page.locator(selector)
|
||||
await element.screenshot({
|
||||
path: filepath
|
||||
})
|
||||
|
||||
return filepath
|
||||
}
|
||||
|
||||
async compareScreenshots(name: string, baselineDir: string = './test-results/baseline'): Promise<boolean> {
|
||||
const baselinePath = path.join(baselineDir, `${name}.png`)
|
||||
const currentPath = await this.takeScreenshot(`${name}-current`)
|
||||
|
||||
if (!fs.existsSync(baselinePath)) {
|
||||
console.warn(`Baseline screenshot not found: ${baselinePath}`)
|
||||
return false
|
||||
}
|
||||
|
||||
// 这里可以添加图片比较逻辑
|
||||
// 例如使用 pixelmatch 或其他图片比较库
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export default ScreenshotHelper
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Page, Locator } from '@playwright/test'
|
||||
|
||||
export class TableHelper {
|
||||
private page: Page
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page
|
||||
}
|
||||
|
||||
async getRowCount(tableSelector: string): Promise<number> {
|
||||
const rows = await this.page.locator(`${tableSelector} tbody tr`).count()
|
||||
return rows
|
||||
}
|
||||
|
||||
async getCellText(tableSelector: string, row: number, column: number): Promise<string> {
|
||||
const cell = this.page.locator(`${tableSelector} tbody tr:nth-child(${row}) td:nth-child(${column})`)
|
||||
return await cell.textContent() || ''
|
||||
}
|
||||
|
||||
async getRowData(tableSelector: string, row: number): Promise<string[]> {
|
||||
const selector = `${tableSelector} tbody tr:nth-child(${row}) td`
|
||||
const cells = await this.page.locator(selector).allTextContents()
|
||||
return cells
|
||||
}
|
||||
|
||||
async clickRowAction(tableSelector: string, row: number, actionSelector: string) {
|
||||
const actionButton = this.page.locator(`${tableSelector} tbody tr:nth-child(${row}) ${actionSelector}`);
|
||||
await actionButton.click();
|
||||
}
|
||||
|
||||
async sortByColumn(tableSelector: string, column: number) {
|
||||
const header = this.page.locator(`${tableSelector} thead tr th:nth-child(${column})`)
|
||||
await header.click()
|
||||
}
|
||||
|
||||
async waitForTableToLoad(tableSelector: string, timeout: number = 5000) {
|
||||
await this.page.waitForSelector(`${tableSelector} tbody tr`, { state: 'visible', timeout })
|
||||
}
|
||||
|
||||
async getTableHeaders(tableSelector: string): Promise<string[]> {
|
||||
const headers = await this.page.locator(`${tableSelector} thead tr th`).allTextContents()
|
||||
return headers
|
||||
}
|
||||
|
||||
async findRowByText(tableSelector: string, text: string): Promise<number> {
|
||||
const rows = await this.page.locator(`${tableSelector} tbody tr`).all()
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const rowText = await rows[i].textContent()
|
||||
if (rowText?.includes(text)) {
|
||||
return i + 1
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
async selectRow(tableSelector: string, row: number) {
|
||||
const checkbox = this.page.locator(`${tableSelector} tbody tr:nth-child(${row}) input[type="checkbox"]`)
|
||||
await checkbox.check()
|
||||
}
|
||||
|
||||
async deselectRow(tableSelector: string, row: number) {
|
||||
const checkbox = this.page.locator(`${tableSelector} tbody tr:nth-child(${row}) input[type="checkbox"]`)
|
||||
await checkbox.uncheck()
|
||||
}
|
||||
|
||||
async selectAllRows(tableSelector: string) {
|
||||
const checkbox = this.page.locator(`${tableSelector} thead input[type="checkbox"]`)
|
||||
await checkbox.check()
|
||||
}
|
||||
|
||||
async getSelectedRows(tableSelector: string): Promise<number[]> {
|
||||
const checkboxes = await this.page.locator(`${tableSelector} tbody input[type="checkbox"]:checked`).all()
|
||||
const selectedRows: number[] = []
|
||||
|
||||
for (let i = 0; i < checkboxes.length; i++) {
|
||||
const row = await checkboxes[i].locator('xpath=ancestor::tr').evaluate((el, index) => {
|
||||
const rows = el.closest('tbody')?.querySelectorAll('tr')
|
||||
return rows ? Array.from(rows).indexOf(el.closest('tr') as HTMLTableRowElement) + 1 : -1
|
||||
}, i)
|
||||
if (row > 0) {
|
||||
selectedRows.push(row)
|
||||
}
|
||||
}
|
||||
|
||||
return selectedRows
|
||||
}
|
||||
}
|
||||
|
||||
export default TableHelper
|
||||
Reference in New Issue
Block a user