/** 购买回归走真实配置页、实例动作和支付界面;只替换已配置后端的 HTTP 响应。 */ import assert from "node:assert/strict"; import test from "node:test"; import { appOrigin, backendOrigin, eventually, fixtureRequest, login, scenario, } from "./helpers/console-browser.mjs"; async function purchaseBoundary(page, { savedCascade } = {}) { const state = { authorized: false, orders: new Map(), created: [], quotes: [], carts: [], settlements: [], requests: [], }; const tree = [ { item_name: "隐藏区域", hidden: 1, children: [{ item_id: 10, item_name: "不应售出的节点", is_leaf: 1 }], }, { item_name: "常规区域", children: [ { item_id: 20, item_name: "可选节点", is_leaf: 1 }, { item_id: 21, item_name: "隐藏节点", is_leaf: 1, hidden: 1 }, ], }, { item_id: 30, item_name: "备用区域", is_leaf: 1 }, ]; // 拦截点与生产 apiBase 一致;非本场景接口继续交给现有独立 HTTP 假后端。 await page .context() .route(`${backendOrigin}/console/v1/**`, async (route) => { const request = route.request(); const path = new URL(request.url()).pathname.replace( /^\/console\/v1/, "", ); const method = request.method(); const body = request.postDataJSON(); const reply = (data, extra = {}) => route.fulfill({ json: { status: 200, msg: "", data, ...extra }, headers: { "Access-Control-Allow-Origin": "*" }, }); const record = () => state.requests.push({ method, path, authenticated: !!request.headers().authorization, }); const respond = (data, extra) => { record(); return reply(data, extra); }; if (path === "/host/5") return respond({ host: { id: 5, product_id: 123, module: "mf_cloud", name: "本地镜像购买实例", status: "Active", billing_cycle: "monthly", upgrade_allowed: false, }, }); if (path === "/host/5/view") return respond({ module: "mf_cloud" }); if (path === "/mf_cloud/5") return respond({ image: { id: 6, name: "Ubuntu 24.04" }, port: 22, username: "root", cpu: 2, memory: 4, config: { reinstall_sms_verify: 0, reset_password_sms_verify: 0 }, }); if (path === "/mf_cloud/5/status") return respond({ status: "on", rescue: false }); if (path === "/mf_cloud/5/disk") return respond({ list: [] }); if (path === "/mf_cloud/5/flow") return respond({ total: 100, used: 0, leave: 100 }); if (path === "/mf_cloud/5/real_data") return respond({ cpu: 0, memory: 0, disk: 0 }); if (path === "/mf_cloud/5/chart") return respond({ list: [] }); if (path === "/product/123/mf_cloud/image") return respond({ list: [ { id: 1, name: "Ubuntu", image: [ { id: 6, name: "Ubuntu 24.04", image_group_id: 1, charge: 1, price: "10.00", }, ], }, ], }); if (path === "/mf_cloud/5/image/check") return respond({ price: state.authorized ? "0.00" : "10.00" }); if (path === "/mf_cloud/5/image/order") { const id = 42 + state.created.length; state.created.push(id); state.orders.set(id, "Unpaid"); return respond({ id }); } if (path === "/mf_cloud/5/reinstall") return respond({}); const order = /^\/order\/(\d+)$/.exec(path); if (order) { const id = Number(order[1]); if (method === "DELETE") { state.orders.set(id, "Cancelled"); return respond({}); } const status = state.orders.get(id) ?? "Unpaid"; return respond({ order: { id, amount: "10.00", amount_unpaid: status === "Paid" ? "0.00" : "10.00", credit: "0.00", type: "new", status, items: [{ id: 1, product_name: "系统镜像", amount: "10.00" }], }, customfields: { is_pre_invoice: 0 }, }); } if (/^\/order\/\d+\/transaction_record$/.test(path)) return respond({ list: [] }); if (path === "/pay") { state.orders.set(Number(body.id), "Paid"); return respond({}, { code: "Paid" }); } if (path === "/product/123") return respond({ product: { id: 123, name: "本地级联配置", module: "idcsmart_common", description: "", }, }); if (path === "/idcsmart_common/product/123/configoption") return respond({ common_product: { pay_type: "free" }, configoptions: [ { id: 5, option_name: "区域", option_type: "cascade", is_show: 1, tree, }, ], }); if (path === "/product/123/self_defined_field/order_page") return respond({ data: [] }); if (path === "/purchase_limit/check") return respond({ allowed: true, message: "" }); if (path === "/product/123/config_option") { state.quotes.push(body); return respond({ price_total: "0.00" }); } if (path === "/cart" && method === "POST") { state.carts.push(body); return respond({}); } if (path === "/cart" && method === "GET") return respond({ list: savedCascade === undefined ? [] : [ { product_id: 123, qty: 1, config_options: { cycle: "free", cascade_configoption: { 5: { item_id: savedCascade, quantity: 1 }, }, }, customfield: {}, self_defined_field: {}, }, ], }); if (path === "/product/settle") { state.settlements.push(body); return respond({ order_id: 42, amount: "0.00" }); } return route.fallback(); }); return state; } test( "image retry reuses unpaid orders and replaces only a cancelled order", { timeout: 60000 }, async (t) => { await scenario(t, {}, async (page) => { const state = await purchaseBoundary(page); await login(page, "/servers/server?id=5"); await page.getByRole("tab", { name: "访问控制", exact: true }).click(); await page.getByRole("button", { name: "重装系统", exact: true }).click(); await page .getByRole("dialog", { name: "重装系统", exact: true }) .getByRole("button", { name: "确认", exact: true }) .click(); await page .getByRole("dialog", { name: "确认重装系统", exact: true }) .getByRole("button", { name: "确认重装", exact: true }) .click(); const purchase = page.getByRole("dialog", { name: "购买系统镜像", exact: true, }); const continuePurchase = () => purchase .getByRole("button", { name: "确认购买 / 继续支付", exact: true }) .click(); const payment = page.getByRole("dialog", { name: "订单支付", exact: true, }); const closePayment = async () => { await payment.getByRole("button", { name: /^(关闭|Close)$/ }).click(); await payment.waitFor({ state: "hidden" }); }; await continuePurchase(); await payment.waitFor(); await closePayment(); await continuePurchase(); await payment.waitFor(); assert.deepEqual( state.created, [42], "Retrying an unpaid order must not create a second order", ); await closePayment(); // 另一标签使用真实订单取消入口,原实例页保持挂载,不用刷新掩盖缓存生命周期。 const orderPage = await page.context().newPage(); await orderPage.goto(`${appOrigin}/orders/42`); await orderPage .getByRole("button", { name: "取消订单", exact: true }) .click(); await orderPage .getByRole("dialog", { name: "取消订单", exact: true }) .getByRole("button", { name: "确认取消", exact: true }) .click(); await eventually( () => state.orders.get(42), (status) => status === "Cancelled", "Second tab must cancel the first order", ); await orderPage.close(); await continuePurchase(); await eventually( () => state.created.length, (count) => count === 2, "Cancelled cached order must be replaced without reloading the instance", ); await payment.waitFor(); assert.deepEqual(state.created, [42, 43]); await payment .getByRole("button", { name: "确认支付", exact: true }) .click(); await eventually( () => state.orders.get(43), (status) => status === "Paid", "Payment must complete through the real payment panel", ); await purchase.waitFor(); await purchase .getByText("镜像授权尚未生效,请稍后重新检查", { exact: true }) .waitFor(); const paidRetryMessage = purchase.getByText( "镜像订单已支付,授权尚未生效,请稍后重新检查", { exact: true }, ); assert.equal(await paidRetryMessage.count(), 0); const readsBeforePaidRetry = state.requests.filter( (request) => request.method === "GET" && request.path === "/order/43", ).length; await continuePurchase(); // 本次 GET 增量和新文案共同证明重试已完成,不能沿用支付回调的旧提示或按钮状态。 await eventually( () => state.requests.filter( (request) => request.method === "GET" && request.path === "/order/43", ).length, (count) => count > readsBeforePaidRetry, "Paid order retry must fetch fresh server state", ); await paidRetryMessage.waitFor(); assert.deepEqual( state.created, [42, 43], "Paid but not-yet-authorized orders must not create another order", ); assert.equal(await payment.isVisible(), false); state.authorized = true; await continuePurchase(); await purchase.waitFor({ state: "hidden" }); assert.deepEqual(state.created, [42, 43]); assert.equal( state.requests.filter((request) => request.path.endsWith("/reinstall")) .length, 0, "Paying for an image must not automatically reinstall the instance", ); assert.ok(state.requests.every((request) => request.authenticated)); assert.ok( (await fixtureRequest("requests")).some( (request) => request.path === "/login", ), ); return { unpaidOrderReused: true, cancelledOrderReplaced: true, paidOrderRetainedUntilAuthorized: true, createdOrders: state.created.length, automaticReinstalls: 0, }; }); }, ); test( "cascade defaults, visible choices and submitted values agree", { timeout: 40000 }, async (t) => { await scenario(t, {}, async (page) => { const state = await purchaseBoundary(page); await login(page, "/products/123"); await eventually( () => state.quotes.length, (count) => count > 0, "Configuration must receive a real quote", ); assert.equal( state.quotes.at(-1).config_options.cascade_configoption[5].item_id, 20, "Default must skip descendants of hidden ancestors", ); const region = page.getByRole("combobox", { name: "区域", exact: true }); assert.match(await region.innerText(), /常规区域.*可选节点/); await region.click(); const labels = await page.getByRole("option").allTextContents(); assert.equal(labels.length, 2); assert.ok( labels.every( (label) => !label.includes("隐藏") && !label.includes("不应售出"), ), ); const quotesBeforeSelection = state.quotes.length; await page.getByRole("option", { name: "备用区域", exact: true }).click(); // 改选非默认项并等本轮新报价;选择处理若失效,不能沿用默认 20 的报价假绿。 await eventually( () => state.quotes .slice(quotesBeforeSelection) .some( (quote) => quote.config_options.cascade_configoption[5].item_id === 30, ), Boolean, "Selecting a non-default visible candidate must request a fresh quote", ); assert.match(await region.innerText(), /备用区域/); await page .getByRole("button", { name: "加入购物车", exact: true }) .click(); await eventually( () => state.carts.length, (count) => count === 1, "Cart must receive the selected visible candidate", ); assert.equal( state.carts[0].config_options.cascade_configoption[5].item_id, 30, ); await page.getByRole("button", { name: "立即购买", exact: true }).click(); await eventually( () => state.settlements.length, (count) => count === 1, "Checkout must submit the same visible candidate", ); assert.equal( state.settlements[0].config_options.cascade_configoption[5].item_id, 30, ); return { visibleChoices: labels.length, defaultVisibleItem: 20, cartVisibleItem: 30, checkoutVisibleItem: 30, }; }); }, ); test( "cart editing does not restore a hidden cascade descendant", { timeout: 40000 }, async (t) => { await scenario(t, {}, async (page) => { const state = await purchaseBoundary(page, { savedCascade: 10 }); await login(page, "/products/123?edit_position=0"); await page .getByRole("button", { name: "重新加载配置", exact: true }) .waitFor(); assert.equal( state.quotes.length, 0, "An unavailable saved candidate must not receive a valid quote", ); assert.equal(state.settlements.length, 0); assert.equal(state.carts.length, 0); return { hiddenSavedCandidateRejected: true, quotesForHiddenCandidate: 0, }; }); }, );