feat: expand market discovery and auction workflow
This commit is contained in:
+220
-4
@@ -114,13 +114,50 @@ async function mockApplication(page, authSession = session()) {
|
||||
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: "" },
|
||||
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 },
|
||||
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: [],
|
||||
intraday: [
|
||||
{ date: "2026-07-24", time: "09:30", open: 10.20, high: 10.24, low: 10.18, close: 10.22, volume: 100, average: 10.22 },
|
||||
{ date: "2026-07-24", time: "09:31", open: 10.22, high: 10.30, low: 10.21, close: 10.28, volume: 130, average: 10.25 },
|
||||
{ date: "2026-07-24", time: "09:32", open: 10.28, high: 10.29, low: 10.20, close: 10.23, volume: 90, average: 10.24 },
|
||||
{ date: "2026-07-24", time: "09:33", open: 10.23, high: 10.34, low: 10.22, close: 10.32, volume: 160, average: 10.27 },
|
||||
{ date: "2026-07-24", time: "09:34", open: 10.32, high: 10.36, low: 10.29, close: 10.34, volume: 120, average: 10.28 },
|
||||
],
|
||||
};
|
||||
} else if (url.pathname === "/api/stock/002141") {
|
||||
payload = {
|
||||
meta: { trade_date: "2026-07-23", realtime: false },
|
||||
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 },
|
||||
],
|
||||
moneyflow: {},
|
||||
notes: [],
|
||||
};
|
||||
} else if (url.pathname === "/api/search/detail") {
|
||||
payload = {
|
||||
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: [
|
||||
{ trade_date: "2026-07-22", open: 3750, high: 3790, low: 3740, close: 3780, volume: 1000 },
|
||||
{ trade_date: "2026-07-23", open: 3782, high: 3810, low: 3770, close: 3800, volume: 1200 },
|
||||
],
|
||||
metrics: [],
|
||||
};
|
||||
} else if (url.pathname === "/api/chart/intraday") {
|
||||
payload = {
|
||||
meta: { trade_date: "2026-07-24", previous_close: 10.1 },
|
||||
entity: { id: url.searchParams.get("id"), type: url.searchParams.get("type") },
|
||||
points: [
|
||||
{ date: "2026-07-24", time: "09:30", open: 10.10, high: 10.18, low: 10.08, close: 10.15, volume: 100, average: 10.15 },
|
||||
{ date: "2026-07-24", time: "09:31", open: 10.15, high: 10.24, low: 10.14, close: 10.22, volume: 130, average: 10.18 },
|
||||
{ date: "2026-07-24", time: "09:32", open: 10.22, high: 10.23, low: 10.16, close: 10.18, volume: 90, average: 10.18 },
|
||||
],
|
||||
};
|
||||
} else if (url.pathname === "/api/watchlist" || url.pathname === "/api/notes") payload = { items: [] };
|
||||
else if (url.pathname === "/api/alerts") {
|
||||
@@ -173,6 +210,56 @@ async function mockApplication(page, authSession = session()) {
|
||||
};
|
||||
} 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/auction") {
|
||||
payload = {
|
||||
meta: { trade_date: "2026-07-22", carried_forward: false, phase: "finalized", available: true, actionable: false },
|
||||
summary: { stock_count: 3, candidate_count: 1, focus_count: 1, one_price_count: 1, up_count: 2, down_count: 1, limit_open_count: 1, strong_open_count: 2, median_change: 1.2, amount_billion: 2.5, amount_change_previous: 12.5, amount_change_5d: 8.2 },
|
||||
expectations: { "超预期": 1, "符合预期": 0, "低于预期": 0 },
|
||||
candidate_meta: { baseline_date: "2026-07-21" },
|
||||
themes: {
|
||||
carry: [{ name: "Test Sector", status: "强承接", prior_limit_count: 2, leader: "Test Stock", matched_count: 1, median_change: 4.2, positive_rate: 100, amount_million: 15 }],
|
||||
new_themes: [{ name: "人工智能", stock_count: 2, median_change: 3.5, amount_million: 26, leaders: ["Test Stock"] }],
|
||||
},
|
||||
amount_history: [
|
||||
{ trade_date: "2026-07-21", amount_billion: 2.2, stock_count: 2 },
|
||||
{ trade_date: "2026-07-22", amount_billion: 2.5, stock_count: 2 },
|
||||
],
|
||||
news_feedback: { available: false, message: "隔夜消息反馈暂不可用", detail: "待稳定的新闻与公告数据接入后开放" },
|
||||
focus_rows: [
|
||||
{ code: "002141", name: "Test Stock", sector: "Test Sector", change: 4.2, price: 10.2, amount_million: 15, volume_ratio: 1.8, turnover_rate: 0.12, source_label: "昨日涨停 · 同花顺热榜", expectation: "超预期", expected_change: 2.2, attention_score: 88.5, core_tags: ["人气前5"], expectation_reason: "昨日首板;竞价涨幅高于预期中枢2.0个百分点,量比1.80" },
|
||||
],
|
||||
one_price_rows: [
|
||||
{ code: "000001", name: "Limit Stock", sector: "Test Sector", change: 10, price: 11, amount_million: 8, volume_ratio: 3.2, source_label: "昨日涨停", prior_streak: 3, core_tags: ["三板以上"], is_market_core: true, is_one_price: true },
|
||||
],
|
||||
watchlist_rows: [
|
||||
{ code: "000002", name: "Watch Stock", sector: "Bank", change: -1.2, price: 9.88, amount_million: 3, volume_ratio: 0.9, expectation: "符合预期", expected_change: 0, attention_score: 32.5, core_tags: [], expectation_reason: "自选观察;竞价反馈接近个人观察基准", is_watchlist: true, available: true },
|
||||
],
|
||||
watchlist_missing_count: 0,
|
||||
rows: [
|
||||
{ code: "002141", name: "Test Stock", sector: "Test Sector", change: 4.2, price: 10.2, amount_million: 15, volume_ratio: 1.8, turnover_rate: 0.12, source_label: "昨日涨停 · 同花顺热榜", expectation: "超预期", expected_change: 2.2, attention_score: 88.5, core_tags: ["人气前5"], expectation_reason: "昨日首板;竞价涨幅高于预期中枢2.0个百分点,量比1.80" },
|
||||
],
|
||||
};
|
||||
} else if (url.pathname === "/api/themes") {
|
||||
payload = {
|
||||
meta: { trade_date: "2026-07-22", carried_forward: false },
|
||||
summary: { theme_count: 1, quoted_count: 1, up_count: 1, down_count: 0, hot_count: 1 },
|
||||
items: [{ code: "885728.TI", name: "人工智能", member_count: 1, change: 2.2, turnover_rate: 3.1, hot_rank: 1, has_quote: true }],
|
||||
};
|
||||
} else if (url.pathname === "/api/themes/detail") {
|
||||
payload = {
|
||||
meta: { trade_date: "2026-07-22" },
|
||||
theme: { code: "885728.TI", name: "人工智能", member_count: 1, change: 2.2, turnover_rate: 3.1 },
|
||||
summary: { member_count: 1, quoted_count: 1, up_count: 1, down_count: 0 },
|
||||
series: [
|
||||
{ trade_date: "2026-07-21", open: 100, high: 104, low: 99, close: 103, change: 3, volume: 1000 },
|
||||
{ trade_date: "2026-07-22", open: 103, high: 106, low: 102, close: 105, change: 1.94, volume: 1200 },
|
||||
],
|
||||
members: [{ code: "002141", name: "Test Stock", change: 2.4, price: 10.8, amount_billion: 3.2, has_quote: true }],
|
||||
};
|
||||
} else if (url.pathname === "/api/popularity") {
|
||||
const hot = { rank: 1, code: "002141", ts_code: "002141.SZ", name: "Test Stock", change: 2.4, price: 10.8, ths_rank: 1, dc_rank: 2, rank_change: 3, concepts: ["人工智能"], dual_source: true };
|
||||
payload = { meta: { trade_date: "2026-07-22", carried_forward: false }, summary: { ths_count: 1, dc_count: 1, dual_count: 1 }, combined: [hot], ths: [{ ...hot, rank: 1 }], dc: [{ ...hot, rank: 2 }] };
|
||||
}
|
||||
else if (url.pathname === "/api/screener/setup") {
|
||||
payload = {
|
||||
trade_date: "20260722",
|
||||
@@ -253,8 +340,8 @@ test("admin shell opens every primary workspace and global search", async ({ pag
|
||||
await page.locator("#closeAssistantDialog").click();
|
||||
|
||||
const views = [
|
||||
"sentimentCycleView", "limitPool", "brokenView", "downView", "yesterdayView",
|
||||
"performanceView", "ladderView", "rotationView", "dragonView", "screenerView",
|
||||
"auctionView", "sentimentCycleView", "limitPool", "brokenView", "downView", "yesterdayView",
|
||||
"performanceView", "ladderView", "rotationView", "themeLibraryView", "popularityView", "dragonView", "screenerView",
|
||||
"mentorView", "heavenView", "reviewWorkspaceView",
|
||||
];
|
||||
for (const view of views) {
|
||||
@@ -267,6 +354,52 @@ test("admin shell opens every primary workspace and global search", async ({ pag
|
||||
await expect(page.locator("#globalSearchInput")).toBeFocused();
|
||||
});
|
||||
|
||||
test("auction, themes and popularity reuse stock detail interactions", async ({ page }) => {
|
||||
await mockApplication(page, session("user", true));
|
||||
await page.goto("/index.html");
|
||||
|
||||
await page.locator('[data-view="auctionView"]').first().click();
|
||||
await expect(page.locator("#auctionPhaseTitle")).toHaveText("今日竞价已定格");
|
||||
await expect(page.locator("#auctionRefreshButton")).toBeHidden();
|
||||
await expect(page.locator("#auctionTableBody tr")).toHaveCount(1);
|
||||
await expect(page.locator("#auctionTableBody")).toContainText("人气前5");
|
||||
await page.locator('[data-auction-filter="above"]').click();
|
||||
await expect(page.locator("#auctionTableBody tr")).toHaveCount(1);
|
||||
await expect(page.locator("#auctionAboveCount")).toHaveText("1");
|
||||
await expect(page.locator("#auctionThemeCarry")).toContainText("强承接");
|
||||
await expect(page.locator("#auctionAmountValue")).toHaveText("2.50 亿");
|
||||
await expect(page.locator("#auctionNewsTitle")).toHaveText("隔夜消息反馈");
|
||||
await expect(page.locator(".auction-news-entry")).toContainText("暂不可用");
|
||||
await page.locator('[data-auction-dataset="onePrice"]').click();
|
||||
await expect(page.locator("#auctionTableBody tr")).toHaveCount(1);
|
||||
await expect(page.locator("#auctionTableBody")).toContainText("三板以上");
|
||||
await expect(page.locator("#auctionExpectationFilterbar")).toBeHidden();
|
||||
await page.locator('[data-auction-dataset="watchlist"]').click();
|
||||
await expect(page.locator("#auctionTableBody tr")).toHaveCount(1);
|
||||
await expect(page.locator("#auctionTableBody")).toContainText("Watch Stock");
|
||||
await page.locator('[data-auction-dataset="focus"]').click();
|
||||
await page.evaluate(() => {
|
||||
state.auctionData.meta = { phase: "selection", available: false, actionable: true };
|
||||
state.auctionData.rows = [];
|
||||
state.auctionData.focus_rows = [];
|
||||
renderAuctionCenter();
|
||||
});
|
||||
await expect(page.locator("#auctionPhaseTitle")).toHaveText("等待最终竞价");
|
||||
await expect(page.locator("#auctionRefreshButton")).toBeVisible();
|
||||
await expect(page.locator("#auctionEmpty")).toContainText("正在等待 9:25 最终竞价数据");
|
||||
|
||||
await page.locator('[data-view="themeLibraryView"]').first().click();
|
||||
await expect(page.locator("#themeDirectory [data-theme-code]")).toHaveCount(1);
|
||||
await expect(page.locator("#themeMemberTableBody tr")).toHaveCount(1);
|
||||
await expect(page.locator("#themeDetailName")).toHaveText("人工智能");
|
||||
|
||||
await page.locator('[data-view="popularityView"]').first().click();
|
||||
await expect(page.locator("#popularityTableBody tr")).toHaveCount(1);
|
||||
await expect(page.locator("#popularityTableBody")).toContainText("双榜共识");
|
||||
await page.locator("#popularityTableBody tr").click();
|
||||
await expect(page.locator("#stockDialog")).toBeVisible();
|
||||
});
|
||||
|
||||
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");
|
||||
@@ -298,6 +431,89 @@ test("stock hover preview ignores the selected historical date", async ({ page }
|
||||
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");
|
||||
await expect(page.locator("#stockPreviewSource")).toHaveText("日 K 行情 · 2 个交易日");
|
||||
await expect(page.locator('[data-preview-chart="daily"]')).toHaveClass(/active/);
|
||||
await page.locator('[data-preview-chart="intraday"]').click();
|
||||
await expect(page.locator("#stockPreviewDate")).toHaveText("2026-07-24");
|
||||
await expect(page.locator("#stockPreviewSource")).toHaveText("最新分时 · 1分钟");
|
||||
const canvasColors = await page.locator("#stockPreviewChart").evaluate((canvas) => {
|
||||
const pixels = canvas.getContext("2d").getImageData(0, 0, canvas.width, canvas.height).data;
|
||||
const colors = new Set();
|
||||
for (let index = 0; index < pixels.length; index += 16) {
|
||||
if (pixels[index + 3]) colors.add(`${pixels[index]},${pixels[index + 1]},${pixels[index + 2]}`);
|
||||
}
|
||||
return colors.size;
|
||||
});
|
||||
expect(canvasColors).toBeGreaterThan(4);
|
||||
});
|
||||
|
||||
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");
|
||||
const pixels = await page.evaluate(() => {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = 40;
|
||||
canvas.height = 80;
|
||||
const context = canvas.getContext("2d");
|
||||
context.fillStyle = CHART_BACKGROUND;
|
||||
context.fillRect(0, 0, 40, 80);
|
||||
const priceY = (value) => 90 - value * 8;
|
||||
drawCandlestick(context, 20, { high: 10, close: 8, open: 6, low: 4 }, priceY, 10);
|
||||
const read = (x, y) => Array.from(context.getImageData(x, y, 1, 1).data);
|
||||
const reddest = (left, top, width, height) => {
|
||||
const data = context.getImageData(left, top, width, height).data;
|
||||
let selected = [0, 0, 0, 0];
|
||||
for (let index = 0; index < data.length; index += 4) {
|
||||
const pixel = [data[index], data[index + 1], data[index + 2], data[index + 3]];
|
||||
if (pixel[0] - pixel[1] > selected[0] - selected[1]) selected = pixel;
|
||||
}
|
||||
return selected;
|
||||
};
|
||||
return {
|
||||
upperWick: reddest(19, 10, 3, 16),
|
||||
bodyCenter: read(20, 34),
|
||||
lowerWick: reddest(19, 43, 3, 17),
|
||||
bodyBorder: reddest(14, 26, 3, 17),
|
||||
};
|
||||
});
|
||||
for (const redPixel of [pixels.upperWick, pixels.lowerWick, pixels.bodyBorder]) {
|
||||
expect(redPixel[0] - redPixel[1]).toBeGreaterThan(40);
|
||||
expect(redPixel[0] - redPixel[2]).toBeGreaterThan(40);
|
||||
}
|
||||
expect(pixels.bodyCenter.slice(0, 3)).toEqual([251, 252, 253]);
|
||||
});
|
||||
|
||||
test("stock and market detail dialogs switch from daily K to intraday", async ({ page }) => {
|
||||
await mockApplication(page, session("user", true));
|
||||
await page.goto("/index.html");
|
||||
|
||||
await page.evaluate(() => openStock("002141", { code: "002141", name: "Test Stock", sector: "Test Sector" }));
|
||||
await page.locator('[data-stock-detail-chart="intraday"]').click();
|
||||
await expect(page.locator("#chartSource")).toHaveText("分时 · 2026-07-24");
|
||||
await expect(page.locator('[data-stock-detail-chart="intraday"]')).toHaveAttribute("aria-pressed", "true");
|
||||
await page.locator('[data-stock-detail-chart="daily"]').click();
|
||||
await expect(page.locator("#chartSource")).toContainText("日 K 行情");
|
||||
await page.locator("#closeStockDialog").click();
|
||||
|
||||
await page.evaluate(() => openEntityDetail({ id: "000001.SH", code: "000001.SH", name: "上证指数", type: "index", type_label: "指数" }));
|
||||
await page.locator('[data-entity-detail-chart="intraday"]').click();
|
||||
await expect(page.locator("#entityDetailDate")).toHaveText("分时 · 2026-07-24");
|
||||
await expect(page.locator('[data-entity-detail-chart="intraday"]')).toHaveAttribute("aria-pressed", "true");
|
||||
const colors = await page.locator("#entityDetailChart").evaluate((canvas) => {
|
||||
const pixels = canvas.getContext("2d").getImageData(0, 0, canvas.width, canvas.height).data;
|
||||
return new Set(Array.from({ length: Math.floor(pixels.length / 16) }, (_, index) => {
|
||||
const offset = index * 16;
|
||||
return `${pixels[offset]},${pixels[offset + 1]},${pixels[offset + 2]},${pixels[offset + 3]}`;
|
||||
})).size;
|
||||
});
|
||||
expect(colors).toBeGreaterThan(4);
|
||||
const offsets = await page.evaluate(() => [
|
||||
intradayMinuteOffset("09:30"),
|
||||
intradayMinuteOffset("11:30"),
|
||||
intradayMinuteOffset("13:00"),
|
||||
intradayMinuteOffset("15:00"),
|
||||
]);
|
||||
expect(offsets).toEqual([0, 120, 120, 240]);
|
||||
});
|
||||
|
||||
test("saved daily fortune opens in the reading dialog without regenerating", async ({ page }) => {
|
||||
|
||||
Reference in New Issue
Block a user