chore: remove UnitTranslationPlugin (#1352)
* chore: remove UnitTranslationPlugin * chore: add plugin slot for sequence
This commit is contained in:
1
.env
1
.env
@@ -4,7 +4,6 @@
|
||||
NODE_ENV='production'
|
||||
|
||||
ACCESS_TOKEN_COOKIE_NAME=''
|
||||
AI_TRANSLATIONS_URL=''
|
||||
BASE_URL=''
|
||||
CONTACT_URL=''
|
||||
CREDENTIALS_BASE_URL=''
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
NODE_ENV='development'
|
||||
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
AI_TRANSLATIONS_URL='http://localhost:18760'
|
||||
BASE_URL='http://localhost:2000'
|
||||
CONTACT_URL='http://localhost:18000/contact'
|
||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
NODE_ENV='test'
|
||||
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
AI_TRANSLATIONS_URL='http://localhost:18760'
|
||||
BASE_URL='http://localhost:2000'
|
||||
CONTACT_URL='http://localhost:18000/contact'
|
||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import UnitTranslationPlugin from '@plugins/UnitTranslationPlugin';
|
||||
import UnitTranslationPlugin from '@edx/unit-translation-selector-plugin';
|
||||
import { PLUGIN_OPERATIONS, DIRECT_PLUGIN } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
// Load environment variables from .env file
|
||||
|
||||
@@ -15,7 +15,6 @@ const config = createConfig('jest', {
|
||||
// See https://stackoverflow.com/questions/72382316/jest-encountered-an-unexpected-token-react-markdown
|
||||
'react-markdown': '<rootDir>/node_modules/react-markdown/react-markdown.min.js',
|
||||
'@src/(.*)': '<rootDir>/src/$1',
|
||||
'@plugins/(.*)': '<rootDir>/plugins/$1',
|
||||
},
|
||||
testTimeout: 30000,
|
||||
globalSetup: "./global-setup.js",
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
## How to develop plugin
|
||||
|
||||
You can define plugin in `env.config.jsx` see `example.env.config.jsx` as example.
|
||||
|
||||
## Current caveat
|
||||
|
||||
- The way for how I deal with override method is still wonky
|
||||
- The redux still require middleware to ignore the plugin's action from serializing
|
||||
- I am not sure how it behave with useCallback, useMemo, ...etc
|
||||
- There are still open question on how to write it properly
|
||||
|
||||
## Current work that should consider core part and extendable for the future plugin framework
|
||||
|
||||
- `usePluingsCallback` is the callback supose to be some level of equality to be using `React.useCallback`. It would try to execute the function, then any plugin that try `registerOverrideMethod`. The order of the it being run isn't the determined. There are a couple things I want to add:
|
||||
- I might consider testing it with `zustand` library to make sure it is portable and not rely on `redux`. I tried to do this with provider, but it seems to run into infinite loop of trigger changed.
|
||||
|
||||
- `registerOverrideMethod` is working like a way to register callback that behave like a middleware. It ran the default one, then pass the result of the default one to the plugin. Any plugin that register the override can update the value. Alternatively, we can override the function completely instead applying each affect. Or we can support both. But it requires a bit more thought out architecture.
|
||||
@@ -1,29 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<UnitTranslationPlugin /> render TranslationSelection when translation is enabled and language is available 1`] = `
|
||||
<TranslationSelection
|
||||
availableLanguages={
|
||||
Array [
|
||||
"en",
|
||||
]
|
||||
}
|
||||
courseId="courseId"
|
||||
id="id"
|
||||
language="en"
|
||||
unitId="unitId"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`<UnitTranslationPlugin /> render translation when the user is staff 1`] = `
|
||||
<TranslationSelection
|
||||
availableLanguages={
|
||||
Array [
|
||||
"en",
|
||||
]
|
||||
}
|
||||
courseId="courseId"
|
||||
id="id"
|
||||
language="en"
|
||||
unitId="unitId"
|
||||
/>
|
||||
`;
|
||||
@@ -1,90 +0,0 @@
|
||||
import { getConfig, camelCaseObject } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
import { stringify } from 'query-string';
|
||||
|
||||
export const fetchTranslationConfig = async (courseId) => {
|
||||
const url = `${
|
||||
getConfig().LMS_BASE_URL
|
||||
}/api/translatable_xblocks/config/?course_id=${encodeURIComponent(courseId)}`;
|
||||
try {
|
||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||
return {
|
||||
enabled: data.feature_enabled,
|
||||
availableLanguages: data.available_translation_languages || [
|
||||
{
|
||||
code: 'en',
|
||||
label: 'English',
|
||||
},
|
||||
{
|
||||
code: 'es',
|
||||
label: 'Spanish',
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
logError(`Translation plugin fail to fetch from ${url}`, error);
|
||||
return {
|
||||
enabled: false,
|
||||
availableLanguages: [],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export async function getTranslationFeedback({
|
||||
courseId,
|
||||
translationLanguage,
|
||||
unitId,
|
||||
userId,
|
||||
}) {
|
||||
const params = stringify({
|
||||
translation_language: translationLanguage,
|
||||
course_id: encodeURIComponent(courseId),
|
||||
unit_id: encodeURIComponent(unitId),
|
||||
user_id: userId,
|
||||
});
|
||||
const fetchFeedbackUrl = `${
|
||||
getConfig().AI_TRANSLATIONS_URL
|
||||
}/api/v1/whole-course-translation-feedback?${params}`;
|
||||
try {
|
||||
const { data } = await getAuthenticatedHttpClient().get(fetchFeedbackUrl);
|
||||
return camelCaseObject(data);
|
||||
} catch (error) {
|
||||
logError(
|
||||
`Translation plugin fail to fetch from ${fetchFeedbackUrl}`,
|
||||
error,
|
||||
);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export async function createTranslationFeedback({
|
||||
courseId,
|
||||
feedbackValue,
|
||||
translationLanguage,
|
||||
unitId,
|
||||
userId,
|
||||
}) {
|
||||
const createFeedbackUrl = `${
|
||||
getConfig().AI_TRANSLATIONS_URL
|
||||
}/api/v1/whole-course-translation-feedback/`;
|
||||
try {
|
||||
const { data } = await getAuthenticatedHttpClient().post(
|
||||
createFeedbackUrl,
|
||||
{
|
||||
course_id: courseId,
|
||||
feedback_value: feedbackValue,
|
||||
translation_language: translationLanguage,
|
||||
unit_id: unitId,
|
||||
user_id: userId,
|
||||
},
|
||||
);
|
||||
return camelCaseObject(data);
|
||||
} catch (error) {
|
||||
logError(
|
||||
`Translation plugin fail to create feedback from ${createFeedbackUrl}`,
|
||||
error,
|
||||
);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
import { camelCaseObject } from '@edx/frontend-platform';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
import { stringify } from 'query-string';
|
||||
|
||||
import {
|
||||
fetchTranslationConfig,
|
||||
getTranslationFeedback,
|
||||
createTranslationFeedback,
|
||||
} from './api';
|
||||
|
||||
const mockGetMethod = jest.fn();
|
||||
const mockPostMethod = jest.fn();
|
||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
getAuthenticatedHttpClient: () => ({
|
||||
get: mockGetMethod,
|
||||
post: mockPostMethod,
|
||||
}),
|
||||
}));
|
||||
jest.mock('@edx/frontend-platform/logging', () => ({
|
||||
logError: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('UnitTranslation api', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('fetchTranslationConfig', () => {
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const expectedResponse = {
|
||||
feature_enabled: true,
|
||||
available_translation_languages: [
|
||||
{
|
||||
code: 'en',
|
||||
label: 'English',
|
||||
},
|
||||
{
|
||||
code: 'es',
|
||||
label: 'Spanish',
|
||||
},
|
||||
],
|
||||
};
|
||||
it('should fetch translation config', async () => {
|
||||
const expectedUrl = `http://localhost:18000/api/translatable_xblocks/config/?course_id=${encodeURIComponent(
|
||||
courseId,
|
||||
)}`;
|
||||
mockGetMethod.mockResolvedValueOnce({ data: expectedResponse });
|
||||
const result = await fetchTranslationConfig(courseId);
|
||||
expect(result).toEqual({
|
||||
enabled: true,
|
||||
availableLanguages: expectedResponse.available_translation_languages,
|
||||
});
|
||||
expect(mockGetMethod).toHaveBeenCalledWith(expectedUrl);
|
||||
});
|
||||
|
||||
it('should return disabled and unavailable languages on error', async () => {
|
||||
mockGetMethod.mockRejectedValueOnce(new Error('error'));
|
||||
const result = await fetchTranslationConfig(courseId);
|
||||
expect(result).toEqual({
|
||||
enabled: false,
|
||||
availableLanguages: [],
|
||||
});
|
||||
expect(logError).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTranslationFeedback', () => {
|
||||
const props = {
|
||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
translationLanguage: 'es',
|
||||
unitId: 'unit-v1:edX+DemoX+Demo_Course+type@video+block@video',
|
||||
userId: 'test_user',
|
||||
};
|
||||
const expectedResponse = {
|
||||
feedback: 'good',
|
||||
};
|
||||
it('should fetch translation feedback', async () => {
|
||||
const params = stringify({
|
||||
translation_language: props.translationLanguage,
|
||||
course_id: encodeURIComponent(props.courseId),
|
||||
unit_id: encodeURIComponent(props.unitId),
|
||||
user_id: props.userId,
|
||||
});
|
||||
const expectedUrl = `http://localhost:18760/api/v1/whole-course-translation-feedback?${params}`;
|
||||
mockGetMethod.mockResolvedValueOnce({ data: expectedResponse });
|
||||
const result = await getTranslationFeedback(props);
|
||||
expect(result).toEqual(camelCaseObject(expectedResponse));
|
||||
expect(mockGetMethod).toHaveBeenCalledWith(expectedUrl);
|
||||
});
|
||||
|
||||
it('should return empty object on error', async () => {
|
||||
mockGetMethod.mockRejectedValueOnce(new Error('error'));
|
||||
const result = await getTranslationFeedback(props);
|
||||
expect(result).toEqual({});
|
||||
expect(logError).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createTranslationFeedback', () => {
|
||||
const props = {
|
||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
feedbackValue: 'good',
|
||||
translationLanguage: 'es',
|
||||
unitId: 'unit-v1:edX+DemoX+Demo_Course+type@video+block@video',
|
||||
userId: 'test_user',
|
||||
};
|
||||
it('should create translation feedback', async () => {
|
||||
const expectedUrl = 'http://localhost:18760/api/v1/whole-course-translation-feedback/';
|
||||
mockPostMethod.mockResolvedValueOnce({});
|
||||
await createTranslationFeedback(props);
|
||||
expect(mockPostMethod).toHaveBeenCalledWith(expectedUrl, {
|
||||
course_id: props.courseId,
|
||||
feedback_value: props.feedbackValue,
|
||||
translation_language: props.translationLanguage,
|
||||
unit_id: props.unitId,
|
||||
user_id: props.userId,
|
||||
});
|
||||
});
|
||||
|
||||
it('should log error on failure', async () => {
|
||||
mockPostMethod.mockRejectedValueOnce(new Error('error'));
|
||||
await createTranslationFeedback(props);
|
||||
expect(logError).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,204 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<FeedbackWidget /> render feedback widget 1`] = `
|
||||
<div
|
||||
className="d-none"
|
||||
>
|
||||
<div
|
||||
className="sequence w-100"
|
||||
>
|
||||
<div
|
||||
className="ml-4 mr-2"
|
||||
>
|
||||
<ActionRow>
|
||||
Rate this page translation
|
||||
<Spacer />
|
||||
<div>
|
||||
<IconButton
|
||||
alt="positive-feedback"
|
||||
className="m-1"
|
||||
iconAs="Icon"
|
||||
id="positive-feedback-button"
|
||||
onClick={[MockFunction onThumbsUpClick]}
|
||||
src="ThumbUpOutline"
|
||||
variant="secondary"
|
||||
/>
|
||||
<IconButton
|
||||
alt="negative-feedback"
|
||||
className="mr-2"
|
||||
iconAs="Icon"
|
||||
id="negative-feedback-button"
|
||||
onClick={[MockFunction onThumbsDownClick]}
|
||||
src="ThumbDownOffAlt"
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="mb-1 text-light action-row-divider"
|
||||
>
|
||||
|
|
||||
</div>
|
||||
<div>
|
||||
<IconButton
|
||||
alt="close-feedback"
|
||||
className="ml-1 mr-2 float-right"
|
||||
iconAs="Icon"
|
||||
id="close-feedback-button"
|
||||
onClick={[MockFunction closeFeedbackWidget]}
|
||||
src="Close"
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
</ActionRow>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<FeedbackWidget /> render gratitude text 1`] = `
|
||||
<div
|
||||
className="d-none"
|
||||
>
|
||||
<div
|
||||
className="sequence w-100"
|
||||
>
|
||||
<div
|
||||
className="ml-4 mr-4"
|
||||
>
|
||||
<ActionRow
|
||||
className="m-2 justify-content-center"
|
||||
>
|
||||
Thank you! Your feedback matters.
|
||||
</ActionRow>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<FeedbackWidget /> renders hidden by default 1`] = `
|
||||
<div
|
||||
className="d-none"
|
||||
>
|
||||
<div
|
||||
className="sequence w-100"
|
||||
>
|
||||
<div
|
||||
className="ml-4 mr-2"
|
||||
>
|
||||
<ActionRow>
|
||||
Rate this page translation
|
||||
<Spacer />
|
||||
<div>
|
||||
<IconButton
|
||||
alt="positive-feedback"
|
||||
className="m-1"
|
||||
iconAs="Icon"
|
||||
id="positive-feedback-button"
|
||||
onClick={[MockFunction onThumbsUpClick]}
|
||||
src="ThumbUpOutline"
|
||||
variant="secondary"
|
||||
/>
|
||||
<IconButton
|
||||
alt="negative-feedback"
|
||||
className="mr-2"
|
||||
iconAs="Icon"
|
||||
id="negative-feedback-button"
|
||||
onClick={[MockFunction onThumbsDownClick]}
|
||||
src="ThumbDownOffAlt"
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="mb-1 text-light action-row-divider"
|
||||
>
|
||||
|
|
||||
</div>
|
||||
<div>
|
||||
<IconButton
|
||||
alt="close-feedback"
|
||||
className="ml-1 mr-2 float-right"
|
||||
iconAs="Icon"
|
||||
id="close-feedback-button"
|
||||
onClick={[MockFunction closeFeedbackWidget]}
|
||||
src="Close"
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
</ActionRow>
|
||||
</div>
|
||||
<div
|
||||
className="ml-4 mr-4"
|
||||
>
|
||||
<ActionRow
|
||||
className="m-2 justify-content-center"
|
||||
>
|
||||
Thank you! Your feedback matters.
|
||||
</ActionRow>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<FeedbackWidget /> renders show when elemReady is true 1`] = `
|
||||
<div
|
||||
className="sequence-container d-inline-flex flex-row w-100"
|
||||
>
|
||||
<div
|
||||
className="sequence w-100"
|
||||
>
|
||||
<div
|
||||
className="ml-4 mr-2"
|
||||
>
|
||||
<ActionRow>
|
||||
Rate this page translation
|
||||
<Spacer />
|
||||
<div>
|
||||
<IconButton
|
||||
alt="positive-feedback"
|
||||
className="m-1"
|
||||
iconAs="Icon"
|
||||
id="positive-feedback-button"
|
||||
onClick={[MockFunction onThumbsUpClick]}
|
||||
src="ThumbUpOutline"
|
||||
variant="secondary"
|
||||
/>
|
||||
<IconButton
|
||||
alt="negative-feedback"
|
||||
className="mr-2"
|
||||
iconAs="Icon"
|
||||
id="negative-feedback-button"
|
||||
onClick={[MockFunction onThumbsDownClick]}
|
||||
src="ThumbDownOffAlt"
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="mb-1 text-light action-row-divider"
|
||||
>
|
||||
|
|
||||
</div>
|
||||
<div>
|
||||
<IconButton
|
||||
alt="close-feedback"
|
||||
className="ml-1 mr-2 float-right"
|
||||
iconAs="Icon"
|
||||
id="close-feedback-button"
|
||||
onClick={[MockFunction closeFeedbackWidget]}
|
||||
src="Close"
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
</ActionRow>
|
||||
</div>
|
||||
<div
|
||||
className="ml-4 mr-4"
|
||||
>
|
||||
<ActionRow
|
||||
className="m-2 justify-content-center"
|
||||
>
|
||||
Thank you! Your feedback matters.
|
||||
</ActionRow>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,116 +0,0 @@
|
||||
import React, {
|
||||
useEffect, useRef, useState,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { ActionRow, IconButton, Icon } from '@openedx/paragon';
|
||||
import { Close, ThumbUpOutline, ThumbDownOffAlt } from '@openedx/paragon/icons';
|
||||
|
||||
import './index.scss';
|
||||
import messages from './messages';
|
||||
import useFeedbackWidget from './useFeedbackWidget';
|
||||
|
||||
const FeedbackWidget = ({
|
||||
courseId,
|
||||
translationLanguage,
|
||||
unitId,
|
||||
userId,
|
||||
}) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const ref = useRef(null);
|
||||
const [elemReady, setElemReady] = useState(false);
|
||||
const {
|
||||
closeFeedbackWidget,
|
||||
showFeedbackWidget,
|
||||
showGratitudeText,
|
||||
onThumbsUpClick,
|
||||
onThumbsDownClick,
|
||||
} = useFeedbackWidget({
|
||||
courseId,
|
||||
translationLanguage,
|
||||
unitId,
|
||||
userId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
const domNode = document.getElementById('whole-course-translation-feedback-widget');
|
||||
domNode.appendChild(ref.current);
|
||||
setElemReady(true);
|
||||
}
|
||||
}, [ref.current]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className={(elemReady) ? 'sequence-container d-inline-flex flex-row w-100' : 'd-none'}>
|
||||
{(showFeedbackWidget || showGratitudeText) ? (
|
||||
<div className="sequence w-100">
|
||||
{
|
||||
showFeedbackWidget && (
|
||||
<div className="ml-4 mr-2">
|
||||
<ActionRow>
|
||||
{formatMessage(messages.rateTranslationText)}
|
||||
<ActionRow.Spacer />
|
||||
<div>
|
||||
<IconButton
|
||||
src={ThumbUpOutline}
|
||||
iconAs={Icon}
|
||||
alt="positive-feedback"
|
||||
onClick={onThumbsUpClick}
|
||||
variant="secondary"
|
||||
className="m-1"
|
||||
id="positive-feedback-button"
|
||||
/>
|
||||
<IconButton
|
||||
src={ThumbDownOffAlt}
|
||||
iconAs={Icon}
|
||||
alt="negative-feedback"
|
||||
onClick={onThumbsDownClick}
|
||||
variant="secondary"
|
||||
className="mr-2"
|
||||
id="negative-feedback-button"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-1 text-light action-row-divider">
|
||||
|
|
||||
</div>
|
||||
<div>
|
||||
<IconButton
|
||||
src={Close}
|
||||
iconAs={Icon}
|
||||
alt="close-feedback"
|
||||
onClick={closeFeedbackWidget}
|
||||
variant="secondary"
|
||||
className="ml-1 mr-2 float-right"
|
||||
id="close-feedback-button"
|
||||
/>
|
||||
</div>
|
||||
</ActionRow>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
showGratitudeText && (
|
||||
<div className="ml-4 mr-4">
|
||||
<ActionRow className="m-2 justify-content-center">
|
||||
{formatMessage(messages.gratitudeText)}
|
||||
</ActionRow>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
FeedbackWidget.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
translationLanguage: PropTypes.string.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
unitId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
FeedbackWidget.defaultProps = {};
|
||||
|
||||
export default FeedbackWidget;
|
||||
@@ -1,4 +0,0 @@
|
||||
.action-row-divider {
|
||||
font-size: 31px;
|
||||
font-weight: 100;
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import FeedbackWidget from './index';
|
||||
import useFeedbackWidget from './useFeedbackWidget';
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useState: jest.fn((value) => [value, jest.fn()]),
|
||||
}));
|
||||
jest.mock('@openedx/paragon', () => jest.requireActual('@edx/react-unit-test-utils').mockComponents({
|
||||
ActionRow: {
|
||||
Spacer: 'Spacer',
|
||||
},
|
||||
IconButton: 'IconButton',
|
||||
Icon: 'Icon',
|
||||
}));
|
||||
jest.mock('@openedx/paragon/icons', () => ({
|
||||
Close: 'Close',
|
||||
ThumbUpOutline: 'ThumbUpOutline',
|
||||
ThumbDownOffAlt: 'ThumbDownOffAlt',
|
||||
}));
|
||||
jest.mock('./useFeedbackWidget');
|
||||
jest.mock('@edx/frontend-platform/i18n', () => {
|
||||
const i18n = jest.requireActual('@edx/frontend-platform/i18n');
|
||||
const { formatMessage } = jest.requireActual('@edx/react-unit-test-utils');
|
||||
return {
|
||||
...i18n,
|
||||
useIntl: jest.fn(() => ({
|
||||
formatMessage,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
describe('<FeedbackWidget />', () => {
|
||||
const props = {
|
||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
translationLanguage: 'es',
|
||||
unitId:
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@37b72b3915204b70acb00c55b604b563',
|
||||
userId: '123',
|
||||
};
|
||||
|
||||
const mockUseFeedbackWidget = ({ showFeedbackWidget, showGratitudeText }) => {
|
||||
useFeedbackWidget.mockReturnValueOnce({
|
||||
closeFeedbackWidget: jest.fn().mockName('closeFeedbackWidget'),
|
||||
sendFeedback: jest.fn().mockName('sendFeedback'),
|
||||
onThumbsUpClick: jest.fn().mockName('onThumbsUpClick'),
|
||||
onThumbsDownClick: jest.fn().mockName('onThumbsDownClick'),
|
||||
showFeedbackWidget,
|
||||
showGratitudeText,
|
||||
});
|
||||
};
|
||||
|
||||
it('renders hidden by default', () => {
|
||||
mockUseFeedbackWidget({
|
||||
showFeedbackWidget: true,
|
||||
showGratitudeText: true,
|
||||
});
|
||||
const wrapper = shallow(<FeedbackWidget {...props} />);
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
expect(wrapper.instance.findByType('div')[0].props.className).toContain(
|
||||
'd-none',
|
||||
);
|
||||
});
|
||||
|
||||
it('renders show when elemReady is true', () => {
|
||||
mockUseFeedbackWidget({
|
||||
showFeedbackWidget: true,
|
||||
showGratitudeText: true,
|
||||
});
|
||||
useState.mockReturnValueOnce([true, jest.fn()]);
|
||||
const wrapper = shallow(<FeedbackWidget {...props} />);
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
expect(wrapper.instance.findByType('div')[0].props.className).not.toContain(
|
||||
'd-none',
|
||||
);
|
||||
});
|
||||
|
||||
it('render empty when showFeedbackWidget and showGratitudeText are false', () => {
|
||||
mockUseFeedbackWidget({
|
||||
showFeedbackWidget: false,
|
||||
showGratitudeText: false,
|
||||
});
|
||||
useState.mockReturnValueOnce([true, jest.fn()]);
|
||||
const wrapper = shallow(<FeedbackWidget {...props} />);
|
||||
expect(wrapper.instance.findByType('div')[0].children.length).toBe(0);
|
||||
});
|
||||
|
||||
it('render feedback widget', () => {
|
||||
mockUseFeedbackWidget({
|
||||
showFeedbackWidget: true,
|
||||
showGratitudeText: false,
|
||||
});
|
||||
const wrapper = shallow(<FeedbackWidget {...props} />);
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('render gratitude text', () => {
|
||||
mockUseFeedbackWidget({
|
||||
showFeedbackWidget: false,
|
||||
showGratitudeText: true,
|
||||
});
|
||||
const wrapper = shallow(<FeedbackWidget {...props} />);
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
rateTranslationText: {
|
||||
id: 'feedbackWidget.rateTranslationText',
|
||||
defaultMessage: 'Rate this page translation',
|
||||
description: 'Title for the feedback widget action row.',
|
||||
},
|
||||
gratitudeText: {
|
||||
id: 'feedbackWidget.gratitudeText',
|
||||
defaultMessage: 'Thank you! Your feedback matters.',
|
||||
description: 'Title for secondary action row.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,82 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { createTranslationFeedback, getTranslationFeedback } from '../data/api';
|
||||
|
||||
const useFeedbackWidget = ({
|
||||
courseId,
|
||||
translationLanguage,
|
||||
unitId,
|
||||
userId,
|
||||
}) => {
|
||||
const [showFeedbackWidget, setShowFeedbackWidget] = useState(false);
|
||||
const [showGratitudeText, setShowGratitudeText] = useState(false);
|
||||
|
||||
const closeFeedbackWidget = useCallback(() => {
|
||||
setShowFeedbackWidget(false);
|
||||
}, [setShowFeedbackWidget]);
|
||||
|
||||
const openFeedbackWidget = useCallback(() => {
|
||||
setShowFeedbackWidget(true);
|
||||
}, [setShowFeedbackWidget]);
|
||||
|
||||
useEffect(async () => {
|
||||
const translationFeedback = await getTranslationFeedback({
|
||||
courseId,
|
||||
translationLanguage,
|
||||
unitId,
|
||||
userId,
|
||||
});
|
||||
setShowFeedbackWidget(!translationFeedback);
|
||||
}, [
|
||||
courseId,
|
||||
translationLanguage,
|
||||
unitId,
|
||||
userId,
|
||||
]);
|
||||
|
||||
const openGratitudeText = useCallback(() => {
|
||||
setShowGratitudeText(true);
|
||||
setTimeout(() => {
|
||||
setShowGratitudeText(false);
|
||||
}, 3000);
|
||||
}, [setShowGratitudeText]);
|
||||
|
||||
const sendFeedback = useCallback(async (feedbackValue) => {
|
||||
await createTranslationFeedback({
|
||||
courseId,
|
||||
feedbackValue,
|
||||
translationLanguage,
|
||||
unitId,
|
||||
userId,
|
||||
});
|
||||
closeFeedbackWidget();
|
||||
openGratitudeText();
|
||||
}, [
|
||||
courseId,
|
||||
translationLanguage,
|
||||
unitId,
|
||||
userId,
|
||||
closeFeedbackWidget,
|
||||
openGratitudeText,
|
||||
]);
|
||||
|
||||
const onThumbsUpClick = useCallback(() => {
|
||||
sendFeedback(true);
|
||||
}, [sendFeedback]);
|
||||
const onThumbsDownClick = useCallback(() => {
|
||||
sendFeedback(false);
|
||||
}, [sendFeedback]);
|
||||
|
||||
return {
|
||||
closeFeedbackWidget,
|
||||
openFeedbackWidget,
|
||||
openGratitudeText,
|
||||
sendFeedback,
|
||||
showFeedbackWidget,
|
||||
showGratitudeText,
|
||||
onThumbsUpClick,
|
||||
onThumbsDownClick,
|
||||
};
|
||||
};
|
||||
|
||||
export default useFeedbackWidget;
|
||||
@@ -1,163 +0,0 @@
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
|
||||
import useFeedbackWidget from './useFeedbackWidget';
|
||||
import { createTranslationFeedback, getTranslationFeedback } from '../data/api';
|
||||
|
||||
jest.mock('../data/api', () => ({
|
||||
createTranslationFeedback: jest.fn(),
|
||||
getTranslationFeedback: jest.fn(),
|
||||
}));
|
||||
|
||||
const initialProps = {
|
||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
translationLanguage: 'es',
|
||||
unitId: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc',
|
||||
userId: 3,
|
||||
};
|
||||
|
||||
const newProps = {
|
||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
translationLanguage: 'fr',
|
||||
unitId: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc',
|
||||
userId: 3,
|
||||
};
|
||||
|
||||
describe('useFeedbackWidget', () => {
|
||||
beforeEach(async () => {
|
||||
getTranslationFeedback.mockReturnValue('');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('closeFeedbackWidget behavior', () => {
|
||||
const { result, waitFor } = renderHook(() => useFeedbackWidget(initialProps));
|
||||
waitFor(() => expect(result.current.showFeedbackWidget.toBe(true)));
|
||||
act(() => {
|
||||
result.current.closeFeedbackWidget();
|
||||
});
|
||||
expect(result.current.showFeedbackWidget).toBe(false);
|
||||
});
|
||||
|
||||
test('openFeedbackWidget behavior', () => {
|
||||
const { result } = renderHook(() => useFeedbackWidget(initialProps));
|
||||
act(() => {
|
||||
result.current.closeFeedbackWidget();
|
||||
});
|
||||
expect(result.current.showFeedbackWidget).toBe(false);
|
||||
act(() => {
|
||||
result.current.openFeedbackWidget();
|
||||
});
|
||||
expect(result.current.showFeedbackWidget).toBe(true);
|
||||
});
|
||||
|
||||
test('openGratitudeText behavior', async () => {
|
||||
const { result, waitFor } = renderHook(() => useFeedbackWidget(initialProps));
|
||||
|
||||
expect(result.current.showGratitudeText).toBe(false);
|
||||
act(() => {
|
||||
result.current.openGratitudeText();
|
||||
});
|
||||
expect(result.current.showGratitudeText).toBe(true);
|
||||
// Wait for 3 seconds to hide the gratitude text
|
||||
waitFor(() => {
|
||||
expect(result.current.showGratitudeText).toBe(false);
|
||||
}, { timeout: 3000 });
|
||||
});
|
||||
|
||||
test('sendFeedback behavior', () => {
|
||||
const { result, waitFor } = renderHook(() => useFeedbackWidget(initialProps));
|
||||
const feedbackValue = true;
|
||||
|
||||
waitFor(() => expect(result.current.showFeedbackWidget.toBe(true)));
|
||||
|
||||
expect(result.current.showGratitudeText).toBe(false);
|
||||
act(() => {
|
||||
result.current.sendFeedback(feedbackValue);
|
||||
});
|
||||
|
||||
waitFor(() => {
|
||||
expect(result.current.showFeedbackWidget).toBe(false);
|
||||
expect(result.current.showGratitudeText).toBe(true);
|
||||
});
|
||||
|
||||
expect(createTranslationFeedback).toHaveBeenCalledWith({
|
||||
courseId: initialProps.courseId,
|
||||
feedbackValue,
|
||||
translationLanguage: initialProps.translationLanguage,
|
||||
unitId: initialProps.unitId,
|
||||
userId: initialProps.userId,
|
||||
});
|
||||
|
||||
// Wait for 3 seconds to hide the gratitude text
|
||||
waitFor(() => {
|
||||
expect(result.current.showGratitudeText).toBe(false);
|
||||
}, { timeout: 3000 });
|
||||
});
|
||||
|
||||
test('onThumbsUpClick behavior', () => {
|
||||
const { result } = renderHook(() => useFeedbackWidget(initialProps));
|
||||
|
||||
act(() => {
|
||||
result.current.onThumbsUpClick();
|
||||
});
|
||||
|
||||
expect(createTranslationFeedback).toHaveBeenCalledWith({
|
||||
courseId: initialProps.courseId,
|
||||
feedbackValue: true,
|
||||
translationLanguage: initialProps.translationLanguage,
|
||||
unitId: initialProps.unitId,
|
||||
userId: initialProps.userId,
|
||||
});
|
||||
});
|
||||
|
||||
test('onThumbsDownClick behavior', () => {
|
||||
const { result } = renderHook(() => useFeedbackWidget(initialProps));
|
||||
|
||||
act(() => {
|
||||
result.current.onThumbsDownClick();
|
||||
});
|
||||
|
||||
expect(createTranslationFeedback).toHaveBeenCalledWith({
|
||||
courseId: initialProps.courseId,
|
||||
feedbackValue: false,
|
||||
translationLanguage: initialProps.translationLanguage,
|
||||
unitId: initialProps.unitId,
|
||||
userId: initialProps.userId,
|
||||
});
|
||||
});
|
||||
|
||||
test('fetch feedback on initialization', () => {
|
||||
const { waitFor } = renderHook(() => useFeedbackWidget(initialProps));
|
||||
waitFor(() => {
|
||||
expect(getTranslationFeedback).toHaveBeenCalledWith({
|
||||
courseId: initialProps.courseId,
|
||||
translationLanguage: initialProps.translationLanguage,
|
||||
unitId: initialProps.unitId,
|
||||
userId: initialProps.userId,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('fetch feedback on props update', () => {
|
||||
const { rerender, waitFor } = renderHook(() => useFeedbackWidget(initialProps));
|
||||
waitFor(() => {
|
||||
expect(getTranslationFeedback).toHaveBeenCalledWith({
|
||||
courseId: initialProps.courseId,
|
||||
translationLanguage: initialProps.translationLanguage,
|
||||
unitId: initialProps.unitId,
|
||||
userId: initialProps.userId,
|
||||
});
|
||||
});
|
||||
rerender(newProps);
|
||||
waitFor(() => {
|
||||
expect(getTranslationFeedback).toHaveBeenCalledWith({
|
||||
courseId: newProps.courseId,
|
||||
translationLanguage: newProps.translationLanguage,
|
||||
unitId: newProps.unitId,
|
||||
userId: newProps.userId,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,53 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useModel } from '@src/generic/model-store';
|
||||
import { VERIFIED_MODES } from '@src/constants';
|
||||
|
||||
import TranslationSelection from './translation-selection';
|
||||
import { fetchTranslationConfig } from './data/api';
|
||||
|
||||
const UnitTranslationPlugin = ({ id, courseId, unitId }) => {
|
||||
const { language, enrollmentMode } = useModel('coursewareMeta', courseId);
|
||||
const { isStaff } = useModel('courseHomeMeta', courseId);
|
||||
const [translationConfig, setTranslationConfig] = useState({
|
||||
enabled: false,
|
||||
availableLanguages: [],
|
||||
});
|
||||
|
||||
const hasVerifiedEnrollment = isStaff || (
|
||||
enrollmentMode !== null
|
||||
&& enrollmentMode !== undefined
|
||||
&& VERIFIED_MODES.includes(enrollmentMode)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasVerifiedEnrollment) {
|
||||
fetchTranslationConfig(courseId).then(setTranslationConfig);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const { enabled, availableLanguages } = translationConfig;
|
||||
|
||||
if (!hasVerifiedEnrollment || !enabled || !language || !availableLanguages.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TranslationSelection
|
||||
id={id}
|
||||
courseId={courseId}
|
||||
language={language}
|
||||
availableLanguages={availableLanguages}
|
||||
unitId={unitId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
UnitTranslationPlugin.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
unitId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default UnitTranslationPlugin;
|
||||
@@ -1,102 +0,0 @@
|
||||
import { when } from 'jest-when';
|
||||
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import { useState } from 'react';
|
||||
import { useModel } from '@src/generic/model-store';
|
||||
|
||||
import UnitTranslationPlugin from './index';
|
||||
|
||||
jest.mock('@src/generic/model-store');
|
||||
jest.mock('./data/api', () => ({
|
||||
fetchTranslationConfig: jest.fn(),
|
||||
}));
|
||||
jest.mock('./translation-selection', () => 'TranslationSelection');
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useState: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('<UnitTranslationPlugin />', () => {
|
||||
const props = {
|
||||
id: 'id',
|
||||
courseId: 'courseId',
|
||||
unitId: 'unitId',
|
||||
};
|
||||
const mockInitialState = ({
|
||||
enabled = true,
|
||||
availableLanguages = ['en'],
|
||||
language = 'en',
|
||||
enrollmentMode = 'verified',
|
||||
isStaff = false,
|
||||
}) => {
|
||||
useState.mockReturnValueOnce([{ enabled, availableLanguages }, jest.fn()]);
|
||||
|
||||
when(useModel)
|
||||
.calledWith('coursewareMeta', props.courseId)
|
||||
.mockReturnValueOnce({ language, enrollmentMode });
|
||||
|
||||
when(useModel)
|
||||
.calledWith('courseHomeMeta', props.courseId)
|
||||
.mockReturnValueOnce({ isStaff });
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('render empty when translation is not enabled', () => {
|
||||
mockInitialState({ enabled: false });
|
||||
|
||||
const wrapper = shallow(<UnitTranslationPlugin {...props} />);
|
||||
|
||||
expect(wrapper.isEmptyRender()).toBe(true);
|
||||
});
|
||||
it('render empty when available languages is empty', () => {
|
||||
mockInitialState({
|
||||
availableLanguages: [],
|
||||
});
|
||||
|
||||
const wrapper = shallow(<UnitTranslationPlugin {...props} />);
|
||||
|
||||
expect(wrapper.isEmptyRender()).toBe(true);
|
||||
});
|
||||
|
||||
it('render empty when course language has not been set', () => {
|
||||
mockInitialState({
|
||||
language: null,
|
||||
});
|
||||
|
||||
const wrapper = shallow(<UnitTranslationPlugin {...props} />);
|
||||
|
||||
expect(wrapper.isEmptyRender()).toBe(true);
|
||||
});
|
||||
|
||||
it('render empty when student is enroll as verified', () => {
|
||||
mockInitialState({
|
||||
enrollmentMode: 'audit',
|
||||
});
|
||||
|
||||
const wrapper = shallow(<UnitTranslationPlugin {...props} />);
|
||||
|
||||
expect(wrapper.isEmptyRender()).toBe(true);
|
||||
});
|
||||
|
||||
it('render translation when the user is staff', () => {
|
||||
mockInitialState({
|
||||
enrollmentMode: 'audit',
|
||||
isStaff: true,
|
||||
});
|
||||
|
||||
const wrapper = shallow(<UnitTranslationPlugin {...props} />);
|
||||
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('render TranslationSelection when translation is enabled and language is available', () => {
|
||||
mockInitialState({});
|
||||
|
||||
const wrapper = shallow(<UnitTranslationPlugin {...props} />);
|
||||
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,82 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
StandardModal,
|
||||
ActionRow,
|
||||
Button,
|
||||
Icon,
|
||||
ListBox,
|
||||
ListBoxOption,
|
||||
} from '@openedx/paragon';
|
||||
import { Check } from '@openedx/paragon/icons';
|
||||
|
||||
import useTranslationModal from './useTranslationModal';
|
||||
import messages from './messages';
|
||||
|
||||
import './TranslationModal.scss';
|
||||
|
||||
const TranslationModal = ({
|
||||
isOpen,
|
||||
close,
|
||||
selectedLanguage,
|
||||
setSelectedLanguage,
|
||||
availableLanguages,
|
||||
}) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { selectedIndex, setSelectedIndex, onSubmit } = useTranslationModal({
|
||||
selectedLanguage,
|
||||
setSelectedLanguage,
|
||||
close,
|
||||
availableLanguages,
|
||||
});
|
||||
|
||||
return (
|
||||
<StandardModal
|
||||
title={formatMessage(messages.languageSelectionModalTitle)}
|
||||
isOpen={isOpen}
|
||||
onClose={close}
|
||||
footerNode={(
|
||||
<ActionRow>
|
||||
<ActionRow.Spacer />
|
||||
<Button variant="tertiary" onClick={close}>
|
||||
{formatMessage(messages.cancelButtonText)}
|
||||
</Button>
|
||||
<Button onClick={onSubmit}>
|
||||
{formatMessage(messages.submitButtonText)}
|
||||
</Button>
|
||||
</ActionRow>
|
||||
)}
|
||||
>
|
||||
<ListBox className="listbox-container">
|
||||
{availableLanguages.map(({ code, label }, index) => (
|
||||
<ListBoxOption
|
||||
className="d-flex justify-content-between"
|
||||
key={code}
|
||||
selectedOptionIndex={selectedIndex}
|
||||
onSelect={() => setSelectedIndex(index)}
|
||||
>
|
||||
{label}
|
||||
{selectedIndex === index && <Icon src={Check} />}
|
||||
</ListBoxOption>
|
||||
))}
|
||||
</ListBox>
|
||||
</StandardModal>
|
||||
);
|
||||
};
|
||||
|
||||
TranslationModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
close: PropTypes.func.isRequired,
|
||||
selectedLanguage: PropTypes.string.isRequired,
|
||||
setSelectedLanguage: PropTypes.func.isRequired,
|
||||
availableLanguages: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
code: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
}),
|
||||
).isRequired,
|
||||
};
|
||||
|
||||
export default TranslationModal;
|
||||
@@ -1,7 +0,0 @@
|
||||
.listbox-container {
|
||||
max-height: 400px;
|
||||
|
||||
:last-child {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import TranslationModal from './TranslationModal';
|
||||
|
||||
jest.mock('./useTranslationModal', () => ({
|
||||
__esModule: true,
|
||||
default: () => ({
|
||||
selectedIndex: 0,
|
||||
setSelectedIndex: jest.fn(),
|
||||
onSubmit: jest.fn().mockName('onSubmit'),
|
||||
}),
|
||||
}));
|
||||
jest.mock('@openedx/paragon', () => jest.requireActual('@edx/react-unit-test-utils').mockComponents({
|
||||
StandardModal: 'StandardModal',
|
||||
ActionRow: {
|
||||
Spacer: 'Spacer',
|
||||
},
|
||||
Button: 'Button',
|
||||
Icon: 'Icon',
|
||||
ListBox: 'ListBox',
|
||||
ListBoxOption: 'ListBoxOption',
|
||||
}));
|
||||
jest.mock('@openedx/paragon/icons', () => ({
|
||||
Check: jest.fn().mockName('icons.Check'),
|
||||
}));
|
||||
jest.mock('@edx/frontend-platform/i18n', () => {
|
||||
const i18n = jest.requireActual('@edx/frontend-platform/i18n');
|
||||
const { formatMessage } = jest.requireActual('@edx/react-unit-test-utils');
|
||||
return {
|
||||
...i18n,
|
||||
useIntl: jest.fn(() => ({
|
||||
formatMessage,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
describe('TranslationModal', () => {
|
||||
const props = {
|
||||
isOpen: true,
|
||||
close: jest.fn().mockName('close'),
|
||||
selectedLanguage: 'en',
|
||||
setSelectedLanguage: jest.fn().mockName('setSelectedLanguage'),
|
||||
availableLanguages: [
|
||||
{
|
||||
code: 'en',
|
||||
label: 'English',
|
||||
},
|
||||
{
|
||||
code: 'es',
|
||||
label: 'Spanish',
|
||||
},
|
||||
],
|
||||
};
|
||||
it('renders correctly', () => {
|
||||
const wrapper = shallow(<TranslationModal {...props} />);
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
expect(wrapper.instance.findByType('ListBoxOption')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TranslationModal renders correctly 1`] = `
|
||||
<StandardModal
|
||||
footerNode={
|
||||
<ActionRow>
|
||||
<Spacer />
|
||||
<Button
|
||||
onClick={[MockFunction close]}
|
||||
variant="tertiary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={[MockFunction onSubmit]}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</ActionRow>
|
||||
}
|
||||
isOpen={true}
|
||||
onClose={[MockFunction close]}
|
||||
title="Translate this course"
|
||||
>
|
||||
<ListBox
|
||||
className="listbox-container"
|
||||
>
|
||||
<ListBoxOption
|
||||
className="d-flex justify-content-between"
|
||||
key="en"
|
||||
onSelect={[Function]}
|
||||
selectedOptionIndex={0}
|
||||
>
|
||||
English
|
||||
<Icon
|
||||
src={[MockFunction icons.Check]}
|
||||
/>
|
||||
</ListBoxOption>
|
||||
<ListBoxOption
|
||||
className="d-flex justify-content-between"
|
||||
key="es"
|
||||
onSelect={[Function]}
|
||||
selectedOptionIndex={0}
|
||||
>
|
||||
Spanish
|
||||
</ListBoxOption>
|
||||
</ListBox>
|
||||
</StandardModal>
|
||||
`;
|
||||
@@ -1,50 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<TranslationSelection /> renders 1`] = `
|
||||
<Fragment>
|
||||
<ProductTour
|
||||
tours={
|
||||
Array [
|
||||
Object {
|
||||
"abitrarily": "defined",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<IconButton
|
||||
alt="change-language"
|
||||
className="mr-2 mb-2 float-right"
|
||||
iconAs="Icon"
|
||||
id="translation-selection-button"
|
||||
onClick={[MockFunction open]}
|
||||
src="Language"
|
||||
variant="primary"
|
||||
/>
|
||||
<TranslationModal
|
||||
availableLanguages={
|
||||
Array [
|
||||
Object {
|
||||
"code": "en",
|
||||
"label": "English",
|
||||
},
|
||||
Object {
|
||||
"code": "es",
|
||||
"label": "Spanish",
|
||||
},
|
||||
]
|
||||
}
|
||||
close={[MockFunction close]}
|
||||
courseId="course-v1:edX+DemoX+Demo_Course"
|
||||
id="plugin-test-id"
|
||||
isOpen={false}
|
||||
selectedLanguage="en"
|
||||
setSelectedLanguage={[MockFunction setSelectedLanguage]}
|
||||
/>
|
||||
<FeedbackWidget
|
||||
courseId="course-v1:edX+DemoX+Demo_Course"
|
||||
translationLanguage="en"
|
||||
unitId="unit-test-id"
|
||||
userId="123"
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -1,115 +0,0 @@
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { IconButton, Icon, ProductTour } from '@openedx/paragon';
|
||||
import { Language } from '@openedx/paragon/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { stringifyUrl } from 'query-string';
|
||||
|
||||
import { registerOverrideMethod } from '@src/generic/plugin-store';
|
||||
|
||||
import TranslationModal from './TranslationModal';
|
||||
import useTranslationTour from './useTranslationTour';
|
||||
import useSelectLanguage from './useSelectLanguage';
|
||||
import FeedbackWidget from '../feedback-widget';
|
||||
|
||||
const TranslationSelection = ({
|
||||
id, courseId, language, availableLanguages, unitId,
|
||||
}) => {
|
||||
const {
|
||||
authenticatedUser: { userId },
|
||||
} = useContext(AppContext);
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
translationTour, isOpen, open, close,
|
||||
} = useTranslationTour();
|
||||
|
||||
const { selectedLanguage, setSelectedLanguage } = useSelectLanguage({
|
||||
courseId,
|
||||
language,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if ((courseId && language && selectedLanguage && unitId && userId) && (language !== selectedLanguage)) {
|
||||
const eventName = 'edx.whole_course_translations.translation_requested';
|
||||
const eventProperties = {
|
||||
courseId,
|
||||
sourceLanguage: language,
|
||||
targetLanguage: selectedLanguage,
|
||||
unitId,
|
||||
userId,
|
||||
};
|
||||
sendTrackEvent(eventName, eventProperties);
|
||||
}
|
||||
}, [courseId, language, selectedLanguage, unitId, userId]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
registerOverrideMethod({
|
||||
pluginName: id,
|
||||
methodName: 'getIFrameUrl',
|
||||
method: (iframeUrl) => {
|
||||
const finalUrl = stringifyUrl({
|
||||
url: iframeUrl,
|
||||
query: {
|
||||
...(language
|
||||
&& selectedLanguage
|
||||
&& language !== selectedLanguage && {
|
||||
src_lang: language,
|
||||
dest_lang: selectedLanguage,
|
||||
}),
|
||||
},
|
||||
});
|
||||
return finalUrl;
|
||||
},
|
||||
}),
|
||||
);
|
||||
}, [language, selectedLanguage]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProductTour tours={[translationTour]} />
|
||||
<IconButton
|
||||
src={Language}
|
||||
iconAs={Icon}
|
||||
alt="change-language"
|
||||
onClick={open}
|
||||
variant="primary"
|
||||
className="mr-2 mb-2 float-right"
|
||||
id="translation-selection-button"
|
||||
/>
|
||||
<TranslationModal
|
||||
isOpen={isOpen}
|
||||
close={close}
|
||||
courseId={courseId}
|
||||
selectedLanguage={selectedLanguage}
|
||||
setSelectedLanguage={setSelectedLanguage}
|
||||
availableLanguages={availableLanguages}
|
||||
id={id}
|
||||
/>
|
||||
<FeedbackWidget
|
||||
courseId={courseId}
|
||||
translationLanguage={selectedLanguage}
|
||||
unitId={unitId}
|
||||
userId={userId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
TranslationSelection.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
unitId: PropTypes.string.isRequired,
|
||||
language: PropTypes.string.isRequired,
|
||||
availableLanguages: PropTypes.arrayOf(PropTypes.shape({
|
||||
code: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
};
|
||||
|
||||
TranslationSelection.defaultProps = {};
|
||||
|
||||
export default TranslationSelection;
|
||||
@@ -1,84 +0,0 @@
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import TranslationSelection from './index';
|
||||
import { initializeTestStore, render } from '../../../src/setupTest';
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useContext: jest.fn().mockName('useContext').mockReturnValue({
|
||||
authenticatedUser: {
|
||||
userId: '123',
|
||||
},
|
||||
}),
|
||||
}));
|
||||
jest.mock('@openedx/paragon', () => ({
|
||||
IconButton: 'IconButton',
|
||||
Icon: 'Icon',
|
||||
ProductTour: 'ProductTour',
|
||||
}));
|
||||
jest.mock('@openedx/paragon/icons', () => ({
|
||||
Language: 'Language',
|
||||
}));
|
||||
jest.mock('./useTranslationTour', () => () => ({
|
||||
translationTour: {
|
||||
abitrarily: 'defined',
|
||||
},
|
||||
isOpen: false,
|
||||
open: jest.fn().mockName('open'),
|
||||
close: jest.fn().mockName('close'),
|
||||
}));
|
||||
jest.mock('react-redux', () => ({
|
||||
useDispatch: jest.fn().mockName('useDispatch'),
|
||||
}));
|
||||
jest.mock('@src/generic/plugin-store', () => ({
|
||||
registerOverrideMethod: jest.fn().mockName('registerOverrideMethod'),
|
||||
}));
|
||||
jest.mock('./TranslationModal', () => 'TranslationModal');
|
||||
jest.mock('./useSelectLanguage', () => () => ({
|
||||
selectedLanguage: 'en',
|
||||
setSelectedLanguage: jest.fn().mockName('setSelectedLanguage'),
|
||||
}));
|
||||
jest.mock('../feedback-widget', () => 'FeedbackWidget');
|
||||
jest.mock('@edx/frontend-platform/analytics');
|
||||
|
||||
describe('<TranslationSelection />', () => {
|
||||
const props = {
|
||||
id: 'plugin-test-id',
|
||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
language: 'en',
|
||||
availableLanguages: [
|
||||
{
|
||||
code: 'en',
|
||||
label: 'English',
|
||||
},
|
||||
{
|
||||
code: 'es',
|
||||
label: 'Spanish',
|
||||
},
|
||||
],
|
||||
unitId: 'unit-test-id',
|
||||
};
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<TranslationSelection {...props} />);
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
});
|
||||
it('does not send track event when source != target language', async () => {
|
||||
await initializeTestStore();
|
||||
render(<TranslationSelection {...props} />);
|
||||
expect(sendTrackEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
it('sends track event when source != target language', async () => {
|
||||
await initializeTestStore();
|
||||
render(<TranslationSelection {...props} />);
|
||||
const eventName = 'edx.whole_course_translations.translation_requested';
|
||||
const eventProperties = {
|
||||
courseId: props.courseId,
|
||||
sourceLanguage: props.language,
|
||||
targetLanguage: 'en',
|
||||
unitId: props.unitId,
|
||||
userId: '123',
|
||||
};
|
||||
expect(sendTrackEvent).not.toHaveBeenCalledWith(eventName, eventProperties);
|
||||
});
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
translationTourModalTitle: {
|
||||
id: 'translationSelection.translationTourModalTitle',
|
||||
defaultMessage: 'New translation feature!',
|
||||
description: 'Title for the translation modal.',
|
||||
},
|
||||
translationTourModalBody: {
|
||||
id: 'translationSelection.translationTourModalBody',
|
||||
defaultMessage: 'Now you can easily translate course content.',
|
||||
description: 'Body for the translation modal.',
|
||||
},
|
||||
tryItButtonText: {
|
||||
id: 'translationSelection.tryItButtonText',
|
||||
defaultMessage: 'Try it',
|
||||
description: 'Button text for the translation modal.',
|
||||
},
|
||||
dismissButtonText: {
|
||||
id: 'translationSelection.dismissButtonText',
|
||||
defaultMessage: 'Dismiss',
|
||||
description: 'Button text for the translation modal.',
|
||||
},
|
||||
languageSelectionModalTitle: {
|
||||
id: 'translationSelection.languageSelectionModalTitle',
|
||||
defaultMessage: 'Translate this course',
|
||||
description: 'Title for the translation modal.',
|
||||
},
|
||||
cancelButtonText: {
|
||||
id: 'translationSelection.cancelButtonText',
|
||||
defaultMessage: 'Cancel',
|
||||
description: 'Button text for the translation modal.',
|
||||
},
|
||||
submitButtonText: {
|
||||
id: 'translationSelection.submitButtonText',
|
||||
defaultMessage: 'Submit',
|
||||
description: 'Button text for the translation modal.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,35 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils';
|
||||
import {
|
||||
getLocalStorage,
|
||||
setLocalStorage,
|
||||
} from '@src/data/localStorage';
|
||||
|
||||
export const selectedLanguageKey = 'selectedLanguages';
|
||||
|
||||
export const stateKeys = StrictDict({
|
||||
selectedLanguage: 'selectedLanguage',
|
||||
});
|
||||
|
||||
const useSelectLanguage = ({ courseId, language }) => {
|
||||
const selectedLanguageItem = getLocalStorage(selectedLanguageKey) || {};
|
||||
const [selectedLanguage, updateSelectedLanguage] = useKeyedState(
|
||||
stateKeys.selectedLanguage,
|
||||
selectedLanguageItem[courseId] || language,
|
||||
);
|
||||
|
||||
const setSelectedLanguage = useCallback((newSelectedLanguage) => {
|
||||
setLocalStorage(selectedLanguageKey, {
|
||||
...selectedLanguageItem,
|
||||
[courseId]: newSelectedLanguage,
|
||||
});
|
||||
updateSelectedLanguage(newSelectedLanguage);
|
||||
});
|
||||
|
||||
return {
|
||||
selectedLanguage,
|
||||
setSelectedLanguage,
|
||||
};
|
||||
};
|
||||
|
||||
export default useSelectLanguage;
|
||||
@@ -1,63 +0,0 @@
|
||||
import { mockUseKeyedState } from '@edx/react-unit-test-utils';
|
||||
import {
|
||||
getLocalStorage,
|
||||
setLocalStorage,
|
||||
} from '@src/data/localStorage';
|
||||
|
||||
import useSelectLanguage, {
|
||||
stateKeys,
|
||||
selectedLanguageKey,
|
||||
} from './useSelectLanguage';
|
||||
|
||||
const state = mockUseKeyedState(stateKeys);
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useCallback: jest.fn((cb, prereqs) => (...args) => [
|
||||
cb(...args),
|
||||
{ cb, prereqs },
|
||||
]),
|
||||
}));
|
||||
jest.mock('@src/data/localStorage', () => ({
|
||||
getLocalStorage: jest.fn(),
|
||||
setLocalStorage: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('useSelectLanguage', () => {
|
||||
const props = {
|
||||
courseId: 'test-course-id',
|
||||
language: 'en',
|
||||
};
|
||||
const languages = [
|
||||
{ code: 'en', label: 'English' },
|
||||
{ code: 'es', label: 'Spanish' },
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
state.mock();
|
||||
});
|
||||
afterEach(() => {
|
||||
state.resetVals();
|
||||
});
|
||||
|
||||
languages.forEach(({ code, label }) => {
|
||||
it(`initializes selectedLanguage to the selected language (${label})`, () => {
|
||||
getLocalStorage.mockReturnValueOnce({ [props.courseId]: code });
|
||||
const { selectedLanguage } = useSelectLanguage(props);
|
||||
|
||||
state.expectInitializedWith(stateKeys.selectedLanguage, code);
|
||||
expect(selectedLanguage).toBe(code);
|
||||
});
|
||||
});
|
||||
|
||||
test('setSelectedLanguage behavior', () => {
|
||||
const { setSelectedLanguage } = useSelectLanguage(props);
|
||||
|
||||
setSelectedLanguage('es');
|
||||
state.expectSetStateCalledWith(stateKeys.selectedLanguage, 'es');
|
||||
expect(setLocalStorage).toHaveBeenCalledWith(selectedLanguageKey, {
|
||||
[props.courseId]: 'es',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils';
|
||||
|
||||
export const stateKeys = StrictDict({
|
||||
selectedIndex: 'selectedIndex',
|
||||
});
|
||||
|
||||
const useTranslationModal = ({
|
||||
selectedLanguage, setSelectedLanguage, close, availableLanguages,
|
||||
}) => {
|
||||
const [selectedIndex, setSelectedIndex] = useKeyedState(
|
||||
stateKeys.selectedIndex,
|
||||
availableLanguages.findIndex((lang) => lang.code === selectedLanguage),
|
||||
);
|
||||
|
||||
const onSubmit = useCallback(() => {
|
||||
const newSelectedLanguage = availableLanguages[selectedIndex].code;
|
||||
setSelectedLanguage(newSelectedLanguage);
|
||||
close();
|
||||
}, [selectedIndex]);
|
||||
|
||||
return {
|
||||
selectedIndex,
|
||||
setSelectedIndex,
|
||||
onSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
export default useTranslationModal;
|
||||
@@ -1,49 +0,0 @@
|
||||
import { mockUseKeyedState } from '@edx/react-unit-test-utils';
|
||||
|
||||
import useTranslationModal, { stateKeys } from './useTranslationModal';
|
||||
|
||||
const state = mockUseKeyedState(stateKeys);
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useCallback: jest.fn((cb, prereqs) => (...args) => ([
|
||||
cb(...args), { cb, prereqs },
|
||||
])),
|
||||
}));
|
||||
|
||||
describe('useTranslationModal', () => {
|
||||
const props = {
|
||||
selectedLanguage: 'en',
|
||||
setSelectedLanguage: jest.fn(),
|
||||
close: jest.fn(),
|
||||
availableLanguages: [
|
||||
{ code: 'en', label: 'English' },
|
||||
{ code: 'es', label: 'Spanish' },
|
||||
],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
state.mock();
|
||||
});
|
||||
afterEach(() => {
|
||||
state.resetVals();
|
||||
});
|
||||
|
||||
it('initializes selectedIndex to the index of the selected language', () => {
|
||||
const { selectedIndex } = useTranslationModal(props);
|
||||
|
||||
state.expectInitializedWith(stateKeys.selectedIndex, 0);
|
||||
expect(selectedIndex).toBe(0);
|
||||
});
|
||||
|
||||
it('onSubmit updates the selected language and closes the modal', () => {
|
||||
const { onSubmit } = useTranslationModal({
|
||||
...props,
|
||||
selectedLanguage: 'es',
|
||||
});
|
||||
onSubmit();
|
||||
expect(props.setSelectedLanguage).toHaveBeenCalledWith('es');
|
||||
expect(props.close).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,62 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useToggle } from '@openedx/paragon';
|
||||
import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const hasSeenTranslationTourKey = 'hasSeenTranslationTour';
|
||||
|
||||
export const stateKeys = StrictDict({
|
||||
showTranslationTour: 'showTranslationTour',
|
||||
});
|
||||
|
||||
const useTranslationTour = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const [isTourEnabled, setIsTourEnabled] = useKeyedState(
|
||||
stateKeys.showTranslationTour,
|
||||
global.localStorage.getItem(hasSeenTranslationTourKey) !== 'true',
|
||||
);
|
||||
const [isOpen, open, close] = useToggle(false);
|
||||
|
||||
const endTour = useCallback(() => {
|
||||
global.localStorage.setItem(hasSeenTranslationTourKey, 'true');
|
||||
setIsTourEnabled(false);
|
||||
}, [isTourEnabled, setIsTourEnabled]);
|
||||
|
||||
const tryIt = useCallback(() => {
|
||||
endTour();
|
||||
open();
|
||||
}, [endTour, open]);
|
||||
|
||||
const translationTour = isTourEnabled
|
||||
? {
|
||||
tourId: 'translation',
|
||||
enabled: isTourEnabled,
|
||||
onDismiss: endTour,
|
||||
onEnd: tryIt,
|
||||
checkpoints: [
|
||||
{
|
||||
title: formatMessage(messages.translationTourModalTitle),
|
||||
body: formatMessage(messages.translationTourModalBody),
|
||||
placement: 'bottom',
|
||||
target: '#translation-selection-button',
|
||||
showDismissButton: true,
|
||||
endButtonText: formatMessage(messages.tryItButtonText),
|
||||
dismissButtonText: formatMessage(messages.dismissButtonText),
|
||||
},
|
||||
],
|
||||
}
|
||||
: {};
|
||||
|
||||
return {
|
||||
translationTour,
|
||||
isOpen,
|
||||
open,
|
||||
close,
|
||||
};
|
||||
};
|
||||
|
||||
export default useTranslationTour;
|
||||
@@ -1,95 +0,0 @@
|
||||
import { mockUseKeyedState } from '@edx/react-unit-test-utils';
|
||||
import { useToggle } from '@openedx/paragon';
|
||||
|
||||
import useTranslationTour, { stateKeys } from './useTranslationTour';
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useCallback: jest.fn((cb, prereqs) => () => {
|
||||
cb();
|
||||
return { useCallback: { cb, prereqs } };
|
||||
}),
|
||||
}));
|
||||
jest.mock('@openedx/paragon', () => ({
|
||||
useToggle: jest.fn(),
|
||||
}));
|
||||
jest.mock('@edx/frontend-platform/i18n', () => {
|
||||
const i18n = jest.requireActual('@edx/frontend-platform/i18n');
|
||||
const { formatMessage } = jest.requireActual('@edx/react-unit-test-utils');
|
||||
// this provide consistent for the test on different platform/timezone
|
||||
const formatDate = jest.fn(date => new Date(date).toISOString()).mockName('useIntl.formatDate');
|
||||
return {
|
||||
...i18n,
|
||||
useIntl: jest.fn(() => ({
|
||||
formatMessage,
|
||||
formatDate,
|
||||
})),
|
||||
defineMessages: m => m,
|
||||
FormattedMessage: () => 'FormattedMessage',
|
||||
};
|
||||
});
|
||||
jest.mock('@src/data/localStorage', () => ({
|
||||
getLocalStorage: jest.fn(),
|
||||
setLocalStorage: jest.fn(),
|
||||
}));
|
||||
|
||||
const state = mockUseKeyedState(stateKeys);
|
||||
|
||||
describe('useTranslationSelection', () => {
|
||||
const mockLocalStroage = {
|
||||
getItem: jest.fn(),
|
||||
setItem: jest.fn(),
|
||||
};
|
||||
|
||||
const toggleOpen = jest.fn();
|
||||
const toggleClose = jest.fn();
|
||||
|
||||
useToggle.mockReturnValue([false, toggleOpen, toggleClose]);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
state.mock();
|
||||
window.localStorage = mockLocalStroage;
|
||||
});
|
||||
afterEach(() => {
|
||||
state.resetVals();
|
||||
delete window.localStorage;
|
||||
});
|
||||
|
||||
it('do not have translation tour if user already seen it', () => {
|
||||
mockLocalStroage.getItem.mockReturnValueOnce('not seen');
|
||||
const { translationTour } = useTranslationTour();
|
||||
|
||||
expect(translationTour.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('show translation tour if user has not seen it', () => {
|
||||
mockLocalStroage.getItem.mockReturnValueOnce('true');
|
||||
const { translationTour } = useTranslationTour();
|
||||
|
||||
expect(translationTour).toMatchObject({});
|
||||
});
|
||||
test('open and close as pass from useToggle', () => {
|
||||
const { isOpen, open, close } = useTranslationTour();
|
||||
expect(isOpen).toBe(false);
|
||||
expect(toggleOpen).toBe(open);
|
||||
expect(toggleClose).toBe(close);
|
||||
});
|
||||
test('end tour on dismiss button click', () => {
|
||||
mockLocalStroage.getItem.mockReturnValueOnce('not seen');
|
||||
const { translationTour } = useTranslationTour();
|
||||
translationTour.onDismiss();
|
||||
expect(mockLocalStroage.setItem).toHaveBeenCalledWith(
|
||||
'hasSeenTranslationTour',
|
||||
'true',
|
||||
);
|
||||
state.expectSetStateCalledWith(stateKeys.showTranslationTour, false);
|
||||
});
|
||||
test('end tour and open modal on try it button click', () => {
|
||||
mockLocalStroage.getItem.mockReturnValueOnce('not seen');
|
||||
const { translationTour } = useTranslationTour();
|
||||
translationTour.onEnd();
|
||||
state.expectSetStateCalledWith(stateKeys.showTranslationTour, false);
|
||||
expect(toggleOpen).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -14,6 +14,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useSelector } from 'react-redux';
|
||||
import SequenceExamWrapper from '@edx/frontend-lib-special-exams';
|
||||
import { breakpoints, useWindowSize } from '@openedx/paragon';
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
import PageLoading from '../../../generic/PageLoading';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
@@ -200,7 +201,13 @@ const Sequence = ({
|
||||
</div>
|
||||
{enableNewSidebar === 'true' ? <NewSidebar /> : <Sidebar />}
|
||||
</div>
|
||||
<div id="whole-course-translation-feedback-widget" />
|
||||
<PluginSlot
|
||||
id="sequence_container_plugin"
|
||||
pluginProps={{
|
||||
courseId,
|
||||
unitId,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -150,7 +150,6 @@ initialize({
|
||||
handlers: {
|
||||
config: () => {
|
||||
mergeConfig({
|
||||
AI_TRANSLATIONS_URL: process.env.AI_TRANSLATIONS_URL || null,
|
||||
CONTACT_URL: process.env.CONTACT_URL || null,
|
||||
CREDENTIALS_BASE_URL: process.env.CREDENTIALS_BASE_URL || null,
|
||||
CREDIT_HELP_LINK_URL: process.env.CREDIT_HELP_LINK_URL || null,
|
||||
|
||||
@@ -6,7 +6,6 @@ const config = createConfig('webpack-dev');
|
||||
config.resolve.alias = {
|
||||
...config.resolve.alias,
|
||||
'@src': path.resolve(__dirname, 'src'),
|
||||
'@plugins': path.resolve(__dirname, 'plugins'),
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
@@ -18,7 +18,6 @@ config.plugins.push(
|
||||
config.resolve.alias = {
|
||||
...config.resolve.alias,
|
||||
'@src': path.resolve(__dirname, 'src'),
|
||||
'@plugins': path.resolve(__dirname, 'plugins'),
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
Reference in New Issue
Block a user