summit-registration-lite 7.0.0 → 7.0.2
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/.claude/rules/summit-registration-lite-i18n.md +62 -0
- package/.claude/rules/summit-registration-lite-step-flow.md +77 -0
- package/.claude/rules/summit-registration-lite-utils.md +71 -0
- package/.codegraph/config.json +141 -0
- package/.github/workflows/ci.yml +54 -0
- package/dist/components/index.css +12 -27
- package/dist/components/index.css.map +1 -1
- package/dist/components/index.js +795 -247
- package/dist/components/index.js.map +1 -1
- package/dist/components/login-passwordless.css +1 -2
- package/dist/components/login-passwordless.js +21 -4
- package/dist/components/login-passwordless.js.map +1 -1
- package/dist/components/login.css +1 -2
- package/dist/components/login.js +69 -2
- package/dist/components/login.js.map +1 -1
- package/dist/components/registration-form.css +12 -26
- package/dist/components/registration-form.css.map +1 -1
- package/dist/components/registration-form.js +790 -243
- package/dist/components/registration-form.js.map +1 -1
- package/dist/components/registration-modal.css +12 -27
- package/dist/components/registration-modal.css.map +1 -1
- package/dist/components/registration-modal.js +794 -246
- package/dist/components/registration-modal.js.map +1 -1
- package/dist/index.css +12 -27
- package/dist/index.css.map +1 -1
- package/dist/index.js +794 -246
- package/dist/index.js.map +1 -1
- package/dist/utils/constants.js +11 -1
- package/dist/utils/constants.js.map +1 -1
- package/e2e/fixtures.js +84 -0
- package/e2e/promo-code-discovery.spec.js +364 -0
- package/package.json +12 -4
- package/playwright.config.js +26 -0
package/dist/utils/constants.js
CHANGED
|
@@ -62,6 +62,7 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
62
62
|
/* harmony export */ "ORDER_STATUS_PAID": () => (/* binding */ ORDER_STATUS_PAID),
|
|
63
63
|
/* harmony export */ "PAYMENT_PROVIDER_LAWPAY": () => (/* binding */ PAYMENT_PROVIDER_LAWPAY),
|
|
64
64
|
/* harmony export */ "PAYMENT_PROVIDER_STRIPE": () => (/* binding */ PAYMENT_PROVIDER_STRIPE),
|
|
65
|
+
/* harmony export */ "PROMO_STATUS": () => (/* binding */ PROMO_STATUS),
|
|
65
66
|
/* harmony export */ "PURCHASE_COMPLETE": () => (/* binding */ PURCHASE_COMPLETE),
|
|
66
67
|
/* harmony export */ "RESEND_TIME": () => (/* binding */ RESEND_TIME),
|
|
67
68
|
/* harmony export */ "STEP_COMPLETE": () => (/* binding */ STEP_COMPLETE),
|
|
@@ -114,7 +115,16 @@ const PURCHASE_COMPLETE = 'purchase_complete'; // ERRORS
|
|
|
114
115
|
|
|
115
116
|
const ERROR_TYPE_ERROR = 'error_type_error';
|
|
116
117
|
const ERROR_TYPE_VALIDATION = 'error_type_validation';
|
|
117
|
-
const ERROR_TYPE_PAYMENT = 'error_type_payment'; //
|
|
118
|
+
const ERROR_TYPE_PAYMENT = 'error_type_payment'; // PROMO CODE STATUS
|
|
119
|
+
|
|
120
|
+
const PROMO_STATUS = {
|
|
121
|
+
IDLE: 'idle',
|
|
122
|
+
SUGGESTED: 'suggested',
|
|
123
|
+
APPLYING: 'applying',
|
|
124
|
+
VALIDATING: 'validating',
|
|
125
|
+
VALID: 'valid',
|
|
126
|
+
INVALID: 'invalid'
|
|
127
|
+
}; // PROVIDERS
|
|
118
128
|
|
|
119
129
|
const PAYMENT_PROVIDER_STRIPE = 'Stripe';
|
|
120
130
|
const PAYMENT_PROVIDER_LAWPAY = 'LawPay';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils/constants.js","mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AACD,O;;UCVA;UACA;;;;;WCDA;WACA;WACA;WACA;WACA,yCAAyC,wCAAwC;WACjF;WACA;WACA;;;;;WCPA;;;;;WCAA;WACA;WACA;WACA,uDAAuD,iBAAiB;WACxE;WACA,gDAAgD,aAAa;WAC7D
|
|
1
|
+
{"version":3,"file":"utils/constants.js","mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AACD,O;;UCVA;UACA;;;;;WCDA;WACA;WACA;WACA;WACA,yCAAyC,wCAAwC;WACjF;WACA;WACA;;;;;WCPA;;;;;WCAA;WACA;WACA;WACA,uDAAuD,iBAAiB;WACxE;WACA,gDAAgD,aAAa;WAC7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACNA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEO,MAAMA,kBAAkB,GAAG,mBAA3B;AACA,MAAMC,gCAAgC,GAAG,uBAAzC;AACA,MAAMC,yBAAyB,GAAG,gBAAlC;AACA,MAAMC,kBAAkB,GAAG,SAA3B;AACA,MAAMC,cAAc,GAAG,SAAvB;AACA,MAAMC,gBAAgB,GAAG,SAAzB;AACA,MAAMC,gBAAgB,GAAG,iBAAzB;AAEA,MAAMC,YAAY,GAAG,sJAArB;AAEA,MAAMC,mBAAmB,GAAG,QAA5B;AACA,MAAMC,oBAAoB,GAAG,aAA7B;AACA,MAAMC,uBAAuB,GAAG,YAAhC;AAEA,MAAMC,2BAA2B,GAAG,SAApC;AACA,MAAMC,iBAAiB,GAAG,MAA1B;AACA,MAAMC,4BAA4B,GAAG,SAArC;AAEA,MAAMC,uBAAuB,GAAG,CAAhC;AACA,MAAMC,kBAAkB,GAAG,CAA3B;AACA,MAAMC,YAAY,GAAG,CAArB;AACA,MAAMC,aAAa,GAAG,CAAtB;AAEA,MAAMC,WAAW,GAAG,EAApB,EAEP;;AACO,MAAMC,SAAS,GAAG,WAAlB;AACA,MAAMC,WAAW,GAAG,aAApB;AACA,MAAMC,cAAc,GAAG,gBAAvB;AACA,MAAMC,iBAAiB,GAAG,mBAA1B,EAEP;;AACO,MAAMC,gBAAgB,GAAE,kBAAxB;AACA,MAAMC,qBAAqB,GAAG,uBAA9B;AACA,MAAMC,kBAAkB,GAAG,oBAA3B,EAEP;;AACO,MAAMC,YAAY,GAAG;AACxBC,EAAAA,IAAI,EAAE,MADkB;AAExBC,EAAAA,SAAS,EAAE,WAFa;AAGxBC,EAAAA,QAAQ,EAAE,UAHc;AAIxBC,EAAAA,UAAU,EAAE,YAJY;AAKxBC,EAAAA,KAAK,EAAE,OALiB;AAMxBC,EAAAA,OAAO,EAAE;AANe,CAArB,EASP;;AACO,MAAMC,uBAAuB,GAAG,QAAhC;AACA,MAAMC,uBAAuB,GAAG,QAAhC,C","sources":["webpack://summit-registration-lite/webpack/universalModuleDefinition","webpack://summit-registration-lite/webpack/bootstrap","webpack://summit-registration-lite/webpack/runtime/define property getters","webpack://summit-registration-lite/webpack/runtime/hasOwnProperty shorthand","webpack://summit-registration-lite/webpack/runtime/make namespace object","webpack://summit-registration-lite/./src/utils/constants.js"],"sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine(\"summit-registration-lite\", [], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"summit-registration-lite\"] = factory();\n\telse\n\t\troot[\"summit-registration-lite\"] = factory();\n})(this, function() {\nreturn ","// The require scope\nvar __webpack_require__ = {};\n\n","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","/**\n * Copyright 2022 OpenStack Foundation\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n **/\n\nexport const AUTH_ERROR_MESSAGE = 'Missing Auth info';\nexport const AUTH_ERROR_MISSING_REFRESH_TOKEN = \"missing Refresh Token\";\nexport const AUTH_ERROR_REQUEST_FAILED = 'Request failed';\nexport const VirtualAccessLevel = 'VIRTUAL';\nexport const DefaultBGColor = '#000000';\nexport const DefaultTextColor = '#FFFFFF';\nexport const DefaultHintColor = 'rgb(58, 63, 65)';\n\nexport const EMAIL_REGEXP = /(([^<>()\\[\\]\\\\.,;:\\s@\"]+(\\.[^<>()\\[\\]\\\\.,;:\\s@\"]+)*)|(\".+\"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))/;\n\nexport const TICKET_OWNER_MYSELF = 'myself';\nexport const TICKET_OWNER_SOMEONE = 'someoneElse';\nexport const TICKET_OWNER_UNASSIGNED = 'unassigned';\n\nexport const TICKET_TYPE_SUBTYPE_PREPAID = 'PrePaid';\nexport const ORDER_STATUS_PAID = 'Paid';\nexport const ORDER_PAYMENT_METHOD_OFFLINE = 'Offline';\n\nexport const STEP_SELECT_TICKET_TYPE = 0;\nexport const STEP_PERSONAL_INFO = 1;\nexport const STEP_PAYMENT = 2;\nexport const STEP_COMPLETE = 3;\n\nexport const RESEND_TIME = 60;\n\n// ANALYTICS\nexport const VIEW_ITEM = 'view_item';\nexport const ADD_TO_CART = 'add_to_cart';\nexport const BEGIN_CHECKOUT = 'begin_checkout';\nexport const PURCHASE_COMPLETE = 'purchase_complete';\n\n// ERRORS\nexport const ERROR_TYPE_ERROR= 'error_type_error';\nexport const ERROR_TYPE_VALIDATION = 'error_type_validation';\nexport const ERROR_TYPE_PAYMENT = 'error_type_payment';\n\n// PROMO CODE STATUS\nexport const PROMO_STATUS = {\n IDLE: 'idle',\n SUGGESTED: 'suggested',\n APPLYING: 'applying',\n VALIDATING: 'validating',\n VALID: 'valid',\n INVALID: 'invalid',\n};\n\n// PROVIDERS\nexport const PAYMENT_PROVIDER_STRIPE = 'Stripe';\nexport const PAYMENT_PROVIDER_LAWPAY = 'LawPay';\n\n"],"names":["AUTH_ERROR_MESSAGE","AUTH_ERROR_MISSING_REFRESH_TOKEN","AUTH_ERROR_REQUEST_FAILED","VirtualAccessLevel","DefaultBGColor","DefaultTextColor","DefaultHintColor","EMAIL_REGEXP","TICKET_OWNER_MYSELF","TICKET_OWNER_SOMEONE","TICKET_OWNER_UNASSIGNED","TICKET_TYPE_SUBTYPE_PREPAID","ORDER_STATUS_PAID","ORDER_PAYMENT_METHOD_OFFLINE","STEP_SELECT_TICKET_TYPE","STEP_PERSONAL_INFO","STEP_PAYMENT","STEP_COMPLETE","RESEND_TIME","VIEW_ITEM","ADD_TO_CART","BEGIN_CHECKOUT","PURCHASE_COMPLETE","ERROR_TYPE_ERROR","ERROR_TYPE_VALIDATION","ERROR_TYPE_PAYMENT","PROMO_STATUS","IDLE","SUGGESTED","APPLYING","VALIDATING","VALID","INVALID","PAYMENT_PROVIDER_STRIPE","PAYMENT_PROVIDER_LAWPAY"],"sourceRoot":""}
|
package/e2e/fixtures.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock API response factories for e2e tests.
|
|
3
|
+
* Each factory returns a response body matching the API contract.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const ticketType = (overrides = {}) => ({
|
|
7
|
+
id: 188,
|
|
8
|
+
name: 'Early Bird Ticket',
|
|
9
|
+
cost: 700,
|
|
10
|
+
currency: 'USD',
|
|
11
|
+
currency_symbol: '$',
|
|
12
|
+
quantity_2_sell: 1000,
|
|
13
|
+
quantity_sold: 0,
|
|
14
|
+
max_quantity_per_order: 10,
|
|
15
|
+
sales_start_date: null,
|
|
16
|
+
sales_end_date: null,
|
|
17
|
+
sub_type: 'Regular',
|
|
18
|
+
audience: 'All',
|
|
19
|
+
badge_type: { id: 1, name: 'General', access_levels: [], badge_features: [] },
|
|
20
|
+
...overrides,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const discoveredCode = (overrides = {}) => ({
|
|
24
|
+
id: 100,
|
|
25
|
+
code: 'EARLYBIRD',
|
|
26
|
+
auto_apply: true,
|
|
27
|
+
allows_to_reassign: true,
|
|
28
|
+
quantity_per_account: 3,
|
|
29
|
+
remaining_quantity_per_account: 3,
|
|
30
|
+
quantity_available: 100,
|
|
31
|
+
quantity_used: 0,
|
|
32
|
+
class_name: 'DOMAIN_AUTHORIZED_DISCOUNT_CODE',
|
|
33
|
+
amount: 50,
|
|
34
|
+
rate: 0,
|
|
35
|
+
allowed_ticket_types: [188],
|
|
36
|
+
badge_features: [],
|
|
37
|
+
tags: [],
|
|
38
|
+
...overrides,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const discoveryResponse = (codes = []) => ({
|
|
42
|
+
total: codes.length,
|
|
43
|
+
per_page: codes.length,
|
|
44
|
+
current_page: 1,
|
|
45
|
+
last_page: 1,
|
|
46
|
+
data: codes,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const ticketTypesResponse = (tickets = []) => ({
|
|
50
|
+
total: tickets.length,
|
|
51
|
+
per_page: tickets.length,
|
|
52
|
+
current_page: 1,
|
|
53
|
+
last_page: 1,
|
|
54
|
+
data: tickets,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const taxTypesResponse = (taxes = []) => ({
|
|
58
|
+
total: taxes.length,
|
|
59
|
+
per_page: taxes.length,
|
|
60
|
+
current_page: 1,
|
|
61
|
+
last_page: 1,
|
|
62
|
+
data: taxes,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const validationResponse = (overrides = {}) => ({
|
|
66
|
+
allows_to_reassign: true,
|
|
67
|
+
...overrides,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const validationError = (message) => ({
|
|
71
|
+
message: 'Validation Failed',
|
|
72
|
+
errors: [message],
|
|
73
|
+
code: 0,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
ticketType,
|
|
78
|
+
discoveredCode,
|
|
79
|
+
discoveryResponse,
|
|
80
|
+
ticketTypesResponse,
|
|
81
|
+
taxTypesResponse,
|
|
82
|
+
validationResponse,
|
|
83
|
+
validationError,
|
|
84
|
+
};
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
const { test, expect } = require('@playwright/test');
|
|
2
|
+
const {
|
|
3
|
+
ticketType,
|
|
4
|
+
discoveredCode,
|
|
5
|
+
discoveryResponse,
|
|
6
|
+
ticketTypesResponse,
|
|
7
|
+
taxTypesResponse,
|
|
8
|
+
validationResponse,
|
|
9
|
+
validationError,
|
|
10
|
+
} = require('./fixtures');
|
|
11
|
+
|
|
12
|
+
// ── Helpers ──
|
|
13
|
+
|
|
14
|
+
const setupRoutes = async (page, { discovery = [], tickets = [], taxes = [], validation = null } = {}) => {
|
|
15
|
+
// Discovery endpoint
|
|
16
|
+
await page.route('**/promo-codes/all/discover*', route =>
|
|
17
|
+
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(discoveryResponse(discovery)) })
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
// Ticket types (with or without promo filter)
|
|
21
|
+
await page.route('**/ticket-types/allowed*', route =>
|
|
22
|
+
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(ticketTypesResponse(tickets)) })
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// Tax types
|
|
26
|
+
await page.route('**/tax-types*', route =>
|
|
27
|
+
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(taxTypesResponse(taxes)) })
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// Validation endpoint (if provided)
|
|
31
|
+
if (validation) {
|
|
32
|
+
await page.route('**/promo-codes/*/apply*', route => {
|
|
33
|
+
if (typeof validation === 'function') {
|
|
34
|
+
validation(route);
|
|
35
|
+
} else {
|
|
36
|
+
route.fulfill({
|
|
37
|
+
status: validation.status || 200,
|
|
38
|
+
contentType: 'application/json',
|
|
39
|
+
body: JSON.stringify(validation.body || validationResponse()),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const selectTicket = async (page, ticketName) => {
|
|
47
|
+
await page.locator('[data-testid="ticket-dropdown"]').click();
|
|
48
|
+
await page.locator(`[data-testid="ticket-list"] >> text=${ticketName}`).click();
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// ── Discovery scenarios ──
|
|
52
|
+
|
|
53
|
+
test.describe('no discovery codes', () => {
|
|
54
|
+
test('shows default promo code UI', async ({ page }) => {
|
|
55
|
+
await setupRoutes(page, {
|
|
56
|
+
tickets: [ticketType()],
|
|
57
|
+
discovery: [],
|
|
58
|
+
});
|
|
59
|
+
await page.goto('/');
|
|
60
|
+
await expect(page.locator('text=Do you have a promo code?')).toBeVisible();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('manual code entry still works', async ({ page }) => {
|
|
64
|
+
await setupRoutes(page, {
|
|
65
|
+
tickets: [ticketType()],
|
|
66
|
+
discovery: [],
|
|
67
|
+
validation: { status: 200, body: validationResponse() },
|
|
68
|
+
});
|
|
69
|
+
await page.goto('/');
|
|
70
|
+
await selectTicket(page, 'Early Bird Ticket');
|
|
71
|
+
await page.fill('input[placeholder="Enter your promo code"]', 'MANUALCODE');
|
|
72
|
+
await page.click('button:has-text("Apply")');
|
|
73
|
+
await expect(page.locator('input[placeholder="Enter your promo code"][readonly]')).toBeVisible();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test.describe('suggestion flow (auto_apply: false)', () => {
|
|
78
|
+
const suggestCode = discoveredCode({ auto_apply: false, code: 'SUGGEST50' });
|
|
79
|
+
|
|
80
|
+
test('shows suggestion when qualifying ticket selected', async ({ page }) => {
|
|
81
|
+
await setupRoutes(page, {
|
|
82
|
+
tickets: [ticketType()],
|
|
83
|
+
discovery: [suggestCode],
|
|
84
|
+
});
|
|
85
|
+
await page.goto('/');
|
|
86
|
+
await selectTicket(page, 'Early Bird Ticket');
|
|
87
|
+
await expect(page.locator('text=You qualify for the following promo code:')).toBeVisible();
|
|
88
|
+
await expect(page.locator('input[placeholder="Enter your promo code"]')).toHaveValue('SUGGEST50');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('no suggestion for non-qualifying ticket', async ({ page }) => {
|
|
92
|
+
const nonQualifyingTicket = ticketType({ id: 999, name: 'Standard Ticket' });
|
|
93
|
+
await setupRoutes(page, {
|
|
94
|
+
tickets: [nonQualifyingTicket],
|
|
95
|
+
discovery: [suggestCode],
|
|
96
|
+
});
|
|
97
|
+
await page.goto('/');
|
|
98
|
+
await selectTicket(page, 'Standard Ticket');
|
|
99
|
+
await expect(page.locator('text=Do you have a promo code?')).toBeVisible();
|
|
100
|
+
await expect(page.locator('input[placeholder="Enter your promo code"]')).toHaveValue('');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('suggestion clears when user edits input', async ({ page }) => {
|
|
104
|
+
await setupRoutes(page, {
|
|
105
|
+
tickets: [ticketType()],
|
|
106
|
+
discovery: [suggestCode],
|
|
107
|
+
});
|
|
108
|
+
await page.goto('/');
|
|
109
|
+
await selectTicket(page, 'Early Bird Ticket');
|
|
110
|
+
await expect(page.locator('text=You qualify for the following promo code:')).toBeVisible();
|
|
111
|
+
await page.fill('input[placeholder="Enter your promo code"]', 'MYCODE');
|
|
112
|
+
await expect(page.locator('text=Do you have a promo code?')).toBeVisible();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('suggestion re-appears when user types discovered code back', async ({ page }) => {
|
|
116
|
+
await setupRoutes(page, {
|
|
117
|
+
tickets: [ticketType()],
|
|
118
|
+
discovery: [suggestCode],
|
|
119
|
+
});
|
|
120
|
+
await page.goto('/');
|
|
121
|
+
await selectTicket(page, 'Early Bird Ticket');
|
|
122
|
+
await page.fill('input[placeholder="Enter your promo code"]', 'OTHER');
|
|
123
|
+
await expect(page.locator('text=Do you have a promo code?')).toBeVisible();
|
|
124
|
+
await page.fill('input[placeholder="Enter your promo code"]', 'SUGGEST50');
|
|
125
|
+
await expect(page.locator('text=You qualify for the following promo code:')).toBeVisible();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test.describe('auto-apply flow (auto_apply: true)', () => {
|
|
130
|
+
const autoCode = discoveredCode({ auto_apply: true, code: 'AUTO100' });
|
|
131
|
+
|
|
132
|
+
test('auto-applies when qualifying ticket selected', async ({ page }) => {
|
|
133
|
+
await setupRoutes(page, {
|
|
134
|
+
tickets: [ticketType()],
|
|
135
|
+
discovery: [autoCode],
|
|
136
|
+
validation: { status: 200, body: validationResponse() },
|
|
137
|
+
});
|
|
138
|
+
await page.goto('/');
|
|
139
|
+
await selectTicket(page, 'Early Bird Ticket');
|
|
140
|
+
await expect(page.locator('text=Following promo code was automatically applied:')).toBeVisible();
|
|
141
|
+
await expect(page.locator('input[placeholder="Enter your promo code"]')).toHaveValue('AUTO100');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('shows suggestion after removing auto-applied code', async ({ page }) => {
|
|
145
|
+
await setupRoutes(page, {
|
|
146
|
+
tickets: [ticketType()],
|
|
147
|
+
discovery: [autoCode],
|
|
148
|
+
validation: { status: 200, body: validationResponse() },
|
|
149
|
+
});
|
|
150
|
+
await page.goto('/');
|
|
151
|
+
await selectTicket(page, 'Early Bird Ticket');
|
|
152
|
+
await expect(page.locator('text=Following promo code was automatically applied:')).toBeVisible();
|
|
153
|
+
await page.click('button:has-text("Remove")');
|
|
154
|
+
await expect(page.locator('text=You qualify for the following promo code:')).toBeVisible();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('does not auto-apply for non-qualifying ticket', async ({ page }) => {
|
|
158
|
+
const nonQualifyingTicket = ticketType({ id: 999, name: 'Standard Ticket' });
|
|
159
|
+
await setupRoutes(page, {
|
|
160
|
+
tickets: [nonQualifyingTicket],
|
|
161
|
+
discovery: [autoCode],
|
|
162
|
+
});
|
|
163
|
+
await page.goto('/');
|
|
164
|
+
await selectTicket(page, 'Standard Ticket');
|
|
165
|
+
await expect(page.locator('text=Do you have a promo code?')).toBeVisible();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test.describe('per-account limit', () => {
|
|
170
|
+
test('shows limit notice when code has quantity_per_account', async ({ page }) => {
|
|
171
|
+
const limitedCode = discoveredCode({ auto_apply: true, quantity_per_account: 2, remaining_quantity_per_account: 2 });
|
|
172
|
+
await setupRoutes(page, {
|
|
173
|
+
tickets: [ticketType()],
|
|
174
|
+
discovery: [limitedCode],
|
|
175
|
+
validation: { status: 200, body: validationResponse() },
|
|
176
|
+
});
|
|
177
|
+
await page.goto('/');
|
|
178
|
+
await selectTicket(page, 'Early Bird Ticket');
|
|
179
|
+
await expect(page.locator('text=Promo code limits 2 tickets per account.')).toBeVisible();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('no limit notice when quantity_per_account is 0', async ({ page }) => {
|
|
183
|
+
const unlimitedCode = discoveredCode({ auto_apply: true, quantity_per_account: 0, remaining_quantity_per_account: null });
|
|
184
|
+
await setupRoutes(page, {
|
|
185
|
+
tickets: [ticketType()],
|
|
186
|
+
discovery: [unlimitedCode],
|
|
187
|
+
validation: { status: 200, body: validationResponse() },
|
|
188
|
+
});
|
|
189
|
+
await page.goto('/');
|
|
190
|
+
await selectTicket(page, 'Early Bird Ticket');
|
|
191
|
+
await expect(page.locator('text=Promo code limits')).not.toBeVisible();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('singular ticket text for limit of 1', async ({ page }) => {
|
|
195
|
+
const singleCode = discoveredCode({ auto_apply: true, quantity_per_account: 1, remaining_quantity_per_account: 1 });
|
|
196
|
+
await setupRoutes(page, {
|
|
197
|
+
tickets: [ticketType()],
|
|
198
|
+
discovery: [singleCode],
|
|
199
|
+
validation: { status: 200, body: validationResponse() },
|
|
200
|
+
});
|
|
201
|
+
await page.goto('/');
|
|
202
|
+
await selectTicket(page, 'Early Bird Ticket');
|
|
203
|
+
await expect(page.locator('text=Promo code limits 1 ticket per account.')).toBeVisible();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('disables Next when remaining_quantity_per_account is 0', async ({ page }) => {
|
|
207
|
+
const exhaustedCode = discoveredCode({ auto_apply: true, quantity_per_account: 2, remaining_quantity_per_account: 0 });
|
|
208
|
+
await setupRoutes(page, {
|
|
209
|
+
tickets: [ticketType()],
|
|
210
|
+
discovery: [exhaustedCode],
|
|
211
|
+
validation: { status: 200, body: validationResponse() },
|
|
212
|
+
});
|
|
213
|
+
await page.goto('/');
|
|
214
|
+
await selectTicket(page, 'Early Bird Ticket');
|
|
215
|
+
await expect(page.locator('button:has-text("Next")')).toBeDisabled();
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test.describe('non-transferable notice', () => {
|
|
220
|
+
test('shows notice when allows_to_reassign is false', async ({ page }) => {
|
|
221
|
+
const nonTransferableCode = discoveredCode({ auto_apply: true, allows_to_reassign: false });
|
|
222
|
+
await setupRoutes(page, {
|
|
223
|
+
tickets: [ticketType()],
|
|
224
|
+
discovery: [nonTransferableCode],
|
|
225
|
+
validation: { status: 200, body: validationResponse({ allows_to_reassign: false }) },
|
|
226
|
+
});
|
|
227
|
+
await page.goto('/');
|
|
228
|
+
await selectTicket(page, 'Early Bird Ticket');
|
|
229
|
+
await expect(page.locator('text=This ticket will be automatically assigned to you')).toBeVisible();
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test.describe('validation errors', () => {
|
|
234
|
+
test('shows error for invalid code', async ({ page }) => {
|
|
235
|
+
await setupRoutes(page, {
|
|
236
|
+
tickets: [ticketType()],
|
|
237
|
+
discovery: [],
|
|
238
|
+
validation: { status: 412, body: validationError('The Promo Code "BAD" is not a valid code.') },
|
|
239
|
+
});
|
|
240
|
+
await page.goto('/');
|
|
241
|
+
await selectTicket(page, 'Early Bird Ticket');
|
|
242
|
+
await page.fill('input[placeholder="Enter your promo code"]', 'BAD');
|
|
243
|
+
await page.click('button:has-text("Apply")');
|
|
244
|
+
await expect(page.locator('text=Promo code entered is not valid.')).toBeVisible();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test('shows error for wrong ticket type', async ({ page }) => {
|
|
248
|
+
await setupRoutes(page, {
|
|
249
|
+
tickets: [ticketType()],
|
|
250
|
+
discovery: [],
|
|
251
|
+
validation: { status: 412, body: validationError('Promo code XYZ can not be applied to Ticket Type Early Bird Ticket.') },
|
|
252
|
+
});
|
|
253
|
+
await page.goto('/');
|
|
254
|
+
await selectTicket(page, 'Early Bird Ticket');
|
|
255
|
+
await page.fill('input[placeholder="Enter your promo code"]', 'XYZ');
|
|
256
|
+
await page.click('button:has-text("Apply")');
|
|
257
|
+
await expect(page.locator('text=Promo code XYZ can not be applied to Ticket Type Early Bird Ticket.')).toBeVisible();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('error clears when user types', async ({ page }) => {
|
|
261
|
+
await setupRoutes(page, {
|
|
262
|
+
tickets: [ticketType()],
|
|
263
|
+
discovery: [],
|
|
264
|
+
validation: { status: 412, body: validationError('The Promo Code "BAD" is not a valid code.') },
|
|
265
|
+
});
|
|
266
|
+
await page.goto('/');
|
|
267
|
+
await selectTicket(page, 'Early Bird Ticket');
|
|
268
|
+
await page.fill('input[placeholder="Enter your promo code"]', 'BAD');
|
|
269
|
+
await page.click('button:has-text("Apply")');
|
|
270
|
+
await expect(page.locator('text=Promo code entered is not valid.')).toBeVisible();
|
|
271
|
+
await page.click('button:has-text("Remove")');
|
|
272
|
+
await page.fill('input[placeholder="Enter your promo code"]', 'NEW');
|
|
273
|
+
await expect(page.locator('text=Promo code entered is not valid.')).not.toBeVisible();
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test.describe('no tickets available', () => {
|
|
278
|
+
test('shows no tickets message', async ({ page }) => {
|
|
279
|
+
await setupRoutes(page, {
|
|
280
|
+
tickets: [],
|
|
281
|
+
discovery: [discoveredCode()],
|
|
282
|
+
});
|
|
283
|
+
await page.goto('/');
|
|
284
|
+
await expect(page.locator('text=no tickets currently available')).toBeVisible();
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test.describe('ticket switching with applied code', () => {
|
|
289
|
+
test('removes discovered code when switching to non-qualifying ticket', async ({ page }) => {
|
|
290
|
+
const qualifying = ticketType({ id: 188, name: 'Early Bird Ticket' });
|
|
291
|
+
const nonQualifying = ticketType({ id: 999, name: 'Standard Ticket' });
|
|
292
|
+
const code = discoveredCode({ auto_apply: true, allowed_ticket_types: [188] });
|
|
293
|
+
|
|
294
|
+
await setupRoutes(page, {
|
|
295
|
+
tickets: [qualifying, nonQualifying],
|
|
296
|
+
discovery: [code],
|
|
297
|
+
validation: { status: 200, body: validationResponse() },
|
|
298
|
+
});
|
|
299
|
+
await page.goto('/');
|
|
300
|
+
await selectTicket(page, 'Early Bird Ticket');
|
|
301
|
+
await expect(page.locator('text=Following promo code was automatically applied:')).toBeVisible();
|
|
302
|
+
|
|
303
|
+
await selectTicket(page, 'Standard Ticket');
|
|
304
|
+
await expect(page.locator('text=Do you have a promo code?')).toBeVisible();
|
|
305
|
+
await expect(page.locator('input[placeholder="Enter your promo code"]')).toHaveValue('');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('suggestion appears when switching back to qualifying ticket', async ({ page }) => {
|
|
309
|
+
const qualifying = ticketType({ id: 188, name: 'Early Bird Ticket' });
|
|
310
|
+
const nonQualifying = ticketType({ id: 999, name: 'Standard Ticket' });
|
|
311
|
+
const code = discoveredCode({ auto_apply: true, allowed_ticket_types: [188] });
|
|
312
|
+
|
|
313
|
+
await setupRoutes(page, {
|
|
314
|
+
tickets: [qualifying, nonQualifying],
|
|
315
|
+
discovery: [code],
|
|
316
|
+
validation: { status: 200, body: validationResponse() },
|
|
317
|
+
});
|
|
318
|
+
await page.goto('/');
|
|
319
|
+
|
|
320
|
+
// Auto-apply on qualifying
|
|
321
|
+
await selectTicket(page, 'Early Bird Ticket');
|
|
322
|
+
await expect(page.locator('text=Following promo code was automatically applied:')).toBeVisible();
|
|
323
|
+
|
|
324
|
+
// Remove
|
|
325
|
+
await page.click('button:has-text("Remove")');
|
|
326
|
+
|
|
327
|
+
// Switch away and back
|
|
328
|
+
await selectTicket(page, 'Standard Ticket');
|
|
329
|
+
await selectTicket(page, 'Early Bird Ticket');
|
|
330
|
+
await expect(page.locator('text=You qualify for the following promo code:')).toBeVisible();
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test.describe('discovery API errors', () => {
|
|
335
|
+
test('widget works normally when discovery returns 500', async ({ page }) => {
|
|
336
|
+
page.route('**/promo-codes/all/discover*', route =>
|
|
337
|
+
route.fulfill({ status: 500, contentType: 'application/json', body: JSON.stringify({ message: 'Internal Server Error' }) })
|
|
338
|
+
);
|
|
339
|
+
page.route('**/ticket-types/allowed*', route =>
|
|
340
|
+
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(ticketTypesResponse([ticketType()])) })
|
|
341
|
+
);
|
|
342
|
+
page.route('**/tax-types*', route =>
|
|
343
|
+
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(taxTypesResponse()) })
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
await page.goto('/');
|
|
347
|
+
await expect(page.locator('text=Do you have a promo code?')).toBeVisible();
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test('widget works normally when discovery returns 403', async ({ page }) => {
|
|
351
|
+
page.route('**/promo-codes/all/discover*', route =>
|
|
352
|
+
route.fulfill({ status: 403, contentType: 'application/json', body: JSON.stringify({ message: 'Forbidden' }) })
|
|
353
|
+
);
|
|
354
|
+
page.route('**/ticket-types/allowed*', route =>
|
|
355
|
+
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(ticketTypesResponse([ticketType()])) })
|
|
356
|
+
);
|
|
357
|
+
page.route('**/tax-types*', route =>
|
|
358
|
+
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(taxTypesResponse()) })
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
await page.goto('/');
|
|
362
|
+
await expect(page.locator('text=Do you have a promo code?')).toBeVisible();
|
|
363
|
+
});
|
|
364
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "summit-registration-lite",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.2",
|
|
4
4
|
"description": "Summit Registration Lite",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
"serve": "webpack-dev-server --open --config webpack.dev.js",
|
|
11
11
|
"copy-extras": "cp -rf package.json README.md dist",
|
|
12
12
|
"publish-package": "yarn build && yarn publish",
|
|
13
|
-
"test": "jest --watch"
|
|
13
|
+
"test": "jest --watch",
|
|
14
|
+
"test:ci": "jest --ci",
|
|
15
|
+
"test:e2e": "npx playwright test"
|
|
14
16
|
},
|
|
15
17
|
"author": "santipalenque@gmail.com",
|
|
16
18
|
"license": "Apache-2.0",
|
|
@@ -26,6 +28,10 @@
|
|
|
26
28
|
"setupFilesAfterEnv": [
|
|
27
29
|
"<rootDir>/jest-setup.js"
|
|
28
30
|
],
|
|
31
|
+
"testPathIgnorePatterns": [
|
|
32
|
+
"<rootDir>/node_modules/",
|
|
33
|
+
"<rootDir>/e2e/"
|
|
34
|
+
],
|
|
29
35
|
"transformIgnorePatterns": [
|
|
30
36
|
"<rootDir>/node_modules/(?!react-dnd|dnd-core|@react-dnd)"
|
|
31
37
|
]
|
|
@@ -42,9 +48,11 @@
|
|
|
42
48
|
"@mui/icons-material": "^5.14.18",
|
|
43
49
|
"@mui/material": "^5.15.20",
|
|
44
50
|
"@mui/utils": "^5.16.8",
|
|
51
|
+
"@playwright/test": "^1.59.1",
|
|
45
52
|
"@react-pdf/renderer": "^3.1.12",
|
|
46
53
|
"@testing-library/jest-dom": "^5.12.0",
|
|
47
54
|
"@testing-library/react": "^11.2.6",
|
|
55
|
+
"@testing-library/react-hooks": "^8.0.1",
|
|
48
56
|
"awesome-bootstrap-checkbox": "^1.0.1",
|
|
49
57
|
"babel-cli": "^6.26.0",
|
|
50
58
|
"babel-jest": "^26.6.3",
|
|
@@ -70,7 +78,6 @@
|
|
|
70
78
|
"mini-css-extract-plugin": "^2.6.0",
|
|
71
79
|
"moment": "^2.22.2",
|
|
72
80
|
"moment-timezone": "^0.5.21",
|
|
73
|
-
"node-sass": "^7.0.1",
|
|
74
81
|
"openstack-uicore-foundation": "4.2.14",
|
|
75
82
|
"optimize-css-assets-webpack-plugin": "^6.0.1",
|
|
76
83
|
"path": "^0.12.7",
|
|
@@ -87,7 +94,7 @@
|
|
|
87
94
|
"react-router-dom": "^4.3.1",
|
|
88
95
|
"react-rte": "^0.16.5",
|
|
89
96
|
"react-select": "^2.4.3",
|
|
90
|
-
"react-test-renderer": "
|
|
97
|
+
"react-test-renderer": "17",
|
|
91
98
|
"react-tooltip": "^3.11.6",
|
|
92
99
|
"react-transition-group": "^1.2.1",
|
|
93
100
|
"redux": "^3.7.2",
|
|
@@ -95,6 +102,7 @@
|
|
|
95
102
|
"redux-persist": "^5.9.1",
|
|
96
103
|
"redux-thunk": "^2.3.0",
|
|
97
104
|
"regenerator-runtime": "^0.13.7",
|
|
105
|
+
"sass": "^1.77.0",
|
|
98
106
|
"sass-loader": "^12.6.0",
|
|
99
107
|
"style-loader": "^3.3.1",
|
|
100
108
|
"superagent": "^3.8.1",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const { defineConfig } = require('@playwright/test');
|
|
2
|
+
|
|
3
|
+
module.exports = defineConfig({
|
|
4
|
+
testDir: './e2e',
|
|
5
|
+
timeout: 30000,
|
|
6
|
+
use: {
|
|
7
|
+
baseURL: 'https://localhost:8888',
|
|
8
|
+
ignoreHTTPSErrors: true,
|
|
9
|
+
},
|
|
10
|
+
projects: [
|
|
11
|
+
{
|
|
12
|
+
name: 'chromium',
|
|
13
|
+
use: { browserName: 'chromium' },
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
reporter: process.env.CI
|
|
17
|
+
? [['html', { open: 'never' }], ['list']]
|
|
18
|
+
: [['list']],
|
|
19
|
+
webServer: {
|
|
20
|
+
command: 'yarn serve',
|
|
21
|
+
url: 'https://localhost:8888',
|
|
22
|
+
ignoreHTTPSErrors: true,
|
|
23
|
+
reuseExistingServer: !process.env.CI,
|
|
24
|
+
timeout: 120000,
|
|
25
|
+
},
|
|
26
|
+
});
|