diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-22 04:29:42 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-22 04:29:42 +0800 |
| commit | b6008d67232756d743699472865025159f6ec533 (patch) | |
| tree | 739cc45c5e9677a08c03cebd0d52a8b773984754 /qa/driver | |
| parent | b5a80f69cb2e2793e5134e23a0eb3e72cdbdbe0b (diff) | |
| download | fund-lab-b6008d67232756d743699472865025159f6ec533.tar.gz | |
Add stock buy to holdings minimal vertical slice (3d-21)
Diffstat (limited to 'qa/driver')
| -rw-r--r-- | qa/driver/browser-test.js | 112 |
1 files changed, 111 insertions, 1 deletions
diff --git a/qa/driver/browser-test.js b/qa/driver/browser-test.js index 22385f8..aaa820c 100644 --- a/qa/driver/browser-test.js +++ b/qa/driver/browser-test.js @@ -37,6 +37,8 @@ let lastNavSeriesResponse = null; let lastBondQuoteResponse = null; let lastStockQuoteResponse = null; let lastStockDailyResponse = null; +let lastStockTradeResponse = null; +let lastStockPositionsResponse = null; function check(name, ok, detail) { results.push({ name, ok, detail: detail || "" }); @@ -201,7 +203,17 @@ async function summaryLabelExists(page, label, name) { lastStockDailyResponse = 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())) { + if (/\/stock-trades$/i.test(new URL(r.url()).pathname) && r.request().method() === "POST" && r.status() < 400) { + try { + lastStockTradeResponse = await r.json(); + } catch {} + } + if (/\/stock-positions$/i.test(new URL(r.url()).pathname) && r.request().method() === "GET" && r.status() < 400) { + try { + 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())) { consoleErrors.push("resource " + r.status() + ": " + r.url()); } }); @@ -255,6 +267,7 @@ async function summaryLabelExists(page, label, name) { await bondsScenario(page); await stocksScenario(page); await stockDailyScenario(page); + await stockTradeScenario(page); } finally { check("G1 无浏览器控制台/页面错误", consoleErrors.length === 0, consoleErrors.slice(0, 3).join(" | ")); await browser.close(); @@ -850,6 +863,103 @@ async function stockDailyScenario(page) { await page.screenshot({ path: SHOTS + "/23-stock-daily.png" }); } +async function stockTradeScenario(page) { + // S4: 股票买入 -> 持仓最小垂直闭环走真实 API(服务端以桩行情价成交)。 + await page.waitForSelector(".stocks-panel", { timeout: 10000 }); + + const token = process.env.QA_TOKEN || "qa-token"; + + lastStockTradeResponse = null; + lastStockPositionsResponse = null; + await page.fill(".stock-code-input", "600519"); + await page.fill(".stock-trade-quantity-input", "100"); + await page.click(".stock-trade-action"); + + await page.waitForSelector(".stock-position-row", { timeout: 10000 }); + + const rowCount = await page.locator(".stock-position-row").count(); + const rowText = ((await page.textContent(".stock-position-row")) || "").trim(); + const trade = lastStockTradeResponse || {}; + const positions = lastStockPositionsResponse || {}; + const rows = Array.isArray(positions.positions) ? positions.positions : []; + const matched = rows.find((item) => item.instrumentCode === "600519") || {}; + + check( + "S4a 真实买入生成持仓行且金额与行情价一致", + trade.instrumentCode === "600519" && + trade.price === "1252.57000000" && + trade.costCash === "125257.00" && + rowCount >= 1 && + rowText.includes("600519") && + matched.quantity === "100.00000000" && + matched.costCash === "125257.00", + `row=${rowText.slice(0, 160)} trade=${JSON.stringify(trade).slice(0, 160)}` + ); + + await page.fill(".stock-code-input", "999999"); + await page.fill(".stock-trade-quantity-input", "100"); + await page.click(".stock-trade-action"); + await page.waitForFunction( + () => { + const element = document.querySelector(".stock-trade-unavailable"); + return element && !element.hidden && element.textContent.includes("买入失败"); + }, + { timeout: 10000 } + ); + + const unavailableText = ((await page.textContent(".stock-trade-unavailable")) || "").trim(); + + const failure = await page.evaluate( + async ({ token, fundId }) => { + const response = await fetch(`/api/funds/${fundId}/stock-trades`, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + "Idempotency-Key": "qastocktradefail", + }, + body: JSON.stringify({ instrumentCode: "999999", quantity: "100" }), + }); + let body = null; + try { + body = await response.json(); + } catch {} + return { status: response.status, body }; + }, + { token, fundId: await currentFundId(page) } + ); + + const positionsAfter = await page.evaluate( + async ({ token, fundId }) => { + const response = await fetch(`/api/funds/${fundId}/stock-positions`, { + headers: { Authorization: `Bearer ${token}` }, + }); + let body = null; + try { + body = await response.json(); + } catch {} + return body; + }, + { token, fundId: await currentFundId(page) } + ); + + const afterRows = positionsAfter && Array.isArray(positionsAfter.positions) ? positionsAfter.positions : []; + const hasBadCode = afterRows.some((item) => item.instrumentCode === "999999"); + + check( + "S4b 无有效行情时买入失败且不产生 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 + "/24-stock-trade.png" }); +} + async function postInvestmentPlanViaApi(page, fundId, token) { return page.evaluate( async ({ fundId, token }) => { |
