diff --git a/src/components/Sidebar/index.jsx b/src/components/Sidebar/index.jsx index 1dfec4b..a7fbd52 100644 --- a/src/components/Sidebar/index.jsx +++ b/src/components/Sidebar/index.jsx @@ -32,7 +32,7 @@ const Sidebar = ({ messageList, experiments, } = useSelector(state => state.learningAssistant); - const { variationKey } = experiments ? experiments[PROMPT_EXPERIMENT_FLAG] : {}; + const { variationKey } = experiments?.[PROMPT_EXPERIMENT_FLAG] || {}; const chatboxContainerRef = useRef(null); const dispatch = useDispatch(); @@ -95,7 +95,7 @@ const Sidebar = ({ ); const getSidebar = () => ( -
+

Hi, I'm Xpert! @@ -137,16 +137,18 @@ const Sidebar = ({ isOpen && (
{disclosureAcknowledged ? (getSidebar()) : ({getMessageForm()})}
diff --git a/src/components/Sidebar/index.test.jsx b/src/components/Sidebar/index.test.jsx new file mode 100644 index 0000000..04b59f3 --- /dev/null +++ b/src/components/Sidebar/index.test.jsx @@ -0,0 +1,115 @@ +import React from 'react'; +import { screen } from '@testing-library/react'; +import { render, act } from '../../utils/utils.test'; +import { initialState } from '../../data/slice'; +import { PROMPT_EXPERIMENT_FLAG, PROMPT_EXPERIMENT_KEY } from '../../constants/experiments'; +import { showControlSurvey, showVariationSurvey } from '../../utils/surveyMonkey'; + +import Sidebar from '.'; + +jest.mock('../../utils/surveyMonkey', () => ({ + showControlSurvey: jest.fn(), + showVariationSurvey: jest.fn(), +})); + +const defaultProps = { + courseId: 'some-course-id', + isOpen: true, + setIsOpen: jest.fn(), + unitId: 'some-unit-id', +}; + +const renderSidebar = async (props = {}, sliceState = {}) => { + const componentProps = { + ...defaultProps, + ...props, + }; + + const initState = { + preloadedState: { + learningAssistant: { + ...initialState, + ...sliceState, + }, + }, + }; + return act(async () => render( + , + initState, + )); +}; + +describe('', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('when it\'s open', () => { + it('should render normally', () => { + renderSidebar(); + expect(screen.queryByTestId('sidebar')).toBeInTheDocument(); + }); + + it('should not render xpert if no disclosureAcknowledged', () => { + renderSidebar(); + expect(screen.queryByTestId('sidebar-xpert')).not.toBeInTheDocument(); + }); + + it('should render xpert if disclosureAcknowledged', () => { + renderSidebar(undefined, { disclosureAcknowledged: true }); + expect(screen.queryByTestId('sidebar-xpert')).toBeInTheDocument(); + }); + }); + + describe('when it\'s not open', () => { + it('should not render', () => { + renderSidebar({ isOpen: false }); + expect(screen.queryByTestId('sidebar')).not.toBeInTheDocument(); + }); + }); + + describe('prompt experiment', () => { + const defaultState = { + messageList: [{ + role: 'user', + content: 'Testing message 1', + timestamp: +Date.now(), + }, { + role: 'user', + content: 'Testing message 2', + timestamp: +Date.now(), + }], + experiments: { + [PROMPT_EXPERIMENT_FLAG]: { + enabled: true, + variationKey: PROMPT_EXPERIMENT_KEY, + }, + }, + }; + + it('should call showVariationSurvey if experiment is active', async () => { + renderSidebar(undefined, defaultState); + + await act(() => { + screen.queryByTestId('close-button').click(); + }); + + expect(showVariationSurvey).toHaveBeenCalled(); + expect(showControlSurvey).not.toHaveBeenCalled(); + }); + + it('should call showControlSurvey if experiment is not active', async () => { + renderSidebar(undefined, { + ...defaultState, + experiments: {}, + }); + + await act(() => { + screen.queryByTestId('close-button').click(); + }); + + expect(showControlSurvey).toHaveBeenCalled(); + expect(showVariationSurvey).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/components/ToggleXpertButton/index.jsx b/src/components/ToggleXpertButton/index.jsx index 840de79..961adfe 100644 --- a/src/components/ToggleXpertButton/index.jsx +++ b/src/components/ToggleXpertButton/index.jsx @@ -23,7 +23,7 @@ const ToggleXpert = ({ contentToolsEnabled, }) => { const { experiments } = useSelector(state => state.learningAssistant); - const { variationKey } = experiments ? experiments[PROMPT_EXPERIMENT_FLAG] : {}; + const { variationKey } = experiments?.[PROMPT_EXPERIMENT_FLAG] || {}; const [hasDismissedCTA, setHasDismissedCTA] = useState(false); const [isModalOpen, setIsModalOpen] = useState(true); const [target, setTarget] = useState(null); diff --git a/src/data/slice.js b/src/data/slice.js index e7cbef2..ec64b93 100644 --- a/src/data/slice.js +++ b/src/data/slice.js @@ -2,19 +2,21 @@ import { createSlice } from '@reduxjs/toolkit'; import { v4 as uuidv4 } from 'uuid'; +export const initialState = { + currentMessage: '', + messageList: [], + apiError: false, + apiIsLoading: false, + conversationId: uuidv4(), + disclosureAcknowledged: false, + sidebarIsOpen: false, + isEnabled: false, + experiments: {}, +}; + export const learningAssistantSlice = createSlice({ name: 'learning-assistant', - initialState: { - currentMessage: '', - messageList: [], - apiError: false, - apiIsLoading: false, - conversationId: uuidv4(), - disclosureAcknowledged: false, - sidebarIsOpen: false, - isEnabled: false, - experiments: {}, - }, + initialState, reducers: { setCurrentMessage: (state, { payload }) => { state.currentMessage = payload.currentMessage; diff --git a/src/data/thunks.js b/src/data/thunks.js index a8c625e..03f1635 100644 --- a/src/data/thunks.js +++ b/src/data/thunks.js @@ -20,7 +20,7 @@ import { PROMPT_EXPERIMENT_FLAG } from '../constants/experiments'; export function addChatMessage(role, content, courseId) { return (dispatch, getState) => { const { messageList, conversationId, experiments } = getState().learningAssistant; - const { variationKey } = experiments ? experiments[PROMPT_EXPERIMENT_FLAG] : {}; + const { variationKey } = 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/setupTest.js b/src/setupTest.js index 95b3726..e4b5e8d 100644 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -1,3 +1,16 @@ import 'core-js/stable'; import 'regenerator-runtime/runtime'; import '@testing-library/jest-dom'; + +jest.mock( + '@optimizely/react-sdk', + () => { + const originalModule = jest.requireActual('@optimizely/react-sdk'); + return { + __esModule: true, + ...originalModule, + useDecision: jest.fn(() => [{ enabled: true, variationKey: 'control' }]), + }; + }, + { virtual: true }, +); diff --git a/src/utils/utils.test.jsx b/src/utils/utils.test.jsx index 3edeef4..8e4e378 100644 --- a/src/utils/utils.test.jsx +++ b/src/utils/utils.test.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { render } from '@testing-library/react'; +import { render, act } from '@testing-library/react'; import { configureStore } from '@reduxjs/toolkit'; import { Provider } from 'react-redux'; import { IntlProvider } from '@edx/frontend-platform/i18n'; @@ -51,4 +51,13 @@ const createRandomResponseForTesting = () => { return { role: 'assistant', content: message.join(' ') }; }; -export { renderWithProviders as render, createRandomResponseForTesting }; +// Helper, that is used to forcibly finalize all promises +// in thunk before running matcher against state. +const executeThunk = async (thunk, dispatch, getState) => { + await thunk(dispatch, getState); + await new Promise(setImmediate); +}; + +export { + renderWithProviders as render, act, createRandomResponseForTesting, executeThunk, +};