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
# Plan directory video resources
docs/plan/*/video/
# Playwright E2E 测试
test-results/
playwright-report/
playwright/.cache/
......
This diff is collapsed. Click to expand it.
/*
* @Date: 2026-01-28 21:45:00
* @Description: Playwright 示例 E2E 测试
*/
import { test, expect } from '@playwright/test'
test.describe('基础功能测试', () => {
test('首页加载成功', async ({ page }) => {
// 访问首页
await page.goto('/')
// 等待页面加载
await page.waitForLoadState('networkidle')
// 检查标题
await expect(page).toHaveTitle(/美乐爱觉/)
// 检查关键元素
const header = page.locator('header').first()
await expect(header).toBeVisible()
})
test('导航功能正常', async ({ page }) => {
await page.goto('/')
// 点击课程列表
await page.click('text=课程')
// 验证导航
await expect(page).toHaveURL(/\/courses-list/)
})
})
test.describe('课程功能测试', () => {
test('浏览课程列表', async ({ page }) => {
await page.goto('/courses-list')
// 等待课程列表加载
await page.waitForSelector('.course-card', { timeout: 5000 })
// 检查课程卡片是否存在
const courseCards = page.locator('.course-card')
const count = await courseCards.count()
expect(count).toBeGreaterThan(0)
})
test('搜索课程', async ({ page }) => {
await page.goto('/courses-list')
// 输入搜索关键字
const searchInput = page.locator('input[placeholder*="搜索"]').first()
await searchInput.fill('Vue')
// 触发搜索
await searchInput.press('Enter')
// 等待结果加载
await page.waitForTimeout(500)
// 验证搜索结果
const courseCards = page.locator('.course-card')
const count = await courseCards.count()
expect(count).toBeGreaterThan(0)
})
})
test.describe('用户认证测试', () => {
test('显示登录页面', async ({ page }) => {
await page.goto('/login')
// 检查登录表单
const loginForm = page.locator('form').first()
await expect(loginForm).toBeVisible()
// 检查手机号输入框
const phoneInput = page.locator('input[name="phone"]')
await expect(phoneInput).toBeVisible()
})
test('登录表单验证', async ({ page }) => {
await page.goto('/login')
// 点击登录按钮(不输入信息)
await page.click('button[type="submit"]')
// 检查错误提示
const errorToast = page.locator('.van-toast--fail')
await expect(errorToast).toBeVisible({ timeout: 3000 })
})
})
test.describe('响应式测试', () => {
test('移动端布局正确', async ({ page }) => {
// 设置移动端视口
await page.setViewportSize({ width: 375, height: 667 })
await page.goto('/')
// 检查移动端导航
const bottomNav = page.locator('.van-tabbar')
await expect(bottomNav).toBeVisible()
})
test('桌面端布局正确', async ({ page }) => {
// 设置桌面端视口
await page.setViewportSize({ width: 1280, height: 720 })
await page.goto('/')
// 检查页面内容
const mainContent = page.locator('main').first()
await expect(mainContent).toBeVisible()
})
})
......@@ -136,7 +136,7 @@ export default [
},
},
// 测试文件配置
// 测试文件配置(Vitest)
{
files: ['**/*.test.js', '**/*.spec.js', 'test/**'],
languageOptions: {
......@@ -154,6 +154,21 @@ export default [
},
},
// E2E 测试文件配置(Playwright)
{
files: ['e2e/**/*.{js,ts}'],
languageOptions: {
globals: {
test: 'readonly',
expect: 'readonly',
beforeAll: 'readonly',
afterAll: 'readonly',
beforeEach: 'readonly',
afterEach: 'readonly',
},
},
},
// Prettier 配置(必须最后)
prettier,
]
......
......@@ -10,6 +10,11 @@
"test": "vitest run",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "playwright test --debug",
"test:e2e:headed": "playwright test --headed",
"test:e2e:report": "playwright show-report",
"lint": "eslint . --fix",
"lint:check": "eslint .",
"format": "prettier --write \"src/**/*.{js,vue,css,less,md,json}\"",
......
/*
* @Date: 2026-01-28 21:45:00
* @Description: Playwright E2E 测试配置
*/
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
// 测试文件位置
testDir: './e2e',
// 测试超时时间(毫秒)
timeout: 30 * 1000,
// 期望超时时间
expect: {
timeout: 5 * 1000,
},
// 失败时重试次数
retries: process.env.CI ? 2 : 0,
// 并行执行
workers: process.env.CI ? 1 : undefined,
// 报告器
reporter: [
['html', { outputFolder: 'playwright-report' }],
['json', { outputFile: 'test-results/test-results.json' }],
['junit', { outputFile: 'test-results/test-results.xml' }],
],
// 共享配置
use: {
// 基础 URL(开发服务器地址)
baseURL: 'http://localhost:5173',
// 追踪失败测试(用于调试)
trace: 'on-first-retry',
// 截图(仅失败时)
screenshot: 'only-on-failure',
// 视频录制(仅失败时)
video: 'retain-on-failure',
// 浏览器视口大小(移动端优先)
viewport: { width: 375, height: 667 },
// 忽略 HTTPS 错误
ignoreHTTPSErrors: true,
// 操作超时
actionTimeout: 10 * 1000,
navigationTimeout: 30 * 1000,
},
// 测试项目(不同浏览器和视口)
projects: [
{
name: 'chromium-mobile',
use: {
...devices['iPhone 12'],
browserName: 'chromium',
},
},
{
name: 'chromium-desktop',
use: {
...devices['Desktop Chrome'],
viewport: { width: 1280, height: 720 },
},
},
{
name: 'webkit-mobile',
use: {
...devices['iPhone 12'],
browserName: 'webkit',
},
},
],
// 开发服务器(测试前启动)
webServer: {
command: 'pnpm dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
timeout: 120 * 1000,
},
})