summaryrefslogtreecommitdiff
path: root/qa/driver/browser-test.js
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-22 06:20:25 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-22 06:20:25 +0800
commitc48b5560778548e2e87e05b1df5b170d7db1a345 (patch)
tree95449a1c1437cba003373454ba77d15a047cbecd /qa/driver/browser-test.js
parent79600c15aef9bd411abe88dcf36286a102b5abea (diff)
downloadfund-lab-c48b5560778548e2e87e05b1df5b170d7db1a345.tar.gz
Add stock sell vertical slice with cash recovery and snapshot pricing (3d-26)
Diffstat (limited to 'qa/driver/browser-test.js')
-rw-r--r--qa/driver/browser-test.js103
1 files changed, 102 insertions, 1 deletions
diff --git a/qa/driver/browser-test.js b/qa/driver/browser-test.js
index b271fbb..fda7c6f 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 lastStockSellResponse = null;
+const stockSellStatuses = [];
let lastBondTradeResponse = null;
let lastBondPositionsResponse = null;
let lastValuationResponse = null;
@@ -214,6 +216,14 @@ async function summaryLabelExists(page, label, name) {
lastStockTradeResponse = await r.json();
} catch {}
}
+ if (/\/stock-sells$/i.test(new URL(r.url()).pathname) && r.request().method() === "POST") {
+ stockSellStatuses.push(r.status());
+ if (r.status() < 400) {
+ try {
+ lastStockSellResponse = 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();
@@ -249,7 +259,7 @@ async function summaryLabelExists(page, label, name) {
lastSipAdvanceResponse = 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()) && !/\/valuation/.test(r.url()) && !/\/rebalance\/plans\//.test(r.url())) {
+ 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-sells/.test(r.url()) && !/\/stock-positions/.test(r.url()) && !/\/bond-trades/.test(r.url()) && !/\/bond-positions/.test(r.url()) && !/\/valuation/.test(r.url()) && !/\/rebalance\/plans\//.test(r.url())) {
consoleErrors.push("resource " + r.status() + ": " + r.url());
}
});
@@ -309,6 +319,7 @@ async function summaryLabelExists(page, label, name) {
await marketRefreshScenario(page);
await sipStatusScenario(page);
await rebalanceArchiveScenario(page);
+ await stockSellScenario(page);
} finally {
check("G1 无浏览器控制台/页面错误", consoleErrors.length === 0, consoleErrors.slice(0, 3).join(" | "));
await browser.close();
@@ -1597,6 +1608,96 @@ async function rebalanceArchiveScenario(page) {
await page.screenshot({ path: SHOTS + "/28-rebalance-archive.png" });
}
+async function stockSellScenario(page) {
+ // F5 (3d-26): 卖出部分股票 -> 持仓减少、现金回收、估值按剩余持仓快照价刷新。
+ await page.waitForSelector(".stock-position-row", { timeout: 10000 });
+
+ const beforeText = ((await page.textContent(".stock-position-row")) || "").trim();
+ check(
+ "F5a 持仓行显示卖出数量输入与卖出按钮",
+ (await page.locator(".stock-sell-quantity-input").count()) >= 1 && (await page.locator(".stock-sell-action").count()) >= 1,
+ beforeText.slice(0, 160)
+ );
+
+ lastStockSellResponse = null;
+ lastValuationResponse = null;
+
+ await page.fill(".stock-sell-quantity-input", "40");
+ await page.click(".stock-sell-action");
+
+ for (let i = 0; i < 200 && lastStockSellResponse === null; i++) {
+ await page.waitForTimeout(100);
+ }
+
+ const sell = lastStockSellResponse || {};
+ check(
+ "F5b 卖出 40 股以快照价成交并回收现金",
+ sell.instrumentCode === "600519" &&
+ sell.quantity === "40.00000000" &&
+ sell.price === "1252.57000000" &&
+ sell.feeAmount === "0.00" &&
+ sell.proceeds === "50102.80",
+ JSON.stringify(sell).slice(0, 260)
+ );
+
+ await page.waitForFunction(
+ () => {
+ const rows = Array.from(document.querySelectorAll(".stock-position-row"));
+ return rows.some((row) => (row.textContent || "").includes("60.00000000"));
+ },
+ null,
+ { timeout: 10000 }
+ );
+ check("F5c 卖出后持仓数量减少为 60", true);
+
+ for (let i = 0; i < 200 && lastValuationResponse === null; i++) {
+ await page.waitForTimeout(100);
+ }
+
+ const valuation = lastValuationResponse || {};
+ const positions = Array.isArray(valuation.positions) ? valuation.positions : [];
+ const stock = positions.find((item) => item.instrumentCode === "600519") || {};
+ const cash = parseFloat(valuation.cash || "0");
+ const marketValue = parseFloat(valuation.positionsMarketValue || "0");
+
+ check(
+ "F5d 卖出后估值按剩余持仓与快照价重算",
+ stock.quantity === "60.00000000" &&
+ stock.marketValue === "75154.20" &&
+ stock.priceSource === "snapshot" &&
+ valuation.portfolioValue &&
+ Math.abs(parseFloat(valuation.portfolioValue) - (cash + marketValue)) < 0.005,
+ `valuation=${JSON.stringify(valuation).slice(0, 280)}`
+ );
+
+ const messageText = ((await page.textContent(".stock-sell-message")) || "").trim();
+ check("F5e 页面显示卖出结果摘要", messageText.includes("已卖出") && messageText.includes("50102.80"), messageText.slice(0, 160));
+
+ // 超卖被拒绝,持仓不变。
+ await page.fill(".stock-sell-quantity-input", "1000");
+ await page.click(".stock-sell-action");
+ await page.waitForFunction(
+ () => {
+ const element = document.querySelector(".stock-sell-message");
+ return element && element.textContent.includes("卖出失败") && element.textContent.includes("not enough");
+ },
+ null,
+ { timeout: 10000 }
+ );
+
+ const oversellStatus = stockSellStatuses[stockSellStatuses.length - 1];
+ check("F5f 超卖被 400 拒绝并提示原因", oversellStatus === 400, `status=${oversellStatus}`);
+
+ const afterOversell = ((await page.textContent(".stock-positions")) || "");
+ check(
+ "F5g 超卖后持仓仍为 60",
+ afterOversell.includes("60.00000000") && !afterOversell.includes("1000.00000000"),
+ afterOversell.slice(0, 160)
+ );
+
+ await page.screenshot({ path: SHOTS + "/30-stock-sell.png" });
+}
+
const failed = results.filter((r) => !r.ok);
console.log(`\n==== ${results.length - failed.length}/${results.length} passed ====`);
process.exit(failed.length > 0 ? 1 : 0);