chore: 添加完整的代码质量和测试工具链
## 新增工具 ### 1. ESLint + Prettier - ESLint 9.39.2 - 代码质量检查(Vue 3 规则) - Prettier 3.8.1 - 代码格式化 - prettier-plugin-tailwindcss - TailwindCSS 类名排序 - 配置文件:eslint.config.js, .prettierrc - VS Code 自动格式化配置 ### 2. Husky + lint-staged - Husky 9.1.7 - Git hooks 管理 - lint-staged 16.2.7 - 暂存文件检查 - pre-commit hook 自动运行 lint 和 format - 提交前自动修复代码问题 ### 3. Playwright E2E 测试 - @playwright/test 1.58.0 - E2E 测试框架 - 已安装 Chromium 和 Chrome Headless Shell - 配置文件:playwright.config.js - 示例测试:e2e/example.spec.js - 支持 3 个测试项目(移动端、桌面端、Safari) ## 配置更新 - package.json:添加 lint、format、test:e2e 等脚本 - eslint.config.js:支持 Vue、Vitest、Playwright - .gitignore:忽略 Playwright 测试结果 ## 新增文档 - docs/ESLINT_PRETTIER.md - ESLint 和 Prettier 使用指南 - docs/HUSKY_LINT_STAGED.md - Git Hooks 配置说明 - docs/PLAYWRIGHT.md - E2E 测试完整指南 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Showing
6 changed files
with
229 additions
and
1 deletions
docs/PLAYWRIGHT.md
0 → 100644
This diff is collapsed. Click to expand it.
e2e/example.spec.js
0 → 100644
| 1 | +/* | ||
| 2 | + * @Date: 2026-01-28 21:45:00 | ||
| 3 | + * @Description: Playwright 示例 E2E 测试 | ||
| 4 | + */ | ||
| 5 | +import { test, expect } from '@playwright/test' | ||
| 6 | + | ||
| 7 | +test.describe('基础功能测试', () => { | ||
| 8 | + test('首页加载成功', async ({ page }) => { | ||
| 9 | + // 访问首页 | ||
| 10 | + await page.goto('/') | ||
| 11 | + | ||
| 12 | + // 等待页面加载 | ||
| 13 | + await page.waitForLoadState('networkidle') | ||
| 14 | + | ||
| 15 | + // 检查标题 | ||
| 16 | + await expect(page).toHaveTitle(/美乐爱觉/) | ||
| 17 | + | ||
| 18 | + // 检查关键元素 | ||
| 19 | + const header = page.locator('header').first() | ||
| 20 | + await expect(header).toBeVisible() | ||
| 21 | + }) | ||
| 22 | + | ||
| 23 | + test('导航功能正常', async ({ page }) => { | ||
| 24 | + await page.goto('/') | ||
| 25 | + | ||
| 26 | + // 点击课程列表 | ||
| 27 | + await page.click('text=课程') | ||
| 28 | + | ||
| 29 | + // 验证导航 | ||
| 30 | + await expect(page).toHaveURL(/\/courses-list/) | ||
| 31 | + }) | ||
| 32 | +}) | ||
| 33 | + | ||
| 34 | +test.describe('课程功能测试', () => { | ||
| 35 | + test('浏览课程列表', async ({ page }) => { | ||
| 36 | + await page.goto('/courses-list') | ||
| 37 | + | ||
| 38 | + // 等待课程列表加载 | ||
| 39 | + await page.waitForSelector('.course-card', { timeout: 5000 }) | ||
| 40 | + | ||
| 41 | + // 检查课程卡片是否存在 | ||
| 42 | + const courseCards = page.locator('.course-card') | ||
| 43 | + const count = await courseCards.count() | ||
| 44 | + | ||
| 45 | + expect(count).toBeGreaterThan(0) | ||
| 46 | + }) | ||
| 47 | + | ||
| 48 | + test('搜索课程', async ({ page }) => { | ||
| 49 | + await page.goto('/courses-list') | ||
| 50 | + | ||
| 51 | + // 输入搜索关键字 | ||
| 52 | + const searchInput = page.locator('input[placeholder*="搜索"]').first() | ||
| 53 | + await searchInput.fill('Vue') | ||
| 54 | + | ||
| 55 | + // 触发搜索 | ||
| 56 | + await searchInput.press('Enter') | ||
| 57 | + | ||
| 58 | + // 等待结果加载 | ||
| 59 | + await page.waitForTimeout(500) | ||
| 60 | + | ||
| 61 | + // 验证搜索结果 | ||
| 62 | + const courseCards = page.locator('.course-card') | ||
| 63 | + const count = await courseCards.count() | ||
| 64 | + | ||
| 65 | + expect(count).toBeGreaterThan(0) | ||
| 66 | + }) | ||
| 67 | +}) | ||
| 68 | + | ||
| 69 | +test.describe('用户认证测试', () => { | ||
| 70 | + test('显示登录页面', async ({ page }) => { | ||
| 71 | + await page.goto('/login') | ||
| 72 | + | ||
| 73 | + // 检查登录表单 | ||
| 74 | + const loginForm = page.locator('form').first() | ||
| 75 | + await expect(loginForm).toBeVisible() | ||
| 76 | + | ||
| 77 | + // 检查手机号输入框 | ||
| 78 | + const phoneInput = page.locator('input[name="phone"]') | ||
| 79 | + await expect(phoneInput).toBeVisible() | ||
| 80 | + }) | ||
| 81 | + | ||
| 82 | + test('登录表单验证', async ({ page }) => { | ||
| 83 | + await page.goto('/login') | ||
| 84 | + | ||
| 85 | + // 点击登录按钮(不输入信息) | ||
| 86 | + await page.click('button[type="submit"]') | ||
| 87 | + | ||
| 88 | + // 检查错误提示 | ||
| 89 | + const errorToast = page.locator('.van-toast--fail') | ||
| 90 | + await expect(errorToast).toBeVisible({ timeout: 3000 }) | ||
| 91 | + }) | ||
| 92 | +}) | ||
| 93 | + | ||
| 94 | +test.describe('响应式测试', () => { | ||
| 95 | + test('移动端布局正确', async ({ page }) => { | ||
| 96 | + // 设置移动端视口 | ||
| 97 | + await page.setViewportSize({ width: 375, height: 667 }) | ||
| 98 | + await page.goto('/') | ||
| 99 | + | ||
| 100 | + // 检查移动端导航 | ||
| 101 | + const bottomNav = page.locator('.van-tabbar') | ||
| 102 | + await expect(bottomNav).toBeVisible() | ||
| 103 | + }) | ||
| 104 | + | ||
| 105 | + test('桌面端布局正确', async ({ page }) => { | ||
| 106 | + // 设置桌面端视口 | ||
| 107 | + await page.setViewportSize({ width: 1280, height: 720 }) | ||
| 108 | + await page.goto('/') | ||
| 109 | + | ||
| 110 | + // 检查页面内容 | ||
| 111 | + const mainContent = page.locator('main').first() | ||
| 112 | + await expect(mainContent).toBeVisible() | ||
| 113 | + }) | ||
| 114 | +}) |
| ... | @@ -136,7 +136,7 @@ export default [ | ... | @@ -136,7 +136,7 @@ export default [ |
| 136 | }, | 136 | }, |
| 137 | }, | 137 | }, |
| 138 | 138 | ||
| 139 | - // 测试文件配置 | 139 | + // 测试文件配置(Vitest) |
| 140 | { | 140 | { |
| 141 | files: ['**/*.test.js', '**/*.spec.js', 'test/**'], | 141 | files: ['**/*.test.js', '**/*.spec.js', 'test/**'], |
| 142 | languageOptions: { | 142 | languageOptions: { |
| ... | @@ -154,6 +154,21 @@ export default [ | ... | @@ -154,6 +154,21 @@ export default [ |
| 154 | }, | 154 | }, |
| 155 | }, | 155 | }, |
| 156 | 156 | ||
| 157 | + // E2E 测试文件配置(Playwright) | ||
| 158 | + { | ||
| 159 | + files: ['e2e/**/*.{js,ts}'], | ||
| 160 | + languageOptions: { | ||
| 161 | + globals: { | ||
| 162 | + test: 'readonly', | ||
| 163 | + expect: 'readonly', | ||
| 164 | + beforeAll: 'readonly', | ||
| 165 | + afterAll: 'readonly', | ||
| 166 | + beforeEach: 'readonly', | ||
| 167 | + afterEach: 'readonly', | ||
| 168 | + }, | ||
| 169 | + }, | ||
| 170 | + }, | ||
| 171 | + | ||
| 157 | // Prettier 配置(必须最后) | 172 | // Prettier 配置(必须最后) |
| 158 | prettier, | 173 | prettier, |
| 159 | ] | 174 | ] | ... | ... |
| ... | @@ -10,6 +10,11 @@ | ... | @@ -10,6 +10,11 @@ |
| 10 | "test": "vitest run", | 10 | "test": "vitest run", |
| 11 | "test:ui": "vitest --ui", | 11 | "test:ui": "vitest --ui", |
| 12 | "test:coverage": "vitest --coverage", | 12 | "test:coverage": "vitest --coverage", |
| 13 | + "test:e2e": "playwright test", | ||
| 14 | + "test:e2e:ui": "playwright test --ui", | ||
| 15 | + "test:e2e:debug": "playwright test --debug", | ||
| 16 | + "test:e2e:headed": "playwright test --headed", | ||
| 17 | + "test:e2e:report": "playwright show-report", | ||
| 13 | "lint": "eslint . --fix", | 18 | "lint": "eslint . --fix", |
| 14 | "lint:check": "eslint .", | 19 | "lint:check": "eslint .", |
| 15 | "format": "prettier --write \"src/**/*.{js,vue,css,less,md,json}\"", | 20 | "format": "prettier --write \"src/**/*.{js,vue,css,less,md,json}\"", | ... | ... |
playwright.config.js
0 → 100644
| 1 | +/* | ||
| 2 | + * @Date: 2026-01-28 21:45:00 | ||
| 3 | + * @Description: Playwright E2E 测试配置 | ||
| 4 | + */ | ||
| 5 | +import { defineConfig, devices } from '@playwright/test' | ||
| 6 | + | ||
| 7 | +export default defineConfig({ | ||
| 8 | + // 测试文件位置 | ||
| 9 | + testDir: './e2e', | ||
| 10 | + | ||
| 11 | + // 测试超时时间(毫秒) | ||
| 12 | + timeout: 30 * 1000, | ||
| 13 | + | ||
| 14 | + // 期望超时时间 | ||
| 15 | + expect: { | ||
| 16 | + timeout: 5 * 1000, | ||
| 17 | + }, | ||
| 18 | + | ||
| 19 | + // 失败时重试次数 | ||
| 20 | + retries: process.env.CI ? 2 : 0, | ||
| 21 | + | ||
| 22 | + // 并行执行 | ||
| 23 | + workers: process.env.CI ? 1 : undefined, | ||
| 24 | + | ||
| 25 | + // 报告器 | ||
| 26 | + reporter: [ | ||
| 27 | + ['html', { outputFolder: 'playwright-report' }], | ||
| 28 | + ['json', { outputFile: 'test-results/test-results.json' }], | ||
| 29 | + ['junit', { outputFile: 'test-results/test-results.xml' }], | ||
| 30 | + ], | ||
| 31 | + | ||
| 32 | + // 共享配置 | ||
| 33 | + use: { | ||
| 34 | + // 基础 URL(开发服务器地址) | ||
| 35 | + baseURL: 'http://localhost:5173', | ||
| 36 | + | ||
| 37 | + // 追踪失败测试(用于调试) | ||
| 38 | + trace: 'on-first-retry', | ||
| 39 | + | ||
| 40 | + // 截图(仅失败时) | ||
| 41 | + screenshot: 'only-on-failure', | ||
| 42 | + | ||
| 43 | + // 视频录制(仅失败时) | ||
| 44 | + video: 'retain-on-failure', | ||
| 45 | + | ||
| 46 | + // 浏览器视口大小(移动端优先) | ||
| 47 | + viewport: { width: 375, height: 667 }, | ||
| 48 | + | ||
| 49 | + // 忽略 HTTPS 错误 | ||
| 50 | + ignoreHTTPSErrors: true, | ||
| 51 | + | ||
| 52 | + // 操作超时 | ||
| 53 | + actionTimeout: 10 * 1000, | ||
| 54 | + navigationTimeout: 30 * 1000, | ||
| 55 | + }, | ||
| 56 | + | ||
| 57 | + // 测试项目(不同浏览器和视口) | ||
| 58 | + projects: [ | ||
| 59 | + { | ||
| 60 | + name: 'chromium-mobile', | ||
| 61 | + use: { | ||
| 62 | + ...devices['iPhone 12'], | ||
| 63 | + browserName: 'chromium', | ||
| 64 | + }, | ||
| 65 | + }, | ||
| 66 | + { | ||
| 67 | + name: 'chromium-desktop', | ||
| 68 | + use: { | ||
| 69 | + ...devices['Desktop Chrome'], | ||
| 70 | + viewport: { width: 1280, height: 720 }, | ||
| 71 | + }, | ||
| 72 | + }, | ||
| 73 | + { | ||
| 74 | + name: 'webkit-mobile', | ||
| 75 | + use: { | ||
| 76 | + ...devices['iPhone 12'], | ||
| 77 | + browserName: 'webkit', | ||
| 78 | + }, | ||
| 79 | + }, | ||
| 80 | + ], | ||
| 81 | + | ||
| 82 | + // 开发服务器(测试前启动) | ||
| 83 | + webServer: { | ||
| 84 | + command: 'pnpm dev', | ||
| 85 | + url: 'http://localhost:5173', | ||
| 86 | + reuseExistingServer: !process.env.CI, | ||
| 87 | + timeout: 120 * 1000, | ||
| 88 | + }, | ||
| 89 | +}) |
-
Please register or login to post a comment