feat: integrate iFinD data and refine intelligent workspaces
This commit is contained in:
+219
-7
@@ -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 }) => {
|
||||
|
||||
Reference in New Issue
Block a user