summit-registration-lite 7.0.5 → 7.0.6

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.
@@ -0,0 +1,103 @@
1
+ const { test, expect } = require('@playwright/test');
2
+ const {
3
+ ticketType,
4
+ discoveredCode,
5
+ discoveryResponse,
6
+ ticketTypesResponse,
7
+ taxTypesResponse,
8
+ validationResponse,
9
+ } = require('./fixtures');
10
+
11
+ // Post-apply auto-select must scan the date-filtered ticket list (`allowedTicketTypes`),
12
+ // not the raw Redux list (`originalTicketTypes`). Otherwise the widget could pick a ticket
13
+ // that's outside its sales window — the dropdown can't display it and the user is stuck.
14
+
15
+ const setupRoutes = async (page, { discovery, tickets, validation }) => {
16
+ await page.route('**/promo-codes/all/discover*', route =>
17
+ route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(discoveryResponse(discovery)) })
18
+ );
19
+ await page.route('**/ticket-types/allowed*', route =>
20
+ route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(ticketTypesResponse(tickets)) })
21
+ );
22
+ await page.route('**/tax-types*', route =>
23
+ route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(taxTypesResponse()) })
24
+ );
25
+ if (validation) {
26
+ await page.route('**/promo-codes/*/apply*', route =>
27
+ route.fulfill({ status: validation.status, contentType: 'application/json', body: JSON.stringify(validation.body) })
28
+ );
29
+ }
30
+ };
31
+
32
+ test.describe('post-apply auto-select respects sales window', () => {
33
+ test('picks the in-window ticket when a discovered code matches both an expired and an active ticket', async ({ page }) => {
34
+ // Code matches both tickets, but Expired Ticket has a sales_end_date in the past.
35
+ // Pre-fix, the auto-select scanned originalTicketTypes (unfiltered) and would land
36
+ // on Expired Ticket — first match in iteration order. Post-fix, only Active Ticket
37
+ // can be auto-selected because Expired Ticket is excluded by the date filter.
38
+ const expiredTicket = ticketType({
39
+ id: 188,
40
+ name: 'Expired Ticket',
41
+ sales_start_date: 1000000, // long ago
42
+ sales_end_date: 2000000, // also long ago
43
+ });
44
+ const activeTicket = ticketType({
45
+ id: 200,
46
+ name: 'Active Ticket',
47
+ sales_start_date: null,
48
+ sales_end_date: null,
49
+ });
50
+ const code = discoveredCode({
51
+ auto_apply: true,
52
+ allowed_ticket_types: [188, 200],
53
+ });
54
+
55
+ await setupRoutes(page, {
56
+ tickets: [expiredTicket, activeTicket],
57
+ discovery: [code],
58
+ validation: { status: 200, body: validationResponse() },
59
+ });
60
+ await page.goto('/');
61
+
62
+ // Auto-apply runs on load; the post-apply effect picks a qualifying ticket.
63
+ // The dropdown's selected-ticket slot shows the chosen ticket's name.
64
+ await expect(page.locator('[data-testid="selected-ticket"]')).toContainText('Active Ticket');
65
+ await expect(page.locator('[data-testid="selected-ticket"]')).not.toContainText('Expired Ticket');
66
+ });
67
+
68
+ test('never picks an expired ticket even when it is the only qualifying one', async ({ page }) => {
69
+ // Code matches only the expired ticket. Two active tickets exist (so the
70
+ // "fall back to the single allowed option" clause cannot mask the bug).
71
+ // Pre-fix, the widget scanned originalTicketTypes and would land on
72
+ // Expired Ticket — the only qualifying one. Post-fix, the qualifying
73
+ // scan is bounded by allowedTicketTypes, so Expired Ticket is never
74
+ // selectable.
75
+ const expiredTicket = ticketType({
76
+ id: 188,
77
+ name: 'Expired Ticket',
78
+ sales_start_date: 1000000,
79
+ sales_end_date: 2000000,
80
+ });
81
+ const activeA = ticketType({ id: 200, name: 'Active A', sales_start_date: null, sales_end_date: null });
82
+ const activeB = ticketType({ id: 201, name: 'Active B', sales_start_date: null, sales_end_date: null });
83
+ const code = discoveredCode({
84
+ auto_apply: true,
85
+ allowed_ticket_types: [188], // only the expired one qualifies
86
+ });
87
+
88
+ await setupRoutes(page, {
89
+ tickets: [expiredTicket, activeA, activeB],
90
+ discovery: [code],
91
+ validation: { status: 200, body: validationResponse() },
92
+ });
93
+ await page.goto('/');
94
+
95
+ // Give the auto-apply + post-apply effects time to settle.
96
+ await page.waitForTimeout(500);
97
+ // Post-fix: no auto-selection happens because the only qualifying ticket
98
+ // is filtered out by date and the fallback ("if there's a single allowed
99
+ // option, pick it") cannot fire when more than one is allowed. The dropdown
100
+ // shows its placeholder. Pre-fix, this slot would have shown "Expired Ticket".
101
+ await expect(page.locator('[data-testid="no-ticket"]')).toBeVisible();
102
+ });
103
+ });
@@ -124,6 +124,27 @@ test.describe('suggestion flow (auto_apply: false)', () => {
124
124
  await page.fill('input[placeholder="Enter your promo code"]', 'SUGGEST50');
125
125
  await expect(page.locator('text=You qualify for the following promo code:')).toBeVisible();
126
126
  });
127
+
128
+ test('banner swaps to "applies to a different ticket" copy after switching to a non-qualifying ticket', async ({ page }) => {
129
+ const qualifying = ticketType({ id: 188, name: 'Early Bird Ticket' });
130
+ const nonQualifying = ticketType({ id: 999, name: 'Standard Ticket' });
131
+ const code = discoveredCode({ auto_apply: false, code: 'SUGGEST50', allowed_ticket_types: [188] });
132
+
133
+ await setupRoutes(page, {
134
+ tickets: [qualifying, nonQualifying],
135
+ discovery: [code],
136
+ });
137
+ await page.goto('/');
138
+
139
+ await selectTicket(page, 'Early Bird Ticket');
140
+ await expect(page.locator('text=You qualify for the following promo code:')).toBeVisible();
141
+
142
+ await selectTicket(page, 'Standard Ticket');
143
+ await expect(page.locator('text=Following promo code applies to a different ticket. Apply to switch.')).toBeVisible();
144
+ // The personal "You qualify" copy must be gone — banner stays visible
145
+ // but accurate about the current pick.
146
+ await expect(page.locator('text=You qualify for the following promo code:')).toHaveCount(0);
147
+ });
127
148
  });
128
149
 
129
150
  test.describe('auto-apply flow (auto_apply: true)', () => {
@@ -154,15 +175,29 @@ test.describe('auto-apply flow (auto_apply: true)', () => {
154
175
  await expect(page.locator('text=You qualify for the following promo code:')).toBeVisible();
155
176
  });
156
177
 
157
- test('does not auto-apply for non-qualifying ticket', async ({ page }) => {
178
+ test('auto-applies on load and surfaces INVALID when the only ticket is non-qualifying', async ({ page }) => {
158
179
  const nonQualifyingTicket = ticketType({ id: 999, name: 'Standard Ticket' });
159
180
  await setupRoutes(page, {
160
181
  tickets: [nonQualifyingTicket],
161
182
  discovery: [autoCode],
183
+ // Per the SDS, early auto-apply fires whenever a single auto_apply code
184
+ // exists — independent of which tickets are present. Re-validation against
185
+ // the lone non-qualifying ticket then catches the mismatch and the user
186
+ // sees the backend's rejection message.
187
+ validation: (route) => {
188
+ if (route.request().url().includes('ticket_type_id%3D%3D999')) {
189
+ return route.fulfill({
190
+ status: 412,
191
+ contentType: 'application/json',
192
+ body: JSON.stringify(validationError('Promo code AUTO100 can not be applied to Ticket Type Standard Ticket.')),
193
+ });
194
+ }
195
+ return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(validationResponse()) });
196
+ },
162
197
  });
163
198
  await page.goto('/');
164
- await selectTicket(page, 'Standard Ticket');
165
- await expect(page.locator('text=Do you have a promo code?')).toBeVisible();
199
+ await expect(page.locator('input[placeholder="Enter your promo code"]')).toHaveValue('AUTO100');
200
+ await expect(page.locator('text=Promo code AUTO100 can not be applied to Ticket Type Standard Ticket.')).toBeVisible();
166
201
  });
167
202
  });
168
203
 
@@ -288,7 +323,7 @@ test.describe('no tickets available', () => {
288
323
  });
289
324
 
290
325
  test.describe('ticket switching with applied code', () => {
291
- test('removes discovered code when switching to non-qualifying ticket', async ({ page }) => {
326
+ test('keeps code applied and surfaces INVALID when switching to non-qualifying ticket', async ({ page }) => {
292
327
  const qualifying = ticketType({ id: 188, name: 'Early Bird Ticket' });
293
328
  const nonQualifying = ticketType({ id: 999, name: 'Standard Ticket' });
294
329
  const code = discoveredCode({ auto_apply: true, allowed_ticket_types: [188] });
@@ -296,15 +331,28 @@ test.describe('ticket switching with applied code', () => {
296
331
  await setupRoutes(page, {
297
332
  tickets: [qualifying, nonQualifying],
298
333
  discovery: [code],
299
- validation: { status: 200, body: validationResponse() },
334
+ // Reject the validate call when targeting the non-qualifying ticket type.
335
+ validation: (route) => {
336
+ if (route.request().url().includes('ticket_type_id%3D%3D999')) {
337
+ return route.fulfill({
338
+ status: 412,
339
+ contentType: 'application/json',
340
+ body: JSON.stringify(validationError('Promo code EARLYBIRD can not be applied to Ticket Type Standard Ticket.')),
341
+ });
342
+ }
343
+ return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(validationResponse()) });
344
+ },
300
345
  });
301
346
  await page.goto('/');
302
347
  await selectTicket(page, 'Early Bird Ticket');
303
348
  await expect(page.locator('text=Following promo code was automatically applied:')).toBeVisible();
304
349
 
305
350
  await selectTicket(page, 'Standard Ticket');
306
- await expect(page.locator('text=Do you have a promo code?')).toBeVisible();
307
- await expect(page.locator('input[placeholder="Enter your promo code"]')).toHaveValue('');
351
+ // The code is no longer silently removed on a non-qualifying ticket switch;
352
+ // it stays applied and is re-validated, surfacing the backend rejection so
353
+ // the user can choose to Remove or pick another qualifying ticket.
354
+ await expect(page.locator('input[placeholder="Enter your promo code"]')).toHaveValue('EARLYBIRD');
355
+ await expect(page.locator('text=Promo code EARLYBIRD can not be applied to Ticket Type Standard Ticket.')).toBeVisible();
308
356
  });
309
357
 
310
358
  test('suggestion appears when switching back to qualifying ticket', async ({ page }) => {
@@ -0,0 +1,57 @@
1
+ const { test, expect } = require('@playwright/test');
2
+ const {
3
+ ticketType,
4
+ discoveryResponse,
5
+ ticketTypesResponse,
6
+ taxTypesResponse,
7
+ validationError,
8
+ } = require('./fixtures');
9
+
10
+ // The unapplied-code warning ("you typed a code but didn't click Apply") must yield
11
+ // to the hook's own validation error once the backend rejects the code. Without the
12
+ // fix, the warning kept its precedence in the merged display slot, masking the
13
+ // more specific "Promo code entered is not valid" message after a failed Apply.
14
+
15
+ const UNAPPLIED_WARNING = "You entered a promo code but it hasn't been applied";
16
+ const INVALID_MESSAGE = 'Promo code entered is not valid.';
17
+
18
+ test('INVALID status clears the unapplied-code warning', async ({ page }) => {
19
+ await page.route('**/promo-codes/all/discover*', route =>
20
+ route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(discoveryResponse([])) })
21
+ );
22
+ await page.route('**/ticket-types/allowed*', route =>
23
+ route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(ticketTypesResponse([ticketType()])) })
24
+ );
25
+ await page.route('**/tax-types*', route =>
26
+ route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(taxTypesResponse()) })
27
+ );
28
+ await page.route('**/promo-codes/*/apply*', route =>
29
+ route.fulfill({
30
+ status: 412,
31
+ contentType: 'application/json',
32
+ body: JSON.stringify(validationError('The Promo Code "BAD" is not a valid code.')),
33
+ })
34
+ );
35
+
36
+ await page.goto('/');
37
+
38
+ // Select a ticket so the Next button is enabled.
39
+ await page.locator('[data-testid="ticket-dropdown"]').click();
40
+ await page.locator('[data-testid="ticket-list"] >> text=Early Bird Ticket').click();
41
+
42
+ // Type a code WITHOUT clicking Apply, then hit Next.
43
+ // This is the trigger for the unapplied-code warning.
44
+ await page.fill('input[placeholder="Enter your promo code"]', 'BAD');
45
+ await page.click('button:has-text("Next")');
46
+
47
+ // Warning is visible, INVALID message is not.
48
+ await expect(page.locator(`text=${UNAPPLIED_WARNING}`)).toBeVisible();
49
+ await expect(page.locator(`text=${INVALID_MESSAGE}`)).not.toBeVisible();
50
+
51
+ // Now click Apply. Backend rejects with 412 — the hook surfaces the INVALID message.
52
+ await page.click('button:has-text("Apply")');
53
+
54
+ // The unapplied warning must give way to the more specific rejection reason.
55
+ await expect(page.locator(`text=${INVALID_MESSAGE}`)).toBeVisible();
56
+ await expect(page.locator(`text=${UNAPPLIED_WARNING}`)).not.toBeVisible();
57
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "summit-registration-lite",
3
- "version": "7.0.5",
3
+ "version": "7.0.6",
4
4
  "description": "Summit Registration Lite",
5
5
  "main": "index.js",
6
6
  "scripts": {