From 0d1af831c10683bb1e5a5853e5924c5ddfb1310d Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Tue, 22 Sep 2026 01:54:55 +0800 Subject: Add bonds probe panel with live bond quote flow (3d-16) --- qa/driver/browser-test.js | 70 +++++++++++++++++++++++++++++++++++++++++++++++ qa/stub/fund-lab-python | 11 ++++++++ 2 files changed, 81 insertions(+) (limited to 'qa') diff --git a/qa/driver/browser-test.js b/qa/driver/browser-test.js index a52fa03..dddc562 100644 --- a/qa/driver/browser-test.js +++ b/qa/driver/browser-test.js @@ -34,6 +34,7 @@ let lastRebalanceExecuteResponse = null; let lastRebalanceExecutionsResponse = null; let lastNavDatesResponse = null; let lastNavSeriesResponse = null; +let lastBondQuoteResponse = null; function check(name, ok, detail) { results.push({ name, ok, detail: detail || "" }); @@ -183,6 +184,11 @@ async function summaryLabelExists(page, label, name) { lastNavSeriesResponse = await r.json(); } catch {} } + if (r.url().includes("/market/bond-quote") && r.request().method() === "GET" && r.status() < 400) { + try { + lastBondQuoteResponse = 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())) { consoleErrors.push("resource " + r.status() + ": " + r.url()); } @@ -234,6 +240,7 @@ async function summaryLabelExists(page, label, name) { await navSeriesScenario(page); await plansScenario(page); await rebalanceScenario(page); + await bondsScenario(page); } finally { check("G1 无浏览器控制台/页面错误", consoleErrors.length === 0, consoleErrors.slice(0, 3).join(" | ")); await browser.close(); @@ -624,6 +631,69 @@ async function navSeriesScenario(page) { await page.screenshot({ path: SHOTS + "/20-nav-series.png" }); } +async function bondsScenario(page) { + // B 系列: 债券行情探针面板走真实 API(QA 环境由桩采集器提供,非前端 mock)。 + await page.waitForSelector(".bonds-panel", { timeout: 10000 }); + + lastBondQuoteResponse = null; + await page.fill(".bond-code-input", "110075"); + await page.click(".bond-quote-action"); + await page.waitForSelector(".bond-quote-result", { timeout: 10000 }); + + const resultText = ((await page.textContent(".bond-quote-result")) || "").trim(); + const quote = lastBondQuoteResponse || {}; + + check( + "B1 真实可用券返回名称与价格字符串", + quote.code === "110075" && + typeof quote.name === "string" && + quote.name.length > 0 && + typeof quote.price === "string" && + quote.price.length > 0 && + resultText.includes(quote.name) && + resultText.includes(quote.price), + `quote=${JSON.stringify(quote).slice(0, 160)} result=${resultText.slice(0, 160)}` + ); + + await page.fill(".bond-code-input", "019547"); + await page.click(".bond-quote-action"); + await page.waitForFunction( + () => { + const element = document.querySelector(".bond-quote-unavailable"); + return element && !element.hidden && element.textContent.includes("不可用"); + }, + { timeout: 10000 } + ); + + const unavailableText = ((await page.textContent(".bond-quote-unavailable")) || "").trim(); + const resultGone = (await page.locator(".bond-quote-result").count()) === 0; + + const token = process.env.QA_TOKEN || "qa-token"; + + const failure = await page.evaluate(async (token) => { + const response = await fetch("/api/market/bond-quote?code=019547", { + headers: { Authorization: `Bearer ${token}` }, + }); + let body = null; + try { + body = await response.json(); + } catch {} + return { status: response.status, body }; + }, token); + + check( + "B2 无有效报价时显示不可用提示且不补零", + unavailableText.includes("不可用") && + resultGone && + failure.status === 503 && + failure.body && + failure.body.error === "MARKET_DATA_UNAVAILABLE", + `message=${unavailableText.slice(0, 200)} status=${failure.status}` + ); + + await page.screenshot({ path: SHOTS + "/21-bonds.png" }); +} + async function postInvestmentPlanViaApi(page, fundId, token) { return page.evaluate( async ({ fundId, token }) => { diff --git a/qa/stub/fund-lab-python b/qa/stub/fund-lab-python index cefcecd..9a171bc 100755 --- a/qa/stub/fund-lab-python +++ b/qa/stub/fund-lab-python @@ -31,6 +31,17 @@ case "$OP" in fi printf '{"schema_version":"fund-lab.akshare.v1","operation":"nav","source":"stub-synthetic","source_revision":"stub-1","_synthetic":true,"collected_at":"%s","instrument":{"code":"%s"},"observations":[{"nav_date":"2026-09-15","published_at":null,"nav":"1.1000","accumulated_nav":null,"daily_return":null},{"nav_date":"2026-09-16","published_at":null,"nav":"1.1205","accumulated_nav":null,"daily_return":"1.86"},{"nav_date":"2026-09-17","published_at":null,"nav":"1.1180","accumulated_nav":null,"daily_return":"-0.22"},{"nav_date":"2026-09-18","published_at":null,"nav":"1.1500","accumulated_nav":null,"daily_return":"2.86"},{"nav_date":"2026-09-21","published_at":null,"nav":"1.1902","accumulated_nav":null,"daily_return":"3.49"}]}\n' "$TS" "$CODE" ;; + bond-quote) + # Synthetic rows mirror the live AKShare convertible-bond spot contract: + # a known live code answers with a name/price, unknown codes fail like the + # "no valid quote" collector error (-> API 503). + if [ "$CODE" = "110075" ]; then + printf '{"schema_version":"fund-lab.akshare.v1","operation":"bond-quote","source":"stub-synthetic","source_revision":"stub-1","_synthetic":true,"collected_at":"%s","instrument":{"code":"%s"},"quote":{"name":"南航转债","price":"106.368","maturity_date":"2026-10-15"}}\n' "$TS" "$CODE" + else + echo "AKShare collector failed: no valid quote for $CODE" >&2 + exit 2 + fi + ;; *) echo "unknown operation: $OP" >&2 exit 2 -- cgit v1.2.3