Commit f445d217 authored by liguangyu06's avatar liguangyu06
Browse files

Make report emails lead with scope, counts, pass rate, and the Playwright link.

parent 4852f397
...@@ -16,7 +16,10 @@ def sendReportEmail() { ...@@ -16,7 +16,10 @@ def sendReportEmail() {
: "【SPD UI自动化】${params.SUITE} ${currentBuild.currentResult} #${env.BUILD_NUMBER}" : "【SPD UI自动化】${params.SUITE} ${currentBuild.currentResult} #${env.BUILD_NUMBER}"
def body = fileExists('reports/latest/email-summary.html') def body = fileExists('reports/latest/email-summary.html')
? readFile(encoding: 'UTF-8', file: 'reports/latest/email-summary.html') ? readFile(encoding: 'UTF-8', file: 'reports/latest/email-summary.html')
: "<p>构建 ${currentBuild.currentResult},未生成摘要。请打开 <a href='${env.BUILD_URL}'>Jenkins</a></p>" : """<p>构建 ${currentBuild.currentResult},未生成详细摘要。</p>
<p>测试范围:${params.SUITE}</p>
<p>Playwright 报告:<a href='${env.BUILD_URL}Playwright_20Report/'>${env.BUILD_URL}Playwright_20Report/</a></p>
<p>Jenkins 构建:<a href='${env.BUILD_URL}'>${env.BUILD_URL}</a></p>"""
if (cc) { if (cc) {
to = [to, cc].findAll { it }.join(',') to = [to, cc].findAll { it }.join(',')
} }
......
...@@ -34,7 +34,7 @@ Chromium 缓存在项目内 `.playwright-browsers/`(已 gitignore),与 `@p ...@@ -34,7 +34,7 @@ Chromium 缓存在项目内 `.playwright-browsers/`(已 gitignore),与 `@p
并行默认按 **文件**`PW_WORKERS`)。不要开 `PW_FULLY_PARALLEL=1`:本仓库大量同页 serial,拆开后同一账号会互踩。总浏览器约 `SHARDS × WORKERS`,单机建议不超过 6。详见 [docs/jenkins.md](docs/jenkins.md) 并行默认按 **文件**`PW_WORKERS`)。不要开 `PW_FULLY_PARALLEL=1`:本仓库大量同页 serial,拆开后同一账号会互踩。总浏览器约 `SHARDS × WORKERS`,单机建议不超过 6。详见 [docs/jenkins.md](docs/jenkins.md)
产物在 `reports/latest/`:Playwright HTML、`junit.xml`、Allure results、`summary.json`。定时跑完会发 **HTML 摘要邮件**失败列表 + Jenkins 报告链接),不要把完整报告当附件。收件人配 `REPORT_EMAIL_TO`,SMTP 与任务拆分见 [docs/jenkins.md](docs/jenkins.md) 产物在 `reports/latest/`:Playwright HTML、`junit.xml`、Allure results、`summary.json`。定时跑完会发 **HTML 摘要邮件**测试范围、执行数、成功/失败、通过率、报告链接),不要把完整报告当附件。收件人配 `REPORT_EMAIL_TO`,SMTP 与任务拆分见 [docs/jenkins.md](docs/jenkins.md)
Jenkins:仓库根目录 `Jenkinsfile`。逐步配置见 [docs/Jenkins配置指南.md](docs/Jenkins配置指南.md),套件与并行约定见 [docs/jenkins.md](docs/jenkins.md)。凭据 ID 默认 `spd-ui-test-account` Jenkins:仓库根目录 `Jenkinsfile`。逐步配置见 [docs/Jenkins配置指南.md](docs/Jenkins配置指南.md),套件与并行约定见 [docs/jenkins.md](docs/jenkins.md)。凭据 ID 默认 `spd-ui-test-account`
......
...@@ -92,7 +92,7 @@ git --version ...@@ -92,7 +92,7 @@ git --version
## 第五步:把测试报告用邮件发给同事 ## 第五步:把测试报告用邮件发给同事
流水线跑完会自动发信,不用再加「构建后操作」。 流水线跑完会自动发信,不用再加「构建后操作」。
邮件里是:**结果摘要 + 失败用例列表 + Jenkins/Playwright 报告链接**,并带上附件 `email-summary.html` 邮件里是:**测试范围、执行用例数、成功/失败、通过率、Playwright 报告链接**以及失败用例列表;并带上附件 `email-summary.html`
完整截图/Trace 请同事点邮件里的链接打开(整包报告太大,邮箱会拒收,所以不当附件)。 完整截图/Trace 请同事点邮件里的链接打开(整包报告太大,邮箱会拒收,所以不当附件)。
### 5.1 确认插件 ### 5.1 确认插件
......
...@@ -98,7 +98,7 @@ npm run test:spd:shard ...@@ -98,7 +98,7 @@ npm run test:spd:shard
## 邮件发给领导 / 同事 ## 邮件发给领导 / 同事
邮件发的是 **一页摘要**通过/失败、耗时、失败用例列表)+ Jenkins 报告链接。 邮件发的是 **一页摘要**测试范围、执行用例数、成功/失败、通过率、失败用例列表)+ Playwright/Jenkins 报告链接。
**不要**把整个 Playwright 目录当附件:体积大,Trace 里还有页面数据。 **不要**把整个 Playwright 目录当附件:体积大,Trace 里还有页面数据。
### 1. Jenkins 插件与 SMTP ### 1. Jenkins 插件与 SMTP
...@@ -134,13 +134,14 @@ Default user e-mail suffix: @公司域名 ...@@ -134,13 +134,14 @@ Default user e-mail suffix: @公司域名
### 3. 领导看到什么 ### 3. 领导看到什么
主题示例:`【SPD UI自动化】nightly 失败 1180/1204 #88` 主题示例:`【SPD UI自动化】smoke 失败 110/119(通过率 92.4%) #13`
正文: 正文:
- 套件、通过/失败/跳过、耗时 - 测试范围(套件说明)
- 执行用例数、成功、失败、通过率
- 失败用例前 30 条 - 失败用例前 30 条
- 链接:Jenkins 构建、Playwright 报告、Allure(装了插件才有效) - 链接:Playwright 报告、Jenkins 构建、Allure(装了插件才有效)
点 Playwright 报告即可看截图和 trace。若公司 Jenkins 需登录,请给领导开只读权限,或把 Jenkins 配在内网 SSO 后发信。 点 Playwright 报告即可看截图和 trace。若公司 Jenkins 需登录,请给领导开只读权限,或把 Jenkins 配在内网 SSO 后发信。
......
...@@ -9,6 +9,8 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; ...@@ -9,6 +9,8 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { getSuite } from '../config/suites.config.mjs';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const LATEST = path.join(ROOT, 'reports', 'latest'); const LATEST = path.join(ROOT, 'reports', 'latest');
...@@ -66,6 +68,20 @@ function parseFailedTests(junitFile, limit = 30) { ...@@ -66,6 +68,20 @@ function parseFailedTests(junitFile, limit = 30) {
return cases; return cases;
} }
function suiteScope(summary) {
if (summary?.description) return summary.description;
try {
return getSuite(summary?.suite || process.env.SUITE).description;
} catch {
return summary?.suite || process.env.SUITE || 'SPD UI 自动化';
}
}
function formatPassRate(passed, executed) {
if (!executed) return '-';
return `${((passed / executed) * 100).toFixed(1)}%`;
}
function statusLabel(summary, jenkinsResult) { function statusLabel(summary, jenkinsResult) {
if (jenkinsResult && jenkinsResult !== 'SUCCESS' && jenkinsResult !== 'UNSTABLE') { if (jenkinsResult && jenkinsResult !== 'SUCCESS' && jenkinsResult !== 'UNSTABLE') {
if (jenkinsResult === 'ABORTED') return { text: '已中止', color: '#6b7280' }; if (jenkinsResult === 'ABORTED') return { text: '已中止', color: '#6b7280' };
...@@ -89,27 +105,40 @@ function main() { ...@@ -89,27 +105,40 @@ function main() {
const failures = Number(counts.failures || 0); const failures = Number(counts.failures || 0);
const errors = Number(counts.errors || 0); const errors = Number(counts.errors || 0);
const skipped = Number(counts.skipped || 0); const skipped = Number(counts.skipped || 0);
const passed = Math.max(0, tests - failures - errors - skipped);
const failTotal = failures + errors; const failTotal = failures + errors;
const passed = Math.max(0, tests - failures - errors - skipped);
const executed = passed + failTotal;
const passRate = formatPassRate(passed, executed || tests);
const jenkinsResult = process.env.BUILD_RESULT || process.env.JENKINS_BUILD_RESULT || ''; const jenkinsResult = process.env.BUILD_RESULT || process.env.JENKINS_BUILD_RESULT || '';
const label = statusLabel(summary, jenkinsResult); const label = statusLabel(summary, jenkinsResult);
const buildUrl = (process.env.BUILD_URL || '').replace(/\/?$/, '/'); const buildUrl = (process.env.BUILD_URL || '').replace(/\/?$/, '/');
const jobName = process.env.JOB_NAME || 'spd-ui-automation'; const jobName = process.env.JOB_NAME || 'SPD';
const buildNo = process.env.BUILD_NUMBER || summary.buildId || '-'; const buildNo = process.env.BUILD_NUMBER || summary.buildId || '-';
const suite = summary.suite || process.env.SUITE || '-'; const suite = summary.suite || process.env.SUITE || '-';
const scope = suiteScope(summary);
const stamp = summary.stamp ? new Date(summary.stamp).toLocaleString('zh-CN', { hour12: false }) : new Date().toLocaleString('zh-CN', { hour12: false }); const stamp = summary.stamp ? new Date(summary.stamp).toLocaleString('zh-CN', { hour12: false }) : new Date().toLocaleString('zh-CN', { hour12: false });
const duration = formatDuration(summary.durationMs); const duration = formatDuration(summary.durationMs);
const failed = parseFailedTests(path.join(LATEST, 'junit.xml')); const failed = parseFailedTests(path.join(LATEST, 'junit.xml'));
const moreFail = failTotal > failed.length ? failTotal - failed.length : 0; const moreFail = failTotal > failed.length ? failTotal - failed.length : 0;
const pwReportUrl = buildUrl ? `${buildUrl}Playwright_20Report/` : '';
const jenkinsUrl = buildUrl;
const allureUrl = buildUrl ? `${buildUrl}allure` : '';
const reportLinks = []; const metric = (title, value, color) => `<td style="padding:10px 8px;text-align:center;width:25%;">
if (buildUrl) { <div style="font-size:12px;color:#6b7280;">${title}</div>
reportLinks.push(`<a href="${escapeHtml(buildUrl)}">Jenkins 构建</a>`); <div style="font-size:26px;font-weight:700;color:${color};margin-top:4px;line-height:1.2;">${value}</div>
reportLinks.push(`<a href="${escapeHtml(buildUrl)}Playwright_20Report/">Playwright 报告</a>`); </td>`;
reportLinks.push(`<a href="${escapeHtml(buildUrl)}allure/">Allure 报告</a>(若已装插件)`);
} else { const reportButtons = buildUrl
reportLinks.push('本地运行:打开 reports/latest/playwright-report/index.html'); ? `<p style="margin:0 0 10px;">
} <a href="${escapeHtml(pwReportUrl)}" style="display:inline-block;padding:10px 16px;background:#047857;color:#fff;text-decoration:none;border-radius:6px;font-weight:600;">打开 Playwright 测试报告</a>
</p>
<p style="margin:0;font-size:13px;line-height:1.8;">
Playwright 报告:<a href="${escapeHtml(pwReportUrl)}">${escapeHtml(pwReportUrl)}</a><br>
Jenkins 构建:<a href="${escapeHtml(jenkinsUrl)}">${escapeHtml(jenkinsUrl)}</a><br>
Allure 报告:<a href="${escapeHtml(allureUrl)}">${escapeHtml(allureUrl)}</a>(未装插件时可能打不开)
</p>`
: `<p style="margin:0;font-size:13px;">本地报告:reports/latest/playwright-report/index.html</p>`;
const failRows = failed.length const failRows = failed.length
? failed ? failed
...@@ -126,40 +155,54 @@ function main() { ...@@ -126,40 +155,54 @@ function main() {
<html lang="zh-CN"> <html lang="zh-CN">
<head><meta charset="UTF-8"><title>SPD UI 自动化报告</title></head> <head><meta charset="UTF-8"><title>SPD UI 自动化报告</title></head>
<body style="margin:0;padding:0;background:#f3f4f6;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;color:#111827;"> <body style="margin:0;padding:0;background:#f3f4f6;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;color:#111827;">
<div style="max-width:720px;margin:24px auto;background:#fff;border-radius:8px;overflow:hidden;border:1px solid #e5e7eb;"> <div style="max-width:760px;margin:24px auto;background:#fff;border-radius:8px;overflow:hidden;border:1px solid #e5e7eb;">
<div style="padding:16px 20px;background:${label.color};color:#fff;"> <div style="padding:16px 20px;background:${label.color};color:#fff;">
<div style="font-size:13px;opacity:.9;">SPD 医用耗材 · UI 自动化</div> <div style="font-size:13px;opacity:.9;">SPD 医用耗材 · UI 自动化测试报告</div>
<div style="font-size:22px;font-weight:700;margin-top:4px;">${escapeHtml(label.text)} · ${escapeHtml(suite)}</div> <div style="font-size:22px;font-weight:700;margin-top:4px;">${escapeHtml(label.text)} · ${escapeHtml(suite)} · 通过率 ${escapeHtml(passRate)}</div>
</div> </div>
<div style="padding:20px;"> <div style="padding:20px;">
<table style="border-collapse:collapse;width:100%;font-size:14px;"> <table style="border-collapse:collapse;width:100%;font-size:14px;margin-bottom:16px;">
<tr><td style="padding:6px 0;color:#6b7280;width:120px;">任务</td><td>${escapeHtml(jobName)} #${escapeHtml(buildNo)}</td></tr> <tr><td style="padding:6px 0;color:#6b7280;width:96px;vertical-align:top;">测试范围</td><td>${escapeHtml(scope)}</td></tr>
<tr><td style="padding:6px 0;color:#6b7280;">时间</td><td>${escapeHtml(stamp)}</td></tr> <tr><td style="padding:6px 0;color:#6b7280;">任务</td><td>${escapeHtml(jobName)} #${escapeHtml(buildNo)}</td></tr>
<tr><td style="padding:6px 0;color:#6b7280;">耗时</td><td>${escapeHtml(duration)}</td></tr> <tr><td style="padding:6px 0;color:#6b7280;">执行时间</td><td>${escapeHtml(stamp)}(耗时 ${escapeHtml(duration)})</td></tr>
<tr><td style="padding:6px 0;color:#6b7280;">用例</td><td>共 ${tests},通过 ${passed},失败 ${failTotal},跳过 ${skipped}</td></tr> </table>
<tr><td style="padding:6px 0;color:#6b7280;">并行</td><td>workers=${escapeHtml(summary.workers ?? '-')} shards=${escapeHtml(summary.shards ?? '-')}</td></tr> <table style="border-collapse:collapse;width:100%;background:#f9fafb;border:1px solid #e5e7eb;border-radius:8px;">
<tr><td style="padding:6px 0;color:#6b7280;">报告</td><td>${reportLinks.join(' ')}</td></tr> <tr>
${metric('执行用例数', executed || tests, '#111827')}
${metric('成功', passed, '#047857')}
${metric('失败', failTotal, failTotal ? '#b91c1c' : '#047857')}
${metric('通过率', passRate, failTotal ? '#b91c1c' : '#047857')}
</tr>
</table> </table>
<h3 style="margin:20px 0 8px;font-size:15px;">失败用例${moreFail ? `(仅列出前 ${failed.length} 条,另有 ${moreFail} 条)` : ''}</h3> <p style="margin:8px 0 0;font-size:12px;color:#6b7280;">跳过 ${skipped} 条未计入通过率;通过率 = 成功 / (成功 + 失败)。</p>
<h3 style="margin:22px 0 10px;font-size:15px;">测试报告</h3>
${reportButtons}
<h3 style="margin:22px 0 8px;font-size:15px;">失败用例${moreFail ? `(仅列出前 ${failed.length} 条,另有 ${moreFail} 条)` : ''}</h3>
<table style="border-collapse:collapse;width:100%;">${failRows}</table> <table style="border-collapse:collapse;width:100%;">${failRows}</table>
<p style="margin:20px 0 0;font-size:12px;color:#6b7280;">完整截图/Trace 请打开 Jenkins 上的 Playwright 报告,邮件不附带大附件。此邮件由定时任务自动发送。</p> <p style="margin:20px 0 0;font-size:12px;color:#6b7280;">完整截图Trace 请点上面的 Playwright 报告。此邮件由定时任务自动发送。</p>
</div> </div>
</div> </div>
</body> </body>
</html> </html>
`; `;
const subject = `【SPD UI自动化】${suite} ${label.text} ${passed}/${tests || '-'} #${buildNo}`; const subject = `【SPD UI自动化】${suite} ${label.text} ${passed}/${executed || tests || '-'}(通过率 ${passRate} #${buildNo}`;
writeFileSync(path.join(LATEST, 'email-summary.html'), html); writeFileSync(path.join(LATEST, 'email-summary.html'), html);
writeFileSync(path.join(LATEST, 'email-subject.txt'), `${subject}\n`); writeFileSync(path.join(LATEST, 'email-subject.txt'), `${subject}\n`);
writeFileSync( writeFileSync(
path.join(LATEST, 'email-summary.txt'), path.join(LATEST, 'email-summary.txt'),
[ [
subject, subject,
`测试范围: ${scope}`,
`任务: ${jobName} #${buildNo}`, `任务: ${jobName} #${buildNo}`,
`结果: ${label.text} 通过 ${passed} / 失败 ${failTotal} / 跳过 ${skipped} / 共 ${tests}`, `执行用例数: ${executed || tests}`,
`成功: ${passed}`,
`失败: ${failTotal}`,
`通过率: ${passRate}`,
skipped ? `跳过: ${skipped}` : '',
`耗时: ${duration}`, `耗时: ${duration}`,
buildUrl ? `Jenkins: ${buildUrl}` : '', pwReportUrl ? `Playwright 报告: ${pwReportUrl}` : '',
jenkinsUrl ? `Jenkins 构建: ${jenkinsUrl}` : '',
'', '',
] ]
.filter(Boolean) .filter(Boolean)
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment