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",
      },
    },
  };
}

function mentorDirectory(role = "admin") {
  const mentors = [
    {
      id: "source-a",
      name: "原帖老师",
      description: "依据长期实盘原帖提炼",
      tagline: "先看周期，再看机会。",
      focus: ["情绪周期", "仓位纪律"],
      evidence: { grade: "A", label: "实盘原帖", note: "长期原始实盘记录" },
      quality: { score: 6, total: 6, status: "pass" },
      private: false,
    },
    {
      id: "source-b",
      name: "多源老师",
      description: "依据公开访谈与多源资料整理",
      tagline: "确认之后再行动。",
      focus: ["主线确认", "风险管理"],
      evidence: { grade: "B", label: "多源整理", note: "公开访谈与多源材料" },
      quality: { score: 6, total: 6, status: "pass" },
      private: false,
    },
    {
      id: "source-c",
      name: "推演老师",
      description: "公开语录较少，以行为推演为主",
      tagline: "只讨论可验证的行为。",
      focus: ["行为推演", "诚实边界"],
      evidence: { grade: "C", label: "行为推演", note: "公开语录较少" },
      quality: { score: 5, total: 6, status: "conditional" },
      private: false,
    },
  ];
  for (let index = 1; index <= 18; index += 1) {
    const grade = ["A", "B", "C"][(index - 1) % 3];
    mentors.push({
      id: `extra-${index}`,
      name: `扩展模型${String(index).padStart(2, "0")}`,
      description: `用于验证完整目录密度的${grade}级思维模型`,
      tagline: "保持证据边界。",
      focus: ["市场结构", "条件预案"],
      evidence: { grade, label: { A: "原始语料", B: "多源整理", C: "行为材料" }[grade], note: `${grade}级测试素材` },
      quality: { score: 6, total: 6, status: "pass" },
      private: false,
    });
  }
  if (role === "admin") mentors.unshift({
    id: "private-owner",
    name: "私有老师",
    description: "依据个人复盘记录提炼",
    tagline: "只对自己开放。",
    focus: ["个人复盘", "交易纪律"],
    evidence: { grade: "A", label: "私有原始语料", note: "仅限管理员本人使用" },
    quality: { score: null, total: null, status: "private" },
    private: true,
  });
  return mentors;
}

async function mockApplication(page, authSession = session(), options = {}) {
  let screenerTracking = {
    batches: [{
      run_id: 44,
      selection_date: "20260721",
      strategy_name: "Repair confirmation",
      items: [{
        id: 9,
        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 },
  };
  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") {
      options.dashboardRequests = (options.dashboardRequests || 0) + 1;
      options.dashboardTradeDates ||= [];
      options.dashboardTradeDates.push(url.searchParams.get("trade_date"));
      if (options.dashboardDelay) {
        await new Promise((resolve) => setTimeout(resolve, options.dashboardDelay));
      }
      if (options.echoDashboardDate) {
        const requestedDate = url.searchParams.get("trade_date");
        payload = { ...dashboard, meta: { ...dashboard.meta, trade_date: requestedDate, requested_date: requestedDate } };
      } else payload = dashboard;
    }
    else if (url.pathname === "/api/stock/002141/preview") {
      if (options.previewDelay) {
        await new Promise((resolve) => setTimeout(resolve, options.previewDelay));
      }
      payload = {
        meta: { trade_date: "2026-07-23", intraday_trade_date: "2026-07-24", realtime: true, intraday_notice: "" },
        stock: { code: "002141", name: "Test Stock", industry: "Test Sector", price: 10.8, change: 2.4 },
        prices: [
          { trade_date: "2026-07-22", open: 10, high: 10.5, low: 9.9, close: 10.2, volume: 1000 },
          { trade_date: "2026-07-23", open: 10.3, high: 10.9, low: 10.2, close: 10.8, volume: 1200 },
        ],
        intraday: [
          { date: "2026-07-24", time: "09:30", open: 10.20, high: 10.24, low: 10.18, close: 10.22, volume: 100, average: 10.22 },
          { date: "2026-07-24", time: "09:31", open: 10.22, high: 10.30, low: 10.21, close: 10.28, volume: 130, average: 10.25 },
          { date: "2026-07-24", time: "09:32", open: 10.28, high: 10.29, low: 10.20, close: 10.23, volume: 90, average: 10.24 },
          { date: "2026-07-24", time: "09:33", open: 10.23, high: 10.34, low: 10.22, close: 10.32, volume: 160, average: 10.27 },
          { date: "2026-07-24", time: "09:34", open: 10.32, high: 10.36, low: 10.29, close: 10.34, volume: 120, average: 10.28 },
        ],
      };
    } else if (url.pathname === "/api/stock/002141") {
      payload = {
        meta: { trade_date: "2026-07-23", realtime: false },
        stock: { code: "002141", name: "Test Stock", industry: "Test Sector", price: 10.8, change: 2.4 },
        prices: [
          { trade_date: "2026-07-22", open: 10, high: 10.5, low: 9.9, close: 10.2, volume: 1000 },
          { trade_date: "2026-07-23", open: 10.3, high: 10.9, low: 10.2, close: 10.8, volume: 1200 },
        ],
        moneyflow: {},
        notes: [],
      };
    } else if (url.pathname === "/api/search/detail") {
      const theme = url.searchParams.get("type") === "theme";
      payload = theme ? {
        meta: { trade_date: "2026-07-23", realtime: false },
        entity: { id: "885728.TI", code: "885728.TI", name: "人工智能", type: "theme", type_label: "题材", value: 1280, change: 2.2 },
        series: [
          { trade_date: "2026-07-22", open: 1220, high: 1260, low: 1210, close: 1250, volume: 1000 },
          { trade_date: "2026-07-23", open: 1255, high: 1290, low: 1248, close: 1280, volume: 1200 },
        ],
        metrics: [],
      } : {
        meta: { trade_date: "2026-07-23", realtime: false },
        entity: { id: "000001.SH", code: "000001.SH", name: "上证指数", type: "index", type_label: "指数", value: 3800, change: 0.5 },
        series: [
          { trade_date: "2026-07-22", open: 3750, high: 3790, low: 3740, close: 3780, volume: 1000 },
          { trade_date: "2026-07-23", open: 3782, high: 3810, low: 3770, close: 3800, volume: 1200 },
        ],
        metrics: [],
      };
    } else if (url.pathname === "/api/chart/intraday") {
      payload = {
        meta: { trade_date: "2026-07-24", previous_close: 10.1 },
        entity: { id: url.searchParams.get("id"), type: url.searchParams.get("type") },
        points: [
          { date: "2026-07-24", time: "09:30", open: 10.10, high: 10.18, low: 10.08, close: 10.15, volume: 100, average: 10.15 },
          { date: "2026-07-24", time: "09:31", open: 10.15, high: 10.24, low: 10.14, close: 10.22, volume: 130, average: 10.18 },
          { date: "2026-07-24", time: "09:32", open: 10.22, high: 10.23, low: 10.16, close: 10.18, volume: 90, average: 10.18 },
        ],
      };
    } else if (url.pathname === "/api/watchlist") {
      payload = { items: [{
        code: "000002", name: "Watch Stock", sector: "Bank", color: "red",
        change: 1.86, return_5d: 8.92, attention_score: 72.4,
        remark: "观察承接，不追高", market_date: "20260722",
      }] };
    } else if (url.pathname === "/api/notes") {
      payload = { items: [{
        id: 12, code: "", stock_name: "", trade_date: "20260722",
        summary: "缩量修复，主线仍待确认", content: "做对了等待确认。", plan: "只做有承接的核心。",
      }] };
    } 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: [{ id: "002141", code: "002141", name: "Test Stock", type: "stock", industry: "Test Sector" }], sectors: [], themes: [], indices: [] } };
    else if (url.pathname === "/api/dragon-tiger/profiles") {
      payload = {
        meta: { status: "success", source: "tushare", cached: true },
        summary: { profile_count: 3, described_count: 2, organization_count: 4 },
        profiles: [
          { id: "hot-money-profile-1", name: "赵老哥", description: "聚焦市场核心标的。", organizations: ["华泰证券浙江分公司", "银河证券绍兴"], organization_count: 2 },
          { id: "hot-money-profile-2", name: "炒股养家", description: "重视情绪与风险收益比。", organizations: ["华鑫证券上海宛平南路"], organization_count: 1 },
          { id: "hot-money-profile-3", name: "作手新一", description: "", organizations: ["国泰海通证券南京太平南路"], organization_count: 1 },
        ],
      };
    }
    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/rotation/members") {
      payload = {
        meta: { trade_date: "20260724", sector_name: url.searchParams.get("sector") || "电网设备", member_count: 3, quoted_count: 2 },
        rows: [
          { code: "002879", name: "长缆科技", change: 4.8, open: 18.21, close: 19.06, amount_billion: 19.6, quoted: true },
          { code: "603221", name: "爱丽家居", change: 1.2, open: 13.05, close: 13.22, amount_billion: 8.7, quoted: true },
          { code: "000001", name: "停牌样本", change: null, open: null, close: null, amount_billion: null, quoted: false },
        ],
      };
    }
    else if (url.pathname === "/api/auction") {
      payload = {
        meta: { trade_date: "2026-07-22", carried_forward: false, phase: "finalized", available: true, actionable: false },
        summary: { stock_count: 3, candidate_count: 1, focus_count: 1, one_price_count: 1, up_count: 2, down_count: 1, limit_open_count: 1, strong_open_count: 2, median_change: 1.2, amount_billion: 2.5, amount_change_previous: 12.5, amount_change_5d: 8.2 },
        expectations: { "超预期": 1, "符合预期": 0, "低于预期": 0 },
        candidate_meta: { baseline_date: "2026-07-21" },
        themes: {
          carry: [{ name: "Test Sector", status: "强承接", prior_limit_count: 2, leader: "Test Stock", matched_count: 1, median_change: 4.2, positive_rate: 100, amount_million: 15 }],
          new_themes: [{ name: "人工智能", stock_count: 2, median_change: 3.5, amount_million: 26, leaders: ["Test Stock"] }],
        },
        amount_history: [
          { trade_date: "2026-07-21", amount_billion: 2.2, stock_count: 2 },
          { trade_date: "2026-07-22", amount_billion: 2.5, stock_count: 2 },
        ],
        news_feedback: { available: false, message: "隔夜消息反馈暂不可用", detail: "待稳定的新闻与公告数据接入后开放" },
        focus_rows: [
          { code: "002141", name: "Test Stock", sector: "Test Sector", change: 4.2, price: 10.2, amount_million: 15, volume_ratio: 1.8, turnover_rate: 0.12, source_label: "昨日涨停 · 同花顺热榜", expectation: "超预期", expected_change: 2.2, attention_score: 88.5, core_tags: ["人气前5"], expectation_reason: "昨日首板；竞价涨幅高于预期中枢2.0个百分点，量比1.80" },
        ],
        one_price_rows: [
          { code: "000001", name: "Limit Stock", sector: "Test Sector", change: 10, price: 11, amount_million: 8, volume_ratio: 3.2, source_label: "昨日涨停", prior_streak: 3, core_tags: ["三板以上"], is_market_core: true, is_one_price: true },
        ],
        watchlist_rows: [
          { code: "000002", name: "Watch Stock", sector: "Bank", change: -1.2, price: 9.88, amount_million: 3, volume_ratio: 0.9, expectation: "符合预期", expected_change: 0, attention_score: 32.5, core_tags: [], expectation_reason: "自选观察；竞价反馈接近个人观察基准", is_watchlist: true, available: true },
        ],
        watchlist_missing_count: 0,
        rows: [
          { code: "002141", name: "Test Stock", sector: "Test Sector", change: 4.2, price: 10.2, amount_million: 15, volume_ratio: 1.8, turnover_rate: 0.12, source_label: "昨日涨停 · 同花顺热榜", expectation: "超预期", expected_change: 2.2, attention_score: 88.5, core_tags: ["人气前5"], expectation_reason: "昨日首板；竞价涨幅高于预期中枢2.0个百分点，量比1.80" },
        ],
      };
    } else if (url.pathname === "/api/themes") {
      payload = {
        meta: { trade_date: "2026-07-22", carried_forward: false },
        summary: { theme_count: 1, quoted_count: 1, up_count: 1, down_count: 0, hot_count: 1 },
        items: [{ code: "885728.TI", name: "人工智能", member_count: 1, change: 2.2, turnover_rate: 3.1, hot_rank: 1, has_quote: true }],
      };
    } else if (url.pathname === "/api/themes/detail") {
      payload = {
        meta: { trade_date: "2026-07-22" },
        theme: { code: "885728.TI", name: "人工智能", member_count: 1, change: 2.2, turnover_rate: 3.1 },
        summary: { member_count: 1, quoted_count: 1, up_count: 1, down_count: 0 },
        series: [
          { trade_date: "2026-07-21", open: 100, high: 104, low: 99, close: 103, change: 3, volume: 1000 },
          { trade_date: "2026-07-22", open: 103, high: 106, low: 102, close: 105, change: 1.94, volume: 1200 },
        ],
        members: [{ code: "002141", name: "Test Stock", change: 2.4, price: 10.8, amount_billion: 3.2, has_quote: true }],
      };
    } else if (url.pathname === "/api/popularity") {
      const hot = { rank: 1, code: "002141", ts_code: "002141.SZ", name: "Test Stock", change: 2.4, price: 10.8, ths_rank: 1, dc_rank: 2, rank_change: 3, concepts: ["人工智能"], dual_source: true };
      payload = { meta: { trade_date: "2026-07-22", carried_forward: false }, summary: { ths_count: 1, dc_count: 1, dual_count: 1 }, combined: [hot], ths: [{ ...hot, rank: 1 }], dc: [{ ...hot, rank: 2 }] };
    }
    else if (url.pathname === "/api/screener/setup") {
      options.screenerSetupRequests = (options.screenerSetupRequests || 0) + 1;
      const factorFields = [
        ["amount_billion", "成交额（亿元）"], ["above_ma20", "站上20日线"],
        ["relative_strength", "相对强度"], ["sector_strength", "板块强度"],
        ["volume_ratio_5d", "5日量比"], ["volatility_10d", "10日波动率"],
        ["dividend_yield_ttm", "股息率TTM"],
      ].map(([id, label]) => ({ id, label }));
      payload = {
        trade_date: "20260722",
        regime: { id: "repair", label: "修复", confidence: 70, reason: "测试", evidence: [] },
        regimes: [{ id: "repair", label: "修复" }],
        factor_fields: factorFields,
        factor_groups: [{ name: "行情与质量", fields: factorFields }],
        operators: [">", ">=", "<", "<=", "==", "between"],
        factor_data: {
          ready: true,
          date_count: 45,
          start_date: "20260518",
          end_date: "20260722",
          health: { market: true, auction: true, valuation: true, fundamental: true, dividend_history: true },
        },
        llm: { configured: true },
        strategies: [
          {
            id: 1, name: "修复确认", description: "保留原有智能策略流程", regimes: ["repair"], builtin: true,
            data_ready: true, missing_data: [],
            formula: {
              meta: { library: "smart" }, universe: { exclude_st: true, listed_days_min: 120 }, filters: [],
              score: [{ field: "relative_strength", weight: 1, direction: "desc" }], limit: 15, min_score: 0.5,
            },
          },
          {
            id: 2, name: "连续分红质量", description: "持续分红、估值与流动性共同约束。", regimes: ["repair"], builtin: true,
            data_ready: true, missing_data: [],
            formula: {
              meta: { library: "curated", category: "红利价值", quality: "A", frequency: "月度", risk: "中低", data_group: "估值与财务" },
              universe: { exclude_st: true, listed_days_min: 720 },
              filters: [{ field: "dividend_yield_ttm", op: ">=", value: 2 }],
              score: [{ field: "dividend_yield_ttm", weight: 1, direction: "desc" }], limit: 20, min_score: 0.5,
            },
          },
        ],
      };
      if (options.additionalScreenerRegimes) {
        payload.regimes.push(...options.additionalScreenerRegimes);
      }
      if (options.additionalScreenerStrategies) {
        payload.strategies.push(...options.additionalScreenerStrategies);
      }
      if (options.latestScreenerResults) {
        payload.latest_results = options.latestScreenerResults;
        payload.latest_result = options.latestScreenerResults.smart || null;
      }
      if (options.recentScreenerResults) {
        payload.recent_results = options.recentScreenerResults;
      }
    } else if (url.pathname === "/api/screener/run") {
      const body = route.request().postDataJSON();
      options.screenerRunBodies = [...(options.screenerRunBodies || []), body];
      const runResult = options.screenerRunResult?.(body);
      payload = {
        result: runResult || options.latestScreenerResults?.[body.mode] || {
          meta: {
            run_id: 99,
            trade_date: "20260722",
            regime: body.regime,
            strategy_name: body.strategy_name,
            mode: body.mode,
          },
          candidates: [],
          disclaimer: "历史统计不代表未来收益",
          backtest: null,
        },
      };
      if (options.recentScreenerResults) {
        options.recentScreenerResults.unshift(payload.result);
      }
    } else if (url.pathname === "/api/screener/tracking") {
      if (route.request().method() === "POST") {
        const body = route.request().postDataJSON();
        screenerTracking = {
          batches: [...screenerTracking.batches, {
            run_id: body.run_id,
            selection_date: "20260722",
            strategy_name: "Manual strategy",
            items: [{ id: 10, code: body.code, name: "Manual Stock", entry_price: 12, observed_days: 0, status: "等待 T+1" }],
          }],
          summary: { ...screenerTracking.summary, total: screenerTracking.summary.total + 1 },
        };
        payload = { ok: true, tracking: screenerTracking };
      } else payload = screenerTracking;
    } else if (/^\/api\/screener\/tracking\/\d+$/.test(url.pathname)) {
      const trackId = Number(url.pathname.split("/").at(-1));
      screenerTracking = {
        batches: screenerTracking.batches.map((batch) => ({
          ...batch,
          items: batch.items.filter((item) => item.id !== trackId),
        })).filter((batch) => batch.items.length),
        summary: { ...screenerTracking.summary, total: Math.max(0, screenerTracking.summary.total - 1) },
      };
      payload = { ok: true, deleted: true, tracking: screenerTracking };
    } else if (url.pathname === "/api/heaven/readings") {
      payload = {
        mode: url.searchParams.get("mode") || "fortune",
        items: [{
          id: 31,
          mode: "fortune",
          context_date: "20260723",
          subject: "2026-07-23 观气",
          subject_detail: "丙午年 · 乙未月 · 己丑日 · 土气偏显",
          answer: "三层气机已经合参，今日宜先定节奏，再看行动。",
          created_at: "2026-07-23T09:12:00+08:00",
        }],
      };
    } else if (url.pathname === "/api/mentors/setup") {
      payload = { trade_date: "20260722", mentors: mentorDirectory(authSession.user.role) };
    } else if (url.pathname === "/api/mentors/chat") {
      await route.fulfill({
        status: 200,
        contentType: "application/x-ndjson; charset=utf-8",
        body: [
          JSON.stringify({ type: "delta", content: "## 判断\n先看市场结构。\n\n" }),
          JSON.stringify({ type: "delta", content: "- 等待确认\n- 控制仓位" }),
          JSON.stringify({ type: "meta", data_trade_date: "20260722", notice: "" }),
          JSON.stringify({ type: "done" }),
        ].join("\n"),
      });
      return;
    }
    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();
  await page.locator("#assistantButton").click();
  await expect(page.locator("#assistantDialog")).toBeVisible();
  await page.locator("#closeAssistantDialog").click();

  const views = [
    "auctionView", "sentimentCycleView", "limitPool", "brokenView", "downView", "yesterdayView",
    "performanceView", "ladderView", "rotationView", "themeLibraryView", "popularityView", "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 expect(page.locator(".module-tab.active")).toHaveCount(1);
  }

  await page.keyboard.press("Control+K");
  await expect(page.locator("#globalSearchDialog")).toBeVisible();
  await expect(page.locator("#globalSearchInput")).toBeFocused();
});

test("fresh visits default to the latest date and sentiment cycle", async ({ page }) => {
  const options = { echoDashboardDate: true };
  await mockApplication(page, session("admin", true), options);
  await page.goto("/index.html?date=2026-07-28");
  const today = await page.evaluate(() => todayString());

  await expect(page.locator("#tradeDate")).toHaveValue(today);
  await expect(page.locator("#sentimentCycleView")).toHaveClass(/active-view/);
  await expect(page.locator('[data-view="sentimentCycleView"]')).toHaveClass(/active/);
  expect(options.dashboardTradeDates.at(-1)).toBe(today);
  expect(new URL(page.url()).searchParams.has("date")).toBe(false);

  await page.evaluate(() => {
    const input = document.querySelector("#tradeDate");
    input.value = "2026-07-28";
    input.dispatchEvent(new Event("change", { bubbles: true }));
  });
  await expect.poll(() => options.dashboardTradeDates.at(-1)).toBe("2026-07-28");
  expect(new URL(page.url()).searchParams.has("date")).toBe(false);
});

test("every primary workspace shares the canonical desktop shell geometry", async ({ page }) => {
  await page.setViewportSize({ width: 1440, height: 900 });
  await mockApplication(page, session("admin", true));
  await page.goto("/index.html");
  const views = [
    "auctionView", "sentimentCycleView", "limitPool", "brokenView", "downView", "yesterdayView",
    "performanceView", "ladderView", "rotationView", "themeLibraryView", "popularityView", "dragonView",
    "screenerView", "mentorView", "heavenView", "reviewWorkspaceView",
  ];
  let reference = null;
  for (const view of views) {
    await page.locator(`[data-view="${view}"]`).first().click();
    const activeView = page.locator(`#${view}`);
    await expect(activeView).not.toHaveClass(/view-entering/);
    const box = await activeView.boundingBox();
    expect(box).not.toBeNull();
    reference ||= { x: box.x, y: box.y, width: box.width };
    expect(Math.abs(box.x - reference.x)).toBeLessThanOrEqual(1);
    expect(Math.abs(box.y - reference.y)).toBeLessThanOrEqual(1);
    expect(Math.abs(box.width - reference.width)).toBeLessThanOrEqual(1);
  }
});

test("manual refresh stays in place without reopening the full-page loader", async ({ page }) => {
  const options = { dashboardDelay: 350 };
  await mockApplication(page, session(), options);
  await page.goto("/index.html");
  await expect(page.locator("#loadingOverlay")).toBeHidden();

  await page.locator("#refreshButton").click();

  await expect(page.locator("#refreshButton")).toBeDisabled();
  await expect(page.locator("#loadingOverlay")).toBeHidden();
  await expect(page.locator("#statusText")).toContainText("刷新");
  await expect(page.locator("#refreshButton")).toBeEnabled();
  expect(options.dashboardRequests).toBe(2);
});

test("night mode covers the application shell and persists across reloads", async ({ page }) => {
  await mockApplication(page, session("admin", true));
  await page.addInitScript(() => {
    if (sessionStorage.getItem("themeTestReady")) return;
    localStorage.removeItem("xiaobaiTheme");
    sessionStorage.setItem("themeTestReady", "1");
  });
  await page.goto("/index.html");

  await expect(page.locator("html")).toHaveAttribute("data-theme", "light");
  await expect(page.locator("#themeToggle")).toHaveAttribute("aria-pressed", "false");
  await page.locator("#themeToggle").click();
  await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
  await expect(page.locator("#themeToggle")).toHaveAttribute("aria-label", "切换到日间模式");
  await expect(page.locator("#themeToggle")).toHaveAttribute("aria-pressed", "true");

  const darkSurfaces = await page.evaluate(() => {
    const color = (selector) => getComputedStyle(document.querySelector(selector)).backgroundColor;
    return {
      body: color("body"),
      sidebar: color(".sidebar"),
      topbar: color(".topbar"),
      tableHead: color("#limitTable thead th"),
    };
  });
  expect(new Set(Object.values(darkSurfaces)).has("rgb(255, 255, 255)")).toBe(false);

  await page.reload();
  await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
  await expect(page.locator("#themeToggle")).toHaveAttribute("aria-pressed", "true");
  await page.keyboard.press("Control+K");
  await expect(page.locator("#globalSearchDialog")).toBeVisible();
  expect(await page.locator("#globalSearchDialog").evaluate((dialog) => getComputedStyle(dialog).backgroundColor)).not.toBe("rgb(255, 255, 255)");
  await page.locator("#closeGlobalSearch").click();

  await page.locator("#themeToggle").click();
  await expect(page.locator("html")).toHaveAttribute("data-theme", "light");
  await expect(page.locator("#themeToggle")).toHaveAttribute("aria-label", "切换到夜间模式");
});

test("collapsed overview and sentiment layout keep a single current reading", async ({ page }) => {
  await page.setViewportSize({ width: 1440, height: 900 });
  await mockApplication(page, session("user", true));
  await page.goto("/index.html");
  await expect(page.locator("#sentimentGauge")).toBeHidden();
  await page.evaluate(() => {
    state.sentimentHistory = {
      available_days: 20,
      rows: [{
        trade_date: "2026-07-22", score: 32, label: "情绪偏弱", phase: "退潮", direction: "降温",
        day_change: -12, seal_rate: 65, limit_up_count: 32, first_board_count: 20,
        second_board_count: 6, three_plus_count: 3, max_height: 4, broken_count: 17,
        limit_down_count: 25, previous_limit_count: 40, previous_positive_count: 12,
        previous_positive_rate: 30, average_previous_change: -1.2, normalization: "固定锚点",
        components: {
          breadth: { label: "市场宽度", score: 28, weight: 20, summary: "红盘家数偏少" },
          limit: { label: "涨停生态", score: 42, weight: 25, summary: "封板率仍需确认" },
          profit: { label: "赚钱效应", score: 31, weight: 30, summary: "昨日反馈偏弱" },
          ladder: { label: "连板结构", score: 38, weight: 15, summary: "高度仍在压缩" },
          amount: { label: "成交活跃度", score: 25, weight: 10, summary: "量能低于均值" },
        },
      }],
    };
    renderSentimentHistory();
  });
  await page.locator('[data-view="sentimentCycleView"]').first().click();

  await expect(page.locator(".sentiment-stage-guide, [data-sentiment-stage]")).toHaveCount(0);
  await expect(page.locator("#sentimentPhaseAdvice")).toHaveText("情绪指标继续走弱。");
  const alignment = await page.evaluate(() => {
    const components = document.querySelector(".sentiment-components-panel").getBoundingClientRect();
    const trend = document.querySelector(".sentiment-trend-panel").getBoundingClientRect();
    const summary = document.querySelector(".sentiment-cycle-summary").getBoundingClientRect();
    const chart = document.querySelector(".sentiment-chart-shell").getBoundingClientRect();
    const detail = document.querySelector(".sentiment-detail-toolbar").getBoundingClientRect();
    const label = document.querySelector(".sentiment-block .metric-label");
    const status = document.querySelector(".sentiment-block .sentiment-text");
    const labelStyle = getComputedStyle(document.querySelector(".sentiment-block .metric-label"));
    const statusStyle = getComputedStyle(document.querySelector(".sentiment-block .sentiment-text"));
    return {
      columnsAligned: Math.abs(trend.top - summary.top) < 1,
      railAligned: Math.abs(summary.x - components.x) < 1 && Math.abs(summary.width - components.width) < 1 && components.top > summary.bottom,
      detailAfterAnalysis: detail.top > Math.max(trend.bottom, components.bottom),
      chartHeight: chart.height,
      sameType: labelStyle.fontSize === statusStyle.fontSize
        && labelStyle.fontWeight === statusStyle.fontWeight
        && labelStyle.lineHeight === statusStyle.lineHeight,
      sameBaseline: Math.abs(label.getBoundingClientRect().y - status.getBoundingClientRect().y) < 0.1,
      noStatusOffset: statusStyle.marginTop === "0px",
    };
  });
  expect(alignment.columnsAligned).toBe(true);
  expect(alignment.railAligned).toBe(true);
  expect(alignment.detailAfterAnalysis).toBe(true);
  expect(alignment.chartHeight).toBeGreaterThanOrEqual(340);
  expect(alignment.sameType).toBe(true);
  expect(alignment.sameBaseline).toBe(true);
  expect(alignment.noStatusOffset).toBe(true);

  const pageFrames = {};
  for (const [view, headSelector] of [
    ["sentimentCycleView", ".sentiment-cycle-toolbar"],
    ["auctionView", ".auction-page-head-v2"],
    ["themeLibraryView", ".theme-page-head-v2"],
    ["popularityView", ".popularity-page-head-v2"],
    ["dragonView", ".dragon-page-head-v2"],
  ]) {
    await page.locator(`[data-view="${view}"]`).first().click();
    await page.waitForTimeout(350);
    pageFrames[view] = await page.evaluate(({ view, headSelector }) => {
      const workspace = document.getElementById(view);
      const frame = workspace.getBoundingClientRect();
      const head = workspace.querySelector(headSelector).getBoundingClientRect();
      const style = getComputedStyle(workspace);
      return {
        frame: [Math.round(frame.x), Math.round(frame.y), Math.round(frame.width)],
        head: [Math.round(head.x), Math.round(head.y), Math.round(head.width)],
        padding: [style.paddingTop, style.paddingRight, style.paddingBottom, style.paddingLeft],
        background: style.backgroundColor,
        border: style.borderTopWidth,
      };
    }, { view, headSelector });
  }
  const sentimentFrame = JSON.stringify(pageFrames.sentimentCycleView);
  for (const view of ["auctionView", "themeLibraryView", "popularityView", "dragonView"]) {
    expect(JSON.stringify(pageFrames[view])).toBe(sentimentFrame);
  }
});

test("limit-up pool separates stock identity and restores the reason column", async ({ page }) => {
  await mockApplication(page, session("user", true));
  await page.goto("/index.html");
  await page.evaluate(() => {
    state.dashboard.limits = [{
      code: "603221", name: "爱丽家居", streak: 4, change: 9.98, price: 14,
      sector: "家居用品", first_time: "09:25:01", last_time: "09:25:01",
      open_times: 0, turnover_rate: 0.41, amount_billion: 1.2,
      seal_amount_million: 5200, reason: "家居消费方向走强",
    }];
    renderLimitTable();
  });
  await page.locator('[data-view="limitPool"]').first().click();

  expect((await page.locator("#limitTable thead th").allTextContents()).map((text) => text.replace(/[↕▲▼]/g, ""))).toEqual([
    "序号", "股票", "连板", "涨幅（%）", "价格（元）", "所属板块", "首封", "最后封板", "开板（次）", "换手率（%）", "成交额（亿）", "封单额（万）", "涨停原因",
  ]);
  const cells = page.locator("#limitTableBody tr").first().locator("td");
  await expect(cells).toHaveCount(13);
  await expect(cells.nth(1)).toContainText("爱丽家居");
  await expect(cells.nth(1)).toContainText("603221");
  await expect(cells.nth(12)).toHaveText("家居消费方向走强");

  for (const [view, table, reasonLabel] of [
    ["limitPool", "limitTable", "涨停原因"],
    ["brokenView", "brokenTable", "炸板原因"],
    ["downView", "downTable", "风险线索"],
    ["yesterdayView", "yesterdayTable", "涨停逻辑"],
  ]) {
    await page.locator(`[data-view="${view}"]`).first().click();
    const geometry = await page.locator(`#${table}`).evaluate((tableNode, reason) => {
      const headers = [...tableNode.tHead.rows[0].cells];
      const widths = headers.map((header) => header.getBoundingClientRect().width);
      const reasonIndex = headers.findIndex((header) => header.textContent.trim() === reason);
      const numeric = headers.map((header, index) => ({ header, index })).filter(({ header }) => header.classList.contains("num"));
      return {
        index: widths[0],
        reason: widths[reasonIndex],
        numericMax: Math.max(...numeric.map(({ index }) => widths[index])),
        numericAligned: numeric.every(({ header }) => getComputedStyle(header).textAlign === "right"),
        numericVariant: numeric.every(({ header }) => getComputedStyle(header).fontVariantNumeric.includes("tabular-nums")),
      };
    }, reasonLabel);
    expect(geometry.reason).toBeGreaterThan(geometry.numericMax);
    expect(geometry.index).toBeLessThan(geometry.numericMax);
    expect(geometry.numericAligned).toBe(true);
    expect(geometry.numericVariant).toBe(true);
  }
});

test("broken pool matches the approved table structure and keeps independent interactions", async ({ page }) => {
  await mockApplication(page, session("user", true));
  await page.goto("/index.html");
  await page.evaluate(() => {
    state.dashboard.broken = [
      { code: "002156", name: "通富微电", change: 9.77, price: 76.64, sector: "半导体", first_time: "10:08:03", open_times: 4, turnover_rate: 17.36, amount_billion: 198.18, reason: "芯片方向冲高回落" },
      { code: "300214", name: "日科化学", change: 15.59, price: 11.86, sector: "化学制品", first_time: "09:54:21", open_times: 1, turnover_rate: 19.02, amount_billion: 10.37, reason: "化工板块异动" },
      { code: "601678", name: "滨化股份", change: 5.14, price: 6.54, sector: "化学原料", first_time: "09:35:55", open_times: 8, turnover_rate: 25.23, amount_billion: 34.64, reason: "高位反复开板" },
    ];
    renderBrokenTable(state.dashboard.broken);
  });
  await page.locator('[data-view="brokenView"]').first().click();

  await expect(page.locator("#brokenView")).toHaveClass(/redesigned-broken-view/);
  expect((await page.locator("#brokenTable thead th").allTextContents()).map((text) => text.replace(/[↕▲▼]/g, ""))).toEqual([
    "序号", "股票", "现价涨幅（%）", "距涨停（%）", "价格（元）", "所属板块", "首次触板", "开板（次）", "换手率（%）", "成交额（亿）", "炸板原因",
  ]);
  await expect(page.locator("#brokenTableBody tr")).toHaveCount(3);
  await expect(page.locator("#brokenTableBody tr").nth(0)).toContainText("0.23");
  await expect(page.locator("#brokenTableBody tr").nth(1)).toContainText("4.41");
  await expect(page.locator("#brokenTableBody tr").nth(2)).toContainText("反复炸 ×8");
  await expect(page.locator("#brokenTableBody")).toContainText("芯片方向冲高回落");

  await page.locator("#brokenSearch").fill("半导体");
  await expect(page.locator("#brokenTableBody tr")).toHaveCount(1);
  await expect(page.locator("#brokenTableBody")).toContainText("通富微电");
  await page.locator("#brokenSearch").fill("");

  await page.locator('[data-broken-sort="amount_billion"]').click();
  await expect(page.locator("#brokenTableBody tr").first()).toContainText("通富微电");
  await page.locator('[data-broken-sort="amount_billion"]').click();
  await expect(page.locator("#brokenTableBody tr").first()).toContainText("日科化学");
  await expect(page.locator("#brokenCount")).toHaveText("3 只");
  await expect(page.locator("#brokenMeta")).toContainText("数据日期 2026-07-22");
  expect(await page.evaluate(() => [
    brokenLimitRate({ code: "600000", name: "普通股票" }),
    brokenLimitRate({ code: "300001", name: "创业板股票" }),
    brokenLimitRate({ code: "830001", name: "北交所股票" }),
    brokenLimitRate({ code: "300001", name: "ST测试" }),
  ])).toEqual([10, 20, 30, 10]);
});

test("down-limit pool matches the approved risk-cluster table structure", async ({ page }) => {
  await mockApplication(page, session("user", true));
  await page.goto("/index.html");
  await page.evaluate(() => {
    state.dashboard.down_limits = [
      { code: "000037", name: "深南电A", change: -10.03, price: 8.97, sector: "电力", turnover_rate: 11.78, amount_billion: 3.68, streak: 2, reason: "连续弱势跌停" },
      { code: "000539", name: "粤电力A", change: -10.02, price: 5.66, sector: "电力", turnover_rate: 5.48, amount_billion: 8.16, streak: 1, reason: "板块集中释放风险" },
      { code: "001896", name: "豫能控股", change: -10.01, price: 14.39, sector: "电力", turnover_rate: 9.09, amount_billion: 20.64, streak: 1, reason: "高位补跌" },
      { code: "300045", name: "华力创通", change: -20.03, price: 11.46, sector: "军工电子", turnover_rate: 9.54, amount_billion: 5.76, streak: 1, reason: "放量破位" },
    ];
    renderDownTable(state.dashboard.down_limits);
  });
  await page.locator('[data-view="downView"]').first().click();

  await expect(page.locator("#downView")).toHaveClass(/redesigned-down-view/);
  expect((await page.locator("#downTable thead th").allTextContents()).map((text) => text.replace(/[↕▲▼]/g, ""))).toEqual([
    "序号", "股票", "跌幅（%）", "价格（元）", "所属板块", "换手率（%）", "成交额（亿）", "连续跌停（天）", "风险线索",
  ]);
  await expect(page.locator("#downTableBody tr")).toHaveCount(4);
  await expect(page.locator("#downSectorCluster")).toHaveText("电力集中跌停 ×3");
  await expect(page.locator("#downSectorCluster")).toBeVisible();
  await expect(page.locator("#downTableBody tr").first()).toContainText("2");
  await expect(page.locator("#downTableBody")).toContainText("连续弱势跌停");

  await page.locator("#downSearch").fill("军工电子");
  await expect(page.locator("#downTableBody tr")).toHaveCount(1);
  await expect(page.locator("#downTableBody")).toContainText("华力创通");
  await page.locator("#downSearch").fill("");

  await page.locator('[data-down-sort="change"]').click();
  await expect(page.locator("#downTableBody tr").first()).toContainText("华力创通");
  await page.locator('[data-down-sort="amount_billion"]').click();
  await expect(page.locator("#downTableBody tr").first()).toContainText("深南电A");
  await expect(page.locator("#downCount")).toHaveText("4 只");
  await expect(page.locator("#downMeta")).toContainText("数据日期 2026-07-22");
});

test("yesterday-limit pool exposes outcome summaries and combined filters", async ({ page }) => {
  await mockApplication(page, session("user", true));
  await page.goto("/index.html");
  await page.evaluate(() => {
    state.dashboard.meta.previous_trade_date = "2026-07-21";
    state.dashboard.yesterday_limits = [
      { code: "000011", name: "深物业A", prior_streak: 1, current_change: 9.99, outcome: "晋级", current_streak: 2, sector: "房地产开发", reason: "地产政策预期" },
      { code: "000533", name: "顺钠股份", prior_streak: 1, current_change: 10.02, outcome: "晋级", current_streak: 2, sector: "电网设备", reason: "电网设备走强" },
      { code: "000017", name: "深中华A", prior_streak: 1, current_change: 2.11, outcome: "断板", current_streak: 0, sector: "饰品", reason: "消费修复" },
      { code: "000035", name: "中国天楹", prior_streak: 1, current_change: -5.38, outcome: "断板", current_streak: 0, sector: "环境治理", reason: "环保题材" },
      { code: "001258", name: "立新能源", prior_streak: 6, current_change: 7.60, outcome: "炸板", current_streak: 0, sector: "电力", reason: "新能源核心" },
      { code: "001388", name: "信通电子", prior_streak: 1, current_change: -9.99, outcome: "跌停", current_streak: 0, sector: "电网设备", reason: "智能电网" },
    ];
    renderYesterdayTable(state.dashboard.yesterday_limits);
  });
  await page.locator('[data-view="yesterdayView"]').first().click();

  await expect(page.locator("#yesterdayView")).toHaveClass(/redesigned-yesterday-view/);
  expect((await page.locator("#yesterdayTable thead th").allTextContents()).map((text) => text.replace(/[↕▲▼]/g, ""))).toEqual([
    "序号", "股票", "昨日高度（板）", "今日涨幅（%）", "今日结果", "当前高度（板）", "所属板块", "涨停逻辑",
  ]);
  await expect(page.locator("#yesterdayAllCount")).toHaveText("6");
  await expect(page.locator("#yesterdayAdvanceCount")).toHaveText("2");
  await expect(page.locator("#yesterdayAdvanceRate")).toHaveText("晋级率 33.3%");
  await expect(page.locator("#yesterdayPositiveCount")).toHaveText("4");
  await expect(page.locator("#yesterdayPositiveRate")).toHaveText("兑现率 66.7%");
  await expect(page.locator("#yesterdayFailCount")).toHaveText("2");
  await expect(page.locator("#yesterdayRiskCount")).toHaveText("2");
  await expect(page.locator("#yesterdayRiskRate")).toHaveText("亏钱效应 33.3%");

  await page.locator('[data-yesterday-filter="positive"]').click();
  await expect(page.locator("#yesterdayTableBody tr")).toHaveCount(4);
  await expect(page.locator("#yesterdayTableBody")).toContainText("深物业A");
  await expect(page.locator("#yesterdayTableBody")).toContainText("立新能源");

  await page.locator('[data-yesterday-filter="risk"]').click();
  await expect(page.locator("#yesterdayTableBody tr")).toHaveCount(2);
  await expect(page.locator("#yesterdayTableBody")).toContainText("立新能源");
  await expect(page.locator("#yesterdayTableBody")).toContainText("信通电子");
  await page.locator("#yesterdaySearch").fill("电网设备");
  await expect(page.locator("#yesterdayTableBody tr")).toHaveCount(1);
  await expect(page.locator("#yesterdayTableBody")).toContainText("信通电子");
  await page.locator("#yesterdaySearch").fill("");
  await page.locator('[data-yesterday-filter="all"]').click();

  await page.locator('[data-yesterday-sort="current_change"]').click();
  await expect(page.locator("#yesterdayTableBody tr").first()).toContainText("顺钠股份");
  await expect(page.locator("#yesterdayTableBody tr").nth(2).locator("td").nth(5)).toBeEmpty();
  await expect(page.locator("#yesterdayTableBody")).toContainText("地产政策预期");
  await expect(page.locator("#yesterdayMeta")).toHaveText(" · 昨日 2026-07-21 → 今日 2026-07-22");
});

test("limit-up performance transfers the approved tier cards and market conclusion", async ({ page }) => {
  await mockApplication(page, session("user", true));
  await page.goto("/index.html");
  await page.evaluate(() => {
    state.dashboard.meta.previous_trade_date = "2026-07-21";
    state.dashboard.meta.updated_at = "2026-07-22T15:00:00+08:00";
    state.dashboard.overview = {
      ...state.dashboard.overview,
      up_count: 555,
      flat_count: 0,
      down_count: 4940,
      limit_up_count: 40,
      limit_down_count: 25,
      sentiment_phase: "退潮",
    };
    state.dashboard.limit_performance = [
      { level: 6, label: "昨日6板", count: 1, advanced: 0, advance_rate: 0, positive_rate: 0, average_change: -9.8 },
      { level: 4, label: "昨日4板", count: 1, advanced: 0, advance_rate: 0, positive_rate: 0, average_change: -6.2 },
      { level: 3, label: "昨日3板", count: 4, advanced: 2, advance_rate: 50, positive_rate: 75, average_change: 3.4 },
      { level: 2, label: "昨日2板", count: 9, advanced: 2, advance_rate: 22.2, positive_rate: 44.4, average_change: 0.8 },
      { level: 1, label: "昨日首板", count: 101, advanced: 13, advance_rate: 12.9, positive_rate: 35.6, average_change: -1.2 },
    ];
    renderPerformance(state.dashboard.limit_performance);
  });
  await page.locator('[data-view="performanceView"]').first().click();

  await expect(page.locator("#performanceView")).toHaveClass(/redesigned-performance-view/);
  await expect(page.locator("#performanceDateRange")).toHaveText("昨日 2026-07-21 → 今日 2026-07-22");
  await expect(page.locator("#performanceCards .performance-stage-card")).toHaveCount(5);
  await expect(page.locator("#performanceCards .performance-stage-card").first()).toContainText("昨日5板+ → 今日");
  await expect(page.locator("#performanceCards .performance-stage-card").first()).toContainText("失效");
  await expect(page.locator("#performanceCards .performance-stage-card").nth(2)).toContainText("50.0%");
  await expect(page.locator("#performanceCards .performance-stage-card").nth(2)).toContainText("活跃");
  await expect(page.locator("#performanceCards .performance-stage-card").nth(4)).toContainText("危险");
  await expect(page.locator("#performanceTableBody")).toHaveCount(0);

  await expect(page.locator("#breadthUpCount")).toHaveText("555");
  await expect(page.locator("#breadthDownCount")).toHaveText("4,940");
  await expect(page.locator("#breadthRatio")).toHaveText("10.1%");
  await expect(page.locator("#breadthWarning")).toHaveText("△ 宽度极差，涨跌停 40:25");
  await expect(page.locator("#performanceConclusion")).toContainText("高位晋级率全线失效");
  await expect(page.locator("#performanceConclusion")).toContainText("昨日3板晋级率最高");
  await expect(page.locator("#performanceConclusion")).toContainText("当前情绪周期「退潮」");
});

test("market ladder transfers tier bands, sorting and structural insights", async ({ page }) => {
  await mockApplication(page, session("admin", true));
  await page.goto("/index.html");
  await page.evaluate(() => {
    state.dashboard.meta.previous_trade_date = "2026-07-21";
    state.dashboard.yesterday_limits = [{ prior_streak: 6 }, { prior_streak: 3 }];
    state.dashboard.limit_performance = [
      { level: 4, label: "昨日3板", advance_rate: 50 },
      { level: 3, label: "昨日2板", advance_rate: 22.2 },
      { level: 2, label: "昨日首板", advance_rate: 12.9 },
    ];
    state.dashboard.ladders = [
      { level: 4, label: "4板", count: 2, stocks: [
        { code: "002879", name: "长缆科技", sector: "电网设备", first_time: "09:25:00", open_times: 2, seal_amount_million: 7247 },
        { code: "603221", name: "爱丽家居", sector: "家居用品", first_time: "09:25:01", open_times: 0, seal_amount_million: 27572 },
      ] },
      { level: 3, label: "3板", count: 2, stocks: [
        { code: "000595", name: "新能股份", sector: "电力", first_time: "09:25:00", open_times: 14, seal_amount_million: 1239 },
        { code: "301234", name: "五洲医疗", sector: "医疗器械", first_time: "09:25:00", open_times: 0, seal_amount_million: 39679 },
      ] },
      { level: 2, label: "2板", count: 9, stocks: Array.from({ length: 9 }, (_, index) => ({
        code: `000${String(index + 1).padStart(3, "0")}`, name: `二板股票${index + 1}`, sector: "电网设备", first_time: `09:3${index}:00`, open_times: index, amount_billion: 1.2,
      })) },
      { level: 1, label: "首板", count: 3, stocks: [
        { code: "002374", name: "中锐股份", sector: "包装印刷", first_time: "09:31:33", open_times: 0, seal_amount_million: 5210 },
        { code: "300414", name: "中光防雷", sector: "通信设备", first_time: "09:34:42", open_times: 1, seal_amount_million: 3365 },
        { code: "002012", name: "凯恩股份", sector: "造纸", first_time: "09:35:45", open_times: 0, seal_amount_million: 2874 },
      ] },
    ];
    renderLadderBoard(state.dashboard.ladders);
  });
  await page.locator('[data-view="ladderView"]').first().click();

  await expect(page.locator("#ladderView")).toHaveClass(/redesigned-ladder-view/);
  await expect(page.locator("#ladderDateRange")).toHaveText("数据日期 2026-07-22");
  await expect(page.locator("#ladderBoard .market-ladder-tier")).toHaveCount(5);
  await expect(page.locator("#ladderBoard .market-ladder-tier").first()).toContainText("5板+");
  await expect(page.locator("#ladderBoard .market-ladder-tier").first()).toContainText("断层");
  await expect(page.locator("#ladderBoard .market-ladder-stock")).toHaveCount(15);
  await expect(page.locator("#ladderBoard .market-ladder-tag.one-price")).toHaveCount(2);
  const equalCardWidths = await page.locator("#ladderBoard .market-ladder-stock").evaluateAll((cards) => cards.map((card) => card.getBoundingClientRect().width));
  expect(Math.max(...equalCardWidths) - Math.min(...equalCardWidths)).toBeLessThan(1);
  await expect(page.locator("#ladderInsights .market-ladder-apex-card")).toContainText("4 板");
  await expect(page.locator("#ladderInsights")).toContainText("较昨日 6 板 ↓ 空间压缩");
  await expect(page.locator("#ladderInsights .market-ladder-rate-list")).toContainText("50.0%");

  await page.locator('[data-ladder-sort="open"]').click();
  await expect(page.locator('[data-ladder-sort="open"]')).toHaveClass(/active/);
  await expect(page.locator("#ladderBoard .market-ladder-tier").nth(2).locator(".market-ladder-stock").first()).toContainText("五洲医疗");
  await page.locator('[data-ladder-level="2"]').click();
  await expect(page.locator("#ladderBoard .market-ladder-tier").nth(3).locator(".market-ladder-stock")).toHaveCount(9);
  await expect(page.locator("#ladderBoard .market-ladder-tier").nth(3).locator(".market-ladder-more")).toContainText("收起");
  await page.locator('[data-ladder-level="2"]').click();
  await expect(page.locator("#ladderBoard .market-ladder-tier").nth(3).locator(".market-ladder-stock")).toHaveCount(8);
  await expect(page.locator("#ladderBoard .market-ladder-tier").nth(3).locator(".market-ladder-more")).toContainText("展开剩余 1 只");

  const ladderOverflow = await page.evaluate(() => {
    const group = state.dashboard.ladders.find((item) => item.level === 2);
    group.stocks = Array.from({ length: 48 }, (_, index) => ({
      code: `001${String(index).padStart(3, "0")}`,
      name: `二板扩展${index + 1}`,
      sector: "电网设备",
      first_time: "09:30:00",
      open_times: index % 4,
      amount_billion: 1.2,
    }));
    group.count = group.stocks.length;
    state.expandedLadderLevels.add(2);
    renderLadderBoard(state.dashboard.ladders);
    const main = document.querySelector(".app-main");
    return {
      clientHeight: main.clientHeight,
      scrollHeight: main.scrollHeight,
      overflowY: getComputedStyle(main).overflowY,
    };
  });
  expect(ladderOverflow.overflowY).toBe("auto");
  expect(ladderOverflow.scrollHeight).toBeGreaterThan(ladderOverflow.clientHeight);
});

test("sector rotation transfers the nine-day matrix, tracking and sortable detail", async ({ page }) => {
  await page.setViewportSize({ width: 1440, height: 900 });
  await mockApplication(page, session("admin", true));
  await page.goto("/index.html");
  await page.evaluate(() => {
    const dates = ["20260714", "20260715", "20260716", "20260717", "20260720", "20260721", "20260722", "20260723", "20260724"];
    state.dashboard.meta.trade_date = "20260724";
    state.rotationHistory = {
      rows: dates.map((tradeDate, dayIndex) => ({
        trade_date: tradeDate,
        sectors: [
          { name: "电网设备", rank: 1, count: 5 + dayIndex, strength: 92 },
          { name: "计算机设备与自动化设备", rank: 2, count: 3, strength: 76 },
          { name: `轮动板块${dayIndex + 1}`, rank: 3, count: 1, strength: 58 },
        ],
      })),
    };
    state.rotationHistoryKey = `${document.querySelector("#tradeDate").value}:9`;
    state.dashboard.sector_rotation = [
      { rank: 1, name: "电网设备", count: 8, previous_count: 3, delta: 5, strength: 96, max_streak: 4, leader: "长缆科技", amount_billion: 19.6 },
      { rank: 2, name: "半导体", count: 3, previous_count: 0, delta: 3, strength: 78, max_streak: 2, leader: "测试股份", amount_billion: 30.0 },
      { rank: 3, name: "电力", count: 1, previous_count: 8, delta: -7, strength: 67, max_streak: 3, leader: "新能股份", amount_billion: 7.3 },
    ];
    state.dashboard.sectors = [
      { name: "电网设备", change: 4.8 },
      { name: "半导体", change: 2.1 },
      { name: "电力", change: -1.6 },
    ];
    renderRotationHistory();
    renderRotationMembers();
  });
  await page.locator('[data-view="rotationView"]').first().click();

  await expect(page.locator("#rotationView")).toHaveClass(/redesigned-rotation-view/);
  await expect(page.locator("#rotationHistoryRange")).toContainText("2026-07-14 → 2026-07-24");
  await expect(page.locator("#rotationHistory .rotation-day")).toHaveCount(9);
  await expect(page.locator("#rotationHistory .rotation-day").first()).toContainText("07-14");
  await expect(page.locator("#rotationHistory .rotation-day").last()).toContainText("07-24");
  await expect(page.locator("#rotationHistory .rotation-day").last()).toHaveClass(/latest-day/);
  await expect(page.locator("#rotationView .rotation-legend")).not.toContainText("单元格 =");
  const firstDayCells = page.locator("#rotationHistory .rotation-day").first().locator(".rotation-sector-chip");
  await expect(firstDayCells.nth(0)).toHaveClass(/heat-strong/);
  await expect(firstDayCells.nth(1)).toHaveClass(/heat-warm/);
  await expect(firstDayCells.nth(2)).toHaveClass(/heat-mild/);
  const cellVisuals = await firstDayCells.evaluateAll((cells) => cells.map((cell) => {
    const style = getComputedStyle(cell);
    return { background: style.backgroundColor, radius: parseFloat(style.borderRadius), duration: style.transitionDuration };
  }));
  expect(new Set(cellVisuals.map((item) => item.background)).size).toBe(3);
  expect(cellVisuals.every((item) => item.radius >= 7)).toBe(true);
  expect(cellVisuals.every((item) => item.duration.includes("0.28s"))).toBe(true);
  await firstDayCells.nth(1).hover();
  await page.waitForTimeout(300);
  expect(await firstDayCells.nth(1).evaluate((cell) => getComputedStyle(cell).transform)).not.toBe("none");
  const matrixFits = await page.locator("#rotationHistory").evaluate((element) => element.scrollWidth <= element.clientWidth + 1);
  expect(matrixFits).toBe(true);
  const longNameWrapsWithoutClipping = await page.locator("#rotationHistory .rotation-sector-chip strong", { hasText: "计算机设备与自动化设备" }).first().evaluate((element) => ({
    horizontal: element.scrollWidth <= element.clientWidth + 1,
    vertical: element.scrollHeight <= element.clientHeight + 1,
  }));
  expect(longNameWrapsWithoutClipping).toEqual({ horizontal: true, vertical: true });

  await page.locator('[data-rotation-sector="电网设备"]').first().click();
  await expect(page.locator("#rotationTracker")).toBeVisible();
  await expect(page.locator("#rotationTracker")).toContainText("近 9 日在榜 9 天");
  await expect(page.locator("#rotationDetailTitle")).toHaveText("电网设备成分股");
  await expect(page.locator("#rotationDetailMeta")).toContainText("2 / 3 只");
  await expect(page.locator("#rotationTableBody tr")).toHaveCount(3);
  await expect(page.locator("#rotationTableBody tr").first()).toContainText("长缆科技");
  await expect(page.locator("#rotationTableBody tr").last()).toContainText("当日无行情");
  await page.locator("#rotationTracker .rotation-track-cancel").click();
  await expect(page.locator("#rotationTracker")).toBeHidden();
  await expect(page.locator("#rotationMembersEmpty")).toContainText("点击上方任意板块查看成分股");
  await page.locator('[data-rotation-order="latest"]').click();
  await expect(page.locator("#rotationHistory .rotation-day").first()).toContainText("07-24");
  await expect(page.locator("#rotationHistoryRange")).toContainText("由近到远，左侧为最新交易日");
});

test("collection auction transfers the prototype hierarchy and keeps every dataset workflow", async ({ page }) => {
  await page.setViewportSize({ width: 1440, height: 900 });
  await mockApplication(page, session("admin", true));
  await page.goto("/index.html");
  await page.locator('[data-view="auctionView"]').first().click();
  await expect(page.locator("#auctionPhaseTitle")).toHaveText("今日竞价已定格");
  await page.evaluate(() => {
    const base = state.auctionData.focus_rows[0];
    state.auctionData.focus_rows = [
      { ...base, code: "002141", name: "高分标的", attention_score: 92, change: 6.2, amount_million: 30, volume_ratio: 4.8 },
      { ...base, code: "000001", name: "低分标的", attention_score: 68, change: -1.5, amount_million: 8, volume_ratio: 1.2, expectation: "低于预期", core_tags: [] },
    ];
    state.auctionData.summary = { ...state.auctionData.summary, focus_count: 2, candidate_count: 2, stock_count: 5470, one_price_count: 1, amount_billion: 202.8 };
    state.auctionData.themes.carry = [
      { name: "新型电力", status: "强承接", prior_limit_count: 5, leader: "立新能源", median_change: 2.36 },
      { name: "专用机械", status: "有承接", prior_limit_count: 6, leader: "长城军工", median_change: 1.82 },
      { name: "化工原料", status: "有承接", prior_limit_count: 8, leader: "金牛化工", median_change: 1.54 },
      { name: "电气设备", status: "承接弱", prior_limit_count: 33, leader: "长缆科技", median_change: 0.88 },
      { name: "运输设备", status: "分歧", prior_limit_count: 5, leader: "北自科技", median_change: 0 },
    ];
    state.auctionData.amount_history = Array.from({ length: 10 }, (_, index) => ({
      trade_date: `2026-07-${String(index + 13).padStart(2, "0")}`,
      amount_billion: 150 + index * 6,
      stock_count: 5000,
    }));
    state.auctionSortKey = "attention_score";
    state.auctionSortDirection = "desc";
    renderAuctionCenter();
  });

  await expect(page.locator("#auctionView")).toHaveClass(/redesigned-auction-view/);
  await expect(page.locator("#auctionSummary > div")).toHaveCount(4);
  await expect(page.locator("#auctionSummary")).toContainText("5,470 只");
  const auctionTabGeometry = await page.locator(".auction-tabs-v2").evaluate((tabs) => {
    const row = tabs.getBoundingClientRect();
    const summary = tabs.querySelector("#auctionSummary").getBoundingClientRect();
    return { rowRight: row.right, summaryRight: summary.right };
  });
  expect(Math.abs(auctionTabGeometry.rowRight - auctionTabGeometry.summaryRight)).toBeLessThanOrEqual(1);
  const datasetLabels = await page.locator("[data-auction-dataset]").allTextContents();
  expect(datasetLabels.map((label) => label.replace(/\s+/g, " ").trim())).toEqual(["重点异动 2", "我的自选 1", "全部候选 2", "竞价一字 1"]);
  const workspaceColumns = await page.locator(".auction-workspace-v2").evaluate((element) => getComputedStyle(element).gridTemplateColumns.split(" ").filter(Boolean).length);
  expect(workspaceColumns).toBe(2);
  await expect(page.locator(".auction-side-v2 .auction-side-card")).toHaveCount(2);
  await expect(page.locator(".auction-news-entry")).toHaveCount(0);
  await expect(page.locator("#auctionThemeCarry .auction-theme-row")).toHaveCount(5);
  await expect(page.locator("#auctionAmountTrend .auction-amount-day")).toHaveCount(10);

  await expect(page.locator("#auctionTableHead th")).toHaveCount(8);
  await expect(page.locator("#auctionTableHead th[data-auction-sort]")).toHaveCount(4);
  await expect(page.locator("#auctionTableBody tr").first()).toContainText("高分标的");
  await page.locator('#auctionTableHead th[data-auction-sort="attention_score"]').click();
  await expect(page.locator("#auctionTableBody tr").first()).toContainText("低分标的");
  await page.locator('[data-auction-dataset="onePrice"]').click();
  await expect(page.locator("#auctionTableHead th")).toHaveCount(6);
  await expect(page.locator("#auctionExpectationControls")).toBeHidden();
  await expect(page.locator("#auctionSearch")).toBeVisible();
  await expect(page.locator("#auctionExportButton")).toBeVisible();
  const download = page.waitForEvent("download");
  await page.locator("#auctionExportButton").click();
  await download;
  await page.locator("#auctionTableBody tr").first().click();
  await expect(page.locator("#stockDialog")).toBeVisible();
  await page.locator("#closeStockDialog").click();
  await page.setViewportSize({ width: 390, height: 844 });
  await expect(page.locator(".auction-summary-v2 > div")).toHaveCount(4);
  await expect(page.locator(".auction-side-v2 .auction-side-card")).toHaveCount(2);
  expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1)).toBe(true);
});

test("auction, themes and popularity reuse stock detail interactions", async ({ page }) => {
  await mockApplication(page, session("user", true));
  await page.goto("/index.html");

  await page.locator('[data-view="auctionView"]').first().click();
  await expect(page.locator("#auctionPhaseTitle")).toHaveText("今日竞价已定格");
  await expect(page.locator("#auctionRefreshButton")).toBeHidden();
  await expect(page.locator("#auctionTableBody tr")).toHaveCount(1);
  await expect(page.locator("#auctionTableBody")).toContainText("人气前5");
  await page.locator('[data-auction-filter="above"]').click();
  await expect(page.locator("#auctionTableBody tr")).toHaveCount(1);
  await expect(page.locator('[data-auction-filter="above"]')).toHaveClass(/active/);
  await expect(page.locator("#auctionThemeCarry")).toContainText("强承接");
  await expect(page.locator("#auctionAmountValue")).toHaveText("2.50 亿");
  await expect(page.locator("#auctionNewsTitle")).toHaveCount(0);
  await expect(page.locator(".auction-news-entry")).toHaveCount(0);
  await page.locator('[data-auction-dataset="onePrice"]').click();
  await expect(page.locator("#auctionTableBody tr")).toHaveCount(1);
  await expect(page.locator("#auctionTableBody")).toContainText("三板以上");
  await expect(page.locator("#auctionExpectationControls")).toBeHidden();
  await expect(page.locator("#auctionSearch")).toBeVisible();
  await page.locator('[data-auction-dataset="watchlist"]').click();
  await expect(page.locator("#auctionTableBody tr")).toHaveCount(1);
  await expect(page.locator("#auctionTableBody")).toContainText("Watch Stock");
  await page.locator('[data-auction-dataset="focus"]').click();
  await page.evaluate(() => {
    state.auctionData.meta = { phase: "selection", available: false, actionable: true };
    state.auctionData.rows = [];
    state.auctionData.focus_rows = [];
    renderAuctionCenter();
  });
  await expect(page.locator("#auctionPhaseTitle")).toHaveText("等待最终竞价");
  await expect(page.locator("#auctionRefreshButton")).toBeVisible();
  await expect(page.locator("#auctionEmpty")).toContainText("正在等待 9:25 最终竞价数据");

  await page.locator('[data-view="themeLibraryView"]').first().click();
  await expect(page.locator("#themeDirectory [data-theme-code]")).toHaveCount(1);
  await expect(page.locator("#themeMemberTableBody tr")).toHaveCount(1);
  await expect(page.locator("#themeDetailName")).toHaveText("人工智能");

  await page.locator('[data-view="popularityView"]').first().click();
  await expect(page.locator("#popularityTableBody tr")).toHaveCount(1);
  await expect(page.locator("#popularitySummary")).toContainText("双榜共识");
  await expect(page.locator("#popularityTableBody")).not.toContainText("双榜共识");
  await page.locator("#popularityTableBody tr").click();
  await expect(page.locator("#stockDialog")).toBeVisible();
});

test("theme library preserves the full master-detail workflow in its redesigned layout", async ({ page }) => {
  await page.setViewportSize({ width: 1440, height: 900 });
  await mockApplication(page, session("user", true));
  await page.goto("/index.html");
  await page.locator('[data-view="themeLibraryView"]').first().click();

  await expect(page.locator("#themeLibraryView")).toHaveClass(/redesigned-theme-view/);
  await expect(page.locator(".theme-summary-v2 > div")).toHaveCount(4);
  await expect(page.locator(".theme-directory-card-v2")).toBeVisible();
  await expect(page.locator(".theme-market-card-v2")).toBeVisible();
  await expect(page.locator(".theme-members-card-v2")).toBeVisible();
  await expect(page.locator("#themeDetailName")).toHaveText("人工智能");
  await expect(page.locator("#themeDetailMetrics > div")).toHaveCount(5);
  await expect(page.locator("#themeMemberCount")).toHaveText("有行情 1 / 1");
  await expect(page.locator("#themeDirectory [data-theme-code]")).toHaveAttribute("aria-pressed", "true");
  await expect(page.locator("#themeDetailChart")).toHaveCount(0);

  const themePreviewRequest = page.waitForRequest((request) => request.url().includes("/api/search/detail?") && request.url().includes("type=theme"));
  await page.locator(".market-preview-trigger").hover();
  await themePreviewRequest;
  await expect(page.locator("#stockPreview")).toBeVisible();
  await expect(page.locator("#stockPreviewName")).toHaveText("人工智能");
  await expect(page.locator("#stockPreviewSource")).toHaveText("日 K 行情 · 2 个交易日");
  const intradayRequest = page.waitForRequest((request) => request.url().includes("/api/chart/intraday?") && request.url().includes("type=theme"));
  await page.locator('[data-preview-chart="intraday"]').click();
  const requestedIntraday = new URL((await intradayRequest).url());
  expect(requestedIntraday.searchParams.get("id")).toBe("885728.TI");
  await expect(page.locator("#stockPreviewSource")).toHaveText("最新分时 · 1分钟");
  await page.locator("#closeStockPreview").click();

  await page.locator("#themeSearch").fill("不存在的题材");
  await expect(page.locator("#themeDirectory [data-theme-code]")).toHaveCount(0);
  await expect(page.locator("#themeDirectory")).toContainText("没有匹配的题材");
  await page.locator("#themeSearch").fill("人工智能");
  await expect(page.locator("#themeDirectory [data-theme-code]")).toHaveCount(1);

  await page.locator("#themeMemberTableBody tr").click();
  await expect(page.locator("#stockDialog")).toBeVisible();
  await page.locator("#closeStockDialog").click();

  await page.evaluate(() => {
    const directory = document.querySelector("#themeDirectory");
    const directorySeed = directory.querySelector("button");
    const members = document.querySelector("#themeMemberTableBody");
    const memberSeed = members.querySelector("tr");
    for (let index = 0; index < 24; index += 1) directory.appendChild(directorySeed.cloneNode(true));
    for (let index = 0; index < 20; index += 1) members.appendChild(memberSeed.cloneNode(true));
  });
  const desktop = await page.evaluate(() => ({
    columns: getComputedStyle(document.querySelector(".theme-library-workspace-v2")).gridTemplateColumns.split(" ").filter(Boolean).length,
    mainOverflows: document.querySelector(".app-main").scrollHeight > document.querySelector(".app-main").clientHeight + 1,
    directoryOverflows: document.querySelector("#themeDirectory").scrollHeight > document.querySelector("#themeDirectory").clientHeight + 1,
    membersOverflow: document.querySelector(".theme-members-frame-v2").scrollHeight > document.querySelector(".theme-members-frame-v2").clientHeight + 1,
  }));
  expect(desktop).toEqual({ columns: 2, mainOverflows: false, directoryOverflows: true, membersOverflow: true });

  await page.setViewportSize({ width: 390, height: 844 });
  await expect(page.locator(".theme-head-actions-v2")).toBeVisible();
  const mobile = await page.evaluate(() => ({
    columns: getComputedStyle(document.querySelector(".theme-library-workspace-v2")).gridTemplateColumns.split(" ").filter(Boolean).length,
    pageFits: document.documentElement.scrollWidth <= window.innerWidth + 1,
    memberTableScrolls: document.querySelector(".theme-members-frame-v2").scrollWidth > document.querySelector(".theme-members-frame-v2").clientWidth + 1,
  }));
  expect(mobile).toEqual({ columns: 1, pageFits: true, memberTableScrolls: true });
});

test("popularity ranking transfers the three-glance hierarchy and dynamic source tables", async ({ page }) => {
  await page.setViewportSize({ width: 1440, height: 900 });
  await mockApplication(page, session("user", true));
  await page.goto("/index.html");
  await page.locator('[data-view="popularityView"]').first().click();

  await expect(page.locator("#popularityView")).toHaveClass(/redesigned-popularity-view/);
  await expect(page.locator("#popularitySummary article")).toHaveCount(3);
  await expect(page.locator("#popularitySummary article").nth(0)).toContainText("同花顺热度 Top3");
  await expect(page.locator("#popularitySummary article").nth(1)).toContainText("东方财富热度 Top3");
  await expect(page.locator("#popularitySummary article").nth(2)).toContainText("双榜共识");
  const glanceStyles = await page.locator("#popularitySummary").evaluate((summary) => ({
    background: getComputedStyle(summary).backgroundColor,
    shadow: getComputedStyle(summary).boxShadow,
    borderWidth: getComputedStyle(summary).borderWidth,
    cardBackgrounds: [...summary.children].map((card) => getComputedStyle(card).backgroundColor),
    cardShadows: [...summary.children].map((card) => getComputedStyle(card).boxShadow),
  }));
  expect(new Set(glanceStyles.cardBackgrounds).size).toBe(1);
  expect(glanceStyles.cardBackgrounds[0]).not.toBe(glanceStyles.background);
  expect(glanceStyles.shadow).toBe("none");
  expect(glanceStyles.borderWidth).toBe("0px");
  expect(glanceStyles.cardShadows).toEqual(["none", "none", "none"]);
  await expect(page.locator("#popularityTableTitle")).toHaveText("双榜综合榜");
  await expect(page.locator("#popularityTableHead th")).toHaveCount(8);
  await expect(page.locator("#popularityTableHead")).not.toContainText("榜单状态");
  await expect(page.locator("#popularityTableBody .popularity-source-tag-v2")).toHaveCount(0);

  await page.locator('[data-popularity-source="ths"]').click();
  await expect(page.locator("#popularityTableTitle")).toHaveText("同花顺榜");
  await expect(page.locator("#popularityTableHead")).toContainText("榜单状态");
  await expect(page.locator("#popularityTableHead")).not.toContainText("东方财富");
  await expect(page.locator("#popularityTableBody .popularity-source-tag-v2")).toHaveText("双榜共识");

  await page.locator('[data-popularity-source="dc"]').click();
  await expect(page.locator("#popularityTableTitle")).toHaveText("东方财富榜");
  await expect(page.locator("#popularityTableHead")).not.toContainText("同花顺");
  await page.locator("#popularitySearch").fill("不存在的股票");
  await expect(page.locator("#popularityEmpty")).toBeVisible();
  await page.locator("#popularitySearch").fill("Test Stock");
  await expect(page.locator("#popularityTableBody tr")).toHaveCount(1);

  await page.locator("#popularityTableBody tr").click();
  await expect(page.locator("#stockDialog")).toBeVisible();
  await page.locator("#closeStockDialog").click();

  await page.locator('[data-popularity-source="combined"]').click();
  await page.evaluate(() => {
    const body = document.querySelector("#popularityTableBody");
    const seed = body.querySelector("tr");
    for (let index = 0; index < 30; index += 1) body.appendChild(seed.cloneNode(true));
  });
  const desktop = await page.evaluate(() => ({
    mainOverflows: document.querySelector(".app-main").scrollHeight > document.querySelector(".app-main").clientHeight + 1,
    tableOverflows: document.querySelector(".popularity-table-frame-v2").scrollHeight > document.querySelector(".popularity-table-frame-v2").clientHeight + 1,
  }));
  expect(desktop).toEqual({ mainOverflows: false, tableOverflows: true });

  await page.setViewportSize({ width: 390, height: 844 });
  const mobile = await page.evaluate(() => ({
    pageFits: document.documentElement.scrollWidth <= window.innerWidth + 1,
    tableScrolls: document.querySelector(".popularity-table-frame-v2").scrollWidth > document.querySelector(".popularity-table-frame-v2").clientWidth + 1,
  }));
  expect(mobile).toEqual({ pageFits: true, tableScrolls: true });
});

test("dragon-tiger redesign keeps the merged empty state and independent card hit zones", async ({ page }) => {
  await page.setViewportSize({ width: 1440, height: 900 });
  await mockApplication(page, session("admin", true));
  await page.goto("/index.html");
  await page.locator('[data-view="dragonView"]').first().click();

  await expect(page.locator("#dragonView")).toHaveClass(/redesigned-dragon-view/);
  await expect(page.locator("#dragonEmptyState")).toBeVisible();
  await expect(page.locator("#dragonEmptyTitle")).toContainText("2026-07-22");
  await expect(page.locator("#dragonDailyContent")).toBeHidden();
  await expect(page.locator("#dragonPreviousButton")).toBeVisible();
  await expect(page.locator("#dragonEmptyRefreshButton")).toBeVisible();

  await page.evaluate(() => {
    const operation = (code, name, net) => ({
      code, name, change: net > 0 ? 6.8 : -2.4, buy_million: net > 0 ? 42 : 5,
      sell_million: net > 0 ? 7 : 31, net_buy_million: net,
      direction: net > 0 ? "买入" : "卖出", seat_name: "测试营业部", tag: "题材核心", reason: "日涨幅偏离值达标",
    });
    const trader = (id, name, net, code) => ({
      id, name, identity_type: "trader", recognized: true, description: "聚焦市场核心，顺势参与强势方向",
      stock_count: 1, operation_count: 1, buy_million: net > 0 ? 42 : 5,
      sell_million: net > 0 ? 7 : 31, net_buy_million: net,
      operations: [operation(code, `${name}标的`, net)],
    });
    state.dragonTiger = {
      meta: { trade_date: "2026-07-22", requested_date: "2026-07-22", status: "success" },
      summary: { trader_count: 3, operation_count: 3, seat_net_buy_million: 34, active_stock_count: 3 },
      traders: [trader("alpha", "甲游资", 35, "002141"), trader("beta", "乙游资", -26, "000001"), trader("gamma", "丙游资", 25, "000002")],
      unclassified_seats: [],
    };
    renderDragonTiger();
  });

  await expect(page.locator("#dragonEmptyState")).toBeHidden();
  await expect(page.locator("#dragonDailyContent")).toBeVisible();
  await expect(page.locator("#dragonSummary .dragon-metric")).toHaveCount(4);
  await expect(page.locator("#dragonTraderList .dragon-trader-card")).toHaveCount(3);
  await expect(page.locator("#dragonTraderList .dragon-card-hit-zone")).toHaveCount(3);
  await expect(page.locator("#dragonTraderDetail")).toBeVisible();
  await expect(page.locator("#dragonTraderDetail")).toContainText("甲游资");
  expect((await page.locator("#dragonTraderDetail .dragon-operation-table thead th").allTextContents()).map((text) => text.replace(/[↕▲▼]/g, ""))).toEqual([
    "序号", "股票", "方向", "涨幅（%）", "买入（百万）", "卖出（百万）", "净额（百万）", "关联席位", "标签 / 上榜原因",
  ]);
  const operationCells = page.locator("#dragonTraderDetail .dragon-operation-table tbody tr").first().locator("td");
  await expect(operationCells.nth(0)).toHaveText("1");
  await expect(operationCells.nth(1)).toContainText("002141");
  await expect(operationCells.nth(1)).toContainText("甲游资标的");

  const hitZones = await page.locator("#dragonTraderList .dragon-card-hit-zone").evaluateAll((zones) => zones.map((zone) => {
    const box = zone.getBoundingClientRect();
    return { left: box.left, right: box.right, width: box.width };
  }));
  expect(hitZones.every((zone) => zone.width >= 18)).toBe(true);
  expect(hitZones.slice(1).every((zone, index) => zone.left >= hitZones[index].right - 1)).toBe(true);

  await page.locator('[data-dragon-trader="gamma"]').hover();
  await expect(page.locator('[data-dragon-card="gamma"]')).toHaveClass(/hovered/);
  await page.locator('[data-dragon-trader="beta"]').click();
  await expect(page.locator('[data-dragon-trader="beta"]')).toHaveAttribute("aria-pressed", "true");
  await expect(page.locator("#dragonTraderDetail")).toContainText("乙游资");

  await page.locator('[data-dragon-filter="buy"]').click();
  await expect(page.locator("#dragonTraderList .dragon-trader-card")).toHaveCount(2);
  await page.locator('[data-dragon-filter="sell"]').click();
  await expect(page.locator("#dragonTraderList .dragon-trader-card")).toHaveCount(1);
  await page.locator("#dragonSearch").fill("不存在");
  await expect(page.locator("#dragonTraderList .dragon-empty")).toBeVisible();
  await expect(page.locator("#dragonTraderDetail")).toBeHidden();
  await page.locator("#dragonSearch").fill("");
  await page.locator('[data-dragon-filter="all"]').click();
  await page.locator("#dragonTraderDetail tbody tr").first().click();
  await expect(page.locator("#stockDialog")).toBeVisible();
  await page.locator("#closeStockDialog").click();

  const scrollOwnership = await page.evaluate(() => {
    const body = document.querySelector("#dragonTraderDetail tbody");
    const seed = body.querySelector("tr");
    for (let index = 0; index < 20; index += 1) body.appendChild(seed.cloneNode(true));
    const daily = document.querySelector("#dragonDailyContent");
    const operations = document.querySelector("#dragonTraderDetail .trader-operations");
    return {
      dailyOverflow: getComputedStyle(daily).overflowY,
      dailyFits: daily.scrollHeight <= daily.clientHeight + 1,
      operationOverflow: getComputedStyle(operations).overflowY,
      operationsScroll: operations.scrollHeight > operations.clientHeight,
      descriptionSize: parseFloat(getComputedStyle(document.querySelector("#dragonTraderDetail .dragon-detail-header p")).fontSize),
    };
  });
  expect(scrollOwnership).toEqual({
    dailyOverflow: "hidden",
    dailyFits: true,
    operationOverflow: "auto",
    operationsScroll: true,
    descriptionSize: 13,
  });

  await page.locator("#dragonProfilesButton").click();
  await expect(page.locator("#dragonProfilesContent")).toBeVisible();
  await expect(page.locator("#dragonDailyContent")).toBeHidden();
  await expect(page.locator("#hotMoneyProfileSummary > span")).toHaveCount(3);
  await expect(page.locator("#hotMoneyProfileList .hot-money-profile-row-v2")).toHaveCount(3);
  await expect(page.locator("#hotMoneyProfileDetail")).toContainText("赵老哥");
  await expect(page.locator("#hotMoneyProfileDetail")).toContainText("华泰证券浙江分公司");
  await page.locator("#hotMoneyProfileSearch").fill("宛平南路");
  await expect(page.locator("#hotMoneyProfileList .hot-money-profile-row-v2")).toHaveCount(1);
  await expect(page.locator("#hotMoneyProfileDetail")).toContainText("炒股养家");
  await page.locator("#hotMoneyProfileSearch").fill("");
  await page.locator('[data-hot-money-profile="hot-money-profile-3"]').click();
  await expect(page.locator("#hotMoneyProfileDetail")).toContainText("名录暂未收录该游资的公开简介");

  await page.setViewportSize({ width: 390, height: 844 });
  const profileMobile = await page.evaluate(() => {
    const list = document.querySelector("#hotMoneyProfileList").getBoundingClientRect();
    const detail = document.querySelector("#hotMoneyProfileDetail").getBoundingClientRect();
    return {
      pageFits: document.documentElement.scrollWidth <= window.innerWidth + 1,
      detailBelowList: detail.top >= list.bottom - 1,
    };
  });
  expect(profileMobile).toEqual({ pageFits: true, detailBelowList: true });
  await page.locator("#dragonDailyButton").click();

  const mobile = await page.evaluate(() => ({
    pageFits: document.documentElement.scrollWidth <= window.innerWidth + 1,
    operationsScroll: document.querySelector("#dragonTraderDetail .trader-operations").scrollWidth > document.querySelector("#dragonTraderDetail .trader-operations").clientWidth + 1,
  }));
  expect(mobile).toEqual({ pageFits: true, operationsScroll: true });
});

test("auction owns a single vertical scroll container across desktop densities", async ({ page }) => {
  await page.setViewportSize({ width: 1920, height: 1080 });
  await mockApplication(page, session("admin", true));
  await page.goto("/index.html");
  await page.locator('[data-view="auctionView"]').first().click();
  await page.evaluate(() => {
    const body = document.querySelector("#auctionTableBody");
    const seed = body.querySelector("tr");
    if (!seed) return;
    for (let index = 0; index < 28; index += 1) body.appendChild(seed.cloneNode(true));
  });

  const measure = () => page.evaluate(() => {
    const box = (selector) => {
      const element = document.querySelector(selector);
      const style = getComputedStyle(element);
      const rect = element.getBoundingClientRect();
      return {
        clientHeight: element.clientHeight,
        scrollHeight: element.scrollHeight,
        clientWidth: element.clientWidth,
        scrollWidth: element.scrollWidth,
        overflowY: style.overflowY,
        top: rect.top,
        bottom: rect.bottom,
        width: rect.width,
      };
    };
    return {
      viewportHeight: innerHeight,
      html: box("html"),
      body: box("body"),
      main: box(".app-main"),
      view: box("#auctionView"),
      primary: box(".auction-primary-card"),
      frame: box(".auction-table-frame-v2"),
      side: box(".auction-side-v2"),
    };
  });

  for (const viewport of [{ width: 1920, height: 1080 }, { width: 1536, height: 864 }]) {
    await page.setViewportSize(viewport);
    const layout = await measure();
    expect(layout.html.scrollHeight).toBeLessThanOrEqual(layout.viewportHeight);
    expect(layout.body.scrollHeight).toBeLessThanOrEqual(layout.viewportHeight);
    expect(layout.main.overflowY).toBe("hidden");
    expect(layout.frame.overflowY).toBe("auto");
    expect(layout.frame.scrollHeight).toBeGreaterThan(layout.frame.clientHeight);
    expect(layout.side.top).toBeLessThan(layout.primary.bottom);
  }

  await page.setViewportSize({ width: 1280, height: 720 });
  const compact = await measure();
  expect(compact.html.scrollHeight).toBeLessThanOrEqual(compact.viewportHeight);
  expect(compact.main.overflowY).toBe("hidden");
  expect(compact.frame.overflowY).toBe("auto");
  expect(compact.frame.scrollHeight).toBeGreaterThan(compact.frame.clientHeight);
  expect(compact.side.top).toBeLessThan(compact.primary.bottom);

  await page.setViewportSize({ width: 3840, height: 2160 });
  const wide = await measure();
  expect(wide.html.scrollHeight).toBeLessThanOrEqual(wide.viewportHeight);
  expect(wide.view.width).toBeLessThanOrEqual(wide.main.clientWidth + 1);
  expect(wide.view.scrollWidth).toBeLessThanOrEqual(wide.view.clientWidth + 1);
});

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('[data-screener-mode="quant"]')).toBeDisabled();
  await expect(page.locator("#quantRunButton")).toBeDisabled();
  await page.locator("#assistantButton").click();
  await expect(page.locator("#settingsDialog")).toBeHidden();
  await expect(page.locator("#assistantDialog")).toBeVisible();
  await expect(page.locator("#assistantMemberGate")).toContainText("复盘助手仅对会员开放");
  await expect(page.locator("#assistantMemberGate")).toBeVisible();
  await expect(page.locator("#assistantMemberContent")).toHaveAttribute("aria-disabled", "true");
  await expect(page.locator("#assistantQuestion")).toBeDisabled();
  await expect(page.locator("[data-assistant-prompt]").first()).toBeDisabled();
  await expect(page.locator("#sendAssistant")).toBeDisabled();
});

test("stock hover preview ignores the selected historical date", async ({ page }) => {
  await mockApplication(page, session("user", true));
  await page.goto("/index.html");
  await page.locator("#tradeDate").fill("2026-07-01");
  const requestPromise = page.waitForRequest((request) => request.url().includes("/api/stock/002141/preview"));
  await page.evaluate(() => showStockPreview("002141", document.querySelector("#globalSearchButton")));
  const request = await requestPromise;
  const requestUrl = new URL(request.url());
  expect(requestUrl.searchParams.has("trade_date")).toBe(false);
  await expect(page.locator("#stockPreviewDate")).toHaveText("2026-07-23");
  await expect(page.locator("#stockPreviewName")).toHaveText("Test Stock");
  await expect(page.locator("#stockPreviewSource")).toHaveText("日 K 行情 · 2 个交易日");
  await expect(page.locator('[data-preview-chart="daily"]')).toHaveClass(/active/);
  await page.locator('[data-preview-chart="intraday"]').click();
  await expect(page.locator("#stockPreviewDate")).toHaveText("2026-07-24");
  await expect(page.locator("#stockPreviewSource")).toHaveText("最新分时 · 1分钟");
  const canvasColors = await page.locator("#stockPreviewChart").evaluate((canvas) => {
    const pixels = canvas.getContext("2d").getImageData(0, 0, canvas.width, canvas.height).data;
    const colors = new Set();
    for (let index = 0; index < pixels.length; index += 16) {
      if (pixels[index + 3]) colors.add(`${pixels[index]},${pixels[index + 1]},${pixels[index + 2]}`);
    }
    return colors.size;
  });
  expect(canvasColors).toBeGreaterThan(4);
});

test("stock hover preview loading state follows the dark chart theme", async ({ page }) => {
  await mockApplication(page, session("user", true), { previewDelay: 500 });
  await page.goto("/index.html");
  await page.evaluate(() => {
    document.documentElement.dataset.theme = "dark";
    showStockPreview("002141", document.querySelector("#globalSearchButton"));
  });
  const loading = page.locator("#stockPreviewLoading");
  await expect(loading).toBeVisible();
  await expect(page.locator('[data-preview-chart="daily"]')).toHaveClass(/active/);
  const colors = await page.evaluate(() => ({
    overlay: getComputedStyle(document.querySelector("#stockPreviewLoading")).backgroundColor,
    chart: getComputedStyle(document.documentElement).getPropertyValue("--chart-background").trim(),
    pixel: Array.from(
      document.querySelector("#stockPreviewChart").getContext("2d").getImageData(10, 10, 1, 1).data,
    ),
  }));
  expect(colors.overlay).not.toBe("rgb(255, 255, 255)");
  expect(colors.chart).toBe("#181b1e");
  expect(colors.pixel.slice(0, 3)).toEqual([24, 27, 30]);
});

test("rising candle body stays hollow and its wick stops at both edges", async ({ page }) => {
  await mockApplication(page, session("user", true));
  await page.goto("/index.html");
  const pixels = await page.evaluate(() => {
    const canvas = document.createElement("canvas");
    canvas.width = 40;
    canvas.height = 80;
    const context = canvas.getContext("2d");
    context.fillStyle = currentChartPalette().background;
    context.fillRect(0, 0, 40, 80);
    const priceY = (value) => 90 - value * 8;
    drawCandlestick(context, 20, { high: 10, close: 8, open: 6, low: 4 }, priceY, 10);
    const read = (x, y) => Array.from(context.getImageData(x, y, 1, 1).data);
    const reddest = (left, top, width, height) => {
      const data = context.getImageData(left, top, width, height).data;
      let selected = [0, 0, 0, 0];
      for (let index = 0; index < data.length; index += 4) {
        const pixel = [data[index], data[index + 1], data[index + 2], data[index + 3]];
        if (pixel[0] - pixel[1] > selected[0] - selected[1]) selected = pixel;
      }
      return selected;
    };
    return {
      upperWick: reddest(19, 10, 3, 16),
      bodyCenter: read(20, 34),
      lowerWick: reddest(19, 43, 3, 17),
      bodyBorder: reddest(14, 26, 3, 17),
    };
  });
  for (const redPixel of [pixels.upperWick, pixels.lowerWick, pixels.bodyBorder]) {
    expect(redPixel[0] - redPixel[1]).toBeGreaterThan(40);
    expect(redPixel[0] - redPixel[2]).toBeGreaterThan(40);
  }
  expect(pixels.bodyCenter.slice(0, 3)).toEqual([251, 252, 253]);
});

test("stock and market detail dialogs switch from daily K to intraday", async ({ page }) => {
  await mockApplication(page, session("user", true));
  await page.goto("/index.html");

  await page.evaluate(() => openStock("002141", { code: "002141", name: "Test Stock", sector: "Test Sector" }));
  await page.locator('[data-stock-detail-chart="intraday"]').click();
  await expect(page.locator("#chartSource")).toHaveText("分时 · 2026-07-24");
  await expect(page.locator('[data-stock-detail-chart="intraday"]')).toHaveAttribute("aria-pressed", "true");
  await page.locator('[data-stock-detail-chart="daily"]').click();
  await expect(page.locator("#chartSource")).toContainText("日 K 行情");
  await page.locator("#closeStockDialog").click();

  await page.evaluate(() => openEntityDetail({ id: "000001.SH", code: "000001.SH", name: "上证指数", type: "index", type_label: "指数" }));
  await page.locator('[data-entity-detail-chart="intraday"]').click();
  await expect(page.locator("#entityDetailDate")).toHaveText("分时 · 2026-07-24");
  await expect(page.locator('[data-entity-detail-chart="intraday"]')).toHaveAttribute("aria-pressed", "true");
  const colors = await page.locator("#entityDetailChart").evaluate((canvas) => {
    const pixels = canvas.getContext("2d").getImageData(0, 0, canvas.width, canvas.height).data;
    return new Set(Array.from({ length: Math.floor(pixels.length / 16) }, (_, index) => {
      const offset = index * 16;
      return `${pixels[offset]},${pixels[offset + 1]},${pixels[offset + 2]},${pixels[offset + 3]}`;
    })).size;
  });
  expect(colors).toBeGreaterThan(4);
  const offsets = await page.evaluate(() => [
    intradayMinuteOffset("09:30"),
    intradayMinuteOffset("11:30"),
    intradayMinuteOffset("13:00"),
    intradayMinuteOffset("15:00"),
  ]);
  expect(offsets).toEqual([0, 120, 120, 240]);
});

test("saved daily fortune opens in the reading dialog without regenerating", async ({ page }) => {
  await mockApplication(page, session("user", true));
  await page.goto("/index.html");
  await page.locator('[data-view="heavenView"]').first().click();
  await page.locator('[data-heaven-panel="fortune"]').click();
  await page.evaluate(() => {
    state.heavenSetup = { field: {}, chart: { available: true } };
    state.heavenInterpretations.fortune = {
      id: 31,
      mode: "fortune",
      context_date: "20260723",
      subject: "2026-07-23 观气",
      subject_detail: "丙午年 · 乙未月 · 己丑日 · 土气偏显",
      answer: Array.from({ length: 36 }, (_, index) => `第${index + 1}节：三层气机已经合参，今日宜先定节奏，再看行动。`).join("\n\n"),
      created_at: "2026-07-23T09:12:00+08:00",
    };
    updateHeavenInterpretationControls();
  });
  await expect(page.locator("#interpretFortuneButton")).toHaveText("已解运");
  const interpretRequests = [];
  page.on("request", (request) => {
    if (request.url().includes("/api/heaven/interpret")) interpretRequests.push(request.url());
  });
  await page.locator("#interpretFortuneButton").click();
  await expect(page.locator("#heavenReadingDialog")).toBeVisible();
  await expect(page.locator("#heavenReadingAnswer")).toContainText("今日宜先定节奏");
  const readingScroll = await page.locator("#heavenReadingCurrent").evaluate((element) => ({
    clientHeight: element.clientHeight,
    scrollHeight: element.scrollHeight,
    overflowY: getComputedStyle(element).overflowY,
  }));
  expect(readingScroll.scrollHeight).toBeGreaterThan(readingScroll.clientHeight);
  expect(readingScroll.overflowY).toBe("auto");
  await page.locator("#heavenReadingCurrent").evaluate((element) => { element.scrollTop = element.scrollHeight; });
  expect(await page.locator("#heavenReadingCurrent").evaluate((element) => element.scrollTop)).toBeGreaterThan(0);
  expect(interpretRequests).toHaveLength(0);
  await page.locator('[data-heaven-reading-tab="history"]').click();
  await expect(page.locator("#heavenReadingHistoryList .heaven-reading-history-item")).toHaveCount(1);
});

test("heaven reading loading uses the matching canvas scene and stops cleanly", async ({ page }) => {
  await mockApplication(page, session("user", true));
  await page.goto("/index.html");
  const cases = [
    ["trend", "hexagram"],
    ["fortune", "fortune"],
    ["heart", "hexagram"],
  ];
  for (const [mode, scene] of cases) {
    await page.evaluate(([readingMode]) => openHeavenReading(readingMode, { loading: true }), [mode]);
    const canvas = page.locator("#heavenReadingCanvas");
    await expect(canvas).toBeVisible();
    await expect(canvas).toHaveAttribute("data-scene", scene);
    await expect(canvas).toHaveAttribute("data-running", "true");
    await expect(canvas).toHaveAttribute("data-looping", "true");
    await page.waitForTimeout(180);
    const pixels = await canvas.evaluate((element) => {
      const context = element.getContext("2d");
      const data = context.getImageData(0, 0, element.width, element.height).data;
      const colors = new Set();
      const step = Math.max(4, Math.floor(data.length / 1200 / 4) * 4);
      for (let index = 0; index < data.length; index += step) {
        colors.add(`${data[index]},${data[index + 1]},${data[index + 2]},${data[index + 3]}`);
      }
      return { width: element.width, height: element.height, colors: colors.size };
    });
    expect(pixels.width).toBeGreaterThan(300);
    expect(pixels.height).toBeGreaterThan(300);
    expect(pixels.colors).toBeGreaterThan(3);
    await page.evaluate(() => { heavenReadingAnimation.startedAt = performance.now() - 13_100; });
    await expect(canvas).toHaveAttribute("data-cycle", "1");
    await expect(canvas).toHaveAttribute("data-running", "true");
    if (mode === "trend") {
      const completionMs = await page.evaluate(async () => {
        const started = performance.now();
        await heavenReadingAnimation.complete();
        return performance.now() - started;
      });
      expect(completionMs).toBeGreaterThanOrEqual(1600);
      expect(completionMs).toBeLessThan(2000);
      await expect(canvas).toHaveAttribute("data-running", "false");
    }
    await page.locator("#closeHeavenReadingDialog").click();
    await expect(canvas).toHaveAttribute("data-running", "false");
    await expect(canvas).toHaveAttribute("data-looping", "false");
  }
});

test("heart breathing prepares once then contracts on each exhale", async ({ page }) => {
  await mockApplication(page, session("user", true));
  await page.goto("/index.html");
  await page.locator('[data-view="heavenView"]').first().click();
  await page.locator('[data-heaven-panel="heart"]').click();
  await page.evaluate(() => startHeartBreathing());

  const timing = await page.evaluate(() => ({
    remaining: state.heartBreathingEndsAt - Date.now(),
    incenseNames: getComputedStyle(document.querySelector("#heartIncenseEmber")).animationName,
    incenseDurations: getComputedStyle(document.querySelector("#heartIncenseEmber")).animationDuration,
    incenseDelays: getComputedStyle(document.querySelector("#heartIncenseEmber")).animationDelay,
    rippleAnimation: getComputedStyle(document.querySelector(".heart-breath-ripple span")).animationName,
  }));
  expect(timing.remaining).toBeGreaterThan(45_000);
  expect(timing.remaining).toBeLessThanOrEqual(46_000);
  expect(timing.incenseNames).toContain("heart-incense-burn");
  expect(timing.incenseNames).toContain("heart-incense-glow");
  expect(timing.incenseDurations).toContain("45s");
  expect(timing.incenseDelays).toContain("1s");
  expect(timing.rippleAnimation).toBe("none");
  await expect(page.locator("#breathingScene")).toHaveAttribute("data-phase", "prepare");
  await expect(page.locator("#breathingPhase")).toHaveText("静");
  await expect(page.locator("#breathingSeconds, #breathingProgress, .breathing-orbit")).toHaveCount(0);

  await page.evaluate(() => {
    clearInterval(state.heartTimer);
    state.heartTimer = null;
    state.heartSeconds = 45;
    state.heartBreathingEndsAt = Date.now() + 44_500;
    updateBreathingDisplay();
  });
  await expect(page.locator("#breathingScene")).toHaveAttribute("data-phase", "inhale");
  await expect(page.locator("#breathingPhase")).toHaveText("吸");
  await expect(page.locator(".heart-breath-ripple span").first()).toHaveCSS("transition-duration", "3s");
  await expect(page.locator(".heart-breath-ripple span").first()).toHaveCSS("transform", /matrix\(1, 0, 0, 1,/);

  await page.evaluate(() => {
    state.heartSeconds = 42;
    state.heartBreathingEndsAt = Date.now() + 41_500;
    updateBreathingDisplay();
  });
  await expect(page.locator("#breathingScene")).toHaveAttribute("data-phase", "hold");
  await expect(page.locator("#breathingPhase")).toHaveText("顿");

  await page.evaluate(() => {
    state.heartSeconds = 39;
    state.heartBreathingEndsAt = Date.now() + 38_500;
    updateBreathingDisplay();
  });
  await expect(page.locator("#breathingScene")).toHaveAttribute("data-phase", "exhale");
  await expect(page.locator("#breathingPhase")).toHaveText("呼");
  await expect(page.locator(".heart-breath-ripple span").first()).toHaveCSS("transition-duration", "4s");
});

test("stylesheet layers do not repeat identical rules in the same cascade context", async ({ page }) => {
  await mockApplication(page, session("admin", true));
  await page.goto("/index.html");

  const duplicates = await page.evaluate(() => {
    const occurrences = new Map();
    const visitRules = (rules, href, context = []) => {
      for (const rule of Array.from(rules || [])) {
        if (typeof rule.selectorText === "string" && rule.style) {
          const key = `${context.join("\u0001")}\u0000${rule.selectorText}\u0000${rule.style.cssText}`;
          const rows = occurrences.get(key) || [];
          rows.push(href);
          occurrences.set(key, rows);
          continue;
        }
        if (!rule.cssRules) continue;
        const condition = rule.conditionText || rule.media?.mediaText || rule.name || "";
        visitRules(rule.cssRules, href, [...context, `${rule.type}:${condition}`]);
      }
    };
    for (const sheet of Array.from(document.styleSheets)) {
      const href = sheet.href ? new URL(sheet.href).pathname : "inline";
      visitRules(sheet.cssRules, href);
    }
    return Array.from(occurrences.entries())
      .filter(([, paths]) => new Set(paths).size > 1)
      .map(([rule, paths]) => ({ rule, paths }));
  });

  expect(duplicates).toEqual([]);
});

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();
  await expect(page.locator('[data-view="limitPool"]')).toHaveClass(/mobile-active/);
  const mobileShell = await page.evaluate(() => {
    const header = document.querySelector(".topbar").getBoundingClientRect();
    const main = document.querySelector(".app-main").getBoundingClientRect();
    return { headerBottom: header.bottom, mainTop: main.top };
  });
  expect(mobileShell.mainTop).toBeGreaterThanOrEqual(mobileShell.headerBottom - 1);
});

test("native dialogs share one lifecycle and success feedback stays content-sized", async ({ page }) => {
  await mockApplication(page, session("admin", true));
  await page.goto("/index.html");
  const dialogIds = [
    "strategyDrawer",
    "tradeLogDialog",
    "watchlistDialog",
    "heavenReadingDialog",
    "globalSearchDialog",
    "entityDetailDialog",
    "stockDialog",
    "alertsDialog",
    "assistantDialog",
    "settingsDialog",
    "adminDialog",
  ];

  await page.evaluate(() => {
    openModalDialog(document.querySelector("#tradeLogDialog"));
    openModalDialog(document.querySelector("#watchlistDialog"));
  });
  await expect(page.locator("dialog[open]")).toHaveCount(1);
  await expect(page.locator("#watchlistDialog")).toBeVisible();
  await expect(page.locator("#tradeLogDialog")).toBeHidden();
  await page.keyboard.press("Escape");
  await expect(page.locator("dialog[open]")).toHaveCount(0);

  await page.locator('[data-view="screenerView"]').first().click();
  await page.locator('[data-screener-mode="quant"]').click();

  for (const id of dialogIds) {
    await page.evaluate((dialogId) => openModalDialog(document.getElementById(dialogId)), id);
    await expect(page.locator(`#${id}`)).toBeVisible();
    await expect(page.locator(`#${id} button[aria-label*="关闭"]`).first()).toBeVisible();
    await page.keyboard.press("Escape");
    await expect(page.locator(`#${id}`)).toBeHidden();
  }

  await page.evaluate(() => showToast("交易记录已保存"));
  const toastBox = await page.locator("#toast").boundingBox();
  expect(toastBox.width).toBeLessThan(260);
  expect(toastBox.height).toBeLessThan(80);
});

test("new review workflows render account-scoped records", async ({ page }) => {
  await page.setViewportSize({ width: 1440, height: 900 });
  await mockApplication(page, session("user", true));
  await page.goto("/index.html");

  await page.locator('[data-view="screenerView"]').first().click();
  await expect(page.locator('[data-screener-step="regime"]')).toHaveAttribute("data-state", "complete");
  await page.locator('[data-screener-mode="quant"]').click();
  await page.locator("#openStrategyDrawerButton").click();
  await expect(page.locator("#strategyDrawer")).toBeVisible();
  await expect(page.locator("#strategyNameInput")).toBeFocused();
  await page.keyboard.press("Escape");
  await expect(page.locator("#strategyDrawer")).toBeHidden();
  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("#reviewDataDate")).toHaveText("2026-07-22");
  const reviewJournal = await page.locator("#reviewWorkspaceView .journal-section").boundingBox();
  const reviewLeft = await page.locator("#reviewWorkspaceView .review-left-stack").boundingBox();
  expect(Math.abs(reviewJournal.width - 360)).toBeLessThanOrEqual(1);
  expect(reviewJournal.x).toBeGreaterThan(reviewLeft.x + reviewLeft.width - 2);
  expect(Math.abs((reviewJournal.y + reviewJournal.height) - (reviewLeft.y + reviewLeft.height))).toBeLessThanOrEqual(1);
  await expect(page.locator("#watchlistTableBody tr")).toHaveCount(1);
  await expect(page.locator("#watchlistTableBody tr").first().locator("td")).toHaveCount(8);
  await expect(page.locator("#watchlistTableBody")).toContainText("+1.86");
  await expect(page.locator("#watchlistTableBody")).toContainText("+8.92");
  await expect(page.locator("#watchlistTableBody")).toContainText("72.4");
  await expect(page.locator("#watchlistTableBody")).toContainText("观察承接，不追高");
  const watchColumns = await page.locator("#reviewWorkspaceView .review-watchlist-table thead th").evaluateAll((headers) => ({
    widths: headers.map((header) => Math.round(header.getBoundingClientRect().width)),
    labels: headers.map((header) => header.textContent.replace(/[↕▲▼]/g, "").trim()),
    numericAligned: headers.filter((header) => header.classList.contains("num")).every((header) => getComputedStyle(header).textAlign === "right"),
  }));
  expect(watchColumns.labels).toEqual(["标记", "股票", "所属板块", "今日涨幅（%）", "5日涨幅（%）", "竞价关注（分）", "跟踪备注", "操作"]);
  expect(watchColumns.widths[6]).toBeGreaterThan(watchColumns.widths[3]);
  expect(watchColumns.numericAligned).toBe(true);
  await expect(page.locator("#journalSummary")).toHaveValue("缩量修复，主线仍待确认");
  await expect(page.locator("#journalContent")).toHaveValue("做对了等待确认。");
  await expect(page.locator("#journalPlan")).toHaveValue("只做有承接的核心。");
  const journalSpacing = await page.evaluate(() => {
    const textarea = document.querySelector("#journalContent");
    const label = textarea.previousElementSibling;
    const labelBox = label.getBoundingClientRect();
    const textareaBox = textarea.getBoundingClientRect();
    return { labelHeight: Math.round(labelBox.height), gap: Math.round(textareaBox.top - labelBox.bottom) };
  });
  expect(journalSpacing).toEqual({ labelHeight: 18, gap: 6 });
  await page.screenshot({ path: "test-results/review-stage17-1440.png", fullPage: true });
  await page.locator("#openWatchlistDialog").click();
  await expect(page.locator("#watchlistDialog")).toBeVisible();
  await page.locator("#watchlistSearchInput").fill("002141");
  await expect(page.locator("#watchlistSearchResults [data-watchlist-result]")).toHaveCount(1);
  await page.locator("#watchlistSearchResults [data-watchlist-result]").click();
  await expect(page.locator("#watchlistSelectionName")).toHaveText("Test Stock");
  await page.locator("#watchlistRemark").fill("等待放量确认");
  await page.locator("#saveWatchlist").click();
  await expect(page.locator("#watchlistDialog")).toBeHidden();
  await expect(page.locator("#tradeLogTableBody tr").first().locator("td")).toHaveCount(9);
  const tradeColumns = await page.locator("#reviewWorkspaceView .trade-log-table thead th").evaluateAll((headers) => ({
    widths: headers.map((header) => Math.round(header.getBoundingClientRect().width)),
    labels: headers.map((header) => header.textContent.replace(/[↕▲▼]/g, "").trim()),
  }));
  expect(tradeColumns.labels).toEqual(["日期", "股票", "动作", "仓位（%）", "盈亏（%）", "盈亏金额（元）", "情绪 / 标签", "交易复核", "操作"]);
  expect(tradeColumns.widths[7]).toBeGreaterThan(tradeColumns.widths[3]);
  expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)).toBeLessThanOrEqual(1);
  await expect(page.locator("#reviewHistoryPanel")).toBeHidden();
  await page.locator("#reviewHistoryToggle").click();
  await expect(page.locator("#reviewHistoryPanel")).toBeVisible();
  await expect(page.locator("#reviewHistoryToggle")).toHaveAttribute("aria-expanded", "true");
  const reviewHistoryOverflow = await page.evaluate(() => {
    const history = document.querySelector("#notesHistory");
    const seed = history.querySelector(".note-row");
    for (let index = 0; index < 18; index += 1) history.appendChild(seed.cloneNode(true));
    const main = document.querySelector(".app-main");
    return {
      mainClientHeight: main.clientHeight,
      mainScrollHeight: main.scrollHeight,
      mainOverflowY: getComputedStyle(main).overflowY,
      historyClientHeight: history.clientHeight,
      historyScrollHeight: history.scrollHeight,
      historyOverflowY: getComputedStyle(history).overflowY,
    };
  });
  expect(reviewHistoryOverflow.mainOverflowY).toBe("auto");
  expect(reviewHistoryOverflow.mainScrollHeight).toBeGreaterThan(reviewHistoryOverflow.mainClientHeight);
  expect(reviewHistoryOverflow.historyOverflowY).toBe("auto");
  expect(reviewHistoryOverflow.historyScrollHeight).toBeGreaterThan(reviewHistoryOverflow.historyClientHeight);
  await expect(page.locator("#tradeLogTableBody tr")).toHaveCount(1);
  const tradeScroll = await page.evaluate(() => {
    const seed = state.tradeEntries[0];
    state.tradeEntries = Array.from({ length: 24 }, (_, index) => ({ ...seed, id: index + 1 }));
    renderTradeLog();
    const frame = document.querySelector("#reviewWorkspaceView .trade-log-table-frame");
    return { scrollHeight: frame.scrollHeight, clientHeight: frame.clientHeight };
  });
  expect(tradeScroll.scrollHeight).toBeGreaterThan(tradeScroll.clientHeight);
  await expect(page.locator("#tradeLogEmpty")).toBeHidden();
  await expect(page.locator("#tradeLogForm")).toBeHidden();
  await page.locator("#openTradeLogDialog").click();
  await expect(page.locator("#tradeLogDialog")).toBeVisible();
  await page.locator("#tradeLogCode").fill("002141");
  await page.locator("#tradeLogName").fill("Test Stock");
  await page.locator("#tradeLogPrice").fill("10.8");
  await page.locator("#saveTradeLog").click();
  await expect(page.locator("#tradeLogDialog")).toBeHidden();
  await expect(page.locator("#reviewWorkspaceView")).toHaveClass(/active-view/);
  await expect(page.locator("#tradeLogTableBody tr")).toHaveCount(1);
  await page.locator('[data-trade-action="edit"]').click();
  await expect(page.locator("#tradeLogDialogTitle")).toHaveText("编辑交易日志");
  await page.locator("#cancelTradeEdit").click();
  await expect(page.locator("#tradeLogDialog")).toBeHidden();

  await page.setViewportSize({ width: 375, height: 812 });
  expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)).toBeLessThanOrEqual(1);
  const mobileLeft = await page.locator("#reviewWorkspaceView .review-left-stack").boundingBox();
  const mobileJournal = await page.locator("#reviewWorkspaceView .journal-section").boundingBox();
  expect(mobileJournal.y).toBeGreaterThanOrEqual(mobileLeft.y + mobileLeft.height - 2);
  await page.setViewportSize({ width: 1440, height: 900 });

  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("#assistantMemberGate")).toBeHidden();
  await expect(page.locator("#assistantMemberContent")).toHaveAttribute("aria-disabled", "false");
  await expect(page.locator("#assistantMessages .assistant-message")).toHaveCount(1);
  await expect(page.locator("#assistantQuestion")).toBeEnabled();
});

test("curated strategies and quant builder form independent screener workspaces", async ({ page }) => {
  await mockApplication(page, session("user", true));
  await page.goto("/index.html");
  await page.locator('[data-view="screenerView"]').first().click();

  await page.locator('[data-screener-mode="curated"]').click();
  await expect(page.locator('[data-screener-panel="smart"]')).toBeHidden();
  await expect(page.locator('[data-screener-panel="curated"]')).toBeVisible();
  await expect(page.locator("#curatedStrategyList .curated-strategy-card")).toHaveCount(1);
  await expect(page.locator("#curatedStrategyName")).toHaveText("连续分红质量");
  await expect(page.locator("#curatedFilterList .curated-rule-row")).toHaveCount(1);
  await expect(page.locator("#curatedRunButton")).toHaveCount(0);
  await expect(page.locator("#curatedHealthMetrics > div")).toHaveCount(4);

  await page.locator('[data-screener-mode="quant"]').click();
  await expect(page.locator('[data-screener-panel="curated"]')).toBeHidden();
  await expect(page.locator('[data-screener-panel="quant"]')).toBeVisible();
  await expect(page.locator("#quantFilterRows .quant-filter-row")).toHaveCount(2);
  await expect(page.locator("#quantScoreRows .quant-score-row")).toHaveCount(5);
  await expect(page.locator('#quantScoreRows input[data-quant-key="weight"]').first()).toHaveAttribute("type", "range");
  await expect(page.locator("#quantWeightTotal")).toHaveText("100%");
  await expect(page.locator("#quantRunButton")).toBeEnabled();

  await page.locator("#addQuantFilterButton").click();
  await expect(page.locator("#quantFilterRows .quant-filter-row")).toHaveCount(3);
  const lastScore = page.locator("#quantScoreRows .quant-score-row").last();
  await expect(lastScore.locator('[data-quant-action="direction"]')).toHaveText("数值越低越优");
  await lastScore.locator('[data-quant-action="direction"]').click();
  await expect(lastScore.locator('[data-quant-action="direction"]')).toHaveText("数值越高越优");
});

test("screener refresh restores every mode and deduplicates setup loading", async ({ page }) => {
  const candidate = (code, name) => ({
    code, name, sector: "测试板块", score_display: 80, historical_probability: 50,
    probability_samples: 20, pct_chg: 1, return_5d: 2, volume_ratio_5d: 1.2,
    sector_strength: 70, reason: "测试来源", risk_flags: [],
  });
  const result = (mode, runId, strategyName, row) => ({
    meta: {
      run_id: runId, trade_date: "20260722", regime: "repair",
      strategy_name: strategyName, mode,
    },
    candidates: [row],
    disclaimer: "历史统计不代表未来收益",
    backtest: null,
  });
  const options = {
    latestScreenerResults: {
      smart: result("smart", 61, "修复确认", candidate("600001", "阶段恢复")),
      curated: result("curated", 62, "连续分红质量", candidate("600002", "策略恢复")),
      quant: result("quant", 63, "自定义量化公式", candidate("600003", "量化恢复")),
    },
  };
  await mockApplication(page, session("user", true), options);
  await page.goto("/index.html?view=screenerView");
  await expect(page.locator("#screenerTableBody")).toContainText("阶段恢复");
  expect(options.screenerSetupRequests).toBe(1);

  await page.locator('[data-screener-mode="curated"]').click();
  await expect(page.locator("#curatedStrategyName")).toHaveText("连续分红质量");
  await expect(page.locator("#screenerTableBody")).toContainText("策略恢复");
  await page.locator('[data-screener-mode="quant"]').click();
  await expect(page.locator("#screenerTableBody")).toContainText("量化恢复");

  await page.reload();
  await expect(page.locator('[data-screener-mode="quant"]')).toHaveClass(/active/);
  await expect(page.locator("#screenerTableBody")).toContainText("量化恢复");
  expect(options.screenerSetupRequests).toBe(2);

  await page.locator("#quantRunButton").click();
  await expect.poll(() => options.screenerRunBodies?.length || 0).toBe(1);
  expect(options.screenerRunBodies[0].mode).toBe("quant");
});

test("screener redesign preserves three clear workspaces across desktop and mobile", async ({ page }) => {
  await page.setViewportSize({ width: 1440, height: 900 });
  await mockApplication(page, session("user", true));
  await page.goto("/index.html");
  await page.locator('[data-view="screenerView"]').first().click();

  await expect(page.locator("#screenerView .screener-page-bar")).toBeVisible();
  await expect(page.locator("#screenerView .screener-step")).toHaveCount(4);
  await expect(page.locator("#regimeLabel")).toHaveText("修复");
  await expect(page.locator("#screenerView .regime-temperature")).toHaveCount(0);
  await expect(page.locator("#regimeEvidenceList")).toContainText("情绪温度");
  const overviewCards = page.locator("#screenerView .screener-overview-card");
  await expect(overviewCards).toHaveCount(2);
  const [regimeBox, strategyBox] = await Promise.all([
    overviewCards.nth(0).boundingBox(),
    overviewCards.nth(1).boundingBox(),
  ]);
  expect(Math.abs(regimeBox.y - strategyBox.y)).toBeLessThanOrEqual(1);
  expect(strategyBox.x).toBeGreaterThan(regimeBox.x + regimeBox.width - 2);
  expect(regimeBox.height).toBeLessThanOrEqual(155);
  expect(strategyBox.height).toBeLessThanOrEqual(155);
  for (const line of await page.locator("#screenerView .step-line").all()) {
    expect((await line.boundingBox()).width).toBeLessThanOrEqual(42);
  }
  const [strategyDescriptionBox, strategyActionsBox] = await Promise.all([
    page.locator("#activeStrategyDescription").boundingBox(),
    page.locator(".screener-strategy-actions").boundingBox(),
  ]);
  expect(strategyActionsBox.y - (strategyDescriptionBox.y + strategyDescriptionBox.height)).toBeLessThanOrEqual(8);
  const trackingEntryStyle = await page.locator("#openScreenerTrackingButton").evaluate((element) => {
    const style = getComputedStyle(element);
    return { background: style.backgroundColor, weight: style.fontWeight };
  });
  expect(trackingEntryStyle.background).not.toBe("rgb(255, 255, 255)");
  expect(Number(trackingEntryStyle.weight)).toBeGreaterThanOrEqual(700);
  await page.evaluate(() => {
    setScreenerResult("smart", {
      meta: { run_id: 46, trade_date: "20260722", realtime: false, regime: "repair", strategy_name: "修复确认" },
      disclaimer: "历史统计不代表未来收益",
      candidates: [{
        code: "600000", ts_code: "600000.SH", name: "浦发银行", sector: "银行", price: 12,
        score_display: 82, historical_probability: 45, probability_samples: 30,
        pct_chg: 1.2, return_5d: 3.4, volume_ratio_5d: 1.5, sector_strength: 78,
        reason: "板块强度、相对强度", risk_flags: [],
      }],
      backtest: {
        samples: 35, win_rate: 8.6, average_3d_return: -5,
        average_drawdown: -11.71, definition: "收盘后选股，未来3日按统一阈值验证。",
      },
    }, { regime: "repair", strategyId: 1, strategyName: "修复确认" });
    renderScreenerResult();
  });
  const completedMarkers = page.locator('#screenerView .screener-step[data-state="complete"] .step-marker');
  await expect(completedMarkers).toHaveCount(3);
  await expect(page.locator("#backtestPanel")).toBeVisible();
  expect((await page.locator("#backtestPanel").boundingBox()).height).toBeLessThanOrEqual(55);
  await expect(page.locator("#backtestMetrics .dragon-metric span")).toHaveCount(4);
  await expect(page.locator("#screenerResultSource")).toHaveText("阶段选股 · 修复 · 修复确认");
  for (const metric of await page.locator("#backtestMetrics .dragon-metric").all()) {
    const box = await metric.boundingBox();
    expect(box.height).toBeLessThanOrEqual(40);
  }
  await page.screenshot({ path: "test-results/screener-stage15-phase-1440.png", fullPage: true });

  await page.evaluate(() => {
    state.screenerSetup.strategies.push({
      id: 4, name: "低波质量", description: "用第二套策略验证整卡选择交互。", regimes: ["repair"], builtin: true,
      data_ready: true, missing_data: [],
      formula: {
        meta: { library: "curated", category: "质量防守", quality: "A", frequency: "月度", risk: "低", data_group: "行情与质量" },
        universe: { exclude_st: true, listed_days_min: 720 },
        filters: [{ field: "return_20d", op: ">=", value: 0 }],
        score: [{ field: "relative_strength", weight: 1, direction: "desc" }], limit: 20, min_score: 0.5,
      },
    });
    renderCuratedStrategyLibrary();
  });
  await page.locator('[data-screener-mode="curated"]').click();
  await expect(page.locator("#curatedStrategyList .curated-strategy-card")).toHaveCount(2);
  await expect(page.locator(".curated-detail-pane")).toBeVisible();
  const secondStrategy = page.locator('#curatedStrategyList [data-curated-strategy="4"]');
  await secondStrategy.click();
  await expect(secondStrategy).toHaveClass(/active/);
  await expect(page.locator('#curatedStrategyList [data-curated-strategy="2"]')).not.toHaveClass(/active/);
  await expect(page.locator("#curatedStrategyName")).toHaveText("低波质量");
  const [libraryBox, detailBox] = await Promise.all([
    page.locator(".curated-library-pane").boundingBox(),
    page.locator(".curated-detail-pane").boundingBox(),
  ]);
  expect(detailBox.x).toBeGreaterThan(libraryBox.x + libraryBox.width - 2);
  expect(Math.abs(detailBox.y - libraryBox.y)).toBeLessThanOrEqual(1);
  expect(libraryBox.width).toBeLessThan(detailBox.width);
  await expect(page.locator("#curatedHealthMetrics > div")).toHaveCount(4);
  await page.screenshot({ path: "test-results/screener-stage15-strategy-1440.png", fullPage: true });

  await page.locator('[data-screener-mode="quant"]').click();
  await expect(page.locator("#quantScoreRows .quant-score-row")).toHaveCount(5);
  await expect(page.locator("#screenerView .quant-intro-band")).toHaveCount(0);
  await expect(page.getByText("执行设置", { exact: true })).toHaveCount(0);
  await expect(page.locator("#screenerResultTitle")).toHaveText("自定义选股结果");
  const [builderBox, summaryBox] = await Promise.all([
    page.locator(".quant-builder-pane").boundingBox(),
    page.locator(".quant-summary-pane").boundingBox(),
  ]);
  expect(Math.abs(builderBox.y - summaryBox.y)).toBeLessThanOrEqual(1);
  expect(summaryBox.x).toBeGreaterThan(builderBox.x + builderBox.width - 2);
  expect(builderBox.width).toBeGreaterThanOrEqual(490);
  expect(builderBox.width).toBeLessThanOrEqual(540);
  const quantRunBox = await page.locator("#quantRunButton").boundingBox();
  expect(quantRunBox.width).toBeLessThan(180);
  expect((await page.locator("#quantFilterRows .quant-filter-row select").first().boundingBox()).width).toBeLessThanOrEqual(225);
  await expect(page.locator('[data-screener-results-slot="quant"] > .screener-results-view')).toBeVisible();
  await page.screenshot({ path: "test-results/screener-stage15-quant-1440.png", fullPage: true });

  await page.setViewportSize({ width: 375, height: 812 });
  const overflow = await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth);
  expect(overflow).toBeLessThanOrEqual(1);
  await expect(page.locator("#screenerView .screener-mode-tabs")).toBeVisible();
});

test("automatic screener results stay read-only and mode results stay isolated", async ({ page }) => {
  await page.setViewportSize({ width: 1440, height: 900 });
  await mockApplication(page, session("user", true));
  await page.goto("/index.html");
  await page.locator('[data-view="screenerView"]').first().click();
  await page.evaluate(() => {
    state.screenerSetup.regimes.push({ id: "retreat", label: "退潮" });
    state.screenerSetup.strategies.push({
      id: 3, name: "退潮防守", description: "仅保留抗跌方向", regimes: ["retreat"], builtin: true,
      data_ready: true, missing_data: [],
      formula: { meta: { library: "smart" }, universe: {}, filters: [], score: [], limit: 10, min_score: 0.5 },
    });
    const candidate = (code, name) => ({
      code, name, sector: "测试板块", score_display: 80, historical_probability: 50,
      probability_samples: 20, pct_chg: 1, return_5d: 2, volume_ratio_5d: 1.2,
      sector_strength: 70, reason: "测试来源", risk_flags: [],
    });
    window.__screenerCandidate = candidate;
    setScreenerResult("smart", {
      meta: { run_id: 51, trade_date: "20260722", regime: "repair", strategy_name: "修复确认" },
      disclaimer: "历史统计不代表未来收益", candidates: [candidate("600001", "阶段结果")],
      backtest: { samples: 37, win_rate: 50, average_3d_return: 1, average_drawdown: -2, definition: "测试" },
    }, { regime: "repair", strategyId: 1, strategyName: "修复确认" });
    renderScreenerSetup();
  });

  await expect(page.locator('#screenerView .screener-step[data-state="complete"]')).toHaveCount(3);
  await expect(page.locator("#screenerTableBody")).toContainText("阶段结果");
  await expect(page.locator('[data-regime]')).toHaveCount(0);
  await expect(page.locator("#screenerRunButton")).toHaveCount(0);
  await expect(page.locator("#syncScreenerButton")).toHaveCount(0);
  await expect(page.locator("#changeStrategyButton")).toHaveCount(0);
  await expect(page.locator("#editStrategyButton")).toHaveCount(0);
  await page.locator('[data-screener-mode="curated"]').click();
  await expect(page.locator("#screenerEmpty")).toContainText("所选策略");
  await page.evaluate(() => {
    setScreenerResult("curated", {
      meta: { run_id: 52, trade_date: "20260722", regime: "repair", strategy_name: "连续分红质量" },
      disclaimer: "历史统计不代表未来收益", candidates: [window.__screenerCandidate("600002", "策略结果")],
    }, { regime: "repair", strategyId: 2, strategyName: "连续分红质量" });
    renderScreenerResult();
  });
  await expect(page.locator("#screenerTableBody")).toContainText("策略结果");
  await expect(page.locator("#screenerTableBody")).not.toContainText("阶段结果");
  await expect(page.locator("#screenerResultSource")).toHaveText("策略选股 · 连续分红质量");

  await page.locator('[data-screener-mode="quant"]').click();
  await expect(page.locator("#screenerEmpty")).toContainText("自定义选股");
  await page.evaluate(() => {
    setScreenerResult("quant", {
      meta: { run_id: 53, trade_date: "20260722", regime: "repair", strategy_name: "自定义量化公式" },
      disclaimer: "历史统计不代表未来收益", candidates: [window.__screenerCandidate("600003", "量化结果")],
    }, { regime: "repair", strategyName: "自定义量化公式" });
    renderScreenerResult();
  });
  await expect(page.locator("#screenerTableBody")).toContainText("量化结果");
  await expect(page.locator("#screenerResultSource")).toHaveText("自定义选股 · 自定义因子权重");

  await page.locator('[data-screener-mode="smart"]').click();
  await expect(page.locator("#screenerTableBody")).toContainText("阶段结果");
  await expect(page.locator("#screenerTableBody")).not.toContainText("策略结果");
  await expect(page.locator("#screenerTableBody")).not.toContainText("量化结果");
});

test("screener restores automatic stage and curated pools across switching and reload", async ({ page }) => {
  const formula = {
    meta: { library: "smart" }, universe: {}, filters: [],
    score: [{ field: "relative_strength", weight: 1, direction: "desc" }],
    limit: 10, min_score: 0.5,
  };
  const candidate = (code, name) => ({
    code, name, sector: "Test Sector", score_display: 80,
    historical_probability: 50, probability_samples: 20, pct_chg: 1,
    return_5d: 2, volume_ratio_5d: 1.2, sector_strength: 70,
    reason: "Context result", risk_flags: [],
  });
  const result = (mode, runId, strategyName, row) => ({
    meta: {
      run_id: runId, trade_date: "20260722", regime: "repair",
      strategy_name: strategyName, mode,
    },
    candidates: [row],
    disclaimer: "Historical statistics do not predict future returns.",
    backtest: null,
  });
  const smartResult = result("smart", 101, "修复确认", candidate("600001", "Smart Repair"));
  const curatedA = result("curated", 102, "连续分红质量", candidate("600002", "Curated A"));
  const curatedB = result("curated", 103, "Quality B", candidate("600003", "Curated B"));
  const options = {
    latestScreenerResults: { smart: smartResult, curated: curatedA },
    recentScreenerResults: [smartResult, curatedA, curatedB],
    additionalScreenerStrategies: [
      {
        id: 4, name: "Quality B", description: "Second curated strategy",
        regimes: ["repair"], builtin: true, data_ready: true, missing_data: [],
        formula: {
          ...formula,
          meta: { library: "curated", category: "Quality", quality: "A", frequency: "Monthly", risk: "Low" },
        },
      },
    ],
  };

  await mockApplication(page, session("user", true), options);
  await page.goto("/index.html?view=screenerView");

  await expect(page.locator("#screenerTableBody")).toContainText("Smart Repair");
  await expect(page.locator("#screenerRunButton")).toHaveCount(0);

  await page.locator('[data-screener-mode="curated"]').click();
  await expect(page.locator("#screenerTableBody")).toContainText("Curated A");
  await page.locator('[data-curated-strategy="4"]').click();
  await expect(page.locator("#screenerTableBody")).toContainText("Curated B");
  await page.locator('[data-curated-strategy="2"]').click();
  await expect(page.locator("#screenerTableBody")).toContainText("Curated A");
  expect(options.screenerRunBodies || []).toHaveLength(0);

  await page.reload();
  await page.locator('[data-screener-mode="smart"]').click();
  await expect(page.locator("#screenerTableBody")).toContainText("Smart Repair");
  await page.locator('[data-screener-mode="curated"]').click();
  await expect(page.locator("#screenerTableBody")).toContainText("Curated A");
  await page.locator('[data-curated-strategy="4"]').click();
  await expect(page.locator("#screenerTableBody")).toContainText("Curated B");
});

test("screener tracking is an internal page populated only by manual candidate actions", async ({ page }) => {
  await page.setViewportSize({ width: 1440, height: 900 });
  await mockApplication(page, session("user", true));
  await page.goto("/index.html");
  await page.locator('[data-view="screenerView"]').first().click();
  await page.evaluate(() => {
    setScreenerResult("smart", {
      meta: { run_id: 45, trade_date: "20260722", realtime: false, regime: "repair", strategy_name: "修复确认" },
      disclaimer: "历史统计不代表未来收益",
      candidates: [{
        code: "600000", ts_code: "600000.SH", name: "浦发银行", sector: "银行", price: 12,
        score_display: 82, historical_probability: 45, probability_samples: 30,
        pct_chg: 1.2, return_5d: 3.4, volume_ratio_5d: 1.5, sector_strength: 78,
        reason: "板块强度、相对强度", risk_flags: [],
      }],
    }, { regime: "repair", strategyId: 1, strategyName: "修复确认" });
    renderScreenerResult();
  });

  const addButton = page.locator('[data-add-tracking="600000"]');
  await expect(addButton).toHaveText("加入跟踪");
  await addButton.click();
  await expect(page.locator('[data-add-tracking="600000"]')).toHaveText("已跟踪");

  await page.locator("#openScreenerTrackingButton").click();
  await expect(page.locator("#screenerTrackingView")).toHaveClass(/active-view/);
  await expect(page.locator('[data-view="screenerView"]').first()).toHaveClass(/active/);
  await expect(page.locator("#trackingTableBody tr")).toHaveCount(2);
  await expect(page.locator('[data-view="screenerTrackingView"]')).toHaveCount(0);
  await page.screenshot({ path: "test-results/screener-stage15-tracking-1440.png", fullPage: true });

  page.once("dialog", (dialog) => dialog.accept());
  await page.locator('[data-remove-tracking="10"]').click();
  await expect(page.locator("#trackingTableBody tr")).toHaveCount(1);
  await page.locator("#closeScreenerTrackingButton").click();
  await expect(page.locator("#screenerView")).toHaveClass(/active-view/);
});

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("#openTradeLogDialog")).toBeVisible();
    await expect(page.locator("#tradeLogForm")).toBeHidden();
    await page.locator("#openTradeLogDialog").click();
    await expect(page.locator("#tradeLogDialog")).toBeVisible();
    await expect(page.locator("#tradeLogForm")).toBeVisible();
    const dialogWidth = await page.locator("#tradeLogDialog").evaluate((dialog) => dialog.getBoundingClientRect().width);
    expect(dialogWidth).toBeLessThanOrEqual(viewport.width);
    await page.locator("#closeTradeLogDialog").click();
    await page.evaluate(() => {
      state.heavenInterpretations.fortune = {
        id: 31,
        subject: "2026-07-23 观气",
        subject_detail: "丙午年 · 乙未月 · 己丑日",
        answer: "当日解运结果",
        context_date: "20260723",
        created_at: "2026-07-23T09:12:00+08:00",
      };
      openHeavenReading("fortune", { loading: false });
    });
    await expect(page.locator("#heavenReadingDialog")).toBeVisible();
    const readingWidth = await page.locator("#heavenReadingDialog").evaluate((dialog) => dialog.getBoundingClientRect().width);
    expect(readingWidth).toBeLessThanOrEqual(viewport.width);
    await page.locator("#closeHeavenReadingDialog").click();
  });
}

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");
});

test("heaven workspace actions remain compact and do not overlap", async ({ page }) => {
  await page.setViewportSize({ width: 1440, height: 900 });
  await mockApplication(page, session("user", true));
  await page.goto("/index.html");
  await page.locator('[data-view="heavenView"]').first().click();

  const titleSize = Number.parseFloat(await page.locator("#heavenView .wt-title-line h1").evaluate((element) => getComputedStyle(element).fontSize));
  expect(titleSize).toBeLessThanOrEqual(32);

  const trendButtons = page.locator(".heaven-trend-actions .button");
  await expect(trendButtons).toHaveCount(3);
  for (let index = 0; index < await trendButtons.count(); index += 1) {
    const box = await trendButtons.nth(index).boundingBox();
    expect(box.width).toBeLessThanOrEqual(130);
    expect(box.height).toBeLessThanOrEqual(44);
  }
  await page.locator('[data-heaven-panel="fortune"]').click();
  const fortuneActions = await page.locator("#heavenFortunePanel .fortune-heading-actions").boundingBox();
  const fortunePanel = await page.locator("#heavenFortunePanel").boundingBox();
  expect(fortuneActions.x + fortuneActions.width).toBeLessThanOrEqual(fortunePanel.x + fortunePanel.width + 1);
  await page.evaluate(() => {
    state.heavenInterpretations.fortune = {
      id: 31,
      mode: "fortune",
      context_date: "20260723",
      subject: "2026-07-23 观气",
      subject_detail: "丙午年 · 乙未月 · 己丑日",
      answer: "三层气机已经合参，今日宜先定节奏，再看行动。",
      created_at: "2026-07-23T09:12:00+08:00",
    };
    openHeavenReading("fortune", { loading: false });
  });
  const readingDialog = await page.locator("#heavenReadingDialog").boundingBox();
  expect(readingDialog.width).toBeLessThanOrEqual(1120);
  expect(readingDialog.width / readingDialog.height).toBeGreaterThan(1.4);
  expect(readingDialog.height).toBeLessThanOrEqual(820);
  const readingHeader = await page.locator("#heavenReadingDialog .dialog-header").boundingBox();
  const readingTabs = await page.locator("#heavenReadingDialog .heaven-reading-tabs").boundingBox();
  expect(readingHeader.y + readingHeader.height).toBeLessThanOrEqual(readingTabs.y + 1);
  await page.locator("#closeHeavenReadingDialog").click();

  await page.locator('[data-heaven-panel="heart"]').click();
  const heartControls = await page.locator(".heart-toolbar-controls").boundingBox();
  const heartPanel = await page.locator("#heavenHeartPanel").boundingBox();
  expect(heartControls.width).toBeLessThanOrEqual(190);
  expect(heartControls.x + heartControls.width).toBeLessThanOrEqual(heartPanel.x + heartPanel.width + 1);
  const historyBox = await page.locator("#historyHeartButton").boundingBox();
  const soundBox = await page.locator("#heartSoundToggle").boundingBox();
  expect(historyBox.width).toBeLessThanOrEqual(100);
  expect(historyBox.x + historyBox.width).toBeLessThanOrEqual(soundBox.x);

});

test("heaven workspace controls fit a narrow viewport", async ({ page }) => {
  await page.setViewportSize({ width: 375, height: 812 });
  await mockApplication(page, session("user", true));
  await page.goto("/index.html");
  await page.locator('[data-view="heavenView"]').first().click();
  expect(await page.evaluate(() => window.scrollY)).toBe(0);

  const trendActions = await page.locator(".heaven-trend-actions").boundingBox();
  expect(trendActions.width).toBeLessThanOrEqual(347);
  expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)).toBeLessThanOrEqual(1);
  await page.locator('[data-heaven-panel="fortune"]').click();
  expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)).toBeLessThanOrEqual(1);
  const fortuneButtons = page.locator("#heavenFortunePanel .fortune-heading-actions .button");
  const firstFortuneButton = await fortuneButtons.first().boundingBox();
  const secondFortuneButton = await fortuneButtons.last().boundingBox();
  expect(Math.abs(firstFortuneButton.width - secondFortuneButton.width)).toBeLessThanOrEqual(1);
  expect(Math.abs(firstFortuneButton.y - secondFortuneButton.y)).toBeLessThanOrEqual(1);
  await page.locator('[data-heaven-panel="heart"]').click();
  const heartControls = await page.locator(".heart-toolbar-controls").boundingBox();
  const heartPanel = await page.locator("#heavenHeartPanel").boundingBox();
  expect(heartControls.width).toBeLessThanOrEqual(heartPanel.width);
  expect(heartControls.x).toBeGreaterThanOrEqual(heartPanel.x - 1);
  expect(heartControls.x + heartControls.width).toBeLessThanOrEqual(375);
  expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)).toBeLessThanOrEqual(1);
});

test("mentor directory exposes evidence filters and private owner metadata", async ({ page }) => {
  await page.setViewportSize({ width: 1440, height: 900 });
  await mockApplication(page, session("admin", true));
  await page.goto("/index.html");
  await page.locator('[data-view="mentorView"]').first().click();

  await expect(page.locator("#mentorView .mentor-page-header .mentor-evidence-filters")).toBeVisible();
  await expect(page.locator("#mentorList .mentor-option")).toHaveCount(22);
  await expect(page.locator('#mentorList [data-mentor-id="private-owner"] .mentor-badge.private')).toContainText("仅自己");
  await expect(page.locator('#mentorList [data-mentor-id="source-a"] .mentor-badge.grade-a')).toHaveText("A");
  await expect(page.locator('#mentorList [data-mentor-id="source-c"] .mentor-badge.quality')).toHaveCount(0);

  await page.locator("#mentorSearchInput").fill("行为推演");
  await expect(page.locator("#mentorList .mentor-option")).toHaveCount(1);
  await expect(page.locator("#mentorCount")).toHaveText("1 / 22 位");
  await page.locator("#mentorSearchInput").fill("");
  await page.locator('[data-mentor-grade="B"]').click();
  await expect(page.locator("#mentorList .mentor-option")).toHaveCount(7);
  await page.locator('#mentorList [data-mentor-id="source-b"]').click();
  await expect(page.locator("#activeMentorName")).toHaveText("多源老师");
  await expect(page.locator("#activeMentorBadges")).toHaveText("B");
  await expect(page.locator("#activeMentorEvidence")).toHaveText("公开访谈与多源材料");
  const mentorLibrary = await page.locator("#mentorView .mentor-sidebar").boundingBox();
  const mentorChat = await page.locator("#mentorView .mentor-chat-panel").boundingBox();
  const mentorInput = await page.locator("#mentorQuestion").boundingBox();
  expect(Math.abs(mentorLibrary.width - 340)).toBeLessThanOrEqual(1);
  expect(mentorChat.x - (mentorLibrary.x + mentorLibrary.width)).toBeGreaterThanOrEqual(11);
  expect(mentorInput.height).toBeLessThanOrEqual(40);
  expect(await page.evaluate(() => document.documentElement.scrollHeight - window.innerHeight)).toBeLessThanOrEqual(1);
  await page.setViewportSize({ width: 1920, height: 947 });
  const expandedMentorLayout = await page.locator("#mentorView .mentor-layout").boundingBox();
  const statusBar = await page.locator(".status-bar").boundingBox();
  const lowerGap = statusBar.y - (expandedMentorLayout.y + expandedMentorLayout.height);
  expect(lowerGap).toBeGreaterThanOrEqual(0);
  expect(lowerGap).toBeLessThanOrEqual(8);
  await page.locator("#overviewToggle").click();
  const openOverviewLayout = await page.locator("#mentorView .mentor-layout").boundingBox();
  const openOverviewGap = statusBar.y - (openOverviewLayout.y + openOverviewLayout.height);
  expect(openOverviewGap).toBeGreaterThanOrEqual(0);
  expect(openOverviewGap).toBeLessThanOrEqual(8);
  expect(await page.evaluate(() => document.documentElement.scrollHeight - window.innerHeight)).toBeLessThanOrEqual(1);
});

test("mentor pins, custom order and streamed replies work together", async ({ page }) => {
  await mockApplication(page, session("admin", true));
  await page.goto("/index.html");
  await page.locator('[data-view="mentorView"]').first().click();

  await page.locator('[data-mentor-pin="source-c"]').click();
  await expect(page.locator("#mentorList [data-mentor-card]").first()).toHaveAttribute("data-mentor-card", "source-c");
  await page.locator('[data-mentor-pin="source-b"]').click();
  await expect(page.locator("#mentorList [data-mentor-card]").first()).toHaveAttribute("data-mentor-card", "source-b");

  await page.locator("#mentorSortToggle").click();
  await page.locator('[data-mentor-target="source-b"][data-mentor-move="down"]').click();
  await expect(page.locator("#mentorList [data-mentor-card]").first()).toHaveAttribute("data-mentor-card", "source-c");

  await page.locator('[data-mentor-id="source-c"]').click();
  await page.locator("#mentorQuestion").fill("现在怎么看？");
  await page.locator("#sendMentorQuestion").click();
  const answer = page.locator("#mentorMessages .mentor-message.assistant").last();
  await expect(answer).toContainText("先看市场结构。");
  await expect(answer.locator(".mentor-answer-list li")).toHaveCount(2);
  await expect(answer.locator("br")).toHaveCount(0);
  await expect(page.locator("#mentorMessages .assistant-stream-caret")).toHaveCount(0);
  await page.locator("#themeToggle").click();
  const darkMessageStyle = await answer.evaluate((element) => {
    const style = getComputedStyle(element);
    const content = element.querySelector(".mentor-message-content");
    const heading = element.querySelector(".mentor-answer-heading");
    const label = element.querySelector(".mentor-message-label");
    const meta = element.querySelector("small");
    return {
      background: style.backgroundColor,
      border: style.borderTopColor,
      shadow: style.boxShadow,
      contentColor: getComputedStyle(content).color,
      headingColor: getComputedStyle(heading).color,
      labelColor: getComputedStyle(label).color,
      metaColor: getComputedStyle(meta).color,
    };
  });
  expect(darkMessageStyle.background).not.toBe("rgb(255, 255, 255)");
  expect(darkMessageStyle.border).not.toBe("rgb(255, 255, 255)");
  expect(darkMessageStyle.shadow).toBe("none");
  expect(darkMessageStyle.contentColor).toBe("rgb(232, 234, 237)");
  expect(darkMessageStyle.headingColor).toBe("rgb(232, 234, 237)");
  expect(darkMessageStyle.labelColor).toBe("rgb(127, 137, 147)");
  expect(darkMessageStyle.metaColor).toBe("rgb(127, 137, 147)");
});

test("mobile mentor directory opens as a searchable selector and hides private mentors", async ({ page }) => {
  await page.setViewportSize({ width: 375, height: 812 });
  await mockApplication(page, session("user", true));
  await page.goto("/index.html");
  await page.locator('[data-view="mentorView"]').first().click();

  await expect(page.locator("#mentorDirectoryToggle")).toBeVisible();
  await page.locator("#mentorDirectoryToggle").click();
  await expect(page.locator("#mentorView .mentor-sidebar")).toHaveClass(/is-open/);
  await expect(page.locator("#mentorList .mentor-option")).toHaveCount(21);
  await expect(page.locator('#mentorList [data-mentor-id="private-owner"]')).toHaveCount(0);
  await page.locator('#mentorList [data-mentor-id="source-c"]').click();
  await expect(page.locator("#mentorView .mentor-sidebar")).not.toHaveClass(/is-open/);
  await expect(page.locator("#mobileActiveMentorName")).toHaveText("推演老师");
  expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)).toBeLessThanOrEqual(1);
});

test("global dialogs share the stage 18 geometry without changing account or admin access", async ({ page }) => {
  await page.setViewportSize({ width: 1440, height: 900 });
  await mockApplication(page, session("admin", true));
  await page.goto("/index.html");

  await page.keyboard.press("Control+K");
  await expect(page.locator("#globalSearchDialog")).toHaveAttribute("aria-labelledby", "globalSearchTitle");
  expect((await page.locator("#globalSearchDialog").boundingBox()).width).toBeLessThanOrEqual(662);
  await page.locator("#closeGlobalSearch").click();

  await page.evaluate(() => openStock("002141", { code: "002141", name: "贤丰控股", sector: "元件" }));
  const stockGeometry = await page.locator("#stockDialog").evaluate((dialog) => ({
    width: dialog.getBoundingClientRect().width,
    height: dialog.getBoundingClientRect().height,
    overflowY: getComputedStyle(dialog).overflowY,
    headerPosition: getComputedStyle(dialog.querySelector(".dialog-header")).position,
    documentOverflow: document.documentElement.scrollWidth - window.innerWidth,
  }));
  expect(stockGeometry.width).toBeLessThanOrEqual(812);
  expect(stockGeometry.height).toBeLessThanOrEqual(878);
  expect(stockGeometry.overflowY).toBe("auto");
  expect(stockGeometry.headerPosition).toBe("sticky");
  expect(stockGeometry.documentOverflow).toBeLessThanOrEqual(1);
  await page.locator("#closeStockDialog").click();

  await page.locator("#alertButton").click();
  await expect(page.locator("#alertsDialog")).toHaveAttribute("aria-labelledby", "alertsDialogTitle");
  expect((await page.locator("#alertsDialog").boundingBox()).width).toBeLessThanOrEqual(722);
  await page.locator("#closeAlertsDialog").click();

  await page.locator("#assistantButton").click();
  const assistantGeometry = await page.locator("#assistantDialog").evaluate((dialog) => ({
    height: dialog.getBoundingClientRect().height,
    overflowY: getComputedStyle(dialog).overflowY,
    contentDisplay: getComputedStyle(dialog.querySelector(".assistant-member-content")).display,
  }));
  expect(assistantGeometry.height).toBeLessThanOrEqual(762);
  expect(assistantGeometry.overflowY).toBe("hidden");
  expect(assistantGeometry.contentDisplay).toBe("flex");
  await page.locator("#closeAssistantDialog").click();

  await page.locator("#accountButton").click();
  await page.locator('[data-account-panel="membership"]').click();
  await expect(page.locator("#settingsDialog")).toHaveAttribute("aria-labelledby", "accountDialogTitle");
  await expect(page.locator("#accountDialogTitle")).toHaveText("会员状态");
  const accountBox = await page.locator("#settingsDialog").boundingBox();
  expect(accountBox.width).toBeLessThanOrEqual(722);
  expect(Math.abs(accountBox.x + accountBox.width / 2 - 720)).toBeLessThanOrEqual(2);
  expect(Math.abs(accountBox.y + accountBox.height / 2 - 450)).toBeLessThanOrEqual(2);
  await page.locator("#closeSettingsDialog").click();

  await page.locator("#settingsButton").click();
  await expect(page.locator("#adminDialog")).toHaveAttribute("aria-labelledby", "adminDialogTitle");
  await expect(page.locator("#adminSectionSelect")).toBeVisible();
  const adminBox = await page.locator("#adminDialog").boundingBox();
  expect(adminBox.width).toBeLessThanOrEqual(902);
  expect(Math.abs(adminBox.x + adminBox.width / 2 - 720)).toBeLessThanOrEqual(2);
  expect(Math.abs(adminBox.y + adminBox.height / 2 - 450)).toBeLessThanOrEqual(2);
  await page.locator("#closeAdminDialog").click();

  await page.setViewportSize({ width: 375, height: 812 });
  await page.locator("#alertButton").click();
  const mobileGeometry = await page.locator("#alertsDialog").evaluate((dialog) => ({
    width: dialog.getBoundingClientRect().width,
    documentOverflow: document.documentElement.scrollWidth - window.innerWidth,
  }));
  expect(mobileGeometry.width).toBeLessThanOrEqual(375);
  expect(mobileGeometry.documentOverflow).toBeLessThanOrEqual(1);
});
