mirror of
https://github.com/sinanyuntu/trade-message-center.git
synced 2026-09-17 13:22:11 +08:00
396 lines
14 KiB
JavaScript
396 lines
14 KiB
JavaScript
// 验证 DOM-only reader 以可见结构采集命名字段,而不是以模板 ID 准入。
|
|
|
|
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import { createOneTalkRenderedCardContentFingerprint } from "@trade-message-center/onetalk-contract";
|
|
import { readOneTalkRenderedCardFromDom } from "../src/onetalk/main-page/card-observer/dom-card-reader.ts";
|
|
|
|
const node = ({
|
|
text = null,
|
|
imageUrl = null,
|
|
viewName = null,
|
|
classNames = [],
|
|
children = [],
|
|
} = {}) => {
|
|
const value = {
|
|
childNodes: children,
|
|
textContent: text,
|
|
parentElement: null,
|
|
classNames: new Set(classNames),
|
|
getAttribute: (name) => {
|
|
if (name === "src") return imageUrl;
|
|
if (name === "view-name") return viewName;
|
|
return null;
|
|
},
|
|
querySelectorAll(selector) {
|
|
const matches = [];
|
|
const visit = (candidate) => {
|
|
if (
|
|
(selector === "img[src]" && candidate.getAttribute("src")) ||
|
|
(selector.startsWith(".") && candidate.classNames?.has(selector.slice(1)))
|
|
)
|
|
matches.push(candidate);
|
|
for (const child of candidate.childNodes ?? []) visit(child);
|
|
};
|
|
for (const child of this.childNodes) visit(child);
|
|
return matches;
|
|
},
|
|
};
|
|
for (const child of children) child.parentElement = value;
|
|
return value;
|
|
};
|
|
|
|
const fieldRow = (label, value) => {
|
|
const labelNode = node({ text: label, viewName: "TextView" });
|
|
const valueNode = node({ text: value, viewName: "TextView" });
|
|
return node({ children: [labelNode, valueNode] });
|
|
};
|
|
|
|
const productStructure = () => {
|
|
const minimumOrder = node({
|
|
children: [
|
|
node({ text: "Minimum order", viewName: "TextView" }),
|
|
node({ text: "500", viewName: "TextView" }),
|
|
node({ text: "pieces", viewName: "TextView" }),
|
|
],
|
|
});
|
|
const productOwner = node({
|
|
classNames: ["dx-event-node"],
|
|
children: [
|
|
node({ text: "Product title", viewName: "TextView" }),
|
|
node({ text: "US $10.00", viewName: "TextView" }),
|
|
minimumOrder,
|
|
node({ imageUrl: "https://img.alicdn.com/product.jpg" }),
|
|
],
|
|
});
|
|
return node({ classNames: ["tpl-wrapper"], children: [productOwner] });
|
|
};
|
|
|
|
const pageFor = ({ conversationId = "conversation-1", selectionReads = [conversationId] } = {}) => {
|
|
let reads = 0;
|
|
return {
|
|
location: { href: "https://onetalk.alibaba.com/message/default.htm" },
|
|
document: {
|
|
querySelectorAll: (selector) => {
|
|
assert.equal(selector, ".contact-item-container.selected[data-cid]");
|
|
const selected = selectionReads[Math.min(reads, selectionReads.length - 1)];
|
|
reads += 1;
|
|
return selected ? [{ getAttribute: () => selected }] : [];
|
|
},
|
|
},
|
|
getComputedStyle: () => ({ display: "block", visibility: "visible" }),
|
|
};
|
|
};
|
|
|
|
const cardFor = (
|
|
root,
|
|
{ cardInfo = { messageId: "message-1" }, wrapperInfo = { messageId: "message-1" } } = {},
|
|
) => {
|
|
const wrapper = { getAttribute: () => JSON.stringify(wrapperInfo) };
|
|
const card = {
|
|
closest: (selector) => {
|
|
assert.equal(selector, ".message-item-wrapper[data-expinfo]");
|
|
return wrapper;
|
|
},
|
|
matches: (selector) => selector === "[data-expinfo]",
|
|
getAttribute: (name) => (name === "data-expinfo" ? JSON.stringify(cardInfo) : null),
|
|
querySelector: (selector) => {
|
|
if (selector === "[data-expinfo]") return card;
|
|
if (selector === ".tpl-wrapper") return root;
|
|
assert.fail(`unexpected selector: ${selector}`);
|
|
},
|
|
querySelectorAll: root.querySelectorAll.bind(root),
|
|
childNodes: root.childNodes,
|
|
};
|
|
root.parentElement = card;
|
|
return card;
|
|
};
|
|
|
|
const orderVariant = ({ templateId = null } = {}) => {
|
|
const product = node({
|
|
classNames: ["list-item"],
|
|
children: [node({ imageUrl: "https://img.alicdn.com/order.jpg" })],
|
|
});
|
|
const root = node({
|
|
classNames: ["tpl-wrapper"],
|
|
children: [
|
|
node({ text: "订单", viewName: "TextView" }),
|
|
product,
|
|
fieldRow("Product count", "3 items"),
|
|
fieldRow("订单状态", "Paid"),
|
|
fieldRow("Total", "US $10.00"),
|
|
fieldRow("Delivery date", "2026-09-16"),
|
|
fieldRow("Shipping address", "1 Market Street"),
|
|
fieldRow("Colour", "Blue"),
|
|
],
|
|
});
|
|
root.getAttribute = (name) => (name === "data-tpl-id" ? templateId : null);
|
|
return root;
|
|
};
|
|
|
|
const readWithFixedTime = (page, card) => {
|
|
const previousNow = Date.now;
|
|
Date.now = () => 1_700_000_000_123;
|
|
try {
|
|
return readOneTalkRenderedCardFromDom(page, card);
|
|
} finally {
|
|
Date.now = previousNow;
|
|
}
|
|
};
|
|
|
|
test("collects a named order structure without using its unknown template ID", () => {
|
|
const content = {
|
|
version: 1,
|
|
image: "https://img.alicdn.com/order.jpg",
|
|
productCount: { display: "3 items", value: 3 },
|
|
status: "Paid",
|
|
total: "US $10.00",
|
|
deliveryDate: "2026-09-16",
|
|
shippingAddress: "1 Market Street",
|
|
};
|
|
const result = readWithFixedTime(
|
|
pageFor(),
|
|
cardFor(orderVariant({ templateId: "3829708538501" })),
|
|
);
|
|
assert.deepEqual(result, {
|
|
conversationId: "conversation-1",
|
|
messageId: "message-1",
|
|
content,
|
|
contentFingerprint: createOneTalkRenderedCardContentFingerprint(content),
|
|
observedAtMs: 1_700_000_000_123,
|
|
});
|
|
});
|
|
|
|
test("collects an order image beside a direct product-count display without list-item markup", () => {
|
|
const root = node({
|
|
classNames: ["tpl-wrapper"],
|
|
children: [
|
|
node({ text: "订单", viewName: "TextView" }),
|
|
node({ imageUrl: "https://img.alicdn.com/order-chevron.jpg" }),
|
|
node({
|
|
children: [
|
|
node({
|
|
children: [
|
|
node({ imageUrl: "https://img.alicdn.com/order-product.jpg" }),
|
|
node({ imageUrl: "https://img.alicdn.com/order-product-second.jpg" }),
|
|
],
|
|
}),
|
|
node({
|
|
children: [
|
|
node({
|
|
children: [
|
|
node({
|
|
text: "5 products",
|
|
viewName: "TextView",
|
|
}),
|
|
],
|
|
}),
|
|
],
|
|
}),
|
|
],
|
|
}),
|
|
fieldRow("订单状态", "Pending"),
|
|
],
|
|
});
|
|
assert.deepEqual(readWithFixedTime(pageFor(), cardFor(root))?.content, {
|
|
version: 1,
|
|
image: "https://img.alicdn.com/order-product.jpg",
|
|
productCount: { display: "5 products", value: 5 },
|
|
status: "Pending",
|
|
});
|
|
});
|
|
|
|
test("omits unrecognized inquiry rows instead of emitting attributes", () => {
|
|
const root = node({
|
|
classNames: ["tpl-wrapper"],
|
|
children: [
|
|
fieldRow("Colour", "Blue"),
|
|
node({ text: "详细需求", viewName: "TextView" }),
|
|
node({ classNames: ["description-container"], text: "Need delivery this week" }),
|
|
node({ classNames: ["dx-event-node"], text: "View details" }),
|
|
],
|
|
});
|
|
assert.deepEqual(readWithFixedTime(pageFor(), cardFor(root))?.content, {
|
|
version: 1,
|
|
requirementText: "Need delivery this week",
|
|
});
|
|
});
|
|
|
|
test("collects an inquiry product, purchase quantity, and requirement without its UI ID", () => {
|
|
const root = node({
|
|
classNames: ["tpl-wrapper"],
|
|
children: [
|
|
node({
|
|
classNames: ["list-item"],
|
|
children: [
|
|
node({ imageUrl: "https://img.alicdn.com/inquiry-product.jpg" }),
|
|
node({ text: "Inquiry product title", viewName: "TextView" }),
|
|
],
|
|
}),
|
|
fieldRow("采购数量", "500 Piece/Pieces"),
|
|
node({ text: "详细需求", viewName: "TextView" }),
|
|
node({
|
|
classNames: ["description-container"],
|
|
text: "Need custom sizing\nSecond line",
|
|
}),
|
|
fieldRow("ID:", "13894257698"),
|
|
],
|
|
});
|
|
assert.deepEqual(readWithFixedTime(pageFor(), cardFor(root))?.content, {
|
|
version: 1,
|
|
title: "Inquiry product title",
|
|
image: "https://img.alicdn.com/inquiry-product.jpg",
|
|
purchaseQuantity: "500 Piece/Pieces",
|
|
requirementText: "Need custom sizing\nSecond line",
|
|
});
|
|
});
|
|
|
|
test("preserves rendered line breaks in an inquiry requirement", () => {
|
|
const requirement = node({
|
|
classNames: ["description-container"],
|
|
text: "Need custom sizingSecond line",
|
|
});
|
|
requirement.innerText = "Need custom sizing\nSecond line";
|
|
const root = node({
|
|
classNames: ["tpl-wrapper"],
|
|
children: [node({ text: "详细需求", viewName: "TextView" }), requirement],
|
|
});
|
|
assert.equal(
|
|
readWithFixedTime(pageFor(), cardFor(root))?.content.requirementText,
|
|
"Need custom sizing\nSecond line",
|
|
);
|
|
});
|
|
|
|
test("rejects an inquiry-shaped card without the detailed-requirement anchor", () => {
|
|
const root = node({
|
|
classNames: ["tpl-wrapper"],
|
|
children: [
|
|
fieldRow("Colour", "Blue"),
|
|
node({ classNames: ["description-container"], text: "Need delivery this week" }),
|
|
],
|
|
});
|
|
assert.equal(readWithFixedTime(pageFor(), cardFor(root)), null);
|
|
});
|
|
|
|
test("rejects a card that matches more than one parser anchor", () => {
|
|
const product = productStructure();
|
|
const root = node({
|
|
classNames: ["tpl-wrapper"],
|
|
children: [...product.childNodes, fieldRow("订单状态", "Pending")],
|
|
});
|
|
assert.equal(readWithFixedTime(pageFor(), cardFor(root)), null);
|
|
});
|
|
|
|
test("rejects cards with a repeated parser anchor", () => {
|
|
const order = orderVariant();
|
|
order.childNodes.push(fieldRow("订单状态", "Duplicate"));
|
|
assert.equal(readWithFixedTime(pageFor(), cardFor(order)), null);
|
|
|
|
const inquiry = node({
|
|
classNames: ["tpl-wrapper"],
|
|
children: [
|
|
node({ text: "详细需求", viewName: "TextView" }),
|
|
node({ text: "详细需求", viewName: "TextView" }),
|
|
node({ classNames: ["description-container"], text: "Need details" }),
|
|
],
|
|
});
|
|
assert.equal(readWithFixedTime(pageFor(), cardFor(inquiry)), null);
|
|
|
|
const product = productStructure();
|
|
product.childNodes.push(node({ text: "Minimum order", viewName: "TextView" }));
|
|
assert.equal(readWithFixedTime(pageFor(), cardFor(product)), null);
|
|
});
|
|
|
|
test("collects a product owner with view-name TextViews and no template metadata", () => {
|
|
assert.deepEqual(readWithFixedTime(pageFor(), cardFor(productStructure()))?.content, {
|
|
version: 1,
|
|
title: "Product title",
|
|
price: "US $10.00",
|
|
minimumOrder: "500 pieces",
|
|
image: "https://img.alicdn.com/product.jpg",
|
|
});
|
|
});
|
|
|
|
test("collects a nested product variant around its minimum-order anchor", () => {
|
|
const textContainer = (text) =>
|
|
node({
|
|
children: [node({ text, viewName: "TextView" })],
|
|
});
|
|
const root = node({
|
|
classNames: ["tpl-wrapper"],
|
|
children: [
|
|
node({
|
|
classNames: ["dx-event-node"],
|
|
children: [
|
|
node({ imageUrl: "https://img.alicdn.com/variant-product.jpg" }),
|
|
textContainer("Super"),
|
|
textContainer("Variant product title"),
|
|
textContainer("$4.50-5.09"),
|
|
node({
|
|
children: [
|
|
node({ text: "最小订购量", viewName: "TextView" }),
|
|
node({ text: "10", viewName: "TextView" }),
|
|
node({ text: "Pieces", viewName: "TextView" }),
|
|
],
|
|
}),
|
|
textContainer("无忧退货"),
|
|
],
|
|
}),
|
|
],
|
|
});
|
|
assert.deepEqual(readWithFixedTime(pageFor(), cardFor(root))?.content, {
|
|
version: 1,
|
|
title: "Variant product title",
|
|
price: "$4.50-5.09",
|
|
minimumOrder: "10 Pieces",
|
|
image: "https://img.alicdn.com/variant-product.jpg",
|
|
});
|
|
});
|
|
|
|
test("does not project unrecognized non-inquiry rows or action labels", () => {
|
|
const root = node({
|
|
classNames: ["tpl-wrapper"],
|
|
children: [
|
|
fieldRow(
|
|
"Women's Elegant Fashionable PU Leather Crossbody Bag",
|
|
"$4.50-5.09最小订购量: 10Pieces无忧退货",
|
|
),
|
|
node({ classNames: ["dx-event-node"], text: "View details" }),
|
|
],
|
|
});
|
|
assert.equal(readWithFixedTime(pageFor(), cardFor(root)), null);
|
|
});
|
|
|
|
test("rejects identity conflicts, unsafe images, invisible nodes, unstable selection, and empty structures", () => {
|
|
assert.equal(
|
|
readWithFixedTime(pageFor(), cardFor(node({ classNames: ["tpl-wrapper"] }))),
|
|
null,
|
|
);
|
|
assert.equal(
|
|
readWithFixedTime(
|
|
pageFor(),
|
|
cardFor(orderVariant(), { wrapperInfo: { messageId: "other" } }),
|
|
),
|
|
null,
|
|
);
|
|
const unsafe = orderVariant();
|
|
unsafe.childNodes[1].childNodes[0].getAttribute = (name) =>
|
|
name === "src" ? "https://img.alicdn.com/order.jpg?token=secret" : null;
|
|
assert.equal(
|
|
readWithFixedTime(pageFor(), cardFor(unsafe))?.content.image,
|
|
undefined,
|
|
"an unsafe optional image group must not suppress recognized text fields",
|
|
);
|
|
const hidden = orderVariant();
|
|
hidden.childNodes[3].childNodes[1].hidden = true;
|
|
assert.equal(readWithFixedTime(pageFor(), cardFor(hidden))?.content.status, undefined);
|
|
assert.equal(
|
|
readWithFixedTime(
|
|
pageFor({ selectionReads: ["conversation-1", "conversation-2"] }),
|
|
cardFor(orderVariant()),
|
|
),
|
|
null,
|
|
);
|
|
});
|