wrapito 0.0.0 → 1.3.0-beta

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Mercadona
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,20 +1,48 @@
1
1
  # 🌯 wrapito
2
+
2
3
  Wrap you tests so that you can test both behaviour and components with less effort.
3
4
 
5
+ ## IMPORTANT
6
+
7
+ This version is agnostic and compatible with both [jest](https://jestjs.io/) and [vitest](https://vitest.dev/).
8
+
9
+ ### Note:
10
+
11
+ From the version 13 wrapito is compatible with the new version of React and requires such versions of dependencies:
12
+
13
+ ```
14
+ "peerDependencies": {
15
+ "@testing-library/jest-dom": ">=5.16.4",
16
+ "@testing-library/react": ">=14.0.0",
17
+ "react-dom": ">=18.0.0",
18
+ "react": ">=18.0.0"
19
+ }
20
+ ```
21
+
22
+ If your project uses React <=18.0.0, you can use wrapito <=12, but we extremely recommend to migrate to newest versions, because we are not maintaining legacy dependencies.
23
+
4
24
  ## 🎯 Motivation
5
- As we are more focused on user interactions than implementation details. In order to test all the user interactions that can be done at a certain point of our app, the upper the component we render in the test, the better.
25
+
26
+ As we are more focused on user interactions than implementation details. In order to test all the user interactions that
27
+ can be done at a certain point of our app, the upper the component we render in the test, the better.
6
28
 
7
29
  ## 💡 The idea
30
+
8
31
  As we test our app we will be in two different scenarios where:
9
- - We will need to test that the user interactions cause the proper side effects such as making http calls or refreshing the UI.
10
- - In case we have a components library, we will need to test these by passing the needed props and checking that it returns (renders) the expected result.
32
+
33
+ - We will need to test that the user interactions cause the proper side effects such as making http calls or refreshing
34
+ the UI.
35
+ - In case we have a components library, we will need to test these by passing the needed props and checking that it
36
+ returns (renders) the expected result.
11
37
 
12
38
  In general, if you want to test behaviour, you need to simulate external actions from user or from http responses.
13
- Most of the existing testing libraries give you control of the user actions and thats why we just ask you to set in the config what is the `render` function of your testing library.
14
39
  Unfortunately, there aren't so many options when it comes to manage http requests and responses in the tests.
15
- To give the mounted component context about which path is the current path where the app should be mounted, what props does the component receive, what http requests will respond with which results or where should the portal be mounted we have used the builder pattern that makes tests much more semantic.
40
+ To give the mounted component context about which path is the current path where the app should be mounted, what props
41
+ does the component receive, what http requests will respond with which results or where should the portal be mounted we
42
+ have used the builder pattern that makes tests much more semantic.
16
43
 
17
44
  ## 🔧 Installing
45
+
18
46
  Using npm:
19
47
 
20
48
  ```sh
@@ -22,6 +50,7 @@ $ npm install wrapito
22
50
  ```
23
51
 
24
52
  ## 👩‍💻 Basic usage
53
+
25
54
  ```js
26
55
  const MyComponent = () => <span>Just a component</span>
27
56
 
@@ -29,45 +58,43 @@ const myWrappedComponent = wrap(MyComponent).mount()
29
58
  ```
30
59
 
31
60
  ## 👣 Initial setup
32
- Because 🌯 `wrapito` doesn't want to know anything about how the components are mounted in the project that uses it, we can specify how we will `mount` our components by passing the rendering/mounting function of our library of preference. This way we make `wrapito` a little bit more agnostic. For example `setup.wrapito.js`
33
61
 
34
- ```js
35
- import { render } from '@testing-library/react'
36
- import { configure } from 'wrapito'
62
+ In the latest version of 🌯 `wrapito` passing the rendering/mounting function is optional, because we use `render` from `@testing-library/react` by default.
37
63
 
38
- configure({
39
- mount: render,
40
- })
41
- ```
64
+ If one or more of your components use a `react portal` in any way, you will need to specify the `id` of the node where
65
+ it will be added.
42
66
 
43
- and add the previous file in `jest.config.json`
67
+ To configure wrapito we recommend adding a setupTests.tsx file and adding there all your custom configs and extensions.
44
68
 
45
69
  ```js
46
- "setupFiles": [
47
- "<rootDir>/config/jest/setup.wrapito.js"
48
- ],
49
- ```
50
-
51
- If one or more of your components use a `react portal` in any way, you will need to specify the `id` of the node where it will be added:
52
-
53
- ```js
54
- import { render } from '@testing-library/react'
55
70
  import { configure } from 'wrapito'
56
71
 
57
72
  configure({
58
- mount: render,
73
+ defaultHost: 'your-host-path',
59
74
  portal: 'modal-root',
75
+ extend: {
76
+ /* Here you can group network calls to reuse them in your tests */
77
+ },
60
78
  })
61
79
  ```
62
80
 
81
+ Add this line in your project setup (vite/cra):
82
+
83
+ ```js
84
+ setupFiles: ['./src/setupTests.tsx']
85
+ ```
86
+
63
87
  ## 🏰 Builder API
64
88
 
65
89
  #### withMocks (Deprecated)
66
90
 
67
- It has the same API than the withNetwork builder. The main difference between them is that withMocks will fail if a given request, done by the production code, is not set up in the `responses object`.
91
+ It has the same API than the withNetwork builder. The main difference between them is that withMocks will fail if a
92
+ given request, done by the production code, is not set up in the `responses object`.
68
93
 
69
94
  #### withNetwork
70
- By using this feature you can configure the responses for your `http requests`. If your component is making a request that is not set up in the `responses object`, it will not be validated and it will return an empty response with code 200.
95
+
96
+ By using this feature you can configure the responses for your `http requests`. If your component is making a request
97
+ that is not set up in the `responses object`, it will not be validated and it will return an empty response with code 200.
71
98
 
72
99
  ```js
73
100
  import { wrap } from 'wrapito'
@@ -75,26 +102,28 @@ import { wrap } from 'wrapito'
75
102
  const responses = {
76
103
  host: 'my-host',
77
104
  method: 'get',
78
- path: '/path/to/get/a/single/product/,
105
+ path: '/path/to/get/a/single/product/',
79
106
  responseBody: { id: 1, name: 'hummus' },
80
107
  status: 200,
81
108
  catchParams: true,
82
109
  delay: 500,
83
110
  }
84
111
 
85
- wrap(MyComponent)
86
- .withNetwork(responses)
87
- .mount()
112
+ wrap(MyComponent).withNetwork(responses).mount()
88
113
  ```
89
114
 
90
115
  You can specify the default `host` via configuration:
116
+
91
117
  ```js
92
118
  import { configure } from 'wrapito'
93
119
 
94
120
  const { API_HOST, API_VERSION } = process.env
95
- configure({ defaultHost: `${ API_HOST }${ API_VERSION }` })
121
+ configure({ defaultHost: `${API_HOST}${API_VERSION}` })
96
122
  ```
97
- In addition, `wrapito` defaults the `method` to `'get'` and `status` to `200`. This means one can use `withNetwork` like this:
123
+
124
+ In addition, `wrapito` defaults the `method` to `'get'` and `status` to `200`. This means one can use `withNetwork` like
125
+ this:
126
+
98
127
  ```js
99
128
  import { wrap } from 'wrapito'
100
129
 
@@ -107,30 +136,33 @@ wrap(MyComponent)
107
136
  .withNetwork(responses)
108
137
  .mount()
109
138
  ```
110
- Now, you might need to mock several `http responses` at the same time and that's why you can also pass an array of responses instead if you wish:
139
+
140
+ Now, you might need to mock several `http responses` at the same time and that's why you can also pass an array of
141
+ responses instead if you wish:
142
+
111
143
  ```js
112
144
  import { wrap } from 'wrapito'
113
145
 
114
146
  const responses = [
115
147
  {
116
- path: '/path/to/get/the/products/list/,
148
+ path: '/path/to/get/the/products/list/',
117
149
  responseBody: [
118
150
  { id: 1, name: 'hummus' },
119
151
  { id: 2, name: 'guacamole' },
120
- ]
152
+ ],
121
153
  },
122
154
  {
123
- path: '/path/to/get/a/single/product/,
155
+ path: '/path/to/get/a/single/product/',
124
156
  responseBody: { id: 1, name: 'hummus' },
125
157
  },
126
158
  ]
127
159
 
128
- wrap(MyComponent)
129
- .withNetwork(responses)
130
- .mount()
160
+ wrap(MyComponent).withNetwork(responses).mount()
131
161
  ```
132
162
 
133
- There might be cases where one request is called several times and we want it to return different responses. An example of this could be an app that shows a list of products that may be updated over time and for this propose the app has a refresh button that will request the list again in order to update its content.
163
+ There might be cases where one request is called several times and we want it to return different responses. An example
164
+ of this could be an app that shows a list of products that may be updated over time and for this propose the app has a
165
+ refresh button that will request the list again in order to update its content.
134
166
 
135
167
  Well, it can be solved by specifying the response as multiple using `multipleResponse` as follows:
136
168
 
@@ -138,31 +170,38 @@ Well, it can be solved by specifying the response as multiple using `multipleRes
138
170
  const responses = {
139
171
  path: '/path/to/get/the/products/list/,
140
172
  multipleResponses: [
141
- {
142
- responseBody: [
143
- { id: 1, name: 'hummus' },
144
- { id: 2, name: 'guacamole' },
145
- ]
146
- },
147
- {
148
- responseBody: [
149
- { id: 1, name: 'hummus' },
150
- { id: 2, name: 'guacamole' },
151
- { id: 3, name: 'peanut butter' },
152
- ]
153
- },
173
+ {
174
+ responseBody: [
175
+ { id: 1, name: 'hummus' },
176
+ { id: 2, name: 'guacamole' },
177
+ ]
178
+ },
179
+ {
180
+ responseBody: [
181
+ { id: 1, name: 'hummus' },
182
+ { id: 2, name: 'guacamole' },
183
+ { id: 3, name: 'peanut butter' },
184
+ ]
185
+ },
154
186
  ],
155
187
  }
156
188
  ```
157
189
 
158
- `multipleResponses` receives an array of responses where one set the `responseBody`, `status` or `headers` for every response.
190
+ `multipleResponses` receives an array of responses where one set the `responseBody`, `status` or `headers` for every
191
+ response.
159
192
 
160
- When `multipleResponses` is present, 🌯 `wrapito` will ignore the `responseBody` at the root of the mock and will return one response per request made at the same time that sets the returned response as `hasBeenReturned`, which means it can be returned again, until all the array of responses is returned. In that case an exception will be raised.
193
+ When `multipleResponses` is present, 🌯 `wrapito` will ignore the `responseBody` at the root of the mock and will return
194
+ one response per request made at the same time that sets the returned response as `hasBeenReturned`, which means it can
195
+ be returned again, until all the array of responses is returned. In that case an exception will be raised.
161
196
 
162
- This behaviour differs from using a single response for a given request as single response for a given request will return the response no matter how many times the request is called.
197
+ This behaviour differs from using a single response for a given request as single response for a given request will
198
+ return the response no matter how many times the request is called.
163
199
 
164
200
  #### atPath
165
- When mounting the whole app, it will be done at the default route passing the default path. But a specific route might need to be mounted and for that reason a path to match that route should be pass here.
201
+
202
+ When mounting the whole app, it will be done at the default route passing the default path. But a specific route might
203
+ need to be mounted and for that reason a path to match that route should be pass here.
204
+
166
205
  ```js
167
206
  import { wrap } from 'wrapito'
168
207
 
@@ -171,20 +210,36 @@ wrap(MyComponent)
171
210
  .mount()
172
211
  ```
173
212
 
213
+ By default it will use the native javascript history API, but you can provide a method to be called for change the app
214
+ route with [`changeRoute`](#changeRoute):
215
+
216
+ ```js
217
+ import { configure } from 'wrapito'
218
+ import { history } from 'app.js'
219
+
220
+ configure({
221
+ ..configuration,
222
+ changeRoute: (route) => history.push(route)
223
+ })
224
+ ```
225
+
174
226
  #### withProps
227
+
175
228
  Pass down the props to the wrapped component:
229
+
176
230
  ```js
177
231
  import { wrap } from 'wrapito'
178
232
 
179
233
  const props = { message: 'MyComponent will receive this as prop' }
180
234
 
181
- wrap(MyComponent)
182
- .withProps(props)
183
- .mount()
235
+ wrap(MyComponent).withProps(props).mount()
184
236
  ```
185
237
 
186
238
  #### composing
187
- As it is basically a builder, all the above functions can be used at the same time and these will be composed underneath:
239
+
240
+ As it is basically a builder, all the above functions can be used at the same time and these will be composed
241
+ underneath:
242
+
188
243
  ```js
189
244
  import { wrap } from 'wrapito'
190
245
 
@@ -202,25 +257,26 @@ wrap(PreparationContainer)
202
257
  ```
203
258
 
204
259
  ## ✨ Utils
260
+
205
261
  #### toHaveBeenFetched
206
- Some times checking only the visual side effects in the UI it's not enough. In the case that we want to check if a particular network side effect is happening, this assertion will come in handy.
262
+
263
+ Some times checking only the visual side effects in the UI it's not enough. In the case that we want to check if a
264
+ particular network side effect is happening, this assertion will come in handy.
207
265
 
208
266
  ```js
209
- wrap(MyComponentMakingHttpCalls)
210
- .withNetwork(responses)
211
- .mount()
267
+ wrap(MyComponentMakingHttpCalls).withNetwork(responses).mount()
212
268
 
213
- expect('/some/path').toHaveBeenFetched()
269
+ expect('/some/path').toHaveBeenFetched()
214
270
  ```
215
271
 
216
272
  #### toHaveBeenFetchedWith
273
+
217
274
  This assertion is an extension of `toHaveBeenFetched` but we will use it if we want to check the properties.
275
+
218
276
  ```js
219
277
  import { wrap, assertions } from 'wrapito'
220
278
 
221
- wrap(MyComponentMakingHttpCalls)
222
- .withNetwork(responses)
223
- .mount()
279
+ wrap(MyComponentMakingHttpCalls).withNetwork(responses).mount()
224
280
 
225
281
  expect('/some/path').toHaveBeenFetchedWith({
226
282
  method: 'POST',
@@ -229,19 +285,17 @@ expect('/some/path').toHaveBeenFetchedWith({
229
285
  ```
230
286
 
231
287
  #### toHaveBeenFetchedTimes
288
+
232
289
  This assertion is to check how many times an API url is called.
290
+
233
291
  ```js
234
292
  import { wrap, assertions } from 'wrapito'
235
293
 
236
294
  expect.extend(assertions)
237
295
 
238
- const responses = [
239
- { path: '/path/to/get/quantity/' },
240
- ]
296
+ const responses = [{ path: '/path/to/get/quantity/' }]
241
297
 
242
- wrap(MyComponentMakingHttpCalls)
243
- .withNetwork(responses)
244
- .mount()
298
+ wrap(MyComponentMakingHttpCalls).withNetwork(responses).mount()
245
299
 
246
300
  expect('/path/to/get/quantity/').toHaveBeenFetchedTimes(1)
247
301
  ```
@@ -250,6 +304,30 @@ expect('/path/to/get/quantity/').toHaveBeenFetchedTimes(1)
250
304
 
251
305
  In order to test the library in a project without releasing the library:
252
306
 
253
- - ```npm run build```
307
+ - `npm run build`
254
308
  - This will generate a local build in the `dist` folder
255
309
  - Copy the content of that folder in `node_modules/wrapito` in your project
310
+
311
+ ## Deploy new version in npm
312
+
313
+ You need to create a new tag for the project. E.g:
314
+
315
+ ```
316
+ git tag v1.0.5
317
+ git push origin v1.0.5
318
+ ```
319
+
320
+ This will run a workflow in github that will publish this version for you.
321
+
322
+ ### Release beta versions
323
+
324
+ WARNING: DO NOT MERGE YOUR PR IF YOU WANT TO DO A BETA RELEASE, SINCE THE CHANGES ARE NOT FULLY TRUSTED THEY SHOULD NOT GO TO MASTER
325
+
326
+ If you need to release beta versions to test things, you may do so with the -beta tag. E.g:
327
+
328
+ ```
329
+ git tag v1.0.5-beta1
330
+ git push origin v1.0.5-beta1
331
+ ```
332
+
333
+ This will run a workflow in github that will publish this version for you as a pre-release.
@@ -0,0 +1,111 @@
1
+ import React from 'react';
2
+ import { RenderResult as RenderResult$1 } from '@testing-library/react';
3
+
4
+ type HttpMethod = UpperCaseHttpMethod | Lowercase<UpperCaseHttpMethod>;
5
+ type UpperCaseHttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'CONNECT' | 'TRACE';
6
+ interface WrapRequest extends Partial<Request> {
7
+ _bodyInit?: string;
8
+ url: string;
9
+ }
10
+ interface RequestOptions {
11
+ host?: string;
12
+ body?: object | string;
13
+ method?: HttpMethod;
14
+ headers?: Record<string, string>;
15
+ }
16
+ interface WrapResponse extends Partial<Response> {
17
+ /** The call's path we want to mock */
18
+ path: string;
19
+ /** To which host the call corresponds to */
20
+ host?: string;
21
+ /** The HTTP method we should intercept, defaults to GET */
22
+ method?: HttpMethod;
23
+ /** The response for this call */
24
+ responseBody?: object | string;
25
+ /** The request to match with the responseBody for this call */
26
+ requestBody?: object;
27
+ /** Allows to return multiple response for a call */
28
+ multipleResponses?: Array<Partial<WrapResponse>>;
29
+ catchParams?: boolean;
30
+ delay?: number;
31
+ hasBeenReturned?: boolean;
32
+ }
33
+ type NetworkResponses = WrapResponse | WrapResponse[];
34
+ type DefaultUserLib = unknown;
35
+ type DefaultUserInstance = unknown;
36
+ type DefaultUserSetupOptions = unknown;
37
+ interface InteractionDescriptor<UserLib = DefaultUserLib, UserInstance = DefaultUserInstance, UserSetupOptions = DefaultUserSetupOptions> {
38
+ UserLib: UserLib;
39
+ UserInstance: UserInstance;
40
+ UserSetupOptions: UserSetupOptions;
41
+ }
42
+ interface Wrap<UserInteraction extends InteractionDescriptor = InteractionDescriptor> {
43
+ withNetwork: (responses?: NetworkResponses) => Wrap<UserInteraction>;
44
+ atPath: (path: string, historyState?: object) => Wrap<UserInteraction>;
45
+ withProps: (props: object) => Wrap<UserInteraction>;
46
+ withInteraction: (config: UserInteraction['UserSetupOptions']) => Wrap<UserInteraction>;
47
+ debugRequests: () => Wrap<UserInteraction>;
48
+ mount: () => RenderResult & {
49
+ user: UserInteraction['UserInstance'];
50
+ };
51
+ }
52
+ interface WrapOptions<SetupOptions = DefaultUserSetupOptions> {
53
+ Component: unknown;
54
+ responses: WrapResponse[];
55
+ props: object;
56
+ path: string;
57
+ historyState?: object;
58
+ hasPath: boolean;
59
+ debug: boolean;
60
+ interactionConfig?: SetupOptions;
61
+ }
62
+ interface WrapExtensionAPI {
63
+ addResponses: (responses: Array<WrapResponse>) => unknown;
64
+ }
65
+ type Extension<UserInteraction extends InteractionDescriptor = InteractionDescriptor> = <T>(extensionAPI: WrapExtensionAPI, args: T) => Wrap<UserInteraction>;
66
+ type Extensions<UserInteraction extends InteractionDescriptor = InteractionDescriptor> = {
67
+ [key: string]: Extension<UserInteraction>;
68
+ };
69
+ type Component = React.ReactElement<any, any>;
70
+ type RenderResult = RenderResult$1;
71
+ type Mount = (component: Component) => RenderResult;
72
+ interface InteractionOptions<UserInteraction extends InteractionDescriptor = InteractionDescriptor> {
73
+ userLib: UserInteraction['UserLib'];
74
+ setup?: (userLib: UserInteraction['UserLib'], options?: UserInteraction['UserSetupOptions']) => UserInteraction['UserInstance'];
75
+ }
76
+ interface Config<UserInteraction extends InteractionDescriptor = InteractionDescriptor> {
77
+ defaultHost: string;
78
+ mount: Mount;
79
+ extend: Extensions<UserInteraction>;
80
+ changeRoute: (path: string) => void;
81
+ history?: BrowserHistory;
82
+ portal?: string;
83
+ portals?: string[];
84
+ handleQueryParams?: boolean;
85
+ interaction?: InteractionOptions<UserInteraction>;
86
+ }
87
+ interface BrowserHistory extends History {
88
+ push: (path: string, historyState?: object) => void;
89
+ }
90
+
91
+ declare const wrap: (component: unknown) => Wrap;
92
+
93
+ declare function configure(newConfig: Partial<Config>): void;
94
+ declare const getConfig: () => Config;
95
+
96
+ declare const matchers: {
97
+ toHaveBeenFetched: (path: string, options?: RequestOptions) => {
98
+ pass: boolean;
99
+ message: () => string;
100
+ };
101
+ toHaveBeenFetchedWith: (path: string, options?: RequestOptions) => {
102
+ pass: boolean;
103
+ message: () => string;
104
+ };
105
+ toHaveBeenFetchedTimes: (path: string, expectedLength: number, options?: RequestOptions) => {
106
+ pass: boolean;
107
+ message: () => string;
108
+ };
109
+ };
110
+
111
+ export { type BrowserHistory, type Component, type Config, type DefaultUserInstance, type DefaultUserLib, type DefaultUserSetupOptions, type Extension, type Extensions, type HttpMethod, type InteractionDescriptor, type InteractionOptions, type Mount, type NetworkResponses, type RenderResult, type RequestOptions, type WrapResponse as Response, type Wrap, type WrapExtensionAPI, type WrapOptions, type WrapRequest, type WrapResponse, matchers as assertions, configure, getConfig, matchers, wrap };
@@ -0,0 +1,111 @@
1
+ import React from 'react';
2
+ import { RenderResult as RenderResult$1 } from '@testing-library/react';
3
+
4
+ type HttpMethod = UpperCaseHttpMethod | Lowercase<UpperCaseHttpMethod>;
5
+ type UpperCaseHttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'CONNECT' | 'TRACE';
6
+ interface WrapRequest extends Partial<Request> {
7
+ _bodyInit?: string;
8
+ url: string;
9
+ }
10
+ interface RequestOptions {
11
+ host?: string;
12
+ body?: object | string;
13
+ method?: HttpMethod;
14
+ headers?: Record<string, string>;
15
+ }
16
+ interface WrapResponse extends Partial<Response> {
17
+ /** The call's path we want to mock */
18
+ path: string;
19
+ /** To which host the call corresponds to */
20
+ host?: string;
21
+ /** The HTTP method we should intercept, defaults to GET */
22
+ method?: HttpMethod;
23
+ /** The response for this call */
24
+ responseBody?: object | string;
25
+ /** The request to match with the responseBody for this call */
26
+ requestBody?: object;
27
+ /** Allows to return multiple response for a call */
28
+ multipleResponses?: Array<Partial<WrapResponse>>;
29
+ catchParams?: boolean;
30
+ delay?: number;
31
+ hasBeenReturned?: boolean;
32
+ }
33
+ type NetworkResponses = WrapResponse | WrapResponse[];
34
+ type DefaultUserLib = unknown;
35
+ type DefaultUserInstance = unknown;
36
+ type DefaultUserSetupOptions = unknown;
37
+ interface InteractionDescriptor<UserLib = DefaultUserLib, UserInstance = DefaultUserInstance, UserSetupOptions = DefaultUserSetupOptions> {
38
+ UserLib: UserLib;
39
+ UserInstance: UserInstance;
40
+ UserSetupOptions: UserSetupOptions;
41
+ }
42
+ interface Wrap<UserInteraction extends InteractionDescriptor = InteractionDescriptor> {
43
+ withNetwork: (responses?: NetworkResponses) => Wrap<UserInteraction>;
44
+ atPath: (path: string, historyState?: object) => Wrap<UserInteraction>;
45
+ withProps: (props: object) => Wrap<UserInteraction>;
46
+ withInteraction: (config: UserInteraction['UserSetupOptions']) => Wrap<UserInteraction>;
47
+ debugRequests: () => Wrap<UserInteraction>;
48
+ mount: () => RenderResult & {
49
+ user: UserInteraction['UserInstance'];
50
+ };
51
+ }
52
+ interface WrapOptions<SetupOptions = DefaultUserSetupOptions> {
53
+ Component: unknown;
54
+ responses: WrapResponse[];
55
+ props: object;
56
+ path: string;
57
+ historyState?: object;
58
+ hasPath: boolean;
59
+ debug: boolean;
60
+ interactionConfig?: SetupOptions;
61
+ }
62
+ interface WrapExtensionAPI {
63
+ addResponses: (responses: Array<WrapResponse>) => unknown;
64
+ }
65
+ type Extension<UserInteraction extends InteractionDescriptor = InteractionDescriptor> = <T>(extensionAPI: WrapExtensionAPI, args: T) => Wrap<UserInteraction>;
66
+ type Extensions<UserInteraction extends InteractionDescriptor = InteractionDescriptor> = {
67
+ [key: string]: Extension<UserInteraction>;
68
+ };
69
+ type Component = React.ReactElement<any, any>;
70
+ type RenderResult = RenderResult$1;
71
+ type Mount = (component: Component) => RenderResult;
72
+ interface InteractionOptions<UserInteraction extends InteractionDescriptor = InteractionDescriptor> {
73
+ userLib: UserInteraction['UserLib'];
74
+ setup?: (userLib: UserInteraction['UserLib'], options?: UserInteraction['UserSetupOptions']) => UserInteraction['UserInstance'];
75
+ }
76
+ interface Config<UserInteraction extends InteractionDescriptor = InteractionDescriptor> {
77
+ defaultHost: string;
78
+ mount: Mount;
79
+ extend: Extensions<UserInteraction>;
80
+ changeRoute: (path: string) => void;
81
+ history?: BrowserHistory;
82
+ portal?: string;
83
+ portals?: string[];
84
+ handleQueryParams?: boolean;
85
+ interaction?: InteractionOptions<UserInteraction>;
86
+ }
87
+ interface BrowserHistory extends History {
88
+ push: (path: string, historyState?: object) => void;
89
+ }
90
+
91
+ declare const wrap: (component: unknown) => Wrap;
92
+
93
+ declare function configure(newConfig: Partial<Config>): void;
94
+ declare const getConfig: () => Config;
95
+
96
+ declare const matchers: {
97
+ toHaveBeenFetched: (path: string, options?: RequestOptions) => {
98
+ pass: boolean;
99
+ message: () => string;
100
+ };
101
+ toHaveBeenFetchedWith: (path: string, options?: RequestOptions) => {
102
+ pass: boolean;
103
+ message: () => string;
104
+ };
105
+ toHaveBeenFetchedTimes: (path: string, expectedLength: number, options?: RequestOptions) => {
106
+ pass: boolean;
107
+ message: () => string;
108
+ };
109
+ };
110
+
111
+ export { type BrowserHistory, type Component, type Config, type DefaultUserInstance, type DefaultUserLib, type DefaultUserSetupOptions, type Extension, type Extensions, type HttpMethod, type InteractionDescriptor, type InteractionOptions, type Mount, type NetworkResponses, type RenderResult, type RequestOptions, type WrapResponse as Response, type Wrap, type WrapExtensionAPI, type WrapOptions, type WrapRequest, type WrapResponse, matchers as assertions, configure, getConfig, matchers, wrap };
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ "use strict";var fe=Object.create;var E=Object.defineProperty;var ge=Object.getOwnPropertyDescriptor;var ye=Object.getOwnPropertyNames;var Re=Object.getPrototypeOf,be=Object.prototype.hasOwnProperty;var Ie=(e,t)=>{for(var n in t)E(e,n,{get:t[n],enumerable:!0})},B=(e,t,n,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of ye(t))!be.call(e,r)&&r!==n&&E(e,r,{get:()=>t[r],enumerable:!(s=ge(t,r))||s.enumerable});return e};var T=(e,t,n)=>(n=e!=null?fe(Re(e)):{},B(t||!e||!e.__esModule?E(n,"default",{value:e,enumerable:!0}):n,e)),we=e=>B(E({},"__esModule",{value:!0}),e);var Ze={};Ie(Ze,{assertions:()=>Xe,configure:()=>xe,getConfig:()=>d,matchers:()=>Xe,wrap:()=>De});module.exports=we(Ze);var Y=T(require("react"),1);var b=T(require("chalk"),1);var C=T(require("object-hash"),1);var $=require("@testing-library/react"),W={defaultHost:"",extend:{},mount:$.render,changeRoute:e=>window.history.replaceState(null,"",e)};function xe(e){W={...W,...e}}var d=()=>({...W});var L=e=>t=>{let{method:n="GET",path:s,host:r=d().defaultHost,requestBody:o=void 0,catchParams:i}=t,l=r+s,c=!d().handleQueryParams||i,u=(0,C.default)({url:c?l:l.split("?")[0],method:n.toUpperCase(),requestBody:o}),a;return"_bodyInit"in e&&e._bodyInit!==void 0&&(a=JSON.parse(e._bodyInit)),(0,C.default)({url:c?e.url:e.url.split("?")[0],method:e.method,requestBody:a})===u};function q(e,t){if(!e)throw new Error(t)}function w(e,t){return typeof t===e}function ke(e){return e instanceof Promise}function D(e,t,n){Object.defineProperty(e,t,n)}function x(e,t,n){Object.defineProperty(e,t,{value:n})}var M=Symbol.for("tinyspy:spy"),Me=new Set,Oe=e=>{e.called=!1,e.callCount=0,e.calls=[],e.results=[],e.next=[]},Ee=e=>(D(e,M,{value:{reset:()=>Oe(e[M])}}),e[M]),P=e=>e[M]||Ee(e);function Te(e){q(w("function",e)||w("undefined",e),"cannot spy on a non-function value");let t=function(...s){let r=P(t);r.called=!0,r.callCount++,r.calls.push(s);let o=r.next.shift();if(o){r.results.push(o);let[u,a]=o;if(u==="ok")return a;throw a}let i,l="ok";if(r.impl)try{new.target?i=Reflect.construct(r.impl,s,new.target):i=r.impl.apply(this,s),l="ok"}catch(u){throw i=u,l="error",r.results.push([l,u]),u}let c=[l,i];if(ke(i)){let u=i.then(a=>c[1]=a).catch(a=>{throw c[0]="error",c[1]=a,a});Object.assign(u,i),i=u}return r.results.push(c),i};x(t,"_isMockFunction",!0),x(t,"length",e?e.length:0),x(t,"name",e&&e.name||"spy");let n=P(t);return n.reset(),n.impl=e,t}var F=(e,t)=>Object.getOwnPropertyDescriptor(e,t),_=(e,t)=>{t!=null&&typeof t=="function"&&t.prototype!=null&&Object.setPrototypeOf(e.prototype,t.prototype)};function G(e,t,n){q(!w("undefined",e),"spyOn could not find an object to spy upon"),q(w("object",e)||w("function",e),"cannot spyOn on a primitive value");let[s,r]=(()=>{if(!w("object",t))return[t,"value"];if("getter"in t&&"setter"in t)throw new Error("cannot spy on both getter and setter");if("getter"in t)return[t.getter,"get"];if("setter"in t)return[t.setter,"set"];throw new Error("specify getter or setter to spy on")})(),o=F(e,s),i=Object.getPrototypeOf(e),l=i&&F(i,s),c=o||l;q(c||s in e,`${String(s)} does not exist`);let u=!1;r==="value"&&c&&!c.value&&c.get&&(r="get",u=!0,n=c.get());let a;c?a=c[r]:r!=="value"?a=()=>e[s]:a=e[s],n||(n=a);let m=Te(n);r==="value"&&_(m,a);let p=f=>{let{value:k,...v}=c||{configurable:!0,writable:!0};r!=="value"&&delete v.writable,v[r]=f,D(e,s,v)},g=()=>c?D(e,s,c):p(a),h=m[M];return x(h,"restore",g),x(h,"getOriginal",()=>u?a():a),x(h,"willCall",f=>(h.impl=f,m)),p(u?()=>(_(m,n),m):m),Me.add(m),m}var Pe=new Set,Ue=0;function Ae(e){let t=e,n,s=[],r=[],o=P(e),i={get calls(){return o.calls},get instances(){return s},get invocationCallOrder(){return r},get results(){return o.results.map(([p,g])=>({type:p==="error"?"throw":"return",value:g}))},get lastCall(){return o.calls[o.calls.length-1]}},l=[],c=!1;function u(...p){return s.push(this),r.push(++Ue),(c?n:l.shift()||n||o.getOriginal()||(()=>{})).apply(this,p)}let a=t.name;t.getMockName=()=>a||"vi.fn()",t.mockName=p=>(a=p,t),t.mockClear=()=>(o.reset(),s=[],r=[],t),t.mockReset=()=>(t.mockClear(),n=()=>{},l=[],t),t.mockRestore=()=>(t.mockReset(),o.restore(),n=void 0,t),t.getMockImplementation=()=>n,t.mockImplementation=p=>(n=p,o.willCall(u),t),t.mockImplementationOnce=p=>(l.push(p),t);function m(p,g){let h=n;n=p,o.willCall(u),c=!0;let f=()=>{n=h,c=!1},k=g();return k instanceof Promise?k.then(()=>(f(),t)):(f(),t)}return t.withImplementation=m,t.mockReturnThis=()=>t.mockImplementation(function(){return this}),t.mockReturnValue=p=>t.mockImplementation(()=>p),t.mockReturnValueOnce=p=>t.mockImplementationOnce(()=>p),t.mockResolvedValue=p=>t.mockImplementation(()=>Promise.resolve(p)),t.mockResolvedValueOnce=p=>t.mockImplementationOnce(()=>Promise.resolve(p)),t.mockRejectedValue=p=>t.mockImplementation(()=>Promise.reject(p)),t.mockRejectedValueOnce=p=>t.mockImplementationOnce(()=>Promise.reject(p)),Object.defineProperty(t,"mock",{get:()=>i}),o.willCall(u),Pe.add(t),t}var U=e=>Ae(G({spy:e||(()=>{})},"spy"));beforeEach(()=>{global.window.fetch=U()});afterEach(()=>{global.window.fetch.mockReset()});var ve=async()=>{let e={json:()=>Promise.resolve(),status:200,ok:!0,headers:new Headers({"Content-Type":"application/json"})};return Promise.resolve(e)},V=async e=>{let{responseBody:t,status:n=200,headers:s={},delay:r}=e,o={json:()=>Promise.resolve(t),status:n,ok:n>=200&&n<=299,headers:new Headers({"Content-Type":"application/json",...s})};return r?new Promise(i=>setTimeout(()=>i(o),r)):Promise.resolve(o)},We=e=>console.warn(`
2
+ ${b.default.white.bold.bgRed("wrapito")} ${b.default.redBright.bold("cannot find any mock matching:")}
3
+ ${b.default.greenBright(`URL: ${e.url}`)}
4
+ ${b.default.greenBright(`METHOD: ${e.method?.toLowerCase()}`)}
5
+ ${b.default.greenBright(`REQUEST BODY: ${e._bodyInit}`)}
6
+ `),Q=async(e,t,n)=>{let s=e.find(L(t));if(!s)return n&&We(t),ve();let{multipleResponses:r}=s;if(!r)return V(s);let o=r.find(i=>!i.hasBeenReturned);if(!o){n&&Ce(s);return}return o.hasBeenReturned=!0,V(o)},J=(e=[],t=!1)=>{global.window.fetch.mockImplementation((s,r)=>{if(typeof s=="string"){let i=new Request(s,r);return Q(e,i,t)}return Q(e,s,t)})},Ce=e=>{let t=`\u{1F32F} Wrapito: Missing response in the multipleResponses array for path ${e.path} and method ${e.method}.`,n=b.default.greenBright(t);console.warn(n)};var H,y=e=>{H={...H,...e}},R=()=>({...H});beforeEach(()=>{global.fetch=U()});afterEach(()=>{global.fetch.mockReset()});var De=e=>(y({Component:e,responses:[],props:{},path:"",hasPath:!1,interactionConfig:void 0,debug:process.env.npm_config_debugRequests==="true"}),I()),I=()=>{let e=Ne();return{withProps:Be,withNetwork:Le,withInteraction:$e,atPath:Fe,debugRequests:_e,mount:Ge,...e}},He=e=>{let t=R(),n=[...t.responses,...e];y({...t,responses:n})},Se=(e,t)=>(t({addResponses:He},e),I()),je=(e,t)=>{let{extend:n}=d(),s=n[t];return{...e,[t]:(...r)=>Se(r,s)}},Ne=()=>{let{extend:e}=d();return Object.keys(e).reduce(je,{})},Be=e=>{let t=R();return y({...t,props:e}),I()},$e=e=>{let t=R();return y({...t,interactionConfig:e}),I()},Le=(e=[])=>{let t=R(),n=Array.isArray(e)?e:[e];return y({...t,responses:[...t.responses,...n]}),I()},Fe=(e,t)=>{let n=R();return y({...n,historyState:t,path:e,hasPath:!0}),I()},_e=()=>{let e=R();return y({...e,debug:!0}),I()},Ge=()=>{let{portal:e,portals:t,changeRoute:n,history:s,mount:r,interaction:o}=d(),{Component:i,props:l,responses:c,path:u,hasPath:a,debug:m,historyState:p,interactionConfig:g}=R(),h=i;e&&z(e),t&&Ve(t),a&&s&&(console.warn("wrapito WARNING: history is DEPRECATED. Pass a changeRoute function to the config instead."),console.warn("Read about changeRoute in: https://github.com/mercadona/wrapito#changeRoute"),s.push(u,p)),a&&!s&&n(u),J(c,m);let f=r(Y.createElement(h,{...l}));if(o){let k=o.setup?o.setup(o.userLib,g):o.userLib;return{...f,user:k}}return{...f,user:void 0}},z=e=>{if(document.getElementById(e))return;let t=document.createElement("div");t.setAttribute("id",e),t.setAttribute("data-testid",e),document.body.appendChild(t)},Ve=e=>{e.forEach(t=>{z(t)})};var S=require("jest-diff"),j=(e,t)=>{let n=t?.host?`\u{1F32F} Wrapito: ${t?.host}${e} ain't got called`:`\u{1F32F} Wrapito: ${e} ain't got called`;return{pass:!1,message:()=>n}},K=(e,t,n)=>({pass:!1,message:()=>`\u{1F32F} Wrapito: ${e} is called ${n} times, you expected ${t} times`}),X=(e,t)=>({pass:!1,message:()=>`\u{1F32F} Wrapito: Fetch method does not match, expected ${e} received ${t??"none"}`}),Z=(e,t)=>{let n=t.map(s=>(0,S.diff)(e,s)).join(`
7
+
8
+ `);return{pass:!1,message:()=>`\u{1F32F} Wrapito: Fetch body does not match.
9
+ ${n}`}},ee=(e,t)=>{let n=t.find(s=>e!==s);return{pass:!1,message:()=>`\u{1F32F} Wrapito: Host request does not match, expected ${e} received ${n}`}},te=(e,t)=>{let n=t.map(s=>(0,S.diff)(e,s)).join(`
10
+
11
+ `);return{pass:!1,message:()=>`\u{1F32F} Wrapito: Fetch headers do not match.
12
+ ${n}`}},ne=()=>({pass:!1,message:()=>"\u{1F32F} Wrapito: Unable to find body."}),N=()=>({pass:!0,message:()=>"Test passing"}),se=(e,t)=>{let n=t?.host?`\u{1F32F} Wrapito: ${t.host}${e} is called`:`\u{1F32F} Wrapito: ${e} is called`;return{pass:!0,message:()=>n}};var re=T(require("deep-equal"),1);var oe=()=>{let e=d().defaultHost;return e?.includes("http")?e:"https://default.com"},Qe=(e="",t,n)=>t.includes(n)?t:e+t,O=e=>e instanceof Request,Je=e=>O(e)?e.url:e,A=(e,t={method:"GET"})=>fetch.mock.calls.filter(([s])=>{let r=Je(s),o=oe(),i=new URL(r,o),l=Qe(t?.host,e,o),c=t?.host||o,u=new URL(l,c),a=i.pathname===u.pathname,m=i.search===u.search,p=i.host===u.host;return u.search?a&&m:t?.host?a&&p:a}),ae=e=>e.flat(1).filter(O).map(t=>t.method),ie=e=>e.flat(1).filter(O).map(t=>t._bodyInit?JSON.parse(t._bodyInit):{}),ce=e=>e.flat(1).filter(O).map(t=>new URL(t.url,oe()).hostname),pe=e=>e.flat(1).filter(O).map(t=>{let n={};return t.headers.forEach((s,r)=>{n[r]=s}),n}),ue=(e,t)=>e&&!t.includes(e),le=(e,t)=>t.map(s=>(0,re.default)(e,s)).every(s=>s===!1),me=(e,t)=>t.every(s=>s!==e),de=(e,t)=>t.every(s=>Object.entries(e).some(([r,o])=>s[r.toLowerCase()]!==o)),he=e=>e.length===0;var Ye=(e,t)=>{let n=A(e);if(he(n))return j(e);let s=ae(n),r=t?.method;if(ue(r,s))return X(r,s);let o=ie(n),i=t?.body;if(!i)return ne();if(le(i,o))return Z(i,o);let l=ce(n),c=t?.host;if(c&&me(c,l))return ee(c,l);let u=pe(n),a=t?.headers;return a&&de(a,u)?te(a,u):N()},ze=(e,t={method:"GET"})=>A(e,t).length?se(e,t):j(e,t),Ke=(e,t,n={method:"GET"})=>{let s=A(e,n);return s.length!==t?K(e,t,s.length):N()},Xe={toHaveBeenFetched:ze,toHaveBeenFetchedWith:Ye,toHaveBeenFetchedTimes:Ke};0&&(module.exports={assertions,configure,getConfig,matchers,wrap});