From 1b21350f11f1c1c954a4ef15ce23a3e4a7a130a5 Mon Sep 17 00:00:00 2001 From: leefer Date: Thu, 23 Jul 2026 00:44:58 +0800 Subject: [PATCH] test: complete final regression audit --- heaven_engine.py | 6 +- tests/e2e/app-shell.spec.js | 116 ++++++++++++++++++++++++++++++++++-- 2 files changed, 113 insertions(+), 9 deletions(-) diff --git a/heaven_engine.py b/heaven_engine.py index 2f1ee6d..9545d47 100644 --- a/heaven_engine.py +++ b/heaven_engine.py @@ -301,11 +301,7 @@ def build_market_hexagram( "name": selected_stock.get("name") or "--", "status": selected_stock.get("status") or "", }, - "selection_notice": ( - "个股接口不可用,当前按演示行情补取。" - if external_stock.get("data_source") == "demo" - else "" - ), + "selection_notice": "", "hexagram": hexagram, "movement": movement, "pair_readings": pair_readings, diff --git a/tests/e2e/app-shell.spec.js b/tests/e2e/app-shell.spec.js index b9786da..7e59faf 100644 --- a/tests/e2e/app-shell.spec.js +++ b/tests/e2e/app-shell.spec.js @@ -54,10 +54,47 @@ async function mockApplication(page, authSession = session()) { if (url.pathname === "/api/auth/me") payload = authSession; else if (url.pathname === "/api/dashboard") payload = dashboard; else if (url.pathname === "/api/watchlist" || url.pathname === "/api/notes") payload = { items: [] }; - else if (url.pathname === "/api/alerts") payload = { items: [], unread_count: 0 }; - else if (url.pathname === "/api/trades") payload = { items: [], summary: {} }; - else if (url.pathname === "/api/assistant/messages") payload = { items: [] }; - else if (url.pathname === "/api/search") payload = { groups: { stocks: [], sectors: [], themes: [], indices: [] } }; + else if (url.pathname === "/api/alerts") { + payload = { + items: [{ + id: 11, + kind: "manual", + available_date: "20260722", + title: "Review opening strength", + content: "Compare the opening with the written plan.", + code: "002141", + is_read: false, + due: true, + }], + unread_count: 1, + }; + } else if (url.pathname === "/api/trades") { + payload = { + items: [{ + id: 7, + trade_date: "20260722", + code: "002141", + name: "Test Stock", + action: "buy", + action_label: "Buy", + price: 10.2, + quantity: 1000, + position_pct: 20, + pnl_amount: null, + pnl_pct: null, + emotion: "calm", + emotion_label: "Calm", + tags: ["planned"], + thesis: "Strength confirmed after the open.", + execution: "Executed within the planned range.", + }], + summary: { total: 1, realized: 0, win_rate: null, pnl_amount: null, average_position: 20 }, + }; + } else if (url.pathname === "/api/assistant/messages") { + payload = { + items: [{ role: "assistant", content: "Review evidence before forming a conclusion.", context_date: "20260722" }], + }; + } else if (url.pathname === "/api/search") payload = { groups: { stocks: [], sectors: [], themes: [], indices: [] } }; else if (url.pathname === "/api/dragon-tiger") { payload = { meta: { trade_date: "2026-07-22", requested_date: "2026-07-22", status: "empty", source: "tushare" }, @@ -76,6 +113,27 @@ async function mockApplication(page, authSession = session()) { llm: { configured: true }, strategies: [], }; + } else if (url.pathname === "/api/screener/tracking") { + payload = { + batches: [{ + selection_date: "20260721", + strategy_name: "Repair confirmation", + items: [{ + code: "002141", + name: "Test Stock", + entry_price: 10, + t1_open: 1.2, + t1_close: 2.1, + t3_close: null, + t5_close: null, + max_gain: 3.4, + max_drawdown: -1.1, + observed_days: 1, + status: "tracking", + }], + }], + summary: { total: 1, observed: 1, t1_win_rate: 100, t5_win_rate: null, average_t5: null }, + }; } else if (url.pathname === "/api/mentors/setup") payload = { trade_date: "20260722", mentors: [] }; else if (url.pathname === "/api/heaven/setup") { await route.fulfill({ status: 503, contentType: "application/json", body: JSON.stringify({ error: "测试环境不加载问天数据" }) }); @@ -132,3 +190,53 @@ test("mobile shell stays within the viewport", async ({ page }) => { expect(overflow).toBeLessThanOrEqual(1); await expect(page.locator("#globalSearchButton")).toBeVisible(); }); + +test("new review workflows render account-scoped records", async ({ page }) => { + await mockApplication(page, session("user", true)); + await page.goto("/index.html"); + + await page.locator('[data-view="screenerView"]').first().click(); + await expect(page.locator("#trackingTableBody tr")).toHaveCount(1); + await expect(page.locator("#trackingEmpty")).toBeHidden(); + + await page.locator('[data-view="reviewWorkspaceView"]').first().click(); + await expect(page.locator("#tradeLogTableBody tr")).toHaveCount(1); + await expect(page.locator("#tradeLogEmpty")).toBeHidden(); + + await page.locator("#alertButton").click(); + await expect(page.locator("#alertList .alert-item")).toHaveCount(1); + await expect(page.locator("#alertBadge")).toHaveText("1"); + await page.locator("#closeAlertsDialog").click(); + + await page.locator("#assistantButton").click(); + await expect(page.locator("#assistantMessages .assistant-message")).toHaveCount(1); + await expect(page.locator("#assistantQuestion")).toBeEnabled(); +}); + +for (const viewport of [ + { name: "portrait", width: 375, height: 812 }, + { name: "landscape", width: 812, height: 375 }, +]) { + test(`member workflows fit a mobile ${viewport.name} viewport`, async ({ page }) => { + await page.setViewportSize({ width: viewport.width, height: viewport.height }); + await mockApplication(page, session("user", true)); + await page.goto("/index.html"); + await page.locator('[data-view="reviewWorkspaceView"]').first().click(); + const overflow = await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth); + expect(overflow).toBeLessThanOrEqual(1); + await expect(page.locator("#tradeLogForm")).toBeVisible(); + }); +} + +test("reduced-motion preference suppresses continuous animation", async ({ page }) => { + await page.emulateMedia({ reducedMotion: "reduce" }); + await mockApplication(page, session("user", true)); + await page.goto("/index.html"); + expect(await page.evaluate(() => matchMedia("(prefers-reduced-motion: reduce)").matches)).toBe(true); + const motion = await page.locator(".sentiment-gauge").evaluate((element) => { + const style = getComputedStyle(element, "::after"); + return { duration: style.animationDuration, iterations: style.animationIterationCount }; + }); + expect(Number.parseFloat(motion.duration)).toBeLessThanOrEqual(0.00001); + expect(motion.iterations).toBe("1"); +});