tailwind-typescript-plugin 1.4.0-beta.11 → 1.4.0-beta.13

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.
@@ -37,6 +37,11 @@ const ts = __importStar(require("typescript/lib/tsserverlibrary"));
37
37
  const TailwindValidator_1 = require("../infrastructure/TailwindValidator");
38
38
  const Logger_1 = require("../utils/Logger");
39
39
  const CompletionService_1 = require("./CompletionService");
40
+ const defaultConfig = {
41
+ utilityFunctions: ['clsx', 'cn', 'classnames', 'classNames', 'cx', 'twMerge'],
42
+ tailwindVariantsEnabled: true,
43
+ classVarianceAuthorityEnabled: true
44
+ };
40
45
  describe('CompletionService', () => {
41
46
  let validator;
42
47
  let completionService;
@@ -74,7 +79,7 @@ describe('CompletionService', () => {
74
79
  return null;
75
80
  });
76
81
  });
77
- completionService = new CompletionService_1.CompletionService(validator, new Logger_1.NoOpLogger());
82
+ completionService = new CompletionService_1.CompletionService(validator, new Logger_1.NoOpLogger(), defaultConfig);
78
83
  });
79
84
  describe('getCompletionsAtPosition', () => {
80
85
  it('should return original completions when not in className context', () => {
@@ -330,5 +335,849 @@ describe('CompletionService', () => {
330
335
  expect(names).not.toContain('flex');
331
336
  });
332
337
  });
338
+ describe('custom utility functions', () => {
339
+ it('should provide completions in custom utility function', () => {
340
+ const customConfig = {
341
+ utilityFunctions: ['myCustomClass', { name: 'styles', from: '@/utils' }],
342
+ tailwindVariantsEnabled: false,
343
+ classVarianceAuthorityEnabled: false
344
+ };
345
+ const customService = new CompletionService_1.CompletionService(validator, new Logger_1.NoOpLogger(), customConfig);
346
+ const sourceCode = 'const x = myCustomClass("fl");';
347
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
348
+ // Position after "fl"
349
+ const position = 27;
350
+ const result = customService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
351
+ expect(result).toBeDefined();
352
+ const names = result.entries.map(e => e.name);
353
+ expect(names).toContain('flex');
354
+ });
355
+ it('should provide completions in tv() when tailwindVariants is enabled', () => {
356
+ const sourceCode = 'const button = tv({ base: "fl" });';
357
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
358
+ // Position at "l" inside "fl" string (position 28)
359
+ // const button = tv({ base: "fl" });
360
+ // ^^ position 27-28
361
+ const position = 28;
362
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
363
+ expect(result).toBeDefined();
364
+ const names = result.entries.map(e => e.name);
365
+ expect(names).toContain('flex');
366
+ });
367
+ it('should NOT provide completions in tv() when tailwindVariants is disabled', () => {
368
+ const customConfig = {
369
+ utilityFunctions: ['clsx'],
370
+ tailwindVariantsEnabled: false,
371
+ classVarianceAuthorityEnabled: false
372
+ };
373
+ const customService = new CompletionService_1.CompletionService(validator, new Logger_1.NoOpLogger(), customConfig);
374
+ const sourceCode = 'const button = tv({ base: "fl" });';
375
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
376
+ // Position after "fl" inside the string
377
+ const position = 29;
378
+ const result = customService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
379
+ // Should not provide Tailwind completions since tv() is not recognized
380
+ expect(result).toBeUndefined();
381
+ });
382
+ it('should provide completions in cva() when classVarianceAuthority is enabled', () => {
383
+ const sourceCode = 'const button = cva("fl");';
384
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
385
+ // Position after "fl"
386
+ const position = 22;
387
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
388
+ expect(result).toBeDefined();
389
+ const names = result.entries.map(e => e.name);
390
+ expect(names).toContain('flex');
391
+ });
392
+ it('should NOT provide completions in cva() when classVarianceAuthority is disabled', () => {
393
+ const customConfig = {
394
+ utilityFunctions: ['clsx'],
395
+ tailwindVariantsEnabled: false,
396
+ classVarianceAuthorityEnabled: false
397
+ };
398
+ const customService = new CompletionService_1.CompletionService(validator, new Logger_1.NoOpLogger(), customConfig);
399
+ const sourceCode = 'const button = cva("fl");';
400
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
401
+ // Position after "fl"
402
+ const position = 22;
403
+ const result = customService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
404
+ // Should not provide Tailwind completions since cva() is not recognized
405
+ expect(result).toBeUndefined();
406
+ });
407
+ it('should provide completions with UtilityFunctionConfig objects', () => {
408
+ const customConfig = {
409
+ utilityFunctions: [{ name: 'customStyles', from: '@/lib/styles' }],
410
+ tailwindVariantsEnabled: false,
411
+ classVarianceAuthorityEnabled: false
412
+ };
413
+ const customService = new CompletionService_1.CompletionService(validator, new Logger_1.NoOpLogger(), customConfig);
414
+ const sourceCode = 'const x = customStyles("fl");';
415
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
416
+ const position = 26;
417
+ const result = customService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
418
+ expect(result).toBeDefined();
419
+ const names = result.entries.map(e => e.name);
420
+ expect(names).toContain('flex');
421
+ });
422
+ it('should provide completions with mixed string and object utility functions', () => {
423
+ const customConfig = {
424
+ utilityFunctions: ['simpleUtil', { name: 'configuredUtil', from: '@/utils' }],
425
+ tailwindVariantsEnabled: false,
426
+ classVarianceAuthorityEnabled: false
427
+ };
428
+ const customService = new CompletionService_1.CompletionService(validator, new Logger_1.NoOpLogger(), customConfig);
429
+ // Test simple string utility
430
+ const sourceCode1 = 'const x = simpleUtil("fl");';
431
+ const sourceFile1 = ts.createSourceFile('test.tsx', sourceCode1, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
432
+ const result1 = customService.getCompletionsAtPosition(ts, sourceFile1, 24, undefined);
433
+ expect(result1).toBeDefined();
434
+ expect(result1.entries.map(e => e.name)).toContain('flex');
435
+ // Test configured utility
436
+ const sourceCode2 = 'const x = configuredUtil("fl");';
437
+ const sourceFile2 = ts.createSourceFile('test.tsx', sourceCode2, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
438
+ const result2 = customService.getCompletionsAtPosition(ts, sourceFile2, 28, undefined);
439
+ expect(result2).toBeDefined();
440
+ expect(result2.entries.map(e => e.name)).toContain('flex');
441
+ });
442
+ it('should provide completions in tv() variants property', () => {
443
+ const sourceCode = `const button = tv({
444
+ base: "flex",
445
+ variants: {
446
+ color: {
447
+ primary: "bg-blue-500 te",
448
+ secondary: "bg-gray-500"
449
+ }
450
+ }
451
+ });`;
452
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
453
+ // Find position after "te" in primary variant
454
+ const position = sourceCode.indexOf('te",') + 2;
455
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
456
+ expect(result).toBeDefined();
457
+ const names = result.entries.map(e => e.name);
458
+ expect(names).toContain('text-white');
459
+ expect(names).toContain('text-black');
460
+ });
461
+ it('should provide completions in cva() variants property', () => {
462
+ const sourceCode = `const button = cva("base-class", {
463
+ variants: {
464
+ size: {
465
+ sm: "p-",
466
+ lg: "p-4"
467
+ }
468
+ }
469
+ });`;
470
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
471
+ // Find position after "p-" in sm variant
472
+ const position = sourceCode.indexOf('p-",') + 2;
473
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
474
+ expect(result).toBeDefined();
475
+ const names = result.entries.map(e => e.name);
476
+ // Should match classes starting with "p-" from the mock (p-4, px-4, py-4)
477
+ expect(names).toContain('p-4');
478
+ });
479
+ it('should provide completions in tv() compoundVariants', () => {
480
+ const sourceCode = `const button = tv({
481
+ base: "flex",
482
+ variants: {
483
+ color: { primary: "bg-blue-500" }
484
+ },
485
+ compoundVariants: [
486
+ { color: "primary", class: "fl" }
487
+ ]
488
+ });`;
489
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
490
+ // Find position after "fl" in compoundVariants class
491
+ const position = sourceCode.indexOf('"fl"') + 3;
492
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
493
+ expect(result).toBeDefined();
494
+ const names = result.entries.map(e => e.name);
495
+ expect(names).toContain('flex');
496
+ });
497
+ it('should enable both tv and cva when both variant options are true', () => {
498
+ const customConfig = {
499
+ utilityFunctions: [],
500
+ tailwindVariantsEnabled: true,
501
+ classVarianceAuthorityEnabled: true
502
+ };
503
+ const customService = new CompletionService_1.CompletionService(validator, new Logger_1.NoOpLogger(), customConfig);
504
+ // Test tv()
505
+ const tvSource = 'const x = tv({ base: "fl" });';
506
+ const tvFile = ts.createSourceFile('test.tsx', tvSource, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
507
+ const tvResult = customService.getCompletionsAtPosition(ts, tvFile, 24, undefined);
508
+ expect(tvResult).toBeDefined();
509
+ // Test cva()
510
+ const cvaSource = 'const x = cva("fl");';
511
+ const cvaFile = ts.createSourceFile('test.tsx', cvaSource, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
512
+ const cvaResult = customService.getCompletionsAtPosition(ts, cvaFile, 17, undefined);
513
+ expect(cvaResult).toBeDefined();
514
+ });
515
+ });
516
+ describe('JSX attribute variations', () => {
517
+ it('should provide completions in "class" attribute', () => {
518
+ const sourceCode = '<div class="fl">Hello</div>';
519
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
520
+ const position = 14;
521
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
522
+ expect(result).toBeDefined();
523
+ const names = result.entries.map(e => e.name);
524
+ expect(names).toContain('flex');
525
+ });
526
+ it('should provide completions in "classList" attribute', () => {
527
+ const sourceCode = '<div classList="fl">Hello</div>';
528
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
529
+ const position = 18;
530
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
531
+ expect(result).toBeDefined();
532
+ const names = result.entries.map(e => e.name);
533
+ expect(names).toContain('flex');
534
+ });
535
+ it('should NOT provide completions in non-className attributes', () => {
536
+ const sourceCode = '<div id="fl">Hello</div>';
537
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
538
+ const position = 11;
539
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
540
+ expect(result).toBeUndefined();
541
+ });
542
+ it('should NOT provide completions in data attributes', () => {
543
+ const sourceCode = '<div data-class="fl">Hello</div>';
544
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
545
+ const position = 19;
546
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
547
+ expect(result).toBeUndefined();
548
+ });
549
+ it('should provide completions in self-closing JSX element', () => {
550
+ const sourceCode = '<input className="fl" />';
551
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
552
+ const position = 20;
553
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
554
+ expect(result).toBeDefined();
555
+ const names = result.entries.map(e => e.name);
556
+ expect(names).toContain('flex');
557
+ });
558
+ it('should provide completions in nested JSX elements', () => {
559
+ const sourceCode = '<div><span className="fl"></span></div>';
560
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
561
+ const position = 24;
562
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
563
+ expect(result).toBeDefined();
564
+ const names = result.entries.map(e => e.name);
565
+ expect(names).toContain('flex');
566
+ });
567
+ });
568
+ describe('utility function contexts', () => {
569
+ it('should provide completions in nested utility function calls', () => {
570
+ const sourceCode = 'const x = cn(clsx("fl"));';
571
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
572
+ const position = 21;
573
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
574
+ expect(result).toBeDefined();
575
+ const names = result.entries.map(e => e.name);
576
+ expect(names).toContain('flex');
577
+ });
578
+ it('should provide completions in twMerge function', () => {
579
+ const sourceCode = 'const x = twMerge("fl");';
580
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
581
+ const position = 21;
582
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
583
+ expect(result).toBeDefined();
584
+ const names = result.entries.map(e => e.name);
585
+ expect(names).toContain('flex');
586
+ });
587
+ it('should provide completions in classnames function (lowercase)', () => {
588
+ const sourceCode = 'const x = classnames("fl");';
589
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
590
+ const position = 24;
591
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
592
+ expect(result).toBeDefined();
593
+ const names = result.entries.map(e => e.name);
594
+ expect(names).toContain('flex');
595
+ });
596
+ it('should provide completions in classNames function (camelCase)', () => {
597
+ const sourceCode = 'const x = classNames("fl");';
598
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
599
+ const position = 24;
600
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
601
+ expect(result).toBeDefined();
602
+ const names = result.entries.map(e => e.name);
603
+ expect(names).toContain('flex');
604
+ });
605
+ it('should provide completions in cx function', () => {
606
+ const sourceCode = 'const x = cx("fl");';
607
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
608
+ const position = 16;
609
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
610
+ expect(result).toBeDefined();
611
+ const names = result.entries.map(e => e.name);
612
+ expect(names).toContain('flex');
613
+ });
614
+ it('should provide completions in conditional utility function argument', () => {
615
+ const sourceCode = 'const x = cn(isActive ? "fl" : "hidden");';
616
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
617
+ // Position inside the first string "fl"
618
+ const position = 27;
619
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
620
+ expect(result).toBeDefined();
621
+ const names = result.entries.map(e => e.name);
622
+ expect(names).toContain('flex');
623
+ });
624
+ it('should provide completions in array argument of utility function', () => {
625
+ const sourceCode = 'const x = cn(["fl", "items-center"]);';
626
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
627
+ // Position inside "fl"
628
+ const position = 17;
629
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
630
+ expect(result).toBeDefined();
631
+ const names = result.entries.map(e => e.name);
632
+ expect(names).toContain('flex');
633
+ });
634
+ it('should provide completions in object value of utility function', () => {
635
+ const sourceCode = 'const x = cn({ "fl": isActive });';
636
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
637
+ // Position inside "fl" key
638
+ const position = 18;
639
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
640
+ expect(result).toBeDefined();
641
+ const names = result.entries.map(e => e.name);
642
+ expect(names).toContain('flex');
643
+ });
644
+ it('should NOT provide completions in unknown function', () => {
645
+ const sourceCode = 'const x = unknownFn("fl");';
646
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
647
+ const position = 23;
648
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
649
+ expect(result).toBeUndefined();
650
+ });
651
+ it('should provide completions with property access expression (method call)', () => {
652
+ // When cn is called as a method: utils.cn("fl")
653
+ const customConfig = {
654
+ utilityFunctions: ['cn'],
655
+ tailwindVariantsEnabled: false,
656
+ classVarianceAuthorityEnabled: false
657
+ };
658
+ const customService = new CompletionService_1.CompletionService(validator, new Logger_1.NoOpLogger(), customConfig);
659
+ const sourceCode = 'const x = utils.cn("fl");';
660
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
661
+ const position = 22;
662
+ const result = customService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
663
+ expect(result).toBeDefined();
664
+ const names = result.entries.map(e => e.name);
665
+ expect(names).toContain('flex');
666
+ });
667
+ });
668
+ describe('edge cases and position handling', () => {
669
+ it('should handle empty className attribute', () => {
670
+ const sourceCode = '<div className="">Hello</div>';
671
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
672
+ // Position inside empty string
673
+ const position = 16;
674
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
675
+ expect(result).toBeDefined();
676
+ expect(result.entries.length).toBeGreaterThan(0);
677
+ });
678
+ it('should handle position at start of string content', () => {
679
+ const sourceCode = '<div className="flex">Hello</div>';
680
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
681
+ // Position at start of string content (right after opening quote)
682
+ const position = 16;
683
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
684
+ expect(result).toBeDefined();
685
+ });
686
+ it('should handle multiple spaces between classes', () => {
687
+ const sourceCode = '<div className="flex items-center">Hello</div>';
688
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
689
+ // Position after multiple spaces
690
+ const position = 23;
691
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
692
+ expect(result).toBeDefined();
693
+ });
694
+ it('should handle trailing space in className', () => {
695
+ const sourceCode = '<div className="flex ">Hello</div>';
696
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
697
+ // Position after trailing space
698
+ const position = 21;
699
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
700
+ expect(result).toBeDefined();
701
+ // flex should be excluded as it's already in the string
702
+ const names = result.entries.map(e => e.name);
703
+ expect(names).not.toContain('flex');
704
+ });
705
+ it('should return existingCompletions when no Tailwind classes match', () => {
706
+ // Mock empty class list
707
+ jest.spyOn(validator, 'getAllClasses').mockReturnValue([]);
708
+ const existingCompletions = {
709
+ isGlobalCompletion: false,
710
+ isMemberCompletion: false,
711
+ isNewIdentifierLocation: false,
712
+ entries: [{ name: 'existing', kind: ts.ScriptElementKind.unknown, sortText: '0' }]
713
+ };
714
+ const sourceCode = '<div className="xyz">Hello</div>';
715
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
716
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, 19, existingCompletions);
717
+ expect(result).toBe(existingCompletions);
718
+ });
719
+ it('should handle cursor position before string start', () => {
720
+ const sourceCode = '<div className="flex">Hello</div>';
721
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
722
+ // Position before the opening quote
723
+ const position = 14;
724
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
725
+ // Should not provide completions when cursor is outside string
726
+ expect(result).toBeUndefined();
727
+ });
728
+ it('should handle very long class names', () => {
729
+ jest
730
+ .spyOn(validator, 'getAllClasses')
731
+ .mockReturnValue(['very-long-tailwind-class-name-that-is-quite-long', 'flex']);
732
+ const sourceCode = '<div className="very">Hello</div>';
733
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
734
+ const position = 20;
735
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
736
+ expect(result).toBeDefined();
737
+ const names = result.entries.map(e => e.name);
738
+ expect(names).toContain('very-long-tailwind-class-name-that-is-quite-long');
739
+ });
740
+ it('should handle special characters in class prefix', () => {
741
+ jest
742
+ .spyOn(validator, 'getAllClasses')
743
+ .mockReturnValue(['w-1/2', 'w-1/3', 'w-1/4', '-mt-4', '-translate-x-1/2']);
744
+ const sourceCode = '<div className="w-1/">Hello</div>';
745
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
746
+ const position = 20;
747
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
748
+ expect(result).toBeDefined();
749
+ const names = result.entries.map(e => e.name);
750
+ expect(names).toContain('w-1/2');
751
+ expect(names).toContain('w-1/3');
752
+ expect(names).toContain('w-1/4');
753
+ });
754
+ it('should handle negative value classes', () => {
755
+ jest.spyOn(validator, 'getAllClasses').mockReturnValue(['-mt-4', '-mb-4', '-ml-4', '-mr-4']);
756
+ const sourceCode = '<div className="-m">Hello</div>';
757
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
758
+ const position = 18;
759
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
760
+ expect(result).toBeDefined();
761
+ const names = result.entries.map(e => e.name);
762
+ expect(names).toContain('-mt-4');
763
+ expect(names).toContain('-mb-4');
764
+ });
765
+ it('should handle arbitrary value classes', () => {
766
+ jest
767
+ .spyOn(validator, 'getAllClasses')
768
+ .mockReturnValue(['w-[100px]', 'h-[50vh]', 'bg-[#ff0000]']);
769
+ const sourceCode = '<div className="w-[">Hello</div>';
770
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
771
+ const position = 19;
772
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
773
+ expect(result).toBeDefined();
774
+ const names = result.entries.map(e => e.name);
775
+ expect(names).toContain('w-[100px]');
776
+ });
777
+ });
778
+ describe('sort text ordering', () => {
779
+ it('should prioritize exact matches', () => {
780
+ jest.spyOn(validator, 'getAllClasses').mockReturnValue(['flex', 'flex-row', 'flex-col']);
781
+ const sourceCode = '<div className="flex">Hello</div>';
782
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
783
+ const position = 20;
784
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
785
+ expect(result).toBeDefined();
786
+ const flexEntry = result.entries.find(e => e.name === 'flex');
787
+ const flexRowEntry = result.entries.find(e => e.name === 'flex-row');
788
+ // Exact match should have lower sortText (higher priority)
789
+ expect(flexEntry.sortText < flexRowEntry.sortText).toBe(true);
790
+ });
791
+ it('should prioritize prefix matches over non-matches', () => {
792
+ jest
793
+ .spyOn(validator, 'getAllClasses')
794
+ .mockReturnValue(['flex', 'flex-row', 'items-center', 'justify-center']);
795
+ const sourceCode = '<div className="fl">Hello</div>';
796
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
797
+ const position = 18;
798
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
799
+ expect(result).toBeDefined();
800
+ const flexEntry = result.entries.find(e => e.name === 'flex');
801
+ // Prefix match should start with "1"
802
+ expect(flexEntry.sortText.startsWith('1')).toBe(true);
803
+ });
804
+ it('should sort alphabetically within same priority', () => {
805
+ jest.spyOn(validator, 'getAllClasses').mockReturnValue(['flex-col', 'flex-row', 'flex']);
806
+ const sourceCode = '<div className="flex-">Hello</div>';
807
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
808
+ const position = 21;
809
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
810
+ expect(result).toBeDefined();
811
+ const names = result.entries.map(e => e.name);
812
+ // Both should be prefix matches with alphabetical ordering
813
+ const colIndex = names.indexOf('flex-col');
814
+ const rowIndex = names.indexOf('flex-row');
815
+ expect(colIndex).toBeLessThan(rowIndex);
816
+ });
817
+ });
818
+ describe('color class detection - additional patterns', () => {
819
+ beforeEach(() => {
820
+ jest
821
+ .spyOn(validator, 'getAllClasses')
822
+ .mockReturnValue([
823
+ 'bg-red-500',
824
+ 'bg-red-500/50',
825
+ 'bg-[#ff0000]',
826
+ 'bg-gradient-to-r',
827
+ 'text-transparent',
828
+ 'text-current',
829
+ 'text-inherit',
830
+ 'placeholder-gray-400',
831
+ 'caret-blue-500',
832
+ 'decoration-pink-500',
833
+ 'divide-slate-200',
834
+ 'ring-offset-white',
835
+ 'shadow-black/25',
836
+ 'bg-opacity-50',
837
+ 'text-opacity-75'
838
+ ]);
839
+ });
840
+ it('should detect color classes with opacity modifiers', () => {
841
+ const sourceCode = '<div className="">Hello</div>';
842
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
843
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, 16, undefined);
844
+ expect(result).toBeDefined();
845
+ expect(result.entries.find(e => e.name === 'bg-red-500/50')?.kindModifiers).toBe('color');
846
+ expect(result.entries.find(e => e.name === 'shadow-black/25')?.kindModifiers).toBe('color');
847
+ });
848
+ it('should detect arbitrary color values', () => {
849
+ const sourceCode = '<div className="">Hello</div>';
850
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
851
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, 16, undefined);
852
+ expect(result).toBeDefined();
853
+ expect(result.entries.find(e => e.name === 'bg-[#ff0000]')?.kindModifiers).toBe('color');
854
+ });
855
+ it('should detect special color values (transparent, current, inherit)', () => {
856
+ const sourceCode = '<div className="">Hello</div>';
857
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
858
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, 16, undefined);
859
+ expect(result).toBeDefined();
860
+ expect(result.entries.find(e => e.name === 'text-transparent')?.kindModifiers).toBe('color');
861
+ expect(result.entries.find(e => e.name === 'text-current')?.kindModifiers).toBe('color');
862
+ expect(result.entries.find(e => e.name === 'text-inherit')?.kindModifiers).toBe('color');
863
+ });
864
+ it('should detect placeholder, caret, and decoration colors', () => {
865
+ const sourceCode = '<div className="">Hello</div>';
866
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
867
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, 16, undefined);
868
+ expect(result).toBeDefined();
869
+ expect(result.entries.find(e => e.name === 'placeholder-gray-400')?.kindModifiers).toBe('color');
870
+ expect(result.entries.find(e => e.name === 'caret-blue-500')?.kindModifiers).toBe('color');
871
+ expect(result.entries.find(e => e.name === 'decoration-pink-500')?.kindModifiers).toBe('color');
872
+ });
873
+ it('should NOT mark gradient direction as color', () => {
874
+ const sourceCode = '<div className="">Hello</div>';
875
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
876
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, 16, undefined);
877
+ expect(result).toBeDefined();
878
+ // bg-gradient-to-r is not a color, it's a gradient direction
879
+ expect(result.entries.find(e => e.name === 'bg-gradient-to-r')?.kindModifiers).toBe('');
880
+ });
881
+ });
882
+ describe('getCompletionEntryDetails - additional cases', () => {
883
+ it('should set color kindModifier in completion details', () => {
884
+ jest
885
+ .spyOn(validator, 'getCssForClasses')
886
+ .mockReturnValue(['.bg-red-500 { background-color: rgb(239 68 68); }']);
887
+ const sourceCode = '<div className="bg-red-500">Hello</div>';
888
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
889
+ const result = completionService.getCompletionEntryDetails(ts, sourceFile, 16, 'bg-red-500');
890
+ expect(result).toBeDefined();
891
+ expect(result.kindModifiers).toBe('color');
892
+ });
893
+ it('should NOT set color kindModifier for non-color class in details', () => {
894
+ const sourceCode = '<div className="flex">Hello</div>';
895
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
896
+ const result = completionService.getCompletionEntryDetails(ts, sourceFile, 16, 'flex');
897
+ expect(result).toBeDefined();
898
+ expect(result.kindModifiers).toBe('');
899
+ });
900
+ it('should handle class with no CSS definition gracefully', () => {
901
+ jest.spyOn(validator, 'getCssForClasses').mockReturnValue([null]);
902
+ jest.spyOn(validator, 'isValidClass').mockReturnValue(true);
903
+ const sourceCode = '<div className="unknown-class">Hello</div>';
904
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
905
+ const result = completionService.getCompletionEntryDetails(ts, sourceFile, 16, 'unknown-class');
906
+ expect(result).toBeDefined();
907
+ expect(result.name).toBe('unknown-class');
908
+ expect(result.documentation).toEqual([]);
909
+ expect(result.displayParts[0].text).toBe('unknown-class');
910
+ });
911
+ it('should format CSS with already formatted multiline input', () => {
912
+ const multilineCss = `.complex {\n display: flex;\n align-items: center;\n}`;
913
+ jest.spyOn(validator, 'getCssForClasses').mockReturnValue([multilineCss]);
914
+ jest.spyOn(validator, 'isValidClass').mockReturnValue(true);
915
+ const sourceCode = '<div className="complex">Hello</div>';
916
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
917
+ const result = completionService.getCompletionEntryDetails(ts, sourceFile, 16, 'complex');
918
+ expect(result).toBeDefined();
919
+ // Should preserve the already formatted CSS
920
+ expect(result.documentation[0].text).toContain(multilineCss);
921
+ });
922
+ });
923
+ describe('template literals', () => {
924
+ it('should provide completions in template literal utility function', () => {
925
+ const sourceCode = 'const x = cn(`fl`);';
926
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
927
+ // Position inside template literal
928
+ const position = 16;
929
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
930
+ expect(result).toBeDefined();
931
+ const names = result.entries.map(e => e.name);
932
+ expect(names).toContain('flex');
933
+ });
934
+ it('should provide completions in NoSubstitutionTemplateLiteral inside utility function', () => {
935
+ const sourceCode = 'const cls = clsx(`flex fl`);';
936
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
937
+ // Position after "fl" inside template literal
938
+ const position = 25;
939
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
940
+ expect(result).toBeDefined();
941
+ const names = result.entries.map(e => e.name);
942
+ expect(names).toContain('flex-row');
943
+ expect(names).toContain('flex-col');
944
+ });
945
+ });
946
+ describe('boundary conditions', () => {
947
+ it('should NOT provide completions inside arrow function body', () => {
948
+ const customConfig = {
949
+ utilityFunctions: [],
950
+ tailwindVariantsEnabled: false,
951
+ classVarianceAuthorityEnabled: false
952
+ };
953
+ const customService = new CompletionService_1.CompletionService(validator, new Logger_1.NoOpLogger(), customConfig);
954
+ const sourceCode = 'const fn = () => { const x = "fl"; };';
955
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
956
+ // Position inside string in arrow function (not a utility function context)
957
+ const position = 32;
958
+ const result = customService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
959
+ expect(result).toBeUndefined();
960
+ });
961
+ it('should NOT provide completions inside regular function body', () => {
962
+ const customConfig = {
963
+ utilityFunctions: [],
964
+ tailwindVariantsEnabled: false,
965
+ classVarianceAuthorityEnabled: false
966
+ };
967
+ const customService = new CompletionService_1.CompletionService(validator, new Logger_1.NoOpLogger(), customConfig);
968
+ const sourceCode = 'function fn() { const x = "fl"; }';
969
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
970
+ // Position inside string in function (not a utility function context)
971
+ const position = 29;
972
+ const result = customService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
973
+ expect(result).toBeUndefined();
974
+ });
975
+ it('should provide completions when utility function is inside arrow function', () => {
976
+ const sourceCode = 'const fn = () => cn("fl");';
977
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
978
+ // Position inside cn() string argument
979
+ const position = 23;
980
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
981
+ expect(result).toBeDefined();
982
+ const names = result.entries.map(e => e.name);
983
+ expect(names).toContain('flex');
984
+ });
985
+ });
986
+ describe('case sensitivity', () => {
987
+ it('should handle case-insensitive prefix matching', () => {
988
+ const sourceCode = '<div className="FL">Hello</div>';
989
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
990
+ const position = 18;
991
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
992
+ expect(result).toBeDefined();
993
+ const names = result.entries.map(e => e.name);
994
+ // Should still match lowercase flex classes even with uppercase prefix
995
+ expect(names).toContain('flex');
996
+ expect(names).toContain('flex-row');
997
+ });
998
+ });
999
+ describe('replacement span', () => {
1000
+ it('should set correct replacement span at start of string', () => {
1001
+ const sourceCode = '<div className="fl">Hello</div>';
1002
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
1003
+ const position = 18;
1004
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
1005
+ expect(result).toBeDefined();
1006
+ const flexEntry = result.entries.find(e => e.name === 'flex');
1007
+ expect(flexEntry.replacementSpan).toEqual({
1008
+ start: 16, // Start of "fl"
1009
+ length: 2 // Length of "fl"
1010
+ });
1011
+ });
1012
+ it('should set correct replacement span after space', () => {
1013
+ const sourceCode = '<div className="flex ite">Hello</div>';
1014
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
1015
+ const position = 24;
1016
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
1017
+ expect(result).toBeDefined();
1018
+ const itemsEntry = result.entries.find(e => e.name === 'items-center');
1019
+ expect(itemsEntry.replacementSpan).toEqual({
1020
+ start: 21, // Start of "ite" (after "flex ")
1021
+ length: 3 // Length of "ite"
1022
+ });
1023
+ });
1024
+ it('should set zero-length replacement span when no prefix', () => {
1025
+ const sourceCode = '<div className="flex ">Hello</div>';
1026
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
1027
+ // Position after space (no prefix yet)
1028
+ const position = 21;
1029
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
1030
+ expect(result).toBeDefined();
1031
+ const itemsEntry = result.entries.find(e => e.name === 'items-center');
1032
+ expect(itemsEntry.replacementSpan).toEqual({
1033
+ start: 21,
1034
+ length: 0 // No prefix to replace
1035
+ });
1036
+ });
1037
+ });
1038
+ describe('getQuickInfoAtPosition (hover)', () => {
1039
+ it('should return hover info for valid Tailwind class', () => {
1040
+ const sourceCode = '<div className="flex">Hello</div>';
1041
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
1042
+ // Position on "flex"
1043
+ const position = 18;
1044
+ const result = completionService.getQuickInfoAtPosition(ts, sourceFile, position);
1045
+ expect(result).toBeDefined();
1046
+ expect(result.displayParts).toBeDefined();
1047
+ expect(result.displayParts.length).toBeGreaterThan(0);
1048
+ expect(result.displayParts[0].text).toContain('display: flex');
1049
+ });
1050
+ it('should return correct textSpan for hovered class', () => {
1051
+ const sourceCode = '<div className="flex">Hello</div>';
1052
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
1053
+ const position = 18;
1054
+ const result = completionService.getQuickInfoAtPosition(ts, sourceFile, position);
1055
+ expect(result).toBeDefined();
1056
+ expect(result.textSpan).toEqual({
1057
+ start: 16, // Start of "flex"
1058
+ length: 4 // Length of "flex"
1059
+ });
1060
+ });
1061
+ it('should return documentation with CSS code block', () => {
1062
+ const sourceCode = '<div className="flex">Hello</div>';
1063
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
1064
+ const position = 18;
1065
+ const result = completionService.getQuickInfoAtPosition(ts, sourceFile, position);
1066
+ expect(result).toBeDefined();
1067
+ expect(result.documentation).toBeDefined();
1068
+ expect(result.documentation.length).toBeGreaterThan(0);
1069
+ const docText = result.documentation[0].text;
1070
+ expect(docText).toContain('```css');
1071
+ expect(docText).toContain('.flex');
1072
+ expect(docText).toContain('display: flex');
1073
+ });
1074
+ it('should return undefined for non-Tailwind class', () => {
1075
+ const sourceCode = '<div className="my-custom-class">Hello</div>';
1076
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
1077
+ const position = 20;
1078
+ const result = completionService.getQuickInfoAtPosition(ts, sourceFile, position);
1079
+ expect(result).toBeUndefined();
1080
+ });
1081
+ it('should return undefined when not in className context', () => {
1082
+ const sourceCode = 'const x = "flex";';
1083
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
1084
+ const position = 13;
1085
+ const result = completionService.getQuickInfoAtPosition(ts, sourceFile, position);
1086
+ expect(result).toBeUndefined();
1087
+ });
1088
+ it('should set color kindModifier for color classes', () => {
1089
+ jest
1090
+ .spyOn(validator, 'getCssForClasses')
1091
+ .mockReturnValue(['.bg-red-500 { background-color: rgb(239 68 68); }']);
1092
+ const sourceCode = '<div className="bg-red-500">Hello</div>';
1093
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
1094
+ const position = 20;
1095
+ const result = completionService.getQuickInfoAtPosition(ts, sourceFile, position);
1096
+ expect(result).toBeDefined();
1097
+ expect(result.kindModifiers).toBe('color');
1098
+ });
1099
+ it('should return hover info for class in utility function', () => {
1100
+ const sourceCode = 'const x = cn("flex items-center");';
1101
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
1102
+ // Position on "flex"
1103
+ const position = 16;
1104
+ const result = completionService.getQuickInfoAtPosition(ts, sourceFile, position);
1105
+ expect(result).toBeDefined();
1106
+ expect(result.displayParts[0].text).toContain('display: flex');
1107
+ });
1108
+ it('should return hover info for second class in list', () => {
1109
+ jest.spyOn(validator, 'getCssForClasses').mockImplementation((classNames) => {
1110
+ return classNames.map(name => {
1111
+ if (name === 'flex')
1112
+ return '.flex { display: flex; }';
1113
+ if (name === 'items-center')
1114
+ return '.items-center { align-items: center; }';
1115
+ return null;
1116
+ });
1117
+ });
1118
+ const sourceCode = '<div className="flex items-center">Hello</div>';
1119
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
1120
+ // Position on "items-center"
1121
+ const position = 25;
1122
+ const result = completionService.getQuickInfoAtPosition(ts, sourceFile, position);
1123
+ expect(result).toBeDefined();
1124
+ expect(result.displayParts[0].text).toContain('align-items: center');
1125
+ expect(result.textSpan).toEqual({
1126
+ start: 21, // Start of "items-center"
1127
+ length: 12 // Length of "items-center"
1128
+ });
1129
+ });
1130
+ it('should return undefined for position on whitespace', () => {
1131
+ const sourceCode = '<div className="flex items-center">Hello</div>';
1132
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
1133
+ // Position on the space between classes
1134
+ const position = 21;
1135
+ const result = completionService.getQuickInfoAtPosition(ts, sourceFile, position);
1136
+ // Should return undefined because we're on whitespace, not a class
1137
+ expect(result).toBeUndefined();
1138
+ });
1139
+ it('should return hover info for class at start of string', () => {
1140
+ const sourceCode = '<div className="flex">Hello</div>';
1141
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
1142
+ // Position at the very start of "flex"
1143
+ const position = 16;
1144
+ const result = completionService.getQuickInfoAtPosition(ts, sourceFile, position);
1145
+ expect(result).toBeDefined();
1146
+ expect(result.displayParts[0].text).toContain('display: flex');
1147
+ });
1148
+ it('should return hover info for class at end of word', () => {
1149
+ const sourceCode = '<div className="flex">Hello</div>';
1150
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
1151
+ // Position at the end of "flex" (just before closing quote)
1152
+ const position = 20;
1153
+ const result = completionService.getQuickInfoAtPosition(ts, sourceFile, position);
1154
+ expect(result).toBeDefined();
1155
+ expect(result.displayParts[0].text).toContain('display: flex');
1156
+ });
1157
+ it('should return undefined when position is outside string', () => {
1158
+ const sourceCode = '<div className="flex">Hello</div>';
1159
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
1160
+ // Position on "className" attribute name
1161
+ const position = 10;
1162
+ const result = completionService.getQuickInfoAtPosition(ts, sourceFile, position);
1163
+ expect(result).toBeUndefined();
1164
+ });
1165
+ it('should return hover info in tv() variants', () => {
1166
+ const sourceCode = `const button = tv({
1167
+ base: "flex",
1168
+ variants: {
1169
+ color: {
1170
+ primary: "bg-blue-500"
1171
+ }
1172
+ }
1173
+ });`;
1174
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
1175
+ // Position on "flex" in base
1176
+ const position = sourceCode.indexOf('"flex"') + 2;
1177
+ const result = completionService.getQuickInfoAtPosition(ts, sourceFile, position);
1178
+ expect(result).toBeDefined();
1179
+ expect(result.displayParts[0].text).toContain('display: flex');
1180
+ });
1181
+ });
333
1182
  });
334
1183
  //# sourceMappingURL=CompletionService.spec.js.map