summaryrefslogtreecommitdiff
path: root/qa/driver/browser-test.js
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-22 04:29:42 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-22 04:29:42 +0800
commitb6008d67232756d743699472865025159f6ec533 (patch)
tree739cc45c5e9677a08c03cebd0d52a8b773984754 /qa/driver/browser-test.js
parentb5a80f69cb2e2793e5134e23a0eb3e72cdbdbe0b (diff)
downloadfund-lab-b6008d67232756d743699472865025159f6ec533.tar.gz
Add stock buy to holdings minimal vertical slice (3d-21)
Diffstat (limited to 'qa/driver/browser-test.js')
-rw-r--r--qa/driver/browser-test.js112
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 }) => {