From 0dfad65b57e2d5abf30ce4ff3eb237b01db34c28 Mon Sep 17 00:00:00 2001 From: dengqichen Date: Fri, 7 Mar 2025 13:36:56 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=88=86=E6=89=B9=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E7=82=B9=E5=87=BB=E8=8F=9C=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.dev | 29 ++--- README.md | 206 ++++++++++++++++-------------- controllers/TestController.js | 160 ----------------------- lib/services/MenuDataService.js | 82 ------------ playwright.config.js | 18 ++- src/controllers/TestController.js | 19 +-- {tests => src}/utils/helpers.js | 0 7 files changed, 140 insertions(+), 374 deletions(-) delete mode 100644 controllers/TestController.js delete mode 100644 lib/services/MenuDataService.js rename {tests => src}/utils/helpers.js (100%) diff --git a/.env.dev b/.env.dev index f741a47..ab452c5 100644 --- a/.env.dev +++ b/.env.dev @@ -1,25 +1,24 @@ -# 基础URL +# 基础配置 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 - -# 数据目录配置 -TEST_DATA_DIR=test-data -MENU_DATA_FILE_PATH=test-data/menu-data.json -TEST_PROGRESS_FILE_PATH=test-data/test-progress.json - -# 测试批次配置 -TEST_BATCH_SIZE=5 -TEST_RETRY_COUNT=3 -TEST_BATCH_INTERVAL=2000 -TEST_MAX_RETRY_DELAY=5000 +EXPECT_TIMEOUT=30000 # 浏览器配置 BROWSER_HEADLESS=false BROWSER_SLOW_MO=50 BROWSER_TIMEOUT=30000 - -# 视窗配置 VIEWPORT_WIDTH=1920 -VIEWPORT_HEIGHT=1080 \ No newline at end of file +VIEWPORT_HEIGHT=1080 + +# 数据目录配置 +MENU_DATA_FILE_PATH=test-data/menu-data.json +TEST_PROGRESS_FILE_PATH=test-data/test-progress.json \ No newline at end of file diff --git a/README.md b/README.md index d5dec0d..ce78a45 100644 --- a/README.md +++ b/README.md @@ -1,137 +1,155 @@ -# Playwright 自动化测试项目 +# Playwright E2E Testing Framework -基于 Playwright 的自动化测试框架,专门用于测试 Web 应用的菜单可访问性。 +本项目是基于Playwright的端到端测试框架,用于自动化测试Web应用的功能和性能。 -## 项目结构 +## 项目架构 ``` -playwright-project/ -├── tests/ # 测试相关文件 -│ ├── e2e/ # 端到端测试用例 -│ │ └── menu.spec.js # 菜单测试用例 -│ ├── pages/ # Page Object Models -│ │ ├── BasePage.js # 基础页面类 -│ │ ├── LoginPage.js # 登录页面类 -│ │ └── MainPage.js # 主页面类 -│ └── utils/ # 测试工具 -│ └── FileUtils.js # 文件操作工具 -├── src/ # 源代码 -│ ├── services/ # 业务服务 -│ │ └── MenuDataService.js # 菜单数据服务 -│ └── controllers/ # 业务控制器 -│ └── TestController.js # 测试控制器 -├── config/ # 配置文件 -│ └── env.js # 环境变量配置 -├── playwright.config.js # Playwright 配置 -└── package.json # 项目配置 +playwright/ +├── src/ +│ ├── controllers/ # 控制器层,负责测试流程控制 +│ ├── pages/ # 页面对象层,封装页面操作 +│ └── services/ # 服务层,处理数据和业务逻辑 +├── tests/ +│ └── e2e/ # 端到端测试用例 +├── .env.dev # 开发环境配置 +└── playwright.config.js # Playwright配置文件 ``` -## 功能特性 +## 配置管理 -- 🔄 批量测试菜单可访问性 -- 💾 自动保存和恢复测试进度 -- 🎯 支持分批次执行测试 -- 🔍 详细的测试报告 -- 🛠 可配置的测试参数 +项目采用分层的配置管理方式,确保配置的统一性和可维护性: -## 环境要求 +### 1. 环境变量配置 (.env.dev) -- Node.js 14+ -- npm 6+ -- Playwright 依赖项 +环境变量配置文件包含所有可配置项,按功能分类: -## 安装 - -```bash -# 安装依赖 -npm install - -# 安装 Playwright 浏览器 -npx playwright install -``` - -## 配置 - -在 `.env.dev` 文件中配置以下环境变量: - -```bash -# 基础URL -BASE_URL=https://your-app-url.com +```ini +# 基础配置 +BASE_URL=https://ibp-dev.longi.com # 测试配置 -TEST_BATCH_SIZE=5 # 每批次测试的菜单数量 -TEST_RETRY_COUNT=3 # 失败重试次数 -TEST_BATCH_INTERVAL=2000 # 批次间隔时间(ms) -MENU_TIME_OUT=30000 # 菜单加载超时时间 +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 # 数据目录配置 -TEST_DATA_DIR=test-data MENU_DATA_FILE_PATH=test-data/menu-data.json TEST_PROGRESS_FILE_PATH=test-data/test-progress.json ``` -## 使用方法 +### 2. Playwright配置 (playwright.config.js) -### 运行测试 +Playwright配置文件统一管理所有浏览器相关的配置: -```bash -# 运行菜单测试 -npm run test:menu +- 浏览器设置(headless模式、视窗大小等) +- 测试超时设置 +- 并发和重试策略 +- 测试报告配置 -# 使用 UI 模式运行测试 -npm run test:menu:ui +### 3. 测试控制器配置 (TestController) -# 调试模式运行测试 -npm run test:menu:debug +测试控制器只管理测试流程相关的配置: -# 清理数据并重新测试 -npm run test:menu:clean -``` +- 批次大小 +- 重试次数 +- 批次间隔 +- 最大重试延迟 -### 查看报告 +## 主要功能 -```bash -npm run report -``` +1. **菜单遍历测试** + - 自动收集菜单数据 + - 批量执行菜单点击测试 + - 支持断点续测 + - 提供测试进度跟踪 -## 测试流程 +2. **智能重试机制** + - 支持失败重试 + - 使用指数退避策略 + - 可配置最大重试次数和延迟 -1. **数据收集** - - 登录系统 - - 收集菜单数据 - - 保存到文件 +3. **灵活的配置系统** + - 所有配置项可通过环境变量覆盖 + - 提供合理的默认值 + - 配置项分类清晰 -2. **批量测试** - - 分批加载菜单 - - 逐个点击测试 - - 记录测试进度 +## 使用说明 + +1. **安装依赖** + ```bash + npm 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. 在 `tests/e2e` 目录下创建测试文件 -2. 使用 Page Object Model 模式 -3. 遵循现有的代码结构 +## 常见问题 -### 修改页面对象 +1. **测试运行失败** + - 检查网络连接 + - 验证环境变量配置 + - 查看错误日志 -1. 在 `tests/pages` 目录下修改或添加页面类 -2. 继承 `BasePage` 类 -3. 实现必要的页面方法 +2. **配置不生效** + - 确认环境变量文件位置正确 + - 检查配置项拼写 + - 重启测试进程 ## 贡献指南 1. Fork 项目 2. 创建特性分支 -3. 提交更改 -4. 推送到分支 -5. 创建 Pull Request +3. 提交变更 +4. 发起 Pull Request ## 许可证 diff --git a/controllers/TestController.js b/controllers/TestController.js deleted file mode 100644 index f1d85b0..0000000 --- a/controllers/TestController.js +++ /dev/null @@ -1,160 +0,0 @@ -const { chromium } = require('@playwright/test'); -const LongiMainPage = require('../tests/pages/LongiMainPage'); -const LongiLoginPage = require('../tests/pages/LongiLoginPage'); -const menuDataService = require('../services/MenuDataService'); - -class TestController { - constructor() { - // 从环境变量获取配置 - this.batchSize = parseInt(process.env.TEST_BATCH_SIZE || '5', 10); - this.retryCount = parseInt(process.env.TEST_RETRY_COUNT || '3', 10); - this.batchInterval = parseInt(process.env.TEST_BATCH_INTERVAL || '2000', 10); - - // 浏览器配置 - this.browserConfig = { - headless: false, // 使用有头模式 - args: ['--start-maximized'], // 最大化窗口 - slowMo: 50 // 放慢操作速度,便于观察 - }; - - console.log('测试配置:', { - batchSize: this.batchSize, - retryCount: this.retryCount, - batchInterval: this.batchInterval - }); - } - - /** - * 执行登录操作 - * @param {import('@playwright/test').Page} page - Playwright页面对象 - * @returns {Promise} 登录是否成功 - */ - async performLogin(page) { - const loginPage = new LongiLoginPage(page); - - // 导航到登录页面 - await loginPage.navigateToLoginPage(); - - // 点击登录按钮 - const loginSuccess = await loginPage.clickLoginButton(); - - if (!loginSuccess) { - console.error('登录失败'); - return false; - } - - console.log('登录成功'); - return true; - } - - /** - * 收集菜单数据 - * @returns {Promise} - 处理后的菜单数据 - */ - async collectMenuData() { - console.log('开始收集菜单数据...'); - const browser = await chromium.launch(this.browserConfig); - const page = await browser.newPage(); - - // 设置视窗大小 - await page.setViewportSize({ width: 1920, height: 1080 }); - - const mainPage = new LongiMainPage(page); - - try { - // 使用 LongiLoginPage 处理登录 - const loginSuccess = await this.performLogin(page); - if (!loginSuccess) { - throw new Error('登录失败,无法收集菜单数据'); - } - - console.log('登录成功,正在获取菜单项...'); - const menuItems = await mainPage.checkAndLoadMenuItems(); - console.log(`成功获取 ${menuItems.length} 个菜单项`); - return menuDataService.saveMenuData(menuItems); - } finally { - await browser.close(); - } - } - - /** - * 执行一批菜单的测试 - * @param {Array} menuBatch - 要测试的菜单数组 - */ - async runBatchTest(menuBatch) { - console.log(`开始执行批次测试,包含 ${menuBatch.length} 个菜单项:`); - console.log(menuBatch.map(m => m.text).join(', ')); - - const browser = await chromium.launch(this.browserConfig); - const page = await browser.newPage(); - - // 设置视窗大小 - await page.setViewportSize({ width: 1920, height: 1080 }); - - const mainPage = new LongiMainPage(page); - - try { - // 使用 LongiLoginPage 处理登录 - const loginSuccess = await this.performLogin(page); - if (!loginSuccess) { - throw new Error('登录失败,无法执行菜单测试'); - } - - console.log('登录成功,开始测试菜单项...'); - - // 使用 handleAllMenuClicks 方法处理所有菜单 - await mainPage.handleAllMenuClicks(menuBatch); - - // 更新进度 - const progress = menuDataService.getProgress(); - const newProgress = [...progress]; - for (const menuItem of menuBatch) { - if (!newProgress.includes(menuItem.id)) { - newProgress.push(menuItem.id); - } - } - menuDataService.saveProgress(newProgress); - } finally { - await browser.close(); - } - } - - /** - * 获取下一批要测试的菜单 - * @returns {Array|null} - 下一批要测试的菜单,如果没有则返回null - */ - getNextBatch() { - const menuData = menuDataService.getMenuData(); - const progress = menuDataService.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 {Object} - 进度信息 - */ - getTestProgress() { - const menuData = menuDataService.getMenuData(); - const progress = menuDataService.getProgress(); - - if (!menuData) { - return { total: 0, completed: 0, remaining: 0 }; - } - - return { - total: menuData.length, - completed: progress.length, - remaining: menuData.length - progress.length - }; - } -} - -module.exports = TestController; \ No newline at end of file diff --git a/lib/services/MenuDataService.js b/lib/services/MenuDataService.js deleted file mode 100644 index 5d68e82..0000000 --- a/lib/services/MenuDataService.js +++ /dev/null @@ -1,82 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -/** - * 菜单数据服务 - * 负责菜单数据的存储和检索 - */ -class MenuDataService { - constructor() { - // 从环境变量获取路径配置 - this.dataDir = 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 }); - } - } - - /** - * 保存菜单数据 - * @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); - } - } -} - -// 导出单例实例 -module.exports = new MenuDataService(); \ No newline at end of file diff --git a/playwright.config.js b/playwright.config.js index 3a24332..896656a 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -10,10 +10,7 @@ module.exports = defineConfig({ timeout: 60 * 60 * 1000, // 1小时 /* 每个测试的预期状态 */ expect: { - /** - * 断言超时时间 - */ - timeout: 30000 + timeout: parseInt(process.env.EXPECT_TIMEOUT || '30000', 10) }, /* 测试运行并发数 */ fullyParallel: false, @@ -34,8 +31,17 @@ module.exports = defineConfig({ screenshot: 'only-on-failure', /* 录制视频 */ video: 'retain-on-failure', - headless: false, - viewport: { width: 1920, height: 1080 }, + /* 浏览器配置 */ + headless: process.env.BROWSER_HEADLESS === 'true', + viewport: { + width: parseInt(process.env.VIEWPORT_WIDTH || '1920', 10), + height: parseInt(process.env.VIEWPORT_HEIGHT || '1080', 10) + }, + /* 浏览器启动选项 */ + launchOptions: { + slowMo: parseInt(process.env.BROWSER_SLOW_MO || '50', 10), + timeout: parseInt(process.env.BROWSER_TIMEOUT || '30000', 10) + } }, /* 配置不同的浏览器项目 */ diff --git a/src/controllers/TestController.js b/src/controllers/TestController.js index 1048294..62be412 100644 --- a/src/controllers/TestController.js +++ b/src/controllers/TestController.js @@ -17,25 +17,11 @@ class TestController { * @private */ initializeConfig() { - // 从环境变量获取配置 + // 从环境变量获取测试相关配置 this.batchSize = parseInt(process.env.TEST_BATCH_SIZE || '5', 10); this.retryCount = parseInt(process.env.TEST_RETRY_COUNT || '3', 10); this.batchInterval = parseInt(process.env.TEST_BATCH_INTERVAL || '2000', 10); this.maxRetryDelay = parseInt(process.env.TEST_MAX_RETRY_DELAY || '5000', 10); - - // 浏览器配置 - this.browserConfig = { - headless: process.env.BROWSER_HEADLESS === 'true', - args: ['--start-maximized'], - slowMo: parseInt(process.env.BROWSER_SLOW_MO || '50', 10), - timeout: parseInt(process.env.BROWSER_TIMEOUT || '30000', 10) - }; - - // 视窗配置 - this.viewportConfig = { - width: parseInt(process.env.VIEWPORT_WIDTH || '1920', 10), - height: parseInt(process.env.VIEWPORT_HEIGHT || '1080', 10) - }; } /** @@ -45,9 +31,8 @@ class TestController { */ async createBrowser() { try { - const browser = await chromium.launch(this.browserConfig); + const browser = await chromium.launch(); const page = await browser.newPage(); - await page.setViewportSize(this.viewportConfig); return { browser, page }; } catch (error) { console.error('创建浏览器实例失败:', error); diff --git a/tests/utils/helpers.js b/src/utils/helpers.js similarity index 100% rename from tests/utils/helpers.js rename to src/utils/helpers.js