diff --git a/src/components/MessageForm/index.jsx b/src/components/MessageForm/index.jsx index 566f5113..8d8d92e0 100644 --- a/src/components/MessageForm/index.jsx +++ b/src/components/MessageForm/index.jsx @@ -1,26 +1,23 @@ import PropTypes from 'prop-types'; import React, { useEffect, useRef } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { useDecision } from '@optimizely/react-sdk'; import { Button, Form, Icon } from '@openedx/paragon'; import { Send } from '@openedx/paragon/icons'; -import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; -import { OPTIMIZELY_PROMPT_EXPERIMENT_KEY } from '../../data/optimizely'; import { acknowledgeDisclosure, addChatMessage, getChatResponse, updateCurrentMessage, } from '../../data/thunks'; +import { usePromptExperimentDecision } from '../../experiments'; const MessageForm = ({ courseId, shouldAutofocus, unitId }) => { const { apiIsLoading, currentMessage, apiError } = useSelector(state => state.learningAssistant); const dispatch = useDispatch(); const inputRef = useRef(); - const { userId } = getAuthenticatedUser(); - const [decision] = useDecision(OPTIMIZELY_PROMPT_EXPERIMENT_KEY, { autoUpdate: true }, { id: userId.toString() }); + const [decision] = usePromptExperimentDecision(); const { enabled, variationKey } = decision || {}; const promptExperimentVariationKey = enabled ? variationKey : undefined; diff --git a/src/components/MessageForm/index.test.jsx b/src/components/MessageForm/index.test.jsx index 0e9a83f2..6c8dba2b 100644 --- a/src/components/MessageForm/index.test.jsx +++ b/src/components/MessageForm/index.test.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { screen, act, fireEvent, waitFor, } from '@testing-library/react'; -import { useDecision } from '@optimizely/react-sdk'; +import { usePromptExperimentDecision } from '../../experiments'; import { render as renderComponent } from '../../utils/utils.test'; import { initialState } from '../../data/slice'; import { OPTIMIZELY_PROMPT_EXPERIMENT_VARIATION_KEYS } from '../../data/optimizely'; @@ -39,8 +39,8 @@ jest.mock('react-redux', () => ({ useDispatch: () => mockDispatch, })); -jest.mock('@optimizely/react-sdk', () => ({ - useDecision: jest.fn(), +jest.mock('../../experiments', () => ({ + usePromptExperimentDecision: jest.fn(), })); jest.mock('../../data/thunks', () => ({ @@ -80,7 +80,7 @@ const render = async (props = {}, sliceState = {}) => { describe('', () => { beforeEach(() => { jest.resetAllMocks(); - useDecision.mockReturnValue([]); + usePromptExperimentDecision.mockReturnValue([]); }); describe('when rendered', () => { @@ -157,7 +157,7 @@ describe('', () => { describe('prmpt experiment', () => { beforeEach(() => { - useDecision.mockReturnValue([{ + usePromptExperimentDecision.mockReturnValue([{ enabled: true, variationKey: OPTIMIZELY_PROMPT_EXPERIMENT_VARIATION_KEYS.UPDATED_PROMPT, }]); diff --git a/src/components/Sidebar/index.jsx b/src/components/Sidebar/index.jsx index b3358176..8f925154 100644 --- a/src/components/Sidebar/index.jsx +++ b/src/components/Sidebar/index.jsx @@ -1,9 +1,7 @@ import React, { useRef, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import PropTypes from 'prop-types'; -import { useDecision } from '@optimizely/react-sdk'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; -import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { Button, Icon, @@ -20,6 +18,7 @@ import ChatBox from '../ChatBox'; import Disclosure from '../Disclosure'; import MessageForm from '../MessageForm'; import './Sidebar.scss'; +import { usePromptExperimentDecision } from '../../experiments'; const Sidebar = ({ courseId, @@ -35,8 +34,7 @@ const Sidebar = ({ const chatboxContainerRef = useRef(null); const dispatch = useDispatch(); - const { userId } = getAuthenticatedUser(); - const [decision] = useDecision(OPTIMIZELY_PROMPT_EXPERIMENT_KEY, { autoUpdate: true }, { id: userId.toString() }); + const [decision] = usePromptExperimentDecision(); const { enabled: enabledExperiment, variationKey } = decision || {}; const experimentPayload = enabledExperiment ? { experiment_name: OPTIMIZELY_PROMPT_EXPERIMENT_KEY, diff --git a/src/components/Sidebar/index.test.jsx b/src/components/Sidebar/index.test.jsx index db56b160..1f02709c 100644 --- a/src/components/Sidebar/index.test.jsx +++ b/src/components/Sidebar/index.test.jsx @@ -1,7 +1,8 @@ import React from 'react'; import { screen, act } from '@testing-library/react'; -import { useDecision } from '@optimizely/react-sdk'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; + +import { usePromptExperimentDecision } from '../../experiments'; import { render as renderComponent } from '../../utils/utils.test'; import { initialState } from '../../data/slice'; import { OPTIMIZELY_PROMPT_EXPERIMENT_KEY, OPTIMIZELY_PROMPT_EXPERIMENT_VARIATION_KEYS } from '../../data/optimizely'; @@ -33,8 +34,8 @@ jest.mock('react-redux', () => ({ useDispatch: () => mockDispatch, })); -jest.mock('@optimizely/react-sdk', () => ({ - useDecision: jest.fn(), +jest.mock('../../experiments', () => ({ + usePromptExperimentDecision: jest.fn(), })); const clearMessagesAction = 'clear-messages-action'; @@ -72,7 +73,7 @@ const render = async (props = {}, sliceState = {}) => { describe('', () => { beforeEach(() => { jest.resetAllMocks(); - useDecision.mockReturnValue([]); + usePromptExperimentDecision.mockReturnValue([]); }); describe('when it\'s open', () => { @@ -124,7 +125,7 @@ describe('', () => { }; it('should call showVariationSurvey if experiment is enabled', () => { - useDecision.mockReturnValue([{ + usePromptExperimentDecision.mockReturnValue([{ enabled: true, variationKey: OPTIMIZELY_PROMPT_EXPERIMENT_VARIATION_KEYS.UPDATED_PROMPT, }]); @@ -154,7 +155,7 @@ describe('', () => { }); it('should dispatch clearMessages() and call sendTrackEvent() with the expected props on clear', () => { - useDecision.mockReturnValue([{ + usePromptExperimentDecision.mockReturnValue([{ enabled: true, variationKey: OPTIMIZELY_PROMPT_EXPERIMENT_VARIATION_KEYS.UPDATED_PROMPT, }]); diff --git a/src/components/ToggleXpertButton/index.jsx b/src/components/ToggleXpertButton/index.jsx index 750d2234..08bc570e 100644 --- a/src/components/ToggleXpertButton/index.jsx +++ b/src/components/ToggleXpertButton/index.jsx @@ -1,6 +1,5 @@ import PropTypes from 'prop-types'; import React, { useState } from 'react'; -import { useDecision } from '@optimizely/react-sdk'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { @@ -15,6 +14,7 @@ import { Close } from '@openedx/paragon/icons'; import { OPTIMIZELY_PROMPT_EXPERIMENT_KEY } from '../../data/optimizely'; import { ReactComponent as XpertLogo } from '../../assets/xpert-logo.svg'; import './index.scss'; +import { usePromptExperimentDecision } from '../../experiments'; const ToggleXpert = ({ isOpen, @@ -27,7 +27,7 @@ const ToggleXpert = ({ const [target, setTarget] = useState(null); const { userId } = getAuthenticatedUser(); - const [decision] = useDecision(OPTIMIZELY_PROMPT_EXPERIMENT_KEY, { autoUpdate: true }, { id: userId.toString() }); + const [decision] = usePromptExperimentDecision(); const { enabled, variationKey } = decision || {}; const experimentPayload = enabled ? { experiment_name: OPTIMIZELY_PROMPT_EXPERIMENT_KEY, diff --git a/src/components/ToggleXpertButton/index.test.jsx b/src/components/ToggleXpertButton/index.test.jsx index 6f4e89d3..e3d32ae9 100644 --- a/src/components/ToggleXpertButton/index.test.jsx +++ b/src/components/ToggleXpertButton/index.test.jsx @@ -1,7 +1,8 @@ import React from 'react'; import { screen, waitFor } from '@testing-library/react'; -import { useDecision } from '@optimizely/react-sdk'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; + +import { usePromptExperimentDecision } from '../../experiments'; import { render as renderComponent } from '../../utils/utils.test'; import { initialState } from '../../data/slice'; import { OPTIMIZELY_PROMPT_EXPERIMENT_KEY, OPTIMIZELY_PROMPT_EXPERIMENT_VARIATION_KEYS } from '../../data/optimizely'; @@ -17,8 +18,8 @@ jest.mock('@edx/frontend-platform/auth', () => ({ getAuthenticatedUser: () => mockedAuthenticatedUser, })); -jest.mock('@optimizely/react-sdk', () => ({ - useDecision: jest.fn(), +jest.mock('../../experiments', () => ({ + usePromptExperimentDecision: jest.fn(), })); const defaultProps = { @@ -52,7 +53,7 @@ describe('', () => { beforeEach(() => { window.localStorage.clear(); jest.clearAllMocks(); - useDecision.mockReturnValue([]); + usePromptExperimentDecision.mockReturnValue([]); }); describe('when it\'s closed', () => { @@ -120,7 +121,7 @@ describe('', () => { describe('prompt experiment', () => { beforeEach(() => { - useDecision.mockReturnValue([{ + usePromptExperimentDecision.mockReturnValue([{ enabled: true, variationKey: OPTIMIZELY_PROMPT_EXPERIMENT_VARIATION_KEYS.UPDATED_PROMPT, }]); diff --git a/src/optimizely/ExperimentsProvider.jsx b/src/experiments/ExperimentsProvider.jsx similarity index 100% rename from src/optimizely/ExperimentsProvider.jsx rename to src/experiments/ExperimentsProvider.jsx diff --git a/src/experiments/experimentHooks.js b/src/experiments/experimentHooks.js new file mode 100644 index 00000000..1d9c33c5 --- /dev/null +++ b/src/experiments/experimentHooks.js @@ -0,0 +1,17 @@ +import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; +import { useDecision } from '@optimizely/react-sdk'; + +import { OPTIMIZELY_PROMPT_EXPERIMENT_KEY } from '../data/optimizely'; + +// eslint-disable-next-line import/prefer-default-export +export function usePromptExperimentDecision() { + const { userId } = getAuthenticatedUser(); + + const [decision] = useDecision( + OPTIMIZELY_PROMPT_EXPERIMENT_KEY, + { autoUpdate: true }, + { overrideUserId: userId.toString() }, + ); + + return [decision]; +} diff --git a/src/experiments/index.jsx b/src/experiments/index.jsx new file mode 100644 index 00000000..c5b5202f --- /dev/null +++ b/src/experiments/index.jsx @@ -0,0 +1,5 @@ +import ExperimentsProvider from './ExperimentsProvider'; + +export * from './experimentHooks'; + +export { ExperimentsProvider }; diff --git a/src/optimizely/index.jsx b/src/optimizely/index.jsx deleted file mode 100644 index a94cb61d..00000000 --- a/src/optimizely/index.jsx +++ /dev/null @@ -1,3 +0,0 @@ -import ExperimentsProvider from './ExperimentsProvider'; - -export default ExperimentsProvider; diff --git a/src/widgets/Xpert.jsx b/src/widgets/Xpert.jsx index 45fb6d23..ade3b1e9 100644 --- a/src/widgets/Xpert.jsx +++ b/src/widgets/Xpert.jsx @@ -1,10 +1,11 @@ import PropTypes from 'prop-types'; import { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; + import { updateSidebarIsOpen, getIsEnabled } from '../data/thunks'; import ToggleXpert from '../components/ToggleXpertButton'; import Sidebar from '../components/Sidebar'; -import ExperimentsProvider from '../optimizely'; +import { ExperimentsProvider } from '../experiments'; const Xpert = ({ courseId, contentToolsEnabled, unitId }) => { const dispatch = useDispatch(); diff --git a/src/widgets/Xpert.test.jsx b/src/widgets/Xpert.test.jsx index 2e677f92..81721577 100644 --- a/src/widgets/Xpert.test.jsx +++ b/src/widgets/Xpert.test.jsx @@ -9,21 +9,18 @@ import Xpert from './Xpert'; import * as surveyMonkey from '../utils/surveyMonkey'; import { render, createRandomResponseForTesting } from '../utils/utils.test'; import { OPTIMIZELY_PROMPT_EXPERIMENT_VARIATION_KEYS } from '../data/optimizely'; +import { usePromptExperimentDecision } from '../experiments'; jest.mock('@edx/frontend-platform/analytics'); jest.mock('@edx/frontend-platform/auth', () => ({ getAuthenticatedUser: jest.fn(() => ({ userId: 1 })), })); -jest.mock('@optimizely/react-sdk', () => ({ - useDecision: jest.fn(), +jest.mock('../experiments', () => ({ + ExperimentsProvider: ({ children }) => children, + usePromptExperimentDecision: jest.fn(), })); -jest.mock('../optimizely', () => ({ children }) => children); - -// 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: '', @@ -34,7 +31,6 @@ 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'; @@ -53,7 +49,7 @@ beforeEach(() => { const responseMessage = createRandomResponseForTesting(); jest.spyOn(api, 'default').mockResolvedValue(responseMessage); jest.spyOn(api, 'fetchLearningAssistantEnabled').mockResolvedValue({ enabled: true }); - useDecision.mockReturnValue([]); + usePromptExperimentDecision.mockReturnValue([]); window.localStorage.clear(); // Popup modal should be ignored for all tests unless explicitly enabled. This is because @@ -254,7 +250,6 @@ 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 }); @@ -289,7 +284,6 @@ 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 }); @@ -319,7 +313,6 @@ 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 }); @@ -387,13 +380,12 @@ test('survey monkey survey should appear after closing sidebar', async () => { currentMessage: '', messageList: [ { role: 'user', content: 'hi', timestamp: new Date() }, - { role: 'user', content: 'hi', timestamp: new Date() }, + { role: 'user', content: 'hi', timestamp: new Date() + 1 }, ], apiIsLoading: false, apiError: false, disclosureAcknowledged: true, sidebarIsOpen: false, - experiments: {}, }, }; render(, { preloadedState: surveyState }); @@ -415,7 +407,7 @@ test('survey monkey variation survey should appear if user is in experiment', as const variationSurvey = jest.spyOn(surveyMonkey, 'showVariationSurvey').mockReturnValueOnce(1); const user = userEvent.setup(); - useDecision.mockReturnValue([{ + usePromptExperimentDecision.mockReturnValue([{ enabled: true, variationKey: OPTIMIZELY_PROMPT_EXPERIMENT_VARIATION_KEYS.UPDATED_PROMPT, }]); @@ -425,13 +417,12 @@ test('survey monkey variation survey should appear if user is in experiment', as currentMessage: '', messageList: [ { role: 'user', content: 'hi', timestamp: new Date() }, - { role: 'user', content: 'hi', timestamp: new Date() }, + { role: 'user', content: 'hi', timestamp: new Date() + 1 }, ], apiIsLoading: false, apiError: false, disclosureAcknowledged: true, sidebarIsOpen: false, - experiments: {}, }, }; render(, { preloadedState: surveyState });