@bookklik/senangstart-css 0.2.10 → 0.2.12

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.
Files changed (39) hide show
  1. package/.agent/skills/add-utility/SKILL.md +65 -0
  2. package/.agent/workflows/add-utility.md +2 -0
  3. package/.agent/workflows/build.md +2 -0
  4. package/.agent/workflows/dev.md +2 -0
  5. package/AGENTS.md +30 -0
  6. package/dist/senangstart-css.js +362 -151
  7. package/dist/senangstart-css.min.js +175 -174
  8. package/dist/senangstart-tw.js +4 -4
  9. package/dist/senangstart-tw.min.js +1 -1
  10. package/docs/ms/reference/visual/ring-color.md +2 -2
  11. package/docs/ms/reference/visual/ring-offset.md +3 -3
  12. package/docs/ms/reference/visual/ring.md +5 -5
  13. package/docs/public/assets/senangstart-css.min.js +175 -174
  14. package/docs/public/llms.txt +10 -10
  15. package/docs/reference/visual/ring-color.md +2 -2
  16. package/docs/reference/visual/ring-offset.md +3 -3
  17. package/docs/reference/visual/ring.md +5 -5
  18. package/package.json +1 -1
  19. package/src/cdn/tw-conversion-engine.js +4 -4
  20. package/src/cli/commands/build.js +42 -14
  21. package/src/cli/commands/dev.js +157 -93
  22. package/src/compiler/generators/css.js +371 -199
  23. package/src/compiler/tokenizer.js +25 -23
  24. package/src/core/tokenizer-core.js +46 -19
  25. package/src/definitions/visual-borders.js +10 -10
  26. package/src/utils/common.js +456 -39
  27. package/src/utils/node-io.js +82 -0
  28. package/tests/integration/dev-recovery.test.js +231 -0
  29. package/tests/unit/cli/memory-limits.test.js +169 -0
  30. package/tests/unit/compiler/css-generation-error-handling.test.js +204 -0
  31. package/tests/unit/compiler/generators/css-errors.test.js +102 -0
  32. package/tests/unit/convert-tailwind.test.js +518 -442
  33. package/tests/unit/utils/common.test.js +376 -26
  34. package/tests/unit/utils/file-timeout.test.js +154 -0
  35. package/tests/unit/utils/theme-validation.test.js +181 -0
  36. package/tests/unit/compiler/generators/css.coverage.test.js +0 -833
  37. package/tests/unit/convert-tailwind.cli.test.js +0 -95
  38. package/tests/unit/security.test.js +0 -206
  39. /package/tests/unit/{convert-tailwind.coverage.test.js → convert-tailwind-edgecases.test.js} +0 -0
@@ -1,442 +1,518 @@
1
- /**
2
- * Tests for convert-tailwind.js
3
- * Tailwind CSS to SenangStart CSS converter
4
- */
5
-
6
- import { describe, it } from 'node:test';
7
- import assert from 'node:assert';
8
- import { convertClass, convertClasses, convertHTML, spacingScale } from '../../scripts/convert-tailwind.js';
9
-
10
- describe('convertClass', () => {
11
- describe('Layout classes', () => {
12
- it('should convert display classes', () => {
13
- assert.deepStrictEqual(convertClass('flex'), { category: 'layout', value: 'flex' });
14
- assert.deepStrictEqual(convertClass('grid'), { category: 'layout', value: 'grid' });
15
- assert.deepStrictEqual(convertClass('hidden'), { category: 'layout', value: 'hidden' });
16
- assert.deepStrictEqual(convertClass('block'), { category: 'layout', value: 'block' });
17
- });
18
-
19
- it('should convert flex direction classes', () => {
20
- assert.deepStrictEqual(convertClass('flex-col'), { category: 'layout', value: 'col' });
21
- assert.deepStrictEqual(convertClass('flex-row'), { category: 'layout', value: 'row' });
22
- assert.deepStrictEqual(convertClass('flex-col-reverse'), { category: 'layout', value: 'col-reverse' });
23
- });
24
-
25
- it('should convert justify/align classes', () => {
26
- assert.deepStrictEqual(convertClass('justify-center'), { category: 'layout', value: 'justify:center' });
27
- assert.deepStrictEqual(convertClass('justify-between'), { category: 'layout', value: 'justify:between' });
28
- assert.deepStrictEqual(convertClass('items-center'), { category: 'layout', value: 'items:center' });
29
- assert.deepStrictEqual(convertClass('items-start'), { category: 'layout', value: 'items:start' });
30
- });
31
-
32
- it('should convert flex grow/shrink classes', () => {
33
- assert.deepStrictEqual(convertClass('flex-grow'), { category: 'layout', value: 'grow' });
34
- assert.deepStrictEqual(convertClass('grow'), { category: 'layout', value: 'grow' });
35
- assert.deepStrictEqual(convertClass('shrink-0'), { category: 'layout', value: 'shrink-0' });
36
- });
37
-
38
- it('should convert position classes', () => {
39
- assert.deepStrictEqual(convertClass('relative'), { category: 'layout', value: 'relative' });
40
- assert.deepStrictEqual(convertClass('absolute'), { category: 'layout', value: 'absolute' });
41
- assert.deepStrictEqual(convertClass('fixed'), { category: 'layout', value: 'fixed' });
42
- });
43
-
44
- it('should convert positional offsets (top, left, right, bottom)', () => {
45
- assert.deepStrictEqual(convertClass('top-0'), { category: 'layout', value: 'top:0' });
46
- assert.deepStrictEqual(convertClass('left-0'), { category: 'layout', value: 'left:0' });
47
- assert.deepStrictEqual(convertClass('bottom-full'), { category: 'layout', value: 'bottom:full' });
48
- assert.deepStrictEqual(convertClass('left-1/2'), { category: 'layout', value: 'left:half' });
49
- assert.deepStrictEqual(convertClass('top-1/3'), { category: 'layout', value: 'top:third' });
50
- assert.deepStrictEqual(convertClass('inset-0'), { category: 'layout', value: 'inset:0' });
51
- });
52
-
53
- it('should convert grid classes', () => {
54
- assert.deepStrictEqual(convertClass('grid-cols-3'), { category: 'layout', value: 'grid-cols:3' });
55
- assert.deepStrictEqual(convertClass('col-span-2'), { category: 'layout', value: 'col-span:2' });
56
- });
57
- });
58
-
59
- describe('Spacing classes', () => {
60
- it('should convert padding classes', () => {
61
- assert.deepStrictEqual(convertClass('p-4'), { category: 'space', value: 'p:medium' });
62
- assert.deepStrictEqual(convertClass('p-8'), { category: 'space', value: 'p:large' });
63
- assert.deepStrictEqual(convertClass('px-4'), { category: 'space', value: 'p-x:medium' });
64
- assert.deepStrictEqual(convertClass('py-2'), { category: 'space', value: 'p-y:small' });
65
- });
66
-
67
- it('should convert margin classes', () => {
68
- assert.deepStrictEqual(convertClass('m-4'), { category: 'space', value: 'm:medium' });
69
- assert.deepStrictEqual(convertClass('mt-8'), { category: 'space', value: 'm-t:large' });
70
- assert.deepStrictEqual(convertClass('mx-auto'), { category: 'space', value: 'm-x:auto' });
71
- });
72
-
73
- it('should convert gap classes', () => {
74
- assert.deepStrictEqual(convertClass('gap-4'), { category: 'space', value: 'g:medium' });
75
- assert.deepStrictEqual(convertClass('gap-x-2'), { category: 'space', value: 'g-x:small' });
76
- });
77
-
78
- it('should convert width/height classes', () => {
79
- assert.deepStrictEqual(convertClass('w-full'), { category: 'space', value: 'w:full' });
80
- assert.deepStrictEqual(convertClass('w-1/2'), { category: 'space', value: 'w:half' });
81
- assert.deepStrictEqual(convertClass('w-1/3'), { category: 'space', value: 'w:third' });
82
- assert.deepStrictEqual(convertClass('w-2/3'), { category: 'space', value: 'w:third-2x' });
83
- assert.deepStrictEqual(convertClass('w-1/4'), { category: 'space', value: 'w:quarter' });
84
- assert.deepStrictEqual(convertClass('w-3/4'), { category: 'space', value: 'w:quarter-3x' });
85
- assert.deepStrictEqual(convertClass('h-screen'), { category: 'space', value: 'h:[100vh]' });
86
- assert.deepStrictEqual(convertClass('max-w-4'), { category: 'space', value: 'max-w:medium' });
87
- });
88
-
89
- it('should convert negative margin classes', () => {
90
- // Standard exact=false
91
- assert.deepStrictEqual(convertClass('-m-4'), { category: 'space', value: 'm:-medium' });
92
- assert.deepStrictEqual(convertClass('-mt-8'), { category: 'space', value: 'm-t:-large' });
93
-
94
- // Exact=true
95
- assert.deepStrictEqual(convertClass('-m-4', { exact: true }), { category: 'space', value: 'm:-tw-4' });
96
-
97
- // Arbitrary
98
- assert.deepStrictEqual(convertClass('-m-[10px]'), { category: 'space', value: 'm:[-10px]' });
99
- });
100
- });
101
-
102
- describe('Visual classes', () => {
103
- it('should convert background color classes', () => {
104
- assert.deepStrictEqual(convertClass('bg-blue-500'), { category: 'visual', value: 'bg:blue-500' });
105
- assert.deepStrictEqual(convertClass('bg-white'), { category: 'visual', value: 'bg:white' });
106
- assert.deepStrictEqual(convertClass('bg-transparent'), { category: 'visual', value: 'bg:transparent' });
107
- });
108
-
109
- it('should convert border color classes', () => {
110
- assert.deepStrictEqual(convertClass('border-gray-900'), { category: 'visual', value: 'border:gray-900' });
111
- assert.deepStrictEqual(convertClass('border-t-gray-900'), { category: 'visual', value: 'border-t:gray-900' });
112
- assert.deepStrictEqual(convertClass('border-b-blue-500'), { category: 'visual', value: 'border-b:blue-500' });
113
- assert.deepStrictEqual(convertClass('border-l-red-300'), { category: 'visual', value: 'border-l:red-300' });
114
- assert.deepStrictEqual(convertClass('border-r-transparent'), { category: 'visual', value: 'border-r:transparent' });
115
- });
116
-
117
- it('should convert text color classes', () => {
118
- assert.deepStrictEqual(convertClass('text-white'), { category: 'visual', value: 'text:white' });
119
- assert.deepStrictEqual(convertClass('text-gray-700'), { category: 'visual', value: 'text:gray-700' });
120
- });
121
-
122
- it('should convert text alignment classes', () => {
123
- assert.deepStrictEqual(convertClass('text-center'), { category: 'visual', value: 'text:center' });
124
- assert.deepStrictEqual(convertClass('text-left'), { category: 'visual', value: 'text:left' });
125
- });
126
-
127
- it('should convert text size classes', () => {
128
- assert.deepStrictEqual(convertClass('text-sm'), { category: 'visual', value: 'text-size:small' });
129
- assert.deepStrictEqual(convertClass('text-xl'), { category: 'visual', value: 'text-size:big' });
130
- assert.deepStrictEqual(convertClass('text-2xl'), { category: 'visual', value: 'text-size:huge' });
131
- });
132
-
133
- it('should convert border radius classes', () => {
134
- assert.deepStrictEqual(convertClass('rounded'), { category: 'visual', value: 'rounded:small' });
135
- assert.deepStrictEqual(convertClass('rounded-lg'), { category: 'visual', value: 'rounded:medium' });
136
- assert.deepStrictEqual(convertClass('rounded-full'), { category: 'visual', value: 'rounded:round' });
137
- });
138
-
139
- it('should convert directional border radius classes', () => {
140
- assert.deepStrictEqual(convertClass('rounded-t-lg'), { category: 'visual', value: 'rounded-t:medium' });
141
- assert.deepStrictEqual(convertClass('rounded-b-lg'), { category: 'visual', value: 'rounded-b:medium' });
142
- assert.deepStrictEqual(convertClass('rounded-l-lg'), { category: 'visual', value: 'rounded-l:medium' });
143
- assert.deepStrictEqual(convertClass('rounded-r-lg'), { category: 'visual', value: 'rounded-r:medium' });
144
- assert.deepStrictEqual(convertClass('rounded-tl-3xl'), { category: 'visual', value: 'rounded-tl:big' });
145
- assert.deepStrictEqual(convertClass('rounded-tr-3xl'), { category: 'visual', value: 'rounded-tr:big' });
146
- assert.deepStrictEqual(convertClass('rounded-bl-full'), { category: 'visual', value: 'rounded-bl:round' });
147
- assert.deepStrictEqual(convertClass('rounded-br-full'), { category: 'visual', value: 'rounded-br:round' });
148
- });
149
-
150
- it('should convert shadow classes', () => {
151
- assert.deepStrictEqual(convertClass('shadow'), { category: 'visual', value: 'shadow:small' });
152
- assert.deepStrictEqual(convertClass('shadow-md'), { category: 'visual', value: 'shadow:medium' });
153
- assert.deepStrictEqual(convertClass('shadow-lg'), { category: 'visual', value: 'shadow:big' });
154
- });
155
-
156
- it('should convert font weight classes', () => {
157
- assert.deepStrictEqual(convertClass('font-bold'), { category: 'visual', value: 'font:tw-bold' });
158
- assert.deepStrictEqual(convertClass('font-medium'), { category: 'visual', value: 'font:tw-medium' });
159
- });
160
-
161
- it('should convert typography keywords', () => {
162
- assert.deepStrictEqual(convertClass('italic'), { category: 'visual', value: 'italic' });
163
- assert.deepStrictEqual(convertClass('uppercase'), { category: 'visual', value: 'uppercase' });
164
- assert.deepStrictEqual(convertClass('underline'), { category: 'visual', value: 'underline' });
165
- assert.deepStrictEqual(convertClass('truncate'), { category: 'visual', value: 'truncate' });
166
- });
167
-
168
- it('should convert opacity classes', () => {
169
- assert.deepStrictEqual(convertClass('opacity-50'), { category: 'visual', value: 'opacity:50' });
170
- assert.deepStrictEqual(convertClass('opacity-100'), { category: 'visual', value: 'opacity:100' });
171
- });
172
-
173
- it('should convert cursor classes', () => {
174
- assert.deepStrictEqual(convertClass('cursor-pointer'), { category: 'visual', value: 'cursor:pointer' });
175
- assert.deepStrictEqual(convertClass('cursor-not-allowed'), { category: 'visual', value: 'cursor:not-allowed' });
176
- });
177
-
178
- it('should convert gradient classes', () => {
179
- assert.deepStrictEqual(convertClass('bg-gradient-to-r'), { category: 'visual', value: 'bg-image:gradient-to-r' });
180
- assert.deepStrictEqual(convertClass('bg-gradient-to-br'), { category: 'visual', value: 'bg-image:gradient-to-br' });
181
- assert.deepStrictEqual(convertClass('from-blue-500'), { category: 'visual', value: 'from:blue-500' });
182
- assert.deepStrictEqual(convertClass('via-purple-500'), { category: 'visual', value: 'via:purple-500' });
183
- assert.deepStrictEqual(convertClass('to-pink-500'), { category: 'visual', value: 'to:pink-500' });
184
- });
185
-
186
- it('should convert translate utilities with fractions', () => {
187
- assert.deepStrictEqual(convertClass('translate-x-1/2'), { category: 'visual', value: 'translate-x:half' });
188
- assert.deepStrictEqual(convertClass('translate-y-full'), { category: 'visual', value: 'translate-y:full' });
189
- assert.deepStrictEqual(convertClass('-translate-x-1/2'), { category: 'visual', value: 'translate-x:-half' });
190
- assert.deepStrictEqual(convertClass('-translate-y-full'), { category: 'visual', value: 'translate-y:-full' });
191
- });
192
- });
193
-
194
- describe('Prefixed classes', () => {
195
- it('should handle responsive prefixes', () => {
196
- assert.deepStrictEqual(convertClass('md:flex'), { category: 'layout', value: 'tw-md:flex' });
197
- assert.deepStrictEqual(convertClass('lg:p-8'), { category: 'space', value: 'tw-lg:p:large' });
198
- });
199
-
200
- it('should handle dark mode prefix', () => {
201
- assert.deepStrictEqual(convertClass('dark:bg-gray-900'), { category: 'visual', value: 'dark:bg:gray-900' });
202
- assert.deepStrictEqual(convertClass('dark:text-white'), { category: 'visual', value: 'dark:text:white' });
203
- });
204
-
205
- it('should handle hover/focus prefixes', () => {
206
- assert.deepStrictEqual(convertClass('hover:bg-blue-600'), { category: 'visual', value: 'hover:bg:blue-600' });
207
- // focus:ring returns null because 'ring' is not a recognized base class
208
- assert.strictEqual(convertClass('focus:ring'), null);
209
- });
210
- });
211
-
212
- describe('Arbitrary values', () => {
213
- it('should handle arbitrary spacing', () => {
214
- assert.deepStrictEqual(convertClass('p-[20px]'), { category: 'space', value: 'p:[20px]' });
215
- assert.deepStrictEqual(convertClass('w-[300px]'), { category: 'space', value: 'w:[300px]' });
216
- });
217
- });
218
-
219
- describe('Unrecognized classes', () => {
220
- it('should return null for unknown classes', () => {
221
- assert.strictEqual(convertClass('some-unknown-class'), null);
222
- assert.strictEqual(convertClass('custom-utility'), null);
223
- });
224
- });
225
- });
226
-
227
- describe('convertClasses', () => {
228
- it('should group classes by category', () => {
229
- const result = convertClasses('flex items-center p-4 bg-blue-500 text-white');
230
-
231
- assert.deepStrictEqual(result.layout, ['flex', 'items:center']);
232
- assert.deepStrictEqual(result.space, ['p:medium']);
233
- assert.deepStrictEqual(result.visual, ['bg:blue-500', 'text:white']);
234
- assert.deepStrictEqual(result.unrecognized, []);
235
- });
236
-
237
- it('should collect unrecognized classes', () => {
238
- const result = convertClasses('flex my-custom-class p-4');
239
-
240
- assert.deepStrictEqual(result.unrecognized, ['my-custom-class']);
241
- });
242
- });
243
-
244
- describe('convertHTML', () => {
245
- it('should convert simple HTML element', () => {
246
- const input = '<div class="flex items-center p-4 bg-blue-500"></div>';
247
- const result = convertHTML(input);
248
-
249
- assert.ok(result.includes('layout="flex items:center"'));
250
- assert.ok(result.includes('space="p:medium"'));
251
- assert.ok(result.includes('visual="bg:blue-500"'));
252
- assert.ok(!result.includes('class='));
253
- });
254
-
255
- it('should preserve unrecognized classes in class attribute', () => {
256
- const input = '<div class="flex my-custom-class"></div>';
257
- const result = convertHTML(input);
258
-
259
- assert.ok(result.includes('layout="flex"'));
260
- assert.ok(result.includes('class="my-custom-class"'));
261
- });
262
-
263
- it('should handle multiple elements', () => {
264
- const input = `
265
- <div class="flex">
266
- <span class="text-white">Hello</span>
267
- </div>
268
- `;
269
- const result = convertHTML(input);
270
-
271
- assert.ok(result.includes('layout="flex"'));
272
- assert.ok(result.includes('visual="text:white"'));
273
- });
274
-
275
- it('should handle single quotes', () => {
276
- const input = "<div class='flex p-4'></div>";
277
- const result = convertHTML(input);
278
-
279
- assert.ok(result.includes('layout="flex"'));
280
- assert.ok(result.includes('space="p:medium"'));
281
- });
282
- });
283
-
284
- describe('spacingScale', () => {
285
- it('should have expected scale mappings', () => {
286
- assert.strictEqual(spacingScale['0'], 'none');
287
- assert.strictEqual(spacingScale['4'], 'medium');
288
- assert.strictEqual(spacingScale['8'], 'large');
289
- assert.strictEqual(spacingScale['12'], 'big');
290
- assert.strictEqual(spacingScale['24'], 'giant');
291
- assert.strictEqual(spacingScale['auto'], 'auto');
292
- });
293
- });
294
-
295
- describe('Exact mode (tw- prefix)', () => {
296
- describe('Spacing with exact mode', () => {
297
- it('should output tw- prefix for padding', () => {
298
- assert.deepStrictEqual(
299
- convertClass('p-4', { exact: true }),
300
- { category: 'space', value: 'p:tw-4' }
301
- );
302
- assert.deepStrictEqual(
303
- convertClass('p-8', { exact: true }),
304
- { category: 'space', value: 'p:tw-8' }
305
- );
306
- });
307
-
308
- it('should output tw- prefix for margin', () => {
309
- assert.deepStrictEqual(
310
- convertClass('mt-4', { exact: true }),
311
- { category: 'space', value: 'm-t:tw-4' }
312
- );
313
- });
314
-
315
- it('should output tw- prefix for gap', () => {
316
- assert.deepStrictEqual(
317
- convertClass('gap-4', { exact: true }),
318
- { category: 'space', value: 'g:tw-4' }
319
- );
320
- });
321
-
322
- it('should output tw- prefix for width/height', () => {
323
- assert.deepStrictEqual(
324
- convertClass('w-8', { exact: true }),
325
- { category: 'space', value: 'w:tw-8' }
326
- );
327
- });
328
- });
329
-
330
- describe('Border radius with exact mode', () => {
331
- it('should output tw- prefix for rounded', () => {
332
- assert.deepStrictEqual(
333
- convertClass('rounded', { exact: true }),
334
- { category: 'visual', value: 'rounded:tw-DEFAULT' }
335
- );
336
- assert.deepStrictEqual(
337
- convertClass('rounded-lg', { exact: true }),
338
- { category: 'visual', value: 'rounded:tw-lg' }
339
- );
340
- });
341
- });
342
-
343
- describe('Shadow with exact mode', () => {
344
- it('should output tw- prefix for shadow', () => {
345
- assert.deepStrictEqual(
346
- convertClass('shadow', { exact: true }),
347
- { category: 'visual', value: 'shadow:tw-DEFAULT' }
348
- );
349
- assert.deepStrictEqual(
350
- convertClass('shadow-lg', { exact: true }),
351
- { category: 'visual', value: 'shadow:tw-lg' }
352
- );
353
- });
354
- });
355
-
356
- describe('Font size with exact mode', () => {
357
- it('should output tw- prefix for text size', () => {
358
- assert.deepStrictEqual(
359
- convertClass('text-2xl', { exact: true }),
360
- { category: 'visual', value: 'text-size:tw-2xl' }
361
- );
362
- });
363
- });
364
-
365
- describe('Divide utilities', () => {
366
- it('should convert divide color', () => {
367
- assert.deepStrictEqual(
368
- convertClass('divide-gray-200'),
369
- { category: 'visual', value: 'divide:gray-200' }
370
- );
371
- });
372
-
373
- it('should convert divide-x', () => {
374
- assert.deepStrictEqual(
375
- convertClass('divide-x-gray-200'),
376
- { category: 'visual', value: 'divide-x:gray-200' }
377
- );
378
- });
379
-
380
- it('should convert divide-y', () => {
381
- assert.deepStrictEqual(
382
- convertClass('divide-y-gray-200'),
383
- { category: 'visual', value: 'divide-y:gray-200' }
384
- );
385
- });
386
-
387
- it('should convert divide width', () => {
388
- assert.deepStrictEqual(
389
- convertClass('divide-2'),
390
- { category: 'visual', value: 'divide-w:regular' }
391
- );
392
- });
393
-
394
- it('should convert divide-x width', () => {
395
- assert.deepStrictEqual(
396
- convertClass('divide-x-2'),
397
- { category: 'visual', value: 'divide-x-w:regular' }
398
- );
399
- });
400
-
401
- it('should convert divide-y width', () => {
402
- assert.deepStrictEqual(
403
- convertClass('divide-y-2'),
404
- { category: 'visual', value: 'divide-y-w:regular' }
405
- );
406
- });
407
-
408
- it('should convert divide style', () => {
409
- assert.deepStrictEqual(
410
- convertClass('divide-dashed'),
411
- { category: 'visual', value: 'divide-style:dashed' }
412
- );
413
- });
414
-
415
- it('should convert divide style with exact mode', () => {
416
- assert.deepStrictEqual(
417
- convertClass('divide-dashed', { exact: true }),
418
- { category: 'visual', value: 'divide-style:dashed' }
419
- );
420
- });
421
- });
422
-
423
- describe('convertClasses with exact mode', () => {
424
- it('should group classes with tw- prefix', () => {
425
- const result = convertClasses('flex p-4 rounded-lg', { exact: true });
426
-
427
- assert.deepStrictEqual(result.layout, ['flex']);
428
- assert.deepStrictEqual(result.space, ['p:tw-4']);
429
- assert.deepStrictEqual(result.visual, ['rounded:tw-lg']);
430
- });
431
- });
432
-
433
- describe('convertHTML with exact mode', () => {
434
- it('should convert HTML with tw- prefix', () => {
435
- const input = '<div class="p-4 rounded-lg"></div>';
436
- const result = convertHTML(input, { exact: true });
437
-
438
- assert.ok(result.includes('space="p:tw-4"'));
439
- assert.ok(result.includes('visual="rounded:tw-lg"'));
440
- });
441
- });
442
- });
1
+ /**
2
+ * Tests for convert-tailwind.js
3
+ * Tailwind CSS to SenangStart CSS converter
4
+ * Merged from convert-tailwind.test.js and convert-tailwind.cli.test.js
5
+ */
6
+
7
+ import { describe, it } from 'node:test';
8
+ import assert from 'node:assert';
9
+ import { convertClass, convertClasses, convertHTML, spacingScale } from '../../scripts/convert-tailwind.js';
10
+ import { execSync } from 'node:child_process';
11
+ import path from 'node:path';
12
+ import fs from 'node:fs';
13
+
14
+ const SCRIPT_PATH = path.join(process.cwd(), 'scripts', 'convert-tailwind.js');
15
+
16
+ describe('convertClass', () => {
17
+ describe('Layout classes', () => {
18
+ it('should convert display classes', () => {
19
+ assert.deepStrictEqual(convertClass('flex'), { category: 'layout', value: 'flex' });
20
+ assert.deepStrictEqual(convertClass('grid'), { category: 'layout', value: 'grid' });
21
+ assert.deepStrictEqual(convertClass('hidden'), { category: 'layout', value: 'hidden' });
22
+ assert.deepStrictEqual(convertClass('block'), { category: 'layout', value: 'block' });
23
+ });
24
+
25
+ it('should convert flex direction classes', () => {
26
+ assert.deepStrictEqual(convertClass('flex-col'), { category: 'layout', value: 'col' });
27
+ assert.deepStrictEqual(convertClass('flex-row'), { category: 'layout', value: 'row' });
28
+ assert.deepStrictEqual(convertClass('flex-col-reverse'), { category: 'layout', value: 'col-reverse' });
29
+ });
30
+
31
+ it('should convert justify/align classes', () => {
32
+ assert.deepStrictEqual(convertClass('justify-center'), { category: 'layout', value: 'justify:center' });
33
+ assert.deepStrictEqual(convertClass('justify-between'), { category: 'layout', value: 'justify:between' });
34
+ assert.deepStrictEqual(convertClass('items-center'), { category: 'layout', value: 'items:center' });
35
+ assert.deepStrictEqual(convertClass('items-start'), { category: 'layout', value: 'items:start' });
36
+ });
37
+
38
+ it('should convert flex grow/shrink classes', () => {
39
+ assert.deepStrictEqual(convertClass('flex-grow'), { category: 'layout', value: 'grow' });
40
+ assert.deepStrictEqual(convertClass('grow'), { category: 'layout', value: 'grow' });
41
+ assert.deepStrictEqual(convertClass('shrink-0'), { category: 'layout', value: 'shrink-0' });
42
+ });
43
+
44
+ it('should convert position classes', () => {
45
+ assert.deepStrictEqual(convertClass('relative'), { category: 'layout', value: 'relative' });
46
+ assert.deepStrictEqual(convertClass('absolute'), { category: 'layout', value: 'absolute' });
47
+ assert.deepStrictEqual(convertClass('fixed'), { category: 'layout', value: 'fixed' });
48
+ });
49
+
50
+ it('should convert positional offsets (top, left, right, bottom)', () => {
51
+ assert.deepStrictEqual(convertClass('top-0'), { category: 'layout', value: 'top:0' });
52
+ assert.deepStrictEqual(convertClass('left-0'), { category: 'layout', value: 'left:0' });
53
+ assert.deepStrictEqual(convertClass('bottom-full'), { category: 'layout', value: 'bottom:full' });
54
+ assert.deepStrictEqual(convertClass('left-1/2'), { category: 'layout', value: 'left:half' });
55
+ assert.deepStrictEqual(convertClass('top-1/3'), { category: 'layout', value: 'top:third' });
56
+ assert.deepStrictEqual(convertClass('inset-0'), { category: 'layout', value: 'inset:0' });
57
+ });
58
+
59
+ it('should convert grid classes', () => {
60
+ assert.deepStrictEqual(convertClass('grid-cols-3'), { category: 'layout', value: 'grid-cols:3' });
61
+ assert.deepStrictEqual(convertClass('col-span-2'), { category: 'layout', value: 'col-span:2' });
62
+ });
63
+ });
64
+
65
+ describe('Spacing classes', () => {
66
+ it('should convert padding classes', () => {
67
+ assert.deepStrictEqual(convertClass('p-4'), { category: 'space', value: 'p:medium' });
68
+ assert.deepStrictEqual(convertClass('p-8'), { category: 'space', value: 'p:large' });
69
+ assert.deepStrictEqual(convertClass('px-4'), { category: 'space', value: 'p-x:medium' });
70
+ assert.deepStrictEqual(convertClass('py-2'), { category: 'space', value: 'p-y:small' });
71
+ });
72
+
73
+ it('should convert margin classes', () => {
74
+ assert.deepStrictEqual(convertClass('m-4'), { category: 'space', value: 'm:medium' });
75
+ assert.deepStrictEqual(convertClass('mt-8'), { category: 'space', value: 'm-t:large' });
76
+ assert.deepStrictEqual(convertClass('mx-auto'), { category: 'space', value: 'm-x:auto' });
77
+ });
78
+
79
+ it('should convert gap classes', () => {
80
+ assert.deepStrictEqual(convertClass('gap-4'), { category: 'space', value: 'g:medium' });
81
+ assert.deepStrictEqual(convertClass('gap-x-2'), { category: 'space', value: 'g-x:small' });
82
+ });
83
+
84
+ it('should convert width/height classes', () => {
85
+ assert.deepStrictEqual(convertClass('w-full'), { category: 'space', value: 'w:full' });
86
+ assert.deepStrictEqual(convertClass('w-1/2'), { category: 'space', value: 'w:half' });
87
+ assert.deepStrictEqual(convertClass('w-1/3'), { category: 'space', value: 'w:third' });
88
+ assert.deepStrictEqual(convertClass('w-2/3'), { category: 'space', value: 'w:third-2x' });
89
+ assert.deepStrictEqual(convertClass('w-1/4'), { category: 'space', value: 'w:quarter' });
90
+ assert.deepStrictEqual(convertClass('w-3/4'), { category: 'space', value: 'w:quarter-3x' });
91
+ assert.deepStrictEqual(convertClass('h-screen'), { category: 'space', value: 'h:[100vh]' });
92
+ assert.deepStrictEqual(convertClass('max-w-4'), { category: 'space', value: 'max-w:medium' });
93
+ });
94
+
95
+ it('should convert negative margin classes', () => {
96
+ assert.deepStrictEqual(convertClass('-m-4'), { category: 'space', value: 'm:-medium' });
97
+ assert.deepStrictEqual(convertClass('-mt-8'), { category: 'space', value: 'm-t:-large' });
98
+ assert.deepStrictEqual(convertClass('-m-4', { exact: true }), { category: 'space', value: 'm:-tw-4' });
99
+ assert.deepStrictEqual(convertClass('-m-[10px]'), { category: 'space', value: 'm:[-10px]' });
100
+ });
101
+ });
102
+
103
+ describe('Visual classes', () => {
104
+ it('should convert background color classes', () => {
105
+ assert.deepStrictEqual(convertClass('bg-blue-500'), { category: 'visual', value: 'bg:blue-500' });
106
+ assert.deepStrictEqual(convertClass('bg-white'), { category: 'visual', value: 'bg:white' });
107
+ assert.deepStrictEqual(convertClass('bg-transparent'), { category: 'visual', value: 'bg:transparent' });
108
+ });
109
+
110
+ it('should convert border color classes', () => {
111
+ assert.deepStrictEqual(convertClass('border-gray-900'), { category: 'visual', value: 'border:gray-900' });
112
+ assert.deepStrictEqual(convertClass('border-t-gray-900'), { category: 'visual', value: 'border-t:gray-900' });
113
+ assert.deepStrictEqual(convertClass('border-b-blue-500'), { category: 'visual', value: 'border-b:blue-500' });
114
+ assert.deepStrictEqual(convertClass('border-l-red-300'), { category: 'visual', value: 'border-l:red-300' });
115
+ assert.deepStrictEqual(convertClass('border-r-transparent'), { category: 'visual', value: 'border-r:transparent' });
116
+ });
117
+
118
+ it('should convert text color classes', () => {
119
+ assert.deepStrictEqual(convertClass('text-white'), { category: 'visual', value: 'text:white' });
120
+ assert.deepStrictEqual(convertClass('text-gray-700'), { category: 'visual', value: 'text:gray-700' });
121
+ });
122
+
123
+ it('should convert text alignment classes', () => {
124
+ assert.deepStrictEqual(convertClass('text-center'), { category: 'visual', value: 'text:center' });
125
+ assert.deepStrictEqual(convertClass('text-left'), { category: 'visual', value: 'text:left' });
126
+ });
127
+
128
+ it('should convert text size classes', () => {
129
+ assert.deepStrictEqual(convertClass('text-sm'), { category: 'visual', value: 'text-size:small' });
130
+ assert.deepStrictEqual(convertClass('text-xl'), { category: 'visual', value: 'text-size:big' });
131
+ assert.deepStrictEqual(convertClass('text-2xl'), { category: 'visual', value: 'text-size:huge' });
132
+ });
133
+
134
+ it('should convert border radius classes', () => {
135
+ assert.deepStrictEqual(convertClass('rounded'), { category: 'visual', value: 'rounded:small' });
136
+ assert.deepStrictEqual(convertClass('rounded-lg'), { category: 'visual', value: 'rounded:medium' });
137
+ assert.deepStrictEqual(convertClass('rounded-full'), { category: 'visual', value: 'rounded:round' });
138
+ });
139
+
140
+ it('should convert directional border radius classes', () => {
141
+ assert.deepStrictEqual(convertClass('rounded-t-lg'), { category: 'visual', value: 'rounded-t:medium' });
142
+ assert.deepStrictEqual(convertClass('rounded-b-lg'), { category: 'visual', value: 'rounded-b:medium' });
143
+ assert.deepStrictEqual(convertClass('rounded-l-lg'), { category: 'visual', value: 'rounded-l:medium' });
144
+ assert.deepStrictEqual(convertClass('rounded-r-lg'), { category: 'visual', value: 'rounded-r:medium' });
145
+ assert.deepStrictEqual(convertClass('rounded-tl-3xl'), { category: 'visual', value: 'rounded-tl:big' });
146
+ assert.deepStrictEqual(convertClass('rounded-tr-3xl'), { category: 'visual', value: 'rounded-tr:big' });
147
+ assert.deepStrictEqual(convertClass('rounded-bl-full'), { category: 'visual', value: 'rounded-bl:round' });
148
+ assert.deepStrictEqual(convertClass('rounded-br-full'), { category: 'visual', value: 'rounded-br:round' });
149
+ });
150
+
151
+ it('should convert shadow classes', () => {
152
+ assert.deepStrictEqual(convertClass('shadow'), { category: 'visual', value: 'shadow:small' });
153
+ assert.deepStrictEqual(convertClass('shadow-md'), { category: 'visual', value: 'shadow:medium' });
154
+ assert.deepStrictEqual(convertClass('shadow-lg'), { category: 'visual', value: 'shadow:big' });
155
+ });
156
+
157
+ it('should convert font weight classes', () => {
158
+ assert.deepStrictEqual(convertClass('font-bold'), { category: 'visual', value: 'font:tw-bold' });
159
+ assert.deepStrictEqual(convertClass('font-medium'), { category: 'visual', value: 'font:tw-medium' });
160
+ });
161
+
162
+ it('should convert typography keywords', () => {
163
+ assert.deepStrictEqual(convertClass('italic'), { category: 'visual', value: 'italic' });
164
+ assert.deepStrictEqual(convertClass('uppercase'), { category: 'visual', value: 'uppercase' });
165
+ assert.deepStrictEqual(convertClass('underline'), { category: 'visual', value: 'underline' });
166
+ assert.deepStrictEqual(convertClass('truncate'), { category: 'visual', value: 'truncate' });
167
+ });
168
+
169
+ it('should convert opacity classes', () => {
170
+ assert.deepStrictEqual(convertClass('opacity-50'), { category: 'visual', value: 'opacity:50' });
171
+ assert.deepStrictEqual(convertClass('opacity-100'), { category: 'visual', value: 'opacity:100' });
172
+ });
173
+
174
+ it('should convert cursor classes', () => {
175
+ assert.deepStrictEqual(convertClass('cursor-pointer'), { category: 'visual', value: 'cursor:pointer' });
176
+ assert.deepStrictEqual(convertClass('cursor-not-allowed'), { category: 'visual', value: 'cursor:not-allowed' });
177
+ });
178
+
179
+ it('should convert gradient classes', () => {
180
+ assert.deepStrictEqual(convertClass('bg-gradient-to-r'), { category: 'visual', value: 'bg-image:gradient-to-r' });
181
+ assert.deepStrictEqual(convertClass('bg-gradient-to-br'), { category: 'visual', value: 'bg-image:gradient-to-br' });
182
+ assert.deepStrictEqual(convertClass('from-blue-500'), { category: 'visual', value: 'from:blue-500' });
183
+ assert.deepStrictEqual(convertClass('via-purple-500'), { category: 'visual', value: 'via:purple-500' });
184
+ assert.deepStrictEqual(convertClass('to-pink-500'), { category: 'visual', value: 'to:pink-500' });
185
+ });
186
+
187
+ it('should convert translate utilities with fractions', () => {
188
+ assert.deepStrictEqual(convertClass('translate-x-1/2'), { category: 'visual', value: 'translate-x:half' });
189
+ assert.deepStrictEqual(convertClass('translate-y-full'), { category: 'visual', value: 'translate-y:full' });
190
+ assert.deepStrictEqual(convertClass('-translate-x-1/2'), { category: 'visual', value: 'translate-x:-half' });
191
+ assert.deepStrictEqual(convertClass('-translate-y-full'), { category: 'visual', value: 'translate-y:-full' });
192
+ });
193
+ });
194
+
195
+ describe('Prefixed classes', () => {
196
+ it('should handle responsive prefixes', () => {
197
+ assert.deepStrictEqual(convertClass('md:flex'), { category: 'layout', value: 'tw-md:flex' });
198
+ assert.deepStrictEqual(convertClass('lg:p-8'), { category: 'space', value: 'tw-lg:p:large' });
199
+ });
200
+
201
+ it('should handle dark mode prefix', () => {
202
+ assert.deepStrictEqual(convertClass('dark:bg-gray-900'), { category: 'visual', value: 'dark:bg:gray-900' });
203
+ assert.deepStrictEqual(convertClass('dark:text-white'), { category: 'visual', value: 'dark:text:white' });
204
+ });
205
+
206
+ it('should handle hover/focus prefixes', () => {
207
+ assert.deepStrictEqual(convertClass('hover:bg-blue-600'), { category: 'visual', value: 'hover:bg:blue-600' });
208
+ assert.strictEqual(convertClass('focus:ring'), null);
209
+ });
210
+ });
211
+
212
+ describe('Arbitrary values', () => {
213
+ it('should handle arbitrary spacing', () => {
214
+ assert.deepStrictEqual(convertClass('p-[20px]'), { category: 'space', value: 'p:[20px]' });
215
+ assert.deepStrictEqual(convertClass('w-[300px]'), { category: 'space', value: 'w:[300px]' });
216
+ });
217
+ });
218
+
219
+ describe('Unrecognized classes', () => {
220
+ it('should return null for unknown classes', () => {
221
+ assert.strictEqual(convertClass('some-unknown-class'), null);
222
+ assert.strictEqual(convertClass('custom-utility'), null);
223
+ });
224
+ });
225
+
226
+ describe('Exact mode (tw- prefix)', () => {
227
+ describe('Spacing with exact mode', () => {
228
+ it('should output tw- prefix for padding', () => {
229
+ assert.deepStrictEqual(
230
+ convertClass('p-4', { exact: true }),
231
+ { category: 'space', value: 'p:tw-4' }
232
+ );
233
+ assert.deepStrictEqual(
234
+ convertClass('p-8', { exact: true }),
235
+ { category: 'space', value: 'p:tw-8' }
236
+ );
237
+ });
238
+
239
+ it('should output tw- prefix for margin', () => {
240
+ assert.deepStrictEqual(
241
+ convertClass('mt-4', { exact: true }),
242
+ { category: 'space', value: 'm-t:tw-4' }
243
+ );
244
+ });
245
+
246
+ it('should output tw- prefix for gap', () => {
247
+ assert.deepStrictEqual(
248
+ convertClass('gap-4', { exact: true }),
249
+ { category: 'space', value: 'g:tw-4' }
250
+ );
251
+ });
252
+
253
+ it('should output tw- prefix for width/height', () => {
254
+ assert.deepStrictEqual(
255
+ convertClass('w-8', { exact: true }),
256
+ { category: 'space', value: 'w:tw-8' }
257
+ );
258
+ });
259
+ });
260
+
261
+ describe('Border radius with exact mode', () => {
262
+ it('should output tw- prefix for rounded', () => {
263
+ assert.deepStrictEqual(
264
+ convertClass('rounded', { exact: true }),
265
+ { category: 'visual', value: 'rounded:tw-DEFAULT' }
266
+ );
267
+ assert.deepStrictEqual(
268
+ convertClass('rounded-lg', { exact: true }),
269
+ { category: 'visual', value: 'rounded:tw-lg' }
270
+ );
271
+ });
272
+ });
273
+
274
+ describe('Shadow with exact mode', () => {
275
+ it('should output tw- prefix for shadow', () => {
276
+ assert.deepStrictEqual(
277
+ convertClass('shadow', { exact: true }),
278
+ { category: 'visual', value: 'shadow:tw-DEFAULT' }
279
+ );
280
+ assert.deepStrictEqual(
281
+ convertClass('shadow-lg', { exact: true }),
282
+ { category: 'visual', value: 'shadow:tw-lg' }
283
+ );
284
+ });
285
+ });
286
+
287
+ describe('Font size with exact mode', () => {
288
+ it('should output tw- prefix for text size', () => {
289
+ assert.deepStrictEqual(
290
+ convertClass('text-2xl', { exact: true }),
291
+ { category: 'visual', value: 'text-size:tw-2xl' }
292
+ );
293
+ });
294
+ });
295
+
296
+ describe('Divide utilities', () => {
297
+ it('should convert divide color', () => {
298
+ assert.deepStrictEqual(
299
+ convertClass('divide-gray-200'),
300
+ { category: 'visual', value: 'divide:gray-200' }
301
+ );
302
+ });
303
+
304
+ it('should convert divide-x', () => {
305
+ assert.deepStrictEqual(
306
+ convertClass('divide-x-gray-200'),
307
+ { category: 'visual', value: 'divide-x:gray-200' }
308
+ );
309
+ });
310
+
311
+ it('should convert divide-y', () => {
312
+ assert.deepStrictEqual(
313
+ convertClass('divide-y-gray-200'),
314
+ { category: 'visual', value: 'divide-y:gray-200' }
315
+ );
316
+ });
317
+
318
+ it('should convert divide width', () => {
319
+ assert.deepStrictEqual(
320
+ convertClass('divide-2'),
321
+ { category: 'visual', value: 'divide-w:regular' }
322
+ );
323
+ });
324
+
325
+ it('should convert divide-x width', () => {
326
+ assert.deepStrictEqual(
327
+ convertClass('divide-x-2'),
328
+ { category: 'visual', value: 'divide-x-w:regular' }
329
+ );
330
+ });
331
+
332
+ it('should convert divide-y width', () => {
333
+ assert.deepStrictEqual(
334
+ convertClass('divide-y-2'),
335
+ { category: 'visual', value: 'divide-y-w:regular' }
336
+ );
337
+ });
338
+
339
+ it('should convert divide style', () => {
340
+ assert.deepStrictEqual(
341
+ convertClass('divide-dashed'),
342
+ { category: 'visual', value: 'divide-style:dashed' }
343
+ );
344
+ });
345
+
346
+ it('should convert divide style with exact mode', () => {
347
+ assert.deepStrictEqual(
348
+ convertClass('divide-dashed', { exact: true }),
349
+ { category: 'visual', value: 'divide-style:dashed' }
350
+ );
351
+ });
352
+ });
353
+
354
+ describe('convertClasses with exact mode', () => {
355
+ it('should group classes with tw- prefix', () => {
356
+ const result = convertClasses('flex p-4 rounded-lg', { exact: true });
357
+
358
+ assert.deepStrictEqual(result.layout, ['flex']);
359
+ assert.deepStrictEqual(result.space, ['p:tw-4']);
360
+ assert.deepStrictEqual(result.visual, ['rounded:tw-lg']);
361
+ });
362
+ });
363
+
364
+ describe('convertHTML with exact mode', () => {
365
+ it('should convert HTML with tw- prefix', () => {
366
+ const input = '<div class="p-4 rounded-lg"></div>';
367
+ const result = convertHTML(input, { exact: true });
368
+
369
+ assert.ok(result.includes('space="p:tw-4"'));
370
+ assert.ok(result.includes('visual="rounded:tw-lg"'));
371
+ });
372
+ });
373
+ });
374
+ });
375
+
376
+ describe('convertClasses', () => {
377
+ it('should group classes by category', () => {
378
+ const result = convertClasses('flex items-center p-4 bg-blue-500 text-white');
379
+
380
+ assert.deepStrictEqual(result.layout, ['flex', 'items:center']);
381
+ assert.deepStrictEqual(result.space, ['p:medium']);
382
+ assert.deepStrictEqual(result.visual, ['bg:blue-500', 'text:white']);
383
+ assert.deepStrictEqual(result.unrecognized, []);
384
+ });
385
+
386
+ it('should collect unrecognized classes', () => {
387
+ const result = convertClasses('flex my-custom-class p-4');
388
+
389
+ assert.deepStrictEqual(result.unrecognized, ['my-custom-class']);
390
+ });
391
+ });
392
+
393
+ describe('convertHTML', () => {
394
+ it('should convert simple HTML element', () => {
395
+ const input = '<div class="flex items-center p-4 bg-blue-500"></div>';
396
+ const result = convertHTML(input);
397
+
398
+ assert.ok(result.includes('layout="flex items:center"'));
399
+ assert.ok(result.includes('space="p:medium"'));
400
+ assert.ok(result.includes('visual="bg:blue-500"'));
401
+ assert.ok(!result.includes('class='));
402
+ });
403
+
404
+ it('should preserve unrecognized classes in class attribute', () => {
405
+ const input = '<div class="flex my-custom-class"></div>';
406
+ const result = convertHTML(input);
407
+
408
+ assert.ok(result.includes('layout="flex"'));
409
+ assert.ok(result.includes('class="my-custom-class"'));
410
+ });
411
+
412
+ it('should handle multiple elements', () => {
413
+ const input = `
414
+ <div class="flex">
415
+ <span class="text-white">Hello</span>
416
+ </div>
417
+ `;
418
+ const result = convertHTML(input);
419
+
420
+ assert.ok(result.includes('layout="flex"'));
421
+ assert.ok(result.includes('visual="text:white"'));
422
+ });
423
+
424
+ it('should handle single quotes', () => {
425
+ const input = "<div class='flex p-4'></div>";
426
+ const result = convertHTML(input);
427
+
428
+ assert.ok(result.includes('layout="flex"'));
429
+ assert.ok(result.includes('space="p:medium"'));
430
+ });
431
+ });
432
+
433
+ describe('spacingScale', () => {
434
+ it('should have expected scale mappings', () => {
435
+ assert.strictEqual(spacingScale['0'], 'none');
436
+ assert.strictEqual(spacingScale['4'], 'medium');
437
+ assert.strictEqual(spacingScale['8'], 'large');
438
+ assert.strictEqual(spacingScale['12'], 'big');
439
+ assert.strictEqual(spacingScale['24'], 'giant');
440
+ assert.strictEqual(spacingScale['auto'], 'auto');
441
+ });
442
+ });
443
+
444
+ describe('convert-tailwind CLI', () => {
445
+ it('should show help message', () => {
446
+ const output = execSync(`node "${SCRIPT_PATH}" --help`).toString();
447
+ assert.match(output, /Usage:/);
448
+ assert.match(output, /Options:/);
449
+ });
450
+
451
+ it('should convert string input', () => {
452
+ const input = '"<div class=\'p-4\'></div>"';
453
+ const output = execSync(`node "${SCRIPT_PATH}" --string ${input}`).toString();
454
+ assert.match(output, /space="p:medium"/);
455
+ assert.doesNotMatch(output, /class=/);
456
+ });
457
+
458
+ it('should support exact mode', () => {
459
+ const input = '"<div class=\'p-4\'></div>"';
460
+ const output = execSync(`node "${SCRIPT_PATH}" --string ${input} --exact`).toString();
461
+ assert.match(output, /space="p:tw-4"/);
462
+ });
463
+
464
+ it('should fail without input file', () => {
465
+ try {
466
+ execSync(`node "${SCRIPT_PATH}" --exact`);
467
+ assert.fail('Should have failed');
468
+ } catch (error) {
469
+ assert.strictEqual(error.status, 1);
470
+ assert.match(error.stderr.toString(), /Error: Input file required/);
471
+ }
472
+ });
473
+
474
+ it('should fail with missing string argument', () => {
475
+ try {
476
+ execSync(`node "${SCRIPT_PATH}" --string`);
477
+ assert.fail('Should have failed');
478
+ } catch (error) {
479
+ assert.strictEqual(error.status, 1);
480
+ assert.match(error.stderr.toString(), /Error: --string requires an HTML string argument/);
481
+ }
482
+ });
483
+
484
+ it('should handle file input/output', () => {
485
+ const inputFile = path.join(process.cwd(), 'tests', 'fixtures', 'test-input.html');
486
+ const outputFile = path.join(process.cwd(), 'tests', 'fixtures', 'test-output.html');
487
+
488
+ fs.mkdirSync(path.dirname(inputFile), { recursive: true });
489
+
490
+ const content = '<div class="p-4 flex"></div>';
491
+ fs.writeFileSync(inputFile, content);
492
+
493
+ execSync(`node "${SCRIPT_PATH}" "${inputFile}" -o "${outputFile}"`);
494
+
495
+ const result = fs.readFileSync(outputFile, 'utf-8');
496
+ assert.match(result, /layout="flex"/);
497
+ assert.match(result, /space="p:medium"/);
498
+
499
+ fs.unlinkSync(inputFile);
500
+ fs.unlinkSync(outputFile);
501
+ });
502
+
503
+ it('should fail on invalid file paths', () => {
504
+ try {
505
+ execSync(`node "${SCRIPT_PATH}" non-existent.html`);
506
+ assert.fail('Should have failed');
507
+ } catch (error) {
508
+ assert.strictEqual(error.status, 1);
509
+ }
510
+ });
511
+
512
+ it('should handle unrecognized classes', () => {
513
+ const input = '"<div class=\'p-4 custom-class\'></div>"';
514
+ const output = execSync(`node "${SCRIPT_PATH}" --string ${input}`).toString();
515
+ assert.match(output, /space="p:medium"/);
516
+ assert.match(output, /class="custom-class"/);
517
+ });
518
+ });