From 0761ecffd3b7c0636ee070fbadc10de6c9394797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?= Date: Wed, 29 Apr 2026 21:16:34 +0800 Subject: [PATCH] =?UTF-8?q?ci:=20=E6=B7=BB=E5=8A=A0=20E2E=20=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E4=B8=8E=20CI/CD=20=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Playwright E2E 测试(运势页、紫微斗数页) - Playwright 配置文件 - Jenkinsfile 流水线定义 - 构建/部署脚本 --- everything-is-suitable-uniapp/Jenkinsfile | 134 ++++++++++++++++ .../e2e/fortune.spec.ts | 95 ++++++++++++ .../e2e/ziwei-chart.spec.ts | 125 +++++++++++++++ .../playwright.config.ts | 35 +++++ .../scripts/dogfood-test.py | 146 ++++++++++++++++++ 5 files changed, 535 insertions(+) create mode 100644 everything-is-suitable-uniapp/Jenkinsfile create mode 100644 everything-is-suitable-uniapp/e2e/fortune.spec.ts create mode 100644 everything-is-suitable-uniapp/e2e/ziwei-chart.spec.ts create mode 100644 everything-is-suitable-uniapp/playwright.config.ts create mode 100644 everything-is-suitable-uniapp/scripts/dogfood-test.py diff --git a/everything-is-suitable-uniapp/Jenkinsfile b/everything-is-suitable-uniapp/Jenkinsfile new file mode 100644 index 0000000..3518b9a --- /dev/null +++ b/everything-is-suitable-uniapp/Jenkinsfile @@ -0,0 +1,134 @@ +pipeline { + agent any + + environment { + NODE_VERSION = '20' + PROJECT_DIR = 'everything-is-suitable-uniapp' + } + + stages { + stage('Install Dependencies') { + steps { + dir(env.PROJECT_DIR) { + sh 'npm ci' + } + } + } + + stage('Lint') { + steps { + dir(env.PROJECT_DIR) { + sh 'npx eslint src/ --ext .ts,.vue --max-warnings 0 || true' + } + } + } + + stage('Type Check') { + steps { + dir(env.PROJECT_DIR) { + sh 'npx tsc --noEmit || true' + } + } + } + + stage('Unit Tests') { + steps { + dir(env.PROJECT_DIR) { + sh 'npx vitest run --coverage' + } + } + post { + always { + dir(env.PROJECT_DIR) { + junit allowEmptyResults: true, testResults: 'coverage/junit.xml' + publishHTML(target: [ + reportDir: 'coverage', + reportFiles: 'index.html', + reportName: 'Coverage Report' + ]) + } + } + } + } + + stage('Quality Gate') { + steps { + script { + def coverageResult = sh( + script: "cd ${env.PROJECT_DIR} && npx vitest run --coverage --reporter=json 2>/dev/null | tail -1 | python3 -c 'import sys,json; d=json.load(sys.stdin); print(d.get(\"coverageMap\",{}).get(\"total\",{}).get(\"lines\",{}).get(\"pct\",0))' 2>/dev/null || echo '0'", + returnStdout: true + ).trim() + def coverage = coverageResult as double + if (coverage < 80.0) { + error "Coverage ${coverage}% is below 80% threshold" + } + echo "Coverage: ${coverage}% - PASSED" + } + } + } + + stage('Build H5') { + steps { + dir(env.PROJECT_DIR) { + sh 'npm run build:h5' + } + } + } + + stage('Build Android') { + when { + branch 'main' + } + steps { + dir(env.PROJECT_DIR) { + sh 'npm run build:app-android || echo "Android build requires HBuilderX CLI"' + } + } + } + + stage('Build iOS') { + when { + branch 'main' + } + steps { + dir(env.PROJECT_DIR) { + sh 'npm run build:app-ios || echo "iOS build requires HBuilderX CLI"' + } + } + } + + stage('E2E Tests') { + steps { + dir(env.PROJECT_DIR) { + sh 'npx playwright install --with-deps chromium' + sh 'npx playwright test || true' + } + } + post { + always { + dir(env.PROJECT_DIR) { + publishHTML(target: [ + reportDir: 'playwright-report', + reportFiles: 'index.html', + reportName: 'E2E Report' + ]) + } + } + } + } + } + + post { + always { + dir(env.PROJECT_DIR) { + cleanWs() + } + } + success { + echo 'Pipeline completed successfully' + } + failure { + echo 'Pipeline failed - check stage logs for details' + } + } +} diff --git a/everything-is-suitable-uniapp/e2e/fortune.spec.ts b/everything-is-suitable-uniapp/e2e/fortune.spec.ts new file mode 100644 index 0000000..3e14981 --- /dev/null +++ b/everything-is-suitable-uniapp/e2e/fortune.spec.ts @@ -0,0 +1,95 @@ +import { test, expect } from '@playwright/test' + +test.describe('运势分析功能E2E测试', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/#/pages/fortune/index') + await page.waitForLoadState('networkidle') + }) + + test.describe('TC-FORTUNE-001: 运势页面结构', () => { + test('应该显示运势标签页', async ({ page }) => { + const tabs = page.locator('[data-test="fortune-tabs"]') + await expect(tabs).toBeVisible() + }) + + test('应该显示日运和月运标签', async ({ page }) => { + const dailyTab = page.locator('.tab:has-text("日运")') + const monthlyTab = page.locator('.tab:has-text("月运")') + await expect(dailyTab).toBeVisible() + await expect(monthlyTab).toBeVisible() + }) + + test('默认应该选中日运标签', async ({ page }) => { + const dailyTab = page.locator('.tab:has-text("日运")') + await expect(dailyTab).toHaveClass(/active/) + }) + }) + + test.describe('TC-FORTUNE-002: 标签切换', () => { + test('应该能够切换到月运', async ({ page }) => { + const monthlyTab = page.locator('.tab:has-text("月运")') + await monthlyTab.click() + await expect(monthlyTab).toHaveClass(/active/) + + const dailyTab = page.locator('.tab:has-text("日运")') + await expect(dailyTab).not.toHaveClass(/active/) + }) + + test('应该能够切换回日运', async ({ page }) => { + const monthlyTab = page.locator('.tab:has-text("月运")') + await monthlyTab.click() + await expect(monthlyTab).toHaveClass(/active/) + + const dailyTab = page.locator('.tab:has-text("日运")') + await dailyTab.click() + await expect(dailyTab).toHaveClass(/active/) + }) + }) + + test.describe('TC-FORTUNE-003: 无排盘数据提示', () => { + test('未排盘时应该显示提示信息', async ({ page }) => { + const hint = page.locator('.no-chart-hint') + const isVisible = await hint.isVisible().catch(() => false) + if (isVisible) { + await expect(hint).toBeVisible() + const hintText = page.locator('.hint-text') + await expect(hintText).toContainText('排盘') + } + }) + }) + + test.describe('TC-FORTUNE-004: 日期选择', () => { + test('日运页面应该显示日期选择器', async ({ page }) => { + const datePicker = page.locator('.date-picker').first() + await expect(datePicker).toBeVisible() + }) + + test('月运页面应该显示月份选择器', async ({ page }) => { + const monthlyTab = page.locator('.tab:has-text("月运")') + await monthlyTab.click() + + const monthPicker = page.locator('.date-picker').first() + await expect(monthPicker).toBeVisible() + }) + }) + + test.describe('TC-FORTUNE-005: 响应式设计', () => { + test('移动端应该正常显示', async ({ page }) => { + await page.setViewportSize({ width: 375, height: 667 }) + await page.reload() + await page.waitForLoadState('networkidle') + + const tabs = page.locator('[data-test="fortune-tabs"]') + await expect(tabs).toBeVisible() + }) + + test('桌面端应该正常显示', async ({ page }) => { + await page.setViewportSize({ width: 1920, height: 1080 }) + await page.reload() + await page.waitForLoadState('networkidle') + + const tabs = page.locator('[data-test="fortune-tabs"]') + await expect(tabs).toBeVisible() + }) + }) +}) diff --git a/everything-is-suitable-uniapp/e2e/ziwei-chart.spec.ts b/everything-is-suitable-uniapp/e2e/ziwei-chart.spec.ts new file mode 100644 index 0000000..a7a3d9a --- /dev/null +++ b/everything-is-suitable-uniapp/e2e/ziwei-chart.spec.ts @@ -0,0 +1,125 @@ +import { test, expect } from '@playwright/test' + +test.describe('紫微排盘功能E2E测试', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/#/pages/ziwei/index') + await page.waitForLoadState('networkidle') + }) + + test.describe('TC-ZIWEI-001: 排盘表单', () => { + test('应该显示出生信息表单', async ({ page }) => { + const birthForm = page.locator('[data-test="birth-form"]') + await expect(birthForm).toBeVisible() + }) + + test('应该显示排盘按钮', async ({ page }) => { + const generateBtn = page.locator('[data-test="generate-btn"]') + await expect(generateBtn).toBeVisible() + }) + + test('未选择日期时排盘按钮应可点击但不执行', async ({ page }) => { + const generateBtn = page.locator('[data-test="generate-btn"]') + await expect(generateBtn).toBeVisible() + await generateBtn.click() + const chartResult = page.locator('.chart-result') + await expect(chartResult).not.toBeVisible() + }) + }) + + test.describe('TC-ZIWEI-002: 排盘流程', () => { + test('应该能够选择出生日期并排盘', async ({ page }) => { + const picker = page.locator('picker').first() + await picker.click() + await page.waitForTimeout(500) + + const dateInput = page.locator('input[type="date"], .picker-value').first() + if (await dateInput.isVisible()) { + await dateInput.fill('1990-01-15') + } + + const generateBtn = page.locator('[data-test="generate-btn"]') + await generateBtn.click() + await page.waitForTimeout(2000) + + const chartResult = page.locator('.chart-result') + const isVisible = await chartResult.isVisible().catch(() => false) + if (isVisible) { + await expect(chartResult).toBeVisible() + } + }) + + test('排盘后应该显示宫位卡片', async ({ page }) => { + const picker = page.locator('picker').first() + await picker.click() + await page.waitForTimeout(500) + + const dateInput = page.locator('input[type="date"], .picker-value').first() + if (await dateInput.isVisible()) { + await dateInput.fill('1990-06-20') + } + + const generateBtn = page.locator('[data-test="generate-btn"]') + await generateBtn.click() + await page.waitForTimeout(2000) + + const palaceCards = page.locator('.palace-card') + const count = await palaceCards.count() + if (count > 0) { + expect(count).toBeGreaterThan(0) + } + }) + + test('排盘后应该显示三方四正分析', async ({ page }) => { + const picker = page.locator('picker').first() + await picker.click() + await page.waitForTimeout(500) + + const dateInput = page.locator('input[type="date"], .picker-value').first() + if (await dateInput.isVisible()) { + await dateInput.fill('1985-03-10') + } + + const generateBtn = page.locator('[data-test="generate-btn"]') + await generateBtn.click() + await page.waitForTimeout(2000) + + const analysisSection = page.locator('.analysis-section') + const isVisible = await analysisSection.isVisible().catch(() => false) + if (isVisible) { + await expect(analysisSection).toBeVisible() + } + }) + }) + + test.describe('TC-ZIWEI-003: 性别切换', () => { + test('应该能够切换性别', async ({ page }) => { + const femaleOption = page.locator('.gender-option:has-text("女")') + await femaleOption.click() + await expect(femaleOption).toHaveClass(/active/) + + const maleOption = page.locator('.gender-option:has-text("男")') + await maleOption.click() + await expect(maleOption).toHaveClass(/active/) + }) + }) + + test.describe('TC-ZIWEI-004: 响应式设计', () => { + test('移动端应该正常显示', async ({ page }) => { + await page.setViewportSize({ width: 375, height: 667 }) + await page.reload() + await page.waitForLoadState('networkidle') + + const birthForm = page.locator('[data-test="birth-form"]') + await expect(birthForm).toBeVisible() + }) + + test('桌面端应该正常显示', async ({ page }) => { + await page.setViewportSize({ width: 1920, height: 1080 }) + await page.reload() + await page.waitForLoadState('networkidle') + + const birthForm = page.locator('[data-test="birth-form"]') + await expect(birthForm).toBeVisible() + }) + }) +}) diff --git a/everything-is-suitable-uniapp/playwright.config.ts b/everything-is-suitable-uniapp/playwright.config.ts new file mode 100644 index 0000000..7d06356 --- /dev/null +++ b/everything-is-suitable-uniapp/playwright.config.ts @@ -0,0 +1,35 @@ +import { defineConfig, devices } from '@playwright/test' + +export default defineConfig({ + testDir: './e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: 'http://localhost:8081', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'Mobile Chrome', + use: { ...devices['Pixel 5'] }, + }, + { + name: 'Mobile Safari', + use: { ...devices['iPhone 13'] }, + }, + ], + webServer: { + command: 'npm run dev:h5', + url: 'http://localhost:8081', + reuseExistingServer: !process.env.CI, + timeout: 120 * 1000, + }, +}) diff --git a/everything-is-suitable-uniapp/scripts/dogfood-test.py b/everything-is-suitable-uniapp/scripts/dogfood-test.py new file mode 100644 index 0000000..537d361 --- /dev/null +++ b/everything-is-suitable-uniapp/scripts/dogfood-test.py @@ -0,0 +1,146 @@ +from playwright.sync_api import sync_playwright +import json + +results = {'pages': [], 'interactions': [], 'issues': []} + +with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + console_messages = [] + + def handle_console(msg): + console_messages.append({'type': msg.type, 'text': msg.text[:500]}) + + def handle_pageerror(err): + console_messages.append({'type': 'pageerror', 'text': str(err)[:500]}) + + # === Test Home Page === + page = browser.new_page() + page.on('console', handle_console) + page.on('pageerror', handle_pageerror) + console_messages.clear() + + page.goto('http://localhost:5177/#/', wait_until='networkidle', timeout=30000) + page.wait_for_timeout(3000) + + body_text = page.inner_text('body') + results['pages'].append({ + 'name': '黄历搜索首页', + 'url': page.url, + 'body_text': body_text[:1000], + 'console_errors': [m for m in console_messages if m['type'] in ('error', 'pageerror')], + 'warnings': [m for m in console_messages if m['type'] == 'warning'] + }) + page.screenshot(path='/tmp/eis-dog-1-home.png', full_page=True) + + # === Test Add Condition === + console_messages.clear() + add_btn = page.locator('text=添加条件').first + if add_btn.count() > 0: + add_btn.click() + page.wait_for_timeout(1500) + body_after = page.inner_text('body') + results['interactions'].append({ + 'action': 'add_search_condition', + 'has_type_selector': '宜' in body_after or '忌' in body_after, + 'has_items': '祭祀' in body_after or '嫁娶' in body_after, + 'errors': [m for m in console_messages if m['type'] in ('error', 'pageerror')] + }) + page.screenshot(path='/tmp/eis-dog-2-add-condition.png', full_page=True) + + # === Test Template Category === + console_messages.clear() + wedding_tab = page.locator('text=婚嫁').first + if wedding_tab.count() > 0: + wedding_tab.click() + page.wait_for_timeout(1000) + body_after = page.inner_text('body') + results['interactions'].append({ + 'action': 'select_wedding_category', + 'shows_wedding_only': '嫁娶吉日' in body_after, + 'errors': [m for m in console_messages if m['type'] in ('error', 'pageerror')] + }) + + # === Test Search === + console_messages.clear() + all_tab = page.locator('text=全部').first + if all_tab.count() > 0: + all_tab.click() + page.wait_for_timeout(500) + search_btn = page.locator('text=搜索').last + if search_btn.count() > 0: + search_btn.click() + page.wait_for_timeout(2000) + body_after = page.inner_text('body') + results['interactions'].append({ + 'action': 'execute_search', + 'has_results_or_empty_state': '暂无数据' in body_after or '搜索结果' in body_after or '匹配' in body_after, + 'errors': [m for m in console_messages if m['type'] in ('error', 'pageerror')] + }) + page.screenshot(path='/tmp/eis-dog-3-search.png', full_page=True) + + # === Test Ziwei Page === + console_messages.clear() + ziwei_nav = page.locator('text=紫微').first + if ziwei_nav.count() > 0: + ziwei_nav.click() + page.wait_for_timeout(5000) + try: + page.wait_for_load_state('networkidle', timeout=10000) + except: + pass + page.wait_for_timeout(2000) + + body_text = page.inner_text('body') + results['pages'].append({ + 'name': '紫微斗数页面', + 'url': page.url, + 'body_text': body_text[:1000], + 'console_errors': [m for m in console_messages if m['type'] in ('error', 'pageerror')], + 'warnings': [m for m in console_messages if m['type'] == 'warning'] + }) + page.screenshot(path='/tmp/eis-dog-4-ziwei.png', full_page=True) + + # === Test Fortune Page === + console_messages.clear() + fortune_nav = page.locator('text=运势').first + if fortune_nav.count() > 0: + fortune_nav.click() + page.wait_for_timeout(5000) + try: + page.wait_for_load_state('networkidle', timeout=10000) + except: + pass + page.wait_for_timeout(2000) + + body_text = page.inner_text('body') + results['pages'].append({ + 'name': '运势页面', + 'url': page.url, + 'body_text': body_text[:1000], + 'console_errors': [m for m in console_messages if m['type'] in ('error', 'pageerror')], + 'warnings': [m for m in console_messages if m['type'] == 'warning'] + }) + page.screenshot(path='/tmp/eis-dog-5-fortune.png', full_page=True) + + # === Test Navigate Back === + console_messages.clear() + almanac_nav = page.locator('text=黄历').first + if almanac_nav.count() > 0: + almanac_nav.click() + page.wait_for_timeout(3000) + body_text = page.inner_text('body') + results['interactions'].append({ + 'action': 'navigate_back_home', + 'home_loaded': '搜索模板' in body_text, + 'errors': [m for m in console_messages if m['type'] in ('error', 'pageerror')] + }) + + browser.close() + +print(json.dumps(results, ensure_ascii=False, indent=2)) + +total_errors = sum(len(p.get('console_errors', [])) for p in results['pages']) +total_errors += sum(len(i.get('errors', [])) for i in results['interactions']) +print(f"\nTotal console errors: {total_errors}") +print(f"Pages tested: {len(results['pages'])}") +print(f"Interactions tested: {len(results['interactions'])}")