Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions DEVELOPMENT_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,21 @@ service 不负责:

如果无法执行,需要在提交说明里写清原因。

### 单元测试里没有 antd 的样式

`src/test/setup.ts` 会清空 antd 注入的样式,只保留一条规则:带 antd 隐藏状态类
(如 `ant-dropdown-hidden`,完整列表见该文件)的元素为 `display: none`。原因是 jsdom 每次计算样式都要把
全部 CSS 规则逐条匹配一遍,带着 antd 的样式时场景测试会慢一个数量级。

写用例时注意:

- 已关闭的下拉、气泡、抽屉和未激活的标签页仍然查不到,`toBeVisible()` 可以照常
使用。
- 其它依赖 antd 样式的断言不成立,包括颜色、尺寸,以及任何靠样式而不是状态类
实现的显隐。
- 相邻的行内元素在可访问名称里不再有空格。例如 `Descriptions` 的一行在浏览器里
读作“标签 值”,测试里是“标签值”,按名称查询时用能容忍空白的正则。

### Eval 模块的单元测试与覆盖率

修改质量评测模块后,运行 `pnpm test:eval`;需要覆盖率时运行
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1284,7 +1284,7 @@ describe("ObjectMetricTrialDrawer", () => {
);

expect(
screen.getByRole("dialog", { name: "试算 · 订单支付额 已发布 · v5" }),
screen.getByRole("dialog", { name: /^试算 · 订单支付额\s*已发布 · v5$/ }),
).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "试算" }));
expect(await screen.findByText("无数据")).toBeInTheDocument();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,14 @@ vi.mock("@/modules/knowledge-network/components/shared/resource-permission-reque

import { ObjectTypeResourceGraph } from "./ObjectTypeResourceGraph";

// A detail row's accessible name is "label value" in a browser, where antd lays
// the two out as separate boxes. Tests run without antd's CSS, so the same two
// inline elements join with no space; accept either.
function detailRow(name: string) {
const parts = name.split(" ").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
return new RegExp(`^${parts.join("\\s*")}$`);
}

const originalMatchMedia = window.matchMedia;

beforeAll(() => {
Expand Down Expand Up @@ -414,13 +422,17 @@ describe("ObjectTypeResourceGraph", () => {
expect(within(graph).queryByText(/适用:|附加影响:/)).not.toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "查看详情" }));
const drawer = await screen.findByRole("dialog", { name: "行动类详情" });
expect(within(drawer).getByRole("row", { name: "行动类别 对象关系操作" })).toBeInTheDocument();
expect(within(drawer).getByRole("row", { name: "具体操作 建立关系" })).toBeInTheDocument();
expect(await within(drawer).findByRole("row", { name: "关系类 客户关系" })).toBeInTheDocument();
expect(
within(drawer).getByRole("row", {
name: "作用范围 销售订单 → 客户关系 → 客户",
}),
within(drawer).getByRole("row", { name: detailRow("行动类别 对象关系操作") }),
).toBeInTheDocument();
expect(
within(drawer).getByRole("row", { name: detailRow("具体操作 建立关系") }),
).toBeInTheDocument();
expect(
await within(drawer).findByRole("row", { name: detailRow("关系类 客户关系") }),
).toBeInTheDocument();
expect(
within(drawer).getByRole("row", { name: detailRow("作用范围 销售订单 → 客户关系 → 客户") }),
).toBeInTheDocument();
});
it("keeps the current object and shows lightweight category-specific empty hints", async () => {
Expand Down Expand Up @@ -795,22 +807,32 @@ describe("ObjectTypeResourceGraph", () => {
expect(actions).not.toBeNull();
expect(descriptions).not.toBeNull();
expect(description).not.toBeNull();
expect(within(drawer).getByRole("row", { name: "资源类型 行动类" })).toBeInTheDocument();
expect(within(drawer).getByRole("row", { name: "行动类别 对象实例操作" })).toBeInTheDocument();
expect(within(drawer).getByRole("row", { name: "具体操作 修改对象实例" })).toBeInTheDocument();
expect(
within(drawer).getByRole("row", { name: "触发条件 订单金额 大于 100" }),
within(drawer).getByRole("row", { name: detailRow("资源类型 行动类") }),
).toBeInTheDocument();
expect(
within(drawer).getByRole("row", { name: detailRow("行动类别 对象实例操作") }),
).toBeInTheDocument();
expect(within(drawer).getByRole("row", { name: "执行来源类型 工具" })).toBeInTheDocument();
expect(
within(drawer).getByRole("row", { name: "执行来源名称 订单核对工具" }),
within(drawer).getByRole("row", { name: detailRow("具体操作 修改对象实例") }),
).toBeInTheDocument();
expect(
within(drawer).getByRole("row", { name: /执行参数 2 个执行参数 · 1 个动态参数/ }),
within(drawer).getByRole("row", { name: detailRow("触发条件 订单金额 大于 100") }),
).toBeInTheDocument();
expect(within(drawer).getByRole("row", { name: "影响对象类 销售订单" })).toBeInTheDocument();
expect(
within(drawer).getByRole("row", { name: "影响说明 同步订单核对结果" }),
within(drawer).getByRole("row", { name: detailRow("执行来源类型 工具") }),
).toBeInTheDocument();
expect(
within(drawer).getByRole("row", { name: detailRow("执行来源名称 订单核对工具") }),
).toBeInTheDocument();
expect(
within(drawer).getByRole("row", { name: /执行参数\s*2 个执行参数 · 1 个动态参数/ }),
).toBeInTheDocument();
expect(
within(drawer).getByRole("row", { name: detailRow("影响对象类 销售订单") }),
).toBeInTheDocument();
expect(
within(drawer).getByRole("row", { name: detailRow("影响说明 同步订单核对结果") }),
).toBeInTheDocument();
expect(
actions!.compareDocumentPosition(descriptions!) & Node.DOCUMENT_POSITION_FOLLOWING,
Expand Down Expand Up @@ -1299,8 +1321,12 @@ describe("ObjectTypeResourceGraph", () => {

fireEvent.click(await screen.findByRole("button", { name: "关系类 客户关系" }));
const drawer = screen.getByRole("dialog", { name: "关系类详情" });
expect(within(drawer).getByRole("row", { name: "起点对象类 销售订单" })).toBeInTheDocument();
expect(within(drawer).getByRole("row", { name: "终点对象类 客户" })).toBeInTheDocument();
expect(
within(drawer).getByRole("row", { name: detailRow("起点对象类 销售订单") }),
).toBeInTheDocument();
expect(
within(drawer).getByRole("row", { name: detailRow("终点对象类 客户") }),
).toBeInTheDocument();
expect(within(drawer).getByText("描述")).toBeInTheDocument();
expect(within(drawer).queryByText("业务定义")).not.toBeInTheDocument();
expect(within(drawer).getByRole("button", { name: "申请权限" })).toHaveAttribute(
Expand Down Expand Up @@ -1330,10 +1356,14 @@ describe("ObjectTypeResourceGraph", () => {
fireEvent.click(await screen.findByRole("button", { name: "对象指标 订单金额" }));
expect(await screen.findByText("v1")).toBeInTheDocument();
expect(screen.getByText("草稿")).toBeInTheDocument();
expect(screen.queryByRole("row", { name: "资源类型 对象指标" })).not.toBeInTheDocument();
expect(screen.getByRole("row", { name: /对象指标编码 order_amount/ })).toBeInTheDocument();
expect(screen.getByRole("row", { name: "对象指标类型 原子对象指标" })).toBeInTheDocument();
expect(screen.getByRole("row", { name: "所属对象类 销售订单" })).toBeInTheDocument();
expect(
screen.queryByRole("row", { name: detailRow("资源类型 对象指标") }),
).not.toBeInTheDocument();
expect(screen.getByRole("row", { name: /对象指标编码\s*order_amount/ })).toBeInTheDocument();
expect(
screen.getByRole("row", { name: detailRow("对象指标类型 原子对象指标") }),
).toBeInTheDocument();
expect(screen.getByRole("row", { name: detailRow("所属对象类 销售订单") })).toBeInTheDocument();
expect(screen.queryByText("计算范围")).not.toBeInTheDocument();
expect(screen.getByText("业务定义")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "详情" })).toBeInTheDocument();
Expand Down
25 changes: 25 additions & 0 deletions src/test/setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
* Conditions. See LICENSE for the full text.
*/

import { Button } from "antd";
import { render } from "@testing-library/react";
import { createElement } from "react";
import { expect, it } from "vitest";

it("reads the scrollbar pseudo-element without a jsdom not-implemented warning", () => {
Expand Down Expand Up @@ -33,3 +36,25 @@ it("reads the scrollbar pseudo-element without a jsdom not-implemented warning",
virtualConsole.off("jsdomError", captureWarning);
}
});

it("keeps the styles antd injects empty so getComputedStyle stays cheap", () => {
render(createElement(Button, null, "OK"));

const injected = [...document.head.querySelectorAll("style[data-rc-order]")];
// If antd stops tagging its style nodes this way, setup.ts no longer empties
// them and every scene test silently slows down by an order of magnitude.
expect(injected.length).toBeGreaterThan(0);
expect(injected.map((style) => style.innerHTML)).toEqual(injected.map(() => ""));
});

it("still hides what antd marks with a hidden state class", () => {
const closedDropdown = document.createElement("div");
closedDropdown.className = "ant-dropdown ant-dropdown-hidden";
document.body.append(closedDropdown);

try {
expect(closedDropdown).not.toBeVisible();
} finally {
closedDropdown.remove();
}
});
56 changes: 56 additions & 0 deletions src/test/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,62 @@ const getComputedStyleWithScrollbarFallback: typeof window.getComputedStyle = (
window.getComputedStyle = getComputedStyleWithScrollbarFallback;
globalThis.getComputedStyle = getComputedStyleWithScrollbarFallback;

// antd's CSS-in-JS injects thousands of rules into <head>. jsdom answers every
// getComputedStyle() by matching each of those rules against the element, and
// Testing Library asks for it on every node when a *ByRole query resolves an
// accessible name, so scene tests spent ~90% of their time in selector
// matching. Tests assert behaviour, not antd's look, so the style nodes that
// rc-util injects (always tagged with data-rc-order before the CSS is written)
// are kept empty. Styles a test writes itself are left alone.
const styleInnerHtml = Object.getOwnPropertyDescriptor(Element.prototype, "innerHTML");
// The native accessors are re-invoked below with the style element as `this`.
// eslint-disable-next-line @typescript-eslint/unbound-method
const { get: readInnerHtml, set: writeInnerHtml } = styleInnerHtml ?? {};
if (readInnerHtml && writeInnerHtml) {
Object.defineProperty(HTMLStyleElement.prototype, "innerHTML", {
configurable: true,
enumerable: true,
get(this: HTMLStyleElement) {
return readInnerHtml.call(this) as string;
},
set(this: HTMLStyleElement, value: string) {
writeInnerHtml.call(this, this.hasAttribute("data-rc-order") ? "" : value);
},
});
}

// The one thing tests do rely on from antd's CSS: a closed dropdown, tooltip,
// popover, drawer or inactive tab pane stays in the DOM and is only hidden by
// a state class. Keep those rules so closed overlays stay out of *ByRole
// queries and toBeVisible() keeps its meaning. The list mirrors the
// `display: none` state classes in antd's component styles.
const antdHiddenStateClasses = [
"ant-collapse-content-hidden",
"ant-drawer-content-wrapper-hidden",
"ant-dropdown-hidden",
"ant-dropdown-menu-hidden",
Comment thread
sh00tg0a1 marked this conversation as resolved.
Resolved
"ant-dropdown-menu-submenu-hidden",
"ant-mentions-dropdown-hidden",
"ant-menu-hidden",
"ant-menu-submenu-hidden",
"ant-modal-mask-hidden",
"ant-picker-dropdown-hidden",
"ant-popover-hidden",
"ant-select-dropdown-hidden",
"ant-tabs-dropdown-hidden",
"ant-tabs-tabpane-hidden",
"ant-tag-hidden",
"ant-tooltip-hidden",
"ant-tour-hidden",
"ant-upload-hidden",
];
const antdHiddenState = document.createElement("style");
antdHiddenState.dataset.testAntdHiddenState = "";
antdHiddenState.textContent = `${antdHiddenStateClasses
.map((name) => `.${name}`)
.join(", ")} { display: none; }`;
document.head.append(antdHiddenState);

afterEach(() => {
cleanup();
});
Loading