@session-services/web-elements 0.1.23 → 0.1.25
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/cjs/session-services-ticketing.cjs.entry.js +218 -71
- package/dist/collection/components/session-services-ticketing/session-services-ticketing.js +81 -14
- package/dist/collection/components/session-services-ticketing/session-services-ticketing.logic.js +93 -9
- package/dist/collection/components/session-services-ticketing/session-services-ticketing.logic.test.js +183 -0
- package/dist/collection/components/session-services-ticketing/session-services-ticketing.test.js +106 -0
- package/dist/components/session-services-ticketing.js +218 -71
- package/dist/esm/session-services-ticketing.entry.js +218 -71
- package/dist/session-services-elements/p-edf63ff2.entry.js +1 -0
- package/dist/session-services-elements/session-services-elements.esm.js +1 -1
- package/dist/types/components/session-services-ticketing/session-services-ticketing.d.ts +3 -0
- package/dist/types/components/session-services-ticketing/session-services-ticketing.logic.d.ts +11 -6
- package/package.json +2 -2
- package/dist/session-services-elements/p-ddf8ca4b.entry.js +0 -1
|
@@ -42,6 +42,7 @@ function section(data = {}) {
|
|
|
42
42
|
reserved: 0,
|
|
43
43
|
limit: 10,
|
|
44
44
|
order: 2,
|
|
45
|
+
visibility: "ALWAYS",
|
|
45
46
|
teamId: "team_1",
|
|
46
47
|
promoterId: "pro_1",
|
|
47
48
|
eventId: "evt_1",
|
|
@@ -113,6 +114,25 @@ function quoteItem(data = {}) {
|
|
|
113
114
|
...data,
|
|
114
115
|
};
|
|
115
116
|
}
|
|
117
|
+
function quote(items) {
|
|
118
|
+
return {
|
|
119
|
+
items,
|
|
120
|
+
price: {
|
|
121
|
+
base: 0,
|
|
122
|
+
booking: 0,
|
|
123
|
+
currency: "AUD",
|
|
124
|
+
discount: 0,
|
|
125
|
+
dispute: 0,
|
|
126
|
+
gateway: 0,
|
|
127
|
+
gross: 0,
|
|
128
|
+
net: 0,
|
|
129
|
+
refunded: 0,
|
|
130
|
+
shipping: 0,
|
|
131
|
+
sms: 0,
|
|
132
|
+
total: 0,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
116
136
|
describe("session-services-ticketing logic", () => {
|
|
117
137
|
it("reads discount coupon codes from query strings", () => {
|
|
118
138
|
expect(discountCouponFromSearch("?discount=save20")).toBe("save20");
|
|
@@ -255,6 +275,169 @@ describe("session-services-ticketing logic", () => {
|
|
|
255
275
|
const items = topLevelTicketItems([main], [sectioned, early]);
|
|
256
276
|
expect(items.map((item) => item.type)).toEqual(["admission", "section"]);
|
|
257
277
|
});
|
|
278
|
+
it("lists every visible admission in list sections", () => {
|
|
279
|
+
const main = section({ id: "sec_main", visibility: "ALWAYS" });
|
|
280
|
+
const first = admission({ id: "adm_first", order: 2, sectionId: main.id });
|
|
281
|
+
const second = admission({
|
|
282
|
+
id: "adm_second",
|
|
283
|
+
order: 1,
|
|
284
|
+
sectionId: main.id,
|
|
285
|
+
});
|
|
286
|
+
const [item] = topLevelTicketItems([main], [first, second]);
|
|
287
|
+
expect(item?.type).toBe("section");
|
|
288
|
+
expect(item?.type === "section" ? item.admissions.map((adm) => adm.id) : []).toEqual(["adm_second", "adm_first"]);
|
|
289
|
+
});
|
|
290
|
+
it("shows the cheapest base-price admission in stacked sections", () => {
|
|
291
|
+
const main = section({ visibility: "STACKED" });
|
|
292
|
+
const expensive = admission({
|
|
293
|
+
id: "adm_expensive",
|
|
294
|
+
order: 1,
|
|
295
|
+
price: { base: 3000, booking: 300, currency: "AUD" },
|
|
296
|
+
sectionId: main.id,
|
|
297
|
+
});
|
|
298
|
+
const cheap = admission({
|
|
299
|
+
id: "adm_cheap",
|
|
300
|
+
order: 2,
|
|
301
|
+
price: { base: 1500, booking: 150, currency: "AUD" },
|
|
302
|
+
sectionId: main.id,
|
|
303
|
+
});
|
|
304
|
+
const [item] = topLevelTicketItems([main], [expensive, cheap]);
|
|
305
|
+
expect(item?.type).toBe("section");
|
|
306
|
+
expect(item?.type === "section" ? item.admissions.map((adm) => adm.id) : []).toEqual(["adm_cheap"]);
|
|
307
|
+
});
|
|
308
|
+
it("skips sold-out admissions when stacking sections", () => {
|
|
309
|
+
const main = section({ visibility: "STACKED" });
|
|
310
|
+
const soldOut = admission({
|
|
311
|
+
id: "adm_sold_out",
|
|
312
|
+
available: 0,
|
|
313
|
+
capacity: 1,
|
|
314
|
+
order: 1,
|
|
315
|
+
price: { base: 1000, booking: 100, currency: "AUD" },
|
|
316
|
+
sectionId: main.id,
|
|
317
|
+
sold: 1,
|
|
318
|
+
});
|
|
319
|
+
const available = admission({
|
|
320
|
+
id: "adm_available",
|
|
321
|
+
order: 2,
|
|
322
|
+
price: { base: 2000, booking: 200, currency: "AUD" },
|
|
323
|
+
sectionId: main.id,
|
|
324
|
+
});
|
|
325
|
+
const data = event({
|
|
326
|
+
admissions: [soldOut, available],
|
|
327
|
+
sections: [main],
|
|
328
|
+
});
|
|
329
|
+
const [item] = topLevelTicketItems([main], data.admissions, {
|
|
330
|
+
event: data,
|
|
331
|
+
mode: "default",
|
|
332
|
+
});
|
|
333
|
+
expect(item?.type).toBe("section");
|
|
334
|
+
expect(item?.type === "section" ? item.admissions.map((adm) => adm.id) : []).toEqual(["adm_available"]);
|
|
335
|
+
});
|
|
336
|
+
it("shows the next stacked admission when the cheapest has fewer tickets than its per-order limit", () => {
|
|
337
|
+
const main = section({ visibility: "STACKED" });
|
|
338
|
+
const lowRemaining = admission({
|
|
339
|
+
id: "adm_low_remaining",
|
|
340
|
+
available: 3,
|
|
341
|
+
limit: 4,
|
|
342
|
+
price: { base: 1000, booking: 100, currency: "AUD" },
|
|
343
|
+
sectionId: main.id,
|
|
344
|
+
});
|
|
345
|
+
const next = admission({
|
|
346
|
+
id: "adm_next",
|
|
347
|
+
order: 2,
|
|
348
|
+
price: { base: 2000, booking: 200, currency: "AUD" },
|
|
349
|
+
sectionId: main.id,
|
|
350
|
+
});
|
|
351
|
+
const [item] = topLevelTicketItems([main], [lowRemaining, next]);
|
|
352
|
+
expect(item?.type).toBe("section");
|
|
353
|
+
expect(item?.type === "section" ? item.admissions.map((adm) => adm.id) : []).toEqual(["adm_low_remaining", "adm_next"]);
|
|
354
|
+
});
|
|
355
|
+
it("shows the next stacked admission when an unlimited per-order limit has fewer than ten tickets left", () => {
|
|
356
|
+
const main = section({ visibility: "STACKED" });
|
|
357
|
+
const lowRemaining = admission({
|
|
358
|
+
id: "adm_low_remaining",
|
|
359
|
+
available: 9,
|
|
360
|
+
limit: -1,
|
|
361
|
+
price: { base: 1000, booking: 100, currency: "AUD" },
|
|
362
|
+
sectionId: main.id,
|
|
363
|
+
});
|
|
364
|
+
const next = admission({
|
|
365
|
+
id: "adm_next",
|
|
366
|
+
order: 2,
|
|
367
|
+
price: { base: 2000, booking: 200, currency: "AUD" },
|
|
368
|
+
sectionId: main.id,
|
|
369
|
+
});
|
|
370
|
+
const [item] = topLevelTicketItems([main], [lowRemaining, next]);
|
|
371
|
+
expect(item?.type).toBe("section");
|
|
372
|
+
expect(item?.type === "section" ? item.admissions.map((adm) => adm.id) : []).toEqual(["adm_low_remaining", "adm_next"]);
|
|
373
|
+
});
|
|
374
|
+
it("keeps stacked admissions collapsed when an unlimited per-order limit has ten tickets left", () => {
|
|
375
|
+
const main = section({ visibility: "STACKED" });
|
|
376
|
+
const current = admission({
|
|
377
|
+
id: "adm_current",
|
|
378
|
+
available: 10,
|
|
379
|
+
limit: -1,
|
|
380
|
+
price: { base: 1000, booking: 100, currency: "AUD" },
|
|
381
|
+
sectionId: main.id,
|
|
382
|
+
});
|
|
383
|
+
const next = admission({
|
|
384
|
+
id: "adm_next",
|
|
385
|
+
order: 2,
|
|
386
|
+
price: { base: 2000, booking: 200, currency: "AUD" },
|
|
387
|
+
sectionId: main.id,
|
|
388
|
+
});
|
|
389
|
+
const [item] = topLevelTicketItems([main], [current, next]);
|
|
390
|
+
expect(item?.type).toBe("section");
|
|
391
|
+
expect(item?.type === "section" ? item.admissions.map((adm) => adm.id) : []).toEqual(["adm_current"]);
|
|
392
|
+
});
|
|
393
|
+
it("uses coupon-adjusted base prices when stacking sections", () => {
|
|
394
|
+
const main = section({ visibility: "STACKED" });
|
|
395
|
+
const general = admission({
|
|
396
|
+
id: "adm_general",
|
|
397
|
+
order: 1,
|
|
398
|
+
price: { base: 2000, booking: 200, currency: "AUD" },
|
|
399
|
+
sectionId: main.id,
|
|
400
|
+
});
|
|
401
|
+
const vip = admission({
|
|
402
|
+
id: "adm_vip",
|
|
403
|
+
order: 2,
|
|
404
|
+
price: { base: 3000, booking: 300, currency: "AUD" },
|
|
405
|
+
sectionId: main.id,
|
|
406
|
+
});
|
|
407
|
+
const [item] = topLevelTicketItems([main], [general, vip], {
|
|
408
|
+
mode: "default",
|
|
409
|
+
previewQuote: quote([
|
|
410
|
+
quoteItem({
|
|
411
|
+
admissionId: "adm_vip",
|
|
412
|
+
sectionId: main.id,
|
|
413
|
+
price: {
|
|
414
|
+
base: 3000,
|
|
415
|
+
booking: 300,
|
|
416
|
+
currency: "AUD",
|
|
417
|
+
discount: -2500,
|
|
418
|
+
gross: 3300,
|
|
419
|
+
net: 800,
|
|
420
|
+
refunded: 0,
|
|
421
|
+
total: 800,
|
|
422
|
+
},
|
|
423
|
+
}),
|
|
424
|
+
]),
|
|
425
|
+
});
|
|
426
|
+
expect(item?.type).toBe("section");
|
|
427
|
+
expect(item?.type === "section" ? item.admissions.map((adm) => adm.id) : []).toEqual(["adm_vip"]);
|
|
428
|
+
});
|
|
429
|
+
it("does not stack unsectioned admissions", () => {
|
|
430
|
+
const main = section({ visibility: "STACKED", order: 2 });
|
|
431
|
+
const sectioned = admission({
|
|
432
|
+
id: "adm_sectioned",
|
|
433
|
+
order: 1,
|
|
434
|
+
sectionId: main.id,
|
|
435
|
+
});
|
|
436
|
+
const unsectioned = admission({ id: "adm_unsectioned", order: 1 });
|
|
437
|
+
const items = topLevelTicketItems([main], [sectioned, unsectioned]);
|
|
438
|
+
expect(items.map((item) => item.type)).toEqual(["admission", "section"]);
|
|
439
|
+
expect(items[0]?.type === "admission" ? items[0].admission.id : "").toBe("adm_unsectioned");
|
|
440
|
+
});
|
|
258
441
|
it("caps unlimited admission quantity at the selector limit", () => {
|
|
259
442
|
const data = event({
|
|
260
443
|
admissions: [admission({ available: -1, capacity: -1 })],
|
package/dist/collection/components/session-services-ticketing/session-services-ticketing.test.js
CHANGED
|
@@ -138,6 +138,10 @@ function widget() {
|
|
|
138
138
|
const item = new SessionsTicketing();
|
|
139
139
|
item.eventId = "evt_1";
|
|
140
140
|
item.event = event();
|
|
141
|
+
item.enableAnalytics = true;
|
|
142
|
+
item.host = {
|
|
143
|
+
getBoundingClientRect: () => ({ top: 0 }),
|
|
144
|
+
};
|
|
141
145
|
item.selectedItems = new Map();
|
|
142
146
|
item.quote = null;
|
|
143
147
|
item.previewQuote = null;
|
|
@@ -194,6 +198,44 @@ describe("session-services-ticketing component", () => {
|
|
|
194
198
|
expect(item.couponError).toBe("Failed to apply coupon. Please check the code.");
|
|
195
199
|
expect(item.client.order.quote).toHaveBeenCalledWith(expect.objectContaining({ couponCode: "BAD" }));
|
|
196
200
|
});
|
|
201
|
+
it("does not commit preview coupons when event reload fails", async () => {
|
|
202
|
+
const item = widget();
|
|
203
|
+
item.appliedCouponCode = "SAVE10";
|
|
204
|
+
item.couponInput = "bad";
|
|
205
|
+
item.loadEventData = vi.fn().mockRejectedValue(new Error("Down"));
|
|
206
|
+
await item.applyCoupon();
|
|
207
|
+
expect(item.appliedCouponCode).toBe("SAVE10");
|
|
208
|
+
expect(item.couponInput).toBe("BAD");
|
|
209
|
+
expect(item.couponError).toBe("Failed to apply coupon. Please check the code.");
|
|
210
|
+
});
|
|
211
|
+
it("does not apply stale cart coupons as preview coupons", async () => {
|
|
212
|
+
const item = widget();
|
|
213
|
+
const firstLoad = deferred();
|
|
214
|
+
const secondLoad = deferred();
|
|
215
|
+
item.couponInput = "old";
|
|
216
|
+
item.selectedItems = new Map([[selectionKey("adm_general", null), 1]]);
|
|
217
|
+
item.loadEventData = vi
|
|
218
|
+
.fn()
|
|
219
|
+
.mockReturnValueOnce(firstLoad.promise)
|
|
220
|
+
.mockReturnValueOnce(secondLoad.promise)
|
|
221
|
+
.mockResolvedValue(undefined);
|
|
222
|
+
item.client.order.quote = vi.fn().mockResolvedValue({ quote: quote() });
|
|
223
|
+
const stale = item.applyCoupon();
|
|
224
|
+
await Promise.resolve();
|
|
225
|
+
expect(item.appliedCouponCode).toBe("OLD");
|
|
226
|
+
item.couponInput = "new";
|
|
227
|
+
const latest = item.applyCoupon();
|
|
228
|
+
await Promise.resolve();
|
|
229
|
+
expect(item.appliedCouponCode).toBe("NEW");
|
|
230
|
+
item.selectedItems = new Map();
|
|
231
|
+
firstLoad.resolve();
|
|
232
|
+
await stale;
|
|
233
|
+
expect(item.appliedCouponCode).toBe("NEW");
|
|
234
|
+
expect(item.client.order.quote).not.toHaveBeenCalledWith(expect.objectContaining({ couponCode: "OLD" }));
|
|
235
|
+
secondLoad.resolve();
|
|
236
|
+
await latest;
|
|
237
|
+
expect(item.appliedCouponCode).toBe("NEW");
|
|
238
|
+
});
|
|
197
239
|
it("keeps preview quote loading separate from cart quote loading", async () => {
|
|
198
240
|
const item = widget();
|
|
199
241
|
const next = deferred();
|
|
@@ -266,6 +308,18 @@ describe("session-services-ticketing component", () => {
|
|
|
266
308
|
});
|
|
267
309
|
expect(item.getCheckoutButtonText()).toBe("Continue");
|
|
268
310
|
});
|
|
311
|
+
it("renders stable checkout e2e hooks", () => {
|
|
312
|
+
const item = widget();
|
|
313
|
+
const view = item.renderReadyState();
|
|
314
|
+
const cards = rendered(view, (node) => node.props["data-testid"] === "event-admission-card");
|
|
315
|
+
const inputs = rendered(view, (node) => node.props["data-testid"] === "event-admission-quantity-input");
|
|
316
|
+
const increments = rendered(view, (node) => node.props["data-testid"] === "event-admission-increment");
|
|
317
|
+
const checkout = rendered(view, (node) => node.props["data-testid"] === "event-checkout-cta-desktop");
|
|
318
|
+
expect(cards[0].props["data-admission-id"]).toBe("adm_general");
|
|
319
|
+
expect(inputs).toHaveLength(1);
|
|
320
|
+
expect(increments).toHaveLength(1);
|
|
321
|
+
expect(checkout).toHaveLength(1);
|
|
322
|
+
});
|
|
269
323
|
it("renders external ticketing in the sticky checkout bar", () => {
|
|
270
324
|
const item = widget();
|
|
271
325
|
item.event = event({ url: "https://tickets.example" });
|
|
@@ -276,6 +330,9 @@ describe("session-services-ticketing component", () => {
|
|
|
276
330
|
expect(links[0].props.href).toBe("https://tickets.example");
|
|
277
331
|
expect(hasClass(links[0], "external-ticket-button")).toBe(true);
|
|
278
332
|
expect(hasClass(links[0], "sticky-checkout-btn")).toBe(true);
|
|
333
|
+
expect(rendered(links[0], (node) => node.tag === "span" &&
|
|
334
|
+
hasClass(node, "external-ticket-domain") &&
|
|
335
|
+
node.children.join("") === "via tickets.example")).toHaveLength(1);
|
|
279
336
|
});
|
|
280
337
|
it("keeps external ticketing under the sticky checkout host state", () => {
|
|
281
338
|
const item = widget();
|
|
@@ -290,6 +347,55 @@ describe("session-services-ticketing component", () => {
|
|
|
290
347
|
expect(links.some((link) => hasClass(link, "inline-external-ticket-button"))).toBe(true);
|
|
291
348
|
expect(links.some((link) => hasClass(link, "sticky-checkout-btn"))).toBe(true);
|
|
292
349
|
});
|
|
350
|
+
it("tracks external ticket clicks by surface", () => {
|
|
351
|
+
const item = widget();
|
|
352
|
+
const track = vi.fn();
|
|
353
|
+
item.analytics = { track };
|
|
354
|
+
item.event = event({ url: "https://tickets.example/event" });
|
|
355
|
+
item.stickyCheckout = true;
|
|
356
|
+
const links = rendered(item.renderReadyState(), (node) => node.tag === "a");
|
|
357
|
+
const inline = links.find((link) => hasClass(link, "inline-external-ticket-button"));
|
|
358
|
+
const sticky = links.find((link) => hasClass(link, "sticky-checkout-btn"));
|
|
359
|
+
expect(typeof inline?.props.onClick).toBe("function");
|
|
360
|
+
expect(typeof sticky?.props.onClick).toBe("function");
|
|
361
|
+
(inline?.props.onClick)();
|
|
362
|
+
(sticky?.props.onClick)();
|
|
363
|
+
expect(track).toHaveBeenCalledWith("Checkout Started", expect.objectContaining({
|
|
364
|
+
event_id: "evt_1",
|
|
365
|
+
team_id: "team_1",
|
|
366
|
+
promoter_id: "pro_1",
|
|
367
|
+
ticketing_mode: "external",
|
|
368
|
+
external_ticket_url: "https://tickets.example/event",
|
|
369
|
+
external_ticket_domain: "tickets.example",
|
|
370
|
+
cta_surface: "inline",
|
|
371
|
+
}));
|
|
372
|
+
expect(track).toHaveBeenCalledWith("Checkout Started", expect.objectContaining({
|
|
373
|
+
ticketing_mode: "external",
|
|
374
|
+
external_ticket_url: "https://tickets.example/event",
|
|
375
|
+
external_ticket_domain: "tickets.example",
|
|
376
|
+
cta_surface: "sticky",
|
|
377
|
+
}));
|
|
378
|
+
});
|
|
379
|
+
it("scrolls to the ticket selector with a 100px top offset", () => {
|
|
380
|
+
const item = widget();
|
|
381
|
+
const scrollTo = vi.fn();
|
|
382
|
+
const tickets = {
|
|
383
|
+
getBoundingClientRect: () => ({ top: 420 }),
|
|
384
|
+
};
|
|
385
|
+
item.host = {
|
|
386
|
+
shadowRoot: {
|
|
387
|
+
querySelector: vi.fn().mockReturnValue(tickets),
|
|
388
|
+
},
|
|
389
|
+
getBoundingClientRect: () => ({ top: 900 }),
|
|
390
|
+
};
|
|
391
|
+
vi.stubGlobal("window", { scrollY: 250, scrollTo });
|
|
392
|
+
item.handleStickyCheckoutClick(getTicketingState(item.event));
|
|
393
|
+
expect(scrollTo).toHaveBeenCalledWith({
|
|
394
|
+
behavior: "smooth",
|
|
395
|
+
top: 570,
|
|
396
|
+
});
|
|
397
|
+
vi.unstubAllGlobals();
|
|
398
|
+
});
|
|
293
399
|
it("classifies only passcode access failures as passcode errors", async () => {
|
|
294
400
|
const item = widget();
|
|
295
401
|
item.client.event.get = vi
|