Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Optimizely refactor #57

Merged
merged 7 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ SITE_NAME=localhost
USER_INFO_COOKIE_NAME='edx-user-info'
APP_ID=''
MFE_CONFIG_API_URL=''
OPTIMIZELY_FULL_STACK_SDK_KEY='test-optimizely-sdk-full-stack-key'
1 change: 1 addition & 0 deletions module.config.js.example
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ module.exports = {
// { moduleName: '@openedx/paragon/icons', dir: '../paragon', dist: 'icons' },
// { moduleName: '@openedx/paragon', dir: '../paragon', dist: 'dist' },
// { moduleName: '@edx/frontend-platform', dir: '../frontend-platform', dist: 'dist' },
// { moduleName: '@optimizely/react-sdk', dir: '../src/frontend-lib-learning-assistant/src/mocks/optimizely', dist: '.' },
],
};
14 changes: 10 additions & 4 deletions src/components/MessageForm/index.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import PropTypes from 'prop-types';
import React, { useEffect, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';

import { Button, Form, Icon } from '@openedx/paragon';
import { Send } from '@openedx/paragon/icons';

Expand All @@ -11,12 +10,17 @@ import {
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 [decision] = usePromptExperimentDecision();
const { enabled, variationKey } = decision || {};
const promptExperimentVariationKey = enabled ? variationKey : undefined;

useEffect(() => {
if (inputRef.current && !apiError && !apiIsLoading && shouldAutofocus) {
inputRef.current.focus();
Expand All @@ -25,10 +29,11 @@ const MessageForm = ({ courseId, shouldAutofocus, unitId }) => {

const handleSubmitMessage = (event) => {
event.preventDefault();

if (currentMessage) {
dispatch(acknowledgeDisclosure(true));
dispatch(addChatMessage('user', currentMessage, courseId));
dispatch(getChatResponse(courseId, unitId));
dispatch(addChatMessage('user', currentMessage, courseId, promptExperimentVariationKey));
dispatch(getChatResponse(courseId, unitId, promptExperimentVariationKey));
}
};

Expand All @@ -43,13 +48,14 @@ const MessageForm = ({ courseId, shouldAutofocus, unitId }) => {
onClick={handleSubmitMessage}
size="inline"
variant="tertiary"
data-testid="message-form-submit"
>
<Icon src={Send} />
</Button>
);

return (
<Form className="w-100 pl-2" onSubmit={handleSubmitMessage}>
<Form className="w-100 pl-2" onSubmit={handleSubmitMessage} data-testid="message-form">
<Form.Group>
<Form.Control
data-hj-suppress
Expand Down
195 changes: 195 additions & 0 deletions src/components/MessageForm/index.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import React from 'react';
import {
screen, act, fireEvent, waitFor,
} from '@testing-library/react';
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';
import {
acknowledgeDisclosure,
addChatMessage,
getChatResponse,
updateCurrentMessage,
} from '../../data/thunks';

import MessageForm from '.';

jest.mock('../../utils/surveyMonkey', () => ({
showControlSurvey: jest.fn(),
showVariationSurvey: jest.fn(),
}));

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

const mockedAuthenticatedUser = { userId: 123 };
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedUser: () => mockedAuthenticatedUser,
}));

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

const mockDispatch = jest.fn();
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useDispatch: () => mockDispatch,
}));

jest.mock('../../experiments', () => ({
usePromptExperimentDecision: jest.fn(),
}));

jest.mock('../../data/thunks', () => ({
acknowledgeDisclosure: jest.fn(),
addChatMessage: jest.fn(),
getChatResponse: jest.fn(),
updateCurrentMessage: jest.fn(),
}));

const defaultProps = {
courseId: 'some-course-id',
isOpen: true,
setIsOpen: jest.fn(),
unitId: 'some-unit-id',
};

const render = async (props = {}, sliceState = {}) => {
const componentProps = {
...defaultProps,
...props,
};

const initState = {
preloadedState: {
learningAssistant: {
...initialState,
...sliceState,
},
},
};
return act(async () => renderComponent(
<MessageForm {...componentProps} />,
initState,
));
};

describe('<MessageForm />', () => {
beforeEach(() => {
jest.resetAllMocks();
usePromptExperimentDecision.mockReturnValue([]);
});

describe('when rendered', () => {
it('should focus if shouldAutofocus is enabled', () => {
const currentMessage = 'How much wood';
const sliceState = {
apiIsLoading: false,
currentMessage,
apiError: false,
};

render({ shouldAutofocus: true }, sliceState);

waitFor(() => {
expect(screen.getByDisplayValue(currentMessage)).toHaveFocus();
});

expect(screen.queryByTestId('message-form')).toBeInTheDocument();
});

it('should dispatch updateCurrentMessage() when updating the form control', () => {
const currentMessage = 'How much wood';
const updatedMessage = 'How much wood coud a woodchuck chuck';
const sliceState = {
apiIsLoading: false,
currentMessage,
apiError: false,
};

render(undefined, sliceState);

act(() => {
const input = screen.getByDisplayValue(currentMessage);
fireEvent.change(input, { target: { value: updatedMessage } });
});

expect(updateCurrentMessage).toHaveBeenCalledWith(updatedMessage);
expect(mockDispatch).toHaveBeenCalledTimes(1);
});

it('should dispatch message on submit as expected', () => {
const currentMessage = 'How much wood could a woodchuck chuck if a woodchuck could chuck wood?';
const sliceState = {
apiIsLoading: false,
currentMessage,
apiError: false,
};

render(undefined, sliceState);

act(() => {
screen.queryByTestId('message-form-submit').click();
});

expect(acknowledgeDisclosure).toHaveBeenCalledWith(true);
expect(addChatMessage).toHaveBeenCalledWith('user', currentMessage, defaultProps.courseId, undefined);
expect(getChatResponse).toHaveBeenCalledWith(defaultProps.courseId, defaultProps.unitId, undefined);
expect(mockDispatch).toHaveBeenCalledTimes(3);
});

it('should not dispatch on submit if there\'s no message', () => {
render();

act(() => {
screen.queryByTestId('message-form-submit').click();
});

expect(acknowledgeDisclosure).not.toHaveBeenCalled();
expect(addChatMessage).not.toHaveBeenCalled();
expect(getChatResponse).not.toHaveBeenCalled();
expect(mockDispatch).not.toHaveBeenCalled();
});
});

describe('prmpt experiment', () => {
beforeEach(() => {
usePromptExperimentDecision.mockReturnValue([{
enabled: true,
variationKey: OPTIMIZELY_PROMPT_EXPERIMENT_VARIATION_KEYS.UPDATED_PROMPT,
}]);
});

it('should include experiment data on submit', () => {
const currentMessage = 'How much wood could a woodchuck chuck if a woodchuck could chuck wood?';
const sliceState = {
apiIsLoading: false,
currentMessage,
apiError: false,
};

render(undefined, sliceState);

act(() => {
screen.queryByTestId('message-form-submit').click();
});

expect(acknowledgeDisclosure).toHaveBeenCalledWith(true);
expect(addChatMessage).toHaveBeenCalledWith(
'user',
currentMessage,
defaultProps.courseId,
OPTIMIZELY_PROMPT_EXPERIMENT_VARIATION_KEYS.UPDATED_PROMPT,
);
expect(getChatResponse).toHaveBeenCalledWith(
defaultProps.courseId,
defaultProps.unitId,
OPTIMIZELY_PROMPT_EXPERIMENT_VARIATION_KEYS.UPDATED_PROMPT,
);
expect(mockDispatch).toHaveBeenCalledTimes(3);
});
});
});
16 changes: 11 additions & 5 deletions src/components/Sidebar/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ import {
import { Close } from '@openedx/paragon/icons';

import { clearMessages } from '../../data/thunks';
import { PROMPT_EXPERIMENT_FLAG, PROMPT_EXPERIMENT_KEY } from '../../constants/experiments';
import { OPTIMIZELY_PROMPT_EXPERIMENT_KEY, OPTIMIZELY_PROMPT_EXPERIMENT_VARIATION_KEYS } from '../../data/optimizely';
import { showControlSurvey, showVariationSurvey } from '../../utils/surveyMonkey';

import APIError from '../APIError';
import ChatBox from '../ChatBox';
import Disclosure from '../Disclosure';
import MessageForm from '../MessageForm';
import './Sidebar.scss';
import { usePromptExperimentDecision } from '../../experiments';

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

const [decision] = usePromptExperimentDecision();
const { enabled: enabledExperiment, variationKey } = decision || {};
const experimentPayload = enabledExperiment ? {
experiment_name: OPTIMIZELY_PROMPT_EXPERIMENT_KEY,
variation_key: variationKey,
} : {};

// this use effect is intended to scroll to the bottom of the chat window, in the case
// that a message is larger than the chat window height.
useEffect(() => {
Expand Down Expand Up @@ -73,7 +79,7 @@ const Sidebar = ({
setIsOpen(false);

if (messageList.length >= 2) {
if (variationKey === PROMPT_EXPERIMENT_KEY) {
if (enabledExperiment && variationKey === OPTIMIZELY_PROMPT_EXPERIMENT_VARIATION_KEYS.UPDATED_PROMPT) {
showVariationSurvey();
} else {
showControlSurvey();
Expand All @@ -85,7 +91,7 @@ const Sidebar = ({
dispatch(clearMessages());
sendTrackEvent('edx.ui.lms.learning_assistant.clear', {
course_id: courseId,
...(variationKey ? { experiment_name: PROMPT_EXPERIMENT_FLAG, variation_key: variationKey } : {}),
...experimentPayload,
});
};

Expand Down
Loading
Loading