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

Add flattenCmd #263

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open

Conversation

abradley2
Copy link

Motivated by the discussion here, we have an application making extensive use of redux-loop's Cmd.map and Cmd.list functionality to support sub-modules (very similar to how Elm apps are traditionally structured) and want to test commands in a convenient way.

Example fork used is published here. In our tests we had a dozen situations where we used a similar getRunCmd function that did much of the logic in flattenCmd and then did a .find

Implementing that function now looks something like:

import { flattenCmd, CmdType, RunCmd } from '@abradley2/redux-loop'

export const getRunCmd = (cmd, func) => flattenCmd(cmd).find((c) => c.type === 'RUN' && c.func === func)

src/cmd.js Outdated
) {
return flattenCmd(cmd.nestedCmd);
}
if (cmd.type === cmdTypes.LIST) {
Copy link
Contributor

Choose a reason for hiding this comment

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

else if?

return cmd.cmds.flatMap(flattenCmd);
}
return [cmd];
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there any use case to justify including Cmd.none values in the array? It might be more useful if the array consists only of run commands and action commands.

Copy link
Author

Choose a reason for hiding this comment

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

I can't really think of a use case other than a testing purpose which would still be covered by checking expect(flatCmd).toEqual([]). I've updated to remove Cmd.none values 👍

Copy link
Member

Choose a reason for hiding this comment

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

I'm torn on this. I kind of like keeping the original cmds in there. though i guess there's not much value. Why filter them out though? The general idea with a flattened list is to say it contains a command right? if someone wants to look for Cmd.none, we should not stop them

index.d.ts Outdated
@@ -266,3 +266,5 @@ export function isLoop(test: any): boolean;
export function getModel<S>(loop: S | Loop<S>): S;

export function getCmd(a: any): CmdType | null;

export function flattenCmd(cmd: CmdType): CmdType[];
Copy link
Contributor

Choose a reason for hiding this comment

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

The return type could be more specific:
ActionCmd<UnknownAction> | NoneCmd | RunCmd<UnknownAction, UnknownAction>

@@ -4,6 +4,10 @@ export const flatten = (array) => {
return concat.apply([], array);
};

export const flatMap = (array, mapFn) => {
return array.reduce((nextArray, cur) => nextArray.concat(mapFn(cur)), []);
Copy link
Author

Choose a reason for hiding this comment

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

Since there are instructions in the docs on how to use this library with browsers that don't natively have Promise or Symbol I felt I should avoid using Array.prototype.flatMap as none of those browsers would support it either.

@laszlopandy
Copy link
Contributor

Thanks for the updates @abradley2!
@bdwain I think this would be a good addition. WDYT?

index.d.ts Outdated
@@ -266,3 +266,5 @@ export function isLoop(test: any): boolean;
export function getModel<S>(loop: S | Loop<S>): S;

export function getCmd(a: any): CmdType | null;

export function flattenCmd(cmd: CmdType): RunCmd<UnknownAction, UnknownAction> | ActionCmd<UnknownAction>;
Copy link
Member

Choose a reason for hiding this comment

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

you accidentally got rid of the array portion of the return type here.

src/cmd.js Outdated
cmd.type === cmdTypes.SET_TIMEOUT ||
cmd.type === cmdTypes.SET_INTERVAL
) {
return flattenCmd(cmd.nestedCmd);
Copy link
Member

Choose a reason for hiding this comment

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

we lose some info here. A map cmd is more than just the nested Cmd. it's also the tagger function. Is there any way we can preserve that in this?

Copy link
Author

@abradley2 abradley2 Apr 13, 2021

Choose a reason for hiding this comment

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

For our tests we haven't ever really felt the need to consider the tagger, but I do see how that can be a relevant detail. I also suppose even Cmd.timeout and Cmd.interval could be.

Here was the solution I landed on, I think it makes the function very agnostic to what level of detail people are seeking to test against. Everything, including Cmd.none is preserved at every level of nesting

import { Cmd, loop, flattenCmd, getCmd } from 'redux-loop';

test('reducer fetches data after loadingStart action', (t) => {
t.plan(2)
Copy link
Member

Choose a reason for hiding this comment

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

what is t.plan?

You may need to test objects from the `Cmd` module that make use of
`Cmd.list` or `Cmd.map`. Imagine in the above example the reducer is updated to
perform multiple commands when loading starts. We want to test that the properly
shaped `fetchDetails` cmd is called, but don't necessarily want to mock out every
Copy link
Member

Choose a reason for hiding this comment

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

what do you mean mock out effects? You shouldn't need to mock any effect because they are not run

Copy link
Author

Choose a reason for hiding this comment

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

updated this language to be a bit more clear 👍

reducer(state, loadingStart(1));
);

const fetchCmd = flattenCmd(resultCmd).find((cmd) => {
Copy link
Member

@bdwain bdwain Apr 1, 2021

Choose a reason for hiding this comment

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

it seems a little odd to search by successActionCreator only to then do a deep equal later. Why not just say

  expect(flattenCmd(resultCmd)).toContain(
    Cmd.run(fetchDetails, {
      successActionCreator: loadingSuccess,
      failActionCreator: loadingError,
      args: [1]
    })
  )

Copy link
Author

Choose a reason for hiding this comment

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

This and t.plan where tape testing library specific. I've updated the examples to use the "jest" assertions which make the example tests a bit more clear

docs/tutorial/Testing.md Outdated Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants