kempo-ui 0.4.7 → 0.4.9
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/AGENTS.md +24 -0
- package/CHANGELOG.md +20 -0
- package/dist/components/Timestamp.js +1 -1
- package/dist/utils/formatTimestamp.js +1 -1
- package/docs/components/timestamp.html +95 -2
- package/docs/src/components/Timestamp.js +1 -1
- package/docs/src/utils/formatTimestamp.js +1 -1
- package/docs-src/components/timestamp.page.html +95 -2
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/src/components/Timestamp.js +3 -3
- package/src/utils/formatTimestamp.js +2 -1
- package/tasks/_template.md +39 -0
- package/tasks/released/0001-identify-timestamp-formats/documentation-page-screenshot.png +0 -0
- package/tasks/released/0001-identify-timestamp-formats/format-examples-and-notes.png +0 -0
- package/tasks/released/0001-identify-timestamp-formats/supported-formats-section.png +0 -0
- package/tasks/released/0001-identify-timestamp-formats/validation-snapshot.txt +236 -0
- package/tasks/released/0001-identify-timestamp-formats.md +114 -0
- package/tests/components/Timestamp.browser-test.js +4 -4
- package/tests/components/Chat.browser-test.js +0 -540
|
@@ -1,540 +0,0 @@
|
|
|
1
|
-
import Chat from '../../src/components/Chat.js';
|
|
2
|
-
|
|
3
|
-
const createChat = async (attrs = {}) => {
|
|
4
|
-
const container = document.createElement('div');
|
|
5
|
-
const parts = [];
|
|
6
|
-
if(attrs.enterNewline) parts.push('enter-newline');
|
|
7
|
-
if(attrs.showStates) parts.push('show-states');
|
|
8
|
-
if(attrs.disabled) parts.push('disabled');
|
|
9
|
-
if(attrs.placeholder !== undefined) parts.push(`placeholder="${attrs.placeholder}"`);
|
|
10
|
-
if(attrs.controls !== undefined) parts.push(`controls="${attrs.controls}"`);
|
|
11
|
-
container.innerHTML = `<k-chat ${parts.join(' ')}></k-chat>`;
|
|
12
|
-
document.body.appendChild(container);
|
|
13
|
-
const el = container.querySelector('k-chat');
|
|
14
|
-
await el.updateComplete;
|
|
15
|
-
return { container, el };
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const cleanup = (container) => {
|
|
19
|
-
if(container && container.parentNode){
|
|
20
|
-
container.parentNode.removeChild(container);
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const messageEls = (el) => [...el.shadowRoot.querySelectorAll('.message')];
|
|
25
|
-
|
|
26
|
-
export default {
|
|
27
|
-
/*
|
|
28
|
-
Element Creation
|
|
29
|
-
*/
|
|
30
|
-
'should create chat element': async ({pass, fail}) => {
|
|
31
|
-
const { container, el } = await createChat();
|
|
32
|
-
if(!(el instanceof Chat)){
|
|
33
|
-
cleanup(container);
|
|
34
|
-
return fail('Element should be instance of Chat');
|
|
35
|
-
}
|
|
36
|
-
cleanup(container);
|
|
37
|
-
pass('Chat element created');
|
|
38
|
-
},
|
|
39
|
-
|
|
40
|
-
'should render an empty message window and an editor': async ({pass, fail}) => {
|
|
41
|
-
const { container, el } = await createChat();
|
|
42
|
-
const wnd = el.shadowRoot.querySelector('.window');
|
|
43
|
-
const editor = el.shadowRoot.querySelector('k-html-editor');
|
|
44
|
-
if(!wnd || !editor){
|
|
45
|
-
cleanup(container);
|
|
46
|
-
return fail('Should render .window and k-html-editor');
|
|
47
|
-
}
|
|
48
|
-
if(messageEls(el).length !== 0){
|
|
49
|
-
cleanup(container);
|
|
50
|
-
return fail('Window should start empty');
|
|
51
|
-
}
|
|
52
|
-
cleanup(container);
|
|
53
|
-
pass('Initial layout rendered');
|
|
54
|
-
},
|
|
55
|
-
|
|
56
|
-
'should re-project user controls into the editor toolbar via the controls slot': async ({pass, fail}) => {
|
|
57
|
-
const container = document.createElement('div');
|
|
58
|
-
container.innerHTML = '<k-chat><button slot="controls" id="user-bold">B</button></k-chat>';
|
|
59
|
-
document.body.appendChild(container);
|
|
60
|
-
const el = container.querySelector('k-chat');
|
|
61
|
-
await el.updateComplete;
|
|
62
|
-
// Children-changed observer might re-render — wait one more cycle
|
|
63
|
-
await el.updateComplete;
|
|
64
|
-
const userBtn = el.querySelector('#user-bold');
|
|
65
|
-
if(!userBtn){
|
|
66
|
-
cleanup(container);
|
|
67
|
-
return fail('User-provided control should remain in the light DOM');
|
|
68
|
-
}
|
|
69
|
-
const controlsSlot = el.shadowRoot.querySelector('slot[name="controls"]');
|
|
70
|
-
if(!controlsSlot){
|
|
71
|
-
cleanup(container);
|
|
72
|
-
return fail('Chat shadow should expose a slot[name="controls"] when controls are provided');
|
|
73
|
-
}
|
|
74
|
-
const assigned = controlsSlot.assignedNodes({ flatten: true });
|
|
75
|
-
if(!assigned.includes(userBtn)){
|
|
76
|
-
cleanup(container);
|
|
77
|
-
return fail('User control should be assigned to the chat controls slot');
|
|
78
|
-
}
|
|
79
|
-
if(controlsSlot.getAttribute('slot') !== 'toolbar-bottom-left'){
|
|
80
|
-
cleanup(container);
|
|
81
|
-
return fail(`Expected controls slot to forward to "toolbar-bottom-left", got "${controlsSlot.getAttribute('slot')}"`);
|
|
82
|
-
}
|
|
83
|
-
cleanup(container);
|
|
84
|
-
pass('User-provided controls forward into the editor toolbar');
|
|
85
|
-
},
|
|
86
|
-
|
|
87
|
-
'should NOT render the controls slot when no controls are provided': async ({pass, fail}) => {
|
|
88
|
-
const { container, el } = await createChat();
|
|
89
|
-
const slot = el.shadowRoot.querySelector('slot[name="controls"]');
|
|
90
|
-
if(slot){
|
|
91
|
-
cleanup(container);
|
|
92
|
-
return fail('Empty <slot name="controls"> would trigger an empty toolbar in the editor — should not render');
|
|
93
|
-
}
|
|
94
|
-
cleanup(container);
|
|
95
|
-
pass('No controls slot rendered when none provided');
|
|
96
|
-
},
|
|
97
|
-
|
|
98
|
-
'should embed a k-html-editor and an icon Send button': async ({pass, fail}) => {
|
|
99
|
-
const { container, el } = await createChat();
|
|
100
|
-
const editor = el.shadowRoot.querySelector('k-html-editor');
|
|
101
|
-
const send = el.shadowRoot.querySelector('.send-btn');
|
|
102
|
-
if(!editor){
|
|
103
|
-
cleanup(container);
|
|
104
|
-
return fail('Should embed k-html-editor');
|
|
105
|
-
}
|
|
106
|
-
if(!send || send.tagName !== 'BUTTON'){
|
|
107
|
-
cleanup(container);
|
|
108
|
-
return fail('Should render Send button');
|
|
109
|
-
}
|
|
110
|
-
const icon = send.querySelector('k-icon');
|
|
111
|
-
if(!icon || icon.getAttribute('name') !== 'send'){
|
|
112
|
-
cleanup(container);
|
|
113
|
-
return fail('Send button should contain a k-icon name="send"');
|
|
114
|
-
}
|
|
115
|
-
cleanup(container);
|
|
116
|
-
pass('Editor + icon Send button rendered');
|
|
117
|
-
},
|
|
118
|
-
|
|
119
|
-
/*
|
|
120
|
-
addMessage
|
|
121
|
-
*/
|
|
122
|
-
'addMessage should append a message and return its id': async ({pass, fail}) => {
|
|
123
|
-
const { container, el } = await createChat();
|
|
124
|
-
const id = el.addMessage({ type: 'incoming', html: 'hello' });
|
|
125
|
-
await el.updateComplete;
|
|
126
|
-
if(typeof id !== 'string' || !id){
|
|
127
|
-
cleanup(container);
|
|
128
|
-
return fail(`Expected a string id, got ${typeof id}`);
|
|
129
|
-
}
|
|
130
|
-
if(el.messages.length !== 1 || el.messages[0].id !== id){
|
|
131
|
-
cleanup(container);
|
|
132
|
-
return fail('Message should be appended with returned id');
|
|
133
|
-
}
|
|
134
|
-
cleanup(container);
|
|
135
|
-
pass('addMessage appends and returns id');
|
|
136
|
-
},
|
|
137
|
-
|
|
138
|
-
'addMessage should default state by type': async ({pass, fail}) => {
|
|
139
|
-
const { container, el } = await createChat();
|
|
140
|
-
el.addMessage({ type: 'incoming', html: 'hi' });
|
|
141
|
-
el.addMessage({ type: 'outgoing', html: 'reply' });
|
|
142
|
-
if(el.messages[0].state !== 'received'){
|
|
143
|
-
cleanup(container);
|
|
144
|
-
return fail(`Incoming default state should be "received", got "${el.messages[0].state}"`);
|
|
145
|
-
}
|
|
146
|
-
if(el.messages[1].state !== 'sent'){
|
|
147
|
-
cleanup(container);
|
|
148
|
-
return fail(`Outgoing default state should be "sent", got "${el.messages[1].state}"`);
|
|
149
|
-
}
|
|
150
|
-
cleanup(container);
|
|
151
|
-
pass('Default states applied by message type');
|
|
152
|
-
},
|
|
153
|
-
|
|
154
|
-
'addMessage should sanitize HTML': async ({pass, fail}) => {
|
|
155
|
-
const { container, el } = await createChat();
|
|
156
|
-
el.addMessage({ type: 'incoming', html: 'safe<script>alert(1)</script><b>bold</b>' });
|
|
157
|
-
const stored = el.messages[0].html;
|
|
158
|
-
if(/script/i.test(stored)){
|
|
159
|
-
cleanup(container);
|
|
160
|
-
return fail(`HTML should be sanitized, got "${stored}"`);
|
|
161
|
-
}
|
|
162
|
-
if(!stored.includes('<b>bold</b>')){
|
|
163
|
-
cleanup(container);
|
|
164
|
-
return fail(`Safe content should be preserved, got "${stored}"`);
|
|
165
|
-
}
|
|
166
|
-
cleanup(container);
|
|
167
|
-
pass('addMessage runs through sanitizer');
|
|
168
|
-
},
|
|
169
|
-
|
|
170
|
-
'addMessage should render outgoing vs incoming with different classes': async ({pass, fail}) => {
|
|
171
|
-
const { container, el } = await createChat();
|
|
172
|
-
el.addMessage({ type: 'incoming', html: 'in' });
|
|
173
|
-
el.addMessage({ type: 'outgoing', html: 'out' });
|
|
174
|
-
await el.updateComplete;
|
|
175
|
-
const els = messageEls(el);
|
|
176
|
-
if(!els[0].classList.contains('incoming') || !els[1].classList.contains('outgoing')){
|
|
177
|
-
cleanup(container);
|
|
178
|
-
return fail('Messages should have incoming/outgoing classes');
|
|
179
|
-
}
|
|
180
|
-
cleanup(container);
|
|
181
|
-
pass('incoming/outgoing classes applied');
|
|
182
|
-
},
|
|
183
|
-
|
|
184
|
-
/*
|
|
185
|
-
updateMessage
|
|
186
|
-
*/
|
|
187
|
-
'updateMessage should change state': async ({pass, fail}) => {
|
|
188
|
-
const { container, el } = await createChat();
|
|
189
|
-
const id = el.addMessage({ type: 'outgoing', html: 'hi', state: 'sending' });
|
|
190
|
-
const ok = el.updateMessage(id, { state: 'sent' });
|
|
191
|
-
if(!ok || el.messages[0].state !== 'sent'){
|
|
192
|
-
cleanup(container);
|
|
193
|
-
return fail(`Expected state updated to "sent", got "${el.messages[0].state}"`);
|
|
194
|
-
}
|
|
195
|
-
cleanup(container);
|
|
196
|
-
pass('updateMessage changes state');
|
|
197
|
-
},
|
|
198
|
-
|
|
199
|
-
'updateMessage should reject invalid states': async ({pass, fail}) => {
|
|
200
|
-
const { container, el } = await createChat();
|
|
201
|
-
const id = el.addMessage({ type: 'outgoing', html: 'hi', state: 'sending' });
|
|
202
|
-
el.updateMessage(id, { state: 'bogus' });
|
|
203
|
-
if(el.messages[0].state !== 'sending'){
|
|
204
|
-
cleanup(container);
|
|
205
|
-
return fail(`Invalid state should be ignored, got "${el.messages[0].state}"`);
|
|
206
|
-
}
|
|
207
|
-
cleanup(container);
|
|
208
|
-
pass('updateMessage ignores invalid states');
|
|
209
|
-
},
|
|
210
|
-
|
|
211
|
-
'updateMessage should sanitize new HTML': async ({pass, fail}) => {
|
|
212
|
-
const { container, el } = await createChat();
|
|
213
|
-
const id = el.addMessage({ type: 'incoming', html: 'old' });
|
|
214
|
-
el.updateMessage(id, { html: 'new<script>x</script>' });
|
|
215
|
-
if(/script/i.test(el.messages[0].html)){
|
|
216
|
-
cleanup(container);
|
|
217
|
-
return fail('Updated HTML should be sanitized');
|
|
218
|
-
}
|
|
219
|
-
cleanup(container);
|
|
220
|
-
pass('updateMessage sanitizes new HTML');
|
|
221
|
-
},
|
|
222
|
-
|
|
223
|
-
'updateMessage should return false for unknown id': async ({pass, fail}) => {
|
|
224
|
-
const { container, el } = await createChat();
|
|
225
|
-
if(el.updateMessage('does-not-exist', { state: 'sent' }) !== false){
|
|
226
|
-
cleanup(container);
|
|
227
|
-
return fail('Should return false when id is unknown');
|
|
228
|
-
}
|
|
229
|
-
cleanup(container);
|
|
230
|
-
pass('updateMessage returns false for unknown id');
|
|
231
|
-
},
|
|
232
|
-
|
|
233
|
-
/*
|
|
234
|
-
removeMessage / clear
|
|
235
|
-
*/
|
|
236
|
-
'removeMessage should drop the matching message': async ({pass, fail}) => {
|
|
237
|
-
const { container, el } = await createChat();
|
|
238
|
-
const id1 = el.addMessage({ type: 'incoming', html: 'a' });
|
|
239
|
-
el.addMessage({ type: 'incoming', html: 'b' });
|
|
240
|
-
el.removeMessage(id1);
|
|
241
|
-
if(el.messages.length !== 1 || el.messages[0].html !== 'b'){
|
|
242
|
-
cleanup(container);
|
|
243
|
-
return fail(`Expected only "b" to remain, got ${JSON.stringify(el.messages.map(m => m.html))}`);
|
|
244
|
-
}
|
|
245
|
-
cleanup(container);
|
|
246
|
-
pass('removeMessage drops the right message');
|
|
247
|
-
},
|
|
248
|
-
|
|
249
|
-
'clear should remove all messages': async ({pass, fail}) => {
|
|
250
|
-
const { container, el } = await createChat();
|
|
251
|
-
el.addMessage({ type: 'incoming', html: 'a' });
|
|
252
|
-
el.addMessage({ type: 'incoming', html: 'b' });
|
|
253
|
-
el.clear();
|
|
254
|
-
if(el.messages.length !== 0){
|
|
255
|
-
cleanup(container);
|
|
256
|
-
return fail(`Expected 0 messages after clear, got ${el.messages.length}`);
|
|
257
|
-
}
|
|
258
|
-
cleanup(container);
|
|
259
|
-
pass('clear removes all messages');
|
|
260
|
-
},
|
|
261
|
-
|
|
262
|
-
/*
|
|
263
|
-
send()
|
|
264
|
-
*/
|
|
265
|
-
'send() should be a no-op when input is empty': async ({pass, fail}) => {
|
|
266
|
-
const { container, el } = await createChat();
|
|
267
|
-
const editor = el.shadowRoot.querySelector('k-html-editor');
|
|
268
|
-
editor.value = '';
|
|
269
|
-
const id = el.send();
|
|
270
|
-
if(id !== null){
|
|
271
|
-
cleanup(container);
|
|
272
|
-
return fail(`Expected null when input empty, got ${id}`);
|
|
273
|
-
}
|
|
274
|
-
if(el.messages.length !== 0){
|
|
275
|
-
cleanup(container);
|
|
276
|
-
return fail('No message should be added when input is empty');
|
|
277
|
-
}
|
|
278
|
-
cleanup(container);
|
|
279
|
-
pass('send() no-ops on empty input');
|
|
280
|
-
},
|
|
281
|
-
|
|
282
|
-
'send() should add an outgoing message and dispatch a send event': async ({pass, fail}) => {
|
|
283
|
-
const { container, el } = await createChat();
|
|
284
|
-
const editor = el.shadowRoot.querySelector('k-html-editor');
|
|
285
|
-
editor.value = '<p>hello</p>';
|
|
286
|
-
let detail = null;
|
|
287
|
-
el.addEventListener('send', (e) => { detail = e.detail; });
|
|
288
|
-
const id = el.send();
|
|
289
|
-
if(!id){
|
|
290
|
-
cleanup(container);
|
|
291
|
-
return fail('send() should return an id');
|
|
292
|
-
}
|
|
293
|
-
if(!detail || detail.id !== id || !detail.html.includes('hello')){
|
|
294
|
-
cleanup(container);
|
|
295
|
-
return fail(`Expected send event with detail.id and html, got ${JSON.stringify(detail)}`);
|
|
296
|
-
}
|
|
297
|
-
if(el.messages.length !== 1 || el.messages[0].type !== 'outgoing'){
|
|
298
|
-
cleanup(container);
|
|
299
|
-
return fail('send() should add an outgoing message');
|
|
300
|
-
}
|
|
301
|
-
cleanup(container);
|
|
302
|
-
pass('send() adds outgoing message and fires event');
|
|
303
|
-
},
|
|
304
|
-
|
|
305
|
-
'send() should clear the editor after submitting': async ({pass, fail}) => {
|
|
306
|
-
const { container, el } = await createChat();
|
|
307
|
-
const editor = el.shadowRoot.querySelector('k-html-editor');
|
|
308
|
-
editor.value = '<p>hi</p>';
|
|
309
|
-
el.send();
|
|
310
|
-
if(editor.value){
|
|
311
|
-
cleanup(container);
|
|
312
|
-
return fail(`Editor should be cleared, got "${editor.value}"`);
|
|
313
|
-
}
|
|
314
|
-
cleanup(container);
|
|
315
|
-
pass('Editor cleared after send');
|
|
316
|
-
},
|
|
317
|
-
|
|
318
|
-
'send() should reject pure-whitespace input': async ({pass, fail}) => {
|
|
319
|
-
const { container, el } = await createChat();
|
|
320
|
-
const editor = el.shadowRoot.querySelector('k-html-editor');
|
|
321
|
-
editor.value = ' \n \t ';
|
|
322
|
-
let sent = false;
|
|
323
|
-
el.addEventListener('send', () => { sent = true; });
|
|
324
|
-
const id = el.send();
|
|
325
|
-
if(id !== null || sent){
|
|
326
|
-
cleanup(container);
|
|
327
|
-
return fail('Pure-whitespace input should not be sent');
|
|
328
|
-
}
|
|
329
|
-
cleanup(container);
|
|
330
|
-
pass('Whitespace-only input rejected');
|
|
331
|
-
},
|
|
332
|
-
|
|
333
|
-
'send() should reject input with only empty paragraphs / br': async ({pass, fail}) => {
|
|
334
|
-
const { container, el } = await createChat();
|
|
335
|
-
const editor = el.shadowRoot.querySelector('k-html-editor');
|
|
336
|
-
editor.value = '<p></p><p><br></p><p> </p>';
|
|
337
|
-
let sent = false;
|
|
338
|
-
el.addEventListener('send', () => { sent = true; });
|
|
339
|
-
const id = el.send();
|
|
340
|
-
if(id !== null || sent){
|
|
341
|
-
cleanup(container);
|
|
342
|
-
return fail('Input that is only empty paragraphs/br should not be sent');
|
|
343
|
-
}
|
|
344
|
-
cleanup(container);
|
|
345
|
-
pass('Empty-paragraph input rejected');
|
|
346
|
-
},
|
|
347
|
-
|
|
348
|
-
'send() should preserve user-intentional blank lines around content': async ({pass, fail}) => {
|
|
349
|
-
const { container, el } = await createChat();
|
|
350
|
-
const editor = el.shadowRoot.querySelector('k-html-editor');
|
|
351
|
-
const raw = '<p><br></p><p>hello</p><p><br></p><p>world</p><p></p>';
|
|
352
|
-
editor.value = raw;
|
|
353
|
-
let detail = null;
|
|
354
|
-
el.addEventListener('send', (e) => { detail = e.detail; });
|
|
355
|
-
el.send();
|
|
356
|
-
if(!detail){
|
|
357
|
-
cleanup(container);
|
|
358
|
-
return fail('send should fire for non-empty input');
|
|
359
|
-
}
|
|
360
|
-
// The content the user typed (including any leading/trailing blank lines)
|
|
361
|
-
// should be sent as-is. Sanitization may rewrite the markup slightly but
|
|
362
|
-
// the structural blank lines must remain.
|
|
363
|
-
if(!detail.html.includes('<p>hello</p>') || !detail.html.includes('<p>world</p>')){
|
|
364
|
-
cleanup(container);
|
|
365
|
-
return fail(`Expected both content paragraphs preserved, got "${detail.html}"`);
|
|
366
|
-
}
|
|
367
|
-
// At least one empty paragraph between hello and world should remain.
|
|
368
|
-
const between = detail.html.split('<p>hello</p>')[1]?.split('<p>world</p>')[0] || '';
|
|
369
|
-
if(!/<br>|<p><\/p>/.test(between)){
|
|
370
|
-
cleanup(container);
|
|
371
|
-
return fail(`Expected the blank line between hello and world preserved, got "${detail.html}"`);
|
|
372
|
-
}
|
|
373
|
-
cleanup(container);
|
|
374
|
-
pass('Blank lines preserved in sent content');
|
|
375
|
-
},
|
|
376
|
-
|
|
377
|
-
'send() should sanitize the submitted HTML': async ({pass, fail}) => {
|
|
378
|
-
const { container, el } = await createChat();
|
|
379
|
-
const editor = el.shadowRoot.querySelector('k-html-editor');
|
|
380
|
-
editor.value = 'safe<script>alert(1)</script>';
|
|
381
|
-
let detail = null;
|
|
382
|
-
el.addEventListener('send', (e) => { detail = e.detail; });
|
|
383
|
-
el.send();
|
|
384
|
-
if(/script/i.test(detail.html)){
|
|
385
|
-
cleanup(container);
|
|
386
|
-
return fail(`Expected sanitized html in event detail, got "${detail.html}"`);
|
|
387
|
-
}
|
|
388
|
-
cleanup(container);
|
|
389
|
-
pass('send() sanitizes submitted HTML');
|
|
390
|
-
},
|
|
391
|
-
|
|
392
|
-
'send() should mark outgoing as sending when show-states is on': async ({pass, fail}) => {
|
|
393
|
-
const { container, el } = await createChat({ showStates: true });
|
|
394
|
-
const editor = el.shadowRoot.querySelector('k-html-editor');
|
|
395
|
-
editor.value = 'hi';
|
|
396
|
-
el.send();
|
|
397
|
-
if(el.messages[0].state !== 'sending'){
|
|
398
|
-
cleanup(container);
|
|
399
|
-
return fail(`Expected state "sending", got "${el.messages[0].state}"`);
|
|
400
|
-
}
|
|
401
|
-
cleanup(container);
|
|
402
|
-
pass('show-states sets initial state to sending');
|
|
403
|
-
},
|
|
404
|
-
|
|
405
|
-
/*
|
|
406
|
-
Disabled
|
|
407
|
-
*/
|
|
408
|
-
'should disable send button and editor when disabled attribute is set': async ({pass, fail}) => {
|
|
409
|
-
const { container, el } = await createChat({ disabled: true });
|
|
410
|
-
const send = el.shadowRoot.querySelector('.send-btn');
|
|
411
|
-
if(!send.disabled){
|
|
412
|
-
cleanup(container);
|
|
413
|
-
return fail('Send button should be disabled');
|
|
414
|
-
}
|
|
415
|
-
cleanup(container);
|
|
416
|
-
pass('Send button disabled');
|
|
417
|
-
},
|
|
418
|
-
|
|
419
|
-
'send() should be a no-op when disabled': async ({pass, fail}) => {
|
|
420
|
-
const { container, el } = await createChat({ disabled: true });
|
|
421
|
-
const editor = el.shadowRoot.querySelector('k-html-editor');
|
|
422
|
-
editor.value = 'hi';
|
|
423
|
-
const id = el.send();
|
|
424
|
-
if(id !== null){
|
|
425
|
-
cleanup(container);
|
|
426
|
-
return fail(`Disabled send should return null, got ${id}`);
|
|
427
|
-
}
|
|
428
|
-
cleanup(container);
|
|
429
|
-
pass('Disabled send no-ops');
|
|
430
|
-
},
|
|
431
|
-
|
|
432
|
-
/*
|
|
433
|
-
Enter-key behavior (default = Slack: Enter sends, Shift+Enter newline)
|
|
434
|
-
*/
|
|
435
|
-
'default: Enter should send (Slack-style)': async ({pass, fail}) => {
|
|
436
|
-
const { container, el } = await createChat();
|
|
437
|
-
const editor = el.shadowRoot.querySelector('k-html-editor');
|
|
438
|
-
editor.value = 'hi';
|
|
439
|
-
let sent = false;
|
|
440
|
-
el.addEventListener('send', () => { sent = true; });
|
|
441
|
-
const evt = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, composed: true, cancelable: true });
|
|
442
|
-
editor.dispatchEvent(evt);
|
|
443
|
-
await Promise.resolve();
|
|
444
|
-
await Promise.resolve();
|
|
445
|
-
if(!sent){
|
|
446
|
-
cleanup(container);
|
|
447
|
-
return fail('Default behavior should send on Enter');
|
|
448
|
-
}
|
|
449
|
-
cleanup(container);
|
|
450
|
-
pass('Enter sends by default');
|
|
451
|
-
},
|
|
452
|
-
|
|
453
|
-
'default: Shift+Enter should NOT send (newline)': async ({pass, fail}) => {
|
|
454
|
-
const { container, el } = await createChat();
|
|
455
|
-
const editor = el.shadowRoot.querySelector('k-html-editor');
|
|
456
|
-
editor.value = 'hi';
|
|
457
|
-
let sent = false;
|
|
458
|
-
el.addEventListener('send', () => { sent = true; });
|
|
459
|
-
const evt = new KeyboardEvent('keydown', { key: 'Enter', shiftKey: true, bubbles: true, composed: true, cancelable: true });
|
|
460
|
-
editor.dispatchEvent(evt);
|
|
461
|
-
await Promise.resolve();
|
|
462
|
-
await Promise.resolve();
|
|
463
|
-
if(sent){
|
|
464
|
-
cleanup(container);
|
|
465
|
-
return fail('Default Shift+Enter should NOT send');
|
|
466
|
-
}
|
|
467
|
-
cleanup(container);
|
|
468
|
-
pass('Shift+Enter does not send by default');
|
|
469
|
-
},
|
|
470
|
-
|
|
471
|
-
'enter-newline: Enter should NOT send (newline)': async ({pass, fail}) => {
|
|
472
|
-
const { container, el } = await createChat({ enterNewline: true });
|
|
473
|
-
const editor = el.shadowRoot.querySelector('k-html-editor');
|
|
474
|
-
editor.value = 'hi';
|
|
475
|
-
let sent = false;
|
|
476
|
-
el.addEventListener('send', () => { sent = true; });
|
|
477
|
-
const evt = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, composed: true, cancelable: true });
|
|
478
|
-
editor.dispatchEvent(evt);
|
|
479
|
-
await Promise.resolve();
|
|
480
|
-
await Promise.resolve();
|
|
481
|
-
if(sent){
|
|
482
|
-
cleanup(container);
|
|
483
|
-
return fail('With enter-newline, Enter should NOT send');
|
|
484
|
-
}
|
|
485
|
-
cleanup(container);
|
|
486
|
-
pass('Enter inserts newline when enter-newline is set');
|
|
487
|
-
},
|
|
488
|
-
|
|
489
|
-
'enter-newline: Shift+Enter should send': async ({pass, fail}) => {
|
|
490
|
-
const { container, el } = await createChat({ enterNewline: true });
|
|
491
|
-
const editor = el.shadowRoot.querySelector('k-html-editor');
|
|
492
|
-
editor.value = 'hi';
|
|
493
|
-
let sent = false;
|
|
494
|
-
el.addEventListener('send', () => { sent = true; });
|
|
495
|
-
const evt = new KeyboardEvent('keydown', { key: 'Enter', shiftKey: true, bubbles: true, composed: true, cancelable: true });
|
|
496
|
-
editor.dispatchEvent(evt);
|
|
497
|
-
await Promise.resolve();
|
|
498
|
-
await Promise.resolve();
|
|
499
|
-
if(!sent){
|
|
500
|
-
cleanup(container);
|
|
501
|
-
return fail('With enter-newline, Shift+Enter should send');
|
|
502
|
-
}
|
|
503
|
-
cleanup(container);
|
|
504
|
-
pass('Shift+Enter sends when enter-newline is set');
|
|
505
|
-
},
|
|
506
|
-
|
|
507
|
-
/*
|
|
508
|
-
State indicator
|
|
509
|
-
*/
|
|
510
|
-
'show-states should render a state indicator on outgoing messages': async ({pass, fail}) => {
|
|
511
|
-
const { container, el } = await createChat({ showStates: true });
|
|
512
|
-
el.addMessage({ type: 'outgoing', html: 'hi', state: 'sending' });
|
|
513
|
-
el.addMessage({ type: 'incoming', html: 'hi back' });
|
|
514
|
-
await el.updateComplete;
|
|
515
|
-
const els = messageEls(el);
|
|
516
|
-
if(!els[0].querySelector('.meta')){
|
|
517
|
-
cleanup(container);
|
|
518
|
-
return fail('Outgoing message should have a .meta indicator');
|
|
519
|
-
}
|
|
520
|
-
if(els[1].querySelector('.meta')){
|
|
521
|
-
cleanup(container);
|
|
522
|
-
return fail('Incoming message should NOT have a state indicator');
|
|
523
|
-
}
|
|
524
|
-
cleanup(container);
|
|
525
|
-
pass('State indicator only on outgoing messages');
|
|
526
|
-
},
|
|
527
|
-
|
|
528
|
-
'without show-states, no state indicator is rendered': async ({pass, fail}) => {
|
|
529
|
-
const { container, el } = await createChat();
|
|
530
|
-
el.addMessage({ type: 'outgoing', html: 'hi', state: 'sending' });
|
|
531
|
-
await el.updateComplete;
|
|
532
|
-
const els = messageEls(el);
|
|
533
|
-
if(els[0].querySelector('.meta')){
|
|
534
|
-
cleanup(container);
|
|
535
|
-
return fail('Should not render .meta when show-states is off');
|
|
536
|
-
}
|
|
537
|
-
cleanup(container);
|
|
538
|
-
pass('No state indicator without show-states');
|
|
539
|
-
}
|
|
540
|
-
};
|