Files
trade-message-center/apps/chrome-extension/test/onetalk-rendered-card-reader.test.js
T

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,
);
});