Skip to content

Commit b46ac4b

Browse files
committed
feat: 🎸 support the version 2.0 of suchjs, add timeout option
BREAKING CHANGE: 🧨 Y ✅ Closes: N
1 parent d6ebec3 commit b46ac4b

File tree

11 files changed

+965
-1436
lines changed

11 files changed

+965
-1436
lines changed

.eslintrc.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,19 @@ module.exports = {
88
jest: true,
99
browser: true,
1010
},
11+
plugins: ['@typescript-eslint'],
1112
extends: [
13+
'eslint:recommended',
1214
'plugin:@typescript-eslint/recommended',
13-
'plugin:prettier/recommended',
15+
'prettier',
1416
],
1517
rules: {
1618
'@typescript-eslint/no-unused-vars': [
1719
'error',
1820
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
1921
],
2022
'no-console': 'warn',
23+
'comma-spacing': 'error'
2124
},
2225
overrides: [
2326
{
@@ -30,7 +33,6 @@ module.exports = {
3033
env: {
3134
node: true,
3235
},
33-
extends: ['plugin:prettier/recommended'],
3436
rules: {
3537
'@typescript-eslint/no-var-requires': 'off',
3638
},

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# such-mock-browser
22

3-
Extend the ability for `suchjs` with `Such.mock` in browser.
3+
Extend the ability for `suchjs` with `Such.mock` in browser, intercept the reqeust made by `XMLHttpRequest` or `fetch` API, response a mock data instead.
44

55
## How to use
66

dist/such-mock-browser.0.0.1.min.js

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/such-mock-browser.0.0.2.min.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/such-mock-browser.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/index.html

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,47 +12,57 @@
1212
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
1313
<script>
1414
$(function(){
15-
const { method, target } = Such.mock;
16-
Such.mock('/a', 'GET', {
15+
console.log(Such);
16+
const globalSuch = Such.default;
17+
const { method, target } = globalSuch.mock;
18+
// intercept
19+
const interceptor = globalSuch.mock.intercept(target.XHR | target.FETCH, {
20+
timeout: [1000,5000]
21+
});
22+
globalSuch.mock('/a', 'GET', {
1723
a: ':string'
1824
});
19-
Such.mock('/b', method.GET | method.POST, {
25+
globalSuch.mock('/b', method.GET | method.POST, {
2026
b: ':number'
2127
});
22-
Such.mock('/c', '*', {
28+
globalSuch.mock('/c', '*', {
2329
c: ':increment:{3}'
2430
});
25-
Such.mock('/d', function(req){
31+
globalSuch.mock('/d', function(req){
2632
return req.method === 'POST';
2733
}, {
2834
d: ':::haha'
2935
});
30-
Such.mock(/\/\w+/, method.ANY, function(req){
36+
globalSuch.mock(/\/\w+/, method.ANY, function(req){
3137
return {
3238
'*': '*'
3339
};
3440
});
35-
Such.mock('/e/:id', 'GET,POST', function(req, params){
41+
globalSuch.mock('/e/:id', 'GET,POST', function(req, params){
3642
return {
3743
id: params.id,
38-
words: Such.as(':string')
44+
words: globalSuch.as(':string')
3945
};
4046
});
41-
Such.mock('/f', 'GET', function(req, params){
42-
return Such.as({ plain: ':::haha,`:string`'});
47+
globalSuch.mock('/f', 'GET', function(req, params){
48+
return globalSuch.as({ plain: ':::haha,`:string`'});
4349
}, {
44-
headers: {
45-
'Content-Type': 'text/html'
50+
timeout: 2000,
51+
transformer: {
52+
headers: {
53+
'Content-Type': 'text/html'
54+
}
4655
}
4756
});
48-
Such.mock('/g', 'GET', function(req){
57+
interceptor.on('response', (req, res) => {
58+
console.log(res);
59+
});
60+
globalSuch.mock('/g', 'GET', function(req){
4961
console.log('触发req');
50-
return Such.as({
51-
'fetch:{1,3}': ':string'
62+
return globalSuch.as({
63+
'fetch{1,3}': ':string'
5264
});
5365
});
54-
// intercept
55-
Such.mock.intercept(target.XHR | target.FETCH);
5666
// xhr
5767
$.get('/a', function(res){
5868
console.log(res);

examples/such-mock-browser.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "such-mock-browser",
3-
"version": "0.0.1",
4-
"description": "Extend suchjs with 'Such.mock' in browser.",
3+
"version": "0.0.2",
4+
"description": "Intercept the request made by 'XMLHttpRequest' and 'fetch' API in the browser, and response a mock data based on 'suchjs' library.",
55
"main": "./lib/index.js",
66
"typings": "./lib/index.d.ts",
77
"repository": "https://github.com/suchjs/such-mock-browser",
@@ -22,23 +22,22 @@
2222
"suchjs": ">= 1.1.2"
2323
},
2424
"dependencies": {
25-
"@mswjs/interceptors": "^0.12.5",
25+
"@mswjs/interceptors": "^0.13.1",
2626
"node-match-path": "^0.6.3"
2727
},
2828
"devDependencies": {
29-
"@typescript-eslint/eslint-plugin": "^4.29.0",
30-
"@typescript-eslint/parser": "^4.29.0",
31-
"eslint": "^7.32.0",
29+
"@typescript-eslint/eslint-plugin": "^5.9.1",
30+
"@typescript-eslint/parser": "^5.9.1",
31+
"eslint": "^8.6.0",
3232
"eslint-config-prettier": "^8.3.0",
33-
"eslint-plugin-prettier": "^3.4.0",
34-
"filemanager-webpack-plugin": "^6.1.4",
35-
"prettier": "^2.3.2",
36-
"suchjs": "^1.1.2",
37-
"terser-webpack-plugin": "^5.1.4",
38-
"ts-loader": "^9.2.5",
39-
"typescript": "^4.3.5",
40-
"webpack": "^5.49.0",
41-
"webpack-cli": "^4.7.2",
42-
"webpack-dev-server": "^3.11.2"
33+
"filemanager-webpack-plugin": "^6.1.7",
34+
"prettier": "^2.5.1",
35+
"suchjs": "^2.1.0",
36+
"terser-webpack-plugin": "^5.3.0",
37+
"ts-loader": "^9.2.6",
38+
"typescript": "^4.5.4",
39+
"webpack": "^5.66.0",
40+
"webpack-cli": "^4.9.1",
41+
"webpack-dev-server": "^4.7.3"
4342
}
4443
}

src/index.ts

Lines changed: 94 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ import {
88
import { interceptXMLHttpRequest } from '@mswjs/interceptors/lib/interceptors/XMLHttpRequest';
99
import { interceptFetch } from '@mswjs/interceptors/lib/interceptors/fetch';
1010
import { match } from 'node-match-path';
11+
import { Such } from 'suchjs/lib/core/such';
1112
// !Use require to import the umd style library such
1213
// eslint-disable-next-line @typescript-eslint/no-var-requires
13-
const Such = require('suchjs/lib/browser');
14+
const windowSuch = require('suchjs/lib/browser');
15+
const globalSuchInstance = windowSuch.default as Such;
16+
const protoOfSuch = Object.getPrototypeOf(globalSuchInstance);
1417
/**
1518
* Interceptor Target
1619
* XHR -> XMLHttpRequest
@@ -21,7 +24,7 @@ enum InterceptorTarget {
2124
FETCH = 0b001 << 1,
2225
}
2326
/**
24-
*
27+
* Request Methods
2528
*/
2629
enum RequestMethod {
2730
GET = 0b1,
@@ -38,21 +41,39 @@ type TReqMatchFunc = (request: IsomorphicRequest, params?: TObj) => boolean;
3841
type TMockDataFunc = (request: IsomorphicRequest, params?: TObj) => unknown;
3942
type TReqMatchParam = string | TReqMatchFunc | RequestMethod;
4043
type TMockDataParam = TMockDataFunc | unknown;
41-
type MockedResult = Partial<MockedResponse> | ((resp: MockedResponse) => void);
44+
type ResponseTransformer = Partial<MockedResponse> | ((resp: MockedResponse) => void);
4245
type RouteItem<T extends string | RegExp = string | RegExp> = [
4346
T,
4447
TReqMatchFunc,
4548
TMockDataFunc,
46-
MockedResult,
49+
ResponseOptions,
4750
];
48-
type SuchExtended = typeof Such & {
51+
export type SuchWithMock = typeof globalSuchInstance & {
4952
mock: (
5053
route: string | RegExp,
5154
match: TReqMatchParam,
5255
data: TMockDataParam,
53-
resp?: MockedResult,
56+
responseOptions?: ResponseOptions,
5457
) => unknown;
5558
};
59+
/**
60+
* ResponseTimeout
61+
*/
62+
interface ResponseTimeout{
63+
timeout?: number | [number, number]
64+
}
65+
/**
66+
* InterceptorOptions
67+
*/
68+
type InterceptorOptions = ResponseTimeout
69+
70+
/**
71+
* ResponseOptions
72+
*/
73+
interface ResponseOptions extends ResponseTimeout {
74+
transformer?: ResponseTransformer
75+
}
76+
5677
// Pick the value => key in ts enum.
5778
const reqMethodPairs: [number, string][] = (() => {
5879
const pairs: [number, string][] = [];
@@ -69,15 +90,70 @@ let interceptor: InterceptorApi;
6990
// routes
7091
let stringRoutes: RouteItem<string>[] = [];
7192
let regexRoutes: RouteItem<RegExp>[] = [];
93+
// interceptRequest
94+
const interceptRequest = (request: IsomorphicRequest, options: InterceptorOptions = {}) => {
95+
const url = request.url;
96+
const location = document.location;
97+
// first, check the host if is equal
98+
if (location.host !== url.host) {
99+
return;
100+
}
101+
// then check the pathname
102+
const loop = (...args: RouteItem[][]) => {
103+
for (let i = 0, j = args.length; i < j; i++) {
104+
const routes = args[i];
105+
for (let l = 0, m = routes.length; l < m; l++) {
106+
const [route, matchFn, dataFn, responseOptions] = routes[l];
107+
const { matches, params } = match(route, url.pathname);
108+
if (matches && matchFn(request, params)) {
109+
const data = dataFn(request, params);
110+
const response = {
111+
status: 200,
112+
statusText: 'Ok',
113+
headers: {
114+
'Content-Type': 'application/json',
115+
},
116+
body: JSON.stringify(data),
117+
};
118+
// if resp parameter is a function
119+
// use the function to transform response
120+
const { timeout, transformer } = responseOptions || {};
121+
if (typeof transformer === 'function') {
122+
transformer(response);
123+
} else if (transformer) {
124+
// if resp is a MockResponse
125+
// extend to override the response
126+
Object.assign(response, transformer);
127+
}
128+
return { response, timeout };
129+
}
130+
}
131+
}
132+
};
133+
// first check the string routes, then the regex routes
134+
const result = loop(stringRoutes, regexRoutes);
135+
if(result){
136+
const { timeout, response } = result;
137+
const useTimeout = timeout || options.timeout;
138+
if(useTimeout === undefined){
139+
return response;
140+
} else {
141+
const delay = Array.isArray(useTimeout) ? globalSuchInstance.utils.makeRandom.apply(null, useTimeout) : useTimeout;
142+
return new Promise((resolve) => {
143+
setTimeout(() => resolve(response), delay);
144+
});
145+
}
146+
}
147+
};
72148
/**
73149
* Such.mock
74150
*/
75-
const ThisSuch: SuchExtended = Object.assign(Such, {
151+
const SuchPrototype: SuchWithMock = Object.assign(protoOfSuch, {
76152
mock(
77153
route: string | RegExp,
78154
match: TReqMatchParam,
79155
data: TMockDataParam,
80-
resp?: MockedResult,
156+
responseOptions?: ResponseOptions,
81157
) {
82158
// handle the match function
83159
let matchFn: TReqMatchFunc;
@@ -109,28 +185,29 @@ const ThisSuch: SuchExtended = Object.assign(Such, {
109185
dataFn = data as TMockDataFunc;
110186
} else {
111187
dataFn = (_req: IsomorphicRequest) => {
112-
return Such.as(data);
188+
return globalSuchInstance.as(data);
113189
};
114190
}
115191
// route
116192
if (typeof route === 'string') {
117-
stringRoutes.push([route, matchFn, dataFn, resp]);
193+
stringRoutes.push([route, matchFn, dataFn, responseOptions]);
194+
} else if (route instanceof RegExp) {
195+
regexRoutes.push([route, matchFn, dataFn, responseOptions]);
118196
} else {
119-
regexRoutes.push([route, matchFn, dataFn, resp]);
197+
throw new Error(`wrong route pattern of type "${typeof route}", please use a string or a RegExp rule instead.`);
120198
}
121199
},
122200
});
123-
124201
/**
125202
* Define properties inject into Such.mock
126203
*/
127-
Object.assign(ThisSuch.mock, {
204+
Object.assign(SuchPrototype.mock, {
128205
// the target need be interceptor, XHR or FETCH or both
129206
target: InterceptorTarget,
130207
// method
131208
method: RequestMethod,
132209
// intercept
133-
intercept(target: InterceptorTarget) {
210+
intercept(target: InterceptorTarget, options: InterceptorOptions = {}): InterceptorApi | never {
134211
const modules: Interceptor[] = [];
135212
if ((target & InterceptorTarget.XHR) > 0) {
136213
modules.push(interceptXMLHttpRequest);
@@ -142,48 +219,11 @@ Object.assign(ThisSuch.mock, {
142219
interceptor = createInterceptor({
143220
modules,
144221
resolver: (request: IsomorphicRequest) => {
145-
const url = request.url;
146-
const location = document.location;
147-
// first, check the host if is equal
148-
if (location.host !== url.host) {
149-
return;
150-
}
151-
// then check the pathname
152-
const loop = (...args: RouteItem[][]) => {
153-
for (let i = 0, j = args.length; i < j; i++) {
154-
const routes = args[i];
155-
for (let l = 0, m = routes.length; l < m; l++) {
156-
const [route, matchFn, dataFn, resp] = routes[l];
157-
const { matches, params } = match(route, url.pathname);
158-
if (matches && matchFn(request, params)) {
159-
const data = dataFn(request, params);
160-
const result = {
161-
status: 200,
162-
statusText: 'Ok',
163-
headers: {
164-
'Content-Type': 'application/json',
165-
},
166-
body: JSON.stringify(data),
167-
};
168-
// if resp parameter is a function
169-
// use the function to transform result
170-
if (typeof resp === 'function') {
171-
resp(result);
172-
} else if (resp) {
173-
// if resp is a MockResponse
174-
// extend to override the result
175-
Object.assign(result, resp);
176-
}
177-
return result;
178-
}
179-
}
180-
}
181-
};
182-
// first check the string routes, then the regex routes
183-
return loop(stringRoutes, regexRoutes);
222+
return interceptRequest(request, options);
184223
},
185224
});
186225
interceptor.apply();
226+
return interceptor;
187227
} else {
188228
throw new Error(
189229
`Wrong interceptor target parameter when call the "Such.mock.intercept": expect at least one interceptor target.`,
@@ -198,4 +238,4 @@ Object.assign(ThisSuch.mock, {
198238
regexRoutes = [];
199239
},
200240
});
201-
export default ThisSuch;
241+
export default windowSuch;

0 commit comments

Comments
 (0)