From 30f0d49a891768b8c89635165eec96c2eb9df7e8 Mon Sep 17 00:00:00 2001
From: Lucas Faria <12522524+lucasheriques@users.noreply.github.com>
Date: Tue, 7 Jan 2025 14:56:03 -0300
Subject: [PATCH] feat: Add a onPreviewSubmit callback to preview surveys
(#1641)
---
src/__tests__/extensions/surveys.test.ts | 100 +++++++++++++++++-
src/extensions/surveys.tsx | 75 ++++++++-----
.../surveys/components/BottomSection.tsx | 11 +-
.../surveys/components/QuestionTypes.tsx | 53 ++++++----
src/extensions/surveys/surveys-utils.tsx | 16 +--
5 files changed, 192 insertions(+), 63 deletions(-)
diff --git a/src/__tests__/extensions/surveys.test.ts b/src/__tests__/extensions/surveys.test.ts
index 3742ade71..dabefd11a 100644
--- a/src/__tests__/extensions/surveys.test.ts
+++ b/src/__tests__/extensions/surveys.test.ts
@@ -1,17 +1,19 @@
+import { act, fireEvent, render, renderHook } from '@testing-library/preact'
import {
generateSurveys,
- renderSurveysPreview,
renderFeedbackWidgetPreview,
- usePopupVisibility,
+ renderSurveysPreview,
SurveyManager,
+ usePopupVisibility,
} from '../../extensions/surveys'
import { createShadow } from '../../extensions/surveys/surveys-utils'
import { Survey, SurveyQuestionType, SurveyType } from '../../posthog-surveys-types'
-import { renderHook, act } from '@testing-library/preact'
+import { beforeEach } from '@jest/globals'
import '@testing-library/jest-dom'
+import { h } from 'preact'
+import { useEffect, useRef, useState } from 'preact/hooks'
import { PostHog } from '../../posthog-core'
-import { beforeEach } from '@jest/globals'
import { DecideResponse } from '../../types'
declare const global: any
@@ -59,6 +61,7 @@ describe('survey display logic', () => {
end_date: null,
current_iteration: null,
current_iteration_start_date: null,
+ feature_flag_keys: [],
},
]
@@ -301,6 +304,7 @@ describe('SurveyManager', () => {
end_date: null,
current_iteration: null,
current_iteration_start_date: null,
+ feature_flag_keys: [],
},
]
})
@@ -408,6 +412,7 @@ describe('SurveyManager', () => {
end_date: null,
current_iteration: null,
current_iteration_start_date: null,
+ feature_flag_keys: [],
}
document.body.innerHTML = '
'
const handleWidgetSelectorMock = jest
@@ -794,4 +799,91 @@ describe('preview renders', () => {
expect(root.getElementsByClassName('ph-survey-widget-tab').length).toBe(1)
expect(root.getElementsByClassName('ph-survey-widget-tab')[0].innerHTML).toContain('preview test')
})
+
+ test('renderSurveysPreview navigates between questions when submitting answers in preview', async () => {
+ function TestSurveyPreview() {
+ const surveyPreviewRef = useRef(null)
+ const [currentPageIndex, setCurrentPageIndex] = useState(0)
+
+ const survey = {
+ id: 'test-survey',
+ name: 'Test Survey',
+ description: 'Test Survey Description',
+ type: SurveyType.Popover,
+ questions: [
+ {
+ type: SurveyQuestionType.Open,
+ question: 'Question 1',
+ description: 'Description 1',
+ descriptionContentType: 'text',
+ originalQuestionIndex: 0,
+ },
+ {
+ type: SurveyQuestionType.Open,
+ question: 'Question 2',
+ description: 'Description 2',
+ descriptionContentType: 'text',
+ originalQuestionIndex: 1,
+ },
+ ],
+ appearance: {
+ backgroundColor: '#ffffff',
+ submitButtonText: 'Next',
+ },
+ start_date: '2024-01-01T00:00:00.000Z',
+ end_date: null,
+ targeting_flag_key: null,
+ linked_flag_key: null,
+ conditions: {},
+ feature_flag_keys: null, // Added this to fix type error
+ } as Survey
+
+ useEffect(() => {
+ console.log('Render effect triggered with page index:', currentPageIndex)
+ if (surveyPreviewRef.current) {
+ renderSurveysPreview({
+ survey,
+ parentElement: surveyPreviewRef.current,
+ previewPageIndex: currentPageIndex,
+ onPreviewSubmit: () => {
+ setCurrentPageIndex((prev) => {
+ console.log('Setting page index from', prev, 'to', prev + 1)
+ return prev + 1
+ })
+ },
+ })
+ }
+ }, [currentPageIndex])
+
+ return h('div', { ref: surveyPreviewRef })
+ }
+
+ // Render the test component
+ const { container } = render(h(TestSurveyPreview, {}))
+
+ // Check if we're on the first question
+ expect(container.textContent).toContain('Question 1')
+ expect(container.textContent).not.toContain('Question 2')
+
+ // Find and fill the textarea
+ const textarea = container.querySelector('textarea')
+ console.log('Found textarea:', !!textarea)
+
+ await act(async () => {
+ fireEvent.change(textarea!, { target: { value: 'Test answer' } })
+ })
+
+ // Find and click the submit button (using button type="button" instead of form-submit class)
+ const submitButton = container.querySelector('button[type="button"]')
+ console.log('Found submit button:', !!submitButton)
+ console.log('Submit button text:', submitButton?.textContent)
+
+ await act(async () => {
+ fireEvent.click(submitButton!)
+ })
+
+ // Check if we're on the second question
+ expect(container.textContent).toContain('Question 2')
+ expect(container.textContent).not.toContain('Question 1')
+ })
})
diff --git a/src/extensions/surveys.tsx b/src/extensions/surveys.tsx
index b1fec5638..402da1bb4 100644
--- a/src/extensions/surveys.tsx
+++ b/src/extensions/surveys.tsx
@@ -9,31 +9,31 @@ import {
SurveyType,
} from '../posthog-surveys-types'
-import { window as _window, document as _document } from '../utils/globals'
-import {
- style,
- defaultSurveyAppearance,
- sendSurveyEvent,
- dismissedSurveyEvent,
- createShadow,
- getContrastingTextColor,
- SurveyContext,
- getDisplayOrderQuestions,
- getSurveySeen,
-} from './surveys/surveys-utils'
import * as Preact from 'preact'
-import { createWidgetShadow, createWidgetStyle } from './surveys-widget'
-import { useState, useEffect, useRef, useContext, useMemo } from 'preact/hooks'
+import { useContext, useEffect, useMemo, useRef, useState } from 'preact/hooks'
+import { document as _document, window as _window } from '../utils/globals'
+import { createLogger } from '../utils/logger'
import { isNull, isNumber } from '../utils/type-utils'
+import { createWidgetShadow, createWidgetStyle } from './surveys-widget'
import { ConfirmationMessage } from './surveys/components/ConfirmationMessage'
+import { Cancel } from './surveys/components/QuestionHeader'
import {
- OpenTextQuestion,
LinkQuestion,
- RatingQuestion,
MultipleChoiceQuestion,
+ OpenTextQuestion,
+ RatingQuestion,
} from './surveys/components/QuestionTypes'
-import { Cancel } from './surveys/components/QuestionHeader'
-import { createLogger } from '../utils/logger'
+import {
+ createShadow,
+ defaultSurveyAppearance,
+ dismissedSurveyEvent,
+ getContrastingTextColor,
+ getDisplayOrderQuestions,
+ getSurveySeen,
+ sendSurveyEvent,
+ style,
+ SurveyContext,
+} from './surveys/surveys-utils'
const logger = createLogger('[Surveys]')
// We cast the types here which is dangerous but protected by the top level generateSurveys call
@@ -277,11 +277,13 @@ export const renderSurveysPreview = ({
parentElement,
previewPageIndex,
forceDisableHtml,
+ onPreviewSubmit,
}: {
survey: Survey
parentElement: HTMLElement
previewPageIndex: number
forceDisableHtml?: boolean
+ onPreviewSubmit?: (res: string | string[] | number | null) => void
}) => {
const surveyStyleSheet = style(survey.appearance)
const styleElement = Object.assign(document.createElement('style'), { innerText: surveyStyleSheet })
@@ -310,6 +312,7 @@ export const renderSurveysPreview = ({
borderRadius: 10,
color: textColor,
}}
+ onPreviewSubmit={onPreviewSubmit}
previewPageIndex={previewPageIndex}
removeSurveyFromFocus={() => {}}
isPopup={true}
@@ -440,6 +443,17 @@ export function usePopupVisibility(
return { isPopupVisible, isSurveySent, setIsPopupVisible }
}
+interface SurveyPopupProps {
+ survey: Survey
+ forceDisableHtml?: boolean
+ posthog?: PostHog
+ style?: React.CSSProperties
+ previewPageIndex?: number | undefined
+ removeSurveyFromFocus: (id: string) => void
+ isPopup?: boolean
+ onPreviewSubmit?: (res: string | string[] | number | null) => void
+}
+
export function SurveyPopup({
survey,
forceDisableHtml,
@@ -448,15 +462,8 @@ export function SurveyPopup({
previewPageIndex,
removeSurveyFromFocus,
isPopup,
-}: {
- survey: Survey
- forceDisableHtml?: boolean
- posthog?: PostHog
- style?: React.CSSProperties
- previewPageIndex?: number | undefined
- removeSurveyFromFocus: (id: string) => void
- isPopup?: boolean
-}) {
+ onPreviewSubmit = () => {},
+}: SurveyPopupProps) {
const isPreviewMode = Number.isInteger(previewPageIndex)
// NB: The client-side code passes the millisecondDelay in seconds, but setTimeout expects milliseconds, so we multiply by 1000
const surveyPopupDelayMilliseconds = survey.appearance?.surveyPopupDelaySeconds
@@ -486,6 +493,7 @@ export function SurveyPopup({
previewPageIndex: previewPageIndex,
handleCloseSurveyPopup: () => dismissedSurveyEvent(survey, posthog, isPreviewMode),
isPopup: isPopup || false,
+ onPreviewSubmit,
}}
>
{!shouldShowConfirmation ? (
@@ -527,7 +535,8 @@ export function Questions({
survey.appearance?.backgroundColor || defaultSurveyAppearance.backgroundColor
)
const [questionsResponses, setQuestionsResponses] = useState({})
- const { isPreviewMode, previewPageIndex, handleCloseSurveyPopup, isPopup } = useContext(SurveyContext)
+ const { isPreviewMode, previewPageIndex, handleCloseSurveyPopup, isPopup, onPreviewSubmit } =
+ useContext(SurveyContext)
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(previewPageIndex || 0)
const surveyQuestions = useMemo(() => getDisplayOrderQuestions(survey), [survey])
@@ -618,6 +627,7 @@ export function Questions({
originalQuestionIndex,
displayQuestionIndex,
}),
+ onPreviewSubmit,
})}
)
@@ -705,6 +715,7 @@ interface GetQuestionComponentProps {
displayQuestionIndex: number
appearance: SurveyAppearance
onSubmit: (res: string | string[] | number | null) => void
+ onPreviewSubmit: (res: string | string[] | number | null) => void
}
const getQuestionComponent = ({
@@ -713,6 +724,7 @@ const getQuestionComponent = ({
displayQuestionIndex,
appearance,
onSubmit,
+ onPreviewSubmit,
}: GetQuestionComponentProps): JSX.Element => {
const questionComponents = {
[SurveyQuestionType.Open]: OpenTextQuestion,
@@ -726,7 +738,12 @@ const getQuestionComponent = ({
question,
forceDisableHtml,
appearance,
- onSubmit,
+ onPreviewSubmit: (res: string | string[] | number | null) => {
+ onPreviewSubmit(res)
+ },
+ onSubmit: (res: string | string[] | number | null) => {
+ onSubmit(res)
+ },
}
const additionalProps: Record = {
diff --git a/src/extensions/surveys/components/BottomSection.tsx b/src/extensions/surveys/components/BottomSection.tsx
index f23f7e563..b75d5e650 100644
--- a/src/extensions/surveys/components/BottomSection.tsx
+++ b/src/extensions/surveys/components/BottomSection.tsx
@@ -2,9 +2,9 @@ import { window } from '../../../utils/globals'
import { SurveyAppearance } from '../../../posthog-surveys-types'
-import { PostHogLogo } from './PostHogLogo'
import { useContext } from 'preact/hooks'
import { SurveyContext, defaultSurveyAppearance, getContrastingTextColor } from '../surveys-utils'
+import { PostHogLogo } from './PostHogLogo'
export function BottomSection({
text,
@@ -12,12 +12,14 @@ export function BottomSection({
appearance,
onSubmit,
link,
+ onPreviewSubmit,
}: {
text: string
submitDisabled: boolean
appearance: SurveyAppearance
onSubmit: () => void
link?: string | null
+ onPreviewSubmit?: () => void
}) {
const { isPreviewMode, isPopup } = useContext(SurveyContext)
const textColor =
@@ -28,11 +30,14 @@ export function BottomSection({
)
@@ -61,11 +67,9 @@ export function LinkQuestion({
forceDisableHtml,
appearance,
onSubmit,
-}: {
+ onPreviewSubmit,
+}: CommonProps & {
question: LinkSurveyQuestion
- forceDisableHtml: boolean
- appearance: SurveyAppearance
- onSubmit: (clicked: string) => void
}) {
return (
<>
@@ -81,6 +85,7 @@ export function LinkQuestion({
link={question.link}
appearance={appearance}
onSubmit={() => onSubmit('link clicked')}
+ onPreviewSubmit={() => onPreviewSubmit('link clicked')}
/>
>
)
@@ -92,12 +97,10 @@ export function RatingQuestion({
displayQuestionIndex,
appearance,
onSubmit,
-}: {
+ onPreviewSubmit,
+}: CommonProps & {
question: RatingSurveyQuestion
- forceDisableHtml: boolean
displayQuestionIndex: number
- appearance: SurveyAppearance
- onSubmit: (rating: number | null) => void
}) {
const scale = question.scale
const starting = question.scale === 10 ? 0 : 1
@@ -175,6 +178,7 @@ export function RatingQuestion({
submitDisabled={isNull(rating) && !question.optional}
appearance={appearance}
onSubmit={() => onSubmit(rating)}
+ onPreviewSubmit={() => onPreviewSubmit(rating)}
/>
>
)
@@ -221,12 +225,10 @@ export function MultipleChoiceQuestion({
displayQuestionIndex,
appearance,
onSubmit,
-}: {
+ onPreviewSubmit,
+}: CommonProps & {
question: MultipleSurveyQuestion
- forceDisableHtml: boolean
displayQuestionIndex: number
- appearance: SurveyAppearance
- onSubmit: (choices: string | string[] | null) => void
}) {
const textRef = useRef(null)
const choices = useMemo(() => getDisplayOrderChoices(question), [question])
@@ -344,6 +346,15 @@ export function MultipleChoiceQuestion({
onSubmit(selectedChoices)
}
}}
+ onPreviewSubmit={() => {
+ if (openChoiceSelected && question.type === SurveyQuestionType.MultipleChoice) {
+ if (isArray(selectedChoices)) {
+ onPreviewSubmit([...selectedChoices, openEndedInput])
+ }
+ } else {
+ onPreviewSubmit(selectedChoices)
+ }
+ }}
/>
)
diff --git a/src/extensions/surveys/surveys-utils.tsx b/src/extensions/surveys/surveys-utils.tsx
index 230651ab1..d4f992678 100644
--- a/src/extensions/surveys/surveys-utils.tsx
+++ b/src/extensions/surveys/surveys-utils.tsx
@@ -1,7 +1,7 @@
-import { PostHog } from '../../posthog-core'
-import { Survey, SurveyAppearance, MultipleSurveyQuestion, SurveyQuestion } from '../../posthog-surveys-types'
-import { window as _window, document as _document } from '../../utils/globals'
import { VNode, cloneElement, createContext } from 'preact'
+import { PostHog } from '../../posthog-core'
+import { MultipleSurveyQuestion, Survey, SurveyAppearance, SurveyQuestion } from '../../posthog-surveys-types'
+import { document as _document, window as _window } from '../../utils/globals'
// We cast the types here which is dangerous but protected by the top level generateSurveys call
const window = _window as Window & typeof globalThis
const document = _document as Document
@@ -36,7 +36,7 @@ export const style = (appearance: SurveyAppearance | null) => {
border-top-right-radius: 10px;
box-shadow: -6px 0 16px -8px rgb(0 0 0 / 8%), -9px 0 28px 0 rgb(0 0 0 / 5%), -12px 0 48px 16px rgb(0 0 0 / 3%);
}
-
+
.survey-box, .thank-you-message-container {
padding: 20px 25px 10px;
display: flex;
@@ -677,16 +677,20 @@ const getSurveyInteractionProperty = (survey: Survey, action: string): string =>
return surveyProperty
}
-export const SurveyContext = createContext<{
+interface SurveyContextProps {
isPreviewMode: boolean
previewPageIndex: number | undefined
handleCloseSurveyPopup: () => void
isPopup: boolean
-}>({
+ onPreviewSubmit: (res: string | string[] | number | null) => void
+}
+
+export const SurveyContext = createContext({
isPreviewMode: false,
previewPageIndex: 0,
handleCloseSurveyPopup: () => {},
isPopup: true,
+ onPreviewSubmit: () => {},
})
interface RenderProps {