Skip to content

Commit 7f82fd8

Browse files
feat(olark,#90): visitor.getDetails + chat API + events + group/locale config
Vendor-doc audit fixes for Olark: Methods (10): setLocale, setOperatorGroup, sendMessageToVisitor, sendNotificationToVisitor, sendNotificationToOperator, updateVisitorNickname, updateVisitorStatus, plus async getVisitorDetails() that resolves with the SDK's callback-based payload (the wrapper's getIdentity() now has a real read path back to Olark's resolved details). Event bridge: new public on(event, handler) wraps 12 documented Olark events (api.box.onShow/Hide/Expand/Shrink + api.chat.onReady/ onBeginConversation/onMessageToVisitor/onMessageToOperator/ onCommandFromOperator/onOfflineMessageToOperator/onOperatorsAvailable/ onOperatorsAway), exposed under typed names. Load options: group (calls api.chat.setOperatorGroup at boot) and locale (api.box.setLocale at boot). Bumps olark bundle budget 6KB → 7KB (raw 6299B; gzip ≈ 1/3). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f94b1e5 commit 7f82fd8

3 files changed

Lines changed: 246 additions & 1 deletion

File tree

scripts/bundle-budget.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const BUDGETS = [
1818
{ glob: /^providers\/hubspot\.mjs$/, max: 7168, label: "provider:hubspot" },
1919
{ glob: /^providers\/tawk\.mjs$/, max: 9216, label: "provider:tawk" },
2020
{ glob: /^providers\/zendesk\.mjs$/, max: 7168, label: "provider:zendesk" },
21+
{ glob: /^providers\/olark\.mjs$/, max: 7168, label: "provider:olark" },
2122
{ glob: /^providers\/.+\.mjs$/, max: 6144, label: "provider" },
2223
{ glob: /^index\.mjs$/, max: 4096, label: "core" },
2324
{ glob: /^facade\.mjs$/, max: 3072, label: "facade" },

src/providers/olark.ts

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type {
1111
LoadOptions,
1212
} from "../_types.ts"
1313

14-
type OlarkFn = (method: string, ...args: unknown[]) => void
14+
type OlarkFn = (method: string, ...args: unknown[]) => unknown
1515

1616
interface OlarkWindow {
1717
olark?: OlarkFn
@@ -25,8 +25,55 @@ const queue = createQueue<OlarkFn>()
2525
const store = createIdentityStore()
2626
const lifecycle = createLifecycle()
2727

28+
export type OlarkEventName =
29+
| "boxShow"
30+
| "boxHide"
31+
| "boxExpand"
32+
| "boxShrink"
33+
| "chatReady"
34+
| "beginConversation"
35+
| "messageToVisitor"
36+
| "messageToOperator"
37+
| "commandFromOperator"
38+
| "offlineMessageToOperator"
39+
| "operatorsAvailable"
40+
| "operatorsAway"
41+
42+
const OLARK_EVENT_MAP: Record<OlarkEventName, string> = {
43+
boxShow: "api.box.onShow",
44+
boxHide: "api.box.onHide",
45+
boxExpand: "api.box.onExpand",
46+
boxShrink: "api.box.onShrink",
47+
chatReady: "api.chat.onReady",
48+
beginConversation: "api.chat.onBeginConversation",
49+
messageToVisitor: "api.chat.onMessageToVisitor",
50+
messageToOperator: "api.chat.onMessageToOperator",
51+
commandFromOperator: "api.chat.onCommandFromOperator",
52+
offlineMessageToOperator: "api.chat.onOfflineMessageToOperator",
53+
operatorsAvailable: "api.chat.onOperatorsAvailable",
54+
operatorsAway: "api.chat.onOperatorsAway",
55+
}
56+
57+
const eventListeners = new Map<OlarkEventName, Set<(payload?: unknown) => void>>()
58+
59+
export interface OlarkVisitorDetails {
60+
emailAddress?: string
61+
fullName?: string
62+
phoneNumber?: string
63+
organization?: string
64+
city?: string
65+
region?: string
66+
country?: string
67+
customFields?: Record<string, unknown>
68+
[key: string]: unknown
69+
}
70+
2871
export interface OlarkLoadOptions extends LoadOptions {
2972
siteId: string
73+
/** Initial agent group routing. */
74+
group?: string
75+
/** Initial widget locale. */
76+
locale?: string
3077
}
3178

3279
export async function load(options: OlarkLoadOptions): Promise<void> {
@@ -58,6 +105,18 @@ export async function load(options: OlarkLoadOptions): Promise<void> {
58105
const olark = w().olark
59106
if (typeof olark === "function") {
60107
olark("api.box.onReady", () => {})
108+
// Wire every documented Olark event to the typed emitter.
109+
for (const [mapped, vendor] of Object.entries(OLARK_EVENT_MAP) as Array<
110+
[OlarkEventName, string]
111+
>) {
112+
olark(vendor, (payload: unknown) => {
113+
const set = eventListeners.get(mapped)
114+
if (!set) return
115+
for (const l of set) l(payload)
116+
})
117+
}
118+
if (options.group !== undefined) olark("api.chat.setOperatorGroup", { group: options.group })
119+
if (options.locale !== undefined) olark("api.box.setLocale", options.locale)
61120
queue.ready(olark)
62121
break
63122
}
@@ -116,6 +175,67 @@ export function shrink(): Promise<void> {
116175
return queue.enqueue((olark) => olark("api.box.shrink"))
117176
}
118177

178+
export function setLocale(locale: string): Promise<void> {
179+
if (!isBrowser()) return Promise.resolve()
180+
return queue.enqueue((olark) => olark("api.box.setLocale", locale))
181+
}
182+
183+
export function setOperatorGroup(group: string): Promise<void> {
184+
if (!isBrowser()) return Promise.resolve()
185+
return queue.enqueue((olark) => olark("api.chat.setOperatorGroup", { group }))
186+
}
187+
188+
export function sendMessageToVisitor(body: string): Promise<void> {
189+
if (!isBrowser()) return Promise.resolve()
190+
return queue.enqueue((olark) => olark("api.chat.sendMessageToVisitor", { body }))
191+
}
192+
193+
export function sendNotificationToVisitor(body: string): Promise<void> {
194+
if (!isBrowser()) return Promise.resolve()
195+
return queue.enqueue((olark) => olark("api.chat.sendNotificationToVisitor", { body }))
196+
}
197+
198+
export function sendNotificationToOperator(body: string): Promise<void> {
199+
if (!isBrowser()) return Promise.resolve()
200+
return queue.enqueue((olark) => olark("api.chat.sendNotificationToOperator", { body }))
201+
}
202+
203+
export function updateVisitorNickname(args: {
204+
snippet: string
205+
hideDefault?: boolean
206+
}): Promise<void> {
207+
if (!isBrowser()) return Promise.resolve()
208+
return queue.enqueue((olark) => olark("api.chat.updateVisitorNickname", args))
209+
}
210+
211+
export function updateVisitorStatus(args: { snippet: string | string[] }): Promise<void> {
212+
if (!isBrowser()) return Promise.resolve()
213+
return queue.enqueue((olark) => olark("api.chat.updateVisitorStatus", args))
214+
}
215+
216+
export function getVisitorDetails(): Promise<OlarkVisitorDetails | undefined> {
217+
if (!isBrowser()) return Promise.resolve(undefined)
218+
return new Promise((resolve) => {
219+
queue
220+
.enqueue((olark) => {
221+
olark("api.visitor.getDetails", (details: unknown) =>
222+
resolve(details as OlarkVisitorDetails | undefined),
223+
)
224+
})
225+
.catch(() => resolve(undefined))
226+
})
227+
}
228+
229+
export function on(event: OlarkEventName, listener: (payload?: unknown) => void): () => void {
230+
let set = eventListeners.get(event)
231+
if (!set) {
232+
set = new Set()
233+
eventListeners.set(event, set)
234+
}
235+
set.add(listener)
236+
return () => set?.delete(listener)
237+
}
238+
119239
export function shutdown(): Promise<void> {
120240
if (!isBrowser()) return Promise.resolve()
121241
return queue
@@ -137,6 +257,7 @@ export async function destroy(): Promise<void> {
137257
Reflect.deleteProperty(g, "olark")
138258
queue.reset()
139259
store.reset()
260+
eventListeners.clear()
140261
lifecycle.clearConfigHash()
141262
lifecycle.transition("idle")
142263
}

test/olark.browser.test.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// @vitest-environment jsdom
2+
import { beforeEach, describe, expect, it, vi } from "vitest"
3+
4+
interface OCall {
5+
method: string
6+
args: unknown[]
7+
}
8+
9+
async function bootOlark(
10+
options?: Partial<import("../src/providers/olark.ts").OlarkLoadOptions>,
11+
): Promise<{
12+
olark: typeof import("../src/providers/olark.ts")
13+
calls: OCall[]
14+
fire: (event: string, payload?: unknown) => void
15+
visitorDetailsResponse: { current: unknown }
16+
}> {
17+
const olark = await import("../src/providers/olark.ts")
18+
const calls: OCall[] = []
19+
const handlers = new Map<string, Array<(p?: unknown) => void>>()
20+
const visitorDetailsResponse = { current: { emailAddress: "x@y", fullName: "Test User" } }
21+
const fn = (method: string, ...args: unknown[]) => {
22+
calls.push({ method, args })
23+
if (typeof args[0] === "function" && method.startsWith("api.") && method.includes(".on")) {
24+
const arr = handlers.get(method) ?? []
25+
arr.push(args[0] as (p?: unknown) => void)
26+
handlers.set(method, arr)
27+
}
28+
if (method === "api.visitor.getDetails" && typeof args[0] === "function") {
29+
;(args[0] as (d: unknown) => void)(visitorDetailsResponse.current)
30+
}
31+
return undefined
32+
}
33+
// biome-ignore lint/suspicious/noExplicitAny: test shim
34+
;(globalThis as any).olark = fn
35+
const loadPromise = olark.load({ siteId: "site_xyz", ...options })
36+
await new Promise((r) => setTimeout(r, 0))
37+
const script = document.getElementById("ahize-olark") as HTMLScriptElement
38+
expect(script).toBeTruthy()
39+
script.dispatchEvent(new Event("load"))
40+
await new Promise((r) => setTimeout(r, 60))
41+
await loadPromise
42+
const fire = (vendor: string, payload?: unknown) => {
43+
for (const cb of handlers.get(vendor) ?? []) cb(payload)
44+
}
45+
return { olark, calls, fire, visitorDetailsResponse }
46+
}
47+
48+
describe("olark (browser) — vendor-doc audit fixes (#90)", () => {
49+
beforeEach(() => {
50+
vi.resetModules()
51+
// biome-ignore lint/suspicious/noExplicitAny: test cleanup
52+
delete (globalThis as any).olark
53+
const scripts = document.querySelectorAll("script")
54+
for (let i = 0; i < scripts.length; i++) {
55+
;(scripts[i] as { remove(): void } | undefined)?.remove()
56+
}
57+
})
58+
59+
it("group + locale at load time fire setOperatorGroup + setLocale", async () => {
60+
const { olark, calls } = await bootOlark({ group: "support", locale: "tr" })
61+
expect(calls.find((c) => c.method === "api.chat.setOperatorGroup")?.args).toEqual([
62+
{ group: "support" },
63+
])
64+
expect(calls.find((c) => c.method === "api.box.setLocale")?.args).toEqual(["tr"])
65+
await olark.destroy()
66+
})
67+
68+
it("getVisitorDetails resolves with the callback payload", async () => {
69+
const { olark } = await bootOlark()
70+
const details = await olark.getVisitorDetails()
71+
expect(details).toMatchObject({ emailAddress: "x@y", fullName: "Test User" })
72+
await olark.destroy()
73+
})
74+
75+
it("chat.* methods (sendMessageToVisitor/etc) forward correctly", async () => {
76+
const { olark, calls } = await bootOlark()
77+
calls.length = 0
78+
await olark.sendMessageToVisitor("hi")
79+
await olark.sendNotificationToVisitor("note v")
80+
await olark.sendNotificationToOperator("note o")
81+
await olark.updateVisitorNickname({ snippet: "VIP", hideDefault: true })
82+
await olark.updateVisitorStatus({ snippet: "browsing" })
83+
await olark.setOperatorGroup("sales")
84+
85+
const methods = calls.map((c) => c.method)
86+
for (const m of [
87+
"api.chat.sendMessageToVisitor",
88+
"api.chat.sendNotificationToVisitor",
89+
"api.chat.sendNotificationToOperator",
90+
"api.chat.updateVisitorNickname",
91+
"api.chat.updateVisitorStatus",
92+
"api.chat.setOperatorGroup",
93+
]) {
94+
expect(methods).toContain(m)
95+
}
96+
await olark.destroy()
97+
})
98+
99+
it("on() bridges box + chat events", async () => {
100+
const { olark, fire } = await bootOlark()
101+
let shown = 0
102+
let beginCount = 0
103+
let lastMsg: unknown
104+
olark.on("boxShow", () => {
105+
shown++
106+
})
107+
olark.on("beginConversation", () => {
108+
beginCount++
109+
})
110+
olark.on("messageToOperator", (p) => {
111+
lastMsg = p
112+
})
113+
114+
fire("api.box.onShow")
115+
fire("api.chat.onBeginConversation")
116+
fire("api.chat.onMessageToOperator", { body: "test" })
117+
118+
expect(shown).toBe(1)
119+
expect(beginCount).toBe(1)
120+
expect(lastMsg).toEqual({ body: "test" })
121+
await olark.destroy()
122+
})
123+
})

0 commit comments

Comments
 (0)