diff --git a/package.json b/package.json index 2a509d7..cdbb4dd 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "packages/example-mina" ], "scripts": { + "test": "yarn workspace connect-miniprogram jest", "build": "yarn workspace connect-miniprogram build", "dev:taro": "yarn workspace example-taro dev:weapp" }, diff --git a/packages/connect-miniprogram/src/connect/async-generator.spec.ts b/packages/connect-miniprogram/src/connect/async-generator.spec.ts new file mode 100644 index 0000000..4686944 --- /dev/null +++ b/packages/connect-miniprogram/src/connect/async-generator.spec.ts @@ -0,0 +1,39 @@ +import { describe, expect, jest, test } from '@jest/globals'; + +import { fireEventQueue } from '../test-utils'; +import { createAsyncGeneratorFromEventPattern } from './async-generator'; + +describe('createAsyncGeneratorFromEventPattern', () => { + const dispose = jest.fn(); + test('creates am async generator', async () => { + const gen = createAsyncGeneratorFromEventPattern( + ({ handleValue, handleEnd }) => { + fireEventQueue([ + () => { + handleValue(0); + }, + () => { + handleValue(1); + }, + () => { + handleValue(2); + }, + () => { + handleEnd(); + }, + ]); + return dispose; + }, + ); + const a = gen(); + expect(dispose).toBeCalledTimes(0); + expect(await a.next()).toEqual({ value: 0, done: false }); + expect(dispose).toBeCalledTimes(0); + expect(await a.next()).toEqual({ value: 1, done: false }); + expect(dispose).toBeCalledTimes(0); + expect(await a.next()).toEqual({ value: 2, done: false }); + expect(dispose).toBeCalledTimes(0); + expect(await a.next()).toEqual({ value: undefined, done: true }); + expect(dispose).toBeCalledTimes(1); + }); +}); diff --git a/packages/connect-miniprogram/src/connect/wx-request.spec.ts b/packages/connect-miniprogram/src/connect/wx-request.spec.ts new file mode 100644 index 0000000..1cc990f --- /dev/null +++ b/packages/connect-miniprogram/src/connect/wx-request.spec.ts @@ -0,0 +1,175 @@ +import { describe, expect, jest, test } from '@jest/globals'; + +import { mockWxRequest } from '../test-utils'; +import { + createWxRequestAsAsyncGenerator, + createWxRequestAsPromise, +} from './wx-request'; + +jest.mock('./envelope', () => ({ + createEnvelopeAsyncGenerator: (s) => s, +})); + +describe('createWxRequestAsPromise', () => { + test('should return a promise, using binary format', () => { + const wxRequest = mockWxRequest({}); + const request = createWxRequestAsPromise( + { + request: wxRequest, + requestOptions: { + forceCellularNetwork: true, + }, + }, + true, + ); + const header = new Headers(); + header.append('foo', 'bar'); + request({ + url: 'https://example.com', + data: 'data', + method: 'POST', + header, + }); + expect(wxRequest).toBeCalledWith({ + url: 'https://example.com', + data: 'data', + method: 'POST', + header: { + foo: 'bar', + }, + responseType: 'arraybuffer', + forceCellularNetwork: true, + fail: expect.any(Function), + success: expect.any(Function), + }); + }); +}); + +describe('createWxRequestAsAsyncGenerator', () => { + test('should return an async generator, not devtool', async () => { + const wxRequest = mockWxRequest({ + responseHeader: { + 'response-header-key': 'response-header-value', + }, + }); + const request = createWxRequestAsAsyncGenerator({ + request: wxRequest, + isDevTool: false, + requestOptions: { + forceCellularNetwork: true, + }, + }); + const reqHeaders = new Headers(); + reqHeaders.append('foo', 'bar'); + const { + header: resHeader, + statusCode, + messageStream, + } = await request({ + url: 'https://example.com', + data: 'data', + method: 'POST', + header: reqHeaders, + }); + expect(wxRequest).toBeCalledWith({ + url: 'https://example.com', + data: 'data', + method: 'POST', + header: { + foo: 'bar', + }, + enableChunked: true, + responseType: 'arraybuffer', + forceCellularNetwork: true, + fail: expect.any(Function), + success: expect.any(Function), + }); + + expect(resHeader.get('response-header-key')).toBe('response-header-value'); + expect(statusCode).toBe(200); + expect(await messageStream.next()).toEqual({ + done: false, + value: new Uint8Array([1, 2, 3]), + }); + expect(await messageStream.next()).toEqual({ + done: false, + value: new Uint8Array([4, 5, 6]), + }); + expect(await messageStream.next()).toEqual({ + done: true, + value: undefined, + }); + }); + + test('should return an async generator, is devtool', async () => { + const wxRequest = mockWxRequest({ + responseHeader: { + 'response-header-key': 'response-header-value', + }, + }); + const request = createWxRequestAsAsyncGenerator({ + request: wxRequest, + isDevTool: true, + requestOptions: { + forceCellularNetwork: true, + }, + }); + const reqHeaders = new Headers(); + reqHeaders.append('foo', 'bar'); + const { + header: resHeader, + statusCode, + messageStream, + } = await request({ + url: 'https://example.com', + data: 'data', + method: 'POST', + header: reqHeaders, + }); + expect(wxRequest).toBeCalledWith({ + url: 'https://example.com', + data: 'data', + method: 'POST', + header: { + foo: 'bar', + }, + responseType: 'arraybuffer', + forceCellularNetwork: true, + fail: expect.any(Function), + success: expect.any(Function), + }); + expect(resHeader.get('response-header-key')).toBe('response-header-value'); + expect(statusCode).toBe(200); + expect(await messageStream.next()).toEqual({ + done: false, + value: new Uint8Array([1, 2, 3, 4, 5, 6]), + }); + expect(await messageStream.next()).toEqual({ + done: true, + value: undefined, + }); + }); + + test('should throw if first chunk is not header', async () => { + const wxRequest = mockWxRequest({ + skipHeadersReceivedHandler: true, + }); + const request = createWxRequestAsAsyncGenerator({ + request: wxRequest, + isDevTool: false, + requestOptions: { + forceCellularNetwork: true, + }, + }); + const reqHeaders = new Headers(); + reqHeaders.append('foo', 'bar'); + expect(async () => { + await request({ + url: 'https://example.com', + data: 'data', + method: 'POST', + header: reqHeaders, + }); + }).rejects.toThrow('missing header'); + }); +}); diff --git a/packages/connect-miniprogram/src/connect/wx-request.ts b/packages/connect-miniprogram/src/connect/wx-request.ts index d4f9077..155ba6a 100644 --- a/packages/connect-miniprogram/src/connect/wx-request.ts +++ b/packages/connect-miniprogram/src/connect/wx-request.ts @@ -112,8 +112,12 @@ function create( } async function demuxStream(iterator: AsyncGenerator) { + const firstChunk = await iterator.next(); + if (firstChunk.done || firstChunk.value.name !== 'HeadersReceived') { + throw new Error('missing header'); + } // first value is header - const headerChunk = (await iterator.next()).value as HeadersReceivedEvent; + const { statusCode, header } = firstChunk.value.payload; async function* messageStream() { for await (const value of iterator) { @@ -123,8 +127,8 @@ async function demuxStream(iterator: AsyncGenerator) { } return { - statusCode: headerChunk.payload.statusCode, - header: new Headers(headerChunk?.payload.header), + statusCode: statusCode, + header: new Headers(header), messageStream: createEnvelopeAsyncGenerator(messageStream()), }; } @@ -133,7 +137,7 @@ export function createWxRequestAsAsyncGenerator({ request, isDevTool, requestOptions, -}: CreateTransportOptions) { +}: Pick) { /** * Weixin devtool has a bug if enableChunked is true. * https://developers.weixin.qq.com/community/develop/doc/000e44fc464560a0a6bf4188f56800 @@ -145,7 +149,10 @@ export function createWxRequestAsAsyncGenerator({ } export function createWxRequestAsPromise( - { request, requestOptions }: CreateTransportOptions, + { + request, + requestOptions, + }: Pick, useBinaryFormat: boolean, ) { return (options: PartialOptions) => diff --git a/packages/connect-miniprogram/src/test-utils.ts b/packages/connect-miniprogram/src/test-utils.ts new file mode 100644 index 0000000..7fcff74 --- /dev/null +++ b/packages/connect-miniprogram/src/test-utils.ts @@ -0,0 +1,84 @@ +import { jest } from '@jest/globals'; + +function wait() { + return new Promise((resolve) => setTimeout(resolve, 0)); +} + +export async function fireEventQueue(fns: (() => void)[]) { + for (const fn of fns) { + await wait(); + fn(); + } +} + +function buffer(arr: number[]) { + return new Uint8Array(arr).buffer; +} + +export const mockWxRequest = ({ + responseHeader = {}, + skipHeadersReceivedHandler = false, +}: { + responseHeader?: Record; + skipHeadersReceivedHandler?: boolean; +}) => { + const headerData: Partial = { + header: responseHeader, + statusCode: 200, + cookies: [], + }; + return jest.fn((options: WechatMiniprogram.RequestOption) => { + let chunkReceivedHandler: undefined | ((res: any) => void); + let headersReceivedHandler: undefined | ((res: any) => void); + + if (options.enableChunked) { + const eventQueue = [ + () => { + if (!skipHeadersReceivedHandler) { + headersReceivedHandler?.(headerData); + } + }, + () => { + chunkReceivedHandler?.({ + data: buffer([1, 2, 3]), + }); + }, + () => { + chunkReceivedHandler?.({ + data: buffer([4, 5, 6]), + }); + }, + () => { + options.success?.( + {} as WechatMiniprogram.RequestSuccessCallbackResult, + ); + }, + ]; + fireEventQueue(eventQueue); + } else { + fireEventQueue([ + () => { + options.success?.({ + ...headerData, + data: buffer([1, 2, 3, 4, 5, 6]), + } as WechatMiniprogram.RequestSuccessCallbackResult); + }, + ]); + } + return { + abort: jest.fn(), + onChunkReceived: jest.fn((fn: (res: any) => void) => { + chunkReceivedHandler = fn; + }), + offChunkReceived: jest.fn(() => { + chunkReceivedHandler = undefined; + }), + onHeadersReceived: jest.fn((fn: (res: any) => void) => { + headersReceivedHandler = fn; + }), + offHeadersReceived: jest.fn(() => { + headersReceivedHandler = undefined; + }), + } as WechatMiniprogram.RequestTask; + }) as typeof wx.request; +};