@node-projects/web-component-designer-visualization-addons 0.1.151 → 0.3.0

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.
@@ -1,3 +1,19 @@
1
+ const sourcePrefixes = [
2
+ { prefix: '?@', itemType: 'attribute' },
3
+ { prefix: '#@', itemType: 'attribute' },
4
+ { prefix: '?$', itemType: 'signalObject' },
5
+ { prefix: '#$', itemType: 'signalObject' },
6
+ { prefix: '??', itemType: 'property' },
7
+ { prefix: '##', itemType: 'property' },
8
+ { prefix: '?', itemType: 'property' },
9
+ { prefix: '#', itemType: 'property' }
10
+ ];
11
+ function parseSource(source) {
12
+ const match = sourcePrefixes.find(x => source.startsWith(x.prefix));
13
+ return match
14
+ ? { name: source.substring(match.prefix.length), ...match }
15
+ : { name: source, prefix: '', itemType: 'signal' };
16
+ }
1
17
  export class VisualizationBindingsRefactorService {
2
18
  getRefactorings(designItems) {
3
19
  let refactorings = [];
@@ -6,33 +22,14 @@ export class VisualizationBindingsRefactorService {
6
22
  if (bindings) {
7
23
  for (let b of bindings) {
8
24
  for (let s of b.bindableObjectNames) {
9
- let itemType = 'signal';
10
- let prefix = "";
11
25
  if (s.includes(':')) {
12
26
  let nm = s.split(':')[0];
13
- let sng = s.substring(nm.length + 1);
14
- if (sng.startsWith('?')) {
15
- sng = sng.substring(1);
16
- prefix = '?';
17
- itemType = 'property';
18
- if (sng.startsWith('?')) {
19
- sng = sng.substring(1);
20
- prefix = '??';
21
- }
22
- }
23
- refactorings.push({ service: this, name: sng, itemType: itemType, designItem: d, type: 'binding', sourceObject: b, display: b.target + '/' + b.targetName + ' - ' + nm + ':', shortName: nm, prefix: prefix });
27
+ const source = parseSource(s.substring(nm.length + 1));
28
+ refactorings.push({ service: this, name: source.name, itemType: source.itemType, designItem: d, type: 'binding', sourceObject: b, display: b.target + '/' + b.targetName + ' - ' + nm + ':', shortName: nm, prefix: source.prefix });
24
29
  }
25
30
  else {
26
- if (s.startsWith('?')) {
27
- s = s.substring(1);
28
- prefix = '?';
29
- itemType = 'property';
30
- if (s.startsWith('?')) {
31
- s = s.substring(1);
32
- prefix = '??';
33
- }
34
- }
35
- refactorings.push({ service: this, name: s, itemType: itemType, designItem: d, type: 'binding', sourceObject: b, display: b.target + '/' + b.targetName, prefix: prefix });
31
+ const source = parseSource(s);
32
+ refactorings.push({ service: this, name: source.name, itemType: source.itemType, designItem: d, type: 'binding', sourceObject: b, display: b.target + '/' + b.targetName, prefix: source.prefix });
36
33
  }
37
34
  }
38
35
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "description": "web-component-designer addon for visualizations",
3
3
  "name": "@node-projects/web-component-designer-visualization-addons",
4
- "version": "0.1.151",
4
+ "version": "0.3.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "author": "jochen.kuehner@gmx.de",
@@ -0,0 +1,527 @@
1
+ /** @jest-environment jsdom */
2
+
3
+ import { beforeAll, describe, expect, test } from '@jest/globals';
4
+ import type { SpecialValueHandler } from '../src/helpers/BindingsHelper.js';
5
+ import type { VisualizationHandler, State } from '../src/interfaces/VisualizationHandler.js';
6
+ import { BindingTarget } from '@node-projects/web-component-designer/dist/elements/item/BindingTarget.js';
7
+
8
+ let BindingsHelper: typeof import('../src/helpers/BindingsHelper.js').BindingsHelper;
9
+ let parseBindingString: typeof import('../src/helpers/BindingsHelper.js').parseBindingString;
10
+ let VisualizationBindingsRefactorService: typeof import('../src/services/VisualizationBindingsRefactorService.js').VisualizationBindingsRefactorService;
11
+
12
+ beforeAll(async () => {
13
+ if (!CSSStyleSheet.prototype.replaceSync)
14
+ CSSStyleSheet.prototype.replaceSync = () => { };
15
+ ({ BindingsHelper, parseBindingString } = await import('../src/helpers/BindingsHelper.js'));
16
+ ({ VisualizationBindingsRefactorService } = await import('../src/services/VisualizationBindingsRefactorService.js'));
17
+ });
18
+
19
+ class TestVisualizationHandler implements VisualizationHandler {
20
+ values = new Map<string, any>();
21
+ callbacks = new Map<string, Set<(id: string, state: State) => void>>();
22
+ subscribeCalls: string[] = [];
23
+ unsubscribeCalls: string[] = [];
24
+ getStateCalls: string[] = [];
25
+ setStateCalls: { id: string, value: any, ack?: boolean }[] = [];
26
+ objects = new Map<string, any>();
27
+ getObjectCalls: string[] = [];
28
+
29
+ getNormalizedSignalName(id: string, relativeSignalPath = '') {
30
+ return relativeSignalPath + id;
31
+ }
32
+
33
+ async getState(id: string): Promise<State> {
34
+ this.getStateCalls.push(id);
35
+ return { val: this.values.get(id) };
36
+ }
37
+
38
+ async setState(id: string, value: any, ack?: boolean): Promise<void> {
39
+ this.setStateCalls.push({ id, value, ack });
40
+ this.values.set(id, value);
41
+ }
42
+
43
+ subscribeState(id: string, callback: (id: string, state: State) => void) {
44
+ this.subscribeCalls.push(id);
45
+ let callbacks = this.callbacks.get(id);
46
+ if (!callbacks) {
47
+ callbacks = new Set();
48
+ this.callbacks.set(id, callbacks);
49
+ }
50
+ callbacks.add(callback);
51
+ return callback;
52
+ }
53
+
54
+ unsubscribeState(id: string, callback: (id: string, state: State) => void) {
55
+ this.unsubscribeCalls.push(id);
56
+ this.callbacks.get(id)?.delete(callback);
57
+ }
58
+
59
+ emit(id: string, value: any) {
60
+ this.values.set(id, value);
61
+ for (const callback of this.callbacks.get(id) ?? [])
62
+ callback(id, { val: value });
63
+ }
64
+
65
+ async getHistoricData() {
66
+ return { values: [] };
67
+ }
68
+
69
+ async getObject(id: string): Promise<any> {
70
+ this.getObjectCalls.push(id);
71
+ return this.objects.get(id);
72
+ }
73
+
74
+ getAllNames() {
75
+ return [];
76
+ }
77
+
78
+ getSignalInformation(): any {
79
+ return null;
80
+ }
81
+ }
82
+
83
+ const flushMicrotasks = async () => {
84
+ await Promise.resolve();
85
+ await Promise.resolve();
86
+ await Promise.resolve();
87
+ await Promise.resolve();
88
+ };
89
+
90
+ describe('BindingsHelper runtime', () => {
91
+ test('shares subscriptions and initial reads for the same signal', async () => {
92
+ const handler = new TestVisualizationHandler();
93
+ handler.values.set('shared', 7);
94
+ const helper = new BindingsHelper(handler);
95
+ const first = document.createElement('div');
96
+ const second = document.createElement('div');
97
+
98
+ const disposeFirst = helper.applyBinding(first, ['value', { signal: 'shared', target: BindingTarget.property }], '', null);
99
+ const disposeSecond = helper.applyBinding(second, ['value', { signal: 'shared', target: BindingTarget.property }], '', null);
100
+ await flushMicrotasks();
101
+
102
+ expect(handler.subscribeCalls).toEqual(['shared']);
103
+ expect(handler.getStateCalls).toEqual(['shared']);
104
+ expect(first['value']).toBe(7);
105
+ expect(second['value']).toBe(7);
106
+
107
+ disposeFirst();
108
+ expect(handler.unsubscribeCalls).toEqual([]);
109
+ disposeSecond();
110
+ expect(handler.unsubscribeCalls).toEqual(['shared']);
111
+ });
112
+
113
+ test('switches only the changed dynamic source', async () => {
114
+ const handler = new TestVisualizationHandler();
115
+ handler.values.set('first', 1);
116
+ handler.values.set('second', 2);
117
+ handler.values.set('fixed', 10);
118
+ const helper = new BindingsHelper(handler);
119
+ const root = document.createElement('div');
120
+ const element = document.createElement('div');
121
+ root['signalName'] = 'first';
122
+
123
+ const dispose = helper.applyBinding(element, ['value', {
124
+ signal: 'dynamic:?signalName;fixed:fixed',
125
+ expression: 'dynamic + fixed',
126
+ twoWay: true,
127
+ events: ['change'],
128
+ target: BindingTarget.property
129
+ }], '', root);
130
+ await flushMicrotasks();
131
+ expect(element['value']).toBe(11);
132
+
133
+ root['signalName'] = 'second';
134
+ root.dispatchEvent(new Event('signal-name-changed'));
135
+ await flushMicrotasks();
136
+
137
+ expect(element['value']).toBe(12);
138
+ expect(handler.subscribeCalls).toEqual(['first', 'fixed', 'second']);
139
+ expect(handler.unsubscribeCalls).toEqual(['first']);
140
+
141
+ root.dispatchEvent(new Event('signal-name-changed'));
142
+ await flushMicrotasks();
143
+ expect(handler.subscribeCalls).toEqual(['first', 'fixed', 'second']);
144
+ expect(handler.unsubscribeCalls).toEqual(['first']);
145
+
146
+ element['value'] = 20;
147
+ element.dispatchEvent(new Event('change'));
148
+ expect(handler.setStateCalls).toEqual([{ id: 'second', value: 20, ack: undefined }]);
149
+
150
+ dispose();
151
+ expect(handler.unsubscribeCalls).toEqual(['first', 'fixed', 'second']);
152
+ });
153
+
154
+ test('switches an indirect property source without rebuilding its binding', async () => {
155
+ const handler = new TestVisualizationHandler();
156
+ handler.values.set('selector-one', 'one');
157
+ handler.values.set('selector-two', 'two');
158
+ handler.values.set('device.one', 1);
159
+ handler.values.set('device.two', 2);
160
+ const helper = new BindingsHelper(handler);
161
+ const root = document.createElement('div');
162
+ const element = document.createElement('div');
163
+ root['selectorSignal'] = 'selector-one';
164
+
165
+ const dispose = helper.applyBinding(element, ['value', {
166
+ signal: 'device.{?selectorSignal}',
167
+ target: BindingTarget.property
168
+ }], '', root);
169
+ await flushMicrotasks();
170
+ expect(element['value']).toBe(1);
171
+
172
+ root['selectorSignal'] = 'selector-two';
173
+ root.dispatchEvent(new Event('selector-signal-changed'));
174
+ await flushMicrotasks();
175
+ expect(element['value']).toBe(2);
176
+ expect(handler.subscribeCalls).toEqual(['selector-one', 'device.one', 'selector-two', 'device.two']);
177
+ expect(handler.unsubscribeCalls).toEqual(['selector-one', 'device.one']);
178
+ dispose();
179
+ });
180
+
181
+ test('binds screen and target attributes and observes their changes', async () => {
182
+ const handler = new TestVisualizationHandler();
183
+ const helper = new BindingsHelper(handler);
184
+ const root = document.createElement('div');
185
+ const element = document.createElement('input');
186
+ root.setAttribute('screen-value', 'one');
187
+ element.setAttribute('target-value', 'two');
188
+
189
+ const disposeScreen = helper.applyBinding(element, ['screenValue', {
190
+ signal: '?@screen-value',
191
+ twoWay: true,
192
+ events: ['change'],
193
+ target: BindingTarget.property
194
+ }], '', root);
195
+ const disposeTarget = helper.applyBinding(element, ['targetValue', {
196
+ signal: '#@target-value',
197
+ target: BindingTarget.property
198
+ }], '', root);
199
+
200
+ expect(element['screenValue']).toBe('one');
201
+ expect(element['targetValue']).toBe('two');
202
+ expect(handler.subscribeCalls).toEqual([]);
203
+
204
+ root.setAttribute('screen-value', 'three');
205
+ element.setAttribute('target-value', 'four');
206
+ await flushMicrotasks();
207
+ expect(element['screenValue']).toBe('three');
208
+ expect(element['targetValue']).toBe('four');
209
+
210
+ element['screenValue'] = 'five';
211
+ element.dispatchEvent(new Event('change'));
212
+ expect(root.getAttribute('screen-value')).toBe('five');
213
+
214
+ disposeScreen();
215
+ disposeTarget();
216
+ root.setAttribute('screen-value', 'ignored');
217
+ element.setAttribute('target-value', 'ignored');
218
+ await flushMicrotasks();
219
+ expect(element['screenValue']).toBe('five');
220
+ expect(element['targetValue']).toBe('four');
221
+ });
222
+
223
+ test('binds signal objects stored in screen and target properties', async () => {
224
+ const handler = new TestVisualizationHandler();
225
+ handler.objects.set('named-object', { id: 'named-object' });
226
+ const helper = new BindingsHelper(handler);
227
+ const root = document.createElement('div');
228
+ const screenElement = document.createElement('div');
229
+ const targetElement = document.createElement('div');
230
+ const namedElement = document.createElement('div');
231
+ root['signalObject'] = { id: 'screen-object' };
232
+ targetElement['signalObject'] = { id: 'target-object' };
233
+
234
+ const disposeScreen = helper.applyBinding(screenElement, ['value', {
235
+ signal: '?$signalObject',
236
+ target: BindingTarget.property
237
+ }], '', root);
238
+ const disposeTarget = helper.applyBinding(targetElement, ['value', {
239
+ signal: '#$signalObject',
240
+ target: BindingTarget.property
241
+ }], '', root);
242
+ const disposeNamed = helper.applyBinding(namedElement, ['value', {
243
+ signal: '$named-object',
244
+ target: BindingTarget.property
245
+ }], '', root);
246
+ await flushMicrotasks();
247
+ expect(screenElement['value']).toEqual({ id: 'screen-object' });
248
+ expect(targetElement['value']).toEqual({ id: 'target-object' });
249
+ expect(namedElement['value']).toEqual({ id: 'named-object' });
250
+
251
+ root['signalObject'] = { id: 'next-object' };
252
+ root.dispatchEvent(new Event('signal-object-changed'));
253
+ await flushMicrotasks();
254
+ expect(screenElement['value']).toEqual({ id: 'next-object' });
255
+ expect(handler.getObjectCalls).toEqual(['named-object']);
256
+
257
+ disposeScreen();
258
+ disposeTarget();
259
+ disposeNamed();
260
+ });
261
+
262
+ test('uses screen and target attributes in indirect signal paths', async () => {
263
+ const handler = new TestVisualizationHandler();
264
+ handler.values.set('device.one', 1);
265
+ handler.values.set('device.two', 2);
266
+ handler.values.set('device.three', 3);
267
+ handler.values.set('device.four', 4);
268
+ const helper = new BindingsHelper(handler);
269
+ const root = document.createElement('div');
270
+ const screenElement = document.createElement('div');
271
+ const targetElement = document.createElement('div');
272
+ root.setAttribute('device-id', 'one');
273
+ targetElement.setAttribute('device-id', 'three');
274
+
275
+ const disposeScreen = helper.applyBinding(screenElement, ['value', {
276
+ signal: 'device.{?@device-id}',
277
+ target: BindingTarget.property
278
+ }], '', root);
279
+ const disposeTarget = helper.applyBinding(targetElement, ['value', {
280
+ signal: 'device.{#@device-id}',
281
+ target: BindingTarget.property
282
+ }], '', root);
283
+ await flushMicrotasks();
284
+ expect(screenElement['value']).toBe(1);
285
+ expect(targetElement['value']).toBe(3);
286
+
287
+ root.setAttribute('device-id', 'two');
288
+ targetElement.setAttribute('device-id', 'four');
289
+ await flushMicrotasks();
290
+ expect(screenElement['value']).toBe(2);
291
+ expect(targetElement['value']).toBe(4);
292
+ expect(handler.unsubscribeCalls).toEqual(['device.one', 'device.three']);
293
+
294
+ disposeScreen();
295
+ disposeTarget();
296
+ });
297
+
298
+ test('keeps serialization output stable while sharing target-specific logic', () => {
299
+ const helper = new BindingsHelper(new TestVisualizationHandler());
300
+ const element = document.createElement('div');
301
+
302
+ expect(helper.serializeBinding(element, 'value', { signal: 'source', target: BindingTarget.property }))
303
+ .toEqual(['bind-prop:value', 'source']);
304
+ expect(helper.serializeBinding(element, 'dataValue', { signal: 'source', target: BindingTarget.attribute }))
305
+ .toEqual(['bind-attr:data-value', 'source']);
306
+ expect(helper.serializeBinding(element, 'isActive', { signal: 'source', target: BindingTarget.class }))
307
+ .toEqual(['bind-class:is-active', 'source']);
308
+ expect(helper.serializeBinding(element, 'backgroundColor', { signal: 'source', target: BindingTarget.css }))
309
+ .toEqual(['bind-css:background-color', 'source']);
310
+ expect(helper.serializeBinding(element, '--mainColor', { signal: 'source', target: BindingTarget.cssvar }))
311
+ .toEqual(['bind-cssvar:main.color', 'source']);
312
+ expect(helper.serializeBinding(element, 'value', {
313
+ signal: 'source',
314
+ converter: { true: 'yes' },
315
+ target: BindingTarget.property
316
+ })).toEqual(['bind-prop:value', '{"signal":"source","converter":{"true":"yes"}}']);
317
+ });
318
+
319
+ test('coalesces bursts and skips identical primitive DOM and write-back updates', async () => {
320
+ const handler = new TestVisualizationHandler();
321
+ handler.values.set('sensor', 1);
322
+ const helper = new BindingsHelper(handler);
323
+ const element = document.createElement('div');
324
+ let writes = 0;
325
+ let renderedValue: any;
326
+ Object.defineProperty(element, 'value', {
327
+ configurable: true,
328
+ get: () => renderedValue,
329
+ set: value => {
330
+ writes++;
331
+ renderedValue = value;
332
+ }
333
+ });
334
+
335
+ const dispose = helper.applyBinding(element, ['value', {
336
+ signal: 'sensor',
337
+ writeBackSignal: 'mirror',
338
+ target: BindingTarget.property
339
+ }], '', null);
340
+ await flushMicrotasks();
341
+ expect(writes).toBe(1);
342
+ expect(handler.setStateCalls).toEqual([{ id: 'mirror', value: 1, ack: true }]);
343
+
344
+ handler.emit('sensor', 1);
345
+ handler.emit('sensor', 1);
346
+ await flushMicrotasks();
347
+ expect(writes).toBe(1);
348
+
349
+ handler.emit('sensor', 2);
350
+ handler.emit('sensor', 3);
351
+ await flushMicrotasks();
352
+ expect(writes).toBe(2);
353
+ expect(renderedValue).toBe(3);
354
+ expect(handler.setStateCalls).toHaveLength(2);
355
+ expect(handler.setStateCalls[1]).toEqual({ id: 'mirror', value: 3, ack: true });
356
+ dispose();
357
+ });
358
+
359
+ test('routes all target types through the shared target writer', async () => {
360
+ const handler = new TestVisualizationHandler();
361
+ handler.values.set('shared', 1);
362
+ const helper = new BindingsHelper(handler);
363
+ const propertyElement = document.createElement('div');
364
+ const attributeElement = document.createElement('div');
365
+ const classElement = document.createElement('div');
366
+ const cssElement = document.createElement('div');
367
+ const cssVarElement = document.createElement('div');
368
+ const visibleElement = document.createElement('div');
369
+ const disposers = [
370
+ helper.applyBinding(propertyElement, ['value', { signal: 'shared', target: BindingTarget.property }], '', null),
371
+ helper.applyBinding(attributeElement, ['data-value', { signal: 'shared', target: BindingTarget.attribute }], '', null),
372
+ helper.applyBinding(classElement, ['active', { signal: 'shared', target: BindingTarget.class }], '', null),
373
+ helper.applyBinding(cssElement, ['opacity', { signal: 'shared', target: BindingTarget.css }], '', null),
374
+ helper.applyBinding(cssVarElement, ['--amount', { signal: 'shared', target: BindingTarget.cssvar }], '', null),
375
+ helper.applyBinding(visibleElement, ['', { signal: 'shared', target: BindingTarget.visible }], '', null)
376
+ ];
377
+ await flushMicrotasks();
378
+
379
+ expect(propertyElement['value']).toBe(1);
380
+ expect(attributeElement.getAttribute('data-value')).toBe('1');
381
+ expect(classElement.classList.contains('active')).toBe(true);
382
+ expect(cssElement.style.opacity).toBe('1');
383
+ expect(cssVarElement.style.getPropertyValue('--amount')).toBe('1');
384
+ expect(visibleElement.style.visibility).toBe('');
385
+
386
+ handler.emit('shared', 0);
387
+ await flushMicrotasks();
388
+ expect(classElement.classList.contains('active')).toBe(false);
389
+ expect(cssElement.style.opacity).toBe('0');
390
+ expect(visibleElement.style.visibility).toBe('collapse');
391
+ expect(handler.subscribeCalls).toEqual(['shared']);
392
+ for (const dispose of disposers)
393
+ dispose();
394
+ });
395
+
396
+ test('removes native two-way listeners on disposal', async () => {
397
+ const handler = new TestVisualizationHandler();
398
+ handler.values.set('sensor', 1);
399
+ const helper = new BindingsHelper(handler);
400
+ const element = document.createElement('input');
401
+ const dispose = helper.applyBinding(element, ['value', {
402
+ signal: 'sensor',
403
+ twoWay: true,
404
+ events: ['change'],
405
+ type: 'number',
406
+ target: BindingTarget.property
407
+ }], '', null);
408
+ await flushMicrotasks();
409
+
410
+ element.value = '5';
411
+ element.dispatchEvent(new Event('change'));
412
+ expect(handler.setStateCalls.at(-1)).toEqual({ id: 'sensor', value: 5, ack: undefined });
413
+
414
+ dispose();
415
+ element.value = '6';
416
+ element.dispatchEvent(new Event('change'));
417
+ expect(handler.setStateCalls).toHaveLength(1);
418
+ });
419
+
420
+ test('removes TypedEvent two-way listeners on disposal', async () => {
421
+ const { TypedEvent } = await import('@node-projects/base-custom-webcomponent');
422
+ const handler = new TestVisualizationHandler();
423
+ handler.values.set('sensor', 1);
424
+ const helper = new BindingsHelper(handler);
425
+ const element = document.createElement('div');
426
+ element['valueChanged'] = new TypedEvent<void>();
427
+ const dispose = helper.applyBinding(element, ['value', {
428
+ signal: 'sensor',
429
+ twoWay: true,
430
+ events: ['valueChanged'],
431
+ target: BindingTarget.property
432
+ }], '', null);
433
+ await flushMicrotasks();
434
+
435
+ element['value'] = 5;
436
+ element['valueChanged'].emit();
437
+ expect(handler.setStateCalls.at(-1)).toEqual({ id: 'sensor', value: 5, ack: undefined });
438
+
439
+ dispose();
440
+ element['value'] = 6;
441
+ element['valueChanged'].emit();
442
+ expect(handler.setStateCalls).toHaveLength(1);
443
+ });
444
+
445
+ test('removes special-value callbacks and ignores late async values', async () => {
446
+ const handler = new TestVisualizationHandler();
447
+ const helper = new BindingsHelper(handler);
448
+ const element = document.createElement('div');
449
+ let resolveValue: (value: any) => void;
450
+ const specialValueHandler: SpecialValueHandler = {
451
+ valueProvider: () => new Promise(resolve => resolveValue = resolve),
452
+ valueChangedCallbacks: new Map()
453
+ };
454
+ const dispose = helper.applyBinding(element, ['value', {
455
+ signal: '§clock',
456
+ target: BindingTarget.property
457
+ }], '', null, specialValueHandler);
458
+
459
+ expect(specialValueHandler.valueChangedCallbacks.get('clock')).toHaveLength(1);
460
+ dispose();
461
+ expect(specialValueHandler.valueChangedCallbacks.has('clock')).toBe(false);
462
+ resolveValue(42);
463
+ await flushMicrotasks();
464
+ expect(element['value']).toBeUndefined();
465
+ });
466
+
467
+ test('uses precompiled exact, range, and template converters', async () => {
468
+ const handler = new TestVisualizationHandler();
469
+ handler.values.set('sensor', 5);
470
+ const helper = new BindingsHelper(handler);
471
+ const element = document.createElement('div');
472
+ const dispose = helper.applyBinding(element, ['value', {
473
+ signal: 'sensor',
474
+ converter: {
475
+ '5': 'exact ${__0}',
476
+ '>=10': 'high ${__0}',
477
+ '0-9': 'low'
478
+ },
479
+ target: BindingTarget.property
480
+ }], '', null);
481
+ await flushMicrotasks();
482
+ expect(element['value']).toBe('exact 5');
483
+
484
+ handler.emit('sensor', 7);
485
+ await flushMicrotasks();
486
+ expect(element['value']).toBe('low');
487
+
488
+ handler.emit('sensor', 12);
489
+ await flushMicrotasks();
490
+ expect(element['value']).toBe('high 12');
491
+ dispose();
492
+ });
493
+ });
494
+
495
+ test('parseBindingString splits without per-character string concatenation', () => {
496
+ expect(parseBindingString('.devices.{room}.{sensor}')).toEqual({
497
+ parts: ['.devices.', '.', ''],
498
+ signals: ['room', 'sensor']
499
+ });
500
+ });
501
+
502
+ test('binding refactoring preserves attribute source prefixes', () => {
503
+ const binding: any = {
504
+ bindableObjectNames: ['screen:?@theme', '#@state', '?$signalObject', '#$signalObject', '##value'],
505
+ target: BindingTarget.property,
506
+ targetName: 'value'
507
+ };
508
+ let savedBinding: any;
509
+ const bindingService = {
510
+ getBindings: () => [binding],
511
+ setBinding: (_designItem: any, value: any) => savedBinding = value
512
+ };
513
+ const designItem: any = { serviceContainer: { bindingService } };
514
+ const service = new VisualizationBindingsRefactorService();
515
+ const refactorings: any[] = service.getRefactorings([designItem]);
516
+
517
+ expect(refactorings.map(x => ({ name: x.name, prefix: x.prefix, itemType: x.itemType }))).toEqual([
518
+ { name: 'theme', prefix: '?@', itemType: 'attribute' },
519
+ { name: 'state', prefix: '#@', itemType: 'attribute' },
520
+ { name: 'signalObject', prefix: '?$', itemType: 'signalObject' },
521
+ { name: 'signalObject', prefix: '#$', itemType: 'signalObject' },
522
+ { name: 'value', prefix: '##', itemType: 'property' }
523
+ ]);
524
+
525
+ service.refactor(refactorings[0], 'theme', 'palette');
526
+ expect(savedBinding.bindableObjectNames).toEqual(['screen:?@palette', '#@state', '?$signalObject', '#$signalObject', '##value']);
527
+ });