mttwm
Version:
Automated CSS-in-JS to Tailwind CSS migration tool for React applications
594 lines (494 loc) • 19.2 kB
text/typescript
import { BreakpointMapper } from '../../src/mappings/breakpoint-mapper.js';
import type { CSSProperties, BreakpointMapping } from '../../src/types.js';
describe('BreakpointMapper', () => {
let breakpointMapper: BreakpointMapper;
beforeEach(() => {
breakpointMapper = new BreakpointMapper();
});
describe('constructor', () => {
it('should initialize with default breakpoints', () => {
const mapper = new BreakpointMapper();
expect(mapper).toBeInstanceOf(BreakpointMapper);
});
it('should merge custom breakpoints with defaults', () => {
const customBreakpoints: Partial<BreakpointMapping> = {
xxl: 2560,
};
const mapper = new BreakpointMapper(customBreakpoints);
expect(mapper).toBeInstanceOf(BreakpointMapper);
});
});
describe('isBreakpoint', () => {
it('should identify breakpoint keys correctly', () => {
expect(breakpointMapper.isBreakpoint('[theme.breakpoints.up(\'md\')]')).toBe(true);
expect(breakpointMapper.isBreakpoint('[theme.breakpoints.down(\'lg\')]')).toBe(true);
expect(breakpointMapper.isBreakpoint('[theme.breakpoints.between(\'sm\', \'xl\')]')).toBe(true);
});
it('should not identify non-breakpoint keys', () => {
expect(breakpointMapper.isBreakpoint('display')).toBe(false);
expect(breakpointMapper.isBreakpoint('margin')).toBe(false);
expect(breakpointMapper.isBreakpoint('color')).toBe(false);
expect(breakpointMapper.isBreakpoint('breakpoint')).toBe(false);
});
});
describe('parseBreakpointKey', () => {
it('should parse up() breakpoints correctly', () => {
const testCases = [
"[theme.breakpoints.up('md')]",
'[theme.breakpoints.up("md")]',
"[theme.breakpoints.up('sm')]",
"[theme.breakpoints.up('lg')]",
];
testCases.forEach((key) => {
const result = breakpointMapper['parseBreakpointKey'](key);
expect(result).toEqual({
direction: 'up',
breakpoint: expect.any(String),
});
});
});
it('should parse down() breakpoints correctly', () => {
const testCases = [
"[theme.breakpoints.down('md')]",
'[theme.breakpoints.down("lg")]',
"[theme.breakpoints.down('sm')]",
];
testCases.forEach((key) => {
const result = breakpointMapper['parseBreakpointKey'](key);
expect(result).toEqual({
direction: 'down',
breakpoint: expect.any(String),
});
});
});
it('should parse between() breakpoints correctly', () => {
const result = breakpointMapper['parseBreakpointKey']("[theme.breakpoints.between('sm', 'lg')]");
expect(result).toEqual({
direction: 'between',
breakpoint: 'sm',
endBreakpoint: 'lg',
});
});
it('should handle different quote styles', () => {
const singleQuote = breakpointMapper['parseBreakpointKey']("[theme.breakpoints.up('md')]");
const doubleQuote = breakpointMapper['parseBreakpointKey']('[theme.breakpoints.up("md")]');
expect(singleQuote).toEqual({ direction: 'up', breakpoint: 'md' });
expect(doubleQuote).toEqual({ direction: 'up', breakpoint: 'md' });
});
it('should return null for invalid breakpoint keys', () => {
const invalidKeys = [
'invalid-key',
'[theme.breakpoints.invalid(\'md\')]',
'[breakpoints.up(\'md\')]',
'theme.breakpoints.up(\'md\')',
];
invalidKeys.forEach((key) => {
const result = breakpointMapper['parseBreakpointKey'](key);
expect(result).toBeNull();
});
});
});
describe('mapBreakpointToTailwind', () => {
it('should map Material-UI breakpoints to Tailwind correctly', () => {
const mappings = [
{ mui: 'xs', tailwind: 'sm' },
{ mui: 'sm', tailwind: 'sm' },
{ mui: 'md', tailwind: 'md' },
{ mui: 'lg', tailwind: 'lg' },
{ mui: 'xl', tailwind: 'xl' },
];
mappings.forEach(({ mui, tailwind }) => {
const result = breakpointMapper['mapBreakpointToTailwind'](mui);
expect(result).toBe(tailwind);
});
});
it('should return original breakpoint for unmapped values', () => {
const result = breakpointMapper['mapBreakpointToTailwind']('xxl');
expect(result).toBe('xxl');
});
});
describe('createResponsiveClasses', () => {
it('should create up() responsive classes', () => {
const result = breakpointMapper['createResponsiveClasses'](
'up',
'md',
undefined,
['flex', 'p-4']
);
expect(result).toEqual(['md:flex', 'md:p-4']);
});
it('should create down() responsive classes', () => {
const result = breakpointMapper['createResponsiveClasses'](
'down',
'md',
undefined,
['hidden', 'text-sm']
);
expect(result).toEqual(['max-md:hidden', 'max-md:text-sm']);
});
it('should create between() responsive classes', () => {
const result = breakpointMapper['createResponsiveClasses'](
'between',
'sm',
'lg',
['block', 'font-bold']
);
expect(result).toEqual(['sm:max-lg:block', 'sm:max-lg:font-bold']);
});
it('should handle empty base classes', () => {
const result = breakpointMapper['createResponsiveClasses'](
'up',
'md',
undefined,
[]
);
expect(result).toEqual([]);
});
it('should handle single base class', () => {
const result = breakpointMapper['createResponsiveClasses'](
'up',
'lg',
undefined,
['grid']
);
expect(result).toEqual(['lg:grid']);
});
});
describe('convertBreakpointStyles', () => {
it('should convert valid breakpoint styles', () => {
const styles: CSSProperties = {
display: 'flex',
padding: '16px',
};
const result = breakpointMapper.convertBreakpointStyles(
"[theme.breakpoints.up('md')]",
styles,
['flex', 'p-4']
);
expect(result.classes).toEqual(['md:flex', 'md:p-4']);
expect(result.warnings).toEqual([]);
});
it('should handle invalid breakpoint keys gracefully', () => {
const styles: CSSProperties = {
display: 'block',
};
const result = breakpointMapper.convertBreakpointStyles(
'invalid-breakpoint-key',
styles,
['block']
);
expect(result.classes).toEqual(['block']);
expect(result.warnings).toHaveLength(1);
expect(result.warnings[0]).toContain('Could not parse breakpoint');
});
it('should catch and handle errors during conversion', () => {
// Mock parseBreakpointKey to throw an error
const originalParse = breakpointMapper['parseBreakpointKey'];
breakpointMapper['parseBreakpointKey'] = jest.fn(() => {
throw new Error('Parsing error');
});
const result = breakpointMapper.convertBreakpointStyles(
"[theme.breakpoints.up('md')]",
{},
['test-class']
);
expect(result.classes).toEqual(['test-class']);
expect(result.warnings).toHaveLength(1);
expect(result.warnings[0]).toContain('Error converting breakpoint');
// Restore original method
breakpointMapper['parseBreakpointKey'] = originalParse;
});
});
describe('extractBreakpointStyles', () => {
it('should separate base styles from breakpoint styles', () => {
const styles: CSSProperties = {
display: 'flex',
padding: '8px',
"[theme.breakpoints.up('md')]": {
padding: '16px',
margin: '8px',
},
"[theme.breakpoints.down('sm')]": {
display: 'block',
},
color: 'red',
};
const result = breakpointMapper.extractBreakpointStyles(styles);
expect(result.baseStyles).toEqual({
display: 'flex',
padding: '8px',
color: 'red',
});
expect(result.breakpointStyles).toEqual({
"[theme.breakpoints.up('md')]": {
padding: '16px',
margin: '8px',
},
"[theme.breakpoints.down('sm')]": {
display: 'block',
},
});
});
it('should handle styles with no breakpoints', () => {
const styles: CSSProperties = {
display: 'flex',
padding: '8px',
color: 'blue',
};
const result = breakpointMapper.extractBreakpointStyles(styles);
expect(result.baseStyles).toEqual(styles);
expect(result.breakpointStyles).toEqual({});
});
it('should handle styles with only breakpoints', () => {
const styles: CSSProperties = {
"[theme.breakpoints.up('md')]": {
display: 'flex',
},
"[theme.breakpoints.down('sm')]": {
display: 'none',
},
};
const result = breakpointMapper.extractBreakpointStyles(styles);
expect(result.baseStyles).toEqual({});
expect(result.breakpointStyles).toEqual(styles);
});
it('should handle non-object breakpoint values', () => {
const styles: CSSProperties = {
display: 'flex',
"[theme.breakpoints.up('md')]": 'invalid-value' as any,
};
const result = breakpointMapper.extractBreakpointStyles(styles);
expect(result.baseStyles).toEqual({ display: 'flex' });
expect(result.breakpointStyles).toEqual({});
});
it('should handle null and array values correctly', () => {
const styles: CSSProperties = {
display: 'flex',
"[theme.breakpoints.up('md')]": null as any,
"[theme.breakpoints.down('sm')]": ['flex', 'p-4'] as any,
};
const result = breakpointMapper.extractBreakpointStyles(styles);
expect(result.baseStyles).toEqual({ display: 'flex' });
expect(result.breakpointStyles).toEqual({});
});
});
describe('hasNestedBreakpoints', () => {
it('should detect nested breakpoints', () => {
const stylesWithBreakpoints: CSSProperties = {
display: 'flex',
"[theme.breakpoints.up('md')]": {
padding: '16px',
},
};
expect(breakpointMapper.hasNestedBreakpoints(stylesWithBreakpoints)).toBe(true);
});
it('should return false for styles without breakpoints', () => {
const stylesWithoutBreakpoints: CSSProperties = {
display: 'flex',
padding: '8px',
color: 'red',
};
expect(breakpointMapper.hasNestedBreakpoints(stylesWithoutBreakpoints)).toBe(false);
});
it('should handle empty styles object', () => {
expect(breakpointMapper.hasNestedBreakpoints({})).toBe(false);
});
});
describe('generateBreakpointComments', () => {
it('should generate descriptive comments for breakpoint styles', () => {
const styles: CSSProperties = {
display: 'flex',
padding: '16px',
margin: '8px',
};
const result = breakpointMapper.generateBreakpointComments(
"[theme.breakpoints.up('md')]",
styles
);
expect(result).toContain('TODO: Review responsive styles');
expect(result).toContain("[theme.breakpoints.up('md')]");
expect(result).toContain('display: flex');
expect(result).toContain('padding: 16px');
expect(result).toContain('margin: 8px');
});
it('should handle empty styles object', () => {
const result = breakpointMapper.generateBreakpointComments(
"[theme.breakpoints.down('sm')]",
{}
);
expect(result).toContain('TODO: Review responsive styles');
expect(result).toContain('Original: { }');
});
it('should handle single style property', () => {
const result = breakpointMapper.generateBreakpointComments(
"[theme.breakpoints.between('sm', 'lg')]",
{ display: 'block' }
);
expect(result).toContain('display: block');
});
});
describe('validateBreakpointLogic', () => {
it('should validate clean breakpoint logic', () => {
const breakpointStyles = {
"[theme.breakpoints.up('sm')]": { display: 'flex' },
"[theme.breakpoints.up('md')]": { padding: '16px' },
};
const result = breakpointMapper.validateBreakpointLogic(breakpointStyles);
expect(result.valid).toBe(true);
expect(result.warnings).toEqual([]);
expect(result.suggestions).toEqual([]);
});
it('should warn about mixed up() and down() breakpoints', () => {
const breakpointStyles = {
"[theme.breakpoints.up('md')]": { display: 'flex' },
"[theme.breakpoints.down('sm')]": { display: 'block' },
};
const result = breakpointMapper.validateBreakpointLogic(breakpointStyles);
expect(result.valid).toBe(false);
expect(result.warnings).toContain('Mixed up() and down() breakpoints detected - ensure logic is correct');
expect(result.suggestions).toContain('Consider using consistent breakpoint direction or between() for ranges');
});
it('should warn about too many breakpoints', () => {
const breakpointStyles = {
"[theme.breakpoints.up('xs')]": { display: 'flex' },
"[theme.breakpoints.up('sm')]": { padding: '8px' },
"[theme.breakpoints.up('md')]": { padding: '16px' },
"[theme.breakpoints.up('lg')]": { padding: '24px' },
"[theme.breakpoints.up('xl')]": { padding: '32px' },
};
const result = breakpointMapper.validateBreakpointLogic(breakpointStyles);
expect(result.valid).toBe(false);
expect(result.warnings).toContain('High number of breakpoints (5) - consider consolidating');
expect(result.suggestions).toContain('Use fewer breakpoints for simpler responsive design');
});
it('should handle empty breakpoint styles', () => {
const result = breakpointMapper.validateBreakpointLogic({});
expect(result.valid).toBe(true);
expect(result.warnings).toEqual([]);
expect(result.suggestions).toEqual([]);
});
});
describe('generateArbitraryBreakpoint', () => {
it('should generate arbitrary up() breakpoints', () => {
const result = breakpointMapper.generateArbitraryBreakpoint(
'up',
768,
['flex', 'p-4']
);
expect(result).toEqual(['min-[768px]:flex', 'min-[768px]:p-4']);
});
it('should generate arbitrary down() breakpoints', () => {
const result = breakpointMapper.generateArbitraryBreakpoint(
'down',
1024,
['hidden', 'text-sm']
);
expect(result).toEqual(['max-[1024px]:hidden', 'max-[1024px]:text-sm']);
});
it('should handle empty class list', () => {
const result = breakpointMapper.generateArbitraryBreakpoint('up', 640, []);
expect(result).toEqual([]);
});
it('should handle single class', () => {
const result = breakpointMapper.generateArbitraryBreakpoint('down', 480, ['block']);
expect(result).toEqual(['max-[480px]:block']);
});
it('should handle various pixel values', () => {
const testCases = [
{ direction: 'up' as const, pixels: 320, class: 'flex' },
{ direction: 'up' as const, pixels: 1200, class: 'grid' },
{ direction: 'down' as const, pixels: 800, class: 'hidden' },
{ direction: 'down' as const, pixels: 1440, class: 'block' },
];
testCases.forEach(({ direction, pixels, class: className }) => {
const result = breakpointMapper.generateArbitraryBreakpoint(direction, pixels, [className]);
const prefix = direction === 'up' ? 'min' : 'max';
expect(result).toEqual([`${prefix}-[${pixels}px]:${className}`]);
});
});
});
describe('integration and complex scenarios', () => {
it('should handle complex responsive design workflow', () => {
const complexStyles: CSSProperties = {
display: 'flex',
padding: '8px',
"[theme.breakpoints.up('sm')]": {
padding: '12px',
margin: '4px',
},
"[theme.breakpoints.up('md')]": {
padding: '16px',
margin: '8px',
display: 'grid',
},
"[theme.breakpoints.down('xs')]": {
display: 'block',
},
};
// Extract breakpoints
const extracted = breakpointMapper.extractBreakpointStyles(complexStyles);
expect(extracted.baseStyles).toEqual({
display: 'flex',
padding: '8px',
});
expect(Object.keys(extracted.breakpointStyles)).toHaveLength(3);
// Validate logic
const validation = breakpointMapper.validateBreakpointLogic(extracted.breakpointStyles);
expect(validation.valid).toBe(false); // Mixed up/down
expect(validation.warnings).toContain('Mixed up() and down() breakpoints detected - ensure logic is correct');
// Convert specific breakpoint
const conversion = breakpointMapper.convertBreakpointStyles(
"[theme.breakpoints.up('md')]",
extracted.breakpointStyles["[theme.breakpoints.up('md')]"],
['p-4', 'm-2', 'grid']
);
expect(conversion.classes).toEqual(['md:p-4', 'md:m-2', 'md:grid']);
expect(conversion.warnings).toEqual([]);
});
it('should handle edge cases with malformed breakpoint keys', () => {
const malformedKeys = [
"[theme.breakpoints.up('')]",
"[theme.breakpoints.up(md)]", // Missing quotes
"[theme.breakpoints.up('md'", // Missing closing bracket
"theme.breakpoints.up('md')]", // Missing opening bracket
];
malformedKeys.forEach((key) => {
const result = breakpointMapper.convertBreakpointStyles(key, {}, ['test']);
expect(result.classes).toEqual(['test']);
expect(result.warnings).toHaveLength(1);
});
});
it('should preserve original classes on conversion failure', () => {
const originalClasses = ['flex', 'p-4', 'text-center'];
const result = breakpointMapper.convertBreakpointStyles(
'invalid-key',
{ display: 'flex' },
originalClasses
);
expect(result.classes).toEqual(originalClasses);
expect(result.warnings).toHaveLength(1);
});
});
describe('custom breakpoints integration', () => {
it('should work with custom breakpoint mapping', () => {
const customBreakpoints: Partial<BreakpointMapping> = {
xs: 0,
sm: 576,
md: 768,
lg: 992,
xl: 1200,
xxl: 1400,
};
const customMapper = new BreakpointMapper(customBreakpoints);
// Should still work with standard operations
expect(customMapper.isBreakpoint("[theme.breakpoints.up('md')]")).toBe(true);
const result = customMapper.convertBreakpointStyles(
"[theme.breakpoints.up('xl')]",
{ display: 'flex' },
['flex']
);
expect(result.classes).toEqual(['xl:flex']);
expect(result.warnings).toEqual([]);
});
});
});