Files
xiaobaifupan/tests/e2e/app-shell.spec.js
T

570 lines
26 KiB
JavaScript

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()) {
await page.route("**/api/**", async (route) => {
const url = new URL(route.request().url());
let payload = { ok: true };
if (url.pathname === "/api/auth/me") payload = authSession;
else if (url.pathname === "/api/dashboard") payload = dashboard;
else if (url.pathname === "/api/stock/002141/preview") {
payload = {
meta: { trade_date: "2026-07-23", realtime: false, 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: [],
};
} else if (url.pathname === "/api/watchlist" || url.pathname === "/api/notes") payload = { items: [] };
else if (url.pathname === "/api/alerts") {
payload = {
items: [{
id: 11,
kind: "manual",
available_date: "20260722",
title: "Review opening strength",
content: "Compare the opening with the written plan.",
code: "002141",
is_read: false,
due: true,
}],
unread_count: 1,
};
} else if (url.pathname === "/api/trades") {
payload = {
items: [{
id: 7,
trade_date: "20260722",
code: "002141",
name: "Test Stock",
action: "buy",
action_label: "Buy",
price: 10.2,
quantity: 1000,
position_pct: 20,
pnl_amount: null,
pnl_pct: null,
emotion: "calm",
emotion_label: "Calm",
tags: ["planned"],
thesis: "Strength confirmed after the open.",
execution: "Executed within the planned range.",
}],
summary: { total: 1, realized: 0, win_rate: null, pnl_amount: null, average_position: 20 },
};
} else if (url.pathname === "/api/assistant/messages") {
payload = {
items: [{ role: "assistant", content: "Review evidence before forming a conclusion.", context_date: "20260722" }],
};
} else if (url.pathname === "/api/search") payload = { groups: { stocks: [], sectors: [], themes: [], indices: [] } };
else if (url.pathname === "/api/dragon-tiger") {
payload = {
meta: { trade_date: "2026-07-22", requested_date: "2026-07-22", status: "empty", source: "tushare" },
summary: {},
traders: [],
unclassified_seats: [],
};
} else if (url.pathname === "/api/sentiment/history") payload = { rows: [], components: [] };
else if (url.pathname === "/api/rotation/history") payload = { days: [], rows: [], sectors: [] };
else if (url.pathname === "/api/screener/setup") {
payload = {
trade_date: "20260722",
regime: { id: "repair", label: "修复", confidence: 70, reason: "测试", evidence: [] },
regimes: [{ id: "repair", label: "修复" }],
factor_data: { ready: false, date_count: 0 },
llm: { configured: true },
strategies: [],
};
} else if (url.pathname === "/api/screener/tracking") {
payload = {
batches: [{
selection_date: "20260721",
strategy_name: "Repair confirmation",
items: [{
code: "002141",
name: "Test Stock",
entry_price: 10,
t1_open: 1.2,
t1_close: 2.1,
t3_close: null,
t5_close: null,
max_gain: 3.4,
max_drawdown: -1.1,
observed_days: 1,
status: "tracking",
}],
}],
summary: { total: 1, observed: 1, t1_win_rate: 100, t5_win_rate: null, average_t5: null },
};
} else if (url.pathname === "/api/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 = [
"sentimentCycleView", "limitPool", "brokenView", "downView", "yesterdayView",
"performanceView", "ladderView", "rotationView", "dragonView", "screenerView",
"mentorView", "heavenView", "reviewWorkspaceView",
];
for (const view of views) {
await page.locator(`[data-view="${view}"]`).first().click();
await expect(page.locator(`#${view}`)).toHaveClass(/active-view/);
}
await page.keyboard.press("Control+K");
await expect(page.locator("#globalSearchDialog")).toBeVisible();
await expect(page.locator("#globalSearchInput")).toBeFocused();
});
test("regular account cannot see admin controls and member features are gated", async ({ page }) => {
await mockApplication(page, session("user", false));
await page.goto("/index.html");
await expect(page.locator("#settingsButton")).toBeHidden();
await expect(page.locator("#syncButton")).toBeHidden();
await expect(page.locator("#accountVipLabel")).toHaveText("非会员");
await page.locator('[data-view="screenerView"]').first().click();
await expect(page.locator("#screenerView .member-gate")).toBeVisible();
await expect(page.locator("#screenerRunButton")).toBeDisabled();
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");
});
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: "三层气机已经合参,今日宜先定节奏,再看行动。",
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("今日宜先定节奏");
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("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();
});
test("new review workflows render account-scoped records", async ({ page }) => {
await mockApplication(page, session("user", true));
await page.goto("/index.html");
await page.locator('[data-view="screenerView"]').first().click();
await expect(page.locator("#trackingTableBody tr")).toHaveCount(1);
await expect(page.locator("#trackingEmpty")).toBeHidden();
await page.locator('[data-view="reviewWorkspaceView"]').first().click();
await expect(page.locator("#tradeLogTableBody tr")).toHaveCount(1);
await expect(page.locator("#tradeLogEmpty")).toBeHidden();
await 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.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();
});
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 .heaven-toolbar h2").evaluate((element) => getComputedStyle(element).fontSize));
expect(titleSize).toBeLessThanOrEqual(22);
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(920);
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();
expect(heartControls.width).toBeLessThanOrEqual(80);
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("#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')).toHaveText("5/6");
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")).toContainText("B · 多源整理");
await expect(page.locator("#activeMentorEvidence")).toHaveText("公开访谈与多源材料");
});
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);
});
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);
});