Skip to content

Commit ac4132d

Browse files
committed
[feature] add FetchingProvider
- gracefully fetch text resources from remote endpoint
1 parent e28df46 commit ac4132d

File tree

5 files changed

+237
-1
lines changed

5 files changed

+237
-1
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"react-dom": "^16.8.2",
4747
"react-scripts": "2.1.5",
4848
"react-test-renderer": "^16.8.2",
49+
"react-testing-library": "^6.0.0",
4950
"rollup": "^1.1.2",
5051
"rollup-plugin-babel": "^4.3.2",
5152
"rollup-plugin-commonjs": "^9.2.0",

src/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/**
22
* The Public API.
33
*/
4-
export * from './lib/messageSource';
4+
export { Provider, withMessages, propTypes } from './lib/messageSource';
5+
export { FetchingProvider } from './lib/FetchingProvider';

src/lib/FetchingProvider.js

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import React, { Component } from 'react';
2+
import PropTypes from 'prop-types';
3+
4+
import { Provider } from './messageSource';
5+
6+
const identity = x => x;
7+
8+
export class FetchingProvider extends Component {
9+
state = {
10+
translations: {},
11+
fetching: false,
12+
};
13+
14+
mounted = false;
15+
16+
static propTypes = {
17+
/**
18+
* The URL which serves the text messages.
19+
* Required.
20+
*/
21+
url: PropTypes.string.isRequired,
22+
23+
/**
24+
* Makes the rendering of the sub-tree synchronous.
25+
* The components will not render until fetching of the text messages finish.
26+
*
27+
* Defaults to true.
28+
*/
29+
blocking: PropTypes.bool,
30+
31+
/**
32+
* A function which can transform the response received from GET /props.url
33+
* to a suitable format:
34+
*
35+
* Example:
36+
* function transform(response) {
37+
* return response.textMessages;
38+
* }
39+
*/
40+
transform: PropTypes.func,
41+
42+
/**
43+
* Invoked when fetching of text messages starts.
44+
*/
45+
onFetchingStart: PropTypes.func,
46+
47+
/**
48+
* Invoked when fetching of text messages finishes.
49+
*/
50+
onFetchingEnd: PropTypes.func,
51+
52+
/**
53+
* Invoked when fetching fails.
54+
*/
55+
onFetchingError: PropTypes.func,
56+
57+
/**
58+
* Children.
59+
*/
60+
children: PropTypes.node,
61+
};
62+
63+
static defaultProps = {
64+
blocking: true,
65+
transform: identity,
66+
onFetchingStart: identity,
67+
onFetchingEnd: identity,
68+
onFetchingError: identity,
69+
};
70+
71+
componentDidMount() {
72+
this.mounted = true;
73+
this.fetchResources();
74+
}
75+
76+
componentDidUpdate(prevProps) {
77+
if (this.props.url !== prevProps.url) {
78+
this.fetchResources();
79+
}
80+
}
81+
82+
componentWillUnmount() {
83+
this.mounted = false;
84+
}
85+
86+
fetchResources = () => {
87+
const { url, transform, onFetchingStart, onFetchingEnd, onFetchingError } = this.props;
88+
89+
this.setState({ fetching: true }, onFetchingStart);
90+
fetch(url)
91+
.then(r => r.json())
92+
.then(response => {
93+
if (this.mounted) {
94+
this.setState(
95+
{
96+
translations: transform(response),
97+
fetching: false,
98+
},
99+
onFetchingEnd,
100+
);
101+
}
102+
})
103+
.catch(onFetchingError);
104+
};
105+
106+
render() {
107+
const { blocking, children } = this.props;
108+
const { translations, fetching } = this.state;
109+
const shouldRenderSubtree = !blocking || (blocking && !fetching);
110+
return <Provider value={translations}>{shouldRenderSubtree ? children : null}</Provider>;
111+
}
112+
}

src/lib/FetchingProvider.test.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import React from 'react';
2+
import * as RTL from 'react-testing-library';
3+
import { FetchingProvider } from './FetchingProvider';
4+
import { withMessages } from './messageSource';
5+
6+
describe('FetchingProvider', () => {
7+
const Spy = withMessages(props => props.getMessage('hello.world'));
8+
9+
beforeEach(() => {
10+
// mock impl of fetch() API
11+
global.fetch = jest.fn(() =>
12+
Promise.resolve({
13+
json: () =>
14+
Promise.resolve({
15+
'hello.world': 'Hello world',
16+
}),
17+
}),
18+
);
19+
});
20+
21+
it('fetches text resources and invokes all lifecycles', async () => {
22+
const transform = jest.fn(x => x);
23+
const onFetchingStart = jest.fn();
24+
const onFetchingEnd = jest.fn();
25+
26+
function TestComponent() {
27+
return (
28+
<FetchingProvider
29+
url="http://any.uri/texts?lang=en"
30+
transform={transform}
31+
onFetchingStart={onFetchingStart}
32+
onFetchingEnd={onFetchingEnd}
33+
>
34+
<Spy />
35+
</FetchingProvider>
36+
);
37+
}
38+
39+
const { getByText } = RTL.render(<TestComponent />);
40+
const spyNode = await RTL.waitForElement(() => getByText('Hello world'));
41+
42+
expect(spyNode).toBeDefined();
43+
expect(spyNode.innerHTML).toBe('Hello world');
44+
expect(transform).toHaveBeenCalled();
45+
expect(onFetchingStart).toHaveBeenCalled();
46+
expect(onFetchingEnd).toHaveBeenCalled();
47+
expect(global.fetch).toHaveBeenCalledTimes(1);
48+
});
49+
50+
it('fetches text resources when url prop changes', async () => {
51+
function TestComponent(props) {
52+
return (
53+
// eslint-disable-next-line react/prop-types
54+
<FetchingProvider url={props.url}>
55+
<Spy />
56+
</FetchingProvider>
57+
);
58+
}
59+
60+
const { getByText, rerender } = RTL.render(<TestComponent url="http://any.uri/texts?lang=en" />);
61+
await RTL.waitForElement(() => getByText('Hello world'));
62+
63+
rerender(<TestComponent url="http://any.uri/texts?lang=de" />);
64+
65+
expect(global.fetch).toHaveBeenCalledTimes(2);
66+
});
67+
68+
it('invokes onFetchingError lifecycle on network failure', async () => {
69+
const onFetchingError = jest.fn();
70+
const faultyFetch = jest.fn(() => Promise.reject(new Error('Failure')));
71+
global.fetch = faultyFetch;
72+
73+
RTL.render(<FetchingProvider url="http://any.uri/texts" onFetchingError={onFetchingError} />);
74+
await RTL.wait(); // until fetch() rejects
75+
76+
expect(faultyFetch).toHaveBeenCalledTimes(1);
77+
expect(onFetchingError).toHaveBeenCalledTimes(1);
78+
});
79+
});

yarn.lock

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,13 @@
722722
dependencies:
723723
regenerator-runtime "^0.12.0"
724724

725+
"@babel/runtime@^7.1.5", "@babel/runtime@^7.3.1":
726+
version "7.3.4"
727+
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83"
728+
integrity sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g==
729+
dependencies:
730+
regenerator-runtime "^0.12.0"
731+
725732
"@babel/template@^7.1.0", "@babel/template@^7.1.2", "@babel/template@^7.2.2":
726733
version "7.2.2"
727734
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.2.2.tgz#005b3fdf0ed96e88041330379e0da9a708eb2907"
@@ -773,6 +780,11 @@
773780
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
774781
integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
775782

783+
"@sheerun/mutationobserver-shim@^0.3.2":
784+
version "0.3.2"
785+
resolved "https://registry.yarnpkg.com/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz#8013f2af54a2b7d735f71560ff360d3a8176a87b"
786+
integrity sha512-vTCdPp/T/Q3oSqwHmZ5Kpa9oI7iLtGl3RQaA/NyLHikvcrPxACkkKVr/XzkSPJWXHRhKGzVvb0urJsbMlRxi1Q==
787+
776788
"@svgr/babel-plugin-add-jsx-attribute@^4.0.0":
777789
version "4.0.0"
778790
resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.0.0.tgz#5acf239cd2747b1a36ec7e708de05d914cb9b948"
@@ -3047,6 +3059,16 @@ dom-serializer@0:
30473059
domelementtype "~1.1.1"
30483060
entities "~1.1.1"
30493061

3062+
dom-testing-library@^3.13.1:
3063+
version "3.16.8"
3064+
resolved "https://registry.yarnpkg.com/dom-testing-library/-/dom-testing-library-3.16.8.tgz#26549b249f131a25e4339ebec9fcaa2e7642527f"
3065+
integrity sha512-VGn2piehGoN9lmZDYd+xoTZwwcS+FoXebvZMw631UhS5LshiLTFNJs9bxRa9W7fVb1cAn9AYKAKZXh67rCDaqw==
3066+
dependencies:
3067+
"@babel/runtime" "^7.1.5"
3068+
"@sheerun/mutationobserver-shim" "^0.3.2"
3069+
pretty-format "^24.0.0"
3070+
wait-for-expect "^1.1.0"
3071+
30503072
domain-browser@^1.1.1:
30513073
version "1.2.0"
30523074
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
@@ -7729,6 +7751,14 @@ pretty-format@^23.6.0:
77297751
ansi-regex "^3.0.0"
77307752
ansi-styles "^3.2.0"
77317753

7754+
pretty-format@^24.0.0:
7755+
version "24.0.0"
7756+
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.0.0.tgz#cb6599fd73ac088e37ed682f61291e4678f48591"
7757+
integrity sha512-LszZaKG665djUcqg5ZQq+XzezHLKrxsA86ZABTozp+oNhkdqa+tG2dX4qa6ERl5c/sRDrAa3lHmwnvKoP+OG/g==
7758+
dependencies:
7759+
ansi-regex "^4.0.0"
7760+
ansi-styles "^3.2.0"
7761+
77327762
private@^0.1.6, private@^0.1.8:
77337763
version "0.1.8"
77347764
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
@@ -8067,6 +8097,14 @@ react-test-renderer@^16.8.2:
80678097
react-is "^16.8.2"
80688098
scheduler "^0.13.2"
80698099

8100+
react-testing-library@^6.0.0:
8101+
version "6.0.0"
8102+
resolved "https://registry.yarnpkg.com/react-testing-library/-/react-testing-library-6.0.0.tgz#81edfcfae8a795525f48685be9bf561df45bb35d"
8103+
integrity sha512-h0h+YLe4KWptK6HxOMnoNN4ngu3W8isrwDmHjPC5gxc+nOZOCurOvbKVYCvvuAw91jdO7VZSm/5KR7TxKnz0qA==
8104+
dependencies:
8105+
"@babel/runtime" "^7.3.1"
8106+
dom-testing-library "^3.13.1"
8107+
80708108
react@^16.8.2:
80718109
version "16.8.2"
80728110
resolved "https://registry.yarnpkg.com/react/-/react-16.8.2.tgz#83064596feaa98d9c2857c4deae1848b542c9c0c"
@@ -9750,6 +9788,11 @@ w3c-hr-time@^1.0.1:
97509788
dependencies:
97519789
browser-process-hrtime "^0.1.2"
97529790

9791+
wait-for-expect@^1.1.0:
9792+
version "1.1.0"
9793+
resolved "https://registry.yarnpkg.com/wait-for-expect/-/wait-for-expect-1.1.0.tgz#6607375c3f79d32add35cd2c87ce13f351a3d453"
9794+
integrity sha512-vQDokqxyMyknfX3luCDn16bSaRcOyH6gGuUXMIbxBLeTo6nWuEWYqMTT9a+44FmW8c2m6TRWBdNvBBjA1hwEKg==
9795+
97539796
walker@~1.0.5:
97549797
version "1.0.7"
97559798
resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"

0 commit comments

Comments
 (0)