-
Notifications
You must be signed in to change notification settings - Fork 154
Rewrite middlewares to sagas #117
Rewrite middlewares to sagas #117
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @mik639, thanks so much for this :D
I have a concern about how it'll merge with #103 - both PRs add a different dependency.saga.js
. @superhawk610's PR is based around having a queue system to group dependency installations, and so I think we can probably use that version of the file, with any changes that @mik639 (as our local Redux Saga expert) suggests?
A misc note: Could you delete import-project.middleware.js
if it's no longer being used?
For the most part this code looks great, but I had a few requested tweaks. Please let me know if you have any questions, or if there's anything I can do to facilitate you and @superhawk610 working together to get both your changes merged
@@ -0,0 +1,160 @@ | |||
import { call, put, cancel, select, takeEvery } from 'redux-saga/effects'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be possible to add Flow typing to this file?
I'm not as worried about dependency.saga.js
for two reasons:
- The original
dependency.middleware.js
didn't have Flow typing, which was my bad - @superhawk610's Queue/group actions that lock package.json #103 PR adds Flow typing for this file.
If you're not comfortable/familiar with Flow, or if you just don't have the time to spare right now, feel free to just add a // TODO: Add Flow
to the top of the file, and I or someone else will try to tackle it shortly after merging.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, added
src/sagas/import-project.saga.js
Outdated
// In the future, maybe I can attach a suffix like `-copy`, but for | ||
// now I'll just reject it outright. | ||
const isAlredyExist = yield select(getInternalProjectById, projectId); | ||
if (isAlredyExist) throw new Error('project-name-already-exists'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: for consistency with the rest of the codebase, can we avoid 1-line conditionals?
if (isAlreadyExist) {
throw new Error('project-name-already-exists');
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, maybe we can use eslint for preventing mistakes like this ;)
src/sagas/import-project.saga.js
Outdated
// support... but for now, I'm prioritizing an A+ experience for | ||
// supported project types. | ||
const type = yield call(inferProjectType, json); | ||
if (!type) throw new Error('unsupported-project-type'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(same comment here about 1-line conditionals)
// Only a single path should be selected | ||
const [path] = paths; | ||
yield put(importExistingProjectStart(path)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good extracted function 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks
const saga = handlePathInput(paths); | ||
expect(saga.next().value).toEqual( | ||
put(importExistingProjectStart('path/to/first')) | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not super familiar with saga testing - wouldn't we want to test that it's equal to a plain object?
const expectedValue = importExistingProjectStart('path/to/first');
// { type: IMPORT_EXISTING_PROJECT_START, path: 'path/to/first' }
expect(saga.next().value).toEqual(expectedValue);
In other words, shouldn't we check importExistingProjectStart()
instead of put(importExistingProjectStart())
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we need to check that saga will dispatch action(effect put), in other words, we expect
expect(saga.next().value).toEqual(put({ type: IMPORT_EXISTING_PROJECT_START, path: 'path/to/first' }));
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@joshwcomeau that's one of the really cool features of redux-saga
that makes testing so consistent - put(/*...*/)
doesn't actually dispatch an action, it describes dispatching the action, so all you have to do when testing is check that the return from the saga matches the expected description.
It's a unique pattern but it has a handful of unique benefits - you can read more here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ahhh, right. Awesome!
}); | ||
}); | ||
|
||
test('showImportDialog saga', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we use it
instead of test
everywhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we can, I didn't use it here, only because it looks not so semantic :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it('runs showImportDialog saga correctly'
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer to move it in its own describe section because it'll help to keep proper hierarchy in tests result
}); | ||
|
||
it('should import project', () => { | ||
global.Date = { now: () => 1532809641976 }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah, right, the createdAt
is done impurely within the saga, same as it was within the middleware.
This solution worries me, though (it won't be "unmocked" when this test file finishes, right? So this could affect subsequent tests). We could have an afterAll
that restores it, but I wonder if there's a better way...
Maybe importProject
could take a second optional argument, getCurrentTime
, which could default to Date.now
. In the test you could provide a mock function like () => 1532809641976
.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. I can replace it with:
const spyOnDate = jest.spyOn(Date, 'now');
spyOnDate.mockReturnValue(1532809641976);
// test
spyOnDate.mockRestore()
in this case mocked value will affect only one testcase
}); | ||
|
||
describe('inferProjectType helper', () => { | ||
it('should return null if field "dependencies" not exist in json', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for checking this 👍
expect(inferProjectType(json)).toBe('create-react-app'); | ||
}); | ||
|
||
it('should return "create-react-app" if json have dependency eslint-config-react-app', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for checking this as well! Great coverage 👍
yield takeEvery(ADD_DEPENDENCY_START, addDependency); | ||
yield takeEvery(UPDATE_DEPENDENCY_START, updateDependency); | ||
yield takeEvery(DELETE_DEPENDENCY_START, deleteDependency); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok I'll take a look
In the situation when we have a few pull requests with rewritten middlewares, I suggest to create a separate branch for merging and testing them, instead of merging to master. What do you think, @joshwcomeau ? |
@mik639 That's a good idea, you should create a My main goal tonight is to get some ground work done on #77, but then I'll be turning my full attention to this no later than tomorrow. Are you developing on Mac or Windows? I'm working from Windows so I have a few Windows-specific lines throughout, I figure that's fine since we'll be merging in Windows support to master sooner or later. First order of business once I get some progress made on #77 is to review this. |
@mik639 ++, like the idea of a working branch to merge and test everything 👍 |
@superhawk610 @joshwcomeau I'm created a branch |
@mik639 I think you should merge yours (#117) first, since it contains the bulk of the work, and then I will merge #103 and #111 in that order to make sure that they conform to your structure. I haven't written tests for |
Yay, so happy that y'all are working towards getting this stuff landed. I think we'll pen a release once this is in, since having the queue and a totally-reworked side effects layer seems like a good thing to capture in a launch :D In addition to all the great stuff folks have been merging in over the last week or two. EDIT: Er, right, we'll also need the UI for the queue, which isn't covered in #103. I got a little ahead of myself 😅. Certainly when the UI is in, though! |
@mik639 I'm working on reviewing this, but now I'm running into the issue @joshwcomeau described here. I'm going to try switching it over to Alright, I've got a Windows version of this PR up at superhawk610/guppy if anybody else wants to follow along. Saga functionality is 💯. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Requesting changes for a minor spelling change. I'd also like to agree on a rule for consistency - yield call([object, 'method'])
vs. yield call(object.method)
.
); | ||
yield put(addDependencyFinish(projectId, dependency)); | ||
} catch (err) { | ||
yield call([console, 'error'], 'Failed to install dependency', err); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm unfamiliar with redux-saga
convention when using call
with an object property. Why use
yield call([console, 'error'], /*...*/);
instead of
yield call(console.error, /*...*/);
? When implementing similar methods, I'm not sure which syntax to use (eg - ipcRenderer.send
and childProcess.spawn
).
src/sagas/import-project.saga.js
Outdated
switch (err.message) { | ||
case 'project-name-already-exists': { | ||
yield call( | ||
dialog.showErrorBox, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's an example of what I mentioned in my previous comment - why use dialog.showErrorBox
over [dialog, 'showErrorBox']
?
src/sagas/import-project.saga.js
Outdated
// In the future, maybe I can attach a suffix like `-copy`, but for | ||
// now I'll just reject it outright. | ||
const isAlredyExist = yield select(getInternalProjectById, projectId); | ||
if (isAlredyExist) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be either isAlreadyExist
or alreadyExists
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I vote for "alreadyExists", flows better IMO :)
const saga = handlePathInput(paths); | ||
expect(saga.next().value).toEqual( | ||
put(importExistingProjectStart('path/to/first')) | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@joshwcomeau that's one of the really cool features of redux-saga
that makes testing so consistent - put(/*...*/)
doesn't actually dispatch an action, it describes dispatching the action, so all you have to do when testing is check that the return from the saga matches the expected description.
It's a unique pattern but it has a handful of unique benefits - you can read more here.
@superhawk610 unfortunately flow-typed redux-saga currently have the issue with describing |
So these three are functionally equivalent - here's a quick test: const { call } = require('redux-saga/effects')
console.log(
JSON.stringify(call([console, 'error'], 'foo')) + '\n' +
JSON.stringify(call([console, console.error], 'foo')) + '\n' +
JSON.stringify(call(console.error, 'foo'))
)
// {"@@redux-saga/IO":true,"CALL":{"context":{},"args":["foo"]}}
// {"@@redux-saga/IO":true,"CALL":{"context":{},"args":["foo"]}}
// {"@@redux-saga/IO":true,"CALL":{"context":null,"args":["foo"]}} @mik639 None of these patterns throw an error with Flow for me yield call([console, 'error'], 'Failed to install dependency', err);
yield call([console, console.error], 'Failed to install dependency', err);
yield call(console.error, 'Failed to install dependency', err);
|
@superhawk610 first two - yes, there are same, but third as you can see has no context. Redux-saga itself has no flow types, it should be typed with libdef. |
Ah, that makes more sense. I didn't have those flow-type definitions pulled on my work machine, just at home, so I didn't catch it. Do you want to switch all of the |
@superhawk610 @joshwcomeau Sorry for delay, I have fixed comments, and have updated pr |
@mik639 Very nice fix with destructuring |
Thanks for merging @superhawk610! Took a quick glance, looks great to me! |
* Replace middlewares with sagas * Remove import-project middleeare from prod store configuration * Add flow-typed redux-saga, review fixes * review fixes
* Replace middlewares with sagas * Remove import-project middleeare from prod store configuration * Add flow-typed redux-saga, review fixes * review fixes
* Rewrite middlewares to sagas (#117) * Tasks middleware to saga (#122) * pass test suite - fix getInternalProjectById in projects.reducer test - move projects.reducer test up a directory - add missing assertions in create-project.service test - remove middlewares directory * review tweaks * bring in line with existing sagas - add note about refactoring stderr handling * update test to use new name (isDevServerFail) * Make some small tweaks before merging * Fix ESLint errors
Related Issue:
As was discussed in #97 I have rewrite custom middlewares with sagas, last one
task.middleware
should be rewritten by @superhawk610 (as he asked)Summary:
Screenshots/GIFs: