native-document 1.0.169 → 1.0.172
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/CHANGELOG.md +62 -1
- package/docs/vitepress-conventions.md +2 -2
- package/package.json +11 -4
- package/src/components/BaseComponent.js +43 -1
- package/src/components/accordion/Accordion.js +1 -0
- package/src/components/accordion/AccordionItem.js +1 -0
- package/src/components/alert/Alert.js +4 -0
- package/src/components/avatar/Avatar.js +1 -0
- package/src/components/avatar/AvatarGroup.js +1 -0
- package/src/components/badge/Badge.js +1 -0
- package/src/components/breadcrumb/BreadCrumb.js +4 -0
- package/src/components/button/Button.js +1 -0
- package/src/components/card/Card.js +1 -0
- package/src/components/context-menu/ContextMenu.js +2 -0
- package/src/components/context-menu/ContextMenuGroup.js +2 -0
- package/src/components/context-menu/ContextMenuItem.js +2 -0
- package/src/components/divider/Divider.js +2 -0
- package/src/components/dropdown/Dropdown.js +1 -0
- package/src/components/dropdown/DropdownDivider.js +1 -0
- package/src/components/dropdown/DropdownGroup.js +1 -0
- package/src/components/dropdown/DropdownItem.js +1 -0
- package/src/components/dropdown/DropdownTrigger.js +1 -0
- package/src/components/form/FormControl.js +1 -0
- package/src/components/form/field/Field.js +1 -0
- package/src/components/form/field/types/StringField.js +1 -1
- package/src/components/icon/Icon.js +1 -0
- package/src/components/list/ListItem.js +1 -0
- package/src/components/menu/Menu.js +3 -0
- package/src/components/menu/MenuDivider.js +1 -0
- package/src/components/menu/MenuGroup.js +1 -0
- package/src/components/menu/MenuItem.js +1 -0
- package/src/components/menu/MenuLink.js +1 -0
- package/src/components/modal/Modal.js +4 -0
- package/src/components/pagination/Pagination.js +1 -0
- package/src/components/popover/Popover.js +1 -0
- package/src/components/popover/PopoverFooter.js +1 -0
- package/src/components/popover/PopoverHeader.js +1 -0
- package/src/components/progress/Progress.js +5 -0
- package/src/components/skeleton/Skeleton.js +4 -0
- package/src/components/slider/Slider.js +1 -0
- package/src/components/spacer/Spacer.js +1 -0
- package/src/components/spinner/Spinner.js +1 -0
- package/src/components/splitter/Splitter.js +1 -0
- package/src/components/splitter/SplitterGutter.js +6 -0
- package/src/components/splitter/SplitterPanel.js +1 -0
- package/src/components/stacks/AbsoluteStack.js +1 -0
- package/src/components/stacks/FixedStack.js +1 -0
- package/src/components/stacks/HStack.js +1 -0
- package/src/components/stacks/PositionStack.js +1 -0
- package/src/components/stacks/RelativeStack.js +1 -0
- package/src/components/stacks/Stack.js +1 -0
- package/src/components/stacks/VStack.js +1 -0
- package/src/components/stepper/Stepper.js +1 -0
- package/src/components/stepper/StepperStep.js +1 -0
- package/src/components/switch/Switch.js +1 -0
- package/src/components/table/DataTable.js +1 -0
- package/src/components/table/SimpleTable.js +1 -0
- package/src/components/tabs/Tabs.js +1 -0
- package/src/components/toast/Toast.js +1 -0
- package/src/components/tooltip/Tooltip.js +1 -0
- package/src/core/data/MemoryManager.js +6 -1
- package/src/core/data/ObservableItem.js +28 -10
- package/src/core/elements/anchor/anchor.js +4 -3
- package/src/core/elements/anchor/one-child-anchor-overwriting.js +4 -4
- package/src/core/elements/content-formatter.js +19 -0
- package/src/core/elements/control/for-each-array-cache.js +59 -0
- package/src/core/elements/control/for-each-array.js +15 -23
- package/src/core/wrappers/AttributesWrapper.js +12 -12
- package/src/core/wrappers/ElementCreator.js +33 -10
- package/src/core/wrappers/HtmlElementWrapper.js +9 -4
- package/src/core/wrappers/NDElement.js +23 -8
- package/src/core/wrappers/NdPrototype.js +31 -39
- package/src/core/wrappers/constants.js +12 -12
- package/src/core/wrappers/prototypes/nd-element-extensions.js +127 -57
- package/src/ui/components/accordion/AccordionItemRender.js +29 -7
- package/src/ui/components/alert/AlertRender.js +6 -0
- package/src/ui/components/breadcrumb/BreadcrumbRender.js +9 -1
- package/src/ui/components/button/ButtonRender.js +3 -0
- package/src/ui/components/contextmenu/ContextmenuRender.js +2 -0
- package/src/ui/components/dropdown/DropdownRender.js +3 -0
- package/src/ui/components/dropdown/item/DropdownItemRender.js +6 -1
- package/src/ui/components/form/FieldCollectionRender.js +4 -0
- package/src/ui/components/form/fields/CheckboxFieldRender.js +4 -0
- package/src/ui/components/form/fields/CheckboxGroupFieldRender.js +4 -0
- package/src/ui/components/form/fields/RadioFieldRender.js +4 -0
- package/src/ui/components/form/fields/RangeFieldRender.js +4 -0
- package/src/ui/components/form/fields/SelectFieldRender.js +5 -0
- package/src/ui/components/form/fields/SliderFieldRender.js +3 -0
- package/src/ui/components/icon/tabler/TablerIconRender.js +0 -1
- package/src/ui/components/list/item/ListItemRender.js +4 -0
- package/src/ui/components/menu/MenuItemRender.js +8 -0
- package/src/ui/components/menu/MenuLinkRender.js +11 -0
- package/src/ui/components/menu/MenuRender.js +5 -0
- package/src/ui/components/menu/helpers.js +2 -1
- package/src/ui/components/modal/ModalRender.js +8 -2
- package/src/ui/components/progress/ProgressRender.js +9 -0
- package/src/ui/components/skeleton/SkeletonRender.js +2 -0
- package/src/ui/components/spinner/SpinnerRender.js +3 -0
- package/src/ui/components/splitter/SplitterGutterRender.js +4 -0
- package/src/ui/components/splitter/SplitterPanelRender.js +4 -0
- package/src/ui/components/splitter/SplitterRender.js +2 -0
- package/src/ui/components/stepper/StepperRender.js +2 -1
- package/src/ui/components/stepper/StepperStepRender.js +8 -1
- package/src/ui/components/switch/SwitchRender.js +3 -0
- package/src/ui/components/table/data-table/DataTableRender.js +4 -0
- package/src/ui/components/table/simple-table/SimpleTableRender.js +2 -0
- package/src/ui/components/toast/ToastRender.js +4 -0
- package/src/ui/utils/aria.js +19 -0
- package/tests/integration/ui/attributes-wrapper.test.js +177 -0
- package/tests/integration/ui/for-each-array.test.js +167 -0
- package/tests/integration/ui/lifecycle.test.js +146 -0
- package/tests/integration/ui/show-if.test.js +168 -0
- package/tests/unit/core/observable-array.test.js +404 -0
- package/tests/unit/core/observable-item.test.js +428 -0
- package/tests/unit/core/observable-object.test.js +239 -0
- package/tests/unit/core/observable-resource.test.js +323 -0
- package/tests/unit/core/store.test.js +347 -0
- package/tests/unit/router/route-matching.test.js +169 -0
- package/tests/unit/router/router-guards.test.js +189 -0
- package/tests/unit/utils/cache.test.js +120 -0
- package/tests/unit/utils/filters.test.js +394 -0
- package/types/elements.d.ts +5 -0
- package/vitest.config.js +15 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { Observable } from '../../../src/core/data/Observable';
|
|
3
|
+
import ObservableArray from '../../../src/core/data/ObservableArray';
|
|
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
|
+
import { Div, Span, ForEachArray } from '../../../elements';
|
|
10
|
+
|
|
11
|
+
const mount = (anchor) => {
|
|
12
|
+
document.body.innerHTML = '';
|
|
13
|
+
document.body.appendChild(anchor);
|
|
14
|
+
return document.body;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const items = (arr = []) => new ObservableArray(arr);
|
|
18
|
+
|
|
19
|
+
describe('ForEachArray', () => {
|
|
20
|
+
|
|
21
|
+
// -- static array ---------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
describe('static array', () => {
|
|
24
|
+
it('renders one element per item', () => {
|
|
25
|
+
const anchor = ForEachArray(['a', 'b', 'c'], (item) => Div({}, item));
|
|
26
|
+
const body = mount(anchor);
|
|
27
|
+
expect(body.querySelectorAll('div').length).toBe(3);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('renders items in order', () => {
|
|
31
|
+
const anchor = ForEachArray(['x', 'y', 'z'], (item) => Div({}, item));
|
|
32
|
+
const body = mount(anchor);
|
|
33
|
+
const divs = body.querySelectorAll('div');
|
|
34
|
+
expect(divs[0].textContent).toBe('x');
|
|
35
|
+
expect(divs[1].textContent).toBe('y');
|
|
36
|
+
expect(divs[2].textContent).toBe('z');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('renders empty array without error', () => {
|
|
40
|
+
const anchor = ForEachArray([], (item) => Div({}, item));
|
|
41
|
+
const body = mount(anchor);
|
|
42
|
+
expect(body.querySelectorAll('div').length).toBe(0);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// -- ObservableArray initial render ---------------------------------------
|
|
47
|
+
|
|
48
|
+
describe('ObservableArray initial render', () => {
|
|
49
|
+
it('renders items from initial ObservableArray', () => {
|
|
50
|
+
const obs = items(['a', 'b', 'c']);
|
|
51
|
+
const anchor = ForEachArray(obs, (item) => Div({}, item));
|
|
52
|
+
const body = mount(anchor);
|
|
53
|
+
expect(body.querySelectorAll('div').length).toBe(3);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('renders empty ObservableArray without error', () => {
|
|
57
|
+
const obs = items([]);
|
|
58
|
+
const anchor = ForEachArray(obs, (item) => Div({}, item));
|
|
59
|
+
const body = mount(anchor);
|
|
60
|
+
expect(body.querySelectorAll('div').length).toBe(0);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// -- push() ---------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
describe('push()', () => {
|
|
67
|
+
it('adds a rendered element to the DOM', () => {
|
|
68
|
+
const obs = items(['a', 'b']);
|
|
69
|
+
const anchor = ForEachArray(obs, (item) => Div({}, item));
|
|
70
|
+
const body = mount(anchor);
|
|
71
|
+
obs.push('c');
|
|
72
|
+
expect(body.querySelectorAll('div').length).toBe(3);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('appended item appears last', () => {
|
|
76
|
+
const obs = items(['a', 'b']);
|
|
77
|
+
const anchor = ForEachArray(obs, (item) => Div({}, item));
|
|
78
|
+
const body = mount(anchor);
|
|
79
|
+
obs.push('c');
|
|
80
|
+
const divs = body.querySelectorAll('div');
|
|
81
|
+
expect(divs[divs.length - 1].textContent).toBe('c');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// -- removeItem() ---------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
describe('removeItem()', () => {
|
|
88
|
+
it('removes the corresponding element', () => {
|
|
89
|
+
const obs = items(['a', 'b', 'c']);
|
|
90
|
+
const anchor = ForEachArray(obs, (item) => Div({}, item));
|
|
91
|
+
const body = mount(anchor);
|
|
92
|
+
obs.removeItem('b');
|
|
93
|
+
expect(body.querySelectorAll('div').length).toBe(2);
|
|
94
|
+
expect(body.textContent).not.toContain('b');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// -- clear() --------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
describe('clear()', () => {
|
|
101
|
+
it('removes all elements', () => {
|
|
102
|
+
const obs = items(['a', 'b', 'c']);
|
|
103
|
+
const anchor = ForEachArray(obs, (item) => Div({}, item));
|
|
104
|
+
const body = mount(anchor);
|
|
105
|
+
obs.clear();
|
|
106
|
+
expect(body.querySelectorAll('div').length).toBe(0);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// -- set() ----------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
describe('set()', () => {
|
|
113
|
+
it('replaces all elements', () => {
|
|
114
|
+
const obs = items(['a', 'b']);
|
|
115
|
+
const anchor = ForEachArray(obs, (item) => Div({}, item));
|
|
116
|
+
const body = mount(anchor);
|
|
117
|
+
obs.set(['x', 'y', 'z']);
|
|
118
|
+
const divs = body.querySelectorAll('div');
|
|
119
|
+
expect(divs.length).toBe(3);
|
|
120
|
+
expect(divs[0].textContent).toBe('x');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// -- swap() ---------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
describe('swap()', () => {
|
|
127
|
+
it('swaps two items in the DOM', () => {
|
|
128
|
+
const obs = items(['a', 'b', 'c']);
|
|
129
|
+
const anchor = ForEachArray(obs, (item) => Div({}, item));
|
|
130
|
+
const body = mount(anchor);
|
|
131
|
+
obs.swap(0, 2);
|
|
132
|
+
const divs = body.querySelectorAll('div');
|
|
133
|
+
expect(divs[0].textContent).toBe('c');
|
|
134
|
+
expect(divs[2].textContent).toBe('a');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// -- index observable -----------------------------------------------------
|
|
139
|
+
|
|
140
|
+
describe('index observable', () => {
|
|
141
|
+
it('passes index observable when callback has 2 params', () => {
|
|
142
|
+
const obs = items(['a', 'b', 'c']);
|
|
143
|
+
const indices = [];
|
|
144
|
+
const anchor = ForEachArray(obs, (item, index) => {
|
|
145
|
+
indices.push(index);
|
|
146
|
+
return Div({}, item);
|
|
147
|
+
});
|
|
148
|
+
mount(anchor);
|
|
149
|
+
expect(indices.length).toBe(3);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// -- order preservation ---------------------------------------------------
|
|
154
|
+
|
|
155
|
+
describe('order preservation', () => {
|
|
156
|
+
it('preserves order after multiple mutations', () => {
|
|
157
|
+
const obs = items(['a', 'b', 'c']);
|
|
158
|
+
const anchor = ForEachArray(obs, (item) => Div({}, item));
|
|
159
|
+
const body = mount(anchor);
|
|
160
|
+
obs.push('d');
|
|
161
|
+
obs.removeItem('b');
|
|
162
|
+
const divs = body.querySelectorAll('div');
|
|
163
|
+
expect([...divs].map((d) => d.textContent)).toEqual(['a', 'c', 'd']);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { Observable } from '../../../src/core/data/Observable';
|
|
3
|
+
|
|
4
|
+
vi.mock('../../../src/core/utils/debug-manager', () => ({
|
|
5
|
+
default: { log: vi.fn(), warn: vi.fn(), error: vi.fn(), disable: vi.fn() },
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
import { Div } from '../../../elements';
|
|
9
|
+
|
|
10
|
+
// Wait for MutationObserver to fire
|
|
11
|
+
const tick = () => new Promise((r) => setTimeout(r, 0));
|
|
12
|
+
|
|
13
|
+
// Helper - returns the HTMLElement and a stable .nd reference
|
|
14
|
+
const mkEl = (text = 'content') => {
|
|
15
|
+
const el = Div({}, text);
|
|
16
|
+
return { el, nd: el.nd };
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe('Lifecycle', () => {
|
|
20
|
+
|
|
21
|
+
// -- mounted() ------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
describe('mounted()', () => {
|
|
24
|
+
it('callback is called when element is inserted into DOM', async () => {
|
|
25
|
+
const { el, nd } = mkEl('hello');
|
|
26
|
+
const cb = vi.fn();
|
|
27
|
+
nd.mounted(cb);
|
|
28
|
+
document.body.innerHTML = '';
|
|
29
|
+
document.body.appendChild(el);
|
|
30
|
+
await tick();
|
|
31
|
+
expect(cb).toHaveBeenCalledOnce();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('callback is not called before insertion', async () => {
|
|
35
|
+
const { nd } = mkEl('hello');
|
|
36
|
+
const cb = vi.fn();
|
|
37
|
+
nd.mounted(cb);
|
|
38
|
+
await tick();
|
|
39
|
+
expect(cb).not.toHaveBeenCalled();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('multiple mounted callbacks are all called', async () => {
|
|
43
|
+
const { el, nd } = mkEl('hello');
|
|
44
|
+
const cb1 = vi.fn();
|
|
45
|
+
const cb2 = vi.fn();
|
|
46
|
+
nd.mounted(cb1);
|
|
47
|
+
nd.mounted(cb2);
|
|
48
|
+
document.body.innerHTML = '';
|
|
49
|
+
document.body.appendChild(el);
|
|
50
|
+
await tick();
|
|
51
|
+
expect(cb1).toHaveBeenCalledOnce();
|
|
52
|
+
expect(cb2).toHaveBeenCalledOnce();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('lifecycle({ mounted }) shorthand works', async () => {
|
|
56
|
+
const { el, nd } = mkEl('hello');
|
|
57
|
+
const cb = vi.fn();
|
|
58
|
+
nd.lifecycle({ mounted: cb });
|
|
59
|
+
document.body.innerHTML = '';
|
|
60
|
+
document.body.appendChild(el);
|
|
61
|
+
await tick();
|
|
62
|
+
expect(cb).toHaveBeenCalledOnce();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// -- unmounted() ----------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
describe('unmounted()', () => {
|
|
69
|
+
it('callback is called when element is removed from DOM', async () => {
|
|
70
|
+
const { el, nd } = mkEl('hello');
|
|
71
|
+
const cb = vi.fn();
|
|
72
|
+
nd.unmounted(cb);
|
|
73
|
+
document.body.innerHTML = '';
|
|
74
|
+
document.body.appendChild(el);
|
|
75
|
+
await tick();
|
|
76
|
+
el.remove();
|
|
77
|
+
await tick();
|
|
78
|
+
expect(cb).toHaveBeenCalledOnce();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('callback is not called before removal', async () => {
|
|
82
|
+
const { el, nd } = mkEl('hello');
|
|
83
|
+
const cb = vi.fn();
|
|
84
|
+
nd.unmounted(cb);
|
|
85
|
+
document.body.innerHTML = '';
|
|
86
|
+
document.body.appendChild(el);
|
|
87
|
+
await tick();
|
|
88
|
+
expect(cb).not.toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('lifecycle({ unmounted }) shorthand works', async () => {
|
|
92
|
+
const { el, nd } = mkEl('hello');
|
|
93
|
+
const cb = vi.fn();
|
|
94
|
+
nd.lifecycle({ unmounted: cb });
|
|
95
|
+
document.body.innerHTML = '';
|
|
96
|
+
document.body.appendChild(el);
|
|
97
|
+
await tick();
|
|
98
|
+
el.remove();
|
|
99
|
+
await tick();
|
|
100
|
+
expect(cb).toHaveBeenCalledOnce();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// -- destroy() ------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
describe('destroy()', () => {
|
|
107
|
+
it('nulls the internal $element reference', () => {
|
|
108
|
+
const { nd } = mkEl('hello');
|
|
109
|
+
nd.destroy();
|
|
110
|
+
expect(nd.$element).toBeNull();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('does not throw when called multiple times', () => {
|
|
114
|
+
const { nd } = mkEl('hello');
|
|
115
|
+
nd.destroy();
|
|
116
|
+
expect(() => nd.destroy()).not.toThrow();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('unsubscribes from lifecycle callbacks after destroy', async () => {
|
|
120
|
+
const { el, nd } = mkEl('hello');
|
|
121
|
+
const cb = vi.fn();
|
|
122
|
+
nd.mounted(cb);
|
|
123
|
+
nd.destroy();
|
|
124
|
+
document.body.innerHTML = '';
|
|
125
|
+
document.body.appendChild(el);
|
|
126
|
+
await tick();
|
|
127
|
+
expect(cb).not.toHaveBeenCalled();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// -- destroyOnUnmount() ---------------------------------------------------
|
|
132
|
+
|
|
133
|
+
describe('destroyOnUnmount()', () => {
|
|
134
|
+
it('nulls $element when element is removed from DOM', async () => {
|
|
135
|
+
const { el, nd } = mkEl('hello');
|
|
136
|
+
nd.destroyOnUnmount();
|
|
137
|
+
document.body.innerHTML = '';
|
|
138
|
+
document.body.appendChild(el);
|
|
139
|
+
await tick();
|
|
140
|
+
el.remove();
|
|
141
|
+
await tick();
|
|
142
|
+
expect(nd.$element).toBeNull();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { Observable } from '../../../src/core/data/Observable';
|
|
3
|
+
|
|
4
|
+
vi.mock('../../../src/core/utils/debug-manager', () => ({
|
|
5
|
+
default: { log: vi.fn(), warn: vi.fn(), error: vi.fn(), disable: vi.fn() },
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
// Initialize NDElement prototypes (toNdElement, nd, etc.)
|
|
9
|
+
import { Div, ShowIf, HideIf, HideIfNot } from '../../../elements';
|
|
10
|
+
|
|
11
|
+
// Helper - create a NativeDocument Div element (requires toNdElement prototype)
|
|
12
|
+
const el = (text = 'content') => Div({}, text);
|
|
13
|
+
|
|
14
|
+
// Mount anchor into the document to enable DOM queries
|
|
15
|
+
const mount = (anchor) => {
|
|
16
|
+
document.body.innerHTML = '';
|
|
17
|
+
document.body.appendChild(anchor);
|
|
18
|
+
return document.body;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe('ShowIf', () => {
|
|
22
|
+
|
|
23
|
+
// -- static boolean -------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
describe('static boolean condition', () => {
|
|
26
|
+
it('returns the element when condition is true', () => {
|
|
27
|
+
const result = ShowIf(true, el('hello'));
|
|
28
|
+
expect(result).not.toBeNull();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('returns null when condition is false', () => {
|
|
32
|
+
const result = ShowIf(false, el('hello'));
|
|
33
|
+
expect(result).toBeNull();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// -- observable condition -------------------------------------------------
|
|
38
|
+
|
|
39
|
+
describe('observable condition', () => {
|
|
40
|
+
it('renders content when condition is initially true', () => {
|
|
41
|
+
const visible = Observable(true);
|
|
42
|
+
const anchor = ShowIf(visible, el('hello'));
|
|
43
|
+
const body = mount(anchor);
|
|
44
|
+
expect(body.textContent).toContain('hello');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('does not render content when condition is initially false', () => {
|
|
48
|
+
const visible = Observable(false);
|
|
49
|
+
const anchor = ShowIf(visible, el('hello'));
|
|
50
|
+
const body = mount(anchor);
|
|
51
|
+
expect(body.textContent).not.toContain('hello');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('mounts content when observable changes to true', () => {
|
|
55
|
+
const visible = Observable(false);
|
|
56
|
+
const anchor = ShowIf(visible, el('hello'));
|
|
57
|
+
const body = mount(anchor);
|
|
58
|
+
visible.set(true);
|
|
59
|
+
expect(body.textContent).toContain('hello');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('unmounts content when observable changes to false', () => {
|
|
63
|
+
const visible = Observable(true);
|
|
64
|
+
const anchor = ShowIf(visible, el('hello'));
|
|
65
|
+
const body = mount(anchor);
|
|
66
|
+
visible.set(false);
|
|
67
|
+
expect(body.textContent).not.toContain('hello');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('remounts content on second true', () => {
|
|
71
|
+
const visible = Observable(true);
|
|
72
|
+
const anchor = ShowIf(visible, el('hello'));
|
|
73
|
+
const body = mount(anchor);
|
|
74
|
+
visible.set(false);
|
|
75
|
+
visible.set(true);
|
|
76
|
+
expect(body.textContent).toContain('hello');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// -- factory child --------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
describe('factory child', () => {
|
|
83
|
+
it('accepts a function as child', () => {
|
|
84
|
+
const visible = Observable(true);
|
|
85
|
+
const factory = () => el('from factory');
|
|
86
|
+
const anchor = ShowIf(visible, factory);
|
|
87
|
+
const body = mount(anchor);
|
|
88
|
+
expect(body.textContent).toContain('from factory');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('shouldKeepInCache=false re-creates element on each mount', () => {
|
|
92
|
+
let callCount = 0;
|
|
93
|
+
const visible = Observable(false);
|
|
94
|
+
const factory = () => { callCount++; return el('dynamic'); };
|
|
95
|
+
ShowIf(visible, factory, { shouldKeepInCache: false });
|
|
96
|
+
visible.set(true);
|
|
97
|
+
visible.set(false);
|
|
98
|
+
visible.set(true);
|
|
99
|
+
expect(callCount).toBe(2);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('shouldKeepInCache=true (default) creates element only once', () => {
|
|
103
|
+
let callCount = 0;
|
|
104
|
+
const visible = Observable(false);
|
|
105
|
+
const factory = () => { callCount++; return el('cached'); };
|
|
106
|
+
ShowIf(visible, factory, { shouldKeepInCache: true });
|
|
107
|
+
visible.set(true);
|
|
108
|
+
visible.set(false);
|
|
109
|
+
visible.set(true);
|
|
110
|
+
expect(callCount).toBe(1);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// -- non-observable condition ---------------------------------------------
|
|
115
|
+
|
|
116
|
+
describe('non-observable condition', () => {
|
|
117
|
+
it('does not throw for invalid condition type', () => {
|
|
118
|
+
// DebugManager.warn is already mocked via vi.mock at the top
|
|
119
|
+
expect(() => ShowIf('invalid', el())).not.toThrow();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// -- HideIf ------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
describe('HideIf', () => {
|
|
128
|
+
it('hides content when condition is true', () => {
|
|
129
|
+
const flag = Observable(true);
|
|
130
|
+
const anchor = HideIf(flag, el('hidden'));
|
|
131
|
+
const body = mount(anchor);
|
|
132
|
+
expect(body.textContent).not.toContain('hidden');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('shows content when condition is false', () => {
|
|
136
|
+
const flag = Observable(false);
|
|
137
|
+
const anchor = HideIf(flag, el('visible'));
|
|
138
|
+
const body = mount(anchor);
|
|
139
|
+
expect(body.textContent).toContain('visible');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('reacts when condition changes', () => {
|
|
143
|
+
const flag = Observable(false);
|
|
144
|
+
const anchor = HideIf(flag, el('content'));
|
|
145
|
+
const body = mount(anchor);
|
|
146
|
+
expect(body.textContent).toContain('content');
|
|
147
|
+
flag.set(true);
|
|
148
|
+
expect(body.textContent).not.toContain('content');
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// -- HideIfNot ---------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
describe('HideIfNot', () => {
|
|
155
|
+
it('shows content when condition is true (same as ShowIf)', () => {
|
|
156
|
+
const visible = Observable(true);
|
|
157
|
+
const anchor = HideIfNot(visible, el('shown'));
|
|
158
|
+
const body = mount(anchor);
|
|
159
|
+
expect(body.textContent).toContain('shown');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('hides content when condition is false', () => {
|
|
163
|
+
const visible = Observable(false);
|
|
164
|
+
const anchor = HideIfNot(visible, el('shown'));
|
|
165
|
+
const body = mount(anchor);
|
|
166
|
+
expect(body.textContent).not.toContain('shown');
|
|
167
|
+
});
|
|
168
|
+
});
|