feat: transcript parent widget component

This commit is contained in:
Kristin Aoki
2022-09-14 11:07:20 -04:00
committed by GitHub
parent 3f303a718d
commit ff636837cf
25 changed files with 811 additions and 114 deletions

View File

@@ -0,0 +1,73 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Alert } from '@edx/paragon';
import { Info } from '@edx/paragon/icons';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import messages from './messages';
export const hooks = {
state: {
isDismissed: (val) => React.useState(val),
},
dismissalHooks: ({ dismissError, isError }) => {
const [isDismissed, setIsDismissed] = hooks.state.isDismissed(false);
React.useEffect(() => {
setIsDismissed(isDismissed && !isError);
},
[isError]);
return {
isDismissed,
dismissAlert: () => {
setIsDismissed(true);
dismissError();
},
};
},
};
export const ErrorAlert = ({
dismissError,
hideHeading,
isError,
children,
}) => {
const { isDismissed, dismissAlert } = hooks.dismissalHooks({ dismissError, isError });
if (!isError || isDismissed) {
return null;
}
return (
<Alert
variant="danger"
icon={Info}
dismissible
onClose={dismissAlert}
>
{!hideHeading
&& (
<Alert.Heading>
<FormattedMessage {...messages.errorTitle} />
</Alert.Heading>
)}
{children}
</Alert>
);
};
ErrorAlert.defaultProps = {
dismissError: null,
hideHeading: false,
};
ErrorAlert.propTypes = {
dismissError: PropTypes.func,
hideHeading: PropTypes.bool,
isError: PropTypes.bool.isRequired,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
};
export default ErrorAlert;

View File

@@ -0,0 +1,83 @@
import React from 'react';
import { shallow } from 'enzyme';
import * as module from './ErrorAlert';
import { MockUseState } from '../../../testUtils';
const { ErrorAlert } = module;
jest.mock('react', () => ({
...jest.requireActual('react'),
useRef: jest.fn(val => ({ current: val })),
useEffect: jest.fn(),
useCallback: (cb, prereqs) => ({ cb, prereqs }),
}));
const state = new MockUseState(module.hooks);
let hook;
const testValue = 'testVALUE';
describe('ErrorAlert component', () => {
describe('Hooks', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('state hooks', () => {
state.testGetter(state.keys.isDismissed);
});
describe('using state', () => {
beforeEach(() => { state.mock(); });
afterEach(() => { state.restore(); });
describe('dismissalHooks', () => {
const props = {
dismissError: jest.fn(),
isError: testValue,
};
beforeEach(() => {
hook = module.hooks.dismissalHooks(props);
});
it('returns isDismissed value, initialized to false', () => {
expect(state.stateVals.isDismissed).toEqual(hook.isDismissed);
});
test('dismissAlert sets isDismissed to true and calls dismissError', () => {
hook.dismissAlert();
expect(state.setState.isDismissed).toHaveBeenCalledWith(true);
expect(props.dismissError).toHaveBeenCalled();
});
test('On Render, calls setIsDismissed', () => {
expect(React.useEffect.mock.calls.length).toEqual(1);
const [cb, prereqs] = React.useEffect.mock.calls[0];
expect(prereqs[0]).toEqual(testValue);
cb();
expect(state.setState.isDismissed).toHaveBeenCalledWith(state.stateVals.isDismissed && !testValue);
});
});
});
});
describe('Component', () => {
describe('Snapshots', () => {
let props;
const msg = <p> An Error Message </p>;
beforeAll(() => {
props = {
dismissError: jest.fn(),
};
jest.spyOn(module.hooks, 'dismissalHooks').mockImplementation(() => ({
isDismissed: false,
dismissAlert: jest.fn().mockName('dismissAlert'),
}));
});
afterAll(() => {
jest.clearAllMocks();
});
test('snapshot: is Null when no error (ErrorAlert)', () => {
expect(shallow(<ErrorAlert {...props}> <p> An Error Message </p> </ErrorAlert>)).toMatchSnapshot();
});
test('snapshot: Loads children and component when error (ErrorAlert)', () => {
expect(shallow(<ErrorAlert {...props} isError hideHeading={false}>{msg}</ErrorAlert>)).toMatchSnapshot();
});
test('snapshot: Does not load heading when hideHeading is true', () => {
expect(shallow(<ErrorAlert {...props} isError hideHeading>{msg}</ErrorAlert>)).toMatchSnapshot();
});
});
});
});

View File

@@ -0,0 +1,39 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import ErrorAlert from './ErrorAlert';
import { selectors } from '../../data/redux';
import { RequestKeys } from '../../data/constants/requests';
export const FetchErrorAlert = ({
message,
// redux
isFetchError,
// inject
}) => (
<ErrorAlert
isError={isFetchError}
>
<FormattedMessage
{...message}
/>
</ErrorAlert>
);
FetchErrorAlert.propTypes = {
message: PropTypes.shape({
id: PropTypes.string,
defaultMessage: PropTypes.string,
description: PropTypes.string,
}).isRequired,
// redux
isFetchError: PropTypes.bool.isRequired,
};
export const mapStateToProps = (state) => ({
isFetchError: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchImages }),
});
export const mapDispatchToProps = {};
export default connect(mapStateToProps, mapDispatchToProps)(FetchErrorAlert);

View File

@@ -0,0 +1,29 @@
import React from 'react';
import { shallow } from 'enzyme';
import { FetchErrorAlert, mapStateToProps } from './FetchErrorAlert';
import { selectors } from '../../data/redux';
import { RequestKeys } from '../../data/constants/requests';
jest.mock('../../data/redux', () => ({
selectors: {
requests: {
isFailed: jest.fn((state, params) => ({ isFailed: { state, params } })),
},
},
}));
describe('FetchErrorAlert', () => {
describe('Snapshots', () => {
test('snapshot: is ErrorAlert with Message error (ErrorAlert)', () => {
expect(shallow(<FetchErrorAlert isFetchError />)).toMatchSnapshot();
});
});
describe('mapStateToProps', () => {
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
test('isFetchError from requests.isFinished', () => {
expect(
mapStateToProps(testState).isFetchError,
).toEqual(selectors.requests.isFailed(testState, { requestKey: RequestKeys.fetchImages }));
});
});
});

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import ErrorAlert from './ErrorAlert';
import { selectors } from '../../data/redux';
import { RequestKeys } from '../../data/constants/requests';
export const UploadErrorAlert = ({
message,
// redux
isUploadError,
// inject
}) => (
<ErrorAlert
isError={isUploadError}
>
<FormattedMessage
{...message}
/>
</ErrorAlert>
);
UploadErrorAlert.propTypes = {
message: PropTypes.shape({
id: PropTypes.string,
defaultMessage: PropTypes.string,
description: PropTypes.string,
}).isRequired,
// redux
isUploadError: PropTypes.bool.isRequired,
};
export const mapStateToProps = (state) => ({
isUploadError: selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadImage }),
});
export const mapDispatchToProps = {};
export default connect(mapStateToProps, mapDispatchToProps)(UploadErrorAlert);

View File

@@ -0,0 +1,29 @@
import React from 'react';
import { shallow } from 'enzyme';
import { UploadErrorAlert, mapStateToProps } from './UploadErrorAlert';
import { selectors } from '../../data/redux';
import { RequestKeys } from '../../data/constants/requests';
jest.mock('../../data/redux', () => ({
selectors: {
requests: {
isFailed: jest.fn((state, params) => ({ isFailed: { state, params } })),
},
},
}));
describe('UploadErrorAlert', () => {
describe('Snapshots', () => {
test('snapshot: is ErrorAlert with Message error (ErrorAlert)', () => {
expect(shallow(<UploadErrorAlert isUploadError />)).toMatchSnapshot();
});
});
describe('mapStateToProps', () => {
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
test('isUploadError from requests.isFinished', () => {
expect(
mapStateToProps(testState).isUploadError,
).toEqual(selectors.requests.isFailed(testState, { requestKey: RequestKeys.uploadImage }));
});
});
});

View File

@@ -0,0 +1,34 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ErrorAlert component Component Snapshots snapshot: Does not load heading when hideHeading is true 1`] = `
<Alert
dismissible={true}
onClose={[MockFunction dismissAlert]}
variant="danger"
>
<p>
An Error Message
</p>
</Alert>
`;
exports[`ErrorAlert component Component Snapshots snapshot: Loads children and component when error (ErrorAlert) 1`] = `
<Alert
dismissible={true}
onClose={[MockFunction dismissAlert]}
variant="danger"
>
<Alert.Heading>
<FormattedMessage
defaultMessage="Error"
description="Title of message presented to user when something goes wrong"
id="authoring.texteditor.selectimagemodal.error.errorTitle"
/>
</Alert.Heading>
<p>
An Error Message
</p>
</Alert>
`;
exports[`ErrorAlert component Component Snapshots snapshot: is Null when no error (ErrorAlert) 1`] = `""`;

View File

@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FetchErrorAlert Snapshots snapshot: is ErrorAlert with Message error (ErrorAlert) 1`] = `
<ErrorAlert
dismissError={null}
hideHeading={false}
isError={true}
>
<FormattedMessage />
</ErrorAlert>
`;

View File

@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UploadErrorAlert Snapshots snapshot: is ErrorAlert with Message error (ErrorAlert) 1`] = `
<ErrorAlert
dismissError={null}
hideHeading={false}
isError={true}
>
<FormattedMessage />
</ErrorAlert>
`;

View File

@@ -0,0 +1,9 @@
export const messages = {
errorTitle: {
id: 'authoring.texteditor.selectimagemodal.error.errorTitle',
defaultMessage: 'Error',
description: 'Title of message presented to user when something goes wrong',
},
};
export default messages;

View File

@@ -0,0 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FileInput component snapshot 1`] = `
<FileInput
acceptedFiles=".srt"
fileInput={
Object {
"addFile": [MockFunction props.fileInput.addFile],
"ref": [Function],
}
}
>
<input
accept=".srt"
className="upload d-none"
onChange={[MockFunction props.fileInput.addFile]}
type="file"
/>
</FileInput>
`;

View File

@@ -0,0 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
export const FileInput = ({ fileInput, acceptedFiles }) => (
<input
accept={acceptedFiles}
className="upload d-none"
onChange={fileInput.addFile}
ref={fileInput.ref}
type="file"
/>
);
FileInput.propTypes = {
acceptedFiles: PropTypes.string.isRequired,
fileInput: PropTypes.shape({
addFile: PropTypes.func,
ref: PropTypes.oneOfType([
// Either a function
PropTypes.func,
// Or the instance of a DOM native element (see the note about SSR)
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
]),
}).isRequired,
};
export default FileInput;

View File

@@ -0,0 +1,33 @@
import React from 'react';
import { mount } from 'enzyme';
import { FileInput } from '.';
describe('FileInput component', () => {
let el;
let container;
let props;
beforeEach(() => {
container = {};
props = {
acceptedFiles: '.srt',
fileInput: {
addFile: jest.fn().mockName('props.fileInput.addFile'),
ref: (input) => { container.ref = input; },
},
};
el = mount(<FileInput {...props} />);
});
test('snapshot', () => {
expect(el).toMatchSnapshot();
});
test('only accepts allowed file types', () => {
expect(el.find('input').props().accept).toEqual('.srt');
});
test('calls fileInput.addFile onChange', () => {
expect(el.find('input').props().onChange).toEqual(props.fileInput.addFile);
});
test('loads ref from fileInput.ref', () => {
expect(container.ref).toEqual(el.find('input').getDOMNode());
});
});