diff options
Diffstat (limited to 'qa/driver')
| -rw-r--r-- | qa/driver/browser-test.js | 162 |
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" }); } |
