ci: 完善项目持续集成配置并添加 Playwright 测试

This commit is contained in:
严浩
2025-09-09 15:36:24 +08:00
parent 7c04f69d1a
commit 706c60ddb8
11 changed files with 1173 additions and 212 deletions

23
.github/workflows/playwright.yaml vendored Normal file
View File

@@ -0,0 +1,23 @@
defaults:
run:
shell: bash
env:
TZ: Asia/Shanghai
on:
push:
workflow_dispatch:
jobs:
playwright:
runs-on: ubuntu-latest
container: mcr.microsoft.com/playwright:v1.55.0-noble
steps:
- name: ⚙️ 设置 Node 环境
uses: yanhao98/composite-actions/setup-node-environment@25eb4dc0c134cc9df2b7c569aa54140a366b45a8
# - name: 📥 安装 Playwright 浏览器
# run: pnpm exec playwright install --with-deps
- name: ▶️ 运行 Playwright 测试
run: |
npx playwright test

20
.husky/README.md Normal file
View File

@@ -0,0 +1,20 @@
### Husky 遇到 command not found: husky
- https://typicode.github.io/husky/zh/troubleshoot.html#找不到命令-command-not-found
- https://typicode.github.io/husky/zh/how-to.html#node-版本管理器和-gui
```shell
ln -s $(which pnpm) $HOME/.local/bin/pnpm
```
```
# if command -v pnpm >/dev/null 2>&1; then
# # 如果 pnpm 可用,直接使用它
# pnpm exec lint-staged
# else
# # 如果 pnpm 不可用,使用 $HOME/.local/bin/pnpm
# # ln -s $(which pnpm) $HOME/.local/bin/pnpm
# echo "找不到 pnpm使用 $HOME/.local/bin/pnpm"
# "$HOME"/.local/bin/pnpm exec lint-staged
# fi
```

5
.husky/commit-msg Normal file
View File

@@ -0,0 +1,5 @@
# 此钩子在 pre-commit 钩子成功完成后,用于检查提交消息。
echo "📝 [Commit-msg] 正在运行 commit-msg 钩子..."
echo "检查提交消息:$1"
pnpm exec commitlint --edit $1
echo "✅ [Commit-msg] commit-msg 钩子完成!"

4
.husky/post-merge Normal file
View File

@@ -0,0 +1,4 @@
# 此钩子在 git merge 或 git pull 成功完成后运行。
echo "🔗 [Post-merge] 正在安装依赖..."
pnpm install
echo "✅ [Post-merge] 依赖安装完成!"

4
.husky/pre-commit Normal file
View File

@@ -0,0 +1,4 @@
# 此钩子在执行 git commit 命令时,在创建提交之前运行。
echo "🧹 [Pre-commit] 正在运行 lint-staged..."
pnpm exec lint-staged
echo "✅ [Pre-commit] lint-staged 完成!"

9
commitlint.config.ts Normal file
View File

@@ -0,0 +1,9 @@
import type { UserConfig } from '@commitlint/types';
const Configuration: UserConfig = {
extends: ['@commitlint/config-conventional'],
formatter: '@commitlint/format',
};
export default Configuration;

View File

@@ -0,0 +1,53 @@
import { test, expect } from '@playwright/test'
test.describe('Vue App', () => {
test('visits the app root url', async ({ page }) => {
await page.goto('/')
await expect(page.locator('h1')).toHaveText('You did it!')
})
test('displays Vue documentation link', async ({ page }) => {
await page.goto('/')
const link = page.locator('a[href="https://vuejs.org/"]')
await expect(link).toBeVisible()
await expect(link).toHaveText('vuejs.org')
await expect(link).toHaveAttribute('target', '_blank')
await expect(link).toHaveAttribute('rel', 'noopener')
})
test('displays button with initial name state', async ({ page }) => {
await page.goto('/')
const button = page.locator('button[aria-label="get name"]')
await expect(button).toBeVisible()
await expect(button).toHaveText('Name from API is: Unknown')
})
test('button click triggers API call', async ({ page }) => {
await page.goto('/')
await page.route('/api/', async (route) => {
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify({ name: 'Test User' }),
})
})
const button = page.locator('button[aria-label="get name"]')
await button.click()
await expect(button).toHaveText('Name from API is: Test User')
})
test('handles API error gracefully', async ({ page }) => {
await page.goto('/')
await page.route('/api/', async (route) => {
await route.abort('failed')
})
const button = page.locator('button[aria-label="get name"]')
await button.click()
await expect(button).toHaveText('Name from API is: Unknown')
})
})

View File

@@ -1,8 +0,0 @@
import { test, expect } from '@playwright/test'
// See here how to get started:
// https://playwright.dev/docs/intro
test('visits the app root url', async ({ page }) => {
await page.goto('/')
await expect(page.locator('h1')).toHaveText('You did it!')
})

View File

@@ -8,16 +8,22 @@
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"
}, },
"scripts": { "scripts": {
"_all": "run-p build-only format type-check lint",
"dev": "vite --port 4730 --host --strictPort", "dev": "vite --port 4730 --host --strictPort",
"build": "run-p type-check \"build-only {@}\" --", "build": "run-p type-check \"build-only {@}\" --",
"preview:vite": "vite preview",
"preview": "pnpm run build && wrangler dev",
"test:unit": "vitest",
"test:e2e": "playwright test",
"build-only": "vite build", "build-only": "vite build",
"preview": "vite preview",
"preview:wrangler": "pnpm run build && wrangler dev",
"test:unit": "vitest",
"test:playwright": "playwright test",
"test:playwright:headless": "HEADLESS=true playwright test",
"test:playwright:ui": "playwright test --ui",
"test:playwright:chromium": "playwright test --project=chromium",
"type-check": "vue-tsc --build", "type-check": "vue-tsc --build",
"lint:oxlint": "oxlint . --fix -D correctness --ignore-path .gitignore", "lint:oxlint": "oxlint . --fix -D correctness --ignore-path .gitignore",
"lint:eslint": "eslint . --fix", "lint:eslint": "eslint . --fix",
"_oxlint_cfg": "oxlint . --fix --ignore-path=.gitignore --print-config",
"__oxlint_-D": "oxlint . --fix --deny=correctness",
"lint": "run-s lint:*", "lint": "run-s lint:*",
"format": "prettier --write src/", "format": "prettier --write src/",
"-wrangler:pages:deploy:preview": "wrangler pages deploy dist --project-name=vue-ts-example-2025 --branch=preview", "-wrangler:pages:deploy:preview": "wrangler pages deploy dist --project-name=vue-ts-example-2025 --branch=preview",
@@ -26,41 +32,65 @@
"-deploy:prod": "run-s build-only wrangler:pages:deploy:prod", "-deploy:prod": "run-s build-only wrangler:pages:deploy:prod",
"wrangler:deploy": "pnpm run build && wrangler deploy", "wrangler:deploy": "pnpm run build && wrangler deploy",
"wrangler:versions:upload": "pnpm run build && wrangler versions upload", "wrangler:versions:upload": "pnpm run build && wrangler versions upload",
"cf-typegen": "wrangler types" "cf-typegen": "wrangler types",
"_dep:dedupe": "pnpm dedupe",
"_dep:update": "pnpm dlx taze major --interactive",
"_sizecheck:Treemap": "pnpm dlx vite-bundle-visualizer -t treemap",
"_sizecheck:Sunburst": "pnpm dlx vite-bundle-visualizer -t sunburst",
"_sizecheck:Network": "pnpm dlx vite-bundle-visualizer -t network",
"_knip": "pnpm dlx knip",
"prepare": "husky"
},
"lint-staged": {
"{src,e2e}/**/*.{js,jsx,ts,tsx,vue}": [
"prettier --write",
"eslint --fix",
"oxlint --fix"
]
},
"pnpm": {
"overrides": {
"vite": "$vite"
}
}, },
"dependencies": { "dependencies": {
"@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.8.1",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"vue": "^3.5.18", "vue": "^3.5.21",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {
"@cloudflare/vite-plugin": "^1.12.3", "@cloudflare/vite-plugin": "^1.12.3",
"@playwright/test": "^1.54.1", "@commitlint/types": "^19.8.1",
"@playwright/test": "^1.55.0",
"@prettier/plugin-oxc": "^0.0.4", "@prettier/plugin-oxc": "^0.0.4",
"@tsconfig/node22": "^22.0.2", "@tsconfig/node22": "^22.0.2",
"@types/jsdom": "^21.1.7", "@types/jsdom": "^21.1.7",
"@types/node": "^22.16.5", "@types/node": "^22.18.1",
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^5.0.1", "@vitejs/plugin-vue-jsx": "^5.1.1",
"@vitest/eslint-plugin": "^1.3.4", "@vitest/eslint-plugin": "^1.3.9",
"@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.6.0", "@vue/eslint-config-typescript": "^14.6.0",
"@vue/test-utils": "^2.4.6", "@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.7.0", "@vue/tsconfig": "^0.8.1",
"eslint": "^9.31.0", "eslint": "^9.35.0",
"eslint-plugin-oxlint": "~1.8.0", "eslint-plugin-oxlint": "~1.14.0",
"eslint-plugin-playwright": "^2.2.0", "eslint-plugin-playwright": "^2.2.2",
"eslint-plugin-vue": "~10.3.0", "eslint-plugin-vue": "~10.4.0",
"jiti": "^2.4.2", "husky": "^9.1.7",
"jiti": "^2.5.1",
"jsdom": "^26.1.0", "jsdom": "^26.1.0",
"lint-staged": "^16.1.6",
"npm-run-all2": "^8.0.4", "npm-run-all2": "^8.0.4",
"oxlint": "~1.8.0", "oxlint": "~1.14.0",
"prettier": "3.6.2", "prettier": "3.6.2",
"typescript": "~5.8.0", "typescript": "~5.9.2",
"vite": "npm:rolldown-vite@latest", "vite": "npm:rolldown-vite@^7.1.8",
"vite-plugin-vue-devtools": "^8.0.0", "vite-plugin-vue-devtools": "^8.0.1",
"vitest": "^3.2.4", "vitest": "^3.2.4",
"vue-tsc": "^3.0.4", "vue-tsc": "^3.0.6",
"wrangler": "^4.34.0" "wrangler": "^4.34.0"
} }
} }

View File

@@ -1,5 +1,8 @@
import process from 'node:process'
import { defineConfig, devices } from '@playwright/test' import { defineConfig, devices } from '@playwright/test'
import process from 'node:process'
// const runningInVSCode = process.env.TERM_PROGRAM === 'vscode'
const baseURL = process.env.CI ? 'http://localhost:4173' : 'http://localhost:4730'
/** /**
* Read environment variables from file. * Read environment variables from file.
@@ -11,7 +14,7 @@ import { defineConfig, devices } from '@playwright/test'
* See https://playwright.dev/docs/test-configuration. * See https://playwright.dev/docs/test-configuration.
*/ */
export default defineConfig({ export default defineConfig({
testDir: './e2e', testDir: './e2e/playwright',
/* Maximum time one test can run for. */ /* Maximum time one test can run for. */
timeout: 30 * 1000, timeout: 30 * 1000,
expect: { expect: {
@@ -34,13 +37,13 @@ export default defineConfig({
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0, actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */ /* Base URL to use in actions like `await page.goto('/')`. */
baseURL: process.env.CI ? 'http://localhost:4173' : 'http://localhost:5173', baseURL,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry', trace: 'on-first-retry',
/* Only on CI systems run the tests headless */ /* Only on CI systems run the tests headless */
headless: !!process.env.CI, headless: !!process.env.CI || process.env.HEADLESS === 'true',
}, },
/* Configure projects for major browsers */ /* Configure projects for major browsers */
@@ -104,7 +107,7 @@ export default defineConfig({
* Playwright will re-use the local server if there is already a dev-server running. * Playwright will re-use the local server if there is already a dev-server running.
*/ */
command: process.env.CI ? 'npm run preview' : 'npm run dev', command: process.env.CI ? 'npm run preview' : 'npm run dev',
port: process.env.CI ? 4173 : 5173, port: Number(new URL(baseURL).port),
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
}, },
}) })

1174
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff