chore: create Multica handoff checkpoint

This commit is contained in:
leefer
2026-08-06 22:54:46 +08:00
parent de635e4f9c
commit d95bcb523a
51 changed files with 3054 additions and 2883 deletions
+105 -31
View File
@@ -491,7 +491,12 @@ async function mockApplication(page, authSession = session(), options = {}) {
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: "meta",
data_trade_date: "20260722",
notice: "",
follow_ups: ["哪些信号代表确认?", "这个判断在什么情况下失效?", "空仓时应该先观察什么?"],
}),
JSON.stringify({ type: "done" }),
].join("\n"),
});
@@ -1931,8 +1936,19 @@ test("mobile shell stays within the viewport", async ({ page }) => {
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("#globalSearchButton")).toBeHidden();
await expect(page.locator("#headerMenuButton")).toBeVisible();
await page.locator("#headerMenuButton").click();
await expect(page.locator("#headerCommandGroup")).toBeVisible();
await expect(page.locator('[data-mobile-command-target="globalSearchButton"]')).toBeVisible();
await page.keyboard.press("Escape");
await expect(page.locator("#headerCommandGroup")).toBeHidden();
await expect(page.locator(".module-nav .mobile-primary-tab:visible")).toHaveCount(5);
await expect(page.locator('[data-view="limitPool"]')).toHaveClass(/mobile-active/);
for (const entry of await page.locator(".module-nav .mobile-primary-tab:visible").all()) {
const box = await entry.boundingBox();
expect(box.height).toBeGreaterThanOrEqual(44);
}
const mobileShell = await page.evaluate(() => {
const header = document.querySelector(".app-header").getBoundingClientRect();
const main = document.querySelector(".app-main").getBoundingClientRect();
@@ -1941,6 +1957,69 @@ test("mobile shell stays within the viewport", async ({ page }) => {
expect(mobileShell.mainTop).toBeGreaterThanOrEqual(mobileShell.headerBottom - 1);
});
test("mobile shell remains usable at supported narrow widths", async ({ page }) => {
await page.setViewportSize({ width: 430, height: 932 });
await mockApplication(page, session("user", true));
await page.goto("/index.html");
const registeredViews = await page.evaluate(() => window.XiaobaiPages.pages.map((entry) => entry.id));
for (const viewport of [
{ width: 430, height: 932 },
{ width: 390, height: 844 },
{ width: 320, height: 720 },
]) {
await page.setViewportSize(viewport);
await expect(page.locator("body")).toHaveClass(/mobile-shell/);
for (const theme of ["light", "dark"]) {
for (const view of registeredViews) {
await page.evaluate(({ targetView, targetTheme }) => {
document.documentElement.dataset.theme = targetTheme;
openView(targetView);
}, { targetView: view, targetTheme: theme });
const geometry = await page.evaluate(() => ({
overflow: document.documentElement.scrollWidth - innerWidth,
visibleEntries: Array.from(document.querySelectorAll(".module-nav .mobile-primary-tab"))
.filter((entry) => getComputedStyle(entry).display !== "none").length,
smallestTarget: Math.min(...Array.from(document.querySelectorAll(".module-nav .mobile-primary-tab"))
.filter((entry) => getComputedStyle(entry).display !== "none")
.map((entry) => entry.getBoundingClientRect().height)),
}));
expect(geometry.overflow, `${view} overflows at ${viewport.width}px in ${theme} theme`).toBeLessThanOrEqual(1);
expect(geometry.visibleEntries).toBe(5);
expect(geometry.smallestTarget).toBeGreaterThanOrEqual(44);
}
}
await page.evaluate(() => openView("ladderView"));
await expect(page.locator("#mobileMarketSelector")).toBeVisible();
await expect(page.locator("#mobileMarketViewSelect option")).toHaveCount(12);
expect(await page.evaluate(() => document.documentElement.scrollWidth - innerWidth)).toBeLessThanOrEqual(1);
}
});
test("768px boundary uses the compact desktop shell", async ({ page }) => {
await page.setViewportSize({ width: 768, height: 900 });
await mockApplication(page, session("user", true));
await page.goto("/index.html");
await page.evaluate(() => openView("sentimentCycleView"));
await expect(page.locator("body")).not.toHaveClass(/mobile-shell/);
const geometry = await page.evaluate(() => {
const nav = document.querySelector(".module-nav").getBoundingClientRect();
const main = document.querySelector(".main").getBoundingClientRect();
return {
overflow: document.documentElement.scrollWidth - innerWidth,
navWidth: nav.width,
navRight: nav.right,
mainLeft: main.left,
mainRight: main.right,
};
});
expect(geometry.overflow).toBeLessThanOrEqual(1);
expect(geometry.navWidth).toBeGreaterThanOrEqual(44);
expect(geometry.navWidth).toBeLessThan(200);
expect(Math.abs(geometry.mainLeft - geometry.navRight)).toBeLessThanOrEqual(1);
expect(geometry.mainRight).toBeLessThanOrEqual(768);
});
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");
@@ -2482,10 +2561,12 @@ test("screener publishes atomically and exposes latest, active, and historical c
const overflow = await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth);
expect(overflow).toBeLessThanOrEqual(1);
await page.locator("#themeToggle").click();
await page.locator("#headerMenuButton").click();
await page.locator('[data-mobile-command-target="themeToggle"]').click();
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
await expect(page.locator("#screenerView .screener-results-view")).toBeVisible();
await page.locator("#themeToggle").click();
await page.locator("#headerMenuButton").click();
await page.locator('[data-mobile-command-target="themeToggle"]').click();
await expect(page.locator("html")).toHaveAttribute("data-theme", "light");
});
@@ -2724,7 +2805,7 @@ test("mentor directory exposes evidence filters and private owner metadata", asy
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("#mentorView .mentor-sidebar .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");
@@ -2742,22 +2823,25 @@ test("mentor directory exposes evidence filters and private owner metadata", asy
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 mentorProfile = await page.locator("#mentorView .mentor-profile-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(Math.abs(mentorLibrary.width - 280)).toBeLessThanOrEqual(1);
expect(Math.abs(mentorProfile.width - 272)).toBeLessThanOrEqual(1);
expect(Math.abs(mentorChat.x - (mentorLibrary.x + mentorLibrary.width))).toBeLessThanOrEqual(1);
expect(Math.abs(mentorProfile.x - (mentorChat.x + mentorChat.width))).toBeLessThanOrEqual(1);
expect(mentorInput.height).toBeGreaterThanOrEqual(48);
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);
expect(lowerGap).toBeLessThanOrEqual(16);
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(openOverviewGap).toBeLessThanOrEqual(16);
expect(await page.evaluate(() => document.documentElement.scrollHeight - window.innerHeight)).toBeLessThanOrEqual(1);
});
@@ -2783,6 +2867,12 @@ test("mentor pins, custom order and streamed replies work together", async ({ pa
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);
const followUps = answer.locator("[data-mentor-follow-up]");
await expect(followUps).toHaveCount(3);
const messageCount = await page.locator("#mentorMessages .mentor-message").count();
await followUps.first().click();
await expect(page.locator("#mentorQuestion")).toHaveValue("哪些信号代表确认?");
await expect(page.locator("#mentorMessages .mentor-message")).toHaveCount(messageCount);
await page.locator("#themeToggle").click();
const userMessage = page.locator("#mentorMessages .mentor-message.user");
const darkUserMessageStyle = await userMessage.evaluate((element) => ({
@@ -2791,9 +2881,9 @@ test("mentor pins, custom order and streamed replies work together", async ({ pa
contentBackground: getComputedStyle(element.querySelector(".mentor-message-content")).backgroundColor,
contentColor: getComputedStyle(element.querySelector(".mentor-message-content")).color,
}));
expect(darkUserMessageStyle.background).toBe("rgb(35, 54, 74)");
expect(darkUserMessageStyle.border).toBe("rgb(66, 105, 142)");
expect(darkUserMessageStyle.contentBackground).toBe("rgba(0, 0, 0, 0)");
expect(darkUserMessageStyle.background).not.toBe("rgb(255, 255, 255)");
expect(darkUserMessageStyle.border).not.toBe("rgb(255, 255, 255)");
expect(darkUserMessageStyle.contentBackground).toBe("rgb(35, 54, 74)");
expect(darkUserMessageStyle.contentColor).toBe("rgb(232, 234, 237)");
const darkMessageStyle = await answer.evaluate((element) => {
const style = getComputedStyle(element);
@@ -2820,23 +2910,6 @@ test("mentor pins, custom order and streamed replies work together", async ({ pa
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));
@@ -2898,7 +2971,8 @@ test("global dialogs share the stage 18 geometry without changing account or adm
await page.locator("#closeAdminDialog").click();
await page.setViewportSize({ width: 375, height: 812 });
await page.locator("#alertButton").click();
await page.locator("#headerMenuButton").click();
await page.locator('[data-mobile-command-target="alertButton"]').click();
const mobileGeometry = await page.locator("#alertsDialog").evaluate((dialog) => ({
width: dialog.getBoundingClientRect().width,
documentOverflow: document.documentElement.scrollWidth - window.innerWidth,