diff --git a/src/components/forms/data/reducer.ts b/src/components/forms/data/reducer.ts index 8f9d8bb343..1aa424be21 100644 --- a/src/components/forms/data/reducer.ts +++ b/src/components/forms/data/reducer.ts @@ -131,7 +131,6 @@ export const FormReducer: FormReducerType = ( ...oldStateMap, [setStateArgs.name]: setStateArgs.state, }; - console.log('setting state to ', newStateMap); return { ...state, stateMap: newStateMap }; } default: return state; diff --git a/src/components/settings/SettingsLMSTab/ErrorReporting/tests/SyncHistory.test.jsx b/src/components/settings/SettingsLMSTab/ErrorReporting/tests/SyncHistory.test.jsx new file mode 100644 index 0000000000..ffcede173a --- /dev/null +++ b/src/components/settings/SettingsLMSTab/ErrorReporting/tests/SyncHistory.test.jsx @@ -0,0 +1,158 @@ +import React from 'react'; +import { + cleanup, screen, waitFor, waitForElementToBeRemoved, +} from '@testing-library/react'; +import '@testing-library/jest-dom'; +import configureMockStore from 'redux-mock-store'; + +import thunk from 'redux-thunk'; +import { Provider } from 'react-redux'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; +import { features } from '../../../../../config'; + +import { renderWithRouter } from '../../../../test/testUtils'; +import SettingsLMSTab from '../..'; +import LmsApiService from '../../../../../data/services/LmsApiService'; +import { getChannelMap } from '../../../../../utils'; + +const mockFetch = jest.fn(); +mockFetch.mockResolvedValue({ data: { refresh_token: 'foobar' } }); + +const channelMapReturn = { + BLACKBOARD: { + fetch: mockFetch, + }, +}; + +jest.mock('../../../../../utils', () => ({ + ...jest.requireActual('../../../../../utils'), + getChannelMap: jest.fn(), +})); + +getChannelMap.mockReturnValue(channelMapReturn); + +const enterpriseId = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'; +const enterpriseSlug = 'test-slug'; +const enableSamlConfigurationScreen = false; +const identityProvider = ''; + +const initialState = { + portalConfiguration: { + enterpriseId, enterpriseSlug, enableSamlConfigurationScreen, identityProvider, + }, +}; + +const existingConfigData = { + data: [{ + channelCode: 'BLACKBOARD', + id: 1, + isValid: [{ missing: [] }, { incorrect: [] }], + active: true, + displayName: 'squish', + enterpriseCustomer: enterpriseId, + lastSyncAttemptedAt: '2022-11-22T20:59:56Z', + lastContentSyncAttemptedAt: '2022-11-22T20:59:56Z', + lastLearnerSyncAttemptedAt: null, + lastSyncErroredAt: null, + lastContentSyncErroredAt: null, + lastLearnerSyncErroredAt: null, + lastModifiedAt: '2023-05-05T14:51:53.473144Z', + }], +}; + +const configData = { + data: { + channelCode: 'BLACKBOARD', + id: 1, + isValid: [{ missing: [] }, { incorrect: [] }], + active: true, + displayName: 'foobar', + enterpriseCustomer: enterpriseId, + lastSyncAttemptedAt: '2022-11-22T20:59:56Z', + lastContentSyncAttemptedAt: '2022-11-22T20:59:56Z', + lastLearnerSyncAttemptedAt: null, + lastSyncErroredAt: null, + lastContentSyncErroredAt: null, + lastLearnerSyncErroredAt: null, + lastModifiedAt: '2023-05-05T14:51:53.473144Z', + }, +}; + +const mockStore = configureMockStore([thunk]); +window.open = jest.fn(); + +describe('Test sync history page full flow', () => { + afterEach(() => { + cleanup(); + jest.clearAllMocks(); + }); + + test('Test opening sync history page', async () => { + getAuthenticatedUser.mockReturnValue({ + administrator: true, + }); + features.FEATURE_INTEGRATION_REPORTING = true; + + const mockFetchSingleConfig = jest.spyOn(LmsApiService, 'fetchSingleBlackboardConfig'); + const mockFetchExistingConfigs = jest.spyOn(LmsApiService, 'fetchEnterpriseCustomerIntegrationConfigs'); + mockFetchSingleConfig.mockResolvedValue(configData); + mockFetchExistingConfigs.mockResolvedValue(existingConfigData); + + const SettingsLMSWrapper = () => ( + + + + + + ); + + renderWithRouter(); + const skeleton = screen.getAllByTestId('skeleton'); + await waitForElementToBeRemoved(skeleton); + await (expect(screen.getByText('squish'))); + + const syncHistoryButton = screen.getByText('View sync history'); + expect(syncHistoryButton.getAttribute('href')).toBe(`/${configData.data.channelCode}/${configData.data.id}`); + }); + test('Test configure action from sync history', async () => { + const mockFetchSingleConfig = jest.spyOn(LmsApiService, 'fetchSingleBlackboardConfig'); + mockFetchSingleConfig.mockResolvedValue(configData); + + const location = { + ...window.location, + search: `?lms=${configData.data.channelCode}&id=${configData.data.id}`, + }; + Object.defineProperty(window, 'location', { + writable: true, + value: location, + }); + + const SettingsLMSWrapper = () => ( + + + + + + ); + + renderWithRouter(); + expect(window.location.search).toEqual(`?lms=${configData.data.channelCode}&id=${configData.data.id}`); + await waitFor(() => expect(mockFetch).toHaveBeenCalledWith('1')); + // opens stepper + await waitFor(() => expect(screen.getByText('New learning platform integration'))); + screen.debug(undefined, 100000); + }); +}); diff --git a/src/components/settings/SettingsLMSTab/index.jsx b/src/components/settings/SettingsLMSTab/index.jsx index 7103e24e69..54c3c3fa1d 100644 --- a/src/components/settings/SettingsLMSTab/index.jsx +++ b/src/components/settings/SettingsLMSTab/index.jsx @@ -1,5 +1,7 @@ import _ from 'lodash'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { + useCallback, useEffect, useMemo, useState, +} from 'react'; import { Link } from 'react-router-dom'; import PropTypes from 'prop-types'; @@ -10,7 +12,7 @@ import { import { Add, Info } from '@edx/paragon/icons'; import { logError } from '@edx/frontend-platform/logging'; -import { camelCaseDictArray, channelMapping } from '../../../utils'; +import { camelCaseDictArray, getChannelMap } from '../../../utils'; import LMSConfigPage from './LMSConfigPage'; import ExistingLMSCardDeck from './ExistingLMSCardDeck'; import NoConfigCard from './NoConfigCard'; @@ -44,6 +46,7 @@ const SettingsLMSTab = ({ const [isLmsStepperOpen, openLmsStepper, closeLmsStepper] = useToggle(false); const toastMessages = [ACTIVATE_TOAST_MESSAGE, DELETE_TOAST_MESSAGE, INACTIVATE_TOAST_MESSAGE, SUBMIT_TOAST_MESSAGE]; const { dispatch } = useFormContext(); + const channelMap = useMemo(() => getChannelMap(), []); // onClick function for existing config cards' edit action const editExistingConfig = useCallback((configData, configType) => { @@ -62,18 +65,20 @@ const SettingsLMSTab = ({ openLmsStepper(); }, [dispatch, openLmsStepper]); - // we pass in params (configId and lmsType) from SyncHistory when user wants to edit that config useEffect(() => { const query = new URLSearchParams(window.location.search); - const fetchData = async () => channelMapping[query.get('lms')].fetch(query.get('id')); - fetchData() - .then((response) => { - editExistingConfig(camelCaseObject(response.data), query.get('id')); - }) - .catch((err) => { - logError(err); - }); - }, [editExistingConfig]); + // if we have passed in params (lmsType and configId) from SyncHistory, user wants to edit that config + if (query.has('lms') && query.has('id')) { + const fetchData = async () => channelMap[query.get('lms')].fetch(query.get('id')); + fetchData() + .then((response) => { + editExistingConfig(camelCaseObject(response.data), query.get('id')); + }) + .catch((err) => { + logError(err); + }); + } + }, [channelMap, editExistingConfig]); const fetchExistingConfigs = useCallback(() => { const options = { enterprise_customer: enterpriseId };