@meetelise/chat 1.57.0 → 1.58.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/WebComponent/me-chat.d.ts +11 -0
- package/dist/src/main/MEChat.d.ts +12 -1
- package/package.json +1 -1
- package/public/dist/index.js +40 -45
- package/src/WebComponent/leasing-chat-styles.ts +1 -2
- package/src/WebComponent/me-chat.ts +25 -4
- package/src/WebComponent/pubnub-chat.ts +14 -16
- package/src/assistantName.test.ts +152 -0
- package/src/main/MEChat.ts +75 -1
- package/src/orgLevelNavigation.test.ts +114 -1
|
@@ -47,8 +47,7 @@ export const leasingChatStyles = css`
|
|
|
47
47
|
justify-content: space-between;
|
|
48
48
|
}
|
|
49
49
|
.leasing-conversation-body-backdrop {
|
|
50
|
-
background:
|
|
51
|
-
backdrop-filter: blur(4px);
|
|
50
|
+
background: #efefef;
|
|
52
51
|
|
|
53
52
|
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
|
54
53
|
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
|
@@ -144,6 +144,11 @@ export class MEChat extends LitElement {
|
|
|
144
144
|
*/
|
|
145
145
|
@property({ type: String })
|
|
146
146
|
private declaredBuildingSlug = "";
|
|
147
|
+
/**
|
|
148
|
+
* Org-level webchat on a host that knows the page's building by id (Elise
|
|
149
|
+
* Studio), set with `setDeclaredBuildingId`. Undefined until the host sets it.
|
|
150
|
+
*/
|
|
151
|
+
private hostDeclaredBuildingId: number | null | undefined = undefined;
|
|
147
152
|
/**
|
|
148
153
|
* Org-level webchat (`centralConvo` started without a building). Stays true
|
|
149
154
|
* after the widget adopts the org's triage general building from
|
|
@@ -700,10 +705,16 @@ export class MEChat extends LitElement {
|
|
|
700
705
|
if (this.centralConvo) {
|
|
701
706
|
const myPubnub = this.myPubnub;
|
|
702
707
|
myPubnub.centralConvo = true;
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
708
|
+
if (this.hostDeclaredBuildingId !== undefined) {
|
|
709
|
+
myPubnub.declaredBuildingId = this.hostDeclaredBuildingId;
|
|
710
|
+
} else {
|
|
711
|
+
// Not awaited: only needed once the visitor sends a message.
|
|
712
|
+
void this.fetchDeclaredBuildingId().then((id) => {
|
|
713
|
+
if (this.hostDeclaredBuildingId === undefined) {
|
|
714
|
+
myPubnub.declaredBuildingId = id;
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
}
|
|
707
718
|
} else {
|
|
708
719
|
this.myPubnub.declaredBuildingId = this.buildingWebchatView.id;
|
|
709
720
|
}
|
|
@@ -814,6 +825,16 @@ export class MEChat extends LitElement {
|
|
|
814
825
|
};
|
|
815
826
|
};
|
|
816
827
|
|
|
828
|
+
/**
|
|
829
|
+
* Org-level webchat: the page's building, sent with later visitor messages as
|
|
830
|
+
* `declaredBuildingId`. For hosts that move between pages without restarting
|
|
831
|
+
* the widget; it doesn't change config or the conversation.
|
|
832
|
+
*/
|
|
833
|
+
setDeclaredBuildingId(id: number | null): void {
|
|
834
|
+
this.hostDeclaredBuildingId = id;
|
|
835
|
+
if (this.myPubnub) this.myPubnub.declaredBuildingId = id;
|
|
836
|
+
}
|
|
837
|
+
|
|
817
838
|
/** The numeric id of the snippet's building in org-level webchat, if it names one. */
|
|
818
839
|
private fetchDeclaredBuildingId = async (): Promise<number | null> => {
|
|
819
840
|
if (!this.declaredBuildingSlug || !this.orgSlug) return null;
|
|
@@ -253,6 +253,8 @@ export class PubnubChat extends LitElement {
|
|
|
253
253
|
return html``; // error should be here
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
const assistantName = this.building?.userFirstName || "Elise";
|
|
257
|
+
|
|
256
258
|
const buildingId = this.building?.id;
|
|
257
259
|
let styles = {};
|
|
258
260
|
if (this._isMobile) {
|
|
@@ -357,9 +359,8 @@ export class PubnubChat extends LitElement {
|
|
|
357
359
|
</li>
|
|
358
360
|
<li
|
|
359
361
|
class="message-container ai-message"
|
|
360
|
-
tabindex="0"
|
|
361
362
|
role="article"
|
|
362
|
-
aria-label="
|
|
363
|
+
aria-label="${assistantName}: Hi I'm ${assistantName}. How can I assist you today?"
|
|
363
364
|
>
|
|
364
365
|
<p
|
|
365
366
|
class=${classMap({
|
|
@@ -368,7 +369,7 @@ export class PubnubChat extends LitElement {
|
|
|
368
369
|
["webchat-font__mobile"]: this._isMobile,
|
|
369
370
|
})}
|
|
370
371
|
>
|
|
371
|
-
Hi I'm
|
|
372
|
+
Hi I'm ${assistantName}. How can I assist you today?
|
|
372
373
|
</p>
|
|
373
374
|
</li>
|
|
374
375
|
${this.messages.map((message) => {
|
|
@@ -378,11 +379,10 @@ export class PubnubChat extends LitElement {
|
|
|
378
379
|
? "message-container lead-message"
|
|
379
380
|
: "message-container ai-message"}
|
|
380
381
|
key=${message.timestamp}
|
|
381
|
-
tabindex="0"
|
|
382
382
|
role="article"
|
|
383
383
|
aria-label="${message.isLeadMessage
|
|
384
384
|
? "You"
|
|
385
|
-
:
|
|
385
|
+
: assistantName}: ${messageAriaLabel(message)}"
|
|
386
386
|
>
|
|
387
387
|
${isSimpleNavigationChatMessage(message)
|
|
388
388
|
? html`<pubnub-navigation
|
|
@@ -436,6 +436,7 @@ export class PubnubChat extends LitElement {
|
|
|
436
436
|
<div id="utilities-inner-footer">
|
|
437
437
|
<textarea
|
|
438
438
|
id="message-input"
|
|
439
|
+
aria-label="Message"
|
|
439
440
|
class=${classMap({
|
|
440
441
|
["webchat-font__desktop"]: !this._isMobile,
|
|
441
442
|
["webchat-font__mobile"]: this._isMobile,
|
|
@@ -535,9 +536,8 @@ export class PubnubChat extends LitElement {
|
|
|
535
536
|
</li>
|
|
536
537
|
<li
|
|
537
538
|
class="message-container ai-message"
|
|
538
|
-
tabindex="0"
|
|
539
539
|
role="article"
|
|
540
|
-
aria-label="
|
|
540
|
+
aria-label="${assistantName}: Hi I'm ${assistantName}. If this is a medical emergency, please call 911. How can I assist you today?"
|
|
541
541
|
>
|
|
542
542
|
<p
|
|
543
543
|
class=${classMap({
|
|
@@ -546,8 +546,8 @@ export class PubnubChat extends LitElement {
|
|
|
546
546
|
["webchat-font__mobile"]: this._isMobile,
|
|
547
547
|
})}
|
|
548
548
|
>
|
|
549
|
-
Hi I'm
|
|
550
|
-
How can I assist you today?
|
|
549
|
+
Hi I'm ${assistantName}. If this is a medical emergency,
|
|
550
|
+
please call 911. How can I assist you today?
|
|
551
551
|
</p>
|
|
552
552
|
</li>
|
|
553
553
|
${this.messages.map((message) => {
|
|
@@ -557,11 +557,10 @@ export class PubnubChat extends LitElement {
|
|
|
557
557
|
? "message-container lead-message"
|
|
558
558
|
: "message-container ai-message"}
|
|
559
559
|
key=${message.timestamp}
|
|
560
|
-
tabindex="0"
|
|
561
560
|
role="article"
|
|
562
561
|
aria-label="${message.isLeadMessage
|
|
563
562
|
? "You"
|
|
564
|
-
:
|
|
563
|
+
: assistantName}: ${messageAriaLabel(message)}"
|
|
565
564
|
>
|
|
566
565
|
${isSimpleNavigationChatMessage(message)
|
|
567
566
|
? html`<pubnub-navigation
|
|
@@ -615,6 +614,7 @@ export class PubnubChat extends LitElement {
|
|
|
615
614
|
<div id="healthcare-inner-footer">
|
|
616
615
|
<textarea
|
|
617
616
|
id="message-input"
|
|
617
|
+
aria-label="Message"
|
|
618
618
|
class=${classMap({
|
|
619
619
|
["webchat-font__desktop"]: !this._isMobile,
|
|
620
620
|
["webchat-font__mobile"]: this._isMobile,
|
|
@@ -734,9 +734,8 @@ export class PubnubChat extends LitElement {
|
|
|
734
734
|
"ai-message": true,
|
|
735
735
|
"with-box-shadow": orgId === 444,
|
|
736
736
|
})}
|
|
737
|
-
tabindex="0"
|
|
738
737
|
role="article"
|
|
739
|
-
aria-label="
|
|
738
|
+
aria-label="${assistantName}: ${this.building.welcomeMessage}"
|
|
740
739
|
>
|
|
741
740
|
<p
|
|
742
741
|
class=${classMap({
|
|
@@ -760,11 +759,10 @@ export class PubnubChat extends LitElement {
|
|
|
760
759
|
"with-box-shadow": orgId === 444,
|
|
761
760
|
})}
|
|
762
761
|
key=${message.timestamp}
|
|
763
|
-
tabindex="0"
|
|
764
762
|
role="article"
|
|
765
763
|
aria-label="${message.isLeadMessage
|
|
766
764
|
? "You"
|
|
767
|
-
:
|
|
765
|
+
: assistantName}: ${messageAriaLabel(message)}"
|
|
768
766
|
>
|
|
769
767
|
${isSimpleNavigationChatMessage(message)
|
|
770
768
|
? html`<pubnub-navigation
|
|
@@ -822,6 +820,7 @@ export class PubnubChat extends LitElement {
|
|
|
822
820
|
>
|
|
823
821
|
<textarea
|
|
824
822
|
id="message-input"
|
|
823
|
+
aria-label="Message"
|
|
825
824
|
class=${classMap({
|
|
826
825
|
["webchat-font__desktop"]: !this._isMobile,
|
|
827
826
|
["webchat-font__mobile"]: this._isMobile,
|
|
@@ -855,7 +854,6 @@ export class PubnubChat extends LitElement {
|
|
|
855
854
|
@click=${() => this.sendMessage(this.messageInput.value)}
|
|
856
855
|
?disabled=${this.myPubnub?.isCurrentlyStreamingMessage}
|
|
857
856
|
aria-label="Send message"
|
|
858
|
-
aria-describedby="Send message"
|
|
859
857
|
>
|
|
860
858
|
${shouldUsePrimaryColorForChatFooter(
|
|
861
859
|
this.building.orgId,
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { expect } from "@esm-bundle/chai";
|
|
2
|
+
import "../public/dist/index";
|
|
3
|
+
import { BuildingWebchatView } from "./fetchBuildingWebchatView";
|
|
4
|
+
import type { SimpleChatMessage } from "./MyPubnub";
|
|
5
|
+
import type { PubnubChat } from "./WebComponent/pubnub-chat";
|
|
6
|
+
|
|
7
|
+
type WidgetTypeName = "default" | "healthcare" | "utilities";
|
|
8
|
+
|
|
9
|
+
declare const afterEach: (callback: () => void) => void;
|
|
10
|
+
declare const it: (name: string, callback: () => void | Promise<void>) => void;
|
|
11
|
+
|
|
12
|
+
const text = (visible: string): string => visible.replace(/\s+/g, " ").trim();
|
|
13
|
+
|
|
14
|
+
const conversation = (): SimpleChatMessage[] => [
|
|
15
|
+
{
|
|
16
|
+
timestamp: 1,
|
|
17
|
+
message: "Do you have a two bedroom?",
|
|
18
|
+
isLeadMessage: true,
|
|
19
|
+
chunks: [{ text: "Do you have a two bedroom?", order: 0, isDone: true }],
|
|
20
|
+
type: "text",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
timestamp: 2,
|
|
24
|
+
message: "Yes, I can help with that.",
|
|
25
|
+
isLeadMessage: false,
|
|
26
|
+
chunks: [{ text: "Yes, I can help with that.", order: 0, isDone: true }],
|
|
27
|
+
type: "text",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
timestamp: 3,
|
|
31
|
+
media: [{ title: null, description: null, url: null }],
|
|
32
|
+
isLeadMessage: false,
|
|
33
|
+
type: "media",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
timestamp: 4,
|
|
37
|
+
navigation: {
|
|
38
|
+
url: "https://example.com/floor-plans",
|
|
39
|
+
label: "the floor plans",
|
|
40
|
+
},
|
|
41
|
+
isLeadMessage: false,
|
|
42
|
+
type: "navigation",
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const mount = async (
|
|
47
|
+
widgetType: WidgetTypeName,
|
|
48
|
+
assistantName: string | null
|
|
49
|
+
): Promise<PubnubChat> => {
|
|
50
|
+
const chat = document.createElement("pubnub-chat") as PubnubChat;
|
|
51
|
+
chat.buildingSlug = "alta-federal-hill";
|
|
52
|
+
chat.orgSlug = "greystar";
|
|
53
|
+
chat.widgetType = widgetType;
|
|
54
|
+
if (assistantName !== null) {
|
|
55
|
+
chat.building = {
|
|
56
|
+
id: 1,
|
|
57
|
+
orgId: 1,
|
|
58
|
+
name: "Alta Federal Hill",
|
|
59
|
+
userFirstName: assistantName,
|
|
60
|
+
welcomeMessage: `Hi, I'm ${assistantName}. How can I help you today?`,
|
|
61
|
+
state: "MD",
|
|
62
|
+
country: "US",
|
|
63
|
+
customAdditionalDisclaimer: null,
|
|
64
|
+
} as BuildingWebchatView;
|
|
65
|
+
}
|
|
66
|
+
document.body.appendChild(chat);
|
|
67
|
+
await chat.updateComplete;
|
|
68
|
+
// firstUpdated clears messages until PubNub has history.
|
|
69
|
+
chat.messages = conversation();
|
|
70
|
+
await chat.updateComplete;
|
|
71
|
+
return chat;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const labels = (chat: PubnubChat): string[] =>
|
|
75
|
+
Array.from(
|
|
76
|
+
chat.shadowRoot?.querySelectorAll<HTMLElement>('[role="article"]') ?? []
|
|
77
|
+
).map((article) => article.getAttribute("aria-label") ?? "");
|
|
78
|
+
|
|
79
|
+
afterEach(() => {
|
|
80
|
+
document.querySelectorAll("pubnub-chat").forEach((chat) => chat.remove());
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("announces leasing messages as the configured assistant", async () => {
|
|
84
|
+
const chat = await mount("default", "Emily");
|
|
85
|
+
const articles = Array.from(
|
|
86
|
+
chat.shadowRoot?.querySelectorAll<HTMLElement>('[role="article"]') ?? []
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect(text(articles[0].innerText)).to.equal(
|
|
90
|
+
"Hi, I'm Emily. How can I help you today?"
|
|
91
|
+
);
|
|
92
|
+
expect(labels(chat)).to.deep.equal([
|
|
93
|
+
"Emily: Hi, I'm Emily. How can I help you today?",
|
|
94
|
+
"You: Do you have a two bedroom?",
|
|
95
|
+
"Emily: Yes, I can help with that.",
|
|
96
|
+
"Emily: Media message",
|
|
97
|
+
"Emily: Take me to the floor plans",
|
|
98
|
+
]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("announces healthcare messages as the configured assistant", async () => {
|
|
102
|
+
const chat = await mount("healthcare", "Ezra");
|
|
103
|
+
const greeting = chat.shadowRoot?.querySelector<HTMLElement>(
|
|
104
|
+
"#message-thread-list > li.message-container"
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
expect(text(greeting?.innerText ?? "")).to.equal(
|
|
108
|
+
"Hi I'm Ezra. If this is a medical emergency, please call 911. How can I assist you today?"
|
|
109
|
+
);
|
|
110
|
+
expect(labels(chat)[0]).to.equal(
|
|
111
|
+
"Ezra: Hi I'm Ezra. If this is a medical emergency, please call 911. How can I assist you today?"
|
|
112
|
+
);
|
|
113
|
+
expect(labels(chat).slice(1)).to.deep.equal([
|
|
114
|
+
"You: Do you have a two bedroom?",
|
|
115
|
+
"Ezra: Yes, I can help with that.",
|
|
116
|
+
"Ezra: Media message",
|
|
117
|
+
"Ezra: Take me to the floor plans",
|
|
118
|
+
]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("announces utilities messages as the configured assistant", async () => {
|
|
122
|
+
const chat = await mount("utilities", "Emily");
|
|
123
|
+
const greeting = chat.shadowRoot?.querySelector<HTMLElement>(
|
|
124
|
+
"#message-thread-list > li.message-container"
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
expect(text(greeting?.innerText ?? "")).to.equal(
|
|
128
|
+
"Hi I'm Emily. How can I assist you today?"
|
|
129
|
+
);
|
|
130
|
+
expect(labels(chat)).to.deep.equal([
|
|
131
|
+
"Emily: Hi I'm Emily. How can I assist you today?",
|
|
132
|
+
"You: Do you have a two bedroom?",
|
|
133
|
+
"Emily: Yes, I can help with that.",
|
|
134
|
+
"Emily: Media message",
|
|
135
|
+
"Emily: Take me to the floor plans",
|
|
136
|
+
]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("keeps Elise when a chat has no configured assistant name", async () => {
|
|
140
|
+
const chat = await mount("utilities", null);
|
|
141
|
+
const greeting = chat.shadowRoot?.querySelector<HTMLElement>(
|
|
142
|
+
"#message-thread-list > li.message-container"
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
expect(text(greeting?.innerText ?? "")).to.equal(
|
|
146
|
+
"Hi I'm Elise. How can I assist you today?"
|
|
147
|
+
);
|
|
148
|
+
expect(labels(chat)[0]).to.equal(
|
|
149
|
+
"Elise: Hi I'm Elise. How can I assist you today?"
|
|
150
|
+
);
|
|
151
|
+
expect(labels(chat)[2]).to.equal("Elise: Yes, I can help with that.");
|
|
152
|
+
});
|
package/src/main/MEChat.ts
CHANGED
|
@@ -40,7 +40,8 @@ export type StudioWebchatMode = "host_managed_context";
|
|
|
40
40
|
export type StudioWebchatSessionScopeType =
|
|
41
41
|
| "building"
|
|
42
42
|
| "building_group"
|
|
43
|
-
| "site"
|
|
43
|
+
| "site"
|
|
44
|
+
| "org";
|
|
44
45
|
|
|
45
46
|
export interface StudioWebchatSessionScope {
|
|
46
47
|
type: StudioWebchatSessionScopeType;
|
|
@@ -60,6 +61,11 @@ export interface StudioWebchatContext {
|
|
|
60
61
|
building?: string | null;
|
|
61
62
|
buildingGroupId?: string | number | null;
|
|
62
63
|
sessionScope?: StudioWebchatSessionScope;
|
|
64
|
+
/**
|
|
65
|
+
* Org scope only: the page's building, sent with visitor messages as
|
|
66
|
+
* `declaredBuildingId`. Null on pages with no single building.
|
|
67
|
+
*/
|
|
68
|
+
declaredBuildingId?: number | null;
|
|
63
69
|
path?: string;
|
|
64
70
|
pageId?: string;
|
|
65
71
|
}
|
|
@@ -323,6 +329,11 @@ export default class MEChat {
|
|
|
323
329
|
return;
|
|
324
330
|
}
|
|
325
331
|
|
|
332
|
+
if (context.sessionScope?.type === "org") {
|
|
333
|
+
this.updateStudioOrgContext(context, opts, updateToken);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
326
337
|
const orgSlug = coerceStudioContextString(context.organization);
|
|
327
338
|
const buildingSlug = coerceStudioContextString(context.building);
|
|
328
339
|
|
|
@@ -368,6 +379,64 @@ export default class MEChat {
|
|
|
368
379
|
});
|
|
369
380
|
}
|
|
370
381
|
|
|
382
|
+
/**
|
|
383
|
+
* Org-level webchat on a Studio site: one org-anchored widget for the whole
|
|
384
|
+
* site, started as the org-level pixel starts it. Route changes only move the
|
|
385
|
+
* page's declared building, so the conversation carries across pages.
|
|
386
|
+
*/
|
|
387
|
+
static updateStudioOrgContext(
|
|
388
|
+
context: StudioWebchatContext,
|
|
389
|
+
opts: Options,
|
|
390
|
+
updateToken: number
|
|
391
|
+
): void {
|
|
392
|
+
const orgSlug = coerceStudioContextString(context.organization);
|
|
393
|
+
if (!orgSlug) {
|
|
394
|
+
this.clearStudioContext({
|
|
395
|
+
source: "elise_studio",
|
|
396
|
+
reason: "missing_context",
|
|
397
|
+
preserveCache: true,
|
|
398
|
+
});
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const normalizedOrgSlug = normalizeOrgSlug(orgSlug);
|
|
403
|
+
const sessionScopeKey = context.sessionScope?.key || `org:${orgSlug}`;
|
|
404
|
+
const declaredBuildingId = coerceDeclaredBuildingId(
|
|
405
|
+
context.declaredBuildingId
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
if (
|
|
409
|
+
this.meChat &&
|
|
410
|
+
this.activeStudioOrgSlug === normalizedOrgSlug &&
|
|
411
|
+
!this.activeStudioBuildingSlug &&
|
|
412
|
+
this.activeStudioSessionScopeKey === sessionScopeKey
|
|
413
|
+
) {
|
|
414
|
+
this.meChat.setDeclaredBuildingId(declaredBuildingId);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
void this.removeWidget().then(() => {
|
|
419
|
+
if (updateToken !== this.studioUpdateToken) return;
|
|
420
|
+
|
|
421
|
+
this.activeStudioOrgSlug = normalizedOrgSlug;
|
|
422
|
+
this.activeStudioBuildingSlug = "";
|
|
423
|
+
this.activeStudioSessionScopeKey = sessionScopeKey;
|
|
424
|
+
this.orgSlug = normalizedOrgSlug;
|
|
425
|
+
|
|
426
|
+
this.start(
|
|
427
|
+
{
|
|
428
|
+
...opts,
|
|
429
|
+
studio: undefined,
|
|
430
|
+
organization: orgSlug,
|
|
431
|
+
building: undefined,
|
|
432
|
+
centralConvo: true,
|
|
433
|
+
},
|
|
434
|
+
false
|
|
435
|
+
);
|
|
436
|
+
this.meChat?.setDeclaredBuildingId(declaredBuildingId);
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
371
440
|
static clearStudioContext(_context?: StudioWebchatClearContext): void {
|
|
372
441
|
void _context;
|
|
373
442
|
this.studioUpdateToken += 1;
|
|
@@ -536,6 +605,11 @@ function normalizeOrgSlug(orgSlug: string): string {
|
|
|
536
605
|
: orgSlug;
|
|
537
606
|
}
|
|
538
607
|
|
|
608
|
+
function coerceDeclaredBuildingId(value: unknown): number | null {
|
|
609
|
+
const id = typeof value === "string" ? Number(value) : value;
|
|
610
|
+
return typeof id === "number" && Number.isInteger(id) && id > 0 ? id : null;
|
|
611
|
+
}
|
|
612
|
+
|
|
539
613
|
function coerceStudioSessionScopeKey(
|
|
540
614
|
sessionScope: StudioWebchatSessionScope | undefined,
|
|
541
615
|
buildingSlug: string
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { expect } from "@esm-bundle/chai";
|
|
2
2
|
import { restore, stub, useFakeXMLHttpRequest } from "sinon/pkg/sinon-esm";
|
|
3
|
-
import "../public/dist/index";
|
|
3
|
+
import MEChat from "../public/dist/index";
|
|
4
4
|
import type MyPubnub from "./MyPubnub";
|
|
5
5
|
import type { SimpleChatMessage } from "./MyPubnub";
|
|
6
6
|
import type { PubnubChat } from "./WebComponent/pubnub-chat";
|
|
@@ -623,3 +623,116 @@ it("still opens a per-building widget's tour scheduler from its tour link", asyn
|
|
|
623
623
|
expect(widgetTakesOverClick(buildingTourLink)).to.equal(true);
|
|
624
624
|
[buildingTourLink, ...others].forEach((link) => link.remove());
|
|
625
625
|
});
|
|
626
|
+
|
|
627
|
+
// --- Org mode on an Elise Studio site ---
|
|
628
|
+
|
|
629
|
+
const STUDIO_SITE = "site-1";
|
|
630
|
+
|
|
631
|
+
const studioOrgContext = (declaredBuildingId: number | null) => ({
|
|
632
|
+
source: "elise_studio" as const,
|
|
633
|
+
siteId: STUDIO_SITE,
|
|
634
|
+
organization: "test-org",
|
|
635
|
+
sessionScope: { type: "org" as const, key: "org:test-org" },
|
|
636
|
+
declaredBuildingId,
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
const studioWidget = async (): Promise<MeChatInternals> => {
|
|
640
|
+
for (let i = 0; i < 200; i++) {
|
|
641
|
+
const meChat = document.querySelector("me-chat") as MeChatInternals | null;
|
|
642
|
+
if (meChat && !meChat.isLoading) {
|
|
643
|
+
await meChat.updateComplete;
|
|
644
|
+
return meChat;
|
|
645
|
+
}
|
|
646
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
647
|
+
}
|
|
648
|
+
throw new Error("Studio widget never finished loading");
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
it("runs a Studio site's org scope as org-level webchat, one widget across pages", async () => {
|
|
652
|
+
const requests = fakeBackend(inRollout);
|
|
653
|
+
MEChat.start({
|
|
654
|
+
studio: { siteId: STUDIO_SITE, mode: "host_managed_context" },
|
|
655
|
+
});
|
|
656
|
+
try {
|
|
657
|
+
// The homepage: no building.
|
|
658
|
+
MEChat.updateStudioContext(studioOrgContext(null));
|
|
659
|
+
const meChat = await studioWidget();
|
|
660
|
+
|
|
661
|
+
expect(initBodies(requests)).to.deep.equal([
|
|
662
|
+
{ building_slug: "", org_slug: "test-org", central_convo: true },
|
|
663
|
+
]);
|
|
664
|
+
expect(meChat.buildingSlug).to.equal(GENERAL_BUILDING);
|
|
665
|
+
expect(meChat.myPubnub?.declaredBuildingId).to.equal(null);
|
|
666
|
+
|
|
667
|
+
// A property page: same widget and conversation, with the page's building.
|
|
668
|
+
MEChat.updateStudioContext(studioOrgContext(445038));
|
|
669
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
670
|
+
|
|
671
|
+
expect(document.querySelectorAll("me-chat")).to.have.length(1);
|
|
672
|
+
expect(document.querySelector("me-chat")).to.equal(meChat);
|
|
673
|
+
expect(initBodies(requests)).to.have.length(1);
|
|
674
|
+
const message = await publishedMessage(meChat.myPubnub as MyPubnub);
|
|
675
|
+
expect(message).to.include({ declaredBuildingId: 445038 });
|
|
676
|
+
|
|
677
|
+
// Back to a page with no building.
|
|
678
|
+
MEChat.updateStudioContext(studioOrgContext(null));
|
|
679
|
+
expect(meChat.myPubnub?.declaredBuildingId).to.equal(null);
|
|
680
|
+
} finally {
|
|
681
|
+
await MEChat.remove();
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it("swaps a Studio site's building widget for org-level webchat", async () => {
|
|
686
|
+
const requests = fakeBackend(inRollout);
|
|
687
|
+
MEChat.start({
|
|
688
|
+
studio: { siteId: STUDIO_SITE, mode: "host_managed_context" },
|
|
689
|
+
});
|
|
690
|
+
try {
|
|
691
|
+
MEChat.updateStudioContext({
|
|
692
|
+
source: "elise_studio",
|
|
693
|
+
siteId: STUDIO_SITE,
|
|
694
|
+
organization: "test-org",
|
|
695
|
+
building: "test-building",
|
|
696
|
+
sessionScope: { type: "building", key: "building:test-building" },
|
|
697
|
+
});
|
|
698
|
+
const buildingWidget = await studioWidget();
|
|
699
|
+
expect(buildingWidget.myPubnub?.centralConvo).to.not.equal(true);
|
|
700
|
+
|
|
701
|
+
MEChat.updateStudioContext(studioOrgContext(445038));
|
|
702
|
+
for (
|
|
703
|
+
let i = 0;
|
|
704
|
+
i < 100 && document.querySelector("me-chat") === buildingWidget;
|
|
705
|
+
i++
|
|
706
|
+
) {
|
|
707
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
708
|
+
}
|
|
709
|
+
const orgWidget = await studioWidget();
|
|
710
|
+
|
|
711
|
+
expect(orgWidget).to.not.equal(buildingWidget);
|
|
712
|
+
expect(document.querySelectorAll("me-chat")).to.have.length(1);
|
|
713
|
+
expect(orgWidget.myPubnub?.centralConvo).to.equal(true);
|
|
714
|
+
expect(orgWidget.myPubnub?.declaredBuildingId).to.equal(445038);
|
|
715
|
+
expect(initBodies(requests).at(-1)).to.deep.equal({
|
|
716
|
+
building_slug: "",
|
|
717
|
+
org_slug: "test-org",
|
|
718
|
+
central_convo: true,
|
|
719
|
+
});
|
|
720
|
+
} finally {
|
|
721
|
+
await MEChat.remove();
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
it("clears a Studio site's org scope without an organization", async () => {
|
|
726
|
+
fakeBackend(inRollout);
|
|
727
|
+
MEChat.start({
|
|
728
|
+
studio: { siteId: STUDIO_SITE, mode: "host_managed_context" },
|
|
729
|
+
});
|
|
730
|
+
try {
|
|
731
|
+
MEChat.updateStudioContext({ ...studioOrgContext(null), organization: "" });
|
|
732
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
733
|
+
|
|
734
|
+
expect(document.querySelector("me-chat")).to.equal(null);
|
|
735
|
+
} finally {
|
|
736
|
+
await MEChat.remove();
|
|
737
|
+
}
|
|
738
|
+
});
|