diff --git a/src/components/Sidebar/index.jsx b/src/components/Sidebar/index.jsx
index b7f924c..1dfec4b 100644
--- a/src/components/Sidebar/index.jsx
+++ b/src/components/Sidebar/index.jsx
@@ -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,
@@ -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();
@@ -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();
+ }
}
};
diff --git a/src/components/ToggleXpertButton/index.jsx b/src/components/ToggleXpertButton/index.jsx
index f14f42e..840de79 100644
--- a/src/components/ToggleXpertButton/index.jsx
+++ b/src/components/ToggleXpertButton/index.jsx
@@ -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);
diff --git a/src/data/thunks.js b/src/data/thunks.js
index ef10379..a8c625e 100644
--- a/src/data/thunks.js
+++ b/src/data/thunks.js
@@ -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.
diff --git a/src/utils/surveyMonkey.js b/src/utils/surveyMonkey.js
new file mode 100644
index 0000000..f1b03b5
--- /dev/null
+++ b/src/utils/surveyMonkey.js
@@ -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,
+};
diff --git a/src/widgets/Xpert.test.jsx b/src/widgets/Xpert.test.jsx
index 437c6a4..61690a5 100644
--- a/src/widgets/Xpert.test.jsx
+++ b/src/widgets/Xpert.test.jsx
@@ -1,11 +1,12 @@
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');
@@ -13,6 +14,35 @@ 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 ();
+ }
+ ),
+ ),
+ };
+ },
+ { 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: '',
@@ -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';
@@ -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(, { 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!';
@@ -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(, { preloadedState: errorState });
@@ -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(, { preloadedState: errorState });
@@ -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(, { preloadedState: errorState });
@@ -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(, { 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(, { 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();
+});