@session-services/web-elements 0.1.21 → 0.1.23

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.
@@ -158,6 +158,92 @@ describe("session-services-ticketing logic", () => {
158
158
  expect(state.disabled).toBe(true);
159
159
  expect(state.label?.text).toContain("On sale");
160
160
  });
161
+ it("treats sold-out sale endings as sold out instead of a price label", () => {
162
+ const data = event({
163
+ admissions: [
164
+ admission({
165
+ saleEndsAt: pastEnd,
166
+ saleEndsBehavior: "SOLD_OUT",
167
+ }),
168
+ ],
169
+ });
170
+ const state = getAdmissionDisplayState({
171
+ admission: data.admissions[0],
172
+ event: data,
173
+ });
174
+ expect(state.disabled).toBe(true);
175
+ expect(state.label).toBeNull();
176
+ expect(state.soldOut).toBe(true);
177
+ expect(state.statusLabel).toBe("Sold Out");
178
+ expect(state.statusTone).toBe("destructive");
179
+ });
180
+ it("moves post-sale labels into admission status", () => {
181
+ const data = event({
182
+ admissions: [
183
+ admission({
184
+ saleEndsAt: pastEnd,
185
+ saleEndsBehavior: "SALE_ENDED",
186
+ }),
187
+ admission({
188
+ id: "adm_door",
189
+ saleEndsAt: pastEnd,
190
+ saleEndsBehavior: "DOOR",
191
+ }),
192
+ ],
193
+ });
194
+ const ended = getAdmissionDisplayState({
195
+ admission: data.admissions[0],
196
+ event: data,
197
+ });
198
+ const door = getAdmissionDisplayState({
199
+ admission: data.admissions[1],
200
+ event: data,
201
+ });
202
+ expect(ended.label).toBeNull();
203
+ expect(ended.statusLabel).toBe("Sale Ended");
204
+ expect(ended.statusTone).toBe("destructive");
205
+ expect(door.label).toBeNull();
206
+ expect(door.statusLabel).toBe("Available At Door");
207
+ expect(door.statusTone).toBe("success");
208
+ });
209
+ it("shows sold-out door admissions as available at door while online sales continue", () => {
210
+ const data = event({
211
+ admissions: [
212
+ admission({
213
+ id: "adm_first_release",
214
+ available: 0,
215
+ sold: 54,
216
+ capacity: 54,
217
+ order: 20,
218
+ saleEndsAt: futureEnd,
219
+ saleEndsBehavior: "DOOR",
220
+ }),
221
+ admission({
222
+ id: "adm_final_release",
223
+ available: 997,
224
+ sold: 3,
225
+ capacity: 1000,
226
+ order: 40,
227
+ saleEndsAt: futureEnd,
228
+ saleEndsBehavior: "DOOR",
229
+ }),
230
+ ],
231
+ });
232
+ const first = getAdmissionDisplayState({
233
+ admission: data.admissions[0],
234
+ event: data,
235
+ });
236
+ const final = getAdmissionDisplayState({
237
+ admission: data.admissions[1],
238
+ event: data,
239
+ });
240
+ expect(first.disabled).toBe(true);
241
+ expect(first.soldOut).toBe(true);
242
+ expect(first.statusLabel).toBe("Available At Door");
243
+ expect(first.statusTone).toBe("success");
244
+ expect(final.disabled).toBe(false);
245
+ expect(final.statusLabel).toBeNull();
246
+ });
161
247
  it("orders top-level sections and unsectioned admissions together", () => {
162
248
  const main = section({ id: "sec_main", order: 2 });
163
249
  const early = admission({ id: "adm_early", order: 1 });
@@ -185,12 +271,13 @@ describe("session-services-ticketing logic", () => {
185
271
  quoteItem: quoteItem(),
186
272
  });
187
273
  expect(price).toEqual({
274
+ base: 1000,
188
275
  discounted: true,
189
- gross: 2200,
190
- total: 1200,
276
+ fees: 200,
277
+ gross: 2000,
191
278
  });
192
279
  });
193
- it("uses quote item total pricing without discount styling", () => {
280
+ it("uses quote item base pricing without discount styling", () => {
194
281
  const price = getAdmissionPriceDisplay({
195
282
  admission: admission(),
196
283
  mode: "default",
@@ -208,19 +295,20 @@ describe("session-services-ticketing logic", () => {
208
295
  }),
209
296
  });
210
297
  expect(price).toEqual({
298
+ base: 2000,
211
299
  discounted: false,
212
- total: 2250,
300
+ fees: 200,
213
301
  });
214
302
  });
215
- it("includes booking fees in admission prices for every mode", () => {
303
+ it("separates admission base and booking fees for every mode", () => {
216
304
  expect(getAdmissionPriceDisplay({
217
305
  admission: admission(),
218
306
  mode: "default",
219
- }).total).toBe(2200);
307
+ })).toMatchObject({ base: 2000, fees: 200 });
220
308
  expect(getAdmissionPriceDisplay({
221
309
  admission: admission(),
222
310
  mode: "door",
223
- }).total).toBe(2200);
311
+ })).toMatchObject({ base: 2000, fees: 200 });
224
312
  });
225
313
  it("uses discounted initial preview pricing when available", () => {
226
314
  const price = getAdmissionPriceDisplay({
@@ -233,20 +321,23 @@ describe("session-services-ticketing logic", () => {
233
321
  mode: "default",
234
322
  });
235
323
  expect(price).toEqual({
324
+ base: 1000,
236
325
  discounted: true,
237
- gross: 2200,
238
- total: 1200,
326
+ fees: 200,
327
+ gross: 2000,
239
328
  });
240
329
  });
241
330
  it("calculates admission savings from discounted display prices", () => {
242
331
  expect(admissionPriceSavings({
332
+ base: 1000,
243
333
  discounted: true,
244
- gross: 2200,
245
- total: 1200,
334
+ fees: 200,
335
+ gross: 2000,
246
336
  })).toBe(1000);
247
337
  expect(admissionPriceSavings({
338
+ base: 2200,
248
339
  discounted: false,
249
- total: 2200,
340
+ fees: 200,
250
341
  })).toBe(0);
251
342
  });
252
343
  it("divides preview quote pricing by preview quantity", () => {
@@ -268,9 +359,10 @@ describe("session-services-ticketing logic", () => {
268
359
  }),
269
360
  });
270
361
  expect(price).toEqual({
362
+ base: 1000,
271
363
  discounted: true,
272
- gross: 2200,
273
- total: 1200,
364
+ fees: 200,
365
+ gross: 2000,
274
366
  });
275
367
  });
276
368
  });
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
  import { SessionsTicketing } from "./session-services-ticketing";
3
- import { selectionKey, } from "./session-services-ticketing.logic";
3
+ import { getTicketingState, selectionKey, } from "./session-services-ticketing.logic";
4
4
  vi.mock("@stencil/core", () => ({
5
5
  Component: () => () => undefined,
6
6
  Element: () => () => undefined,
@@ -9,7 +9,7 @@ vi.mock("@stencil/core", () => ({
9
9
  Prop: () => () => undefined,
10
10
  State: () => () => undefined,
11
11
  Watch: () => () => undefined,
12
- h: () => undefined,
12
+ h: (tag, props, ...children) => ({ tag, props: props ?? {}, children }),
13
13
  }));
14
14
  const futureStart = "2030-01-01T20:00:00";
15
15
  const futureEnd = "2030-01-01T23:00:00";
@@ -115,6 +115,25 @@ function deferred() {
115
115
  });
116
116
  return { promise, resolve };
117
117
  }
118
+ function isRenderNode(value) {
119
+ if (!value || typeof value !== "object")
120
+ return false;
121
+ return "tag" in value && "props" in value && "children" in value;
122
+ }
123
+ function rendered(value, predicate) {
124
+ if (Array.isArray(value)) {
125
+ return value.flatMap((item) => rendered(item, predicate));
126
+ }
127
+ if (!isRenderNode(value))
128
+ return [];
129
+ const matches = predicate(value) ? [value] : [];
130
+ return matches.concat(value.children.flatMap((item) => rendered(item, predicate)));
131
+ }
132
+ function hasClass(node, name) {
133
+ return String(node.props.class ?? "")
134
+ .split(/\s+/)
135
+ .includes(name);
136
+ }
118
137
  function widget() {
119
138
  const item = new SessionsTicketing();
120
139
  item.eventId = "evt_1";
@@ -131,6 +150,7 @@ function widget() {
131
150
  item.quoteLoading = false;
132
151
  item.previewLoading = false;
133
152
  item.passcodeDenied = false;
153
+ item.stickyCheckout = false;
134
154
  item.error = null;
135
155
  item.activePasscode = "";
136
156
  item.cartUpdate = { emit: vi.fn() };
@@ -246,6 +266,30 @@ describe("session-services-ticketing component", () => {
246
266
  });
247
267
  expect(item.getCheckoutButtonText()).toBe("Continue");
248
268
  });
269
+ it("renders external ticketing in the sticky checkout bar", () => {
270
+ const item = widget();
271
+ item.event = event({ url: "https://tickets.example" });
272
+ item.stickyCheckout = true;
273
+ const state = getTicketingState(item.event);
274
+ const links = rendered(item.renderStickyExternalTicketing(state), (node) => node.tag === "a");
275
+ expect(links).toHaveLength(1);
276
+ expect(links[0].props.href).toBe("https://tickets.example");
277
+ expect(hasClass(links[0], "external-ticket-button")).toBe(true);
278
+ expect(hasClass(links[0], "sticky-checkout-btn")).toBe(true);
279
+ });
280
+ it("keeps external ticketing under the sticky checkout host state", () => {
281
+ const item = widget();
282
+ item.event = event({ url: "https://tickets.example" });
283
+ item.stickyCheckout = true;
284
+ const view = item.renderReadyState();
285
+ const hosts = rendered(view, (node) => node.tag === "mock-host");
286
+ const links = rendered(view, (node) => node.tag === "a");
287
+ expect(getTicketingState(item.event).showExternalTicketing).toBe(true);
288
+ expect(hosts[0].props["data-sticky-checkout"]).toBe("true");
289
+ expect(links).toHaveLength(2);
290
+ expect(links.some((link) => hasClass(link, "inline-external-ticket-button"))).toBe(true);
291
+ expect(links.some((link) => hasClass(link, "sticky-checkout-btn"))).toBe(true);
292
+ });
249
293
  it("classifies only passcode access failures as passcode errors", async () => {
250
294
  const item = widget();
251
295
  item.client.event.get = vi