chore: complete stage 19 regression hardening

This commit is contained in:
leefer
2026-07-28 23:34:10 +08:00
parent 1cc80583b3
commit c30d2107b3
6 changed files with 179 additions and 41 deletions
+106 -21
View File
@@ -259,6 +259,16 @@ async function mockApplication(page, authSession = session(), options = {}) {
};
} 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/rotation/members") {
payload = {
meta: { trade_date: "20260724", sector_name: url.searchParams.get("sector") || "电网设备", member_count: 3, quoted_count: 2 },
rows: [
{ code: "002879", name: "长缆科技", change: 4.8, open: 18.21, close: 19.06, amount_billion: 19.6, quoted: true },
{ code: "603221", name: "爱丽家居", change: 1.2, open: 13.05, close: 13.22, amount_billion: 8.7, quoted: true },
{ code: "000001", name: "停牌样本", change: null, open: null, close: null, amount_billion: null, quoted: false },
],
};
}
else if (url.pathname === "/api/auction") {
payload = {
meta: { trade_date: "2026-07-22", carried_forward: false, phase: "finalized", available: true, actionable: false },
@@ -476,6 +486,29 @@ test("admin shell opens every primary workspace and global search", async ({ pag
await expect(page.locator("#globalSearchInput")).toBeFocused();
});
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));
await page.goto("/index.html");
const views = [
"auctionView", "sentimentCycleView", "limitPool", "brokenView", "downView", "yesterdayView",
"performanceView", "ladderView", "rotationView", "themeLibraryView", "popularityView", "dragonView",
"screenerView", "mentorView", "heavenView", "reviewWorkspaceView",
];
let reference = null;
for (const view of views) {
await page.locator(`[data-view="${view}"]`).first().click();
const activeView = page.locator(`#${view}`);
await expect(activeView).not.toHaveClass(/view-entering/);
const box = await activeView.boundingBox();
expect(box).not.toBeNull();
reference ||= { x: box.x, y: box.y, width: box.width };
expect(Math.abs(box.x - reference.x)).toBeLessThanOrEqual(1);
expect(Math.abs(box.y - reference.y)).toBeLessThanOrEqual(1);
expect(Math.abs(box.width - reference.width)).toBeLessThanOrEqual(1);
}
});
test("manual refresh stays in place without reopening the full-page loader", async ({ page }) => {
const options = { dashboardDelay: 350 };
await mockApplication(page, session(), options);
@@ -842,7 +875,7 @@ test("limit-up performance transfers the approved tier cards and market conclusi
await expect(page.locator("#performanceView")).toHaveClass(/redesigned-performance-view/);
await expect(page.locator("#performanceDateRange")).toHaveText("昨日 2026-07-21 → 今日 2026-07-22");
await expect(page.locator("#performanceCards .performance-stage-card")).toHaveCount(5);
await expect(page.locator("#performanceCards .performance-stage-card").first()).toContainText("昨日6板 → 今日");
await expect(page.locator("#performanceCards .performance-stage-card").first()).toContainText("昨日5板+ → 今日");
await expect(page.locator("#performanceCards .performance-stage-card").first()).toContainText("失效");
await expect(page.locator("#performanceCards .performance-stage-card").nth(2)).toContainText("50.0%");
await expect(page.locator("#performanceCards .performance-stage-card").nth(2)).toContainText("活跃");
@@ -944,7 +977,7 @@ test("sector rotation transfers the nine-day matrix, tracking and sortable detai
{ name: "电力", change: -1.6 },
];
renderRotationHistory();
renderRotationTable(state.dashboard.sector_rotation, state.dashboard.sectors);
renderRotationMembers();
});
await page.locator('[data-view="rotationView"]').first().click();
@@ -980,13 +1013,14 @@ test("sector rotation transfers the nine-day matrix, tracking and sortable detai
await page.locator('[data-rotation-sector="电网设备"]').first().click();
await expect(page.locator("#rotationTracker")).toBeVisible();
await expect(page.locator("#rotationTracker")).toContainText("近 9 日在榜 9 天");
await expect(page.locator("#rotationTableBody tr").first()).toHaveClass(/selected/);
await expect(page.locator("#rotationDetailTitle")).toHaveText("电网设备成分股");
await expect(page.locator("#rotationDetailMeta")).toContainText("2 / 3 只");
await expect(page.locator("#rotationTableBody tr")).toHaveCount(3);
await expect(page.locator("#rotationTableBody tr").first()).toContainText("长缆科技");
await expect(page.locator("#rotationTableBody tr").last()).toContainText("当日无行情");
await page.locator("#rotationTracker .rotation-track-cancel").click();
await expect(page.locator("#rotationTracker")).toBeHidden();
await expect(page.locator("#rotationTableBody .trend-new")).toContainText("新进");
await page.locator('#rotationTable th[title^="变化"]').click();
await expect(page.locator("#rotationTableBody tr").first()).toContainText("-7");
await expect(page.locator("#rotationMembersEmpty")).toContainText("点击上方任意板块查看成分股");
await page.locator('[data-rotation-order="latest"]').click();
await expect(page.locator("#rotationHistory .rotation-day").first()).toContainText("07-24");
await expect(page.locator("#rotationHistoryRange")).toContainText("由近到远,左侧为最新交易日");
@@ -1433,7 +1467,7 @@ test("regular account cannot see admin controls and member features are gated",
await expect(page.locator("#accountVipLabel")).toHaveText("非会员");
await page.locator('[data-view="screenerView"]').first().click();
await expect(page.locator("#screenerView .member-gate")).toBeVisible();
await page.locator('[data-screener-mode="quant"]').click();
await expect(page.locator('[data-screener-mode="quant"]')).toBeDisabled();
await expect(page.locator("#quantRunButton")).toBeDisabled();
await page.locator("#assistantButton").click();
await expect(page.locator("#settingsDialog")).toBeHidden();
@@ -1481,7 +1515,7 @@ test("rising candle body stays hollow and its wick stops at both edges", async (
canvas.width = 40;
canvas.height = 80;
const context = canvas.getContext("2d");
context.fillStyle = CHART_BACKGROUND;
context.fillStyle = currentChartPalette().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);
@@ -1639,21 +1673,18 @@ test("heart breathing prepares once then contracts on each exhale", async ({ pag
const timing = await page.evaluate(() => ({
remaining: state.heartBreathingEndsAt - Date.now(),
incenseDuration: getComputedStyle(document.querySelector("#heartIncenseEmber")).animationDuration,
incenseDelay: getComputedStyle(document.querySelector("#heartIncenseEmber")).animationDelay,
rippleDuration: getComputedStyle(document.querySelector(".heart-breath-ripple span")).animationDuration,
rippleDelay: getComputedStyle(document.querySelector(".heart-breath-ripple span")).animationDelay,
incenseNames: getComputedStyle(document.querySelector("#heartIncenseEmber")).animationName,
incenseDurations: getComputedStyle(document.querySelector("#heartIncenseEmber")).animationDuration,
incenseDelays: getComputedStyle(document.querySelector("#heartIncenseEmber")).animationDelay,
rippleAnimation: getComputedStyle(document.querySelector(".heart-breath-ripple span")).animationName,
rippleKeyframes: document.querySelector(".heart-breath-ripple span").getAnimations()[0].effect.getKeyframes(),
}));
expect(timing.remaining).toBeGreaterThan(45_000);
expect(timing.remaining).toBeLessThanOrEqual(46_000);
expect(timing.incenseDuration).toBe("45s");
expect(timing.incenseDelay).toBe("1s");
expect(timing.rippleDuration).toBe("9s");
expect(timing.rippleDelay).toBe("1s");
expect(timing.rippleAnimation).toContain("heart-breath-ripple");
expect(timing.rippleKeyframes.at(-1).transform).toContain("0.62");
expect(timing.incenseNames).toContain("heart-incense-burn");
expect(timing.incenseNames).toContain("heart-incense-glow");
expect(timing.incenseDurations).toContain("45s");
expect(timing.incenseDelays).toContain("1s");
expect(timing.rippleAnimation).toBe("none");
await expect(page.locator("#breathingScene")).toHaveAttribute("data-phase", "prepare");
await expect(page.locator("#breathingPhase")).toHaveText("静");
await expect(page.locator("#breathingSeconds, #breathingProgress, .breathing-orbit")).toHaveCount(0);
@@ -1667,6 +1698,8 @@ test("heart breathing prepares once then contracts on each exhale", async ({ pag
});
await expect(page.locator("#breathingScene")).toHaveAttribute("data-phase", "inhale");
await expect(page.locator("#breathingPhase")).toHaveText("吸");
await expect(page.locator(".heart-breath-ripple span").first()).toHaveCSS("transition-duration", "3s");
await expect(page.locator(".heart-breath-ripple span").first()).toHaveCSS("transform", /matrix\(1, 0, 0, 1,/);
await page.evaluate(() => {
state.heartSeconds = 42;
@@ -1683,6 +1716,7 @@ test("heart breathing prepares once then contracts on each exhale", async ({ pag
});
await expect(page.locator("#breathingScene")).toHaveAttribute("data-phase", "exhale");
await expect(page.locator("#breathingPhase")).toHaveText("呼");
await expect(page.locator(".heart-breath-ripple span").first()).toHaveCSS("transition-duration", "4s");
});
test("mobile shell stays within the viewport", async ({ page }) => {
@@ -1692,6 +1726,56 @@ 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();
const mobileShell = await page.evaluate(() => {
const header = document.querySelector(".topbar").getBoundingClientRect();
const main = document.querySelector(".app-main").getBoundingClientRect();
return { headerBottom: header.bottom, mainTop: main.top };
});
expect(mobileShell.mainTop).toBeGreaterThanOrEqual(mobileShell.headerBottom - 1);
});
test("native dialogs share one lifecycle and success feedback stays content-sized", async ({ page }) => {
await mockApplication(page, session("admin", true));
await page.goto("/index.html");
const dialogIds = [
"strategyDrawer",
"tradeLogDialog",
"watchlistDialog",
"heavenReadingDialog",
"globalSearchDialog",
"entityDetailDialog",
"stockDialog",
"alertsDialog",
"assistantDialog",
"settingsDialog",
"adminDialog",
];
await page.evaluate(() => {
openModalDialog(document.querySelector("#tradeLogDialog"));
openModalDialog(document.querySelector("#watchlistDialog"));
});
await expect(page.locator("dialog[open]")).toHaveCount(1);
await expect(page.locator("#watchlistDialog")).toBeVisible();
await expect(page.locator("#tradeLogDialog")).toBeHidden();
await page.keyboard.press("Escape");
await expect(page.locator("dialog[open]")).toHaveCount(0);
await page.locator('[data-view="screenerView"]').first().click();
await page.locator('[data-screener-mode="quant"]').click();
for (const id of dialogIds) {
await page.evaluate((dialogId) => openModalDialog(document.getElementById(dialogId)), id);
await expect(page.locator(`#${id}`)).toBeVisible();
await expect(page.locator(`#${id} button[aria-label*="关闭"]`).first()).toBeVisible();
await page.keyboard.press("Escape");
await expect(page.locator(`#${id}`)).toBeHidden();
}
await page.evaluate(() => showToast("交易记录已保存"));
const toastBox = await page.locator("#toast").boundingBox();
expect(toastBox.width).toBeLessThan(260);
expect(toastBox.height).toBeLessThan(80);
});
test("new review workflows render account-scoped records", async ({ page }) => {
@@ -1701,6 +1785,7 @@ test("new review workflows render account-scoped records", async ({ page }) => {
await page.locator('[data-view="screenerView"]').first().click();
await expect(page.locator('[data-screener-step="regime"]')).toHaveAttribute("data-state", "complete");
await page.locator('[data-screener-mode="quant"]').click();
await page.locator("#openStrategyDrawerButton").click();
await expect(page.locator("#strategyDrawer")).toBeVisible();
await expect(page.locator("#strategyNameInput")).toBeFocused();
@@ -2314,7 +2399,7 @@ test("mentor directory exposes evidence filters and private owner metadata", asy
await expect(page.locator("#mentorList .mentor-option")).toHaveCount(7);
await page.locator('#mentorList [data-mentor-id="source-b"]').click();
await expect(page.locator("#activeMentorName")).toHaveText("多源老师");
await expect(page.locator("#activeMentorBadges")).toContainText("B · 多源整理");
await expect(page.locator("#activeMentorBadges")).toHaveText("B");
await expect(page.locator("#activeMentorEvidence")).toHaveText("公开访谈与多源材料");
const mentorLibrary = await page.locator("#mentorView .mentor-sidebar").boundingBox();
const mentorChat = await page.locator("#mentorView .mentor-chat-panel").boundingBox();