esupgrade 2025.0.2 → 2025.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -231,6 +231,143 @@ for (const key of Object.keys(obj)) {
231
231
  assert.match(result.code, /for \(const \[a, b\] of items\)/)
232
232
  })
233
233
  })
234
+
235
+ describe("Array.from() to spread", () => {
236
+ test("Array.from().map() to [...].map()", () => {
237
+ const input = `const doubled = Array.from(numbers).map(n => n * 2);`
238
+
239
+ const result = transform(input)
240
+
241
+ assert.strictEqual(result.modified, true)
242
+ assert.match(result.code, /\[\.\.\.numbers\]\.map/)
243
+ assert.doesNotMatch(result.code, /Array\.from/)
244
+ })
245
+
246
+ test("Array.from().filter() to [...].filter()", () => {
247
+ const input = `const filtered = Array.from(items).filter(x => x > 5);`
248
+
249
+ const result = transform(input)
250
+
251
+ assert.strictEqual(result.modified, true)
252
+ assert.match(result.code, /\[\.\.\.items\]\.filter/)
253
+ })
254
+
255
+ test("Array.from().some() to [...].some()", () => {
256
+ const input = `const hasValue = Array.from(collection).some(item => item.active);`
257
+
258
+ const result = transform(input)
259
+
260
+ assert.strictEqual(result.modified, true)
261
+ assert.match(result.code, /\[\.\.\.collection\]\.some/)
262
+ })
263
+
264
+ test("Array.from().every() to [...].every()", () => {
265
+ const input = `const allValid = Array.from(items).every(x => x.valid);`
266
+
267
+ const result = transform(input)
268
+
269
+ assert.strictEqual(result.modified, true)
270
+ assert.match(result.code, /\[\.\.\.items\]\.every/)
271
+ })
272
+
273
+ test("Array.from().find() to [...].find()", () => {
274
+ const input = `const found = Array.from(elements).find(el => el.id === 'target');`
275
+
276
+ const result = transform(input)
277
+
278
+ assert.strictEqual(result.modified, true)
279
+ assert.match(result.code, /\[\.\.\.elements\]\.find/)
280
+ })
281
+
282
+ test("Array.from().reduce() to [...].reduce()", () => {
283
+ const input = `const sum = Array.from(values).reduce((a, b) => a + b, 0);`
284
+
285
+ const result = transform(input)
286
+
287
+ assert.strictEqual(result.modified, true)
288
+ assert.match(result.code, /\[\.\.\.values\]\.reduce/)
289
+ })
290
+
291
+ test("Array.from() standalone to [...]", () => {
292
+ const input = `const arr = Array.from(iterable);`
293
+
294
+ const result = transform(input)
295
+
296
+ assert.strictEqual(result.modified, true)
297
+ assert.match(result.code, /const arr = \[\.\.\.iterable\]/)
298
+ })
299
+
300
+ test("Array.from() with property access", () => {
301
+ const input = `const length = Array.from(items).length;`
302
+
303
+ const result = transform(input)
304
+
305
+ assert.strictEqual(result.modified, true)
306
+ assert.match(result.code, /\[\.\.\.items\]\.length/)
307
+ })
308
+
309
+ test("Array.from().forEach() should NOT be transformed (handled by other transformer)", () => {
310
+ const input = `Array.from(items).forEach(item => console.log(item));`
311
+
312
+ const result = transform(input)
313
+
314
+ // Should be transformed by arrayFromForEachToForOf, not arrayFromToSpread
315
+ assert.strictEqual(result.modified, true)
316
+ assert.match(result.code, /for \(const item of items\)/)
317
+ assert.doesNotMatch(result.code, /\[\.\.\./)
318
+ })
319
+
320
+ test("Array.from() with mapping function should NOT be transformed", () => {
321
+ const input = `const doubled = Array.from(numbers, n => n * 2);`
322
+
323
+ const result = transform(input)
324
+
325
+ // Should not transform because there's a mapping function
326
+ assert.strictEqual(result.modified, false)
327
+ assert.match(result.code, /Array\.from\(numbers, n => n \* 2\)/)
328
+ })
329
+
330
+ test("Array.from() with thisArg should NOT be transformed", () => {
331
+ const input = `const result = Array.from(items, function(x) { return x * this.multiplier; }, context);`
332
+
333
+ const result = transform(input)
334
+
335
+ // Should not transform because there are 3 arguments
336
+ assert.strictEqual(result.modified, false)
337
+ assert.match(result.code, /Array\.from/)
338
+ })
339
+
340
+ test("Array.from() chained methods", () => {
341
+ const input = `const result = Array.from(set).map(x => x * 2).filter(x => x > 10);`
342
+
343
+ const result = transform(input)
344
+
345
+ assert.strictEqual(result.modified, true)
346
+ assert.match(result.code, /\[\.\.\.set\]\.map/)
347
+ })
348
+
349
+ test("Array.from() with complex iterable", () => {
350
+ const input = `const arr = Array.from(document.querySelectorAll('.item'));`
351
+
352
+ const result = transform(input)
353
+
354
+ assert.strictEqual(result.modified, true)
355
+ assert.match(result.code, /\[\.\.\.document\.querySelectorAll\('\.item'\)\]/)
356
+ })
357
+
358
+ test("Array.from() tracks line numbers", () => {
359
+ const input = `// Line 1
360
+ const result = Array.from(items).map(x => x * 2);`
361
+
362
+ const result = transform(input)
363
+
364
+ assert.strictEqual(result.modified, true)
365
+ assert.strictEqual(result.changes.length, 1)
366
+ assert.strictEqual(result.changes[0].type, "arrayFromToSpread")
367
+ assert.strictEqual(result.changes[0].line, 2)
368
+ })
369
+ })
370
+
234
371
  describe("const and let", () => {
235
372
  test("var to const when not reassigned", () => {
236
373
  const input = `
@@ -384,6 +521,77 @@ var x = 1;`
384
521
  })
385
522
  })
386
523
 
524
+ describe("Math.pow() to exponentiation operator", () => {
525
+ test("Math.pow() to **", () => {
526
+ const input = `const result = Math.pow(2, 3);`
527
+
528
+ const result = transform(input)
529
+
530
+ assert.strictEqual(result.modified, true)
531
+ assert.match(result.code, /2 \*\* 3/)
532
+ })
533
+
534
+ test("Math.pow() with variables", () => {
535
+ const input = `const power = Math.pow(base, exponent);`
536
+
537
+ const result = transform(input)
538
+
539
+ assert.strictEqual(result.modified, true)
540
+ assert.match(result.code, /base \*\* exponent/)
541
+ })
542
+
543
+ test("Math.pow() with complex expressions", () => {
544
+ const input = `const result = Math.pow(x + 1, y * 2);`
545
+
546
+ const result = transform(input)
547
+
548
+ assert.strictEqual(result.modified, true)
549
+ assert.match(result.code, /\(x \+ 1\) \*\* \(y \* 2\)/)
550
+ })
551
+
552
+ test("Math.pow() in expressions", () => {
553
+ const input = `const area = Math.PI * Math.pow(radius, 2);`
554
+
555
+ const result = transform(input)
556
+
557
+ assert.strictEqual(result.modified, true)
558
+ assert.match(result.code, /Math\.PI \* radius \*\* 2/)
559
+ })
560
+
561
+ test("Math.pow() should not transform with wrong number of arguments", () => {
562
+ const input = `const result = Math.pow(2);`
563
+
564
+ const result = transform(input)
565
+
566
+ assert.strictEqual(result.modified, false)
567
+ assert.match(result.code, /Math\.pow\(2\)/)
568
+ })
569
+
570
+ test("Math.pow() nested calls", () => {
571
+ const input = `const result = Math.pow(Math.pow(2, 3), 4);`
572
+
573
+ const result = transform(input)
574
+
575
+ assert.strictEqual(result.modified, true)
576
+ // Only the outer Math.pow is transformed in a single pass
577
+ assert.match(result.code, /Math\.pow\(2, 3\) \*\* 4/)
578
+ })
579
+
580
+ test("Math.pow() tracks line numbers", () => {
581
+ const input = `// Line 1
582
+ const result = Math.pow(2, 3);`
583
+
584
+ const result = transform(input)
585
+
586
+ assert.strictEqual(result.modified, true)
587
+ const mathPowChanges = result.changes.filter(
588
+ (c) => c.type === "mathPowToExponentiation",
589
+ )
590
+ assert.strictEqual(mathPowChanges.length, 1)
591
+ assert.strictEqual(mathPowChanges[0].line, 2)
592
+ })
593
+ })
594
+
387
595
  test("no changes needed", () => {
388
596
  const input = `
389
597
  const x = 1;
@@ -425,4 +633,964 @@ var x = 1;`
425
633
  assert.strictEqual(result.modified, true)
426
634
  assert.match(result.code, /const x = 1/)
427
635
  })
636
+
637
+ describe("traditional for loop to for...of", () => {
638
+ test("basic for loop with array indexing", () => {
639
+ const input = `
640
+ for (let i = 0; i < items.length; i++) {
641
+ const item = items[i];
642
+ console.log(item);
643
+ }
644
+ `
645
+
646
+ const result = transform(input)
647
+
648
+ assert.strictEqual(result.modified, true)
649
+ assert.match(result.code, /for \(const item of items\)/)
650
+ assert.match(result.code, /console\.log\(item\)/)
651
+ assert.doesNotMatch(result.code, /items\[i\]/)
652
+ })
653
+
654
+ test("for loop with const variable", () => {
655
+ const input = `
656
+ for (let i = 0; i < arr.length; i++) {
657
+ const element = arr[i];
658
+ process(element);
659
+ }
660
+ `
661
+
662
+ const result = transform(input)
663
+
664
+ assert.strictEqual(result.modified, true)
665
+ assert.match(result.code, /for \(const element of arr\)/)
666
+ })
667
+
668
+ test("for loop with let variable", () => {
669
+ const input = `
670
+ for (let i = 0; i < arr.length; i++) {
671
+ let element = arr[i];
672
+ element = transform(element);
673
+ console.log(element);
674
+ }
675
+ `
676
+
677
+ const result = transform(input)
678
+
679
+ assert.strictEqual(result.modified, true)
680
+ assert.match(result.code, /for \(let element of arr\)/)
681
+ })
682
+
683
+ test("for loop with var variable", () => {
684
+ const input = `
685
+ for (let i = 0; i < arr.length; i++) {
686
+ var element = arr[i];
687
+ console.log(element);
688
+ }
689
+ `
690
+
691
+ const result = transform(input)
692
+
693
+ assert.strictEqual(result.modified, true)
694
+ // Note: var is also converted to const by the varToConst transformer
695
+ assert.match(result.code, /for \(const element of arr\)/)
696
+ })
697
+
698
+ test("should NOT transform when index is used in body", () => {
699
+ const input = `
700
+ for (let i = 0; i < items.length; i++) {
701
+ const item = items[i];
702
+ console.log(item, i);
703
+ }
704
+ `
705
+
706
+ const result = transform(input)
707
+
708
+ assert.strictEqual(result.modified, false)
709
+ assert.match(result.code, /for \(let i = 0; i < items\.length; i\+\+\)/)
710
+ })
711
+
712
+ test("should NOT transform when no array access statement", () => {
713
+ const input = `
714
+ for (let i = 0; i < items.length; i++) {
715
+ console.log(i);
716
+ }
717
+ `
718
+
719
+ const result = transform(input)
720
+
721
+ assert.strictEqual(result.modified, false)
722
+ assert.match(result.code, /for \(let i = 0/)
723
+ })
724
+
725
+ test("should NOT transform when body is empty", () => {
726
+ const input = `
727
+ for (let i = 0; i < items.length; i++) {
728
+ }
729
+ `
730
+
731
+ const result = transform(input)
732
+
733
+ assert.strictEqual(result.modified, false)
734
+ })
735
+
736
+ test("should NOT transform when using different increment", () => {
737
+ const input = `
738
+ for (let i = 0; i < items.length; i += 2) {
739
+ const item = items[i];
740
+ console.log(item);
741
+ }
742
+ `
743
+
744
+ const result = transform(input)
745
+
746
+ assert.strictEqual(result.modified, false)
747
+ assert.match(result.code, /i \+= 2/)
748
+ })
749
+
750
+ test("should NOT transform when starting from non-zero", () => {
751
+ const input = `
752
+ for (let i = 1; i < items.length; i++) {
753
+ const item = items[i];
754
+ console.log(item);
755
+ }
756
+ `
757
+
758
+ const result = transform(input)
759
+
760
+ assert.strictEqual(result.modified, false)
761
+ assert.match(result.code, /let i = 1/)
762
+ })
763
+
764
+ test("should NOT transform when using <= instead of <", () => {
765
+ const input = `
766
+ for (let i = 0; i <= items.length; i++) {
767
+ const item = items[i];
768
+ console.log(item);
769
+ }
770
+ `
771
+
772
+ const result = transform(input)
773
+
774
+ assert.strictEqual(result.modified, false)
775
+ assert.match(result.code, /i <= items\.length/)
776
+ })
777
+
778
+ test("should NOT transform when accessing different array", () => {
779
+ const input = `
780
+ for (let i = 0; i < items.length; i++) {
781
+ const item = otherArray[i];
782
+ console.log(item);
783
+ }
784
+ `
785
+
786
+ const result = transform(input)
787
+
788
+ assert.strictEqual(result.modified, false)
789
+ assert.match(result.code, /otherArray\[i\]/)
790
+ })
791
+
792
+ test("should NOT transform when first statement is not variable declaration", () => {
793
+ const input = `
794
+ for (let i = 0; i < items.length; i++) {
795
+ console.log(items[i]);
796
+ }
797
+ `
798
+
799
+ const result = transform(input)
800
+
801
+ assert.strictEqual(result.modified, false)
802
+ assert.match(result.code, /for \(let i = 0/)
803
+ })
804
+
805
+ test("should NOT transform when using different index variable", () => {
806
+ const input = `
807
+ for (let i = 0; i < items.length; i++) {
808
+ const item = items[j];
809
+ console.log(item);
810
+ }
811
+ `
812
+
813
+ const result = transform(input)
814
+
815
+ assert.strictEqual(result.modified, false)
816
+ })
817
+
818
+ test("should transform with ++i (prefix increment)", () => {
819
+ const input = `
820
+ for (let i = 0; i < items.length; ++i) {
821
+ const item = items[i];
822
+ console.log(item);
823
+ }
824
+ `
825
+
826
+ const result = transform(input)
827
+
828
+ assert.strictEqual(result.modified, true)
829
+ assert.match(result.code, /for \(const item of items\)/)
830
+ })
831
+
832
+ test("multiple statements after array access", () => {
833
+ const input = `
834
+ for (let i = 0; i < items.length; i++) {
835
+ const item = items[i];
836
+ console.log(item);
837
+ process(item);
838
+ cleanup();
839
+ }
840
+ `
841
+
842
+ const result = transform(input)
843
+
844
+ assert.strictEqual(result.modified, true)
845
+ assert.match(result.code, /for \(const item of items\)/)
846
+ assert.match(result.code, /console\.log\(item\)/)
847
+ assert.match(result.code, /process\(item\)/)
848
+ assert.match(result.code, /cleanup\(\)/)
849
+ })
850
+
851
+ test("should NOT transform when init is not a variable declaration", () => {
852
+ const input = `
853
+ for (i = 0; i < items.length; i++) {
854
+ const item = items[i];
855
+ console.log(item);
856
+ }
857
+ `
858
+
859
+ const result = transform(input)
860
+
861
+ assert.strictEqual(result.modified, false)
862
+ })
863
+
864
+ test("should NOT transform when init has multiple declarations", () => {
865
+ const input = `
866
+ for (let i = 0, j = 0; i < items.length; i++) {
867
+ const item = items[i];
868
+ console.log(item);
869
+ }
870
+ `
871
+
872
+ const result = transform(input)
873
+
874
+ assert.strictEqual(result.modified, false)
875
+ })
876
+
877
+ test("should NOT transform when init id is not an identifier", () => {
878
+ const input = `
879
+ for (let [i] = [0]; i < items.length; i++) {
880
+ const item = items[i];
881
+ console.log(item);
882
+ }
883
+ `
884
+
885
+ const result = transform(input)
886
+
887
+ assert.strictEqual(result.modified, false)
888
+ })
889
+
890
+ test("should NOT transform when test is not a binary expression", () => {
891
+ const input = `
892
+ for (let i = 0; items.length; i++) {
893
+ const item = items[i];
894
+ console.log(item);
895
+ }
896
+ `
897
+
898
+ const result = transform(input)
899
+
900
+ assert.strictEqual(result.modified, false)
901
+ })
902
+
903
+ test("should NOT transform when test operator is not <", () => {
904
+ const input = `
905
+ for (let i = 0; i <= items.length; i++) {
906
+ const item = items[i];
907
+ console.log(item);
908
+ }
909
+ `
910
+
911
+ const result = transform(input)
912
+
913
+ assert.strictEqual(result.modified, false)
914
+ })
915
+
916
+ test("should NOT transform when test left is not the index variable", () => {
917
+ const input = `
918
+ for (let i = 0; j < items.length; i++) {
919
+ const item = items[i];
920
+ console.log(item);
921
+ }
922
+ `
923
+
924
+ const result = transform(input)
925
+
926
+ assert.strictEqual(result.modified, false)
927
+ })
928
+
929
+ test("should NOT transform when test right is not a member expression", () => {
930
+ const input = `
931
+ for (let i = 0; i < 10; i++) {
932
+ const item = items[i];
933
+ console.log(item);
934
+ }
935
+ `
936
+
937
+ const result = transform(input)
938
+
939
+ assert.strictEqual(result.modified, false)
940
+ })
941
+
942
+ test("should NOT transform when test right property is not 'length'", () => {
943
+ const input = `
944
+ for (let i = 0; i < items.size; i++) {
945
+ const item = items[i];
946
+ console.log(item);
947
+ }
948
+ `
949
+
950
+ const result = transform(input)
951
+
952
+ assert.strictEqual(result.modified, false)
953
+ })
954
+
955
+ test("should NOT transform when test right object is not an identifier", () => {
956
+ const input = `
957
+ for (let i = 0; i < getItems().length; i++) {
958
+ const item = getItems()[i];
959
+ console.log(item);
960
+ }
961
+ `
962
+
963
+ const result = transform(input)
964
+
965
+ assert.strictEqual(result.modified, false)
966
+ })
967
+
968
+ test("should NOT transform when update is not an update expression", () => {
969
+ const input = `
970
+ for (let i = 0; i < items.length; i = i + 1) {
971
+ const item = items[i];
972
+ console.log(item);
973
+ }
974
+ `
975
+
976
+ const result = transform(input)
977
+
978
+ assert.strictEqual(result.modified, false)
979
+ })
980
+
981
+ test("should NOT transform when update argument is not the index variable", () => {
982
+ const input = `
983
+ for (let i = 0; i < items.length; j++) {
984
+ const item = items[i];
985
+ console.log(item);
986
+ }
987
+ `
988
+
989
+ const result = transform(input)
990
+
991
+ assert.strictEqual(result.modified, false)
992
+ })
993
+
994
+ test("should NOT transform when update operator is not ++", () => {
995
+ const input = `
996
+ for (let i = 0; i < items.length; i--) {
997
+ const item = items[i];
998
+ console.log(item);
999
+ }
1000
+ `
1001
+
1002
+ const result = transform(input)
1003
+
1004
+ assert.strictEqual(result.modified, false)
1005
+ })
1006
+
1007
+ test("should NOT transform when body is not a block statement", () => {
1008
+ const input = `
1009
+ for (let i = 0; i < items.length; i++)
1010
+ console.log(items[i]);
1011
+ `
1012
+
1013
+ const result = transform(input)
1014
+
1015
+ assert.strictEqual(result.modified, false)
1016
+ })
1017
+
1018
+ test("should NOT transform when first statement has multiple declarations", () => {
1019
+ const input = `
1020
+ for (let i = 0; i < items.length; i++) {
1021
+ const item = items[i], other = null;
1022
+ console.log(item);
1023
+ }
1024
+ `
1025
+
1026
+ const result = transform(input)
1027
+
1028
+ assert.strictEqual(result.modified, false)
1029
+ })
1030
+
1031
+ test("should NOT transform when first statement id is not an identifier", () => {
1032
+ const input = `
1033
+ for (let i = 0; i < items.length; i++) {
1034
+ const [item] = items[i];
1035
+ console.log(item);
1036
+ }
1037
+ `
1038
+
1039
+ const result = transform(input)
1040
+
1041
+ assert.strictEqual(result.modified, false)
1042
+ })
1043
+
1044
+ test("should NOT transform when first statement init is not a member expression", () => {
1045
+ const input = `
1046
+ for (let i = 0; i < items.length; i++) {
1047
+ const item = getItem(i);
1048
+ console.log(item);
1049
+ }
1050
+ `
1051
+
1052
+ const result = transform(input)
1053
+
1054
+ assert.strictEqual(result.modified, false)
1055
+ })
1056
+
1057
+ test("should NOT transform when member expression object name doesn't match", () => {
1058
+ const input = `
1059
+ for (let i = 0; i < items.length; i++) {
1060
+ const item = other[i];
1061
+ console.log(item);
1062
+ }
1063
+ `
1064
+
1065
+ const result = transform(input)
1066
+
1067
+ assert.strictEqual(result.modified, false)
1068
+ })
1069
+
1070
+ test("should NOT transform when member expression property doesn't match index", () => {
1071
+ const input = `
1072
+ for (let i = 0; i < items.length; i++) {
1073
+ const item = items[j];
1074
+ console.log(item);
1075
+ }
1076
+ `
1077
+
1078
+ const result = transform(input)
1079
+
1080
+ assert.strictEqual(result.modified, false)
1081
+ })
1082
+
1083
+ test("should NOT transform when member expression is not computed", () => {
1084
+ const input = `
1085
+ for (let i = 0; i < items.length; i++) {
1086
+ const item = items.i;
1087
+ console.log(item);
1088
+ }
1089
+ `
1090
+
1091
+ const result = transform(input)
1092
+
1093
+ assert.strictEqual(result.modified, false)
1094
+ })
1095
+
1096
+ test("tracks line numbers for forLoopToForOf", () => {
1097
+ const input = `// Line 1
1098
+ for (let i = 0; i < items.length; i++) {
1099
+ const item = items[i];
1100
+ console.log(item);
1101
+ }`
1102
+
1103
+ const result = transform(input)
1104
+
1105
+ assert.strictEqual(result.modified, true)
1106
+ const forLoopChanges = result.changes.filter((c) => c.type === "forLoopToForOf")
1107
+ assert.strictEqual(forLoopChanges.length, 1)
1108
+ assert.strictEqual(forLoopChanges[0].line, 2)
1109
+ })
1110
+ })
1111
+
1112
+ describe("iterable forEach to for...of", () => {
1113
+ test("document.querySelectorAll().forEach() to for...of", () => {
1114
+ const input = `
1115
+ document.querySelectorAll('.item').forEach(item => {
1116
+ console.log(item);
1117
+ });
1118
+ `
1119
+
1120
+ const result = transform(input)
1121
+
1122
+ assert.strictEqual(result.modified, true)
1123
+ assert.match(
1124
+ result.code,
1125
+ /for \(const item of document\.querySelectorAll\(['"]\.item['"]\)\)/,
1126
+ )
1127
+ assert.match(result.code, /console\.log\(item\)/)
1128
+ })
1129
+
1130
+ test("document.getElementsByTagName().forEach() to for...of", () => {
1131
+ const input = `
1132
+ document.getElementsByTagName('div').forEach(div => {
1133
+ div.classList.add('active');
1134
+ });
1135
+ `
1136
+
1137
+ const result = transform(input)
1138
+
1139
+ assert.strictEqual(result.modified, true)
1140
+ assert.match(
1141
+ result.code,
1142
+ /for \(const div of document\.getElementsByTagName\(['"]div['"]\)\)/,
1143
+ )
1144
+ })
1145
+
1146
+ test("document.getElementsByClassName().forEach() to for...of", () => {
1147
+ const input = `
1148
+ document.getElementsByClassName('button').forEach(button => {
1149
+ button.disabled = true;
1150
+ });
1151
+ `
1152
+
1153
+ const result = transform(input)
1154
+
1155
+ assert.strictEqual(result.modified, true)
1156
+ assert.match(
1157
+ result.code,
1158
+ /for \(const button of document\.getElementsByClassName\(['"]button['"]\)\)/,
1159
+ )
1160
+ })
1161
+
1162
+ test("document.getElementsByName().forEach() to for...of", () => {
1163
+ const input = `
1164
+ document.getElementsByName('email').forEach(input => {
1165
+ input.required = true;
1166
+ });
1167
+ `
1168
+
1169
+ const result = transform(input)
1170
+
1171
+ assert.strictEqual(result.modified, true)
1172
+ assert.match(
1173
+ result.code,
1174
+ /for \(const input of document\.getElementsByName\(['"]email['"]\)\)/,
1175
+ )
1176
+ })
1177
+
1178
+ test("element variable querySelectorAll should NOT transform", () => {
1179
+ const input = `
1180
+ element.querySelectorAll('span').forEach(span => {
1181
+ span.remove();
1182
+ });
1183
+ `
1184
+
1185
+ const result = transform(input)
1186
+
1187
+ // Should NOT transform because element is not from document chain
1188
+ assert.strictEqual(result.modified, false)
1189
+ assert.match(result.code, /element\.querySelectorAll/)
1190
+ })
1191
+
1192
+ test("chained element method .querySelectorAll().forEach() to for...of", () => {
1193
+ const input = `
1194
+ document.getElementById('container').querySelectorAll('p').forEach(p => {
1195
+ p.textContent = '';
1196
+ });
1197
+ `
1198
+
1199
+ const result = transform(input)
1200
+
1201
+ assert.strictEqual(result.modified, true)
1202
+ assert.match(
1203
+ result.code,
1204
+ /for \(const p of document\.getElementById\(['"]container['"]\)\.querySelectorAll\(['"]p['"]\)\)/,
1205
+ )
1206
+ })
1207
+
1208
+ test("window.frames property forEach() to for...of", () => {
1209
+ const input = `
1210
+ window.frames.forEach(frame => {
1211
+ frame.postMessage('hello', '*');
1212
+ });
1213
+ `
1214
+
1215
+ const result = transform(input)
1216
+
1217
+ assert.strictEqual(result.modified, true)
1218
+ assert.match(result.code, /for \(const frame of window\.frames\)/)
1219
+ })
1220
+
1221
+ test("should NOT transform arrow function without braces (expression body)", () => {
1222
+ const input = `
1223
+ document.querySelectorAll('.item').forEach(item => item.remove());
1224
+ `
1225
+
1226
+ const result = transform(input)
1227
+
1228
+ assert.strictEqual(result.modified, false)
1229
+ assert.match(result.code, /forEach\(item => item\.remove\(\)\)/)
1230
+ })
1231
+
1232
+ test("should NOT transform with index parameter", () => {
1233
+ const input = `
1234
+ document.querySelectorAll('.item').forEach((item, index) => {
1235
+ console.log(item, index);
1236
+ });
1237
+ `
1238
+
1239
+ const result = transform(input)
1240
+
1241
+ assert.strictEqual(result.modified, false)
1242
+ assert.match(result.code, /forEach\(\(item, index\) =>/)
1243
+ })
1244
+
1245
+ test("should NOT transform with array parameter", () => {
1246
+ const input = `
1247
+ document.querySelectorAll('.item').forEach((item, index, array) => {
1248
+ console.log(item, index, array);
1249
+ });
1250
+ `
1251
+
1252
+ const result = transform(input)
1253
+
1254
+ assert.strictEqual(result.modified, false)
1255
+ assert.match(result.code, /forEach\(\(item, index, array\) =>/)
1256
+ })
1257
+
1258
+ test("should NOT transform with non-inline callback (reference)", () => {
1259
+ const input = `
1260
+ document.querySelectorAll('.item').forEach(handleItem);
1261
+ `
1262
+
1263
+ const result = transform(input)
1264
+
1265
+ assert.strictEqual(result.modified, false)
1266
+ assert.match(result.code, /forEach\(handleItem\)/)
1267
+ })
1268
+
1269
+ test("should NOT transform without callback", () => {
1270
+ const input = `
1271
+ document.querySelectorAll('.item').forEach();
1272
+ `
1273
+
1274
+ const result = transform(input)
1275
+
1276
+ assert.strictEqual(result.modified, false)
1277
+ assert.match(result.code, /forEach\(\)/)
1278
+ })
1279
+
1280
+ test("should NOT transform unknown methods", () => {
1281
+ const input = `
1282
+ document.querySomething('.item').forEach(item => {
1283
+ console.log(item);
1284
+ });
1285
+ `
1286
+
1287
+ const result = transform(input)
1288
+
1289
+ assert.strictEqual(result.modified, false)
1290
+ assert.match(result.code, /querySomething/)
1291
+ })
1292
+
1293
+ test("should NOT transform non-document objects with querySelectorAll", () => {
1294
+ const input = `
1295
+ myObject.querySelectorAll('.item').forEach(item => {
1296
+ console.log(item);
1297
+ });
1298
+ `
1299
+
1300
+ const result = transform(input)
1301
+
1302
+ assert.strictEqual(result.modified, false)
1303
+ assert.match(result.code, /myObject\.querySelectorAll/)
1304
+ })
1305
+
1306
+ test("should NOT transform window methods not in allowed list", () => {
1307
+ const input = `
1308
+ window.querySelectorAll('.item').forEach(item => {
1309
+ console.log(item);
1310
+ });
1311
+ `
1312
+
1313
+ const result = transform(input)
1314
+
1315
+ assert.strictEqual(result.modified, false)
1316
+ assert.match(result.code, /window\.querySelectorAll/)
1317
+ })
1318
+
1319
+ test("transforms with function expression", () => {
1320
+ const input = `
1321
+ document.querySelectorAll('button').forEach(function(btn) {
1322
+ btn.disabled = true;
1323
+ });
1324
+ `
1325
+
1326
+ const result = transform(input)
1327
+
1328
+ assert.strictEqual(result.modified, true)
1329
+ assert.match(
1330
+ result.code,
1331
+ /for \(const btn of document\.querySelectorAll\(['"]button['"]\)\)/,
1332
+ )
1333
+ })
1334
+
1335
+ test("tracks line numbers correctly", () => {
1336
+ const input = `// Line 1
1337
+ document.querySelectorAll('.item').forEach(item => {
1338
+ console.log(item);
1339
+ });`
1340
+
1341
+ const result = transform(input)
1342
+
1343
+ assert.strictEqual(result.modified, true)
1344
+ assert.strictEqual(result.changes.length, 1)
1345
+ assert.strictEqual(result.changes[0].type, "iterableForEachToForOf")
1346
+ assert.strictEqual(result.changes[0].line, 2)
1347
+ })
1348
+
1349
+ test("handles complex selector strings", () => {
1350
+ const input = `
1351
+ document.querySelectorAll('[data-toggle="modal"]').forEach(el => {
1352
+ el.addEventListener('click', handleClick);
1353
+ });
1354
+ `
1355
+
1356
+ const result = transform(input)
1357
+
1358
+ assert.strictEqual(result.modified, true)
1359
+ assert.match(
1360
+ result.code,
1361
+ /for \(const el of document\.querySelectorAll\(['"]\[data-toggle="modal"\]['"]\)\)/,
1362
+ )
1363
+ })
1364
+
1365
+ test("preserves multiline function bodies", () => {
1366
+ const input = `
1367
+ document.querySelectorAll('.item').forEach(item => {
1368
+ const value = item.value;
1369
+ console.log(value);
1370
+ item.classList.add('processed');
1371
+ });
1372
+ `
1373
+
1374
+ const result = transform(input)
1375
+
1376
+ assert.strictEqual(result.modified, true)
1377
+ assert.match(result.code, /for \(const item of/)
1378
+ assert.match(result.code, /const value = item\.value/)
1379
+ assert.match(result.code, /console\.log\(value\)/)
1380
+ assert.match(result.code, /item\.classList\.add/)
1381
+ })
1382
+
1383
+ test("should NOT transform element variables with getElementsByTagName", () => {
1384
+ const input = `
1385
+ container.getElementsByTagName('input').forEach(input => {
1386
+ input.value = '';
1387
+ });
1388
+ `
1389
+
1390
+ const result = transform(input)
1391
+
1392
+ // Should not transform because container is not from document chain
1393
+ assert.strictEqual(result.modified, false)
1394
+ assert.match(result.code, /container\.getElementsByTagName/)
1395
+ })
1396
+
1397
+ test("should NOT transform element variables with getElementsByClassName", () => {
1398
+ const input = `
1399
+ section.getElementsByClassName('warning').forEach(warning => {
1400
+ warning.style.display = 'none';
1401
+ });
1402
+ `
1403
+
1404
+ const result = transform(input)
1405
+
1406
+ // Should not transform because section is not from document chain
1407
+ assert.strictEqual(result.modified, false)
1408
+ assert.match(result.code, /section\.getElementsByClassName/)
1409
+ })
1410
+
1411
+ test("should NOT transform window.querySelectorAll (not in allowed methods)", () => {
1412
+ const input = `
1413
+ window.querySelectorAll('.item').forEach(item => {
1414
+ console.log(item);
1415
+ });
1416
+ `
1417
+
1418
+ const result = transform(input)
1419
+
1420
+ // Should not transform because querySelectorAll is not a window method
1421
+ assert.strictEqual(result.modified, false)
1422
+ assert.match(result.code, /window\.querySelectorAll/)
1423
+ })
1424
+
1425
+ test("should NOT transform property access on unknown objects", () => {
1426
+ const input = `
1427
+ customObject.frames.forEach(frame => {
1428
+ frame.postMessage('test', '*');
1429
+ });
1430
+ `
1431
+
1432
+ const result = transform(input)
1433
+
1434
+ // Should not transform because customObject is not window
1435
+ assert.strictEqual(result.modified, false)
1436
+ assert.match(result.code, /customObject\.frames/)
1437
+ })
1438
+
1439
+ test("should NOT transform when method callee is not a member expression", () => {
1440
+ const input = `
1441
+ getSomething().forEach(item => {
1442
+ console.log(item);
1443
+ });
1444
+ `
1445
+
1446
+ const result = transform(input)
1447
+
1448
+ // Should not transform because callee is not a member expression
1449
+ assert.strictEqual(result.modified, false)
1450
+ assert.match(result.code, /getSomething\(\)\.forEach/)
1451
+ })
1452
+
1453
+ test("should NOT transform when method name cannot be extracted (computed property)", () => {
1454
+ const input = `
1455
+ document['querySelectorAll']('.item').forEach(item => {
1456
+ console.log(item);
1457
+ });
1458
+ `
1459
+
1460
+ const result = transform(input)
1461
+
1462
+ // Should not transform because method name is computed (string literal, not identifier)
1463
+ assert.strictEqual(result.modified, false)
1464
+ assert.match(result.code, /document\['querySelectorAll'\]/)
1465
+ })
1466
+
1467
+ test("should NOT transform unknown document methods", () => {
1468
+ const input = `
1469
+ document.customMethod().forEach(item => {
1470
+ console.log(item);
1471
+ });
1472
+ `
1473
+
1474
+ const result = transform(input)
1475
+
1476
+ // Should not transform because customMethod is not in allowed list
1477
+ assert.strictEqual(result.modified, false)
1478
+ assert.match(result.code, /document\.customMethod/)
1479
+ })
1480
+
1481
+ test("should NOT transform chained call from non-document origin", () => {
1482
+ const input = `
1483
+ element.querySelector('div').querySelectorAll('span').forEach(span => {
1484
+ span.remove();
1485
+ });
1486
+ `
1487
+
1488
+ const result = transform(input)
1489
+
1490
+ // Should not transform because chain doesn't start with document
1491
+ assert.strictEqual(result.modified, false)
1492
+ assert.match(result.code, /element\.querySelector/)
1493
+ })
1494
+
1495
+ test("should NOT transform chained call with unknown method", () => {
1496
+ const input = `
1497
+ document.getElementById('x').customMethod().forEach(item => {
1498
+ console.log(item);
1499
+ });
1500
+ `
1501
+
1502
+ const result = transform(input)
1503
+
1504
+ // Should not transform because customMethod is not in document methods
1505
+ assert.strictEqual(result.modified, false)
1506
+ assert.match(result.code, /customMethod/)
1507
+ })
1508
+
1509
+ test("should NOT transform when caller object is neither identifier nor member/call expression", () => {
1510
+ const input = `
1511
+ (function() { return document; })().querySelectorAll('.item').forEach(item => {
1512
+ console.log(item);
1513
+ });
1514
+ `
1515
+
1516
+ const result = transform(input)
1517
+
1518
+ // Should not transform because caller is a function expression
1519
+ assert.strictEqual(result.modified, false)
1520
+ assert.match(result.code, /forEach/)
1521
+ })
1522
+
1523
+ test("should NOT transform when caller object is ThisExpression", () => {
1524
+ const input = `
1525
+ this.querySelectorAll('.item').forEach(item => {
1526
+ console.log(item);
1527
+ });
1528
+ `
1529
+
1530
+ const result = transform(input)
1531
+
1532
+ // Should not transform because caller is this, not document
1533
+ assert.strictEqual(result.modified, false)
1534
+ assert.match(result.code, /this\.querySelectorAll/)
1535
+ })
1536
+
1537
+ test("should NOT transform when forEach object is neither MemberExpression nor CallExpression", () => {
1538
+ const input = `
1539
+ items.forEach(item => {
1540
+ console.log(item);
1541
+ });
1542
+ `
1543
+
1544
+ const result = transform(input)
1545
+
1546
+ // Should not transform because items is just an identifier, not a method call or property
1547
+ assert.strictEqual(result.modified, false)
1548
+ assert.match(result.code, /items\.forEach/)
1549
+ })
1550
+
1551
+ test("deeply nested document chain should transform", () => {
1552
+ const input = `
1553
+ document.getElementById('a').querySelector('b').querySelectorAll('c').forEach(item => {
1554
+ item.remove();
1555
+ });
1556
+ `
1557
+
1558
+ const result = transform(input)
1559
+
1560
+ // Should transform because it chains from document
1561
+ assert.strictEqual(result.modified, true)
1562
+ assert.match(
1563
+ result.code,
1564
+ /for \(const item of document\.getElementById\(['"]a['"]\)\.querySelector\(['"]b['"]\)\.querySelectorAll\(['"]c['"]\)\)/,
1565
+ )
1566
+ })
1567
+
1568
+ test("should NOT transform when callee in chain is not a member expression", () => {
1569
+ const input = `
1570
+ getDocument().querySelectorAll('span').forEach(item => {
1571
+ item.textContent = 'test';
1572
+ });
1573
+ `
1574
+
1575
+ const result = transform(input)
1576
+
1577
+ // Should NOT transform - chain starts with function call, not document
1578
+ assert.strictEqual(result.modified, false)
1579
+ assert.match(result.code, /getDocument\(\)\.querySelectorAll/)
1580
+ })
1581
+
1582
+ test("should transform document property access with querySelectorAll", () => {
1583
+ const input = `
1584
+ document.body.querySelectorAll('div').forEach(div => {
1585
+ div.remove();
1586
+ });
1587
+ `
1588
+
1589
+ const result = transform(input)
1590
+
1591
+ // Should transform - chains from document through property access
1592
+ assert.strictEqual(result.modified, true)
1593
+ assert.match(result.code, /for \(const div of document\.body\.querySelectorAll/)
1594
+ })
1595
+ })
428
1596
  })