Commit 8e1241ab authored by Jesse Hallam's avatar Jesse Hallam Committed by Saturnino Abril
Browse files

MM-8528: prevent duplicate posts on retries (#1108)

* pass through console logs even when failing the test

* add unit test to verify ability to click retry before re-render occurs

* don't allow post retry if already successfully submitted

* add tests for component changing post_id
parent 7ce4503b
......@@ -5,20 +5,11 @@ import PropTypes from 'prop-types';
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {createPost} from 'actions/post_actions.jsx';
export default class FailedPostOptions extends React.PureComponent {
static propTypes = {
/*
* The failed post
*/
post: PropTypes.object.isRequired,
actions: PropTypes.shape({
/**
* The function to delete the post
*/
createPost: PropTypes.func.isRequired,
removePost: PropTypes.func.isRequired,
}).isRequired,
}
......@@ -32,27 +23,44 @@ export default class FailedPostOptions extends React.PureComponent {
this.submitting = false;
}
componentWillReceiveProps(nextProps) {
if (nextProps.post.id !== this.props.post.id) {
this.setState({
submitting: false,
submitted: false,
});
}
}
retryPost(e) {
e.preventDefault();
if (this.submitting) {
// Don't retry if already retrying or previously retried and succeeded (and waiting for
// re-render).
if (this.state.submitting || this.state.submitted) {
return;
}
this.submitting = true;
this.setState({
submitting: true,
});
const post = {...this.props.post};
Reflect.deleteProperty(post, 'id');
createPost(post,
this.props.actions.createPost(post,
() => {
this.submitting = false;
this.setState({
submitted: true,
});
},
(err) => {
if (err && err.id && err.id === 'api.post.create_post.root_id.app_error') {
this.showPostDeletedModal();
}
this.submitting = false;
this.setState({
submitting: false,
});
}
);
}
......
......@@ -5,6 +5,8 @@ import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {removePost} from 'mattermost-redux/actions/posts';
import {createPost} from 'actions/post_actions.jsx';
import FailedPostOptions from './failed_post_options.jsx';
function mapStateToProps(state, ownProps) {
......@@ -15,9 +17,12 @@ function mapStateToProps(state, ownProps) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
removePost,
}, dispatch),
actions: {
...bindActionCreators({
removePost,
}, dispatch),
createPost,
},
};
}
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/post_view/FailedPostOptions should match snapshot 1`] = `
<span
className="pending-post-actions"
>
<a
className="post-retry"
href="#"
onClick={[Function]}
>
<FormattedMessage
defaultMessage="Retry"
id="pending_post_actions.retry"
values={Object {}}
/>
</a>
-
<a
className="post-cancel"
href="#"
onClick={[Function]}
>
<FormattedMessage
defaultMessage="Cancel"
id="pending_post_actions.cancel"
values={Object {}}
/>
</a>
</span>
`;
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import FailedPostOptions from 'components/post_view/failed_post_options/failed_post_options.jsx';
describe('components/post_view/FailedPostOptions', () => {
const baseProps = {
post: {
id: 'post_id',
},
actions: {
createPost: jest.fn(),
removePost: jest.fn(),
},
};
test('should match snapshot', () => {
const wrapper = shallow(<FailedPostOptions {...baseProps}/>);
expect(wrapper).toMatchSnapshot();
});
test('should create post at most once', () => {
const props = {
...baseProps,
actions: {
...baseProps.actions,
createPost: jest.fn().
mockImplementationOnce((post, success, failure) => failure()).
mockImplementation((post, success) => success()),
},
};
const wrapper = shallow(<FailedPostOptions {...props}/>);
const e = {preventDefault: jest.fn()};
// First attempt should fail, allowing retry
wrapper.find('.post-retry').simulate('click', e);
expect(props.actions.createPost.mock.calls.length).toBe(1);
// Second attempt should succeed
wrapper.find('.post-retry').simulate('click', e);
expect(props.actions.createPost.mock.calls.length).toBe(2);
// Third attempt should be ignored, since already succeeded.
wrapper.find('.post-retry').simulate('click', e);
expect(props.actions.createPost.mock.calls.length).toBe(2);
// Next attempt should succeed when post id changes.
wrapper.setProps({
...props,
post: {
...props.post,
id: 'post_id_new',
},
});
wrapper.find('.post-retry').simulate('click', e);
expect(props.actions.createPost.mock.calls.length).toBe(3);
});
});
......@@ -21,14 +21,37 @@ Object.defineProperty(document, 'execCommand', {
value: (cmd) => supportedCommands.includes(cmd),
});
beforeEach(() => {
console.log = jest.fn((error) => {
throw new Error('Unexpected console log: ' + error);
let logs;
let warns;
let errors;
beforeAll(() => {
console.originalLog = console.log;
console.log = jest.fn((...params) => {
console.originalLog(...params);
logs.push(params);
});
console.warn = jest.fn((error) => {
throw new Error('Unexpected console warning: ' + error);
console.originalWarn = console.warn;
console.warn = jest.fn((...params) => {
console.originalWarn.call(...params);
warns.push(params);
});
console.error = jest.fn((error) => {
throw new Error('Unexpected console error: ' + error);
console.originalError = console.error;
console.error = jest.fn((...params) => {
console.originalError.call(...params);
errors.push(params);
});
});
beforeEach(() => {
logs = [];
warns = [];
errors = [];
});
afterEach(() => {
if (logs.length > 0 || warns.length > 0 || errors.length > 0) {
throw new Error('Unexpected console logs');
}
});
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment