diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-22 05:23:52 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-22 05:23:52 +0800 |
| commit | 6abd4c206fbfe8369b3fde4c905619d08f13c5d1 (patch) | |
| tree | f53ce1746591b640f78cd11d63c8411160059d98 /qa/driver | |
| parent | 545f2a507fd0138f7fd50ed7fb2631d0273db625 (diff) | |
| download | fund-lab-6abd4c206fbfe8369b3fde4c905619d08f13c5d1.tar.gz | |
Add SIP plan pause/resume and rebalance plan archive lifecycle (3d-24)
Diffstat (limited to 'qa/driver')
| -rw-r--r-- | qa/driver/browser-test.js | 186 |
1 files changed, 185 insertions, 1 deletions
diff --git a/qa/driver/browser-test.js b/qa/driver/browser-test.js index 8ad9ab5..dd49263 100644 --- a/qa/driver/browser-test.js +++ b/qa/driver/browser-test.js @@ -42,6 +42,8 @@ let lastStockPositionsResponse = null; let lastBondTradeResponse = null; let lastBondPositionsResponse = null; let lastValuationResponse = null; +let lastSipStatusResponse = null; +let lastSipAdvanceResponse = null; function check(name, ok, detail) { results.push({ name, ok, detail: detail || "" }); @@ -231,7 +233,17 @@ async function summaryLabelExists(page, label, name) { lastValuationResponse = await r.json(); } catch {} } - if (r.status() >= 400 && !/\/api\/instruments\//.test(r.url()) && !/\/api\/market\//.test(r.url()) && !/\/orders/.test(r.url()) && !/\/redemptions/.test(r.url()) && !/\/capital/.test(r.url()) && !/\/stock-trades/.test(r.url()) && !/\/stock-positions/.test(r.url()) && !/\/bond-trades/.test(r.url()) && !/\/bond-positions/.test(r.url()) && !/\/valuation/.test(r.url())) { + if (/\/sip\/plans\/[0-9a-f-]+\/(pause|resume)$/i.test(new URL(r.url()).pathname) && r.request().method() === "POST" && r.status() < 400) { + try { + lastSipStatusResponse = await r.json(); + } catch {} + } + if (/\/sip\/advance$/i.test(new URL(r.url()).pathname) && r.request().method() === "POST" && r.status() < 400) { + try { + lastSipAdvanceResponse = await r.json(); + } catch {} + } + if (r.status() >= 400 && !/\/api\/instruments\//.test(r.url()) && !/\/api\/market\//.test(r.url()) && !/\/orders/.test(r.url()) && !/\/redemptions/.test(r.url()) && !/\/capital/.test(r.url()) && !/\/stock-trades/.test(r.url()) && !/\/stock-positions/.test(r.url()) && !/\/bond-trades/.test(r.url()) && !/\/bond-positions/.test(r.url()) && !/\/valuation/.test(r.url()) && !/\/rebalance\/plans\//.test(r.url())) { consoleErrors.push("resource " + r.status() + ": " + r.url()); } }); @@ -288,6 +300,8 @@ async function summaryLabelExists(page, label, name) { await stockTradeScenario(page); await bondTradeScenario(page); await valuationScenario(page); + await sipStatusScenario(page); + await rebalanceArchiveScenario(page); } finally { check("G1 无浏览器控制台/页面错误", consoleErrors.length === 0, consoleErrors.slice(0, 3).join(" | ")); await browser.close(); @@ -1220,6 +1234,111 @@ async function plansScenario(page) { await page.screenshot({ path: SHOTS + "/17-investment-plans.png" }); } +async function sipStatusScenario(page) { + // P8: 定投计划暂停/恢复最小闭环(暂停时不扣款,恢复后到期继续执行)。 + await page.waitForSelector(".sip-panel", { timeout: 10000 }); + const fundId = await currentFundId(page); + const token = process.env.QA_TOKEN || "qa-token"; + + const planId = await page.evaluate( + async ({ fundId, token }) => { + const response = await fetch(`/api/funds/${fundId}/sip/plans`, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + "Idempotency-Key": "qasippauseresume", + }, + body: JSON.stringify({ instrumentCode: "000001", amount: "10.00", frequency: "weekly" }), + }); + if (response.status !== 201) return ""; + const body = await response.json(); + return body.id || ""; + }, + { fundId, token } + ); + + await page.click(".sip-plans-refresh-action"); + await page.waitForSelector(".sip-panel .sip-plans .order-row", { timeout: 15000 }); + + lastSipStatusResponse = null; + await page.click(".sip-pause-action"); + + for (let i = 0; i < 100 && lastSipStatusResponse === null; i++) { + await page.waitForTimeout(100); + } + + const pausedText = ((await page.textContent(".sip-plans")) || "").trim(); + + const pausedAdvance = await page.evaluate( + async ({ fundId, token }) => { + const response = await fetch(`/api/funds/${fundId}/sip/advance`, { + method: "POST", + headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, + body: JSON.stringify({ endDate: "2026-10-31" }), + }); + const body = await response.json(); + return body; + }, + { fundId, token } + ); + + const pausedPlans = pausedAdvance && Array.isArray(pausedAdvance.plans) ? pausedAdvance.plans : []; + const pausedOutcomes = pausedPlans.reduce( + (acc, plan) => acc.concat(Array.isArray(plan.executions) ? plan.executions : []), + [] + ); + + check( + "P8a 暂停定投计划不参与到期扣款", + planId !== "" && + lastSipStatusResponse && + lastSipStatusResponse.status === "paused" && + pausedText.includes("已暂停") && + pausedOutcomes.length === 0, + `plan=${planId} status=${lastSipStatusResponse?.status} outcomes=${pausedOutcomes.length} text=${pausedText.slice(0, 160)}` + ); + + lastSipStatusResponse = null; + await page.click(".sip-resume-action"); + + for (let i = 0; i < 100 && lastSipStatusResponse === null; i++) { + await page.waitForTimeout(100); + } + + const resumedText = ((await page.textContent(".sip-plans")) || "").trim(); + + const resumedAdvance = await page.evaluate( + async ({ fundId, token }) => { + const response = await fetch(`/api/funds/${fundId}/sip/advance`, { + method: "POST", + headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, + body: JSON.stringify({ endDate: "2026-10-31" }), + }); + const body = await response.json(); + return body; + }, + { fundId, token } + ); + + const resumedPlans = resumedAdvance && Array.isArray(resumedAdvance.plans) ? resumedAdvance.plans : []; + const resumedOutcomes = resumedPlans.reduce( + (acc, plan) => acc.concat(Array.isArray(plan.executions) ? plan.executions : []), + [] + ); + + check( + "P8b 恢复定投计划后到期继续执行", + lastSipStatusResponse && + lastSipStatusResponse.status === "active" && + resumedText.includes("进行中") && + resumedOutcomes.length > 0, + `status=${lastSipStatusResponse?.status} outcomes=${resumedOutcomes.length} text=${resumedText.slice(0, 160)}` + ); + + await page.screenshot({ path: SHOTS + "/27-sip-status.png" }); +} + async function rebalanceScenario(page) { // R 系列: 再平衡最小闭环(接 P 场景:现金 5343.00,持仓 134.01949252 @1.1902) await page.waitForSelector(".rebalance-panel", { timeout: 10000 }); @@ -1334,6 +1453,71 @@ async function rebalanceScenario(page) { await page.screenshot({ path: SHOTS + "/18-rebalance.png" }); } +async function rebalanceArchiveScenario(page) { + // P9: 再平衡计划归档后不可预览/执行。 + await page.waitForSelector(".rebalance-panel", { timeout: 10000 }); + const fundId = await currentFundId(page); + const token = process.env.QA_TOKEN || "qa-token"; + + await page.waitForSelector(".rebalance-plans .rebalance-plan-row", { timeout: 15000 }); + + const activePreviewCount = await page.locator(".rebalance-preview-action").count(); + const activeArchiveCount = await page.locator(".rebalance-archive-action").count(); + + await page.click(".rebalance-archive-action"); + await page.waitForFunction( + () => { + const element = document.querySelector(".rebalance-plans"); + return element && element.textContent.includes("已归档"); + }, + { timeout: 10000 } + ); + + const archivedText = ((await page.textContent(".rebalance-plans")) || "").trim(); + const archivedPreviewCount = await page.locator(".rebalance-preview-action").count(); + + const planId = await page.evaluate( + async ({ fundId, token }) => { + const response = await fetch(`/api/funds/${fundId}/rebalance/plans`, { + headers: { Authorization: `Bearer ${token}` }, + }); + const plans = await response.json(); + const archived = (Array.isArray(plans) ? plans : []).find((plan) => plan.status === "archived"); + return archived ? archived.id : ""; + }, + { fundId, token } + ); + + const previewStatus = await page.evaluate( + async ({ fundId, planId, token }) => { + const response = await fetch(`/api/funds/${fundId}/rebalance/plans/${planId}/preview`, { + headers: { Authorization: `Bearer ${token}` }, + }); + let body = null; + try { + body = await response.json(); + } catch {} + return { status: response.status, body }; + }, + { fundId, planId, token } + ); + + check( + "P9 归档后再平衡计划不可预览且按钮隐藏", + activePreviewCount > 0 && + activeArchiveCount > 0 && + archivedText.includes("已归档") && + archivedPreviewCount === 0 && + planId !== "" && + previewStatus.status === 409 && + previewStatus.body && + previewStatus.body.error === "PLAN_ARCHIVED", + `active=${activePreviewCount} archived=${archivedPreviewCount} status=${previewStatus.status} text=${archivedText.slice(0, 160)}` + ); + + await page.screenshot({ path: SHOTS + "/28-rebalance-archive.png" }); +} + const failed = results.filter((r) => !r.ok); console.log(`\n==== ${results.length - failed.length}/${results.length} passed ====`); process.exit(failed.length > 0 ? 1 : 0); |
