fix(analytics): enforce trustworthy chart rendering
Some checks failed
CD Pipeline / deploy (push) Has been cancelled

This commit is contained in:
ogt
2026-07-16 15:55:22 +08:00
parent e23d1c0266
commit 911393190d
18 changed files with 1230 additions and 155 deletions

View File

@@ -46,6 +46,11 @@ def test_chart_theme_has_cdn_timeout_and_fallback_sources():
assert "cdn.jsdelivr.net/npm/chart.js@4.4.6" in script
assert "unpkg.com/chart.js@4.4.6" in script
assert "cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.6" in script
assert "!Array.isArray(ds.backgroundColor)" in script
assert "!Array.isArray(ds.borderColor)" in script
assert script.index("...originalScale") < script.index("border: { color: theme.faint")
assert "callback: v => formatNumber(v)" not in script
assert "autoSkip: true" in script
def test_sales_chart_runtime_guard_rejects_axis_only_canvases():
@@ -55,3 +60,68 @@ def test_sales_chart_runtime_guard_rejects_axis_only_canvases():
assert "colorPixels" in script
assert "has no colored chart marks" in script
assert "has no non-zero dataset values" in script
def test_all_analysis_pages_have_runtime_chart_integrity_contracts():
script = (ROOT / "scripts" / "check_analysis_chart_integrity.js").read_text(encoding="utf-8")
for route in (
"/daily_sales?month=2026-04",
"/growth_analysis?start_month=2026-04&end_month=2026-04",
"/sales_analysis?start_date=2026-04-01&end_date=2026-04-30",
"/monthly_summary_analysis?start_month=2026-04&end_month=2026-04",
):
assert route in script
for viewport in ("1440, height: 1000", "1024, height: 900", "390, height: 844"):
assert viewport in script
assert "loading overlay remains visible" in script
assert "has no colored chart marks" in script
assert "extreme percent values are not bounded with traceable markers" in script
assert "single point is not visibly emphasized" in script
assert "hides drawable payload behind an empty state" in script
assert "analysis tab lost the active period" in script
assert "rendered numeric indexes instead of product labels" in script
assert "contains labels outside the selected April period" in script
assert "explicitEmpty" in script
def test_analysis_chart_failure_states_are_explicit_and_bounded():
sales = (ROOT / "web/static/js/page-sales-analysis.js").read_text(encoding="utf-8")
daily = (ROOT / "web/static/js/page-daily-sales.js").read_text(encoding="utf-8")
growth = (ROOT / "web/static/js/page-growth.js").read_text(encoding="utf-8")
monthly = (ROOT / "web/static/js/page-monthly-summary.js").read_text(encoding="utf-8")
assert "document.documentElement.dataset.salesCharts = 'ready'" in sales
assert "series.chart_values" in sales
assert "renderChartEmpty(bcgEl" in sales
assert "renderChartEmpty(hmEl" in sales
assert "originalValue" in daily
assert "clippedCount" in daily
assert "function dateAxis(element)" in daily
assert "overlapping date ticks" in (ROOT / "scripts/check_analysis_chart_integrity.js").read_text(encoding="utf-8")
assert "beginAtZero: true" in growth
assert "pointRadiusFor" in growth
assert "REQUEST_TIMEOUT_MS" in monthly
assert "activeFetchController.abort()" in monthly
assert "const refreshForViewport = () => {" in monthly
assert "const refreshForViewport = () => fetchData()" not in monthly
assert "dataset.monthlyCharts = 'ready'" in monthly
assert "safeHtmlText(p.value[3])" in monthly
assert "safeHtmlText(ps[0].name)" in monthly
def test_analysis_tables_use_local_traditional_chinese_language_contract():
helper = (ROOT / "web/static/js/datatables-zh-hant.js").read_text(encoding="utf-8")
assert "window.EwoooCDataTableLanguage" in helper
assert "沒有符合條件的資料" in helper
assets = [
ROOT / "web/static/js/page-daily-sales.js",
ROOT / "web/static/js/page-sales-analysis.js",
ROOT / "web/static/js/page-monthly-summary.js",
ROOT / "templates/abc_analysis_detail.html",
]
for asset in assets:
source = asset.read_text(encoding="utf-8")
assert "cdn.datatables.net/plug-ins" not in source
assert "EwoooCDataTableLanguage()" in source

View File

@@ -0,0 +1,37 @@
import pandas as pd
from routes.sales_routes import _build_top_product_chart_data
def test_top_product_chart_aggregates_sku_rows_before_ranking():
rows = pd.DataFrame(
[
{'pid': 'P1', 'name': '同名商品', 'amount': 100},
{'pid': 'P1', 'name': '同名商品', 'amount': 50},
{'pid': 'P2', 'name': '同名商品', 'amount': 90},
{'pid': 'P3', 'name': '獨立商品', 'amount': 200},
{'pid': 'P4', 'name': '零業績商品', 'amount': 0},
]
)
result = _build_top_product_chart_data(rows, 'pid', 'name', 'amount', 'amount')
assert result['chart_values'] == [200.0, 150.0, 90.0]
assert result['labels'] == ['獨立商品', '同名商品 [P1]', '同名商品 [P2]']
assert result['metric_label'] == '銷售金額 ($)'
def test_top_product_chart_returns_explicit_empty_contract():
result = _build_top_product_chart_data(
pd.DataFrame(columns=['name', 'amount']),
None,
'name',
'amount',
'qty',
)
assert result == {
'labels': [],
'chart_values': [],
'metric_label': '銷售數量',
}