summaryrefslogtreecommitdiff
path: root/qa/driver/browser-test.js
diff options
context:
space:
mode:
Diffstat (limited to 'qa/driver/browser-test.js')
-rw-r--r--qa/driver/browser-test.js157
1 files changed, 154 insertions, 3 deletions
diff --git a/qa/driver/browser-test.js b/qa/driver/browser-test.js
index 7a45441..2f18ac2 100644
--- a/qa/driver/browser-test.js
+++ b/qa/driver/browser-test.js
@@ -7,10 +7,14 @@ const EXE = "/home/somhairle/.cache/ms-playwright/chromium-1243/chrome-linux64/c
const results = [];
const consoleErrors = [];
const fundPosts = [];
+const orderPosts = [];
+const orderPostStatuses = [];
let fundPostResponses = 0;
let fundGets = 0;
+let orderGets = 0;
let createdFund = null;
let createdFundResponse = null;
+let lastOrdersResponse = null;
function check(name, ok, detail) {
results.push({ name, ok, detail: detail || "" });
@@ -89,21 +93,32 @@ async function summaryLabelExists(page, label, name) {
if (!createdFundResponse) createdFundResponse = await r.json();
} catch {}
}
- if (r.url().includes("/api/funds/") && r.request().method() === "GET" && r.status() < 400) fundGets++;
- if (r.status() >= 400 && !/\/api\/instruments\//.test(r.url())) {
+ if (r.url().includes("/api/funds/") && !r.url().includes("/orders") && r.request().method() === "GET" && r.status() < 400) fundGets++;
+ if (r.url().includes("/api/funds/") && r.url().includes("/orders") && r.request().method() === "GET" && r.status() < 400) {
+ orderGets++;
+ try {
+ lastOrdersResponse = await r.json();
+ } catch {}
+ }
+ if (r.url().includes("/orders") && r.request().method() === "POST") orderPostStatuses.push(r.status());
+ if (r.status() >= 400 && !/\/api\/instruments\//.test(r.url()) && !/\/orders/.test(r.url())) {
consoleErrors.push("resource " + r.status() + ": " + r.url());
}
});
page.on("request", (r) => {
- if (r.url().includes("/api/funds") && r.method() === "POST") {
+ 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") {
+ orderPosts.push({ url: r.url(), key: r.headers()["idempotency-key"] || "", body: r.postData() || "" });
+ }
});
page.on("console", (m) => {
if (m.type() !== "error") return;
const text = m.text();
if (/favicon/i.test(text)) return;
if (/401 \(Unauthorized\)/.test(text)) return;
+ if (/orders/.test(text)) return;
if (/Failed to load resource/.test(text) && !text.includes("api/")) return;
consoleErrors.push("console: " + text);
});
@@ -117,6 +132,7 @@ async function summaryLabelExists(page, label, name) {
await rereadScenario(page);
await staleTokenScenario(page);
await idempotentRetryScenario(page);
+ await orderScenario(page);
} finally {
check("G1 无浏览器控制台/页面错误", consoleErrors.length === 0, consoleErrors.slice(0, 3).join(" | "));
await browser.close();
@@ -278,3 +294,138 @@ async function idempotentRetryScenario(page) {
await page.unroute("**/api/funds");
await page.screenshot({ path: SHOTS + "/06-idempotent-retry.png" });
}
+
+async function orderScenario(page) {
+ await page.waitForSelector(".subscribe-panel", { timeout: 10000 });
+ const eyebrow = ((await page.textContent(".subscribe-panel .eyebrow")) || "").trim();
+ check("G1 认购面板 eyebrow 为 04 / SUBSCRIBE", eyebrow === "04 / SUBSCRIBE", eyebrow);
+ for (const sel of [".order-code-input", ".order-amount-input", ".order-fee-input", ".order-submit-action"]) {
+ const count = await page.locator(sel).count();
+ check(`G1 认购面板包含 ${sel}`, count === 1, `count=${count}`);
+ }
+ const emptyHint = ((await page.textContent(".pending-orders")) || "").trim();
+ check("G2 初始无待确认订单提示", emptyHint.includes("暂无待确认订单"), emptyHint.slice(0, 40));
+
+ await page.click(".order-submit-action");
+ await expectText(page, ".error-banner", "请输入基金代码", "G3 空代码校验提示");
+ await page.fill(".order-code-input", "000001");
+ await page.fill(".order-amount-input", "abc");
+ await page.click(".order-submit-action");
+ await expectText(
+ page,
+ ".error-banner",
+ "认购金额必须是大于零的两位小数金额,例如 100.00",
+ "G4 非法金额校验提示"
+ );
+ await page.fill(".order-amount-input", "100.00");
+ await page.fill(".order-fee-input", "0.00");
+ await page.click(".order-submit-action");
+ await expectText(
+ page,
+ ".error-banner",
+ "认购手续费必须是大于零的两位小数金额,例如 1.50",
+ "G5 非法手续费校验提示"
+ );
+ check("G6 校验阶段无认购 POST", orderPosts.length === 0, `posts=${orderPosts.length}`);
+
+ await page.fill(".search-input", "000001");
+ await page.click(".search-row .primary-action");
+ await page.waitForSelector(".result-list", { timeout: 15000 });
+ const resultText = ((await page.textContent(".result-list")) || "");
+ check("G7 搜索到目录代码 000001", resultText.includes("000001"), resultText.slice(0, 80));
+
+ 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 });
+ check("G8 成功下单恰好 1 次认购 POST", orderPosts.length === 1, `posts=${orderPosts.length}`);
+ const post = orderPosts[0] || { key: "", body: "" };
+ check("G9 认购幂等键为 32 位十六进制", /^[0-9a-f]{32}$/.test(post.key), post.key);
+ let body = {};
+ try {
+ body = JSON.parse(post.body);
+ } catch {}
+ check(
+ "G10 请求体为两位小数字符串",
+ body.fundCode === "000001" && body.amount === "100.00" && body.feeAmount === "1.50",
+ post.body.slice(0, 160)
+ );
+ check("G11 认购响应 201 OrderCreated", orderPostStatuses[0] === 201, String(orderPostStatuses[0]));
+ const rowText = ((await page.textContent(".pending-orders")) || "");
+ check(
+ "G12 订单行为已提交·待确认且无份额收益",
+ rowText.includes("000001") && rowText.includes("金额 100.00") && rowText.includes("预留合计 101.50") && rowText.includes("已提交 · 待确认") && !rowText.includes("份额") && !rowText.includes("收益"),
+ rowText.slice(0, 200)
+ );
+ await summaryValue(page, "可用现金", "19898.50", "G13 下单后可用现金 19898.50");
+ await summaryValue(page, "保留现金", "101.50", "G14 下单后保留现金 101.50");
+ check("G15 建档后自动读取订单列表", orderGets >= 1, `gets=${orderGets}`);
+ await page.screenshot({ path: SHOTS + "/07-order-created.png" });
+
+ await page.fill(".order-amount-input", "19900.00");
+ await page.fill(".order-fee-input", "1.00");
+ await page.click(".order-submit-action");
+ await page.waitForSelector(".error-banner", { timeout: 15000 });
+ const insufficientText = ((await page.textContent(".error-banner")) || "").trim();
+ check(
+ "G16 超额认购被 409 拒绝",
+ insufficientText.includes("available cash is not enough"),
+ insufficientText.slice(0, 120)
+ );
+ const rowsAfterConflict = await page.locator(".order-row").count();
+ check("G17 409 后无新订单", rowsAfterConflict === 1, `rows=${rowsAfterConflict}`);
+ await summaryValue(page, "可用现金", "19898.50", "G18 409 后可用现金不变");
+ await page.screenshot({ path: SHOTS + "/08-order-insufficient.png" });
+
+ let aborted = false;
+ await page.route("**/api/funds/*/orders", async (route) => {
+ if (route.request().method() !== "POST") {
+ await route.continue();
+ return;
+ }
+ if (!aborted) {
+ aborted = true;
+ await route.abort("failed");
+ } else {
+ await route.continue();
+ }
+ });
+ await page.fill(".order-amount-input", "10.00");
+ await page.fill(".order-fee-input", "0.50");
+ await page.click(".order-submit-action");
+ await page.waitForSelector(".error-banner", { timeout: 15000 });
+ check("G19 网络失败显示错误横幅", true);
+ const failedKey = orderPosts[orderPosts.length - 1]?.key || "";
+ await page.click(".order-submit-action");
+ await page.waitForFunction(() => document.querySelectorAll(".order-row").length === 2, null, { timeout: 15000 });
+ const retryKey = orderPosts[orderPosts.length - 1]?.key || "";
+ check("G20 重试复用同一认购幂等键", failedKey !== "" && failedKey === retryKey, `${failedKey} vs ${retryKey}`);
+ await summaryValue(page, "可用现金", "19888.00", "G21 重试后可用现金 19888.00");
+ await summaryValue(page, "保留现金", "112.00", "G22 重试后保留现金 112.00");
+ await page.unroute("**/api/funds/*/orders");
+ await page.screenshot({ path: SHOTS + "/09-order-retry.png" });
+
+ await page.route("**/api/funds/*/orders", async (route) => {
+ if (route.request().method() !== "GET") {
+ await route.continue();
+ return;
+ }
+ await new Promise((r) => setTimeout(r, 1200));
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify(lastOrdersResponse || []),
+ });
+ });
+ await page.click(".orders-refresh-action");
+ await page.fill(".token-input", "qa-token-rotated");
+ await page.waitForSelector(".order-row", { state: "detached", timeout: 10000 });
+ check("G23 换 token 立即清空待确认订单", true);
+ 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" });
+}