/**
* Tests for: no-prop-drilling-depth
*
* Tests the detection of prop drilling through multiple components.
*/
import { RuleTester } from "@typescript-eslint/rule-tester";
import { describe, it, afterAll, beforeEach } from "vitest";
import rule, { clearPropCache } from "./no-prop-drilling-depth.js";
RuleTester.afterAll = afterAll;
RuleTester.describe = describe;
RuleTester.it = it;
// Clear cache between tests
beforeEach(() => {
clearPropCache();
});
const ruleTester = new RuleTester({
languageOptions: {
ecmaVersion: 2022,
sourceType: "module",
parserOptions: {
ecmaFeatures: { jsx: true },
},
},
});
ruleTester.run("no-prop-drilling-depth", rule, {
valid: [
// ============================================
// PROP USED DIRECTLY
// ============================================
{
name: "prop used directly in component",
code: `
function UserProfile({ user }) {
return
{user.name}
;
}
`,
},
{
name: "prop used in multiple places",
code: `
function UserCard({ user }) {
return (
);
}
`,
},
// ============================================
// PROP PASSED ONCE (within threshold)
// ============================================
{
name: "prop passed to one child (within default threshold)",
code: `
function Parent({ user }) {
return ;
}
function Child({ user }) {
return {user.name}
;
}
`,
},
{
name: "prop passed and used",
code: `
function Parent({ user }) {
console.log(user.id);
return ;
}
function Child({ user }) {
return {user.name}
;
}
`,
},
// ============================================
// IGNORED PROPS
// ============================================
{
name: "className is ignored by default",
code: `
function Wrapper({ className }) {
return ;
}
function Container({ className }) {
return ;
}
function Inner({ className }) {
return ;
}
`,
},
{
name: "style is ignored by default",
code: `
function A({ style }) {
return ;
}
function B({ style }) {
return ;
}
function C({ style }) {
return ;
}
`,
},
{
name: "children is ignored by default",
code: `
function Layout({ children }) {
return ;
}
function Wrapper({ children }) {
return ;
}
function Container({ children }) {
return {children}
;
}
`,
},
// ============================================
// WITHIN CUSTOM THRESHOLD
// ============================================
{
name: "drilling within custom higher threshold",
code: `
function A({ data }) {
return ;
}
function B({ data }) {
return ;
}
function C({ data }) {
return ;
}
function D({ data }) {
return {data.value}
;
}
`,
options: [{ maxDepth: 3 }],
},
// ============================================
// IGNORED COMPONENT PATTERNS
// ============================================
{
name: "ignored component pattern - Layout",
code: `
function MainLayout({ user }) {
return ;
}
function InnerLayout({ user }) {
return ;
}
function Content({ user }) {
return ;
}
`,
options: [{ ignoreComponents: ["^.*Layout$"] }],
},
// ============================================
// NO PROPS
// ============================================
{
name: "component without props",
code: `
function Header() {
return Header
;
}
`,
},
// ============================================
// NON-COMPONENT FUNCTIONS
// ============================================
{
name: "non-component function (lowercase)",
code: `
function helper({ data }) {
return data.value;
}
`,
},
// ============================================
// PROP USED SOMEWHERE IN CHAIN
// ============================================
{
name: "prop used in middle of chain",
code: `
function A({ user }) {
return ;
}
function B({ user }) {
console.log(user.id); // Used here
return ;
}
function C({ user }) {
return {user.name}
;
}
`,
},
// ============================================
// CUSTOM IGNORED PROPS
// ============================================
{
name: "custom ignored prop",
code: `
function A({ theme }) {
return ;
}
function B({ theme }) {
return ;
}
function C({ theme }) {
return ;
}
`,
options: [{ ignoredProps: ["theme"] }],
},
// ============================================
// DIFFERENT PROPS TO DIFFERENT CHILDREN
// ============================================
{
name: "different props to different children",
code: `
function Parent({ user, settings }) {
return (
);
}
function UserView({ user }) {
return {user.name}
;
}
function SettingsView({ settings }) {
return {settings.theme}
;
}
`,
},
],
invalid: [
// ============================================
// DRILLING EXCEEDS DEFAULT THRESHOLD (2)
// ============================================
{
name: "prop drilled through 3 components",
code: `
function Grandparent({ user }) {
return ;
}
function Parent({ user }) {
return ;
}
function Child({ user }) {
return ;
}
function Grandchild({ user }) {
return {user.name}
;
}
`,
errors: [
{
messageId: "propDrilling",
data: {
propName: "user",
depth: "3",
path: "Grandparent → Parent → Child → Grandchild",
},
},
],
},
{
name: "prop drilled through 4 components",
code: `
function A({ data }) {
return ;
}
function B({ data }) {
return ;
}
function C({ data }) {
return ;
}
function D({ data }) {
return ;
}
function E({ data }) {
return {data.value}
;
}
`,
errors: [
{
messageId: "propDrilling",
data: {
propName: "data",
depth: "3",
path: "A → B → C → D",
},
},
{
messageId: "propDrilling",
data: {
propName: "data",
depth: "3",
path: "B → C → D → E",
},
},
],
},
// ============================================
// MULTIPLE DRILLED PROPS
// ============================================
{
name: "multiple props drilled",
code: `
function Top({ user, settings }) {
return ;
}
function Middle({ user, settings }) {
return ;
}
function Bottom({ user, settings }) {
return ;
}
function Final({ user, settings }) {
return {user.name} - {settings.theme}
;
}
`,
errors: [
{
messageId: "propDrilling",
data: {
propName: "user",
depth: "3",
path: "Top → Middle → Bottom → Final",
},
},
{
messageId: "propDrilling",
data: {
propName: "settings",
depth: "3",
path: "Top → Middle → Bottom → Final",
},
},
],
},
// ============================================
// CUSTOM LOWER THRESHOLD
// ============================================
{
name: "drilling exceeds custom threshold of 1",
code: `
function Parent({ config }) {
return ;
}
function Child({ config }) {
return ;
}
function Grandchild({ config }) {
return {config.value}
;
}
`,
options: [{ maxDepth: 1 }],
errors: [
{
messageId: "propDrilling",
data: {
propName: "config",
depth: "2",
path: "Parent → Child → Grandchild",
},
},
],
},
// ============================================
// ARROW FUNCTION COMPONENTS
// ============================================
{
name: "arrow function components drilling",
code: `
const A = ({ item }) => ;
const B = ({ item }) => ;
const C = ({ item }) => ;
const D = ({ item }) => {item.value}
;
`,
errors: [
{
messageId: "propDrilling",
data: {
propName: "item",
depth: "3",
path: "A → B → C → D",
},
},
],
},
// ============================================
// MIXED FUNCTION STYLES
// ============================================
{
name: "mixed function declaration and arrow",
code: `
function Container({ data }) {
return ;
}
const Wrapper = ({ data }) => ;
function Inner({ data }) {
return ;
}
const Deep = ({ data }) => {data.value}
;
`,
errors: [
{
messageId: "propDrilling",
data: {
propName: "data",
depth: "3",
path: "Container → Wrapper → Inner → Deep",
},
},
],
},
// ============================================
// PROP NOT IN IGNORED LIST
// ============================================
{
name: "custom prop not in default ignored list",
code: `
function A({ theme }) {
return ;
}
function B({ theme }) {
return ;
}
function C({ theme }) {
return ;
}
function D({ theme }) {
return {theme}
;
}
`,
errors: [
{
messageId: "propDrilling",
data: {
propName: "theme",
depth: "3",
path: "A → B → C → D",
},
},
],
},
// ============================================
// DRILLING IN NESTED JSX
// ============================================
{
name: "drilling through nested JSX structure",
code: `
function Page({ user }) {
return (
);
}
function Main({ user }) {
return (
);
}
function Content({ user }) {
return ;
}
function Profile({ user }) {
return {user.name}
;
}
`,
errors: [
{
messageId: "propDrilling",
data: {
propName: "user",
depth: "3",
path: "Page → Main → Content → Profile",
},
},
],
},
],
});