native-document 1.0.169 → 1.0.173

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/CHANGELOG.md +100 -2
  2. package/docs/vitepress-conventions.md +2 -2
  3. package/index.d.ts +1 -1
  4. package/package.json +11 -4
  5. package/src/components/BaseComponent.js +43 -1
  6. package/src/components/accordion/Accordion.js +1 -0
  7. package/src/components/accordion/AccordionItem.js +1 -0
  8. package/src/components/alert/Alert.js +4 -0
  9. package/src/components/avatar/Avatar.js +1 -0
  10. package/src/components/avatar/AvatarGroup.js +1 -0
  11. package/src/components/badge/Badge.js +1 -0
  12. package/src/components/breadcrumb/BreadCrumb.js +4 -0
  13. package/src/components/button/Button.js +1 -0
  14. package/src/components/card/Card.js +1 -0
  15. package/src/components/context-menu/ContextMenu.js +2 -0
  16. package/src/components/context-menu/ContextMenuGroup.js +2 -0
  17. package/src/components/context-menu/ContextMenuItem.js +2 -0
  18. package/src/components/divider/Divider.js +2 -0
  19. package/src/components/dropdown/Dropdown.js +1 -0
  20. package/src/components/dropdown/DropdownDivider.js +1 -0
  21. package/src/components/dropdown/DropdownGroup.js +1 -0
  22. package/src/components/dropdown/DropdownItem.js +1 -0
  23. package/src/components/dropdown/DropdownTrigger.js +1 -0
  24. package/src/components/form/FormControl.js +1 -0
  25. package/src/components/form/field/Field.js +1 -0
  26. package/src/components/form/field/types/StringField.js +1 -1
  27. package/src/components/icon/Icon.js +1 -0
  28. package/src/components/list/ListItem.js +1 -0
  29. package/src/components/menu/Menu.js +3 -0
  30. package/src/components/menu/MenuDivider.js +1 -0
  31. package/src/components/menu/MenuGroup.js +1 -0
  32. package/src/components/menu/MenuItem.js +1 -0
  33. package/src/components/menu/MenuLink.js +1 -0
  34. package/src/components/modal/Modal.js +4 -0
  35. package/src/components/pagination/Pagination.js +1 -0
  36. package/src/components/popover/Popover.js +1 -0
  37. package/src/components/popover/PopoverFooter.js +1 -0
  38. package/src/components/popover/PopoverHeader.js +1 -0
  39. package/src/components/progress/Progress.js +5 -0
  40. package/src/components/skeleton/Skeleton.js +4 -0
  41. package/src/components/slider/Slider.js +1 -0
  42. package/src/components/spacer/Spacer.js +1 -0
  43. package/src/components/spinner/Spinner.js +1 -0
  44. package/src/components/splitter/Splitter.js +1 -0
  45. package/src/components/splitter/SplitterGutter.js +6 -0
  46. package/src/components/splitter/SplitterPanel.js +1 -0
  47. package/src/components/stacks/AbsoluteStack.js +1 -0
  48. package/src/components/stacks/FixedStack.js +1 -0
  49. package/src/components/stacks/HStack.js +1 -0
  50. package/src/components/stacks/PositionStack.js +1 -0
  51. package/src/components/stacks/RelativeStack.js +1 -0
  52. package/src/components/stacks/Stack.js +1 -0
  53. package/src/components/stacks/VStack.js +1 -0
  54. package/src/components/stepper/Stepper.js +1 -0
  55. package/src/components/stepper/StepperStep.js +1 -0
  56. package/src/components/switch/Switch.js +1 -0
  57. package/src/components/table/DataTable.js +1 -0
  58. package/src/components/table/SimpleTable.js +1 -0
  59. package/src/components/tabs/Tabs.js +1 -0
  60. package/src/components/toast/Toast.js +1 -0
  61. package/src/components/tooltip/Tooltip.js +1 -0
  62. package/src/core/data/MemoryManager.js +6 -1
  63. package/src/core/data/ObservableItem.js +33 -15
  64. package/src/core/data/ObservableObject.js +1 -2
  65. package/src/core/elements/anchor/anchor.js +4 -3
  66. package/src/core/elements/anchor/one-child-anchor-overwriting.js +4 -4
  67. package/src/core/elements/content-formatter.js +19 -0
  68. package/src/core/elements/control/for-each-array-cache.js +59 -0
  69. package/src/core/elements/control/for-each-array.js +15 -23
  70. package/src/core/wrappers/AttributesWrapper.js +12 -12
  71. package/src/core/wrappers/ElementCreator.js +33 -10
  72. package/src/core/wrappers/HtmlElementWrapper.js +17 -13
  73. package/src/core/wrappers/NDElement.js +23 -8
  74. package/src/core/wrappers/NdPrototype.js +64 -39
  75. package/src/core/wrappers/constants.js +12 -12
  76. package/src/core/wrappers/prototypes/nd-element-extensions.js +127 -57
  77. package/src/core/wrappers/template-cloner/NodeCloner.js +10 -6
  78. package/src/core/wrappers/template-cloner/TemplateCloner.js +7 -4
  79. package/src/fetch/NativeFetch.js +1 -0
  80. package/src/ui/components/accordion/AccordionItemRender.js +29 -7
  81. package/src/ui/components/alert/AlertRender.js +6 -0
  82. package/src/ui/components/breadcrumb/BreadcrumbRender.js +9 -1
  83. package/src/ui/components/button/ButtonRender.js +3 -0
  84. package/src/ui/components/contextmenu/ContextmenuRender.js +2 -0
  85. package/src/ui/components/dropdown/DropdownRender.js +3 -0
  86. package/src/ui/components/dropdown/item/DropdownItemRender.js +6 -1
  87. package/src/ui/components/form/FieldCollectionRender.js +4 -0
  88. package/src/ui/components/form/fields/CheckboxFieldRender.js +4 -0
  89. package/src/ui/components/form/fields/CheckboxGroupFieldRender.js +4 -0
  90. package/src/ui/components/form/fields/FieldRender.js +1 -1
  91. package/src/ui/components/form/fields/RadioFieldRender.js +4 -0
  92. package/src/ui/components/form/fields/RangeFieldRender.js +4 -0
  93. package/src/ui/components/form/fields/SelectFieldRender.js +5 -0
  94. package/src/ui/components/form/fields/SliderFieldRender.js +3 -0
  95. package/src/ui/components/icon/tabler/TablerIconRender.js +0 -1
  96. package/src/ui/components/list/item/ListItemRender.js +4 -0
  97. package/src/ui/components/menu/MenuItemRender.js +8 -0
  98. package/src/ui/components/menu/MenuLinkRender.js +11 -0
  99. package/src/ui/components/menu/MenuRender.js +5 -0
  100. package/src/ui/components/menu/helpers.js +2 -1
  101. package/src/ui/components/modal/ModalRender.js +8 -2
  102. package/src/ui/components/progress/ProgressRender.js +9 -0
  103. package/src/ui/components/skeleton/SkeletonRender.js +2 -0
  104. package/src/ui/components/spinner/SpinnerRender.js +3 -0
  105. package/src/ui/components/splitter/SplitterGutterRender.js +4 -0
  106. package/src/ui/components/splitter/SplitterPanelRender.js +4 -0
  107. package/src/ui/components/splitter/SplitterRender.js +2 -0
  108. package/src/ui/components/stepper/StepperRender.js +2 -1
  109. package/src/ui/components/stepper/StepperStepRender.js +8 -1
  110. package/src/ui/components/switch/SwitchRender.js +3 -0
  111. package/src/ui/components/table/data-table/DataTableRender.js +4 -0
  112. package/src/ui/components/table/simple-table/SimpleTableRender.js +2 -0
  113. package/src/ui/components/toast/ToastRender.js +4 -0
  114. package/src/ui/utils/aria.js +19 -0
  115. package/tests/integration/ui/attributes-wrapper.test.js +177 -0
  116. package/tests/integration/ui/for-each-array.test.js +167 -0
  117. package/tests/integration/ui/lifecycle.test.js +146 -0
  118. package/tests/integration/ui/show-if.test.js +168 -0
  119. package/tests/unit/core/observable-array.test.js +404 -0
  120. package/tests/unit/core/observable-item.test.js +428 -0
  121. package/tests/unit/core/observable-object.test.js +239 -0
  122. package/tests/unit/core/observable-resource.test.js +323 -0
  123. package/tests/unit/core/store.test.js +347 -0
  124. package/tests/unit/router/route-matching.test.js +169 -0
  125. package/tests/unit/router/router-guards.test.js +189 -0
  126. package/tests/unit/utils/cache.test.js +120 -0
  127. package/tests/unit/utils/filters.test.js +394 -0
  128. package/types/elements.d.ts +5 -0
  129. package/types/native-fetch.d.ts +1 -1
  130. package/types/observable-resource.d.ts +2 -2
  131. package/types/observable.d.ts +137 -21
  132. package/types/service.d.ts +2 -1
  133. package/utils.js +7 -4
  134. package/vitest.config.js +15 -0
@@ -0,0 +1,169 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Route } from '../../../src/router/Route';
3
+
4
+ const route = (path, options = {}) => new Route(path, () => {}, options);
5
+
6
+ describe('Route matching', () => {
7
+
8
+ // -- exact path -----------------------------------------------------------
9
+
10
+ describe('exact path', () => {
11
+ it('matches an exact path', () => {
12
+ const r = route('/about');
13
+ expect(r.match('/about')).toEqual({});
14
+ });
15
+
16
+ it('does not match a different path', () => {
17
+ const r = route('/about');
18
+ expect(r.match('/contact')).toBe(false);
19
+ });
20
+
21
+ it('normalizes leading and trailing slashes', () => {
22
+ const r = route('about/');
23
+ expect(r.match('about')).toEqual({});
24
+ });
25
+
26
+ it('does not match a partial path', () => {
27
+ const r = route('/about');
28
+ expect(r.match('/about/us')).toBe(false);
29
+ });
30
+
31
+ it('matches root path /', () => {
32
+ const r = route('/');
33
+ expect(r.match('/')).toEqual({});
34
+ });
35
+ });
36
+
37
+ // -- :param extraction ----------------------------------------------------
38
+
39
+ describe('param extraction', () => {
40
+ it('extracts a single param', () => {
41
+ const r = route('/user/{id}');
42
+ expect(r.match('/user/42')).toEqual({ id: '42' });
43
+ });
44
+
45
+ it('extracts multiple params', () => {
46
+ const r = route('/user/{userId}/post/{postId}');
47
+ expect(r.match('/user/1/post/99')).toEqual({ userId: '1', postId: '99' });
48
+ });
49
+
50
+ it('does not match if param segment is missing', () => {
51
+ const r = route('/user/{id}');
52
+ expect(r.match('/user/')).toBe(false);
53
+ });
54
+
55
+ it('does not match if extra segments are present', () => {
56
+ const r = route('/user/{id}');
57
+ expect(r.match('/user/42/extra')).toBe(false);
58
+ });
59
+ });
60
+
61
+ // -- typed params ---------------------------------------------------------
62
+
63
+ describe('typed params', () => {
64
+ it('{id:id} matches numeric id', () => {
65
+ const r = route('/post/{id:id}');
66
+ expect(r.match('/post/123')).toEqual({ id: '123' });
67
+ });
68
+
69
+ it('{id:id} does not match non-numeric', () => {
70
+ const r = route('/post/{id:id}');
71
+ expect(r.match('/post/abc')).toBe(false);
72
+ });
73
+
74
+ it('{lang:lang} matches a two-letter language code', () => {
75
+ const r = route('/{lang:lang}/home');
76
+ expect(r.match('/fr/home')).toEqual({ lang: 'fr' });
77
+ });
78
+
79
+ it('{lang:lang} does not match three-letter code', () => {
80
+ const r = route('/{lang:lang}/home');
81
+ expect(r.match('/fra/home')).toBe(false);
82
+ });
83
+
84
+ it('{token:token} matches alphanumeric token', () => {
85
+ const r = route('/token/{value:token}');
86
+ expect(r.match('/token/abc123_-')).toEqual({ value: 'abc123_-' });
87
+ });
88
+
89
+ it('{n:number} matches integer', () => {
90
+ const r = route('/page/{n:number}');
91
+ expect(r.match('/page/5')).toEqual({ n: '5' });
92
+ });
93
+
94
+ it('{n:number} matches float', () => {
95
+ const r = route('/price/{n:number}');
96
+ expect(r.match('/price/3.14')).toEqual({ n: '3.14' });
97
+ });
98
+ });
99
+
100
+ // -- custom param validators ----------------------------------------------
101
+
102
+ describe('custom param validators (with)', () => {
103
+ it('accepts a custom regex pattern via options.with', () => {
104
+ const r = route('/status/{code}', {
105
+ with: { code: 'active|inactive|pending' },
106
+ });
107
+ expect(r.match('/status/active')).toEqual({ code: 'active' });
108
+ expect(r.match('/status/unknown')).toBe(false);
109
+ });
110
+ });
111
+
112
+ // -- url() ----------------------------------------------------------------
113
+
114
+ describe('url()', () => {
115
+ it('generates a URL from params', () => {
116
+ const r = route('/user/{id}');
117
+ expect(r.url({ params: { id: 42 } })).toBe('/user/42');
118
+ });
119
+
120
+ it('generates a URL with query string', () => {
121
+ const r = route('/search');
122
+ const url = r.url({ query: { q: 'hello', page: '1' } });
123
+ expect(url).toContain('/search?');
124
+ expect(url).toContain('q=hello');
125
+ expect(url).toContain('page=1');
126
+ });
127
+
128
+ it('throws if a required param is missing', () => {
129
+ const r = route('/user/{id}');
130
+ expect(() => r.url({ params: {} })).toThrow();
131
+ });
132
+
133
+ it('prepends basePath if provided', () => {
134
+ const r = route('/about');
135
+ expect(r.url({ basePath: '/app' })).toBe('/app/about');
136
+ });
137
+ });
138
+
139
+ // -- route metadata -------------------------------------------------------
140
+
141
+ describe('route metadata', () => {
142
+ it('name() returns the route name', () => {
143
+ const r = route('/about', { name: 'about' });
144
+ expect(r.name()).toBe('about');
145
+ });
146
+
147
+ it('name() returns null without a name', () => {
148
+ const r = route('/about');
149
+ expect(r.name()).toBeNull();
150
+ });
151
+
152
+ it('path() returns the normalized path', () => {
153
+ // trim() strips trailing slashes
154
+ const r = route('/about');
155
+ expect(r.path()).toBe('/about');
156
+ });
157
+
158
+ it('shouldRebuild() returns false by default', () => {
159
+ const r = route('/about');
160
+ expect(r.shouldRebuild()).toBe(false);
161
+ });
162
+
163
+ it('shouldRebuild() returns true when set', () => {
164
+ const r = route('/about', { shouldRebuild: true });
165
+ expect(r.shouldRebuild()).toBe(true);
166
+ });
167
+ });
168
+
169
+ });
@@ -0,0 +1,189 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { Route } from '../../../src/router/Route';
3
+ import Router from '../../../src/router/Router';
4
+
5
+ vi.mock('../../../src/core/utils/debug-manager', () => ({
6
+ default: { log: vi.fn(), warn: vi.fn(), error: vi.fn(), disable: vi.fn() },
7
+ }));
8
+
9
+ // Test helpers
10
+ const component = () => document.createElement('div');
11
+
12
+ const makeRouter = (callback) => {
13
+ const router = new Router({ mode: 'memory' });
14
+ callback(router);
15
+ return router;
16
+ };
17
+
18
+ describe('Router guards (middlewares)', () => {
19
+
20
+ // -- middleware execution -------------------------------------------------
21
+
22
+ describe('middleware execution', () => {
23
+ it('middleware is called on route change', () => {
24
+ const guard = vi.fn((_req, next) => next());
25
+ const router = makeRouter((r) => {
26
+ r.add('/home', component, { middlewares: [guard] });
27
+ });
28
+ const resolved = router.resolve('/home');
29
+ router.handleRouteChange(resolved.route, resolved.params, resolved.query, '/home');
30
+ expect(guard).toHaveBeenCalledOnce();
31
+ });
32
+
33
+ it('middleware receives the request object', () => {
34
+ let capturedRequest = null;
35
+ const guard = vi.fn((req, next) => {
36
+ capturedRequest = req;
37
+ next();
38
+ });
39
+ const router = makeRouter((r) => {
40
+ r.add('/about', component, { middlewares: [guard] });
41
+ });
42
+ const resolved = router.resolve('/about');
43
+ router.handleRouteChange(resolved.route, {}, {}, '/about');
44
+ expect(capturedRequest).toMatchObject({ path: '/about' });
45
+ });
46
+
47
+ it('multiple middlewares are called in order', () => {
48
+ const order = [];
49
+ const g1 = (_req, next) => { order.push(1); next(); };
50
+ const g2 = (_req, next) => { order.push(2); next(); };
51
+ const g3 = (_req, next) => { order.push(3); next(); };
52
+
53
+ const router = makeRouter((r) => {
54
+ r.add('/page', component, { middlewares: [g1, g2, g3] });
55
+ });
56
+ const resolved = router.resolve('/page');
57
+ router.handleRouteChange(resolved.route, {}, {}, '/page');
58
+ expect(order).toEqual([1, 2, 3]);
59
+ });
60
+
61
+ it('not calling next() stops the chain', () => {
62
+ const g1 = vi.fn((_req, _next) => {}); // does not call next
63
+ const g2 = vi.fn((_req, next) => next());
64
+
65
+ const router = makeRouter((r) => {
66
+ r.add('/blocked', component, { middlewares: [g1, g2] });
67
+ });
68
+ const resolved = router.resolve('/blocked');
69
+ router.handleRouteChange(resolved.route, {}, {}, '/blocked');
70
+ expect(g1).toHaveBeenCalledOnce();
71
+ expect(g2).not.toHaveBeenCalled();
72
+ });
73
+ });
74
+
75
+ // -- group middlewares ----------------------------------------------------
76
+
77
+ describe('group middlewares', () => {
78
+ it('group middleware is applied to all routes in the group', () => {
79
+ const authGuard = vi.fn((_req, next) => next());
80
+
81
+ const router = makeRouter((r) => {
82
+ r.group('/admin', { middlewares: [authGuard] }, () => {
83
+ r.add('/dashboard', component);
84
+ r.add('/settings', component);
85
+ });
86
+ });
87
+
88
+ const r1 = router.resolve('/admin/dashboard');
89
+ router.handleRouteChange(r1.route, {}, {}, '/admin/dashboard');
90
+
91
+ const r2 = router.resolve('/admin/settings');
92
+ router.handleRouteChange(r2.route, {}, {}, '/admin/settings');
93
+
94
+ expect(authGuard).toHaveBeenCalledTimes(2);
95
+ });
96
+
97
+ it('group middleware runs before route-level middleware', () => {
98
+ const order = [];
99
+ const groupGuard = (_req, next) => { order.push('group'); next(); };
100
+ const routeGuard = (_req, next) => { order.push('route'); next(); };
101
+
102
+ const router = makeRouter((r) => {
103
+ r.group('/admin', { middlewares: [groupGuard] }, () => {
104
+ r.add('/users', component, { middlewares: [routeGuard] });
105
+ });
106
+ });
107
+
108
+ const resolved = router.resolve('/admin/users');
109
+ router.handleRouteChange(resolved.route, {}, {}, '/admin/users');
110
+ expect(order).toEqual(['group', 'route']);
111
+ });
112
+ });
113
+
114
+ // -- subscribe() ----------------------------------------------------------
115
+
116
+ describe('subscribe()', () => {
117
+ it('listener is called after all middlewares', () => {
118
+ const listener = vi.fn();
119
+ const guard = (_req, next) => next();
120
+
121
+ const router = makeRouter((r) => {
122
+ r.add('/home', component, { middlewares: [guard] });
123
+ });
124
+ router.subscribe(listener);
125
+
126
+ const resolved = router.resolve('/home');
127
+ router.handleRouteChange(resolved.route, {}, {}, '/home');
128
+ expect(listener).toHaveBeenCalledOnce();
129
+ });
130
+
131
+ it('unsubscribe stops the listener from being called', () => {
132
+ const listener = vi.fn();
133
+ const router = makeRouter((r) => r.add('/home', component));
134
+ const unsub = router.subscribe(listener);
135
+ unsub();
136
+
137
+ const resolved = router.resolve('/home');
138
+ router.handleRouteChange(resolved.route, {}, {}, '/home');
139
+ expect(listener).not.toHaveBeenCalled();
140
+ });
141
+
142
+ it('subscribe throws if argument is not a function', () => {
143
+ const router = makeRouter((r) => r.add('/home', component));
144
+ expect(() => router.subscribe('not-a-fn')).toThrow();
145
+ });
146
+ });
147
+
148
+ // -- resolve() ------------------------------------------------------------
149
+
150
+ describe('resolve()', () => {
151
+ it('resolves a path to a route', () => {
152
+ const router = makeRouter((r) => r.add('/about', component));
153
+ const resolved = router.resolve('/about');
154
+ expect(resolved.route).toBeDefined();
155
+ expect(resolved.params).toEqual({});
156
+ });
157
+
158
+ it('resolves a named route', () => {
159
+ const router = makeRouter((r) => {
160
+ r.add('/about', component, { name: 'about' });
161
+ });
162
+ const resolved = router.resolve({ name: 'about' });
163
+ expect(resolved.route.name()).toBe('about');
164
+ });
165
+
166
+ it('throws for an unknown path', () => {
167
+ const router = makeRouter((r) => r.add('/home', component));
168
+ expect(() => router.resolve('/unknown')).toThrow();
169
+ });
170
+
171
+ it('parses query string', () => {
172
+ const router = makeRouter((r) => r.add('/search', component));
173
+ const resolved = router.resolve('/search?q=hello&page=2');
174
+ expect(resolved.query).toMatchObject({ q: 'hello', page: '2' });
175
+ });
176
+ });
177
+
178
+ // -- currentState() -------------------------------------------------------
179
+
180
+ describe('currentState()', () => {
181
+ it('reflects the current route after handleRouteChange', () => {
182
+ const router = makeRouter((r) => r.add('/home', component));
183
+ const resolved = router.resolve('/home');
184
+ router.handleRouteChange(resolved.route, {}, {}, '/home');
185
+ expect(router.currentState().path).toBe('/home');
186
+ });
187
+ });
188
+
189
+ });
@@ -0,0 +1,120 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { once, singleton, memoize } from '../../../src/core/utils/cache';
3
+
4
+ describe('Cache utilities', () => {
5
+
6
+ // -- singleton() ----------------------------------------------------------
7
+
8
+ describe('singleton() — once(fn)', () => {
9
+ it('calls the function on first access', () => {
10
+ const fn = vi.fn(() => ({ value: 42 }));
11
+ const get = singleton(fn);
12
+ get();
13
+ expect(fn).toHaveBeenCalledOnce();
14
+ });
15
+
16
+ it('returns the same result on subsequent calls', () => {
17
+ const fn = vi.fn(() => ({ value: 42 }));
18
+ const get = singleton(fn);
19
+ const r1 = get();
20
+ const r2 = get();
21
+ expect(r1).toBe(r2);
22
+ expect(fn).toHaveBeenCalledOnce();
23
+ });
24
+
25
+ it('passes arguments to the function on first call', () => {
26
+ const fn = vi.fn((x) => x * 2);
27
+ const get = singleton(fn);
28
+ expect(get(5)).toBe(10);
29
+ expect(get(99)).toBe(10); // cached result from first call
30
+ });
31
+ });
32
+
33
+ // -- once() — autoOnce proxy ---------------------------------------------
34
+
35
+ describe('once() — autoOnce(fn)', () => {
36
+ it('calls the factory on first property access', () => {
37
+ const factory = vi.fn(() => ({ name: 'store', count: 0 }));
38
+ const proxy = once(factory);
39
+ const _ = proxy.name;
40
+ expect(factory).toHaveBeenCalledOnce();
41
+ });
42
+
43
+ it('does not call the factory again on subsequent access', () => {
44
+ const factory = vi.fn(() => ({ name: 'store' }));
45
+ const proxy = once(factory);
46
+ const _a = proxy.name;
47
+ const _b = proxy.name;
48
+ expect(factory).toHaveBeenCalledOnce();
49
+ });
50
+
51
+ it('returns the correct property value', () => {
52
+ const proxy = once(() => ({ x: 10, y: 20 }));
53
+ expect(proxy.x).toBe(10);
54
+ expect(proxy.y).toBe(20);
55
+ });
56
+
57
+ it('returns undefined for unknown properties', () => {
58
+ const proxy = once(() => ({ x: 1 }));
59
+ expect(proxy.unknown).toBeUndefined();
60
+ });
61
+ });
62
+
63
+ // -- memoize() — autoMemoize proxy ----------------------------------------
64
+
65
+ describe('memoize() — autoMemoize(fn)', () => {
66
+ // autoMemoize behaviour:
67
+ // - fn.length === 0 -> calls fn(key), caches and returns the value
68
+ // - fn.length > 0 -> returns a memoized (...args) => fn(...args, key)
69
+
70
+ it('fn.length === 0 — returns the value directly', () => {
71
+ // fn ignores key, autoMemoize calls fn(key) and caches the result
72
+ let callCount = 0;
73
+ const fn = function() { callCount++; return 'fixed'; };
74
+ const proxy = memoize(fn);
75
+ expect(proxy.any).toBe('fixed');
76
+ expect(callCount).toBe(1);
77
+ });
78
+
79
+ it('fn.length === 0 — caches per key', () => {
80
+ const calls = [];
81
+ const fn = function() { return calls.push(fn) && `r${calls.length}`; };
82
+ const proxy = memoize(fn);
83
+ const r1 = proxy.home;
84
+ const r2 = proxy.home;
85
+ expect(r1).toBe(r2);
86
+ expect(calls.length).toBe(1);
87
+ });
88
+
89
+ it('fn.length === 0 — independent keys call fn separately', () => {
90
+ const calls = [];
91
+ const fn = function() { calls.push(1); return `r${calls.length}`; };
92
+ const proxy = memoize(fn);
93
+ const r1 = proxy.home;
94
+ const r2 = proxy.search;
95
+ expect(r1).not.toBe(r2);
96
+ expect(calls.length).toBe(2);
97
+ });
98
+
99
+ it('fn.length > 0 — returns a function per key', () => {
100
+ // fn(value, key): value is passed by caller, key is appended by autoMemoize
101
+ const fn = function(value, key) { return `${key}:${value}`; };
102
+ const proxy = memoize(fn);
103
+ expect(typeof proxy.fr).toBe('function');
104
+ expect(proxy.fr('hello')).toBe('fr:hello');
105
+ });
106
+
107
+ it('fn.length > 0 — caches the function per key', () => {
108
+ let callCount = 0;
109
+ // fn must have length > 0 — declare at least one param
110
+ const fn = function(_value, _key) { callCount++; };
111
+ const proxy = memoize(fn);
112
+ proxy.fr('hello');
113
+ // second access to proxy.fr returns the cached wrapper, fn not called again
114
+ const cached = proxy.fr;
115
+ expect(typeof cached).toBe('function');
116
+ expect(callCount).toBe(1);
117
+ });
118
+ });
119
+
120
+ });