hookehuyr

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>
...@@ -44,3 +44,8 @@ mlaj ...@@ -44,3 +44,8 @@ mlaj
44 44
45 # Plan directory video resources 45 # Plan directory video resources
46 docs/plan/*/video/ 46 docs/plan/*/video/
47 +
48 +# Playwright E2E 测试
49 +test-results/
50 +playwright-report/
51 +playwright/.cache/
......
This diff is collapsed. Click to expand it.
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}\"",
......
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 +})