feat: complete strategy and market data improvements
This commit is contained in:
+136
-21
@@ -135,12 +135,20 @@ async function mockApplication(page, authSession = session(), options = {}) {
|
||||
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));
|
||||
}
|
||||
payload = dashboard;
|
||||
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 },
|
||||
@@ -168,7 +176,16 @@ async function mockApplication(page, authSession = session(), options = {}) {
|
||||
notes: [],
|
||||
};
|
||||
} else if (url.pathname === "/api/search/detail") {
|
||||
payload = {
|
||||
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: [
|
||||
@@ -486,6 +503,27 @@ test("admin shell opens every primary workspace and global search", async ({ pag
|
||||
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));
|
||||
@@ -564,7 +602,7 @@ test("night mode covers the application shell and persists across reloads", asyn
|
||||
await expect(page.locator("#themeToggle")).toHaveAttribute("aria-label", "切换到夜间模式");
|
||||
});
|
||||
|
||||
test("collapsed overview and sentiment decision layout keep a single current reading", async ({ page }) => {
|
||||
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");
|
||||
@@ -591,49 +629,37 @@ test("collapsed overview and sentiment decision layout keep a single current rea
|
||||
});
|
||||
await page.locator('[data-view="sentimentCycleView"]').first().click();
|
||||
|
||||
await expect(page.locator("#sentimentStageGuideTitle")).toHaveText("判定口径");
|
||||
await expect(page.locator('[data-sentiment-stage]:visible')).toHaveCount(1);
|
||||
await expect(page.locator('[data-sentiment-stage="退潮"]')).toBeVisible();
|
||||
await expect(page.locator(".sentiment-stage-guide, [data-sentiment-stage]")).toHaveCount(0);
|
||||
await expect(page.locator("#sentimentPhaseAdvice")).toHaveText("情绪指标继续走弱。");
|
||||
const alignment = await page.evaluate(() => {
|
||||
const guide = document.querySelector(".sentiment-stage-guide").getBoundingClientRect();
|
||||
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 currentGuide = document.querySelector("[data-sentiment-stage].current");
|
||||
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 rangeHeader = document.querySelector(".sentiment-stage-guide-head > span:nth-child(3)").getBoundingClientRect();
|
||||
const rangeValue = currentGuide.querySelector(".stage-range").getBoundingClientRect();
|
||||
const labelStyle = getComputedStyle(document.querySelector(".sentiment-block .metric-label"));
|
||||
const statusStyle = getComputedStyle(document.querySelector(".sentiment-block .sentiment-text"));
|
||||
return {
|
||||
mainAligned: Math.abs(guide.x - trend.x) < 1 && Math.abs(guide.width - trend.width) < 1 && guide.top > trend.bottom,
|
||||
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,
|
||||
detailVisible: detail.top < innerHeight,
|
||||
detailAfterAnalysis: detail.top > Math.max(trend.bottom, components.bottom),
|
||||
chartHeight: chart.height,
|
||||
guideRowHeight: currentGuide.getBoundingClientRect().height,
|
||||
guideIsWhite: getComputedStyle(currentGuide).backgroundColor === "rgb(255, 255, 255)",
|
||||
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",
|
||||
rangeAligned: Math.abs(rangeHeader.right - rangeValue.right) < 1,
|
||||
};
|
||||
});
|
||||
expect(alignment.mainAligned).toBe(true);
|
||||
expect(alignment.columnsAligned).toBe(true);
|
||||
expect(alignment.railAligned).toBe(true);
|
||||
expect(alignment.detailVisible).toBe(true);
|
||||
expect(alignment.detailAfterAnalysis).toBe(true);
|
||||
expect(alignment.chartHeight).toBeGreaterThanOrEqual(340);
|
||||
expect(alignment.guideRowHeight).toBeLessThanOrEqual(52);
|
||||
expect(alignment.guideIsWhite).toBe(true);
|
||||
expect(alignment.sameType).toBe(true);
|
||||
expect(alignment.sameBaseline).toBe(true);
|
||||
expect(alignment.noStatusOffset).toBe(true);
|
||||
expect(alignment.rangeAligned).toBe(true);
|
||||
|
||||
const pageFrames = {};
|
||||
for (const [view, headSelector] of [
|
||||
@@ -946,6 +972,29 @@ test("market ladder transfers tier bands, sorting and structural insights", asyn
|
||||
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 }) => {
|
||||
@@ -1159,7 +1208,20 @@ test("theme library preserves the full master-detail workflow in its redesigned
|
||||
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");
|
||||
expect(await page.locator("#themeDetailChart").evaluate((canvas) => canvas.toDataURL().length)).toBeGreaterThan(100);
|
||||
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);
|
||||
@@ -1507,6 +1569,28 @@ test("stock hover preview ignores the selected historical date", async ({ page }
|
||||
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");
|
||||
@@ -1726,6 +1810,7 @@ test("mobile shell stays within the viewport", async ({ page }) => {
|
||||
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();
|
||||
@@ -1848,6 +1933,24 @@ test("new review workflows render account-scoped records", async ({ page }) => {
|
||||
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];
|
||||
@@ -2447,15 +2550,27 @@ test("mentor pins, custom order and streamed replies work together", async ({ pa
|
||||
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 }) => {
|
||||
|
||||
Reference in New Issue
Block a user