test: add EditorFooter tests (#16)

* test: add EditorFooter tests
This commit is contained in:
connorhaugh
2022-02-22 13:24:15 -05:00
committed by GitHub
parent d2e89d7b28
commit 6ca93a7297
4 changed files with 239 additions and 102 deletions

View File

@@ -0,0 +1,142 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EditorFooter snapshots Save Failed, error message raised 1`] = `
<div
className="editor-footer mt-auto"
>
<Toast
onClose={[MockFunction nullMethod]}
show={true}
>
<FormattedMessage
defaultMessage="Error: Content save failed. Try again later."
description="Error message displayed when content fails to save."
id="authoring.editorfooter.save.error"
/>
</Toast>
<Component>
<ActionRow>
<Component />
<Button
aria-label="Discard Changes and Return to Learning Context"
onClick={
Object {
"handleCancelClicked": Object {
"returnUrl": "hocuspocus.ca",
},
}
}
variant="tertiary"
>
Cancel
</Button>
<Button
aria-label="Save Changes and Return to Learning Context"
disabled={false}
onClick={
Object {
"handleSaveClicked": Object {
"editorRef": [MockFunction args.editorRef],
"returnUrl": "hocuspocus.ca",
"saveBlock": [MockFunction args.saveBlock],
},
}
}
>
<FormattedMessage
defaultMessage="Save"
description="Label for Save button"
id="authoring.editorfooter.savebutton.label"
/>
</Button>
</ActionRow>
</Component>
</div>
`;
exports[`EditorFooter snapshots not intialized, Spinner appears and button is disabled 1`] = `
<div
className="editor-footer mt-auto"
>
<Component>
<ActionRow>
<Component />
<Button
aria-label="Discard Changes and Return to Learning Context"
onClick={
Object {
"handleCancelClicked": Object {
"returnUrl": "hocuspocus.ca",
},
}
}
variant="tertiary"
>
Cancel
</Button>
<Button
aria-label="Save Changes and Return to Learning Context"
disabled={true}
onClick={
Object {
"handleSaveClicked": Object {
"editorRef": [MockFunction args.editorRef],
"returnUrl": "hocuspocus.ca",
"saveBlock": [MockFunction args.saveBlock],
},
}
}
>
<Spinner
animation="border"
className="mr-3"
/>
</Button>
</ActionRow>
</Component>
</div>
`;
exports[`EditorFooter snapshots renders as expected with default behavior 1`] = `
<div
className="editor-footer mt-auto"
>
<Component>
<ActionRow>
<Component />
<Button
aria-label="Discard Changes and Return to Learning Context"
onClick={
Object {
"handleCancelClicked": Object {
"returnUrl": "hocuspocus.ca",
},
}
}
variant="tertiary"
>
Cancel
</Button>
<Button
aria-label="Save Changes and Return to Learning Context"
disabled={false}
onClick={
Object {
"handleSaveClicked": Object {
"editorRef": [MockFunction args.editorRef],
"returnUrl": "hocuspocus.ca",
"saveBlock": [MockFunction args.saveBlock],
},
}
}
>
<FormattedMessage
defaultMessage="Save"
description="Label for Save button"
id="authoring.editorfooter.savebutton.label"
/>
</Button>
</ActionRow>
</Component>
</div>
`;

View File

@@ -10,10 +10,11 @@ import {
Toast,
} from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { nullMethod, saveTextBlock, navigateCallback } from '../../hooks';
import { RequestKeys } from '../../data/constants/requests';
import { selectors, thunkActions } from '../../data/redux';
import { saveTextBlock, navigateCallback } from '../../hooks';
import messages from '../messages';
import * as module from '.';
@@ -30,7 +31,7 @@ export const EditorFooter = ({
}) => (
<div className="editor-footer mt-auto">
{saveFailed && (
<Toast><FormattedMessage {...messages.contentSaveFailed} /></Toast>
<Toast show onClose={nullMethod}><FormattedMessage {...messages.contentSaveFailed} /></Toast>
)}
<ModalDialog.Footer>

View File

@@ -1,104 +1,93 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { mount } from 'enzyme';
import EditorFooter from './EditorFooter';
import EditorPageContext from './EditorPageContext';
import { ActionStates } from './data/constants';
import EditorPageProvider from './EditorPageProvider';
import { saveBlock } from './data/api';
import { shallow } from 'enzyme';
import * as module from './index';
import { selectors, thunkActions } from '../../data/redux';
import { RequestKeys } from '../../data/constants/requests';
import { saveTextBlock, navigateCallback } from '../../hooks';
const locationTemp = window.location;
beforeAll(() => {
delete window.location;
window.location = {
assign: jest.fn(),
};
});
afterAll(() => {
window.location = locationTemp;
});
jest.mock('./data/api', () => {
const originalModule = jest.requireActual('./data/api');
// Mock the default export and named export saveBlock
return {
__esModule: true,
...originalModule,
saveBlock: jest.fn(() => {}),
};
});
jest.spyOn(React, 'useRef').mockReturnValue({
current: {
getContent: () => '',
},
});
test('Rendering: loaded', () => {
const context = {
unitUrlLoading: ActionStates.FINISHED,
};
render(
<EditorPageContext.Provider value={context}>
<EditorFooter />
</EditorPageContext.Provider>,
);
expect(screen.getByText('Cancel')).toBeTruthy();
expect(screen.getByText('Add To Course')).toBeTruthy();
});
test('Rendering: loading url', () => {
const context = {
unitUrlLoading: ActionStates.NOT_BEGUN,
};
render(
<EditorPageContext.Provider value={context}>
<EditorFooter />
</EditorPageContext.Provider>,
);
expect(screen.getByText('Cancel')).toBeTruthy();
expect(screen.getAllByRole('button', { 'aria-label': 'Save' })).toBeTruthy();
expect(screen.queryByText('Add To Course')).toBeNull();
});
test('Navigation: Cancel', () => {
const context = {
unitUrlLoading: ActionStates.FINISHED,
unitUrl: {
data: {
ancestors:
[
{ id: 'fakeblockid' },
],
},
jest.mock('../../data/redux', () => ({
thunkActions: {
app: {
saveBlock: jest.fn().mockName('thunkActions.app.saveBlock'),
},
studioEndpointUrl: 'Testurl',
};
render(
<EditorPageContext.Provider value={context}>
<EditorFooter />
</EditorPageContext.Provider>,
);
expect(screen.getByText('Cancel')).toBeTruthy();
userEvent.click(screen.getByText('Cancel'));
expect(window.location.assign).toHaveBeenCalled();
});
},
selectors: {
app: {
isInitialized: jest.fn(state => ({ isInitialized: state })),
studioEndpointUrl: jest.fn(state => ({ studioEndpointUrl: state })),
},
requests: {
isFailed: jest.fn((state, params) => ({ isFailed: { state, params } })),
},
},
}));
test('Navigation: Save', () => {
const wrapper = mount(
<EditorPageProvider
blockType="html"
courseId="myCourse101"
blockId="redosablocksalot"
studioEndpointUrl="celaicboss.axe"
>
<EditorFooter />
</EditorPageProvider>,
);
const button = wrapper.find({ children: 'Add To Course' });
expect(button).toBeTruthy();
button.simulate('click');
expect(saveBlock).toHaveBeenCalled();
expect(window.location.assign).toHaveBeenCalled();
jest.mock('.', () => ({
__esModule: true, // Use it when dealing with esModules
...jest.requireActual('./index'),
handleCancelClicked: jest.fn(args => ({ handleCancelClicked: args })),
handleSaveClicked: jest.fn(args => ({ handleSaveClicked: args })),
}
));
jest.mock('../../hooks', () => ({
saveTextBlock: jest.fn(),
navigateCallback: jest.fn(),
nullMethod: jest.fn().mockName('nullMethod'),
}));
describe('EditorFooter', () => {
const props = {
editorRef: jest.fn().mockName('args.editorRef'),
isInitialized: true,
returnUrl: 'hocuspocus.ca',
saveFailed: false,
saveBlock: jest.fn().mockName('args.saveBlock'),
};
describe('behavior', () => {
const realmodule = jest.requireActual('./index');
test('handleSaveClicked calls saveTextBlock', () => {
const createdCallback = realmodule.handleSaveClicked(props);
createdCallback();
expect(saveTextBlock).toHaveBeenCalled();
});
test('handleCancelClicked calls navigateCallback', () => {
realmodule.handleCancelClicked({ returnUrl: props.returnUrl });
expect(navigateCallback).toHaveBeenCalledWith(props.returnUrl);
});
});
describe('snapshots', () => {
test('renders as expected with default behavior', () => {
expect(shallow(<module.EditorFooter {...props} />)).toMatchSnapshot();
});
test('not intialized, Spinner appears and button is disabled', () => {
expect(shallow(<module.EditorFooter {...props} isInitialized={false} />)).toMatchSnapshot();
});
test('Save Failed, error message raised', () => {
expect(shallow(<module.EditorFooter {...props} saveFailed />)).toMatchSnapshot();
});
});
describe('mapStateToProps', () => {
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
test('isInitialized from app.isInitialized', () => {
expect(
module.mapStateToProps(testState).isInitialized,
).toEqual(selectors.app.isInitialized(testState));
});
test('studioEndpointUrl from app.studioEndpointUrl', () => {
expect(
module.mapStateToProps(testState).studioEndpointUrl,
).toEqual(selectors.app.studioEndpointUrl(testState));
});
test('saveFailed from requests.isFailed', () => {
expect(
module.mapStateToProps(testState).saveFailed,
).toEqual(selectors.requests.isFailed(testState, { requestKey: RequestKeys.saveBlock }));
});
});
describe('mapDispatchToProps', () => {
test('saveBlock from thunkActions.app.saveBlock', () => {
expect(module.mapDispatchToProps.saveBlock).toEqual(thunkActions.app.saveBlock);
});
});
});

View File

@@ -1,4 +1,6 @@
import { useRef, useEffect, useCallback, useState } from 'react';
import {
useRef, useEffect, useCallback, useState,
} from 'react';
export const initializeApp = ({ initialize, data }) => useEffect(() => initialize(data), []);
@@ -28,3 +30,6 @@ export const saveTextBlock = ({
content: editorRef.current.getContent(),
});
};
// for toast onClose to avoid console warnings
export const nullMethod = () => {};