summaryrefslogtreecommitdiff
path: root/qa/driver
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-21 23:47:50 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-21 23:47:50 +0800
commit36ee23a0c406c16e975276238a861299899ec79a (patch)
tree7085f24566b7c53ad75c1ddf8130e563513f673a /qa/driver
parentd4b539a2791a0cd4d098fecfece0039073a9f7e2 (diff)
downloadfund-lab-36ee23a0c406c16e975276238a861299899ec79a.tar.gz
Add rebalancing execution slice (3d-11)
Diffstat (limited to 'qa/driver')
-rw-r--r--qa/driver/browser-test.js139
1 files changed, 139 insertions, 0 deletions
diff --git a/qa/driver/browser-test.js b/qa/driver/browser-test.js
index 6b2d2ec..c7ee468 100644
--- a/qa/driver/browser-test.js
+++ b/qa/driver/browser-test.js
@@ -28,6 +28,10 @@ let lastRedemptionConfirmResponse = null;
let lastReturnsResponse = null;
let lastPlanRunResponse = null;
let lastPlansResponse = null;
+let lastRebalancePlansResponse = null;
+let lastRebalancePreviewResponse = null;
+let lastRebalanceExecuteResponse = null;
+let lastRebalanceExecutionsResponse = null;
function check(name, ok, detail) {
results.push({ name, ok, detail: detail || "" });
@@ -147,6 +151,26 @@ async function summaryLabelExists(page, label, name) {
lastPlansResponse = await r.json();
} catch {}
}
+ if (/\/rebalance\/plans$/i.test(new URL(r.url()).pathname) && r.request().method() === "GET" && r.status() < 400) {
+ try {
+ lastRebalancePlansResponse = await r.json();
+ } catch {}
+ }
+ if (/\/rebalance\/plans\/[0-9a-f-]+\/preview$/i.test(new URL(r.url()).pathname) && r.request().method() === "GET" && r.status() < 400) {
+ try {
+ lastRebalancePreviewResponse = await r.json();
+ } catch {}
+ }
+ if (/\/rebalance\/plans\/[0-9a-f-]+\/execute$/i.test(new URL(r.url()).pathname) && r.request().method() === "POST" && r.status() < 400) {
+ try {
+ lastRebalanceExecuteResponse = await r.json();
+ } catch {}
+ }
+ if (/\/rebalance\/executions$/i.test(new URL(r.url()).pathname) && r.request().method() === "GET" && r.status() < 400) {
+ try {
+ lastRebalanceExecutionsResponse = 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());
}
@@ -195,6 +219,7 @@ async function summaryLabelExists(page, label, name) {
await dividendScenario(page);
await returnsScenario(page);
await plansScenario(page);
+ await rebalanceScenario(page);
} finally {
check("G1 无浏览器控制台/页面错误", consoleErrors.length === 0, consoleErrors.slice(0, 3).join(" | "));
await browser.close();
@@ -574,6 +599,120 @@ async function plansScenario(page) {
await page.screenshot({ path: SHOTS + "/17-investment-plans.png" });
}
+async function rebalanceScenario(page) {
+ // R 系列: 再平衡最小闭环(接 P 场景:现金 5343.00,持仓 134.01949252 @1.1902)
+ await page.waitForSelector(".rebalance-panel", { timeout: 10000 });
+
+ const eyebrow = ((await page.textContent(".rebalance-panel .eyebrow")) || "").trim();
+ check("P3 再平衡面板 eyebrow 为 12 / REBALANCE", eyebrow === "12 / REBALANCE", eyebrow);
+ await page.locator(".rebalance-target-code-input").first().fill("000001");
+ await page.locator(".rebalance-target-percent-input").first().fill("100.00");
+ await page.click(".rebalance-submit-action");
+ const planRowAppeared = await page
+ .waitForSelector(".rebalance-plan-row", { timeout: 15000 })
+ .then(() => true)
+ .catch(() => false);
+
+ for (let i = 0; i < 200 && lastRebalancePlansResponse === null; i++) {
+ await page.waitForTimeout(100);
+ }
+
+ const plans = Array.isArray(lastRebalancePlansResponse) ? lastRebalancePlansResponse : [];
+ const plan = plans.length > 0 ? plans[0] : {};
+ const planText = ((await page.textContent(".rebalance-plans")) || "");
+
+ check(
+ "P4 目标权重计划创建(000001 100%)",
+ planRowAppeared && planText.includes("000001 100.00%") && Array.isArray(plan.targets) && plan.targets[0]?.targetPercent === "100.00",
+ `plans=${JSON.stringify(plans).slice(0, 200)}`
+ );
+
+ lastRebalancePreviewResponse = null;
+ await page.click(".rebalance-preview-action");
+ for (let i = 0; i < 200 && lastRebalancePreviewResponse === null; i++) {
+ await page.waitForTimeout(100);
+ }
+
+ const preview = lastRebalancePreviewResponse || {};
+ const previewRows = Array.isArray(preview.rows) ? preview.rows : [];
+ const previewRow = previewRows.length > 0 ? previewRows[0] : {};
+ const previewText = ((await page.textContent(".rebalance-preview")) || "");
+
+ check(
+ "P5 预览展示目标对比当前及差额买入",
+ previewRow.instrumentCode === "000001" &&
+ previewRow.targetPercent === "100.00" &&
+ previewRow.currentValue === "159.51" &&
+ previewRow.currentPercent === "2.89" &&
+ previewRow.action === "buy" &&
+ previewRow.deltaAmount === "5343.00" &&
+ preview.availableCash === "5343.00" &&
+ preview.equity === "5502.51" &&
+ previewText.includes("目标 100.00%") &&
+ previewText.includes("当前 2.89%"),
+ `preview=${JSON.stringify(preview).slice(0, 260)}`
+ );
+
+ lastRebalanceExecuteResponse = null;
+ await page.click(".rebalance-execute-action");
+ for (let i = 0; i < 200 && lastRebalanceExecuteResponse === null; i++) {
+ await page.waitForTimeout(100);
+ }
+
+ const execution = lastRebalanceExecuteResponse || {};
+ const outcomes = Array.isArray(execution.outcomes) ? execution.outcomes : [];
+
+ for (let i = 0; i < 200 && (lastPositionsResponse === null || lastPositionsResponse.availableCash !== "0.01"); i++) {
+ await page.waitForTimeout(100);
+ }
+
+ for (let i = 0; i < 200 && lastRebalanceExecutionsResponse === null; i++) {
+ await page.waitForTimeout(100);
+ }
+
+ const executions = Array.isArray(lastRebalanceExecutionsResponse) ? lastRebalanceExecutionsResponse : [];
+ const boughtUnits = parseFloat(lastPositionsResponse?.positions?.[0]?.units || "0");
+
+ check(
+ "P6 执行再平衡按当日净值补足差额",
+ execution.runDate === "2026-09-21" &&
+ outcomes.length === 1 &&
+ outcomes[0].instrumentCode === "000001" &&
+ outcomes[0].action === "buy" &&
+ outcomes[0].amount === "5343.00" &&
+ outcomes[0].status === "succeeded" &&
+ lastPositionsResponse &&
+ lastPositionsResponse.availableCash === "0.01" &&
+ boughtUnits > 134.01949252 &&
+ executions.length === 1 &&
+ executions[0].action === "buy" &&
+ executions[0].status === "succeeded",
+ `outcomes=${JSON.stringify(outcomes).slice(0, 200)} cash=${lastPositionsResponse?.availableCash} units=${lastPositionsResponse?.positions?.[0]?.units}`
+ );
+
+ const unitsAfterFirst = lastPositionsResponse?.positions?.[0]?.units;
+
+ lastRebalanceExecuteResponse = null;
+ await page.click(".rebalance-execute-action");
+ for (let i = 0; i < 200 && lastRebalanceExecuteResponse === null; i++) {
+ await page.waitForTimeout(100);
+ }
+
+ const replay = lastRebalanceExecuteResponse || {};
+ const replayOutcomes = Array.isArray(replay.outcomes) ? replay.outcomes : [];
+
+ check(
+ "P7 再次执行容差内无新交易",
+ replayOutcomes.length === 0 &&
+ lastPositionsResponse &&
+ lastPositionsResponse.availableCash === "0.01" &&
+ lastPositionsResponse.positions?.[0]?.units === unitsAfterFirst,
+ `outcomes=${JSON.stringify(replayOutcomes).slice(0, 160)} cash=${lastPositionsResponse?.availableCash}`
+ );
+
+ await page.screenshot({ path: SHOTS + "/18-rebalance.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);