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.js162
1 files changed, 158 insertions, 4 deletions
diff --git a/qa/driver/browser-test.js b/qa/driver/browser-test.js
index 4e557d3..46a8f85 100644
--- a/qa/driver/browser-test.js
+++ b/qa/driver/browser-test.js
@@ -9,12 +9,17 @@ const consoleErrors = [];
const fundPosts = [];
const orderPosts = [];
const orderPostStatuses = [];
+const confirmPosts = [];
+const confirmPostStatuses = [];
+let positionGets = 0;
+let lastPositionsResponse = null;
let fundPostResponses = 0;
let fundGets = 0;
let orderGets = 0;
let createdFund = null;
let createdFundResponse = null;
let lastOrdersResponse = null;
+let lastConfirmResponse = null;
function check(name, ok, detail) {
results.push({ name, ok, detail: detail || "" });
@@ -101,6 +106,18 @@ async function summaryLabelExists(page, label, name) {
} catch {}
}
if (r.url().includes("/orders") && r.request().method() === "POST") orderPostStatuses.push(r.status());
+ if (r.url().includes("/confirm") && r.request().method() === "POST") {
+ confirmPostStatuses.push(r.status());
+ try {
+ lastConfirmResponse = await r.json();
+ } catch {}
+ }
+ if (r.url().includes("/positions") && r.request().method() === "GET" && r.status() < 400) {
+ positionGets++;
+ try {
+ lastPositionsResponse = await r.json();
+ } catch {}
+ }
if (r.status() >= 400 && !/\/api\/instruments\//.test(r.url()) && !/\/orders/.test(r.url())) {
consoleErrors.push("resource " + r.status() + ": " + r.url());
}
@@ -109,11 +126,15 @@ async function summaryLabelExists(page, label, name) {
if (r.url().includes("/api/funds") && r.method() === "POST" && !r.url().includes("/orders")) {
fundPosts.push({ url: r.url(), key: r.headers()["idempotency-key"] || "", body: r.postData() || "" });
}
- if (r.url().includes("/orders") && r.method() === "POST") {
+ if (r.url().includes("/orders") && !r.url().includes("/confirm") && r.method() === "POST") {
orderPosts.push({ url: r.url(), key: r.headers()["idempotency-key"] || "", body: r.postData() || "" });
}
+ if (r.url().includes("/confirm") && r.method() === "POST") {
+ confirmPosts.push({ url: r.url(), key: r.headers()["idempotency-key"] || "" });
+ }
});
page.on("console", (m) => {
+ if (/^DBG /.test(m.text())) console.log("[dbg] " + m.text());
if (m.type() !== "error") return;
const text = m.text();
if (/favicon/i.test(text)) return;
@@ -133,6 +154,7 @@ async function summaryLabelExists(page, label, name) {
await staleTokenScenario(page);
await idempotentRetryScenario(page);
await orderScenario(page);
+ await confirmScenario(page);
} finally {
check("G1 无浏览器控制台/页面错误", consoleErrors.length === 0, consoleErrors.slice(0, 3).join(" | "));
await browser.close();
@@ -518,7 +540,139 @@ async function orderScenario(page) {
await page.waitForTimeout(1800);
const rowsAfterStale = await page.locator(".order-row").count();
check("G24 延迟订单响应落地后仍被清除(stale 忽略)", rowsAfterStale === 0, `rows=${rowsAfterStale}`);
- await page.unroute("**/api/funds/*/orders");
- await page.fill(".token-input", "qa-token");
- await page.screenshot({ path: SHOTS + "/10-order-stale-token.png" });
+ await page.unroute("**/api/funds/*/orders");
+ await page.fill(".token-input", "qa-token");
+ await page.screenshot({ path: SHOTS + "/10-order-stale-token.png" });
+}
+
+async function confirmScenario(page) {
+ // J 系列: 确认动作 + 确认状态展示 + 持仓联动(G23/G24 换 token 后 createdFund 已清空,先重建基金)
+ await page.waitForSelector(".positions-panel", { timeout: 10000 });
+ const panelEyebrow = ((await page.textContent(".positions-panel .eyebrow")) || "").trim();
+ check("J1 持仓面板 eyebrow 为 05 / HOLDINGS", panelEyebrow === "05 / HOLDINGS", panelEyebrow);
+ const noFundHint = ((await page.textContent(".positions-panel")) || "").trim();
+ check("J2 创建基金前持仓面板提示", noFundHint.includes("创建基金后"), noFundHint.slice(0, 60));
+
+ await page.fill(".fund-name-input", "确认持仓测试");
+ await page.fill(".fund-cash-input", "5000.00");
+ await page.click(".fund-create-action");
+ await page.waitForSelector(".fund-summary", { timeout: 15000 });
+
+ await page.fill(".order-code-input", "000001");
+ await page.fill(".order-amount-input", "100.00");
+ await page.fill(".order-fee-input", "1.50");
+ await page.click(".order-submit-action");
+ await page.waitForSelector(".order-row", { timeout: 15000 });
+ const submittedText = ((await page.textContent(".pending-orders")) || "");
+ check(
+ "J3 下单后显示已提交·待确认与交易日",
+ submittedText.includes("已提交 · 待确认") && submittedText.includes("交易日 2026-09-21"),
+ submittedText.slice(0, 160)
+ );
+ check("J4 下单后无持仓行", (await page.locator(".position-row").count()) === 0, "");
+
+ // 采集净值入库(getNav 只读库,refreshNav 才写库),确保确认时可用
+ await page.fill(".search-input", "000001");
+ await page.click(".search-row .primary-action");
+ await page.waitForSelector(".result-list", { timeout: 15000 });
+ await page.click(".result-list .result-row");
+ await page.click(".nav-panel .primary-action");
+ await page.waitForSelector(".nav-panel .metric-strip", { timeout: 20000 });
+ await page.waitForTimeout(500);
+
+ await page.click(".order-confirm-action");
+ for (let i = 0; i < 200 && (confirmPostStatuses.length === 0 || lastConfirmResponse === null); i++) {
+ await page.waitForTimeout(100);
+ }
+ check("J5 确认 POST 恰好 1 次且幂等键 32 位十六进制", confirmPosts.length === 1 && /^[0-9a-f]{32}$/.test(confirmPosts[0].key), JSON.stringify(confirmPosts));
+ const confirmStatus = lastConfirmResponse ? lastConfirmResponse.status : "";
+ const confirmed = confirmStatus === "confirmed";
+ check("J6 确认响应状态为 confirmed 或 pending_nav", confirmed || confirmStatus === "pending_nav", confirmStatus);
+ if (confirmed) {
+ check(
+ "J7 已确认分支展示 NAV/份额/日期精确字符串",
+ lastConfirmResponse.confirmedNav === "1.19020000" &&
+ /^[0-9]+\.[0-9]{8}$/.test(lastConfirmResponse.confirmedUnits || "") &&
+ lastConfirmResponse.confirmedNavDate === "2026-09-21",
+ JSON.stringify(lastConfirmResponse).slice(0, 200)
+ );
+ } else {
+ check(
+ "J7 待净值分支展示 pendingReason",
+ typeof lastConfirmResponse.pendingReason === "string" && lastConfirmResponse.pendingReason.length > 0,
+ JSON.stringify(lastConfirmResponse).slice(0, 200)
+ );
+ }
+
+ // 订单行展示确认状态与结果
+ await page.waitForFunction(
+ () => {
+ const rows = Array.from(document.querySelectorAll(".order-row"));
+ return rows.some((r) => r.textContent.includes("已确认") || r.textContent.includes("等待净值"));
+ },
+ null,
+ { timeout: 15000 }
+ );
+ const orderRowText = ((await page.textContent(".pending-orders")) || "");
+ if (confirmed) {
+ check(
+ "J8 订单行显示已确认与确认净值 1.19020000",
+ orderRowText.includes("已确认") && orderRowText.includes("确认净值 1.19020000"),
+ orderRowText.slice(0, 240)
+ );
+ } else {
+ check(
+ "J8 订单行显示等待净值与原因",
+ orderRowText.includes("等待净值") && orderRowText.includes("净值尚未公布") === false,
+ orderRowText.slice(0, 240)
+ );
+ }
+
+ // 持仓联动:确认后持仓接口数据展示
+ if (confirmed) {
+ await page.waitForSelector(".position-row", { timeout: 15000 });
+ } else {
+ await page.waitForFunction(
+ () => {
+ const panel = document.querySelector(".positions-panel");
+ return panel !== null && (panel.textContent.includes("暂无持仓") || panel.textContent.includes("持仓尚未读取") === false);
+ },
+ null,
+ { timeout: 15000 }
+ );
+ }
+ const positionsText = ((await page.textContent(".positions-panel")) || "");
+ const cashTexts = await page.$$eval(".positions-panel .fund-detail-value", (els) => els.map((el) => el.textContent.trim()));
+ if (confirmed) {
+ const units = lastConfirmResponse.confirmedUnits;
+ check(
+ "J9 确认后持仓行显示份额/成本/估值净值",
+ positionsText.includes("000001") &&
+ positionsText.includes(`份额 ${units}`) &&
+ positionsText.includes("成本") &&
+ positionsText.includes("1.19020000"),
+ positionsText.slice(0, 260)
+ );
+ check("J10 确认后保留现金归零", cashTexts.includes("0.00"), JSON.stringify(cashTexts));
+ } else {
+ check(
+ "J9 待净值分支无持仓行且现金未动",
+ (await page.locator(".position-row").count()) === 0 && cashTexts.includes("101.50"),
+ JSON.stringify({ cashTexts, rows: await page.locator(".position-row").count() })
+ );
+ }
+ // 现金核算: 下单冻结 reservedTotal;确认后 invested 转持仓成本、residual 退回、费用消耗
+ const cashCheck =
+ confirmed
+ ? Math.abs(
+ parseFloat(cashTexts[0]) -
+ (5000 - 101.50 + parseFloat(lastConfirmResponse.confirmedResidualCash || "0"))
+ ) < 0.005 && cashTexts[1] === "0.00"
+ : Math.abs(parseFloat(cashTexts[0]) + parseFloat(cashTexts[1]) - 5000) < 0.005;
+ check(
+ "J11 确认后现金核算一致",
+ cashCheck,
+ JSON.stringify({ cashTexts, residual: lastConfirmResponse ? lastConfirmResponse.confirmedResidualCash : null })
+ );
+ await page.screenshot({ path: SHOTS + "/12-confirm-positions.png" });
}