/* eslint-disable @typescript-eslint/no-empty-function */ import { describe, expect, it, vi } from "vitest"; import { transform } from "../../../../test/helpers/babelTransform.js"; describe("className visitor - basic transformation", () => { it("should still transform className props", () => { const input = ` import { View } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); // Enable JSX // Should have StyleSheet expect(output).toContain("StyleSheet.create"); expect(output).toContain("_twStyles"); // Should replace className with style expect(output).not.toContain("className"); expect(output).toContain("style:"); }); it("should work with both tw and className in same file", () => { const input = ` import { tw } from '@mgcrea/react-native-tailwind'; import { View } from 'react-native'; const styles = tw\`bg-red-500\`; export function Component() { return ; } `; const output = transform(input, undefined, true); // Enable JSX // Should have both styles in StyleSheet expect(output).toContain("_bg_red_500"); expect(output).toContain("_m_4_p_2"); }); it("should merge className with function-based style prop", () => { const input = ` import { TextInput } from 'react-native'; export function Component() { return ( [ baseStyles, focused && focusedStyles, ]} /> ); } `; const output = transform(input, undefined, true); // Enable JSX // Should have StyleSheet with className styles expect(output).toContain("StyleSheet.create"); // Style keys are sorted alphabetically: bg-gray-100 comes before border expect(output).toContain("_bg_gray_100_border_border_gray_300"); // Should create a wrapper function that merges both // The wrapper should call the original function and merge results expect(output).toContain("_state"); expect(output).toContain("_twStyles._bg_gray_100_border_border_gray_300"); // Should not have className in output expect(output).not.toContain("className"); // Should have a function that accepts state and returns an array expect(output).toMatch(/_state\s*=>/); }); it("should preserve 'use client' directive when injecting StyleSheet.create", () => { const input = ` 'use client'; import { View } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); // 'use client' should be the first statement const lines = output.split("\n").filter((l: string) => l.trim()); const useClientIndex = lines.findIndex( (l: string) => l.includes("'use client'") || l.includes('"use client"'), ); expect(useClientIndex).toBe(0); // StyleSheet.create should be in the output expect(output).toContain("StyleSheet.create"); expect(output).toContain("_twStyles"); // Imports should come after 'use client', before StyleSheet.create const importIndex = lines.findIndex((l: string) => l.includes("import")); const styleSheetIndex = lines.findIndex((l: string) => l.includes("StyleSheet.create")); expect(importIndex).toBeGreaterThan(useClientIndex); expect(styleSheetIndex).toBeGreaterThan(importIndex); }); it("should merge dynamic className with function-based style prop", () => { const input = ` import { TextInput } from 'react-native'; export function Component({ isError }) { return ( [ baseStyles, focused && focusedStyles, ]} /> ); } `; const output = transform(input, undefined, true); // Enable JSX // Should have StyleSheet with both className styles expect(output).toContain("StyleSheet.create"); expect(output).toContain("_border"); expect(output).toContain("_border_red_500"); expect(output).toContain("_border_gray_300"); // Should create a wrapper function that merges dynamic styles with function result expect(output).toContain("_state"); // Should not have className in output expect(output).not.toContain("className"); }); it('should transform className={"..."} (string literal in expression container)', () => { const input = ` import { View } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); // Should have StyleSheet expect(output).toContain("StyleSheet.create"); expect(output).toContain("_twStyles"); // Should replace className with style expect(output).not.toContain("className"); expect(output).toContain("style:"); // Should have the expected style keys expect(output).toContain("_flex_row_items_center_justify_start"); }); it('should transform className={"..."} with modifiers', () => { const input = ` import { Pressable } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); // Should have StyleSheet with both base and active styles expect(output).toContain("_bg_blue_500_p_4"); expect(output).toContain("_active_bg_blue_700"); // Should have style function for active modifier (Pressable uses 'pressed' parameter) expect(output).toMatch(/(pressed|_state)/); // Should not have className in output expect(output).not.toContain("className"); }); it('should transform className={"..."} with platform modifiers', () => { const input = ` import { View } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); // Should have Platform import expect(output).toContain("Platform"); expect(output).toMatch(/from ['"]react-native['"]/); // Match both single and double quotes // Should have Platform.select expect(output).toContain("Platform.select"); // Should have platform-specific styles expect(output).toContain("_ios_p_6"); expect(output).toContain("_android_p_8"); // Should not have className in output expect(output).not.toContain("className"); }); it('should handle empty className={""}', () => { const input = ` import { View } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); // Should remove empty className attribute entirely expect(output).not.toContain("className"); expect(output).not.toContain("style="); }); }); describe("className visitor - placeholder: modifier", () => { it("should transform placeholder:text-{color} to placeholderTextColor prop", () => { const input = ` import { TextInput } from 'react-native'; export function Component() { return ( ); } `; const output = transform(input, undefined, true); // Should have placeholderTextColor prop with correct hex value (from custom palette) expect(output).toContain('placeholderTextColor: "#99a1af"'); // Should still have style for border-2 expect(output).toContain("StyleSheet.create"); expect(output).toContain("_border_2"); // Should not have className in output expect(output).not.toContain("className"); }); it("should support placeholder colors with opacity", () => { const input = ` import { TextInput } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); // Should have 8-digit hex with alpha channel (custom palette red-500, uppercased) expect(output).toContain('placeholderTextColor: "#FB2C3680"'); }); it("should support arbitrary placeholder colors", () => { const input = ` import { TextInput } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); expect(output).toContain('placeholderTextColor: "#ff0000"'); }); it("should combine placeholder: with other modifiers", () => { const input = ` import { TextInput } from 'react-native'; export function Component() { return ( ); } `; const output = transform(input, undefined, true); // Should have placeholderTextColor prop (custom palette gray-400) expect(output).toContain('placeholderTextColor: "#99a1af"'); // Should have focus: modifier handling (style function) expect(output).toContain("focused"); expect(output).toMatch(/style[\s\S]*=>/); // Style function // Should not have className expect(output).not.toContain("className"); }); it("should handle multiple placeholder: classes (last wins)", () => { const input = ` import { TextInput } from 'react-native'; export function Component() { return ( ); } `; const output = transform(input, undefined, true); // Blue should win (last color, custom palette blue-500) expect(output).toContain('placeholderTextColor: "#2b7fff"'); }); it("should ignore non-text utilities in placeholder: modifier", () => { const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); const input = ` import { TextInput } from 'react-native'; export function Component() { return ( ); } `; const output = transform(input, undefined, true); // Should still have the valid text color (custom palette gray-400) expect(output).toContain('placeholderTextColor: "#99a1af"'); // Should not have font-bold anywhere expect(output).not.toContain("fontWeight"); consoleSpy.mockRestore(); }); it.skip("should work with custom colors", () => { // Note: This test would require setting up a tailwind.config file // For now, we'll skip custom color testing in Babel tests // Custom colors are tested in the parser tests }); it("should not transform placeholder: on non-TextInput elements", () => { const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); const input = ` import { View } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); // Should not have placeholderTextColor prop (View doesn't support it) expect(output).not.toContain("placeholderTextColor"); // Should warn about unsupported modifier // (The warning happens because View doesn't support any modifiers) consoleSpy.mockRestore(); }); }); describe("className visitor - platform modifiers", () => { it("should transform platform modifiers to Platform.select()", () => { const input = ` import React from 'react'; import { View } from 'react-native'; export function Component() { return ( ); } `; const output = transform(input, undefined, true); // Should import Platform from react-native expect(output).toContain("Platform"); expect(output).toMatch(/import.*Platform.*from ['"]react-native['"]/); // Should generate Platform.select() expect(output).toContain("Platform.select"); // Should have base padding style expect(output).toContain("_p_4"); // Should have iOS and Android specific styles expect(output).toContain("_ios_p_6"); expect(output).toContain("_android_p_8"); // Should have correct style values in StyleSheet.create expect(output).toMatch(/padding:\s*16/); // p-4 expect(output).toMatch(/padding:\s*24/); // p-6 (ios) expect(output).toMatch(/padding:\s*32/); // p-8 (android) }); it("should support multiple platform modifiers on same element", () => { const input = ` import React from 'react'; import { View } from 'react-native'; export function Component() { return ( ); } `; const output = transform(input, undefined, true); // Should have Platform import expect(output).toContain("Platform"); // Should have base styles (combined key) expect(output).toContain("_bg_white_p_4"); // Should have iOS specific styles (combined key for multiple ios: modifiers) expect(output).toContain("_ios_bg_blue_50_p_6"); // Should have Android specific styles (combined key for multiple android: modifiers) expect(output).toContain("_android_bg_green_50_p_8"); // Should contain Platform.select with both platforms expect(output).toMatch(/Platform\.select\s*\(\s*\{[\s\S]*ios:/); expect(output).toMatch(/Platform\.select\s*\(\s*\{[\s\S]*android:/); }); it("should support web platform modifier", () => { const input = ` import React from 'react'; import { View } from 'react-native'; export function Component() { return ( ); } `; const output = transform(input, undefined, true); // Should have Platform.select with web expect(output).toContain("Platform.select"); expect(output).toContain("web:"); expect(output).toContain("_web_p_2"); }); it("should work with platform modifiers on all components", () => { const input = ` import React from 'react'; import { View, Text, ScrollView } from 'react-native'; export function Component() { return ( Platform text ); } `; const output = transform(input, undefined, true); // Should work on View - check for Platform.select separately (not checking style= format) expect(output).toContain("Platform.select"); // Should work on Text expect(output).toContain("_ios_text_lg"); expect(output).toContain("_android_text_xl"); // Should work on ScrollView contentContainerStyle expect(output).toContain("contentContainerStyle"); }); it("should combine platform modifiers with state modifiers", () => { const input = ` import React from 'react'; import { Pressable, Text } from 'react-native'; export function Component() { return ( Button ); } `; const output = transform(input, undefined, true); // Should have Platform.select for platform modifiers expect(output).toContain("Platform.select"); expect(output).toContain("_ios_shadow_md"); expect(output).toContain("_android_shadow_sm"); // Should have state modifier function for active expect(output).toMatch(/\(\s*\{\s*pressed\s*\}\s*\)\s*=>/); expect(output).toContain("pressed"); expect(output).toContain("_active_bg_blue_700"); // Should have base styles expect(output).toContain("_bg_blue_500"); expect(output).toContain("_p_4"); }); it("should handle platform-specific colors", () => { const input = ` import React from 'react'; import { View, Text } from 'react-native'; export function Component() { return ( Platform colors ); } `; const output = transform(input, undefined, true); // Should have color values in StyleSheet expect(output).toMatch(/#[0-9A-F]{6}/i); // Hex color format // Should have platform-specific color classes expect(output).toContain("_ios_text_blue_900"); expect(output).toContain("_android_text_green_900"); }); it("should only add Platform import once when needed", () => { const input = ` import React from 'react'; import { View } from 'react-native'; export function Component() { return ( <> ); } `; const output = transform(input, undefined, true); // Should have Platform import expect(output).toContain("Platform"); // Count how many times Platform is imported (should be once) const platformImports = output.match(/import.*Platform.*from ['"]react-native['"]/g); expect(platformImports).toHaveLength(1); }); it("should merge with existing Platform import", () => { const input = ` import React from 'react'; import { View, Platform } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); // Should still use Platform.select expect(output).toContain("Platform.select"); // Should not duplicate Platform import - Platform appears in import and Platform.select calls expect(output).toMatch(/Platform.*react-native/); }); it("should handle platform modifiers without base classes", () => { const input = ` import React from 'react'; import { View } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); // Should only have Platform.select, no base style expect(output).toContain("Platform.select"); expect(output).toContain("_ios_p_6"); expect(output).toContain("_android_p_8"); // Should not have generic padding without platform prefix // Check that non-platform-prefixed style keys don't exist expect(output).not.toMatch(/(? { it("should transform dark: modifier to conditional expression", () => { const input = ` import React from 'react'; import { View } from 'react-native'; export function Component() { return ( ); } `; const output = transform(input, undefined, true); // Should import useColorScheme expect(output).toContain("useColorScheme"); expect(output).toMatch(/import.*useColorScheme.*from ['"]react-native['"]/); // Should inject colorScheme hook in component expect(output).toContain("_twColorScheme"); expect(output).toContain("useColorScheme()"); // Should have base bg-white style expect(output).toContain("_bg_white"); // Should have dark:bg-gray-900 style expect(output).toContain("_dark_bg_gray_900"); // Should generate conditional: _twColorScheme === 'dark' && ... expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/); }); it("should support both dark: and light: modifiers", () => { const input = ` import React from 'react'; import { View } from 'react-native'; export function Component() { return ( ); } `; const output = transform(input, undefined, true); // Should have all three styles expect(output).toContain("_bg_gray_100"); expect(output).toContain("_dark_bg_gray_900"); expect(output).toContain("_light_bg_white"); // Should have both conditionals expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/); expect(output).toMatch(/_twColorScheme\s*===\s*['"]light['"]/); }); it("should inject hook once for multiple elements with color scheme modifiers", () => { const input = ` import React from 'react'; import { View, Text } from 'react-native'; export function Component() { return ( <> ); } `; const output = transform(input, undefined, true); // Count occurrences of useColorScheme() call - should be exactly 1 const hookCallMatches = output.match(/=\s*useColorScheme\(\)/g); expect(hookCallMatches).toHaveLength(1); // Should have color scheme variable expect(output).toContain("_twColorScheme"); }); it("should work with color scheme and platform modifiers together", () => { const input = ` import React from 'react'; import { View } from 'react-native'; export function Component() { return ( ); } `; const output = transform(input, undefined, true); // Should have Platform import expect(output).toContain("Platform"); // Should have useColorScheme import expect(output).toContain("useColorScheme"); // Should have Platform.select for ios: expect(output).toContain("Platform.select"); expect(output).toContain("_ios_p_6"); // Should have color scheme conditional for dark: expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/); expect(output).toContain("_dark_bg_gray_900"); }); it("should only add useColorScheme import once when needed", () => { const input = ` import React from 'react'; import { View } from 'react-native'; export function Component() { return ( <> ); } `; const output = transform(input, undefined, true); // Count useColorScheme imports const importMatches = output.match(/import.*useColorScheme.*from ['"]react-native['"]/g); expect(importMatches).toHaveLength(1); }); it("should merge with existing useColorScheme import", () => { const input = ` import React from 'react'; import { View, useColorScheme } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); // Should still use useColorScheme expect(output).toContain("useColorScheme"); // Should inject hook call expect(output).toContain("_twColorScheme"); expect(output).toContain("useColorScheme()"); }); it("should work with concise arrow functions", () => { const input = ` import React from 'react'; import { View } from 'react-native'; const Component = () => ; `; const output = transform(input, undefined, true); // Should inject useColorScheme import expect(output).toContain("useColorScheme"); // Should convert concise arrow to block statement and inject hook expect(output).toContain("_twColorScheme"); expect(output).toContain("useColorScheme()"); expect(output).toContain("return"); // Should have the style expect(output).toContain("_dark_bg_gray_900"); expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/); }); it("should inject hook at component level when dark: used in nested callback", () => { const input = ` import React from 'react'; import { View } from 'react-native'; export function Component() { const items = [1, 2, 3]; return ( {items.map(item => ( ))} ); } `; const output = transform(input, undefined, true); // Should inject hook at Component level (not in map callback) expect(output).toContain("_twColorScheme"); expect(output).toContain("useColorScheme()"); // Hook should be injected in Component function, not in map callback // Count occurrences - should be exactly 1 at Component level const hookCallMatches = output.match(/=\s*useColorScheme\(\)/g); expect(hookCallMatches).toHaveLength(1); // Should still generate conditional expression expect(output).toContain("_dark_bg_gray_900"); expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/); }); it("should handle dynamic expressions with dark:/light: modifiers", () => { const input = ` import React from 'react'; import { View } from 'react-native'; export function Component({ isActive }) { return ( ); } `; const output = transform(input, undefined, true); // Should inject useColorScheme expect(output).toContain("useColorScheme"); expect(output).toContain("_twColorScheme"); // Should have both dark styles expect(output).toContain("_dark_bg_blue_500"); expect(output).toContain("_dark_bg_gray_900"); // Should have conditional expressions for color scheme expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/); }); it("should handle dynamic expressions with platform modifiers", () => { const input = ` import React from 'react'; import { View } from 'react-native'; export function Component({ isLarge }) { return ( ); } `; const output = transform(input, undefined, true); // Should inject Platform import expect(output).toContain("Platform"); // Should have both ios styles expect(output).toContain("_ios_p_8"); expect(output).toContain("_ios_p_6"); // Should have Platform.select expect(output).toContain("Platform.select"); }); it("should skip color scheme modifiers when used outside component scope", () => { // Suppress console.warn for this test const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); const input = ` import React from 'react'; import { View } from 'react-native'; // Class component - no function component scope class MyComponent extends React.Component { render() { return ; } } `; const output = transform(input, undefined, true); // Should warn about invalid context expect(consoleWarnSpy).toHaveBeenCalledWith( expect.stringContaining("dark:/light: modifiers require a function component scope"), ); // Should NOT inject useColorScheme import (no valid component scope) expect(output).not.toContain("useColorScheme"); // Should NOT have _twColorScheme variable reference (would cause ReferenceError) expect(output).not.toContain("_twColorScheme"); // Should NOT have dark: style conditional (skipped due to no component scope) expect(output).not.toContain("_dark_bg_gray_900"); // Should still transform base classes (p-4) expect(output).toContain("_p_4"); consoleWarnSpy.mockRestore(); }); }); describe("className visitor - custom color scheme hook", () => { it("should use custom import source for color scheme hook", () => { const input = ` import React from 'react'; import { View } from 'react-native'; export function Component() { return ; } `; const output = transform( input, { colorScheme: { importFrom: "@/hooks/useColorScheme", importName: "useColorScheme", }, }, true, ); // Should import from custom source expect(output).toContain('from "@/hooks/useColorScheme"'); expect(output).not.toContain('useColorScheme } from "react-native"'); // Should inject hook call expect(output).toContain("_twColorScheme = useColorScheme()"); // Should have conditional styling expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/); }); it("should use custom hook name", () => { const input = ` import React from 'react'; import { View } from 'react-native'; export function Component() { return ; } `; const output = transform( input, { colorScheme: { importFrom: "@react-navigation/native", importName: "useTheme", }, }, true, ); // Should import useTheme from React Navigation expect(output).toContain('from "@react-navigation/native"'); expect(output).toContain("useTheme"); // Should call useTheme hook expect(output).toContain("_twColorScheme = useTheme()"); // Should have conditional styling expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/); }); it("should merge custom hook with existing import from same source", () => { const input = ` import React from 'react'; import { View, Text } from 'react-native'; import { useNavigation } from '@react-navigation/native'; export function Component() { const navigation = useNavigation(); return ( navigation.navigate('Home')}>Go Home ); } `; const output = transform( input, { colorScheme: { importFrom: "@react-navigation/native", importName: "useTheme", }, }, true, ); // Should merge with existing import (both useNavigation and useTheme in same import) expect(output).toMatch( /import\s+\{\s*useNavigation[^}]*useTheme[^}]*\}\s+from\s+['"]@react-navigation\/native['"]/, ); expect(output).toContain("useNavigation()"); expect(output).toContain("useTheme()"); // Should only have one import from that source const importCount = (output.match(/@react-navigation\/native/g) ?? []).length; expect(importCount).toBe(1); }); it("should not duplicate custom hook if already imported", () => { const input = ` import React from 'react'; import { View } from 'react-native'; import { useColorScheme } from '@/hooks/useColorScheme'; export function Component() { return ; } `; const output = transform( input, { colorScheme: { importFrom: "@/hooks/useColorScheme", importName: "useColorScheme", }, }, true, ); // Should not add duplicate import const importMatches = output.match(/import.*useColorScheme.*from ['"]@\/hooks\/useColorScheme['"]/g); expect(importMatches).toHaveLength(1); // Should still inject hook call expect(output).toContain("_twColorScheme = useColorScheme()"); }); it("should use react-native by default when no custom config provided", () => { const input = ` import React from 'react'; import { View } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); // Should use default react-native import (can be single or double quotes) expect(output).toMatch(/useColorScheme\s*}\s*from\s+['"]react-native['"]/); expect(output).not.toContain("@/hooks"); expect(output).not.toContain("@react-navigation"); // Should inject hook call with default name expect(output).toContain("_twColorScheme = useColorScheme()"); }); it("should create separate import when only type-only import exists", () => { const input = ` import React from 'react'; import { View } from 'react-native'; import type { NavigationProp } from '@react-navigation/native'; export function Component() { return ; } `; const output = transform( input, { colorScheme: { importFrom: "@react-navigation/native", importName: "useTheme", }, }, true, ); // TypeScript preset strips type-only imports, but the important thing is: // 1. useTheme hook is imported (not skipped thinking it was already imported) // 2. Hook is correctly called in the component expect(output).toMatch(/import\s+\{\s*useTheme\s*\}\s+from\s+['"]@react-navigation\/native['"]/); expect(output).toContain("_twColorScheme = useTheme()"); }); it("should use aliased identifier when hook is already imported with alias", () => { const input = ` import React from 'react'; import { View, Text } from 'react-native'; import { useTheme as navTheme } from '@react-navigation/native'; export function Component() { const theme = navTheme(); return ( {theme.dark ? 'Dark' : 'Light'} ); } `; const output = transform( input, { colorScheme: { importFrom: "@react-navigation/native", importName: "useTheme", }, }, true, ); // Should not add duplicate import const importMatches = output.match( /import\s+\{[^}]*useTheme[^}]*\}\s+from\s+['"]@react-navigation\/native['"]/g, ); expect(importMatches).toHaveLength(1); // Should still have the aliased import expect(output).toMatch(/useTheme\s+as\s+navTheme/); // Should call the aliased name (navTheme), not the export name (useTheme) // Both the user's code and our injected hook should use navTheme expect(output).toContain("_twColorScheme = navTheme()"); expect(output).not.toContain("_twColorScheme = useTheme()"); }); it("should not treat type-only imports as having the hook", () => { const input = ` import React from 'react'; import { View } from 'react-native'; import type { useColorScheme } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); // Should add a VALUE import for useColorScheme (type import doesn't count) expect(output).toMatch(/import\s+\{[^}]*useColorScheme[^}]*\}\s+from\s+['"]react-native['"]/); // Should inject the hook expect(output).toContain("_twColorScheme = useColorScheme()"); // Should have both type-only and value imports in output // (TypeScript preset keeps type imports for type checking) const colorSchemeMatches = output.match(/useColorScheme/g); expect(colorSchemeMatches).toBeTruthy(); expect(colorSchemeMatches?.length).toBeGreaterThanOrEqual(2); // At least in import and hook call }); it("should handle both type-only and aliased imports together", () => { const input = ` import React from 'react'; import { View, Text } from 'react-native'; import type { Theme } from '@react-navigation/native'; import { useTheme as getNavTheme } from '@react-navigation/native'; export function Component() { const theme = getNavTheme(); return ( {theme.dark ? 'Dark Mode' : 'Light Mode'} ); } `; const output = transform( input, { colorScheme: { importFrom: "@react-navigation/native", importName: "useTheme", }, }, true, ); // TypeScript preset strips type-only imports // The important thing is: should not add duplicate import, and should use aliased name expect(output).toMatch( /import\s+\{[^}]*useTheme\s+as\s+getNavTheme[^}]*\}\s+from\s+['"]@react-navigation\/native['"]/, ); // Should not add duplicate import - useTheme should only appear in the aliased import const useThemeImports = output.match( /import\s+\{[^}]*useTheme[^}]*\}\s+from\s+['"]@react-navigation\/native['"]/g, ); expect(useThemeImports).toHaveLength(1); // Should call the aliased name for both user code and our injected hook expect(output).toContain("_twColorScheme = getNavTheme()"); expect(output).not.toContain("_twColorScheme = useTheme()"); }); }); describe("className visitor - directional border colors", () => { it("should transform directional border colors with preset values", () => { const input = ` import { View } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); // Should have StyleSheet expect(output).toContain("StyleSheet.create"); // Should generate styles with borderTopColor and borderLeftColor expect(output).toMatch(/borderTopColor[:\s]*['"]#[0-9A-F]{6}['"]/i); expect(output).toMatch(/borderLeftColor[:\s]*['"]#[0-9A-F]{6}['"]/i); // Should not have className in output expect(output).not.toContain("className"); }); it("should combine directional border width and color", () => { const input = ` import { View } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); // Should have both borderLeftWidth and borderLeftColor in the StyleSheet expect(output).toMatch(/borderLeftWidth[:\s]*2/); expect(output).toMatch(/borderLeftColor[:\s]*['"]#[0-9A-F]{6}['"]/i); // Should not have className in output expect(output).not.toContain("className"); }); it("should support directional border colors with opacity", () => { const input = ` import { View } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); // Should have 8-digit hex colors with alpha channel expect(output).toMatch(/borderTopColor[:\s]*['"]#[0-9A-F]{8}['"]/i); expect(output).toMatch(/borderBottomColor[:\s]*['"]#[0-9A-F]{8}['"]/i); }); it("should support directional border colors with arbitrary hex values", () => { const input = ` import { View } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); // Should have borderTopColor and borderRightColor expect(output).toMatch(/borderTopColor[:\s]*['"]#[0-9a-fA-F]{6}['"]/); expect(output).toMatch(/borderRightColor[:\s]*['"]#[0-9a-fA-F]{6}['"]/); }); it("should support all four directional border colors", () => { const input = ` import { View } from 'react-native'; export function Component() { return ( ); } `; const output = transform(input, undefined, true); // Should have all four directional color properties expect(output).toMatch(/borderTopColor[:\s]*['"]#[0-9A-F]{6}['"]/i); expect(output).toMatch(/borderRightColor[:\s]*['"]#[0-9A-F]{6}['"]/i); expect(output).toMatch(/borderBottomColor[:\s]*['"]#[0-9A-F]{6}['"]/i); expect(output).toMatch(/borderLeftColor[:\s]*['"]#[0-9A-F]{6}['"]/i); }); it("should combine directional widths, colors, and general border color", () => { const input = ` import { View } from 'react-native'; export function Component() { return ( ); } `; const output = transform(input, undefined, true); // Should have general border properties expect(output).toMatch(/borderWidth[:\s]*1/); expect(output).toMatch(/borderColor[:\s]*['"]#[0-9A-F]{6}['"]/i); // Should have directional left border properties expect(output).toMatch(/borderLeftWidth[:\s]*4/); expect(output).toMatch(/borderLeftColor[:\s]*['"]#[0-9A-F]{6}['"]/i); }); it("should work with dynamic className containing directional border colors", () => { const input = ` import { View } from 'react-native'; export function Component({ isError }) { return ( ); } `; const output = transform(input, undefined, true); // Should have StyleSheet with both color options expect(output).toContain("_border_t_2"); expect(output).toContain("_border_t_red_500"); expect(output).toContain("_border_t_gray_300"); // Should have conditional expression with both styles expect(output).toMatch(/isError\s*\?\s*_twStyles\._border_t_red_500/); }); }); describe("className visitor - directional modifiers (RTL/LTR)", () => { it("should transform rtl: modifier and inject I18nManager import", () => { const input = ` import { View } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); // Should import I18nManager expect(output).toContain("I18nManager"); // Should declare _twIsRTL variable expect(output).toContain("_twIsRTL"); expect(output).toContain("I18nManager.isRTL"); // Should have StyleSheet with rtl style expect(output).toContain("StyleSheet.create"); expect(output).toContain("_rtl_mr_4"); // Should have conditional for RTL expect(output).toMatch(/_twIsRTL\s*&&\s*_twStyles\._rtl_mr_4/); }); it("should transform ltr: modifier with negated conditional", () => { const input = ` import { View } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); // Should import I18nManager expect(output).toContain("I18nManager"); // Should have StyleSheet with ltr style expect(output).toContain("_ltr_ml_4"); // Should have negated conditional for LTR (!_twIsRTL) expect(output).toMatch(/!\s*_twIsRTL\s*&&\s*_twStyles\._ltr_ml_4/); }); it("should combine rtl: and ltr: modifiers", () => { const input = ` import { View } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); // Should have both styles expect(output).toContain("_rtl_mr_4"); expect(output).toContain("_ltr_ml_4"); // Should have both conditionals expect(output).toMatch(/_twIsRTL\s*&&\s*_twStyles\._rtl_mr_4/); expect(output).toMatch(/!\s*_twIsRTL\s*&&\s*_twStyles\._ltr_ml_4/); }); it("should combine directional modifiers with base classes", () => { const input = ` import { View } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); // Should have base style expect(output).toContain("_bg_white_p_4"); // Should have directional styles expect(output).toContain("_rtl_pr_8"); expect(output).toContain("_ltr_pl_8"); // Should generate an array with base and conditional styles expect(output).toMatch(/style:\s*\[/); }); it("should combine directional modifiers with platform modifiers", () => { const input = ` import { View } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); // Should have Platform import expect(output).toContain("Platform"); // Should have I18nManager import expect(output).toContain("I18nManager"); // Should have all styles expect(output).toContain("_p_4"); expect(output).toContain("_ios_p_6"); expect(output).toContain("_rtl_mr_4"); // Should have Platform.select expect(output).toContain("Platform.select"); // Should have RTL conditional expect(output).toMatch(/_twIsRTL\s*&&/); }); it("should not add I18nManager import if already present", () => { const input = ` import { View, I18nManager } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); // Should have only one I18nManager import (merged, not duplicated) const i18nMatches = output.match(/I18nManager/g); // Should have I18nManager in: import, variable declaration, and style usage expect(i18nMatches).toBeTruthy(); // Should not have duplicate imports expect(output).not.toMatch(/import\s*\{[^}]*I18nManager[^}]*I18nManager[^}]*\}/); }); it("should work with directional logical properties", () => { const input = ` import { View } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); // Should have logical property styles expect(output).toContain("_rtl_ms_4"); expect(output).toContain("_ltr_me_4"); // Should contain marginStart and marginEnd in the StyleSheet expect(output).toContain("marginStart"); expect(output).toContain("marginEnd"); }); it("should combine directional modifiers with color scheme modifiers", () => { const input = ` import { View } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); // Should have useColorScheme expect(output).toContain("useColorScheme"); // Should have I18nManager expect(output).toContain("I18nManager"); // Should have all styles expect(output).toContain("_bg_white"); expect(output).toContain("_dark_bg_gray_900"); expect(output).toContain("_rtl_pr_4"); // Should have both conditionals expect(output).toMatch(/_twColorScheme\s*===\s*["']dark["']/); expect(output).toMatch(/_twIsRTL\s*&&/); }); it("should handle aliased I18nManager import", () => { const input = ` import { View, I18nManager as RTL } from 'react-native'; export function Component() { // Use RTL somewhere so TypeScript doesn't strip the unused import const isRtl = RTL.isRTL; return ; } `; const output = transform(input, undefined, true); // Should use the aliased identifier RTL.isRTL instead of I18nManager.isRTL expect(output).toContain("RTL.isRTL"); // Should preserve the aliased import expect(output).toContain("I18nManager as RTL"); // Should not add a separate I18nManager import without alias expect(output).not.toMatch(/I18nManager,|,\s*I18nManager\s*[,}]/); }); it("should preserve 'use client' directive when injecting I18nManager variable", () => { const input = ` 'use client'; import { View } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); // 'use client' should be the first statement const lines = output.split("\n").filter((l: string) => l.trim()); const useClientIndex = lines.findIndex( (l: string) => l.includes("'use client'") || l.includes('"use client"'), ); expect(useClientIndex).toBe(0); // I18nManager variable should come after imports, not before 'use client' expect(output).toContain("_twIsRTL"); expect(output).toContain("I18nManager.isRTL"); }); it("should preserve 'use strict' directive when injecting I18nManager variable", () => { const input = ` 'use strict'; import { View } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); // 'use strict' should be preserved at the top const lines = output.split("\n").filter((l: string) => l.trim()); const useStrictIndex = lines.findIndex( (l: string) => l.includes("'use strict'") || l.includes('"use strict"'), ); expect(useStrictIndex).toBe(0); // I18nManager variable should work correctly expect(output).toContain("_twIsRTL"); expect(output).toContain("I18nManager.isRTL"); }); it("should expand text-start to directional modifiers", () => { const input = ` import { Text } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); // Should have I18nManager import (text-start expands to ltr:/rtl: modifiers) expect(output).toContain("I18nManager"); // Should have both ltr and rtl styles expect(output).toContain("_ltr_text_left"); expect(output).toContain("_rtl_text_right"); // Should have conditionals for both expect(output).toMatch(/_twIsRTL\s*&&/); expect(output).toMatch(/!\s*_twIsRTL\s*&&/); }); it("should expand text-end to directional modifiers", () => { const input = ` import { Text } from 'react-native'; export function Component() { return ; } `; const output = transform(input, undefined, true); // Should have I18nManager import expect(output).toContain("I18nManager"); // text-end expands to ltr:text-right rtl:text-left expect(output).toContain("_ltr_text_right"); expect(output).toContain("_rtl_text_left"); }); });