summaryrefslogtreecommitdiff
path: root/qa
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-21 23:21:54 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-21 23:21:54 +0800
commitd4b539a2791a0cd4d098fecfece0039073a9f7e2 (patch)
treedfd314b37fa24379ef13dcd66a1de76e2a923bf2 /qa
parent12c4d3625458c830a2746a1f277fb684e9c498cb (diff)
downloadfund-lab-d4b539a2791a0cd4d098fecfece0039073a9f7e2.tar.gz
Add scheduled investment plans (3d-10)
Diffstat (limited to 'qa')
-rw-r--r--qa/driver/browser-test.js111
1 files changed, 111 insertions, 0 deletions
diff --git a/qa/driver/browser-test.js b/qa/driver/browser-test.js
index 6403dc5..6b2d2ec 100644
--- a/qa/driver/browser-test.js
+++ b/qa/driver/browser-test.js
@@ -26,6 +26,8 @@ let lastOrdersResponse = null;
let lastConfirmResponse = null;
let lastRedemptionConfirmResponse = null;
let lastReturnsResponse = null;
+let lastPlanRunResponse = null;
+let lastPlansResponse = null;
function check(name, ok, detail) {
results.push({ name, ok, detail: detail || "" });
@@ -135,6 +137,16 @@ async function summaryLabelExists(page, label, name) {
lastReturnsResponse = await r.json();
} catch {}
}
+ if (r.url().includes("/investment-plans/run") && r.request().method() === "POST" && r.status() < 400) {
+ try {
+ lastPlanRunResponse = await r.json();
+ } catch {}
+ }
+ if (/\/investment-plans$/i.test(new URL(r.url()).pathname) && r.request().method() === "GET" && r.status() < 400) {
+ try {
+ lastPlansResponse = await r.json();
+ } catch {}
+ }
if (r.status() >= 400 && !/\/api\/instruments\//.test(r.url()) && !/\/orders/.test(r.url()) && !/\/redemptions/.test(r.url()) && !/\/capital/.test(r.url())) {
consoleErrors.push("resource " + r.status() + ": " + r.url());
}
@@ -182,6 +194,7 @@ async function summaryLabelExists(page, label, name) {
await capitalScenario(page);
await dividendScenario(page);
await returnsScenario(page);
+ await plansScenario(page);
} finally {
check("G1 无浏览器控制台/页面错误", consoleErrors.length === 0, consoleErrors.slice(0, 3).join(" | "));
await browser.close();
@@ -463,6 +476,104 @@ async function returnsScenario(page) {
await page.screenshot({ path: SHOTS + "/16-returns-curve.png" });
}
+async function postInvestmentPlanViaApi(page, fundId, token) {
+ return page.evaluate(
+ async ({ fundId, token }) => {
+ const randomKey = () => {
+ if (window.crypto && window.crypto.randomUUID) return window.crypto.randomUUID().replace(/-/g, "");
+ return Math.random().toString(16).slice(2).padEnd(32, "0").slice(0, 32);
+ };
+ const response = await fetch(`/api/funds/${fundId}/investment-plans`, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${token}`,
+ "Idempotency-Key": randomKey(),
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ instrumentCode: "000001", amount: "100.00", frequency: "daily" }),
+ });
+ return response.status;
+ },
+ { fundId, token }
+ );
+}
+
+async function plansScenario(page) {
+ // P 系列: 定投计划执行最小闭环(接 R 场景:现金 5442.99,持仓 50.00000000)
+ await page.waitForSelector(".plans-panel", { timeout: 10000 });
+ const fundId = await currentFundId(page);
+ const token = process.env.QA_TOKEN || "qa-token";
+
+ const createStatus = await postInvestmentPlanViaApi(page, fundId, token);
+ await page.click(".plans-refresh-action");
+ const planRowAppeared = await page
+ .waitForSelector(".plans-list .order-row", { timeout: 15000 })
+ .then(() => true)
+ .catch(() => false);
+ const planListText = ((await page.textContent(".plans-list")) || "");
+
+ lastPlanRunResponse = null;
+ await page.click(".plans-run-action");
+ for (let i = 0; i < 200 && lastPlanRunResponse === null; i++) {
+ await page.waitForTimeout(100);
+ }
+ const run = lastPlanRunResponse || {};
+ const runPlans = Array.isArray(run.plans) ? run.plans : [];
+ const firstPlan = runPlans.length > 0 ? runPlans[0] : {};
+ const runs = Array.isArray(firstPlan.runs) ? firstPlan.runs : [];
+ const firstRun = runs.length > 0 ? runs[0] : {};
+
+ for (let i = 0; i < 200 && (lastPositionsResponse === null || lastPositionsResponse.availableCash !== "5343.00"); i++) {
+ await page.waitForTimeout(100);
+ }
+
+ for (let i = 0; i < 200 && lastPlansResponse === null; i++) {
+ await page.waitForTimeout(100);
+ }
+
+ const plans = Array.isArray(lastPlansResponse) ? lastPlansResponse : [];
+ const plan = plans.length > 0 ? plans[0] : {};
+
+ check(
+ "P1 到期定投计划按当日净值扣款买入",
+ createStatus === 201 &&
+ planRowAppeared &&
+ planListText.includes("000001") &&
+ planListText.includes("金额 100.00") &&
+ planListText.includes("频率 每日") &&
+ plan.amount === "100.00" &&
+ plan.frequency === "daily" &&
+ run.processingDate === "2026-09-21" &&
+ firstRun.runDate === "2026-09-21" &&
+ firstRun.status === "succeeded" &&
+ lastPositionsResponse &&
+ lastPositionsResponse.availableCash === "5343.00" &&
+ lastPositionsResponse.positions?.[0]?.units === "134.01949252",
+ `status=${createStatus} run=${JSON.stringify(run).slice(0, 240)} cash=${lastPositionsResponse?.availableCash}`
+ );
+
+ lastPlanRunResponse = null;
+ await page.click(".plans-run-action");
+ for (let i = 0; i < 200 && lastPlanRunResponse === null; i++) {
+ await page.waitForTimeout(100);
+ }
+ const replay = lastPlanRunResponse || {};
+ const replayPlans = Array.isArray(replay.plans) ? replay.plans : [];
+ const replayRuns = replayPlans.length > 0 && Array.isArray(replayPlans[0].runs) ? replayPlans[0].runs : [];
+
+ check(
+ "P2 重复执行幂等且不产生第二笔扣款",
+ replayRuns.length === 1 &&
+ replayRuns[0].status === "succeeded" &&
+ lastPositionsResponse &&
+ lastPositionsResponse.availableCash === "5343.00" &&
+ lastPositionsResponse.positions?.[0]?.units === "134.01949252",
+ `runs=${JSON.stringify(replayRuns).slice(0, 200)} cash=${lastPositionsResponse?.availableCash}`
+ );
+
+ await page.screenshot({ path: SHOTS + "/17-investment-plans.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);