diff --git a/.gitignore b/.gitignore index f759f0926d2..7e41de355a5 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,5 @@ playwright/.cache/ coverage/ dump.rdb + +compiled-locales diff --git a/app/helpers/application_html_formatters_helper.rb b/app/helpers/application_html_formatters_helper.rb index efb0e2a675c..b7afe4e5ddf 100644 --- a/app/helpers/application_html_formatters_helper.rb +++ b/app/helpers/application_html_formatters_helper.rb @@ -86,13 +86,7 @@ def self.build_html_pipeline(custom_options) # List of video hosting site URLs to allow VIDEO_URL_WHITELIST = Regexp.union( /\A(?:https?:)?\/\/(?:www\.)?(?:m.)?youtube\.com\//, - /\A(?:https?:)?\/\/(?:www\.)?youtu.be\//, - /\A(?:https?:)?\/\/(?:www\.)?(?:player.)?vimeo\.com\//, - /\A(?:https?:)?\/\/(?:www\.)?vine\.co\//, - /\A(?:https?:)?\/\/(?:www\.)?instagram\.com\//, - /\A(?:https?:)?\/\/(?:www\.)?(?:geo.)?dailymotion\.com\//, - /\A(?:https?:)?\/\/(?:www\.)?dai\.ly\//, - /\A(?:https?:)?\/\/(?:www\.)?youku\.com\// + /\A(?:https?:)?\/\/(?:www\.)?youtu.be\// ).freeze OEMBED_WHITELIST_TRANSFORMER = lambda do |env| diff --git a/client/.babelrc b/client/.babelrc index 91b03cadc80..1c9ea75a9bb 100644 --- a/client/.babelrc +++ b/client/.babelrc @@ -14,14 +14,6 @@ "ast": true } ], - [ - "babel-plugin-import", - { - "libraryName": "lodash", - "libraryDirectory": "", - "camel2DashComponentName": false - } - ], [ "babel-plugin-import", { @@ -44,7 +36,14 @@ "env": { "production": { "plugins": [ - ["react-remove-properties", { "properties": ["data-testid"] }] + ["react-remove-properties", { "properties": ["data-testid"] }], + [ + "transform-react-remove-prop-types", + { + "mode": "remove", + "removeImport": true + } + ] ] }, "test": { diff --git a/client/app/__test__/setup.js b/client/app/__test__/setup.js index f34b92f8144..ce09d53127a 100644 --- a/client/app/__test__/setup.js +++ b/client/app/__test__/setup.js @@ -14,10 +14,6 @@ import './mocks/matchMedia'; Enzyme.configure({ adapter: new Adapter() }); -require('@babel/polyfill'); -// Our jquery is from CDN and loaded at runtime, so this is required in test. -const jQuery = require('jquery'); - const timeZone = 'Asia/Singapore'; const intlCache = createIntlCache(); const intl = createIntl({ locale: 'en', timeZone }, intlCache); @@ -47,8 +43,6 @@ const buildContextOptions = (store) => { global.courseId = courseId; global.window = window; global.muiTheme = muiTheme; -global.$ = jQuery; -global.jQuery = jQuery; global.buildContextOptions = buildContextOptions; window.history.pushState({}, '', `/courses/${courseId}`); diff --git a/client/app/bundles/common/DashboardPage.tsx b/client/app/bundles/common/DashboardPage.tsx index 4798315b944..be171e86977 100644 --- a/client/app/bundles/common/DashboardPage.tsx +++ b/client/app/bundles/common/DashboardPage.tsx @@ -2,7 +2,6 @@ import { defineMessages } from 'react-intl'; import { Navigate } from 'react-router-dom'; import { ArrowForward } from '@mui/icons-material'; import { Avatar, Stack, Typography } from '@mui/material'; -import moment from 'moment'; import { HomeLayoutCourseData } from 'types/home'; import { getCourseLogoUrl } from 'course/helper'; @@ -13,6 +12,7 @@ import { useAppContext } from 'lib/containers/AppContainer'; import { getUrlParameter } from 'lib/helpers/url-helpers'; import useItems from 'lib/hooks/items/useItems'; import useTranslation from 'lib/hooks/useTranslation'; +import moment from 'lib/moment'; import NewCourseButton from './components/NewCourseButton'; diff --git a/client/app/bundles/common/PrivacyPolicyPage/PrivacyPolicyPage.tsx b/client/app/bundles/common/PrivacyPolicyPage/PrivacyPolicyPage.tsx deleted file mode 100644 index 4d655be2330..00000000000 --- a/client/app/bundles/common/PrivacyPolicyPage/PrivacyPolicyPage.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import MarkdownPage from 'lib/components/core/layouts/MarkdownPage'; - -import privacyPolicy from './privacy-policy.md'; - -const PrivacyPolicyPage = (): JSX.Element => ( - -); - -export default PrivacyPolicyPage; diff --git a/client/app/bundles/common/PrivacyPolicyPage/index.tsx b/client/app/bundles/common/PrivacyPolicyPage/index.tsx index 58ce9822021..d9e6bce2ab1 100644 --- a/client/app/bundles/common/PrivacyPolicyPage/index.tsx +++ b/client/app/bundles/common/PrivacyPolicyPage/index.tsx @@ -1,10 +1,8 @@ -import { lazy, Suspense } from 'react'; import { defineMessages } from 'react-intl'; -const PrivacyPolicyPage = lazy( - () => - import(/* webpackChunkName: "PrivacyPolicyPage" */ './PrivacyPolicyPage'), -); +import MarkdownPage from 'lib/components/core/layouts/MarkdownPage'; + +import privacyPolicy from './privacy-policy.md'; const translations = defineMessages({ privacyPolicy: { @@ -13,12 +11,10 @@ const translations = defineMessages({ }, }); -const SuspensedPrivacyPolicyPage = (): JSX.Element => ( - - - +const PrivacyPolicyPage = (): JSX.Element => ( + ); const handle = translations.privacyPolicy; -export default Object.assign(SuspensedPrivacyPolicyPage, { handle }); +export default Object.assign(PrivacyPolicyPage, { handle }); diff --git a/client/app/bundles/common/TermsOfServicePage/TermsOfServicePage.tsx b/client/app/bundles/common/TermsOfServicePage/TermsOfServicePage.tsx deleted file mode 100644 index 938510a6efb..00000000000 --- a/client/app/bundles/common/TermsOfServicePage/TermsOfServicePage.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import MarkdownPage from 'lib/components/core/layouts/MarkdownPage'; - -import termsOfService from './terms-of-service.md'; - -const TermsOfServicePage = (): JSX.Element => ( - -); - -export default TermsOfServicePage; diff --git a/client/app/bundles/common/TermsOfServicePage/index.tsx b/client/app/bundles/common/TermsOfServicePage/index.tsx index 594e2c2c111..b92d2dc089c 100644 --- a/client/app/bundles/common/TermsOfServicePage/index.tsx +++ b/client/app/bundles/common/TermsOfServicePage/index.tsx @@ -1,10 +1,8 @@ -import { lazy, Suspense } from 'react'; import { defineMessages } from 'react-intl'; -const TermsOfServicePage = lazy( - () => - import(/* webpackChunkName: "TermsOfServicePage" */ './TermsOfServicePage'), -); +import MarkdownPage from 'lib/components/core/layouts/MarkdownPage'; + +import termsOfService from './terms-of-service.md'; const translations = defineMessages({ termsOfService: { @@ -13,12 +11,10 @@ const translations = defineMessages({ }, }); -const SuspensedTermsOfServicePage = (): JSX.Element => ( - - - +const TermsOfServicePage = (): JSX.Element => ( + ); const handle = translations.termsOfService; -export default Object.assign(SuspensedTermsOfServicePage, { handle }); +export default Object.assign(TermsOfServicePage, { handle }); diff --git a/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx b/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx index e0011aa1349..89288695de2 100644 --- a/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx +++ b/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx @@ -1,24 +1,16 @@ +import { useRef, useState } from 'react'; import { defineMessages } from 'react-intl'; -import { Button } from '@mui/material'; +import { LoadingButton } from '@mui/lab'; import CourseAPI from 'api/course'; import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; -require('jquery-ui/ui/widgets/sortable'); - interface AchievementReorderingProps { handleReordering: (state: boolean) => void; isReordering: boolean; } -const styles = { - AchievementReorderingButton: { - fontSize: 14, - marginRight: 12, - }, -}; - const translations = defineMessages({ startReorderAchievement: { id: 'course.achievement.AchievementReordering.startReorderAchievement', @@ -26,7 +18,7 @@ const translations = defineMessages({ }, endReorderAchievement: { id: 'course.achievement.AchievementReordering.endReorderAchievement', - defaultMessage: 'Save New Ordering', + defaultMessage: 'Done reordering', }, updateFailed: { id: 'course.achievement.AchievementReordering.updateFailed', @@ -38,12 +30,6 @@ const translations = defineMessages({ }, }); -// Serialise the ordered achievements as data for the API call. -function serializedOrdering(): string { - const options = { attribute: 'achievementid', key: 'achievement_order[]' }; - return $('tbody').first().sortable('serialize', options); -} - const AchievementReordering = ( props: AchievementReorderingProps, ): JSX.Element => { @@ -60,35 +46,80 @@ const AchievementReordering = ( } } + const [loadingSortable, setLoadingSortable] = useState(false); + + const sortableCallbacksRef = useRef<{ + enable: () => void; + disable: () => void; + }>(); + return ( - + ); }; diff --git a/client/app/bundles/course/assessment/components/AssessmentForm/__test__/index.test.tsx b/client/app/bundles/course/assessment/components/AssessmentForm/__test__/index.test.tsx index ca8aaa08382..aa752c80139 100644 --- a/client/app/bundles/course/assessment/components/AssessmentForm/__test__/index.test.tsx +++ b/client/app/bundles/course/assessment/components/AssessmentForm/__test__/index.test.tsx @@ -1,5 +1,5 @@ import { ComponentProps } from 'react'; -import { fireEvent, render, RenderResult } from 'test-utils'; +import { fireEvent, render, RenderResult, waitFor } from 'test-utils'; import AssessmentForm from '..'; @@ -61,143 +61,173 @@ beforeEach(() => { }); describe('', () => { - it('renders assessment details sections options', () => { + it('renders assessment details sections options', async () => { renderForm(); - expect(form.getByText('Assessment details')).toBeVisible(); - expect(form.getByLabelText('Starts at *')).toBeVisible(); - expect(form.getByLabelText('Ends at')).toBeVisible(); - expect(form.getByLabelText('Title *')).toHaveValue(INITIAL_VALUES.title); - expect(form.getByText('Description')).toBeVisible(); - expect(form.getByDisplayValue(INITIAL_VALUES.description)).toBeVisible(); + await waitFor(() => { + expect(form.getByText('Assessment details')).toBeVisible(); + expect(form.getByLabelText('Starts at *')).toBeVisible(); + expect(form.getByLabelText('Ends at')).toBeVisible(); + expect(form.getByLabelText('Title *')).toHaveValue(INITIAL_VALUES.title); + expect(form.getByText('Description')).toBeVisible(); + expect(form.getByDisplayValue(INITIAL_VALUES.description)).toBeVisible(); + }); }); - it('renders grading section options', () => { + it('renders grading section options', async () => { renderForm(); - expect(form.getByText('Grading')).toBeVisible(); - expect(form.getByText('Grading mode')).toBeVisible(); + await waitFor(() => { + expect(form.getByText('Grading')).toBeVisible(); + expect(form.getByText('Grading mode')).toBeVisible(); - expect(form.getByText('Autograded')).toBeVisible(); - expect(form.getByDisplayValue('autograded')).not.toBeChecked(); + expect(form.getByText('Autograded')).toBeVisible(); + expect(form.getByDisplayValue('autograded')).not.toBeChecked(); - expect(form.getByText('Manual')).toBeVisible(); - expect(form.getByDisplayValue('manual')).toBeChecked(); + expect(form.getByText('Manual')).toBeVisible(); + expect(form.getByDisplayValue('manual')).toBeChecked(); - expect(form.getByLabelText('Public test cases')).toBeChecked(); - expect(form.getByLabelText('Private test cases')).toBeChecked(); - expect(form.getByLabelText('Evaluation test cases')).not.toBeChecked(); + expect(form.getByLabelText('Public test cases')).toBeChecked(); + expect(form.getByLabelText('Private test cases')).toBeChecked(); + expect(form.getByLabelText('Evaluation test cases')).not.toBeChecked(); - expect( - form.getByLabelText('Enable delayed grade publication'), - ).not.toBeChecked(); + expect( + form.getByLabelText('Enable delayed grade publication'), + ).not.toBeChecked(); + }); }); - it('renders answers and test cases section options', () => { + it('renders answers and test cases section options', async () => { renderForm(); - expect(form.getByText('Answers and test cases')).toBeVisible(); - expect(form.getByLabelText('Allow to skip steps')).not.toBeChecked(); - expect( - form.getByLabelText('Allow submission with incorrect answers'), - ).not.toBeChecked(); - expect(form.getByLabelText('Show private test cases')).not.toBeChecked(); - expect(form.getByLabelText('Show evaluation test cases')).not.toBeChecked(); - expect(form.getByLabelText('Show MCQ/MRQ solution(s)')).not.toBeChecked(); + await waitFor(() => { + expect(form.getByText('Answers and test cases')).toBeVisible(); + expect(form.getByLabelText('Allow to skip steps')).not.toBeChecked(); + expect( + form.getByLabelText('Allow submission with incorrect answers'), + ).not.toBeChecked(); + expect(form.getByLabelText('Show private test cases')).not.toBeChecked(); + expect( + form.getByLabelText('Show evaluation test cases'), + ).not.toBeChecked(); + expect(form.getByLabelText('Show MCQ/MRQ solution(s)')).not.toBeChecked(); + }); }); - it('renders organization section options', () => { + it('renders organization section options', async () => { renderForm(); - expect(form.getByText('Organization')).toBeVisible(); - expect(form.getByText('Single Page')).toBeVisible(); + await waitFor(() => { + expect(form.getByText('Organization')).toBeVisible(); + expect(form.getByText('Single Page')).toBeVisible(); + }); }); - it('renders exams and access control section options', () => { + it('renders exams and access control section options', async () => { renderForm(); - expect(form.getByText('Exams and access control')).toBeVisible(); - expect( - form.getByLabelText('Block students from viewing finalized submissions'), - ).not.toBeChecked(); - expect(form.getByLabelText('Show MCQ submit result')).not.toBeChecked(); - expect(form.getByLabelText('Enable password protection')).not.toBeChecked(); + await waitFor(() => { + expect(form.getByText('Exams and access control')).toBeVisible(); + expect( + form.getByLabelText( + 'Block students from viewing finalized submissions', + ), + ).not.toBeChecked(); + expect(form.getByLabelText('Show MCQ submit result')).not.toBeChecked(); + expect( + form.getByLabelText('Enable password protection'), + ).not.toBeChecked(); + }); }); - it('does not render gamified options when course is not gamified', () => { + it('does not render gamified options when course is not gamified', async () => { renderForm(); - expect(form.queryByText('Gamification')).not.toBeInTheDocument(); - expect(form.queryByLabelText('Bonus ends at')).not.toBeInTheDocument(); - expect(form.queryByLabelText('Base EXP')).not.toBeInTheDocument(); - expect(form.queryByLabelText('Time Bonus EXP')).not.toBeInTheDocument(); + await waitFor(() => { + expect(form.queryByText('Gamification')).not.toBeInTheDocument(); + expect(form.queryByLabelText('Bonus ends at')).not.toBeInTheDocument(); + expect(form.queryByLabelText('Base EXP')).not.toBeInTheDocument(); + expect(form.queryByLabelText('Time Bonus EXP')).not.toBeInTheDocument(); + }); }); - it('renders gamified options when course is gamified', () => { + it('renders gamified options when course is gamified', async () => { props.gamified = true; renderForm(); - expect(form.getByText('Gamification')).toBeVisible(); - expect(form.getByLabelText('Bonus ends at')).toBeVisible(); - expect(form.getByLabelText('Base EXP')).toHaveValue( - INITIAL_VALUES.base_exp.toString(), - ); - expect(form.getByLabelText('Time Bonus EXP')).toHaveValue( - INITIAL_VALUES.time_bonus_exp.toString(), - ); + await waitFor(() => { + expect(form.getByText('Gamification')).toBeVisible(); + expect(form.getByLabelText('Bonus ends at')).toBeVisible(); + expect(form.getByLabelText('Base EXP')).toHaveValue( + INITIAL_VALUES.base_exp.toString(), + ); + expect(form.getByLabelText('Time Bonus EXP')).toHaveValue( + INITIAL_VALUES.time_bonus_exp.toString(), + ); + }); }); - it('does not render editing options when rendered in new assessment page', () => { - expect(form.queryByText('Visibility')).not.toBeInTheDocument(); - expect(form.queryByText('Published')).not.toBeInTheDocument(); - expect(form.queryByText('Draft')).not.toBeInTheDocument(); - expect(form.queryByText('Files')).not.toBeInTheDocument(); - expect(form.queryByText('Add Files')).not.toBeInTheDocument(); - expect(form.queryByText('Unlock conditions')).not.toBeInTheDocument(); - expect(form.queryByText('Add a condition')).not.toBeInTheDocument(); + it('does not render editing options when rendered in new assessment page', async () => { + await waitFor(() => { + expect(form.queryByText('Visibility')).not.toBeInTheDocument(); + expect(form.queryByText('Published')).not.toBeInTheDocument(); + expect(form.queryByText('Draft')).not.toBeInTheDocument(); + expect(form.queryByText('Files')).not.toBeInTheDocument(); + expect(form.queryByText('Add Files')).not.toBeInTheDocument(); + expect(form.queryByText('Unlock conditions')).not.toBeInTheDocument(); + expect(form.queryByText('Add a condition')).not.toBeInTheDocument(); + }); }); - it('renders editing options when rendered in edit assessment page', () => { + it('renders editing options when rendered in edit assessment page', async () => { props.editing = true; renderForm(); - expect(form.getByText('Visibility')).toBeVisible(); - expect(form.getByText('Published')).toBeVisible(); - expect(form.getByDisplayValue('published')).not.toBeChecked(); - expect(form.getByText('Draft')).toBeVisible(); - expect(form.getByDisplayValue('draft')).toBeChecked(); - expect(form.getByText('Files')).toBeVisible(); - expect(form.getByText('Add Files')).toBeVisible(); - expect(form.queryByText('Unlock conditions')).not.toBeInTheDocument(); - expect(form.queryByText('Add a condition')).not.toBeInTheDocument(); + await waitFor(() => { + expect(form.getByText('Visibility')).toBeVisible(); + expect(form.getByText('Published')).toBeVisible(); + expect(form.getByDisplayValue('published')).not.toBeChecked(); + expect(form.getByText('Draft')).toBeVisible(); + expect(form.getByDisplayValue('draft')).toBeChecked(); + expect(form.getByText('Files')).toBeVisible(); + expect(form.getByText('Add Files')).toBeVisible(); + expect(form.queryByText('Unlock conditions')).not.toBeInTheDocument(); + expect(form.queryByText('Add a condition')).not.toBeInTheDocument(); + }); props.gamified = true; renderForm(); - expect(form.getByText('Unlock conditions')).toBeVisible(); - expect(form.getByText('Add a condition')).toBeVisible(); + await waitFor(() => { + expect(form.getByText('Unlock conditions')).toBeVisible(); + expect(form.getByText('Add a condition')).toBeVisible(); + }); }); - it('prevents grading mode switching when there are submissions', () => { + it('prevents grading mode switching when there are submissions', async () => { props.modeSwitching = false; renderForm(); - expect(form.getByDisplayValue('autograded')).toBeDisabled(); - expect(form.getByDisplayValue('manual')).toBeDisabled(); + await waitFor(() => { + expect(form.getByDisplayValue('autograded')).toBeDisabled(); + expect(form.getByDisplayValue('manual')).toBeDisabled(); + }); }); - it('disables unavailable options in autograded mode', () => { + it('disables unavailable options in autograded mode', async () => { renderForm(); - expect(form.getByLabelText('Allow to skip steps')).toBeDisabled(); - expect( - form.getByLabelText('Allow submission with incorrect answers'), - ).toBeDisabled(); - expect( - form.getByLabelText('Enable delayed grade publication'), - ).toBeEnabled(); - expect(form.getByLabelText('Show MCQ submit result')).toBeDisabled(); - expect(form.getByLabelText('Enable password protection')).toBeEnabled(); + await waitFor(() => { + expect(form.getByLabelText('Allow to skip steps')).toBeDisabled(); + expect( + form.getByLabelText('Allow submission with incorrect answers'), + ).toBeDisabled(); + expect( + form.getByLabelText('Enable delayed grade publication'), + ).toBeEnabled(); + expect(form.getByLabelText('Show MCQ submit result')).toBeDisabled(); + expect(form.getByLabelText('Enable password protection')).toBeEnabled(); + }); const autogradedRadio = form.getByDisplayValue('autograded'); fireEvent.click(autogradedRadio); @@ -213,18 +243,20 @@ describe('', () => { expect(form.getByLabelText('Enable password protection')).toBeDisabled(); }); - it('handles password protection options', () => { + it('handles password protection options', async () => { renderForm(); - expect( - form.queryByLabelText('Assessment password *'), - ).not.toBeInTheDocument(); - expect( - form.queryByLabelText('Enable session protection'), - ).not.toBeInTheDocument(); - expect( - form.queryByLabelText('Session unlock password *'), - ).not.toBeInTheDocument(); + await waitFor(() => { + expect( + form.queryByLabelText('Assessment password *'), + ).not.toBeInTheDocument(); + expect( + form.queryByLabelText('Enable session protection'), + ).not.toBeInTheDocument(); + expect( + form.queryByLabelText('Session unlock password *'), + ).not.toBeInTheDocument(); + }); const passwordCheckbox = form.getByLabelText('Enable password protection'); expect(passwordCheckbox).toBeEnabled(); @@ -260,19 +292,25 @@ describe('', () => { expect(browserAuthorizationCheckbox).toBeChecked(); }); - it('renders personalised timelines options when enabled', () => { + it('renders personalised timelines options when enabled', async () => { renderForm(); - expect(form.queryByLabelText('Has personal times')).not.toBeInTheDocument(); - expect( - form.queryByLabelText('Affects personal times'), - ).not.toBeInTheDocument(); + await waitFor(() => { + expect( + form.queryByLabelText('Has personal times'), + ).not.toBeInTheDocument(); + expect( + form.queryByLabelText('Affects personal times'), + ).not.toBeInTheDocument(); + }); props.showPersonalizedTimelineFeatures = true; renderForm(); - expect(form.getByLabelText('Has personal times')).toBeEnabled(); - expect(form.getByLabelText('Affects personal times')).toBeEnabled(); + await waitFor(() => { + expect(form.getByLabelText('Has personal times')).toBeEnabled(); + expect(form.getByLabelText('Affects personal times')).toBeEnabled(); + }); }); // Randomized Assessment is temporarily hidden (PR#5406) diff --git a/client/app/bundles/course/assessment/components/FileManager/__test__/index.test.tsx b/client/app/bundles/course/assessment/components/FileManager/__test__/index.test.tsx index 813ee464d05..e1c813724ed 100644 --- a/client/app/bundles/course/assessment/components/FileManager/__test__/index.test.tsx +++ b/client/app/bundles/course/assessment/components/FileManager/__test__/index.test.tsx @@ -41,8 +41,8 @@ beforeEach(() => { beforeEach(mock.reset); describe('', () => { - it('shows existing files', () => { - expect(fileManager.getByText('Material 1')).toBeVisible(); + it('shows existing files', async () => { + expect(await fileManager.findByText('Material 1')).toBeVisible(); expect(fileManager.getByText('Material 2')).toBeVisible(); }); @@ -56,8 +56,7 @@ describe('', () => { }); const uploadApi = jest.spyOn(CourseAPI.materialFolders, 'upload'); - const addFilesButton = fileManager.getByText('Add Files'); - expect(addFilesButton).toBeVisible(); + expect(await fileManager.findByText('Add Files')).toBeVisible(); const fileInput = fileManager.getByTestId('FileInput'); diff --git a/client/app/bundles/course/assessment/pages/AssessmentEdit/__test__/index.test.tsx b/client/app/bundles/course/assessment/pages/AssessmentEdit/__test__/index.test.tsx index dfe6b9f9bf1..f0d4ad09a8c 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentEdit/__test__/index.test.tsx +++ b/client/app/bundles/course/assessment/pages/AssessmentEdit/__test__/index.test.tsx @@ -64,7 +64,7 @@ describe('', () => { it('submits correct form data', async () => { const user = userEvent.setup(); - const title = form.getByLabelText('Title *'); + const title = await form.findByLabelText('Title *'); await user.type(title, '{Control>}a{/Control}{Delete}'); await user.type(title, NEW_VALUES.title); expect(title).toHaveValue(NEW_VALUES.title); diff --git a/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimeline.tsx b/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimeline.tsx index ea2939f22d5..e14221272c6 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimeline.tsx +++ b/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimeline.tsx @@ -1,8 +1,8 @@ import { useState } from 'react'; -import moment from 'moment'; import { HeartbeatDetail } from 'types/channels/liveMonitoring'; import { BrowserAuthorizationMethod } from 'course/assessment/components/monitoring/BrowserAuthorizationMethodOptionsFormFields/common'; +import moment from 'lib/moment'; import HeartbeatDetailCard from './HeartbeatDetailCard'; import HeartbeatsTimelineChart from './HeartbeatsTimelineChart'; diff --git a/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimelineChart.tsx b/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimelineChart.tsx index 3e47f456841..68af626e572 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimelineChart.tsx +++ b/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimelineChart.tsx @@ -10,12 +10,12 @@ import { PointStyle, } from 'chart.js'; import zoomPlugin from 'chartjs-plugin-zoom'; -import moment from 'moment'; import palette from 'theme/palette'; import { HeartbeatDetail } from 'types/channels/liveMonitoring'; import { useAppSelector } from 'lib/hooks/store'; import useTranslation from 'lib/hooks/useTranslation'; +import moment from 'lib/moment'; import translations from '../../../translations'; import { select } from '../selectors'; diff --git a/client/app/bundles/course/assessment/pages/AssessmentMonitoring/utils.ts b/client/app/bundles/course/assessment/pages/AssessmentMonitoring/utils.ts index 3ab5a503a8a..ca23d6dc429 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentMonitoring/utils.ts +++ b/client/app/bundles/course/assessment/pages/AssessmentMonitoring/utils.ts @@ -1,4 +1,4 @@ -import moment from 'moment'; +import moment from 'lib/moment'; export type Presence = 'alive' | 'late' | 'missing'; diff --git a/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackFiles.tsx b/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackFiles.tsx index 192f1e1c003..f605762135f 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackFiles.tsx +++ b/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackFiles.tsx @@ -1,5 +1,4 @@ -import { FC, useRef, useState } from 'react'; -import ReactAce from 'react-ace'; +import { ComponentRef, FC, useRef, useState } from 'react'; import { MessageFile } from 'types/course/assessment/submission/liveFeedback'; import ProgrammingFileDownloadChip from 'course/assessment/submission/components/answers/Programming/ProgrammingFileDownloadChip'; @@ -12,7 +11,7 @@ interface Props { const LiveFeedbackFiles: FC = (props) => { const { file } = props; - const editorRef = useRef(null); + const editorRef = useRef>(null); const [selectedLine, setSelectedLine] = useState(1); diff --git a/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackMessageHistory.tsx b/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackMessageHistory.tsx index 7860f3ca075..fa9f6f7b250 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackMessageHistory.tsx +++ b/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackMessageHistory.tsx @@ -1,6 +1,5 @@ import { Dispatch, FC, SetStateAction } from 'react'; import { Typography } from '@mui/material'; -import moment from 'moment'; import { LiveFeedbackChatMessage } from 'types/course/assessment/submission/liveFeedback'; import { @@ -8,7 +7,7 @@ import { justifyPosition, } from 'course/assessment/submission/components/GetHelpChatPage/utils'; import MarkdownText from 'course/assessment/submission/components/MarkdownText'; -import { SHORT_DATE_TIME_FORMAT } from 'lib/moment'; +import moment, { SHORT_DATE_TIME_FORMAT } from 'lib/moment'; interface Props { messages: LiveFeedbackChatMessage[]; diff --git a/client/app/bundles/course/assessment/pages/AssessmentsIndex/__test__/index.test.jsx b/client/app/bundles/course/assessment/pages/AssessmentsIndex/__test__/index.test.jsx index ba7ad0ac5a0..58c55b6f988 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentsIndex/__test__/index.test.jsx +++ b/client/app/bundles/course/assessment/pages/AssessmentsIndex/__test__/index.test.jsx @@ -6,7 +6,7 @@ describe('', () => { it('renders the index page', async () => { const page = render(); - const newButton = page.getByRole('button'); + const newButton = await page.findByRole('button'); fireEvent.click(newButton); expect(page.getByRole('heading', { name: 'New Assessment' })).toBeVisible(); diff --git a/client/app/bundles/course/assessment/question/commons/utils.ts b/client/app/bundles/course/assessment/question/commons/utils.ts index 6fafe7ade33..1bc2dadb851 100644 --- a/client/app/bundles/course/assessment/question/commons/utils.ts +++ b/client/app/bundles/course/assessment/question/commons/utils.ts @@ -1,4 +1,4 @@ -import { isNumber } from 'lodash'; +import isNumber from 'lodash-es/isNumber'; const getNumberBetweenTwoSquareBrackets = (str: string): number | undefined => { const match = str.match(/\[(\d+)\]/); diff --git a/client/app/bundles/course/assessment/submission/components/ScribingView/__test__/ScribingToolbar.test.jsx b/client/app/bundles/course/assessment/submission/components/ScribingView/__test__/ScribingToolbar.test.jsx index 6da2252b19b..e882c00fce9 100644 --- a/client/app/bundles/course/assessment/submission/components/ScribingView/__test__/ScribingToolbar.test.jsx +++ b/client/app/bundles/course/assessment/submission/components/ScribingView/__test__/ScribingToolbar.test.jsx @@ -151,13 +151,13 @@ beforeEach(() => { describe('ScribingToolbar', () => { it('renders tool popovers', async () => { const page = render(); - expect(page.getAllByRole('button')).toHaveLength(20); + expect(await page.findAllByRole('button')).toHaveLength(20); }); it('renders color pickers', async () => { const page = render(); - const buttons = page.getAllByRole('button'); + const buttons = await page.findAllByRole('button'); fireEvent.click(buttons[2]); expect(page.getByText('Text')).toBeVisible(); @@ -183,7 +183,7 @@ describe('ScribingToolbar', () => { dispatch(setColoringToolColor(answerId, coloringTool, color)), ); - const buttons = page.getAllByRole('button'); + const buttons = await page.findAllByRole('button'); fireEvent.click(buttons[2]); const colorPicker = page.getByLabelText('Color Picker'); diff --git a/client/app/bundles/course/assessment/submission/components/answers/Answer.tsx b/client/app/bundles/course/assessment/submission/components/answers/Answer.tsx index 4a758bd2b12..d6791cb2299 100644 --- a/client/app/bundles/course/assessment/submission/components/answers/Answer.tsx +++ b/client/app/bundles/course/assessment/submission/components/answers/Answer.tsx @@ -1,209 +1,83 @@ -import { defineMessages } from 'react-intl'; -import { Alert, Card, CardContent } from '@mui/material'; +import { lazy, LazyExoticComponent, Suspense } from 'react'; +import { Alert } from '@mui/material'; import { QuestionType } from 'types/course/assessment/question'; import { SubmissionQuestionData } from 'types/course/assessment/submission/question/types'; +import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import useTranslation from 'lib/hooks/useTranslation'; import PastAnswers from '../../containers/PastAnswers'; -import ScribingView from '../../containers/ScribingView'; -import VoiceResponseAnswer from '../../containers/VoiceResponseAnswer'; -import FileUploadAnswer from './FileUpload'; -import ForumPostResponseAnswer from './ForumPostResponse'; -import MultipleChoiceAnswer from './MultipleChoice'; -import MultipleResponseAnswer from './MultipleResponse'; -import ProgrammingAnswer from './Programming'; -import TextResponseAnswer from './TextResponse'; -import { - AnswerPropsMap, - FileUploadAnswerProps, - ForumPostResponseAnswerProps, - McqAnswerProps, - MrqAnswerProps, - ProgrammingAnswerProps, - ScribingAnswerProps, - TextResponseAnswerProps, - VoiceResponseAnswerProps, -} from './types'; +import type { AnswerPropsMap } from './types'; -const translations = defineMessages({ - rendererNotImplemented: { - id: 'course.assessment.submission.Answer.rendererNotImplemented', - defaultMessage: - 'The display for this question type has not been implemented yet.', - }, - missingAnswer: { - id: 'course.assessment.submission.Answer.missingAnswer', - defaultMessage: - 'There is no answer submitted for this question - this might be caused by \ - the addition of this question after the submission is submitted.', - }, -}); +const AnswerNotImplemented = lazy( + () => + import( + /* webpackChunkName: "AnswerNotImplemented" */ + './AnswerNotImplemented' + ), +); -const MultipleChoice = (props: McqAnswerProps): JSX.Element => { - const { - question, - answerId, - readOnly, - graderView, - showMcqMrqSolution, - saveAnswerAndUpdateClientVersion, - } = props; - return ( - - ); -}; - -const MultipleResponse = (props: MrqAnswerProps): JSX.Element => { - const { - question, - answerId, - readOnly, - graderView, - showMcqMrqSolution, - saveAnswerAndUpdateClientVersion, - } = props; - return ( - - ); -}; - -const Programming = (props: ProgrammingAnswerProps): JSX.Element => { - const { question, answerId, readOnly, saveAnswerAndUpdateClientVersion } = - props; - return ( - - ); -}; - -const TextResponse = (props: TextResponseAnswerProps): JSX.Element => { - const { - question, - answerId, - readOnly, - graderView, - saveAnswerAndUpdateClientVersion, - handleUploadTextResponseFiles, - } = props; - return ( - - ); -}; - -const FileUpload = (props: FileUploadAnswerProps): JSX.Element => { - const { question, answerId, readOnly, handleUploadTextResponseFiles } = props; - return ( - - ); -}; - -const Scribing = (props: ScribingAnswerProps): JSX.Element => { - const { question, answerId } = props; - return ; -}; - -const VoiceResponse = (props: VoiceResponseAnswerProps): JSX.Element => { - const { question, answerId, readOnly, saveAnswerAndUpdateClientVersion } = - props; - return ( - - ); -}; - -const ForumPostResponse = ( - props: ForumPostResponseAnswerProps, -): JSX.Element => { - const { question, answerId, readOnly, saveAnswerAndUpdateClientVersion } = - props; - return ( - - ); -}; - -const AnswerNotImplemented = (): JSX.Element => { - const { t } = useTranslation(); - - return ( - - {t(translations.rendererNotImplemented)} - - ); -}; - -export const AnswerMapper = { - MultipleChoice: (props: McqAnswerProps): JSX.Element => ( - +const answerComponents: Record< + Exclude, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + LazyExoticComponent +> = { + MultipleChoice: lazy( + () => + import( + /* webpackChunkName: "MultipleChoiceAdapter" */ + './adapters/MultipleChoiceAdapter' + ), ), - MultipleResponse: (props: MrqAnswerProps): JSX.Element => ( - + MultipleResponse: lazy( + () => + import( + /* webpackChunkName: "MultipleResponseAdapter" */ + './adapters/MultipleResponseAdapter' + ), ), - Programming: (props: ProgrammingAnswerProps): JSX.Element => ( - + Programming: lazy( + () => + import( + /* webpackChunkName: "ProgrammingAdapter" */ + './adapters/ProgrammingAdapter' + ), ), - TextResponse: (props: TextResponseAnswerProps): JSX.Element => ( - + TextResponse: lazy( + () => + import( + /* webpackChunkName: "TextResponseAdapter" */ + './adapters/TextResponseAdapter' + ), ), - FileUpload: (props: FileUploadAnswerProps): JSX.Element => ( - + FileUpload: lazy( + () => + import( + /* webpackChunkName: "FileUploadAdapter" */ + './adapters/FileUploadAdapter' + ), ), - Comprehension: (): JSX.Element => , - Scribing: (props: ScribingAnswerProps): JSX.Element => ( - + Scribing: lazy( + () => + import( + /* webpackChunkName: "ScribingAdapter" */ + './adapters/ScribingAdapter' + ), ), - VoiceResponse: (props: VoiceResponseAnswerProps): JSX.Element => ( - + VoiceResponse: lazy( + () => + import( + /* webpackChunkName: "VoiceResponseAdapter" */ + './adapters/VoiceResponseAdapter' + ), ), - ForumPostResponse: (props: ForumPostResponseAnswerProps): JSX.Element => ( - + ForumPostResponse: lazy( + () => + import( + /* webpackChunkName: "ForumPostResponseAdapter" */ + './adapters/ForumPostResponseAdapter' + ), ), }; @@ -214,32 +88,38 @@ interface AnswerComponentProps { answerProps: AnswerPropsMap[T]; } -const Answer = ( - props: AnswerComponentProps, -): JSX.Element => { - const { answerId, questionType, question, answerProps } = props; +const SuspensefulAnswer = ({ + answerId, + questionType, + question, + answerProps, +}: AnswerComponentProps): JSX.Element => { const { t } = useTranslation(); - if (!answerId) { - return {t(translations.missingAnswer)}; - } - - if (question.viewHistory) { - return ; - } + if (!answerId) + return ( + + {t({ + id: 'course.assessment.submission.Answer.missingAnswer', + defaultMessage: + 'There is no answer submitted for this question - this might be caused by the addition of this question after the submission is submitted.', + })} + + ); - const Component = AnswerMapper[questionType]; + if (question.viewHistory) return ; - if (!Component) { - return ; - } + // @ts-expect-error + const Adapter = answerComponents[questionType]; + if (!Adapter) return ; - // "Any" type is used here as the props are dynamically generated - // depending on the different answer type and typescript - // does not support union typing for the elements. - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return Component(answerProps as any); + return ; }; +const Answer: typeof SuspensefulAnswer = (props) => ( + }> + + +); + export default Answer; diff --git a/client/app/bundles/course/assessment/submission/components/answers/AnswerNotImplemented.tsx b/client/app/bundles/course/assessment/submission/components/answers/AnswerNotImplemented.tsx new file mode 100644 index 00000000000..30b35151d16 --- /dev/null +++ b/client/app/bundles/course/assessment/submission/components/answers/AnswerNotImplemented.tsx @@ -0,0 +1,21 @@ +import { Card, CardContent } from '@mui/material'; + +import useTranslation from 'lib/hooks/useTranslation'; + +const AnswerNotImplemented = (): JSX.Element => { + const { t } = useTranslation(); + + return ( + + + {t({ + id: 'course.assessment.submission.Answer.rendererNotImplemented', + defaultMessage: + 'The display for this question type has not been implemented yet.', + })} + + + ); +}; + +export default AnswerNotImplemented; diff --git a/client/app/bundles/course/assessment/submission/components/answers/Programming/__test__/ProgrammingFile.test.tsx b/client/app/bundles/course/assessment/submission/components/answers/Programming/__test__/ProgrammingFile.test.tsx index 7a5e3c95603..c4fd5c6dd40 100644 --- a/client/app/bundles/course/assessment/submission/components/answers/Programming/__test__/ProgrammingFile.test.tsx +++ b/client/app/bundles/course/assessment/submission/components/answers/Programming/__test__/ProgrammingFile.test.tsx @@ -59,6 +59,8 @@ describe('', () => { at: [url], }); - expect(page.getByText('file is too big', { exact: false })).toBeVisible(); + expect( + await page.findByText('file is too big', { exact: false }), + ).toBeVisible(); }); }); diff --git a/client/app/bundles/course/assessment/submission/components/answers/Programming/index.jsx b/client/app/bundles/course/assessment/submission/components/answers/Programming/index.jsx index 681af3e71ba..27d3d871e3b 100644 --- a/client/app/bundles/course/assessment/submission/components/answers/Programming/index.jsx +++ b/client/app/bundles/course/assessment/submission/components/answers/Programming/index.jsx @@ -7,9 +7,6 @@ import { getIsSavingAnswer } from 'course/assessment/submission/selectors/answer import { getSubmission } from 'course/assessment/submission/selectors/submissions'; import { useAppSelector } from 'lib/hooks/store'; -import 'ace-builds/src-noconflict/mode-python'; -import 'ace-builds/src-noconflict/theme-github'; - import CodaveriFeedbackStatus from '../../../containers/CodaveriFeedbackStatus'; import ProgrammingImportEditor from '../../../containers/ProgrammingImport/ProgrammingImportEditor'; import { questionShape } from '../../../propTypes'; diff --git a/client/app/bundles/course/assessment/submission/components/answers/adapters/FileUploadAdapter.tsx b/client/app/bundles/course/assessment/submission/components/answers/adapters/FileUploadAdapter.tsx new file mode 100644 index 00000000000..09cc52f2064 --- /dev/null +++ b/client/app/bundles/course/assessment/submission/components/answers/adapters/FileUploadAdapter.tsx @@ -0,0 +1,17 @@ +import FileUploadAnswer from '../FileUpload'; +import type { FileUploadAnswerProps } from '../types'; + +const FileUploadAdapter = (props: FileUploadAnswerProps): JSX.Element => { + const { question, answerId, readOnly, handleUploadTextResponseFiles } = props; + return ( + + ); +}; + +export default FileUploadAdapter; diff --git a/client/app/bundles/course/assessment/submission/components/answers/adapters/ForumPostResponseAdapter.tsx b/client/app/bundles/course/assessment/submission/components/answers/adapters/ForumPostResponseAdapter.tsx new file mode 100644 index 00000000000..82bccc54a88 --- /dev/null +++ b/client/app/bundles/course/assessment/submission/components/answers/adapters/ForumPostResponseAdapter.tsx @@ -0,0 +1,20 @@ +import ForumPostResponseAnswer from '../ForumPostResponse'; +import type { ForumPostResponseAnswerProps } from '../types'; + +const ForumPostResponseAdapter = ( + props: ForumPostResponseAnswerProps, +): JSX.Element => { + const { question, answerId, readOnly, saveAnswerAndUpdateClientVersion } = + props; + return ( + + ); +}; + +export default ForumPostResponseAdapter; diff --git a/client/app/bundles/course/assessment/submission/components/answers/adapters/MultipleChoiceAdapter.tsx b/client/app/bundles/course/assessment/submission/components/answers/adapters/MultipleChoiceAdapter.tsx new file mode 100644 index 00000000000..f26bb95dddf --- /dev/null +++ b/client/app/bundles/course/assessment/submission/components/answers/adapters/MultipleChoiceAdapter.tsx @@ -0,0 +1,26 @@ +import MultipleChoiceAnswer from '../MultipleChoice'; +import type { McqAnswerProps } from '../types'; + +const MultipleChoiceAdapter = (props: McqAnswerProps): JSX.Element => { + const { + question, + answerId, + readOnly, + graderView, + showMcqMrqSolution, + saveAnswerAndUpdateClientVersion, + } = props; + return ( + + ); +}; + +export default MultipleChoiceAdapter; diff --git a/client/app/bundles/course/assessment/submission/components/answers/adapters/MultipleResponseAdapter.tsx b/client/app/bundles/course/assessment/submission/components/answers/adapters/MultipleResponseAdapter.tsx new file mode 100644 index 00000000000..01877b712e1 --- /dev/null +++ b/client/app/bundles/course/assessment/submission/components/answers/adapters/MultipleResponseAdapter.tsx @@ -0,0 +1,26 @@ +import MultipleResponseAnswer from '../MultipleResponse'; +import type { MrqAnswerProps } from '../types'; + +const MultipleResponseAdapter = (props: MrqAnswerProps): JSX.Element => { + const { + question, + answerId, + readOnly, + graderView, + showMcqMrqSolution, + saveAnswerAndUpdateClientVersion, + } = props; + return ( + + ); +}; + +export default MultipleResponseAdapter; diff --git a/client/app/bundles/course/assessment/submission/components/answers/adapters/ProgrammingAdapter.tsx b/client/app/bundles/course/assessment/submission/components/answers/adapters/ProgrammingAdapter.tsx new file mode 100644 index 00000000000..8b3170fdf30 --- /dev/null +++ b/client/app/bundles/course/assessment/submission/components/answers/adapters/ProgrammingAdapter.tsx @@ -0,0 +1,18 @@ +import ProgrammingAnswer from '../Programming'; +import type { ProgrammingAnswerProps } from '../types'; + +const ProgrammingAdapter = (props: ProgrammingAnswerProps): JSX.Element => { + const { question, answerId, readOnly, saveAnswerAndUpdateClientVersion } = + props; + return ( + + ); +}; + +export default ProgrammingAdapter; diff --git a/client/app/bundles/course/assessment/submission/components/answers/adapters/ScribingAdapter.tsx b/client/app/bundles/course/assessment/submission/components/answers/adapters/ScribingAdapter.tsx new file mode 100644 index 00000000000..3bbc73aeb5b --- /dev/null +++ b/client/app/bundles/course/assessment/submission/components/answers/adapters/ScribingAdapter.tsx @@ -0,0 +1,9 @@ +import ScribingView from '../../../containers/ScribingView'; +import type { ScribingAnswerProps } from '../types'; + +const ScribingAdapter = (props: ScribingAnswerProps): JSX.Element => { + const { question, answerId } = props; + return ; +}; + +export default ScribingAdapter; diff --git a/client/app/bundles/course/assessment/submission/components/answers/adapters/TextResponseAdapter.tsx b/client/app/bundles/course/assessment/submission/components/answers/adapters/TextResponseAdapter.tsx new file mode 100644 index 00000000000..5b14509e4ff --- /dev/null +++ b/client/app/bundles/course/assessment/submission/components/answers/adapters/TextResponseAdapter.tsx @@ -0,0 +1,26 @@ +import TextResponseAnswer from '../TextResponse'; +import type { TextResponseAnswerProps } from '../types'; + +const TextResponseAdapter = (props: TextResponseAnswerProps): JSX.Element => { + const { + question, + answerId, + readOnly, + graderView, + saveAnswerAndUpdateClientVersion, + handleUploadTextResponseFiles, + } = props; + return ( + + ); +}; + +export default TextResponseAdapter; diff --git a/client/app/bundles/course/assessment/submission/components/answers/adapters/VoiceResponseAdapter.tsx b/client/app/bundles/course/assessment/submission/components/answers/adapters/VoiceResponseAdapter.tsx new file mode 100644 index 00000000000..15109b3b536 --- /dev/null +++ b/client/app/bundles/course/assessment/submission/components/answers/adapters/VoiceResponseAdapter.tsx @@ -0,0 +1,18 @@ +import VoiceResponseAnswer from '../../../containers/VoiceResponseAnswer'; +import type { VoiceResponseAnswerProps } from '../types'; + +const VoiceResponseAdapter = (props: VoiceResponseAnswerProps): JSX.Element => { + const { question, answerId, readOnly, saveAnswerAndUpdateClientVersion } = + props; + return ( + + ); +}; + +export default VoiceResponseAdapter; diff --git a/client/app/bundles/course/assessment/submission/containers/TestCaseView/__test__/index.test.js b/client/app/bundles/course/assessment/submission/containers/TestCaseView/__test__/index.test.js index a60d3cad126..8a70c76bcc0 100644 --- a/client/app/bundles/course/assessment/submission/containers/TestCaseView/__test__/index.test.js +++ b/client/app/bundles/course/assessment/submission/containers/TestCaseView/__test__/index.test.js @@ -1,5 +1,5 @@ /* eslint-disable sonarjs/no-duplicate-string */ -import { render, within } from 'test-utils'; +import { render, waitFor, within } from 'test-utils'; import { VisibleTestCaseView } from 'course/assessment/submission/containers/TestCaseView'; @@ -59,27 +59,29 @@ const getWarning = (page, text) => describe('TestCaseView', () => { describe('when viewing as staff', () => { - it('renders all test cases and standard streams', () => { + it('renders all test cases and standard streams', async () => { const page = render(); - expect(page.getByText('Public Test Cases')).toBeVisible(); + expect(await page.findByText('Public Test Cases')).toBeVisible(); expect(page.getByText('Private Test Cases')).toBeVisible(); expect(page.getByText('Evaluation Test Cases')).toBeVisible(); expect(page.getByText('Standard Output')).toBeVisible(); expect(page.getByText('Standard Error')).toBeVisible(); }); - it('renders staff-only warnings', () => { + it('renders staff-only warnings', async () => { const page = render(); - expect(getWarning(page, 'Private Test Cases')).toBeVisible(); - expect(getWarning(page, 'Evaluation Test Cases')).toBeVisible(); - expect(getWarning(page, 'Standard Output')).toBeVisible(); - expect(getWarning(page, 'Standard Error')).toBeVisible(); + await waitFor(() => { + expect(getWarning(page, 'Private Test Cases')).toBeVisible(); + expect(getWarning(page, 'Evaluation Test Cases')).toBeVisible(); + expect(getWarning(page, 'Standard Output')).toBeVisible(); + expect(getWarning(page, 'Standard Error')).toBeVisible(); + }); }); describe('when showEvaluation & showPrivate are true', () => { - it('renders staff-only warnings when assessment is not yet published', () => { + it('renders staff-only warnings when assessment is not yet published', async () => { const page = render( { />, ); - expect(getWarning(page, 'Private Test Cases')).toBeVisible(); - expect(getWarning(page, 'Evaluation Test Cases')).toBeVisible(); + await waitFor(() => { + expect(getWarning(page, 'Private Test Cases')).toBeVisible(); + expect(getWarning(page, 'Evaluation Test Cases')).toBeVisible(); + }); }); - it('does not render staff-only warnings when assessment is published', () => { + it('does not render staff-only warnings when assessment is published', async () => { const page = render( { />, ); - expect(getWarning(page, 'Private Test Cases')).not.toBeInTheDocument(); - expect( - getWarning(page, 'Evaluation Test Cases'), - ).not.toBeInTheDocument(); + await waitFor(() => { + expect( + getWarning(page, 'Private Test Cases'), + ).not.toBeInTheDocument(); + expect( + getWarning(page, 'Evaluation Test Cases'), + ).not.toBeInTheDocument(); + }); }); }); describe('when students can see standard streams', () => { - it('does not render staff-only warnings', () => { + it('does not render staff-only warnings', async () => { const page = render( { />, ); - expect(getWarning(page, 'Standard Output')).not.toBeInTheDocument(); - expect(getWarning(page, 'Standard Error')).not.toBeInTheDocument(); + await waitFor(() => { + expect(getWarning(page, 'Standard Output')).not.toBeInTheDocument(); + expect(getWarning(page, 'Standard Error')).not.toBeInTheDocument(); + }); }); }); }); describe('when viewing as student', () => { - it('does not show any staff-only warnings', () => { + it('does not show any staff-only warnings', async () => { const page = render( { />, ); - expect(getWarning(page, 'Private Test Cases')).not.toBeInTheDocument(); - expect(getWarning(page, 'Evaluation Test Cases')).not.toBeInTheDocument(); - expect(getWarning(page, 'Standard Output')).not.toBeInTheDocument(); - expect(getWarning(page, 'Standard Error')).not.toBeInTheDocument(); + await waitFor(() => { + expect(getWarning(page, 'Private Test Cases')).not.toBeInTheDocument(); + expect( + getWarning(page, 'Evaluation Test Cases'), + ).not.toBeInTheDocument(); + expect(getWarning(page, 'Standard Output')).not.toBeInTheDocument(); + expect(getWarning(page, 'Standard Error')).not.toBeInTheDocument(); + }); }); - it('shows standard streams when the flag is enabled', () => { + it('shows standard streams when the flag is enabled', async () => { const page = render( { />, ); - expect(page.getByText('Standard Output')).toBeVisible(); + expect(await page.findByText('Standard Output')).toBeVisible(); expect(page.getByText('Standard Error')).toBeVisible(); }); describe('when showEvaluation & showPrivate flags are enabled', () => { - it('shows private and evaluation tests after assessment is published', () => { + it('shows private and evaluation tests after assessment is published', async () => { const page = render( { />, ); - expect(page.getByText('Private Test Cases')).toBeVisible(); + expect(await page.findByText('Private Test Cases')).toBeVisible(); expect(page.getByText('Evaluation Test Cases')).toBeVisible(); }); - it('does not show private and evaluation tests before assessment is published', () => { + it('does not show private and evaluation tests before assessment is published', async () => { const page = render( { />, ); - expect(page.queryByText('Private Test Cases')).not.toBeInTheDocument(); - expect( - page.queryByText('Evaluation Test Cases'), - ).not.toBeInTheDocument(); + await waitFor(() => { + expect( + page.queryByText('Private Test Cases'), + ).not.toBeInTheDocument(); + expect( + page.queryByText('Evaluation Test Cases'), + ).not.toBeInTheDocument(); + }); }); }); }); diff --git a/client/app/bundles/course/assessment/submission/pages/SubmissionsIndex/__test__/submissionsTable.test.jsx b/client/app/bundles/course/assessment/submission/pages/SubmissionsIndex/__test__/submissionsTable.test.jsx index 32392aa6b7f..7cd1b323d9b 100644 --- a/client/app/bundles/course/assessment/submission/pages/SubmissionsIndex/__test__/submissionsTable.test.jsx +++ b/client/app/bundles/course/assessment/submission/pages/SubmissionsIndex/__test__/submissionsTable.test.jsx @@ -47,7 +47,7 @@ const defaultProps = { describe('', () => { describe('when canViewLogs, canUnsubmitSubmission and canDeleteAllSubmissions are set to true ', () => { - it('renders the submissions table with access log links', () => { + it('renders the submissions table with access log links', async () => { const page = render( ', () => { />, ); - expect(page.getByText('John').closest('tr')).toBeVisible(); + expect((await page.findByText('John')).closest('tr')).toBeVisible(); expect(page.getByTestId('HistoryIcon').closest('button')).toBeVisible(); expect(page.getByTestId('DeleteIcon').closest('button')).toBeVisible(); expect( @@ -70,7 +70,7 @@ describe('', () => { }); describe('when canViewLogs, canUnsubmitSubmission and canDeleteAllSubmissions are set to false', () => { - it('renders the submissions table without access log links', () => { + it('renders the submissions table without access log links', async () => { const page = render( ', () => { />, ); - expect(page.getByText('John').closest('tr')).toBeVisible(); + expect((await page.findByText('John')).closest('tr')).toBeVisible(); expect(page.queryByTestId('HistoryIcon')).not.toBeInTheDocument(); expect(page.queryByTestId('DeleteIcon')).not.toBeInTheDocument(); expect(page.queryByTestId('RemoveCircleIcon')).not.toBeInTheDocument(); diff --git a/client/app/bundles/course/assessment/submission/reducers/liveFeedbackChats/index.ts b/client/app/bundles/course/assessment/submission/reducers/liveFeedbackChats/index.ts index 60f12b53400..bcdd7d98d7e 100644 --- a/client/app/bundles/course/assessment/submission/reducers/liveFeedbackChats/index.ts +++ b/client/app/bundles/course/assessment/submission/reducers/liveFeedbackChats/index.ts @@ -4,10 +4,9 @@ import { type EntityState, PayloadAction, } from '@reduxjs/toolkit'; -import { shuffle } from 'lodash'; -import moment from 'moment'; +import shuffle from 'lodash-es/shuffle'; -import { SHORT_TIME_FORMAT } from 'lib/moment'; +import moment, { SHORT_TIME_FORMAT } from 'lib/moment'; import { getLocalStorageValue, diff --git a/client/app/bundles/course/assessment/submission/reducers/scribing.js b/client/app/bundles/course/assessment/submission/reducers/scribing.js index beffdc744cf..73572c8f9e2 100644 --- a/client/app/bundles/course/assessment/submission/reducers/scribing.js +++ b/client/app/bundles/course/assessment/submission/reducers/scribing.js @@ -1,5 +1,5 @@ import { produce } from 'immer'; -import { isNumber } from 'lodash'; +import isNumber from 'lodash-es/isNumber'; import actions, { canvasActionTypes, diff --git a/client/app/bundles/course/container/index.ts b/client/app/bundles/course/container/index.ts deleted file mode 100644 index 379f1d945fc..00000000000 --- a/client/app/bundles/course/container/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as CourseContainer } from './CourseContainer'; diff --git a/client/app/bundles/course/duplication/pages/Duplication/__test__/DuplicateButton.test.tsx b/client/app/bundles/course/duplication/pages/Duplication/__test__/DuplicateButton.test.tsx index 72537eb0921..7c49e3a4e81 100644 --- a/client/app/bundles/course/duplication/pages/Duplication/__test__/DuplicateButton.test.tsx +++ b/client/app/bundles/course/duplication/pages/Duplication/__test__/DuplicateButton.test.tsx @@ -68,13 +68,13 @@ const expectedPayload = { }; describe('', () => { - it('allows duplication to be triggered with the correct parameters', () => { + it('allows duplication to be triggered with the correct parameters', async () => { const spy = jest.spyOn(CourseAPI.duplication, 'duplicateItems'); store.dispatch(loadObjectsList(data)); const page = render(); - fireEvent.click(page.getByRole('button')); + fireEvent.click(await page.findByRole('button')); fireEvent.click(page.getByRole('button', { name: 'Duplicate' })); expect(spy).toHaveBeenCalledWith(data.sourceCourse.id, expectedPayload); diff --git a/client/app/bundles/course/experience-points/index.tsx b/client/app/bundles/course/experience-points/index.tsx index 5710b918470..2280f27349e 100644 --- a/client/app/bundles/course/experience-points/index.tsx +++ b/client/app/bundles/course/experience-points/index.tsx @@ -25,7 +25,7 @@ const translations = defineMessages({ }, forumDisbursementTab: { id: 'course.experiencePoints.disbursement.DisbursementIndex.forumTab', - defaultMessage: 'Forum Participation Disbursement', + defaultMessage: 'Forum Participation', }, generalDisbursementTab: { id: 'course.experiencePoints.disbursement.DisbursementIndex.generalTab', diff --git a/client/app/bundles/course/lesson-plan/containers/LessonPlanLayout/__test__/NewEventButton.test.jsx b/client/app/bundles/course/lesson-plan/containers/LessonPlanLayout/__test__/NewEventButton.test.jsx index 22c0df52d35..861387c7ffc 100644 --- a/client/app/bundles/course/lesson-plan/containers/LessonPlanLayout/__test__/NewEventButton.test.jsx +++ b/client/app/bundles/course/lesson-plan/containers/LessonPlanLayout/__test__/NewEventButton.test.jsx @@ -33,7 +33,7 @@ describe('', () => { { state }, ); - fireEvent.click(page.getByRole('button', { name: 'New Event' })); + fireEvent.click(await page.findByRole('button', { name: 'New Event' })); fireEvent.change(page.getByLabelText('Title', { exact: false }), { target: { value: eventData.title }, diff --git a/client/app/bundles/course/lesson-plan/containers/LessonPlanLayout/__test__/NewMilestoneButton.test.jsx b/client/app/bundles/course/lesson-plan/containers/LessonPlanLayout/__test__/NewMilestoneButton.test.jsx index 9e1bb2697e4..44cb7217f35 100644 --- a/client/app/bundles/course/lesson-plan/containers/LessonPlanLayout/__test__/NewMilestoneButton.test.jsx +++ b/client/app/bundles/course/lesson-plan/containers/LessonPlanLayout/__test__/NewMilestoneButton.test.jsx @@ -29,7 +29,7 @@ describe('', () => { { state }, ); - fireEvent.click(page.getByRole('button', { name: 'New Milestone' })); + fireEvent.click(await page.findByRole('button', { name: 'New Milestone' })); fireEvent.change(page.getByLabelText('Title', { exact: false }), { target: { value: milestoneData.title }, @@ -48,8 +48,11 @@ describe('', () => { }); }); - it('is hidden when canManageLessonPlan is false', () => { + it('is hidden when canManageLessonPlan is false', async () => { const page = render(); - expect(page.queryByRole('button')).not.toBeInTheDocument(); + + await waitFor(() => + expect(page.queryByRole('button')).not.toBeInTheDocument(), + ); }); }); diff --git a/client/app/bundles/course/lesson-plan/pages/LessonPlanEdit/__test__/ItemRow.test.jsx b/client/app/bundles/course/lesson-plan/pages/LessonPlanEdit/__test__/ItemRow.test.jsx index a8cc1ddd790..c111410256f 100644 --- a/client/app/bundles/course/lesson-plan/pages/LessonPlanEdit/__test__/ItemRow.test.jsx +++ b/client/app/bundles/course/lesson-plan/pages/LessonPlanEdit/__test__/ItemRow.test.jsx @@ -51,7 +51,7 @@ describe('', () => { { state }, ); - const input = page.getByDisplayValue(startAt); + const input = await page.findByDisplayValue(startAt); fireEvent.change(input, { target: { value: newStartAt } }); fireEvent.blur(input); @@ -67,7 +67,7 @@ describe('', () => { ); }); - it('clears end date', () => { + it('clears end date', async () => { const spy = jest.spyOn(CourseAPI.lessonPlan, 'updateItem'); const page = render( @@ -83,7 +83,7 @@ describe('', () => { { state }, ); - const input = page.getByDisplayValue(endAt); + const input = await page.findByDisplayValue(endAt); fireEvent.change(input, { target: { value: '' } }); fireEvent.blur(input); diff --git a/client/app/bundles/course/lesson-plan/pages/LessonPlanEdit/__test__/MilestoneRow.test.jsx b/client/app/bundles/course/lesson-plan/pages/LessonPlanEdit/__test__/MilestoneRow.test.jsx index 6e0aee4ee9f..70c386317cf 100644 --- a/client/app/bundles/course/lesson-plan/pages/LessonPlanEdit/__test__/MilestoneRow.test.jsx +++ b/client/app/bundles/course/lesson-plan/pages/LessonPlanEdit/__test__/MilestoneRow.test.jsx @@ -37,7 +37,7 @@ describe('', () => { { state: { lessonPlan: { milestones: [milestoneData] } } }, ); - const input = page.getByDisplayValue(startAt); + const input = await page.findByDisplayValue(startAt); fireEvent.change(input, { target: { value: newStartAt } }); fireEvent.blur(input); diff --git a/client/app/bundles/course/lesson-plan/pages/LessonPlanShow/LessonPlanItem/__test__/AdminTools.test.jsx b/client/app/bundles/course/lesson-plan/pages/LessonPlanShow/LessonPlanItem/__test__/AdminTools.test.jsx index b9846af2590..97ac7d9cfe9 100644 --- a/client/app/bundles/course/lesson-plan/pages/LessonPlanShow/LessonPlanItem/__test__/AdminTools.test.jsx +++ b/client/app/bundles/course/lesson-plan/pages/LessonPlanShow/LessonPlanItem/__test__/AdminTools.test.jsx @@ -13,14 +13,17 @@ const state = { const renderElement = (item) => render(, { state }); describe('', () => { - it('does not show admin menu for lesson plan events', () => { + it('does not show admin menu for lesson plan events', async () => { const page = renderElement({ title: 'Event', eventId: 7 }); - expect(page.getAllByRole('button')).toHaveLength(2); + expect(await page.findAllByRole('button')).toHaveLength(2); }); - it('does not show admin menu for non-event lesson plan items', () => { + it('does not show admin menu for non-event lesson plan items', async () => { const wrapper = renderElement({ title: 'eventId absent' }); - expect(wrapper.queryAllByRole('button')).toHaveLength(0); + + await waitFor(() => + expect(wrapper.queryAllByRole('button')).toHaveLength(0), + ); }); it('allows event to be deleted', async () => { @@ -35,7 +38,7 @@ describe('', () => { { state }, ); - fireEvent.click(page.getAllByRole('button')[1]); + fireEvent.click((await page.findAllByRole('button'))[1]); fireEvent.click(page.getByRole('button', { name: 'Delete' })); await waitFor(() => expect(spy).toHaveBeenCalledWith(eventId)); @@ -71,7 +74,7 @@ describe('', () => { { state }, ); - fireEvent.click(page.getAllByRole('button')[0]); + fireEvent.click((await page.findAllByRole('button'))[0]); const description = 'Add nice description'; fireEvent.change(page.getByLabelText('Description'), { diff --git a/client/app/bundles/course/lesson-plan/pages/LessonPlanShow/__test__/LessonPlanShow.test.jsx b/client/app/bundles/course/lesson-plan/pages/LessonPlanShow/__test__/LessonPlanShow.test.jsx index 01783d3729f..2a6dc7decee 100644 --- a/client/app/bundles/course/lesson-plan/pages/LessonPlanShow/__test__/LessonPlanShow.test.jsx +++ b/client/app/bundles/course/lesson-plan/pages/LessonPlanShow/__test__/LessonPlanShow.test.jsx @@ -1,4 +1,4 @@ -import { render } from 'test-utils'; +import { render, waitFor } from 'test-utils'; import { LessonPlanShow } from '../index'; @@ -39,7 +39,7 @@ const data = { describe('', () => { describe('when all milestones are expanded by default', () => { - it('shows all visible items', () => { + it('shows all visible items', async () => { const page = render( ', () => { />, ); - data.groups.forEach((group) => - group.items.forEach((item) => - expect(page.getByText(item.title)).toBeVisible(), - ), - ); + await waitFor(() => { + data.groups.forEach((group) => + group.items.forEach((item) => + expect(page.getByText(item.title)).toBeVisible(), + ), + ); + }); }); }); describe('when none of the milestones are expanded by default', () => { - it('shows no items', () => { + it('shows no items', async () => { const page = render( ', () => { {...data} />, ); - - data.groups.forEach((group) => - group.items.forEach((item) => - expect(page.queryByText(item.title)).not.toBeInTheDocument(), - ), - ); + await waitFor(() => { + data.groups.forEach((group) => + group.items.forEach((item) => + expect(page.queryByText(item.title)).not.toBeInTheDocument(), + ), + ); + }); }); }); describe('when only one of the current milestone is expanded by default', () => { - it('shows items for current group', () => { + it('shows items for current group', async () => { const page = render( ', () => { const hiddenItem = data.groups[0].items[0].title; const shownItem = data.groups[1].items[0].title; - expect(page.queryByText(hiddenItem)).not.toBeInTheDocument(); - expect(page.getByText(shownItem)).toBeVisible(); + await waitFor(() => { + expect(page.queryByText(hiddenItem)).not.toBeInTheDocument(); + expect(page.getByText(shownItem)).toBeVisible(); + }); }); }); }); diff --git a/client/app/bundles/course/lesson-plan/pages/LessonPlanShow/__test__/MilestoneAdminTools.test.jsx b/client/app/bundles/course/lesson-plan/pages/LessonPlanShow/__test__/MilestoneAdminTools.test.jsx index 6ec7ffd0cde..3a070983da1 100644 --- a/client/app/bundles/course/lesson-plan/pages/LessonPlanShow/__test__/MilestoneAdminTools.test.jsx +++ b/client/app/bundles/course/lesson-plan/pages/LessonPlanShow/__test__/MilestoneAdminTools.test.jsx @@ -12,31 +12,35 @@ const renderElement = (canManageLessonPlan, milestone) => { }; describe('', () => { - it('hides admin tools for dummy milestone', () => { + it('hides admin tools for dummy milestone', async () => { const page = renderElement(true, { id: undefined, title: 'Ungrouped Items', }); - expect(page.queryByRole('button')).not.toBeInTheDocument(); + await waitFor(() => + expect(page.queryByRole('button')).not.toBeInTheDocument(), + ); }); - it('hides admin tools when user does not have permissions', () => { + it('hides admin tools when user does not have permissions', async () => { const page = renderElement(false, { id: 4, title: 'User-defined Milestone', }); - expect(page.queryByRole('button')).not.toBeInTheDocument(); + await waitFor(() => + expect(page.queryByRole('button')).not.toBeInTheDocument(), + ); }); - it('shows admin tools when user has permissions', () => { + it('shows admin tools when user has permissions', async () => { const page = renderElement(true, { id: 4, title: 'User-defined Milestone', }); - expect(page.getAllByRole('button')).toHaveLength(2); + expect(await page.findAllByRole('button')).toHaveLength(2); }); it('allows milestone to be deleted', async () => { @@ -58,7 +62,7 @@ describe('', () => { { state: { lessonPlan: { flags: { canManageLessonPlan: true } } } }, ); - fireEvent.click(page.getAllByRole('button')[1]); + fireEvent.click((await page.findAllByRole('button'))[1]); fireEvent.click(page.getByRole('button', { name: 'Delete' })); await waitFor(() => expect(spy).toHaveBeenCalledWith(milestoneId)); @@ -93,7 +97,7 @@ describe('', () => { { state: { lessonPlan: { flags: { canManageLessonPlan: true } } } }, ); - fireEvent.click(page.getAllByRole('button')[0]); + fireEvent.click((await page.findAllByRole('button'))[0]); fireEvent.change(page.getByLabelText('Description'), { target: { value: description }, diff --git a/client/app/bundles/course/reference-timelines/components/DayCalendar/DayCalendar.tsx b/client/app/bundles/course/reference-timelines/components/DayCalendar/DayCalendar.tsx index 719499bdd2c..d3ba32d4527 100644 --- a/client/app/bundles/course/reference-timelines/components/DayCalendar/DayCalendar.tsx +++ b/client/app/bundles/course/reference-timelines/components/DayCalendar/DayCalendar.tsx @@ -2,9 +2,9 @@ import { forwardRef, useImperativeHandle, useRef, useState } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { FixedSizeList as List } from 'react-window'; import { Button, Typography } from '@mui/material'; -import moment from 'moment'; import useTranslation from 'lib/hooks/useTranslation'; +import moment from 'lib/moment'; import translations from '../../translations'; import { diff --git a/client/app/bundles/course/reference-timelines/components/DayCalendar/DayColumn.tsx b/client/app/bundles/course/reference-timelines/components/DayCalendar/DayColumn.tsx index 4ca273feeb8..7cee1062211 100644 --- a/client/app/bundles/course/reference-timelines/components/DayCalendar/DayColumn.tsx +++ b/client/app/bundles/course/reference-timelines/components/DayCalendar/DayColumn.tsx @@ -1,7 +1,8 @@ import { CSSProperties, memo } from 'react'; import { areEqual } from 'react-window'; import { Typography } from '@mui/material'; -import moment from 'moment'; + +import moment from 'lib/moment'; import { getSecondsFromDays, isToday, isWeekend } from '../../utils'; diff --git a/client/app/bundles/course/reference-timelines/components/SubmitIndicator.tsx b/client/app/bundles/course/reference-timelines/components/SubmitIndicator.tsx index 416fd39637f..8d77d7dca2b 100644 --- a/client/app/bundles/course/reference-timelines/components/SubmitIndicator.tsx +++ b/client/app/bundles/course/reference-timelines/components/SubmitIndicator.tsx @@ -1,10 +1,10 @@ import { useEffect, useState } from 'react'; import { Cancel, CheckCircle } from '@mui/icons-material'; import { Chip, Grow, Tooltip, Typography } from '@mui/material'; -import moment from 'moment'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import useTranslation from 'lib/hooks/useTranslation'; +import moment from 'lib/moment'; import { useLastSaved } from '../contexts'; import translations from '../translations'; diff --git a/client/app/bundles/course/reference-timelines/components/TimeBar/DurationBar.tsx b/client/app/bundles/course/reference-timelines/components/TimeBar/DurationBar.tsx index b5b64dc5b89..82d74dd9123 100644 --- a/client/app/bundles/course/reference-timelines/components/TimeBar/DurationBar.tsx +++ b/client/app/bundles/course/reference-timelines/components/TimeBar/DurationBar.tsx @@ -1,5 +1,6 @@ import { MouseEventHandler, ReactNode, TouchEventHandler } from 'react'; -import moment from 'moment'; + +import moment from 'lib/moment'; import { DAY_WIDTH_PIXELS, diff --git a/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBar.tsx b/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBar.tsx index f2d206374ed..9dbae1ef05a 100644 --- a/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBar.tsx +++ b/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBar.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react'; -import moment from 'moment'; + +import moment from 'lib/moment'; import { DAY_WIDTH_PIXELS, getDaysFromWidth } from '../../utils'; import HorizontallyDraggable from '../HorizontallyDraggable'; diff --git a/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBarHandle.tsx b/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBarHandle.tsx index 9c5c2a28c03..b6cd8a2ac82 100644 --- a/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBarHandle.tsx +++ b/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBarHandle.tsx @@ -5,7 +5,8 @@ import { TouchEventHandler, } from 'react'; import { Typography } from '@mui/material'; -import moment from 'moment'; + +import moment from 'lib/moment'; interface HandleContentProps { side: 'start' | 'end'; diff --git a/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopup.tsx b/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopup.tsx index ceb9814270b..209aca0c70b 100644 --- a/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopup.tsx +++ b/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopup.tsx @@ -1,5 +1,4 @@ import { Typography } from '@mui/material'; -import moment from 'moment'; import { ItemWithTimeData, TimelineData, @@ -8,6 +7,7 @@ import { import { useAppDispatch } from 'lib/hooks/store'; import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; +import moment from 'lib/moment'; import { useSetLastSaved } from '../../contexts'; import { createTime, deleteTime, updateTime } from '../../operations'; diff --git a/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopupForm.tsx b/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopupForm.tsx index b7a7309ffa8..bdb650773d4 100644 --- a/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopupForm.tsx +++ b/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopupForm.tsx @@ -1,11 +1,11 @@ import { Controller } from 'react-hook-form'; import { Button, Collapse } from '@mui/material'; -import moment from 'moment'; import { date, object, ref } from 'yup'; import FormDateTimePickerField from 'lib/components/form/fields/DateTimePickerField'; import Form from 'lib/components/form/Form'; import useTranslation from 'lib/hooks/useTranslation'; +import moment from 'lib/moment'; import formTranslations from 'lib/translations/form'; import { useLastSaved } from '../../contexts'; diff --git a/client/app/bundles/course/reference-timelines/components/TimelinesStack/AssignableTimeline.tsx b/client/app/bundles/course/reference-timelines/components/TimelinesStack/AssignableTimeline.tsx index 989b619bf31..1256c169631 100644 --- a/client/app/bundles/course/reference-timelines/components/TimelinesStack/AssignableTimeline.tsx +++ b/client/app/bundles/course/reference-timelines/components/TimelinesStack/AssignableTimeline.tsx @@ -1,11 +1,12 @@ import { useState } from 'react'; -import moment from 'moment'; import { ItemWithTimeData, TimeData, TimelineData, } from 'types/course/referenceTimelines'; +import moment from 'lib/moment'; + import { useLastSaved } from '../../contexts'; import { DraftableTimeData } from '../../utils'; import TimePopup from '../TimePopup'; diff --git a/client/app/bundles/course/reference-timelines/components/TimelinesStack/Timeline.tsx b/client/app/bundles/course/reference-timelines/components/TimelinesStack/Timeline.tsx index c24c262ccc8..9cb7223ab5b 100644 --- a/client/app/bundles/course/reference-timelines/components/TimelinesStack/Timeline.tsx +++ b/client/app/bundles/course/reference-timelines/components/TimelinesStack/Timeline.tsx @@ -1,10 +1,10 @@ import { ReactNode, useState } from 'react'; import { Add } from '@mui/icons-material'; import { Typography } from '@mui/material'; -import moment from 'moment'; import { TimeData } from 'types/course/referenceTimelines'; import useTranslation from 'lib/hooks/useTranslation'; +import moment from 'lib/moment'; import translations from '../../translations'; import { DAY_WIDTH_PIXELS, getSecondsFromDays } from '../../utils'; diff --git a/client/app/bundles/course/reference-timelines/contexts/LastSavedContext.tsx b/client/app/bundles/course/reference-timelines/contexts/LastSavedContext.tsx index c868b65d4ba..873433794ad 100644 --- a/client/app/bundles/course/reference-timelines/contexts/LastSavedContext.tsx +++ b/client/app/bundles/course/reference-timelines/contexts/LastSavedContext.tsx @@ -6,7 +6,8 @@ import { useContext, useState, } from 'react'; -import moment from 'moment'; + +import moment from 'lib/moment'; type FetchStatus = 'loading' | 'success' | 'failure'; diff --git a/client/app/bundles/course/reference-timelines/utils.ts b/client/app/bundles/course/reference-timelines/utils.ts index b10859b880e..9cce968a80b 100644 --- a/client/app/bundles/course/reference-timelines/utils.ts +++ b/client/app/bundles/course/reference-timelines/utils.ts @@ -1,6 +1,7 @@ -import moment from 'moment'; import { TimeData } from 'types/course/referenceTimelines'; +import moment from 'lib/moment'; + const SECONDS_IN_A_DAY = 86_400 as const; export const DAY_WIDTH_PIXELS = 30 as const; diff --git a/client/app/bundles/course/reference-timelines/views/DayView/ItemsSidebar.tsx b/client/app/bundles/course/reference-timelines/views/DayView/ItemsSidebar.tsx index e0821f4979e..12081c2be64 100644 --- a/client/app/bundles/course/reference-timelines/views/DayView/ItemsSidebar.tsx +++ b/client/app/bundles/course/reference-timelines/views/DayView/ItemsSidebar.tsx @@ -1,10 +1,11 @@ import { Typography } from '@mui/material'; -import moment from 'moment'; import { ItemWithTimeData, TimelineData, } from 'types/course/referenceTimelines'; +import moment from 'lib/moment'; + import { getDaysFromSeconds } from '../../utils'; import TimelineSidebarItem from './TimelineSidebarItem'; diff --git a/client/app/bundles/course/survey/containers/RespondButton/__test__/index.test.jsx b/client/app/bundles/course/survey/containers/RespondButton/__test__/index.test.jsx index 3ea1c1d1eb1..12d9d2c5be9 100644 --- a/client/app/bundles/course/survey/containers/RespondButton/__test__/index.test.jsx +++ b/client/app/bundles/course/survey/containers/RespondButton/__test__/index.test.jsx @@ -28,7 +28,7 @@ describe('', () => { />, ); - fireEvent.click(page.getByRole('button')); + fireEvent.click(await page.findByRole('button')); await waitFor(() => { expect(spyCreate).toHaveBeenCalledWith(surveyId); diff --git a/client/app/bundles/course/survey/containers/SurveyLayout/__test__/AdminMenu.test.jsx b/client/app/bundles/course/survey/containers/SurveyLayout/__test__/AdminMenu.test.jsx index 620ec5559b2..a154565bc75 100644 --- a/client/app/bundles/course/survey/containers/SurveyLayout/__test__/AdminMenu.test.jsx +++ b/client/app/bundles/course/survey/containers/SurveyLayout/__test__/AdminMenu.test.jsx @@ -7,7 +7,7 @@ import DeleteConfirmation from 'lib/containers/DeleteConfirmation'; import AdminMenu from '../AdminMenu'; describe('', () => { - it('does not render button if user cannot edit or update', () => { + it('does not render button if user cannot edit or update', async () => { const survey = { id: 2, title: 'Survey', @@ -19,7 +19,9 @@ describe('', () => { , ); - expect(page.queryByRole('button')).not.toBeInTheDocument(); + await waitFor(() => + expect(page.queryByRole('button')).not.toBeInTheDocument(), + ); }); it('allows surveys to be deleted', async () => { @@ -37,7 +39,7 @@ describe('', () => { , ); - fireEvent.click(page.getByRole('button')); + fireEvent.click(await page.findByRole('button')); fireEvent.click(page.getByText('Delete Survey')); fireEvent.click(page.getByRole('button', { name: 'Delete' })); @@ -74,7 +76,7 @@ describe('', () => { , ); - fireEvent.click(page.getByRole('button')); + fireEvent.click(await page.findByRole('button')); fireEvent.click(page.getByText('Edit Survey')); const description = 'To update description'; diff --git a/client/app/bundles/course/survey/pages/ResponseIndex/__test__/RemindButton.test.tsx b/client/app/bundles/course/survey/pages/ResponseIndex/__test__/RemindButton.test.tsx index b573d452e08..0737b339cee 100644 --- a/client/app/bundles/course/survey/pages/ResponseIndex/__test__/RemindButton.test.tsx +++ b/client/app/bundles/course/survey/pages/ResponseIndex/__test__/RemindButton.test.tsx @@ -6,11 +6,11 @@ import { CourseUserType } from 'lib/components/core/CourseUserTypeTabs'; import RemindButton from '../RemindButton'; describe('', () => { - it('renders confirmation dialog that triggers the reminder', () => { + it('renders confirmation dialog that triggers the reminder', async () => { const spyRemind = jest.spyOn(CourseAPI.survey.surveys, 'remind'); const page = render(); - const button = page.getByRole('button'); + const button = await page.findByRole('button'); fireEvent.click(button); fireEvent.click(page.getByText('Cancel')); diff --git a/client/app/bundles/course/survey/pages/ResponseShow/__test__/index.test.jsx b/client/app/bundles/course/survey/pages/ResponseShow/__test__/index.test.jsx index eefc8b20d54..d76f7aabf30 100644 --- a/client/app/bundles/course/survey/pages/ResponseShow/__test__/index.test.jsx +++ b/client/app/bundles/course/survey/pages/ResponseShow/__test__/index.test.jsx @@ -1,4 +1,4 @@ -import { render } from 'test-utils'; +import { render, waitFor } from 'test-utils'; import CourseAPI from 'api/course'; import { LOADING_INDICATOR_TEST_ID } from 'lib/components/core/LoadingIndicator'; @@ -25,7 +25,7 @@ describe('', () => { [responseUrl], ); - expect(spyFetch).toHaveBeenCalled(); + await waitFor(() => expect(spyFetch).toHaveBeenCalled()); }); it('shows form and admin buttons if user has permissions and page is loaded', async () => { @@ -65,13 +65,13 @@ describe('', () => { [responseUrl], ); - expect(page.getByText(data.response.creator_name)).toBeVisible(); + expect(await page.findByText(data.response.creator_name)).toBeVisible(); expect(page.getByText(data.survey.description)).toBeVisible(); expect(page.getByRole('button', { name: 'View' })).toBeVisible(); expect(page.getByRole('button', { name: 'Unsubmit' })).toBeVisible(); }); - it('shows only description and loading indicator when loading', () => { + it('shows only description and loading indicator when loading', async () => { const surveyId = 2; const responseId = 2; @@ -103,7 +103,7 @@ describe('', () => { , ); - expect(page.getByText(data.survey.description)).toBeVisible(); + expect(await page.findByText(data.survey.description)).toBeVisible(); expect(page.getByTestId(LOADING_INDICATOR_TEST_ID)).toBeVisible(); }); }); diff --git a/client/app/bundles/course/survey/pages/SurveyIndex/__test__/NewSurveyButton.test.jsx b/client/app/bundles/course/survey/pages/SurveyIndex/__test__/NewSurveyButton.test.jsx index 16464a423b6..740ee64e02a 100644 --- a/client/app/bundles/course/survey/pages/SurveyIndex/__test__/NewSurveyButton.test.jsx +++ b/client/app/bundles/course/survey/pages/SurveyIndex/__test__/NewSurveyButton.test.jsx @@ -40,7 +40,7 @@ describe('', () => { { state }, ); - fireEvent.click(page.getByRole('button')); + fireEvent.click(await page.findByRole('button')); fireEvent.change(page.getByLabelText('Title', { exact: false }), { target: { value: survey.title }, diff --git a/client/app/bundles/course/survey/pages/SurveyResults/__test__/ResultsQuestion.test.tsx b/client/app/bundles/course/survey/pages/SurveyResults/__test__/ResultsQuestion.test.tsx index 3548e614e64..2e1d85ddc0b 100644 --- a/client/app/bundles/course/survey/pages/SurveyResults/__test__/ResultsQuestion.test.tsx +++ b/client/app/bundles/course/survey/pages/SurveyResults/__test__/ResultsQuestion.test.tsx @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { fireEvent, render, within } from 'test-utils'; +import { fireEvent, render, waitFor, within } from 'test-utils'; import ResultsQuestion from '../ResultsQuestion'; @@ -57,7 +57,7 @@ const getMultipleChoiceData = (optionCount) => { }; }; -const testExpandLongQuestion = (question): void => { +const testExpandLongQuestion = async (question) => { const page = render( { />, ); - expect(page.queryByRole('table')).not.toBeInTheDocument(); + await waitFor(() => + expect(page.queryByRole('table')).not.toBeInTheDocument(), + ); - const expandButton = page.getAllByRole('button')[0]; + const expandButton = (await page.findAllByRole('button'))[0]; fireEvent.click(expandButton); - expect(page.getByRole('table')).toBeVisible(); + expect(await page.findByRole('table')).toBeVisible(); }; describe('', () => { @@ -86,7 +88,7 @@ describe('', () => { testExpandLongQuestion(question); }); - it('allows sorting by percentage', () => { + it('allows sorting by percentage', async () => { const question = getMultipleChoiceData(2); const page = render( @@ -98,7 +100,7 @@ describe('', () => { />, ); - let lastRow = page.getAllByRole('row').at(-1)!; + let lastRow = (await page.findAllByRole('row')).at(-1)!; expect(within(lastRow).getByText('1')).toBeVisible(); const sortToggle = page.getByRole('checkbox'); diff --git a/client/app/bundles/course/survey/pages/SurveyShow/Section/__test__/DeleteSectionButton.test.tsx b/client/app/bundles/course/survey/pages/SurveyShow/Section/__test__/DeleteSectionButton.test.tsx index 5bf6e1d6d99..4e5cd8d86bd 100644 --- a/client/app/bundles/course/survey/pages/SurveyShow/Section/__test__/DeleteSectionButton.test.tsx +++ b/client/app/bundles/course/survey/pages/SurveyShow/Section/__test__/DeleteSectionButton.test.tsx @@ -6,7 +6,7 @@ import DeleteConfirmation from 'lib/containers/DeleteConfirmation'; import DeleteSectionButton from '../DeleteSectionButton'; describe('', () => { - it('injects handlers that allow survey sections to be deleted', () => { + it('injects handlers that allow survey sections to be deleted', async () => { const surveyId = 1; const sectionId = 7; const url = `/courses/${global.courseId}/surveys/${surveyId}`; @@ -21,7 +21,7 @@ describe('', () => { , ); - fireEvent.click(page.getByRole('button')); + fireEvent.click(await page.findByRole('button')); fireEvent.click(page.getByRole('button', { name: 'Delete' })); expect(spyDelete).toHaveBeenCalledWith(sectionId); diff --git a/client/app/bundles/course/survey/pages/SurveyShow/Section/__test__/EditSectionButton.test.jsx b/client/app/bundles/course/survey/pages/SurveyShow/Section/__test__/EditSectionButton.test.jsx index 04315662c15..23af599c47e 100644 --- a/client/app/bundles/course/survey/pages/SurveyShow/Section/__test__/EditSectionButton.test.jsx +++ b/client/app/bundles/course/survey/pages/SurveyShow/Section/__test__/EditSectionButton.test.jsx @@ -31,7 +31,7 @@ describe('', () => { , ); - fireEvent.click(page.getByRole('button')); + fireEvent.click(await page.findByRole('button')); fireEvent.change(page.getByLabelText('Description'), { target: { value: newDescription }, diff --git a/client/app/bundles/course/survey/pages/SurveyShow/Section/__test__/MoveDownButton.test.tsx b/client/app/bundles/course/survey/pages/SurveyShow/Section/__test__/MoveDownButton.test.tsx index de1daefabae..3561a3be517 100644 --- a/client/app/bundles/course/survey/pages/SurveyShow/Section/__test__/MoveDownButton.test.tsx +++ b/client/app/bundles/course/survey/pages/SurveyShow/Section/__test__/MoveDownButton.test.tsx @@ -22,7 +22,7 @@ describe('', () => { const spyMove = jest.spyOn(CourseAPI.survey.surveys, 'reorderSections'); const page = render(); - const moveDownButton = page.getByRole('button'); + const moveDownButton = await page.findByRole('button'); fireEvent.click(moveDownButton); diff --git a/client/app/bundles/course/survey/pages/SurveyShow/Section/__test__/MoveUpButton.test.jsx b/client/app/bundles/course/survey/pages/SurveyShow/Section/__test__/MoveUpButton.test.jsx index a4ef88a86a1..9c9a2e8c5dd 100644 --- a/client/app/bundles/course/survey/pages/SurveyShow/Section/__test__/MoveUpButton.test.jsx +++ b/client/app/bundles/course/survey/pages/SurveyShow/Section/__test__/MoveUpButton.test.jsx @@ -22,7 +22,7 @@ describe('', () => { const spyMove = jest.spyOn(CourseAPI.survey.surveys, 'reorderSections'); const page = render(); - const moveUpButton = page.getByRole('button'); + const moveUpButton = await page.findByRole('button'); fireEvent.click(moveUpButton); diff --git a/client/app/bundles/course/survey/pages/SurveyShow/Section/__test__/NewQuestionButton.test.jsx b/client/app/bundles/course/survey/pages/SurveyShow/Section/__test__/NewQuestionButton.test.jsx index ee4a749f913..d9aa3277108 100644 --- a/client/app/bundles/course/survey/pages/SurveyShow/Section/__test__/NewQuestionButton.test.jsx +++ b/client/app/bundles/course/survey/pages/SurveyShow/Section/__test__/NewQuestionButton.test.jsx @@ -20,7 +20,7 @@ describe('', () => { , ); - fireEvent.click(page.getByRole('button')); + fireEvent.click(await page.findByRole('button')); fireEvent.change(page.getByLabelText('Question Text', { exact: false }), { target: { value: questionText }, diff --git a/client/app/bundles/course/survey/pages/SurveyShow/__test__/DownloadResponsesButton.test.tsx b/client/app/bundles/course/survey/pages/SurveyShow/__test__/DownloadResponsesButton.test.tsx index 21d3541e7a5..41c26084cde 100644 --- a/client/app/bundles/course/survey/pages/SurveyShow/__test__/DownloadResponsesButton.test.tsx +++ b/client/app/bundles/course/survey/pages/SurveyShow/__test__/DownloadResponsesButton.test.tsx @@ -5,11 +5,11 @@ import CourseAPI from 'api/course'; import DownloadResponsesButton from '../DownloadResponsesButton'; describe('', () => { - it('injects handlers that allows survey responses to be downloaded', () => { + it('injects handlers that allows survey responses to be downloaded', async () => { const spyRemind = jest.spyOn(CourseAPI.survey.surveys, 'download'); const page = render(); - const downloadButton = page.getByRole('button'); + const downloadButton = await page.findByRole('button'); fireEvent.click(downloadButton); diff --git a/client/app/bundles/course/survey/pages/SurveyShow/__test__/NewSectionButton.test.jsx b/client/app/bundles/course/survey/pages/SurveyShow/__test__/NewSectionButton.test.jsx index 7996a9de6a4..44bd92f87ab 100644 --- a/client/app/bundles/course/survey/pages/SurveyShow/__test__/NewSectionButton.test.jsx +++ b/client/app/bundles/course/survey/pages/SurveyShow/__test__/NewSectionButton.test.jsx @@ -17,7 +17,7 @@ describe('', () => { , ); - const newSectionButton = page.getByRole('button'); + const newSectionButton = await page.findByRole('button'); fireEvent.click(newSectionButton); const titleField = page.getByLabelText('Title', { exact: false }); diff --git a/client/app/bundles/course/user-email-subscriptions/__test__/index.test.jsx b/client/app/bundles/course/user-email-subscriptions/__test__/index.test.jsx index 6d21e400c47..d25f7c0c861 100644 --- a/client/app/bundles/course/user-email-subscriptions/__test__/index.test.jsx +++ b/client/app/bundles/course/user-email-subscriptions/__test__/index.test.jsx @@ -42,7 +42,7 @@ describe('', () => { const page = render(, { state }); - const toggle = page.getByRole('checkbox'); + const toggle = await page.findByRole('checkbox'); fireEvent.click(toggle); await waitFor(() => { diff --git a/client/app/bundles/course/user-notification/components/__test__/LevelReachedPopup.test.tsx b/client/app/bundles/course/user-notification/components/__test__/LevelReachedPopup.test.tsx index 0b69d2ee68a..f08b309c520 100644 --- a/client/app/bundles/course/user-notification/components/__test__/LevelReachedPopup.test.tsx +++ b/client/app/bundles/course/user-notification/components/__test__/LevelReachedPopup.test.tsx @@ -3,19 +3,21 @@ import { LevelReachedNotification } from 'types/course/userNotifications'; import LevelReachedPopup from '../LevelReachedPopup'; -const renderPopup = (data: LevelReachedNotification): HTMLElement => { +const renderPopup = async ( + data: LevelReachedNotification, +): Promise => { const page = render( , ); - return page.getByRole('dialog'); + return page.findByRole('dialog'); }; describe('', () => { describe('when leaderboard is disabled', () => { - it('shows the reached level but does not show leaderboard button', () => { + it('shows the reached level but does not show leaderboard button', async () => { const popup = within( - renderPopup({ + await renderPopup({ id: 69, notificationType: 'levelReached', levelNumber: 5, @@ -33,9 +35,9 @@ describe('', () => { }); describe('when the student is on the leaderboard', () => { - it('shows the reached level, position, and the leaderboard button', () => { + it('shows the reached level, position, and the leaderboard button', async () => { const popup = within( - renderPopup({ + await renderPopup({ id: 69, notificationType: 'levelReached', levelNumber: 5, @@ -51,9 +53,9 @@ describe('', () => { }); describe('when the student is not on the leaderboard', () => { - it('shows the reached level, leaderboard button, but no position', () => { + it('shows the reached level, leaderboard button, but no position', async () => { const popup = within( - renderPopup({ + await renderPopup({ id: 69, notificationType: 'levelReached', levelNumber: 5, diff --git a/client/app/bundles/course/video/submission/containers/VideoPlayer.jsx b/client/app/bundles/course/video/submission/containers/VideoPlayer.jsx index a1c3c8e0b83..cf8332f1a8b 100644 --- a/client/app/bundles/course/video/submission/containers/VideoPlayer.jsx +++ b/client/app/bundles/course/video/submission/containers/VideoPlayer.jsx @@ -82,7 +82,7 @@ class VideoPlayer extends Component { UNSAFE_componentWillMount() { if (VideoPlayer.ReactPlayer !== undefined) return; // Already loaded - import(/* webpackChunkName: "video" */ 'react-player').then( + import(/* webpackChunkName: "video" */ 'react-player/youtube').then( (ReactPlayer) => { VideoPlayer.ReactPlayer = ReactPlayer.default; this.forceUpdate(); diff --git a/client/app/index.tsx b/client/app/index.tsx index 4f9a06b92d1..193c778ab48 100644 --- a/client/app/index.tsx +++ b/client/app/index.tsx @@ -1,8 +1,6 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; -import './initializers'; - import App from './App'; import 'theme/index.css'; diff --git a/client/app/initializers.js b/client/app/initializers.js deleted file mode 100644 index eb9d60a4816..00000000000 --- a/client/app/initializers.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable global-require */ - -function loadModules() { - require('lib/initializers/ace-editor'); - // Require web font last so that it doesn't block the load of current module. - require('lib/initializers/webfont'); -} - -if (!global.Intl) { - Promise.all([ - import(/* webpackChunkName: "intl" */ 'intl'), - import(/* webpackChunkName: "intl" */ 'intl/locale-data/jsonp/en'), - import(/* webpackChunkName: "intl" */ 'intl/locale-data/jsonp/zh'), - ]) - .then(() => { - loadModules(); - }) - .catch((e) => { - throw e; - }); -} else { - loadModules(); -} diff --git a/client/app/lib/components/core/__test__/ErrorText.test.tsx b/client/app/lib/components/core/__test__/ErrorText.test.tsx index 5aa4bd0a73d..37dbfe23b42 100644 --- a/client/app/lib/components/core/__test__/ErrorText.test.tsx +++ b/client/app/lib/components/core/__test__/ErrorText.test.tsx @@ -1,4 +1,4 @@ -import { render } from 'test-utils'; +import { render, waitForElementToBeRemoved } from 'test-utils'; import ErrorText from '../ErrorText'; @@ -6,16 +6,22 @@ describe('', () => { describe('when input is a string', () => { const errors = 'An error.'; - it('displays it', () => { - expect(render()).toMatchSnapshot(); + it('displays it', async () => { + const page = render(); + await waitForElementToBeRemoved(page.getByRole('progressbar')); + + expect(page).toMatchSnapshot(); }); }); describe('when input is an array', () => { const errors = ['An error.', 'Another error.']; - it('displays each error', () => { - expect(render()).toMatchSnapshot(); + it('displays each error', async () => { + const page = render(); + await waitForElementToBeRemoved(page.getByRole('progressbar')); + + expect(page).toMatchSnapshot(); }); }); }); diff --git a/client/app/lib/components/core/__test__/LoadingIndicator.test.tsx b/client/app/lib/components/core/__test__/LoadingIndicator.test.tsx index 4d0cd5888a4..427cc222218 100644 --- a/client/app/lib/components/core/__test__/LoadingIndicator.test.tsx +++ b/client/app/lib/components/core/__test__/LoadingIndicator.test.tsx @@ -1,4 +1,9 @@ -import { render, RenderResult } from 'test-utils'; +import { + render, + RenderResult, + screen, + waitForElementToBeRemoved, +} from 'test-utils'; import LoadingIndicator, { LOADING_INDICATOR_TEST_ID, @@ -7,8 +12,9 @@ import LoadingIndicator, { let documentBody: RenderResult; describe('', () => { - beforeEach(() => { + beforeEach(async () => { documentBody = render(); + await waitForElementToBeRemoved(screen.getByRole('progressbar')); }); it('shows the loading indicator', () => { diff --git a/client/app/lib/components/core/buttons/__test__/DeleteButton.test.tsx b/client/app/lib/components/core/buttons/__test__/DeleteButton.test.tsx index 0761829c63e..a7e825cb109 100644 --- a/client/app/lib/components/core/buttons/__test__/DeleteButton.test.tsx +++ b/client/app/lib/components/core/buttons/__test__/DeleteButton.test.tsx @@ -1,4 +1,11 @@ -import { fireEvent, render, RenderResult, screen } from 'test-utils'; +import { + fireEvent, + render, + RenderResult, + screen, + waitFor, + waitForElementToBeRemoved, +} from 'test-utils'; import DeleteButton from '../DeleteButton'; @@ -14,22 +21,29 @@ describe('', () => { ); }); - it('shows the delete icon button', () => { - expect(documentBody.getByTestId('DeleteIconButton')).toBeVisible(); + it('shows the delete icon button', async () => { + expect(await documentBody.findByTestId('DeleteIconButton')).toBeVisible(); expect(documentBody.getByTestId('DeleteIcon')).toBeVisible(); }); - it('does not show the confirmation dialog when clicked', () => { + it('does not show the confirmation dialog when clicked', async () => { // Before clicking - expect(documentBody.queryByTitle(PROMPT_TITLE)).toBeNull(); + await waitFor(() => + expect(documentBody.queryByTitle(PROMPT_TITLE)).not.toBeInTheDocument(), + ); + + fireEvent.click(await screen.findByTestId('DeleteIconButton')); - fireEvent.click(screen.getByTestId('DeleteIconButton')); // After clicking - expect(documentBody.queryByTitle(PROMPT_TITLE)).toBeNull(); + await waitFor(() => + expect(documentBody.queryByTitle(PROMPT_TITLE)).toBeNull(), + ); }); - it('matches the snapshot', () => { + it('matches the snapshot', async () => { const { baseElement } = documentBody; + await waitForElementToBeRemoved(screen.getByRole('progressbar')); + expect(baseElement).toMatchSnapshot(); }); }); @@ -46,8 +60,8 @@ describe('', () => { ); }); - it('shows the delete icon button', () => { - expect(documentBody.getByTestId('DeleteIconButton')).toBeVisible(); + it('shows the delete icon button', async () => { + expect(await documentBody.findByTestId('DeleteIconButton')).toBeVisible(); expect(documentBody.getByTestId('DeleteIcon')).toBeVisible(); expect(documentBody.getByTestId('DeleteIconButton')).toBeDisabled(); }); @@ -65,16 +79,18 @@ describe('', () => { ); }); - it('shows the delete icon button', () => { - expect(documentBody.getByTestId('DeleteIconButton')).toBeVisible(); + it('shows the delete icon button', async () => { + expect(await documentBody.findByTestId('DeleteIconButton')).toBeVisible(); expect(documentBody.getByTestId('DeleteIcon')).toBeVisible(); }); - it('shows the confirmation dialog when clicked', () => { + it('shows the confirmation dialog when clicked', async () => { // Before clicking delete button - expect(documentBody.queryByTitle(PROMPT_TITLE)).toBeNull(); + await waitFor(() => + expect(documentBody.queryByTitle(PROMPT_TITLE)).not.toBeInTheDocument(), + ); - fireEvent.click(screen.getByTestId('DeleteIconButton')); + fireEvent.click(await screen.findByTestId('DeleteIconButton')); // After clicking delete button expect(documentBody.getByText(PROMPT_TITLE)).toBeVisible(); diff --git a/client/app/lib/components/core/buttons/__test__/EditButton.test.tsx b/client/app/lib/components/core/buttons/__test__/EditButton.test.tsx index 91331878ef5..62e4f4556c0 100644 --- a/client/app/lib/components/core/buttons/__test__/EditButton.test.tsx +++ b/client/app/lib/components/core/buttons/__test__/EditButton.test.tsx @@ -1,4 +1,9 @@ -import { render, RenderResult } from 'test-utils'; +import { + render, + RenderResult, + screen, + waitForElementToBeRemoved, +} from 'test-utils'; import EditButton from '../EditButton'; @@ -9,13 +14,15 @@ describe('', () => { documentBody = render(); }); - it('shows the edit icon button', () => { - expect(documentBody.getByTestId('EditIconButton')).toBeVisible(); + it('shows the edit icon button', async () => { + expect(await documentBody.findByTestId('EditIconButton')).toBeVisible(); expect(documentBody.getByTestId('EditIcon')).toBeVisible(); }); - it('matches the snapshot', () => { + it('matches the snapshot', async () => { const { baseElement } = documentBody; + await waitForElementToBeRemoved(screen.getByRole('progressbar')); + expect(baseElement).toMatchSnapshot(); }); }); diff --git a/client/app/lib/components/core/fields/CKEditor.css b/client/app/lib/components/core/fields/CKEditor.css index cdec3ab717f..cb1f81e2c40 100644 --- a/client/app/lib/components/core/fields/CKEditor.css +++ b/client/app/lib/components/core/fields/CKEditor.css @@ -1,6 +1,5 @@ /* Below are needed to ensure ckeditor popup (eg link) is rendered properly in MUI dialog */ - .ck-body-wrapper { position: absolute; z-index: 1500; @@ -8,3 +7,7 @@ is rendered properly in MUI dialog */ display: none; } } + +.ck-editor__editable { + max-height: 35rem; +} diff --git a/client/app/lib/components/core/fields/CKEditorField.tsx b/client/app/lib/components/core/fields/CKEditorField.tsx new file mode 100644 index 00000000000..01c96b50d31 --- /dev/null +++ b/client/app/lib/components/core/fields/CKEditorField.tsx @@ -0,0 +1,63 @@ +import { CKEditor } from '@ckeditor/ckeditor5-react'; +import type { FileLoader, UploadAdapter, UploadResponse } from 'ckeditor5'; +import ClassicEditor from 'coursemology-ckeditor'; + +import attachmentsAPI from 'api/Attachments'; + +import 'coursemology-ckeditor/build/index.css'; +import './CKEditor.css'; + +class SimpleUploadAdapter implements UploadAdapter { + private loader: FileLoader; + + constructor(loader: FileLoader) { + this.loader = loader; + } + + async upload(): Promise { + const file = await this.loader.file; + if (file === null) return {}; + + const data = (await attachmentsAPI.create(file)).data; + if (!data.success) return {}; + + return { default: `/attachments/${data.id}` }; + } +} + +const CKEditorField = ({ + placeholder, + disabled, + value, + autoFocus, + onChange, + onBlur, + onFocus, +}: { + placeholder?: string; + disabled?: boolean; + value?: string; + autoFocus?: boolean; + onChange?: (value: string) => void; + onBlur?: () => void; + onFocus?: () => void; +}): JSX.Element => ( + onChange?.(editor.getData())} + onFocus={onFocus} + onReady={(editor) => { + editor.plugins.get('FileRepository').createUploadAdapter = ( + loader, + ): UploadAdapter => new SimpleUploadAdapter(loader); + + if (autoFocus) editor.focus(); + }} + /> +); + +export default CKEditorField; diff --git a/client/app/lib/components/core/fields/CKEditorRichText.tsx b/client/app/lib/components/core/fields/CKEditorRichText.tsx index 8f22ac841e9..383c92f9f73 100644 --- a/client/app/lib/components/core/fields/CKEditorRichText.tsx +++ b/client/app/lib/components/core/fields/CKEditorRichText.tsx @@ -1,15 +1,25 @@ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { useState } from 'react'; -import CustomEditor from '@ckeditor/ckeditor5-build-custom'; -import { CKEditor } from '@ckeditor/ckeditor5-react'; -import { FormHelperText, InputLabel } from '@mui/material'; +import { lazy, Suspense, useState } from 'react'; +import { FormHelperText, InputLabel, Skeleton } from '@mui/material'; import { cyan } from '@mui/material/colors'; -import attachmentsAPI from 'api/Attachments'; +const CKEditorField = lazy( + () => import(/* webpackChunkName: "CKEditorField" */ './CKEditorField'), +); -import './CKEditor.css'; - -interface Props { +const CKEditorRichText = ({ + label, + value, + onChange, + disabled, + error, + field, + required, + name, + inputId, + disableMargins, + placeholder, + autofocus, +}: { name: string; onChange: (text: string) => void; value: string; @@ -22,57 +32,15 @@ interface Props { label?: string; placeholder?: string; required?: boolean | undefined; -} - -const uploadAdapter = (loader) => { - return { - upload: () => - new Promise((resolve, reject) => { - loader.file.then((file: File) => { - attachmentsAPI - .create(file) - .then((response) => response.data) - .then((data) => { - if (data.success) { - resolve({ default: `/attachments/${data.id}` }); - } - }) - .catch((err) => { - reject(err); - }); - }); - }), - abort: () => {}, - }; -}; - -const CKEditorRichText = (props: Props) => { - const { - label, - value, - onChange, - disabled, - error, - field, - required, - name, - inputId, - disableMargins, - placeholder, - autofocus, - } = props; - +}): JSX.Element => { const [isFocused, setIsFocused] = useState(false); const textFieldLabelColor = isFocused ? cyan[500] : undefined; return (
{ > {label && ( {label} )} +