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 {