import { parseSync } from "@babel/core"; import * as t from "@babel/types"; import { describe, expect, it } from "vitest"; import { getComponentModifierSupport, getStatePropertyForModifier } from "./componentSupport"; /** * Helper to create a JSXOpeningElement from JSX code */ function createJSXElement(code: string): t.JSXOpeningElement { const ast = parseSync(code, { sourceType: "module", plugins: [["@babel/plugin-syntax-jsx", {}]], filename: "test.tsx", configFile: false, babelrc: false, }); if (!ast) { throw new Error(`Failed to parse: ${code}`); } // Find the JSXOpeningElement in the AST let element: t.JSXOpeningElement | null = null; const traverse = (node: t.Node) => { if (t.isJSXOpeningElement(node)) { element = node; return; } for (const key in node) { // @ts-expect-error - Dynamic key access if (node[key] && typeof node[key] === "object") { // @ts-expect-error - Dynamic key access traverse(node[key] as t.Node); } } }; traverse(ast); if (!element) { throw new Error(`Could not find JSXOpeningElement in: ${code}`); } return element; } describe("getComponentModifierSupport", () => { describe("Supported components", () => { it("should recognize Pressable component", () => { const element = createJSXElement(""); const result = getComponentModifierSupport(element, t); expect(result).toEqual({ component: "Pressable", supportedModifiers: ["active", "hover", "focus", "disabled"], }); }); it("should recognize TextInput component", () => { const element = createJSXElement(""); const result = getComponentModifierSupport(element, t); expect(result).toEqual({ component: "TextInput", supportedModifiers: ["focus", "disabled", "placeholder"], }); }); it("should recognize Pressable with attributes", () => { const element = createJSXElement(''); const result = getComponentModifierSupport(element, t); expect(result).toEqual({ component: "Pressable", supportedModifiers: ["active", "hover", "focus", "disabled"], }); }); it("should recognize TextInput with attributes", () => { const element = createJSXElement(''); const result = getComponentModifierSupport(element, t); expect(result).toEqual({ component: "TextInput", supportedModifiers: ["focus", "disabled", "placeholder"], }); }); it("should recognize TouchableOpacity component", () => { const element = createJSXElement(""); const result = getComponentModifierSupport(element, t); expect(result).toEqual({ component: "TouchableOpacity", supportedModifiers: ["active", "disabled"], }); }); it("should recognize TouchableOpacity with attributes", () => { const element = createJSXElement(''); const result = getComponentModifierSupport(element, t); expect(result).toEqual({ component: "TouchableOpacity", supportedModifiers: ["active", "disabled"], }); }); }); describe("Member expressions", () => { it("should recognize ReactNative.Pressable", () => { const element = createJSXElement(""); const result = getComponentModifierSupport(element, t); expect(result).toEqual({ component: "Pressable", supportedModifiers: ["active", "hover", "focus", "disabled"], }); }); it("should recognize RN.TextInput", () => { const element = createJSXElement(""); const result = getComponentModifierSupport(element, t); expect(result).toEqual({ component: "TextInput", supportedModifiers: ["focus", "disabled", "placeholder"], }); }); it("should recognize nested member expressions", () => { const element = createJSXElement(""); const result = getComponentModifierSupport(element, t); // Should extract "TextInput" from the rightmost property expect(result).toEqual({ component: "TextInput", supportedModifiers: ["focus", "disabled", "placeholder"], }); }); it("should recognize Pressable in namespaced imports", () => { const element = createJSXElement(""); const result = getComponentModifierSupport(element, t); expect(result).toEqual({ component: "Pressable", supportedModifiers: ["active", "hover", "focus", "disabled"], }); }); }); describe("Unsupported components", () => { it("should return null for View", () => { const element = createJSXElement(""); const result = getComponentModifierSupport(element, t); expect(result).toBeNull(); }); it("should return null for custom components", () => { const element = createJSXElement(""); const result = getComponentModifierSupport(element, t); expect(result).toBeNull(); }); it("should return null for Text", () => { const element = createJSXElement(""); const result = getComponentModifierSupport(element, t); expect(result).toBeNull(); }); it("should return null for Image", () => { const element = createJSXElement(""); const result = getComponentModifierSupport(element, t); expect(result).toBeNull(); }); }); describe("Edge cases", () => { it("should be case-sensitive", () => { // lowercase "pressable" should not match const element = createJSXElement(""); const result = getComponentModifierSupport(element, t); expect(result).toBeNull(); }); it("should not match similar names", () => { const element1 = createJSXElement(""); const result1 = getComponentModifierSupport(element1, t); expect(result1).toBeNull(); const element2 = createJSXElement(""); const result2 = getComponentModifierSupport(element2, t); expect(result2).toBeNull(); const element3 = createJSXElement(""); const result3 = getComponentModifierSupport(element3, t); expect(result3).toBeNull(); }); it("should handle self-closing tags", () => { const element = createJSXElement(""); const result = getComponentModifierSupport(element, t); expect(result).not.toBeNull(); expect(result?.component).toBe("Pressable"); }); it("should return null for non-JSXOpeningElement nodes", () => { // Test with a random node type const identifier = t.identifier("foo"); const result = getComponentModifierSupport(identifier, t); expect(result).toBeNull(); }); it("should return null for JSXFragment", () => { // JSXFragment doesn't have a JSXOpeningElement, so create a mock fragment const fragment = { type: "JSXFragment", openingFragment: {}, closingFragment: {}, children: [], }; const result = getComponentModifierSupport(fragment as t.Node, t); expect(result).toBeNull(); }); }); describe("Modifier support differences", () => { it("should show Pressable supports active modifier but TextInput does not", () => { const pressable = createJSXElement(""); const textInput = createJSXElement(""); const pressableResult = getComponentModifierSupport(pressable, t); const textInputResult = getComponentModifierSupport(textInput, t); expect(pressableResult?.supportedModifiers).toContain("active"); expect(textInputResult?.supportedModifiers).not.toContain("active"); }); it("should show both support focus modifier", () => { const pressable = createJSXElement(""); const textInput = createJSXElement(""); const pressableResult = getComponentModifierSupport(pressable, t); const textInputResult = getComponentModifierSupport(textInput, t); expect(pressableResult?.supportedModifiers).toContain("focus"); expect(textInputResult?.supportedModifiers).toContain("focus"); }); it("should show TextInput supports placeholder but Pressable does not", () => { const pressable = createJSXElement(""); const textInput = createJSXElement(""); const pressableResult = getComponentModifierSupport(pressable, t); const textInputResult = getComponentModifierSupport(textInput, t); expect(pressableResult?.supportedModifiers).not.toContain("placeholder"); expect(textInputResult?.supportedModifiers).toContain("placeholder"); }); it("should show both support disabled modifier", () => { const pressable = createJSXElement(""); const textInput = createJSXElement(""); const pressableResult = getComponentModifierSupport(pressable, t); const textInputResult = getComponentModifierSupport(textInput, t); expect(pressableResult?.supportedModifiers).toContain("disabled"); expect(textInputResult?.supportedModifiers).toContain("disabled"); }); }); }); describe("getStatePropertyForModifier", () => { it("should map active to pressed", () => { expect(getStatePropertyForModifier("active")).toBe("pressed"); }); it("should map hover to hovered", () => { expect(getStatePropertyForModifier("hover")).toBe("hovered"); }); it("should map focus to focused", () => { expect(getStatePropertyForModifier("focus")).toBe("focused"); }); it("should map disabled to disabled", () => { expect(getStatePropertyForModifier("disabled")).toBe("disabled"); }); it("should return pressed as fallback for unknown modifiers", () => { // @ts-expect-error - Testing fallback with invalid modifier expect(getStatePropertyForModifier("unknown")).toBe("pressed"); // @ts-expect-error - Testing fallback with invalid modifier expect(getStatePropertyForModifier("invalid")).toBe("pressed"); // @ts-expect-error - Testing fallback with invalid modifier expect(getStatePropertyForModifier("")).toBe("pressed"); }); it("should handle all Pressable modifier states", () => { // Pressable supports: active, hover, focus, disabled const pressableModifiers: Array<"active" | "hover" | "focus" | "disabled"> = [ "active", "hover", "focus", "disabled", ]; const expectedMapping = { active: "pressed", hover: "hovered", focus: "focused", disabled: "disabled", }; for (const modifier of pressableModifiers) { expect(getStatePropertyForModifier(modifier)).toBe(expectedMapping[modifier]); } }); it("should handle all TextInput modifier states", () => { // TextInput supports: focus, disabled, placeholder // Note: placeholder doesn't have a state property (it's a prop, not state) const textInputModifiers: Array<"focus" | "disabled"> = ["focus", "disabled"]; const expectedMapping = { focus: "focused", disabled: "disabled", }; for (const modifier of textInputModifiers) { expect(getStatePropertyForModifier(modifier)).toBe(expectedMapping[modifier]); } }); }); describe("Integration - Real-world scenarios", () => { it("should correctly identify modifiers for a Pressable button", () => { const element = createJSXElement( '', ); const result = getComponentModifierSupport(element, t); expect(result).not.toBeNull(); expect(result?.component).toBe("Pressable"); // Verify all used modifiers are supported expect(result?.supportedModifiers).toContain("active"); expect(result?.supportedModifiers).toContain("hover"); expect(result?.supportedModifiers).toContain("disabled"); }); it("should correctly identify modifiers for a TextInput field", () => { const element = createJSXElement( '', ); const result = getComponentModifierSupport(element, t); expect(result).not.toBeNull(); expect(result?.component).toBe("TextInput"); // Verify all used modifiers are supported expect(result?.supportedModifiers).toContain("focus"); expect(result?.supportedModifiers).toContain("disabled"); expect(result?.supportedModifiers).toContain("placeholder"); }); it("should handle namespaced components from imports", () => { const element = createJSXElement(''); const result = getComponentModifierSupport(element, t); expect(result).not.toBeNull(); expect(result?.component).toBe("Pressable"); expect(result?.supportedModifiers).toContain("active"); }); it("should return null for unsupported components with modifiers", () => { const element = createJSXElement(''); const result = getComponentModifierSupport(element, t); // View doesn't support modifiers expect(result).toBeNull(); }); it("should map all Pressable modifiers to correct state properties", () => { const element = createJSXElement(""); const result = getComponentModifierSupport(element, t); expect(result).not.toBeNull(); // Test each supported modifier maps correctly const modifiers = result?.supportedModifiers as Array<"active" | "hover" | "focus" | "disabled">; for (const modifier of modifiers) { const stateProp = getStatePropertyForModifier(modifier); expect(stateProp).toBeTruthy(); expect(typeof stateProp).toBe("string"); } // Verify the mappings expect(getStatePropertyForModifier("active")).toBe("pressed"); expect(getStatePropertyForModifier("hover")).toBe("hovered"); expect(getStatePropertyForModifier("focus")).toBe("focused"); expect(getStatePropertyForModifier("disabled")).toBe("disabled"); }); it("should map all TextInput modifiers to correct state properties", () => { const element = createJSXElement(""); const result = getComponentModifierSupport(element, t); expect(result).not.toBeNull(); // Filter out placeholder as it doesn't have a state property const stateModifiers = result?.supportedModifiers.filter((m) => m !== "placeholder") as Array< "focus" | "disabled" >; for (const modifier of stateModifiers) { const stateProp = getStatePropertyForModifier(modifier); expect(stateProp).toBeTruthy(); expect(typeof stateProp).toBe("string"); } // Verify the mappings expect(getStatePropertyForModifier("focus")).toBe("focused"); expect(getStatePropertyForModifier("disabled")).toBe("disabled"); }); });