diff --git a/eslint-rules/harness.test.mjs b/eslint-rules/harness.test.mjs index 8eca8dc87..9aeadfe47 100644 --- a/eslint-rules/harness.test.mjs +++ b/eslint-rules/harness.test.mjs @@ -14,8 +14,44 @@ const syntaxOnlyConfig = config.filter((entry) => !entry?.languageOptions?.parse const linter = new Linter({ configType: "flat" }); -/** 在指定文件路径下 lint 一段代码,返回非致命诊断的 ruleId 列表;解析失败直接抛出。 */ -function ruleIdsAt(code, filename) { +const sourceFilenamePattern = /\.(?:[cm]?js|[jt]sx?)$/; + +const configuredRuleIds = new Set(syntaxOnlyConfig.flatMap((entry) => Object.keys(entry?.rules ?? {}))); + +function assertFixtureCode(code) { + if (typeof code !== "string") { + throw new TypeError("code must be a string"); + } +} + +function assertFixtureFilename(filename) { + if (typeof filename !== "string") { + throw new TypeError("filename must be a string"); + } + + const trimmed = filename.trim(); + if (trimmed !== filename || trimmed === "" || !sourceFilenamePattern.test(filename)) { + throw new TypeError(`filename must be a non-empty source filename: ${filename}`); + } +} + +function assertRuleId(ruleId) { + if (typeof ruleId !== "string" || ruleId.trim() === "") { + throw new TypeError("ruleId must be a non-empty string"); + } + if (!configuredRuleIds.has(ruleId)) { + throw new Error(`Unknown configured ruleId: ${ruleId}`); + } +} + +/** + * 用真实 ESLint flat config 检查虚拟文件,返回非致命诊断的 ruleId 列表。 + * `filename` 决定配置作用域;解析失败会抛出异常,避免无效夹具伪装成规则放行。 + * Vitest 用例用 `ruleCountAt({ code, filename, ruleId })` 断言命中数量,并先合并同名虚拟文件的代码片段。 + */ +function ruleIdsAt({ code, filename }) { + assertFixtureCode(code); + assertFixtureFilename(filename); const messages = linter.verify(code, syntaxOnlyConfig, { filename }); const fatal = messages.find((m) => m.fatal); if (fatal) { @@ -24,50 +60,100 @@ function ruleIdsAt(code, filename) { return messages.map((m) => m.ruleId); } +/** 返回指定 ruleId 的诊断数量。 */ +function ruleCountAt({ code, filename, ruleId }) { + assertRuleId(ruleId); + return ruleIdsAt({ code, filename }).filter((id) => id === ruleId).length; +} + describe("harness lint 规则", () => { // 首次 linter.verify 需解析整套 flat config(typescript-eslint 解析器 + react 插件),冷启动开销大; // 在 beforeAll 预热一次(走 hook 超时而非 850ms 测试超时),避免这次一次性成本偶发落到某个 it 里超时。 beforeAll(() => { - ruleIdsAt(`const x = 1;`, "src/pages/foo.tsx"); + ruleIdsAt({ code: `const x = 1;`, filename: "src/pages/foo.tsx" }); }); describe("① scriptcat/no-i18n-default-value:禁止 t() 内联 defaultValue 兜底", () => { const RULE = "scriptcat/no-i18n-default-value"; it("拦截 t(key, { defaultValue })", () => { - const ids = ruleIdsAt(`const x = t("ns:a.b", { defaultValue: "中文兜底" });`, "src/pages/foo.tsx"); - expect(ids).toContain(RULE); + expect( + ruleCountAt({ + code: `const x = t("ns:a.b", { defaultValue: "中文兜底" });`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("拦截 i18n.t / i18next.t 的 defaultValue", () => { - expect(ruleIdsAt(`i18n.t("k", { defaultValue: "x" });`, "src/pages/foo.tsx")).toContain(RULE); - expect(ruleIdsAt(`i18next.t("k", { defaultValue: "x" });`, "src/pages/foo.tsx")).toContain(RULE); + expect( + ruleCountAt({ + code: `i18n.t("k", { defaultValue: "x" }); i18next.t("k", { defaultValue: "x" });`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(2); }); it("放行不带 defaultValue 的 t() 调用", () => { - expect(ruleIdsAt(`const x = t("ns:a.b");`, "src/pages/foo.tsx")).not.toContain(RULE); - expect(ruleIdsAt(`const x = t("ns:a.b", { count: 1 });`, "src/pages/foo.tsx")).not.toContain(RULE); + expect( + ruleCountAt({ + code: `const x = t("ns:a.b"); const y = t("ns:a.b", { count: 1 });`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(0); }); it("不误伤组件的 defaultValue 属性(如 )", () => { - expect(ruleIdsAt(`const e = ;`, "src/pages/foo.tsx")).not.toContain(RULE); + expect( + ruleCountAt({ + code: `const e = ;`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(0); }); it("不误伤非 i18n 的成员调用(如 GM_getValue 第二参对象)", () => { - expect(ruleIdsAt(`store.getValue("k", { defaultValue: 1 });`, "src/pages/foo.tsx")).not.toContain(RULE); + expect( + ruleCountAt({ + code: `store.getValue("k", { defaultValue: 1 });`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(0); }); it("拦截可选链 / computed 成员调用中的 defaultValue", () => { - expect(ruleIdsAt(`i18n?.t("k", { defaultValue: "x" });`, "src/pages/foo.tsx")).toContain(RULE); - expect(ruleIdsAt(`i18next["t"]("k", { defaultValue: "x" });`, "src/pages/foo.tsx")).toContain(RULE); + expect( + ruleCountAt({ + code: `i18n?.t("k", { defaultValue: "x" }); i18next["t"]("k", { defaultValue: "x" });`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(2); }); it("拦截 computed defaultValue key", () => { - expect(ruleIdsAt(`t("k", { ["defaultValue"]: "x" });`, "src/pages/foo.tsx")).toContain(RULE); + expect( + ruleCountAt({ + code: `t("k", { ["defaultValue"]: "x" });`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("明确只禁止 defaultValue 对象兜底,不拦截非对象第二参", () => { - expect(ruleIdsAt(`t("k", "fallback text");`, "src/pages/foo.tsx")).not.toContain(RULE); + expect( + ruleCountAt({ + code: `t("k", "fallback text");`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(0); }); }); @@ -75,13 +161,23 @@ describe("harness lint 规则", () => { const RULE = "no-restricted-imports"; it("拦截从 @radix-ui/react-* 单包导入", () => { - const ids = ruleIdsAt(`import { Dialog } from "@radix-ui/react-dialog";`, "src/pages/components/ui/dialog.tsx"); - expect(ids).toContain(RULE); + expect( + ruleCountAt({ + code: `import { Dialog } from "@radix-ui/react-dialog";`, + filename: "src/pages/components/ui/dialog.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("放行从合并包 radix-ui 导入", () => { - const ids = ruleIdsAt(`import { Dialog } from "radix-ui";`, "src/pages/components/ui/dialog.tsx"); - expect(ids).not.toContain(RULE); + expect( + ruleCountAt({ + code: `import { Dialog } from "radix-ui";`, + filename: "src/pages/components/ui/dialog.tsx", + ruleId: RULE, + }) + ).toBe(0); }); }); @@ -89,48 +185,93 @@ describe("harness lint 规则", () => { const RULE = "scriptcat/no-raw-color-classname"; it("拦截 bg-white 等原始颜色", () => { - expect(ruleIdsAt(`const e =
;`, "src/pages/foo.tsx")).toContain(RULE); + expect( + ruleCountAt({ + code: `const e =
;`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("拦截带变体前缀的原始颜色(dark:bg-gray-800)", () => { - expect(ruleIdsAt(`const e =
;`, "src/pages/foo.tsx")).toContain(RULE); + expect( + ruleCountAt({ + code: `const e =
;`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("拦截任意值十六进制颜色 bg-[#fff]", () => { - expect(ruleIdsAt(`const e =
;`, "src/pages/foo.tsx")).toContain(RULE); + expect( + ruleCountAt({ + code: `const e =
;`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("拦截 cn() 内的原始颜色", () => { - expect(ruleIdsAt(`const e =
;`, "src/pages/foo.tsx")).toContain(RULE); + expect( + ruleCountAt({ + code: `const e =
;`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("放行设计令牌 bg-background / text-foreground / bg-card", () => { expect( - ruleIdsAt(`const e =
;`, "src/pages/foo.tsx") - ).not.toContain(RULE); - expect( - ruleIdsAt(`const e =
;`, "src/pages/foo.tsx") - ).not.toContain(RULE); + ruleCountAt({ + code: `const e =
; const f =
;`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(0); }); it("仅作用于 src/pages(src/app 不受限)", () => { - expect(ruleIdsAt(`const e =
;`, "src/app/foo.tsx")).not.toContain(RULE); + expect( + ruleCountAt({ + code: `const e =
;`, + filename: "src/app/foo.tsx", + ruleId: RULE, + }) + ).toBe(0); }); it("拦截非灰阶 Tailwind 色板(text-red-500 / bg-blue-600)", () => { - expect(ruleIdsAt(`const e =
;`, "src/pages/foo.tsx")).toContain(RULE); - expect(ruleIdsAt(`const e =
;`, "src/pages/foo.tsx")).toContain(RULE); + expect( + ruleCountAt({ + code: `const e =
; const f =
;`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(2); }); it("拦截带变体和 opacity 后缀的原始色板", () => { - expect(ruleIdsAt(`const e =
;`, "src/pages/foo.tsx")).toContain(RULE); - expect(ruleIdsAt(`const e =
;`, "src/pages/foo.tsx")).toContain( - RULE - ); + expect( + ruleCountAt({ + code: `const e =
; const f =
;`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(2); }); it("拦截 cn() 内的非灰阶原始颜色", () => { - expect(ruleIdsAt(`const e =
;`, "src/pages/foo.tsx")).toContain(RULE); + expect( + ruleCountAt({ + code: `const e =
;`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(1); }); }); @@ -139,60 +280,67 @@ describe("harness lint 规则", () => { it("拦截 fireEvent 及其导入别名", () => { expect( - ruleIdsAt( - `import { fireEvent as fe, waitFor } from "@testing-library/react"; waitFor(() => fe.click(button));`, - "src/pages/example.test.tsx" - ) - ).toContain(RULE); + ruleCountAt({ + code: `import { fireEvent as fe, waitFor } from "@testing-library/react"; waitFor(() => fe.click(button));`, + filename: "src/pages/example.test.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("拦截 userEvent 实例交互", () => { expect( - ruleIdsAt( - `import userEvent from "@testing-library/user-event"; import { waitFor } from "@testing-library/react"; const user = userEvent.setup(); waitFor(() => user.click(button));`, - "src/pages/example.test.tsx" - ) - ).toContain(RULE); + ruleCountAt({ + code: `import userEvent from "@testing-library/user-event"; import { waitFor } from "@testing-library/react"; const user = userEvent.setup(); waitFor(() => user.click(button));`, + filename: "src/pages/example.test.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("拦截 userEvent 导入别名实例交互", () => { expect( - ruleIdsAt( - `import { userEvent as ue } from "@testing-library/user-event"; import { waitFor } from "@testing-library/react"; const u = ue.setup(); waitFor(() => u.type(input, "x"));`, - "src/pages/example.test.tsx" - ) - ).toContain(RULE); + ruleCountAt({ + code: `import { userEvent as ue } from "@testing-library/user-event"; import { waitFor } from "@testing-library/react"; const u = ue.setup(); waitFor(() => u.type(input, "x"));`, + filename: "src/pages/example.test.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("拦截 waitFor 导入别名", () => { expect( - ruleIdsAt( - `import { fireEvent, waitFor as until } from "@testing-library/react"; until(() => fireEvent.click(button));`, - "src/pages/example.test.tsx" - ) - ).toContain(RULE); + ruleCountAt({ + code: `import { fireEvent, waitFor as until } from "@testing-library/react"; until(() => fireEvent.click(button));`, + filename: "src/pages/example.test.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("放行同名普通函数和被局部变量遮蔽的导入名", () => { expect( - ruleIdsAt( - `function waitFor(callback) { callback(); } const fireEvent = { click() {} }; waitFor(() => fireEvent.click(button));`, - "src/pages/example.test.tsx" - ) - ).not.toContain(RULE); - expect( - ruleIdsAt( - `import { fireEvent as fe, waitFor } from "@testing-library/react"; function run(fe) { waitFor(() => fe.click(button)); }`, - "src/pages/example.test.tsx" - ) - ).not.toContain(RULE); - - expect( - ruleIdsAt( - `import { fireEvent, waitFor } from "@testing-library/react"; fireEvent.click(button); waitFor(() => expect(screen.getByText("done")).toBeInTheDocument());`, - "src/pages/example.test.tsx" - ) - ).not.toContain(RULE); + ruleCountAt({ + code: ` + import { fireEvent, fireEvent as fe, waitFor } from "@testing-library/react"; + + function localCase() { + function waitFor(callback) { callback(); } + const fireEvent = { click() {} }; + waitFor(() => fireEvent.click(button)); + } + + function run(fe) { + waitFor(() => fe.click(button)); + } + + fireEvent.click(button); + waitFor(() => expect(screen.getByText("done")).toBeInTheDocument()); + `, + filename: "src/pages/example.test.tsx", + ruleId: RULE, + }) + ).toBe(0); }); }); @@ -201,38 +349,43 @@ describe("harness lint 规则", () => { it("拦截仅包装 getBy 存在性断言的 waitFor", () => { expect( - ruleIdsAt( - `import { waitFor } from "@testing-library/react"; waitFor(() => expect(screen.getByText("done")).toBeInTheDocument());`, - "src/pages/example.test.tsx" - ) - ).toContain(RULE); + ruleCountAt({ + code: `import { waitFor } from "@testing-library/react"; waitFor(() => expect(screen.getByText("done")).toBeInTheDocument());`, + filename: "src/pages/example.test.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("拦截 waitFor 导入别名并放行同名普通函数", () => { expect( - ruleIdsAt( - `import { waitFor as until } from "@testing-library/react"; until(() => expect(screen.getByText("done")).toBeInTheDocument());`, - "src/pages/example.test.tsx" - ) - ).toContain(RULE); - expect( - ruleIdsAt( - `function waitFor(callback) { callback(); } waitFor(() => expect(screen.getByText("done")).toBeInTheDocument());`, - "src/pages/example.test.tsx" - ) - ).not.toContain(RULE); + ruleCountAt({ + code: ` + import { waitFor, waitFor as until } from "@testing-library/react"; + until(() => expect(screen.getByText("done")).toBeInTheDocument()); + + function localCase() { + function waitFor(callback) { callback(); } + waitFor(() => expect(screen.getByText("done")).toBeInTheDocument()); + } + `, + filename: "src/pages/example.test.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("放行多断言和非存在性断言", () => { expect( - ruleIdsAt( - `waitFor(() => { expect(screen.getByText("done")).toBeInTheDocument(); expect(api).toHaveBeenCalled(); });`, - "src/pages/example.test.tsx" - ) - ).not.toContain(RULE); - expect( - ruleIdsAt(`waitFor(() => expect(screen.getByRole("button")).toBeEnabled());`, "src/pages/example.test.tsx") - ).not.toContain(RULE); + ruleCountAt({ + code: ` + waitFor(() => { expect(screen.getByText("done")).toBeInTheDocument(); expect(api).toHaveBeenCalled(); }); + waitFor(() => expect(screen.getByRole("button")).toBeEnabled()); + `, + filename: "src/pages/example.test.tsx", + ruleId: RULE, + }) + ).toBe(0); }); }); @@ -240,23 +393,37 @@ describe("harness lint 规则", () => { const RULE = "scriptcat/no-test-fixed-sleep"; it("拦截 Playwright waitForTimeout 和 timer Promise", () => { - expect(ruleIdsAt(`await page.waitForTimeout(500);`, "e2e/example.spec.ts")).toContain(RULE); expect( - ruleIdsAt(`await new Promise((resolve) => setTimeout(resolve, 0));`, "src/pages/example.test.tsx") - ).toContain(RULE); + ruleCountAt({ + code: `await page.waitForTimeout(500);`, + filename: "e2e/example.spec.ts", + ruleId: RULE, + }) + ).toBe(1); expect( - ruleIdsAt(`await new Promise((resolve) => setTimeout(() => resolve(), 0));`, "src/pages/example.test.tsx") - ).toContain(RULE); + ruleCountAt({ + code: `await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(() => resolve(), 0));`, + filename: "src/pages/example.test.tsx", + ruleId: RULE, + }) + ).toBe(2); }); it("放行带有逐处豁免注释的时序让步和其他 setTimeout", () => { expect( - ruleIdsAt( - `// eslint-disable-next-line scriptcat/no-test-fixed-sleep -- listener registration contract\nawait new Promise((resolve) => setTimeout(resolve, 0));`, - "src/pages/example.test.tsx" - ) - ).not.toContain(RULE); - expect(ruleIdsAt(`setTimeout(tick, 10);`, "e2e/example.spec.ts")).not.toContain(RULE); + ruleCountAt({ + code: `// eslint-disable-next-line scriptcat/no-test-fixed-sleep -- listener registration contract\nawait new Promise((resolve) => setTimeout(resolve, 0));`, + filename: "src/pages/example.test.tsx", + ruleId: RULE, + }) + ).toBe(0); + expect( + ruleCountAt({ + code: `setTimeout(tick, 10);`, + filename: "e2e/example.spec.ts", + ruleId: RULE, + }) + ).toBe(0); }); }); @@ -265,54 +432,72 @@ describe("harness lint 规则", () => { it("拦截显式的一页以上分页边界写法及其 const 别名", () => { expect( - ruleIdsAt( - `const total = NETWORK_RULES_PAGE_SIZE + 1; const rows = Array.from({ length: total }, makeRow);`, - "src/pages/example.test.tsx" - ) - ).toContain(RULE); - expect(ruleIdsAt(`Array.from({ length: PAGE_ROWS + 1 }, makeRow);`, "src/pages/example.test.tsx")).toContain( - RULE - ); + ruleCountAt({ + code: ` + const total = NETWORK_RULES_PAGE_SIZE + 1; + const rows = Array.from({ length: total }, makeRow); + Array.from({ length: PAGE_ROWS + 1 }, makeRow); + `, + filename: "src/pages/example.test.tsx", + ruleId: RULE, + }) + ).toBe(2); }); it("放行非边界夹具和非页面测试", () => { - expect(ruleIdsAt(`Array.from({ length: 20 }, makeRow);`, "src/pages/example.test.tsx")).not.toContain(RULE); - expect(ruleIdsAt(`Array.from({ length: PAGE_SIZE + 2 }, makeRow);`, "src/pages/example.test.tsx")).not.toContain( - RULE - ); - expect(ruleIdsAt(`Array.from({ length: itemCount + 1 }, makeItem);`, "src/pages/example.test.tsx")).not.toContain( - RULE - ); - expect(ruleIdsAt(`Array.from({ length: PAGE_SIZE + 1 }, makeRow);`, "src/pkg/example.test.ts")).not.toContain( - RULE - ); + expect( + ruleCountAt({ + code: ` + Array.from({ length: 20 }, makeRow); + Array.from({ length: PAGE_SIZE + 2 }, makeRow); + Array.from({ length: itemCount + 1 }, makeItem); + `, + filename: "src/pages/example.test.tsx", + ruleId: RULE, + }) + ).toBe(0); + expect( + ruleCountAt({ + code: `Array.from({ length: PAGE_SIZE + 1 }, makeRow);`, + filename: "src/pkg/example.test.ts", + ruleId: RULE, + }) + ).toBe(0); }); it("只追踪 const 的单级别名", () => { expect( - ruleIdsAt(`let total = PAGE_SIZE + 1; Array.from({ length: total }, makeRow);`, "src/pages/example.test.tsx") - ).not.toContain(RULE); - expect( - ruleIdsAt( - `const total = PAGE_SIZE + 1; const count = total; Array.from({ length: count }, makeRow);`, - "src/pages/example.test.tsx" - ) - ).not.toContain(RULE); + ruleCountAt({ + code: ` + let total = PAGE_SIZE + 1; + Array.from({ length: total }, makeRow); + + const aliasedTotal = PAGE_SIZE + 1; + const count = aliasedTotal; + Array.from({ length: count }, makeRow); + `, + filename: "src/pages/example.test.tsx", + ruleId: RULE, + }) + ).toBe(0); }); it("放行词法遮蔽和逐处说明的边界夹具", () => { expect( - ruleIdsAt( - `const Array = { from() {} }; Array.from({ length: PAGE_SIZE + 1 }, makeRow);`, - "src/pages/example.test.tsx" - ) - ).not.toContain(RULE); - expect( - ruleIdsAt( - `// eslint-disable-next-line scriptcat/no-test-large-boundary-fixture -- pagination boundary\nArray.from({ length: PAGE_SIZE + 1 }, makeRow);`, - "src/pages/example.test.tsx" - ) - ).not.toContain(RULE); + ruleCountAt({ + code: ` + function shadowedArrayCase() { + const Array = { from() {} }; + Array.from({ length: PAGE_SIZE + 1 }, makeRow); + } + + // eslint-disable-next-line scriptcat/no-test-large-boundary-fixture -- pagination boundary + Array.from({ length: PAGE_SIZE + 1 }, makeRow); + `, + filename: "src/pages/example.test.tsx", + ruleId: RULE, + }) + ).toBe(0); }); }); @@ -321,21 +506,39 @@ describe("harness lint 规则", () => { it("拦截 ui 组件里的 forwardRef(...)", () => { const code = `import { forwardRef } from "react"; const C = forwardRef(() => null);`; - expect(ruleIdsAt(code, "src/pages/components/ui/foo.tsx")).toContain(RULE); + expect( + ruleCountAt({ + code, + filename: "src/pages/components/ui/foo.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("拦截 ui 组件里的 React.forwardRef(...)", () => { - expect(ruleIdsAt(`const C = React.forwardRef(() => null);`, "src/pages/components/ui/foo.tsx")).toContain(RULE); + expect( + ruleCountAt({ + code: `const C = React.forwardRef(() => null);`, + filename: "src/pages/components/ui/foo.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("拦截 ui 目录之外的页面级组件使用 forwardRef", () => { const code = `import { forwardRef } from "react"; const C = forwardRef(() => null);`; - expect(ruleIdsAt(code, "src/pages/options/routes/ScriptList/components.tsx")).toContain(RULE); + expect( + ruleCountAt({ + code, + filename: "src/pages/options/routes/ScriptList/components.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("放行 src/pages 之外的组件使用 forwardRef", () => { const code = `import { forwardRef } from "react"; const C = forwardRef(() => null);`; - expect(ruleIdsAt(code, "src/app/foo.tsx")).not.toContain(RULE); + expect(ruleCountAt({ code, filename: "src/app/foo.tsx", ruleId: RULE })).toBe(0); }); }); });