213 lines
6.9 KiB
TypeScript
213 lines
6.9 KiB
TypeScript
import { Suspense, defineComponent, h } from 'vue'
|
|
import type { RouteLocationRaw, Router } from 'vue-router'
|
|
import { createMemoryHistory, createRouter } from 'vue-router'
|
|
import { createTestingPinia } from '@pinia/testing'
|
|
import type { RenderOptions } from '@testing-library/vue'
|
|
import { HttpResponse, http, matchRequestUrl } from 'msw'
|
|
import type { SetupServer } from 'msw/node'
|
|
import { setupServer } from 'msw/node'
|
|
import { afterAll, afterEach, beforeAll } from 'vitest'
|
|
import AppLink from 'src/components/AppLink.vue'
|
|
import { routes } from 'src/router'
|
|
|
|
export function createTestRouter(base?: string): Router {
|
|
return createRouter({
|
|
routes,
|
|
history: createMemoryHistory(base),
|
|
})
|
|
}
|
|
|
|
interface RenderOptionsArgs {
|
|
props: Record<string, unknown>
|
|
slots: Record<string, (...args: unknown[]) => unknown>
|
|
|
|
router?: Router
|
|
initialRoute: RouteLocationRaw
|
|
|
|
initialState: Record<string, unknown>
|
|
stubActions: boolean
|
|
}
|
|
|
|
const scheduler = typeof setImmediate === 'function' ? setImmediate : setTimeout
|
|
|
|
export function flushPromises(): Promise<void> {
|
|
return new Promise((resolve) => {
|
|
scheduler(resolve, 0)
|
|
})
|
|
}
|
|
|
|
export function renderOptions(): RenderOptions
|
|
export function renderOptions(args: Partial<Omit<RenderOptionsArgs, 'initialRoute'>>): RenderOptions
|
|
export async function renderOptions(args: (Partial<RenderOptionsArgs> & { initialRoute: RouteLocationRaw })): Promise<RenderOptions>
|
|
export function renderOptions(args: Partial<RenderOptionsArgs> = {}): RenderOptions | Promise<RenderOptions> {
|
|
const router = args.router || createTestRouter()
|
|
|
|
const result = {
|
|
props: args.props,
|
|
slots: args.slots,
|
|
global: {
|
|
plugins: [
|
|
router,
|
|
createTestingPinia({
|
|
initialState: {
|
|
user: { user: null },
|
|
...args.initialState,
|
|
},
|
|
stubActions: args.stubActions ?? false,
|
|
}),
|
|
],
|
|
components: { AppLink },
|
|
},
|
|
}
|
|
|
|
const { initialRoute } = args
|
|
|
|
if (!initialRoute)
|
|
return result
|
|
|
|
return new Promise((resolve) => {
|
|
void router.replace(initialRoute).then(() => resolve(result))
|
|
})
|
|
}
|
|
|
|
export function asyncWrapper(component: ReturnType<typeof defineComponent>, props?: Record<string, unknown>): ReturnType<typeof defineComponent> {
|
|
return defineComponent({
|
|
render() {
|
|
return h(
|
|
'div',
|
|
{ id: 'root' },
|
|
h(Suspense, null, {
|
|
default() {
|
|
// eslint-disable-next-line ts/no-unsafe-argument
|
|
return h(component, props)
|
|
},
|
|
fallback: h('div', 'Loading...'),
|
|
}),
|
|
)
|
|
},
|
|
})
|
|
}
|
|
|
|
async function waitForServerRequest(server: SetupServer, method: string, url: string, flush = true): Promise<Request> {
|
|
let expectedRequestId = ''
|
|
let expectedRequest: Request
|
|
|
|
const result = await new Promise<Request>((resolve, reject) => {
|
|
server.events.on('request:match', ({ request, requestId }) => {
|
|
const matchesMethod = request.method.toLowerCase() === method.toLowerCase()
|
|
const matchesUrl = matchRequestUrl(new URL(request.url), url)
|
|
if (matchesMethod && matchesUrl) {
|
|
expectedRequestId = requestId
|
|
expectedRequest = request
|
|
}
|
|
})
|
|
|
|
server.events.on('response:mocked', ({ requestId: reqId }) => {
|
|
if (reqId === expectedRequestId)
|
|
resolve(expectedRequest)
|
|
})
|
|
|
|
server.events.on('request:unhandled', ({ request: req, requestId: reqId }) => {
|
|
if (reqId === expectedRequestId)
|
|
reject(new Error(`The ${req.method} ${req.url} request was unhandled.`))
|
|
})
|
|
})
|
|
flush && await flushPromises()
|
|
return result
|
|
}
|
|
|
|
type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'all' | 'ALL'
|
|
|
|
type Listener =
|
|
| [HttpMethod, string, number, object]
|
|
| [HttpMethod, string, number]
|
|
| [HttpMethod, string, object]
|
|
| [string, number, object]
|
|
| [HttpMethod, string]
|
|
| [string, object]
|
|
| [string]
|
|
|
|
/**
|
|
* Sets up a mock server with provided listeners.
|
|
*
|
|
* @example
|
|
* const server = setupMockServer(
|
|
* ['/api/articles/markdown', { article }],
|
|
* ['/api/articles/markdown', 200, { article }],
|
|
* ['GET', '/api/articles/markdown', { article }],
|
|
* ['GET', '/api/articles/markdown', 200, { article }],
|
|
* ['DELETE', '/api/articles/comment'],
|
|
* ['DELETE', '/api/articles/comment', 204]
|
|
* )
|
|
*
|
|
* it('...', async () => {
|
|
* await server.waitForRequest('/api/articles/markdown')
|
|
* await server.waitForRequest('GET', '/api/articles/markdown')
|
|
* })
|
|
*/
|
|
|
|
export function setupMockServer(...listeners: Listener[]) {
|
|
const parseArgs = (args: Listener): [string, string, number, (object | null)] => {
|
|
if (args.length === 4)
|
|
return args
|
|
if (args.length === 3) {
|
|
if (typeof args[1] === 'number')
|
|
return ['all', args[0], args[1], args[2] as object] // ['all', path, 200, object]
|
|
if (typeof args[2] === 'number')
|
|
return [args[0], args[1], args[2], null] // [method, path, status, null]
|
|
return [args[0], args[1], 200, args[2]] // [method, path, 200, object]
|
|
}
|
|
if (args.length === 2) {
|
|
if (typeof args[1] === 'string')
|
|
return [args[0], args[1], 200, null]
|
|
return ['all', args[0], 200, args[1]]
|
|
}
|
|
return ['all', args[0], 200, null]
|
|
}
|
|
|
|
const server = setupServer(
|
|
...listeners.map((args) => {
|
|
let [method, path, status, response] = parseArgs(args)
|
|
method = method.toLowerCase()
|
|
return http[method as 'all'](`${import.meta.env.VITE_API_HOST}${path}`, () => {
|
|
return HttpResponse.json(response, { status })
|
|
})
|
|
}),
|
|
)
|
|
|
|
beforeAll(() => void server.listen())
|
|
afterEach(() => void server.resetHandlers())
|
|
afterAll(() => void server.close())
|
|
|
|
async function waitForRequest(path: string): Promise<Request>
|
|
async function waitForRequest(path: string, flush: boolean): Promise<Request>
|
|
async function waitForRequest(method: HttpMethod, path: string): Promise<Request>
|
|
async function waitForRequest(method: HttpMethod, path: string, flush: boolean): Promise<Request>
|
|
async function waitForRequest(...args: [string] | [string, boolean] | [HttpMethod, string] | [HttpMethod, string, boolean]): Promise<Request> {
|
|
const [method, path, flush] = args.length === 1
|
|
? ['all', args[0]] // ['all', path]
|
|
: args.length === 2 && typeof args[1] === 'boolean'
|
|
? ['all', args[0], args[1]] // ['all', path, flush]
|
|
: args.length === 2
|
|
? [args[0], args[1]] // [method, path]
|
|
: args // [method, path, flush]
|
|
return waitForServerRequest(server, method, path, flush)
|
|
}
|
|
|
|
const originalUse = server.use.bind(server)
|
|
|
|
function use(...listeners: Listener[]) {
|
|
originalUse(
|
|
...listeners.map((args) => {
|
|
let [method, path, status, response] = parseArgs(args)
|
|
method = method.toLowerCase()
|
|
return http[method as 'all'](`${import.meta.env.VITE_API_HOST}${path}`, () => {
|
|
return HttpResponse.json(response, { status })
|
|
})
|
|
}),
|
|
)
|
|
}
|
|
|
|
return Object.assign(server, { waitForRequest, use })
|
|
}
|