Files
ewoooc/tests/test_frontend_v2_assets.py
OoO fbc85fcedc
All checks were successful
CD Pipeline / deploy (push) Successful in 2m11s
refactor(vendor): 抽出缺貨 API 查詢服務
2026-05-01 14:12:56 +08:00

289 lines
14 KiB
Python
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.
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def test_frontend_v2_shell_assets_exist_and_are_indexed():
assert (ROOT / "web/static/css/ewoooc-tokens.css").exists()
assert (ROOT / "web/static/css/ewoooc-shell.css").exists()
assert (ROOT / "templates/ewoooc_base.html").exists()
assert (ROOT / "templates/components/_ewoooc_shell.html").exists()
agents = (ROOT / "AGENTS.md").read_text(encoding="utf-8")
constitution = (ROOT / "CONSTITUTION.md").read_text(encoding="utf-8")
roadmap = (ROOT / "docs/guides/frontend_upgrade_roadmap.md").read_text(encoding="utf-8")
assert "docs/guides/frontend_upgrade_roadmap.md" in agents
assert "前端 V2 視覺基準" in constitution
assert "禁止用 mock data" in roadmap
def test_frontend_v2_shell_uses_real_runtime_context():
shell = (ROOT / "templates/components/_ewoooc_shell.html").read_text(encoding="utf-8")
base = (ROOT / "templates/ewoooc_base.html").read_text(encoding="utf-8")
assert "scheduler_stats" in shell
assert "session.get('username')" in shell
assert "next_run|default(None)" in shell
assert "components/_ewoooc_shell.html" in base
assert 'name="ui" value="v2"' not in base
forbidden_markers = [
"mockProducts",
"mockData",
"fake",
"假商品",
"假 KPI",
]
combined = shell + "\n" + base
assert all(marker not in combined for marker in forbidden_markers)
def test_dashboard_v2_is_production_default_and_uses_real_dashboard_data():
route_source = (ROOT / "routes/dashboard_routes.py").read_text(encoding="utf-8")
dashboard = (ROOT / "templates/dashboard_v2.html").read_text(encoding="utf-8")
assert "request.args.get('ui') == 'legacy'" in route_source
assert "template_name = 'dashboard.html' if request.args.get('ui') == 'legacy' else 'dashboard_v2.html'" in route_source
assert "get_full_dashboard_data()" in route_source
assert "MockRecord" not in route_source
assert "{% for item in items %}" in dashboard
assert "ui='v2'" not in dashboard
assert 'name="ui" value="v2"' not in dashboard
assert "mockProducts" not in dashboard
assert "假商品" not in dashboard
def test_dashboard_v2_restores_real_price_history_chart():
route_source = (ROOT / "routes/api_routes.py").read_text(encoding="utf-8")
dashboard = (ROOT / "templates/dashboard_v2.html").read_text(encoding="utf-8")
assert "@api_bp.route('/api/history/<int:product_id>')" in route_source
assert "PRICE_HISTORY_RANGES" in route_source
assert "'week': {'days': 7" in route_source
assert "'month': {'days': 30" in route_source
assert "'quarter': {'days': 90" in route_source
assert "'year': {'days': 365" in route_source
assert "session.query(PriceRecord)" in route_source
assert "PriceRecord.product_id == product.id" in route_source
assert "https://cdn.jsdelivr.net/npm/chart.js" in dashboard
assert 'id="historyModal"' in dashboard
assert 'id="priceChart"' in dashboard
assert "data-product-id=\"{{ product.id }}\"" in dashboard
assert "data-history-trigger" in dashboard
assert "onclick=\"event.stopPropagation(); showHistory(this.dataset.productId, this.dataset.productName);\"" in dashboard
assert "data-history-range=\"week\"" in dashboard
assert "data-history-range=\"month\"" in dashboard
assert "data-history-range=\"quarter\"" in dashboard
assert "data-history-range=\"year\"" in dashboard
assert "fetch(`/api/history/${productId}?range=${activeHistoryRange}&format=v2`)" in dashboard
assert "priceChartInstance = new Chart" in dashboard
assert "目前沒有可顯示的歷史價格紀錄" in dashboard
def test_dashboard_v2_shows_pchome_competitor_pricing_and_links():
route_source = (ROOT / "routes/dashboard_routes.py").read_text(encoding="utf-8")
api_source = (ROOT / "routes/api_routes.py").read_text(encoding="utf-8")
feeder_source = (ROOT / "services/competitor_price_feeder.py").read_text(encoding="utf-8")
scheduler_source = (ROOT / "scheduler.py").read_text(encoding="utf-8")
dashboard = (ROOT / "templates/dashboard_v2.html").read_text(encoding="utf-8")
migration = (ROOT / "migrations/022_competitor_price_history.sql").read_text(encoding="utf-8")
assert "_load_pchome_competitor_map(" in route_source
assert "FROM competitor_prices" in route_source
assert "competitor_product_id" in route_source
assert "https://24h.pchome.com.tw/prod/" in route_source
assert "_build_competitor_decision(" in route_source
assert "PChome 優勢" in route_source
assert "MOMO 威脅" in route_source
assert "item['pchome_competitor']" in route_source
assert "item['competitor_decision']" in route_source
assert "MOMO 價格" in dashboard
assert "PChome 價格" in dashboard
assert "競價判讀" in dashboard
assert "MOMO {{ product.i_code }}" in dashboard
assert "PChome {{ competitor.product_id }}" in dashboard
assert "competitor.product_url" in dashboard
assert "dashboard-competition-badge" in dashboard
assert "decision.summary" in dashboard
assert "PChome 待比對" in dashboard
assert "series.pchome" in dashboard
assert "label: 'PChome'" in dashboard
assert "含 PChome 歷史快照" in dashboard
assert "competitor_price_history" in migration
assert "momo_price" in migration
assert "competitor_product_id" in migration
assert "CREATE INDEX IF NOT EXISTS idx_comp_price_history_sku_source_time" in migration
assert "_ensure_competitor_price_history_table" in feeder_source
assert "INSERT INTO competitor_price_history" in feeder_source
assert "history_written" in feeder_source
assert "momo_price" in feeder_source
assert "competitor_price_history" in api_source
assert "'series': {" in api_source
assert "'pchome': competitor_data" in api_source
assert "history_written" in scheduler_source
def test_ai_product_pick_agent_uses_real_competitor_data_and_dashboard_action():
feeder_source = (ROOT / "services/competitor_price_feeder.py").read_text(encoding="utf-8")
agent_source = (ROOT / "services/ai_product_pick_agent.py").read_text(encoding="utf-8")
route_source = (ROOT / "routes/ai_routes.py").read_text(encoding="utf-8")
template = (ROOT / "templates/ai_intelligence.html").read_text(encoding="utf-8")
assert "MIN_MATCH_SCORE = 0.42" in feeder_source
assert "MAX_SEARCH_TERMS" in feeder_source
assert "_build_search_keywords" in feeder_source
assert "_search_pchome_candidates" in feeder_source
assert "crawler.search_products(keyword, limit=SEARCH_LIMIT)" in feeder_source
assert "_fetch_unmatched_priority_skus" in feeder_source
assert "run_unmatched_priority" in feeder_source
assert "generate_product_pick_list" in agent_source
assert "competitor_prices" in agent_source
assert "competitor_price_history" in agent_source
assert "daily_sales_snapshot" in agent_source
assert "ai_price_recommendations" in agent_source
assert "'product_pick'" in agent_source
assert "PChomeProductPickAgent" in agent_source
assert "PChome 價格優勢" in agent_source
assert "_daily_sales_columns" in agent_source
assert '"總業績"' in agent_source
assert "{date_col}::date" in agent_source
assert "conn.rollback()" in agent_source
assert "@ai_bp.route('/api/ai/product-picks/generate', methods=['POST'])" in route_source
assert "generate_product_pick_list(engine" in route_source
assert "@ai_bp.route('/api/ai/pchome-match/backfill', methods=['POST'])" in route_source
assert "run_unmatched_priority(limit=limit)" in route_source
assert "generate_product_pick_list(engine, limit=30)" in route_source
assert "完成後會重算 AI 挑品清單" in route_source
assert "match_rate" in route_source
assert "product_pick_count" in route_source
scheduler_source = (ROOT / "scheduler.py").read_text(encoding="utf-8")
run_scheduler_source = (ROOT / "run_scheduler.py").read_text(encoding="utf-8")
agent_actions_source = (ROOT / "services/agent_actions.py").read_text(encoding="utf-8")
assert "def run_pchome_match_backfill_task" in scheduler_source
assert "_save_stats('pchome_match_backfill'" in scheduler_source
assert "run_pchome_match_backfill_task" in run_scheduler_source
assert "每日 10:30pchome_match_backfill" in run_scheduler_source
assert '"run_pchome_match_backfill_task"' in agent_actions_source
assert "產生挑品清單" in template
assert "補抓待比對" in template
assert "generatePickList" in template
assert "backfillPchomeMatches" in template
assert "/api/ai/product-picks/generate" in template
assert "/api/ai/pchome-match/backfill" in template
assert "'product_pick':['bg-success'" in template
assert "kpiMatchRate" in template
def test_edm_dashboard_v2_is_production_default_and_uses_real_campaign_data():
route_source = (ROOT / "routes/edm_routes.py").read_text(encoding="utf-8")
template = (ROOT / "templates/edm_dashboard_v2.html").read_text(encoding="utf-8")
assert route_source.count("request.args.get('ui') == 'legacy'") == 5
assert "template_name = 'edm_dashboard.html' if request.args.get('ui') == 'legacy' else 'edm_dashboard_v2.html'" in route_source
assert "{% for slot, stats in slot_stats.items() %}" in template
assert "{% for item in items %}" in template
assert "scheduler_stats.get(task_key, [])" in template
assert "@api_bp.route('/api/history/i-code/<path:i_code>')" in (ROOT / "routes/api_routes.py").read_text(encoding="utf-8")
assert "data-campaign-filter=\"new\"" in template
assert "data-campaign-filter=\"up\"" in template
assert "data-campaign-filter=\"down\"" in template
assert "data-campaign-filter=\"delisted\"" in template
assert "data-campaign-history-trigger" in template
assert "showCampaignHistory(this.dataset.iCode, this.dataset.productName)" in template
assert "fetch(`/api/history/i-code/${encodeURIComponent(iCode)}?range=${activeCampaignHistoryRange}`)" in template
assert "data-campaign-history-range=\"week\"" in template
assert "data-campaign-history-range=\"month\"" in template
assert "data-campaign-history-range=\"quarter\"" in template
assert "data-campaign-history-range=\"year\"" in template
assert "applyCampaignFilter(card, button.dataset.campaignFilter)" in template
assert "?ui=v2" not in template
assert "ui='v2'" not in template
assert "mock" not in template.lower()
assert "假商品" not in template
def test_vendor_stockout_v2_is_production_default_and_uses_real_vendor_data():
route_source = (ROOT / "routes/vendor_routes.py").read_text(encoding="utf-8")
service_source = (ROOT / "services/vendor_stockout_query_service.py").read_text(encoding="utf-8")
template = (ROOT / "templates/vendor_stockout_index_v2.html").read_text(encoding="utf-8")
assert "request.args.get('ui') == 'legacy'" in route_source
assert not (ROOT / "web/templates/vendor_stockout_index_v2.html").exists()
assert "vendor_stockout_index_v2.html" in route_source
assert "get_vendor_dashboard_stats(vendor_db)" in route_source
assert "def get_vendor_dashboard_stats(vendor_db)" in service_source
assert "session.query(VendorStockout).count()" in service_source
assert "EmailSendLog" in service_source
assert "stats.pending_stockouts" in template
assert "stats.email_success_rate" in template
assert "ui='v2'" not in template
assert "mock" not in template.lower()
assert "" not in template
def test_vendor_stockout_list_v2_is_production_default_and_uses_real_stockout_rows():
route_source = (ROOT / "routes/vendor_routes.py").read_text(encoding="utf-8")
service_source = (ROOT / "services/vendor_stockout_query_service.py").read_text(encoding="utf-8")
template = (ROOT / "templates/vendor_stockout_list_v2.html").read_text(encoding="utf-8")
assert "vendor_stockout_list_v2.html" in route_source
assert "get_vendor_stockout_list_context(" in route_source
assert "request.args.get('page', 1, type=int)" in route_source
assert "def get_vendor_stockout_list_context(" in service_source
assert "_apply_stockout_filters(" in service_source
assert "session.query(VendorStockout)" in service_source
assert "VendorStockout.status == 'pending'" in service_source
assert "VendorStockout.batch_id == batch_id" in service_source
assert "from flask" not in service_source
assert "{% for record in records %}" in template
assert "{% for batch in batches %}" in template
assert "stats.pending" in template
assert "record.product_code" in template
assert "record.product_name" in template
assert "record.vendor_name" in template
assert "ui='v2'" not in template
assert "mock" not in template.lower()
assert "" not in template
def test_vendor_stockout_api_queries_are_extracted_to_service():
route_source = (ROOT / "routes/vendor_routes.py").read_text(encoding="utf-8")
service_source = (ROOT / "services/vendor_stockout_query_service.py").read_text(encoding="utf-8")
api_section = route_source.split("def api_get_stockout_list():", 1)[1].split("@vendor_bp.route('/api/stockout/<int:stockout_id>'", 1)[0]
assert "get_stockout_api_list_payload(" in api_section
assert "get_stockout_batches_payload(vendor_db)" in api_section
assert "session.query(VendorStockout)" not in api_section
assert "def get_stockout_api_list_payload(" in service_source
assert "def get_stockout_batches_payload(vendor_db)" in service_source
assert "'batch_number': record.batch_id" in service_source
assert "'send_status': record.status or 'pending'" in service_source
assert "'latest_date': batch.latest_date.isoformat()" in service_source
assert "from flask" not in service_source
def test_vendor_stockout_import_v2_is_feature_flagged_and_does_not_ship_sample_rows():
route_source = (ROOT / "routes/vendor_routes.py").read_text(encoding="utf-8")
template = (ROOT / "templates/vendor_stockout_import_v2.html").read_text(encoding="utf-8")
template_function = route_source.split("def api_import_template():", 1)[1].split("@vendor_bp.route('/api/stockout/list'", 1)[0]
assert "vendor_stockout_import_v2.html" in route_source
assert "template_columns = [" in template_function
assert "pd.DataFrame(columns=template_columns)" in template_function
assert "fetchWithCSRF('/vendor-stockout/api/import/excel'" in template
assert "vendor_stockout" in template
assert "範例" not in template_function
assert "ui='v2'" not in template
assert "mock" not in template.lower()
assert "" not in template