play.wenpai.net/scripts/play-verify
elementary-qa 36b7ad5288
All checks were successful
Deploy play.wenpai.net / deploy (push) Successful in 3s
feat: add 7-layer test infrastructure for play.wenpai.net
- Schema validation (validate-plugins): field/type/enum/blueprint existence checks
- Deep verification (play-verify): admin notice bar, plugin activation, JS error capture
- Performance baseline (play-verify): navigation + playground load timing
- Visual regression (play-visual): 3 viewports, ImageMagick pixel diff
- Cross-browser (play-verify): --browser firefox/webkit, --mobile viewport
- Accessibility (play-a11y): axe-core WCAG 2 AA audit
- Error monitoring (play-smoke): blueprint URL reachability for all zip references
- Fix: color contrast on featured card (opacity 0.45→0.7) for WCAG AA compliance

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 14:51:08 +08:00

350 lines
14 KiB
JavaScript
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
// play-verify — play.wenpai.net 线上插件快捷方式验证工具
// 用法: play-verify <plugin-id> [--all] [--screenshot] [--report] [--post-forum]
// play-verify --all --browser firefox (跨浏览器测试)
// play-verify --all --browser webkit (Safari 近似)
// play-verify --all --mobile (移动端 viewport)
// 示例: play-verify wpslug
// play-verify --all
// play-verify wpslug --screenshot
// play-verify --all --report
// play-verify --all --report --post-forum
const { chromium, firefox, webkit } = require('playwright');
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const SITE = 'https://play.wenpai.net';
const PLUGINS_JSON = path.join(process.env.HOME, 'play/plugins.json');
const SCREENSHOT_DIR = path.join(process.env.HOME, 'test-results', new Date().toISOString().slice(0, 10), 'play-verify');
const args = process.argv.slice(2);
const doAll = args.includes('--all');
const doScreenshot = args.includes('--screenshot');
const doReport = args.includes('--report');
const doForum = args.includes('--post-forum');
const doMobile = args.includes('--mobile');
const browserIdx = args.indexOf('--browser');
const browserName = browserIdx >= 0 ? args[browserIdx + 1] : 'chromium';
const pluginId = args.find(a => !a.startsWith('--') && a !== browserName);
if (!pluginId && !doAll) {
console.log('用法: play-verify <plugin-id> [--all] [--screenshot] [--report] [--post-forum]');
console.log(' play-verify wpslug 验证单个插件');
console.log(' play-verify --all 验证所有插件');
console.log(' play-verify wpslug --screenshot 验证并截图');
console.log(' play-verify --all --report 验证并生成 markdown 报告');
console.log(' play-verify --all --report --post-forum 报告并发论坛');
process.exit(0);
}
async function getPlugins() {
const data = JSON.parse(fs.readFileSync(PLUGINS_JSON, 'utf8'));
// 排除 env 类型(无插件可验证),保留 plugin/combo/featured
return data.filter(p => p.group !== 'env');
}
function extractVersionFromBlueprint(id) {
const bpPath = path.join(process.env.HOME, 'play/blueprints', `${id}.json`);
try {
const bp = JSON.parse(fs.readFileSync(bpPath, 'utf8'));
for (const step of bp.steps || []) {
if (step.step === 'installPlugin' && step.pluginData?.url) {
const m = step.pluginData.url.match(/[\w-]-([\d.]+)\.zip$/);
if (m) return m[1];
}
}
} catch (_) {}
return null;
}
async function verifyPlugin(browser, plugin) {
const id = plugin.id || plugin;
const name = plugin.name || id;
const results = { id, name, checks: [], pass: true, timing: {} };
const t0 = Date.now();
// 自动从蓝图提取版本号plugins.json 没有时)
if (!plugin.version) plugin.version = extractVersionFromBlueprint(id);
const viewport = doMobile
? { width: 375, height: 812 }
: { width: 1280, height: 800 };
const page = await browser.newPage({ viewport });
// 监听 JS 错误和 console.error
const jsErrors = [];
page.on('pageerror', err => jsErrors.push(err.message));
page.on('console', msg => {
if (msg.type() === 'error') jsErrors.push(msg.text());
});
try {
// 1. 快捷方式 302 重定向
const resp = await page.goto(`${SITE}/?plugin=${id}`, { waitUntil: 'load', timeout: 60000 });
const chain = resp.request().redirectedFrom();
if (chain) {
const location = page.url();
const hasFullUrl = location.includes('blueprint-url=https');
results.checks.push({ test: '302 重定向', pass: true, detail: hasFullUrl ? '完整 URL' : '相对路径' });
if (!hasFullUrl) results.pass = false;
} else {
results.checks.push({ test: '302 重定向', pass: false, detail: '无重定向' });
results.pass = false;
}
// 2. 等待 Playground 加载(计时)
const tNavDone = Date.now();
results.timing.navigationMs = tNavDone - t0;
await page.waitForTimeout(35000);
const tPlaygroundDone = Date.now();
results.timing.playgroundLoadMs = tPlaygroundDone - t0;
// 3. 检查蓝图加载错误
const outerBody = await page.textContent('body').catch(() => '');
const blueprintError = outerBody.includes('蓝图加载失败') || outerBody.includes('Blueprint file');
results.checks.push({ test: '蓝图加载', pass: !blueprintError, detail: blueprintError ? '加载失败' : 'OK' });
if (blueprintError) {
results.pass = false;
await page.close();
return results;
}
// 4. 找到 WordPress iframe
let wpFrame = null;
for (const frame of page.frames()) {
const url = frame.url();
if (url.includes('scope:') || (url.includes('wp-admin') && !url.includes('playground.html'))) {
wpFrame = frame;
break;
}
}
if (!wpFrame) {
results.checks.push({ test: 'WordPress iframe', pass: false, detail: '未找到' });
results.pass = false;
await page.close();
return results;
}
results.checks.push({ test: 'WordPress iframe', pass: true, detail: wpFrame.url().substring(0, 80) });
// 5. iframe 内容检查
const wpBody = await wpFrame.textContent('body').catch(() => '');
// PHP 错误
const phpError = wpBody.includes('Fatal error') || wpBody.includes('Warning:') || wpBody.includes('Parse error');
results.checks.push({ test: '无 PHP 错误', pass: !phpError, detail: phpError ? '有错误' : 'OK' });
if (phpError) results.pass = false;
// 版本号 — 从 plugins.php 插件列表提取(最可靠来源)
const expectedVersion = plugin.version ? plugin.version.replace('v', '') : null;
if (expectedVersion) {
let versionFound = false;
let versionDetail = '未检测到';
try {
await wpFrame.evaluate(() => { window.location.href = '/wp-admin/plugins.php'; });
await wpFrame.waitForSelector('.wp-list-table', { timeout: 15000 });
const pluginRow = await wpFrame.evaluateHandle((pluginId) => {
for (const row of document.querySelectorAll('tr[data-slug]')) {
const cb = row.querySelector('input[type="checkbox"]');
if (cb && cb.value.startsWith(pluginId + '/')) return row;
}
return null;
}, id);
const isValid = await pluginRow.evaluate(el => el instanceof HTMLElement).catch(() => false);
if (isValid) {
const versionText = await pluginRow.evaluate(el => {
const ve = el.querySelector('.plugin-version-author-uri');
return ve ? ve.textContent : '';
});
const match = versionText.match(/([\d.]+)\s*版本/i) || versionText.match(/(?:Version)\s*([\d.]+)/i);
if (match) {
versionFound = match[1] === expectedVersion;
versionDetail = `plugins.php: ${match[1]}`;
} else {
versionDetail = 'plugins.php: 版本文本未匹配';
}
// 附赠:检查插件激活状态
const isActive = await pluginRow.evaluate(el => el.classList.contains('active'));
results.checks.push({ test: '插件激活', pass: isActive, detail: isActive ? '已激活' : '未激活' });
if (!isActive) results.pass = false;
} else {
versionDetail = 'plugins.php: 未找到插件行';
}
// 检查 wp-admin 通知栏是否有错误/警告
const adminNotices = await wpFrame.evaluate(() => {
const notices = [];
for (const el of document.querySelectorAll('.notice-error, .notice-warning, .error')) {
const text = el.textContent.trim();
if (text) notices.push(text.substring(0, 120));
}
return notices;
}).catch(() => []);
if (adminNotices.length > 0) {
results.checks.push({ test: '管理通知', pass: false, detail: adminNotices.join(' | ') });
results.pass = false;
} else {
results.checks.push({ test: '管理通知', pass: true, detail: '无错误/警告通知' });
}
} catch (_) {
// Fallback: 回退到 body 全文搜索
const wpBodyFallback = await wpFrame.textContent('body').catch(() => '');
versionFound = wpBodyFallback.includes(expectedVersion);
versionDetail = versionFound ? `fallback: ${expectedVersion}` : 'fallback: 未检测到';
}
results.checks.push({ test: `版本号 ${expectedVersion}`, pass: versionFound, detail: versionDetail });
if (!versionFound) results.pass = false;
}
// 中文界面
const isChinese = wpBody.includes('设置') || wpBody.includes('仪表盘') || wpBody.includes('插件') || wpBody.includes('文章');
results.checks.push({ test: '中文界面', pass: isChinese, detail: isChinese ? 'OK' : '未检测到中文' });
// 截图
if (doScreenshot) {
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
await page.screenshot({ path: path.join(SCREENSHOT_DIR, `${id}.png`), fullPage: true });
results.checks.push({ test: '截图', pass: true, detail: `${SCREENSHOT_DIR}/${id}.png` });
}
// JS 错误汇报
// 过滤掉 Playground 内部的已知噪音
const realErrors = jsErrors.filter(e =>
!e.includes('service-worker') &&
!e.includes('Failed to fetch') &&
!e.includes('net::ERR_')
);
if (realErrors.length > 0) {
results.checks.push({ test: 'JS 错误', pass: false, detail: realErrors.slice(0, 3).join(' | ') });
results.pass = false;
} else {
results.checks.push({ test: 'JS 错误', pass: true, detail: `无 (${jsErrors.length} 条已知噪音已过滤)` });
}
} catch (err) {
results.checks.push({ test: '运行异常', pass: false, detail: err.message.substring(0, 100) });
results.pass = false;
} finally {
await page.close();
}
return results;
}
function generateReport(allResults) {
const now = new Date();
const dateStr = now.toISOString().slice(0, 10);
const timeStr = now.toTimeString().slice(0, 5);
const passed = allResults.filter(r => r.pass).length;
const failed = allResults.filter(r => !r.pass).length;
const total = allResults.length;
let md = `# play.wenpai.net 验证报告\n\n`;
md += `**日期**: ${dateStr} ${timeStr} \n`;
md += `**总览**: ${passed} 通过 / ${failed} 失败 / ${total} 总计\n\n`;
md += `## 逐插件检查\n\n`;
for (const r of allResults) {
const icon = r.pass ? '✅' : '❌';
md += `### ${icon} ${r.name} (\`${r.id}\`)\n\n`;
for (const c of r.checks) {
md += `- [${c.pass ? 'x' : ' '}] **${c.test}**: ${c.detail}\n`;
}
md += `\n`;
}
if (failed > 0) {
md += `## 失败汇总\n\n`;
for (const r of allResults.filter(r => !r.pass)) {
const failedChecks = r.checks.filter(c => !c.pass).map(c => c.test).join(', ');
md += `- **${r.name}**: ${failedChecks}\n`;
}
md += `\n`;
}
return md;
}
(async () => {
const engines = { chromium, firefox, webkit };
const engine = engines[browserName];
if (!engine) {
console.error(`不支持的浏览器: ${browserName},可选: chromium, firefox, webkit`);
process.exit(1);
}
console.log(`浏览器: ${browserName}${doMobile ? ' (移动端)' : ''}`);
const launchOpts = { headless: true };
const browser = await engine.launch(launchOpts);
const plugins = doAll ? await getPlugins() : [pluginId];
const allResults = [];
for (const p of plugins) {
const plugin = typeof p === 'string' ? { id: p, name: p } : p;
process.stdout.write(`\n验证 ${plugin.name || plugin.id} ... `);
const result = await verifyPlugin(browser, plugin);
allResults.push(result);
console.log(result.pass ? 'PASS' : 'FAIL');
for (const c of result.checks) {
console.log(` [${c.pass ? '✓' : '✗'}] ${c.test}: ${c.detail}`);
}
if (result.timing.playgroundLoadMs) {
console.log(` [⏱] 加载耗时: ${(result.timing.playgroundLoadMs / 1000).toFixed(1)}s (导航 ${(result.timing.navigationMs / 1000).toFixed(1)}s)`);
}
}
await browser.close();
// 汇总
const passed = allResults.filter(r => r.pass).length;
const failed = allResults.filter(r => !r.pass).length;
console.log(`\n=== 汇总: ${passed} 通过, ${failed} 失败, 共 ${allResults.length} ===`);
if (doScreenshot) {
console.log(`截图目录: ${SCREENSHOT_DIR}`);
}
// 保存结果
if (doAll || doScreenshot || doReport || doForum) {
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
fs.writeFileSync(path.join(SCREENSHOT_DIR, 'results.json'), JSON.stringify(allResults, null, 2));
}
// 保存性能基线
if (doAll) {
const perfPath = path.join(SCREENSHOT_DIR, 'performance-baseline.json');
const perfData = allResults.map(r => ({
id: r.id, name: r.name, date: new Date().toISOString(),
navigationMs: r.timing.navigationMs || null,
playgroundLoadMs: r.timing.playgroundLoadMs || null
}));
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
fs.writeFileSync(perfPath, JSON.stringify(perfData, null, 2));
console.log(`性能基线: ${perfPath}`);
}
// 生成 markdown 报告
if (doReport || doForum) {
const report = generateReport(allResults);
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
const reportPath = path.join(SCREENSHOT_DIR, 'verify-report.md');
fs.writeFileSync(reportPath, report);
console.log(`报告: ${reportPath}`);
if (doForum) {
try {
const title = `play.wenpai.net 验证报告 ${new Date().toISOString().slice(0, 10)}`;
execSync(`forum-cli post --title "${title}" --body "${report.replace(/"/g, '\\"')}"`, { stdio: 'inherit' });
console.log('已发布到论坛');
} catch (e) {
console.error('论坛发布失败:', e.message);
}
}
}
process.exit(failed > 0 ? 1 : 0);
})();