-
-
Notifications
You must be signed in to change notification settings - Fork 490
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: support concurrent test runs via "server.boundary" (#2000)
- Loading branch information
1 parent
f805d34
commit 450e7bc
Showing
15 changed files
with
439 additions
and
139 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,113 +1,82 @@ | ||
import { | ||
BatchInterceptor, | ||
HttpRequestEventMap, | ||
Interceptor, | ||
InterceptorReadyState, | ||
} from '@mswjs/interceptors' | ||
import { invariant } from 'outvariant' | ||
import { SetupApi } from '~/core/SetupApi' | ||
import { RequestHandler } from '~/core/handlers/RequestHandler' | ||
import { LifeCycleEventsMap, SharedOptions } from '~/core/sharedOptions' | ||
import { RequiredDeep } from '~/core/typeUtils' | ||
import { handleRequest } from '~/core/utils/handleRequest' | ||
import { devUtils } from '~/core/utils/internal/devUtils' | ||
import { mergeRight } from '~/core/utils/internal/mergeRight' | ||
import { SetupServer } from './glossary' | ||
import { AsyncLocalStorage } from 'node:async_hooks' | ||
import { ClientRequestInterceptor } from '@mswjs/interceptors/ClientRequest' | ||
import { XMLHttpRequestInterceptor } from '@mswjs/interceptors/XMLHttpRequest' | ||
import { FetchInterceptor } from '@mswjs/interceptors/fetch' | ||
import { HandlersController } from '~/core/SetupApi' | ||
import type { RequestHandler } from '~/core/handlers/RequestHandler' | ||
import type { SetupServer } from './glossary' | ||
import { SetupServerCommonApi } from './SetupServerCommonApi' | ||
|
||
const DEFAULT_LISTEN_OPTIONS: RequiredDeep<SharedOptions> = { | ||
onUnhandledRequest: 'warn', | ||
} | ||
|
||
export class SetupServerApi | ||
extends SetupApi<LifeCycleEventsMap> | ||
implements SetupServer | ||
{ | ||
protected readonly interceptor: BatchInterceptor< | ||
Array<Interceptor<HttpRequestEventMap>>, | ||
HttpRequestEventMap | ||
> | ||
private resolvedOptions: RequiredDeep<SharedOptions> | ||
const store = new AsyncLocalStorage<RequestHandlersContext>() | ||
|
||
constructor( | ||
interceptors: Array<{ | ||
new (): Interceptor<HttpRequestEventMap> | ||
}>, | ||
...handlers: Array<RequestHandler> | ||
) { | ||
super(...handlers) | ||
type RequestHandlersContext = { | ||
initialHandlers: Array<RequestHandler> | ||
handlers: Array<RequestHandler> | ||
} | ||
|
||
this.interceptor = new BatchInterceptor({ | ||
name: 'setup-server', | ||
interceptors: interceptors.map((Interceptor) => new Interceptor()), | ||
}) | ||
this.resolvedOptions = {} as RequiredDeep<SharedOptions> | ||
/** | ||
* A handlers controller that utilizes `AsyncLocalStorage` in Node.js | ||
* to prevent the request handlers list from being a shared state | ||
* across mutliple tests. | ||
*/ | ||
class AsyncHandlersController implements HandlersController { | ||
private rootContext: RequestHandlersContext | ||
|
||
this.init() | ||
constructor(initialHandlers: Array<RequestHandler>) { | ||
this.rootContext = { initialHandlers, handlers: [] } | ||
} | ||
|
||
/** | ||
* Subscribe to all requests that are using the interceptor object | ||
*/ | ||
private init(): void { | ||
this.interceptor.on('request', async ({ request, requestId }) => { | ||
const response = await handleRequest( | ||
request, | ||
requestId, | ||
this.currentHandlers, | ||
this.resolvedOptions, | ||
this.emitter, | ||
) | ||
|
||
if (response) { | ||
request.respondWith(response) | ||
} | ||
get context(): RequestHandlersContext { | ||
return store.getStore() || this.rootContext | ||
} | ||
|
||
return | ||
}) | ||
public prepend(runtimeHandlers: Array<RequestHandler>) { | ||
this.context.handlers.unshift(...runtimeHandlers) | ||
} | ||
|
||
this.interceptor.on( | ||
'response', | ||
({ response, isMockedResponse, request, requestId }) => { | ||
this.emitter.emit( | ||
isMockedResponse ? 'response:mocked' : 'response:bypass', | ||
{ | ||
response, | ||
request, | ||
requestId, | ||
}, | ||
) | ||
}, | ||
) | ||
public reset(nextHandlers: Array<RequestHandler>) { | ||
const context = this.context | ||
context.handlers = [] | ||
context.initialHandlers = | ||
nextHandlers.length > 0 ? nextHandlers : context.initialHandlers | ||
} | ||
|
||
public listen(options: Partial<SharedOptions> = {}): void { | ||
this.resolvedOptions = mergeRight( | ||
DEFAULT_LISTEN_OPTIONS, | ||
options, | ||
) as RequiredDeep<SharedOptions> | ||
public currentHandlers(): Array<RequestHandler> { | ||
const { initialHandlers, handlers } = this.context | ||
return handlers.concat(initialHandlers) | ||
} | ||
} | ||
|
||
// Apply the interceptor when starting the server. | ||
this.interceptor.apply() | ||
export class SetupServerApi | ||
extends SetupServerCommonApi | ||
implements SetupServer | ||
{ | ||
constructor(handlers: Array<RequestHandler>) { | ||
super( | ||
[ClientRequestInterceptor, XMLHttpRequestInterceptor, FetchInterceptor], | ||
handlers, | ||
) | ||
|
||
this.subscriptions.push(() => { | ||
this.interceptor.dispose() | ||
}) | ||
this.handlersController = new AsyncHandlersController(handlers) | ||
} | ||
|
||
// Assert that the interceptor has been applied successfully. | ||
// Also guards us from forgetting to call "interceptor.apply()" | ||
// as a part of the "listen" method. | ||
invariant( | ||
[InterceptorReadyState.APPLYING, InterceptorReadyState.APPLIED].includes( | ||
this.interceptor.readyState, | ||
), | ||
devUtils.formatMessage( | ||
'Failed to start "setupServer": the interceptor failed to apply. This is likely an issue with the library and you should report it at "%s".', | ||
), | ||
'https://github.com/mswjs/msw/issues/new/choose', | ||
) | ||
public boundary<Fn extends (...args: Array<any>) => unknown>( | ||
callback: Fn, | ||
): (...args: Parameters<Fn>) => ReturnType<Fn> { | ||
return (...args: Parameters<Fn>): ReturnType<Fn> => { | ||
return store.run<any, any>( | ||
{ | ||
initialHandlers: this.handlersController.currentHandlers(), | ||
handlers: [], | ||
}, | ||
callback, | ||
...args, | ||
) | ||
} | ||
} | ||
|
||
public close(): void { | ||
this.dispose() | ||
super.close() | ||
store.disable() | ||
} | ||
} |
Oops, something went wrong.