rebuild(stage-8): deliver market insight workspaces
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const { expect, test } = require("@playwright/test");
|
||||
|
||||
const evidence = path.resolve(__dirname, "../../docs/evidence/stage-8");
|
||||
test.beforeAll(() => fs.mkdirSync(evidence, { recursive: true }));
|
||||
|
||||
async function authenticate(page) {
|
||||
await page.goto("/");
|
||||
await page.getByLabel("账号名").fill("stage8admin");
|
||||
await page.getByLabel("密码").fill("Stage8-pass-123!");
|
||||
await page.getByRole("button", { name: "登录", exact: true }).click();
|
||||
await expect(page.locator(".sidebar, .field-error")).toBeVisible();
|
||||
if (!(await page.locator(".sidebar").isVisible())) {
|
||||
await page.getByRole("tab", { name: "注册" }).click();
|
||||
await page.getByRole("button", { name: "注册并登录" }).click();
|
||||
}
|
||||
}
|
||||
|
||||
const summary = {
|
||||
context: {
|
||||
requested_date: "2026-07-30", actual_date: "2026-07-30", previous_date: "2026-07-29",
|
||||
observed_at: "2026-07-30T15:00:00+08:00", state: "final", carried_forward: false, message: "",
|
||||
},
|
||||
values: { up_count: 2800, down_count: 2100, limit_up: 58, limit_down: 5, broken: 20, seal_rate: 74.4, amount: 1500000000000, temperature: 48 },
|
||||
};
|
||||
|
||||
const candidate = (code, name, expectation, score) => ({
|
||||
identifier: `${code}.SZ`, code, name, sector: "机器人", source_label: "昨日涨停 · 同花顺热榜",
|
||||
expectation, change: expectation === "超预期" ? 6.8 : 2.1, expected_change: 4,
|
||||
attention_score: score, volume_ratio: 1.8, amount_million: 22.5, is_market_core: score > 80,
|
||||
});
|
||||
const auction = {
|
||||
requested_date: "2026-07-30", trade_date: "2026-07-30", observed_at: "2026-07-30T09:25:00+08:00",
|
||||
state: "final", carried_forward: false, message: "", phase: "finalized", current_available: true, coverage: 0.98,
|
||||
summary: { stock_count: 5200, focus_count: 2, one_price_count: 1, amount_billion: 42.03, amount_change_previous: -12.4 },
|
||||
expectations: { 超预期: 1, 符合预期: 1, 低于预期: 0 },
|
||||
focus_rows: [candidate("000001", "平安银行", "超预期", 88), candidate("000002", "万科A", "符合预期", 72)],
|
||||
rows: [candidate("000001", "平安银行", "超预期", 88), candidate("000002", "万科A", "符合预期", 72)],
|
||||
one_price_rows: [{ ...candidate("000003", "国华网安", "", 0), attention_score: null, expectation: "" }],
|
||||
watchlist_rows: [], watchlist_ready: false,
|
||||
themes: { carry: [{ name: "机器人", status: "强承接", matched_count: 6, median_change: 4.2 }], new_themes: [{ name: "算力租赁", stock_count: 3, leaders: ["平安银行", "万科A"] }] },
|
||||
amount_history: [
|
||||
{ trade_date: "2026-07-28", amount_billion: 36.2, stock_count: 5180 },
|
||||
{ trade_date: "2026-07-29", amount_billion: 47.8, stock_count: 5190 },
|
||||
{ trade_date: "2026-07-30", amount_billion: 42.03, stock_count: 5200 },
|
||||
],
|
||||
};
|
||||
const themes = {
|
||||
requested_date: "2026-07-30", trade_date: "2026-07-30", observed_at: "2026-07-30T15:00:00+08:00",
|
||||
state: "archive", carried_forward: false, message: "", summary: { theme_count: 3, quoted_count: 3, up_count: 2, down_count: 1 },
|
||||
items: [
|
||||
{ code: "885001.TI", name: "机器人", member_count: 120, change: 3.2, turnover_rate: 4.8, hot_rank: 1, has_quote: true },
|
||||
{ code: "885002.TI", name: "算力租赁", member_count: 82, change: 1.5, turnover_rate: 3.2, hot_rank: 2, has_quote: true },
|
||||
{ code: "885003.TI", name: "保险", member_count: 18, change: -0.8, turnover_rate: 1.1, hot_rank: 8, has_quote: true },
|
||||
],
|
||||
};
|
||||
const themeDetail = {
|
||||
trade_date: "2026-07-30", observed_at: "2026-07-30T15:00:00+08:00", state: "archive", message: "",
|
||||
theme: themes.items[0], summary: { member_count: 3, quoted_count: 2, up_count: 2, down_count: 0, turnover_rate: 4.8 },
|
||||
members: [
|
||||
{ identifier: "000001.SZ", code: "000001", name: "平安银行", change: 3.2, close: 12.3, amount: 500000000, quoted: true },
|
||||
{ identifier: "000002.SZ", code: "000002", name: "万科A", change: 1.1, close: 8.4, amount: 220000000, quoted: true },
|
||||
{ identifier: "000003.SZ", code: "000003", name: "停牌样本", change: null, close: null, amount: null, quoted: false },
|
||||
],
|
||||
};
|
||||
const hotRow = (rank, code, name, dual = true) => ({ rank, identifier: `${code}.SZ`, code, name, change: 2.4, price: 12.2, ths_rank: rank, dc_rank: dual ? rank + 1 : null, dual_source: dual, rank_change: 2, concepts: ["机器人"], reason: "市场关注度上升" });
|
||||
const popularity = {
|
||||
requested_date: "2026-07-30", trade_date: "2026-07-29", observed_at: "2026-07-29T15:00:00+08:00",
|
||||
state: "archive", carried_forward: true, message: "当日榜单尚未生成,显示最近有效榜单",
|
||||
summary: { ths_count: 3, dc_count: 3, dual_count: 2 },
|
||||
combined: [hotRow(1, "000001", "平安银行"), hotRow(2, "000002", "万科A"), hotRow(3, "000003", "国华网安", false)],
|
||||
ths: [hotRow(1, "000001", "平安银行"), hotRow(2, "000002", "万科A")],
|
||||
dc: [hotRow(1, "000002", "万科A"), hotRow(2, "000001", "平安银行")],
|
||||
};
|
||||
const dragon = {
|
||||
requested_date: "2026-07-30", trade_date: "2026-07-30", previous_date: "2026-07-29",
|
||||
observed_at: "2026-07-30T18:00:00+08:00", state: "archive", carried_forward: false,
|
||||
status: "partial", message: "部分营业部尚未归类",
|
||||
summary: { official_stock_count: 76, trader_count: 2, operation_count: 3, unclassified_count: 1, net_million: 38.5 },
|
||||
traders: [
|
||||
{ name: "章盟主", description: "偏好主线核心与大成交标的", net_million: 28.5, operation_count: 2 },
|
||||
{ name: "作手新一", description: "重视情绪拐点与辨识度", net_million: 12.2, operation_count: 1 },
|
||||
],
|
||||
operations: [
|
||||
{ identifier: "000001.SZ", code: "000001", name: "平安银行", direction: "买入", buy_million: 30, sell_million: 5, net_million: 25, trader_name: "章盟主", seat_name: "国泰君安上海江苏路", reason: "日涨幅偏离值达7%", recognized: true },
|
||||
{ identifier: "000002.SZ", code: "000002", name: "万科A", direction: "买入", buy_million: 15, sell_million: 2.8, net_million: 12.2, trader_name: "作手新一", seat_name: "国泰君安南京太平南路", reason: "日振幅值达15%", recognized: true },
|
||||
{ identifier: "000003.SZ", code: "000003", name: "国华网安", direction: "买入", buy_million: 4.1, sell_million: 2.8, net_million: 1.3, trader_name: "", seat_name: "测试待归类营业部", reason: "连续三个交易日涨幅偏离", recognized: false },
|
||||
],
|
||||
unclassified_seats: [{ seat_name: "测试待归类营业部", net_million: 1.3, operation_count: 1 }],
|
||||
profiles: [
|
||||
{ name: "章盟主", description: "偏好主线核心与大成交标的", organization_count: 2, organizations: ["国泰君安上海江苏路", "国泰君安上海新闸路"] },
|
||||
{ name: "作手新一", description: "重视情绪拐点与辨识度", organization_count: 1, organizations: ["国泰君安南京太平南路"] },
|
||||
],
|
||||
};
|
||||
|
||||
async function mockStage8(page) {
|
||||
await page.route("**/api/market/summary", (route) => route.fulfill({ contentType: "application/json", body: JSON.stringify(summary) }));
|
||||
await page.route("**/api/market/workspaces/*", (route) => route.fulfill({ contentType: "application/json", body: JSON.stringify({ trade_date: "2026-07-30", observed_at: "2026-07-30T15:00:00+08:00", carried_forward: false, message: "", overview: summary.values, sentiment: {}, history: [] }) }));
|
||||
await page.route("**/api/market/insights/*", (route) => {
|
||||
const key = new URL(route.request().url()).pathname.split("/").pop();
|
||||
const payload = key === "auction" ? auction : key === "themes" ? themes : key === "popularity" ? popularity : dragon;
|
||||
return route.fulfill({ contentType: "application/json", body: JSON.stringify(payload) });
|
||||
});
|
||||
await page.route("**/api/market/themes/*", (route) => route.fulfill({ contentType: "application/json", body: JSON.stringify(themeDetail) }));
|
||||
await page.route("**/api/market/entities/theme/*/charts/*", (route) => route.fulfill({ contentType: "application/json", body: JSON.stringify({ entity_type: "theme", identifier: "885001.TI", code: "885001", name: "机器人", interval: "day", trade_date: "2026-07-30", observed_at: "2026-07-30T15:00:00+08:00", previous_close: 1000, range_start: null, range_end: null, points: [{ time: "2026-07-29", open: 1000, high: 1020, low: 990, close: 1010, volume: 100, amount: 1000, average: null }, { time: "2026-07-30", open: 1010, high: 1060, low: 1005, close: 1050, volume: 150, amount: 1600, average: null }] }) }));
|
||||
}
|
||||
|
||||
test("stage 8 market insights preserve lifecycle, hierarchy and empty-state semantics", async ({ page }) => {
|
||||
const consoleErrors = [];
|
||||
page.on("console", (message) => { if (message.type() === "error" && !message.text().includes("401 (Unauthorized)")) consoleErrors.push(message.text()); });
|
||||
await mockStage8(page);
|
||||
await authenticate(page);
|
||||
|
||||
await page.getByRole("link", { name: /集合竞价/ }).click();
|
||||
await expect(page.getByText("竞价成交额", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("42.03 亿", { exact: true })).toBeVisible();
|
||||
await expect(page.locator(".auction-bars").getByText("42.03", { exact: true })).toBeVisible();
|
||||
await page.screenshot({ path: path.join(evidence, "auction-light-1920x1080.jpg"), type: "jpeg", quality: 82 });
|
||||
|
||||
await page.getByRole("link", { name: /题材库/ }).click();
|
||||
await expect(page.getByRole("heading", { name: "机器人成分股" })).toHaveCount(0);
|
||||
await expect(page.getByText("停牌样本")).toBeVisible();
|
||||
await page.getByRole("button", { name: /机器人/ }).first().hover();
|
||||
await expect(page.locator(".theme-preview-popover")).toBeVisible();
|
||||
|
||||
await page.getByRole("link", { name: /人气热榜/ }).click();
|
||||
await page.getByRole("button", { name: "夜间" }).click();
|
||||
await expect(page.locator(".popularity-summary-strip article")).toHaveCount(3);
|
||||
expect(await page.locator(".popularity-summary-strip article").first().evaluate((element) => getComputedStyle(element).boxShadow)).toBe("none");
|
||||
await page.screenshot({ path: path.join(evidence, "popularity-dark-1920x1080.jpg"), type: "jpeg", quality: 82 });
|
||||
|
||||
await page.getByRole("link", { name: /龙虎榜/ }).click();
|
||||
await expect(page.getByRole("heading", { name: "当日操作明细" })).toBeVisible();
|
||||
await expect(page.getByText("测试待归类营业部").first()).toBeVisible();
|
||||
await page.getByRole("button", { name: "游资档案" }).click();
|
||||
await expect(page.getByRole("heading", { name: "章盟主" })).toBeVisible();
|
||||
await expect(page.locator(".dialog-backdrop")).toHaveCount(0);
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)).toBe(0);
|
||||
await page.screenshot({ path: path.join(evidence, "dragon-profiles-dark-390x844.jpg"), type: "jpeg", quality: 82 });
|
||||
expect(consoleErrors).toEqual([]);
|
||||
});
|
||||
Reference in New Issue
Block a user