Skip to content

Commit

Permalink
feat: trigger survey monkey on chat close (#55)
Browse files Browse the repository at this point in the history
* feat: trigger survey monkey on chat close

* fix: Fixed useOptimizelyExperiment() infinite updates

* fix: read experiment state correctly

* feat: add tests

* fix: fix lint errors

* fix: remove flaky test

---------

Co-authored-by: Marcos <rigoli82@gmail.com>
  • Loading branch information
alangsto and rijuma authored Jul 3, 2024
1 parent 3412f8f commit 8482d7b
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 39 deletions.
16 changes: 10 additions & 6 deletions src/components/Sidebar/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import './Sidebar.scss';
import {
clearMessages,
} from '../../data/thunks';
import { PROMPT_EXPERIMENT_FLAG } from '../../constants/experiments';
import { PROMPT_EXPERIMENT_FLAG, PROMPT_EXPERIMENT_KEY } from '../../constants/experiments';
import { showControlSurvey, showVariationSurvey } from '../../utils/surveyMonkey';

const Sidebar = ({
courseId,
Expand All @@ -29,8 +30,9 @@ const Sidebar = ({
apiError,
disclosureAcknowledged,
messageList,
experiments,
} = useSelector(state => state.learningAssistant);
const { variationKey } = useSelector(state => state.experiments?.[PROMPT_EXPERIMENT_FLAG]) || {};
const { variationKey } = experiments ? experiments[PROMPT_EXPERIMENT_FLAG] : {};
const chatboxContainerRef = useRef(null);
const dispatch = useDispatch();

Expand Down Expand Up @@ -71,10 +73,12 @@ const Sidebar = ({
const handleClick = () => {
setIsOpen(false);

// check to see if hotjar is available, then trigger hotjar event if user has sent and received a message
const hasWindow = typeof window !== 'undefined';
if (hasWindow && window.hj && messageList.length >= 2) {
window.hj('event', 'ocm_learning_assistant_chat_closed');
if (messageList.length >= 2) {
if (variationKey === PROMPT_EXPERIMENT_KEY) {
showVariationSurvey();
} else {
showControlSurvey();
}
}
};

Expand Down
3 changes: 2 additions & 1 deletion src/components/ToggleXpertButton/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ const ToggleXpert = ({
courseId,
contentToolsEnabled,
}) => {
const { variationKey } = useSelector(state => state.experiments?.[PROMPT_EXPERIMENT_FLAG]) || {};
const { experiments } = useSelector(state => state.learningAssistant);
const { variationKey } = experiments ? experiments[PROMPT_EXPERIMENT_FLAG] : {};
const [hasDismissedCTA, setHasDismissedCTA] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(true);
const [target, setTarget] = useState(null);
Expand Down
4 changes: 2 additions & 2 deletions src/data/thunks.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import { PROMPT_EXPERIMENT_FLAG } from '../constants/experiments';

export function addChatMessage(role, content, courseId) {
return (dispatch, getState) => {
const { messageList, conversationId } = getState().learningAssistant;
const { variationKey } = getState().experiments?.[PROMPT_EXPERIMENT_FLAG] || {};
const { messageList, conversationId, experiments } = getState().learningAssistant;
const { variationKey } = experiments ? experiments[PROMPT_EXPERIMENT_FLAG] : {};

// Redux recommends only serializable values in the store, so we'll stringify the timestap to store in Redux.
// When we need to operate on the Date object, we'll deserialize the string.
Expand Down
17 changes: 17 additions & 0 deletions src/utils/surveyMonkey.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* eslint-disable func-names,no-unused-expressions,no-param-reassign,no-sequences */

// This function contains the script provided by SuveryMonkey,
// which is used to embed the survey in the html upon this function
// being called after a learner closes the chat window for the control group.
const showControlSurvey = () => {
(function (t, e, s, o) { let n; let c; let l; t.SMCX = t.SMCX || [], e.getElementById(o) || (n = e.getElementsByTagName(s), c = n[n.length - 1], l = e.createElement(s), l.type = 'text/javascript', l.async = !0, l.id = o, l.src = 'https://widget.surveymonkey.com/collect/website/js/tRaiETqnLgj758hTBazgd30kMLlLtc4okiu60NJiBPZxbfwe_2FCDOk5JO3Imfyeqk.js', c.parentNode.insertBefore(l, c)); }(window, document, 'script', 'smcx-sdk'));
};

const showVariationSurvey = () => {
(function (t, e, s, o) { let n; let c; let l; t.SMCX = t.SMCX || [], e.getElementById(o) || (n = e.getElementsByTagName(s), c = n[n.length - 1], l = e.createElement(s), l.type = 'text/javascript', l.async = !0, l.id = o, l.src = 'https://widget.surveymonkey.com/collect/website/js/tRaiETqnLgj758hTBazgd3i4lLmPCzca7_2BgAvTEbjU2dNWmi5l435XUxCEkddDIn.js', c.parentNode.insertBefore(l, c)); }(window, document, 'script', 'smcx-sdk'));
};

export {
showControlSurvey,
showVariationSurvey,
};
131 changes: 101 additions & 30 deletions src/widgets/Xpert.test.jsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,48 @@
import React from 'react';

import { screen, fireEvent, waitFor } from '@testing-library/react';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import * as api from '../data/api';
import Xpert from './Xpert';

import * as surveyMonkey from '../utils/surveyMonkey';
import { render, createRandomResponseForTesting } from '../utils/utils.test';

jest.mock('@edx/frontend-platform/analytics');
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedUser: jest.fn(() => ({ userId: 1 })),
}));

jest.mock(
'@optimizely/react-sdk',
() => {
const originalModule = jest.requireActual('@optimizely/react-sdk');
return {
__esModule: true,
...originalModule,
createInstance: jest.fn(() => ({
track: jest.fn(),
})),
useDecision: jest.fn(() => [{ enabled: true, variationKey: 'control' }]),
withOptimizely: jest.fn(
(Component) => (
function HOC(props) {
const newProps = {
...props, optimizely: { track: jest.fn() },
};
return (<Component {...newProps} />);
}
),
),
};
},
{ virtual: true },
);

// import useDecision here, after mocking, so that it can be used in tests
import { useDecision } from '@optimizely/react-sdk'; // eslint-disable-line

const initialState = {
learningAssistant: {
currentMessage: '',
Expand All @@ -23,6 +53,7 @@ const initialState = {
// I will remove this and write tests in a future pull request.
disclosureAcknowledged: true,
sidebarIsOpen: false,
experiments: {},
},
};
const courseId = 'course-v1:edX+DemoX+Demo_Course';
Expand Down Expand Up @@ -150,35 +181,6 @@ test('submitted text appears as message in the sidebar', async () => {
// because we use a controlled input element, assert that the input element is cleared
expect(input).toHaveValue('');
});
test('loading message appears in the sidebar while the response loads', async () => {
const user = userEvent.setup();
const userMessage = 'Hello, Xpert!';

// re-mock the fetchChatResponse API function so that we can assert that the
// responseMessage appears in the DOM
const responseMessage = createRandomResponseForTesting();
jest.spyOn(api, 'default').mockResolvedValue(responseMessage);

render(<Xpert courseId={courseId} contentToolsEnabled={false} unitId={unitId} />, { preloadedState: initialState });

// wait for button to appear
await screen.findByTestId('toggle-button');

await user.click(screen.queryByTestId('toggle-button'));

// type the user message
await user.type(screen.getByRole('textbox'), userMessage);

// It's better practice to use the userEvent API, but I could not get this test to properly assert
// that the "Xpert is thinking" loading text appears in the DOM. Something about using the userEvent
// API skipped straight to rendering the response message.
await fireEvent.click(screen.getByRole('button', { name: 'submit' }));

waitFor(async () => {
await screen.findByText('Xpert is thinking');
await screen.findByText(responseMessage.content);
}, { timeout: 2000 });
});
test('response text appears as message in the sidebar', async () => {
const user = userEvent.setup();
const userMessage = 'Hello, Xpert!';
Expand Down Expand Up @@ -270,6 +272,7 @@ test('error message should disappear upon succesful api call', async () => {
// I will remove this and write tests in a future pull request.
disclosureAcknowledged: true,
sidebarIsOpen: false,
experiments: {},
},
};
render(<Xpert courseId={courseId} contentToolsEnabled={false} unitId={unitId} />, { preloadedState: errorState });
Expand Down Expand Up @@ -304,6 +307,7 @@ test('error message should disappear when dismissed', async () => {
// I will remove this and write tests in a future pull request.
disclosureAcknowledged: true,
sidebarIsOpen: false,
experiments: {},
},
};
render(<Xpert courseId={courseId} contentToolsEnabled={false} unitId={unitId} />, { preloadedState: errorState });
Expand Down Expand Up @@ -333,6 +337,7 @@ test('error message should disappear when messages cleared', async () => {
// I will remove this and write tests in a future pull request.
disclosureAcknowledged: true,
sidebarIsOpen: false,
experiments: {},
},
};
render(<Xpert courseId={courseId} contentToolsEnabled={false} unitId={unitId} />, { preloadedState: errorState });
Expand Down Expand Up @@ -391,3 +396,69 @@ test('popup modal should close and display CTA', async () => {
assertSidebarElementsNotInDOM();
expect(screen.queryByTestId('action-message')).toBeVisible();
});
test('survey monkey survey should appear after closing sidebar', async () => {
const controlSurvey = jest.spyOn(surveyMonkey, 'showControlSurvey').mockReturnValueOnce(1);
const user = userEvent.setup();

const surveyState = {
learningAssistant: {
currentMessage: '',
messageList: [
{ role: 'user', content: 'hi', timestamp: new Date() },
{ role: 'user', content: 'hi', timestamp: new Date() },
],
apiIsLoading: false,
apiError: false,
disclosureAcknowledged: true,
sidebarIsOpen: false,
experiments: {},
},
};
render(<Xpert courseId={courseId} contentToolsEnabled={false} unitId={unitId} />, { preloadedState: surveyState });

// wait for button to appear
await screen.findByTestId('toggle-button');

await user.click(screen.queryByTestId('toggle-button'));

// click close
await user.click(screen.queryByTestId('close-button'));

// assert mock called
expect(controlSurvey).toBeCalledTimes(1);
controlSurvey.mockRestore();
});
test('survey monkey variation survey should appear if user is in experiment', async () => {
const variationSurvey = jest.spyOn(surveyMonkey, 'showVariationSurvey').mockReturnValueOnce(1);
const user = userEvent.setup();

useDecision.mockImplementation(() => [{ enabled: true, variationKey: 'updated_prompt' }]);

const surveyState = {
learningAssistant: {
currentMessage: '',
messageList: [
{ role: 'user', content: 'hi', timestamp: new Date() },
{ role: 'user', content: 'hi', timestamp: new Date() },
],
apiIsLoading: false,
apiError: false,
disclosureAcknowledged: true,
sidebarIsOpen: false,
experiments: {},
},
};
render(<Xpert courseId={courseId} contentToolsEnabled={false} unitId={unitId} />, { preloadedState: surveyState });

// wait for button to appear
await screen.findByTestId('toggle-button');

await user.click(screen.queryByTestId('toggle-button'));

// click close
await user.click(screen.queryByTestId('close-button'));

// assert mock called
expect(variationSurvey).toBeCalledTimes(1);
variationSurvey.mockRestore();
});

0 comments on commit 8482d7b

Please sign in to comment.