@session-services/web-elements 0.1.28 → 0.1.31
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/README.md +5 -0
- package/dist/cjs/{index-BYBgrATL.js → index-B2ZmNnji.js} +292 -245
- package/dist/cjs/loader.cjs.js +2 -2
- package/dist/cjs/session-services-elements.cjs.js +3 -3
- package/dist/cjs/session-services-formatted-text.cjs.entry.js +8 -20
- package/dist/cjs/session-services-ticketing.cjs.entry.js +2188 -23012
- package/dist/collection/collection-manifest.json +4 -3
- package/dist/collection/components/session-services-ticketing/bot-detection.js +83 -0
- package/dist/collection/components/session-services-ticketing/bot-detection.test.js +74 -0
- package/dist/collection/components/session-services-ticketing/session-services-ticketing.css +164 -36
- package/dist/collection/components/session-services-ticketing/session-services-ticketing.js +811 -262
- package/dist/collection/components/session-services-ticketing/session-services-ticketing.logic.js +39 -8
- package/dist/collection/components/session-services-ticketing/session-services-ticketing.logic.test.js +47 -3
- package/dist/collection/components/session-services-ticketing/session-services-ticketing.test.js +611 -20
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +1 -1311
- package/dist/components/session-services-formatted-text.js +1 -1802
- package/dist/components/session-services-ticketing.js +1 -25072
- package/dist/esm/{index-CNOpF6tk.js → index-DhbVMqIT.js} +292 -245
- package/dist/esm/loader.js +3 -3
- package/dist/esm/session-services-elements.js +4 -4
- package/dist/esm/session-services-formatted-text.entry.js +8 -20
- package/dist/esm/session-services-ticketing.entry.js +2188 -23012
- package/dist/session-services-elements/{p-703444ed.entry.js → p-8483ca88.entry.js} +1 -1
- package/dist/session-services-elements/p-DhbVMqIT.js +2 -0
- package/dist/session-services-elements/p-d7264f6b.entry.js +1 -0
- package/dist/session-services-elements/session-services-elements.esm.js +1 -1
- package/dist/types/components/session-services-ticketing/bot-detection.d.ts +13 -0
- package/dist/types/components/session-services-ticketing/bot-detection.test.d.ts +1 -0
- package/dist/types/components/session-services-ticketing/session-services-ticketing.d.ts +67 -9
- package/dist/types/components/session-services-ticketing/session-services-ticketing.logic.d.ts +11 -2
- package/dist/types/components.d.ts +36 -8
- package/dist/types/index.d.ts +1 -1
- package/dist/types/stencil-public-runtime.d.ts +67 -5
- package/package.json +15 -11
- package/dist/session-services-elements/p-1acfcdfd.entry.js +0 -1
- package/dist/session-services-elements/p-CNOpF6tk.js +0 -2
package/dist/collection/components/session-services-ticketing/session-services-ticketing.test.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest";
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { SessionsTicketing } from "./session-services-ticketing";
|
|
3
3
|
import { getTicketingState, selectionKey, } from "./session-services-ticketing.logic";
|
|
4
|
+
const botDetection = vi.hoisted(() => ({
|
|
5
|
+
detectBot: vi.fn(() => ({ bot: false, signals: [] })),
|
|
6
|
+
isProductionApi: vi.fn((apiUrl) => apiUrl === "https://api.session.services"),
|
|
7
|
+
}));
|
|
8
|
+
const createClient = vi.hoisted(() => vi.fn(() => ({})));
|
|
9
|
+
const createAnalytics = vi.hoisted(() => vi.fn(() => ({ page: vi.fn(), track: vi.fn() })));
|
|
10
|
+
vi.mock("./bot-detection", () => botDetection);
|
|
11
|
+
vi.mock("@session-services/sdk", () => ({ createClient }));
|
|
12
|
+
vi.mock("@session-services/sdk/analytics", async (importOriginal) => ({
|
|
13
|
+
...(await importOriginal()),
|
|
14
|
+
createAnalytics,
|
|
15
|
+
}));
|
|
4
16
|
vi.mock("@stencil/core", () => ({
|
|
5
17
|
Component: () => () => undefined,
|
|
6
18
|
Element: () => () => undefined,
|
|
@@ -129,6 +141,15 @@ function rendered(value, predicate) {
|
|
|
129
141
|
const matches = predicate(value) ? [value] : [];
|
|
130
142
|
return matches.concat(value.children.flatMap((item) => rendered(item, predicate)));
|
|
131
143
|
}
|
|
144
|
+
function textContent(value) {
|
|
145
|
+
if (typeof value === "string")
|
|
146
|
+
return value;
|
|
147
|
+
if (Array.isArray(value))
|
|
148
|
+
return value.map(textContent).join(" ");
|
|
149
|
+
if (!isRenderNode(value))
|
|
150
|
+
return "";
|
|
151
|
+
return textContent(value.children);
|
|
152
|
+
}
|
|
132
153
|
function hasClass(node, name) {
|
|
133
154
|
return String(node.props.class ?? "")
|
|
134
155
|
.split(/\s+/)
|
|
@@ -136,6 +157,7 @@ function hasClass(node, name) {
|
|
|
136
157
|
}
|
|
137
158
|
function widget() {
|
|
138
159
|
const item = new SessionsTicketing();
|
|
160
|
+
item.apiUrl = "https://api.session.services";
|
|
139
161
|
item.eventId = "evt_1";
|
|
140
162
|
item.event = event();
|
|
141
163
|
item.enableAnalytics = true;
|
|
@@ -146,20 +168,30 @@ function widget() {
|
|
|
146
168
|
item.quote = null;
|
|
147
169
|
item.previewQuote = null;
|
|
148
170
|
item.quoteError = null;
|
|
171
|
+
item.checkoutError = null;
|
|
149
172
|
item.couponError = null;
|
|
150
173
|
item.appliedCouponCode = "";
|
|
151
174
|
item.couponInput = "";
|
|
152
175
|
item.couponCode = undefined;
|
|
153
176
|
item.initialPreviewQuote = undefined;
|
|
154
177
|
item.quoteLoading = false;
|
|
178
|
+
item.checkoutLoading = false;
|
|
155
179
|
item.previewLoading = false;
|
|
156
180
|
item.passcodeDenied = false;
|
|
157
181
|
item.stickyCheckout = false;
|
|
182
|
+
item.tenantId = "tnt_1";
|
|
183
|
+
item.theme = "light";
|
|
158
184
|
item.error = null;
|
|
185
|
+
item.errorKind = null;
|
|
186
|
+
item.initialLoading = false;
|
|
187
|
+
item.eventLoading = false;
|
|
159
188
|
item.activePasscode = "";
|
|
189
|
+
item.passcodeInput = "";
|
|
190
|
+
item.passcodeLoading = false;
|
|
160
191
|
item.cartUpdate = { emit: vi.fn() };
|
|
161
192
|
item.checkoutInit = { emit: vi.fn() };
|
|
162
193
|
item.errorOccurred = { emit: vi.fn() };
|
|
194
|
+
item.analyticsEventSkipped = { emit: vi.fn() };
|
|
163
195
|
item.client = {
|
|
164
196
|
event: {
|
|
165
197
|
get: vi.fn(),
|
|
@@ -172,6 +204,91 @@ function widget() {
|
|
|
172
204
|
return item;
|
|
173
205
|
}
|
|
174
206
|
describe("session-services-ticketing component", () => {
|
|
207
|
+
beforeEach(() => {
|
|
208
|
+
botDetection.detectBot.mockReset();
|
|
209
|
+
botDetection.detectBot.mockReturnValue({ bot: false, signals: [] });
|
|
210
|
+
botDetection.isProductionApi.mockReset();
|
|
211
|
+
botDetection.isProductionApi.mockImplementation((apiUrl) => apiUrl === "https://api.session.services");
|
|
212
|
+
createClient.mockReset();
|
|
213
|
+
createClient.mockReturnValue({});
|
|
214
|
+
createAnalytics.mockReset();
|
|
215
|
+
createAnalytics.mockReturnValue({ page: vi.fn(), track: vi.fn() });
|
|
216
|
+
});
|
|
217
|
+
afterEach(() => {
|
|
218
|
+
vi.useRealTimers();
|
|
219
|
+
vi.unstubAllGlobals();
|
|
220
|
+
});
|
|
221
|
+
it("defers production API loading when automation is detected", async () => {
|
|
222
|
+
const item = widget();
|
|
223
|
+
const loadEventData = vi
|
|
224
|
+
.fn()
|
|
225
|
+
.mockResolvedValue({ status: "success", event: event() });
|
|
226
|
+
item.event = null;
|
|
227
|
+
item.loadEventData = loadEventData;
|
|
228
|
+
botDetection.detectBot.mockReturnValue({
|
|
229
|
+
bot: true,
|
|
230
|
+
signals: ["webdriver"],
|
|
231
|
+
});
|
|
232
|
+
await item.initializeComponent();
|
|
233
|
+
expect(createClient).not.toHaveBeenCalled();
|
|
234
|
+
expect(loadEventData).not.toHaveBeenCalled();
|
|
235
|
+
expect(item.verificationRequired).toBe(true);
|
|
236
|
+
expect(textContent(item.pendingView())).toContain("Let’s get your tickets");
|
|
237
|
+
expect(textContent(item.pendingView())).toContain("We just need you to confirm before we load the ticket options");
|
|
238
|
+
expect(textContent(item.pendingView())).toContain("Load tickets");
|
|
239
|
+
expect(hasClass(rendered(item.pendingView(), (node) => node.tag === "button")[0], "checkout-btn")).toBe(true);
|
|
240
|
+
});
|
|
241
|
+
it("loads the normal flow after the user continues", async () => {
|
|
242
|
+
const item = widget();
|
|
243
|
+
const loadEventData = vi
|
|
244
|
+
.fn()
|
|
245
|
+
.mockResolvedValue({ status: "success", event: event() });
|
|
246
|
+
item.event = null;
|
|
247
|
+
item.loadEventData = loadEventData;
|
|
248
|
+
botDetection.detectBot.mockReturnValue({
|
|
249
|
+
bot: true,
|
|
250
|
+
signals: ["webdriver"],
|
|
251
|
+
});
|
|
252
|
+
await item.initializeComponent();
|
|
253
|
+
const button = rendered(item.pendingView(), (node) => node.tag === "button")[0];
|
|
254
|
+
await button.props.onClick();
|
|
255
|
+
expect(botDetection.detectBot).toHaveBeenCalledOnce();
|
|
256
|
+
expect(createClient).toHaveBeenCalledWith({
|
|
257
|
+
environment: "https://api.session.services",
|
|
258
|
+
tenantId: "tnt_1",
|
|
259
|
+
});
|
|
260
|
+
expect(loadEventData).toHaveBeenCalledOnce();
|
|
261
|
+
expect(item.verificationRequired).toBe(false);
|
|
262
|
+
await item.initializeComponent();
|
|
263
|
+
expect(botDetection.detectBot).toHaveBeenCalledOnce();
|
|
264
|
+
expect(loadEventData).toHaveBeenCalledTimes(2);
|
|
265
|
+
});
|
|
266
|
+
it("bypasses detection outside the production API", async () => {
|
|
267
|
+
const item = widget();
|
|
268
|
+
const loadEventData = vi
|
|
269
|
+
.fn()
|
|
270
|
+
.mockResolvedValue({ status: "success", event: event() });
|
|
271
|
+
item.apiUrl = "https://j-api.dev.session.services";
|
|
272
|
+
item.event = null;
|
|
273
|
+
item.loadEventData = loadEventData;
|
|
274
|
+
await item.initializeComponent();
|
|
275
|
+
expect(botDetection.detectBot).not.toHaveBeenCalled();
|
|
276
|
+
expect(createClient).toHaveBeenCalledOnce();
|
|
277
|
+
expect(loadEventData).toHaveBeenCalledOnce();
|
|
278
|
+
expect(item.verificationRequired).toBe(false);
|
|
279
|
+
});
|
|
280
|
+
it("uses the checkout action style for recoverable load errors", () => {
|
|
281
|
+
const item = widget();
|
|
282
|
+
item.error = "Failed to load event data";
|
|
283
|
+
item.event = null;
|
|
284
|
+
item.loading = false;
|
|
285
|
+
const view = item.pendingView();
|
|
286
|
+
const button = rendered(view, (node) => node.tag === "button")[0];
|
|
287
|
+
expect(textContent(view)).toContain("Tickets didn’t load");
|
|
288
|
+
expect(textContent(view)).toContain("Something got in the way. Try again to load the ticket options.");
|
|
289
|
+
expect(textContent(button)).toBe("Try again");
|
|
290
|
+
expect(hasClass(button, "checkout-btn")).toBe(true);
|
|
291
|
+
});
|
|
175
292
|
it("clears stale cart quotes when requoting fails", async () => {
|
|
176
293
|
const item = widget();
|
|
177
294
|
item.quote = quote();
|
|
@@ -182,6 +299,24 @@ describe("session-services-ticketing component", () => {
|
|
|
182
299
|
expect(item.quoteError).toBe("Failed to update cart. Please try again.");
|
|
183
300
|
expect(item.isCheckoutDisabled()).toBe(true);
|
|
184
301
|
});
|
|
302
|
+
it("offers an explicit retry when a cart quote fails", async () => {
|
|
303
|
+
const item = widget();
|
|
304
|
+
item.selectedItems = new Map([[selectionKey("adm_general", null), 1]]);
|
|
305
|
+
item.client.event.get = vi.fn().mockResolvedValue({
|
|
306
|
+
event: item.event,
|
|
307
|
+
});
|
|
308
|
+
item.client.order.quote = vi
|
|
309
|
+
.fn()
|
|
310
|
+
.mockRejectedValueOnce(new Error("Down"))
|
|
311
|
+
.mockResolvedValueOnce({ quote: quote() });
|
|
312
|
+
await item.updateQuote();
|
|
313
|
+
const view = item.renderReadyState();
|
|
314
|
+
const retry = rendered(view, (node) => node.tag === "button" && textContent(node) === "Try again");
|
|
315
|
+
expect(retry).toHaveLength(1);
|
|
316
|
+
await retry[0].props.onClick();
|
|
317
|
+
expect(item.quote).not.toBeNull();
|
|
318
|
+
expect(item.quoteError).toBeNull();
|
|
319
|
+
});
|
|
185
320
|
it("does not commit failed cart coupon applications", async () => {
|
|
186
321
|
const item = widget();
|
|
187
322
|
const current = quote();
|
|
@@ -189,15 +324,18 @@ describe("session-services-ticketing component", () => {
|
|
|
189
324
|
item.appliedCouponCode = "SAVE10";
|
|
190
325
|
item.couponInput = "bad";
|
|
191
326
|
item.selectedItems = new Map([[selectionKey("adm_general", null), 1]]);
|
|
327
|
+
item.client.event.get = vi.fn().mockResolvedValue({
|
|
328
|
+
event: item.event,
|
|
329
|
+
});
|
|
192
330
|
item.client.order.quote = vi
|
|
193
331
|
.fn()
|
|
194
332
|
.mockRejectedValue(new Error("Invalid coupon"));
|
|
195
333
|
await item.applyCoupon();
|
|
196
334
|
expect(item.appliedCouponCode).toBe("SAVE10");
|
|
197
|
-
expect(item.couponInput).toBe("
|
|
335
|
+
expect(item.couponInput).toBe("SAVE10");
|
|
198
336
|
expect(item.quote).toBe(current);
|
|
199
337
|
expect(item.couponError).toBe("Failed to apply coupon. Please check the code.");
|
|
200
|
-
expect(item.client.order.quote).
|
|
338
|
+
expect(vi.mocked(item.client.order.quote).mock.calls[0][0]).toEqual(expect.objectContaining({ couponCode: "BAD" }));
|
|
201
339
|
});
|
|
202
340
|
it("does not commit preview coupons when event reload fails", async () => {
|
|
203
341
|
const item = widget();
|
|
@@ -206,7 +344,7 @@ describe("session-services-ticketing component", () => {
|
|
|
206
344
|
item.loadEventData = vi.fn().mockRejectedValue(new Error("Down"));
|
|
207
345
|
await item.applyCoupon();
|
|
208
346
|
expect(item.appliedCouponCode).toBe("SAVE10");
|
|
209
|
-
expect(item.couponInput).toBe("
|
|
347
|
+
expect(item.couponInput).toBe("SAVE10");
|
|
210
348
|
expect(item.couponError).toBe("Failed to apply coupon. Please check the code.");
|
|
211
349
|
});
|
|
212
350
|
it("does not apply stale cart coupons as preview coupons", async () => {
|
|
@@ -219,24 +357,61 @@ describe("session-services-ticketing component", () => {
|
|
|
219
357
|
.fn()
|
|
220
358
|
.mockReturnValueOnce(firstLoad.promise)
|
|
221
359
|
.mockReturnValueOnce(secondLoad.promise)
|
|
222
|
-
.mockResolvedValue(
|
|
360
|
+
.mockResolvedValue({
|
|
361
|
+
status: "success",
|
|
362
|
+
event: item.event,
|
|
363
|
+
});
|
|
223
364
|
item.client.order.quote = vi.fn().mockResolvedValue({ quote: quote() });
|
|
224
365
|
const stale = item.applyCoupon();
|
|
225
366
|
await Promise.resolve();
|
|
226
|
-
expect(item.appliedCouponCode).toBe("
|
|
367
|
+
expect(item.appliedCouponCode).toBe("");
|
|
227
368
|
item.couponInput = "new";
|
|
228
369
|
const latest = item.applyCoupon();
|
|
229
370
|
await Promise.resolve();
|
|
230
|
-
expect(item.appliedCouponCode).toBe("
|
|
231
|
-
|
|
232
|
-
|
|
371
|
+
expect(item.appliedCouponCode).toBe("");
|
|
372
|
+
firstLoad.resolve({
|
|
373
|
+
status: "success",
|
|
374
|
+
event: item.event,
|
|
375
|
+
});
|
|
233
376
|
await stale;
|
|
234
|
-
expect(item.appliedCouponCode).toBe("
|
|
235
|
-
expect(
|
|
236
|
-
|
|
377
|
+
expect(item.appliedCouponCode).toBe("");
|
|
378
|
+
expect(vi
|
|
379
|
+
.mocked(item.client.order.quote)
|
|
380
|
+
.mock.calls.every(([input]) => input.couponCode !== "OLD")).toBe(true);
|
|
381
|
+
secondLoad.resolve({
|
|
382
|
+
status: "success",
|
|
383
|
+
event: item.event,
|
|
384
|
+
});
|
|
237
385
|
await latest;
|
|
238
386
|
expect(item.appliedCouponCode).toBe("NEW");
|
|
239
387
|
});
|
|
388
|
+
it("fetches exactly one cart quote when applying a coupon", async () => {
|
|
389
|
+
const item = widget();
|
|
390
|
+
item.couponInput = "save10";
|
|
391
|
+
item.selectedItems = new Map([[selectionKey("adm_general", null), 2]]);
|
|
392
|
+
item.client.event.get = vi.fn().mockResolvedValue({
|
|
393
|
+
event: item.event,
|
|
394
|
+
});
|
|
395
|
+
item.client.order.quote = vi.fn().mockResolvedValue({ quote: quote() });
|
|
396
|
+
await item.applyCoupon();
|
|
397
|
+
expect(item.client.order.quote).toHaveBeenCalledOnce();
|
|
398
|
+
expect(vi.mocked(item.client.order.quote).mock.calls[0][0]).toEqual(expect.objectContaining({
|
|
399
|
+
couponCode: "SAVE10",
|
|
400
|
+
items: [expect.objectContaining({ quantity: 2 })],
|
|
401
|
+
}));
|
|
402
|
+
});
|
|
403
|
+
it("fetches exactly one preview quote when applying a coupon", async () => {
|
|
404
|
+
const item = widget();
|
|
405
|
+
item.couponInput = "save10";
|
|
406
|
+
item.selectedItems = new Map();
|
|
407
|
+
item.client.event.get = vi.fn().mockResolvedValue({
|
|
408
|
+
event: item.event,
|
|
409
|
+
});
|
|
410
|
+
item.client.order.quote = vi.fn().mockResolvedValue({ quote: quote() });
|
|
411
|
+
await item.applyCoupon();
|
|
412
|
+
expect(item.client.order.quote).toHaveBeenCalledOnce();
|
|
413
|
+
expect(vi.mocked(item.client.order.quote).mock.calls[0][0]).toEqual(expect.objectContaining({ couponCode: "SAVE10" }));
|
|
414
|
+
});
|
|
240
415
|
it("keeps preview quote loading separate from cart quote loading", async () => {
|
|
241
416
|
const item = widget();
|
|
242
417
|
const next = deferred();
|
|
@@ -321,6 +496,12 @@ describe("session-services-ticketing component", () => {
|
|
|
321
496
|
expect(increments).toHaveLength(1);
|
|
322
497
|
expect(checkout).toHaveLength(1);
|
|
323
498
|
});
|
|
499
|
+
it("does not add a redundant tab stop to admission cards", () => {
|
|
500
|
+
const item = widget();
|
|
501
|
+
const cards = rendered(item.renderReadyState(), (node) => node.props["data-testid"] === "event-admission-card");
|
|
502
|
+
expect(cards).toHaveLength(1);
|
|
503
|
+
expect(cards[0].props.tabindex).toBeUndefined();
|
|
504
|
+
});
|
|
324
505
|
it("renders external ticketing in the sticky checkout bar", () => {
|
|
325
506
|
const item = widget();
|
|
326
507
|
item.event = event({ url: "https://tickets.example" });
|
|
@@ -357,10 +538,16 @@ describe("session-services-ticketing component", () => {
|
|
|
357
538
|
const links = rendered(item.renderReadyState(), (node) => node.tag === "a");
|
|
358
539
|
const inline = links.find((link) => hasClass(link, "inline-external-ticket-button"));
|
|
359
540
|
const sticky = links.find((link) => hasClass(link, "sticky-checkout-btn"));
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
(
|
|
363
|
-
(
|
|
541
|
+
const inlineClick = inline?.props.onClick;
|
|
542
|
+
const stickyClick = sticky?.props.onClick;
|
|
543
|
+
expect(typeof inlineClick).toBe("function");
|
|
544
|
+
expect(typeof stickyClick).toBe("function");
|
|
545
|
+
if (typeof inlineClick !== "function" ||
|
|
546
|
+
typeof stickyClick !== "function") {
|
|
547
|
+
throw new Error("Expected both external ticket links to be clickable");
|
|
548
|
+
}
|
|
549
|
+
inlineClick();
|
|
550
|
+
stickyClick();
|
|
364
551
|
expect(track).toHaveBeenCalledWith("Checkout Started", expect.objectContaining({
|
|
365
552
|
event_id: "evt_1",
|
|
366
553
|
team_id: "team_1",
|
|
@@ -377,22 +564,116 @@ describe("session-services-ticketing component", () => {
|
|
|
377
564
|
cta_surface: "sticky",
|
|
378
565
|
}));
|
|
379
566
|
});
|
|
567
|
+
it("tracks page, product, and product list views after event load", () => {
|
|
568
|
+
const item = widget();
|
|
569
|
+
const page = vi.fn();
|
|
570
|
+
const track = vi.fn();
|
|
571
|
+
item.analytics = { page, track };
|
|
572
|
+
item.trackLoadedEvent(item.event);
|
|
573
|
+
expect(page).toHaveBeenCalledWith("Ticketing Widget", expect.objectContaining({
|
|
574
|
+
event_id: "evt_1",
|
|
575
|
+
team_id: "team_1",
|
|
576
|
+
promoter_id: "pro_1",
|
|
577
|
+
event_name: "Test Event",
|
|
578
|
+
event_slug: "event",
|
|
579
|
+
}));
|
|
580
|
+
expect(track).toHaveBeenCalledWith("Product Viewed", expect.objectContaining({
|
|
581
|
+
event_id: "evt_1",
|
|
582
|
+
product_id: "evt_1",
|
|
583
|
+
value: 22,
|
|
584
|
+
price: 22,
|
|
585
|
+
currency: "AUD",
|
|
586
|
+
}));
|
|
587
|
+
expect(track).toHaveBeenCalledWith("Product List Viewed", expect.objectContaining({
|
|
588
|
+
list_id: "evt_1",
|
|
589
|
+
products: [
|
|
590
|
+
expect.objectContaining({
|
|
591
|
+
admission_id: "adm_general",
|
|
592
|
+
price: 22,
|
|
593
|
+
price_base: 2000,
|
|
594
|
+
price_booking_fee: 200,
|
|
595
|
+
}),
|
|
596
|
+
],
|
|
597
|
+
}));
|
|
598
|
+
expect(item.analyticsEventSkipped.emit).not.toHaveBeenCalled();
|
|
599
|
+
});
|
|
600
|
+
it("uses currency minor units for ticketing widget analytics", () => {
|
|
601
|
+
const item = widget();
|
|
602
|
+
const track = vi.fn();
|
|
603
|
+
item.analytics = { page: vi.fn(), track };
|
|
604
|
+
item.event = event({
|
|
605
|
+
currency: "JPY",
|
|
606
|
+
admissions: [
|
|
607
|
+
admission({
|
|
608
|
+
price: {
|
|
609
|
+
base: 2200,
|
|
610
|
+
booking: 0,
|
|
611
|
+
currency: "JPY",
|
|
612
|
+
},
|
|
613
|
+
}),
|
|
614
|
+
],
|
|
615
|
+
});
|
|
616
|
+
item.trackLoadedEvent(item.event);
|
|
617
|
+
expect(track).toHaveBeenCalledWith("Product Viewed", expect.objectContaining({
|
|
618
|
+
value: 2200,
|
|
619
|
+
price: 2200,
|
|
620
|
+
currency: "JPY",
|
|
621
|
+
}));
|
|
622
|
+
expect(track).toHaveBeenCalledWith("Product List Viewed", expect.objectContaining({
|
|
623
|
+
products: [
|
|
624
|
+
expect.objectContaining({
|
|
625
|
+
price: 2200,
|
|
626
|
+
price_base: 2200,
|
|
627
|
+
price_booking_fee: 0,
|
|
628
|
+
}),
|
|
629
|
+
],
|
|
630
|
+
}));
|
|
631
|
+
});
|
|
632
|
+
it("emits an analytics diagnostic when event identity is incomplete", () => {
|
|
633
|
+
const item = widget();
|
|
634
|
+
const page = vi.fn();
|
|
635
|
+
const track = vi.fn();
|
|
636
|
+
item.analytics = { page, track };
|
|
637
|
+
const missingTeam = event({ teamId: "" });
|
|
638
|
+
item.event = missingTeam;
|
|
639
|
+
item.trackLoadedEvent(missingTeam);
|
|
640
|
+
expect(page).not.toHaveBeenCalled();
|
|
641
|
+
expect(track).not.toHaveBeenCalled();
|
|
642
|
+
expect(item.analyticsEventSkipped.emit).toHaveBeenCalledWith({
|
|
643
|
+
eventType: "Page Viewed",
|
|
644
|
+
eventId: "evt_1",
|
|
645
|
+
reason: "missing_team_id",
|
|
646
|
+
promoterId: "pro_1",
|
|
647
|
+
teamId: undefined,
|
|
648
|
+
});
|
|
649
|
+
});
|
|
380
650
|
it("tracks hosted checkout starts with the created order id", async () => {
|
|
381
651
|
const item = widget();
|
|
382
652
|
const track = vi.fn();
|
|
383
653
|
item.analytics = { track };
|
|
384
|
-
item.quote = quote();
|
|
385
654
|
item.selectedItems = new Map([[selectionKey("adm_general", null), 1]]);
|
|
655
|
+
item.client.order.quote = vi.fn().mockResolvedValue({ quote: quote() });
|
|
386
656
|
item.client.order.create = vi.fn().mockResolvedValue({
|
|
387
657
|
order: {
|
|
388
658
|
checkoutUrl: "https://checkout.example/ord_1",
|
|
389
659
|
id: "ord_1",
|
|
390
660
|
},
|
|
391
661
|
});
|
|
662
|
+
await item.updateQuote();
|
|
392
663
|
await item.initiateCheckout();
|
|
393
664
|
expect(track).toHaveBeenCalledWith("Checkout Started", expect.objectContaining({
|
|
394
665
|
order_id: "ord_1",
|
|
395
666
|
value: 22,
|
|
667
|
+
revenue: 22,
|
|
668
|
+
currency: "AUD",
|
|
669
|
+
products: [
|
|
670
|
+
expect.objectContaining({
|
|
671
|
+
admission_id: "adm_general",
|
|
672
|
+
price: 22,
|
|
673
|
+
price_base: 2000,
|
|
674
|
+
price_booking_fee: 200,
|
|
675
|
+
}),
|
|
676
|
+
],
|
|
396
677
|
}));
|
|
397
678
|
});
|
|
398
679
|
it("scrolls to the ticket selector with a 100px top offset", () => {
|
|
@@ -407,13 +688,29 @@ describe("session-services-ticketing component", () => {
|
|
|
407
688
|
},
|
|
408
689
|
getBoundingClientRect: () => ({ top: 900 }),
|
|
409
690
|
};
|
|
410
|
-
vi.stubGlobal("window", {
|
|
411
|
-
|
|
691
|
+
vi.stubGlobal("window", {
|
|
692
|
+
matchMedia: vi.fn().mockReturnValue({ matches: false }),
|
|
693
|
+
scrollY: 250,
|
|
694
|
+
scrollTo,
|
|
695
|
+
});
|
|
696
|
+
item.handleStickyCheckoutClick(getTicketingState(event()));
|
|
412
697
|
expect(scrollTo).toHaveBeenCalledWith({
|
|
413
698
|
behavior: "smooth",
|
|
414
699
|
top: 570,
|
|
415
700
|
});
|
|
416
|
-
|
|
701
|
+
});
|
|
702
|
+
it("shows only the placeholder name for secret event locations", () => {
|
|
703
|
+
const item = widget();
|
|
704
|
+
const node = item.renderEventLocation(event({
|
|
705
|
+
locationRevealAt: futureStart,
|
|
706
|
+
location: {
|
|
707
|
+
...event().location,
|
|
708
|
+
name: "Secret location",
|
|
709
|
+
},
|
|
710
|
+
}), "admission-venue");
|
|
711
|
+
expect(node.children).toHaveLength(2);
|
|
712
|
+
expect(node.children).toContain("Secret location");
|
|
713
|
+
expect(textContent(node)).not.toContain("Address revealed");
|
|
417
714
|
});
|
|
418
715
|
it("classifies only passcode access failures as passcode errors", async () => {
|
|
419
716
|
const item = widget();
|
|
@@ -430,4 +727,298 @@ describe("session-services-ticketing component", () => {
|
|
|
430
727
|
await item.loadEventData();
|
|
431
728
|
expect(item.passcodeDenied).toBe(true);
|
|
432
729
|
});
|
|
730
|
+
it("starts initial loading without delaying the first render", () => {
|
|
731
|
+
const item = widget();
|
|
732
|
+
const initialize = vi.fn().mockResolvedValue(undefined);
|
|
733
|
+
item.event = null;
|
|
734
|
+
item.initialLoading = true;
|
|
735
|
+
item.loading = true;
|
|
736
|
+
item.initializeComponent = initialize;
|
|
737
|
+
expect(item.componentWillLoad()).toBeUndefined();
|
|
738
|
+
expect(initialize).toHaveBeenCalledOnce();
|
|
739
|
+
const view = item.pendingView();
|
|
740
|
+
const status = rendered(view, (node) => node.props.role === "status");
|
|
741
|
+
expect(status).toHaveLength(1);
|
|
742
|
+
expect(status[0].props["aria-live"]).toBe("polite");
|
|
743
|
+
expect(rendered(view, (node) => hasClass(node, "loading-admission"))).toHaveLength(3);
|
|
744
|
+
});
|
|
745
|
+
it("keeps ticket content mounted during a background event refresh", () => {
|
|
746
|
+
const item = widget();
|
|
747
|
+
item.initialLoading = false;
|
|
748
|
+
item.loading = false;
|
|
749
|
+
item.eventLoading = true;
|
|
750
|
+
const view = item.render();
|
|
751
|
+
expect(rendered(view, (node) => node.props["data-testid"] === "event-admission-card")).toHaveLength(1);
|
|
752
|
+
expect(rendered(view, (node) => hasClass(node, "loading-admission"))).toHaveLength(0);
|
|
753
|
+
expect(rendered(view, (node) => node.props["aria-busy"] === "true")).not.toHaveLength(0);
|
|
754
|
+
});
|
|
755
|
+
it("ignores an event response after a newer event load wins", async () => {
|
|
756
|
+
const item = widget();
|
|
757
|
+
const first = deferred();
|
|
758
|
+
const second = deferred();
|
|
759
|
+
const firstEvent = event({ id: "evt_first", slug: "first" });
|
|
760
|
+
const secondEvent = event({ id: "evt_second", slug: "second" });
|
|
761
|
+
item.event = null;
|
|
762
|
+
item.initialLoading = false;
|
|
763
|
+
item.loading = false;
|
|
764
|
+
item.client.event.get = vi
|
|
765
|
+
.fn()
|
|
766
|
+
.mockReturnValueOnce(first.promise)
|
|
767
|
+
.mockReturnValueOnce(second.promise);
|
|
768
|
+
item.eventId = firstEvent.id;
|
|
769
|
+
const stale = item.loadEventData();
|
|
770
|
+
item.eventId = secondEvent.id;
|
|
771
|
+
const latest = item.loadEventData();
|
|
772
|
+
second.resolve({ event: secondEvent });
|
|
773
|
+
await latest;
|
|
774
|
+
first.resolve({ event: firstEvent });
|
|
775
|
+
await stale;
|
|
776
|
+
expect(item.event?.id).toBe(secondEvent.id);
|
|
777
|
+
expect(item.eventId).toBe(secondEvent.id);
|
|
778
|
+
});
|
|
779
|
+
it("does not commit an event response after disconnection", async () => {
|
|
780
|
+
const item = widget();
|
|
781
|
+
const response = deferred();
|
|
782
|
+
item.event = null;
|
|
783
|
+
item.initialLoading = false;
|
|
784
|
+
item.loading = false;
|
|
785
|
+
item.client.event.get = vi.fn().mockReturnValue(response.promise);
|
|
786
|
+
vi.stubGlobal("document", { removeEventListener: vi.fn() });
|
|
787
|
+
const pending = item.loadEventData();
|
|
788
|
+
item.disconnectedCallback();
|
|
789
|
+
response.resolve({ event: event() });
|
|
790
|
+
await pending;
|
|
791
|
+
expect(item.event).toBeNull();
|
|
792
|
+
vi.unstubAllGlobals();
|
|
793
|
+
});
|
|
794
|
+
it("invalidates an active quote as soon as its selection changes", async () => {
|
|
795
|
+
vi.useFakeTimers();
|
|
796
|
+
const item = widget();
|
|
797
|
+
const stale = deferred();
|
|
798
|
+
item.selectedItems = new Map([[selectionKey("adm_general", null), 1]]);
|
|
799
|
+
item.client.order.quote = vi.fn().mockReturnValue(stale.promise);
|
|
800
|
+
const pending = item.updateQuote();
|
|
801
|
+
await Promise.resolve();
|
|
802
|
+
await item.updateQuantity("adm_general", null, -1);
|
|
803
|
+
stale.resolve({ quote: quote() });
|
|
804
|
+
await pending;
|
|
805
|
+
expect(item.selectedItems.size).toBe(0);
|
|
806
|
+
expect(item.quote).toBeNull();
|
|
807
|
+
expect(item.isCheckoutDisabled()).toBe(true);
|
|
808
|
+
vi.clearAllTimers();
|
|
809
|
+
vi.useRealTimers();
|
|
810
|
+
});
|
|
811
|
+
it("rolls back a coupon removal when the replacement quote fails", async () => {
|
|
812
|
+
const item = widget();
|
|
813
|
+
const currentEvent = item.event;
|
|
814
|
+
const currentQuote = quote();
|
|
815
|
+
item.appliedCouponCode = "SAVE10";
|
|
816
|
+
item.couponInput = "SAVE10";
|
|
817
|
+
item.quote = currentQuote;
|
|
818
|
+
item.selectedItems = new Map([[selectionKey("adm_general", null), 1]]);
|
|
819
|
+
item.client.event.get = vi.fn().mockResolvedValue({ event: currentEvent });
|
|
820
|
+
item.client.order.quote = vi.fn().mockRejectedValue(new Error("Down"));
|
|
821
|
+
await item.removeCoupon();
|
|
822
|
+
expect(item.appliedCouponCode).toBe("SAVE10");
|
|
823
|
+
expect(item.couponInput).toBe("SAVE10");
|
|
824
|
+
expect(item.event).toBe(currentEvent);
|
|
825
|
+
expect(item.quote).toBe(currentQuote);
|
|
826
|
+
expect(item.couponError).toContain("remove coupon");
|
|
827
|
+
});
|
|
828
|
+
it("keeps a valid quote retryable after checkout creation fails", async () => {
|
|
829
|
+
const item = widget();
|
|
830
|
+
item.selectedItems = new Map([[selectionKey("adm_general", null), 1]]);
|
|
831
|
+
item.client.order.quote = vi.fn().mockResolvedValue({ quote: quote() });
|
|
832
|
+
item.client.order.create = vi.fn().mockRejectedValue(new Error("Down"));
|
|
833
|
+
await item.updateQuote();
|
|
834
|
+
await item.initiateCheckout();
|
|
835
|
+
await item.initiateCheckout();
|
|
836
|
+
expect(item.quote).not.toBeNull();
|
|
837
|
+
expect(item.quoteError).toBeNull();
|
|
838
|
+
expect(item.checkoutError?.toLowerCase()).toContain("checkout");
|
|
839
|
+
expect(item.isCheckoutDisabled()).toBe(false);
|
|
840
|
+
const checkout = rendered(item.renderReadyState(), (node) => node.props["data-testid"] === "event-checkout-cta-desktop")[0];
|
|
841
|
+
expect(checkout.props.disabled).toBe(false);
|
|
842
|
+
const calls = vi.mocked(item.client.order.create).mock.calls;
|
|
843
|
+
expect(calls).toHaveLength(2);
|
|
844
|
+
const firstRequest = calls[0]?.[0];
|
|
845
|
+
if (typeof firstRequest !== "object" ||
|
|
846
|
+
firstRequest === null ||
|
|
847
|
+
!("idempotencyKey" in firstRequest) ||
|
|
848
|
+
typeof firstRequest.idempotencyKey !== "string") {
|
|
849
|
+
throw new Error("Expected the first checkout request to have a key");
|
|
850
|
+
}
|
|
851
|
+
const idempotencyKey = firstRequest.idempotencyKey;
|
|
852
|
+
expect(calls[0]?.[0]).toEqual(expect.objectContaining({ idempotencyKey: expect.any(String) }));
|
|
853
|
+
expect(calls[1]?.[0]).toEqual(expect.objectContaining({
|
|
854
|
+
idempotencyKey,
|
|
855
|
+
}));
|
|
856
|
+
});
|
|
857
|
+
it("blocks quantity changes while order creation is pending", async () => {
|
|
858
|
+
const item = widget();
|
|
859
|
+
const pending = deferred();
|
|
860
|
+
item.selectedItems = new Map([[selectionKey("adm_general", null), 1]]);
|
|
861
|
+
item.client.order.quote = vi.fn().mockResolvedValue({ quote: quote() });
|
|
862
|
+
item.client.order.create = vi.fn().mockReturnValue(pending.promise);
|
|
863
|
+
await item.updateQuote();
|
|
864
|
+
const checkout = item.initiateCheckout();
|
|
865
|
+
await vi.waitFor(() => {
|
|
866
|
+
expect(item.client.order.create).toHaveBeenCalledOnce();
|
|
867
|
+
});
|
|
868
|
+
await item.updateQuantity("adm_general", null, 1);
|
|
869
|
+
expect(item.selectedItems.get(selectionKey("adm_general", null))).toBe(1);
|
|
870
|
+
pending.resolve({ order: { id: "ord_1" } });
|
|
871
|
+
await checkout;
|
|
872
|
+
});
|
|
873
|
+
it("closes the modal when a refreshed event removes its admission", () => {
|
|
874
|
+
const item = widget();
|
|
875
|
+
item.openModalAdmissionId = "adm_removed";
|
|
876
|
+
item.pruneSelectedItems(event());
|
|
877
|
+
expect(item.openModalAdmissionId).toBeNull();
|
|
878
|
+
});
|
|
879
|
+
it("keeps refresh feedback visible when its retry fails", async () => {
|
|
880
|
+
const item = widget();
|
|
881
|
+
item.client.event.get = vi.fn().mockRejectedValue(new Error("Still down"));
|
|
882
|
+
await item.retryEventLoad();
|
|
883
|
+
expect(item.refreshError).toBe("Still down");
|
|
884
|
+
expect(item.error).toBeNull();
|
|
885
|
+
});
|
|
886
|
+
it("shows the applied coupon consistently until it is removed", () => {
|
|
887
|
+
const item = widget();
|
|
888
|
+
item.appliedCouponCode = "SAVE10";
|
|
889
|
+
item.couponInput = "BAD";
|
|
890
|
+
const input = rendered(item.renderReadyState(), (node) => node.tag === "input" && node.props["aria-label"] === "Coupon code")[0];
|
|
891
|
+
expect(input.props.value).toBe("SAVE10");
|
|
892
|
+
expect(input.props.disabled || input.props.readonly).toBe(true);
|
|
893
|
+
});
|
|
894
|
+
it("renders configuration failures without an unusable retry action", async () => {
|
|
895
|
+
const item = widget();
|
|
896
|
+
item.event = null;
|
|
897
|
+
item.tenantId = "";
|
|
898
|
+
await item.initializeComponent();
|
|
899
|
+
const view = item.pendingView();
|
|
900
|
+
expect(item.errorKind).toBe("configuration");
|
|
901
|
+
expect(textContent(view).toLowerCase()).toMatch(/configur|set ?up/);
|
|
902
|
+
expect(rendered(view, (node) => node.tag === "button")).toHaveLength(0);
|
|
903
|
+
});
|
|
904
|
+
it("renders not found separately from retryable load failures", async () => {
|
|
905
|
+
const item = widget();
|
|
906
|
+
item.event = null;
|
|
907
|
+
item.initialLoading = false;
|
|
908
|
+
item.loading = false;
|
|
909
|
+
item.client.event.get = vi.fn().mockRejectedValue(Object.assign(new Error("Missing"), {
|
|
910
|
+
code: "NOT_FOUND",
|
|
911
|
+
}));
|
|
912
|
+
await item.loadEventData();
|
|
913
|
+
const view = item.pendingView();
|
|
914
|
+
expect(item.errorKind).toBe("not_found");
|
|
915
|
+
expect(textContent(view)).toContain("Event not found");
|
|
916
|
+
expect(rendered(view, (node) => node.tag === "button")).toHaveLength(0);
|
|
917
|
+
});
|
|
918
|
+
it("announces transient load failures and keeps retry available", async () => {
|
|
919
|
+
const item = widget();
|
|
920
|
+
item.event = null;
|
|
921
|
+
item.initialLoading = false;
|
|
922
|
+
item.loading = false;
|
|
923
|
+
item.client.event.get = vi.fn().mockRejectedValue(new Error("Down"));
|
|
924
|
+
await item.loadEventData();
|
|
925
|
+
const view = item.pendingView();
|
|
926
|
+
const alert = rendered(view, (node) => node.props.role === "alert");
|
|
927
|
+
const retry = rendered(view, (node) => node.tag === "button" && textContent(node) === "Try again");
|
|
928
|
+
expect(item.errorKind).toBe("transient");
|
|
929
|
+
expect(alert).toHaveLength(1);
|
|
930
|
+
expect(alert[0].props["aria-live"]).toBe("assertive");
|
|
931
|
+
expect(retry).toHaveLength(1);
|
|
932
|
+
expect(hasClass(retry[0], "checkout-btn")).toBe(true);
|
|
933
|
+
});
|
|
934
|
+
it("keeps passcode feedback associated with the mounted form", () => {
|
|
935
|
+
const item = widget();
|
|
936
|
+
item.event = null;
|
|
937
|
+
item.initialLoading = false;
|
|
938
|
+
item.loading = false;
|
|
939
|
+
item.error = "Incorrect passcode";
|
|
940
|
+
item.errorKind = "access";
|
|
941
|
+
item.passcodeDenied = true;
|
|
942
|
+
item.activePasscode = "wrong";
|
|
943
|
+
item.passcodeInput = "wrong";
|
|
944
|
+
item.passcodeLoading = true;
|
|
945
|
+
const view = item.pendingView();
|
|
946
|
+
const input = rendered(view, (node) => node.tag === "input" && node.props.name === "passcode")[0];
|
|
947
|
+
const button = rendered(view, (node) => node.tag === "button" && node.props.type === "submit")[0];
|
|
948
|
+
const feedback = rendered(view, (node) => node.props.id === input.props["aria-describedby"])[0];
|
|
949
|
+
expect(input.props["aria-invalid"]).toBe(true);
|
|
950
|
+
expect(input.props["aria-describedby"]).toBeTruthy();
|
|
951
|
+
expect(feedback.props.role).toBe("alert");
|
|
952
|
+
expect(textContent(button)).toContain("Unlocking");
|
|
953
|
+
});
|
|
954
|
+
it("describes ticket detail dialogs with their visible content", () => {
|
|
955
|
+
const item = widget();
|
|
956
|
+
item.event = event({
|
|
957
|
+
admissions: [admission({ description: "Includes balcony access." })],
|
|
958
|
+
});
|
|
959
|
+
item.openModalAdmissionId = "adm_general";
|
|
960
|
+
const view = item.renderReadyState();
|
|
961
|
+
const dialog = rendered(view, (node) => node.props.role === "dialog")[0];
|
|
962
|
+
const description = rendered(view, (node) => node.props.id === dialog.props["aria-describedby"])[0];
|
|
963
|
+
expect(dialog.props["aria-modal"]).toBe("true");
|
|
964
|
+
expect(dialog.props["aria-describedby"]).toBeTruthy();
|
|
965
|
+
expect(textContent(description)).toContain("Includes balcony access.");
|
|
966
|
+
});
|
|
967
|
+
it("renders door-only sticky messaging as information, not a button", () => {
|
|
968
|
+
const item = widget();
|
|
969
|
+
item.stickyCheckout = true;
|
|
970
|
+
item.event = event({
|
|
971
|
+
admissions: [
|
|
972
|
+
admission({
|
|
973
|
+
id: "adm_door",
|
|
974
|
+
saleEndsAt: pastStart,
|
|
975
|
+
saleEndsBehavior: "DOOR",
|
|
976
|
+
}),
|
|
977
|
+
],
|
|
978
|
+
});
|
|
979
|
+
const view = item.renderReadyState();
|
|
980
|
+
const sticky = rendered(view, (node) => hasClass(node, "sticky-checkout-bar"));
|
|
981
|
+
expect(sticky).toHaveLength(1);
|
|
982
|
+
expect(textContent(sticky[0]).toLowerCase()).toContain("at the door");
|
|
983
|
+
expect(rendered(sticky[0], (node) => node.tag === "button")).toHaveLength(0);
|
|
984
|
+
});
|
|
985
|
+
it("recreates analytics for a new client without replaying a refresh view", async () => {
|
|
986
|
+
const item = widget();
|
|
987
|
+
const firstAnalytics = { page: vi.fn(), track: vi.fn() };
|
|
988
|
+
const secondAnalytics = { page: vi.fn(), track: vi.fn() };
|
|
989
|
+
const firstClient = {
|
|
990
|
+
event: { get: vi.fn().mockResolvedValue({ event: event() }) },
|
|
991
|
+
order: {
|
|
992
|
+
create: vi.fn(),
|
|
993
|
+
quote: vi.fn().mockResolvedValue({ quote: quote() }),
|
|
994
|
+
},
|
|
995
|
+
};
|
|
996
|
+
const secondEvent = event({ id: "evt_2", slug: "event-2" });
|
|
997
|
+
const secondClient = {
|
|
998
|
+
event: { get: vi.fn().mockResolvedValue({ event: secondEvent }) },
|
|
999
|
+
order: {
|
|
1000
|
+
create: vi.fn(),
|
|
1001
|
+
quote: vi.fn().mockResolvedValue({ quote: quote() }),
|
|
1002
|
+
},
|
|
1003
|
+
};
|
|
1004
|
+
createClient
|
|
1005
|
+
.mockReturnValueOnce(firstClient)
|
|
1006
|
+
.mockReturnValueOnce(secondClient);
|
|
1007
|
+
createAnalytics
|
|
1008
|
+
.mockReturnValueOnce(firstAnalytics)
|
|
1009
|
+
.mockReturnValueOnce(secondAnalytics);
|
|
1010
|
+
item.event = null;
|
|
1011
|
+
item.initialLoading = true;
|
|
1012
|
+
item.loading = true;
|
|
1013
|
+
await item.initializeComponent();
|
|
1014
|
+
await item.loadEventData();
|
|
1015
|
+
expect(firstAnalytics.page).toHaveBeenCalledOnce();
|
|
1016
|
+
item.tenantId = "tnt_2";
|
|
1017
|
+
item.eventId = secondEvent.id;
|
|
1018
|
+
await item.initializeComponent();
|
|
1019
|
+
expect(createAnalytics).toHaveBeenCalledTimes(2);
|
|
1020
|
+
expect(createAnalytics).toHaveBeenNthCalledWith(1, expect.objectContaining({ client: firstClient }));
|
|
1021
|
+
expect(createAnalytics).toHaveBeenNthCalledWith(2, expect.objectContaining({ client: secondClient }));
|
|
1022
|
+
expect(secondAnalytics.page).toHaveBeenCalledOnce();
|
|
1023
|
+
});
|
|
433
1024
|
});
|