Files
smart-fin-chart/test/regress.mjs
T

114 lines
5.0 KiB
JavaScript
Raw 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.
import * as echarts from 'echarts';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseCSV } from '../js/csv.js';
import { getChartBuilder } from '../js/charts/index.js';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const manifest = JSON.parse(fs.readFileSync(path.join(ROOT, 'manifest.json'), 'utf8'));
const STYLE_RULES = {
legendTop: 85,
gridTop: 145,
lineSeries: { symbolSize: 9, borderWidth: 2.5 },
};
function entriesFromManifest(value) {
const entries = [];
value.companies.forEach((company) => {
if (!company.chartCatalog || !Array.isArray(company.tabs)) {
throw new Error(`公司 ${company.id || '(空)'} 缺少 chartCatalog 或 tabs`);
}
const chartIds = Object.keys(company.chartCatalog);
company.tabs.forEach((tab) => {
if (!Array.isArray(tab.chartRefs)) throw new Error(`栏目 ${tab.id} 缺少 chartRefs`);
const refs = new Set();
tab.chartRefs.forEach((chartId) => {
if (!company.chartCatalog[chartId]) throw new Error(`栏目 ${tab.id} 引用了不存在的图表:${chartId}`);
if (refs.has(chartId)) throw new Error(`栏目 ${tab.id} 重复引用图表:${chartId}`);
refs.add(chartId);
});
});
chartIds.forEach((id) => {
const chart = company.chartCatalog[id];
if (!getChartBuilder(chart.key)) throw new Error(`manifest 图表 ${chart.id} 引用了未注册 key:${chart.key}`);
if (Boolean(chart.csv) === Boolean(chart.views)) {
throw new Error(`manifest 图表 ${chart.id} 必须声明 csv 或 views 之一`);
}
if (chart.views && (!chart.defaultView || !chart.views[chart.defaultView])) {
throw new Error(`manifest 图表 ${chart.id} 的 defaultView 无效`);
}
const sources = chart.views
? Object.entries(chart.views).map(([view, csv]) => ({ view, csv }))
: [{ view: null, csv: chart.csv }];
if (!sources.every(({ csv }) => typeof csv === 'string' && csv.length > 0)) {
throw new Error(`manifest 图表 ${chart.id} 缺少 CSV`);
}
sources.forEach(({ view, csv }) => entries.push({ ...chart, id, company, view, csv }));
});
});
return entries;
}
const all = entriesFromManifest(manifest);
const only = process.argv.slice(2);
const targets = only.length === 0
? all
: all.filter((entry) => only.includes(entry.id) || only.includes(entry.key));
const unknown = only.filter((target) => !all.some((entry) => entry.id === target || entry.key === target));
if (unknown.length) console.log(`⚠ 未找到图表:${unknown.join(', ')}`);
let ok = 0;
let fail = 0;
const styleWarnings = [];
for (const entry of targets) {
const label = entry.view ? `${entry.id}:${entry.view}` : entry.id;
try {
const csvPath = path.resolve(ROOT, entry.csv);
if (!csvPath.startsWith(`${ROOT}${path.sep}`)) throw new Error(`CSV 路径越过项目根目录:${entry.csv}`);
const rows = parseCSV(fs.readFileSync(csvPath, 'utf8'));
const chart = echarts.init(null, null, { renderer: 'svg', ssr: true, width: 1100, height: 720 });
const option = getChartBuilder(entry.key)(rows, { width: 1100, height: 720, view: entry.view || undefined });
chart.setOption(option);
const svg = chart.renderToSVGString();
chart.dispose();
const badAttr = /style="[^"]*"[^"=>\s][^>]*>/.test(svg.slice(0, 3000));
if (option.legend?.top !== undefined && option.legend.top !== STYLE_RULES.legendTop) {
styleWarnings.push(`${label}: legend.top=${option.legend.top} (期望 ${STYLE_RULES.legendTop})`);
}
if (option.grid?.top !== undefined && option.legend?.top !== undefined && option.grid.top !== STYLE_RULES.gridTop) {
styleWarnings.push(`${label}: grid.top=${option.grid.top} (期望 ${STYLE_RULES.gridTop})`);
}
const circles = [...svg.matchAll(/<path d="M1 0A1[^"]*" transform="matrix\(([\d.]+),0,0,[\d.]+,[^)]+\)" fill="#fff" stroke="[^"]*" stroke-width="([\d.]+)"/g)];
if (circles.length > 0) {
const scale = +circles[0][1];
const strokeWidth = +circles[0][2];
if (Math.abs(scale * 2 - STYLE_RULES.lineSeries.symbolSize) > 0.1) {
styleWarnings.push(`${label}: 折线 symbolSize=${(scale * 2).toFixed(1)} (期望 ${STYLE_RULES.lineSeries.symbolSize})`);
}
if (Math.abs(strokeWidth * scale - STYLE_RULES.lineSeries.borderWidth) > 0.1) {
styleWarnings.push(`${label}: 折线 borderWidth=${(strokeWidth * scale).toFixed(1)} (期望 ${STYLE_RULES.lineSeries.borderWidth})`);
}
}
console.log(`${badAttr ? '⚠' : '✓'} ${label.padEnd(30)} ${String(svg.length).padStart(6)}B`);
ok += 1;
} catch (error) {
console.log(`✗ ${label.padEnd(30)} ${error.message}`);
fail += 1;
}
}
console.log(`\n${ok} 张成功 / ${fail} 张失败`);
if (styleWarnings.length) {
console.log(`\n⚠ ${styleWarnings.length} 处样式偏离规范:`);
styleWarnings.forEach((warning) => console.log(` ${warning}`));
} else {
console.log('✓ 样式规范全部符合');
}
process.exit(fail > 0 || unknown.length > 0 ? 1 : 0);