zero-query 0.8.9 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -3
- package/cli/commands/bundle.js +15 -2
- package/cli/commands/dev/devtools/js/core.js +16 -2
- package/cli/commands/dev/devtools/js/elements.js +4 -1
- package/cli/commands/dev/overlay.js +20 -0
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +184 -44
- package/dist/zquery.min.js +2 -2
- package/index.d.ts +6 -2
- package/package.json +1 -1
- package/src/component.js +28 -7
- package/src/core.js +62 -12
- package/src/diff.js +11 -5
- package/src/expression.js +1 -0
- package/src/http.js +17 -1
- package/src/reactive.js +8 -2
- package/src/router.js +37 -8
- package/src/ssr.js +1 -1
- package/src/store.js +5 -0
- package/src/utils.js +12 -6
- package/tests/cli.test.js +456 -0
- package/tests/component.test.js +1387 -0
- package/tests/core.test.js +893 -1
- package/tests/diff.test.js +891 -0
- package/tests/errors.test.js +179 -0
- package/tests/expression.test.js +569 -0
- package/tests/http.test.js +160 -1
- package/tests/reactive.test.js +320 -0
- package/tests/router.test.js +1187 -0
- package/tests/ssr.test.js +261 -0
- package/tests/store.test.js +210 -0
- package/tests/utils.test.js +186 -0
- package/types/store.d.ts +3 -0
package/tests/expression.test.js
CHANGED
|
@@ -480,3 +480,572 @@ describe('expression parser — edge cases', () => {
|
|
|
480
480
|
expect(eval_('{}')).toEqual({});
|
|
481
481
|
});
|
|
482
482
|
});
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
// ---------------------------------------------------------------------------
|
|
486
|
+
// Optional chaining edge cases
|
|
487
|
+
// ---------------------------------------------------------------------------
|
|
488
|
+
|
|
489
|
+
describe('expression parser — optional chaining edge cases', () => {
|
|
490
|
+
it('returns undefined for null base with ?.', () => {
|
|
491
|
+
expect(eval_('a?.b', { a: null })).toBeUndefined();
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('returns undefined for undefined base with ?.', () => {
|
|
495
|
+
expect(eval_('a?.b', { a: undefined })).toBeUndefined();
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('chains multiple optional access', () => {
|
|
499
|
+
expect(eval_('a?.b?.c', { a: { b: { c: 42 } } })).toBe(42);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it('chains optional where middle is null', () => {
|
|
503
|
+
expect(eval_('a?.b?.c', { a: { b: null } })).toBeUndefined();
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('optional chaining with method call', () => {
|
|
507
|
+
expect(eval_('arr?.length', { arr: [1, 2, 3] })).toBe(3);
|
|
508
|
+
expect(eval_('arr?.length', { arr: null })).toBeUndefined();
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it('optional chaining on computed property', () => {
|
|
512
|
+
expect(eval_('obj?.[key]', { obj: { x: 1 }, key: 'x' })).toBe(1);
|
|
513
|
+
expect(eval_('obj?.[key]', { obj: null, key: 'x' })).toBeUndefined();
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
// ---------------------------------------------------------------------------
|
|
519
|
+
// Complex property access
|
|
520
|
+
// ---------------------------------------------------------------------------
|
|
521
|
+
|
|
522
|
+
describe('expression parser — complex property access', () => {
|
|
523
|
+
it('accesses deeply nested objects', () => {
|
|
524
|
+
const scope = { a: { b: { c: { d: { e: 'deep' } } } } };
|
|
525
|
+
expect(eval_('a.b.c.d.e', scope)).toBe('deep');
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it('accesses array index then property', () => {
|
|
529
|
+
expect(eval_('arr[0].name', { arr: [{ name: 'first' }] })).toBe('first');
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it('accesses property then array index', () => {
|
|
533
|
+
expect(eval_('obj.items[1]', { obj: { items: ['a', 'b', 'c'] } })).toBe('b');
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('accesses computed property with variable', () => {
|
|
537
|
+
expect(eval_('obj[key]', { obj: { x: 10, y: 20 }, key: 'y' })).toBe(20);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it('accesses nested computed property', () => {
|
|
541
|
+
const scope = { data: { users: { alice: { age: 30 } } }, name: 'alice' };
|
|
542
|
+
expect(eval_('data.users[name].age', scope)).toBe(30);
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
// ---------------------------------------------------------------------------
|
|
548
|
+
// Arrow function edge cases
|
|
549
|
+
// ---------------------------------------------------------------------------
|
|
550
|
+
|
|
551
|
+
describe('expression parser — arrow function edge cases', () => {
|
|
552
|
+
it('no-param arrow function', () => {
|
|
553
|
+
const fn = eval_('() => 42');
|
|
554
|
+
expect(fn()).toBe(42);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it('single-param arrow (no parens)', () => {
|
|
558
|
+
const fn = eval_('x => x * 2');
|
|
559
|
+
expect(fn(5)).toBe(10);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('multi-param arrow', () => {
|
|
563
|
+
const fn = eval_('(a, b) => a + b');
|
|
564
|
+
expect(fn(3, 4)).toBe(7);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it('arrow using outer scope', () => {
|
|
568
|
+
const fn = eval_('x => x + y', { y: 10 });
|
|
569
|
+
expect(fn(5)).toBe(15);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it('arrow with ternary body', () => {
|
|
573
|
+
const fn = eval_('x => x > 0 ? "pos" : "neg"');
|
|
574
|
+
expect(fn(1)).toBe('pos');
|
|
575
|
+
expect(fn(-1)).toBe('neg');
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
// ---------------------------------------------------------------------------
|
|
581
|
+
// Template literal edge cases
|
|
582
|
+
// ---------------------------------------------------------------------------
|
|
583
|
+
|
|
584
|
+
describe('expression parser — template literal edge cases', () => {
|
|
585
|
+
it('template with no interpolation', () => {
|
|
586
|
+
expect(eval_('`hello world`')).toBe('hello world');
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it('template with multiple interpolations', () => {
|
|
590
|
+
expect(eval_('`${a} and ${b}`', { a: 'foo', b: 'bar' })).toBe('foo and bar');
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it('template with expression in interpolation', () => {
|
|
594
|
+
expect(eval_('`sum is ${a + b}`', { a: 3, b: 4 })).toBe('sum is 7');
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it('template with nested property access', () => {
|
|
598
|
+
expect(eval_('`Hello ${user.name}`', { user: { name: 'Alice' } })).toBe('Hello Alice');
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it('template with ternary in interpolation', () => {
|
|
602
|
+
expect(eval_('`${x > 0 ? "yes" : "no"}`', { x: 1 })).toBe('yes');
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it('empty template literal', () => {
|
|
606
|
+
expect(eval_('``')).toBe('');
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
// ---------------------------------------------------------------------------
|
|
612
|
+
// Nullish coalescing edge cases
|
|
613
|
+
// ---------------------------------------------------------------------------
|
|
614
|
+
|
|
615
|
+
describe('expression parser — nullish coalescing edge cases', () => {
|
|
616
|
+
it('returns left side for 0', () => {
|
|
617
|
+
expect(eval_('x ?? 10', { x: 0 })).toBe(0);
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
it('returns left side for empty string', () => {
|
|
621
|
+
expect(eval_('x ?? "default"', { x: '' })).toBe('');
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it('returns left side for false', () => {
|
|
625
|
+
expect(eval_('x ?? true', { x: false })).toBe(false);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it('returns right side for null', () => {
|
|
629
|
+
expect(eval_('x ?? 10', { x: null })).toBe(10);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
it('returns right side for undefined', () => {
|
|
633
|
+
expect(eval_('x ?? 10', { x: undefined })).toBe(10);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
it('chains with ||', () => {
|
|
637
|
+
expect(eval_('(a ?? b) || c', { a: null, b: 0, c: 5 })).toBe(5);
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
// ---------------------------------------------------------------------------
|
|
643
|
+
// Typeof edge cases
|
|
644
|
+
// ---------------------------------------------------------------------------
|
|
645
|
+
|
|
646
|
+
describe('expression parser — typeof edge cases', () => {
|
|
647
|
+
it('typeof undefined variable returns "undefined"', () => {
|
|
648
|
+
expect(eval_('typeof x')).toBe('undefined');
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it('typeof number', () => {
|
|
652
|
+
expect(eval_('typeof x', { x: 42 })).toBe('number');
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it('typeof string', () => {
|
|
656
|
+
expect(eval_('typeof x', { x: 'hi' })).toBe('string');
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it('typeof object', () => {
|
|
660
|
+
expect(eval_('typeof x', { x: {} })).toBe('object');
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it('typeof null', () => {
|
|
664
|
+
expect(eval_('typeof x', { x: null })).toBe('object');
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
it('typeof function', () => {
|
|
668
|
+
expect(eval_('typeof x', { x: () => {} })).toBe('function');
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
it('typeof boolean', () => {
|
|
672
|
+
expect(eval_('typeof x', { x: true })).toBe('boolean');
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
// ---------------------------------------------------------------------------
|
|
678
|
+
// Array/Object literal edge cases
|
|
679
|
+
// ---------------------------------------------------------------------------
|
|
680
|
+
|
|
681
|
+
describe('expression parser — array/object literal edge cases', () => {
|
|
682
|
+
it('array with trailing expression', () => {
|
|
683
|
+
expect(eval_('[1, 2, 3].length')).toBe(3);
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it('array with mixed types', () => {
|
|
687
|
+
expect(eval_('[1, "two", true, null]')).toEqual([1, 'two', true, null]);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
it('object with computed values', () => {
|
|
691
|
+
expect(eval_('{ x: a + 1, y: b * 2 }', { a: 2, b: 3 })).toEqual({ x: 3, y: 6 });
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
it('nested array literal', () => {
|
|
695
|
+
expect(eval_('[[1, 2], [3, 4]]')).toEqual([[1, 2], [3, 4]]);
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
it('nested object literal', () => {
|
|
699
|
+
expect(eval_('{ a: { b: 1 } }')).toEqual({ a: { b: 1 } });
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
it('array of objects', () => {
|
|
703
|
+
expect(eval_('[{ x: 1 }, { x: 2 }]')).toEqual([{ x: 1 }, { x: 2 }]);
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
// ---------------------------------------------------------------------------
|
|
709
|
+
// Method call edge cases
|
|
710
|
+
// ---------------------------------------------------------------------------
|
|
711
|
+
|
|
712
|
+
describe('expression parser — method call edge cases', () => {
|
|
713
|
+
it('chained string methods', () => {
|
|
714
|
+
expect(eval_('"Hello World".toLowerCase().split(" ")')).toEqual(['hello', 'world']);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
it('array filter', () => {
|
|
718
|
+
expect(eval_('items.filter(x => x > 2)', { items: [1, 2, 3, 4] })).toEqual([3, 4]);
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
it('array map', () => {
|
|
722
|
+
expect(eval_('items.map(x => x * 2)', { items: [1, 2, 3] })).toEqual([2, 4, 6]);
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
it('array includes', () => {
|
|
726
|
+
expect(eval_('items.includes(2)', { items: [1, 2, 3] })).toBe(true);
|
|
727
|
+
expect(eval_('items.includes(5)', { items: [1, 2, 3] })).toBe(false);
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
it('string includes', () => {
|
|
731
|
+
expect(eval_('"hello world".includes("world")')).toBe(true);
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
it('string startsWith/endsWith', () => {
|
|
735
|
+
expect(eval_('"hello".startsWith("hel")')).toBe(true);
|
|
736
|
+
expect(eval_('"hello".endsWith("lo")')).toBe(true);
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
it('JSON.stringify', () => {
|
|
740
|
+
expect(eval_('JSON.stringify({ a: 1 })')).toBe('{"a":1}');
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
it('JSON.parse', () => {
|
|
744
|
+
expect(eval_('JSON.parse(\'{"a":1}\')')).toEqual({ a: 1 });
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it('Math.max with multiple args', () => {
|
|
748
|
+
expect(eval_('Math.max(1, 5, 3, 2, 4)')).toBe(5);
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
it('Math.min with multiple args', () => {
|
|
752
|
+
expect(eval_('Math.min(1, 5, 3, 2, 4)')).toBe(1);
|
|
753
|
+
});
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
// ---------------------------------------------------------------------------
|
|
758
|
+
// Multi-scope resolution
|
|
759
|
+
// ---------------------------------------------------------------------------
|
|
760
|
+
|
|
761
|
+
describe('expression parser — multi-scope resolution', () => {
|
|
762
|
+
it('resolves from first scope when available', () => {
|
|
763
|
+
expect(eval_('x', { x: 1 }, { x: 2 })).toBe(1);
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
it('falls back to second scope', () => {
|
|
767
|
+
expect(eval_('y', { x: 1 }, { y: 2 })).toBe(2);
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
it('resolves different keys from different scopes', () => {
|
|
771
|
+
expect(eval_('x + y', { x: 10 }, { y: 20 })).toBe(30);
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
// ---------------------------------------------------------------------------
|
|
777
|
+
// Security: blocked access
|
|
778
|
+
// ---------------------------------------------------------------------------
|
|
779
|
+
|
|
780
|
+
describe('expression parser — security', () => {
|
|
781
|
+
it('blocks constructor access', () => {
|
|
782
|
+
expect(() => eval_('"".constructor')).not.toThrow();
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
it('blocks __proto__ access', () => {
|
|
786
|
+
expect(() => eval_('obj.__proto__', { obj: {} })).not.toThrow();
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
it('handles invalid expressions gracefully', () => {
|
|
790
|
+
expect(() => eval_('++++')).not.toThrow();
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
it('global access is sandboxed — no window', () => {
|
|
794
|
+
expect(eval_('typeof window')).toBe('undefined');
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it('global access is sandboxed — no document', () => {
|
|
798
|
+
expect(eval_('typeof document')).toBe('undefined');
|
|
799
|
+
});
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
// ---------------------------------------------------------------------------
|
|
804
|
+
// Comparison edge cases
|
|
805
|
+
// ---------------------------------------------------------------------------
|
|
806
|
+
|
|
807
|
+
describe('expression parser — comparison edge cases', () => {
|
|
808
|
+
it('strict equality with type mismatch', () => {
|
|
809
|
+
expect(eval_('1 === "1"')).toBe(false);
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
it('loose equality with type coercion', () => {
|
|
813
|
+
expect(eval_('1 == "1"')).toBe(true);
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
it('strict inequality', () => {
|
|
817
|
+
expect(eval_('1 !== "1"')).toBe(true);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it('comparison with null', () => {
|
|
821
|
+
expect(eval_('null === null')).toBe(true);
|
|
822
|
+
expect(eval_('null == undefined')).toBe(true);
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
it('greater than / less than edge cases', () => {
|
|
826
|
+
expect(eval_('0 < -1')).toBe(false);
|
|
827
|
+
expect(eval_('-1 < 0')).toBe(true);
|
|
828
|
+
});
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
// ---------------------------------------------------------------------------
|
|
833
|
+
// Grouping / precedence
|
|
834
|
+
// ---------------------------------------------------------------------------
|
|
835
|
+
|
|
836
|
+
describe('expression parser — grouping and precedence', () => {
|
|
837
|
+
it('parentheses override precedence', () => {
|
|
838
|
+
expect(eval_('(2 + 3) * 4')).toBe(20);
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
it('nested parentheses', () => {
|
|
842
|
+
expect(eval_('((1 + 2) * (3 + 4))')).toBe(21);
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
it('logical operators precedence', () => {
|
|
846
|
+
expect(eval_('true || false && false')).toBe(true);
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
it('ternary with comparison', () => {
|
|
850
|
+
expect(eval_('x > 5 ? "big" : "small"', { x: 10 })).toBe('big');
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
it('nested ternary', () => {
|
|
854
|
+
expect(eval_('x > 10 ? "big" : x > 5 ? "med" : "small"', { x: 7 })).toBe('med');
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
// ===========================================================================
|
|
860
|
+
// new keyword — safe constructors
|
|
861
|
+
// ===========================================================================
|
|
862
|
+
|
|
863
|
+
describe('safeEval — new keyword', () => {
|
|
864
|
+
it('creates new Date (no args)', () => {
|
|
865
|
+
const result = eval_('new Date');
|
|
866
|
+
expect(result).toBeInstanceOf(Date);
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
it('creates new Array (no args)', () => {
|
|
870
|
+
const result = eval_('new Array');
|
|
871
|
+
expect(result).toBeInstanceOf(Array);
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
it('Map/Set/RegExp not in globals — new returns undefined', () => {
|
|
875
|
+
// Map, Set, RegExp are listed as safe constructors but not as globals,
|
|
876
|
+
// so the ident resolves to undefined and new fails
|
|
877
|
+
expect(eval_('new Map')).toBeUndefined();
|
|
878
|
+
expect(eval_('new Set')).toBeUndefined();
|
|
879
|
+
expect(eval_('new RegExp')).toBeUndefined();
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
it('new with args — parser treats as new (call(...))', () => {
|
|
883
|
+
// Parser's parsePostfix consumes (args) as a call expression, so
|
|
884
|
+
// new Date(2024,0,1) becomes new(Date(2024,0,1)) — returns undefined
|
|
885
|
+
const result = eval_('new Date(2024, 0, 1)');
|
|
886
|
+
expect(result).toBeUndefined();
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
it('blocks unsafe constructors', () => {
|
|
890
|
+
const result = eval_('new Function');
|
|
891
|
+
expect(result).toBeUndefined();
|
|
892
|
+
});
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
// ===========================================================================
|
|
897
|
+
// void operator
|
|
898
|
+
// ===========================================================================
|
|
899
|
+
|
|
900
|
+
describe('safeEval — void operator', () => {
|
|
901
|
+
it('returns undefined', () => {
|
|
902
|
+
expect(eval_('void 0')).toBeUndefined();
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
it('returns undefined for any expression', () => {
|
|
906
|
+
expect(eval_('void "hello"')).toBeUndefined();
|
|
907
|
+
});
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
// ===========================================================================
|
|
912
|
+
// AST cache
|
|
913
|
+
// ===========================================================================
|
|
914
|
+
|
|
915
|
+
describe('safeEval — AST cache', () => {
|
|
916
|
+
it('returns same result on repeated evaluation (cache hit)', () => {
|
|
917
|
+
const r1 = eval_('1 + 2');
|
|
918
|
+
const r2 = eval_('1 + 2');
|
|
919
|
+
expect(r1).toBe(3);
|
|
920
|
+
expect(r2).toBe(3);
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
it('handles many unique expressions without error (cache eviction)', () => {
|
|
924
|
+
// Generate enough unique expressions to trigger cache eviction (>512)
|
|
925
|
+
for (let i = 0; i < 520; i++) {
|
|
926
|
+
expect(eval_(`${i} + 1`)).toBe(i + 1);
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
// ===========================================================================
|
|
933
|
+
// Optional call ?.()
|
|
934
|
+
// ===========================================================================
|
|
935
|
+
|
|
936
|
+
describe('safeEval — optional call ?.()', () => {
|
|
937
|
+
it('calls function when not null', () => {
|
|
938
|
+
expect(eval_('fn?.()', { fn: () => 42 })).toBe(42);
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
it('returns undefined when callee is null', () => {
|
|
942
|
+
expect(eval_('fn?.()', { fn: null })).toBeUndefined();
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
it('returns undefined when callee is undefined', () => {
|
|
946
|
+
expect(eval_('fn?.()', {})).toBeUndefined();
|
|
947
|
+
});
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
// ===========================================================================
|
|
952
|
+
// Global builtins
|
|
953
|
+
// ===========================================================================
|
|
954
|
+
|
|
955
|
+
describe('safeEval — global builtins', () => {
|
|
956
|
+
it('accesses Date constructor', () => {
|
|
957
|
+
expect(eval_('Date.now()')).toBeGreaterThan(0);
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
it('accesses Array.isArray', () => {
|
|
961
|
+
expect(eval_('Array.isArray(items)', { items: [1, 2] })).toBe(true);
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
it('accesses Object.keys', () => {
|
|
965
|
+
expect(eval_('Object.keys(obj).length', { obj: { a: 1, b: 2 } })).toBe(2);
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
it('accesses String methods', () => {
|
|
969
|
+
expect(eval_('String(42)')).toBe('42');
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
it('accesses Number function', () => {
|
|
973
|
+
expect(eval_('Number("42")')).toBe(42);
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
it('accesses Boolean function', () => {
|
|
977
|
+
expect(eval_('Boolean(0)')).toBe(false);
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
it('accesses parseInt', () => {
|
|
981
|
+
expect(eval_('parseInt("42abc")')).toBe(42);
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
it('accesses parseFloat', () => {
|
|
985
|
+
expect(eval_('parseFloat("3.14")')).toBe(3.14);
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
it('accesses isNaN', () => {
|
|
989
|
+
expect(eval_('isNaN(NaN)')).toBe(true);
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
it('accesses isFinite', () => {
|
|
993
|
+
expect(eval_('isFinite(42)')).toBe(true);
|
|
994
|
+
expect(eval_('isFinite(Infinity)')).toBe(false);
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
it('accesses Infinity', () => {
|
|
998
|
+
expect(eval_('Infinity')).toBe(Infinity);
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
it('accesses NaN', () => {
|
|
1002
|
+
expect(eval_('isNaN(NaN)')).toBe(true);
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
it('accesses encodeURIComponent', () => {
|
|
1006
|
+
expect(eval_('encodeURIComponent("hello world")')).toBe('hello%20world');
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
it('accesses decodeURIComponent', () => {
|
|
1010
|
+
expect(eval_('decodeURIComponent("hello%20world")')).toBe('hello world');
|
|
1011
|
+
});
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
// ===========================================================================
|
|
1016
|
+
// Number methods
|
|
1017
|
+
// ===========================================================================
|
|
1018
|
+
|
|
1019
|
+
describe('safeEval — number methods', () => {
|
|
1020
|
+
it('calls toFixed', () => {
|
|
1021
|
+
expect(eval_('x.toFixed(2)', { x: 3.14159 })).toBe('3.14');
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
it('calls toString on number', () => {
|
|
1025
|
+
expect(eval_('x.toString()', { x: 42 })).toBe('42');
|
|
1026
|
+
});
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
|
|
1030
|
+
// ===========================================================================
|
|
1031
|
+
// Empty/edge expressions
|
|
1032
|
+
// ===========================================================================
|
|
1033
|
+
|
|
1034
|
+
describe('safeEval — edge cases', () => {
|
|
1035
|
+
it('returns undefined for empty string', () => {
|
|
1036
|
+
expect(eval_('')).toBeUndefined();
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
it('returns undefined for whitespace-only', () => {
|
|
1040
|
+
expect(eval_(' ')).toBeUndefined();
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
it('fast path for simple identifier', () => {
|
|
1044
|
+
expect(eval_('name', { name: 'Alice' })).toBe('Alice');
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
it('handles hasOwnProperty access', () => {
|
|
1048
|
+
const obj = { a: 1 };
|
|
1049
|
+
expect(eval_('obj.hasOwnProperty("a")', { obj })).toBe(true);
|
|
1050
|
+
});
|
|
1051
|
+
});
|