Skip to main content
Glama
react-parser.test.ts10.7 kB
import { describe, it, expect } from "vitest"; // Test the React prop extraction patterns directly describe("React Parser - extractConstArrays", () => { function extractConstArrays(content: string): Map<string, string[]> { const constArrays = new Map<string, string[]>(); const constRegex = /const\s+(\w+)\s*=\s*\[([^\]]+)\]\s*as\s+const/g; let match; while ((match = constRegex.exec(content)) !== null) { const name = match[1]; const values = match[2] .split(",") .map((s) => s.trim().replace(/['"]/g, "")) .filter((s) => s.length > 0); constArrays.set(name, values); } return constArrays; } it("should extract const arrays", () => { const content = ` export const sizes = ['s', 'm', 'l'] as const; export const themes = ['primary', 'secondary', 'danger'] as const; `; const arrays = extractConstArrays(content); expect(arrays.get("sizes")).toEqual(["s", "m", "l"]); expect(arrays.get("themes")).toEqual(["primary", "secondary", "danger"]); }); it("should handle double quotes", () => { const content = `export const variants = ["solid", "bordered", "ghost"] as const;`; const arrays = extractConstArrays(content); expect(arrays.get("variants")).toEqual(["solid", "bordered", "ghost"]); }); }); describe("React Parser - extractPropsFromInterface", () => { function extractPropsFromInterface( content: string, interfaceName: string, constArrays: Map<string, string[]> ) { const props: Array<{ name: string; type: string; required: boolean; options?: string[]; }> = []; const seenProps = new Set<string>(); const interfaceRegex = new RegExp( `interface\\s+${interfaceName}\\s*(?:extends\\s+([^{]+))?\\s*\\{([\\s\\S]*?)\\}`, "g" ); const match = interfaceRegex.exec(content); if (!match) return props; const extendsClause = match[1]; const interfaceBody = match[2]; // Recursively get props from extended interfaces if (extendsClause) { const extendedInterfaces = extendsClause .split(",") .map((s) => s.trim().replace(/<[^>]+>/, "")) .filter((s) => s.length > 0 && !s.startsWith("Omit") && !s.includes("HTMLAttributes")); for (const extInterface of extendedInterfaces) { const extProps = extractPropsFromInterface(content, extInterface, constArrays); for (const prop of extProps) { if (!seenProps.has(prop.name)) { seenProps.add(prop.name); props.push(prop); } } } } const propRegex = /(\w+)(\?)?:\s*([^;\n]+)/g; let propMatch; while ((propMatch = propRegex.exec(interfaceBody)) !== null) { const propName = propMatch[1]; const isOptional = !!propMatch[2]; const propType = propMatch[3].trim(); if (seenProps.has(propName)) continue; if (propType.includes("=>") || propName.startsWith("on")) continue; if (propName === "children" || propName === "className") continue; seenProps.add(propName); let options: string[] | undefined; const typeRefMatch = propType.match(/^T(\w+)$/); if (typeRefMatch) { const typeName = typeRefMatch[1].toLowerCase(); for (const [constName, values] of constArrays) { const constNameLower = constName.toLowerCase(); // Match if type contains const name or const name contains type suffix // e.g., "buttonvariant" should match "variant" or "variants" if ( typeName.includes(constNameLower) || typeName.includes(constNameLower.replace(/s$/, "")) ) { options = values; break; } } } if (propType.includes("|") && propType.includes("'")) { options = propType .split("|") .map((s) => s.trim().replace(/['"]/g, "")) .filter((s) => s && s !== "undefined"); } props.push({ name: propName, type: propType, required: !isOptional, options: options && options.length > 1 ? options : undefined, }); } return props; } it("should extract props from simple interface", () => { const content = ` interface IButtonProps { variant?: TButtonVariant; size?: TButtonSize; isDisabled?: boolean; } `; const constArrays = new Map<string, string[]>(); constArrays.set("variants", ["solid", "bordered"]); constArrays.set("sizes", ["s", "m", "l"]); const props = extractPropsFromInterface(content, "IButtonProps", constArrays); expect(props).toHaveLength(3); expect(props[0].name).toBe("variant"); expect(props[0].options).toEqual(["solid", "bordered"]); expect(props[1].name).toBe("size"); expect(props[1].options).toEqual(["s", "m", "l"]); expect(props[2].name).toBe("isDisabled"); expect(props[2].type).toBe("boolean"); }); it("should handle interface inheritance", () => { const content = ` interface IBaseProps { id?: string; testId?: string; } interface IButtonProps extends IBaseProps { variant?: string; size?: string; } `; const props = extractPropsFromInterface(content, "IButtonProps", new Map()); expect(props).toHaveLength(4); expect(props.map((p) => p.name)).toContain("id"); expect(props.map((p) => p.name)).toContain("testId"); expect(props.map((p) => p.name)).toContain("variant"); expect(props.map((p) => p.name)).toContain("size"); }); it("should extract inline union types as options", () => { const content = ` interface IButtonProps { theme?: 'primary' | 'secondary' | 'danger'; size?: 's' | 'm' | 'l'; } `; const props = extractPropsFromInterface(content, "IButtonProps", new Map()); expect(props[0].options).toEqual(["primary", "secondary", "danger"]); expect(props[1].options).toEqual(["s", "m", "l"]); }); it("should skip callback props and children", () => { const content = ` interface IButtonProps { onClick?: () => void; onHover?: (event: MouseEvent) => void; children?: ReactNode; className?: string; label?: string; } `; const props = extractPropsFromInterface(content, "IButtonProps", new Map()); expect(props).toHaveLength(1); expect(props[0].name).toBe("label"); }); }); describe("React Parser - extractCallbacks", () => { function extractCallbacks(content: string) { const events: Array<{ name: string; description?: string }> = []; const seenEvents = new Set<string>(); const callbackPatterns = [ /(\bon[A-Z]\w*)\??:\s*\([^)]*\)\s*=>\s*\w+/g, /(\bon[A-Z]\w*)\??:\s*\w*EventHandler/g, ]; for (const pattern of callbackPatterns) { let match; while ((match = pattern.exec(content)) !== null) { const eventName = match[1]; if (!seenEvents.has(eventName)) { seenEvents.add(eventName); events.push({ name: eventName, description: `${eventName.replace(/^on/, "")} event callback`, }); } } } return events; } it("should extract callback props", () => { const content = ` interface IButtonProps { onClick?: () => void; onHover?: (event: MouseEvent) => void; onChange?: (value: string) => void; } `; const callbacks = extractCallbacks(content); expect(callbacks).toHaveLength(3); expect(callbacks.map((c) => c.name)).toContain("onClick"); expect(callbacks.map((c) => c.name)).toContain("onHover"); expect(callbacks.map((c) => c.name)).toContain("onChange"); }); it("should extract EventHandler types", () => { const content = ` interface IInputProps { onFocus?: FocusEventHandler; onBlur?: FocusEventHandler; } `; const callbacks = extractCallbacks(content); expect(callbacks).toHaveLength(2); expect(callbacks.map((c) => c.name)).toContain("onFocus"); expect(callbacks.map((c) => c.name)).toContain("onBlur"); }); }); describe("React Parser - hasChildrenProp", () => { function hasChildrenProp(content: string): boolean { return ( content.includes("children") || content.includes("PropsWithChildren") || content.includes("ReactNode") ); } it("should detect children prop", () => { expect(hasChildrenProp("interface Props { children: ReactNode }")).toBe(true); expect(hasChildrenProp("type Props = PropsWithChildren<{}>")).toBe(true); expect(hasChildrenProp("interface Props { label: string }")).toBe(false); }); }); describe("React Parser - inferCategory", () => { function inferCategory(componentName: string): string { const name = componentName.toLowerCase(); if (["button", "link", "optionbutton"].some((n) => name.includes(n))) return "action"; if ( ["input", "select", "checkbox", "radio", "toggle", "textarea", "field"].some((n) => name.includes(n) ) ) return "form"; if (["accordion", "breadcrumb", "menu", "pagination", "tabs"].some((n) => name.includes(n))) return "navigation"; if ( ["badge", "flag", "loader", "modal", "notification", "progress", "tooltip"].some((n) => name.includes(n) ) ) return "feedback"; if (["card", "divider", "layer"].some((n) => name.includes(n))) return "layout"; if (["table", "heading", "hero", "listbox", "rating", "tag"].some((n) => name.includes(n))) return "data-display"; return "other"; } it("should categorize action components", () => { expect(inferCategory("Button")).toBe("action"); expect(inferCategory("Link")).toBe("action"); expect(inferCategory("OptionButton")).toBe("action"); }); it("should categorize form components", () => { expect(inferCategory("Input")).toBe("form"); expect(inferCategory("TextInput")).toBe("form"); expect(inferCategory("Checkbox")).toBe("form"); expect(inferCategory("Select")).toBe("form"); expect(inferCategory("TextField")).toBe("form"); }); it("should categorize navigation components", () => { expect(inferCategory("Breadcrumb")).toBe("navigation"); expect(inferCategory("Menu")).toBe("navigation"); expect(inferCategory("Tabs")).toBe("navigation"); }); it("should categorize feedback components", () => { expect(inferCategory("Modal")).toBe("feedback"); expect(inferCategory("Notification")).toBe("feedback"); expect(inferCategory("Loader")).toBe("feedback"); expect(inferCategory("ProgressBar")).toBe("feedback"); }); it("should return other for unknown components", () => { expect(inferCategory("CustomComponent")).toBe("other"); expect(inferCategory("MyWidget")).toBe("other"); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/MerzoukeMansouri/adeo-mozaic-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server