Skip to content

Commit 7010769

Browse files
author
Tim Kindberg
committed
Add implementation and test
1 parent 9023e1f commit 7010769

File tree

6 files changed

+730
-603
lines changed

6 files changed

+730
-603
lines changed

.babelrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"presets": ["es2015", "react"]
2+
"presets": ["es2015", "react", "stage-0"]
33
}

README.md

+50-113
Original file line numberDiff line numberDiff line change
@@ -1,134 +1,71 @@
1-
# jest-when
1+
# react-context-reverse
22

33
```
4-
npm i --save-dev jest-when
4+
npm i --save-dev react-context-reverse
55
```
66

7-
A sugary way to mock return values for specific arguments only.
7+
Similar to React.createContext() but instead of the ancestor passing data to it's descendant, the descendant passes data to a single ancestor.
88

9-
#### Basic usage:
9+
#### Usage:
1010

11-
```javascript
12-
import { when } from "jest-when";
11+
Use the `createReverseContext` function just like you would use the `React.createContext` function.
1312

14-
const fn = jest.fn();
15-
when(fn)
16-
.calledWith(1)
17-
.mockReturnValue("yay!");
13+
The difference is it returns a Context object with a:
1814

19-
const result = fn(1);
20-
expect(result).toEqual("yay!");
21-
```
22-
23-
#### Supports multiple args:
24-
25-
```javascript
26-
import { when } from "jest-when";
27-
28-
const fn = jest.fn();
29-
when(fn)
30-
.calledWith(1, true, "foo")
31-
.mockReturnValue("yay!");
32-
33-
const result = fn(1, true, "foo");
34-
expect(result).toEqual("yay!");
35-
```
36-
37-
#### Supports training for single calls
38-
39-
```javascript
40-
import { when } from "jest-when";
41-
42-
const fn = jest.fn();
43-
when(fn)
44-
.calledWith(1, true, "foo")
45-
.mockReturnValueOnce("yay!");
46-
when(fn)
47-
.calledWith(1, true, "foo")
48-
.mockReturnValueOnce("nay!");
15+
- `<Context.ReverseProvider>`: Component used in a child/descendant component to provide a value to the context.
16+
- `<Context.ReverseConsumer>`: Component used in the parent/ancestor component to consume the provided value from the context.
4917

50-
expect(fn(1, true, "foo")).toEqual("yay!");
51-
expect(fn(1, true, "foo")).toEqual("nay!");
52-
expect(fn(1, true, "foo")).toBeUndefined();
53-
```
54-
55-
#### Supports Promises
56-
57-
```javascript
58-
import { when } from "jest-when";
18+
**Note:** I made the names `ReverseProvider` and `ReverseConsumer` to avoid confusion with a regular `Provider` and `Consumer`.
5919

60-
const fn = jest.fn();
61-
when(fn)
62-
.calledWith(1, true, "foo")
63-
.mockResolvedValue("yay!");
64-
when(fn)
65-
.calledWith(2, false, "bar")
66-
.mockResolvedValueOnce("nay!");
20+
#### Example:
6721

68-
expect(await fn(1, true, "foo")).toEqual("yay!");
69-
expect(await fn(1, true, "foo")).toEqual("yay!");
70-
71-
expect(await fn(2, false, "bar")).toEqual("nay!");
72-
expect(await fn(2, false, "bar")).toBeUndefined();
73-
```
22+
In this example, we are going to add a disabled class to a `<label>` if it's nested `<input>` is disabled.
7423

75-
#### Supports jest matchers:
24+
```jsx
25+
////////////////
26+
/* Example.js */
27+
////////////////
7628

77-
```javascript
78-
import { when } from "jest-when";
29+
import { Label } from "./Label";
30+
import { Checkbox } from "./Checkbox";
7931

80-
const fn = jest.fn();
81-
when(fn)
82-
.calledWith(
83-
expect.anything(),
84-
expect.any(Number),
85-
expect.arrayContaining(false)
86-
)
87-
.mockReturnValue("yay!");
88-
89-
const result = fn("whatever", 100, [true, false]);
90-
expect(result).toEqual("yay!");
91-
```
92-
93-
#### Supports compound declarations:
94-
95-
```javascript
96-
import { when } from "jest-when";
97-
98-
const fn = jest.fn();
99-
when(fn)
100-
.calledWith(1)
101-
.mockReturnValue("no");
102-
when(fn)
103-
.calledWith(2)
104-
.mockReturnValue("way?");
105-
when(fn)
106-
.calledWith(3)
107-
.mockReturnValue("yes");
108-
when(fn)
109-
.calledWith(4)
110-
.mockReturnValue("way!");
111-
112-
expect(fn(1)).toEqual("no");
113-
expect(fn(2)).toEqual("way?");
114-
expect(fn(3)).toEqual("yes");
115-
expect(fn(4)).toEqual("way!");
116-
expect(fn(5)).toEqual(undefined);
117-
```
32+
export const Example = () => (
33+
<Label>
34+
<Checkbox disabled /> // Note: the child has some disabled state to share
35+
Check Me
36+
</Label>
37+
);
11838

119-
#### Assert the args:
39+
//////////////
40+
/* Label.js */
41+
//////////////
12042

121-
Use `expectCalledWith` instead to run an assertion that the `fn` was called with the provided args. Your test will fail if the jest mock function is ever called without those exact `expectCalledWith` params.
43+
import { createReverseContext } from 'react-context-reverse'
12244

123-
Disclaimer: This won't really work very well with compound declarations, because one of them will always fail, and throw an assertion error.
45+
// We start by creating a reverse context to consume
46+
// the disabled context of the child checkbox
47+
export const DisabledContext = createReverseContext(false);
12448

125-
```javascript
126-
import { when } from "jest-when";
49+
// In the parent we use the ReverseConsumer, it provides
50+
// the value of the context via a child function
51+
export const Label = props => (
52+
<DisabledContext.ReverseConsumer>
53+
{disabled => (
54+
<label {...props} className={cx("Label", disabled && "is-disabled")} />
55+
)}
56+
</DisabledContext.ReverseConsumer>
57+
);
12758

128-
const fn = jest.fn();
129-
when(fn)
130-
.expectCalledWith(1)
131-
.mockReturnValue("x");
59+
//////////////
60+
/* Input.js */
61+
//////////////
62+
import { DisabledContext } from "./Label";
13263

133-
fn(2); // Will throw a helpful jest assertion error with args diff
64+
// In the child we use the ReverseProvider, we provide
65+
// the value to the context
66+
export const Checkbox = props => (
67+
<DisabledContext.ReverseProvider value={props.disabled}>
68+
<input {...props} type="checkbox" className="Checkbox" />
69+
</DisabledContext.ReverseProvider>
70+
);
13471
```

package.json

+10-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
2-
"name": "react-reverse-context",
2+
"name": "react-context-reverse",
33
"version": "0.0.1",
4-
"description": "Normal React context flow down. Reverse context flows up, from a descendant to a single ancestor.",
4+
"description": "Normal React context flows down. Reverse context flows up, from a descendant to a single ancestor.",
55
"main": "dist/reverse-context.js",
66
"files": [
77
"dist/reverse-context.js"
@@ -24,13 +24,18 @@
2424
"babel-jest": "^22.1.0",
2525
"babel-preset-es2015": "^6.24.1",
2626
"babel-preset-react": "^6.24.1",
27+
"babel-preset-stage-0": "^6.24.1",
2728
"husky": "^0.14.3",
28-
"jest": "^22.1.1",
29+
"jest": "^23.6.0",
2930
"prettier": "1.14.2",
3031
"pretty-quick": "^1.6.0",
32+
"react": "^16.3.0",
33+
"react-dom": "^16.3.0",
34+
"react-testing-library": "^5.2.0",
3135
"regenerator-runtime": "^0.11.1"
3236
},
33-
"dependencies": {
34-
"expect": "^22.1.0"
37+
"peerDependencies": {
38+
"react": "^16.3.0",
39+
"react-dom": "^16.3.0"
3540
}
3641
}

src/reverse-context.js

+47-81
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,50 @@
1-
import { equals } from "expect/build/jasmine_utils";
2-
3-
export class WhenMock {
4-
constructor(fn, debug) {
5-
this.fn = fn;
6-
this.callMocks = [];
7-
this.debug = debug;
8-
this.log = (...args) => this.debug && console.log(...args);
9-
10-
const mockReturnValue = (matchers, assertCall, once = false) => val => {
11-
this.callMocks.push({ matchers, val, assertCall, once });
12-
13-
this.fn.mockImplementation((...args) => {
14-
this.log("mocked impl", args);
15-
16-
for (let i = 0; i < this.callMocks.length; i++) {
17-
const { matchers, val, assertCall } = this.callMocks[i];
18-
const match = matchers.reduce((match, matcher, i) => {
19-
this.log(`matcher check, match: ${match}, index: ${i}`);
20-
21-
// Propagate failure to the end
22-
if (!match) {
23-
return false;
24-
}
25-
26-
const arg = args[i];
27-
28-
this.log(` matcher: ${matcher}`);
29-
this.log(` arg: ${arg}`);
30-
31-
// Assert the match for better messaging during a failure
32-
if (assertCall) {
33-
expect(arg).toEqual(matcher);
34-
}
35-
36-
return equals(arg, matcher);
37-
}, true);
38-
39-
if (match) {
40-
let removedOneItem = false;
41-
this.callMocks = this.callMocks.filter(
42-
mock =>
43-
!(
44-
mock.once &&
45-
equals(mock.matchers, matchers) &&
46-
!removedOneItem & (removedOneItem = true)
47-
)
48-
);
49-
return val;
50-
}
51-
}
52-
});
53-
};
54-
55-
const mockResolvedValueOnce = (matchers, assertCall) => val =>
56-
mockReturnValueOnce(matchers, assertCall)(Promise.resolve(val));
57-
58-
const mockResolvedValue = (matchers, assertCall) => val =>
59-
mockReturnValue(matchers, assertCall)(Promise.resolve(val));
60-
61-
const mockReturnValueOnce = (matchers, assertCall) => val =>
62-
mockReturnValue(matchers, assertCall, true)(val);
63-
64-
this.calledWith = (...matchers) => ({
65-
mockReturnValue: mockReturnValue(matchers, false),
66-
mockReturnValueOnce: mockReturnValueOnce(matchers, false),
67-
mockResolvedValue: mockResolvedValue(matchers, false),
68-
mockResolvedValueOnce: mockResolvedValueOnce(matchers, false)
69-
});
1+
import React from "react";
2+
3+
/**
4+
* Similar to React.createContext() but instead of the ancestor passing data to it's descendant,
5+
* the descendant passes data to a single ancestor.
6+
*
7+
* @param defaultValue The default value for the reverse context
8+
* @returns {{ReverseConsumer: ReverseConsumer, ReverseProvider: ReverseProvider}}
9+
*/
10+
export function createReverseContext(defaultValue) {
11+
const NativeContext = React.createContext();
12+
13+
class ReverseProvider extends React.Component {
14+
setParentState = () => {};
15+
componentDidMount() {
16+
this.setParentState(this.props.value);
17+
}
18+
componentDidUpdate() {
19+
this.setParentState(this.props.value);
20+
}
21+
22+
render() {
23+
return (
24+
<NativeContext.Consumer>
25+
{setState => {
26+
if (setState) this.setParentState = setState;
27+
return this.props.children;
28+
}}
29+
</NativeContext.Consumer>
30+
);
31+
}
32+
}
7033

71-
this.expectCalledWith = (...matchers) => ({
72-
mockReturnValue: mockReturnValue(matchers, true),
73-
mockReturnValueOnce: mockReturnValueOnce(matchers, true),
74-
mockResolvedValue: mockResolvedValue(matchers, true),
75-
mockResolvedValueOnce: mockResolvedValueOnce(matchers, true)
76-
});
34+
class ReverseConsumer extends React.Component {
35+
state = {};
36+
setParentState = value => this.setState({ value });
37+
render() {
38+
return (
39+
<NativeContext.Provider value={this.setParentState}>
40+
{this.props.children(this.state.value)}
41+
</NativeContext.Provider>
42+
);
43+
}
7744
}
78-
}
7945

80-
export const reverseContext = (fn, { debug = false } = {}) => {
81-
if (fn.__whenMock__ instanceof WhenMock) return fn.__whenMock__;
82-
fn.__whenMock__ = new WhenMock(fn, debug);
83-
return fn.__whenMock__;
84-
};
46+
return {
47+
ReverseConsumer,
48+
ReverseProvider
49+
};
50+
}

0 commit comments

Comments
 (0)