@shopgate/webpack 7.30.3-beta.2 → 7.30.3-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/local-packages/pwa-extension-kit/src/components/ProductCharacteristics/__snapshots__/spec.jsx.snap +428 -0
- package/local-packages/pwa-extension-kit/src/components/ProductCharacteristics/index.jsx +7 -0
- package/local-packages/pwa-extension-kit/src/components/ProductCharacteristics/spec.jsx +128 -0
- package/local-packages/pwa-extension-kit/src/components/README.md +38 -0
- package/local-packages/pwa-extension-kit/src/components/index.js +8 -0
- package/local-packages/pwa-extension-kit/src/components/index.spec.js +11 -0
- package/local-packages/pwa-extension-kit/src/connectors/README.md +222 -0
- package/local-packages/pwa-extension-kit/src/connectors/index.js +23 -0
- package/local-packages/pwa-extension-kit/src/connectors/index.spec.js +29 -0
- package/local-packages/pwa-extension-kit/src/connectors/selectors/user.js +24 -0
- package/local-packages/pwa-extension-kit/src/connectors/withHistoryActions.jsx +75 -0
- package/local-packages/pwa-extension-kit/src/connectors/withHistoryActions.spec.jsx +72 -0
- package/local-packages/pwa-extension-kit/src/connectors/withPageProductId.jsx +61 -0
- package/local-packages/pwa-extension-kit/src/connectors/withPageProductId.spec.jsx +73 -0
- package/local-packages/pwa-extension-kit/src/connectors/withPageState.jsx +49 -0
- package/local-packages/pwa-extension-kit/src/connectors/withPageState.spec.jsx +59 -0
- package/local-packages/pwa-extension-kit/src/connectors/withProductContext.jsx +38 -0
- package/local-packages/pwa-extension-kit/src/connectors/withProductContext.spec.jsx +55 -0
- package/local-packages/pwa-extension-kit/src/connectors/withThemeComponents.jsx +37 -0
- package/local-packages/pwa-extension-kit/src/connectors/withThemeComponents.spec.jsx +43 -0
- package/local-packages/pwa-extension-kit/src/connectors/withUser.jsx +61 -0
- package/local-packages/pwa-extension-kit/src/connectors/withUser.spec.jsx +101 -0
- package/local-packages/pwa-extension-kit/src/env/README.md +28 -0
- package/local-packages/pwa-extension-kit/src/env/helpers/index.js +7 -0
- package/local-packages/pwa-extension-kit/src/env/helpers/index.spec.js +12 -0
- package/local-packages/pwa-extension-kit/src/env/helpers/isIOSTheme.js +10 -0
- package/local-packages/pwa-extension-kit/src/env/helpers/isIOSTheme.spec.js +16 -0
- package/local-packages/pwa-extension-kit/src/env/index.js +7 -0
- package/local-packages/pwa-extension-kit/src/env/index.spec.js +13 -0
- package/local-packages/pwa-extension-kit/src/helpers/README.md +30 -0
- package/local-packages/pwa-extension-kit/src/helpers/TaggedLogger.js +50 -0
- package/local-packages/pwa-extension-kit/src/helpers/TaggedLogger.spec.js +41 -0
- package/local-packages/pwa-extension-kit/src/helpers/_getConsole.js +4 -0
- package/local-packages/pwa-extension-kit/src/helpers/index.js +7 -0
- package/local-packages/pwa-extension-kit/src/helpers/index.spec.js +12 -0
- package/local-packages/pwa-extension-kit/src/index.js +16 -0
- package/local-packages/pwa-extension-kit/src/index.spec.js +16 -0
- package/local-packages/pwa-extension-kit/src/proptypes/README.md +9 -0
- package/local-packages/pwa-extension-kit/src/proptypes/User.js +15 -0
- package/local-packages/pwa-extension-kit/src/proptypes/index.js +7 -0
- package/package.json +2 -2
- package/webpack.config.js +7 -15
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# Shopgate Connect - PWA Extension Kit - /connectors - Data connectors
|
|
2
|
+
## Purpose
|
|
3
|
+
|
|
4
|
+
Connectors are [Higher Order Components](https://reactjs.org/docs/higher-order-components.html) which are made to simplify reading data or actions from PWA app by frontend components.
|
|
5
|
+
|
|
6
|
+
Connectors are meant to replace some PWA redux selectors/actions or Contexts by using them in a simpler way and hiding the logic behind gathering basic data which is proven to be commonly used by many extensions.
|
|
7
|
+
|
|
8
|
+
## Available connectors
|
|
9
|
+
### withHistoryActions
|
|
10
|
+
Connects provided component with a wrapper behind internal PWA routing actions.
|
|
11
|
+
|
|
12
|
+
#### Possible usage
|
|
13
|
+
Everywhere as a React component rendered within the PWA app.
|
|
14
|
+
|
|
15
|
+
#### Props provided
|
|
16
|
+
- `historyPush(pathname: string, options: object[optional])` - Opens new page in a safe way. Detects link types. Internal links (deep links) which are handled by the PWA app. External links are handled by opening the in-app-browser.
|
|
17
|
+
- `state (object[optional])` - Route state is a property of options and passed from history action. Is NOT the redux state. It is metadata only for the routing transition.
|
|
18
|
+
- Values passed to state are defined in route action.
|
|
19
|
+
- `historyReplace(pathname: string, options: object[optional])` - Replaces current page with provided pathname if pathname is an internal link. If pathname is external link it works exactly as `historyPush`. If you intend to replace your current page with in-app-browser, use combination of `historyPush` and `historyPop` instead.
|
|
20
|
+
- `state (object[optional])` - Route state is a property of options and passed from history action. Is NOT the redux state. It is metadata only for the routing transition.
|
|
21
|
+
- Values passed to state are defined in route action.
|
|
22
|
+
- `historyPop()` - Pops current page from a history stack. Works like a "back" button.
|
|
23
|
+
|
|
24
|
+
#### Example usage
|
|
25
|
+
```jsx
|
|
26
|
+
import React, { Component } from 'react'
|
|
27
|
+
import { withHistoryActions } from '@shopgate-ps/pwa-extension-kit/connectors';
|
|
28
|
+
|
|
29
|
+
class MyComponent extends Component
|
|
30
|
+
handleClick = () => {
|
|
31
|
+
this.props.historyPush('/example', { state: { title: 'foo' });
|
|
32
|
+
}
|
|
33
|
+
handleDismiss = () => {
|
|
34
|
+
this.props.historyPop();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
render() {
|
|
38
|
+
return (
|
|
39
|
+
<Fragment>
|
|
40
|
+
<button onClick={this.handleOpen}>Open page</button>
|
|
41
|
+
<button onClick={this.handleDismiss}>Dismiss</button>
|
|
42
|
+
</Fragment>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default withHistoryActions(MyComponent);
|
|
48
|
+
```
|
|
49
|
+
### withPageProductId
|
|
50
|
+
Connector which provides `hex2Bin` decoded `productId` which is also available in the page pathname params. It's intended to be used with or without additional product selectors like `getProduct`, `getBaseProduct` depending on the extension needs.
|
|
51
|
+
|
|
52
|
+
#### Possible usage
|
|
53
|
+
This connector will only work while being rendered on a page which has :productId pathname param. Current pages which offer this param are:
|
|
54
|
+
- Product Page ('/item/:productId')
|
|
55
|
+
- Write review page ('/item/:productId/write-review')
|
|
56
|
+
|
|
57
|
+
#### Example usage
|
|
58
|
+
##### Getting productId as is
|
|
59
|
+
```jsx
|
|
60
|
+
// Page pathname where component is rendered: '/item/31323334'
|
|
61
|
+
import { withPageProductId } from '@shopgate-ps/pwa-extension-kit/data/connectors';
|
|
62
|
+
|
|
63
|
+
// Will produce <div>This product id is: 1234</div>
|
|
64
|
+
const MyComponent = ({ productId}) => (<div>This product id is: ${productId}</div);
|
|
65
|
+
|
|
66
|
+
export default withPageProductId(MyComponent);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
##### Getting always a base product id
|
|
70
|
+
Some products has variants. If variant is selected by user or product page is rendered directly with selected variant (by for example deep linking), product id available in the pathname is an id of currently selected variant.
|
|
71
|
+
In this case, in order to read the `baseProductId` (product id of a variant's parent product), there's need to use additional PWA redux selectors.
|
|
72
|
+
```jsx
|
|
73
|
+
// Page pathname where component is rendered: '/item/313233342d76617269616e742d31'
|
|
74
|
+
import { connect } from 'react-redux'; // Provides connection to redux.
|
|
75
|
+
import { withPageProductId } from '@shopgate-ps/pwa-extension-kit/data/connectors'; // Fetches and decodes productId from a pathname
|
|
76
|
+
import { getBaseProductId } from '@shopgate/pwa-common-commerce/product/selectors'; // Makes sure productId always comes from base product.
|
|
77
|
+
|
|
78
|
+
// Will produce <div>This product id is: 1234</div>
|
|
79
|
+
const MyComponent = ({ baseProductId }) => (<div>This product id is: ${baseProductId}</div);
|
|
80
|
+
|
|
81
|
+
// Function that maps state and given props to component props.
|
|
82
|
+
// State is a current redux state
|
|
83
|
+
// Props in this example are made by `withPageProductId` connector and pass decoded `1234-variant-1` (encoded version: `313233342d76617269616e742d31`)
|
|
84
|
+
const mapStateToProps = (state, props) => ({
|
|
85
|
+
baseProductId: getBaseProductId(state, props)
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Component connected with redux only
|
|
89
|
+
const ConnectedComponent = connect(mapStateToProps)(MyComponent);
|
|
90
|
+
|
|
91
|
+
// ConnectedComponent wrapped with data connector.
|
|
92
|
+
export default withPageProductId(ConnectedComponent);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### withPageState
|
|
96
|
+
Connects provided component with a page state: `isLoading`, `isVisible`.
|
|
97
|
+
|
|
98
|
+
#### Props provided
|
|
99
|
+
- `isLoading (bool)` - Tells if page on which component is rendered is currently loading (usually with a visible progress bar).
|
|
100
|
+
- `isVisilble (bool)` - Tells if page on which component is rendered is currently visible. Returns false when page is somewhere in the background. For example if another page is pushed to the history stack, but the one where component is rendered is still mounted via the DOM caching.
|
|
101
|
+
- `pathname (string)` - The pathname of page on which component is rendered (as in `window.location.pathname`).
|
|
102
|
+
- `pattern (string)` - The pattern of a route of a page on which component is rendered. Handy for some comparison (see example usage).
|
|
103
|
+
- `location (string)` - The location of page on which component is rendered (as pathname but also contains query strings).
|
|
104
|
+
- `state (object[optional])` - Route state passed from history action. Is NOT the redux state. It is metadata only for the routing transition.
|
|
105
|
+
- Values passed to state are defined in route action.
|
|
106
|
+
|
|
107
|
+
#### Example usage
|
|
108
|
+
```jsx
|
|
109
|
+
import React from 'react';
|
|
110
|
+
import { CART_PATH } from '@shopgate/pwa-common-commerce/cart/constants';
|
|
111
|
+
import { withPageState } from '@shopgate-ps/pwa-extension-kit/connectors';
|
|
112
|
+
import LoadingIndicator from '...';
|
|
113
|
+
|
|
114
|
+
const MyComponent = ({ isVisible, isLoading, pattern, state }) => {
|
|
115
|
+
// Returns null when component is rendered in the cart page.
|
|
116
|
+
// Usually this approach is only needed in general-use portals like `app-bar.*`.
|
|
117
|
+
if (pattern === CART_PATH) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
if (!isVisible) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
if (isLoading) {
|
|
124
|
+
return <LoadingIndicator />
|
|
125
|
+
}
|
|
126
|
+
return <div>{state.title || 'Default title'}</div>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export default withPageState(MyComponent);
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### withUser
|
|
133
|
+
Connects provided component with a user data state.
|
|
134
|
+
|
|
135
|
+
#### Props provided
|
|
136
|
+
- `user (Object)` - User data object with following properties:
|
|
137
|
+
- `isLoggedIn (bool)` - Whether the user is logged in.
|
|
138
|
+
- `id (string|number|null)` - User id, null when user is logged out.
|
|
139
|
+
- `email (string|null)` - User email, null when user is logged out.
|
|
140
|
+
- `firstName (string|null)` - User first name, null when user is logged out.
|
|
141
|
+
- `lastName (string|null)` - User last name (can contain middle name in rare edge cases), null when user is logged out.
|
|
142
|
+
- `displayName (string|null)` - User name, ready to be displayed in the UI, null when user is logged out.
|
|
143
|
+
|
|
144
|
+
#### Example usage
|
|
145
|
+
```jsx
|
|
146
|
+
import { withUser } from '@shopgate-ps/pwa-extension-kit/connectors';
|
|
147
|
+
import User from '@shopgate-ps/pwa-extension-kit/proptypes';
|
|
148
|
+
|
|
149
|
+
const MyComponent = ({ user }) => {
|
|
150
|
+
if (!user.isLoggedIn) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return <div>Hello {user.displayName}</div>
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
MyComponent.propTypes = {
|
|
158
|
+
user: User.isRequired,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export default withUser(MyComponent);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### withProductContext
|
|
165
|
+
Connects provided component with a product context
|
|
166
|
+
|
|
167
|
+
#### Props provided
|
|
168
|
+
- `options (object)` - Current selected product options
|
|
169
|
+
- `productId (string)` - Id of the current shown product
|
|
170
|
+
- `variantId (string)` - Id of the current selected variant
|
|
171
|
+
- `conditioner (Function)` - Helper class for ProductCharacteristic component
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
#### Example usage
|
|
175
|
+
```jsx
|
|
176
|
+
import React from 'react';
|
|
177
|
+
import { withProductContext } from '@shopgate/pwa-extension-kit/connectors';
|
|
178
|
+
|
|
179
|
+
const MyComponent = ({ options, productId, variantId, conditioner }) => {
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<ProductCharacteristics
|
|
183
|
+
productId={productId}
|
|
184
|
+
variantId={variantId}
|
|
185
|
+
render={this.renderer}
|
|
186
|
+
conditioner={conditioner}
|
|
187
|
+
finishTimeout={200}
|
|
188
|
+
/>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export default withProductContext(MyComponent);
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### withThemeComponents
|
|
196
|
+
Connects provided component with some components provided by the Theme.
|
|
197
|
+
Theme components are theme specific - maintain functionality with theme specific UI.
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
#### Props provided
|
|
201
|
+
- `AppBar (ReactComponent)` - <AppBar> Component from the Theme. It can be used to add our default header to your custom page.
|
|
202
|
+
- `Drawer (ReactComponent)` - <Drawer> Component from the Theme. It is intended to be used to provide an additional UI layer to a page.
|
|
203
|
+
- `View (ReactComponent)` - <View> Component from the Theme. Wrapper if you want to create a custom page.
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
#### Example usage
|
|
207
|
+
```jsx
|
|
208
|
+
import React from 'react';
|
|
209
|
+
import { withThemeComponents } from '@shopgate/pwa-extension-kit/connectors';
|
|
210
|
+
|
|
211
|
+
const MyComponent = ({ AppBar, View }) => {
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<View>
|
|
215
|
+
<AppBar>
|
|
216
|
+
<h1>My custom page</h1>
|
|
217
|
+
</View>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export default withThemeComponents(MyComponent);
|
|
222
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import withPageProductId from './withPageProductId';
|
|
2
|
+
import withPageState from './withPageState';
|
|
3
|
+
import withHistoryActions from './withHistoryActions';
|
|
4
|
+
import withUser from './withUser';
|
|
5
|
+
import withThemeComponents from './withThemeComponents';
|
|
6
|
+
import withProductContext from './withProductContext';
|
|
7
|
+
|
|
8
|
+
export { withHistoryActions };
|
|
9
|
+
export { withPageProductId };
|
|
10
|
+
export { withPageState };
|
|
11
|
+
export { withUser };
|
|
12
|
+
export { withThemeComponents };
|
|
13
|
+
export { withProductContext };
|
|
14
|
+
|
|
15
|
+
export default {
|
|
16
|
+
withHistoryActions,
|
|
17
|
+
withPageProductId,
|
|
18
|
+
withPageState,
|
|
19
|
+
withUser,
|
|
20
|
+
withThemeComponents,
|
|
21
|
+
withProductContext,
|
|
22
|
+
};
|
|
23
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import connectors, {
|
|
2
|
+
withPageProductId,
|
|
3
|
+
withHistoryActions,
|
|
4
|
+
withPageState,
|
|
5
|
+
withThemeComponents,
|
|
6
|
+
withProductContext,
|
|
7
|
+
withUser,
|
|
8
|
+
} from './index';
|
|
9
|
+
|
|
10
|
+
describe('data/connectors', () => {
|
|
11
|
+
it('should export all functions as default', () => {
|
|
12
|
+
expect(typeof connectors).toBe('object');
|
|
13
|
+
expect(typeof connectors.withHistoryActions).toBe('function');
|
|
14
|
+
expect(typeof connectors.withPageProductId).toBe('function');
|
|
15
|
+
expect(typeof connectors.withPageState).toBe('function');
|
|
16
|
+
expect(typeof connectors.withThemeComponents).toBe('function');
|
|
17
|
+
expect(typeof connectors.withProductContext).toBe('function');
|
|
18
|
+
expect(typeof connectors.withUser).toBe('function');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should export withPageProductId as named export', () => {
|
|
22
|
+
expect(typeof withHistoryActions).toBe('function');
|
|
23
|
+
expect(typeof withPageProductId).toBe('function');
|
|
24
|
+
expect(typeof withPageState).toBe('function');
|
|
25
|
+
expect(typeof withUser).toBe('function');
|
|
26
|
+
expect(typeof withThemeComponents).toBe('function');
|
|
27
|
+
expect(typeof withProductContext).toBe('function');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createSelector } from 'reselect';
|
|
2
|
+
import { getUserData } from '@shopgate/pwa-common/selectors/user';
|
|
3
|
+
|
|
4
|
+
export const getUserLastName = createSelector(
|
|
5
|
+
getUserData,
|
|
6
|
+
(data) => {
|
|
7
|
+
if (!data) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return data.lastName;
|
|
12
|
+
}
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export const getUserId = createSelector(
|
|
16
|
+
getUserData,
|
|
17
|
+
(data) =>{
|
|
18
|
+
if (!data) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return data.id;
|
|
23
|
+
}
|
|
24
|
+
)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React, { Component } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { connect } from 'react-redux';
|
|
4
|
+
import { historyPush, historyReplace, historyPop } from '@shopgate/pwa-common/actions/router';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Exports Wrapped component
|
|
8
|
+
*/
|
|
9
|
+
class WithHistoryActions extends Component {
|
|
10
|
+
static propTypes = {
|
|
11
|
+
historyPop: PropTypes.func.isRequired,
|
|
12
|
+
historyPush: PropTypes.func.isRequired,
|
|
13
|
+
historyReplace: PropTypes.func.isRequired,
|
|
14
|
+
WrappedComponent: PropTypes.func.isRequired,
|
|
15
|
+
otherProps: PropTypes.shape({}),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
static defaultProps = {
|
|
19
|
+
otherProps: {},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Handles historyPush
|
|
24
|
+
* @param {string} pathname pathname
|
|
25
|
+
* @param {Object} options passed options
|
|
26
|
+
*/
|
|
27
|
+
handlePush = (pathname, options) => {
|
|
28
|
+
this.props.historyPush({
|
|
29
|
+
pathname,
|
|
30
|
+
...options,
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Handles historyReplace
|
|
36
|
+
* @param {string} pathname pathname
|
|
37
|
+
* @param {Object} options passed options
|
|
38
|
+
*/
|
|
39
|
+
handleReplace = (pathname, options) => {
|
|
40
|
+
this.props.historyReplace({
|
|
41
|
+
pathname,
|
|
42
|
+
...options,
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
handlePop = () => {
|
|
47
|
+
this.props.historyPop();
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @inheritDoc
|
|
52
|
+
*/
|
|
53
|
+
render() {
|
|
54
|
+
const { WrappedComponent, otherProps } = this.props;
|
|
55
|
+
return (
|
|
56
|
+
<WrappedComponent
|
|
57
|
+
historyPop={this.handlePop}
|
|
58
|
+
historyPush={this.handlePush}
|
|
59
|
+
historyReplace={this.handleReplace}
|
|
60
|
+
{...otherProps}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const mapDispatchToProps = {
|
|
67
|
+
historyPush,
|
|
68
|
+
historyPop,
|
|
69
|
+
historyReplace,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const Connected = connect(null, mapDispatchToProps)(WithHistoryActions);
|
|
73
|
+
|
|
74
|
+
export default WrappedComponent => props =>
|
|
75
|
+
<Connected WrappedComponent={WrappedComponent} otherProps={props} />;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { mount } from 'enzyme';
|
|
3
|
+
import withHistoryActions from './withHistoryActions';
|
|
4
|
+
|
|
5
|
+
const mockedAction = jest.fn();
|
|
6
|
+
jest.mock('react-redux', () => ({
|
|
7
|
+
connect: () => Component => props => (
|
|
8
|
+
<Component
|
|
9
|
+
historyPush={(...args) => mockedAction('historyPush', ...args)}
|
|
10
|
+
historyPop={(...args) => mockedAction('historyPop', ...args)}
|
|
11
|
+
historyReplace={(...args) => mockedAction('historyReplace', ...args)}
|
|
12
|
+
{...props}
|
|
13
|
+
/>
|
|
14
|
+
),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
describe('connectors/withHistoryActions', () => {
|
|
18
|
+
// eslint-disable-next-line react/prop-types, require-jsdoc
|
|
19
|
+
const TestedComponent = props => <div>Other prop: {props.foo}</div>;
|
|
20
|
+
const ConnectedComponent = withHistoryActions(TestedComponent);
|
|
21
|
+
let component;
|
|
22
|
+
let props;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
jest.clearAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should render component with specified props', () => {
|
|
29
|
+
component = mount(<ConnectedComponent foo="bar" />);
|
|
30
|
+
props = component.find('TestedComponent').props();
|
|
31
|
+
|
|
32
|
+
expect(typeof props.historyPop).toBe('function');
|
|
33
|
+
expect(typeof props.historyPush).toBe('function');
|
|
34
|
+
expect(typeof props.historyReplace).toBe('function');
|
|
35
|
+
expect(props.foo).toBe('bar');
|
|
36
|
+
|
|
37
|
+
expect(mockedAction).not.toHaveBeenCalled();
|
|
38
|
+
});
|
|
39
|
+
describe('Actions', () => {
|
|
40
|
+
const actions = ['historyPush', 'historyPop', 'historyReplace'];
|
|
41
|
+
actions.forEach((action) => {
|
|
42
|
+
it(`should call ${action}`, () => {
|
|
43
|
+
const pathname = 'PATHNAME';
|
|
44
|
+
if (action === 'historyPop') {
|
|
45
|
+
props[action]();
|
|
46
|
+
expect(mockedAction).toHaveBeenCalledWith(action);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
props[action](pathname);
|
|
50
|
+
expect(mockedAction).toHaveBeenCalledWith(action, {
|
|
51
|
+
pathname,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
it(`should call ${action} with options`, () => {
|
|
55
|
+
const pathname = 'PATHNAME';
|
|
56
|
+
const options = {
|
|
57
|
+
state: {},
|
|
58
|
+
};
|
|
59
|
+
if (action === 'historyPop') {
|
|
60
|
+
props[action]();
|
|
61
|
+
expect(mockedAction).toHaveBeenCalledWith(action);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
props[action](pathname, options);
|
|
65
|
+
expect(mockedAction).toHaveBeenCalledWith(action, {
|
|
66
|
+
pathname,
|
|
67
|
+
...options,
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React, { Component } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { RouteContext } from '@shopgate/pwa-common/context';
|
|
4
|
+
import { hex2bin } from '@shopgate/pwa-common/helpers/data';
|
|
5
|
+
import { TaggedLogger } from '../helpers';
|
|
6
|
+
|
|
7
|
+
const logger = new TaggedLogger('withPageProductId');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Reads productId from params and tries to hex2bin decode it.
|
|
11
|
+
* @param {Object} params Params.
|
|
12
|
+
* @returns {string|null|false}
|
|
13
|
+
*/
|
|
14
|
+
function decodeProductIdFromParams(params) {
|
|
15
|
+
let decodedProductId = null;
|
|
16
|
+
if (typeof params.productId === 'undefined') {
|
|
17
|
+
const message = 'Connector is probably rendered outside of page containing "productId" pattern param. Please check documentation for more information: https://github.com/shopgate-professional-services/pwa-extension-kit/blob/master/src/data/connectors/README.md#withPageProductId';
|
|
18
|
+
logger.error(message);
|
|
19
|
+
|
|
20
|
+
return decodedProductId;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
decodedProductId = hex2bin(params.productId);
|
|
24
|
+
|
|
25
|
+
if (params.productId && !decodedProductId) {
|
|
26
|
+
logger.warn('Wrapping with empty productId. Possibly productId used in a pathname is not bin2hex encoded.');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return decodedProductId;
|
|
30
|
+
}
|
|
31
|
+
// eslint-disable-next-line react/prefer-stateless-function, require-jsdoc
|
|
32
|
+
class WithPageProductId extends Component {
|
|
33
|
+
static propTypes = {
|
|
34
|
+
WrappedComponent: PropTypes.func.isRequired,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @inheritDoc
|
|
39
|
+
*/
|
|
40
|
+
render() {
|
|
41
|
+
const { WrappedComponent, ...otherProps } = this.props;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<RouteContext.Consumer>
|
|
45
|
+
{ ({ params }) =>
|
|
46
|
+
<WrappedComponent productId={decodeProductIdFromParams(params)} {...otherProps} />
|
|
47
|
+
}
|
|
48
|
+
</RouteContext.Consumer>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Returns a Wrapped Component with automatic props.productId read from RouteContext.
|
|
55
|
+
* @param {Function} WrappedComponent Component which will be wrapped with data connector.
|
|
56
|
+
* @returns {Function} React component.
|
|
57
|
+
*/
|
|
58
|
+
const withPageProductId = WrappedComponent => props =>
|
|
59
|
+
<WithPageProductId WrappedComponent={WrappedComponent} {...props} />;
|
|
60
|
+
|
|
61
|
+
export default withPageProductId;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { mount } from 'enzyme';
|
|
3
|
+
import withPageProductId from './withPageProductId';
|
|
4
|
+
|
|
5
|
+
let mockedProductId;
|
|
6
|
+
|
|
7
|
+
const mockedLogger = jest.fn();
|
|
8
|
+
jest.mock('../helpers/TaggedLogger', () => class MockedTaggedLogger {
|
|
9
|
+
// eslint-disable-next-line max-len
|
|
10
|
+
// eslint-disable-next-line class-methods-use-this, require-jsdoc, extra-rules/potential-point-free
|
|
11
|
+
warn(...args) {
|
|
12
|
+
mockedLogger(...args);
|
|
13
|
+
}
|
|
14
|
+
// eslint-disable-next-line max-len
|
|
15
|
+
// eslint-disable-next-line class-methods-use-this, require-jsdoc, extra-rules/potential-point-free
|
|
16
|
+
error(...args) {
|
|
17
|
+
mockedLogger(...args);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
jest.mock('@shopgate/pwa-common/context', () => ({
|
|
22
|
+
RouteContext: {
|
|
23
|
+
Consumer: (props) => {
|
|
24
|
+
// eslint-disable-next-line react/prop-types
|
|
25
|
+
const Child = props.children;
|
|
26
|
+
return <Child params={{ productId: mockedProductId }} />;
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
describe('connectors/withPageProductId', () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
jest.clearAllMocks();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// eslint-disable-next-line react/prop-types, require-jsdoc
|
|
37
|
+
const MockedComponent = props => <div>{props.productId}</div>;
|
|
38
|
+
|
|
39
|
+
it('should render with productId', () => {
|
|
40
|
+
mockedProductId = '31323334';
|
|
41
|
+
const Component = withPageProductId(MockedComponent);
|
|
42
|
+
|
|
43
|
+
const component = mount(<Component otherProp={1} />);
|
|
44
|
+
expect(component.find('MockedComponent').props()).toEqual({
|
|
45
|
+
productId: '1234',
|
|
46
|
+
otherProp: 1,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should render with missing productId', () => {
|
|
51
|
+
mockedProductId = undefined;
|
|
52
|
+
const Component = withPageProductId(MockedComponent);
|
|
53
|
+
|
|
54
|
+
const component = mount(<Component otherProp={1} />);
|
|
55
|
+
expect(component.find('MockedComponent').props()).toEqual({
|
|
56
|
+
productId: null,
|
|
57
|
+
otherProp: 1,
|
|
58
|
+
});
|
|
59
|
+
expect(mockedLogger).toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should render with invalid productId', () => {
|
|
63
|
+
mockedProductId = '123';
|
|
64
|
+
const Component = withPageProductId(MockedComponent);
|
|
65
|
+
|
|
66
|
+
const component = mount(<Component otherProp={1} />);
|
|
67
|
+
expect(component.find('MockedComponent').props()).toEqual({
|
|
68
|
+
productId: false,
|
|
69
|
+
otherProp: 1,
|
|
70
|
+
});
|
|
71
|
+
expect(mockedLogger).toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React, { Component } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { LoadingContext } from '@shopgate/pwa-common/providers/';
|
|
4
|
+
import { RouteContext } from '@shopgate/pwa-common/context';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* WithPageState component.
|
|
8
|
+
*/
|
|
9
|
+
// eslint-disable-next-line react/prefer-stateless-function, require-jsdoc
|
|
10
|
+
class WithPageState extends Component {
|
|
11
|
+
static propTypes = {
|
|
12
|
+
WrappedComponent: PropTypes.func.isRequired,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @inheritDoc
|
|
17
|
+
*/
|
|
18
|
+
render() {
|
|
19
|
+
const { WrappedComponent, ...otherProps } = this.props;
|
|
20
|
+
return (
|
|
21
|
+
<RouteContext.Consumer>
|
|
22
|
+
{({
|
|
23
|
+
visible,
|
|
24
|
+
pathname,
|
|
25
|
+
pattern,
|
|
26
|
+
location,
|
|
27
|
+
state,
|
|
28
|
+
}) => (
|
|
29
|
+
<LoadingContext.Consumer>
|
|
30
|
+
{({ isLoading }) => (
|
|
31
|
+
<WrappedComponent
|
|
32
|
+
isLoading={isLoading(pathname)}
|
|
33
|
+
isVisible={visible}
|
|
34
|
+
pathname={pathname}
|
|
35
|
+
pattern={pattern}
|
|
36
|
+
location={location}
|
|
37
|
+
state={state}
|
|
38
|
+
{...otherProps}
|
|
39
|
+
/>
|
|
40
|
+
)}
|
|
41
|
+
</LoadingContext.Consumer>
|
|
42
|
+
)}
|
|
43
|
+
</RouteContext.Consumer>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default WrappedComponent => props =>
|
|
49
|
+
<WithPageState WrappedComponent={WrappedComponent} {...props} />;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { mount } from 'enzyme';
|
|
3
|
+
import withPageState from './withPageState';
|
|
4
|
+
|
|
5
|
+
const isLoadingSpy = jest.fn();
|
|
6
|
+
// eslint-disable-next-line require-jsdoc
|
|
7
|
+
const mockedIsLoading = (...args) => {
|
|
8
|
+
isLoadingSpy(...args);
|
|
9
|
+
return true;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// eslint-disable-next-line react/prop-types, require-jsdoc
|
|
13
|
+
const TestingComponent = props => <div>Other prop: {props.foo}</div>;
|
|
14
|
+
|
|
15
|
+
jest.mock('@shopgate/pwa-common/providers/', () => ({
|
|
16
|
+
LoadingContext: {
|
|
17
|
+
// eslint-disable-next-line react/prop-types
|
|
18
|
+
Consumer: ({ children, ...otherProps }) => {
|
|
19
|
+
const Child = children;
|
|
20
|
+
return <Child isLoading={mockedIsLoading} {...otherProps} />;
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
jest.mock('@shopgate/pwa-common/context', () => ({
|
|
26
|
+
RouteContext: {
|
|
27
|
+
// eslint-disable-next-line react/prop-types
|
|
28
|
+
Consumer: ({ children, ...otherProps }) => {
|
|
29
|
+
const Child = children;
|
|
30
|
+
return (
|
|
31
|
+
<Child
|
|
32
|
+
pathname="/foo/bar"
|
|
33
|
+
pattern="/foo/:id"
|
|
34
|
+
location="/foo/bar?foo=bar"
|
|
35
|
+
visible
|
|
36
|
+
state={{ title: 'foo' }}
|
|
37
|
+
{...otherProps}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
describe('connectors/withPageState', () => {
|
|
45
|
+
it('should render with specified props', () => {
|
|
46
|
+
const ConnectedComponent = withPageState(TestingComponent);
|
|
47
|
+
const component = mount(<ConnectedComponent foo="bar" />);
|
|
48
|
+
expect(isLoadingSpy).toHaveBeenCalledWith('/foo/bar');
|
|
49
|
+
expect(component.find('TestingComponent').props()).toEqual({
|
|
50
|
+
isVisible: true,
|
|
51
|
+
isLoading: true,
|
|
52
|
+
foo: 'bar',
|
|
53
|
+
location: '/foo/bar?foo=bar',
|
|
54
|
+
pattern: '/foo/:id',
|
|
55
|
+
pathname: '/foo/bar',
|
|
56
|
+
state: { title: 'foo' },
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|