1 parent
e27f08e36e
commit
b788f890f8
5 files changed
+288
-59
No files matched your search
+19
-9
@@ -17,8 +17,14 @@ function e(e, t = {}) {
|
|||||||
function t(e) {
|
function t(e) {
|
||||||
return JSON.parse(JSON.stringify(e));
|
return JSON.parse(JSON.stringify(e));
|
||||||
}
|
}
|
||||||
function n(n) {
|
// 版本只接受服务端对规范购物车生成的 SHA-256,缺失时停止位置写入。
|
||||||
return n.map((n) => t(e(n)));
|
function requireCartRevision(cart) {
|
||||||
|
if (
|
||||||
|
typeof cart?.revision !== `string` ||
|
||||||
|
!/^[a-f0-9]{64}$/.test(cart.revision)
|
||||||
|
)
|
||||||
|
throw Error(`购物车版本缺失,请返回购物车刷新后重试`);
|
||||||
|
return cart.revision;
|
||||||
}
|
}
|
||||||
// 递归排序对象键并省略 undefined,使相同内容不受属性插入顺序影响。
|
// 递归排序对象键并省略 undefined,使相同内容不受属性插入顺序影响。
|
||||||
function r(e) {
|
function r(e) {
|
||||||
@@ -76,11 +82,12 @@ async function l(e) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* n 为 API 客户端,r 包含 position/item;返回函数的第二参 u 检查报价是否仍有效。
|
* n 为 API 客户端,r 包含首次读取时绑定的 position/item/revision;第二参 u 检查报价是否仍有效。
|
||||||
* 保存前重取购物车并比较原始项,提交 expected_item 供后端进一步检测并发变更。
|
* 保存始终提交原始版本;冲突必须返回购物车重新确认,不能用新版本授权旧位置。
|
||||||
*/
|
*/
|
||||||
function u(n, r) {
|
function u(n, r) {
|
||||||
let a = r ? t(r.item) : null,
|
let a = r ? t(r.item) : null,
|
||||||
|
revision = r ? requireCartRevision(r) : null,
|
||||||
c = !1;
|
c = !1;
|
||||||
return async (l, u) => {
|
return async (l, u) => {
|
||||||
if (!r || !a) throw Error(`购物车编辑上下文缺失`);
|
if (!r || !a) throw Error(`购物车编辑上下文缺失`);
|
||||||
@@ -88,13 +95,10 @@ function u(n, r) {
|
|||||||
c = !0;
|
c = !0;
|
||||||
let d = t(e(a, l));
|
let d = t(e(a, l));
|
||||||
try {
|
try {
|
||||||
let c = (await n.get(`/cart`)).list?.[r.position];
|
|
||||||
if (!c || i(c) !== i(a))
|
|
||||||
throw Error(`购物车商品已变化,请返回购物车刷新后重新编辑`);
|
|
||||||
if (!u()) throw Error(`配置已改变,请重新确认报价后保存`);
|
if (!u()) throw Error(`配置已改变,请重新确认报价后保存`);
|
||||||
let l = await n.put(`/cart/${r.position}`, {
|
let l = await n.put(`/cart/${r.position}`, {
|
||||||
...d,
|
...d,
|
||||||
expected_item: e(a),
|
expected_revision: revision,
|
||||||
}),
|
}),
|
||||||
f = a;
|
f = a;
|
||||||
a = t({
|
a = t({
|
||||||
@@ -109,9 +113,15 @@ function u(n, r) {
|
|||||||
(p.fingerprints[r.position] = await s(a)),
|
(p.fingerprints[r.position] = await s(a)),
|
||||||
l
|
l
|
||||||
);
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if ([409, 428].includes(Number(error?.status)))
|
||||||
|
throw Error(`购物车状态已变化,请返回购物车刷新并重新确认配置`, {
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
c = !1;
|
c = !1;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export { l as a, c as i, t as n, a as o, u as r, n as t };
|
export { l as a, c as i, t as n, a as o, u as r, requireCartRevision };
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
/**
|
/**
|
||||||
* 购物车:读取服务端购物车、逐项重新报价、保留选择并结算。修改使用 revision 或 expected_cart 进行并发校验,不能把列表下标当作永久商品标识。
|
* 购物车:读取服务端购物车、逐项重新报价、保留选择并结算。位置写入必须携带同次读取的 revision,不能把列表下标当作永久商品标识。
|
||||||
*/
|
*/
|
||||||
import "~/assets/styles/PaymentDialog.BNFKQF1U.css";
|
import "~/assets/styles/PaymentDialog.BNFKQF1U.css";
|
||||||
import "~/assets/styles/cart.CSS2vTmS.css";
|
import "~/assets/styles/cart.CSS2vTmS.css";
|
||||||
@@ -42,7 +42,7 @@ import { t as configuration_display_t } from "~/lib/configuration-display.js";
|
|||||||
import { t as purchase_limit_state_t } from "~/lib/purchase-limit-state.js";
|
import { t as purchase_limit_state_t } from "~/lib/purchase-limit-state.js";
|
||||||
import { a as cart_edit_a } from "~/lib/cart-edit.js";
|
import { a as cart_edit_a } from "~/lib/cart-edit.js";
|
||||||
import { i as cart_edit_i } from "~/lib/cart-edit.js";
|
import { i as cart_edit_i } from "~/lib/cart-edit.js";
|
||||||
import { t as cart_edit_t } from "~/lib/cart-edit.js";
|
import { requireCartRevision } from "~/lib/cart-edit.js";
|
||||||
// 只接受普通对象形状的数据,供配置摘要解析使用。
|
// 只接受普通对象形状的数据,供配置摘要解析使用。
|
||||||
function E(e) {
|
function E(e) {
|
||||||
return typeof e == `object` && e && !Array.isArray(e) ? e : void 0;
|
return typeof e == `object` && e && !Array.isArray(e) ? e : void 0;
|
||||||
@@ -387,14 +387,7 @@ var me = {
|
|||||||
G = !1,
|
G = !1,
|
||||||
K = new Map(),
|
K = new Map(),
|
||||||
q = () => JSON.stringify(D.value),
|
q = () => JSON.stringify(D.value),
|
||||||
J = () =>
|
J = () => ({ expected_revision: O.value }),
|
||||||
O.value
|
|
||||||
? {
|
|
||||||
expected_revision: O.value,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
expected_cart: cart_edit_t(D.value),
|
|
||||||
},
|
|
||||||
Y = (e) =>
|
Y = (e) =>
|
||||||
JSON.stringify([
|
JSON.stringify([
|
||||||
e.product_id,
|
e.product_id,
|
||||||
@@ -524,11 +517,7 @@ var me = {
|
|||||||
if (G || t !== W) return;
|
if (G || t !== W) return;
|
||||||
if (
|
if (
|
||||||
((D.value = i.list ?? []),
|
((D.value = i.list ?? []),
|
||||||
(O.value =
|
(O.value = requireCartRevision(i)),
|
||||||
typeof i.revision == `string` &&
|
|
||||||
/^[a-f0-9]{64}$/.test(i.revision)
|
|
||||||
? i.revision
|
|
||||||
: ``),
|
|
||||||
(z.value = Object.fromEntries(
|
(z.value = Object.fromEntries(
|
||||||
D.value.flatMap((e, t) => {
|
D.value.flatMap((e, t) => {
|
||||||
let n = r.get(Y(e));
|
let n = r.get(Y(e));
|
||||||
@@ -563,15 +552,17 @@ var me = {
|
|||||||
let t = A.value.indexOf(e);
|
let t = A.value.indexOf(e);
|
||||||
t === -1 ? A.value.push(e) : A.value.splice(t, 1);
|
t === -1 ? A.value.push(e) : A.value.splice(t, 1);
|
||||||
}
|
}
|
||||||
// 遇到 409 并发冲突时重载购物车并清空选择,要求用户重新确认,避免误改已移动的下标。
|
// 并发冲突或版本缺失时重载列表并清空选择,只在重新确认后允许用户再提交。
|
||||||
async function Q(e, t) {
|
async function Q(e, t) {
|
||||||
let n = e instanceof Error ? e.message : t;
|
let n = e instanceof Error ? e.message : t;
|
||||||
e && typeof e == `object` && `status` in e && e.status === 409
|
e && typeof e == `object` && [409, 428].includes(Number(e.status))
|
||||||
? (($.value = null),
|
? (($.value = null),
|
||||||
(A.value = []),
|
(A.value = []),
|
||||||
await Z(),
|
await Z(),
|
||||||
(A.value = []),
|
(A.value = []),
|
||||||
(M.value = N.value ? `${n};${M.value}` : n))
|
(M.value = N.value
|
||||||
|
? `${n};${M.value}`
|
||||||
|
: `${n};请重新选择商品并确认后再提交`))
|
||||||
: (M.value = n);
|
: (M.value = n);
|
||||||
}
|
}
|
||||||
// 先保存购物车编辑上下文,再进入对应商品页;商品页会再次确认该位置仍匹配商品。
|
// 先保存购物车编辑上下文,再进入对应商品页;商品页会再次确认该位置仍匹配商品。
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import PaymentDialog from "~/components/PaymentDialog.vue";
|
|||||||
import { t as product_order_module_t } from "~/lib/product-order-module.js";
|
import { t as product_order_module_t } from "~/lib/product-order-module.js";
|
||||||
import ConfigLoadingShell from "~/components/config/ConfigLoadingShell.vue";
|
import ConfigLoadingShell from "~/components/config/ConfigLoadingShell.vue";
|
||||||
import { n as cart_edit_n } from "~/lib/cart-edit.js";
|
import { n as cart_edit_n } from "~/lib/cart-edit.js";
|
||||||
|
import { requireCartRevision } from "~/lib/cart-edit.js";
|
||||||
import CommonConfig from "~/components/config/CommonConfig.vue";
|
import CommonConfig from "~/components/config/CommonConfig.vue";
|
||||||
import FinanceConfig from "~/components/config/FinanceConfig.vue";
|
import FinanceConfig from "~/components/config/FinanceConfig.vue";
|
||||||
import JyCloudConfig from "~/components/config/JyCloudConfig.vue";
|
import JyCloudConfig from "~/components/config/JyCloudConfig.vue";
|
||||||
@@ -119,7 +120,8 @@ var j = {
|
|||||||
if (K.value !== null && U.value)
|
if (K.value !== null && U.value)
|
||||||
return {
|
return {
|
||||||
position: K.value,
|
position: K.value,
|
||||||
item: U.value,
|
item: U.value.item,
|
||||||
|
revision: U.value.revision,
|
||||||
};
|
};
|
||||||
return undefined;
|
return undefined;
|
||||||
}),
|
}),
|
||||||
@@ -150,11 +152,15 @@ var j = {
|
|||||||
if (r.order_allowed === !1)
|
if (r.order_allowed === !1)
|
||||||
throw Error(r.order_block_reason || `该产品已停止订购`);
|
throw Error(r.order_block_reason || `该产品已停止订购`);
|
||||||
if (K.value !== null) {
|
if (K.value !== null) {
|
||||||
let t = (await L.get(`/cart`))?.list?.[K.value];
|
let t = await L.get(`/cart`),
|
||||||
if (!t || Number(t.product_id) !== Number(R))
|
item = t?.list?.[K.value];
|
||||||
|
if (!item || Number(item.product_id) !== Number(R))
|
||||||
throw Error(`购物车商品已变化,请返回购物车重新编辑`);
|
throw Error(`购物车商品已变化,请返回购物车重新编辑`);
|
||||||
if (e !== Z) return;
|
if (e !== Z) return;
|
||||||
U.value = cart_edit_n(t);
|
U.value = {
|
||||||
|
item: cart_edit_n(item),
|
||||||
|
revision: requireCartRevision(t),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
let i = await product_order_module_t(L, r);
|
let i = await product_order_module_t(L, r);
|
||||||
if (e !== Z) return;
|
if (e !== Z) return;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/** 购买回归走真实配置页、实例动作和支付界面;只替换已配置后端的 HTTP 响应。 */
|
/** 购买回归走真实配置页、实例动作和支付界面;只替换已配置后端的 HTTP 响应。 */
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import {
|
import {
|
||||||
appOrigin,
|
appOrigin,
|
||||||
@@ -10,7 +11,10 @@ import {
|
|||||||
scenario,
|
scenario,
|
||||||
} from "./helpers/console-browser.mjs";
|
} from "./helpers/console-browser.mjs";
|
||||||
|
|
||||||
async function purchaseBoundary(page, { savedCascade } = {}) {
|
async function purchaseBoundary(
|
||||||
|
page,
|
||||||
|
{ savedCascade, missingCartRevision = false, rejectCartWrites = 0 } = {},
|
||||||
|
) {
|
||||||
const state = {
|
const state = {
|
||||||
authorized: false,
|
authorized: false,
|
||||||
orders: new Map(),
|
orders: new Map(),
|
||||||
@@ -19,7 +23,29 @@ async function purchaseBoundary(page, { savedCascade } = {}) {
|
|||||||
carts: [],
|
carts: [],
|
||||||
settlements: [],
|
settlements: [],
|
||||||
requests: [],
|
requests: [],
|
||||||
|
cartReads: 0,
|
||||||
|
cartRevisions: [],
|
||||||
|
cartWrites: [],
|
||||||
|
cartRows:
|
||||||
|
savedCascade === undefined
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
product_id: 123,
|
||||||
|
qty: 1,
|
||||||
|
config_options: {
|
||||||
|
cycle: "free",
|
||||||
|
cascade_configoption: {
|
||||||
|
5: { item_id: savedCascade, quantity: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
customfield: {},
|
||||||
|
self_defined_field: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
const revision = () =>
|
||||||
|
createHash("sha256").update(JSON.stringify(state.cartRows)).digest("hex");
|
||||||
const tree = [
|
const tree = [
|
||||||
{
|
{
|
||||||
item_name: "隐藏区域",
|
item_name: "隐藏区域",
|
||||||
@@ -179,26 +205,44 @@ async function purchaseBoundary(page, { savedCascade } = {}) {
|
|||||||
state.carts.push(body);
|
state.carts.push(body);
|
||||||
return respond({});
|
return respond({});
|
||||||
}
|
}
|
||||||
if (path === "/cart" && method === "GET")
|
if (path === "/cart" && method === "GET") {
|
||||||
|
state.cartReads++;
|
||||||
|
state.cartRevisions.push(revision());
|
||||||
return respond({
|
return respond({
|
||||||
list:
|
list: state.cartRows.map((row) => ({ ...row, name: "本地级联配置" })),
|
||||||
savedCascade === undefined
|
...(missingCartRevision ? {} : { revision: revision() }),
|
||||||
? []
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
product_id: 123,
|
|
||||||
qty: 1,
|
|
||||||
config_options: {
|
|
||||||
cycle: "free",
|
|
||||||
cascade_configoption: {
|
|
||||||
5: { item_id: savedCascade, quantity: 1 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
customfield: {},
|
|
||||||
self_defined_field: {},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
if (method === "PUT" && /^\/cart\/\d+(\/qty)?$/.test(path)) {
|
||||||
|
const currentRevision = revision();
|
||||||
|
state.cartWrites.push({
|
||||||
|
method,
|
||||||
|
path,
|
||||||
|
body,
|
||||||
|
cartReads: state.cartReads,
|
||||||
|
revision: currentRevision,
|
||||||
|
});
|
||||||
|
const status =
|
||||||
|
rejectCartWrites ||
|
||||||
|
(!body?.expected_revision
|
||||||
|
? 428
|
||||||
|
: body.expected_revision !== currentRevision
|
||||||
|
? 409
|
||||||
|
: 200);
|
||||||
|
if (status !== 200)
|
||||||
|
return respond({}, { status, msg: "购物车状态待重新确认" });
|
||||||
|
const position = Number(path.split("/")[2]);
|
||||||
|
state.cartRows[position] = path.endsWith("/qty")
|
||||||
|
? { ...state.cartRows[position], qty: body.qty }
|
||||||
|
: {
|
||||||
|
product_id: body.product_id,
|
||||||
|
qty: body.qty,
|
||||||
|
config_options: body.config_options,
|
||||||
|
customfield: body.customfield,
|
||||||
|
self_defined_field: body.self_defined_field,
|
||||||
|
};
|
||||||
|
return respond({ revision: revision() });
|
||||||
|
}
|
||||||
if (path === "/product/settle") {
|
if (path === "/product/settle") {
|
||||||
state.settlements.push(body);
|
state.settlements.push(body);
|
||||||
return respond({ order_id: 42, amount: "0.00" });
|
return respond({ order_id: 42, amount: "0.00" });
|
||||||
@@ -440,3 +484,175 @@ test(
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"cart configuration keeps the original revision without fetching a replacement before saving",
|
||||||
|
{ timeout: 30000 },
|
||||||
|
(t) =>
|
||||||
|
scenario(t, {}, async (page) => {
|
||||||
|
const state = await purchaseBoundary(page, { savedCascade: 20 });
|
||||||
|
await login(page, "/products/123?edit_position=0");
|
||||||
|
const save = page.getByRole("button", { name: "保存配置", exact: true });
|
||||||
|
await eventually(
|
||||||
|
() => save.isEnabled(),
|
||||||
|
Boolean,
|
||||||
|
"编辑配置必须先取得有效报价",
|
||||||
|
);
|
||||||
|
await save.click();
|
||||||
|
await eventually(
|
||||||
|
() => state.cartWrites.length,
|
||||||
|
(count) => count === 1,
|
||||||
|
"配置只能提交一次",
|
||||||
|
);
|
||||||
|
const write = state.cartWrites[0];
|
||||||
|
assert.equal(write.body.expected_revision, write.revision);
|
||||||
|
assert.equal(write.cartReads, 1);
|
||||||
|
assert.equal(Object.hasOwn(write.body, "expected_item"), false);
|
||||||
|
assert.equal(Object.hasOwn(write.body, "expected_cart"), false);
|
||||||
|
await page.waitForURL((url) => url.pathname === "/cart");
|
||||||
|
return {
|
||||||
|
originalRevisionSubmitted: true,
|
||||||
|
readsBeforeSave: write.cartReads,
|
||||||
|
configurationWrites: 1,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const status of [409, 428]) {
|
||||||
|
test(
|
||||||
|
`cart editing stops after server ${status} and requires returning to the cart`,
|
||||||
|
{ timeout: 30000 },
|
||||||
|
(t) =>
|
||||||
|
scenario(t, {}, async (page) => {
|
||||||
|
const state = await purchaseBoundary(page, {
|
||||||
|
savedCascade: 20,
|
||||||
|
rejectCartWrites: status === 428 ? status : 0,
|
||||||
|
});
|
||||||
|
await login(page, "/products/123?edit_position=0");
|
||||||
|
const save = page.getByRole("button", {
|
||||||
|
name: "保存配置",
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
await eventually(
|
||||||
|
() => save.isEnabled(),
|
||||||
|
Boolean,
|
||||||
|
"编辑配置必须先取得有效报价",
|
||||||
|
);
|
||||||
|
const originalRevision = state.cartRevisions[0];
|
||||||
|
// 另一会话只改动兄弟项,当前商品仍相同,也不能把新版本套在旧位置上。
|
||||||
|
if (status === 409)
|
||||||
|
state.cartRows.push({ ...state.cartRows[0], qty: 2 });
|
||||||
|
await save.click();
|
||||||
|
await eventually(
|
||||||
|
() => state.cartWrites.length,
|
||||||
|
(count) => count === 1,
|
||||||
|
"冲突只允许一次写入尝试",
|
||||||
|
);
|
||||||
|
await page.locator(".p-message-error").waitFor();
|
||||||
|
assert.equal(new URL(page.url()).pathname, "/products/123");
|
||||||
|
assert.equal(state.cartReads, 1);
|
||||||
|
assert.equal(
|
||||||
|
state.cartWrites[0].body.expected_revision,
|
||||||
|
originalRevision,
|
||||||
|
);
|
||||||
|
if (status === 409)
|
||||||
|
assert.notEqual(state.cartWrites[0].revision, originalRevision);
|
||||||
|
await page
|
||||||
|
.locator(".product-links")
|
||||||
|
.getByRole("link", { name: "购物车", exact: true })
|
||||||
|
.click();
|
||||||
|
await page.waitForURL((url) => url.pathname === "/cart");
|
||||||
|
await eventually(
|
||||||
|
() => state.cartReads,
|
||||||
|
(count) => count === 2,
|
||||||
|
"返回购物车后重新读取可确认的列表",
|
||||||
|
);
|
||||||
|
assert.equal(state.cartWrites.length, 1);
|
||||||
|
return {
|
||||||
|
rejectedStatus: status,
|
||||||
|
configurationWrites: 1,
|
||||||
|
readsOnlyAfterReturning: 2,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
`cart server ${status} clears the selection without retrying a positional mutation`,
|
||||||
|
{ timeout: 30000 },
|
||||||
|
(t) =>
|
||||||
|
scenario(t, {}, async (page) => {
|
||||||
|
const state = await purchaseBoundary(page, {
|
||||||
|
savedCascade: 20,
|
||||||
|
rejectCartWrites: status,
|
||||||
|
});
|
||||||
|
await login(page, "/cart");
|
||||||
|
const selected = page.getByRole("checkbox", {
|
||||||
|
name: "选择 本地级联配置",
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
await selected.waitFor();
|
||||||
|
assert.equal(await selected.isChecked(), true);
|
||||||
|
await page
|
||||||
|
.getByRole("button", {
|
||||||
|
name: "增加 本地级联配置 的数量",
|
||||||
|
exact: true,
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
await eventually(
|
||||||
|
() => state.cartWrites.length,
|
||||||
|
(count) => count === 1,
|
||||||
|
"修改数量只提交一次",
|
||||||
|
);
|
||||||
|
await eventually(
|
||||||
|
() => selected.isChecked(),
|
||||||
|
(checked) => !checked,
|
||||||
|
"冲突后必须重新选择商品确认",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
state.cartWrites[0].body.expected_revision,
|
||||||
|
state.cartWrites[0].revision,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
Object.hasOwn(state.cartWrites[0].body, "expected_cart"),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
await page
|
||||||
|
.getByRole("button", { name: "提交订单", exact: true })
|
||||||
|
.isDisabled(),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(state.cartWrites.length, 1);
|
||||||
|
return {
|
||||||
|
rejectedStatus: status,
|
||||||
|
mutationAttempts: 1,
|
||||||
|
selectionCleared: true,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test(
|
||||||
|
"a cart response without a revision offers reload and cannot mutate old positions",
|
||||||
|
{ timeout: 30000 },
|
||||||
|
(t) =>
|
||||||
|
scenario(t, {}, async (page) => {
|
||||||
|
const state = await purchaseBoundary(page, {
|
||||||
|
savedCascade: 20,
|
||||||
|
missingCartRevision: true,
|
||||||
|
});
|
||||||
|
await login(page, "/cart");
|
||||||
|
await page
|
||||||
|
.getByRole("button", { name: "重新加载", exact: true })
|
||||||
|
.waitFor();
|
||||||
|
assert.equal(await page.locator(".cart-item").count(), 0);
|
||||||
|
assert.equal(
|
||||||
|
await page
|
||||||
|
.getByRole("button", { name: "提交订单", exact: true })
|
||||||
|
.count(),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
assert.equal(state.cartWrites.length, 0);
|
||||||
|
return { missingRevisionBlocked: true, mutationAttempts: 0 };
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
* 所有内容来自生产已有的本地 HTTP 边界;尺寸断言读取实际 DOM,不检查源码形状。
|
* 所有内容来自生产已有的本地 HTTP 边界;尺寸断言读取实际 DOM,不检查源码形状。
|
||||||
*/
|
*/
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
import { test } from "node:test";
|
import { test } from "node:test";
|
||||||
import { firefox } from "playwright";
|
import { firefox } from "playwright";
|
||||||
import {
|
import {
|
||||||
@@ -118,17 +119,22 @@ async function layoutBoundary(
|
|||||||
create_time: 1700000000,
|
create_time: 1700000000,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (path === "/cart")
|
if (path === "/cart") {
|
||||||
|
const list = Array.from({ length: 14 }, (_, index) => ({
|
||||||
|
product_id: 123,
|
||||||
|
qty: 1,
|
||||||
|
name: `云产品 ${index + 1}`,
|
||||||
|
config_options: { cpu: index + 1 },
|
||||||
|
customfield: {},
|
||||||
|
self_defined_field: {},
|
||||||
|
}));
|
||||||
return reply({
|
return reply({
|
||||||
list: Array.from({ length: 14 }, (_, index) => ({
|
list,
|
||||||
product_id: 123,
|
revision: createHash("sha256")
|
||||||
qty: 1,
|
.update(JSON.stringify(list))
|
||||||
name: `云产品 ${index + 1}`,
|
.digest("hex"),
|
||||||
config_options: { cpu: index + 1 },
|
|
||||||
customfield: {},
|
|
||||||
self_defined_field: {},
|
|
||||||
})),
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
if (path === "/product/123/config_option")
|
if (path === "/product/123/config_option")
|
||||||
return reply({
|
return reply({
|
||||||
price_total: "10.00",
|
price_total: "10.00",
|
||||||
|
|||||||
Reference in new issue
Block a user