from pathlib import Path ROOT = Path(__file__).resolve().parents[1] def test_daily_sales_canvas_is_primary_and_fallback_is_opt_in(): css = (ROOT / "web/static/css/page-daily-sales.css").read_text(encoding="utf-8") script = (ROOT / "web/static/js/page-daily-sales.js").read_text(encoding="utf-8") assert ".chart-container.has-html-chart canvas" not in css assert ".chart-container.chart-fallback-active canvas" in css assert ".chart-container.chart-empty-active canvas" in css assert ".chart-container:not(.chart-fallback-active) .chart-fallback-list" in css assert "node.content && node.content.textContent" in script assert "chart-empty-active" in script assert "Math.abs(Number(value)) > 1e-9" in script render_body = script.split("function renderAllCharts()", 1)[1].split("function bootCharts()", 1)[0] assert "renderHtmlChartFallbacks();" not in render_body assert "catch(error =>" in script assert "renderHtmlChartFallbacks();" in script.split("catch(error =>", 1)[1] def test_growth_analysis_canvas_is_primary_and_fallback_is_opt_in(): css = (ROOT / "web/static/css/page-growth-bem.css").read_text(encoding="utf-8") script = (ROOT / "web/static/js/page-growth.js").read_text(encoding="utf-8") assert ".ga-chart-card__body.has-html-chart canvas" not in css assert ".ga-chart-card__body.chart-fallback-active canvas" in css assert ".ga-chart-card__body.chart-empty-active canvas" in css assert ".ga-chart-card__body:not(.chart-fallback-active) .ga-chart-snapshot" in css assert "node.content && node.content.textContent" in script assert "chart-empty-active" in script assert "Math.abs(Number(value)) > 1e-9" in script render_body = script.split("function renderCharts()", 1)[1].split("function bootCharts()", 1)[0] assert "renderHtmlChartFallbacks();" not in render_body assert "catch(error =>" in script assert "renderHtmlChartFallbacks();" in script.split("catch(error =>", 1)[1] def test_chart_theme_has_cdn_timeout_and_fallback_sources(): script = (ROOT / "web/static/js" / "analysis-chart-theme.js").read_text(encoding="utf-8") assert "CHART_LOAD_TIMEOUT_MS" in script assert "Chart.js 載入逾時" in script 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(): script = (ROOT / "scripts" / "check_sales_charts_runtime.js").read_text(encoding="utf-8") assert "nonZeroPoints" in script 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