const { test, expect } = require("@playwright/test"); const dashboard = { meta: { trade_date: "2026-07-22", requested_date: "2026-07-22", source: "tushare", realtime: false, cached: true, market_status: "closed", updated_at: "2026-07-22T15:00:00+08:00", }, overview: { up_count: 2100, down_count: 2800, limit_up_count: 42, limit_down_count: 8, broken_count: 17, seal_rate: 71.2, amount_billion: 12600, sentiment_score: 48, }, limits: [], broken: [], down_limits: [], yesterday_limits: [], limit_performance: [], ladders: [], sectors: [], sector_rotation: [], }; function session(role = "admin", subscribed = true) { return { authenticated: true, csrf_token: "test-csrf", user: { id: role === "admin" ? 1 : 2, username: role === "admin" ? "admin_user" : "normal_user", role, membership: { active: role === "admin" || subscribed, subscribed, is_admin: role === "admin", }, }, }; } async function mockApplication(page, authSession = session()) { await page.route("**/api/**", async (route) => { const url = new URL(route.request().url()); let payload = { ok: true }; 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/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" }, summary: {}, traders: [], unclassified_seats: [], }; } else if (url.pathname === "/api/sentiment/history") payload = { rows: [], components: [] }; else if (url.pathname === "/api/rotation/history") payload = { days: [], rows: [], sectors: [] }; else if (url.pathname === "/api/screener/setup") { payload = { trade_date: "20260722", regime: { id: "repair", label: "修复", confidence: 70, reason: "测试", evidence: [] }, regimes: [{ id: "repair", label: "修复" }], factor_data: { ready: false, date_count: 0 }, llm: { configured: true }, strategies: [], }; } 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: "测试环境不加载问天数据" }) }); return; } await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(payload) }); }); } test("admin shell opens every primary workspace and global search", async ({ page }) => { await mockApplication(page); await page.goto("/index.html"); await expect(page.locator("#authGate")).toBeHidden(); await expect(page.locator("#settingsButton")).toBeVisible(); await expect(page.locator("#syncButton")).toBeVisible(); await page.locator("#alertButton").click(); await expect(page.locator("#alertsDialog")).toBeVisible(); await page.locator("#closeAlertsDialog").click(); const views = [ "sentimentCycleView", "limitPool", "brokenView", "downView", "yesterdayView", "performanceView", "ladderView", "rotationView", "dragonView", "screenerView", "mentorView", "heavenView", "reviewWorkspaceView", ]; for (const view of views) { await page.locator(`[data-view="${view}"]`).first().click(); await expect(page.locator(`#${view}`)).toHaveClass(/active-view/); } await page.keyboard.press("Control+K"); await expect(page.locator("#globalSearchDialog")).toBeVisible(); await expect(page.locator("#globalSearchInput")).toBeFocused(); }); test("regular account cannot see admin controls and member features are gated", async ({ page }) => { await mockApplication(page, session("user", false)); await page.goto("/index.html"); await expect(page.locator("#settingsButton")).toBeHidden(); await expect(page.locator("#syncButton")).toBeHidden(); await expect(page.locator("#accountVipLabel")).toHaveText("非会员"); await page.locator('[data-view="screenerView"]').first().click(); await expect(page.locator("#screenerView .member-gate")).toBeVisible(); await expect(page.locator("#screenerRunButton")).toBeDisabled(); }); test("mobile shell stays within the viewport", async ({ page }) => { await page.setViewportSize({ width: 375, height: 812 }); await mockApplication(page, session("user", true)); await page.goto("/index.html"); const overflow = await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth); expect(overflow).toBeLessThanOrEqual(1); await expect(page.locator("#globalSearchButton")).toBeVisible(); });