@node-projects/web-component-designer-visualization-addons 0.1.152 → 0.3.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.
@@ -0,0 +1,167 @@
1
+ /** @jest-environment jsdom */
2
+ import { afterEach, beforeAll, expect, jest, test } from '@jest/globals';
3
+ import { BindingTarget } from '@node-projects/web-component-designer/dist/elements/item/BindingTarget.js';
4
+
5
+ Object.defineProperty(window, 'matchMedia', { value: () => ({ matches: false }) });
6
+ CSSStyleSheet.prototype.replaceSync = function () { };
7
+ Object.defineProperty(ShadowRoot.prototype, 'adoptedStyleSheets', { writable: true, value: [] });
8
+ Object.defineProperty(globalThis, 'ResizeObserver', { value: class { observe() { } disconnect() { } unobserve() { } } });
9
+ Element.prototype.scrollTo = function () { };
10
+ HTMLDialogElement.prototype.showModal = function () { this.open = true; };
11
+ HTMLDialogElement.prototype.close = function () { this.open = false; this.dispatchEvent(new Event('close')); };
12
+
13
+ let EditingDocument: typeof import('@node-projects/web-component-designer/dist/elements/EditingDocument.js').EditingDocument;
14
+ let ServiceContainer: typeof import('@node-projects/web-component-designer/dist/elements/services/ServiceContainer.js').ServiceContainer;
15
+ let DefaultHtmlParserService: typeof import('@node-projects/web-component-designer/dist/elements/services/htmlParserService/DefaultHtmlParserService.js').DefaultHtmlParserService;
16
+ let HtmlWriterService: typeof import('@node-projects/web-component-designer/dist/elements/services/htmlWriterService/HtmlWriterService.js').HtmlWriterService;
17
+ let DesignItemService: typeof import('@node-projects/web-component-designer/dist/elements/services/designItemService/DesignItemService.js').DesignItemService;
18
+ let BindingService: typeof import('@node-projects/web-component-designer/dist/elements/services/bindingsService/BaseCustomWebcomponentBindingsService.js').BaseCustomWebcomponentBindingsService;
19
+ let ClassService: typeof import('../src/components/ClassBindingsPropertiesService').ClassBindingsPropertiesService;
20
+ let ClassGrid: typeof import('../src/components/PropertyGridClassBindings').PropertyGridClassBindings;
21
+ let PropertyList: typeof import('@node-projects/web-component-designer/dist/elements/widgets/propertyGrid/PropertyGridPropertyList.js').PropertyGridPropertyList;
22
+ let CommonPropertiesService: typeof import('@node-projects/web-component-designer/dist/elements/services/propertiesService/services/CommonPropertiesService.js').CommonPropertiesService;
23
+ let DefaultPropertyEditorTypesService: typeof import('@node-projects/web-component-designer/dist/elements/services/propertiesService/DefaultPropertyEditorTypesService.js').DefaultPropertyEditorTypesService;
24
+
25
+ beforeAll(async () => {
26
+ ({ EditingDocument } = await import('@node-projects/web-component-designer/dist/elements/EditingDocument.js'));
27
+ ({ ServiceContainer } = await import('@node-projects/web-component-designer/dist/elements/services/ServiceContainer.js'));
28
+ ({ DefaultHtmlParserService } = await import('@node-projects/web-component-designer/dist/elements/services/htmlParserService/DefaultHtmlParserService.js'));
29
+ ({ HtmlWriterService } = await import('@node-projects/web-component-designer/dist/elements/services/htmlWriterService/HtmlWriterService.js'));
30
+ ({ DesignItemService } = await import('@node-projects/web-component-designer/dist/elements/services/designItemService/DesignItemService.js'));
31
+ ({ BaseCustomWebcomponentBindingsService: BindingService } = await import('@node-projects/web-component-designer/dist/elements/services/bindingsService/BaseCustomWebcomponentBindingsService.js'));
32
+ ({ ClassBindingsPropertiesService: ClassService } = await import('../src/components/ClassBindingsPropertiesService'));
33
+ ({ PropertyGridClassBindings: ClassGrid } = await import('../src/components/PropertyGridClassBindings'));
34
+ ({ PropertyGridPropertyList: PropertyList } = await import('@node-projects/web-component-designer/dist/elements/widgets/propertyGrid/PropertyGridPropertyList.js'));
35
+ ({ CommonPropertiesService } = await import('@node-projects/web-component-designer/dist/elements/services/propertiesService/services/CommonPropertiesService.js'));
36
+ ({ DefaultPropertyEditorTypesService } = await import('@node-projects/web-component-designer/dist/elements/services/propertiesService/DefaultPropertyEditorTypesService.js'));
37
+ });
38
+
39
+ afterEach(() => document.body.replaceChildren());
40
+
41
+ async function fixture(html = '<button class="button primary" class:is-active="[[isActive]]">Submit</button>') {
42
+ const services = new ServiceContainer();
43
+ services.register('designItemService', new DesignItemService());
44
+ services.register('htmlParserService', new DefaultHtmlParserService());
45
+ services.register('htmlWriterService', new HtmlWriterService());
46
+ services.register('bindingService', new BindingService());
47
+ services.register('propertyEditorTypesService', new DefaultPropertyEditorTypesService());
48
+ services.config.demoViewWidget = null;
49
+ const doc = new EditingDocument(services);
50
+ await doc.loadHtml(html);
51
+ const item = doc.rootDesignItem.firstChild;
52
+ return { services, doc, item, bindingService: services.bindingService };
53
+ }
54
+
55
+ test('renames and removes class bindings as individual undoable operations', async () => {
56
+ const { doc, item } = await fixture();
57
+ const service = new ClassService();
58
+ const binding = service.getBindings(item)[0];
59
+ service.rename(item, binding, 'is-ready');
60
+ expect(item.hasAttribute('class:is-active')).toBe(false);
61
+ expect(item.getAttribute('class:is-ready')).toBe('[[isActive]]');
62
+ doc.instanceServiceContainer.undoService.undo();
63
+ expect(item.getAttribute('class:is-active')).toBe('[[isActive]]');
64
+ expect(item.hasAttribute('class:is-ready')).toBe(false);
65
+ doc.instanceServiceContainer.undoService.redo();
66
+ service.remove(item, service.getBindings(item)[0]);
67
+ expect(service.getBindings(item)).toHaveLength(0);
68
+ doc.instanceServiceContainer.undoService.undo();
69
+ expect(item.getAttribute('class:is-ready')).toBe('[[isActive]]');
70
+ expect(item.getAttribute('class')).toBe('button primary');
71
+ await doc.dispose();
72
+ });
73
+
74
+ test('validates lowercase HTML-safe names and rejects duplicate targets', async () => {
75
+ const { doc, item } = await fixture();
76
+ const service = new ClassService();
77
+ for (const name of ['', 'myClassName', 'two classes', 'bad"name', 'bad=name', 'bad/name'])
78
+ expect(service.validateName(item, name)).toBeTruthy();
79
+ expect(service.validateName(item, 'is-active')).toContain('already');
80
+ expect(service.validateName(item, 'is-active', 'is-active')).toBeNull();
81
+ expect(service.validateName(item, 'has-error')).toBeNull();
82
+ await doc.dispose();
83
+ });
84
+
85
+ test('source-created bindings appear under class and open the host editor with a class target', async () => {
86
+ const { services, doc, item } = await fixture();
87
+ const openEditor = jest.fn(async () => { });
88
+ services.config.openBindingsEditor = openEditor;
89
+ const { ClassBindingsPropertyGridExtensionProvider } = await import('../src/components/ClassBindingsPropertyGridExtensionProvider');
90
+ services.propertyGridExtensions.push(new ClassBindingsPropertyGridExtensionProvider());
91
+ const list = new PropertyList(services);
92
+ document.body.appendChild(list);
93
+ list.setPropertiesService(new CommonPropertiesService());
94
+ await list.createElements(item);
95
+ list.designItemsChanged([item]);
96
+ const section = list.shadowRoot.querySelector('node-projects-property-grid-class-bindings') as InstanceType<typeof ClassGrid>;
97
+ expect(section.previousElementSibling.querySelector('.editor-control').id).toBe('class');
98
+ expect(section.shadowRoot.textContent).toContain('is-active');
99
+ (section.shadowRoot.querySelector('.name') as HTMLButtonElement).click();
100
+ await Promise.resolve();
101
+ expect(openEditor).toHaveBeenCalledWith(expect.objectContaining({ name: 'is-active', type: 'boolean' }), [item], expect.objectContaining({ targetName: 'is-active' }), BindingTarget.class);
102
+ await doc.dispose();
103
+ });
104
+
105
+ test('addition asks only for a class name then delegates to the configured binding editor', async () => {
106
+ const { services, doc, item } = await fixture();
107
+ const openEditor = jest.fn(async () => {});
108
+ services.config.openBindingsEditor = openEditor;
109
+ const grid = new ClassGrid();
110
+ document.body.appendChild(grid);
111
+ grid.refresh([item]);
112
+ grid.addBinding();
113
+ const form = grid.shadowRoot.querySelector('form');
114
+ expect(form.querySelectorAll('input')).toHaveLength(1);
115
+ (form.querySelector('input') as HTMLInputElement).value = 'is-busy';
116
+ form.dispatchEvent(new Event('submit', { cancelable: true }));
117
+ await Promise.resolve();
118
+ expect(openEditor).toHaveBeenCalledWith(expect.objectContaining({ name: 'is-busy', type: 'boolean' }), [item], undefined, BindingTarget.class);
119
+ expect(item.hasAttribute('class:is-busy')).toBe(false);
120
+ await doc.dispose();
121
+ });
122
+
123
+ test('canceling addition leaves the document unchanged and multiple selection disables addition', async () => {
124
+ const { services, doc, item } = await fixture();
125
+ services.config.openBindingsEditor = async () => {};
126
+ const original = await doc.serialize();
127
+ const grid = new ClassGrid();
128
+ document.body.appendChild(grid);
129
+ grid.refresh([item]);
130
+ grid.addBinding();
131
+ (grid.shadowRoot.querySelector('.actions button[type="button"]') as HTMLButtonElement).click();
132
+ expect(await doc.serialize()).toBe(original);
133
+ grid.refresh([item, item]);
134
+ expect((grid.shadowRoot.querySelector('[aria-label="Add class binding"]') as HTMLButtonElement).disabled).toBe(true);
135
+ expect(grid.shadowRoot.textContent).toContain('Select one element');
136
+ await doc.dispose();
137
+ });
138
+
139
+ test('failed renames roll back the new attribute instead of leaving two bindings', async () => {
140
+ const { doc, item, bindingService } = await fixture();
141
+ const service = new ClassService();
142
+ const clear = jest.spyOn(bindingService, 'clearBinding').mockReturnValue(false);
143
+ expect(() => service.rename(item, service.getBindings(item)[0], 'is-ready')).toThrow('previous binding');
144
+ expect(item.hasAttribute('class:is-ready')).toBe(false);
145
+ expect(item.getAttribute('class:is-active')).toBe('[[isActive]]');
146
+ clear.mockRestore();
147
+ await doc.dispose();
148
+ });
149
+
150
+ test('editing after a source refresh uses changed binding options even if the summary is unchanged', async () => {
151
+ const { services, doc, item, bindingService } = await fixture();
152
+ const binding = bindingService.getBindings(item)[0];
153
+ let invert = false;
154
+ const read = jest.spyOn(bindingService, 'getBindings').mockImplementation(() => [{ ...binding, invert }]);
155
+ const openEditor = jest.fn(async () => { });
156
+ services.config.openBindingsEditor = openEditor;
157
+ const grid = new ClassGrid();
158
+ document.body.appendChild(grid);
159
+ grid.refresh([item]);
160
+ invert = true;
161
+ grid.refresh([item]);
162
+ (grid.shadowRoot.querySelector('.name') as HTMLButtonElement).click();
163
+ await Promise.resolve();
164
+ expect(openEditor).toHaveBeenCalledWith(expect.anything(), [item], expect.objectContaining({ invert: true }), BindingTarget.class);
165
+ read.mockRestore();
166
+ await doc.dispose();
167
+ });