feat: complete heaven readings and screener publication
This commit is contained in:
+177
-1
@@ -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: [],
|
||||
|
||||
Reference in New Issue
Block a user