@nyaruka/temba-components 0.124.3 → 0.126.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.
- package/.eslintrc.js +3 -1
- package/CHANGELOG.md +23 -0
- package/demo/data/flows/sample-flow.json +926 -0
- package/demo/data/server/sample-flow.json +0 -0
- package/demo/flow/example.html +46 -0
- package/demo/index.html +155 -144
- package/demo/webchat/example.html +71 -0
- package/dist/temba-components.js +89 -41
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/chart/TembaChart.js +118 -48
- package/out-tsc/src/chart/TembaChart.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +70 -5
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/EditorNode.js +140 -4
- package/out-tsc/src/flow/EditorNode.js.map +1 -1
- package/out-tsc/src/flow/Plumber.js +57 -0
- package/out-tsc/src/flow/Plumber.js.map +1 -1
- package/out-tsc/src/flow/config.js +70 -20
- package/out-tsc/src/flow/config.js.map +1 -1
- package/out-tsc/src/formfield/FormField.js +4 -1
- package/out-tsc/src/formfield/FormField.js.map +1 -1
- package/out-tsc/src/interfaces.js +1 -0
- package/out-tsc/src/interfaces.js.map +1 -1
- package/out-tsc/src/store/AppState.js +22 -5
- package/out-tsc/src/store/AppState.js.map +1 -1
- package/out-tsc/src/utils/index.js +3 -0
- package/out-tsc/src/utils/index.js.map +1 -1
- package/out-tsc/src/webchat/WebChat.js +2 -0
- package/out-tsc/src/webchat/WebChat.js.map +1 -1
- package/out-tsc/test/temba-chart.test.js +29 -15
- package/out-tsc/test/temba-chart.test.js.map +1 -1
- package/out-tsc/test/temba-flow-node-drag.test.js +257 -0
- package/out-tsc/test/temba-flow-node-drag.test.js.map +1 -0
- package/out-tsc/test/temba-formfield.test.js +94 -0
- package/out-tsc/test/temba-formfield.test.js.map +1 -0
- package/out-tsc/test/temba-integration-markdown.test.js +36 -0
- package/out-tsc/test/temba-integration-markdown.test.js.map +1 -0
- package/out-tsc/test/temba-select.test.js +14 -1
- package/out-tsc/test/temba-select.test.js.map +1 -1
- package/package.json +2 -1
- package/screenshots/truth/formfield/markdown-errors.png +0 -0
- package/screenshots/truth/formfield/no-errors.png +0 -0
- package/screenshots/truth/formfield/plain-text-errors.png +0 -0
- package/screenshots/truth/formfield/widget-only-markdown-errors.png +0 -0
- package/screenshots/truth/integration/checkbox-markdown-errors.png +0 -0
- package/src/chart/TembaChart.ts +130 -48
- package/src/flow/Editor.ts +78 -6
- package/src/flow/EditorNode.ts +164 -4
- package/src/flow/Plumber.ts +65 -0
- package/src/flow/config.ts +71 -20
- package/src/formfield/FormField.ts +4 -1
- package/src/interfaces.ts +2 -1
- package/src/store/AppState.ts +28 -7
- package/src/utils/index.ts +3 -0
- package/src/webchat/WebChat.ts +2 -0
- package/test/temba-chart.test.ts +35 -15
- package/test/temba-flow-node-drag.test.ts +337 -0
- package/test/temba-formfield.test.ts +121 -0
- package/test/temba-integration-markdown.test.ts +45 -0
- package/test/temba-select.test.ts +17 -0
- package/web-dev-server.config.mjs +43 -0
package/src/webchat/WebChat.ts
CHANGED
|
@@ -412,6 +412,7 @@ export class WebChat extends LitElement {
|
|
|
412
412
|
private sendSockMessage(
|
|
413
413
|
cmd: GetHistoryCmd | StartChatCmd | SendMsgCmd | Ack
|
|
414
414
|
) {
|
|
415
|
+
// eslint-disable-next-line no-console
|
|
415
416
|
console.log('out', cmd);
|
|
416
417
|
this.sock.send(JSON.stringify(cmd));
|
|
417
418
|
}
|
|
@@ -447,6 +448,7 @@ export class WebChat extends LitElement {
|
|
|
447
448
|
this.sock.onmessage = function (event: MessageEvent) {
|
|
448
449
|
webChat.status = ChatStatus.CONNECTED;
|
|
449
450
|
const msg = JSON.parse(event.data) as SockMsg;
|
|
451
|
+
// eslint-disable-next-line no-console
|
|
450
452
|
console.log('in', msg);
|
|
451
453
|
if (msg.type === 'chat_started') {
|
|
452
454
|
const response = msg as StartChatResponse;
|
package/test/temba-chart.test.ts
CHANGED
|
@@ -61,11 +61,11 @@ describe('temba-chart', () => {
|
|
|
61
61
|
const chart: TembaChart = await getChart();
|
|
62
62
|
|
|
63
63
|
// Test that formatDuration property exists and defaults to false
|
|
64
|
-
expect(chart.
|
|
64
|
+
expect(chart.yType).to.equal('count');
|
|
65
65
|
|
|
66
66
|
// Test that we can set formatDuration to true
|
|
67
|
-
chart.
|
|
68
|
-
expect(chart.
|
|
67
|
+
chart.yType = 'duration';
|
|
68
|
+
expect(chart.yType).to.equal('duration');
|
|
69
69
|
});
|
|
70
70
|
|
|
71
71
|
it('formats duration values correctly', async () => {
|
|
@@ -83,7 +83,7 @@ describe('temba-chart', () => {
|
|
|
83
83
|
]
|
|
84
84
|
};
|
|
85
85
|
|
|
86
|
-
chart.
|
|
86
|
+
chart.yType = 'duration';
|
|
87
87
|
chart.data = durationData;
|
|
88
88
|
await chart.updateComplete;
|
|
89
89
|
|
|
@@ -91,7 +91,7 @@ describe('temba-chart', () => {
|
|
|
91
91
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
92
92
|
|
|
93
93
|
// Test that the chart was created and has the duration formatting enabled
|
|
94
|
-
expect(chart.
|
|
94
|
+
expect(chart.yType).to.equal('duration');
|
|
95
95
|
expect(chart.chart).to.exist;
|
|
96
96
|
|
|
97
97
|
// Test that the chart configuration includes the duration formatting
|
|
@@ -115,6 +115,7 @@ describe('temba-chart', () => {
|
|
|
115
115
|
dataset: { label: 'Process Time' },
|
|
116
116
|
parsed: { y: 68787 }
|
|
117
117
|
};
|
|
118
|
+
|
|
118
119
|
expect(tooltipCallback.call({}, mockContext)).to.equal(
|
|
119
120
|
'Process Time: 19h 6m'
|
|
120
121
|
);
|
|
@@ -122,7 +123,7 @@ describe('temba-chart', () => {
|
|
|
122
123
|
|
|
123
124
|
it('formats various duration edge cases correctly', async () => {
|
|
124
125
|
const chart: TembaChart = await getChart();
|
|
125
|
-
chart.
|
|
126
|
+
chart.yType = 'duration';
|
|
126
127
|
chart.data = sampleData;
|
|
127
128
|
await chart.updateComplete;
|
|
128
129
|
|
|
@@ -146,21 +147,40 @@ describe('temba-chart', () => {
|
|
|
146
147
|
expect(tickCallback.call({}, 1209600, 10, [])).to.equal('14d'); // 2 weeks in seconds
|
|
147
148
|
});
|
|
148
149
|
|
|
149
|
-
it('
|
|
150
|
-
const chart: TembaChart = await getChart(
|
|
150
|
+
it('applies xFormat when xType is time', async () => {
|
|
151
|
+
const chart: TembaChart = await getChart({
|
|
152
|
+
xType: 'time',
|
|
153
|
+
xFormat: 'DD'
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
chart.data = sampleData;
|
|
157
|
+
await chart.updateComplete;
|
|
158
|
+
|
|
159
|
+
// Wait for the chart to be created after data is set
|
|
160
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
161
|
+
|
|
162
|
+
expect(chart.chart).to.exist;
|
|
163
|
+
expect(chart.chart.options.scales.x.type).to.equal('time');
|
|
164
|
+
expect(
|
|
165
|
+
(chart.chart.options.scales.x as any).time.displayFormats.day
|
|
166
|
+
).to.equal('DD');
|
|
167
|
+
});
|
|
151
168
|
|
|
152
|
-
|
|
153
|
-
|
|
169
|
+
it('does not include time config when xType is category', async () => {
|
|
170
|
+
const chart: TembaChart = await getChart({
|
|
171
|
+
xType: 'category',
|
|
172
|
+
xFormat: 'DD'
|
|
173
|
+
});
|
|
154
174
|
|
|
155
175
|
chart.data = sampleData;
|
|
156
176
|
await chart.updateComplete;
|
|
157
177
|
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
expect(chart.formatDuration).to.equal(true);
|
|
178
|
+
// Wait for the chart to be created after data is set
|
|
179
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
161
180
|
|
|
162
|
-
chart.
|
|
163
|
-
expect(chart.
|
|
181
|
+
expect(chart.chart).to.exist;
|
|
182
|
+
expect(chart.chart.options.scales.x.type).to.equal('category');
|
|
183
|
+
expect((chart.chart.options.scales.x as any).time).to.be.undefined;
|
|
164
184
|
});
|
|
165
185
|
});
|
|
166
186
|
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { fixture, assert } from '@open-wc/testing';
|
|
2
|
+
import { html } from 'lit';
|
|
3
|
+
|
|
4
|
+
describe('temba-flow-node drag and drop functionality', () => {
|
|
5
|
+
it('should add drag styling and event listeners to elements', async () => {
|
|
6
|
+
// Create a simple div to test our drag functionality
|
|
7
|
+
const testElement = await fixture(html`
|
|
8
|
+
<div
|
|
9
|
+
class="node"
|
|
10
|
+
style="position: absolute; left: 100px; top: 100px; width: 200px; height: 100px; background: white; border: 1px solid #ccc; cursor: move;"
|
|
11
|
+
>
|
|
12
|
+
Test Node
|
|
13
|
+
<div class="exit" style="padding: 5px; background: red;">Exit</div>
|
|
14
|
+
</div>
|
|
15
|
+
`);
|
|
16
|
+
|
|
17
|
+
// Test that the node has the correct cursor style
|
|
18
|
+
const computedStyle = window.getComputedStyle(testElement);
|
|
19
|
+
assert.equal(computedStyle.cursor, 'move');
|
|
20
|
+
|
|
21
|
+
// Test drag event simulation
|
|
22
|
+
let dragStarted = false;
|
|
23
|
+
let dragEnded = false;
|
|
24
|
+
let newPosition = { left: 0, top: 0 };
|
|
25
|
+
|
|
26
|
+
// Simulate our drag implementation
|
|
27
|
+
let isDragging = false;
|
|
28
|
+
let dragStartPos = { x: 0, y: 0 };
|
|
29
|
+
let nodeStartPos = { left: 100, top: 100 };
|
|
30
|
+
|
|
31
|
+
const handleMouseDown = (event: MouseEvent) => {
|
|
32
|
+
const target = event.target as HTMLElement;
|
|
33
|
+
if (target.classList.contains('exit') || target.closest('.exit')) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
isDragging = true;
|
|
38
|
+
dragStarted = true;
|
|
39
|
+
dragStartPos = { x: event.clientX, y: event.clientY };
|
|
40
|
+
nodeStartPos = { left: 100, top: 100 };
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
event.stopPropagation();
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const handleMouseMove = (event: MouseEvent) => {
|
|
46
|
+
if (!isDragging) return;
|
|
47
|
+
|
|
48
|
+
const deltaX = event.clientX - dragStartPos.x;
|
|
49
|
+
const deltaY = event.clientY - dragStartPos.y;
|
|
50
|
+
|
|
51
|
+
const newLeft = nodeStartPos.left + deltaX;
|
|
52
|
+
const newTop = nodeStartPos.top + deltaY;
|
|
53
|
+
|
|
54
|
+
// Update position
|
|
55
|
+
(testElement as HTMLElement).style.left = `${newLeft}px`;
|
|
56
|
+
(testElement as HTMLElement).style.top = `${newTop}px`;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const handleMouseUp = (event: MouseEvent) => {
|
|
60
|
+
if (!isDragging) return;
|
|
61
|
+
|
|
62
|
+
isDragging = false;
|
|
63
|
+
dragEnded = true;
|
|
64
|
+
|
|
65
|
+
const deltaX = event.clientX - dragStartPos.x;
|
|
66
|
+
const deltaY = event.clientY - dragStartPos.y;
|
|
67
|
+
|
|
68
|
+
newPosition = {
|
|
69
|
+
left: nodeStartPos.left + deltaX,
|
|
70
|
+
top: nodeStartPos.top + deltaY
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Add event listeners
|
|
75
|
+
testElement.addEventListener('mousedown', handleMouseDown);
|
|
76
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
77
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
78
|
+
|
|
79
|
+
// Test drag from node (should work)
|
|
80
|
+
const mouseDownEvent = new MouseEvent('mousedown', {
|
|
81
|
+
clientX: 150,
|
|
82
|
+
clientY: 150,
|
|
83
|
+
bubbles: true
|
|
84
|
+
});
|
|
85
|
+
testElement.dispatchEvent(mouseDownEvent);
|
|
86
|
+
|
|
87
|
+
assert.isTrue(dragStarted, 'Drag should start when clicking on node');
|
|
88
|
+
|
|
89
|
+
const mouseMoveEvent = new MouseEvent('mousemove', {
|
|
90
|
+
clientX: 200,
|
|
91
|
+
clientY: 200,
|
|
92
|
+
bubbles: true
|
|
93
|
+
});
|
|
94
|
+
document.dispatchEvent(mouseMoveEvent);
|
|
95
|
+
|
|
96
|
+
const mouseUpEvent = new MouseEvent('mouseup', {
|
|
97
|
+
clientX: 200,
|
|
98
|
+
clientY: 200,
|
|
99
|
+
bubbles: true
|
|
100
|
+
});
|
|
101
|
+
document.dispatchEvent(mouseUpEvent);
|
|
102
|
+
|
|
103
|
+
assert.isTrue(dragEnded, 'Drag should end on mouse up');
|
|
104
|
+
assert.equal(
|
|
105
|
+
newPosition.left,
|
|
106
|
+
150,
|
|
107
|
+
'New left position should be calculated correctly'
|
|
108
|
+
);
|
|
109
|
+
assert.equal(
|
|
110
|
+
newPosition.top,
|
|
111
|
+
150,
|
|
112
|
+
'New top position should be calculated correctly'
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Test that position was visually updated
|
|
116
|
+
assert.equal((testElement as HTMLElement).style.left, '150px');
|
|
117
|
+
assert.equal((testElement as HTMLElement).style.top, '150px');
|
|
118
|
+
|
|
119
|
+
// Reset for next test
|
|
120
|
+
dragStarted = false;
|
|
121
|
+
dragEnded = false;
|
|
122
|
+
|
|
123
|
+
// Test drag from exit element (should not work)
|
|
124
|
+
const exitElement = testElement.querySelector('.exit') as HTMLElement;
|
|
125
|
+
const exitMouseDownEvent = new MouseEvent('mousedown', {
|
|
126
|
+
clientX: 150,
|
|
127
|
+
clientY: 150,
|
|
128
|
+
bubbles: true
|
|
129
|
+
});
|
|
130
|
+
exitElement.dispatchEvent(exitMouseDownEvent);
|
|
131
|
+
|
|
132
|
+
assert.isFalse(
|
|
133
|
+
dragStarted,
|
|
134
|
+
'Drag should not start when clicking on exit element'
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Clean up
|
|
138
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
139
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should snap node positions to 20px grid during drag and drop', async () => {
|
|
143
|
+
// Create a test node element
|
|
144
|
+
const testElement = await fixture(html`
|
|
145
|
+
<div
|
|
146
|
+
class="node"
|
|
147
|
+
style="position: absolute; left: 100px; top: 100px; width: 200px; height: 100px; background: white; border: 1px solid #ccc; cursor: move;"
|
|
148
|
+
>
|
|
149
|
+
Test Node for Grid Snapping
|
|
150
|
+
</div>
|
|
151
|
+
`);
|
|
152
|
+
|
|
153
|
+
// Helper function to snap values to 20px grid (same as implementation)
|
|
154
|
+
const snapToGrid = (value: number): number => {
|
|
155
|
+
return Math.round(value / 20) * 20;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Simulate drag implementation with grid snapping
|
|
159
|
+
let isDragging = false;
|
|
160
|
+
let dragStartPos = { x: 0, y: 0 };
|
|
161
|
+
let nodeStartPos = { left: 100, top: 100 };
|
|
162
|
+
let finalPosition = { left: 0, top: 0 };
|
|
163
|
+
|
|
164
|
+
const handleMouseDown = (event: MouseEvent) => {
|
|
165
|
+
isDragging = true;
|
|
166
|
+
dragStartPos = { x: event.clientX, y: event.clientY };
|
|
167
|
+
// Get current position from element style
|
|
168
|
+
const currentLeft =
|
|
169
|
+
parseInt((testElement as HTMLElement).style.left) || 0;
|
|
170
|
+
const currentTop = parseInt((testElement as HTMLElement).style.top) || 0;
|
|
171
|
+
nodeStartPos = { left: currentLeft, top: currentTop };
|
|
172
|
+
event.preventDefault();
|
|
173
|
+
event.stopPropagation();
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const handleMouseMove = (event: MouseEvent) => {
|
|
177
|
+
if (!isDragging) return;
|
|
178
|
+
|
|
179
|
+
const deltaX = event.clientX - dragStartPos.x;
|
|
180
|
+
const deltaY = event.clientY - dragStartPos.y;
|
|
181
|
+
|
|
182
|
+
const newLeft = nodeStartPos.left + deltaX;
|
|
183
|
+
const newTop = nodeStartPos.top + deltaY;
|
|
184
|
+
|
|
185
|
+
// Snap to 20px grid
|
|
186
|
+
const snappedLeft = snapToGrid(newLeft);
|
|
187
|
+
const snappedTop = snapToGrid(newTop);
|
|
188
|
+
|
|
189
|
+
// Update position with snapped values
|
|
190
|
+
(testElement as HTMLElement).style.left = `${snappedLeft}px`;
|
|
191
|
+
(testElement as HTMLElement).style.top = `${snappedTop}px`;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const handleMouseUp = (event: MouseEvent) => {
|
|
195
|
+
if (!isDragging) return;
|
|
196
|
+
isDragging = false;
|
|
197
|
+
|
|
198
|
+
const deltaX = event.clientX - dragStartPos.x;
|
|
199
|
+
const deltaY = event.clientY - dragStartPos.y;
|
|
200
|
+
|
|
201
|
+
const newLeft = nodeStartPos.left + deltaX;
|
|
202
|
+
const newTop = nodeStartPos.top + deltaY;
|
|
203
|
+
|
|
204
|
+
// Snap final position to grid
|
|
205
|
+
finalPosition = {
|
|
206
|
+
left: snapToGrid(newLeft),
|
|
207
|
+
top: snapToGrid(newTop)
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Add event listeners
|
|
212
|
+
testElement.addEventListener('mousedown', handleMouseDown);
|
|
213
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
214
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
215
|
+
|
|
216
|
+
// Test Case 1: Drag to position that should snap to grid
|
|
217
|
+
// Starting at (100, 100), drag by (33, 27) pixels
|
|
218
|
+
// Should result in (133, 127) -> snapped to (140, 120)
|
|
219
|
+
const mouseDownEvent1 = new MouseEvent('mousedown', {
|
|
220
|
+
clientX: 150,
|
|
221
|
+
clientY: 150,
|
|
222
|
+
bubbles: true
|
|
223
|
+
});
|
|
224
|
+
testElement.dispatchEvent(mouseDownEvent1);
|
|
225
|
+
|
|
226
|
+
const mouseMoveEvent1 = new MouseEvent('mousemove', {
|
|
227
|
+
clientX: 183, // 150 + 33
|
|
228
|
+
clientY: 177, // 150 + 27
|
|
229
|
+
bubbles: true
|
|
230
|
+
});
|
|
231
|
+
document.dispatchEvent(mouseMoveEvent1);
|
|
232
|
+
|
|
233
|
+
// Check that position is snapped during drag
|
|
234
|
+
assert.equal((testElement as HTMLElement).style.left, '140px');
|
|
235
|
+
assert.equal((testElement as HTMLElement).style.top, '120px');
|
|
236
|
+
|
|
237
|
+
const mouseUpEvent1 = new MouseEvent('mouseup', {
|
|
238
|
+
clientX: 183,
|
|
239
|
+
clientY: 177,
|
|
240
|
+
bubbles: true
|
|
241
|
+
});
|
|
242
|
+
document.dispatchEvent(mouseUpEvent1);
|
|
243
|
+
|
|
244
|
+
// Check final snapped position
|
|
245
|
+
assert.equal(
|
|
246
|
+
finalPosition.left,
|
|
247
|
+
140,
|
|
248
|
+
'Final left position should be snapped to 140px'
|
|
249
|
+
);
|
|
250
|
+
assert.equal(
|
|
251
|
+
finalPosition.top,
|
|
252
|
+
120,
|
|
253
|
+
'Final top position should be snapped to 120px'
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// Test Case 2: Test different snap scenarios
|
|
257
|
+
// Reset position
|
|
258
|
+
nodeStartPos = { left: 60, top: 80 };
|
|
259
|
+
(testElement as HTMLElement).style.left = '60px';
|
|
260
|
+
(testElement as HTMLElement).style.top = '80px';
|
|
261
|
+
|
|
262
|
+
const mouseDownEvent2 = new MouseEvent('mousedown', {
|
|
263
|
+
clientX: 100,
|
|
264
|
+
clientY: 100,
|
|
265
|
+
bubbles: true
|
|
266
|
+
});
|
|
267
|
+
testElement.dispatchEvent(mouseDownEvent2);
|
|
268
|
+
|
|
269
|
+
// Drag by (15, 25) -> should snap to nearest 20px grid
|
|
270
|
+
// (60 + 15, 80 + 25) = (75, 105) -> snapped to (80, 100)
|
|
271
|
+
const mouseMoveEvent2 = new MouseEvent('mousemove', {
|
|
272
|
+
clientX: 115, // 100 + 15
|
|
273
|
+
clientY: 125, // 100 + 25
|
|
274
|
+
bubbles: true
|
|
275
|
+
});
|
|
276
|
+
document.dispatchEvent(mouseMoveEvent2);
|
|
277
|
+
|
|
278
|
+
assert.equal((testElement as HTMLElement).style.left, '80px');
|
|
279
|
+
assert.equal((testElement as HTMLElement).style.top, '100px');
|
|
280
|
+
|
|
281
|
+
const mouseUpEvent2 = new MouseEvent('mouseup', {
|
|
282
|
+
clientX: 115,
|
|
283
|
+
clientY: 125,
|
|
284
|
+
bubbles: true
|
|
285
|
+
});
|
|
286
|
+
document.dispatchEvent(mouseUpEvent2);
|
|
287
|
+
|
|
288
|
+
assert.equal(
|
|
289
|
+
finalPosition.left,
|
|
290
|
+
80,
|
|
291
|
+
'Final left position should be snapped to 80px'
|
|
292
|
+
);
|
|
293
|
+
assert.equal(
|
|
294
|
+
finalPosition.top,
|
|
295
|
+
100,
|
|
296
|
+
'Final top position should be snapped to 100px'
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
// Test Case 3: Test exact grid positions remain unchanged
|
|
300
|
+
nodeStartPos = { left: 120, top: 160 }; // Already on grid
|
|
301
|
+
(testElement as HTMLElement).style.left = '120px';
|
|
302
|
+
(testElement as HTMLElement).style.top = '160px';
|
|
303
|
+
|
|
304
|
+
const mouseDownEvent3 = new MouseEvent('mousedown', {
|
|
305
|
+
clientX: 200,
|
|
306
|
+
clientY: 200,
|
|
307
|
+
bubbles: true
|
|
308
|
+
});
|
|
309
|
+
testElement.dispatchEvent(mouseDownEvent3);
|
|
310
|
+
|
|
311
|
+
// Small movement that should stay on same grid position
|
|
312
|
+
const mouseMoveEvent3 = new MouseEvent('mousemove', {
|
|
313
|
+
clientX: 202, // +2 pixels
|
|
314
|
+
clientY: 203, // +3 pixels
|
|
315
|
+
bubbles: true
|
|
316
|
+
});
|
|
317
|
+
document.dispatchEvent(mouseMoveEvent3);
|
|
318
|
+
|
|
319
|
+
// (120 + 2, 160 + 3) = (122, 163) -> snapped to (120, 160)
|
|
320
|
+
assert.equal((testElement as HTMLElement).style.left, '120px');
|
|
321
|
+
assert.equal((testElement as HTMLElement).style.top, '160px');
|
|
322
|
+
|
|
323
|
+
const mouseUpEvent3 = new MouseEvent('mouseup', {
|
|
324
|
+
clientX: 202,
|
|
325
|
+
clientY: 203,
|
|
326
|
+
bubbles: true
|
|
327
|
+
});
|
|
328
|
+
document.dispatchEvent(mouseUpEvent3);
|
|
329
|
+
|
|
330
|
+
assert.equal(finalPosition.left, 120, 'Position should remain on grid');
|
|
331
|
+
assert.equal(finalPosition.top, 160, 'Position should remain on grid');
|
|
332
|
+
|
|
333
|
+
// Clean up
|
|
334
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
335
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { html, fixture, expect } from '@open-wc/testing';
|
|
2
|
+
import { FormField } from '../src/formfield/FormField';
|
|
3
|
+
import { assertScreenshot, getClip } from './utils.test';
|
|
4
|
+
|
|
5
|
+
describe('temba-field', () => {
|
|
6
|
+
it('renders field with plain text errors', async () => {
|
|
7
|
+
const formField: FormField = await fixture(html`
|
|
8
|
+
<temba-field
|
|
9
|
+
label="Test Field"
|
|
10
|
+
name="test"
|
|
11
|
+
.errors=${['This is a plain text error', 'Another error message']}
|
|
12
|
+
>
|
|
13
|
+
<input type="text" />
|
|
14
|
+
</temba-field>
|
|
15
|
+
`);
|
|
16
|
+
|
|
17
|
+
await formField.updateComplete;
|
|
18
|
+
|
|
19
|
+
// Check that errors are rendered
|
|
20
|
+
const errorElements = formField.shadowRoot.querySelectorAll('.alert-error');
|
|
21
|
+
expect(errorElements.length).to.equal(2);
|
|
22
|
+
expect(errorElements[0].textContent.trim()).to.equal(
|
|
23
|
+
'This is a plain text error'
|
|
24
|
+
);
|
|
25
|
+
expect(errorElements[1].textContent.trim()).to.equal(
|
|
26
|
+
'Another error message'
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
await assertScreenshot('formfield/plain-text-errors', getClip(formField));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('renders field with markdown errors', async () => {
|
|
33
|
+
const formField: FormField = await fixture(html`
|
|
34
|
+
<temba-field
|
|
35
|
+
label="Test Field"
|
|
36
|
+
name="test"
|
|
37
|
+
.errors=${[
|
|
38
|
+
'This is **bold** text',
|
|
39
|
+
'This has a [link](https://example.com)',
|
|
40
|
+
'This is *italic* and **bold** with a [link](https://example.com)'
|
|
41
|
+
]}
|
|
42
|
+
>
|
|
43
|
+
<input type="text" />
|
|
44
|
+
</temba-field>
|
|
45
|
+
`);
|
|
46
|
+
|
|
47
|
+
await formField.updateComplete;
|
|
48
|
+
|
|
49
|
+
// Check that errors are rendered
|
|
50
|
+
const errorElements = formField.shadowRoot.querySelectorAll('.alert-error');
|
|
51
|
+
expect(errorElements.length).to.equal(3);
|
|
52
|
+
|
|
53
|
+
// First error should have bold text
|
|
54
|
+
const firstError = errorElements[0];
|
|
55
|
+
const boldElement = firstError.querySelector('strong');
|
|
56
|
+
expect(boldElement).to.not.be.null;
|
|
57
|
+
expect(boldElement.textContent).to.equal('bold');
|
|
58
|
+
|
|
59
|
+
// Second error should have a link
|
|
60
|
+
const secondError = errorElements[1];
|
|
61
|
+
const linkElement = secondError.querySelector('a');
|
|
62
|
+
expect(linkElement).to.not.be.null;
|
|
63
|
+
expect(linkElement.getAttribute('href')).to.equal('https://example.com');
|
|
64
|
+
expect(linkElement.textContent).to.equal('link');
|
|
65
|
+
|
|
66
|
+
// Third error should have both bold, italic, and link
|
|
67
|
+
const thirdError = errorElements[2];
|
|
68
|
+
const thirdBoldElement = thirdError.querySelector('strong');
|
|
69
|
+
const thirdItalicElement = thirdError.querySelector('em');
|
|
70
|
+
const thirdLinkElement = thirdError.querySelector('a');
|
|
71
|
+
expect(thirdBoldElement).to.not.be.null;
|
|
72
|
+
expect(thirdItalicElement).to.not.be.null;
|
|
73
|
+
expect(thirdLinkElement).to.not.be.null;
|
|
74
|
+
|
|
75
|
+
await assertScreenshot('formfield/markdown-errors', getClip(formField));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('renders field without errors', async () => {
|
|
79
|
+
const formField: FormField = await fixture(html`
|
|
80
|
+
<temba-field label="Test Field" name="test">
|
|
81
|
+
<input type="text" />
|
|
82
|
+
</temba-field>
|
|
83
|
+
`);
|
|
84
|
+
|
|
85
|
+
await formField.updateComplete;
|
|
86
|
+
|
|
87
|
+
// Check that no errors are rendered
|
|
88
|
+
const errorElements = formField.shadowRoot.querySelectorAll('.alert-error');
|
|
89
|
+
expect(errorElements.length).to.equal(0);
|
|
90
|
+
|
|
91
|
+
await assertScreenshot('formfield/no-errors', getClip(formField));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('renders in widget-only mode with errors', async () => {
|
|
95
|
+
const formField: FormField = await fixture(html`
|
|
96
|
+
<temba-field
|
|
97
|
+
widget_only
|
|
98
|
+
.errors=${['Widget only **error** with [link](https://example.com)']}
|
|
99
|
+
>
|
|
100
|
+
<input type="text" />
|
|
101
|
+
</temba-field>
|
|
102
|
+
`);
|
|
103
|
+
|
|
104
|
+
await formField.updateComplete;
|
|
105
|
+
|
|
106
|
+
// Check that error is rendered in widget-only mode
|
|
107
|
+
const errorElements = formField.shadowRoot.querySelectorAll('.alert-error');
|
|
108
|
+
expect(errorElements.length).to.equal(1);
|
|
109
|
+
|
|
110
|
+
const errorElement = errorElements[0];
|
|
111
|
+
const boldElement = errorElement.querySelector('strong');
|
|
112
|
+
const linkElement = errorElement.querySelector('a');
|
|
113
|
+
expect(boldElement).to.not.be.null;
|
|
114
|
+
expect(linkElement).to.not.be.null;
|
|
115
|
+
|
|
116
|
+
await assertScreenshot(
|
|
117
|
+
'formfield/widget-only-markdown-errors',
|
|
118
|
+
getClip(formField)
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { html, fixture, expect } from '@open-wc/testing';
|
|
2
|
+
import { Checkbox } from '../src/checkbox/Checkbox';
|
|
3
|
+
import { assertScreenshot, getClip } from './utils.test';
|
|
4
|
+
|
|
5
|
+
describe('FormElement markdown integration', () => {
|
|
6
|
+
it('renders checkbox with markdown errors', async () => {
|
|
7
|
+
const checkbox: Checkbox = await fixture(html`
|
|
8
|
+
<temba-checkbox
|
|
9
|
+
label="Accept Terms"
|
|
10
|
+
.errors=${[
|
|
11
|
+
'Please read the **terms and conditions** at [this link](https://example.com)',
|
|
12
|
+
'This field *requires* acceptance'
|
|
13
|
+
]}
|
|
14
|
+
></temba-checkbox>
|
|
15
|
+
`);
|
|
16
|
+
|
|
17
|
+
await checkbox.updateComplete;
|
|
18
|
+
|
|
19
|
+
// Check that errors are rendered with markdown
|
|
20
|
+
const errorElements = checkbox.shadowRoot
|
|
21
|
+
.querySelectorAll('temba-field')[0]
|
|
22
|
+
.shadowRoot.querySelectorAll('.alert-error');
|
|
23
|
+
expect(errorElements.length).to.equal(2);
|
|
24
|
+
|
|
25
|
+
// First error should have bold text and link
|
|
26
|
+
const firstError = errorElements[0];
|
|
27
|
+
const boldElement = firstError.querySelector('strong');
|
|
28
|
+
const linkElement = firstError.querySelector('a');
|
|
29
|
+
expect(boldElement).to.not.be.null;
|
|
30
|
+
expect(boldElement.textContent).to.equal('terms and conditions');
|
|
31
|
+
expect(linkElement).to.not.be.null;
|
|
32
|
+
expect(linkElement.getAttribute('href')).to.equal('https://example.com');
|
|
33
|
+
|
|
34
|
+
// Second error should have italic text
|
|
35
|
+
const secondError = errorElements[1];
|
|
36
|
+
const italicElement = secondError.querySelector('em');
|
|
37
|
+
expect(italicElement).to.not.be.null;
|
|
38
|
+
expect(italicElement.textContent).to.equal('requires');
|
|
39
|
+
|
|
40
|
+
await assertScreenshot(
|
|
41
|
+
'integration/checkbox-markdown-errors',
|
|
42
|
+
getClip(checkbox)
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -5,6 +5,7 @@ import { Options } from '../src/options/Options';
|
|
|
5
5
|
import { Select, SelectOption } from '../src/select/Select';
|
|
6
6
|
import {
|
|
7
7
|
assertScreenshot,
|
|
8
|
+
delay,
|
|
8
9
|
getClip,
|
|
9
10
|
getOptions,
|
|
10
11
|
loadStore,
|
|
@@ -660,6 +661,22 @@ describe('temba-select', () => {
|
|
|
660
661
|
|
|
661
662
|
await openSelect(clock, select);
|
|
662
663
|
|
|
664
|
+
// Wait for pagination to complete - keep checking until fetching is false
|
|
665
|
+
// and we have the expected number of results (15 = 3 pages * 5 items)
|
|
666
|
+
let attempts = 0;
|
|
667
|
+
const maxAttempts = 10;
|
|
668
|
+
while (select.fetching || select.visibleOptions.length < 15) {
|
|
669
|
+
if (attempts >= maxAttempts) {
|
|
670
|
+
throw new Error(
|
|
671
|
+
`Pagination did not complete after ${maxAttempts} attempts. fetching: ${select.fetching}, visibleOptions: ${select.visibleOptions.length}`
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
await select.updateComplete;
|
|
675
|
+
clock.runAll();
|
|
676
|
+
attempts++;
|
|
677
|
+
await delay(100);
|
|
678
|
+
}
|
|
679
|
+
|
|
663
680
|
// should have all three pages visible right away
|
|
664
681
|
assert.equal(select.visibleOptions.length, 15);
|
|
665
682
|
});
|