/**
* Tests for: zustand-use-selectors
*
* Tests the enforcement of selector functions when accessing Zustand stores.
*/
import { RuleTester } from "@typescript-eslint/rule-tester";
import { describe, it, afterAll } from "vitest";
import rule from "./zustand-use-selectors.js";
RuleTester.afterAll = afterAll;
RuleTester.describe = describe;
RuleTester.it = it;
const ruleTester = new RuleTester({
languageOptions: {
ecmaVersion: 2022,
sourceType: "module",
parserOptions: {
ecmaFeatures: { jsx: true },
},
},
});
ruleTester.run("zustand-use-selectors", rule, {
valid: [
// ============================================
// ARROW FUNCTION SELECTORS
// ============================================
{
name: "arrow function selector - single property",
code: `const count = useStore((state) => state.count);`,
},
{
name: "arrow function selector - nested property",
code: `const name = useUserStore((s) => s.user.name);`,
},
{
name: "arrow function selector - computed value",
code: `const total = useCartStore((s) => s.items.reduce((a, b) => a + b.price, 0));`,
},
{
name: "arrow function selector - returning object",
code: `const { count, name } = useStore((s) => ({ count: s.count, name: s.name }));`,
},
{
name: "arrow function selector - with block body",
code: `const items = useStore((state) => { return state.items; });`,
},
// ============================================
// NAMED SELECTOR REFERENCES
// ============================================
{
name: "named selector function",
code: `
const selectCount = (state) => state.count;
const count = useStore(selectCount);
`,
},
{
name: "imported selector",
code: `const user = useAuthStore(selectCurrentUser);`,
},
{
name: "selector from object",
code: `const count = useStore(selectors.count);`,
},
// ============================================
// USESHALLOW PATTERN
// ============================================
{
name: "useShallow with arrow selector",
code: `const { count, name } = useStore(useShallow((s) => ({ count: s.count, name: s.name })));`,
},
{
name: "useShallow with named selector",
code: `const data = useStore(useShallow(selectUserData));`,
},
{
name: "useShallow - complex selection",
code: `const items = useCartStore(useShallow((s) => s.items.filter(i => i.active)));`,
},
// ============================================
// FUNCTION EXPRESSION SELECTORS
// ============================================
{
name: "function expression selector",
code: `const count = useStore(function(state) { return state.count; });`,
},
// ============================================
// ACTIONS/METHODS (still with selector)
// ============================================
{
name: "selecting an action",
code: `const increment = useStore((s) => s.increment);`,
},
{
name: "selecting multiple actions",
code: `const { increment, decrement } = useStore((s) => ({ increment: s.increment, decrement: s.decrement }));`,
},
// ============================================
// NON-STORE HOOKS (not matching pattern)
// ============================================
{
name: "useState - not a store",
code: `const [count, setCount] = useState(0);`,
},
{
name: "useEffect - not a store",
code: `useEffect(() => {}, []);`,
},
{
name: "custom hook not matching pattern",
code: `const data = useData();`,
},
{
name: "useSomething - not matching Store pattern",
code: `const result = useSomething();`,
},
// ============================================
// CUSTOM STORE PATTERNS
// ============================================
{
name: "custom pattern - useGlobalStore",
code: `const value = useGlobalStore((s) => s.value);`,
options: [{ storePattern: "^useGlobal\\w*$" }],
},
// ============================================
// INLINE SELECTORS (allowed by default)
// ============================================
{
name: "inline arrow allowed by default",
code: `const count = useStore((s) => s.count);`,
options: [{ requireNamedSelectors: false }],
},
// ============================================
// NAMED SELECTORS REQUIRED (when option set)
// ============================================
{
name: "named selector when required",
code: `const count = useStore(selectCount);`,
options: [{ requireNamedSelectors: true }],
},
{
name: "useShallow allowed even when named required",
code: `const data = useStore(useShallow((s) => s.data));`,
options: [{ requireNamedSelectors: true, allowShallow: true }],
},
],
invalid: [
// ============================================
// NO SELECTOR AT ALL
// ============================================
{
name: "no arguments to store hook",
code: `const state = useStore();`,
errors: [
{
messageId: "missingSelector",
data: { storeName: "useStore" },
},
],
},
{
name: "no arguments - destructuring",
code: `const { count, name } = useStore();`,
errors: [
{
messageId: "missingSelector",
data: { storeName: "useStore" },
},
],
},
{
name: "no arguments - custom store name",
code: `const user = useAuthStore();`,
errors: [
{
messageId: "missingSelector",
data: { storeName: "useAuthStore" },
},
],
},
{
name: "no arguments - useCartStore",
code: `const cart = useCartStore();`,
errors: [
{
messageId: "missingSelector",
data: { storeName: "useCartStore" },
},
],
},
// ============================================
// MULTIPLE VIOLATIONS
// ============================================
{
name: "multiple stores without selectors",
code: `
const auth = useAuthStore();
const cart = useCartStore();
const ui = useUIStore();
`,
errors: [
{
messageId: "missingSelector",
data: { storeName: "useAuthStore" },
},
{
messageId: "missingSelector",
data: { storeName: "useCartStore" },
},
{
messageId: "missingSelector",
data: { storeName: "useUIStore" },
},
],
},
// ============================================
// IN COMPONENT CONTEXT
// ============================================
{
name: "inside component - no selector",
code: `
function Counter() {
const { count } = useStore();
return {count};
}
`,
errors: [
{
messageId: "missingSelector",
data: { storeName: "useStore" },
},
],
},
{
name: "inside arrow component - no selector",
code: `
const Counter = () => {
const state = useCounterStore();
return {state.count};
};
`,
errors: [
{
messageId: "missingSelector",
data: { storeName: "useCounterStore" },
},
],
},
// ============================================
// REQUIRE NAMED SELECTORS VIOLATIONS
// ============================================
{
name: "inline arrow when named required",
code: `const count = useStore((s) => s.count);`,
options: [{ requireNamedSelectors: true }],
errors: [
{
messageId: "useSelectorFunction",
data: { storeName: "useStore" },
},
],
},
{
name: "inline function expression when named required",
code: `const count = useStore(function(s) { return s.count; });`,
options: [{ requireNamedSelectors: true }],
errors: [
{
messageId: "useSelectorFunction",
data: { storeName: "useStore" },
},
],
},
// ============================================
// CUSTOM PATTERNS
// ============================================
{
name: "custom pattern match without selector",
code: `const value = useGlobalData();`,
options: [{ storePattern: "^useGlobal\\w*$" }],
errors: [
{
messageId: "missingSelector",
data: { storeName: "useGlobalData" },
},
],
},
// ============================================
// MIXED VALID AND INVALID
// ============================================
{
name: "mix of valid and invalid store calls",
code: `
const count = useStore((s) => s.count); // valid
const user = useAuthStore(); // invalid
const cart = useCartStore((s) => s.items); // valid
const ui = useUIStore(); // invalid
`,
errors: [
{
messageId: "missingSelector",
data: { storeName: "useAuthStore" },
},
{
messageId: "missingSelector",
data: { storeName: "useUIStore" },
},
],
},
// ============================================
// EDGE CASES
// ============================================
{
name: "store in conditional",
code: `const data = condition ? useStore() : null;`,
errors: [
{
messageId: "missingSelector",
data: { storeName: "useStore" },
},
],
},
{
name: "store in object property",
code: `const obj = { data: useDataStore() };`,
errors: [
{
messageId: "missingSelector",
data: { storeName: "useDataStore" },
},
],
},
{
name: "store as function argument",
code: `someFunction(useStore());`,
errors: [
{
messageId: "missingSelector",
data: { storeName: "useStore" },
},
],
},
],
});