feat: complete heaven readings and screener publication

This commit is contained in:
leefer
2026-08-05 23:44:17 +08:00
parent 1d01d114a5
commit c6d648cebe
34 changed files with 3538 additions and 225 deletions
+177 -1
View File
@@ -415,6 +415,15 @@ async function mockApplication(page, authSession = session(), options = {}) {
if (options.recentScreenerResults) {
payload.recent_results = options.recentScreenerResults;
}
if (options.screenerPublishedRuns) {
for (const strategy of payload.strategies) {
const publishedRun = options.screenerPublishedRuns[strategy.name];
if (publishedRun) strategy.published_run = publishedRun;
}
}
if (options.screenerSetupOverride) {
payload = { ...payload, ...options.screenerSetupOverride };
}
} else if (url.pathname === "/api/screener/run") {
const body = route.request().postDataJSON();
options.screenerRunBodies = [...(options.screenerRunBodies || []), body];
@@ -1781,6 +1790,7 @@ test("heart breathing prepares once then contracts on each exhale", async ({ pag
await page.goto("/index.html");
await page.locator('[data-view="heavenView"]').first().click();
await page.locator('[data-heaven-panel="heart"]').click();
await page.locator('[data-heart-question-preset="unthemed"]').click();
await page.evaluate(() => startHeartBreathing());
const timing = await page.evaluate(() => ({
@@ -1831,6 +1841,58 @@ test("heart breathing prepares once then contracts on each exhale", async ({ pag
await expect(page.locator(".heart-breath-ripple span").first()).toHaveCSS("transition-duration", "4s");
});
test("heart question presets stay editable and lock only during the ritual", async ({ page }) => {
await page.emulateMedia({ reducedMotion: "reduce" });
await mockApplication(page, session("user", true));
await page.goto("/index.html");
await page.locator('[data-view="heavenView"]').first().click();
await page.locator('[data-heaven-panel="heart"]').click();
const input = page.locator("#heartQuestionInput");
const start = page.locator("#startBreathingButton");
const trade = page.locator('[data-heart-question-preset="trade"]');
const mind = page.locator('[data-heart-question-preset="mind"]');
const unthemed = page.locator('[data-heart-question-preset="unthemed"]');
await expect(input).toHaveValue("");
await expect(start).toBeDisabled();
await expect(trade).toHaveAttribute("aria-pressed", "false");
await expect(mind).toHaveAttribute("aria-pressed", "false");
await expect(unthemed).toHaveAttribute("aria-pressed", "false");
await trade.click();
await expect(input).toHaveValue("关于我心中的这笔交易,此刻最需要看清的机会、阻碍与风险是什么?");
await expect(trade).toHaveAttribute("aria-pressed", "true");
await mind.click();
await expect(input).toHaveValue("此刻影响我交易判断的情绪、执念或盲点是什么?");
await expect(mind).toHaveAttribute("aria-pressed", "true");
await unthemed.click();
await expect(input).toHaveValue("不设具体问题,只观此刻一念。");
await expect(unthemed).toHaveAttribute("aria-pressed", "true");
await input.fill("我是否因为害怕错过而忽略了这笔交易的退出条件?");
await expect(start).toBeEnabled();
await expect(trade).toHaveAttribute("aria-pressed", "false");
await expect(mind).toHaveAttribute("aria-pressed", "false");
await expect(unthemed).toHaveAttribute("aria-pressed", "false");
expect(await page.evaluate(() => ({
question: state.heartQuestion,
preset: state.heartQuestionPreset,
}))).toEqual({
question: "我是否因为害怕错过而忽略了这笔交易的退出条件?",
preset: "custom",
});
await start.click();
await expect(page.locator("#heartBreathing")).toHaveClass(/active-heart-stage/);
await expect(input).toBeDisabled();
await expect(trade).toBeDisabled();
await page.locator("#heartBreathing [data-heart-return]").click();
await expect(page.locator("#heartIntro")).toHaveClass(/active-heart-stage/);
await expect(input).toBeEnabled();
await expect(input).toHaveValue("我是否因为害怕错过而忽略了这笔交易的退出条件?");
await expect(trade).toBeEnabled();
});
test("stylesheet layers do not repeat identical rules in the same cascade context", async ({ page }) => {
await mockApplication(page, session("admin", true));
await page.goto("/index.html");
@@ -2247,6 +2309,9 @@ test("screener redesign preserves three clear workspaces across desktop and mobi
const overflow = await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth);
expect(overflow).toBeLessThanOrEqual(1);
await expect(page.locator("#screenerView .screener-mode-tabs")).toBeVisible();
await page.locator('[data-screener-mobile-view="results"]').click();
await expect(page.locator("#screenerView .screener-results-view")).toBeVisible();
await expect(page.locator("#screenerEmpty")).toBeVisible();
});
test("automatic screener results stay read-only and mode results stay isolated", async ({ page }) => {
@@ -2283,7 +2348,7 @@ test("automatic screener results stay read-only and mode results stay isolated",
await expect(page.locator("#changeStrategyButton")).toHaveCount(0);
await expect(page.locator("#editStrategyButton")).toHaveCount(0);
await page.locator('[data-screener-mode="curated"]').click();
await expect(page.locator("#screenerEmpty")).toContainText("所选策略");
await expect(page.locator("#screenerEmpty")).toContainText("尚无成功发布");
await page.evaluate(() => {
setScreenerResult("curated", {
meta: { run_id: 52, trade_date: "20260722", regime: "repair", strategy_name: "连续分红质量" },
@@ -2313,6 +2378,117 @@ test("automatic screener results stay read-only and mode results stay isolated",
await expect(page.locator("#screenerTableBody")).not.toContainText("量化结果");
});
test("screener publishes atomically and exposes latest, active, and historical candidates", async ({ page }) => {
const candidate = {
code: "600001", ts_code: "600001.SH", name: "已发布候选", sector: "电力设备", price: 12,
score_display: 82, historical_probability: 45, probability_samples: 30,
pct_chg: 1.2, return_5d: 3.4, volume_ratio_5d: 1.5, sector_strength: 78,
reason: "板块强度、相对强度", risk_flags: [],
};
const smartResult = {
meta: {
run_id: 201, trade_date: "20260803", regime: "repair",
strategy_name: "修复确认", mode: "smart",
},
candidates: [candidate],
disclaimer: "历史统计不代表未来收益",
backtest: null,
};
const activeRow = {
mode: "smart", code: candidate.code, name: candidate.name, sector: candidate.sector,
selection_date: "20260803", score_display: candidate.score_display,
hits: [{
strategy_name: "修复确认", selection_date: "20260803", active: true,
validity: { label: "阶段内有效" }, valid_until: "", remaining_trading_days: null,
score_display: candidate.score_display,
}],
};
const expiredRow = {
mode: "smart", code: "600002", name: "历史候选", sector: "银行",
selection_date: "20260727", score_display: 76,
hits: [{
strategy_name: "修复确认", selection_date: "20260727", active: false,
validity: { label: "阶段内有效" }, valid_until: "20260801", remaining_trading_days: 0,
score_display: 76,
}],
};
const options = {
latestScreenerResults: { smart: smartResult },
recentScreenerResults: [smartResult],
screenerPublishedRuns: {
修复确认: { trade_date: "20260803", status: "ready", detail: "1 只候选" },
连续分红质量: { trade_date: "20260803", status: "no_signal", detail: "数据完整,暂无符合条件个股" },
数据缺失策略: { trade_date: "20260803", status: "missing_data", detail: "缺少近5日资金流" },
},
additionalScreenerStrategies: [{
id: 4, name: "数据缺失策略", description: "验证数据不足状态", regimes: ["repair"],
builtin: true, data_ready: false, missing_data: ["近5日资金流"],
formula: {
meta: { library: "curated", category: "资金动量", quality: "A", frequency: "每日", risk: "中" },
universe: { exclude_st: true }, filters: [], score: [], limit: 20, min_score: 0.5,
},
}],
screenerSetupOverride: {
trade_date: "20260804",
requested_trade_date: "20260804",
published_batch: {
trade_date: "20260803", status: "complete", is_fallback: true,
completed_count: 28, skipped_count: 1, finished_at: "2026-08-03T15:30:00+08:00",
notice: "所选日期候选正在生成,当前保留上一成功批次",
},
automatic_status: {
trade_date: "20260804", status: "running", retaining_trade_date: "20260803",
},
active_signals: [activeRow],
candidate_history: [activeRow, expiredRow],
},
};
await page.setViewportSize({ width: 1920, height: 1080 });
await mockApplication(page, session("user", true), options);
await page.goto("/index.html?view=screenerView");
await expect(page.locator("#screenerDateLabel")).toContainText("候选批次 2026-08-03");
await expect(page.locator("#screenerBatchNotice")).toContainText("当前保留上一成功批次");
await expect(page.locator("#screenerTableBody")).toContainText(candidate.name);
await page.locator('[data-screener-archive-view="active"]').click();
await expect(page.locator("#screenerResultTitle")).toHaveText("持续有效信号");
await expect(page.locator("#screenerArchiveTableBody")).toContainText(candidate.name);
await expect(page.locator("#screenerArchiveTableBody")).not.toContainText("历史候选");
await page.locator('[data-screener-archive-view="history"]').click();
await expect(page.locator("#screenerResultTitle")).toHaveText("历史入选记录");
await expect(page.locator("#screenerArchiveTableBody")).toContainText(candidate.name);
await expect(page.locator("#screenerArchiveTableBody")).toContainText("历史候选");
await page.locator('[data-screener-mode="curated"]').click();
await expect(page.locator("#screenerEmpty")).toHaveText("数据完整,暂无符合条件个股");
await expect(page.locator("#screenerTableBody")).not.toContainText(candidate.name);
await page.locator('[data-curated-strategy="4"]').click();
await expect(page.locator("#screenerEmpty")).toContainText("缺少必需数据");
await expect(page.locator("#curatedDataStatus")).toContainText("缺少近5日资金流");
await page.locator('[data-screener-mode="smart"]').click();
await page.reload();
await expect(page.locator("#screenerTableBody")).toContainText(candidate.name);
await expect(page.locator("#screenerBatchNotice")).toContainText("当前保留上一成功批次");
expect(options.screenerSetupRequests).toBe(2);
await page.setViewportSize({ width: 390, height: 844 });
await page.locator('[data-screener-mobile-view="results"]').click();
await expect(page.locator("#screenerView .screener-results-view")).toBeVisible();
await expect(page.locator("#screenerTableBody")).toContainText(candidate.name);
const overflow = await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth);
expect(overflow).toBeLessThanOrEqual(1);
await page.locator("#themeToggle").click();
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
await expect(page.locator("#screenerView .screener-results-view")).toBeVisible();
await page.locator("#themeToggle").click();
await expect(page.locator("html")).toHaveAttribute("data-theme", "light");
});
test("screener restores automatic stage and curated pools across switching and reload", async ({ page }) => {
const formula = {
meta: { library: "smart" }, universe: {}, filters: [],