@product7/feedback-sdk 1.6.9 → 1.7.1

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,398 @@
1
+ # Survey Widget — QA Test Cases
2
+
3
+ ## Overview
4
+ This document covers manual test cases for verifying the survey widget functionality across all supported survey types (NPS, CSAT, CES, Custom, and Multi-page).
5
+
6
+ ---
7
+
8
+ ## Prerequisites
9
+
10
+ - SDK is initialized with a valid `workspace` and `userContext`
11
+ - `init()` has been called successfully (session token received)
12
+ - Browser DevTools open to monitor network requests and console
13
+
14
+ ---
15
+
16
+ ## Test Environment Setup
17
+
18
+ ```js
19
+ const sdk = new FeedbackSDK({
20
+ workspace: 'your-workspace',
21
+ userContext: { user_id: 'qa_user_01', email: 'qa@example.com' }
22
+ });
23
+ await sdk.init();
24
+ ```
25
+
26
+ ---
27
+
28
+ ## TC-01: NPS Survey — Basic Display
29
+
30
+ **Objective:** Verify NPS survey renders correctly with 11-point scale.
31
+
32
+ **Steps:**
33
+ 1. Call `sdk.showSurvey({ surveyType: 'nps', ratingScale: 11, title: 'How likely are you to recommend us?', lowLabel: 'Not likely', highLabel: 'Very likely' })`
34
+ 2. Observe the survey widget on screen
35
+
36
+ **Expected Results:**
37
+ - [ ] Survey widget appears at the configured position (default: bottom-right)
38
+ - [ ] Title text is displayed: "How likely are you to recommend us?"
39
+ - [ ] 11 score buttons are shown, numbered 0 through 10
40
+ - [ ] "Not likely" label appears below the leftmost button
41
+ - [ ] "Very likely" label appears below the rightmost button
42
+ - [ ] A close (✕) button is visible in the top-right corner of the widget
43
+
44
+ ---
45
+
46
+ ## TC-02: NPS Survey — Score Selection
47
+
48
+ **Objective:** Verify score buttons highlight correctly on selection.
49
+
50
+ **Steps:**
51
+ 1. Show NPS survey (as in TC-01)
52
+ 2. Click score button **7**
53
+ 3. Click score button **9**
54
+
55
+ **Expected Results:**
56
+ - [ ] After clicking 7: button 7 is visually highlighted/selected
57
+ - [ ] After clicking 9: button 9 is highlighted, button 7 is no longer highlighted
58
+ - [ ] Only one button is selected at a time
59
+
60
+ ---
61
+
62
+ ## TC-03: NPS Survey — Submission
63
+
64
+ **Objective:** Verify NPS survey submits the selected score.
65
+
66
+ **Steps:**
67
+ 1. Show NPS survey with `showSubmitButton: true` and a valid `surveyId`
68
+ 2. Click score button **8**
69
+ 3. Click the **Submit** button
70
+ 4. Observe network tab
71
+
72
+ **Expected Results:**
73
+ - [ ] A POST request is sent to `/widget/surveys/{surveyId}/responses`
74
+ - [ ] Request payload contains `rating: 8`
75
+ - [ ] Survey widget closes after submission
76
+ - [ ] A success notification ("Thank you for your feedback!") appears briefly
77
+ - [ ] `survey:submitted` event is fired (verify in console with `sdk.on('survey:submitted', console.log)`)
78
+
79
+ ---
80
+
81
+ ## TC-04: NPS Survey — Submit Without Score
82
+
83
+ **Objective:** Verify error message when submitting with no score selected.
84
+
85
+ **Steps:**
86
+ 1. Show NPS survey with `showSubmitButton: true`
87
+ 2. Do NOT click any score button
88
+ 3. Click the **Submit** button
89
+
90
+ **Expected Results:**
91
+ - [ ] Error message "Please select a rating" appears inside the widget
92
+ - [ ] Survey does NOT close
93
+ - [ ] No API request is made
94
+ - [ ] Error disappears after ~3 seconds
95
+
96
+ ---
97
+
98
+ ## TC-05: NPS Survey — Auto-submit on Select
99
+
100
+ **Objective:** Verify survey auto-submits when `autoSubmitOnSelect: true`.
101
+
102
+ **Steps:**
103
+ 1. Call `sdk.showSurvey({ surveyType: 'nps', autoSubmitOnSelect: true, surveyId: 'survey-id' })`
104
+ 2. Click any score button
105
+
106
+ **Expected Results:**
107
+ - [ ] Survey submits immediately upon clicking a score (no submit button click needed)
108
+ - [ ] POST request is sent with the selected score
109
+ - [ ] Survey closes and success notification shows
110
+
111
+ ---
112
+
113
+ ## TC-06: CSAT Survey — Display and Selection
114
+
115
+ **Objective:** Verify CSAT survey renders emoji buttons correctly.
116
+
117
+ **Steps:**
118
+ 1. Call `sdk.showSurvey({ surveyType: 'csat', title: 'How satisfied are you?' })`
119
+ 2. Click the 4th emoji (😊)
120
+
121
+ **Expected Results:**
122
+ - [ ] 5 emoji buttons are displayed (😞 😟 😐 🙂 😄)
123
+ - [ ] Clicked emoji is visually selected/highlighted
124
+ - [ ] Other emojis are deselected
125
+ - [ ] Score corresponds to position (1–5)
126
+
127
+ ---
128
+
129
+ ## TC-07: CES Survey — Display and Selection
130
+
131
+ **Objective:** Verify CES survey renders effort scale buttons.
132
+
133
+ **Steps:**
134
+ 1. Call `sdk.showSurvey({ surveyType: 'ces', title: 'How easy was it?' })`
135
+ 2. Click **Easy**
136
+
137
+ **Expected Results:**
138
+ - [ ] 5 buttons displayed: Very Difficult / Difficult / Neutral / Easy / Very Easy
139
+ - [ ] Clicked button is highlighted
140
+ - [ ] Other buttons are deselected
141
+
142
+ ---
143
+
144
+ ## TC-08: Survey with Feedback Text Input
145
+
146
+ **Objective:** Verify optional feedback textarea appears and value is submitted.
147
+
148
+ **Steps:**
149
+ 1. Show any survey with `showFeedbackInput: true` and `showSubmitButton: true`
150
+ 2. Select a score
151
+ 3. Type "Great product!" in the feedback textarea
152
+ 4. Click Submit
153
+
154
+ **Expected Results:**
155
+ - [ ] Textarea is visible below the score selection
156
+ - [ ] Typed text appears in the textarea
157
+ - [ ] Submitted payload includes `feedback: "Great product!"`
158
+
159
+ ---
160
+
161
+ ## TC-09: Survey Dismiss — Close Button
162
+
163
+ **Objective:** Verify close button dismisses the survey.
164
+
165
+ **Steps:**
166
+ 1. Show any survey
167
+ 2. Click the ✕ (close) button
168
+
169
+ **Expected Results:**
170
+ - [ ] Survey widget fades out and is removed from the DOM
171
+ - [ ] `survey:dismissed` event is fired
172
+ - [ ] If `onDismiss` callback was provided, it is called
173
+ - [ ] If a valid `surveyId` was set, a POST request is sent to `/widget/surveys/{surveyId}/dismiss`
174
+
175
+ ---
176
+
177
+ ## TC-10: Survey Dismiss — Escape Key
178
+
179
+ **Objective:** Verify pressing Escape closes the survey.
180
+
181
+ **Steps:**
182
+ 1. Show any survey
183
+ 2. Press the **Escape** key on the keyboard
184
+
185
+ **Expected Results:**
186
+ - [ ] Survey closes (same behaviour as TC-09)
187
+ - [ ] `survey:dismissed` event fires
188
+
189
+ ---
190
+
191
+ ## TC-11: Survey — Disabled State
192
+
193
+ **Objective:** Verify survey is suppressed when `enabled: false`.
194
+
195
+ **Steps:**
196
+ 1. Call `sdk.showSurvey({ surveyType: 'nps', enabled: false })`
197
+
198
+ **Expected Results:**
199
+ - [ ] No survey widget appears in the DOM
200
+ - [ ] `showSurvey()` returns `null`
201
+ - [ ] `survey:suppressed` event is fired with `reason: 'disabled'`
202
+
203
+ ---
204
+
205
+ ## TC-12: Survey — Position Variants
206
+
207
+ **Objective:** Verify survey renders in each supported position.
208
+
209
+ **Steps:**
210
+ 1. Test each position: `bottom-right`, `bottom-left`, `center`
211
+ 2. Call `sdk.showSurvey({ surveyType: 'nps', position: '<position>' })`
212
+
213
+ **Expected Results:**
214
+ | Position | Expected Appearance |
215
+ |---|---|
216
+ | `bottom-right` | Widget anchored to bottom-right corner |
217
+ | `bottom-left` | Widget anchored to bottom-left corner |
218
+ | `center` | Widget centered on screen with backdrop overlay |
219
+
220
+ - [ ] For `center`: backdrop overlay is displayed behind the widget
221
+ - [ ] Clicking the backdrop (center mode) closes the survey
222
+
223
+ ---
224
+
225
+ ## TC-13: Survey — Theme Variants
226
+
227
+ **Objective:** Verify light and dark themes apply correctly.
228
+
229
+ **Steps:**
230
+ 1. Test `theme: 'light'` and `theme: 'dark'`
231
+
232
+ **Expected Results:**
233
+ - [ ] Light theme: white/light background, dark text
234
+ - [ ] Dark theme: dark background, light text
235
+ - [ ] Theme class is applied to the survey element
236
+
237
+ ---
238
+
239
+ ## TC-14: Multi-page Survey — Navigation
240
+
241
+ **Objective:** Verify multi-page survey navigates between pages correctly.
242
+
243
+ **Setup:**
244
+ ```js
245
+ sdk.showSurvey({
246
+ surveyId: 'multi-page-id',
247
+ pages: [
248
+ { id: 'p1', type: 'rating', title: 'Rate our product', position: 0, ratingConfig: { scale: 5 } },
249
+ { id: 'p2', type: 'text', title: 'Any comments?', position: 1 },
250
+ ]
251
+ });
252
+ ```
253
+
254
+ **Steps:**
255
+ 1. Observe page 1
256
+ 2. Click a rating button
257
+ 3. Click **Next**
258
+ 4. Observe page 2
259
+ 5. Click **Back**
260
+ 6. Observe page 1 again
261
+
262
+ **Expected Results:**
263
+ - [ ] Page 1 shows "Page 1 of 2" progress indicator
264
+ - [ ] Action button on page 1 reads "Next"
265
+ - [ ] After clicking Next: page 2 loads, progress shows "Page 2 of 2"
266
+ - [ ] Page 2 has a **Back** button
267
+ - [ ] Action button on page 2 reads "Submit"
268
+ - [ ] After clicking Back: page 1 is shown again
269
+
270
+ ---
271
+
272
+ ## TC-15: Multi-page Survey — Validation
273
+
274
+ **Objective:** Verify each page validates input before advancing.
275
+
276
+ **Steps (Rating page):**
277
+ 1. Display multi-page survey on page 1 (rating type)
278
+ 2. Click **Next** without selecting a rating
279
+
280
+ **Expected Results:**
281
+ - [ ] Error message "Please select a rating" appears
282
+ - [ ] Survey does NOT advance to page 2
283
+
284
+ **Steps (Text page):**
285
+ 1. Advance to a text-type page
286
+ 2. Click **Submit** without entering any text
287
+
288
+ **Expected Results:**
289
+ - [ ] Error message "Please enter an answer" appears
290
+ - [ ] Survey does NOT submit
291
+
292
+ **Steps (Multiple choice page):**
293
+ 1. Advance to a multiple-choice page
294
+ 2. Click **Submit** without selecting any option
295
+
296
+ **Expected Results:**
297
+ - [ ] Error message "Please select an option" appears
298
+
299
+ ---
300
+
301
+ ## TC-16: Multi-page Survey — Full Submission
302
+
303
+ **Objective:** Verify complete multi-page survey submits all page answers.
304
+
305
+ **Steps:**
306
+ 1. Display the multi-page survey (TC-14 setup)
307
+ 2. On page 1: select a rating, click **Next**
308
+ 3. On page 2: type a comment, click **Submit**
309
+ 4. Check network request
310
+
311
+ **Expected Results:**
312
+ - [ ] POST to `/widget/surveys/{surveyId}/responses` is made
313
+ - [ ] Payload `answers.page_answers` contains entries for both pages
314
+ - [ ] Success notification appears after submission
315
+
316
+ ---
317
+
318
+ ## TC-17: getActiveSurveys() — Fetch and Display
319
+
320
+ **Objective:** Verify active surveys can be fetched and shown programmatically.
321
+
322
+ **Steps:**
323
+ 1. Call `const surveys = await sdk.getActiveSurveys()`
324
+ 2. Log result to console
325
+ 3. Call `sdk.showSurveyById(surveys[0].surveyId)`
326
+
327
+ **Expected Results:**
328
+ - [ ] `getActiveSurveys()` returns an array of survey objects
329
+ - [ ] Each survey has `surveyId`, `surveyType`, `title` fields
330
+ - [ ] `showSurveyById()` successfully renders the matching survey
331
+ - [ ] Correct survey type is shown (NPS/CSAT/CES matches the fetched type)
332
+
333
+ ---
334
+
335
+ ## TC-18: showSurveyById() — Survey Not Found
336
+
337
+ **Objective:** Verify error handling when survey ID doesn't exist.
338
+
339
+ **Steps:**
340
+ 1. Call `await sdk.showSurveyById('nonexistent-id')`
341
+
342
+ **Expected Results:**
343
+ - [ ] Promise rejects with an error message containing "not found"
344
+ - [ ] No survey widget appears in the DOM
345
+
346
+ ---
347
+
348
+ ## TC-19: Survey — onSubmit Callback
349
+
350
+ **Objective:** Verify `onSubmit` callback receives the correct response object.
351
+
352
+ **Steps:**
353
+ 1. Call:
354
+ ```js
355
+ sdk.showSurvey({
356
+ surveyType: 'nps',
357
+ ratingScale: 11,
358
+ showSubmitButton: true,
359
+ onSubmit: (response) => console.log('onSubmit:', response)
360
+ });
361
+ ```
362
+ 2. Select score 9 and click Submit
363
+ 3. Check console output
364
+
365
+ **Expected Results:**
366
+ - [ ] `onSubmit` is called once
367
+ - [ ] `response.type` is `'nps'`
368
+ - [ ] `response.score` is `9`
369
+ - [ ] `response.timestamp` is a valid ISO date string
370
+
371
+ ---
372
+
373
+ ## TC-20: Survey — SDK Not Initialized Guard
374
+
375
+ **Objective:** Verify SDK throws when survey methods are called before `init()`.
376
+
377
+ **Steps:**
378
+ 1. Create SDK instance but do NOT call `init()`
379
+ 2. Call `sdk.showSurvey({ surveyType: 'nps' })`
380
+
381
+ **Expected Results:**
382
+ - [ ] Error is thrown: "SDK must be initialized before showing surveys. Call init() first."
383
+ - [ ] No network requests are made
384
+
385
+ ---
386
+
387
+ ## Regression Checklist
388
+
389
+ After any change to survey code, verify:
390
+
391
+ - [ ] All 4 survey types render without console errors (NPS, CSAT, CES, Custom)
392
+ - [ ] Score selection works for each type
393
+ - [ ] Submit sends correct payload to the API
394
+ - [ ] Dismiss/close cleans up the DOM (no lingering elements)
395
+ - [ ] Multi-page navigation works forward and backward
396
+ - [ ] `survey:shown`, `survey:submitted`, `survey:dismissed`, `survey:suppressed` events all fire at the right time
397
+ - [ ] `autoSubmitOnSelect: true` does not show submit button
398
+ - [ ] `enabled: false` prevents the survey from rendering
@@ -179,31 +179,172 @@
179
179
  const MOCK_SURVEYS = [
180
180
  {
181
181
  id: 'mock_nps_survey',
182
- type: 'nps',
183
- title: 'How likely are you to recommend us?',
184
- description: 'Your feedback helps us improve',
185
- low_label: 'Not likely',
186
- high_label: 'Very likely',
187
- trigger: 'manual',
182
+ name: 'NPS Score',
183
+ slug: 'nps',
188
184
  status: 'active',
185
+ trigger_type: 'manual',
186
+ pages: [
187
+ {
188
+ id: 'nps-page-1',
189
+ type: 'rating',
190
+ title: 'How likely are you to recommend us to a friend?',
191
+ description: '',
192
+ placeholder: '',
193
+ rating_config: { scale: 11, low_label: 'Very unlikely', high_label: 'Very likely', survey_type: 'nps' },
194
+ multiple_choice_config: null,
195
+ link_config: null,
196
+ after_this_page: { default: 'end_survey', conditions: [] },
197
+ required: true,
198
+ },
199
+ ],
189
200
  },
190
201
  {
191
202
  id: 'mock_csat_survey',
192
- type: 'csat',
193
- title: 'How satisfied are you?',
194
- description: 'Rate your experience with our product',
195
- trigger: 'manual',
203
+ name: 'CSAT Survey',
204
+ slug: 'csat',
196
205
  status: 'active',
206
+ trigger_type: 'manual',
207
+ pages: [
208
+ {
209
+ id: 'csat-page-1',
210
+ type: 'rating',
211
+ title: 'How satisfied are you with our service?',
212
+ description: '',
213
+ placeholder: '',
214
+ rating_config: { scale: 5, low_label: 'Very dissatisfied', high_label: 'Very satisfied', survey_type: 'emoji' },
215
+ multiple_choice_config: null,
216
+ link_config: null,
217
+ after_this_page: { default: 'end_survey', conditions: [] },
218
+ required: true,
219
+ },
220
+ ],
197
221
  },
198
222
  {
199
223
  id: 'mock_ces_survey',
200
- type: 'ces',
201
- title: 'How easy was it?',
202
- description: 'Rate the ease of completing your task',
203
- low_label: 'Very difficult',
204
- high_label: 'Very easy',
205
- trigger: 'manual',
224
+ name: 'Customer Effort Score',
225
+ slug: 'ces',
226
+ status: 'active',
227
+ trigger_type: 'manual',
228
+ pages: [
229
+ {
230
+ id: 'ces-page-1',
231
+ type: 'rating',
232
+ title: 'How easy was it to use our product?',
233
+ description: '',
234
+ placeholder: '',
235
+ rating_config: { scale: 5, low_label: 'Very difficult', high_label: 'Very easy', survey_type: 'ces' },
236
+ multiple_choice_config: null,
237
+ link_config: null,
238
+ after_this_page: { default: 'end_survey', conditions: [] },
239
+ required: true,
240
+ },
241
+ ],
242
+ },
243
+ {
244
+ id: 'mock_open_question_survey',
245
+ name: 'Open Question',
246
+ slug: 'open-question',
247
+ status: 'active',
248
+ trigger_type: 'manual',
249
+ pages: [
250
+ {
251
+ id: 'open-page-1',
252
+ type: 'text',
253
+ title: 'What could we do better?',
254
+ description: 'Is there anything we could do to make our product better for you?',
255
+ placeholder: 'Type your answer here',
256
+ rating_config: null,
257
+ multiple_choice_config: null,
258
+ link_config: null,
259
+ after_this_page: { default: 'end_survey', conditions: [] },
260
+ required: true,
261
+ },
262
+ ],
263
+ },
264
+ {
265
+ id: 'mock_product_idea_poll_survey',
266
+ name: 'Product Idea Poll',
267
+ slug: 'product-idea-poll',
206
268
  status: 'active',
269
+ trigger_type: 'manual',
270
+ pages: [
271
+ {
272
+ id: 'poll-page-1',
273
+ type: 'multiple_choice',
274
+ title: 'Which feature should we add next?',
275
+ description: 'Vote on the feature you would like to see next.',
276
+ placeholder: '',
277
+ rating_config: null,
278
+ multiple_choice_config: {
279
+ allow_multiple_selection: false,
280
+ survey_type: 'regular',
281
+ options: [
282
+ { id: 'opt1', label: 'Better reporting' },
283
+ { id: 'opt2', label: 'Mobile app' },
284
+ { id: 'opt3', label: 'Integrations' },
285
+ { id: 'opt4', label: 'AI features' },
286
+ ],
287
+ },
288
+ link_config: null,
289
+ after_this_page: { default: 'end_survey', conditions: [] },
290
+ required: true,
291
+ },
292
+ ],
293
+ },
294
+ {
295
+ id: 'mock_pmf_survey',
296
+ name: 'Product Market Fit (PMF)',
297
+ slug: 'pmf',
298
+ status: 'active',
299
+ trigger_type: 'manual',
300
+ pages: [
301
+ {
302
+ id: 'pmf-page-1',
303
+ type: 'multiple_choice',
304
+ title: 'How would you feel if you could no longer use our product?',
305
+ description: '',
306
+ placeholder: '',
307
+ rating_config: null,
308
+ multiple_choice_config: {
309
+ allow_multiple_selection: false,
310
+ survey_type: 'regular',
311
+ options: [
312
+ { id: 'very_disappointed', label: 'Very disappointed' },
313
+ { id: 'somewhat_disappointed', label: 'Somewhat disappointed' },
314
+ { id: 'not_disappointed', label: 'Not disappointed' },
315
+ ],
316
+ },
317
+ link_config: null,
318
+ after_this_page: { default: 'end_survey', conditions: [] },
319
+ required: true,
320
+ },
321
+ ],
322
+ },
323
+ {
324
+ id: 'mock_user_interview_survey',
325
+ name: 'User Interview Request',
326
+ slug: 'user-interview',
327
+ status: 'active',
328
+ trigger_type: 'manual',
329
+ pages: [
330
+ {
331
+ id: 'interview-page-1',
332
+ type: 'link',
333
+ title: 'Would you like to hop on a quick demo?',
334
+ description: 'We would love to hear your thoughts and answer any questions you have.',
335
+ placeholder: '',
336
+ rating_config: null,
337
+ multiple_choice_config: null,
338
+ link_config: {
339
+ button_text: 'Book a demo',
340
+ link_text: '',
341
+ redirect_url: 'https://example.com',
342
+ open_in: 'new_tab',
343
+ },
344
+ after_this_page: { default: 'end_survey', conditions: [] },
345
+ required: true,
346
+ },
347
+ ],
207
348
  },
208
349
  ];
209
350
 
@@ -7368,6 +7509,8 @@
7368
7509
  return this._renderMultipleChoicePage(page);
7369
7510
  case 'text':
7370
7511
  return this._renderTextPage(page);
7512
+ case 'link':
7513
+ return this._renderLinkPage(page);
7371
7514
  default:
7372
7515
  return this._renderTextPage(page);
7373
7516
  }
@@ -7426,6 +7569,21 @@
7426
7569
  `;
7427
7570
  }
7428
7571
 
7572
+ if (ratingType === 'ces') {
7573
+ const cesLabels = ['Very Difficult', 'Difficult', 'Neutral', 'Easy', 'Very Easy'];
7574
+ return `
7575
+ <div class="feedback-survey-ces" data-page-id="${pageId}">
7576
+ ${cesLabels
7577
+ .map((label, i) => {
7578
+ const score = i + 1;
7579
+ const selected = currentRating === score ? ' selected' : '';
7580
+ return `<button class="feedback-survey-page-rating-btn feedback-survey-ces-btn${selected}" data-page-id="${pageId}" data-score="${score}">${label}</button>`;
7581
+ })
7582
+ .join('')}
7583
+ </div>
7584
+ `;
7585
+ }
7586
+
7429
7587
  if (ratingType === 'emoji' && scale === 5) {
7430
7588
  const emojis = [
7431
7589
  '\uD83D\uDE1E',
@@ -7468,7 +7626,9 @@
7468
7626
  page.multipleChoiceConfig || page.multiple_choice_config || {};
7469
7627
  const options = Array.isArray(config.options) ? config.options : [];
7470
7628
  const allowMultiple =
7471
- config.allow_multiple === true || config.multiple === true;
7629
+ config.allow_multiple === true ||
7630
+ config.multiple === true ||
7631
+ config.allow_multiple_selection === true;
7472
7632
  const pageAnswer = this.surveyState.pageAnswers[pageId] || {};
7473
7633
  const selectedValues = Array.isArray(pageAnswer.values)
7474
7634
  ? pageAnswer.values
@@ -7495,10 +7655,36 @@
7495
7655
  const pageId = page.id || `page_${this.surveyState.currentPageIndex}`;
7496
7656
  const pageAnswer = this.surveyState.pageAnswers[pageId] || {};
7497
7657
  const value = pageAnswer.text || '';
7658
+ const placeholder = page.placeholder || 'Type your answer...';
7498
7659
 
7499
7660
  return `
7500
7661
  <div class="feedback-survey-text-page" data-page-id="${pageId}">
7501
- <textarea class="feedback-survey-page-textarea" data-page-id="${pageId}" placeholder="Type your answer...">${value}</textarea>
7662
+ <textarea class="feedback-survey-page-textarea" data-page-id="${pageId}" placeholder="${placeholder}">${value}</textarea>
7663
+ </div>
7664
+ `;
7665
+ }
7666
+
7667
+ _renderLinkPage(page) {
7668
+ const pageId = page.id || `page_${this.surveyState.currentPageIndex}`;
7669
+ const config = page.link_config || page.linkConfig || {};
7670
+ const buttonText = config.button_text || config.buttonText || 'Click here';
7671
+ const linkText = config.link_text || config.linkText || '';
7672
+ const redirectUrl = config.redirect_url || config.redirectUrl || '#';
7673
+ const openIn = config.open_in || config.openIn || 'new_tab';
7674
+ const target = openIn === 'new_tab' ? '_blank' : '_self';
7675
+ const pageAnswer = this.surveyState.pageAnswers[pageId] || {};
7676
+ const clicked = pageAnswer.clicked === true;
7677
+
7678
+ return `
7679
+ <div class="feedback-survey-link-page" data-page-id="${pageId}">
7680
+ ${linkText ? `<p class="feedback-survey-link-text">${linkText}</p>` : ''}
7681
+ <a class="feedback-survey-link-btn${clicked ? ' selected' : ''}"
7682
+ href="${redirectUrl}"
7683
+ target="${target}"
7684
+ rel="noopener noreferrer"
7685
+ data-page-id="${pageId}">
7686
+ ${buttonText}
7687
+ </a>
7502
7688
  </div>
7503
7689
  `;
7504
7690
  }
@@ -7747,6 +7933,24 @@
7747
7933
  });
7748
7934
  }
7749
7935
  }
7936
+
7937
+ if (page.type === 'link') {
7938
+ this.surveyElement
7939
+ .querySelectorAll('.feedback-survey-link-btn')
7940
+ .forEach((btn) => {
7941
+ btn.addEventListener('click', () => {
7942
+ const pId = btn.dataset.pageId;
7943
+ this._setPageAnswer(pId, { clicked: true });
7944
+ btn.classList.add('selected');
7945
+
7946
+ const navigation = page.afterThisPage || page.after_this_page;
7947
+ const goesTo = navigation ? navigation.default : null;
7948
+ if (!goesTo || goesTo === 'end_survey') {
7949
+ setTimeout(() => this._handleSubmit(), 400);
7950
+ }
7951
+ });
7952
+ });
7953
+ }
7750
7954
  }
7751
7955
 
7752
7956
  _selectNPS(score) {
@@ -7938,6 +8142,13 @@
7938
8142
  }
7939
8143
  }
7940
8144
 
8145
+ if (page.type === 'link') {
8146
+ if (page.required && !answer.clicked) {
8147
+ this._showError('Please click the link to continue');
8148
+ return false;
8149
+ }
8150
+ }
8151
+
7941
8152
  return true;
7942
8153
  }
7943
8154
 
@@ -8667,6 +8878,7 @@
8667
8878
  type: page.type || 'rating',
8668
8879
  title: page.title || '',
8669
8880
  description: page.description || '',
8881
+ placeholder: page.placeholder || '',
8670
8882
  required: page.required !== false,
8671
8883
  position: page.position ?? index,
8672
8884
  ratingConfig: page.ratingConfig || page.rating_config || null,