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 = ``;
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');
});
});
});