feat: integrate iFinD data and refine intelligent workspaces

This commit is contained in:
leefer
2026-07-28 16:38:56 +08:00
parent 6adeb54458
commit f4b2d7152a
22 changed files with 6481 additions and 389 deletions
+219 -7
View File
@@ -133,7 +133,13 @@ async function mockApplication(page, authSession = session(), options = {}) {
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/dashboard") {
options.dashboardRequests = (options.dashboardRequests || 0) + 1;
if (options.dashboardDelay) {
await new Promise((resolve) => setTimeout(resolve, options.dashboardDelay));
}
payload = dashboard;
}
else if (url.pathname === "/api/stock/002141/preview") {
payload = {
meta: { trade_date: "2026-07-23", intraday_trade_date: "2026-07-24", realtime: true, intraday_notice: "" },
@@ -233,6 +239,17 @@ async function mockApplication(page, authSession = session(), options = {}) {
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" },
@@ -336,15 +353,25 @@ async function mockApplication(page, authSession = session(), options = {}) {
},
],
};
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: options.latestScreenerResults?.[body.mode] || {
result: runResult || options.latestScreenerResults?.[body.mode] || {
meta: {
run_id: 99,
trade_date: "20260722",
@@ -357,6 +384,9 @@ async function mockApplication(page, authSession = session(), options = {}) {
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();
@@ -446,6 +476,61 @@ test("admin shell opens every primary workspace and global search", async ({ pag
await expect(page.locator("#globalSearchInput")).toBeFocused();
});
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 decision layout keep a single current reading", async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 });
await mockApplication(page, session("user", true));
@@ -1241,7 +1326,32 @@ test("dragon-tiger redesign keeps the merged empty state and independent card hi
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,
@@ -1954,6 +2064,93 @@ test("screener stage completion follows its execution context and mode results s
await expect(page.locator("#screenerTableBody")).not.toContainText("量化结果");
});
test("screener keeps results for each stage and curated strategy 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 options = {
recentScreenerResults: [],
additionalScreenerRegimes: [{ id: "retreat", label: "Retreat" }],
additionalScreenerStrategies: [
{
id: 3, name: "Retreat Defense", description: "Retreat-stage strategy",
regimes: ["retreat"], builtin: true, data_ready: true, missing_data: [], formula,
},
{
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" },
},
},
],
};
options.screenerRunResult = (body) => {
const candidateName = body.mode === "smart"
? body.regime === "retreat" ? "Smart Retreat" : "Smart Repair"
: body.strategy_name === "Quality B" ? "Curated B" : "Curated A";
return {
meta: {
run_id: 100 + options.recentScreenerResults.length,
trade_date: "20260722",
regime: body.regime,
strategy_name: body.strategy_name,
mode: body.mode,
},
candidates: [{
code: `60000${options.recentScreenerResults.length + 1}`,
name: candidateName,
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: [],
}],
disclaimer: "Historical statistics do not predict future returns.",
backtest: null,
};
};
await mockApplication(page, session("user", true), options);
await page.goto("/index.html?view=screenerView");
await page.locator("#screenerRunButton").click();
await expect(page.locator("#screenerTableBody")).toContainText("Smart Repair");
await page.locator('[data-regime="retreat"]').click();
await page.locator("#screenerRunButton").click();
await expect(page.locator("#screenerTableBody")).toContainText("Smart Retreat");
await page.locator('[data-regime="repair"]').click();
await expect(page.locator("#screenerTableBody")).toContainText("Smart Repair");
await page.locator('[data-regime="retreat"]').click();
await expect(page.locator("#screenerTableBody")).toContainText("Smart Retreat");
await page.locator('[data-screener-mode="curated"]').click();
await page.locator('[data-curated-run="2"]').click();
await expect(page.locator("#screenerTableBody")).toContainText("Curated A");
await page.locator('[data-curated-run="4"]').click();
await expect(page.locator("#screenerTableBody")).toContainText("Curated B");
await page.locator('[data-curated-strategy="2"] .curated-card-description').click();
await expect(page.locator("#screenerTableBody")).toContainText("Curated A");
await page.reload();
await page.locator('[data-screener-mode="smart"]').click();
await expect(page.locator("#screenerTableBody")).toContainText("Smart Repair");
await page.locator('[data-regime="retreat"]').click();
await expect(page.locator("#screenerTableBody")).toContainText("Smart Retreat");
await page.locator('[data-screener-mode="curated"]').click();
await expect(page.locator("#screenerTableBody")).toContainText("Curated A");
await page.locator('[data-curated-strategy="4"] .curated-card-description').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));
@@ -2048,8 +2245,8 @@ test("heaven workspace actions remain compact and do not overlap", async ({ page
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 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);
@@ -2075,7 +2272,8 @@ test("heaven workspace actions remain compact and do not overlap", async ({ page
openHeavenReading("fortune", { loading: false });
});
const readingDialog = await page.locator("#heavenReadingDialog").boundingBox();
expect(readingDialog.width).toBeLessThanOrEqual(920);
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();
@@ -2113,7 +2311,9 @@ test("heaven workspace controls fit a narrow viewport", async ({ page }) => {
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);
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);
});
@@ -2128,7 +2328,7 @@ test("mentor directory exposes evidence filters and private owner metadata", asy
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 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);
@@ -2183,6 +2383,18 @@ 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);
await page.locator("#themeToggle").click();
const darkMessageStyle = await answer.evaluate((element) => {
const style = getComputedStyle(element);
return {
background: style.backgroundColor,
border: style.borderTopColor,
shadow: style.boxShadow,
};
});
expect(darkMessageStyle.background).not.toBe("rgb(255, 255, 255)");
expect(darkMessageStyle.border).not.toBe("rgb(255, 255, 255)");
expect(darkMessageStyle.shadow).toBe("none");
});
test("mobile mentor directory opens as a searchable selector and hides private mentors", async ({ page }) => {