import * as babel from '@babel/core'; import * as path from 'path'; // Import the plugin const plugin = require('../web.js'); /** * Helper to transform code and extract detected icon names */ function transform(code: string, options = {}, filename = 'test.tsx') { const result = babel.transformSync(code, { filename, presets: [ ['@babel/preset-react', { runtime: 'automatic' }], '@babel/preset-typescript', ], plugins: [[plugin, options]], babelrc: false, configFile: false, }); return result; } /** * Extract icon names that were detected by checking the generated code */ function getDetectedIcons(transformedCode: string | null | undefined): string[] { if (!transformedCode) return []; // Look for the registerMany call and extract icon names // Pattern: IconRegistry.registerMany({ "icon-name": _mdiIconName, ... }) // Note: Babel outputs double quotes const match = transformedCode.match(/registerMany\(\{([^}]+)\}\)/); if (!match) return []; // Match both single and double quoted strings const iconMatches = match[1].matchAll(/["']([^"']+)["']:/g); return Array.from(iconMatches).map(m => m[1]); } describe('MDI Icon Registry Babel Plugin', () => { describe('JSX String Literal Detection', () => { it('detects icon name in Icon component', () => { const code = ``; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toContain('home'); }); it('detects icon name in IconSvg component', () => { const code = ``; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toContain('account'); }); it('detects icon name with mdi: prefix', () => { const code = ``; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toContain('home'); }); it('detects icon in JSX expression container', () => { const code = ``; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toContain('cog'); }); it('detects kebab-case icon names', () => { const code = ``; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toContain('account-circle'); }); }); describe('Button Component Icon Props', () => { it('detects leftIcon prop', () => { const code = ``; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toContain('chevron-left'); }); it('detects rightIcon prop', () => { const code = ``; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toContain('chevron-right'); }); it('detects both leftIcon and rightIcon', () => { const code = ``; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toContain('arrow-left'); expect(icons).toContain('arrow-right'); }); }); describe('Input Component Icon Props', () => { it('detects leftIcon in Input', () => { const code = ``; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toContain('magnify'); }); it('detects rightIcon in Input', () => { const code = ``; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toContain('close'); }); }); describe('Other Components with Icon Props', () => { it('detects icon in Badge', () => { const code = ``; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toContain('check'); }); it('detects icon in Alert', () => { const code = ``; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toContain('alert-circle'); }); it('detects icon and deleteIcon in Chip', () => { const code = ``; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toContain('tag'); expect(icons).toContain('close-circle'); }); it('detects icon in MenuItem', () => { const code = `Settings`; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toContain('cog'); }); it('detects leading and trailing in ListItem', () => { const code = ``; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toContain('account'); expect(icons).toContain('chevron-right'); }); }); describe('Ternary Expression Detection', () => { it('detects both icons in ternary expression in props', () => { const code = ``; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toContain('eye'); expect(icons).toContain('eye-off'); }); it('detects icons in ternary with different conditions', () => { const code = ``; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toContain('alert'); expect(icons).toContain('check'); }); }); describe('Logical Expression Detection', () => { it('detects icon in logical AND expression', () => { const code = ``; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toContain('star'); }); it('detects icon in logical OR expression', () => { const code = ``; const result = transform(code); const icons = getDetectedIcons(result?.code); // This tests the right side of OR expect(icons).toContain('star'); }); }); describe('Variable Reference Detection', () => { it('detects icon from variable with string literal', () => { const code = ` const iconName = "home"; `; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toContain('home'); }); it('detects both icons from variable with ternary expression', () => { const code = ` const iconName = isPasswordVisible ? 'eye-off' : 'eye'; `; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toContain('eye'); expect(icons).toContain('eye-off'); }); it('detects icon from variable with logical expression', () => { const code = ` const iconName = showIcon && 'star'; `; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toContain('star'); }); it('detects icons from multiple variable assignments', () => { const code = ` const icon1 = "home"; const icon2 = isActive ? "check" : "close"; <> `; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toContain('home'); expect(icons).toContain('check'); expect(icons).toContain('close'); }); }); describe('Object Property Detection', () => { it('detects icon in object literal', () => { const code = ` const menuItem = { icon: "home", label: "Home" }; `; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toContain('home'); }); it('detects leftIcon and rightIcon in object', () => { const code = ` const buttonConfig = { leftIcon: "arrow-left", rightIcon: "arrow-right", }; `; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toContain('arrow-left'); expect(icons).toContain('arrow-right'); }); it('detects leading and trailing in object', () => { const code = ` const listItemConfig = { leading: "account", trailing: "chevron-right", }; `; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toContain('account'); expect(icons).toContain('chevron-right'); }); it('detects icons in array of objects', () => { const code = ` const menuItems = [ { icon: "home", label: "Home" }, { icon: "cog", label: "Settings" }, { icon: "account", label: "Profile" }, ]; `; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toContain('home'); expect(icons).toContain('cog'); expect(icons).toContain('account'); }); }); describe('Config Icons Option (removed)', () => { it('does not inject icons from legacy icons option', () => { const code = `const x = 1;`; const result = transform(code, { icons: ['star', 'heart'] }); const icons = getDetectedIcons(result?.code); // icons option no longer injects — use IconRegistry.register() manually instead expect(icons).toEqual([]); }); }); describe('Multiple Icons in Same File', () => { it('deduplicates repeated icon names', () => { const code = ` <> `; const result = transform(code); const icons = getDetectedIcons(result?.code); // Should only appear once expect(icons.filter(i => i === 'home')).toHaveLength(1); }); it('collects all unique icons from complex component', () => { const code = ` function MyComponent({ isEditing }) { const statusIcon = isEditing ? "pencil" : "check"; return (
); } `; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toContain('arrow-left'); expect(icons).toContain('arrow-right'); expect(icons).toContain('magnify'); expect(icons).toContain('close'); expect(icons).toContain('pencil'); expect(icons).toContain('check'); expect(icons).toContain('information'); }); }); describe('Cases That Should NOT Be Detected', () => { it('does not detect icons for non-icon components', () => { const code = `
`; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).not.toContain('home'); }); it('does not detect non-icon props', () => { const code = ``; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toHaveLength(0); }); it('does not detect icons with invalid characters', () => { const code = ``; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).not.toContain('home@invalid'); }); it('does not detect dynamic template literals', () => { const code = ''; const result = transform(code); const icons = getDetectedIcons(result?.code); // Dynamic template literals cannot be statically analyzed expect(icons).toHaveLength(0); }); it('does not detect icons from function calls', () => { const code = ``; const result = transform(code); const icons = getDetectedIcons(result?.code); // Function return values cannot be statically analyzed expect(icons).toHaveLength(0); }); it('does not detect icons from complex expressions', () => { const code = ``; const result = transform(code); const icons = getDetectedIcons(result?.code); // Array access cannot be statically analyzed expect(icons).toHaveLength(0); }); it('does not detect icons from object member access', () => { const code = ``; const result = transform(code); const icons = getDetectedIcons(result?.code); // Object property access cannot be statically analyzed expect(icons).toHaveLength(0); }); it('does not detect non-icon object properties', () => { const code = ` const config = { title: "home", description: "star", }; `; const result = transform(code); const icons = getDetectedIcons(result?.code); // title and description are not icon props expect(icons).not.toContain('home'); expect(icons).not.toContain('star'); }); it('does not detect text children as icon names', () => { const code = `eye`; const result = transform(code); const icons = getDetectedIcons(result?.code); // Text content should not be detected as icons expect(icons).not.toContain('eye'); }); it('does not detect icon-like strings in other props', () => { const code = ` `; const result = transform(code); const icons = getDetectedIcons(result?.code); // data-name and aria-label are not icon props expect(icons).not.toContain('home'); expect(icons).not.toContain('eye'); }); it('does not detect icon names in className or style props', () => { const code = `
Content
`; const result = transform(code); const icons = getDetectedIcons(result?.code); // className values and data attributes should not be detected expect(icons).not.toContain('home'); expect(icons).not.toContain('account'); // Note: style.icon might be detected by ObjectProperty visitor - that's intentional }); it('does not detect string literals in JSX text content', () => { const code = ` The eye icon shows visibility Click home to go back `; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).not.toContain('eye'); expect(icons).not.toContain('home'); }); it('does not detect icons from string concatenation', () => { const code = ``; const result = transform(code); const icons = getDetectedIcons(result?.code); // Binary expression (string concat) cannot be statically analyzed expect(icons).not.toContain('home'); expect(icons).not.toContain('home-outline'); }); it('does not inject code when no icons are found', () => { const code = `const x = 1 + 2;`; const result = transform(code); // Should not have IconRegistry import or registerMany call expect(result?.code).not.toContain('IconRegistry'); expect(result?.code).not.toContain('registerMany'); expect(result?.code).not.toContain('@mdi/js'); }); it('does not detect invalid icon names that are not in @mdi/js', () => { const code = ``; const result = transform(code); const icons = getDetectedIcons(result?.code); // Invalid icon names should be filtered out during validation expect(icons).not.toContain('definitely-not-a-real-mdi-icon-xyz123'); }); }); describe('Edge Cases', () => { it('handles empty icon name gracefully', () => { const code = ``; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toHaveLength(0); }); it('handles null-like values gracefully', () => { const code = ``; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toHaveLength(0); }); it('handles spread props without crashing', () => { const code = ``; const result = transform(code); // Should not crash, just not detect any icons expect(result?.code).toBeDefined(); }); it('handles nested ternary expressions', () => { const code = ``; const result = transform(code); const icons = getDetectedIcons(result?.code); // Should at least detect the top-level branches expect(icons).toContain('home'); // Nested ternary may or may not be fully detected depending on implementation }); it('preserves original code functionality', () => { const code = ` const MyComponent = () => { const [visible, setVisible] = useState(false); return setVisible(!visible)} />; }; `; const result = transform(code); // Original code should still be present expect(result?.code).toContain('useState'); expect(result?.code).toContain('setVisible'); }); }); describe('Custom Components via `components` Option', () => { it('detects icon in custom component with single icon prop', () => { const code = ``; const result = transform(code, { components: { NavItem: ['icon'] } }); const icons = getDetectedIcons(result?.code); expect(icons).toContain('home'); }); it('detects icons in custom component with multiple icon props', () => { const code = ``; const result = transform(code, { components: { SidebarLink: ['icon', 'activeIcon'] } }); const icons = getDetectedIcons(result?.code); expect(icons).toContain('cog'); expect(icons).toContain('cog-outline'); }); it('detects ternary icon values in custom components', () => { const code = ``; const result = transform(code, { components: { NavItem: ['icon'] } }); const icons = getDetectedIcons(result?.code); expect(icons).toContain('home'); expect(icons).toContain('home-outline'); }); it('detects variable-referenced icons in custom components', () => { const code = ` const navIcon = "account"; ; `; const result = transform(code, { components: { NavItem: ['icon'] } }); const icons = getDetectedIcons(result?.code); expect(icons).toContain('account'); }); it('detects icons in object properties using custom prop names', () => { const code = ` const items = [ { activeIcon: "home", label: "Home" }, { activeIcon: "cog", label: "Settings" }, ]; `; const result = transform(code, { components: { NavItem: ['activeIcon'] } }); const icons = getDetectedIcons(result?.code); expect(icons).toContain('home'); expect(icons).toContain('cog'); }); it('works alongside built-in components', () => { const code = ` <> `; const result = transform(code, { components: { NavItem: ['icon'] } }); const icons = getDetectedIcons(result?.code); expect(icons).toContain('star'); expect(icons).toContain('home'); }); it('does not detect custom prop names without config', () => { // 'activeIcon' is not a built-in icon prop name, so without config it won't be detected const code = ``; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).not.toContain('home'); }); }); describe('Real-World Input Password Toggle Pattern', () => { it('detects eye icons from password toggle pattern', () => { // This is the actual pattern used in Input.web.tsx const code = ` const renderPasswordToggleIcon = () => { const iconName = isPasswordVisible ? 'eye-off' : 'eye'; return ( ); }; `; const result = transform(code); const icons = getDetectedIcons(result?.code); expect(icons).toContain('eye'); expect(icons).toContain('eye-off'); }); }); });