feat(admin): 添加用户管理相关文件

添加用户管理视图、API和状态管理文件
This commit is contained in:
张翔
2026-03-28 14:37:29 +08:00
commit 08ea5fbe98
1643 changed files with 255646 additions and 0 deletions
@@ -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