Compare commits

...

26 Commits

Author SHA1 Message Date
dengqichen
a6e47ed527 增加生产环境配置文件。 2025-03-25 17:19:52 +08:00
dengqichen
7595f3b933 增加生产环境配置文件。 2025-03-25 17:14:29 +08:00
dengqichen
e59c5c5c60 增加生产环境配置文件。 2025-03-25 17:13:39 +08:00
dengqichen
aa3f497434 优化分批执行点击菜单 2025-03-11 14:56:16 +08:00
dengqichen
3921448aa1 优化分批执行点击菜单 2025-03-11 10:40:08 +08:00
dengqichen
aa4b2ff884 优化分批执行点击菜单 2025-03-11 09:27:12 +08:00
dengqichen
47d7883874 优化分批执行点击菜单 2025-03-11 09:25:11 +08:00
dengqichen
f7fdf43abb 优化分批执行点击菜单 2025-03-10 18:16:23 +08:00
dengqichen
c9dc8a83ee 优化分批执行点击菜单 2025-03-10 16:50:41 +08:00
dengqichen
8d38687583 优化分批执行点击菜单 2025-03-10 15:45:40 +08:00
dengqichen
f528c74656 优化分批执行点击菜单 2025-03-07 18:31:36 +08:00
dengqichen
e7798329a2 优化分批执行点击菜单 2025-03-07 18:30:26 +08:00
dengqichen
2f6456f027 优化分批执行点击菜单 2025-03-07 17:47:18 +08:00
dengqichen
970d194ab3 优化分批执行点击菜单 2025-03-07 17:29:21 +08:00
dengqichen
40863e3eca 优化分批执行点击菜单 2025-03-07 16:53:34 +08:00
dengqichen
015f8abf32 优化分批执行点击菜单 2025-03-07 14:55:54 +08:00
dengqichen
6221a18dd3 优化分批执行点击菜单 2025-03-07 14:52:52 +08:00
dengqichen
03e91a0ff9 优化分批执行点击菜单 2025-03-07 14:34:48 +08:00
dengqichen
c08b274fbd 优化分批执行点击菜单 2025-03-07 14:21:05 +08:00
dengqichen
82fb7ebe96 优化分批执行点击菜单 2025-03-07 13:50:01 +08:00
dengqichen
0dfad65b57 优化分批执行点击菜单 2025-03-07 13:36:56 +08:00
dengqichen
ff891e82b0 优化分批执行点击菜单 2025-03-07 13:05:31 +08:00
dengqichen
c8bd981cc4 优化分批执行点击菜单 2025-03-07 10:21:10 +08:00
dengqichen
15b08c65df 优化分批执行点击菜单 2025-03-07 09:54:56 +08:00
dengqichen
6aa60dd29d 优化分批执行点击菜单 2025-03-07 09:42:48 +08:00
dengqichen
eb8d523d91 优化分批执行点击菜单 2025-03-06 17:49:42 +08:00
28 changed files with 1897 additions and 385 deletions

View File

@ -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
View 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

View File

@ -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
View File

@ -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

View File

@ -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
View 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
View 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)

View File

@ -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",

View File

@ -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
View 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

View File

@ -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);
// }

View File

@ -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);
// }

View 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
View 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
View 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
View 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();

View File

@ -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 元素选择器

View File

@ -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);
}
}
}

View File

@ -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);

View 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
View 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();

View 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();

View 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();

View 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;

View File

@ -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 要保存的数据对象
@ -51,7 +105,7 @@ class FileUtils {
*/
static saveToJsonFile(data, filePath, options = {}) {
try {
const { pretty = true, encoding = 'utf8' } = options;
const {pretty = true, encoding = 'utf8'} = options;
const indent = pretty ? 2 : 0;
// 确保目录存在
@ -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;
@ -102,7 +152,7 @@ class FileUtils {
*/
static saveTextToFile(text, filePath, options = {}) {
try {
const { encoding = 'utf8' } = options;
const {encoding = 'utf8'} = options;
// 确保目录存在
const dirPath = path.dirname(filePath);
@ -127,7 +177,7 @@ class FileUtils {
*/
static loadTextFromFile(filePath, options = {}) {
try {
const { encoding = 'utf8' } = options;
const {encoding = 'utf8'} = options;
if (!fs.existsSync(filePath)) {
console.error(`文件不存在: ${filePath}`);

37
tests/e2e/menu.spec.js Normal file
View 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}`);
});
});

View File

@ -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);
});