@mgcrea/react-native-tailwind 0.12.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -2031
- package/dist/babel/index.cjs +1726 -1094
- package/dist/babel/plugin/componentScope.d.ts +26 -0
- package/dist/babel/plugin/componentScope.ts +87 -0
- package/dist/babel/plugin/state.d.ts +123 -0
- package/dist/babel/plugin/state.ts +185 -0
- package/dist/babel/plugin/visitors/className.d.ts +11 -0
- package/{src/babel/plugin.test.ts → dist/babel/plugin/visitors/className.test.ts} +285 -572
- package/dist/babel/plugin/visitors/className.ts +652 -0
- package/dist/babel/plugin/visitors/className.windowDimensions.test.ts +406 -0
- package/dist/babel/plugin/visitors/imports.d.ts +11 -0
- package/dist/babel/plugin/visitors/imports.test.ts +88 -0
- package/dist/babel/plugin/visitors/imports.ts +116 -0
- package/dist/babel/plugin/visitors/program.d.ts +15 -0
- package/dist/babel/plugin/visitors/program.test.ts +325 -0
- package/dist/babel/plugin/visitors/program.ts +116 -0
- package/dist/babel/plugin/visitors/tw.d.ts +16 -0
- package/dist/babel/plugin/visitors/tw.test.ts +771 -0
- package/dist/babel/plugin/visitors/tw.ts +148 -0
- package/dist/babel/plugin.d.ts +3 -96
- package/dist/babel/plugin.test.ts +470 -0
- package/dist/babel/plugin.ts +28 -963
- package/dist/babel/utils/colorSchemeModifierProcessing.ts +11 -0
- package/dist/babel/utils/componentSupport.test.ts +20 -7
- package/dist/babel/utils/componentSupport.ts +2 -0
- package/dist/babel/utils/directionalModifierProcessing.d.ts +34 -0
- package/dist/babel/utils/directionalModifierProcessing.ts +99 -0
- package/dist/babel/utils/modifierProcessing.ts +21 -0
- package/dist/babel/utils/platformModifierProcessing.ts +11 -0
- package/dist/babel/utils/styleInjection.d.ts +31 -0
- package/dist/babel/utils/styleInjection.ts +253 -7
- package/dist/babel/utils/twProcessing.d.ts +2 -0
- package/dist/babel/utils/twProcessing.ts +103 -3
- package/dist/babel/utils/windowDimensionsProcessing.d.ts +56 -0
- package/dist/babel/utils/windowDimensionsProcessing.ts +121 -0
- package/dist/components/TouchableOpacity.d.ts +35 -0
- package/dist/components/TouchableOpacity.js +1 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.js +1 -0
- package/dist/config/markers.d.ts +5 -0
- package/dist/config/markers.js +1 -0
- package/dist/index.d.ts +2 -5
- package/dist/index.js +1 -1
- package/dist/parser/borders.d.ts +3 -1
- package/dist/parser/borders.js +1 -1
- package/dist/parser/borders.test.js +1 -1
- package/dist/parser/colors.js +1 -1
- package/dist/parser/colors.test.js +1 -1
- package/dist/parser/index.d.ts +2 -2
- package/dist/parser/index.js +1 -1
- package/dist/parser/layout.js +1 -1
- package/dist/parser/layout.test.js +1 -1
- package/dist/parser/modifiers.d.ts +32 -2
- package/dist/parser/modifiers.js +1 -1
- package/dist/parser/modifiers.test.js +1 -1
- package/dist/parser/sizing.js +1 -1
- package/dist/parser/spacing.d.ts +1 -1
- package/dist/parser/spacing.js +1 -1
- package/dist/parser/spacing.test.js +1 -1
- package/dist/parser/typography.test.js +1 -1
- package/dist/runtime.cjs +1 -1
- package/dist/runtime.cjs.map +4 -4
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +4 -4
- package/package.json +6 -6
- package/src/babel/plugin/componentScope.ts +87 -0
- package/src/babel/plugin/state.ts +185 -0
- package/src/babel/plugin/visitors/className.test.ts +1625 -0
- package/src/babel/plugin/visitors/className.ts +652 -0
- package/src/babel/plugin/visitors/className.windowDimensions.test.ts +406 -0
- package/src/babel/plugin/visitors/imports.test.ts +88 -0
- package/src/babel/plugin/visitors/imports.ts +116 -0
- package/src/babel/plugin/visitors/program.test.ts +325 -0
- package/src/babel/plugin/visitors/program.ts +116 -0
- package/src/babel/plugin/visitors/tw.test.ts +771 -0
- package/src/babel/plugin/visitors/tw.ts +148 -0
- package/src/babel/plugin.ts +28 -963
- package/src/babel/utils/colorSchemeModifierProcessing.ts +11 -0
- package/src/babel/utils/componentSupport.test.ts +20 -7
- package/src/babel/utils/componentSupport.ts +2 -0
- package/src/babel/utils/directionalModifierProcessing.ts +99 -0
- package/src/babel/utils/modifierProcessing.ts +21 -0
- package/src/babel/utils/platformModifierProcessing.ts +11 -0
- package/src/babel/utils/styleInjection.ts +253 -7
- package/src/babel/utils/twProcessing.ts +103 -3
- package/src/babel/utils/windowDimensionsProcessing.ts +121 -0
- package/src/components/TouchableOpacity.tsx +71 -0
- package/src/components/index.ts +3 -0
- package/src/config/markers.ts +5 -0
- package/src/index.ts +4 -5
- package/src/parser/borders.test.ts +162 -0
- package/src/parser/borders.ts +67 -9
- package/src/parser/colors.test.ts +249 -0
- package/src/parser/colors.ts +38 -0
- package/src/parser/index.ts +4 -2
- package/src/parser/layout.test.ts +74 -0
- package/src/parser/layout.ts +94 -0
- package/src/parser/modifiers.test.ts +206 -0
- package/src/parser/modifiers.ts +62 -3
- package/src/parser/sizing.ts +11 -0
- package/src/parser/spacing.test.ts +66 -0
- package/src/parser/spacing.ts +15 -5
- package/src/parser/typography.test.ts +8 -0
- package/src/parser/typography.ts +4 -0
|
@@ -0,0 +1,1625 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-empty-function */
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { transform } from "../../../../test/helpers/babelTransform.js";
|
|
4
|
+
|
|
5
|
+
describe("className visitor - basic transformation", () => {
|
|
6
|
+
it("should still transform className props", () => {
|
|
7
|
+
const input = `
|
|
8
|
+
import { View } from 'react-native';
|
|
9
|
+
export function Component() {
|
|
10
|
+
return <View className="m-4 p-2 bg-blue-500" />;
|
|
11
|
+
}
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
const output = transform(input, undefined, true); // Enable JSX
|
|
15
|
+
|
|
16
|
+
// Should have StyleSheet
|
|
17
|
+
expect(output).toContain("StyleSheet.create");
|
|
18
|
+
expect(output).toContain("_twStyles");
|
|
19
|
+
|
|
20
|
+
// Should replace className with style
|
|
21
|
+
expect(output).not.toContain("className");
|
|
22
|
+
expect(output).toContain("style:");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should work with both tw and className in same file", () => {
|
|
26
|
+
const input = `
|
|
27
|
+
import { tw } from '@mgcrea/react-native-tailwind';
|
|
28
|
+
import { View } from 'react-native';
|
|
29
|
+
|
|
30
|
+
const styles = tw\`bg-red-500\`;
|
|
31
|
+
|
|
32
|
+
export function Component() {
|
|
33
|
+
return <View className="m-4 p-2" />;
|
|
34
|
+
}
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
const output = transform(input, undefined, true); // Enable JSX
|
|
38
|
+
|
|
39
|
+
// Should have both styles in StyleSheet
|
|
40
|
+
expect(output).toContain("_bg_red_500");
|
|
41
|
+
expect(output).toContain("_m_4_p_2");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should merge className with function-based style prop", () => {
|
|
45
|
+
const input = `
|
|
46
|
+
import { TextInput } from 'react-native';
|
|
47
|
+
export function Component() {
|
|
48
|
+
return (
|
|
49
|
+
<TextInput
|
|
50
|
+
className="border border-gray-300 bg-gray-100"
|
|
51
|
+
style={({ focused, disabled }) => [
|
|
52
|
+
baseStyles,
|
|
53
|
+
focused && focusedStyles,
|
|
54
|
+
]}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
const output = transform(input, undefined, true); // Enable JSX
|
|
61
|
+
|
|
62
|
+
// Should have StyleSheet with className styles
|
|
63
|
+
expect(output).toContain("StyleSheet.create");
|
|
64
|
+
// Style keys are sorted alphabetically: bg-gray-100 comes before border
|
|
65
|
+
expect(output).toContain("_bg_gray_100_border_border_gray_300");
|
|
66
|
+
|
|
67
|
+
// Should create a wrapper function that merges both
|
|
68
|
+
// The wrapper should call the original function and merge results
|
|
69
|
+
expect(output).toContain("_state");
|
|
70
|
+
expect(output).toContain("_twStyles._bg_gray_100_border_border_gray_300");
|
|
71
|
+
|
|
72
|
+
// Should not have className in output
|
|
73
|
+
expect(output).not.toContain("className");
|
|
74
|
+
|
|
75
|
+
// Should have a function that accepts state and returns an array
|
|
76
|
+
expect(output).toMatch(/_state\s*=>/);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should preserve 'use client' directive when injecting StyleSheet.create", () => {
|
|
80
|
+
const input = `
|
|
81
|
+
'use client';
|
|
82
|
+
import { View } from 'react-native';
|
|
83
|
+
export function Component() {
|
|
84
|
+
return <View className="m-4 p-2" />;
|
|
85
|
+
}
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
const output = transform(input, undefined, true);
|
|
89
|
+
|
|
90
|
+
// 'use client' should be the first statement
|
|
91
|
+
const lines = output.split("\n").filter((l: string) => l.trim());
|
|
92
|
+
const useClientIndex = lines.findIndex(
|
|
93
|
+
(l: string) => l.includes("'use client'") || l.includes('"use client"'),
|
|
94
|
+
);
|
|
95
|
+
expect(useClientIndex).toBe(0);
|
|
96
|
+
|
|
97
|
+
// StyleSheet.create should be in the output
|
|
98
|
+
expect(output).toContain("StyleSheet.create");
|
|
99
|
+
expect(output).toContain("_twStyles");
|
|
100
|
+
|
|
101
|
+
// Imports should come after 'use client', before StyleSheet.create
|
|
102
|
+
const importIndex = lines.findIndex((l: string) => l.includes("import"));
|
|
103
|
+
const styleSheetIndex = lines.findIndex((l: string) => l.includes("StyleSheet.create"));
|
|
104
|
+
expect(importIndex).toBeGreaterThan(useClientIndex);
|
|
105
|
+
expect(styleSheetIndex).toBeGreaterThan(importIndex);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should merge dynamic className with function-based style prop", () => {
|
|
109
|
+
const input = `
|
|
110
|
+
import { TextInput } from 'react-native';
|
|
111
|
+
export function Component({ isError }) {
|
|
112
|
+
return (
|
|
113
|
+
<TextInput
|
|
114
|
+
className={\`border \${isError ? 'border-red-500' : 'border-gray-300'}\`}
|
|
115
|
+
style={({ focused }) => [
|
|
116
|
+
baseStyles,
|
|
117
|
+
focused && focusedStyles,
|
|
118
|
+
]}
|
|
119
|
+
/>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
`;
|
|
123
|
+
|
|
124
|
+
const output = transform(input, undefined, true); // Enable JSX
|
|
125
|
+
|
|
126
|
+
// Should have StyleSheet with both className styles
|
|
127
|
+
expect(output).toContain("StyleSheet.create");
|
|
128
|
+
expect(output).toContain("_border");
|
|
129
|
+
expect(output).toContain("_border_red_500");
|
|
130
|
+
expect(output).toContain("_border_gray_300");
|
|
131
|
+
|
|
132
|
+
// Should create a wrapper function that merges dynamic styles with function result
|
|
133
|
+
expect(output).toContain("_state");
|
|
134
|
+
|
|
135
|
+
// Should not have className in output
|
|
136
|
+
expect(output).not.toContain("className");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should transform className={"..."} (string literal in expression container)', () => {
|
|
140
|
+
const input = `
|
|
141
|
+
import { View } from 'react-native';
|
|
142
|
+
export function Component() {
|
|
143
|
+
return <View className={"flex-row items-center justify-start"} />;
|
|
144
|
+
}
|
|
145
|
+
`;
|
|
146
|
+
|
|
147
|
+
const output = transform(input, undefined, true);
|
|
148
|
+
|
|
149
|
+
// Should have StyleSheet
|
|
150
|
+
expect(output).toContain("StyleSheet.create");
|
|
151
|
+
expect(output).toContain("_twStyles");
|
|
152
|
+
|
|
153
|
+
// Should replace className with style
|
|
154
|
+
expect(output).not.toContain("className");
|
|
155
|
+
expect(output).toContain("style:");
|
|
156
|
+
|
|
157
|
+
// Should have the expected style keys
|
|
158
|
+
expect(output).toContain("_flex_row_items_center_justify_start");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should transform className={"..."} with modifiers', () => {
|
|
162
|
+
const input = `
|
|
163
|
+
import { Pressable } from 'react-native';
|
|
164
|
+
export function Component() {
|
|
165
|
+
return <Pressable className={"bg-blue-500 active:bg-blue-700 p-4"} />;
|
|
166
|
+
}
|
|
167
|
+
`;
|
|
168
|
+
|
|
169
|
+
const output = transform(input, undefined, true);
|
|
170
|
+
|
|
171
|
+
// Should have StyleSheet with both base and active styles
|
|
172
|
+
expect(output).toContain("_bg_blue_500_p_4");
|
|
173
|
+
expect(output).toContain("_active_bg_blue_700");
|
|
174
|
+
|
|
175
|
+
// Should have style function for active modifier (Pressable uses 'pressed' parameter)
|
|
176
|
+
expect(output).toMatch(/(pressed|_state)/);
|
|
177
|
+
|
|
178
|
+
// Should not have className in output
|
|
179
|
+
expect(output).not.toContain("className");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should transform className={"..."} with platform modifiers', () => {
|
|
183
|
+
const input = `
|
|
184
|
+
import { View } from 'react-native';
|
|
185
|
+
export function Component() {
|
|
186
|
+
return <View className={"p-4 ios:p-6 android:p-8"} />;
|
|
187
|
+
}
|
|
188
|
+
`;
|
|
189
|
+
|
|
190
|
+
const output = transform(input, undefined, true);
|
|
191
|
+
|
|
192
|
+
// Should have Platform import
|
|
193
|
+
expect(output).toContain("Platform");
|
|
194
|
+
expect(output).toMatch(/from ['"]react-native['"]/); // Match both single and double quotes
|
|
195
|
+
|
|
196
|
+
// Should have Platform.select
|
|
197
|
+
expect(output).toContain("Platform.select");
|
|
198
|
+
|
|
199
|
+
// Should have platform-specific styles
|
|
200
|
+
expect(output).toContain("_ios_p_6");
|
|
201
|
+
expect(output).toContain("_android_p_8");
|
|
202
|
+
|
|
203
|
+
// Should not have className in output
|
|
204
|
+
expect(output).not.toContain("className");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should handle empty className={""}', () => {
|
|
208
|
+
const input = `
|
|
209
|
+
import { View } from 'react-native';
|
|
210
|
+
export function Component() {
|
|
211
|
+
return <View className={""} />;
|
|
212
|
+
}
|
|
213
|
+
`;
|
|
214
|
+
|
|
215
|
+
const output = transform(input, undefined, true);
|
|
216
|
+
|
|
217
|
+
// Should remove empty className attribute entirely
|
|
218
|
+
expect(output).not.toContain("className");
|
|
219
|
+
expect(output).not.toContain("style=");
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("className visitor - placeholder: modifier", () => {
|
|
224
|
+
it("should transform placeholder:text-{color} to placeholderTextColor prop", () => {
|
|
225
|
+
const input = `
|
|
226
|
+
import { TextInput } from 'react-native';
|
|
227
|
+
export function Component() {
|
|
228
|
+
return (
|
|
229
|
+
<TextInput
|
|
230
|
+
className="border-2 placeholder:text-gray-400"
|
|
231
|
+
placeholder="Email"
|
|
232
|
+
/>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
`;
|
|
236
|
+
|
|
237
|
+
const output = transform(input, undefined, true);
|
|
238
|
+
|
|
239
|
+
// Should have placeholderTextColor prop with correct hex value (from custom palette)
|
|
240
|
+
expect(output).toContain('placeholderTextColor: "#99a1af"');
|
|
241
|
+
|
|
242
|
+
// Should still have style for border-2
|
|
243
|
+
expect(output).toContain("StyleSheet.create");
|
|
244
|
+
expect(output).toContain("_border_2");
|
|
245
|
+
|
|
246
|
+
// Should not have className in output
|
|
247
|
+
expect(output).not.toContain("className");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("should support placeholder colors with opacity", () => {
|
|
251
|
+
const input = `
|
|
252
|
+
import { TextInput } from 'react-native';
|
|
253
|
+
export function Component() {
|
|
254
|
+
return <TextInput className="placeholder:text-red-500/50" />;
|
|
255
|
+
}
|
|
256
|
+
`;
|
|
257
|
+
|
|
258
|
+
const output = transform(input, undefined, true);
|
|
259
|
+
|
|
260
|
+
// Should have 8-digit hex with alpha channel (custom palette red-500, uppercased)
|
|
261
|
+
expect(output).toContain('placeholderTextColor: "#FB2C3680"');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("should support arbitrary placeholder colors", () => {
|
|
265
|
+
const input = `
|
|
266
|
+
import { TextInput } from 'react-native';
|
|
267
|
+
export function Component() {
|
|
268
|
+
return <TextInput className="placeholder:text-[#ff0000]" />;
|
|
269
|
+
}
|
|
270
|
+
`;
|
|
271
|
+
|
|
272
|
+
const output = transform(input, undefined, true);
|
|
273
|
+
|
|
274
|
+
expect(output).toContain('placeholderTextColor: "#ff0000"');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("should combine placeholder: with other modifiers", () => {
|
|
278
|
+
const input = `
|
|
279
|
+
import { TextInput } from 'react-native';
|
|
280
|
+
export function Component() {
|
|
281
|
+
return (
|
|
282
|
+
<TextInput
|
|
283
|
+
className="border-2 focus:border-blue-500 placeholder:text-gray-400"
|
|
284
|
+
placeholder="Email"
|
|
285
|
+
/>
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
`;
|
|
289
|
+
|
|
290
|
+
const output = transform(input, undefined, true);
|
|
291
|
+
|
|
292
|
+
// Should have placeholderTextColor prop (custom palette gray-400)
|
|
293
|
+
expect(output).toContain('placeholderTextColor: "#99a1af"');
|
|
294
|
+
|
|
295
|
+
// Should have focus: modifier handling (style function)
|
|
296
|
+
expect(output).toContain("focused");
|
|
297
|
+
expect(output).toMatch(/style[\s\S]*=>/); // Style function
|
|
298
|
+
|
|
299
|
+
// Should not have className
|
|
300
|
+
expect(output).not.toContain("className");
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("should handle multiple placeholder: classes (last wins)", () => {
|
|
304
|
+
const input = `
|
|
305
|
+
import { TextInput } from 'react-native';
|
|
306
|
+
export function Component() {
|
|
307
|
+
return (
|
|
308
|
+
<TextInput className="placeholder:text-red-500 placeholder:text-blue-500" />
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
`;
|
|
312
|
+
|
|
313
|
+
const output = transform(input, undefined, true);
|
|
314
|
+
|
|
315
|
+
// Blue should win (last color, custom palette blue-500)
|
|
316
|
+
expect(output).toContain('placeholderTextColor: "#2b7fff"');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("should ignore non-text utilities in placeholder: modifier", () => {
|
|
320
|
+
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
321
|
+
|
|
322
|
+
const input = `
|
|
323
|
+
import { TextInput } from 'react-native';
|
|
324
|
+
export function Component() {
|
|
325
|
+
return (
|
|
326
|
+
<TextInput className="placeholder:font-bold placeholder:text-gray-400" />
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
`;
|
|
330
|
+
|
|
331
|
+
const output = transform(input, undefined, true);
|
|
332
|
+
|
|
333
|
+
// Should still have the valid text color (custom palette gray-400)
|
|
334
|
+
expect(output).toContain('placeholderTextColor: "#99a1af"');
|
|
335
|
+
|
|
336
|
+
// Should not have font-bold anywhere
|
|
337
|
+
expect(output).not.toContain("fontWeight");
|
|
338
|
+
|
|
339
|
+
consoleSpy.mockRestore();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it.skip("should work with custom colors", () => {
|
|
343
|
+
// Note: This test would require setting up a tailwind.config file
|
|
344
|
+
// For now, we'll skip custom color testing in Babel tests
|
|
345
|
+
// Custom colors are tested in the parser tests
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("should not transform placeholder: on non-TextInput elements", () => {
|
|
349
|
+
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
350
|
+
|
|
351
|
+
const input = `
|
|
352
|
+
import { View } from 'react-native';
|
|
353
|
+
export function Component() {
|
|
354
|
+
return <View className="placeholder:text-gray-400" />;
|
|
355
|
+
}
|
|
356
|
+
`;
|
|
357
|
+
|
|
358
|
+
const output = transform(input, undefined, true);
|
|
359
|
+
|
|
360
|
+
// Should not have placeholderTextColor prop (View doesn't support it)
|
|
361
|
+
expect(output).not.toContain("placeholderTextColor");
|
|
362
|
+
|
|
363
|
+
// Should warn about unsupported modifier
|
|
364
|
+
// (The warning happens because View doesn't support any modifiers)
|
|
365
|
+
|
|
366
|
+
consoleSpy.mockRestore();
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
describe("className visitor - platform modifiers", () => {
|
|
371
|
+
it("should transform platform modifiers to Platform.select()", () => {
|
|
372
|
+
const input = `
|
|
373
|
+
import React from 'react';
|
|
374
|
+
import { View } from 'react-native';
|
|
375
|
+
|
|
376
|
+
export function Component() {
|
|
377
|
+
return (
|
|
378
|
+
<View className="p-4 ios:p-6 android:p-8" />
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
`;
|
|
382
|
+
|
|
383
|
+
const output = transform(input, undefined, true);
|
|
384
|
+
|
|
385
|
+
// Should import Platform from react-native
|
|
386
|
+
expect(output).toContain("Platform");
|
|
387
|
+
expect(output).toMatch(/import.*Platform.*from ['"]react-native['"]/);
|
|
388
|
+
|
|
389
|
+
// Should generate Platform.select()
|
|
390
|
+
expect(output).toContain("Platform.select");
|
|
391
|
+
|
|
392
|
+
// Should have base padding style
|
|
393
|
+
expect(output).toContain("_p_4");
|
|
394
|
+
|
|
395
|
+
// Should have iOS and Android specific styles
|
|
396
|
+
expect(output).toContain("_ios_p_6");
|
|
397
|
+
expect(output).toContain("_android_p_8");
|
|
398
|
+
|
|
399
|
+
// Should have correct style values in StyleSheet.create
|
|
400
|
+
expect(output).toMatch(/padding:\s*16/); // p-4
|
|
401
|
+
expect(output).toMatch(/padding:\s*24/); // p-6 (ios)
|
|
402
|
+
expect(output).toMatch(/padding:\s*32/); // p-8 (android)
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("should support multiple platform modifiers on same element", () => {
|
|
406
|
+
const input = `
|
|
407
|
+
import React from 'react';
|
|
408
|
+
import { View } from 'react-native';
|
|
409
|
+
|
|
410
|
+
export function Component() {
|
|
411
|
+
return (
|
|
412
|
+
<View className="bg-white ios:bg-blue-50 android:bg-green-50 p-4 ios:p-6 android:p-8" />
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
`;
|
|
416
|
+
|
|
417
|
+
const output = transform(input, undefined, true);
|
|
418
|
+
|
|
419
|
+
// Should have Platform import
|
|
420
|
+
expect(output).toContain("Platform");
|
|
421
|
+
|
|
422
|
+
// Should have base styles (combined key)
|
|
423
|
+
expect(output).toContain("_bg_white_p_4");
|
|
424
|
+
|
|
425
|
+
// Should have iOS specific styles (combined key for multiple ios: modifiers)
|
|
426
|
+
expect(output).toContain("_ios_bg_blue_50_p_6");
|
|
427
|
+
|
|
428
|
+
// Should have Android specific styles (combined key for multiple android: modifiers)
|
|
429
|
+
expect(output).toContain("_android_bg_green_50_p_8");
|
|
430
|
+
|
|
431
|
+
// Should contain Platform.select with both platforms
|
|
432
|
+
expect(output).toMatch(/Platform\.select\s*\(\s*\{[\s\S]*ios:/);
|
|
433
|
+
expect(output).toMatch(/Platform\.select\s*\(\s*\{[\s\S]*android:/);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("should support web platform modifier", () => {
|
|
437
|
+
const input = `
|
|
438
|
+
import React from 'react';
|
|
439
|
+
import { View } from 'react-native';
|
|
440
|
+
|
|
441
|
+
export function Component() {
|
|
442
|
+
return (
|
|
443
|
+
<View className="p-4 web:p-2" />
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
`;
|
|
447
|
+
|
|
448
|
+
const output = transform(input, undefined, true);
|
|
449
|
+
|
|
450
|
+
// Should have Platform.select with web
|
|
451
|
+
expect(output).toContain("Platform.select");
|
|
452
|
+
expect(output).toContain("web:");
|
|
453
|
+
expect(output).toContain("_web_p_2");
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it("should work with platform modifiers on all components", () => {
|
|
457
|
+
const input = `
|
|
458
|
+
import React from 'react';
|
|
459
|
+
import { View, Text, ScrollView } from 'react-native';
|
|
460
|
+
|
|
461
|
+
export function Component() {
|
|
462
|
+
return (
|
|
463
|
+
<View className="ios:bg-blue-500 android:bg-green-500">
|
|
464
|
+
<Text className="ios:text-lg android:text-xl">Platform text</Text>
|
|
465
|
+
<ScrollView contentContainerClassName="ios:p-4 android:p-8" />
|
|
466
|
+
</View>
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
`;
|
|
470
|
+
|
|
471
|
+
const output = transform(input, undefined, true);
|
|
472
|
+
|
|
473
|
+
// Should work on View - check for Platform.select separately (not checking style= format)
|
|
474
|
+
expect(output).toContain("Platform.select");
|
|
475
|
+
|
|
476
|
+
// Should work on Text
|
|
477
|
+
expect(output).toContain("_ios_text_lg");
|
|
478
|
+
expect(output).toContain("_android_text_xl");
|
|
479
|
+
|
|
480
|
+
// Should work on ScrollView contentContainerStyle
|
|
481
|
+
expect(output).toContain("contentContainerStyle");
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it("should combine platform modifiers with state modifiers", () => {
|
|
485
|
+
const input = `
|
|
486
|
+
import React from 'react';
|
|
487
|
+
import { Pressable, Text } from 'react-native';
|
|
488
|
+
|
|
489
|
+
export function Component() {
|
|
490
|
+
return (
|
|
491
|
+
<Pressable className="bg-blue-500 active:bg-blue-700 ios:shadow-md android:shadow-sm p-4">
|
|
492
|
+
<Text className="text-white">Button</Text>
|
|
493
|
+
</Pressable>
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
`;
|
|
497
|
+
|
|
498
|
+
const output = transform(input, undefined, true);
|
|
499
|
+
|
|
500
|
+
// Should have Platform.select for platform modifiers
|
|
501
|
+
expect(output).toContain("Platform.select");
|
|
502
|
+
expect(output).toContain("_ios_shadow_md");
|
|
503
|
+
expect(output).toContain("_android_shadow_sm");
|
|
504
|
+
|
|
505
|
+
// Should have state modifier function for active
|
|
506
|
+
expect(output).toMatch(/\(\s*\{\s*pressed\s*\}\s*\)\s*=>/);
|
|
507
|
+
expect(output).toContain("pressed");
|
|
508
|
+
expect(output).toContain("_active_bg_blue_700");
|
|
509
|
+
|
|
510
|
+
// Should have base styles
|
|
511
|
+
expect(output).toContain("_bg_blue_500");
|
|
512
|
+
expect(output).toContain("_p_4");
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it("should handle platform-specific colors", () => {
|
|
516
|
+
const input = `
|
|
517
|
+
import React from 'react';
|
|
518
|
+
import { View, Text } from 'react-native';
|
|
519
|
+
|
|
520
|
+
export function Component() {
|
|
521
|
+
return (
|
|
522
|
+
<View className="bg-gray-100 ios:bg-blue-50 android:bg-green-50">
|
|
523
|
+
<Text className="text-gray-900 ios:text-blue-900 android:text-green-900">
|
|
524
|
+
Platform colors
|
|
525
|
+
</Text>
|
|
526
|
+
</View>
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
`;
|
|
530
|
+
|
|
531
|
+
const output = transform(input, undefined, true);
|
|
532
|
+
|
|
533
|
+
// Should have color values in StyleSheet
|
|
534
|
+
expect(output).toMatch(/#[0-9A-F]{6}/i); // Hex color format
|
|
535
|
+
|
|
536
|
+
// Should have platform-specific color classes
|
|
537
|
+
expect(output).toContain("_ios_text_blue_900");
|
|
538
|
+
expect(output).toContain("_android_text_green_900");
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it("should only add Platform import once when needed", () => {
|
|
542
|
+
const input = `
|
|
543
|
+
import React from 'react';
|
|
544
|
+
import { View } from 'react-native';
|
|
545
|
+
|
|
546
|
+
export function Component() {
|
|
547
|
+
return (
|
|
548
|
+
<>
|
|
549
|
+
<View className="ios:p-4" />
|
|
550
|
+
<View className="android:p-8" />
|
|
551
|
+
<View className="ios:bg-blue-500" />
|
|
552
|
+
</>
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
`;
|
|
556
|
+
|
|
557
|
+
const output = transform(input, undefined, true);
|
|
558
|
+
|
|
559
|
+
// Should have Platform import
|
|
560
|
+
expect(output).toContain("Platform");
|
|
561
|
+
|
|
562
|
+
// Count how many times Platform is imported (should be once)
|
|
563
|
+
const platformImports = output.match(/import.*Platform.*from ['"]react-native['"]/g);
|
|
564
|
+
expect(platformImports).toHaveLength(1);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it("should merge with existing Platform import", () => {
|
|
568
|
+
const input = `
|
|
569
|
+
import React from 'react';
|
|
570
|
+
import { View, Platform } from 'react-native';
|
|
571
|
+
|
|
572
|
+
export function Component() {
|
|
573
|
+
return <View className="ios:p-4 android:p-8" />;
|
|
574
|
+
}
|
|
575
|
+
`;
|
|
576
|
+
|
|
577
|
+
const output = transform(input, undefined, true);
|
|
578
|
+
|
|
579
|
+
// Should still use Platform.select
|
|
580
|
+
expect(output).toContain("Platform.select");
|
|
581
|
+
|
|
582
|
+
// Should not duplicate Platform import - Platform appears in import and Platform.select calls
|
|
583
|
+
expect(output).toMatch(/Platform.*react-native/);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it("should handle platform modifiers without base classes", () => {
|
|
587
|
+
const input = `
|
|
588
|
+
import React from 'react';
|
|
589
|
+
import { View } from 'react-native';
|
|
590
|
+
|
|
591
|
+
export function Component() {
|
|
592
|
+
return <View className="ios:p-6 android:p-8" />;
|
|
593
|
+
}
|
|
594
|
+
`;
|
|
595
|
+
|
|
596
|
+
const output = transform(input, undefined, true);
|
|
597
|
+
|
|
598
|
+
// Should only have Platform.select, no base style
|
|
599
|
+
expect(output).toContain("Platform.select");
|
|
600
|
+
expect(output).toContain("_ios_p_6");
|
|
601
|
+
expect(output).toContain("_android_p_8");
|
|
602
|
+
|
|
603
|
+
// Should not have generic padding without platform prefix
|
|
604
|
+
// Check that non-platform-prefixed style keys don't exist
|
|
605
|
+
expect(output).not.toMatch(/(?<!_ios|_android|_web)_p_4:/);
|
|
606
|
+
expect(output).not.toMatch(/(?<!_ios|_android|_web)_p_6:/);
|
|
607
|
+
expect(output).not.toMatch(/(?<!_ios|_android|_web)_p_8:/);
|
|
608
|
+
});
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
describe("className visitor - color scheme modifiers", () => {
|
|
612
|
+
it("should transform dark: modifier to conditional expression", () => {
|
|
613
|
+
const input = `
|
|
614
|
+
import React from 'react';
|
|
615
|
+
import { View } from 'react-native';
|
|
616
|
+
|
|
617
|
+
export function Component() {
|
|
618
|
+
return (
|
|
619
|
+
<View className="bg-white dark:bg-gray-900" />
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
`;
|
|
623
|
+
|
|
624
|
+
const output = transform(input, undefined, true);
|
|
625
|
+
|
|
626
|
+
// Should import useColorScheme
|
|
627
|
+
expect(output).toContain("useColorScheme");
|
|
628
|
+
expect(output).toMatch(/import.*useColorScheme.*from ['"]react-native['"]/);
|
|
629
|
+
|
|
630
|
+
// Should inject colorScheme hook in component
|
|
631
|
+
expect(output).toContain("_twColorScheme");
|
|
632
|
+
expect(output).toContain("useColorScheme()");
|
|
633
|
+
|
|
634
|
+
// Should have base bg-white style
|
|
635
|
+
expect(output).toContain("_bg_white");
|
|
636
|
+
|
|
637
|
+
// Should have dark:bg-gray-900 style
|
|
638
|
+
expect(output).toContain("_dark_bg_gray_900");
|
|
639
|
+
|
|
640
|
+
// Should generate conditional: _twColorScheme === 'dark' && ...
|
|
641
|
+
expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it("should support both dark: and light: modifiers", () => {
|
|
645
|
+
const input = `
|
|
646
|
+
import React from 'react';
|
|
647
|
+
import { View } from 'react-native';
|
|
648
|
+
|
|
649
|
+
export function Component() {
|
|
650
|
+
return (
|
|
651
|
+
<View className="bg-gray-100 dark:bg-gray-900 light:bg-white" />
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
`;
|
|
655
|
+
|
|
656
|
+
const output = transform(input, undefined, true);
|
|
657
|
+
|
|
658
|
+
// Should have all three styles
|
|
659
|
+
expect(output).toContain("_bg_gray_100");
|
|
660
|
+
expect(output).toContain("_dark_bg_gray_900");
|
|
661
|
+
expect(output).toContain("_light_bg_white");
|
|
662
|
+
|
|
663
|
+
// Should have both conditionals
|
|
664
|
+
expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
|
|
665
|
+
expect(output).toMatch(/_twColorScheme\s*===\s*['"]light['"]/);
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
it("should inject hook once for multiple elements with color scheme modifiers", () => {
|
|
669
|
+
const input = `
|
|
670
|
+
import React from 'react';
|
|
671
|
+
import { View, Text } from 'react-native';
|
|
672
|
+
|
|
673
|
+
export function Component() {
|
|
674
|
+
return (
|
|
675
|
+
<>
|
|
676
|
+
<View className="dark:bg-gray-900" />
|
|
677
|
+
<Text className="dark:text-white" />
|
|
678
|
+
<View className="light:bg-white" />
|
|
679
|
+
</>
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
`;
|
|
683
|
+
|
|
684
|
+
const output = transform(input, undefined, true);
|
|
685
|
+
|
|
686
|
+
// Count occurrences of useColorScheme() call - should be exactly 1
|
|
687
|
+
const hookCallMatches = output.match(/=\s*useColorScheme\(\)/g);
|
|
688
|
+
expect(hookCallMatches).toHaveLength(1);
|
|
689
|
+
|
|
690
|
+
// Should have color scheme variable
|
|
691
|
+
expect(output).toContain("_twColorScheme");
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
it("should work with color scheme and platform modifiers together", () => {
|
|
695
|
+
const input = `
|
|
696
|
+
import React from 'react';
|
|
697
|
+
import { View } from 'react-native';
|
|
698
|
+
|
|
699
|
+
export function Component() {
|
|
700
|
+
return (
|
|
701
|
+
<View className="p-4 ios:p-6 dark:bg-gray-900" />
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
`;
|
|
705
|
+
|
|
706
|
+
const output = transform(input, undefined, true);
|
|
707
|
+
|
|
708
|
+
// Should have Platform import
|
|
709
|
+
expect(output).toContain("Platform");
|
|
710
|
+
|
|
711
|
+
// Should have useColorScheme import
|
|
712
|
+
expect(output).toContain("useColorScheme");
|
|
713
|
+
|
|
714
|
+
// Should have Platform.select for ios:
|
|
715
|
+
expect(output).toContain("Platform.select");
|
|
716
|
+
expect(output).toContain("_ios_p_6");
|
|
717
|
+
|
|
718
|
+
// Should have color scheme conditional for dark:
|
|
719
|
+
expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
|
|
720
|
+
expect(output).toContain("_dark_bg_gray_900");
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
it("should only add useColorScheme import once when needed", () => {
|
|
724
|
+
const input = `
|
|
725
|
+
import React from 'react';
|
|
726
|
+
import { View } from 'react-native';
|
|
727
|
+
|
|
728
|
+
export function Component() {
|
|
729
|
+
return (
|
|
730
|
+
<>
|
|
731
|
+
<View className="dark:bg-black" />
|
|
732
|
+
<View className="light:bg-white" />
|
|
733
|
+
</>
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
`;
|
|
737
|
+
|
|
738
|
+
const output = transform(input, undefined, true);
|
|
739
|
+
|
|
740
|
+
// Count useColorScheme imports
|
|
741
|
+
const importMatches = output.match(/import.*useColorScheme.*from ['"]react-native['"]/g);
|
|
742
|
+
expect(importMatches).toHaveLength(1);
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it("should merge with existing useColorScheme import", () => {
|
|
746
|
+
const input = `
|
|
747
|
+
import React from 'react';
|
|
748
|
+
import { View, useColorScheme } from 'react-native';
|
|
749
|
+
|
|
750
|
+
export function Component() {
|
|
751
|
+
return <View className="dark:bg-gray-900" />;
|
|
752
|
+
}
|
|
753
|
+
`;
|
|
754
|
+
|
|
755
|
+
const output = transform(input, undefined, true);
|
|
756
|
+
|
|
757
|
+
// Should still use useColorScheme
|
|
758
|
+
expect(output).toContain("useColorScheme");
|
|
759
|
+
|
|
760
|
+
// Should inject hook call
|
|
761
|
+
expect(output).toContain("_twColorScheme");
|
|
762
|
+
expect(output).toContain("useColorScheme()");
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it("should work with concise arrow functions", () => {
|
|
766
|
+
const input = `
|
|
767
|
+
import React from 'react';
|
|
768
|
+
import { View } from 'react-native';
|
|
769
|
+
|
|
770
|
+
const Component = () => <View className="dark:bg-gray-900" />;
|
|
771
|
+
`;
|
|
772
|
+
|
|
773
|
+
const output = transform(input, undefined, true);
|
|
774
|
+
|
|
775
|
+
// Should inject useColorScheme import
|
|
776
|
+
expect(output).toContain("useColorScheme");
|
|
777
|
+
|
|
778
|
+
// Should convert concise arrow to block statement and inject hook
|
|
779
|
+
expect(output).toContain("_twColorScheme");
|
|
780
|
+
expect(output).toContain("useColorScheme()");
|
|
781
|
+
expect(output).toContain("return");
|
|
782
|
+
|
|
783
|
+
// Should have the style
|
|
784
|
+
expect(output).toContain("_dark_bg_gray_900");
|
|
785
|
+
expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
it("should inject hook at component level when dark: used in nested callback", () => {
|
|
789
|
+
const input = `
|
|
790
|
+
import React from 'react';
|
|
791
|
+
import { View } from 'react-native';
|
|
792
|
+
|
|
793
|
+
export function Component() {
|
|
794
|
+
const items = [1, 2, 3];
|
|
795
|
+
return (
|
|
796
|
+
<View>
|
|
797
|
+
{items.map(item => (
|
|
798
|
+
<View key={item} className="dark:bg-gray-900" />
|
|
799
|
+
))}
|
|
800
|
+
</View>
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
`;
|
|
804
|
+
|
|
805
|
+
const output = transform(input, undefined, true);
|
|
806
|
+
|
|
807
|
+
// Should inject hook at Component level (not in map callback)
|
|
808
|
+
expect(output).toContain("_twColorScheme");
|
|
809
|
+
expect(output).toContain("useColorScheme()");
|
|
810
|
+
|
|
811
|
+
// Hook should be injected in Component function, not in map callback
|
|
812
|
+
// Count occurrences - should be exactly 1 at Component level
|
|
813
|
+
const hookCallMatches = output.match(/=\s*useColorScheme\(\)/g);
|
|
814
|
+
expect(hookCallMatches).toHaveLength(1);
|
|
815
|
+
|
|
816
|
+
// Should still generate conditional expression
|
|
817
|
+
expect(output).toContain("_dark_bg_gray_900");
|
|
818
|
+
expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
it("should handle dynamic expressions with dark:/light: modifiers", () => {
|
|
822
|
+
const input = `
|
|
823
|
+
import React from 'react';
|
|
824
|
+
import { View } from 'react-native';
|
|
825
|
+
|
|
826
|
+
export function Component({ isActive }) {
|
|
827
|
+
return (
|
|
828
|
+
<View className={\`p-4 \${isActive ? "dark:bg-blue-500" : "dark:bg-gray-900"}\`} />
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
`;
|
|
832
|
+
|
|
833
|
+
const output = transform(input, undefined, true);
|
|
834
|
+
|
|
835
|
+
// Should inject useColorScheme
|
|
836
|
+
expect(output).toContain("useColorScheme");
|
|
837
|
+
expect(output).toContain("_twColorScheme");
|
|
838
|
+
|
|
839
|
+
// Should have both dark styles
|
|
840
|
+
expect(output).toContain("_dark_bg_blue_500");
|
|
841
|
+
expect(output).toContain("_dark_bg_gray_900");
|
|
842
|
+
|
|
843
|
+
// Should have conditional expressions for color scheme
|
|
844
|
+
expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
it("should handle dynamic expressions with platform modifiers", () => {
|
|
848
|
+
const input = `
|
|
849
|
+
import React from 'react';
|
|
850
|
+
import { View } from 'react-native';
|
|
851
|
+
|
|
852
|
+
export function Component({ isLarge }) {
|
|
853
|
+
return (
|
|
854
|
+
<View className={\`p-4 \${isLarge ? "ios:p-8" : "ios:p-6"}\`} />
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
`;
|
|
858
|
+
|
|
859
|
+
const output = transform(input, undefined, true);
|
|
860
|
+
|
|
861
|
+
// Should inject Platform import
|
|
862
|
+
expect(output).toContain("Platform");
|
|
863
|
+
|
|
864
|
+
// Should have both ios styles
|
|
865
|
+
expect(output).toContain("_ios_p_8");
|
|
866
|
+
expect(output).toContain("_ios_p_6");
|
|
867
|
+
|
|
868
|
+
// Should have Platform.select
|
|
869
|
+
expect(output).toContain("Platform.select");
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
it("should skip color scheme modifiers when used outside component scope", () => {
|
|
873
|
+
// Suppress console.warn for this test
|
|
874
|
+
const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
875
|
+
|
|
876
|
+
const input = `
|
|
877
|
+
import React from 'react';
|
|
878
|
+
import { View } from 'react-native';
|
|
879
|
+
|
|
880
|
+
// Class component - no function component scope
|
|
881
|
+
class MyComponent extends React.Component {
|
|
882
|
+
render() {
|
|
883
|
+
return <View className="p-4 dark:bg-gray-900" />;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
`;
|
|
887
|
+
|
|
888
|
+
const output = transform(input, undefined, true);
|
|
889
|
+
|
|
890
|
+
// Should warn about invalid context
|
|
891
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
892
|
+
expect.stringContaining("dark:/light: modifiers require a function component scope"),
|
|
893
|
+
);
|
|
894
|
+
|
|
895
|
+
// Should NOT inject useColorScheme import (no valid component scope)
|
|
896
|
+
expect(output).not.toContain("useColorScheme");
|
|
897
|
+
|
|
898
|
+
// Should NOT have _twColorScheme variable reference (would cause ReferenceError)
|
|
899
|
+
expect(output).not.toContain("_twColorScheme");
|
|
900
|
+
|
|
901
|
+
// Should NOT have dark: style conditional (skipped due to no component scope)
|
|
902
|
+
expect(output).not.toContain("_dark_bg_gray_900");
|
|
903
|
+
|
|
904
|
+
// Should still transform base classes (p-4)
|
|
905
|
+
expect(output).toContain("_p_4");
|
|
906
|
+
|
|
907
|
+
consoleWarnSpy.mockRestore();
|
|
908
|
+
});
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
describe("className visitor - custom color scheme hook", () => {
|
|
912
|
+
it("should use custom import source for color scheme hook", () => {
|
|
913
|
+
const input = `
|
|
914
|
+
import React from 'react';
|
|
915
|
+
import { View } from 'react-native';
|
|
916
|
+
|
|
917
|
+
export function Component() {
|
|
918
|
+
return <View className="dark:bg-gray-900" />;
|
|
919
|
+
}
|
|
920
|
+
`;
|
|
921
|
+
|
|
922
|
+
const output = transform(
|
|
923
|
+
input,
|
|
924
|
+
{
|
|
925
|
+
colorScheme: {
|
|
926
|
+
importFrom: "@/hooks/useColorScheme",
|
|
927
|
+
importName: "useColorScheme",
|
|
928
|
+
},
|
|
929
|
+
},
|
|
930
|
+
true,
|
|
931
|
+
);
|
|
932
|
+
|
|
933
|
+
// Should import from custom source
|
|
934
|
+
expect(output).toContain('from "@/hooks/useColorScheme"');
|
|
935
|
+
expect(output).not.toContain('useColorScheme } from "react-native"');
|
|
936
|
+
|
|
937
|
+
// Should inject hook call
|
|
938
|
+
expect(output).toContain("_twColorScheme = useColorScheme()");
|
|
939
|
+
|
|
940
|
+
// Should have conditional styling
|
|
941
|
+
expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
it("should use custom hook name", () => {
|
|
945
|
+
const input = `
|
|
946
|
+
import React from 'react';
|
|
947
|
+
import { View } from 'react-native';
|
|
948
|
+
|
|
949
|
+
export function Component() {
|
|
950
|
+
return <View className="dark:bg-gray-900" />;
|
|
951
|
+
}
|
|
952
|
+
`;
|
|
953
|
+
|
|
954
|
+
const output = transform(
|
|
955
|
+
input,
|
|
956
|
+
{
|
|
957
|
+
colorScheme: {
|
|
958
|
+
importFrom: "@react-navigation/native",
|
|
959
|
+
importName: "useTheme",
|
|
960
|
+
},
|
|
961
|
+
},
|
|
962
|
+
true,
|
|
963
|
+
);
|
|
964
|
+
|
|
965
|
+
// Should import useTheme from React Navigation
|
|
966
|
+
expect(output).toContain('from "@react-navigation/native"');
|
|
967
|
+
expect(output).toContain("useTheme");
|
|
968
|
+
|
|
969
|
+
// Should call useTheme hook
|
|
970
|
+
expect(output).toContain("_twColorScheme = useTheme()");
|
|
971
|
+
|
|
972
|
+
// Should have conditional styling
|
|
973
|
+
expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
it("should merge custom hook with existing import from same source", () => {
|
|
977
|
+
const input = `
|
|
978
|
+
import React from 'react';
|
|
979
|
+
import { View, Text } from 'react-native';
|
|
980
|
+
import { useNavigation } from '@react-navigation/native';
|
|
981
|
+
|
|
982
|
+
export function Component() {
|
|
983
|
+
const navigation = useNavigation();
|
|
984
|
+
return (
|
|
985
|
+
<View className="dark:bg-gray-900">
|
|
986
|
+
<Text onPress={() => navigation.navigate('Home')}>Go Home</Text>
|
|
987
|
+
</View>
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
`;
|
|
991
|
+
|
|
992
|
+
const output = transform(
|
|
993
|
+
input,
|
|
994
|
+
{
|
|
995
|
+
colorScheme: {
|
|
996
|
+
importFrom: "@react-navigation/native",
|
|
997
|
+
importName: "useTheme",
|
|
998
|
+
},
|
|
999
|
+
},
|
|
1000
|
+
true,
|
|
1001
|
+
);
|
|
1002
|
+
|
|
1003
|
+
// Should merge with existing import (both useNavigation and useTheme in same import)
|
|
1004
|
+
expect(output).toMatch(
|
|
1005
|
+
/import\s+\{\s*useNavigation[^}]*useTheme[^}]*\}\s+from\s+['"]@react-navigation\/native['"]/,
|
|
1006
|
+
);
|
|
1007
|
+
expect(output).toContain("useNavigation()");
|
|
1008
|
+
expect(output).toContain("useTheme()");
|
|
1009
|
+
|
|
1010
|
+
// Should only have one import from that source
|
|
1011
|
+
const importCount = (output.match(/@react-navigation\/native/g) ?? []).length;
|
|
1012
|
+
expect(importCount).toBe(1);
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
it("should not duplicate custom hook if already imported", () => {
|
|
1016
|
+
const input = `
|
|
1017
|
+
import React from 'react';
|
|
1018
|
+
import { View } from 'react-native';
|
|
1019
|
+
import { useColorScheme } from '@/hooks/useColorScheme';
|
|
1020
|
+
|
|
1021
|
+
export function Component() {
|
|
1022
|
+
return <View className="dark:bg-gray-900" />;
|
|
1023
|
+
}
|
|
1024
|
+
`;
|
|
1025
|
+
|
|
1026
|
+
const output = transform(
|
|
1027
|
+
input,
|
|
1028
|
+
{
|
|
1029
|
+
colorScheme: {
|
|
1030
|
+
importFrom: "@/hooks/useColorScheme",
|
|
1031
|
+
importName: "useColorScheme",
|
|
1032
|
+
},
|
|
1033
|
+
},
|
|
1034
|
+
true,
|
|
1035
|
+
);
|
|
1036
|
+
|
|
1037
|
+
// Should not add duplicate import
|
|
1038
|
+
const importMatches = output.match(/import.*useColorScheme.*from ['"]@\/hooks\/useColorScheme['"]/g);
|
|
1039
|
+
expect(importMatches).toHaveLength(1);
|
|
1040
|
+
|
|
1041
|
+
// Should still inject hook call
|
|
1042
|
+
expect(output).toContain("_twColorScheme = useColorScheme()");
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
it("should use react-native by default when no custom config provided", () => {
|
|
1046
|
+
const input = `
|
|
1047
|
+
import React from 'react';
|
|
1048
|
+
import { View } from 'react-native';
|
|
1049
|
+
|
|
1050
|
+
export function Component() {
|
|
1051
|
+
return <View className="dark:bg-gray-900" />;
|
|
1052
|
+
}
|
|
1053
|
+
`;
|
|
1054
|
+
|
|
1055
|
+
const output = transform(input, undefined, true);
|
|
1056
|
+
|
|
1057
|
+
// Should use default react-native import (can be single or double quotes)
|
|
1058
|
+
expect(output).toMatch(/useColorScheme\s*}\s*from\s+['"]react-native['"]/);
|
|
1059
|
+
expect(output).not.toContain("@/hooks");
|
|
1060
|
+
expect(output).not.toContain("@react-navigation");
|
|
1061
|
+
|
|
1062
|
+
// Should inject hook call with default name
|
|
1063
|
+
expect(output).toContain("_twColorScheme = useColorScheme()");
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
it("should create separate import when only type-only import exists", () => {
|
|
1067
|
+
const input = `
|
|
1068
|
+
import React from 'react';
|
|
1069
|
+
import { View } from 'react-native';
|
|
1070
|
+
import type { NavigationProp } from '@react-navigation/native';
|
|
1071
|
+
|
|
1072
|
+
export function Component() {
|
|
1073
|
+
return <View className="dark:bg-gray-900" />;
|
|
1074
|
+
}
|
|
1075
|
+
`;
|
|
1076
|
+
|
|
1077
|
+
const output = transform(
|
|
1078
|
+
input,
|
|
1079
|
+
{
|
|
1080
|
+
colorScheme: {
|
|
1081
|
+
importFrom: "@react-navigation/native",
|
|
1082
|
+
importName: "useTheme",
|
|
1083
|
+
},
|
|
1084
|
+
},
|
|
1085
|
+
true,
|
|
1086
|
+
);
|
|
1087
|
+
|
|
1088
|
+
// TypeScript preset strips type-only imports, but the important thing is:
|
|
1089
|
+
// 1. useTheme hook is imported (not skipped thinking it was already imported)
|
|
1090
|
+
// 2. Hook is correctly called in the component
|
|
1091
|
+
expect(output).toMatch(/import\s+\{\s*useTheme\s*\}\s+from\s+['"]@react-navigation\/native['"]/);
|
|
1092
|
+
expect(output).toContain("_twColorScheme = useTheme()");
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
it("should use aliased identifier when hook is already imported with alias", () => {
|
|
1096
|
+
const input = `
|
|
1097
|
+
import React from 'react';
|
|
1098
|
+
import { View, Text } from 'react-native';
|
|
1099
|
+
import { useTheme as navTheme } from '@react-navigation/native';
|
|
1100
|
+
|
|
1101
|
+
export function Component() {
|
|
1102
|
+
const theme = navTheme();
|
|
1103
|
+
return (
|
|
1104
|
+
<View className="dark:bg-gray-900">
|
|
1105
|
+
<Text>{theme.dark ? 'Dark' : 'Light'}</Text>
|
|
1106
|
+
</View>
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
`;
|
|
1110
|
+
|
|
1111
|
+
const output = transform(
|
|
1112
|
+
input,
|
|
1113
|
+
{
|
|
1114
|
+
colorScheme: {
|
|
1115
|
+
importFrom: "@react-navigation/native",
|
|
1116
|
+
importName: "useTheme",
|
|
1117
|
+
},
|
|
1118
|
+
},
|
|
1119
|
+
true,
|
|
1120
|
+
);
|
|
1121
|
+
|
|
1122
|
+
// Should not add duplicate import
|
|
1123
|
+
const importMatches = output.match(
|
|
1124
|
+
/import\s+\{[^}]*useTheme[^}]*\}\s+from\s+['"]@react-navigation\/native['"]/g,
|
|
1125
|
+
);
|
|
1126
|
+
expect(importMatches).toHaveLength(1);
|
|
1127
|
+
|
|
1128
|
+
// Should still have the aliased import
|
|
1129
|
+
expect(output).toMatch(/useTheme\s+as\s+navTheme/);
|
|
1130
|
+
|
|
1131
|
+
// Should call the aliased name (navTheme), not the export name (useTheme)
|
|
1132
|
+
// Both the user's code and our injected hook should use navTheme
|
|
1133
|
+
expect(output).toContain("_twColorScheme = navTheme()");
|
|
1134
|
+
expect(output).not.toContain("_twColorScheme = useTheme()");
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
it("should not treat type-only imports as having the hook", () => {
|
|
1138
|
+
const input = `
|
|
1139
|
+
import React from 'react';
|
|
1140
|
+
import { View } from 'react-native';
|
|
1141
|
+
import type { useColorScheme } from 'react-native';
|
|
1142
|
+
|
|
1143
|
+
export function Component() {
|
|
1144
|
+
return <View className="dark:bg-gray-900" />;
|
|
1145
|
+
}
|
|
1146
|
+
`;
|
|
1147
|
+
|
|
1148
|
+
const output = transform(input, undefined, true);
|
|
1149
|
+
|
|
1150
|
+
// Should add a VALUE import for useColorScheme (type import doesn't count)
|
|
1151
|
+
expect(output).toMatch(/import\s+\{[^}]*useColorScheme[^}]*\}\s+from\s+['"]react-native['"]/);
|
|
1152
|
+
|
|
1153
|
+
// Should inject the hook
|
|
1154
|
+
expect(output).toContain("_twColorScheme = useColorScheme()");
|
|
1155
|
+
|
|
1156
|
+
// Should have both type-only and value imports in output
|
|
1157
|
+
// (TypeScript preset keeps type imports for type checking)
|
|
1158
|
+
const colorSchemeMatches = output.match(/useColorScheme/g);
|
|
1159
|
+
expect(colorSchemeMatches).toBeTruthy();
|
|
1160
|
+
if (colorSchemeMatches) {
|
|
1161
|
+
expect(colorSchemeMatches.length).toBeGreaterThanOrEqual(2); // At least in import and hook call
|
|
1162
|
+
}
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
it("should handle both type-only and aliased imports together", () => {
|
|
1166
|
+
const input = `
|
|
1167
|
+
import React from 'react';
|
|
1168
|
+
import { View, Text } from 'react-native';
|
|
1169
|
+
import type { Theme } from '@react-navigation/native';
|
|
1170
|
+
import { useTheme as getNavTheme } from '@react-navigation/native';
|
|
1171
|
+
|
|
1172
|
+
export function Component() {
|
|
1173
|
+
const theme = getNavTheme();
|
|
1174
|
+
return (
|
|
1175
|
+
<View className="dark:bg-gray-900">
|
|
1176
|
+
<Text>{theme.dark ? 'Dark Mode' : 'Light Mode'}</Text>
|
|
1177
|
+
</View>
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
`;
|
|
1181
|
+
|
|
1182
|
+
const output = transform(
|
|
1183
|
+
input,
|
|
1184
|
+
{
|
|
1185
|
+
colorScheme: {
|
|
1186
|
+
importFrom: "@react-navigation/native",
|
|
1187
|
+
importName: "useTheme",
|
|
1188
|
+
},
|
|
1189
|
+
},
|
|
1190
|
+
true,
|
|
1191
|
+
);
|
|
1192
|
+
|
|
1193
|
+
// TypeScript preset strips type-only imports
|
|
1194
|
+
// The important thing is: should not add duplicate import, and should use aliased name
|
|
1195
|
+
expect(output).toMatch(
|
|
1196
|
+
/import\s+\{[^}]*useTheme\s+as\s+getNavTheme[^}]*\}\s+from\s+['"]@react-navigation\/native['"]/,
|
|
1197
|
+
);
|
|
1198
|
+
|
|
1199
|
+
// Should not add duplicate import - useTheme should only appear in the aliased import
|
|
1200
|
+
const useThemeImports = output.match(
|
|
1201
|
+
/import\s+\{[^}]*useTheme[^}]*\}\s+from\s+['"]@react-navigation\/native['"]/g,
|
|
1202
|
+
);
|
|
1203
|
+
expect(useThemeImports).toHaveLength(1);
|
|
1204
|
+
|
|
1205
|
+
// Should call the aliased name for both user code and our injected hook
|
|
1206
|
+
expect(output).toContain("_twColorScheme = getNavTheme()");
|
|
1207
|
+
expect(output).not.toContain("_twColorScheme = useTheme()");
|
|
1208
|
+
});
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
describe("className visitor - directional border colors", () => {
|
|
1212
|
+
it("should transform directional border colors with preset values", () => {
|
|
1213
|
+
const input = `
|
|
1214
|
+
import { View } from 'react-native';
|
|
1215
|
+
export function Component() {
|
|
1216
|
+
return <View className="border-t-red-500 border-l-blue-500" />;
|
|
1217
|
+
}
|
|
1218
|
+
`;
|
|
1219
|
+
|
|
1220
|
+
const output = transform(input, undefined, true);
|
|
1221
|
+
|
|
1222
|
+
// Should have StyleSheet
|
|
1223
|
+
expect(output).toContain("StyleSheet.create");
|
|
1224
|
+
|
|
1225
|
+
// Should generate styles with borderTopColor and borderLeftColor
|
|
1226
|
+
expect(output).toMatch(/borderTopColor[:\s]*['"]#[0-9A-F]{6}['"]/i);
|
|
1227
|
+
expect(output).toMatch(/borderLeftColor[:\s]*['"]#[0-9A-F]{6}['"]/i);
|
|
1228
|
+
|
|
1229
|
+
// Should not have className in output
|
|
1230
|
+
expect(output).not.toContain("className");
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
it("should combine directional border width and color", () => {
|
|
1234
|
+
const input = `
|
|
1235
|
+
import { View } from 'react-native';
|
|
1236
|
+
export function Component() {
|
|
1237
|
+
return <View className="border-l-2 border-l-red-500" />;
|
|
1238
|
+
}
|
|
1239
|
+
`;
|
|
1240
|
+
|
|
1241
|
+
const output = transform(input, undefined, true);
|
|
1242
|
+
|
|
1243
|
+
// Should have both borderLeftWidth and borderLeftColor in the StyleSheet
|
|
1244
|
+
expect(output).toMatch(/borderLeftWidth[:\s]*2/);
|
|
1245
|
+
expect(output).toMatch(/borderLeftColor[:\s]*['"]#[0-9A-F]{6}['"]/i);
|
|
1246
|
+
|
|
1247
|
+
// Should not have className in output
|
|
1248
|
+
expect(output).not.toContain("className");
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
it("should support directional border colors with opacity", () => {
|
|
1252
|
+
const input = `
|
|
1253
|
+
import { View } from 'react-native';
|
|
1254
|
+
export function Component() {
|
|
1255
|
+
return <View className="border-t-red-500/50 border-b-blue-500/80" />;
|
|
1256
|
+
}
|
|
1257
|
+
`;
|
|
1258
|
+
|
|
1259
|
+
const output = transform(input, undefined, true);
|
|
1260
|
+
|
|
1261
|
+
// Should have 8-digit hex colors with alpha channel
|
|
1262
|
+
expect(output).toMatch(/borderTopColor[:\s]*['"]#[0-9A-F]{8}['"]/i);
|
|
1263
|
+
expect(output).toMatch(/borderBottomColor[:\s]*['"]#[0-9A-F]{8}['"]/i);
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
it("should support directional border colors with arbitrary hex values", () => {
|
|
1267
|
+
const input = `
|
|
1268
|
+
import { View } from 'react-native';
|
|
1269
|
+
export function Component() {
|
|
1270
|
+
return <View className="border-t-[#ff0000] border-r-[#abc]" />;
|
|
1271
|
+
}
|
|
1272
|
+
`;
|
|
1273
|
+
|
|
1274
|
+
const output = transform(input, undefined, true);
|
|
1275
|
+
|
|
1276
|
+
// Should have borderTopColor and borderRightColor
|
|
1277
|
+
expect(output).toMatch(/borderTopColor[:\s]*['"]#[0-9a-fA-F]{6}['"]/);
|
|
1278
|
+
expect(output).toMatch(/borderRightColor[:\s]*['"]#[0-9a-fA-F]{6}['"]/);
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
it("should support all four directional border colors", () => {
|
|
1282
|
+
const input = `
|
|
1283
|
+
import { View } from 'react-native';
|
|
1284
|
+
export function Component() {
|
|
1285
|
+
return (
|
|
1286
|
+
<View className="border-t-red-500 border-r-blue-500 border-b-green-500 border-l-yellow-500" />
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
`;
|
|
1290
|
+
|
|
1291
|
+
const output = transform(input, undefined, true);
|
|
1292
|
+
|
|
1293
|
+
// Should have all four directional color properties
|
|
1294
|
+
expect(output).toMatch(/borderTopColor[:\s]*['"]#[0-9A-F]{6}['"]/i);
|
|
1295
|
+
expect(output).toMatch(/borderRightColor[:\s]*['"]#[0-9A-F]{6}['"]/i);
|
|
1296
|
+
expect(output).toMatch(/borderBottomColor[:\s]*['"]#[0-9A-F]{6}['"]/i);
|
|
1297
|
+
expect(output).toMatch(/borderLeftColor[:\s]*['"]#[0-9A-F]{6}['"]/i);
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
it("should combine directional widths, colors, and general border color", () => {
|
|
1301
|
+
const input = `
|
|
1302
|
+
import { View } from 'react-native';
|
|
1303
|
+
export function Component() {
|
|
1304
|
+
return (
|
|
1305
|
+
<View className="border border-gray-300 border-l-4 border-l-blue-500" />
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
`;
|
|
1309
|
+
|
|
1310
|
+
const output = transform(input, undefined, true);
|
|
1311
|
+
|
|
1312
|
+
// Should have general border properties
|
|
1313
|
+
expect(output).toMatch(/borderWidth[:\s]*1/);
|
|
1314
|
+
expect(output).toMatch(/borderColor[:\s]*['"]#[0-9A-F]{6}['"]/i);
|
|
1315
|
+
|
|
1316
|
+
// Should have directional left border properties
|
|
1317
|
+
expect(output).toMatch(/borderLeftWidth[:\s]*4/);
|
|
1318
|
+
expect(output).toMatch(/borderLeftColor[:\s]*['"]#[0-9A-F]{6}['"]/i);
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
it("should work with dynamic className containing directional border colors", () => {
|
|
1322
|
+
const input = `
|
|
1323
|
+
import { View } from 'react-native';
|
|
1324
|
+
export function Component({ isError }) {
|
|
1325
|
+
return (
|
|
1326
|
+
<View className={\`border-t-2 \${isError ? 'border-t-red-500' : 'border-t-gray-300'}\`} />
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
`;
|
|
1330
|
+
|
|
1331
|
+
const output = transform(input, undefined, true);
|
|
1332
|
+
|
|
1333
|
+
// Should have StyleSheet with both color options
|
|
1334
|
+
expect(output).toContain("_border_t_2");
|
|
1335
|
+
expect(output).toContain("_border_t_red_500");
|
|
1336
|
+
expect(output).toContain("_border_t_gray_300");
|
|
1337
|
+
|
|
1338
|
+
// Should have conditional expression with both styles
|
|
1339
|
+
expect(output).toMatch(/isError\s*\?\s*_twStyles\._border_t_red_500/);
|
|
1340
|
+
});
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
describe("className visitor - directional modifiers (RTL/LTR)", () => {
|
|
1344
|
+
it("should transform rtl: modifier and inject I18nManager import", () => {
|
|
1345
|
+
const input = `
|
|
1346
|
+
import { View } from 'react-native';
|
|
1347
|
+
export function Component() {
|
|
1348
|
+
return <View className="rtl:mr-4" />;
|
|
1349
|
+
}
|
|
1350
|
+
`;
|
|
1351
|
+
|
|
1352
|
+
const output = transform(input, undefined, true);
|
|
1353
|
+
|
|
1354
|
+
// Should import I18nManager
|
|
1355
|
+
expect(output).toContain("I18nManager");
|
|
1356
|
+
|
|
1357
|
+
// Should declare _twIsRTL variable
|
|
1358
|
+
expect(output).toContain("_twIsRTL");
|
|
1359
|
+
expect(output).toContain("I18nManager.isRTL");
|
|
1360
|
+
|
|
1361
|
+
// Should have StyleSheet with rtl style
|
|
1362
|
+
expect(output).toContain("StyleSheet.create");
|
|
1363
|
+
expect(output).toContain("_rtl_mr_4");
|
|
1364
|
+
|
|
1365
|
+
// Should have conditional for RTL
|
|
1366
|
+
expect(output).toMatch(/_twIsRTL\s*&&\s*_twStyles\._rtl_mr_4/);
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
it("should transform ltr: modifier with negated conditional", () => {
|
|
1370
|
+
const input = `
|
|
1371
|
+
import { View } from 'react-native';
|
|
1372
|
+
export function Component() {
|
|
1373
|
+
return <View className="ltr:ml-4" />;
|
|
1374
|
+
}
|
|
1375
|
+
`;
|
|
1376
|
+
|
|
1377
|
+
const output = transform(input, undefined, true);
|
|
1378
|
+
|
|
1379
|
+
// Should import I18nManager
|
|
1380
|
+
expect(output).toContain("I18nManager");
|
|
1381
|
+
|
|
1382
|
+
// Should have StyleSheet with ltr style
|
|
1383
|
+
expect(output).toContain("_ltr_ml_4");
|
|
1384
|
+
|
|
1385
|
+
// Should have negated conditional for LTR (!_twIsRTL)
|
|
1386
|
+
expect(output).toMatch(/!\s*_twIsRTL\s*&&\s*_twStyles\._ltr_ml_4/);
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
it("should combine rtl: and ltr: modifiers", () => {
|
|
1390
|
+
const input = `
|
|
1391
|
+
import { View } from 'react-native';
|
|
1392
|
+
export function Component() {
|
|
1393
|
+
return <View className="rtl:mr-4 ltr:ml-4" />;
|
|
1394
|
+
}
|
|
1395
|
+
`;
|
|
1396
|
+
|
|
1397
|
+
const output = transform(input, undefined, true);
|
|
1398
|
+
|
|
1399
|
+
// Should have both styles
|
|
1400
|
+
expect(output).toContain("_rtl_mr_4");
|
|
1401
|
+
expect(output).toContain("_ltr_ml_4");
|
|
1402
|
+
|
|
1403
|
+
// Should have both conditionals
|
|
1404
|
+
expect(output).toMatch(/_twIsRTL\s*&&\s*_twStyles\._rtl_mr_4/);
|
|
1405
|
+
expect(output).toMatch(/!\s*_twIsRTL\s*&&\s*_twStyles\._ltr_ml_4/);
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
it("should combine directional modifiers with base classes", () => {
|
|
1409
|
+
const input = `
|
|
1410
|
+
import { View } from 'react-native';
|
|
1411
|
+
export function Component() {
|
|
1412
|
+
return <View className="p-4 bg-white rtl:pr-8 ltr:pl-8" />;
|
|
1413
|
+
}
|
|
1414
|
+
`;
|
|
1415
|
+
|
|
1416
|
+
const output = transform(input, undefined, true);
|
|
1417
|
+
|
|
1418
|
+
// Should have base style
|
|
1419
|
+
expect(output).toContain("_bg_white_p_4");
|
|
1420
|
+
|
|
1421
|
+
// Should have directional styles
|
|
1422
|
+
expect(output).toContain("_rtl_pr_8");
|
|
1423
|
+
expect(output).toContain("_ltr_pl_8");
|
|
1424
|
+
|
|
1425
|
+
// Should generate an array with base and conditional styles
|
|
1426
|
+
expect(output).toMatch(/style:\s*\[/);
|
|
1427
|
+
});
|
|
1428
|
+
|
|
1429
|
+
it("should combine directional modifiers with platform modifiers", () => {
|
|
1430
|
+
const input = `
|
|
1431
|
+
import { View } from 'react-native';
|
|
1432
|
+
export function Component() {
|
|
1433
|
+
return <View className="p-4 ios:p-6 rtl:mr-4" />;
|
|
1434
|
+
}
|
|
1435
|
+
`;
|
|
1436
|
+
|
|
1437
|
+
const output = transform(input, undefined, true);
|
|
1438
|
+
|
|
1439
|
+
// Should have Platform import
|
|
1440
|
+
expect(output).toContain("Platform");
|
|
1441
|
+
|
|
1442
|
+
// Should have I18nManager import
|
|
1443
|
+
expect(output).toContain("I18nManager");
|
|
1444
|
+
|
|
1445
|
+
// Should have all styles
|
|
1446
|
+
expect(output).toContain("_p_4");
|
|
1447
|
+
expect(output).toContain("_ios_p_6");
|
|
1448
|
+
expect(output).toContain("_rtl_mr_4");
|
|
1449
|
+
|
|
1450
|
+
// Should have Platform.select
|
|
1451
|
+
expect(output).toContain("Platform.select");
|
|
1452
|
+
|
|
1453
|
+
// Should have RTL conditional
|
|
1454
|
+
expect(output).toMatch(/_twIsRTL\s*&&/);
|
|
1455
|
+
});
|
|
1456
|
+
|
|
1457
|
+
it("should not add I18nManager import if already present", () => {
|
|
1458
|
+
const input = `
|
|
1459
|
+
import { View, I18nManager } from 'react-native';
|
|
1460
|
+
export function Component() {
|
|
1461
|
+
return <View className="rtl:mr-4" />;
|
|
1462
|
+
}
|
|
1463
|
+
`;
|
|
1464
|
+
|
|
1465
|
+
const output = transform(input, undefined, true);
|
|
1466
|
+
|
|
1467
|
+
// Should have only one I18nManager import (merged, not duplicated)
|
|
1468
|
+
const i18nMatches = output.match(/I18nManager/g);
|
|
1469
|
+
// Should have I18nManager in: import, variable declaration, and style usage
|
|
1470
|
+
expect(i18nMatches).toBeTruthy();
|
|
1471
|
+
// Should not have duplicate imports
|
|
1472
|
+
expect(output).not.toMatch(/import\s*\{[^}]*I18nManager[^}]*I18nManager[^}]*\}/);
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
it("should work with directional logical properties", () => {
|
|
1476
|
+
const input = `
|
|
1477
|
+
import { View } from 'react-native';
|
|
1478
|
+
export function Component() {
|
|
1479
|
+
return <View className="rtl:ms-4 ltr:me-4" />;
|
|
1480
|
+
}
|
|
1481
|
+
`;
|
|
1482
|
+
|
|
1483
|
+
const output = transform(input, undefined, true);
|
|
1484
|
+
|
|
1485
|
+
// Should have logical property styles
|
|
1486
|
+
expect(output).toContain("_rtl_ms_4");
|
|
1487
|
+
expect(output).toContain("_ltr_me_4");
|
|
1488
|
+
|
|
1489
|
+
// Should contain marginStart and marginEnd in the StyleSheet
|
|
1490
|
+
expect(output).toContain("marginStart");
|
|
1491
|
+
expect(output).toContain("marginEnd");
|
|
1492
|
+
});
|
|
1493
|
+
|
|
1494
|
+
it("should combine directional modifiers with color scheme modifiers", () => {
|
|
1495
|
+
const input = `
|
|
1496
|
+
import { View } from 'react-native';
|
|
1497
|
+
export function Component() {
|
|
1498
|
+
return <View className="bg-white dark:bg-gray-900 rtl:pr-4" />;
|
|
1499
|
+
}
|
|
1500
|
+
`;
|
|
1501
|
+
|
|
1502
|
+
const output = transform(input, undefined, true);
|
|
1503
|
+
|
|
1504
|
+
// Should have useColorScheme
|
|
1505
|
+
expect(output).toContain("useColorScheme");
|
|
1506
|
+
|
|
1507
|
+
// Should have I18nManager
|
|
1508
|
+
expect(output).toContain("I18nManager");
|
|
1509
|
+
|
|
1510
|
+
// Should have all styles
|
|
1511
|
+
expect(output).toContain("_bg_white");
|
|
1512
|
+
expect(output).toContain("_dark_bg_gray_900");
|
|
1513
|
+
expect(output).toContain("_rtl_pr_4");
|
|
1514
|
+
|
|
1515
|
+
// Should have both conditionals
|
|
1516
|
+
expect(output).toMatch(/_twColorScheme\s*===\s*["']dark["']/);
|
|
1517
|
+
expect(output).toMatch(/_twIsRTL\s*&&/);
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1520
|
+
it("should handle aliased I18nManager import", () => {
|
|
1521
|
+
const input = `
|
|
1522
|
+
import { View, I18nManager as RTL } from 'react-native';
|
|
1523
|
+
export function Component() {
|
|
1524
|
+
// Use RTL somewhere so TypeScript doesn't strip the unused import
|
|
1525
|
+
const isRtl = RTL.isRTL;
|
|
1526
|
+
return <View className="rtl:mr-4" />;
|
|
1527
|
+
}
|
|
1528
|
+
`;
|
|
1529
|
+
|
|
1530
|
+
const output = transform(input, undefined, true);
|
|
1531
|
+
|
|
1532
|
+
// Should use the aliased identifier RTL.isRTL instead of I18nManager.isRTL
|
|
1533
|
+
expect(output).toContain("RTL.isRTL");
|
|
1534
|
+
// Should preserve the aliased import
|
|
1535
|
+
expect(output).toContain("I18nManager as RTL");
|
|
1536
|
+
// Should not add a separate I18nManager import without alias
|
|
1537
|
+
expect(output).not.toMatch(/I18nManager,|,\s*I18nManager\s*[,}]/);
|
|
1538
|
+
});
|
|
1539
|
+
|
|
1540
|
+
it("should preserve 'use client' directive when injecting I18nManager variable", () => {
|
|
1541
|
+
const input = `
|
|
1542
|
+
'use client';
|
|
1543
|
+
import { View } from 'react-native';
|
|
1544
|
+
export function Component() {
|
|
1545
|
+
return <View className="rtl:mr-4" />;
|
|
1546
|
+
}
|
|
1547
|
+
`;
|
|
1548
|
+
|
|
1549
|
+
const output = transform(input, undefined, true);
|
|
1550
|
+
|
|
1551
|
+
// 'use client' should be the first statement
|
|
1552
|
+
const lines = output.split("\n").filter((l: string) => l.trim());
|
|
1553
|
+
const useClientIndex = lines.findIndex(
|
|
1554
|
+
(l: string) => l.includes("'use client'") || l.includes('"use client"'),
|
|
1555
|
+
);
|
|
1556
|
+
expect(useClientIndex).toBe(0);
|
|
1557
|
+
|
|
1558
|
+
// I18nManager variable should come after imports, not before 'use client'
|
|
1559
|
+
expect(output).toContain("_twIsRTL");
|
|
1560
|
+
expect(output).toContain("I18nManager.isRTL");
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
it("should preserve 'use strict' directive when injecting I18nManager variable", () => {
|
|
1564
|
+
const input = `
|
|
1565
|
+
'use strict';
|
|
1566
|
+
import { View } from 'react-native';
|
|
1567
|
+
export function Component() {
|
|
1568
|
+
return <View className="rtl:mr-4" />;
|
|
1569
|
+
}
|
|
1570
|
+
`;
|
|
1571
|
+
|
|
1572
|
+
const output = transform(input, undefined, true);
|
|
1573
|
+
|
|
1574
|
+
// 'use strict' should be preserved at the top
|
|
1575
|
+
const lines = output.split("\n").filter((l: string) => l.trim());
|
|
1576
|
+
const useStrictIndex = lines.findIndex(
|
|
1577
|
+
(l: string) => l.includes("'use strict'") || l.includes('"use strict"'),
|
|
1578
|
+
);
|
|
1579
|
+
expect(useStrictIndex).toBe(0);
|
|
1580
|
+
|
|
1581
|
+
// I18nManager variable should work correctly
|
|
1582
|
+
expect(output).toContain("_twIsRTL");
|
|
1583
|
+
expect(output).toContain("I18nManager.isRTL");
|
|
1584
|
+
});
|
|
1585
|
+
|
|
1586
|
+
it("should expand text-start to directional modifiers", () => {
|
|
1587
|
+
const input = `
|
|
1588
|
+
import { Text } from 'react-native';
|
|
1589
|
+
export function Component() {
|
|
1590
|
+
return <Text className="text-start" />;
|
|
1591
|
+
}
|
|
1592
|
+
`;
|
|
1593
|
+
|
|
1594
|
+
const output = transform(input, undefined, true);
|
|
1595
|
+
|
|
1596
|
+
// Should have I18nManager import (text-start expands to ltr:/rtl: modifiers)
|
|
1597
|
+
expect(output).toContain("I18nManager");
|
|
1598
|
+
|
|
1599
|
+
// Should have both ltr and rtl styles
|
|
1600
|
+
expect(output).toContain("_ltr_text_left");
|
|
1601
|
+
expect(output).toContain("_rtl_text_right");
|
|
1602
|
+
|
|
1603
|
+
// Should have conditionals for both
|
|
1604
|
+
expect(output).toMatch(/_twIsRTL\s*&&/);
|
|
1605
|
+
expect(output).toMatch(/!\s*_twIsRTL\s*&&/);
|
|
1606
|
+
});
|
|
1607
|
+
|
|
1608
|
+
it("should expand text-end to directional modifiers", () => {
|
|
1609
|
+
const input = `
|
|
1610
|
+
import { Text } from 'react-native';
|
|
1611
|
+
export function Component() {
|
|
1612
|
+
return <Text className="text-end" />;
|
|
1613
|
+
}
|
|
1614
|
+
`;
|
|
1615
|
+
|
|
1616
|
+
const output = transform(input, undefined, true);
|
|
1617
|
+
|
|
1618
|
+
// Should have I18nManager import
|
|
1619
|
+
expect(output).toContain("I18nManager");
|
|
1620
|
+
|
|
1621
|
+
// text-end expands to ltr:text-right rtl:text-left
|
|
1622
|
+
expect(output).toContain("_ltr_text_right");
|
|
1623
|
+
expect(output).toContain("_rtl_text_left");
|
|
1624
|
+
});
|
|
1625
|
+
});
|