Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redux logic errors #1954

Merged
merged 28 commits into from
Jan 31, 2020
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4168920
Start migration updateProjectSource saga
joshling1919 Nov 13, 2019
c26af06
Update updateProjectSource
joshling1919 Nov 15, 2019
fe8d719
Use redux-logic for errors
joshling1919 Nov 16, 2019
9bf3a0d
Remove errors sagas unit tests
joshling1919 Nov 16, 2019
1227492
Reorganize errors logic
joshling1919 Dec 2, 2019
e40ff0f
Start validateProjectOnChange tests
joshling1919 Dec 3, 2019
982f9d4
Finish validateProjectOnChange specs
joshling1919 Dec 5, 2019
e429d3b
Write test for validateSource helper
joshling1919 Dec 6, 2019
7a6faef
Start validateCurrentProject test
joshling1919 Dec 7, 2019
4e4e64c
Finish validateProjectOnChange tests
joshling1919 Dec 10, 2019
adfd45b
Merge branch 'master' of https://github.com/popcodeorg/popcode into r…
joshling1919 Jan 9, 2020
9d7a55c
Refactor validateSource logic tests
joshling1919 Jan 13, 2020
1080acb
Lint validateCurrentProject and validateProjectOnChange
joshling1919 Jan 13, 2020
33eb932
Improve validateCurrentProject and validateProjectOnChange tests
joshling1919 Jan 13, 2020
80cf62f
Ensure that only current validations are dispatched
joshling1919 Jan 13, 2020
9a6a751
Merge branch 'master' of https://github.com/popcodeorg/popcode into r…
joshling1919 Jan 14, 2020
12b20a0
Merge branch 'master' of https://github.com/popcodeorg/popcode into r…
joshling1919 Jan 15, 2020
99c37ca
Lint errors logic
joshling1919 Jan 15, 2020
c2be8db
Merge branch 'master' of https://github.com/popcodeorg/popcode into r…
joshling1919 Jan 23, 2020
5520295
Start validateProject logic
joshling1919 Jan 23, 2020
23c2613
Refactor errors logic to use validateProject
joshling1919 Jan 25, 2020
c81b993
Merge branch 'master' of https://github.com/popcodeorg/popcode into r…
joshling1919 Jan 25, 2020
c12991d
Run lint
joshling1919 Jan 25, 2020
5a567a8
Merge branch 'master' into redux-logic-errors
outoftime Jan 25, 2020
3b666dd
Refactor validateProject
joshling1919 Jan 27, 2020
fbe3d0c
Merge branch 'redux-logic-errors' of https://github.com/joshling1919/…
joshling1919 Jan 27, 2020
8826d1e
Merge branch 'master' of https://github.com/popcodeorg/popcode into r…
joshling1919 Jan 27, 2020
43505c8
Merge branch 'master' into redux-logic-errors
outoftime Jan 31, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/logic/__tests__/helpers/validateSource.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import validateSource from '../../helpers/validateSource';
import {validatedSource} from '../../../actions/errors';

const mockValidationErrors = {
css: 'invalid CSS selector',
};

const mockCssValidate = jest.fn(x => mockValidationErrors);

jest.mock('../../../util/retryingFailedImports', () =>
jest.fn(x => ({
css: mockCssValidate,
})),
);

const mockActionCreator = (language, errors) => ({
type: 'VALIDATED_SOURCE',
payload: {
errors: {
[language]: errors[language],
language,
},
},
});

jest.mock('../../../actions/errors', () => ({
validatedSource: jest.fn((language, errors) =>
mockActionCreator(language, errors),
),
}));

test('calls validateSource with validationErrors', async () => {
const language = 'css';
const source = 'dib { color: green;}';
const projectAttributes = {};
const dispatch = jest.fn();
await validateSource({language, source, projectAttributes}, dispatch);
expect(mockCssValidate).toHaveBeenCalledWith(source, projectAttributes);
expect(validatedSource).toHaveBeenCalledWith(language, mockValidationErrors);
const payload = validatedSource(language, mockValidationErrors);
expect(dispatch).toHaveBeenCalledWith(payload);
});
56 changes: 56 additions & 0 deletions src/logic/__tests__/validateCurrentProject.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import validateCurrentProject from '../validateCurrentProject';

import Analyzer from '../../analyzers';
import validateSource from '../helpers/validateSource';
import {getCurrentProject} from '../../selectors';

const mockHtmlSource = '<html></html>';
const mockCssSource = 'div {}';

jest.mock('../../analyzers');
jest.mock('../helpers/validateSource');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this I think may be going a little further than we want with mocking—in particular the fact that validateCurrentProject delegates some of its logic to the validateSource helper is essentially an implementation detail of validateCurrentProject; if in the future we decided to pull all of that logic directly into validateCurrentProject, I don’t think we would consider that breaking from the standpoint of the unit tests.

So my proposal would be to just test the behavior of the validateCurrentProject logic here, including any behavior that happens to be implemented in validateSource; and remove the unit test for the validateSource helper.

jest.mock('../../selectors', () => ({
getCurrentProject: jest.fn(() => ({
sources: {
html: mockHtmlSource,
css: mockCssSource,
},
})),
}));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest this also intuitively is probably not something I would have mocked if I were writing this test, although I think the argument is much less clear-cut than with the helper module commented on above. I could truly go either way on this, but wanted to flag it as something to think about, particularly in terms of “which decision is going to make this test more robust?”, with “robust” meaning “sensitive to breaking changes in the code under test, and insensitive to non-breaking changes in the code under test”.


const dummyState = {};

test('should validate current project', async () => {
const dispatch = jest.fn();
const done = jest.fn();
await validateCurrentProject.process(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, this is a bit of a pain in the ass, but awaiting the call to process is actually testing implementation details of that method; from the standpoint of redux-logic, process does not have to be an async (or Promise-returning) function; it just needs to call done() when it’s done. I think the best way to test the relevant interface is something like:

await new Promise((resolve) => {
  validateCurrentProject.process(
    {getState: constant(dummyState)},
    dispatch,
    resolve,
  )
});

FWIW I was looking through other logic tests for examples and am realizing I totally failed to catch a bunch of other cases of what you did here, so I’ll need to go back and fix those—sorry!!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey so! I dug into fixing this issue for the existing tests, and ended up just writing a test helper that actually runs logics through the redux-logic middleware, so the tests can focus on what the logics do rather than trying to mimic the implementation of redux-logic. Check out #2026 for all the sordid details!

{
getState: () => dummyState,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think eslint-plugin-lodash (introduced after you created this branch) is going to tell you to use constant here.

},
dispatch,
done,
);
expect(getCurrentProject).toHaveBeenCalledWith(dummyState);
const analyzerPayload = getCurrentProject(dummyState);
expect(Analyzer).toHaveBeenCalledWith(analyzerPayload);
const analyzer = new Analyzer(analyzerPayload);
expect(validateSource.mock.calls).toEqual([
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would probably use expect.arrayContaining here since we’re not sensitive to the order of the calls.

[
{
language: 'html',
source: mockHtmlSource,
projectAttributes: analyzer,
},
dispatch,
],
[
{
language: 'css',
source: mockCssSource,
projectAttributes: analyzer,
},
dispatch,
],
]);
expect(done).toHaveBeenCalled();
});
40 changes: 40 additions & 0 deletions src/logic/__tests__/validateProjectOnChange.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import validateProjectOnChange from '../validateProjectOnChange';

import Analyzer from '../../analyzers';
import validateSource from '../helpers/validateSource';
import {getCurrentProject} from '../../selectors';

jest.mock('../../analyzers');
jest.mock('../helpers/validateSource');
jest.mock('../../selectors', () => ({
getCurrentProject: jest.fn(x => x),
}));

const dummyState = {};

test('should validate project on change', async () => {
const language = 'html';
const newValue = 'dummy new value';
const dispatch = jest.fn();
const done = jest.fn();
await validateProjectOnChange.process(
{
getState: () => dummyState,
action: {payload: {language, newValue}},
},
dispatch,
done,
);
expect(getCurrentProject).toHaveBeenCalledWith(dummyState);
const analyzerPayload = getCurrentProject(dummyState);
expect(Analyzer).toHaveBeenCalledWith(analyzerPayload);
expect(validateSource).toHaveBeenCalledWith(
{
language,
source: newValue,
projectAttributes: new Analyzer(getCurrentProject(dummyState)),
},
dispatch,
);
expect(done).toHaveBeenCalled();
});
18 changes: 18 additions & 0 deletions src/logic/helpers/validateSource.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import retryingFailedImports from '../../util/retryingFailedImports';
import {validatedSource} from '../../actions/errors';

function importValidations() {
return retryingFailedImports(() =>
import(
/* webpackChunkName: "mainAsync" */
'../../validations'
),
);
}

export default async ({language, source, projectAttributes}, dispatch) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extreme nit / not a blocker, but all else being equal I’d make this a regular named function rather than an arrow function (since it’s at the top level of the module / not being inlined in a larger expression).

const validations = await importValidations();
const validate = validations[language];
const validationErrors = await validate(source, projectAttributes);
await dispatch(validatedSource(language, validationErrors));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How come we’re awaiting the dispatch here? dispatch should be a void function; I wouldn’t expect it to return a promise…

};
4 changes: 4 additions & 0 deletions src/logic/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import linkGithubIdentity from './linkGithubIdentity';
import logout from './logout';
import startAccountMigration from './startAccountMigration';
import unlinkGithubIdentity from './unlinkGithubIdentity';
import validateCurrentProject from './validateCurrentProject';
import validateProjectOnChange from './validateProjectOnChange';

export default [
linkGithubIdentity,
logout,
startAccountMigration,
unlinkGithubIdentity,
validateCurrentProject,
validateProjectOnChange,
];
32 changes: 32 additions & 0 deletions src/logic/validateCurrentProject.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {createLogic} from 'redux-logic';
import Analyzer from '../analyzers';
import {getCurrentProject} from '../selectors';
import validateSource from './helpers/validateSource';

export default createLogic({
type: [
'CHANGE_CURRENT_PROJECT',
'GIST_IMPORTED',
'SNAPSHOT_IMPORTED',
'PROJECT_RESTORED_FROM_LAST_SESSION',
'TOGGLE_LIBRARY',
],
async process({getState}, dispatch, done) {
const state = getState();
const currentProject = getCurrentProject(state);
const analyzer = new Analyzer(currentProject);

const validatePromises = [];
for (const language of Reflect.ownKeys(currentProject.sources)) {
const source = currentProject.sources[language];
validatePromises.push(
validateSource(
{language, source, projectAttributes: analyzer},
dispatch,
),
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: a map would be a more idiomatic way to construct this array; I would probably use lodash’s map directly over currentProject.sources since it will iterate over object entries.

await Promise.all(validatePromises);
done();
},
});
28 changes: 28 additions & 0 deletions src/logic/validateProjectOnChange.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {createLogic} from 'redux-logic';
import Analyzer from '../analyzers';
import {getCurrentProject} from '../selectors';
import validateSource from './helpers/validateSource';

export default createLogic({
type: 'UPDATE_PROJECT_SOURCE',
latest: true,
async process(
{
getState,
action: {
payload: {language, newValue},
},
},
dispatch,
done,
) {
const state = getState();
const analyzer = new Analyzer(getCurrentProject(state));

await validateSource(
{language, source: newValue, projectAttributes: analyzer},
dispatch,
);
done();
},
});
80 changes: 0 additions & 80 deletions src/sagas/errors.js

This file was deleted.

2 changes: 0 additions & 2 deletions src/sagas/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {all} from 'redux-saga/effects';

import manageUserState from './manageUserState';
import watchErrors from './errors';
import watchProjects from './projects';
import watchUi from './ui';
import watchClients from './clients';
Expand All @@ -11,7 +10,6 @@ import watchAssignments from './assignments';
export default function* rootSaga() {
yield all([
manageUserState(),
watchErrors(),
watchProjects(),
watchUi(),
watchClients(),
Expand Down
Loading