-
Notifications
You must be signed in to change notification settings - Fork 991
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
Fixes #22683 - Refactor PasswordStrength folder structure #5240
Fixes #22683 - Refactor PasswordStrength folder structure #5240
Conversation
Issues: #22473 |
const mapDispatchToProps = dispatch => bindActionCreators(actions, dispatch); | ||
|
||
// export reducers | ||
export const reducers = { passwordStrength: reducer }; |
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.
The passwordStrength
key must match the key in mapStateToProps
, otherwise, it will break!
This is the reason i decided to move the key deceleration from reducers/index.js
into this object.
I think it would be more reliable if we keep all the configuration of the PasswordStrength
inside the folder, so the combineReducers
method should consume the key and the reducer.
statistics, | ||
hosts, | ||
notifications, | ||
toasts, | ||
passwordStrength, | ||
...passwordStrengthReducers, |
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.
it a bit confused situation when you have both ways implemented here.
IMHO if we adopt this approach (which i can see some benefits from) we should change this entire file
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.
Will do it in a different pr 👍
@sharvit i like the use of redux-selectors... can you explain a bit more about the domain driven folder structure? I guess typically actions/reducers are used by a single connected component, but what if they are used by many different connected components (or potentially unrelated connected components)? I would just appreciate some background reading material on this...The folder structure proposed seems like it would help in most cases though... |
Thanks @priley86, this is actually a very good question. Let's say I have another component called import { updatePassword } from '../PasswordStrength/PasswordStrengthActions';
import { doesPasswordsMatch } from '../PasswordStrength/PasswordStrengthSelectors';
import { reducers as PasswordStrengthReducers } from '../PasswordStrength';
import PasswordGenerator from './PasswordGenerator';
const PasswordStrengthReducerKey = Object.keys(PasswordStrengthReducers)[0];
// map state to props
const mapStateToProps = ({ [PasswordStrengthReducerKey]: passwordStrength }) => ({
password: passwordStrength.password,
passwordConfirmation: passwordStrength.passwordConfirmation,
doesPasswordsMatch: doesPasswordsMatch(passwordStrength),
});
// map action dispatchers to props
const mapDispatchToProps = dispatch => bindActionCreators({ updatePassword }, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(PasswordGenerator); This kind of makes me wonder, should we strict the Maybe it can be? export const reducer = { key: 'passwordStrength', reducer: PasswordStrengthReducer }; Or? export const reducerKey = 'passwordStrength';
export const reducer = PasswordStrengthReducer; @priley86 you can see the discussion lead us to this decision here: TL;DR |
This post gave us some motivation: More cool links about the topic: |
@sharvit thanks a lot for the links... i like the new structure much better (and it makes sense to me). Will start trying to migrate this way soon myself.
but is there ever a case for:
I still don't know if that use case exists? Obviously we are trying to avoid it with the new "domain" path though... |
@priley86, I honestly not sure about it. From one side, having a common reducer feels like a great tool in my toolbox. I think we should continue in this direction and wait to see if this use-case appear. |
@sharvit yea.. it makes sense... i also see the case for continuing to nest "shared" or "common" appropriately (like you have currently in Foreman)... so basically, I would think you have the structure you've proposed above for each component, and nesting appropriately within It kind of sticks for me, but not sure what others think... |
Overall I like this approach. I can't wrap my head around why they call it a "selector" but I do understand why it's factored out. Do we want to just put these in the reducers file though? Will they be used outside of the context of a reducer? |
@waldenraines - Usually, they used to do stuff like |
@sharvit reviewing this approach once more (and would like to call it out)... one of the things I like most is you've introduced I read these articles on the integration tests topic and I agree w/ them (integration can be more high value than unit or e2e for us): Was having this sort of a debate w/ a friend recently ;) |
@sharvit i had no issues with this structure. Very nice work. I've started a parallel PR for our v2v efforts here. I need to do a bit more homework with the tests you've started here, but was at least able to render the I'd also like to propose we adopt something similar to Ryan Florence's route hierarchy/shared module structure noted above (link). Feel free to revise as you'd like 😸. I'm happy with it though... |
@priley86 we kind of had the same debate here. So we end up asking ourselves, How should we write those unit-tests? and what about integration tests? I believe our products will be more reliable if we will split them into as many stand-alone packages as we can. e2e testing can be leaner now since we will have integration testing for all inner packages. We are currently doing kind of an e2e testing in our rails stack and I would love moving them into the js stack. We are still not a single-page-app, we are not using It's nice to see you could adapt it so fast! 👍 while I'm still waiting for more feedback... Let's keep updating each other, I think it works well. |
@sharvit 👍 Great change, I like it |
@amirfefer can you review please? thanks |
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.
Overall I like this change.
Consider combine here the Integration helper PR I mentioned in the inline comments
@@ -0,0 +1,2 @@ | |||
export const doesPasswordsMatch = ({ password, passwordConfirmation }) => |
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 like the selector pattern
here you can find a nice explanation for that.
should we adopt this pattern in the entire application?
There's a nice library that uses this pattern: redux-reselect
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, I believe we should adopt this pattern but I wouldn't run to migrating, mostly for new features.
Doesn't the redux-reselect syntax feel a bit confusing?
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.
was actually going to suggest some kind of memoized selector as well (after seeing that redux-reselect) previously... not much code there: https://github.com/reactjs/reselect/blob/master/src/index.js
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.
@priley86 @amirfefer Now when I understand that reselect
act as a memoized selector and I read more about it, it makes a lot of sense to use it (because it's memorized).
But, don't you think it takes a very simple concept like selectors and makes it really complicated to write and understand?
especially for developers how new to the concept of redux.
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 is just minor performance gain in many cases. I think we abstract that method into a common helper (either reselect or just write our own). Then it becomes less about beginner knowledge and just a function. Personal preference though...
|
||
describe('PasswordStrength actions', () => { | ||
it('should update password', () => | ||
expect(updatePassword('some-password')).toMatchSnapshot()); |
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 like this simplicity, though with async actions, should we adopt redux suggestion ?
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 in general, I will try to experience with it more in later PR and we will continue to discuss it there.
@@ -0,0 +1,75 @@ | |||
import React from 'react'; |
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 add integeration.test
prefix
|
||
import PasswordStrength, { reducers } from '../index'; | ||
|
||
const fixtures = { |
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.
extract this to another file? a specific file for the entire fixtures?
document.getElementById = jest.fn(id => ({ value: fixtures[id].password })); | ||
|
||
describe('PasswordStrength integration test', () => { | ||
const generateStore = () => createStore(combineReducers({ ...reducers })); |
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.
instead of generate store each time, would it be better to add integration setup (helper) ?
something like this: https://github.com/theforeman/foreman/pull/5205/files#diff-6d91af9e34087f473783a4de94e3c806
maybe it better to combined those two PRs?
I took inspiration from this blog
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, i think it makes sense thanks for pointing out 👍
Will update soon
|
||
const setInputValue = (input, value) => { | ||
input.instance().value = value; // eslint-disable-line no-param-reassign | ||
input.simulate('change', { target: { value } }); |
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 think you can do it in one line:
input.simulate('change', {target: {value: 'My new value'}});
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.
Not in this case, i was disappointed to see that the plugin takes the value from the input instead of from the event.
statistics, | ||
hosts, | ||
notifications, | ||
toasts, | ||
passwordStrength, | ||
...passwordStrengthReducers, |
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 a bit confused situation when you have both ways implemented here.
IMHO if we adopt this approach (which i can see some benefits from) we should change this entire file
const component = shallow(<PasswordStrength {...props} />); | ||
|
||
expect(toJson(component)).toMatchSnapshot(); | ||
expect(document.getElementById.mock.calls).toMatchSnapshot('getElementById calls'); |
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.
doesn't it better to use toBeCalled
function in this case? (instead of snapshot)
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.
Then I will only know that the function was called.
The snapshot contains the fact that the functions called twice and the arguments from each call:
https://github.com/theforeman/foreman/pull/5240/files#diff-04cd101c699dfc26787bd687fc50fba2R194
|
||
describe('triggering', () => { | ||
const setInputValue = (input, value) => { | ||
input.instance().value = value; // eslint-disable-line no-param-reassign |
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 here, you can make it one line.
what is the status of this pr? thanks |
007987f
to
d7cc82b
Compare
@ohadlevy Amazing timing just pushed :)
@amirfefer any thoughts? |
7e8300f
to
96ab056
Compare
I have just rebased it, @amirfefer @ohadlevy can you have another look? |
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 @sharvit
LGTM 👍
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 tested this and it works fine. I found one typo and a pf variable nitpick in the code.
I also have one question about userInputs
that I'd like to be answered prior to merging this.
<CommonForm | ||
label={__('Verify')} | ||
touched={true} | ||
error={doesPasswordsMatch ? verify.error : __('Password do not match')} |
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.
Typo: "Passwords do not match"
@@ -1,4 +1,4 @@ | |||
@import '../../../common/colors.scss'; | |||
@import '../../common/colors.scss'; |
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.
Nitpick: I know that you're just moving the file, but when we're touching it already, could you replace the font families with $font-family-base
pf variable, please?
const userInputs = | ||
userInputIds && userInputIds.length > 0 | ||
? userInputIds.map(input => document.getElementById(input).value) | ||
: []; |
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.
Just out of curiosity: What's shown in the strength meter when the password contains the username? I tried to simulate that but without any luck.
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 should show 'WEAK'. I just tested it and it works.
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 see, you're right. I confirm it works.
const passwordInput = component.find(`input#${props.data.id}`); | ||
setInputValue(passwordInput, 'some-value'); | ||
|
||
expect(props.updatePassword.mock.calls).toMatchSnapshot(); |
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 this be checked with toHaveBeenCalledWith
instead? It would make the test more readable.
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 personally think the snapshot is more readable because you move the expected data to a different file.
- If I would use
toHaveBeenCalledWith
i would miss other calls. snapshot will snap the whole calls made to the 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.
Having the expected data in a different file is exactly the reason why I think it's not readable :) You have to look at one more place to check if the test is correct. I consider the snapshots to be prone to such mistakes in general. In testing visual part of component's it's acceptable, but I'd like to avoid it elsewhere.
I've experienced it couple of times that the PR author just blindly updated tests without actually checking what's in the snapshot.
Anyway, it's not that big issue so I'm not going to block this PR and I'll open a discourse thread on that topic.
96ab056
to
a4f8806
Compare
Thanks for the review @tstrachota, I just updated. |
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.
Snapshots need update after a string change.
Apart from that it's ready to go.
* migrate PasswordStrength to a domain level folder-structure ref #22473
a4f8806
to
8a6aa93
Compare
Thanks @tstrachota! updated 👍 |
Waiting for code owner review from @theforeman/packaging. @ekohl would you mind taking a look? |
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.
Apologies for missing this. It's a false positive for packaging since this does touch package.json but not the actual packaging.
Merged. Thank you @sharvit and everybody involved for such long running efforts! |
Thanks for merging, please don't forget to set the redmine releaas afterwards. I set it to 1.19 now. |
I have been starting from the
PasswordStrength
because it was the easiest (the files are most organized).http://projects.theforeman.org/issues/22473
The current goal is to make this component perfect so we can use it later as a boilerplate.
Before continuing to the other components, I want to make sure everyone is agree about the new structure.
Goals:
In order to do so, i refactor the component, the tests, the actions and the reducers.
I migrated the actions from
updatePassword
andcheckPasswordsMatch
toupdatePassword
,updatePasswordConfirmation
and created a selector calldoesPasswordsMatch
.I have been trying many different variations and i feel i most like this structure:
Why i moved the tests into the
__tests__
folder?The file list just getting to long and it can get confusing.
Why do we need
index.js
? Why not connecting the component into the store from thePasswordStrength.js
?index.js
responsibility is to integrate the different parts of the domain.How do we scale it to have more inner components?
Creating a
components
folder and put them inside.Looking forward to get some feedback here.
@amirfefer @ohadlevy @waldenraines @tstrachota @danseethaler @priley86