Skip to content

Commit 41767f7

Browse files
Akshay090claude
andcommitted
test: add unit tests for getCalleeName, isHookCall, and countSetStateCalls
Tests cover both Identifier (useEffect) and MemberExpression (React.useEffect) patterns to validate namespace hook call detection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 691be5b commit 41767f7

File tree

1 file changed

+153
-0
lines changed

1 file changed

+153
-0
lines changed
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { describe, expect, it } from "vitest";
2+
import { getCalleeName, isHookCall, countSetStateCalls } from "../src/plugin/helpers.js";
3+
4+
describe("getCalleeName", () => {
5+
it("returns name from Identifier node", () => {
6+
expect(getCalleeName({ type: "Identifier", name: "useEffect" })).toBe("useEffect");
7+
});
8+
9+
it("returns property name from MemberExpression node", () => {
10+
expect(
11+
getCalleeName({
12+
type: "MemberExpression",
13+
object: { type: "Identifier", name: "React" },
14+
property: { type: "Identifier", name: "useEffect" },
15+
}),
16+
).toBe("useEffect");
17+
});
18+
19+
it("returns null for non-Identifier MemberExpression property", () => {
20+
expect(
21+
getCalleeName({
22+
type: "MemberExpression",
23+
object: { type: "Identifier", name: "React" },
24+
property: { type: "Literal", value: "useEffect" },
25+
}),
26+
).toBeNull();
27+
});
28+
29+
it("returns null for null/undefined input", () => {
30+
expect(getCalleeName(null)).toBeNull();
31+
expect(getCalleeName(undefined)).toBeNull();
32+
});
33+
34+
it("returns null for unsupported node types", () => {
35+
expect(getCalleeName({ type: "CallExpression" })).toBeNull();
36+
});
37+
});
38+
39+
describe("isHookCall", () => {
40+
const makeDirectCall = (name: string) => ({
41+
type: "CallExpression",
42+
callee: { type: "Identifier", name },
43+
arguments: [],
44+
});
45+
46+
const makeNamespaceCall = (namespace: string, name: string) => ({
47+
type: "CallExpression",
48+
callee: {
49+
type: "MemberExpression",
50+
object: { type: "Identifier", name: namespace },
51+
property: { type: "Identifier", name },
52+
},
53+
arguments: [],
54+
});
55+
56+
it("matches direct hook call with string hookName", () => {
57+
expect(isHookCall(makeDirectCall("useEffect"), "useEffect")).toBe(true);
58+
});
59+
60+
it("does not match wrong direct hook call", () => {
61+
expect(isHookCall(makeDirectCall("useState"), "useEffect")).toBe(false);
62+
});
63+
64+
it("matches namespace hook call with string hookName", () => {
65+
expect(isHookCall(makeNamespaceCall("React", "useEffect"), "useEffect")).toBe(true);
66+
});
67+
68+
it("matches namespace hook call with any namespace", () => {
69+
expect(isHookCall(makeNamespaceCall("MyLib", "useEffect"), "useEffect")).toBe(true);
70+
});
71+
72+
it("matches direct hook call with Set hookName", () => {
73+
const hooks = new Set(["useEffect", "useLayoutEffect"]);
74+
expect(isHookCall(makeDirectCall("useEffect"), hooks)).toBe(true);
75+
expect(isHookCall(makeDirectCall("useLayoutEffect"), hooks)).toBe(true);
76+
expect(isHookCall(makeDirectCall("useState"), hooks)).toBe(false);
77+
});
78+
79+
it("matches namespace hook call with Set hookName", () => {
80+
const hooks = new Set(["useEffect", "useLayoutEffect"]);
81+
expect(isHookCall(makeNamespaceCall("React", "useEffect"), hooks)).toBe(true);
82+
expect(isHookCall(makeNamespaceCall("React", "useState"), hooks)).toBe(false);
83+
});
84+
85+
it("rejects non-CallExpression nodes", () => {
86+
expect(isHookCall({ type: "Identifier", name: "useEffect" }, "useEffect")).toBe(false);
87+
});
88+
});
89+
90+
describe("countSetStateCalls", () => {
91+
it("counts direct setter calls", () => {
92+
const node = {
93+
type: "BlockStatement",
94+
body: [
95+
{
96+
type: "ExpressionStatement",
97+
expression: {
98+
type: "CallExpression",
99+
callee: { type: "Identifier", name: "setName" },
100+
arguments: [{ type: "Literal", value: "John" }],
101+
},
102+
},
103+
{
104+
type: "ExpressionStatement",
105+
expression: {
106+
type: "CallExpression",
107+
callee: { type: "Identifier", name: "setAge" },
108+
arguments: [{ type: "Literal", value: 30 }],
109+
},
110+
},
111+
],
112+
};
113+
expect(countSetStateCalls(node)).toBe(2);
114+
});
115+
116+
it("counts namespace setter calls (React.useState pattern)", () => {
117+
const node = {
118+
type: "BlockStatement",
119+
body: [
120+
{
121+
type: "ExpressionStatement",
122+
expression: {
123+
type: "CallExpression",
124+
callee: {
125+
type: "MemberExpression",
126+
object: { type: "Identifier", name: "actions" },
127+
property: { type: "Identifier", name: "setName" },
128+
},
129+
arguments: [{ type: "Literal", value: "John" }],
130+
},
131+
},
132+
],
133+
};
134+
expect(countSetStateCalls(node)).toBe(1);
135+
});
136+
137+
it("does not count non-setter calls", () => {
138+
const node = {
139+
type: "BlockStatement",
140+
body: [
141+
{
142+
type: "ExpressionStatement",
143+
expression: {
144+
type: "CallExpression",
145+
callee: { type: "Identifier", name: "fetchData" },
146+
arguments: [],
147+
},
148+
},
149+
],
150+
};
151+
expect(countSetStateCalls(node)).toBe(0);
152+
});
153+
});

0 commit comments

Comments
 (0)