summaryrefslogtreecommitdiff
path: root/qa/driver/browser-test.js
diff options
context:
space:
mode:
Diffstat (limited to 'qa/driver/browser-test.js')
-rw-r--r--qa/driver/browser-test.js186
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);