@meetelise/chat 1.59.0 → 1.59.2
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/dist/src/WebComponent/Scheduler/tour-scheduler.d.ts +4 -0
- package/dist/src/WebComponent/actions/call-us-window.d.ts +5 -0
- package/dist/src/WebComponent/actions/email-us-window.d.ts +5 -0
- package/dist/src/WebComponent/me-chat.d.ts +15 -9
- package/dist/src/WebComponent/pubnub-chat.d.ts +14 -0
- package/package.json +1 -1
- package/public/dist/index.js +5594 -5573
- package/src/WebComponent/Scheduler/tour-scheduler.ts +27 -0
- package/src/WebComponent/actions/call-us-window.ts +22 -0
- package/src/WebComponent/actions/email-us-window.ts +22 -0
- package/src/WebComponent/me-chat.ts +89 -136
- package/src/WebComponent/me-select.ts +1 -1
- package/src/WebComponent/pubnub-chat-styles.ts +12 -0
- package/src/WebComponent/pubnub-chat.ts +186 -7
- package/src/chatKeyboard.test.ts +430 -0
- package/src/disclaimers.ts +36 -3
- package/src/meChatAutoOpen.test.ts +143 -0
- package/src/pubnub-chat.test.ts +184 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { expect } from "@esm-bundle/chai";
|
|
2
|
+
import { sendKeys } from "@web/test-runner-commands";
|
|
3
|
+
import "../public/dist/index";
|
|
4
|
+
import { CURRENT_CONSENT_ID } from "./disclaimers";
|
|
5
|
+
import type { BuildingWebchatView } from "./fetchBuildingWebchatView";
|
|
6
|
+
import type { MEChat } from "./WebComponent/me-chat";
|
|
7
|
+
import type { MESelect } from "./WebComponent/me-select";
|
|
8
|
+
import type { PubnubChat } from "./WebComponent/pubnub-chat";
|
|
9
|
+
import type { TourScheduler } from "./WebComponent/Scheduler/tour-scheduler";
|
|
10
|
+
|
|
11
|
+
declare const afterEach: (callback: () => void) => void;
|
|
12
|
+
declare const it: (name: string, callback: () => void | Promise<void>) => void;
|
|
13
|
+
|
|
14
|
+
type MeChatTest = MEChat & {
|
|
15
|
+
updateComplete: Promise<boolean>;
|
|
16
|
+
displayPubnubChat: boolean;
|
|
17
|
+
buildingWebchatView: BuildingWebchatView | null;
|
|
18
|
+
buildingSlug: string;
|
|
19
|
+
orgSlug: string;
|
|
20
|
+
focusChatOnMount: boolean;
|
|
21
|
+
requiresConsent: boolean;
|
|
22
|
+
launcherButton: HTMLElement | null;
|
|
23
|
+
hideLauncher: boolean;
|
|
24
|
+
isLoading: boolean;
|
|
25
|
+
_isMobile: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const building = {
|
|
29
|
+
id: 1,
|
|
30
|
+
orgId: 1,
|
|
31
|
+
name: "Aventine",
|
|
32
|
+
welcomeMessage: "Welcome",
|
|
33
|
+
state: "NY",
|
|
34
|
+
country: "US",
|
|
35
|
+
layoutOptions: [],
|
|
36
|
+
tourTypeOptions: [],
|
|
37
|
+
} as unknown as BuildingWebchatView;
|
|
38
|
+
|
|
39
|
+
const nextTask = (): Promise<void> =>
|
|
40
|
+
new Promise((resolve) => {
|
|
41
|
+
setTimeout(resolve, 0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const deepActive = (): Element | null => {
|
|
45
|
+
let active: Element | null = document.activeElement;
|
|
46
|
+
while (active?.shadowRoot?.activeElement) {
|
|
47
|
+
active = active.shadowRoot.activeElement;
|
|
48
|
+
}
|
|
49
|
+
return active;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const tabStops = (container: HTMLElement): HTMLElement[] => {
|
|
53
|
+
const stops: HTMLElement[] = [];
|
|
54
|
+
const visit = (node: ParentNode): void => {
|
|
55
|
+
if (node instanceof Element && node.shadowRoot) visit(node.shadowRoot);
|
|
56
|
+
node.querySelectorAll<HTMLElement>("*").forEach((el) => {
|
|
57
|
+
const focusable =
|
|
58
|
+
el.tagName === "BUTTON" ||
|
|
59
|
+
el.tagName === "TEXTAREA" ||
|
|
60
|
+
el.tagName === "INPUT" ||
|
|
61
|
+
(el.tagName === "A" && el.hasAttribute("href"));
|
|
62
|
+
if (
|
|
63
|
+
focusable &&
|
|
64
|
+
el.getAttribute("tabindex") !== "-1" &&
|
|
65
|
+
el.getClientRects().length > 0 &&
|
|
66
|
+
!("disabled" in el && (el as HTMLButtonElement).disabled)
|
|
67
|
+
) {
|
|
68
|
+
stops.push(el);
|
|
69
|
+
}
|
|
70
|
+
if (el.shadowRoot) visit(el.shadowRoot);
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
visit(container);
|
|
74
|
+
return stops;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const dispatchTab = (target: HTMLElement, shiftKey = false): KeyboardEvent => {
|
|
78
|
+
const event = new KeyboardEvent("keydown", {
|
|
79
|
+
key: "Tab",
|
|
80
|
+
bubbles: true,
|
|
81
|
+
cancelable: true,
|
|
82
|
+
composed: true,
|
|
83
|
+
shiftKey,
|
|
84
|
+
});
|
|
85
|
+
target.dispatchEvent(event);
|
|
86
|
+
return event;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const mountChat = async (mobile: boolean): Promise<PubnubChat> => {
|
|
90
|
+
const chat = document.createElement("pubnub-chat") as PubnubChat;
|
|
91
|
+
chat.buildingSlug = "test-building";
|
|
92
|
+
chat.orgSlug = "test-org";
|
|
93
|
+
chat.building = building;
|
|
94
|
+
chat._isMobile = mobile;
|
|
95
|
+
document.body.appendChild(chat);
|
|
96
|
+
await chat.updateComplete;
|
|
97
|
+
return chat;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
afterEach(() => {
|
|
101
|
+
document.body.innerHTML = "";
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("keeps the desktop dialog non-modal and does not wrap Tab", async () => {
|
|
105
|
+
const chat = await mountChat(false);
|
|
106
|
+
const container = chat.shadowRoot?.getElementById("pubnub-chat-container");
|
|
107
|
+
expect(container?.getAttribute("role")).to.equal("dialog");
|
|
108
|
+
expect(container?.hasAttribute("aria-modal")).to.equal(false);
|
|
109
|
+
|
|
110
|
+
const stops = tabStops(container as HTMLElement);
|
|
111
|
+
const last = stops[stops.length - 1];
|
|
112
|
+
last.focus();
|
|
113
|
+
const event = dispatchTab(last);
|
|
114
|
+
expect(event.defaultPrevented).to.equal(false);
|
|
115
|
+
expect(deepActive()).to.equal(last);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("wraps Tab inside the tour overlay and does not wrap on desktop chat", async () => {
|
|
119
|
+
const chat = await mountChat(false);
|
|
120
|
+
const container = chat.shadowRoot?.getElementById(
|
|
121
|
+
"pubnub-chat-container"
|
|
122
|
+
) as HTMLElement;
|
|
123
|
+
expect(container.hasAttribute("aria-modal")).to.equal(false);
|
|
124
|
+
const chatStops = tabStops(container);
|
|
125
|
+
const chatLast = chatStops[chatStops.length - 1];
|
|
126
|
+
chatLast.focus();
|
|
127
|
+
const chatTab = dispatchTab(chatLast);
|
|
128
|
+
expect(chatTab.defaultPrevented).to.equal(false);
|
|
129
|
+
expect(deepActive()).to.equal(chatLast);
|
|
130
|
+
|
|
131
|
+
const tour = document.createElement("tour-scheduler") as TourScheduler & {
|
|
132
|
+
firstUpdated: () => Promise<void>;
|
|
133
|
+
_loadForCurrentBuilding: () => Promise<void>;
|
|
134
|
+
waitingForAvailabilities: boolean;
|
|
135
|
+
shouldAllowScheduleLoading: boolean;
|
|
136
|
+
shouldAllowScheduling: boolean;
|
|
137
|
+
updateComplete: Promise<boolean>;
|
|
138
|
+
};
|
|
139
|
+
tour.firstUpdated = async () => undefined;
|
|
140
|
+
tour._loadForCurrentBuilding = async () => undefined;
|
|
141
|
+
tour.waitingForAvailabilities = false;
|
|
142
|
+
tour.shouldAllowScheduleLoading = false;
|
|
143
|
+
tour.shouldAllowScheduling = true;
|
|
144
|
+
tour.primaryColor = "#646987";
|
|
145
|
+
document.body.appendChild(tour);
|
|
146
|
+
await tour.updateComplete;
|
|
147
|
+
tour.waitingForAvailabilities = false;
|
|
148
|
+
tour.shouldAllowScheduleLoading = false;
|
|
149
|
+
tour.shouldAllowScheduling = true;
|
|
150
|
+
await tour.updateComplete;
|
|
151
|
+
|
|
152
|
+
const stops = tabStops(tour);
|
|
153
|
+
expect(stops.length).to.be.greaterThan(1);
|
|
154
|
+
expect(stops.includes(deepActive() as HTMLElement)).to.equal(true);
|
|
155
|
+
const first = stops[0];
|
|
156
|
+
const last = stops[stops.length - 1];
|
|
157
|
+
last.focus();
|
|
158
|
+
const wrapped = dispatchTab(last);
|
|
159
|
+
expect(wrapped.defaultPrevented).to.equal(true);
|
|
160
|
+
expect(deepActive()).to.equal(first);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("keeps Tab inside the tour overlay while it is loading", async () => {
|
|
164
|
+
const tour = document.createElement("tour-scheduler") as TourScheduler & {
|
|
165
|
+
firstUpdated: () => Promise<void>;
|
|
166
|
+
_loadForCurrentBuilding: () => Promise<void>;
|
|
167
|
+
updateComplete: Promise<boolean>;
|
|
168
|
+
};
|
|
169
|
+
tour.firstUpdated = async () => undefined;
|
|
170
|
+
tour._loadForCurrentBuilding = async () => undefined;
|
|
171
|
+
tour.primaryColor = "#646987";
|
|
172
|
+
document.body.appendChild(tour);
|
|
173
|
+
await tour.updateComplete;
|
|
174
|
+
|
|
175
|
+
expect(tabStops(tour).length).to.equal(0);
|
|
176
|
+
expect(deepActive()).to.equal(tour);
|
|
177
|
+
const held = dispatchTab(tour);
|
|
178
|
+
expect(held.defaultPrevented).to.equal(true);
|
|
179
|
+
expect(deepActive()).to.equal(tour);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("makes the mobile dialog modal and wraps Tab inside it", async () => {
|
|
183
|
+
const chat = await mountChat(true);
|
|
184
|
+
const container = chat.shadowRoot?.getElementById(
|
|
185
|
+
"pubnub-chat-container"
|
|
186
|
+
) as HTMLElement;
|
|
187
|
+
expect(container.getAttribute("aria-modal")).to.equal("true");
|
|
188
|
+
|
|
189
|
+
const stops = tabStops(container);
|
|
190
|
+
expect(stops.length).to.be.greaterThan(1);
|
|
191
|
+
const first = stops[0];
|
|
192
|
+
const last = stops[stops.length - 1];
|
|
193
|
+
|
|
194
|
+
last.focus();
|
|
195
|
+
const forward = dispatchTab(last);
|
|
196
|
+
expect(forward.defaultPrevented).to.equal(true);
|
|
197
|
+
expect(deepActive()).to.equal(first);
|
|
198
|
+
|
|
199
|
+
first.focus();
|
|
200
|
+
const backward = dispatchTab(first, true);
|
|
201
|
+
expect(backward.defaultPrevented).to.equal(true);
|
|
202
|
+
expect(deepActive()).to.equal(last);
|
|
203
|
+
|
|
204
|
+
const outside = document.createElement("button");
|
|
205
|
+
outside.textContent = "Page";
|
|
206
|
+
document.body.appendChild(outside);
|
|
207
|
+
outside.focus();
|
|
208
|
+
const leaked = dispatchTab(outside);
|
|
209
|
+
expect(leaked.defaultPrevented).to.equal(false);
|
|
210
|
+
expect(deepActive()).to.equal(outside);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("focuses the message field on a user open and not on auto-open", async () => {
|
|
214
|
+
const pageInput = document.createElement("input");
|
|
215
|
+
document.body.appendChild(pageInput);
|
|
216
|
+
pageInput.focus();
|
|
217
|
+
|
|
218
|
+
(window as unknown as { eliseAi: Record<string, unknown> }).eliseAi = {};
|
|
219
|
+
const widget = document.createElement("me-chat") as MeChatTest;
|
|
220
|
+
document.body.appendChild(widget);
|
|
221
|
+
await widget.updateComplete;
|
|
222
|
+
widget.buildingSlug = "test-building";
|
|
223
|
+
widget.orgSlug = "test-org";
|
|
224
|
+
widget.primaryColor = "#646987";
|
|
225
|
+
widget.backgroundColor = "#ffffff";
|
|
226
|
+
widget.buildingWebchatView = building;
|
|
227
|
+
widget.focusChatOnMount = false;
|
|
228
|
+
widget.displayPubnubChat = true;
|
|
229
|
+
await widget.updateComplete;
|
|
230
|
+
const pubnub = widget.shadowRoot?.getElementById("pubnub-chat") as PubnubChat;
|
|
231
|
+
await pubnub.updateComplete;
|
|
232
|
+
expect(deepActive()).to.equal(pageInput);
|
|
233
|
+
expect(widget.focusChatOnMount).to.equal(false);
|
|
234
|
+
|
|
235
|
+
widget.displayPubnubChat = false;
|
|
236
|
+
await widget.updateComplete;
|
|
237
|
+
widget.focusChatOnMount = true;
|
|
238
|
+
widget.displayPubnubChat = true;
|
|
239
|
+
await widget.updateComplete;
|
|
240
|
+
const reopened = widget.shadowRoot?.getElementById(
|
|
241
|
+
"pubnub-chat"
|
|
242
|
+
) as PubnubChat;
|
|
243
|
+
await reopened.updateComplete;
|
|
244
|
+
await nextTask();
|
|
245
|
+
expect(deepActive()?.id).to.equal("message-input");
|
|
246
|
+
expect(widget.focusChatOnMount).to.equal(false);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("focuses the message field on open when consent is already stored", async () => {
|
|
250
|
+
const consentKey = "stored-consent-orgConsent";
|
|
251
|
+
localStorage.setItem(
|
|
252
|
+
consentKey,
|
|
253
|
+
JSON.stringify({
|
|
254
|
+
consentGiven: true,
|
|
255
|
+
timestamp: Date.now(),
|
|
256
|
+
consentId: CURRENT_CONSENT_ID,
|
|
257
|
+
})
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
const pageInput = document.createElement("input");
|
|
261
|
+
document.body.appendChild(pageInput);
|
|
262
|
+
pageInput.focus();
|
|
263
|
+
|
|
264
|
+
(window as unknown as { eliseAi: Record<string, unknown> }).eliseAi = {};
|
|
265
|
+
const widget = document.createElement("me-chat") as MeChatTest;
|
|
266
|
+
document.body.appendChild(widget);
|
|
267
|
+
await widget.updateComplete;
|
|
268
|
+
widget.buildingSlug = "test-building";
|
|
269
|
+
widget.orgSlug = "stored-consent-org";
|
|
270
|
+
widget.primaryColor = "#646987";
|
|
271
|
+
widget.backgroundColor = "#ffffff";
|
|
272
|
+
widget.buildingWebchatView = building;
|
|
273
|
+
widget.requiresConsent = true;
|
|
274
|
+
widget.focusChatOnMount = false;
|
|
275
|
+
widget.displayPubnubChat = true;
|
|
276
|
+
await widget.updateComplete;
|
|
277
|
+
const pubnub = widget.shadowRoot?.getElementById("pubnub-chat") as PubnubChat;
|
|
278
|
+
await pubnub.updateComplete;
|
|
279
|
+
await nextTask();
|
|
280
|
+
expect(deepActive()).to.equal(pageInput);
|
|
281
|
+
|
|
282
|
+
widget.displayPubnubChat = false;
|
|
283
|
+
await widget.updateComplete;
|
|
284
|
+
widget.focusChatOnMount = true;
|
|
285
|
+
widget.displayPubnubChat = true;
|
|
286
|
+
await widget.updateComplete;
|
|
287
|
+
const reopened = widget.shadowRoot?.getElementById(
|
|
288
|
+
"pubnub-chat"
|
|
289
|
+
) as PubnubChat;
|
|
290
|
+
await reopened.updateComplete;
|
|
291
|
+
await nextTask();
|
|
292
|
+
expect(deepActive()?.id).to.equal("message-input");
|
|
293
|
+
localStorage.removeItem(consentKey);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("focuses the dialog until consent, then the message field", async () => {
|
|
297
|
+
localStorage.removeItem("consent-gate-orgConsent");
|
|
298
|
+
(window as unknown as { eliseAi: Record<string, unknown> }).eliseAi = {};
|
|
299
|
+
const widget = document.createElement("me-chat") as MeChatTest;
|
|
300
|
+
document.body.appendChild(widget);
|
|
301
|
+
await widget.updateComplete;
|
|
302
|
+
widget.buildingSlug = "test-building";
|
|
303
|
+
widget.orgSlug = "consent-gate-org";
|
|
304
|
+
widget.primaryColor = "#646987";
|
|
305
|
+
widget.backgroundColor = "#ffffff";
|
|
306
|
+
widget.buildingWebchatView = building;
|
|
307
|
+
widget.requiresConsent = true;
|
|
308
|
+
widget.focusChatOnMount = true;
|
|
309
|
+
widget.displayPubnubChat = true;
|
|
310
|
+
await widget.updateComplete;
|
|
311
|
+
const pubnub = widget.shadowRoot?.getElementById("pubnub-chat") as PubnubChat;
|
|
312
|
+
await pubnub.updateComplete;
|
|
313
|
+
await nextTask();
|
|
314
|
+
expect(deepActive()?.id).to.equal("pubnub-chat-container");
|
|
315
|
+
|
|
316
|
+
const agree = pubnub.shadowRoot?.querySelector(
|
|
317
|
+
".agreeButton"
|
|
318
|
+
) as HTMLButtonElement | null;
|
|
319
|
+
expect(agree?.textContent?.trim()).to.equal("I agree");
|
|
320
|
+
agree?.click();
|
|
321
|
+
await pubnub.updateComplete;
|
|
322
|
+
await nextTask();
|
|
323
|
+
expect(deepActive()?.id).to.equal("message-input");
|
|
324
|
+
const footer = pubnub.shadowRoot?.getElementById("footer");
|
|
325
|
+
expect(footer?.classList.contains("hidden")).to.equal(false);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("returns focus to the launcher from Minimize chat", async () => {
|
|
329
|
+
const launcher = document.createElement("button");
|
|
330
|
+
launcher.textContent = "Chat with us";
|
|
331
|
+
document.body.appendChild(launcher);
|
|
332
|
+
|
|
333
|
+
(window as unknown as { eliseAi: Record<string, unknown> }).eliseAi = {};
|
|
334
|
+
const widget = document.createElement("me-chat") as MeChatTest;
|
|
335
|
+
document.body.appendChild(widget);
|
|
336
|
+
await widget.updateComplete;
|
|
337
|
+
widget.buildingSlug = "test-building";
|
|
338
|
+
widget.orgSlug = "test-org";
|
|
339
|
+
widget.primaryColor = "#646987";
|
|
340
|
+
widget.backgroundColor = "#ffffff";
|
|
341
|
+
widget.buildingWebchatView = building;
|
|
342
|
+
widget._isMobile = false;
|
|
343
|
+
widget.isLoading = false;
|
|
344
|
+
widget.hideLauncher = true;
|
|
345
|
+
widget.displayPubnubChat = true;
|
|
346
|
+
await widget.updateComplete;
|
|
347
|
+
widget.launcherButton = launcher;
|
|
348
|
+
|
|
349
|
+
const actions = widget.shadowRoot?.getElementById(
|
|
350
|
+
"chatAdditionalActionsPubnub"
|
|
351
|
+
);
|
|
352
|
+
const miniHost = actions?.shadowRoot?.querySelector(
|
|
353
|
+
"minimize-expand-button"
|
|
354
|
+
) as HTMLElement & { updateComplete?: Promise<boolean> };
|
|
355
|
+
await miniHost?.updateComplete;
|
|
356
|
+
const minimize = miniHost?.shadowRoot?.querySelector("button");
|
|
357
|
+
expect(minimize?.getAttribute("aria-label")).to.equal("Minimize chat");
|
|
358
|
+
minimize?.click();
|
|
359
|
+
await widget.updateComplete;
|
|
360
|
+
expect(widget.displayPubnubChat).to.equal(false);
|
|
361
|
+
expect(deepActive()).to.equal(launcher);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("returns focus to the launcher on Escape", async () => {
|
|
365
|
+
const launcher = document.createElement("button");
|
|
366
|
+
launcher.setAttribute("aria-label", "Chat with us");
|
|
367
|
+
launcher.textContent = "Chat with us";
|
|
368
|
+
document.body.appendChild(launcher);
|
|
369
|
+
|
|
370
|
+
(window as unknown as { eliseAi: Record<string, unknown> }).eliseAi = {};
|
|
371
|
+
const widget = document.createElement("me-chat") as MeChatTest;
|
|
372
|
+
document.body.appendChild(widget);
|
|
373
|
+
await widget.updateComplete;
|
|
374
|
+
widget.buildingSlug = "test-building";
|
|
375
|
+
widget.orgSlug = "test-org";
|
|
376
|
+
widget.primaryColor = "#646987";
|
|
377
|
+
widget.backgroundColor = "#ffffff";
|
|
378
|
+
widget.buildingWebchatView = building;
|
|
379
|
+
widget._isMobile = false;
|
|
380
|
+
widget.isLoading = false;
|
|
381
|
+
widget.hideLauncher = true;
|
|
382
|
+
widget.displayPubnubChat = true;
|
|
383
|
+
await widget.updateComplete;
|
|
384
|
+
widget.launcherButton = launcher;
|
|
385
|
+
|
|
386
|
+
const pubnub = widget.shadowRoot?.getElementById("pubnub-chat") as PubnubChat;
|
|
387
|
+
await pubnub.updateComplete;
|
|
388
|
+
const container = pubnub.shadowRoot?.getElementById(
|
|
389
|
+
"pubnub-chat-container"
|
|
390
|
+
) as HTMLElement;
|
|
391
|
+
const input = pubnub.shadowRoot?.getElementById(
|
|
392
|
+
"message-input"
|
|
393
|
+
) as HTMLTextAreaElement;
|
|
394
|
+
input.focus();
|
|
395
|
+
container.dispatchEvent(
|
|
396
|
+
new KeyboardEvent("keydown", {
|
|
397
|
+
key: "Escape",
|
|
398
|
+
bubbles: true,
|
|
399
|
+
composed: true,
|
|
400
|
+
})
|
|
401
|
+
);
|
|
402
|
+
await widget.updateComplete;
|
|
403
|
+
expect(widget.displayPubnubChat).to.equal(false);
|
|
404
|
+
expect(deepActive()).to.equal(launcher);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("moves Tab from an open, scrolling select to the next control", async () => {
|
|
408
|
+
const select = document.createElement("me-select") as MESelect & {
|
|
409
|
+
updateComplete: Promise<boolean>;
|
|
410
|
+
};
|
|
411
|
+
select.options = Array.from({ length: 20 }, (_, i) => ({
|
|
412
|
+
label: `Option ${i}`,
|
|
413
|
+
value: `${i}`,
|
|
414
|
+
}));
|
|
415
|
+
const next = document.createElement("button");
|
|
416
|
+
next.textContent = "Next";
|
|
417
|
+
document.body.append(select, next);
|
|
418
|
+
await select.updateComplete;
|
|
419
|
+
|
|
420
|
+
const input = select.shadowRoot?.querySelector<HTMLInputElement>(
|
|
421
|
+
"#selectText"
|
|
422
|
+
) as HTMLInputElement;
|
|
423
|
+
input.focus();
|
|
424
|
+
input.click();
|
|
425
|
+
await select.updateComplete;
|
|
426
|
+
expect(select.shadowRoot?.querySelector('[role="listbox"]')).to.exist;
|
|
427
|
+
|
|
428
|
+
await sendKeys({ press: "Tab" });
|
|
429
|
+
expect(deepActive()).to.equal(next);
|
|
430
|
+
});
|
package/src/disclaimers.ts
CHANGED
|
@@ -314,6 +314,36 @@ export const getConsentPreChatDisclaimer = (
|
|
|
314
314
|
? CANADA_PRIVACY_POLICY_URL
|
|
315
315
|
: privacyPolicyUrl;
|
|
316
316
|
|
|
317
|
+
const focusMessageInput = (event: Event): void => {
|
|
318
|
+
const root = (event.currentTarget as HTMLElement | null)?.getRootNode();
|
|
319
|
+
if (!(root instanceof ShadowRoot)) return;
|
|
320
|
+
const host = root.host as HTMLElement & {
|
|
321
|
+
updateComplete?: Promise<unknown>;
|
|
322
|
+
focusMessageField?: () => boolean;
|
|
323
|
+
};
|
|
324
|
+
// The agree control is what has focus. Hiding it sends focus to the dialog
|
|
325
|
+
// (tabindex=-1). Move the caret to the message field after that.
|
|
326
|
+
const focus = (): void => {
|
|
327
|
+
if (typeof host.focusMessageField === "function") {
|
|
328
|
+
host.focusMessageField();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const input = root.getElementById("message-input");
|
|
332
|
+
if (input instanceof HTMLElement && input.getClientRects().length > 0) {
|
|
333
|
+
input.focus();
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
const afterUpdate = (): void => {
|
|
337
|
+
focus();
|
|
338
|
+
setTimeout(focus, 0);
|
|
339
|
+
};
|
|
340
|
+
if (host.updateComplete) {
|
|
341
|
+
void host.updateComplete.then(afterUpdate);
|
|
342
|
+
} else {
|
|
343
|
+
afterUpdate();
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
317
347
|
if (orgSlug === "avb" && !localStorage.getItem(consentKey ?? "")) {
|
|
318
348
|
return html`
|
|
319
349
|
<div class="legal-confirmation-inner-container">
|
|
@@ -349,8 +379,9 @@ export const getConsentPreChatDisclaimer = (
|
|
|
349
379
|
|
|
350
380
|
<button
|
|
351
381
|
class="agreeButton"
|
|
352
|
-
@click=${() => {
|
|
382
|
+
@click=${(e: Event) => {
|
|
353
383
|
changeConsent(true);
|
|
384
|
+
focusMessageInput(e);
|
|
354
385
|
const consentData = {
|
|
355
386
|
consentGiven: true,
|
|
356
387
|
timestamp: new Date().getTime(),
|
|
@@ -379,8 +410,9 @@ export const getConsentPreChatDisclaimer = (
|
|
|
379
410
|
<div class="confirmation-with-checkbox">
|
|
380
411
|
<div
|
|
381
412
|
class="checkbox"
|
|
382
|
-
@click=${() => {
|
|
413
|
+
@click=${(e: Event) => {
|
|
383
414
|
changeConsent(true);
|
|
415
|
+
focusMessageInput(e);
|
|
384
416
|
}}
|
|
385
417
|
>
|
|
386
418
|
${CheckboxEmpty}
|
|
@@ -422,8 +454,9 @@ export const getConsentPreChatDisclaimer = (
|
|
|
422
454
|
|
|
423
455
|
<button
|
|
424
456
|
class="agreeButton"
|
|
425
|
-
@click=${() => {
|
|
457
|
+
@click=${(e: Event) => {
|
|
426
458
|
changeConsent(true);
|
|
459
|
+
focusMessageInput(e);
|
|
427
460
|
}}
|
|
428
461
|
>
|
|
429
462
|
I agree
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { expect } from "@esm-bundle/chai";
|
|
2
|
+
import { restore, stub, useFakeXMLHttpRequest } from "sinon/pkg/sinon-esm";
|
|
3
|
+
import "../public/dist/index";
|
|
4
|
+
import type { BuildingWebchatView } from "./fetchBuildingWebchatView";
|
|
5
|
+
|
|
6
|
+
declare const afterEach: (callback: () => void) => void;
|
|
7
|
+
declare const beforeEach: (callback: () => void) => void;
|
|
8
|
+
declare const it: (name: string, callback: () => void | Promise<void>) => void;
|
|
9
|
+
|
|
10
|
+
type FakeRequest = XMLHttpRequest & {
|
|
11
|
+
method: string;
|
|
12
|
+
url: string;
|
|
13
|
+
requestBody?: string;
|
|
14
|
+
respond: (
|
|
15
|
+
status: number,
|
|
16
|
+
headers?: Record<string, string>,
|
|
17
|
+
body?: string
|
|
18
|
+
) => void;
|
|
19
|
+
onSend?: () => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type MeChatInternals = HTMLElement & {
|
|
23
|
+
updateComplete: Promise<boolean>;
|
|
24
|
+
isLoading: boolean;
|
|
25
|
+
displayPubnubChat: boolean;
|
|
26
|
+
onExitChat: () => void;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const view = (overrides: Partial<BuildingWebchatView>) => ({
|
|
30
|
+
id: 445038,
|
|
31
|
+
orgId: 53,
|
|
32
|
+
name: "Test Building",
|
|
33
|
+
userId: 7,
|
|
34
|
+
active: 1,
|
|
35
|
+
userFirstName: "Elise",
|
|
36
|
+
welcomeMessage: "Welcome, I'm Elise!",
|
|
37
|
+
launchedDateTime: "2024-01-01T00:00:00Z",
|
|
38
|
+
webchatScopeConfigured: true,
|
|
39
|
+
displayStyle: "pills",
|
|
40
|
+
shouldShowChatDesktop: true,
|
|
41
|
+
shouldShowChatMobile: true,
|
|
42
|
+
phoneNumber: null,
|
|
43
|
+
layoutOptions: [],
|
|
44
|
+
tourTypeOptions: [],
|
|
45
|
+
autoOpenChat: true,
|
|
46
|
+
...overrides,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const fakeBackend = (): FakeRequest[] => {
|
|
50
|
+
const requests: FakeRequest[] = [];
|
|
51
|
+
const fakeXhr = useFakeXMLHttpRequest();
|
|
52
|
+
fakeXhr.onCreate = (request: FakeRequest) => {
|
|
53
|
+
requests.push(request);
|
|
54
|
+
request.onSend = () =>
|
|
55
|
+
setTimeout(() => {
|
|
56
|
+
const json = (status: number, body: unknown) =>
|
|
57
|
+
request.respond(
|
|
58
|
+
status,
|
|
59
|
+
{ "Content-Type": "application/json" },
|
|
60
|
+
JSON.stringify(body)
|
|
61
|
+
);
|
|
62
|
+
const { url } = request;
|
|
63
|
+
if (url.includes("/organization/test-org/building/test-building/v2")) {
|
|
64
|
+
return json(200, view({}));
|
|
65
|
+
}
|
|
66
|
+
if (url.includes("/organization/test-org/webchat/v2")) {
|
|
67
|
+
return json(200, view({ id: 0, userId: null, name: "Test Org" }));
|
|
68
|
+
}
|
|
69
|
+
if (url.endsWith("/webchat/init")) {
|
|
70
|
+
return json(200, {
|
|
71
|
+
session_token: "session",
|
|
72
|
+
lead_id: "lead_test",
|
|
73
|
+
channel: "webchat_lead_test",
|
|
74
|
+
expires_in_seconds: 3600,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (url.includes("chat-ui-feature-flag")) return json(200, false);
|
|
78
|
+
return json(200, null);
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
return requests;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const startWidget = async (): Promise<MeChatInternals> => {
|
|
85
|
+
const meChat = document.createElement("me-chat") as MeChatInternals;
|
|
86
|
+
meChat.setAttribute("orgslug", "test-org");
|
|
87
|
+
meChat.setAttribute("buildingslug", "test-building");
|
|
88
|
+
document.body.appendChild(meChat);
|
|
89
|
+
for (let i = 0; i < 200 && meChat.isLoading; i++) {
|
|
90
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
91
|
+
}
|
|
92
|
+
expect(meChat.isLoading, "widget finished loading").to.equal(false);
|
|
93
|
+
await meChat.updateComplete;
|
|
94
|
+
return meChat;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const dismissKey = "com.elise.webchat.autoOpenDismissed=test-org=test-building";
|
|
98
|
+
|
|
99
|
+
beforeEach(() => {
|
|
100
|
+
localStorage.clear();
|
|
101
|
+
sessionStorage.clear();
|
|
102
|
+
(window as unknown as { eliseAi: unknown }).eliseAi = {};
|
|
103
|
+
stub(window, "fetch").resolves(new Response("{}"));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
afterEach(() => {
|
|
107
|
+
document
|
|
108
|
+
.querySelectorAll("me-chat, pubnub-chat")
|
|
109
|
+
.forEach((el) => el.remove());
|
|
110
|
+
restore();
|
|
111
|
+
localStorage.clear();
|
|
112
|
+
sessionStorage.clear();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("auto-opens on first visit and records suppression in localStorage", async () => {
|
|
116
|
+
fakeBackend();
|
|
117
|
+
const meChat = await startWidget();
|
|
118
|
+
|
|
119
|
+
expect(meChat.displayPubnubChat).to.equal(true);
|
|
120
|
+
expect(localStorage.getItem(dismissKey)).to.equal("true");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("does not auto-open on a later visit after dismissal", async () => {
|
|
124
|
+
localStorage.setItem(dismissKey, "true");
|
|
125
|
+
sessionStorage.clear();
|
|
126
|
+
fakeBackend();
|
|
127
|
+
const meChat = await startWidget();
|
|
128
|
+
expect(meChat.displayPubnubChat).to.equal(false);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("still opens chat manually when auto-open was dismissed", async () => {
|
|
132
|
+
localStorage.setItem(dismissKey, "true");
|
|
133
|
+
fakeBackend();
|
|
134
|
+
const meChat = await startWidget();
|
|
135
|
+
expect(meChat.displayPubnubChat).to.equal(false);
|
|
136
|
+
|
|
137
|
+
(
|
|
138
|
+
window as unknown as { eliseAi: { onOpenChat: () => void } }
|
|
139
|
+
).eliseAi.onOpenChat();
|
|
140
|
+
await meChat.updateComplete;
|
|
141
|
+
|
|
142
|
+
expect(meChat.displayPubnubChat).to.equal(true);
|
|
143
|
+
});
|