Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6e47ed527 | ||
|
|
7595f3b933 | ||
|
|
e59c5c5c60 | ||
|
|
aa3f497434 | ||
|
|
3921448aa1 | ||
|
|
aa4b2ff884 | ||
|
|
47d7883874 | ||
|
|
f7fdf43abb | ||
|
|
c9dc8a83ee | ||
|
|
8d38687583 | ||
|
|
f528c74656 | ||
|
|
e7798329a2 | ||
|
|
2f6456f027 | ||
|
|
970d194ab3 | ||
|
|
40863e3eca | ||
|
|
015f8abf32 | ||
|
|
6221a18dd3 | ||
|
|
03e91a0ff9 | ||
|
|
c08b274fbd | ||
|
|
82fb7ebe96 | ||
|
|
0dfad65b57 | ||
|
|
ff891e82b0 | ||
|
|
c8bd981cc4 | ||
|
|
15b08c65df | ||
|
|
6aa60dd29d | ||
|
|
eb8d523d91 |
37
.env.dev
37
.env.dev
@ -1,5 +1,36 @@
|
||||
# 基础URL
|
||||
# 基础配置
|
||||
BASE_URL=https://ibp-dev.longi.com
|
||||
MENU_DATA_FILE_PATH=data/longi/menu-data.json
|
||||
|
||||
MENU_TIME_OUT=30000
|
||||
# 登录配置
|
||||
IBP_USERNAME=admin
|
||||
IBP_PASSWORD=123456
|
||||
|
||||
# 测试配置
|
||||
TEST_DATA_DIR=test-data
|
||||
TEST_BATCH_SIZE=1
|
||||
TEST_RETRY_COUNT=3
|
||||
TEST_BATCH_INTERVAL=2000
|
||||
TEST_MAX_RETRY_DELAY=10000
|
||||
|
||||
# 超时配置
|
||||
MENU_TIME_OUT=60000
|
||||
EXPECT_TIMEOUT=3600000
|
||||
|
||||
# 浏览器配置
|
||||
BROWSER_HEADLESS=false
|
||||
BROWSER_SLOW_MO=100
|
||||
BROWSER_TIMEOUT=120000
|
||||
VIEWPORT_WIDTH=1920
|
||||
VIEWPORT_HEIGHT=1080
|
||||
|
||||
# 数据目录配置
|
||||
MENU_DATA_FILE_PATH=test-data/menu-data.json
|
||||
TEST_PROGRESS_FILE_PATH=test-data/test-progress.json
|
||||
|
||||
# 页面加载配置
|
||||
PAGE_LOAD_MAX_RETRIES=5
|
||||
PAGE_LOAD_RETRY_INTERVAL=3000
|
||||
PAGE_LOAD_STABILITY_DELAY=2000
|
||||
|
||||
# 邮件配置
|
||||
EMAIL_RECIPIENTS=shengzeqiang@iscmtech.com, wanghaipeng@iscmtech.com
|
||||
36
.env.prod
Normal file
36
.env.prod
Normal file
@ -0,0 +1,36 @@
|
||||
# 基础配置
|
||||
BASE_URL=https://ibp.longi.com/main/#/login?debug=ly
|
||||
|
||||
# 登录配置
|
||||
IBP_USERNAME=p_caosheng
|
||||
IBP_PASSWORD=lianyu_123456
|
||||
|
||||
# 测试配置
|
||||
TEST_DATA_DIR=test-data
|
||||
TEST_BATCH_SIZE=1
|
||||
TEST_RETRY_COUNT=3
|
||||
TEST_BATCH_INTERVAL=2000
|
||||
TEST_MAX_RETRY_DELAY=10000
|
||||
|
||||
# 超时配置
|
||||
MENU_TIME_OUT=60000
|
||||
EXPECT_TIMEOUT=36000000
|
||||
|
||||
# 浏览器配置
|
||||
BROWSER_HEADLESS=false
|
||||
BROWSER_SLOW_MO=100
|
||||
BROWSER_TIMEOUT=1200000
|
||||
VIEWPORT_WIDTH=1920
|
||||
VIEWPORT_HEIGHT=1080
|
||||
|
||||
# 数据目录配置
|
||||
MENU_DATA_FILE_PATH=test-data/menu-data.json
|
||||
TEST_PROGRESS_FILE_PATH=test-data/test-progress.json
|
||||
|
||||
# 页面加载配置
|
||||
PAGE_LOAD_MAX_RETRIES=5
|
||||
PAGE_LOAD_RETRY_INTERVAL=3000
|
||||
PAGE_LOAD_STABILITY_DELAY=2000
|
||||
|
||||
# 邮件配置
|
||||
EMAIL_RECIPIENTS=shengzeqiang@iscmtech.com, wanghaipeng@iscmtech.com
|
||||
38
.env.uat
38
.env.uat
@ -1,2 +1,36 @@
|
||||
# 基础URL
|
||||
BASE_URL=https://ibp-uat.longi.com
|
||||
# 基础配置
|
||||
BASE_URL=https://ibp-test.longi.com/main/#/login?debug=ly
|
||||
|
||||
# 登录配置
|
||||
IBP_USERNAME=zidonghuatest
|
||||
IBP_PASSWORD=Lianyu_123
|
||||
|
||||
# 测试配置
|
||||
TEST_DATA_DIR=test-data
|
||||
TEST_BATCH_SIZE=1
|
||||
TEST_RETRY_COUNT=3
|
||||
TEST_BATCH_INTERVAL=2000
|
||||
TEST_MAX_RETRY_DELAY=10000
|
||||
|
||||
# 超时配置
|
||||
MENU_TIME_OUT=60000
|
||||
EXPECT_TIMEOUT=36000000
|
||||
|
||||
# 浏览器配置
|
||||
BROWSER_HEADLESS=false
|
||||
BROWSER_SLOW_MO=100
|
||||
BROWSER_TIMEOUT=1200000
|
||||
VIEWPORT_WIDTH=1920
|
||||
VIEWPORT_HEIGHT=1080
|
||||
|
||||
# 数据目录配置
|
||||
MENU_DATA_FILE_PATH=test-data/menu-data.json
|
||||
TEST_PROGRESS_FILE_PATH=test-data/test-progress.json
|
||||
|
||||
# 页面加载配置
|
||||
PAGE_LOAD_MAX_RETRIES=5
|
||||
PAGE_LOAD_RETRY_INTERVAL=3000
|
||||
PAGE_LOAD_STABILITY_DELAY=2000
|
||||
|
||||
# 邮件配置
|
||||
EMAIL_RECIPIENTS=shengzeqiang@iscmtech.com, wanghaipeng@iscmtech.com
|
||||
159
README.md
159
README.md
@ -0,0 +1,159 @@
|
||||
# Playwright E2E Testing Framework
|
||||
|
||||
本项目是基于Playwright的端到端测试框架,用于自动化测试Web应用的功能和性能。
|
||||
|
||||
## 项目架构
|
||||
|
||||
```
|
||||
playwright/
|
||||
├── src/
|
||||
│ ├── controllers/ # 控制器层,负责测试流程控制
|
||||
│ ├── pages/ # 页面对象层,封装页面操作
|
||||
│ └── services/ # 服务层,处理数据和业务逻辑
|
||||
├── tests/
|
||||
│ └── e2e/ # 端到端测试用例
|
||||
├── .env.dev # 开发环境配置
|
||||
└── playwright.config.js # Playwright配置文件
|
||||
```
|
||||
|
||||
## 配置管理
|
||||
|
||||
项目采用分层的配置管理方式,确保配置的统一性和可维护性:
|
||||
|
||||
### 1. 环境变量配置 (.env.dev)
|
||||
|
||||
环境变量配置文件包含所有可配置项,按功能分类:
|
||||
|
||||
```ini
|
||||
# 基础配置
|
||||
BASE_URL=https://ibp-dev.longi.com
|
||||
|
||||
# 测试配置
|
||||
TEST_DATA_DIR=test-data
|
||||
TEST_BATCH_SIZE=5
|
||||
TEST_RETRY_COUNT=3
|
||||
TEST_BATCH_INTERVAL=1000
|
||||
TEST_MAX_RETRY_DELAY=5000
|
||||
|
||||
# 超时配置
|
||||
MENU_TIME_OUT=30000
|
||||
EXPECT_TIMEOUT=30000
|
||||
|
||||
# 浏览器配置
|
||||
BROWSER_HEADLESS=false
|
||||
BROWSER_SLOW_MO=50
|
||||
BROWSER_TIMEOUT=30000
|
||||
VIEWPORT_WIDTH=1920
|
||||
VIEWPORT_HEIGHT=1080
|
||||
|
||||
# 数据目录配置
|
||||
MENU_DATA_FILE_PATH=test-data/menu-data.json
|
||||
TEST_PROGRESS_FILE_PATH=test-data/test-progress.json
|
||||
```
|
||||
|
||||
### 2. Playwright配置 (playwright.config.js)
|
||||
|
||||
Playwright配置文件统一管理所有浏览器相关的配置:
|
||||
|
||||
- 浏览器设置(headless模式、视窗大小等)
|
||||
- 测试超时设置
|
||||
- 并发和重试策略
|
||||
- 测试报告配置
|
||||
|
||||
### 3. 测试控制器配置 (TestController)
|
||||
|
||||
测试控制器只管理测试流程相关的配置:
|
||||
|
||||
- 批次大小
|
||||
- 重试次数
|
||||
- 批次间隔
|
||||
- 最大重试延迟
|
||||
|
||||
## 主要功能
|
||||
|
||||
1. **菜单遍历测试**
|
||||
- 自动收集菜单数据
|
||||
- 批量执行菜单点击测试
|
||||
- 支持断点续测
|
||||
- 提供测试进度跟踪
|
||||
|
||||
2. **智能重试机制**
|
||||
- 支持失败重试
|
||||
- 使用指数退避策略
|
||||
- 可配置最大重试次数和延迟
|
||||
|
||||
3. **灵活的配置系统**
|
||||
- 所有配置项可通过环境变量覆盖
|
||||
- 提供合理的默认值
|
||||
- 配置项分类清晰
|
||||
|
||||
## 使用说明
|
||||
|
||||
1. **安装依赖**
|
||||
```bash
|
||||
npm install
|
||||
|
||||
npx playwright install
|
||||
```
|
||||
|
||||
2. **配置环境变量**
|
||||
- 复制 `.env.dev` 到 `.env`
|
||||
- 根据需要修改配置项
|
||||
|
||||
3. **运行测试**
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
4. **查看报告**
|
||||
```bash
|
||||
npm run show-report
|
||||
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **配置管理**
|
||||
- 所有配置统一在 `.env` 文件中管理
|
||||
- 不同环境使用不同的 `.env` 文件
|
||||
- 避免在代码中硬编码配置值
|
||||
|
||||
2. **测试用例编写**
|
||||
- 使用页面对象模式
|
||||
- 保持测试用例独立性
|
||||
- 合理使用断言和超时设置
|
||||
|
||||
3. **错误处理**
|
||||
- 实现合理的重试机制
|
||||
- 记录详细的错误日志
|
||||
- 提供清晰的错误信息
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 确保环境变量配置正确
|
||||
2. 注意浏览器配置只在 `playwright.config.js` 中管理
|
||||
3. 测试用例应该是独立和可重复的
|
||||
4. 定期检查和清理测试数据
|
||||
|
||||
## 常见问题
|
||||
|
||||
1. **测试运行失败**
|
||||
- 检查网络连接
|
||||
- 验证环境变量配置
|
||||
- 查看错误日志
|
||||
|
||||
2. **配置不生效**
|
||||
- 确认环境变量文件位置正确
|
||||
- 检查配置项拼写
|
||||
- 重启测试进程
|
||||
|
||||
## 贡献指南
|
||||
|
||||
1. Fork 项目
|
||||
2. 创建特性分支
|
||||
3. 提交变更
|
||||
4. 发起 Pull Request
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT
|
||||
@ -1,10 +1,26 @@
|
||||
const path = require('path');
|
||||
require('dotenv-flow').config({
|
||||
node_env: process.env.NODE_ENV || 'dev',
|
||||
const dotenv = require('dotenv-flow');
|
||||
|
||||
// 获取当前环境
|
||||
const NODE_ENV = process.env.NODE_ENV || 'dev';
|
||||
|
||||
// 加载环境变量
|
||||
const result = dotenv.config({
|
||||
node_env: NODE_ENV,
|
||||
path: path.resolve(process.cwd()),
|
||||
default_node_env: 'dev'
|
||||
});
|
||||
|
||||
// 解析环境变量中的变量引用
|
||||
Object.keys(process.env).forEach(key => {
|
||||
const value = process.env[key];
|
||||
if (typeof value === 'string') {
|
||||
process.env[key] = value.replace(/\${([^}]+)}/g, (_, varName) => {
|
||||
return process.env[varName] || '';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Current NODE_ENV:', process.env.NODE_ENV);
|
||||
// console.log('Loaded BASE_URL:', process.env.BASE_URL);
|
||||
console.log('Environment files path:', path.resolve(process.cwd()));
|
||||
15
manage-automation.bat
Normal file
15
manage-automation.bat
Normal file
@ -0,0 +1,15 @@
|
||||
@echo off
|
||||
chcp 65001 > nul
|
||||
echo Starting Task Manager...
|
||||
|
||||
net session >nul 2>&1
|
||||
if %errorLevel% == 0 (
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0manage-task-new.ps1"
|
||||
) else (
|
||||
echo Run as Administrator required
|
||||
echo Right-click and select "Run as Administrator"
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
exit
|
||||
314
manage-task-new.ps1
Normal file
314
manage-task-new.ps1
Normal file
@ -0,0 +1,314 @@
|
||||
# 设置控制台编码
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# 获取当前目录的完整路径
|
||||
$currentPath = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition
|
||||
$scriptPath = Join-Path $currentPath "run-tests.bat"
|
||||
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||
|
||||
# Task Configuration
|
||||
$taskNamePrefix = "Playwright_AutoTest"
|
||||
$taskDesc = "Run Playwright tests daily"
|
||||
|
||||
function Show-Menu {
|
||||
Clear-Host
|
||||
Write-Host "`n================== Playwright Test Manager ==================`n"
|
||||
Write-Host "1. View All Tasks Status"
|
||||
Write-Host "2. Set Daily Task - DEV Environment (Default: 23:00)"
|
||||
Write-Host "3. Set Daily Task - UAT Environment"
|
||||
Write-Host "4. Set Daily Task - PROD Environment"
|
||||
Write-Host "5. Run Test Now"
|
||||
Write-Host "6. Delete Tasks"
|
||||
Write-Host "Q. Exit"
|
||||
Write-Host "`nEnter your choice (1-6, or Q to exit): " -NoNewline
|
||||
}
|
||||
|
||||
function Get-TaskStatus {
|
||||
$environments = @("Dev", "UAT", "PROD")
|
||||
$found = $false
|
||||
|
||||
Write-Host "`nTasks Status:"
|
||||
foreach ($env in $environments) {
|
||||
$taskName = "${taskNamePrefix}_$env"
|
||||
$task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
||||
|
||||
if ($task) {
|
||||
$found = $true
|
||||
Write-Host "`n$env Environment Task:"
|
||||
Write-Host "Name: $taskName"
|
||||
Write-Host "State: $($task.State)"
|
||||
|
||||
# 获取更详细的任务信息
|
||||
$taskInfo = Get-ScheduledTaskInfo -TaskName $taskName -ErrorAction SilentlyContinue
|
||||
$trigger = $task.Triggers[0]
|
||||
|
||||
if ($trigger) {
|
||||
Write-Host "Schedule: Daily at $($trigger.StartBoundary.Split('T')[1].Substring(0,5))"
|
||||
if ($taskInfo -and $taskInfo.NextRunTime) {
|
||||
Write-Host "Next Run: $($taskInfo.NextRunTime.ToString('yyyy-MM-dd HH:mm'))"
|
||||
} else {
|
||||
Write-Host "Next Run: Task needs to be enabled"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Last Result: $($task.LastTaskResult)"
|
||||
Write-Host "Working Directory: $($task.Actions[0].WorkingDirectory)"
|
||||
Write-Host "Run As User: $($task.Principal.UserId)"
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $found) {
|
||||
Write-Host "`nNo tasks found. Please set up tasks using options 2, 3 or 4."
|
||||
}
|
||||
Pause-Script
|
||||
}
|
||||
|
||||
function Set-DailyTask {
|
||||
param (
|
||||
[string]$runTime = "23:00",
|
||||
[string]$environment
|
||||
)
|
||||
|
||||
$taskName = "${taskNamePrefix}_$environment"
|
||||
Write-Host "`nSetting up daily task for $environment environment..."
|
||||
|
||||
$scriptPath = Join-Path $PSScriptRoot "run-tests.bat"
|
||||
if (-not (Test-Path $scriptPath)) {
|
||||
Write-Host "`nError: Test script not found at: $scriptPath"
|
||||
Write-Host "Path: $scriptPath"
|
||||
Pause-Script
|
||||
return
|
||||
}
|
||||
|
||||
# 确保时间格式正确
|
||||
if ($runTime -notmatch "^([01]?[0-9]|2[0-3]):[0-5][0-9]$") {
|
||||
Write-Host "`nError: Invalid time format. Please use HH:mm format (e.g., 23:00)"
|
||||
Pause-Script
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
# 创建任务操作 - 添加环境参数
|
||||
$envArg = $environment.ToLower()
|
||||
$action = New-ScheduledTaskAction -Execute $scriptPath -WorkingDirectory $PSScriptRoot -Argument $envArg
|
||||
if (-not $action) {
|
||||
throw "Failed to create task action"
|
||||
}
|
||||
|
||||
# 创建任务触发器
|
||||
$trigger = New-ScheduledTaskTrigger -Daily -At $runTime
|
||||
if (-not $trigger) {
|
||||
throw "Failed to create task trigger"
|
||||
}
|
||||
|
||||
# 创建任务设置
|
||||
$settings = New-ScheduledTaskSettingsSet `
|
||||
-RestartCount 3 `
|
||||
-RestartInterval (New-TimeSpan -Minutes 5) `
|
||||
-StartWhenAvailable `
|
||||
-DontStopOnIdleEnd `
|
||||
-ExecutionTimeLimit (New-TimeSpan -Hours 2)
|
||||
if (-not $settings) {
|
||||
throw "Failed to create task settings"
|
||||
}
|
||||
|
||||
# 创建任务主体
|
||||
$principal = New-ScheduledTaskPrincipal -UserId $currentUser -RunLevel Highest
|
||||
if (-not $principal) {
|
||||
throw "Failed to create task principal"
|
||||
}
|
||||
|
||||
$existingTask = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
||||
if ($existingTask) {
|
||||
# 更新现有任务
|
||||
$result = Set-ScheduledTask -TaskName $taskName `
|
||||
-Action $action `
|
||||
-Trigger $trigger `
|
||||
-Settings $settings `
|
||||
-Principal $principal
|
||||
|
||||
if ($result) {
|
||||
Write-Host "`nTask successfully updated!"
|
||||
} else {
|
||||
throw "Failed to update task"
|
||||
}
|
||||
} else {
|
||||
# 创建新任务
|
||||
$result = Register-ScheduledTask -TaskName $taskName `
|
||||
-Description "$taskDesc ($environment Environment)" `
|
||||
-Action $action `
|
||||
-Trigger $trigger `
|
||||
-Settings $settings `
|
||||
-Principal $principal
|
||||
|
||||
if ($result) {
|
||||
Write-Host "`nTask successfully created!"
|
||||
} else {
|
||||
throw "Failed to create task"
|
||||
}
|
||||
}
|
||||
|
||||
# 验证任务创建/更新是否成功
|
||||
$updatedTask = Get-ScheduledTask -TaskName $taskName -ErrorAction Stop
|
||||
if ($updatedTask) {
|
||||
Write-Host "`nTask Details:"
|
||||
Write-Host "Task Name: $taskName"
|
||||
Write-Host "Environment: $environment"
|
||||
Write-Host "Run Time: Daily at $runTime"
|
||||
if ($updatedTask.NextRunTime) {
|
||||
Write-Host "Next Run: $($updatedTask.NextRunTime.ToString('yyyy-MM-dd HH:mm'))"
|
||||
} else {
|
||||
Write-Host "Next Run: Not scheduled"
|
||||
}
|
||||
Write-Host "Status: $($updatedTask.State)"
|
||||
Write-Host "Run As: $($updatedTask.Principal.UserId)"
|
||||
} else {
|
||||
Write-Host "`nWarning: Task was created but could not be verified."
|
||||
}
|
||||
} catch {
|
||||
Write-Host "`nError setting up task: $_"
|
||||
Write-Host "Please make sure you have administrative privileges."
|
||||
Write-Host "Error details: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
Pause-Script
|
||||
}
|
||||
|
||||
function Update-TaskTime {
|
||||
Write-Host "`nSelect environment:"
|
||||
Write-Host "1. Development (dev)"
|
||||
Write-Host "2. UAT"
|
||||
Write-Host "`nEnter your choice (1-2): " -NoNewline
|
||||
|
||||
$envChoice = Read-Host
|
||||
$environment = switch ($envChoice) {
|
||||
"1" { "dev" }
|
||||
"2" { "uat" }
|
||||
default {
|
||||
Write-Host "`nInvalid choice. Using default (dev)"
|
||||
"dev"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "`nEnter new time (HH:mm, e.g. 02:00): " -NoNewline
|
||||
$newTime = Read-Host
|
||||
if ($newTime -match "^([01]?[0-9]|2[0-3]):[0-5][0-9]$") {
|
||||
Set-DailyTask -runTime $newTime -environment $environment
|
||||
} else {
|
||||
Write-Host "`nInvalid time format"
|
||||
Pause-Script
|
||||
}
|
||||
}
|
||||
|
||||
function Start-TestNow {
|
||||
Write-Host "`nSelect environment:"
|
||||
Write-Host "1. Development (dev)"
|
||||
Write-Host "2. UAT"
|
||||
Write-Host "3. PROD"
|
||||
Write-Host "`nEnter your choice (1-3): " -NoNewline
|
||||
|
||||
$envChoice = Read-Host
|
||||
$env = switch ($envChoice) {
|
||||
"1" { "dev" }
|
||||
"2" { "uat" }
|
||||
"3" { "prod" }
|
||||
default {
|
||||
Write-Host "`nInvalid choice. Using default (dev)"
|
||||
"dev"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "`nStarting test in $env environment..."
|
||||
try {
|
||||
$scriptPath = Join-Path $PSScriptRoot "run-tests.bat"
|
||||
# 使用cmd直接运行,这样可以看到实时输出
|
||||
& cmd /c $scriptPath $env
|
||||
Write-Host "`nTest completed"
|
||||
} catch {
|
||||
Write-Host "`nError: $_"
|
||||
}
|
||||
Pause-Script
|
||||
}
|
||||
|
||||
function Remove-AutomationTask {
|
||||
Write-Host "`nSelect tasks to delete:"
|
||||
Write-Host "1. DEV Environment Task"
|
||||
Write-Host "2. UAT Environment Task"
|
||||
Write-Host "3. PROD Environment Task"
|
||||
Write-Host "4. All Tasks"
|
||||
Write-Host "`nEnter your choice (1-4): " -NoNewline
|
||||
|
||||
$choice = Read-Host
|
||||
$tasksToDelete = switch ($choice) {
|
||||
"1" { @("Dev") }
|
||||
"2" { @("UAT") }
|
||||
"3" { @("PROD") }
|
||||
"4" { @("Dev", "UAT", "PROD") }
|
||||
default {
|
||||
Write-Host "`nInvalid choice."
|
||||
Pause-Script
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($env in $tasksToDelete) {
|
||||
$taskName = "${taskNamePrefix}_$env"
|
||||
$task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
||||
if ($task) {
|
||||
try {
|
||||
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false
|
||||
Write-Host "`nTask '$taskName' deleted"
|
||||
} catch {
|
||||
Write-Host "`nError deleting task '$taskName': $_"
|
||||
}
|
||||
} else {
|
||||
Write-Host "`nTask '$taskName' not found"
|
||||
}
|
||||
}
|
||||
Pause-Script
|
||||
}
|
||||
|
||||
function Pause-Script {
|
||||
Write-Host "`nPress Enter to continue..." -NoNewline
|
||||
Read-Host
|
||||
}
|
||||
|
||||
# Main Loop
|
||||
do {
|
||||
Show-Menu
|
||||
$choice = Read-Host
|
||||
|
||||
switch ($choice.ToUpper()) {
|
||||
"1" { Get-TaskStatus }
|
||||
"2" { Set-DailyTask -environment "Dev" }
|
||||
"3" {
|
||||
Write-Host "`nEnter time for UAT task (HH:mm, e.g. 02:00): " -NoNewline
|
||||
$uatTime = Read-Host
|
||||
if ($uatTime -match "^([01]?[0-9]|2[0-3]):[0-5][0-9]$") {
|
||||
Set-DailyTask -runTime $uatTime -environment "UAT"
|
||||
} else {
|
||||
Write-Host "`nInvalid time format"
|
||||
Pause-Script
|
||||
}
|
||||
}
|
||||
"4" {
|
||||
Write-Host "`nEnter time for PROD task (HH:mm, e.g. 02:30): " -NoNewline
|
||||
$prodTime = Read-Host
|
||||
if ($prodTime -match "^([01]?[0-9]|2[0-3]):[0-5][0-9]$") {
|
||||
Set-DailyTask -runTime $prodTime -environment "PROD"
|
||||
} else {
|
||||
Write-Host "`nInvalid time format"
|
||||
Pause-Script
|
||||
}
|
||||
}
|
||||
"5" { Start-TestNow }
|
||||
"6" { Remove-AutomationTask }
|
||||
"Q" { exit }
|
||||
default {
|
||||
Write-Host "`nInvalid choice. Please try again."
|
||||
Pause-Script
|
||||
}
|
||||
}
|
||||
} while ($true)
|
||||
@ -9,7 +9,10 @@
|
||||
"report": "playwright show-report",
|
||||
"codegen": "playwright codegen",
|
||||
"debug": "playwright test --debug",
|
||||
"test:longi:check-normal:dev": "cross-env NODE_ENV=dev playwright test tests/longi-ibp/check-page-normal.test.js --headed --project=chromium"
|
||||
"test:menu": "cross-env NODE_ENV=dev playwright test tests/e2e/menu.spec.js",
|
||||
"test:menu:ui": "cross-env NODE_ENV=dev playwright test tests/e2e/menu.spec.js --ui",
|
||||
"test:menu:debug": "cross-env NODE_ENV=dev playwright test tests/e2e/menu.spec.js --debug",
|
||||
"test:menu:clean": "cross-env NODE_ENV=dev rimraf test-data/* && npm run test:menu"
|
||||
},
|
||||
"keywords": [
|
||||
"playwright",
|
||||
@ -24,7 +27,8 @@
|
||||
"chalk": "^4.1.2",
|
||||
"commander": "^11.1.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"faker": "^5.5.3"
|
||||
"faker": "^5.5.3",
|
||||
"nodemailer": "^6.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"allure-playwright": "^2.9.2",
|
||||
|
||||
@ -1,40 +1,69 @@
|
||||
// @ts-check
|
||||
const {defineConfig, devices} = require('@playwright/test');
|
||||
const testLifecycle = require('./src/hooks/testLifecycle');
|
||||
const dotenv = require('dotenv');
|
||||
|
||||
// 根据环境加载对应的.env文件
|
||||
const env = process.env.NODE_ENV || 'dev';
|
||||
const envPath = `.env.${env}`;
|
||||
dotenv.config({ path: envPath });
|
||||
console.log(`[Playwright Config] 已加载环境配置: ${envPath}`);
|
||||
|
||||
/**
|
||||
* Read environment variables and process them
|
||||
*/
|
||||
function getBaseUrl() {
|
||||
const baseUrl = process.env.BASE_URL;
|
||||
if (!baseUrl) {
|
||||
throw new Error('BASE_URL environment variable is not set');
|
||||
}
|
||||
// 确保基础URL不包含hash和query参数
|
||||
return baseUrl.split('#')[0].split('?')[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://playwright.dev/docs/test-configuration
|
||||
* @type {import('@playwright/test').PlaywrightTestConfig}
|
||||
*/
|
||||
module.exports = defineConfig({
|
||||
const config = {
|
||||
testDir: './tests',
|
||||
/* 测试超时时间 */
|
||||
timeout: 300 * 1000,
|
||||
timeout: parseInt(process.env.EXPECT_TIMEOUT || '30000'),
|
||||
/* 每个测试的预期状态 */
|
||||
expect: {
|
||||
/**
|
||||
* 断言超时时间
|
||||
*/
|
||||
timeout: 30000
|
||||
timeout: parseInt(process.env.EXPECT_TIMEOUT || '30000')
|
||||
},
|
||||
/* 测试运行并发数 */
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* 失败重试次数 */
|
||||
retries: 0,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: 1,
|
||||
/* 测试报告相关 */
|
||||
reporter: [
|
||||
['html', {open: 'never'}],
|
||||
['list']
|
||||
],
|
||||
reporter: 'html',
|
||||
/* 共享设置 */
|
||||
use: {
|
||||
/* 基础URL */
|
||||
// baseURL: 'http://localhost:3000',
|
||||
|
||||
baseURL: getBaseUrl(),
|
||||
/* 收集测试追踪信息 */
|
||||
trace: 'on-first-retry',
|
||||
/* 自动截图 */
|
||||
screenshot: 'only-on-failure',
|
||||
screenshot: {
|
||||
mode: 'only-on-failure'
|
||||
},
|
||||
/* 录制视频 */
|
||||
video: 'on-first-retry',
|
||||
video: 'retain-on-failure',
|
||||
/* 浏览器配置 */
|
||||
headless: process.env.BROWSER_HEADLESS === 'true',
|
||||
viewport: {
|
||||
width: parseInt(process.env.VIEWPORT_WIDTH || '1920'),
|
||||
height: parseInt(process.env.VIEWPORT_HEIGHT || '1080')
|
||||
},
|
||||
/* 浏览器启动选项 */
|
||||
launchOptions: {
|
||||
slowMo: parseInt(process.env.BROWSER_SLOW_MO || '50'),
|
||||
timeout: parseInt(process.env.BROWSER_TIMEOUT || '30000')
|
||||
}
|
||||
},
|
||||
|
||||
/* 配置不同的浏览器项目 */
|
||||
@ -43,23 +72,6 @@ module.exports = defineConfig({
|
||||
name: 'chromium',
|
||||
use: {...devices['Desktop Chrome']},
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
use: {...devices['Desktop Firefox']},
|
||||
},
|
||||
{
|
||||
name: 'webkit',
|
||||
use: {...devices['Desktop Safari']},
|
||||
},
|
||||
/* 移动设备测试 */
|
||||
{
|
||||
name: 'Mobile Chrome',
|
||||
use: {...devices['Pixel 5']},
|
||||
},
|
||||
{
|
||||
name: 'Mobile Safari',
|
||||
use: {...devices['iPhone 12']},
|
||||
},
|
||||
],
|
||||
|
||||
/* 本地开发服务器配置 */
|
||||
@ -68,4 +80,10 @@ module.exports = defineConfig({
|
||||
// port: 3000,
|
||||
// reuseExistingServer: !process.env.CI,
|
||||
// },
|
||||
});
|
||||
|
||||
/* 全局设置和清理 */
|
||||
globalSetup: './src/hooks/setup.js',
|
||||
globalTeardown: './src/hooks/teardown.js'
|
||||
};
|
||||
|
||||
module.exports = defineConfig(config);
|
||||
22
run-tests.bat
Normal file
22
run-tests.bat
Normal file
@ -0,0 +1,22 @@
|
||||
@echo off
|
||||
echo Starting automated tests...
|
||||
cd /d %~dp0
|
||||
|
||||
:: 设置控制台编码为UTF-8
|
||||
chcp 65001 > nul
|
||||
|
||||
:: 设置默认环境为dev
|
||||
if "%1"=="" (
|
||||
set TEST_ENV=dev
|
||||
) else (
|
||||
set TEST_ENV=%1
|
||||
)
|
||||
|
||||
echo [Batch] Preparing to run tests in %TEST_ENV% environment...
|
||||
|
||||
:: 运行测试并发送报告
|
||||
set NODE_ENV=%TEST_ENV%
|
||||
node src/scripts/runTestsAndSendReport.js
|
||||
|
||||
echo.
|
||||
echo [Batch] Execution completed
|
||||
@ -1,26 +0,0 @@
|
||||
// #!/usr/bin/env node
|
||||
//
|
||||
// /**
|
||||
// * 龙蛟IBP系统测试运行脚本
|
||||
// */
|
||||
// const {execSync} = require('child_process');
|
||||
// const chalk = require('chalk');
|
||||
//
|
||||
// console.log(chalk.blue('开始运行龙蛟IBP系统测试...'));
|
||||
//
|
||||
// // 默认运行最简单的测试
|
||||
// const testFile = process.argv[2] || 'tests/longi-ibp/simple-login.test.js';
|
||||
//
|
||||
// // 构建命令 - 使用--project而不是--browser
|
||||
// const command = `npx playwright test ${testFile} --headed --project=chromium`;
|
||||
//
|
||||
// console.log(chalk.yellow(`运行命令: ${command}`));
|
||||
//
|
||||
// try {
|
||||
// // 执行命令
|
||||
// execSync(command, {stdio: 'inherit'});
|
||||
// console.log(chalk.green('测试完成!'));
|
||||
// } catch (error) {
|
||||
// console.error(chalk.red('测试执行失败:'), error.message);
|
||||
// process.exit(1);
|
||||
// }
|
||||
@ -1,91 +0,0 @@
|
||||
// #!/usr/bin/env node
|
||||
//
|
||||
// /**
|
||||
// * 测试运行脚本
|
||||
// * 提供命令行界面来运行测试
|
||||
// */
|
||||
// const {program} = require('commander');
|
||||
// const {execSync} = require('child_process');
|
||||
// const chalk = require('chalk');
|
||||
// const path = require('path');
|
||||
// const fs = require('fs');
|
||||
//
|
||||
// // 设置命令行选项
|
||||
// program
|
||||
// .version('1.0.0')
|
||||
// .description('Playwright 自动化测试运行工具')
|
||||
// .option('-b, --browser <browser>', '指定浏览器 (chromium, firefox, webkit)', 'chromium')
|
||||
// .option('-t, --test <pattern>', '指定测试文件或目录')
|
||||
// .option('-h, --headless', '无头模式运行', false)
|
||||
// .option('-r, --reporter <reporter>', '指定报告格式 (list, html, json)', 'list')
|
||||
// .option('-p, --parallel <number>', '并行运行数量', '3')
|
||||
// .option('-s, --screenshot', '失败时截图', false)
|
||||
// .option('-v, --video', '录制视频', false)
|
||||
// .option('-d, --debug', '调试模式', false)
|
||||
// .option('-u, --ui', '使用UI模式', false)
|
||||
// .option('-c, --config <path>', '指定配置文件路径')
|
||||
// .parse(process.argv);
|
||||
//
|
||||
// const options = program.opts();
|
||||
//
|
||||
// // 构建命令
|
||||
// let command = 'npx playwright test';
|
||||
//
|
||||
// // 添加测试文件或目录
|
||||
// if (options.test) {
|
||||
// command += ` "${options.test}"`;
|
||||
// }
|
||||
//
|
||||
// // 添加浏览器
|
||||
// command += ` --project=${options.browser}`;
|
||||
//
|
||||
// // 添加报告格式
|
||||
// if (options.reporter) {
|
||||
// command += ` --reporter=${options.reporter}`;
|
||||
// }
|
||||
//
|
||||
// // 添加并行数量
|
||||
// if (options.parallel) {
|
||||
// command += ` --workers=${options.parallel}`;
|
||||
// }
|
||||
//
|
||||
// // 添加截图选项
|
||||
// if (options.screenshot) {
|
||||
// command += ' --screenshot=on';
|
||||
// }
|
||||
//
|
||||
// // 添加视频选项
|
||||
// if (options.video) {
|
||||
// command += ' --video=on';
|
||||
// }
|
||||
//
|
||||
// // 添加无头模式选项
|
||||
// if (!options.headless) {
|
||||
// command += ' --headed';
|
||||
// }
|
||||
//
|
||||
// // 添加调试模式
|
||||
// if (options.debug) {
|
||||
// command += ' --debug';
|
||||
// }
|
||||
//
|
||||
// // 添加UI模式
|
||||
// if (options.ui) {
|
||||
// command = 'npx playwright test --ui';
|
||||
// }
|
||||
//
|
||||
// // 添加配置文件
|
||||
// if (options.config) {
|
||||
// command += ` --config="${options.config}"`;
|
||||
// }
|
||||
//
|
||||
// console.log(chalk.blue('运行命令:'), chalk.yellow(command));
|
||||
//
|
||||
// try {
|
||||
// // 执行命令
|
||||
// execSync(command, {stdio: 'inherit'});
|
||||
// console.log(chalk.green('测试完成!'));
|
||||
// } catch (error) {
|
||||
// console.error(chalk.red('测试执行失败:'), error.message);
|
||||
// process.exit(1);
|
||||
// }
|
||||
25
src/controllers/LongiTestController.js
Normal file
25
src/controllers/LongiTestController.js
Normal file
@ -0,0 +1,25 @@
|
||||
const menuDataService = require('../services/LongiTestService');
|
||||
|
||||
/**
|
||||
* 测试控制器
|
||||
* 负责协调测试流程
|
||||
*/
|
||||
class LongiTestController {
|
||||
/**
|
||||
* 获取并保存菜单数据
|
||||
* @returns {Promise<Array>} 菜单数据数组
|
||||
*/
|
||||
async fetchAndSaveMenuData() {
|
||||
return await menuDataService.fetchAndSaveMenuData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行所有菜单的测试
|
||||
* @returns {Promise<Object>} - 测试结果
|
||||
*/
|
||||
async runAllTests() {
|
||||
return await menuDataService.runAllTests();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LongiTestController;
|
||||
11
src/hooks/setup.js
Normal file
11
src/hooks/setup.js
Normal file
@ -0,0 +1,11 @@
|
||||
const testLifecycle = require('./testLifecycle');
|
||||
|
||||
/**
|
||||
* 全局设置
|
||||
* 在所有测试开始前执行
|
||||
*/
|
||||
async function globalSetup() {
|
||||
await testLifecycle.beforeAll();
|
||||
}
|
||||
|
||||
module.exports = globalSetup;
|
||||
18
src/hooks/teardown.js
Normal file
18
src/hooks/teardown.js
Normal file
@ -0,0 +1,18 @@
|
||||
const testLifecycle = require('./testLifecycle');
|
||||
|
||||
/**
|
||||
* 全局清理
|
||||
* 在所有测试结束后执行
|
||||
*/
|
||||
async function globalTeardown() {
|
||||
console.log('开始执行全局清理...');
|
||||
try {
|
||||
await testLifecycle.afterAll();
|
||||
console.log('全局清理完成');
|
||||
} catch (error) {
|
||||
console.error('全局清理出错:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = globalTeardown;
|
||||
183
src/hooks/testLifecycle.js
Normal file
183
src/hooks/testLifecycle.js
Normal file
@ -0,0 +1,183 @@
|
||||
const performanceService = require('../services/PerformanceService');
|
||||
const FileUtils = require('../utils/FileUtils');
|
||||
|
||||
/**
|
||||
* 测试生命周期管理
|
||||
* 负责管理测试的全局生命周期,包括:
|
||||
* 1. 测试开始前的初始化工作
|
||||
* 2. 测试结束后的清理和报告生成
|
||||
* 3. 其他全局测试设置
|
||||
*/
|
||||
class TestLifecycle {
|
||||
constructor() {
|
||||
this.reportPath = process.env.PERFORMANCE_REPORT_PATH || './test-results/performance-report.html';
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试开始前的初始化
|
||||
* 在所有测试开始前执行,用于准备测试环境
|
||||
*/
|
||||
async beforeAll() {
|
||||
// 确保测试结果目录存在
|
||||
await FileUtils.ensureDirectoryExists('./test-results');
|
||||
// 清空之前的性能数据
|
||||
await performanceService.clearPerformanceData();
|
||||
console.log('✨ 环境初始化完成');
|
||||
}
|
||||
|
||||
async afterAll() {
|
||||
console.log('开始生成测试报告...');
|
||||
try {
|
||||
const performanceData = performanceService.getPerformanceData();
|
||||
console.log('获取到性能数据:', performanceData.length, '条记录');
|
||||
// 获取当前环境
|
||||
const env = process.env.NODE_ENV || 'dev';
|
||||
const report = this.generateReport(performanceData, env);
|
||||
await this.saveReport(report);
|
||||
console.log(`📊 ${env} 测试报告已生成: ${this.reportPath}`);
|
||||
} catch (error) {
|
||||
console.error('生成测试报告出错:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成测试报告
|
||||
* @param {Object} data 性能数据对象
|
||||
* @param {String} env 当前环境
|
||||
* @returns {String} 测试报告字符串
|
||||
*/
|
||||
generateReport(performanceData, env) {
|
||||
// 统计数据
|
||||
const totalTests = performanceData.length;
|
||||
const successTests = performanceData.filter(record => record.success).length;
|
||||
const failedTests = totalTests - successTests;
|
||||
const successRate = totalTests > 0 ? ((successTests / totalTests) * 100).toFixed(2) : '0.00';
|
||||
const totalDuration = performanceData.reduce((sum, record) => sum + record.duration, 0);
|
||||
|
||||
// 按页面分组统计
|
||||
const pageStats = {};
|
||||
performanceData.forEach(record => {
|
||||
const { pageName, duration, success, errorMessage } = record;
|
||||
if (!pageStats[pageName]) {
|
||||
pageStats[pageName] = {
|
||||
totalTests: 0,
|
||||
successCount: 0,
|
||||
failureCount: 0,
|
||||
totalDuration: 0,
|
||||
errors: []
|
||||
};
|
||||
}
|
||||
|
||||
const stats = pageStats[pageName];
|
||||
stats.totalTests++;
|
||||
success ? stats.successCount++ : stats.failureCount++;
|
||||
stats.totalDuration += duration;
|
||||
|
||||
if (!success && errorMessage) {
|
||||
stats.errors.push(errorMessage);
|
||||
}
|
||||
});
|
||||
|
||||
// 生成HTML报告
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>测试执行报告 (${env})</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
|
||||
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||
.header { text-align: center; margin-bottom: 30px; }
|
||||
.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px; }
|
||||
.summary-item { background: #f8f9fa; padding: 15px; border-radius: 6px; text-align: center; }
|
||||
.success { color: #28a745; }
|
||||
.failure { color: #dc3545; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
|
||||
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #dee2e6; }
|
||||
th { background: #f8f9fa; }
|
||||
tr:hover { background: #f8f9fa; }
|
||||
.status-true { color: #28a745; }
|
||||
.status-false { color: #dc3545; }
|
||||
.error-list { color: #dc3545; margin-top: 5px; font-size: 0.9em; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>测试执行报告 (${env})</h1>
|
||||
<p>执行时间: ${new Date().toLocaleString()}</p>
|
||||
</div>
|
||||
|
||||
<div class="summary">
|
||||
<div class="summary-item">
|
||||
<h3>总用例数</h3>
|
||||
<p>${totalTests}</p>
|
||||
</div>
|
||||
<div class="summary-item success">
|
||||
<h3>成功数</h3>
|
||||
<p>${successTests}</p>
|
||||
</div>
|
||||
<div class="summary-item failure">
|
||||
<h3>失败数</h3>
|
||||
<p>${failedTests}</p>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<h3>成功率</h3>
|
||||
<p>${successRate}%</p>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<h3>总耗时</h3>
|
||||
<p>${(totalDuration / 1000).toFixed(2)}秒</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>测试执行详情</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>页面名称</th>
|
||||
<th>测试结果</th>
|
||||
<th>执行时间</th>
|
||||
<th>成功数/总数</th>
|
||||
<th>错误信息</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${performanceData.map(record => {
|
||||
const stats = pageStats[record.pageName];
|
||||
return `
|
||||
<tr>
|
||||
<td>${record.pageName}</td>
|
||||
<td class="status-${record.success}">${record.success ? '通过' : '失败'}</td>
|
||||
<td>${(record.duration / 1000).toFixed(2)}秒</td>
|
||||
<td>${stats ? `${stats.successCount}/${stats.totalTests}` : '0/0'}</td>
|
||||
<td class="status-false">${record.errorMessage || ''}</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存测试报告
|
||||
* @param {String} report 测试报告字符串
|
||||
* @returns {String} 保存后的报告路径
|
||||
*/
|
||||
async saveReport(report) {
|
||||
try {
|
||||
await FileUtils.writeFile(this.reportPath, report);
|
||||
return this.reportPath;
|
||||
} catch (error) {
|
||||
console.error('保存测试报告失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new TestLifecycle();
|
||||
@ -3,6 +3,8 @@
|
||||
* 提供所有页面共用的方法和属性
|
||||
*/
|
||||
|
||||
const performanceService = require('../services/PerformanceService');
|
||||
|
||||
class BasePage {
|
||||
/**
|
||||
* 创建页面对象
|
||||
@ -74,10 +76,34 @@ class BasePage {
|
||||
* @param {string} url 目标URL
|
||||
*/
|
||||
async navigate(url) {
|
||||
// 先进行初始导航
|
||||
await this.page.goto(url, {
|
||||
waitUntil: 'networkidle',
|
||||
timeout: this.config.timeout.navigate
|
||||
});
|
||||
|
||||
// 获取当前URL
|
||||
const currentUrl = await this.page.url();
|
||||
|
||||
// 如果原始URL包含hash或query参数,但当前URL没有,则重新导航
|
||||
if (url !== currentUrl) {
|
||||
// 解析原始URL中的hash和query参数
|
||||
const urlParts = url.split('#');
|
||||
if (urlParts.length > 1) {
|
||||
// 等待一下以确保页面已经初步加载
|
||||
await this.page.waitForTimeout(1000);
|
||||
|
||||
// 使用evaluate来设置完整的URL
|
||||
await this.page.evaluate((fullUrl) => {
|
||||
window.location.href = fullUrl;
|
||||
}, url);
|
||||
|
||||
// 等待页面加载完成
|
||||
await this.page.waitForLoadState('networkidle', {
|
||||
timeout: this.config.timeout.navigate
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -90,7 +116,7 @@ class BasePage {
|
||||
*/
|
||||
async waitForElement(selector, options = {}) {
|
||||
try {
|
||||
const element = options.firstOnly ? this.page.locator(selector).first() : this.page.locator(selector);
|
||||
let element = options.firstOnly ? this.page.locator(selector).first() : this.page.locator(selector);
|
||||
|
||||
await element.waitFor({
|
||||
state: 'visible',
|
||||
@ -112,7 +138,7 @@ class BasePage {
|
||||
* @param {Object} options 选项
|
||||
*/
|
||||
async clickBySelector(selector, options = {}) {
|
||||
const element = await this.waitForElement(selector, options);
|
||||
let element = await this.waitForElement(selector, options);
|
||||
await element.click(options);
|
||||
}
|
||||
|
||||
@ -166,7 +192,7 @@ class BasePage {
|
||||
* @param {Object} options 选项
|
||||
*/
|
||||
async fill(selector, value, options = {}) {
|
||||
const element = await this.waitForElement(selector, options);
|
||||
let element = await this.waitForElement(selector, options);
|
||||
await element.fill(value);
|
||||
}
|
||||
|
||||
@ -192,20 +218,28 @@ class BasePage {
|
||||
async waitForIBPPageLoadWithRetry(pageName) {
|
||||
console.log(`等待页面 ${pageName} 数据加载...`);
|
||||
|
||||
let startTime = Date.now();
|
||||
let retryCount = 0;
|
||||
const {maxRetries, retryInterval, stabilityDelay} = this.config.pageLoad;
|
||||
let {maxRetries, retryInterval, stabilityDelay} = this.config.pageLoad;
|
||||
let errorMessage = '';
|
||||
|
||||
try {
|
||||
while (retryCount < maxRetries) {
|
||||
// 检查错误状态
|
||||
const hasError = await this.checkPageError(pageName);
|
||||
if (hasError) return false;
|
||||
let hasError = await this.checkPageError(pageName);
|
||||
if (hasError) {
|
||||
errorMessage = await this.getErrorMessage();
|
||||
break;
|
||||
}
|
||||
|
||||
// 检查加载状态
|
||||
const isLoading = await this.elementExistsBySelector(this.selectors.loadingMask);
|
||||
let isLoading = await this.elementExistsBySelector(this.selectors.loadingMask);
|
||||
if (!isLoading) {
|
||||
await this.waitForTimeout(stabilityDelay);
|
||||
console.log(`✅ 页面 ${pageName} 加载完成`);
|
||||
|
||||
// 记录成功的性能数据
|
||||
await performanceService.recordSuccess(pageName, startTime, Date.now());
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -213,9 +247,17 @@ class BasePage {
|
||||
await this.waitForTimeout(retryInterval);
|
||||
}
|
||||
|
||||
console.error(`页面加载超时: ${pageName}, 重试次数: ${maxRetries}`);
|
||||
if (retryCount >= maxRetries) {
|
||||
errorMessage = `页面加载超时: ${pageName}, 重试次数: ${maxRetries}`;
|
||||
}
|
||||
|
||||
// 记录失败的性能数据
|
||||
await performanceService.recordFailure(pageName, startTime, Date.now(), errorMessage);
|
||||
console.error(errorMessage);
|
||||
return false;
|
||||
} catch (error) {
|
||||
// 记录异常的性能数据
|
||||
await performanceService.recordError(pageName, startTime, Date.now(), error);
|
||||
console.error(`页面加载出错: ${pageName}, 错误信息: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
@ -242,6 +284,21 @@ class BasePage {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误信息
|
||||
* @returns {Promise<string>} 错误信息
|
||||
* @private
|
||||
*/
|
||||
async getErrorMessage() {
|
||||
const errorSelectors = [this.selectors.errorBox, this.selectors.errorMessage];
|
||||
for (const selector of errorSelectors) {
|
||||
const elements = await this.page.locator(selector).all();
|
||||
if (elements.length > 0) {
|
||||
return await this.getTextByElement(elements[0]);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前URL
|
||||
@ -251,7 +308,6 @@ class BasePage {
|
||||
return this.page.url();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 等待指定时间
|
||||
* @param {number} ms 等待时间(毫秒)
|
||||
@ -260,6 +316,14 @@ class BasePage {
|
||||
await this.page.waitForTimeout(ms);
|
||||
}
|
||||
|
||||
async waitForVisible(selectors) {
|
||||
await this.page.locator(selectors).waitFor({
|
||||
state: 'visible',
|
||||
timeout: 10000
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 检查元素是否存在
|
||||
* @param {string} selector 元素选择器
|
||||
@ -51,8 +51,12 @@ class LongiLoginPage extends BasePage {
|
||||
* 导航到登录页面
|
||||
*/
|
||||
async navigateToLoginPage() {
|
||||
console.log('当前使用的 BASE_URL:', process.env.BASE_URL);
|
||||
await this.navigate(process.env.BASE_URL);
|
||||
const baseUrl = process.env.BASE_URL;
|
||||
console.log('当前使用的 BASE_URL:', baseUrl);
|
||||
|
||||
// 确保使用完整的URL,包括hash和query参数
|
||||
const fullUrl = baseUrl.includes('#') ? baseUrl : `${baseUrl}#/login?debug=ly`;
|
||||
await this.navigate(fullUrl);
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
@ -96,7 +100,10 @@ class LongiLoginPage extends BasePage {
|
||||
* @param {string} username 用户名
|
||||
*/
|
||||
async enterUsername(username) {
|
||||
await this.page.locator(this.selectors.usernameInput).fill('');
|
||||
await this.fill(this.selectors.usernameInput, username);
|
||||
// 等待输入稳定
|
||||
await this.page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -104,45 +111,51 @@ class LongiLoginPage extends BasePage {
|
||||
* @param {string} password 密码
|
||||
*/
|
||||
async enterPassword(password) {
|
||||
await this.page.locator(this.selectors.passwordInput).fill('');
|
||||
await this.fill(this.selectors.passwordInput, password);
|
||||
// 等待输入稳定
|
||||
await this.page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 输入验证码
|
||||
* @param {string} captcha 验证码
|
||||
*/
|
||||
async enterCaptcha(captcha) {
|
||||
await this.fill(this.selectors.captchaInput, captcha);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取验证码图片元素
|
||||
* @returns {Promise<import('@playwright/test').Locator>} 验证码图片元素
|
||||
*/
|
||||
async getCaptchaImage() {
|
||||
return this.waitForElement(this.selectors.captchaImage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行登录操作
|
||||
* @param {string} username 用户名
|
||||
* @param {string} password 密码
|
||||
* @param {string} captcha 验证码
|
||||
* @returns {Promise<boolean>} 登录是否成功
|
||||
*/
|
||||
async login(username, password, captcha) {
|
||||
async login() {
|
||||
try {
|
||||
await this.enterUsername(username);
|
||||
await this.enterPassword(password);
|
||||
await this.enterCaptcha(captcha);
|
||||
console.log('开始登录流程...');
|
||||
console.log('使用的登录信息:', {
|
||||
username: process.env.IBP_USERNAME,
|
||||
baseUrl: process.env.BASE_URL
|
||||
});
|
||||
|
||||
if (await this.clickLoginButton()) {
|
||||
return await this.isLoginSuccessful();
|
||||
// 确保页面已加载
|
||||
await this.waitForPageLoad();
|
||||
|
||||
await this.waitForVisible(this.selectors.usernameInput);
|
||||
console.log('用户名输入框已就绪');
|
||||
|
||||
// 清空并输入用户名
|
||||
await this.enterUsername(process.env.IBP_USERNAME);
|
||||
console.log('用户名已输入');
|
||||
|
||||
await this.waitForVisible(this.selectors.passwordInput);
|
||||
console.log('密码输入框已就绪');
|
||||
|
||||
await this.enterPassword(process.env.IBP_PASSWORD);
|
||||
console.log('密码已输入');
|
||||
|
||||
// 点击登录按钮
|
||||
console.log('准备点击登录按钮...');
|
||||
const loginSuccess = await this.clickLoginButton();
|
||||
if (!loginSuccess) {
|
||||
throw new error('登录失败')
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('登录过程出错:', error.message);
|
||||
return false;
|
||||
console.error('登录过程出错:', error);
|
||||
// 记录更多错误信息
|
||||
console.error('错误堆栈:', error.stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -33,9 +33,11 @@ class LongiMainPage extends BasePage {
|
||||
thirdLevelIndicator: '.el-icon-arrow-right',
|
||||
subMenuIndicator: '.el-sub-menu__icon-arrow',
|
||||
// Tab相关
|
||||
tabContainer: '.workSpaceBaseTab .el-tabs__item',
|
||||
tabContainer: '.workSpaceBaseTab>.el-tabs__header .el-tabs__item',
|
||||
activeTab: '.vab-tabs .el-tabs--card .el-tabs__item.is-active',
|
||||
closeButton: '.el-icon.is-icon-close'
|
||||
closeButton: '.el-icon.is-icon-close',
|
||||
tabItems: '.workSpaceBaseTab .el-tabs__item',
|
||||
nextTabButton: '.el-icon.is-icon-next'
|
||||
});
|
||||
}
|
||||
|
||||
@ -102,64 +104,24 @@ class LongiMainPage extends BasePage {
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查菜单数据文件是否存在并加载数据
|
||||
* 获取菜单项数据
|
||||
*/
|
||||
async checkAndLoadMenuItems() {
|
||||
async getMenuItems() {
|
||||
try {
|
||||
// 加载JSON文件
|
||||
const menuItems = FileUtils.loadFromJsonFile(process.env.MENU_DATA_FILE_PATH);
|
||||
|
||||
// 检查是否成功加载数据
|
||||
if (menuItems && Array.isArray(menuItems) && menuItems.length > 0) {
|
||||
console.log(`从文件 ${process.env.BASE_URL} 成功加载了 ${menuItems.length} 个菜单项`);
|
||||
return menuItems;
|
||||
} else {
|
||||
await this.expandSideMenu();
|
||||
return await this.findAndSaveMenuItems();
|
||||
}
|
||||
return await this.findMenuItems();
|
||||
} catch (error) {
|
||||
// 文件操作错误需要被捕获并处理
|
||||
console.error(`检查并加载菜单项时出错: ${error}`);
|
||||
console.error(`获取菜单项时出错: ${error}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找菜单项并保存到文件
|
||||
*/
|
||||
async findAndSaveMenuItems() {
|
||||
// 查找菜单项
|
||||
const menuItems = await this.findMenuItems();
|
||||
|
||||
// 如果没有找到菜单项,则返回空数组
|
||||
if (!menuItems || menuItems.length === 0) {
|
||||
console.warn('未找到任何菜单项,无法保存到文件');
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
// 过滤掉不能序列化的element属性
|
||||
const menuItemsForSave = menuItems.map(({element, ...rest}) => rest);
|
||||
|
||||
// 保存到文件
|
||||
FileUtils.saveToJsonFile(menuItemsForSave, process.env.MENU_DATA_FILE_PATH);
|
||||
|
||||
console.log(`已找到并保存 ${menuItems.length} 个菜单项到文件: ${process.env.MENU_DATA_FILE_PATH}`);
|
||||
|
||||
return menuItems;
|
||||
} catch (error) {
|
||||
// 文件操作错误需要被捕获
|
||||
console.error('保存菜单项到文件时出错:', error);
|
||||
return menuItems; // 即使保存失败也返回找到的菜单项
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找所有菜单项
|
||||
* @returns {Promise<Array>} 菜单项数组
|
||||
*/
|
||||
async findMenuItems() {
|
||||
console.log('开始查找菜单项...');
|
||||
// console.log('开始查找菜单项...');
|
||||
|
||||
// 等待菜单加载完成
|
||||
await this.page.waitForSelector(this.selectors.sideNav, {
|
||||
@ -168,7 +130,7 @@ class LongiMainPage extends BasePage {
|
||||
|
||||
// 获取所有菜单项
|
||||
const items = await this.page.locator(this.selectors.menuItems).all();
|
||||
console.log(`找到 ${items.length} 个菜单项元素`);
|
||||
// console.log(`找到 ${items.length} 个菜单项元素`);
|
||||
const menuItems = [];
|
||||
|
||||
// 处理每个菜单项
|
||||
@ -194,7 +156,6 @@ class LongiMainPage extends BasePage {
|
||||
|
||||
// 检查是否有三级菜单指示器
|
||||
const hasThirdLevelIndicator = await item.evaluate(el => {
|
||||
// 检查是否有特定的三级菜单指示器
|
||||
return el.classList.contains('is-opened') ||
|
||||
el.querySelector('.third-level-menu') !== null ||
|
||||
el.querySelector('.el-menu--inline') !== null;
|
||||
@ -206,7 +167,7 @@ class LongiMainPage extends BasePage {
|
||||
// 综合判断是否有三级菜单
|
||||
const hasThirdMenu = isSubMenuTitle || hasSubMenuIndicator || hasThirdLevelIndicator;
|
||||
|
||||
console.log(`菜单项 "${text.trim()}" ${hasThirdMenu ? '有' : '没有'}三级菜单 (通过DOM结构判断)`);
|
||||
// console.log(`菜单项 "${text.trim()}" ${hasThirdMenu ? '有' : '没有'}三级菜单 (通过DOM结构判断)`);
|
||||
|
||||
// 生成唯一标识符,结合索引和文本
|
||||
const uniqueId = `menu_${i}_${text.trim().replace(/\s+/g, '_')}`;
|
||||
@ -406,9 +367,7 @@ class LongiMainPage extends BasePage {
|
||||
* @param {Object} parentMenu 父级菜单(可选)
|
||||
*/
|
||||
async handleMenuClick(menuInfo, parentMenu = null) {
|
||||
const menuPath = await this.getMenuPath(menuInfo, parentMenu);
|
||||
console.log(`点击菜单: ${menuPath}`);
|
||||
|
||||
const menuPath = this.getMenuPath(menuInfo, parentMenu);
|
||||
if (!await this.clickByElement(menuInfo.element)) {
|
||||
return;
|
||||
}
|
||||
@ -423,21 +382,30 @@ class LongiMainPage extends BasePage {
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单个TAB页
|
||||
* @param {Object} tabInfo TAB页信息对象
|
||||
* @param {Object} parentMenu 父级菜单对象
|
||||
* 查找标签页信息
|
||||
* @param {Object} menu 菜单对象
|
||||
* @returns {Promise<Array>} 标签页信息数组
|
||||
* @private
|
||||
*/
|
||||
async handleSingleTab(tabInfo, parentMenu) {
|
||||
try {
|
||||
const menuPath = parentMenu.path || parentMenu.text;
|
||||
const tabPath = `${menuPath} > ${tabInfo.text}`;
|
||||
console.log(`🔹 处理TAB页: ${tabPath}`);
|
||||
|
||||
await tabInfo.element.click();
|
||||
await this.waitForIBPPageLoadWithRetry(tabPath);
|
||||
} catch (error) {
|
||||
console.error(`处理TAB页失败 [${parentMenu.text} > ${tabInfo.text}]:`, error.message);
|
||||
async findTabInfos(menu) {
|
||||
let tabs = await this.page.locator(this.selectors.tabContainer).all();
|
||||
if (tabs.length === 0) {
|
||||
console.log(`📝 ${menu.text} 没有TAB页`);
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log(`📑 ${menu.text} 找到 ${tabs.length} 个TAB页`);
|
||||
let tabInfos = [];
|
||||
for (const tab of tabs) {
|
||||
let text = await tab.textContent();
|
||||
let isActive = await tab.evaluate(el => el.classList.contains('is-active'));
|
||||
tabInfos.push({
|
||||
text: text.trim(),
|
||||
element: tab,
|
||||
isActive: isActive
|
||||
});
|
||||
}
|
||||
return tabInfos;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -448,21 +416,19 @@ class LongiMainPage extends BasePage {
|
||||
try {
|
||||
await this.waitForTimeout(1000);
|
||||
|
||||
const tabs = await this.page.locator(this.selectors.tabContainer).all();
|
||||
if (tabs.length === 0) {
|
||||
console.log(`📝 ${menu.text} 没有TAB页`);
|
||||
let tabInfos = await this.findTabInfos(menu);
|
||||
if (tabInfos.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`📑 ${menu.text} 找到 ${tabs.length} 个TAB页`);
|
||||
const tabInfos = await this.findTabInfos(tabs);
|
||||
|
||||
let hasSkippedActiveTab = false;
|
||||
for (const tabInfo of tabInfos) {
|
||||
if (!tabInfo.isActive) {
|
||||
await this.handleSingleTab(tabInfo, menu);
|
||||
} else {
|
||||
if (tabInfo.isActive && !hasSkippedActiveTab) {
|
||||
console.log(`⏭️ 跳过当前激活的TAB页: ${menu.text} > ${tabInfo.text}`);
|
||||
hasSkippedActiveTab = true;
|
||||
continue;
|
||||
}
|
||||
await this.handleSingleTab(tabInfo, menu);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`处理TAB页失败 [${menu.text}]:`, error.message);
|
||||
@ -470,19 +436,20 @@ class LongiMainPage extends BasePage {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取TAB页信息
|
||||
* @param {Array<Object>} tabs TAB页元素数组
|
||||
* @returns {Promise<Array>} TAB页信息数组
|
||||
* @private
|
||||
* 处理单个TAB页
|
||||
* @param {Object} tabInfo TAB页信息对象
|
||||
* @param {Object} parentMenu 父级菜单对象
|
||||
*/
|
||||
async findTabInfos(tabs) {
|
||||
return Promise.all(
|
||||
tabs.map(async element => ({
|
||||
text: await this.getTextByElement(element),
|
||||
isActive: await element.evaluate(el => el.classList.contains('is-active')),
|
||||
element: element
|
||||
}))
|
||||
);
|
||||
async handleSingleTab(tabInfo, parentMenu) {
|
||||
let menuPath, tabPath = '';
|
||||
try {
|
||||
menuPath = parentMenu.path || parentMenu.text;
|
||||
tabPath = `${menuPath} > ${tabInfo.text}`;
|
||||
await tabInfo.element.click();
|
||||
await this.waitForIBPPageLoadWithRetry(tabPath);
|
||||
} catch (error) {
|
||||
console.error(`处理TAB页失败 [${tabPath}]:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -490,11 +457,8 @@ class LongiMainPage extends BasePage {
|
||||
*/
|
||||
async closeActiveTab(parentMenu) {
|
||||
try {
|
||||
console.log(`🗑️ 正在关闭页面 "${parentMenu.text}" 的tab...`);
|
||||
|
||||
const activeTab = this.page.locator(this.selectors.activeTab);
|
||||
const closeButton = activeTab.locator(this.selectors.closeButton);
|
||||
|
||||
let activeTab = this.page.locator(this.selectors.activeTab);
|
||||
let closeButton = activeTab.locator(this.selectors.closeButton);
|
||||
if (await this.canCloseTab(activeTab, closeButton)) {
|
||||
await closeButton.waitFor({state: 'visible', timeout: 5000});
|
||||
await this.clickByElement(closeButton);
|
||||
@ -515,8 +479,8 @@ class LongiMainPage extends BasePage {
|
||||
* @private
|
||||
*/
|
||||
async canCloseTab(activeTab, closeButton) {
|
||||
const hasActiveTab = await activeTab.count() > 0;
|
||||
const hasCloseButton = await closeButton.count() > 0;
|
||||
let hasActiveTab = await activeTab.count() > 0;
|
||||
let hasCloseButton = await closeButton.count() > 0;
|
||||
return hasActiveTab && hasCloseButton;
|
||||
}
|
||||
|
||||
@ -527,7 +491,7 @@ class LongiMainPage extends BasePage {
|
||||
async cleanupMemory(context = '') {
|
||||
try {
|
||||
await this.page.evaluate((selector) => {
|
||||
const elements = document.querySelectorAll(selector);
|
||||
let elements = document.querySelectorAll(selector);
|
||||
elements.forEach(el => el.remove());
|
||||
if (window.gc) window.gc();
|
||||
}, this.selectors.temporaryElements);
|
||||
108
src/scripts/runTestsAndSendReport.js
Normal file
108
src/scripts/runTestsAndSendReport.js
Normal file
@ -0,0 +1,108 @@
|
||||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const emailService = require('../services/EmailService');
|
||||
const dotenv = require('dotenv');
|
||||
|
||||
// 获取环境参数
|
||||
const env = process.env.TEST_ENV || 'dev'; // 默认使用dev环境
|
||||
console.log(`[Node] Starting test execution process...`);
|
||||
console.log(`[Node] Current environment: ${env}`);
|
||||
|
||||
async function runTestsAndSendReport() {
|
||||
const startTime = new Date();
|
||||
console.log(`[Node] Test started at: ${startTime.toLocaleString()}`);
|
||||
|
||||
try {
|
||||
// 加载环境变量
|
||||
const envFile = `.env.${env}`;
|
||||
console.log(`[Node] Loading environment file: ${envFile}`);
|
||||
|
||||
if (!fs.existsSync(envFile)) {
|
||||
throw new Error(`Environment file ${envFile} not found`);
|
||||
}
|
||||
|
||||
// 加载环境变量
|
||||
const envConfig = dotenv.config({ path: envFile });
|
||||
if (envConfig.error) {
|
||||
throw new Error(`Failed to load environment variables: ${envConfig.error.message}`);
|
||||
}
|
||||
|
||||
// 验证必要的环境变量
|
||||
if (!process.env.BASE_URL) {
|
||||
throw new Error('BASE_URL environment variable is not set in ' + envFile);
|
||||
}
|
||||
|
||||
console.log(`[Node] Environment loaded: BASE_URL = ${process.env.BASE_URL}`);
|
||||
|
||||
// 运行测试
|
||||
console.log('[Node] Starting Playwright test execution...');
|
||||
execSync('npx playwright test', {
|
||||
stdio: 'inherit',
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: env
|
||||
}
|
||||
});
|
||||
console.log('[Node] Test execution completed');
|
||||
|
||||
// 读取性能报告
|
||||
console.log('[Node] Reading performance report...');
|
||||
const reportPath = path.join(process.cwd(), 'test-results', 'performance-report.html');
|
||||
const performanceReport = fs.readFileSync(reportPath, 'utf8');
|
||||
|
||||
// 发送邮件
|
||||
console.log('[Node] Sending test report email...');
|
||||
const emailRecipients = process.env.EMAIL_RECIPIENTS || 'wanghaipeng@iscmtech.com';
|
||||
console.log(`[Node] Email recipients: ${emailRecipients}`);
|
||||
const result = await emailService.sendMail({
|
||||
to: emailRecipients,
|
||||
subject: `隆基需求计划页面可用性-自动化测试报告 (${env}) - ${startTime.toLocaleDateString()}`,
|
||||
html: performanceReport
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log('[Node] Test report email sent successfully');
|
||||
} else {
|
||||
console.error('[Node] Failed to send test report email:', result.error);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Node] Error:', error.message);
|
||||
|
||||
// 如果测试失败,尝试读取已生成的报告
|
||||
try {
|
||||
const reportPath = path.join(process.cwd(), 'test-results', 'performance-report.html');
|
||||
if (fs.existsSync(reportPath)) {
|
||||
const performanceReport = fs.readFileSync(reportPath, 'utf8');
|
||||
const emailRecipients = process.env.EMAIL_RECIPIENTS || 'wanghaipeng@iscmtech.com';
|
||||
const result = await emailService.sendMail({
|
||||
to: emailRecipients,
|
||||
subject: `隆基需求计划页面可用性-自动化测试报告 (${env}) - ${startTime.toLocaleDateString()}`,
|
||||
html: performanceReport
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log('[Node] Error report email sent successfully');
|
||||
}
|
||||
} else {
|
||||
console.error('[Node] No performance report found');
|
||||
}
|
||||
} catch (emailError) {
|
||||
console.error('[Node] Failed to send error report:', emailError.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 执行脚本
|
||||
console.log('[Node] ==========================================');
|
||||
console.log('[Node] Starting test automation process...');
|
||||
console.log('[Node] ==========================================');
|
||||
runTestsAndSendReport().then(() => {
|
||||
console.log('[Node] Process will exit in 3 seconds...');
|
||||
setTimeout(() => process.exit(0), 3000);
|
||||
}).catch(error => {
|
||||
console.error('[Node] Fatal error:', error);
|
||||
console.log('[Node] Process will exit in 3 seconds...');
|
||||
setTimeout(() => process.exit(1), 3000);
|
||||
});
|
||||
31
src/scripts/testEmail.js
Normal file
31
src/scripts/testEmail.js
Normal file
@ -0,0 +1,31 @@
|
||||
const emailService = require('../services/EmailService');
|
||||
|
||||
async function testEmailSending() {
|
||||
const testEmail = {
|
||||
to: 'dengqichen@iscmtech.com', // 修改为指定的收件邮箱
|
||||
subject: '测试邮件 - Playwright自动化测试',
|
||||
html: `
|
||||
<h1>Playwright自动化测试邮件</h1>
|
||||
<p>这是一封测试邮件,用于验证邮件发送功能是否正常工作。</p>
|
||||
<p>发送时间:${new Date().toLocaleString()}</p>
|
||||
`
|
||||
};
|
||||
|
||||
try {
|
||||
console.log('开始发送测试邮件...');
|
||||
const result = await emailService.sendMail(testEmail);
|
||||
|
||||
if (result.success) {
|
||||
console.log('测试邮件发送成功!');
|
||||
console.log('消息ID:', result.messageId);
|
||||
} else {
|
||||
console.error('测试邮件发送失败!');
|
||||
console.error('错误信息:', result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('执行测试时发生错误:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行测试
|
||||
testEmailSending();
|
||||
43
src/services/EmailService.js
Normal file
43
src/services/EmailService.js
Normal file
@ -0,0 +1,43 @@
|
||||
const nodemailer = require('nodemailer');
|
||||
|
||||
class EmailService {
|
||||
constructor() {
|
||||
this.transporter = nodemailer.createTransport({
|
||||
host: 'smtphz.qiye.163.com',
|
||||
port: 465,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: 'monitor@iscmtech.com',
|
||||
pass: 'qiye2024@'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送邮件
|
||||
* @param {Object} options 邮件选项
|
||||
* @param {string} options.to 收件人邮箱
|
||||
* @param {string} options.subject 邮件主题
|
||||
* @param {string} options.html HTML格式的邮件内容
|
||||
* @returns {Promise<Object>} 发送结果
|
||||
*/
|
||||
async sendMail({ to, subject, html }) {
|
||||
try {
|
||||
const mailOptions = {
|
||||
from: 'monitor@iscmtech.com',
|
||||
to,
|
||||
subject,
|
||||
html
|
||||
};
|
||||
|
||||
const info = await this.transporter.sendMail(mailOptions);
|
||||
console.log('邮件发送成功:', info.messageId);
|
||||
return { success: true, messageId: info.messageId };
|
||||
} catch (error) {
|
||||
console.error('邮件发送失败:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new EmailService();
|
||||
345
src/services/LongiTestService.js
Normal file
345
src/services/LongiTestService.js
Normal file
@ -0,0 +1,345 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const LongiMainPage = require('../pages/LongiMainPage');
|
||||
const LongiLoginPage = require('../pages/LongiLoginPage');
|
||||
const {chromium} = require('@playwright/test');
|
||||
|
||||
/**
|
||||
* 菜单数据服务
|
||||
* 负责菜单数据的存储和检索,以及浏览器操作
|
||||
*/
|
||||
class LongiTestService {
|
||||
constructor() {
|
||||
this.initializeConfig();
|
||||
this.initializePaths();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化配置
|
||||
* @private
|
||||
*/
|
||||
initializeConfig() {
|
||||
this.batchSize = parseInt(process.env.TEST_BATCH_SIZE);
|
||||
this.retryCount = parseInt(process.env.TEST_RETRY_COUNT);
|
||||
this.batchInterval = parseInt(process.env.TEST_BATCH_INTERVAL);
|
||||
this.maxRetryDelay = parseInt(process.env.TEST_MAX_RETRY_DELAY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化路径
|
||||
* @private
|
||||
*/
|
||||
initializePaths() {
|
||||
this.dataDir = path.join(process.cwd(), process.env.TEST_DATA_DIR);
|
||||
this.menuDataPath = process.env.MENU_DATA_FILE_PATH;
|
||||
this.progressPath = process.env.TEST_PROGRESS_FILE_PATH;
|
||||
|
||||
if (!fs.existsSync(this.dataDir)) {
|
||||
fs.mkdirSync(this.dataDir, {recursive: true});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建浏览器实例
|
||||
* @returns {Promise<{browser: Browser, page: Page}>}
|
||||
* @private
|
||||
*/
|
||||
async createBrowser() {
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage();
|
||||
return {browser, page};
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行登录操作
|
||||
* @param {Page} page - Playwright页面对象
|
||||
* @returns {Promise<void>}
|
||||
* @private
|
||||
*/
|
||||
async performLogin(page) {
|
||||
const loginPage = new LongiLoginPage(page);
|
||||
await loginPage.navigateToLoginPage();
|
||||
await loginPage.login()
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行一批菜单的测试
|
||||
* @param {Array} menuBatch - 要测试的菜单数组
|
||||
*/
|
||||
async runBatchTest(menuBatch) {
|
||||
if (!menuBatch?.length) return;
|
||||
|
||||
console.log(`开始执行批次测试,包含 ${menuBatch.length} 个菜单项:`);
|
||||
console.log(menuBatch.map(m => m.text).join(', '));
|
||||
|
||||
let browser, page;
|
||||
try {
|
||||
({browser, page} = await this.createBrowser());
|
||||
await this.performLogin(page);
|
||||
|
||||
const mainPage = new LongiMainPage(page);
|
||||
await mainPage.handleAllMenuClicks(menuBatch);
|
||||
} catch (error) {
|
||||
console.error('批次测试执行失败:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新测试进度
|
||||
* @param {Array} completedBatch - 完成测试的菜单批次
|
||||
* @private
|
||||
*/
|
||||
async updateTestProgress(completedBatch) {
|
||||
if (!completedBatch?.length) return;
|
||||
|
||||
const progress = this.getProgress();
|
||||
const newProgress = [...progress];
|
||||
|
||||
// 验证并更新进度
|
||||
for (const menuItem of completedBatch) {
|
||||
if (!menuItem?.id) {
|
||||
console.warn('警告: 菜单项缺少ID', menuItem);
|
||||
continue;
|
||||
}
|
||||
if (!newProgress.includes(menuItem.id)) {
|
||||
newProgress.push(menuItem.id);
|
||||
}
|
||||
}
|
||||
|
||||
// 验证新进度的有效性
|
||||
const menuData = this.getMenuData();
|
||||
if (newProgress.length > menuData.length) {
|
||||
console.error('错误: 进度数超过总菜单数');
|
||||
return;
|
||||
}
|
||||
|
||||
this.saveProgress(newProgress);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下一批要测试的菜单
|
||||
* @returns {Array|null} - 下一批要测试的菜单,如果没有则返回null
|
||||
*/
|
||||
getNextBatch() {
|
||||
const menuData = this.getMenuData();
|
||||
const progress = this.getProgress();
|
||||
|
||||
if (!menuData) return null;
|
||||
|
||||
const remainingMenus = menuData.filter(menu => !progress.includes(menu.id));
|
||||
if (remainingMenus.length === 0) return null;
|
||||
|
||||
return remainingMenus.slice(0, this.batchSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取并保存菜单数据
|
||||
* @returns {Promise<Array>} 菜单数据数组
|
||||
*/
|
||||
async fetchAndSaveMenuData() {
|
||||
console.log('开始获取菜单数据...');
|
||||
let browser, page;
|
||||
|
||||
try {
|
||||
browser = await chromium.launch();
|
||||
page = await browser.newPage();
|
||||
// 登录
|
||||
await this.performLogin(page);
|
||||
// 获取菜单数据
|
||||
const mainPage = new LongiMainPage(page);
|
||||
const menuItems = await mainPage.getMenuItems();
|
||||
// 保存数据
|
||||
this.saveMenuData(menuItems);
|
||||
return menuItems;
|
||||
} catch (error) {
|
||||
console.error(`获取并保存菜单数据时出错: ${error}`);
|
||||
return null;
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存菜单数据
|
||||
* @param {Array} menuItems - 从页面获取的原始菜单项
|
||||
* @returns {Array} - 处理后的菜单数据
|
||||
*/
|
||||
saveMenuData(menuItems) {
|
||||
const menuData = menuItems.map((menu, index) => ({
|
||||
id: index + 1,
|
||||
text: menu.text,
|
||||
path: menu.path || menu.text,
|
||||
hasThirdMenu: menu.hasThirdMenu,
|
||||
parentMenu: menu.parentMenu
|
||||
}));
|
||||
fs.writeFileSync(this.menuDataPath, JSON.stringify(menuData, null, 2));
|
||||
return menuData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取菜单数据
|
||||
* @returns {Array|null} - 菜单数据数组,如果文件不存在则返回null
|
||||
*/
|
||||
getMenuData() {
|
||||
if (!fs.existsSync(this.menuDataPath)) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(this.menuDataPath, 'utf8'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存测试进度
|
||||
* @param {Array} completedMenus - 已完成测试的菜单ID数组
|
||||
*/
|
||||
saveProgress(completedMenus) {
|
||||
fs.writeFileSync(this.progressPath, JSON.stringify(completedMenus, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取测试进度
|
||||
* @returns {Array} - 已完成测试的菜单ID数组
|
||||
*/
|
||||
getProgress() {
|
||||
if (!fs.existsSync(this.progressPath)) {
|
||||
return [];
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(this.progressPath, 'utf8'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有数据文件
|
||||
*/
|
||||
clearAll() {
|
||||
if (fs.existsSync(this.menuDataPath)) {
|
||||
fs.unlinkSync(this.menuDataPath);
|
||||
}
|
||||
if (fs.existsSync(this.progressPath)) {
|
||||
fs.unlinkSync(this.progressPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行所有菜单的测试
|
||||
* @returns {Promise<Object>} - 测试结果
|
||||
*/
|
||||
async runAllTests() {
|
||||
try {
|
||||
// 获取菜单数据并验证
|
||||
const menuData = this.getMenuData();
|
||||
if (!menuData?.length) {
|
||||
throw new Error('没有可用的菜单数据,请先执行 fetchAndSaveMenuData');
|
||||
}
|
||||
|
||||
// 清理之前的进度
|
||||
this.saveProgress([]);
|
||||
|
||||
let retryCount = 0;
|
||||
let lastProgress = null;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
const batch = this.getNextBatch();
|
||||
if (!batch?.length) {
|
||||
const progress = this.getTestProgress();
|
||||
|
||||
// 验证进度数据的有效性
|
||||
if (!this.isValidProgress(progress)) {
|
||||
throw new Error('进度数据无效');
|
||||
}
|
||||
|
||||
if (progress.completed < progress.total && retryCount < this.retryCount) {
|
||||
console.log(`\n还有未完成的测试,尝试重试 (${retryCount + 1}/${this.retryCount})...`);
|
||||
retryCount++;
|
||||
const delay = Math.min(this.batchInterval * Math.pow(2, retryCount), this.maxRetryDelay);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
continue;
|
||||
}
|
||||
|
||||
return progress;
|
||||
}
|
||||
|
||||
await this.runBatchTest(batch);
|
||||
await this.updateTestProgress(batch);
|
||||
|
||||
// 验证进度是否正确更新
|
||||
const currentProgress = this.getTestProgress();
|
||||
if (lastProgress && currentProgress.completed <= lastProgress.completed) {
|
||||
console.warn('警告: 进度可能未正确更新');
|
||||
}
|
||||
lastProgress = currentProgress;
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, this.batchInterval));
|
||||
} catch (error) {
|
||||
console.error('测试执行失败:', error);
|
||||
if (error.message.includes('登录失败')) {
|
||||
throw error; // 登录失败直接终止
|
||||
}
|
||||
// 其他错误继续下一批次
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('运行所有测试失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证进度数据的有效性
|
||||
* @param {Object} progress - 进度数据
|
||||
* @returns {boolean} - 是否有效
|
||||
* @private
|
||||
*/
|
||||
isValidProgress(progress) {
|
||||
if (!progress || typeof progress !== 'object') return false;
|
||||
|
||||
const menuData = this.getMenuData();
|
||||
if (!menuData?.length) return false;
|
||||
|
||||
return (
|
||||
typeof progress.total === 'number' &&
|
||||
typeof progress.completed === 'number' &&
|
||||
typeof progress.remaining === 'number' &&
|
||||
progress.total === menuData.length &&
|
||||
progress.completed >= 0 &&
|
||||
progress.completed <= progress.total &&
|
||||
progress.remaining === (progress.total - progress.completed)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取测试进度信息
|
||||
* @returns {Object} - 进度信息,包含总数、已完成数和剩余数
|
||||
*/
|
||||
getTestProgress() {
|
||||
try {
|
||||
const menuData = this.getMenuData();
|
||||
const progress = this.getProgress();
|
||||
|
||||
if (!menuData?.length) {
|
||||
console.warn('警告: 没有可用的菜单数据');
|
||||
return {total: 0, completed: 0, remaining: 0};
|
||||
}
|
||||
|
||||
const completed = progress.length;
|
||||
const total = menuData.length;
|
||||
const remaining = total - completed;
|
||||
|
||||
// 验证数据一致性
|
||||
if (completed > total) {
|
||||
console.error('错误: 完成数超过总数');
|
||||
return {total, completed: total, remaining: 0};
|
||||
}
|
||||
|
||||
return {total, completed, remaining};
|
||||
} catch (error) {
|
||||
console.error('获取测试进度失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
module.exports = new LongiTestService();
|
||||
118
src/services/PerformanceService.js
Normal file
118
src/services/PerformanceService.js
Normal file
@ -0,0 +1,118 @@
|
||||
const FileUtils = require('../utils/FileUtils');
|
||||
|
||||
/**
|
||||
* 性能数据服务
|
||||
* 负责记录和管理页面性能数据
|
||||
*/
|
||||
class PerformanceService {
|
||||
constructor() {
|
||||
this.performanceData = [];
|
||||
this.performanceDataPath = process.env.PERFORMANCE_DATA_FILE_PATH || './test-results/performance-data.json';
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录成功的页面加载性能数据
|
||||
* @param {string} pageName 页面名称
|
||||
* @param {number} startTime 开始时间
|
||||
* @param {number} endTime 结束时间
|
||||
*/
|
||||
async recordSuccess(pageName, startTime, endTime) {
|
||||
await this._recordPerformance({
|
||||
pageName,
|
||||
startTime,
|
||||
endTime,
|
||||
duration: endTime - startTime,
|
||||
success: true,
|
||||
errorMessage: null
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录失败的页面加载性能数据
|
||||
* @param {string} pageName 页面名称
|
||||
* @param {number} startTime 开始时间
|
||||
* @param {number} endTime 结束时间
|
||||
* @param {string} errorMessage 错误信息
|
||||
*/
|
||||
async recordFailure(pageName, startTime, endTime, errorMessage) {
|
||||
await this._recordPerformance({
|
||||
pageName,
|
||||
startTime,
|
||||
endTime,
|
||||
duration: endTime - startTime,
|
||||
success: false,
|
||||
errorMessage
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录异常的页面加载性能数据
|
||||
* @param {string} pageName 页面名称
|
||||
* @param {number} startTime 开始时间
|
||||
* @param {number} endTime 结束时间
|
||||
* @param {Error} error 错误对象
|
||||
*/
|
||||
async recordError(pageName, startTime, endTime, error) {
|
||||
await this._recordPerformance({
|
||||
pageName,
|
||||
startTime,
|
||||
endTime,
|
||||
duration: endTime - startTime,
|
||||
success: false,
|
||||
errorMessage: error.message
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部方法:记录性能数据
|
||||
* @private
|
||||
*/
|
||||
async _recordPerformance(data) {
|
||||
this.performanceData.push({
|
||||
...data,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
await this.savePerformanceData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存性能数据到文件
|
||||
*/
|
||||
async savePerformanceData() {
|
||||
try {
|
||||
await FileUtils.writeJsonFile(this.performanceDataPath, this.performanceData);
|
||||
} catch (error) {
|
||||
console.error('保存性能数据失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空性能数据
|
||||
*/
|
||||
async clearPerformanceData() {
|
||||
this.performanceData = [];
|
||||
await this.savePerformanceData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取性能数据
|
||||
* @returns {Array} 性能数据数组
|
||||
*/
|
||||
getPerformanceData() {
|
||||
try {
|
||||
// 从文件读取最新数据
|
||||
const data = FileUtils.loadFromJsonFile(this.performanceDataPath);
|
||||
if (data) {
|
||||
this.performanceData = data;
|
||||
}
|
||||
return this.performanceData;
|
||||
} catch (error) {
|
||||
console.error('读取性能数据失败:', error);
|
||||
return this.performanceData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建单例实例
|
||||
const performanceService = new PerformanceService();
|
||||
module.exports = performanceService;
|
||||
@ -2,7 +2,8 @@
|
||||
* 文件操作工具类
|
||||
* 提供文件读写、目录创建等常用操作
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const fs = require('fs').promises;
|
||||
const fsSync = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class FileUtils {
|
||||
@ -25,21 +26,74 @@ class FileUtils {
|
||||
/**
|
||||
* 确保目录存在,如果不存在则创建
|
||||
* @param {string} dirPath 目录路径
|
||||
* @returns {boolean} 操作是否成功
|
||||
*/
|
||||
static ensureDirectoryExists(dirPath) {
|
||||
static async ensureDirectoryExists(dirPath) {
|
||||
try {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
console.log(`目录已创建: ${dirPath}`);
|
||||
await fs.access(dirPath);
|
||||
} catch {
|
||||
await fs.mkdir(dirPath, { recursive: true });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入JSON文件
|
||||
* @param {string} filePath 文件路径
|
||||
* @param {Object} data 要写入的数据
|
||||
*/
|
||||
static async writeJsonFile(filePath, data) {
|
||||
await this.ensureDirectoryExists(path.dirname(filePath));
|
||||
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入文本文件
|
||||
* @param {string} filePath 文件路径
|
||||
* @param {string} content 要写入的内容
|
||||
*/
|
||||
static async writeFile(filePath, content) {
|
||||
await this.ensureDirectoryExists(path.dirname(filePath));
|
||||
await fs.writeFile(filePath, content, 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取JSON文件
|
||||
* @param {string} filePath 文件路径
|
||||
* @returns {Promise<Object>} 读取的数据
|
||||
*/
|
||||
static async readJsonFile(filePath) {
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
return JSON.parse(content);
|
||||
} catch (error) {
|
||||
console.error(`创建目录失败: ${dirPath}`, error);
|
||||
return false;
|
||||
console.error(`读取JSON文件失败 [${filePath}]:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
* @param {string} filePath 文件路径
|
||||
*/
|
||||
static async deleteFile(filePath) {
|
||||
try {
|
||||
await fs.unlink(filePath);
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') { // 如果错误不是"文件不存在"
|
||||
console.error(`删除文件失败 [${filePath}]:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件是否存在
|
||||
* @param {string} filePath 文件路径
|
||||
* @returns {boolean} 文件是否存在
|
||||
*/
|
||||
static fileExists(filePath) {
|
||||
return fsSync.existsSync(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存对象到JSON文件
|
||||
* @param {Object} data 要保存的数据对象
|
||||
@ -71,21 +125,17 @@ class FileUtils {
|
||||
/**
|
||||
* 从JSON文件加载对象
|
||||
* @param {string} filePath 文件路径
|
||||
* @param {Object} options 选项
|
||||
* @param {string} options.encoding 文件编码 (默认: 'utf8')
|
||||
* @returns {Object|null} 加载的对象,如果失败则返回null
|
||||
*/
|
||||
static loadFromJsonFile(filePath, options = {}) {
|
||||
static loadFromJsonFile(filePath) {
|
||||
try {
|
||||
const { encoding = 'utf8' } = options;
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.error(`文件不存在: ${filePath}`);
|
||||
if (!this.fileExists(filePath)) {
|
||||
console.warn(`文件不存在: ${filePath}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(filePath, encoding);
|
||||
return JSON.parse(data);
|
||||
const content = fsSync.readFileSync(filePath, 'utf8');
|
||||
return JSON.parse(content);
|
||||
} catch (error) {
|
||||
console.error(`加载JSON文件失败: ${filePath}`, error);
|
||||
return null;
|
||||
37
tests/e2e/menu.spec.js
Normal file
37
tests/e2e/menu.spec.js
Normal file
@ -0,0 +1,37 @@
|
||||
// 加载环境变量
|
||||
require('../../config/env');
|
||||
|
||||
const {test} = require('@playwright/test');
|
||||
const TestController = require('../../src/controllers/LongiTestController');
|
||||
|
||||
test.describe('IBP系统菜单可访问性测试', () => {
|
||||
let controller;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
controller = new TestController();
|
||||
});
|
||||
|
||||
test('应能成功获取系统所有可测试的菜单项', async () => {
|
||||
const menuData = await controller.fetchAndSaveMenuData();
|
||||
test.expect(menuData).toBeTruthy();
|
||||
test.expect(menuData.length).toBeGreaterThan(0);
|
||||
console.log(`✓ 成功收集 ${menuData.length} 个菜单项`);
|
||||
});
|
||||
|
||||
test('应能成功访问所有菜单页面', async () => {
|
||||
// 执行所有测试并获取进度
|
||||
const progress = await controller.runAllTests();
|
||||
|
||||
// 验证测试结果
|
||||
test.expect(progress).toBeTruthy();
|
||||
test.expect(progress.total).toBeGreaterThan(0);
|
||||
test.expect(progress.completed).toBe(progress.total);
|
||||
test.expect(progress.remaining).toBe(0);
|
||||
|
||||
// 输出测试统计
|
||||
console.log('\n测试完成!');
|
||||
console.log(`总菜单数: ${progress.total}`);
|
||||
console.log(`完成数量: ${progress.completed}`);
|
||||
console.log(`剩余数量: ${progress.remaining}`);
|
||||
});
|
||||
});
|
||||
@ -1,30 +0,0 @@
|
||||
require('../../config/env');
|
||||
const {test, expect} = require('@playwright/test');
|
||||
const LongiLoginPage = require('../pages/LongiLoginPage');
|
||||
const LongiMainPage = require('../pages/LongiMainPage');
|
||||
|
||||
// 设置更长的超时时间:1小时
|
||||
test.setTimeout(3600000);
|
||||
|
||||
test('隆基登录', async ({page}) => {
|
||||
// 1. 创建页面对象
|
||||
const loginPage = new LongiLoginPage(page);
|
||||
const mainPage = new LongiMainPage(page);
|
||||
|
||||
// 2. 导航到登录页面
|
||||
await loginPage.navigateToLoginPage();
|
||||
|
||||
// 4. 点击登录按钮 - 使用页面对象模型
|
||||
const clickSuccess = await loginPage.clickLoginButton();
|
||||
|
||||
// 5. 验证点击是否成功
|
||||
expect(clickSuccess, '验证是否登录成功').toBeTruthy();
|
||||
|
||||
// 10. 检查菜单数据文件是否存在
|
||||
let menuItems = await mainPage.checkAndLoadMenuItems();
|
||||
|
||||
// 11. 使用菜单数据进行后续操作
|
||||
console.log(`共有 ${menuItems.length} 个菜单项可用于测试`);
|
||||
|
||||
await mainPage.handleAllMenuClicks(menuItems);
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user