From 0183b86f18d9897ff9487e6d864898954397991d Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Tue, 22 Sep 2026 04:45:10 +0800 Subject: Add bond buy to holdings minimal vertical slice (3d-22) --- qa/driver/browser-test.js | 113 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 112 insertions(+), 1 deletion(-) (limited to 'qa') diff --git a/qa/driver/browser-test.js b/qa/driver/browser-test.js index aaa820c..14cffc7 100644 --- a/qa/driver/browser-test.js +++ b/qa/driver/browser-test.js @@ -39,6 +39,8 @@ let lastStockQuoteResponse = null; let lastStockDailyResponse = null; let lastStockTradeResponse = null; let lastStockPositionsResponse = null; +let lastBondTradeResponse = null; +let lastBondPositionsResponse = null; function check(name, ok, detail) { results.push({ name, ok, detail: detail || "" }); @@ -213,7 +215,17 @@ async function summaryLabelExists(page, label, name) { lastStockPositionsResponse = 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())) { + if (/\/bond-trades$/i.test(new URL(r.url()).pathname) && r.request().method() === "POST" && r.status() < 400) { + try { + lastBondTradeResponse = await r.json(); + } catch {} + } + if (/\/bond-positions$/i.test(new URL(r.url()).pathname) && r.request().method() === "GET" && r.status() < 400) { + try { + lastBondPositionsResponse = 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())) { consoleErrors.push("resource " + r.status() + ": " + r.url()); } }); @@ -268,6 +280,7 @@ async function summaryLabelExists(page, label, name) { await stocksScenario(page); await stockDailyScenario(page); await stockTradeScenario(page); + await bondTradeScenario(page); } finally { check("G1 无浏览器控制台/页面错误", consoleErrors.length === 0, consoleErrors.slice(0, 3).join(" | ")); await browser.close(); @@ -960,6 +973,104 @@ async function stockTradeScenario(page) { await page.screenshot({ path: SHOTS + "/24-stock-trade.png" }); } +async function bondTradeScenario(page) { + // B5: 债券买入 -> 持仓最小垂直闭环走真实 API(服务端以桩行情价成交)。 + await page.waitForSelector(".bonds-panel", { timeout: 10000 }); + + const token = process.env.QA_TOKEN || "qa-token"; + + lastBondTradeResponse = null; + lastBondPositionsResponse = null; + await page.fill(".bond-code-input", "110075"); + await page.fill(".bond-trade-quantity-input", "10"); + await page.click(".bond-trade-action"); + + await page.waitForSelector(".bond-position-row", { timeout: 10000 }); + + const rowCount = await page.locator(".bond-position-row").count(); + const rowText = ((await page.textContent(".bond-position-row")) || "").trim(); + const trade = lastBondTradeResponse || {}; + const positions = lastBondPositionsResponse || {}; + const rows = Array.isArray(positions.positions) ? positions.positions : []; + const matched = rows.find((item) => item.instrumentCode === "110075") || {}; + + check( + "B5a 真实买入生成债券持仓行且金额与行情价一致", + trade.instrumentCode === "110075" && + trade.price === "106.36800000" && + trade.costCash === "1063.68" && + rowCount >= 1 && + rowText.includes("110075") && + matched.quantity === "10.00000000" && + matched.costCash === "1063.68", + `row=${rowText.slice(0, 160)} trade=${JSON.stringify(trade).slice(0, 160)}` + ); + + await page.fill(".bond-code-input", "999999"); + await page.fill(".bond-trade-quantity-input", "10"); + await page.click(".bond-trade-action"); + await page.waitForFunction( + () => { + const element = document.querySelector(".bond-trade-unavailable"); + return element && !element.hidden && element.textContent.includes("买入失败"); + }, + { timeout: 10000 } + ); + + const unavailableText = ((await page.textContent(".bond-trade-unavailable")) || "").trim(); + const fundId = await currentFundId(page); + + const failure = await page.evaluate( + async ({ token, fundId }) => { + const response = await fetch(`/api/funds/${fundId}/bond-trades`, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + "Idempotency-Key": "qabondtradefail", + }, + body: JSON.stringify({ instrumentCode: "999999", quantity: "10" }), + }); + let body = null; + try { + body = await response.json(); + } catch {} + return { status: response.status, body }; + }, + { token, fundId } + ); + + const positionsAfter = await page.evaluate( + async ({ token, fundId }) => { + const response = await fetch(`/api/funds/${fundId}/bond-positions`, { + headers: { Authorization: `Bearer ${token}` }, + }); + let body = null; + try { + body = await response.json(); + } catch {} + return body; + }, + { token, fundId } + ); + + const afterRows = positionsAfter && Array.isArray(positionsAfter.positions) ? positionsAfter.positions : []; + const hasBadCode = afterRows.some((item) => item.instrumentCode === "999999"); + + check( + "B5b 无有效行情时买入失败且不产生 999999 持仓", + unavailableText.includes("买入失败") && + !unavailableText.includes("0.00") && + failure.status === 503 && + failure.body && + failure.body.error === "MARKET_DATA_UNAVAILABLE" && + !hasBadCode, + `message=${unavailableText.slice(0, 200)} status=${failure.status} codes=${afterRows.map((item) => item.instrumentCode).join(",")}` + ); + + await page.screenshot({ path: SHOTS + "/25-bond-trade.png" }); +} + async function postInvestmentPlanViaApi(page, fundId, token) { return page.evaluate( async ({ fundId, token }) => { -- cgit v1.2.3