|
1 | 1 | import { describe, it, expect, vi } from 'vitest' |
2 | | -import { createTrackingProxy, getProxyPath } from '../../src/signals/trackingProxy' |
| 2 | +import { createTrackingProxy, getProxyPath, unwrap } from '../../src/signals/trackingProxy' |
3 | 3 | import { createPathSignalRegistry } from '../../src/signals/pathSignalRegistry' |
4 | 4 | import { alienEngine } from '../../src/signals/engine' |
5 | 5 | import type { PathSignalRegistry } from '../../src/signals/pathSignalRegistry' |
@@ -707,3 +707,108 @@ describe('createTrackingProxy', () => { |
707 | 707 | scope.stop() |
708 | 708 | }) |
709 | 709 | }) |
| 710 | + |
| 711 | +describe('unwrap', () => { |
| 712 | + function makeRegistry(): PathSignalRegistry { |
| 713 | + return createPathSignalRegistry(alienEngine) |
| 714 | + } |
| 715 | + |
| 716 | + it('returns the raw target from a tracking proxy', () => { |
| 717 | + const state = { name: 'Alice', nested: { x: 1 } } |
| 718 | + const registry = makeRegistry() |
| 719 | + const proxy = createTrackingProxy(state, '', registry, registry.proxyCache) |
| 720 | + |
| 721 | + expect(unwrap(proxy)).toBe(state) |
| 722 | + }) |
| 723 | + |
| 724 | + it('returns nested raw target from a child proxy', () => { |
| 725 | + const state = { nested: { x: 1 } } |
| 726 | + const registry = makeRegistry() |
| 727 | + const proxy = createTrackingProxy(state, '', registry, registry.proxyCache) |
| 728 | + |
| 729 | + const childProxy = proxy.nested |
| 730 | + expect(childProxy).not.toBe(state.nested) // is a proxy |
| 731 | + expect(unwrap(childProxy)).toBe(state.nested) // unwraps to raw |
| 732 | + }) |
| 733 | + |
| 734 | + it('returns the value unchanged for non-proxy objects', () => { |
| 735 | + const obj = { a: 1 } |
| 736 | + expect(unwrap(obj)).toBe(obj) |
| 737 | + }) |
| 738 | + |
| 739 | + it('returns primitives unchanged', () => { |
| 740 | + expect(unwrap(42)).toBe(42) |
| 741 | + expect(unwrap('hello')).toBe('hello') |
| 742 | + expect(unwrap(true)).toBe(true) |
| 743 | + expect(unwrap(null)).toBe(null) |
| 744 | + expect(unwrap(undefined)).toBe(undefined) |
| 745 | + }) |
| 746 | + |
| 747 | + it('enables identity comparison between unwrapped proxy and raw value', () => { |
| 748 | + const item = { id: 1, name: 'first' } |
| 749 | + const state = { items: [item], current: item } |
| 750 | + const registry = makeRegistry() |
| 751 | + const proxy = createTrackingProxy(state, '', registry, registry.proxyCache) |
| 752 | + |
| 753 | + // Without unwrap: proxy !== raw |
| 754 | + const proxiedCurrent = proxy.current |
| 755 | + expect(proxiedCurrent === item).toBe(false) |
| 756 | + |
| 757 | + // With unwrap: raw === raw |
| 758 | + expect(unwrap(proxiedCurrent) === item).toBe(true) |
| 759 | + }) |
| 760 | + |
| 761 | + it('indexOf/includes auto-unwrap proxy arguments', () => { |
| 762 | + const state = { |
| 763 | + items: [ |
| 764 | + { id: 1, name: 'a' }, |
| 765 | + { id: 2, name: 'b' }, |
| 766 | + { id: 3, name: 'c' }, |
| 767 | + ], |
| 768 | + } |
| 769 | + const registry = makeRegistry() |
| 770 | + const proxy = createTrackingProxy(state, '', registry, registry.proxyCache) |
| 771 | + |
| 772 | + // Access an item through the proxy — get back a proxy wrapper |
| 773 | + const proxiedItem = proxy.items[1] |
| 774 | + expect(proxiedItem).not.toBe(state.items[1]) // it's a proxy |
| 775 | + |
| 776 | + // indexOf/includes auto-unwrap proxy args internally, so these just work |
| 777 | + expect(proxy.items.indexOf(proxiedItem)).toBe(1) |
| 778 | + expect(proxy.items.includes(proxiedItem)).toBe(true) |
| 779 | + |
| 780 | + // Also works with manually unwrapped value |
| 781 | + expect(proxy.items.indexOf(unwrap(proxiedItem))).toBe(1) |
| 782 | + }) |
| 783 | + |
| 784 | + it('find() callback needs unwrap for identity comparison', () => { |
| 785 | + const state = { |
| 786 | + items: [ |
| 787 | + { id: 1, name: 'a' }, |
| 788 | + { id: 2, name: 'b' }, |
| 789 | + { id: 3, name: 'c' }, |
| 790 | + ], |
| 791 | + } |
| 792 | + const registry = makeRegistry() |
| 793 | + const proxy = createTrackingProxy(state, '', registry, registry.proxyCache) |
| 794 | + |
| 795 | + // Get a proxy-wrapped item |
| 796 | + const proxiedItem = proxy.items[1] |
| 797 | + const rawItem = unwrap(proxiedItem) |
| 798 | + |
| 799 | + // find callback receives raw elements; unwrapped proxy === raw element |
| 800 | + const found = proxy.items.find( |
| 801 | + (item: { id: number; name: string }) => item === rawItem, |
| 802 | + ) |
| 803 | + |
| 804 | + expect(found).toBeDefined() |
| 805 | + // find() returns a proxied result (lazy signal registration) |
| 806 | + expect(found).not.toBe(state.items[1]) |
| 807 | + expect(unwrap(found)).toBe(state.items[1]) |
| 808 | + |
| 809 | + // Accessing properties on the found proxy registers signals |
| 810 | + const name = (found as { name: string }).name |
| 811 | + expect(name).toBe('b') |
| 812 | + expect(registry.has('items.{id:2}.name')).toBe(true) |
| 813 | + }) |
| 814 | +}) |
0 commit comments