Skip to content

Commit

Permalink
Merge pull request #428 from alovajs/beta-fix/bug-and-feature
Browse files Browse the repository at this point in the history
fix: return response data in `middleware.next` of `useSQRequest`
  • Loading branch information
JOU-amjs authored Jul 1, 2024
2 parents dab7288 + 03bca82 commit 00cca44
Show file tree
Hide file tree
Showing 12 changed files with 193 additions and 110 deletions.
6 changes: 6 additions & 0 deletions .changeset/shiny-pigs-pretend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@alova/shared': patch
'alova': patch
---

return response data in `middleware.next` of `useSQRequest`
5 changes: 5 additions & 0 deletions .changeset/thin-zebras-return.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@alova/mock': patch
---

fix that throw error when return null in mock api
4 changes: 2 additions & 2 deletions internal/jest.setup.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ Object.defineProperties(globalThis, {
}
});

// if the environment is jsdom, set process.browser to true
// if the environment is jsdom, set process.cwd to undefined
if (typeof window !== 'undefined') {
(process as any).browser = true;
(process as any).cwd = undefined;
}

// undici must import after defining TextDecoder and TextEncoder, otherwise it will throw error
Expand Down
126 changes: 68 additions & 58 deletions packages/adapter-mock/src/MockRequest.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isFn, isNumber, isString, noop } from '@alova/shared/function';
import { falseValue, trueValue, undefinedValue } from '@alova/shared/vars';
import { isFn, isNumber, isString, newInstance, usePromise } from '@alova/shared/function';
import { falseValue, promiseReject, promiseResolve, trueValue, undefinedValue } from '@alova/shared/vars';
import type { AlovaGenerics, Method, RequestElements } from 'alova';
import { Mock, MockRequestInit } from '~/typings';
import consoleRequestInfo from './consoleRequestInfo';
Expand Down Expand Up @@ -103,44 +103,45 @@ export default function MockRequest<RequestConfig, Response, ResponseHeader>(
});
return httpAdapter(elements, method);
}
throw new Error(`could not find the httpAdapter which send request.\n[url]${url}`);
throw new Error(`cannot find the httpAdapter.\n[url]${url}`);
}

let timer: NodeJS.Timeout;
let rejectFn: (reason?: any) => void = noop;
const promiseResolver = usePromise();
const { resolve } = promiseResolver;
let { promise: resonpsePromise, reject } = promiseResolver;
const timeout = method.config.timeout || 0;
if (timeout > 0) {
setTimeout(() => {
rejectFn(new Error('request timeout'));
reject(new Error('request timeout'));
}, timeout);
}
const resonpsePromise = new Promise<any>((resolve, reject) => {
rejectFn = reject;
timer = setTimeout(() => {
// response支持返回promise对象
try {
const res = isFn(mockDataRaw)
? mockDataRaw({
query,
params,
data: isString(data) || !data ? {} : data,
headers: requestHeaders
})
: mockDataRaw;

// 这段代码表示,将内部reject赋值到外部,如果超时了则立即触发reject,或者等待res(如果res为promise)resolve
resolve(
new Promise<any>((resolveInner, rejectInner) => {
rejectFn = rejectInner;
Promise.resolve(res).then(resolveInner).catch(rejectInner);
const timer = setTimeout(() => {
// response支持返回promise对象
try {
const res = isFn(mockDataRaw)
? mockDataRaw({
query,
params,
data: isString(data) || !data ? {} : data,
headers: requestHeaders
})
);
} catch (error) {
reject(error);
}
}, delay);
})
.then(response => {
: mockDataRaw;

// 这段代码表示,将内部reject赋值到外部,如果超时了则立即触发reject,或者等待res(如果res为promise)resolve
resolve(
newInstance(Promise<any>, (resolveInner, rejectInner) => {
reject = rejectInner;
promiseResolve(res).then(resolveInner).catch(rejectInner);
})
);
} catch (error) {
reject(error);
}
}, delay);

resonpsePromise = resonpsePromise
.then((response: any) => {
let status = 200;
let statusText = 'ok';
let responseHeaders = {};
Expand All @@ -150,7 +151,7 @@ export default function MockRequest<RequestConfig, Response, ResponseHeader>(
if (response === undefinedValue) {
status = 404;
statusText = 'api not found';
} else if (isNumber(response.status) && isString(response.statusText)) {
} else if (response && isNumber(response.status) && isString(response.statusText)) {
// 返回了自定义状态码和状态文本,将它作为响应信息
status = response.status;
statusText = response.statusText;
Expand All @@ -161,42 +162,51 @@ export default function MockRequest<RequestConfig, Response, ResponseHeader>(
body = response;
}

// 打印模拟数据请求信息
isFn(mockRequestLogger) &&
mockRequestLogger({
isMock: trueValue,
url,
method: type,
params,
headers: requestHeaders,
query,
data: (data as any) || {},
responseHeaders,
response: body
});
return onMockResponse(
{ status, statusText, responseHeaders, body },
{
headers: requestHeaders,
query,
params,
data: (data as any) || {}
},
method
);
return newInstance(Promise, (resolve, reject) => {
try {
const res = onMockResponse(
{ status, statusText, responseHeaders, body },
{
headers: requestHeaders,
query,
params,
data: (data as any) || {}
},
method
);
resolve(res);
} catch (error) {
reject(error);
}
}).then(response => {
// 打印模拟数据请求信息
isFn(mockRequestLogger) &&
mockRequestLogger({
isMock: trueValue,
url,
method: type,
params,
headers: requestHeaders,
query,
data: (data as any) || {},
responseHeaders,
response: body
});
return response;
});
})
.catch(error => Promise.reject(onMockError(error, method)));
.catch(error => promiseReject(onMockError(error, method)));

// 返回响应数据
return {
response: () =>
resonpsePromise.then(({ response }) =>
(response as any).toString() === '[object Response]' ? (response as any).clone() : response
response && response.toString() === '[object Response]' ? (response as any).clone() : response
),
headers: () => resonpsePromise.then(({ headers }) => headers),
abort: () => {
clearTimeout(timer);
rejectFn(new Error('The user abort request'));
reject(new Error('The user abort request'));
}
};
};
Expand Down
128 changes: 91 additions & 37 deletions packages/adapter-mock/test/mockRequest.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,62 +66,72 @@ describe('mock request', () => {
)
.send();
expect(payload).toStrictEqual({ id: 1 });
expect(mockApi).toBeCalled();
expect(mockResponse).toBeCalled();
expect(mockApi).toHaveBeenCalled();
expect(mockResponse).toHaveBeenCalled();
});

test('should receive all request data', async () => {
test('should call `mockRequestLogger` and receive all request data', async () => {
const mocks = defineMock({
'[POST]/detail': () => ({
id: 1
})
}),
'[POST]/detail2': null
});

const mockFn = jest.fn();
// 模拟数据请求适配器
const mockRequestAdapter = createAlovaMockAdapter([mocks], {
delay: 10,
onMockResponse: responseData => ({
response: responseData.body,
headers: {}
}),
mockRequestLogger: ({ isMock, url, method, headers, query, data, responseHeaders, response }) => {
mockFn();
expect(isMock).toBeTruthy();
expect(url).toBe('http://xxx/detail?aa=1&bb=2');
expect(method).toBe('POST');
expect(headers).toStrictEqual({
customHeader: 1
});
expect(query).toStrictEqual({
aa: '1',
bb: '2'
});
expect(data).toStrictEqual({});
expect(responseHeaders).toStrictEqual({});
expect(response).toStrictEqual({
id: 1
});
onMockResponse: (responseData, _, method) => {
if (method.url === '/detail2') {
throw new Error('response error');
}
return {
response: responseData.body,
headers: {}
};
},
mockRequestLogger: logger => {
mockFn(logger);
}
});

const alovaInst = createAlova({
baseURL: 'http://xxx',
requestAdapter: mockRequestAdapter
});
const payload = await alovaInst
.Post(
'/detail?aa=1&bb=2',
{},
{
headers: {
customHeader: 1
}
const payload = await alovaInst.Post(
'/detail?aa=1&bb=2',
{},
{
headers: {
customHeader: 1
}
)
.send();
}
);
expect(payload).toStrictEqual({ id: 1 });
expect(mockFn).toBeCalled();
expect(mockFn).toHaveBeenCalledTimes(1);
expect(mockFn).toHaveBeenCalledWith({
isMock: true,
url: 'http://xxx/detail?aa=1&bb=2',
method: 'POST',
headers: {
customHeader: 1
},
params: {},
query: {
aa: '1',
bb: '2'
},
data: {},
responseHeaders: {},
response: {
id: 1
}
});

await expect(alovaInst.Post('/detail2').send()).rejects.toThrow('response error');
expect(mockFn).toHaveBeenCalledTimes(1); // mockRequestLogger will not be called when throw error in `onMockResponse`
});

test('response with status and statusText', async () => {
Expand Down Expand Up @@ -162,7 +172,51 @@ describe('mock request', () => {
expect(err.name).toBe('403');
expect(err.message).toBe('customer error');
}
expect(mockFn).toBeCalledTimes(1);
expect(mockFn).toHaveBeenCalledTimes(1);
});

test('should return null when return `null` in mock', async () => {
const mocks = defineMock({
'[POST]/detail': () => null
});

// 模拟数据请求适配器
const mockRequestAdapter = createAlovaMockAdapter([mocks], {
delay: 10,
onMockResponse({ body }) {
return {
response: body,
headers: {}
};
}
});
const alova = createAlova({
baseURL: 'http://xxx',
requestAdapter: mockRequestAdapter
});
const data = await alova.Post('/detail');
expect(data).toBeNull();
});

test('should throw error of 404 when return `undefined` in mock', async () => {
const mocks = defineMock({
'[POST]/detail': () => undefined
});

// 模拟数据请求适配器
const mockRequestAdapter = createAlovaMockAdapter([mocks], {
delay: 10
});
const alova = createAlova({
baseURL: 'http://xxx',
requestAdapter: mockRequestAdapter
});
const response = await alova.Post<Response>('/detail');
expect(response.status).toBe(404);
expect(response.statusText).toBe('api not found');

// access api that not exists
expect(alova.Post<Response>('/detail234')).rejects.toThrow('cannot find the httpAdapter');
});

test('should receive error when throw it in mock function', async () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/adapter-mock/typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export interface StatusResponse {
body?: any;
}
export type MockFunction = (request: MockServerRequest) => StatusResponse | any;
export type Mock = Record<string, MockFunction | string | number | Record<string, any> | any[]>;
export type Mock = Record<string, MockFunction | string | number | null | Record<string, any> | any[]>;

export interface MockWrapper {
enable: boolean;
Expand Down
4 changes: 4 additions & 0 deletions packages/alova/typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,10 @@ export interface ReferingObject {
* the map of tracked state keys
*/
trackedKeys: Record<string, boolean>;
/**
* has been bound error event
*/
bindError: boolean;
[key: string]: any;
}
export interface StatesHook<State, Computed, Watched = State | Computed, Export = State> {
Expand Down
Loading

0 comments on commit 00cca44

Please sign in to comment.