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

Fixes #22683 - Refactor PasswordStrength folder structure #5240

Merged

Conversation

sharvit
Copy link
Contributor

@sharvit sharvit commented Feb 7, 2018

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:

  • Migrate to a domain level folder structure
  • Have integration test for the connected component
  • Have unit tests for all the other parts
  • Make it scaleable
  • Introduce redux selectors

In order to do so, i refactor the component, the tests, the actions and the reducers.

I migrated the actions from updatePassword and checkPasswordsMatch to updatePassword, updatePasswordConfirmation and created a selector call doesPasswordsMatch.

I have been trying many different variations and i feel i most like this structure:

PasswordStrength
├── __tests__
   ├── __snapshots__
      ├── integration.test.js.snap
      ├── PasswordStrengthActions.test.js.snap
      ├── PasswordStrengthReducer.test.js.snap
      └── PasswordStrength.test.js.snap
   ├── integration.test.js // integration test
   ├── PasswordStrengthActions.test.js // actions unit tests
   ├── PasswordStrengthReducer.test.js // reducer unit tests
   ├── PasswordStrengthSelectors.test.js // selectors unit tests
   └── PasswordStrength.test.js // component unit tests
├── index.js // integration file
├── PasswordStrength.js // component
├── PasswordStrength.scss // styles
├── PasswordStrengthActions.js // actions
├── PasswordStrengthConstants.js // consts
├── PasswordStrengthReducer.js // reducers
└── PasswordStrengthSelectors.js // selectors

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 the PasswordStrength.js?

  1. When we lose the reference to the unconnected component, it became hard to unit-test them because you must use a store.
  2. It feels right when 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

@theforeman-bot
Copy link
Member

Issues: #22473

const mapDispatchToProps = dispatch => bindActionCreators(actions, dispatch);

// export reducers
export const reducers = { passwordStrength: reducer };
Copy link
Contributor Author

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,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

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

Copy link
Contributor Author

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 👍

@priley86
Copy link

priley86 commented Feb 7, 2018

@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...

@sharvit
Copy link
Contributor Author

sharvit commented Feb 8, 2018

Thanks @priley86, this is actually a very good question.

Let's say I have another component called PasswordGenerator written in the same way and I want to use some staff from the PasswordStrength.

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 export reducers to only one reducer?

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:
https://community.theforeman.org/t/suggestion-refactor-the-react-app-folder-structure/7843/6

TL;DR
It makes development confusing when you work on a component and the component files are split into many different directories.

@sharvit
Copy link
Contributor Author

sharvit commented Feb 8, 2018

@priley86
Copy link

priley86 commented Feb 8, 2018

@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.

react-app/components/PasswordStrength
├── __tests__
│   ├── __snapshots__
│   │   ├── index.test.js.snap
│   │   ├── PasswordStrengthActions.test.js.snap
│   │   ├── PasswordStrengthReducer.test.js.snap
│   │   └── PasswordStrength.test.js.snap
│   ├── index.test.js // integration test
│   ├── PasswordStrengthActions.test.js // actions unit tests
│   ├── PasswordStrengthReducer.test.js // reducer unit tests
│   ├── PasswordStrengthSelectors.test.js // selectors unit tests
│   └── PasswordStrength.test.js // component unit tests
├── index.js // integration file
├── PasswordStrength.js // component
├── PasswordStrength.scss // styles
├── PasswordStrengthActions.js // actions
├── PasswordStrengthConstants.js // consts
├── PasswordStrengthReducer.js // reducers
└── PasswordStrengthSelectors.js // selectors

but is there ever a case for:

react_app/redux/reducers/index.js (root reducer for the domain reducers mentioned above)
react_app/redux/reducers/common/someCommonReducer/someCommonReducerEverywhere.js

I still don't know if that use case exists? Obviously we are trying to avoid it with the new "domain" path though...

@sharvit
Copy link
Contributor Author

sharvit commented Feb 8, 2018

@priley86, I honestly not sure about it.

From one side, having a common reducer feels like a great tool in my toolbox.
From the other side, we never had the need to use it and it may feel like an anti-pattern so I think we can avoid that (even for the long-term).

I think we should continue in this direction and wait to see if this use-case appear.

@priley86
Copy link

priley86 commented Feb 8, 2018

@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 screens/App like it is here:
https://gist.github.com/ryanflorence/daafb1e3cb8ad740b346#shared-modules

It kind of sticks for me, but not sure what others think...

@waldenraines
Copy link

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?

@sharvit
Copy link
Contributor Author

sharvit commented Feb 11, 2018

@waldenraines - Usually, they used to do stuff like getTodoById(id) selectTodosFromToday() selectTodosByCategory(category)

@priley86
Copy link

priley86 commented Feb 11, 2018

@sharvit reviewing this approach once more (and would like to call it out)... one of the things I like most is you've introduced react_app/components/PasswordStrength/__tests__/index.test.js integrational tests. I personally see more value from writing these kind of tests than for us to be responsible for writing a lot of e2e tests running in the selenium browser (as we might have previously). Maybe QE team can help with those sort of e2e tests? If we do become responsible, maybe we can look at something easy to configure like cypress.io, but i'm not sure what tools we have available for that.

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):
https://kentcdodds.com/post/write-integration-tests/
https://hackernoon.com/low-effort-high-value-integration-tests-in-redux-apps-d3a590bd9fd5

Was having this sort of a debate w/ a friend recently ;)

@priley86
Copy link

priley86 commented Feb 13, 2018

@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 mapStateToProps results (giving me some simple integration tests).

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...

@sharvit
Copy link
Contributor Author

sharvit commented Feb 13, 2018

@priley86 we kind of had the same debate here.
We wrote unit tests that doing tons of integration tests because they don't mock the imported modules. We only did mocking for the ajax calls.

So we end up asking ourselves, How should we write those unit-tests? and what about integration tests?
We can't migrate all our tests to be pure unit-tests without having a solution for 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.
In this case, e2e don't need to do more than just loading the app, navigate between the pages and make sure they contain the relevant packages inside. I don't see a need to interact with the inner components.

We are currently doing kind of an e2e testing in our rails stack and I would love moving them into the js stack.
I have no experience with tools like cypress.io but from watching their videos, it feels to me like a closed-garden and an overkill for our needs.

We are still not a single-page-app, we are not using react-router and big parts of our UI are still old legacy code written with .erb files and jquery.
In our react-redux stack, we mostly have connected components which we mount into to relevant part in the UI example.
So we can't really adopt the full screens approach at the moment, but I am a big fan of this approach and embrace everyone to use it 👍
We will keep monitoring how it works for other and will adopt it later if it will still be relevant. This pr is a step into this future.

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.

@amirfefer
Copy link
Member

amirfefer commented Feb 13, 2018

@sharvit 👍 Great change, I like it

@ohadlevy ohadlevy changed the title WIP: Fixes #22473 - Refactor react_app folder structure Fixes #22473 - Refactor react_app folder structure Feb 18, 2018
@ohadlevy
Copy link
Member

@amirfefer can you review please? thanks

Copy link
Member

@amirfefer amirfefer left a 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 }) =>
Copy link
Member

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

Copy link
Contributor Author

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?

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

Copy link
Contributor Author

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.

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());
Copy link
Member

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 ?

Copy link
Contributor Author

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';
Copy link
Member

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 = {
Copy link
Member

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 }));
Copy link
Member

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

Copy link
Contributor Author

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 } });
Copy link
Member

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'}});

Copy link
Contributor Author

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,
Copy link
Member

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');
Copy link
Member

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)

Copy link
Contributor Author

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
Copy link
Member

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.

@ohadlevy
Copy link
Member

what is the status of this pr? thanks

@sharvit
Copy link
Contributor Author

sharvit commented Feb 25, 2018

@ohadlevy Amazing timing just pushed :)

  1. Added an IntegrationTestHelper.js
  2. The integration test is taking a snapshot of state and action (only state before)

@amirfefer any thoughts?

@sharvit sharvit force-pushed the refactor/react-app-folder-structure branch from 7e8300f to 96ab056 Compare April 29, 2018 16:01
@sharvit
Copy link
Contributor Author

sharvit commented Apr 29, 2018

I have just rebased it, @amirfefer @ohadlevy can you have another look?

Copy link
Member

@amirfefer amirfefer left a comment

Choose a reason for hiding this comment

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

Thanks @sharvit
LGTM 👍

Copy link
Member

@tstrachota tstrachota left a 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')}
Copy link
Member

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';
Copy link
Member

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)
: [];
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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();
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

@sharvit
Copy link
Contributor Author

sharvit commented Apr 30, 2018

Thanks for the review @tstrachota, I just updated.
Can you have another look please?

Copy link
Member

@tstrachota tstrachota left a 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
@sharvit
Copy link
Contributor Author

sharvit commented May 8, 2018

Thanks @tstrachota! updated 👍

@tstrachota
Copy link
Member

Waiting for code owner review from @theforeman/packaging. @ekohl would you mind taking a look?

Copy link
Member

@ekohl ekohl left a 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.

@tstrachota tstrachota merged commit a671eca into theforeman:develop May 16, 2018
@tstrachota
Copy link
Member

Merged. Thank you @sharvit and everybody involved for such long running efforts!

@ares
Copy link
Member

ares commented May 16, 2018

Thanks for merging, please don't forget to set the redmine releaas afterwards. I set it to 1.19 now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.