@velajs/testing 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -2
- package/dist/index.d.ts +484 -15
- package/dist/index.js +866 -14
- package/dist/index.js.map +1 -0
- package/dist/test-ws-connection-BHlEKwQz.js +166 -0
- package/dist/test-ws-connection-BHlEKwQz.js.map +1 -0
- package/dist/websocket-node/index.d.ts +1 -1
- package/dist/websocket-node/index.js +67 -102
- package/dist/websocket-node/index.js.map +1 -0
- package/package.json +59 -45
- package/dist/db/test-database.d.ts +0 -27
- package/dist/db/test-database.js +0 -19
- package/dist/http/path-utils.d.ts +0 -11
- package/dist/http/path-utils.js +0 -37
- package/dist/http/test-http-client.d.ts +0 -37
- package/dist/http/test-http-client.js +0 -61
- package/dist/http/test-http-request.d.ts +0 -46
- package/dist/http/test-http-request.js +0 -87
- package/dist/http/test-response.d.ts +0 -76
- package/dist/http/test-response.js +0 -166
- package/dist/sse/test-sse-connection.d.ts +0 -46
- package/dist/sse/test-sse-connection.js +0 -196
- package/dist/sse/test-sse-request.d.ts +0 -30
- package/dist/sse/test-sse-request.js +0 -62
- package/dist/test.d.ts +0 -5
- package/dist/test.js +0 -6
- package/dist/testing-module.builder.d.ts +0 -25
- package/dist/testing-module.builder.js +0 -160
- package/dist/testing-module.d.ts +0 -87
- package/dist/testing-module.js +0 -141
- package/dist/ws/test-ws-connection.d.ts +0 -41
- package/dist/ws/test-ws-connection.js +0 -105
- package/dist/ws/test-ws-request.d.ts +0 -43
- package/dist/ws/test-ws-request.js +0 -69
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["VelaApplication"],"sources":["../src/http/path-utils.ts","../src/http/test-response.ts","../src/http/test-http-request.ts","../src/http/test-http-client.ts","../src/sse/test-sse-connection.ts","../src/sse/test-sse-request.ts","../src/testing-module.ts","../src/testing-module.builder.ts","../src/test.ts"],"sourcesContent":["// Ported from @stratal/testing (MIT, © Temitayo Fadojutimi).\n\n/**\n * Read the value at a dot-notation path (e.g. `data.user.id`).\n * Returns `undefined` when any segment along the way is null/undefined.\n */\nexport function getValueAtPath(obj: unknown, path: string): unknown {\n const parts = path.split('.');\n let current: unknown = obj;\n\n for (const part of parts) {\n if (current === null || current === undefined) {\n return undefined;\n }\n current = (current as Record<string, unknown>)[part];\n }\n\n return current;\n}\n\n/**\n * Whether a dot-notation path exists on the object, even when the value at the\n * path is `null`/`undefined`. Distinguishes \"key present but null\" from \"key\n * absent\".\n */\nexport function hasValueAtPath(obj: unknown, path: string): boolean {\n const parts = path.split('.');\n let current: unknown = obj;\n\n for (const part of parts) {\n if (current === null || current === undefined) {\n return false;\n }\n\n if (typeof current !== 'object') {\n return false;\n }\n\n const record = current as Record<string, unknown>;\n\n if (!(part in record)) {\n return false;\n }\n\n current = record[part];\n }\n\n return true;\n}\n","// Ported from @stratal/testing (MIT, © Temitayo Fadojutimi), minus Macroable —\n// vela has no Macroable, so TestResponse is a plain class.\nimport { expect } from 'vitest';\nimport { getValueAtPath, hasValueAtPath } from './path-utils.js';\n\n/**\n * TestResponse\n *\n * Wraps a `Response` with fluent, chainable assertions. Synchronous status /\n * header assertions return `this`; JSON assertions (which must read the body)\n * return `Promise<this>`.\n *\n * @example\n * ```ts\n * const res = await module.http.get('/users/1').send();\n * res.assertOk();\n * await res.assertJsonPath('data.id', 1);\n * ```\n */\nexport class TestResponse {\n private jsonData: unknown = null;\n private textData: string | null = null;\n\n constructor(private readonly response: Response) {}\n\n /** The raw `Response`. */\n get raw(): Response {\n return this.response;\n }\n\n /** The response status code. */\n get status(): number {\n return this.response.status;\n }\n\n /** The response headers. */\n get headers(): Headers {\n return this.response.headers;\n }\n\n /** Parse (and cache) the response body as JSON. */\n async json<T = unknown>(): Promise<T> {\n if (this.jsonData === null) {\n this.jsonData = await this.response.clone().json();\n }\n return this.jsonData as T;\n }\n\n /** Read (and cache) the response body as text. */\n async text(): Promise<string> {\n this.textData ??= await this.response.clone().text();\n return this.textData;\n }\n\n // ============================================================\n // Status assertions\n // ============================================================\n\n /** Assert status is 200 OK. */\n assertOk(): this {\n return this.assertStatus(200);\n }\n\n /** Assert status is 201 Created. */\n assertCreated(): this {\n return this.assertStatus(201);\n }\n\n /** Assert status is 204 No Content. */\n assertNoContent(): this {\n return this.assertStatus(204);\n }\n\n /** Assert status is 400 Bad Request. */\n assertBadRequest(): this {\n return this.assertStatus(400);\n }\n\n /** Assert status is 401 Unauthorized. */\n assertUnauthorized(): this {\n return this.assertStatus(401);\n }\n\n /** Assert status is 403 Forbidden. */\n assertForbidden(): this {\n return this.assertStatus(403);\n }\n\n /** Assert status is 404 Not Found. */\n assertNotFound(): this {\n return this.assertStatus(404);\n }\n\n /** Assert status is 422 Unprocessable Entity. */\n assertUnprocessable(): this {\n return this.assertStatus(422);\n }\n\n /** Assert status is 500 Internal Server Error. */\n assertServerError(): this {\n return this.assertStatus(500);\n }\n\n /** Assert the response has the given status code. */\n assertStatus(expected: number): this {\n expect(this.response.status, `Expected status ${expected}, got ${this.response.status}`).toBe(\n expected,\n );\n return this;\n }\n\n /** Assert the status is in the 2xx range. */\n assertSuccessful(): this {\n expect(\n this.response.status >= 200 && this.response.status < 300,\n `Expected successful status (2xx), got ${this.response.status}`,\n ).toBe(true);\n return this;\n }\n\n // ============================================================\n // JSON assertions\n // ============================================================\n\n /** Assert each key in `expected` equals the corresponding top-level value. */\n async assertJson(expected: Record<string, unknown>): Promise<this> {\n const actual = await this.json<Record<string, unknown>>();\n\n for (const [key, value] of Object.entries(expected)) {\n expect(\n actual[key],\n `Expected JSON key \"${key}\" to be ${JSON.stringify(value)}, got ${JSON.stringify(actual[key])}`,\n ).toStrictEqual(value);\n }\n\n return this;\n }\n\n /** Assert the value at a dot-notation path equals `expected`. */\n async assertJsonPath(path: string, expected: unknown): Promise<this> {\n const json = await this.json();\n const actual = getValueAtPath(json, path);\n\n expect(\n actual,\n `Expected JSON path \"${path}\" to be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,\n ).toStrictEqual(expected);\n\n return this;\n }\n\n /** Assert every path/value pair in `expectations` matches (batch assert). */\n async assertJsonPaths(expectations: Record<string, unknown>): Promise<this> {\n const json = await this.json();\n\n for (const [path, expected] of Object.entries(expectations)) {\n const actual = getValueAtPath(json, path);\n expect(\n actual,\n `Expected JSON path \"${path}\" to be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,\n ).toStrictEqual(expected);\n }\n\n return this;\n }\n\n /** Assert the top-level JSON object has every key in `structure`. */\n async assertJsonStructure(structure: string[]): Promise<this> {\n const json = await this.json<Record<string, unknown>>();\n\n for (const key of structure) {\n expect(\n key in json,\n `Expected JSON to have key \"${key}\", got keys: ${JSON.stringify(Object.keys(json))}`,\n ).toBe(true);\n }\n\n return this;\n }\n\n /** Assert a path exists (value may be anything, including `null`). */\n async assertJsonPathExists(path: string): Promise<this> {\n const json = await this.json();\n\n expect(hasValueAtPath(json, path), `Expected JSON path \"${path}\" to exist`).toBe(true);\n\n return this;\n }\n\n /** Assert a path does not exist. */\n async assertJsonPathMissing(path: string): Promise<this> {\n const json = await this.json();\n\n expect(hasValueAtPath(json, path), `Expected JSON path \"${path}\" to not exist`).toBe(false);\n\n return this;\n }\n\n /** Assert the value at a path satisfies a predicate. */\n async assertJsonPathMatches(path: string, matcher: (value: unknown) => boolean): Promise<this> {\n const json = await this.json();\n const value = getValueAtPath(json, path);\n\n expect(\n matcher(value),\n `Expected JSON path \"${path}\" to match predicate, got ${JSON.stringify(value)}`,\n ).toBe(true);\n\n return this;\n }\n\n /** Assert the string value at a path contains `substring`. */\n async assertJsonPathContains(path: string, substring: string): Promise<this> {\n const json = await this.json();\n const value = getValueAtPath(json, path);\n\n expect(\n typeof value === 'string',\n `Expected JSON path \"${path}\" to be a string, got ${typeof value}`,\n ).toBe(true);\n\n expect(\n (value as string).includes(substring),\n `Expected JSON path \"${path}\" to contain \"${substring}\", got \"${String(value)}\"`,\n ).toBe(true);\n\n return this;\n }\n\n /** Assert the array value at a path includes `item`. */\n async assertJsonPathIncludes(path: string, item: unknown): Promise<this> {\n const json = await this.json();\n const value = getValueAtPath(json, path);\n\n expect(\n Array.isArray(value),\n `Expected JSON path \"${path}\" to be an array, got ${typeof value}`,\n ).toBe(true);\n\n expect(\n (value as unknown[]).includes(item),\n `Expected JSON path \"${path}\" to include ${JSON.stringify(item)}`,\n ).toBe(true);\n\n return this;\n }\n\n /** Assert the array value at a path has `count` items. */\n async assertJsonPathCount(path: string, count: number): Promise<this> {\n const json = await this.json();\n const value = getValueAtPath(json, path);\n\n expect(\n Array.isArray(value),\n `Expected JSON path \"${path}\" to be an array, got ${typeof value}`,\n ).toBe(true);\n\n expect(\n (value as unknown[]).length,\n `Expected JSON path \"${path}\" to have ${count} items, got ${(value as unknown[]).length}`,\n ).toBe(count);\n\n return this;\n }\n\n // ============================================================\n // Header assertions\n // ============================================================\n\n /** Assert a header is present, optionally equal to `expected`. */\n assertHeader(name: string, expected?: string): this {\n const actual = this.response.headers.get(name);\n\n expect(actual !== null, `Expected header \"${name}\" to be present`).toBe(true);\n\n if (expected !== undefined) {\n expect(actual, `Expected header \"${name}\" to be \"${expected}\", got \"${actual}\"`).toBe(\n expected,\n );\n }\n\n return this;\n }\n\n /** Assert a header is absent. */\n assertHeaderMissing(name: string): this {\n const actual = this.response.headers.get(name);\n\n expect(actual, `Expected header \"${name}\" to be absent, but got \"${actual}\"`).toBeNull();\n\n return this;\n }\n}\n","// Adapted from @stratal/testing (MIT, © Temitayo Fadojutimi): stratal's hard\n// AuthService import is replaced by a generic auth-resolver seam so\n// @velajs/testing stays free of optional-package dependencies.\nimport type { ActingAsResolver, TestPrincipal, TestingModule } from '../testing-module.js';\nimport { TestResponse } from './test-response.js';\n\n/**\n * TestHttpRequest\n *\n * Fluent builder for a single test HTTP request. `send()` builds a `Request`\n * and drives it through `module.fetch()` (the full Hono pipeline).\n *\n * @example\n * ```ts\n * const res = await module.http\n * .post('/users')\n * .withBody({ name: 'A' })\n * .withHeaders({ 'X-Trace': '1' })\n * .send();\n * res.assertCreated();\n * ```\n */\nexport class TestHttpRequest {\n private body: unknown = undefined;\n private readonly requestHeaders: Headers;\n private principal: TestPrincipal | null = null;\n private resolver: ActingAsResolver | null = null;\n\n constructor(\n private readonly method: string,\n private readonly path: string,\n headers: Headers,\n private readonly module: TestingModule,\n private readonly host: string | null = null,\n ) {\n this.requestHeaders = new Headers(headers);\n }\n\n /** Set the request body (JSON-serialized on send). */\n withBody(data: unknown): this {\n this.body = data;\n return this;\n }\n\n /** Merge additional headers. */\n withHeaders(headers: Record<string, string>): this {\n for (const [key, value] of Object.entries(headers)) {\n this.requestHeaders.set(key, value);\n }\n return this;\n }\n\n /** Set `Content-Type: application/json`. */\n asJson(): this {\n this.requestHeaders.set('Content-Type', 'application/json');\n return this;\n }\n\n /**\n * Authenticate the request as `principal`. The `resolver` (or a default one\n * registered via `module.setAuthResolver`) turns the principal into request\n * headers. The resolver signature `(module, principal) => Promise<Headers>`\n * is the cross-package contract sibling packages (e.g. `@velajs/better-auth`)\n * build against.\n */\n actingAs(principal: TestPrincipal, resolver?: ActingAsResolver): this {\n this.principal = principal;\n this.resolver = resolver ?? null;\n return this;\n }\n\n /** Build the `Request` and send it through `module.fetch()`. */\n async send(): Promise<TestResponse> {\n await this.applyAuthentication();\n\n const hasBody = this.body !== undefined && this.body !== null;\n if (hasBody && !this.requestHeaders.has('Content-Type')) {\n this.requestHeaders.set('Content-Type', 'application/json');\n }\n\n const url = new URL(this.path, `http://${this.host ?? 'localhost'}`);\n const request = new Request(url.toString(), {\n method: this.method,\n headers: this.requestHeaders,\n body: hasBody ? JSON.stringify(this.body) : null,\n });\n\n const response = await this.module.fetch(request);\n return new TestResponse(response);\n }\n\n private async applyAuthentication(): Promise<void> {\n if (!this.principal) return;\n\n const resolver = this.resolver ?? this.module.getAuthResolver();\n if (!resolver) {\n throw new Error(\n 'actingAs() requires an auth resolver. Pass one explicitly — ' +\n 'actingAs(principal, resolver) — or register a default with ' +\n 'module.setAuthResolver(resolver). For better-auth: ' +\n 'import { actingAs } from \"@velajs/better-auth/testing\".',\n );\n }\n\n const headers = await resolver(this.module, this.principal);\n for (const [key, value] of headers.entries()) {\n this.requestHeaders.set(key, value);\n }\n }\n}\n","// Adapted from @stratal/testing (MIT, © Temitayo Fadojutimi). Stratal's i18n\n// `withLocale` is dropped (vela i18n differs — optional follow-up).\nimport type { TestingModule } from '../testing-module.js';\nimport { TestHttpRequest } from './test-http-request.js';\n\n/**\n * TestHttpClient\n *\n * Fluent entry point for test HTTP requests. `forHost`/`withHeaders` return a\n * new immutable client; the verb methods start a {@link TestHttpRequest}.\n *\n * @example\n * ```ts\n * const res = await module.http\n * .forHost('example.com')\n * .post('/users')\n * .withBody({ name: 'A' })\n * .send();\n * res.assertCreated();\n * ```\n */\nexport class TestHttpClient {\n constructor(\n private readonly module: TestingModule,\n private readonly host: string | null = null,\n private readonly defaultHeaders: Headers = new Headers(),\n ) {}\n\n /**\n * Return a new client bound to `host`. Also sets the `Host` header so domain\n * routing works even when the runtime reads the header rather than the URL.\n */\n forHost(host: string): TestHttpClient {\n const headers = new Headers(this.defaultHeaders);\n headers.set('Host', host);\n return new TestHttpClient(this.module, host, headers);\n }\n\n /** Return a new client with additional default headers on every request. */\n withHeaders(headers: Record<string, string>): TestHttpClient {\n const next = new Headers(this.defaultHeaders);\n for (const [key, value] of Object.entries(headers)) {\n next.set(key, value);\n }\n return new TestHttpClient(this.module, this.host, next);\n }\n\n get(path: string): TestHttpRequest {\n return this.createRequest('GET', path);\n }\n\n post(path: string): TestHttpRequest {\n return this.createRequest('POST', path);\n }\n\n put(path: string): TestHttpRequest {\n return this.createRequest('PUT', path);\n }\n\n patch(path: string): TestHttpRequest {\n return this.createRequest('PATCH', path);\n }\n\n delete(path: string): TestHttpRequest {\n return this.createRequest('DELETE', path);\n }\n\n private createRequest(method: string, path: string): TestHttpRequest {\n return new TestHttpRequest(method, path, this.defaultHeaders, this.module, this.host);\n }\n}\n","// Ported near-verbatim from @stratal/testing (MIT, © Temitayo Fadojutimi).\n// Web-standard only (ReadableStream + TextDecoder), so it is edge-pure.\nimport { expect } from 'vitest';\n\n/** A parsed Server-Sent Event. */\nexport interface TestSseEvent {\n data: string;\n event?: string;\n id?: string;\n retry?: number;\n}\n\n/**\n * TestSseConnection\n *\n * Reads a streaming `text/event-stream` response body and exposes queue-based\n * wait/assert helpers over the parsed events.\n *\n * @example\n * ```ts\n * const sse = await module.sse('/stream/events').connect();\n * await sse.assertEventData('ping');\n * await sse.waitForEnd();\n * ```\n */\nexport class TestSseConnection {\n private readonly eventQueue: TestSseEvent[] = [];\n private eventWaiters: ((event: TestSseEvent) => void)[] = [];\n private streamEnded = false;\n private endWaiters: (() => void)[] = [];\n\n constructor(private readonly response: Response) {\n this.startReading();\n }\n\n /** The raw `Response`. */\n get raw(): Response {\n return this.response;\n }\n\n /** Wait for the next event (rejects after `timeout` ms). */\n async waitForEvent(timeout = 5000): Promise<TestSseEvent> {\n if (this.eventQueue.length > 0) {\n return this.eventQueue.shift()!;\n }\n\n if (this.streamEnded) {\n throw new Error('SSE: stream has ended, no more events');\n }\n\n return new Promise<TestSseEvent>((resolve, reject) => {\n const waiter = (event: TestSseEvent): void => {\n clearTimeout(timer);\n resolve(event);\n };\n\n const timer = setTimeout(() => {\n const index = this.eventWaiters.indexOf(waiter);\n if (index !== -1) this.eventWaiters.splice(index, 1);\n reject(new Error(`SSE: no event received within ${timeout}ms`));\n }, timeout);\n\n this.eventWaiters.push(waiter);\n });\n }\n\n /** Wait for the stream to end (rejects after `timeout` ms). */\n async waitForEnd(timeout = 5000): Promise<void> {\n if (this.streamEnded) return;\n\n return new Promise<void>((resolve, reject) => {\n const waiter = (): void => {\n clearTimeout(timer);\n resolve();\n };\n\n const timer = setTimeout(() => {\n const index = this.endWaiters.indexOf(waiter);\n if (index !== -1) this.endWaiters.splice(index, 1);\n reject(new Error(`SSE: stream did not end within ${timeout}ms`));\n }, timeout);\n\n this.endWaiters.push(waiter);\n });\n }\n\n /** Collect all remaining events until the stream ends. */\n async collectEvents(timeout = 5000): Promise<TestSseEvent[]> {\n const events: TestSseEvent[] = [];\n\n if (this.streamEnded) {\n return [...this.eventQueue.splice(0)];\n }\n\n return new Promise<TestSseEvent[]>((resolve, reject) => {\n const originalDispatch = this.dispatchEvent.bind(this);\n this.dispatchEvent = (event: TestSseEvent): void => {\n events.push(event);\n originalDispatch(event);\n };\n\n const endWaiter = (): void => {\n clearTimeout(timer);\n this.dispatchEvent = originalDispatch;\n resolve(events);\n };\n\n const timer = setTimeout(() => {\n this.dispatchEvent = originalDispatch;\n const index = this.endWaiters.indexOf(endWaiter);\n if (index !== -1) this.endWaiters.splice(index, 1);\n reject(new Error(`SSE: stream did not end within ${timeout}ms`));\n }, timeout);\n\n events.push(...this.eventQueue.splice(0));\n\n this.endWaiters.push(endWaiter);\n });\n }\n\n /** Assert the next event matches the expected partial shape. */\n async assertEvent(expected: Partial<TestSseEvent>, timeout = 5000): Promise<void> {\n const event = await this.waitForEvent(timeout);\n expect(event).toMatchObject(expected);\n }\n\n /** Assert the next event's `data` equals `expected`. */\n async assertEventData(expected: string, timeout = 5000): Promise<void> {\n const event = await this.waitForEvent(timeout);\n expect(event.data, `Expected SSE data \"${expected}\", got \"${event.data}\"`).toBe(expected);\n }\n\n /** Assert the next event's `data` is JSON equal to `expected`. */\n async assertJsonEventData<T>(expected: T, timeout = 5000): Promise<void> {\n const event = await this.waitForEvent(timeout);\n const parsed = JSON.parse(event.data) as unknown;\n expect(parsed).toEqual(expected);\n }\n\n private startReading(): void {\n const body = this.response.body;\n if (!body) {\n this.streamEnded = true;\n return;\n }\n\n const reader = body.getReader() as ReadableStreamDefaultReader<Uint8Array>;\n const decoder = new TextDecoder();\n let buffer = '';\n\n const read = async (): Promise<void> => {\n try {\n for (;;) {\n const { done, value } = await reader.read();\n\n if (done) {\n if (buffer.trim()) {\n const event = this.parseEvent(buffer);\n if (event) this.dispatchEvent(event);\n }\n this.endStream();\n return;\n }\n\n buffer += decoder.decode(value, { stream: true });\n\n const parts = buffer.split('\\n\\n');\n buffer = parts.pop()!;\n\n for (const part of parts) {\n if (!part.trim()) continue;\n const event = this.parseEvent(part);\n if (event) this.dispatchEvent(event);\n }\n }\n } catch {\n this.endStream();\n }\n };\n\n void read();\n }\n\n private endStream(): void {\n this.streamEnded = true;\n for (const waiter of this.endWaiters) {\n waiter();\n }\n this.endWaiters = [];\n }\n\n private parseEvent(raw: string): TestSseEvent | null {\n const lines = raw.split('\\n');\n const dataLines: string[] = [];\n let event: string | undefined;\n let id: string | undefined;\n let retry: number | undefined;\n\n for (const line of lines) {\n if (line.startsWith(':')) continue; // comment line\n\n const colonIndex = line.indexOf(':');\n if (colonIndex === -1) continue;\n\n const field = line.slice(0, colonIndex);\n const value =\n line[colonIndex + 1] === ' ' ? line.slice(colonIndex + 2) : line.slice(colonIndex + 1);\n\n switch (field) {\n case 'data':\n dataLines.push(value);\n break;\n case 'event':\n event = value;\n break;\n case 'id':\n id = value;\n break;\n case 'retry': {\n const parsed = parseInt(value, 10);\n if (!Number.isNaN(parsed)) retry = parsed;\n break;\n }\n }\n }\n\n if (dataLines.length === 0) return null;\n\n const result: TestSseEvent = { data: dataLines.join('\\n') };\n if (event !== undefined) result.event = event;\n if (id !== undefined) result.id = id;\n if (retry !== undefined) result.retry = retry;\n\n return result;\n }\n\n private dispatchEvent(event: TestSseEvent): void {\n if (this.eventWaiters.length > 0) {\n this.eventWaiters.shift()!(event);\n } else {\n this.eventQueue.push(event);\n }\n }\n}\n","// Adapted from @stratal/testing (MIT, © Temitayo Fadojutimi). Auth uses the\n// generic resolver seam instead of a hard AuthService import.\nimport { expect } from 'vitest';\nimport type { ActingAsResolver, TestPrincipal, TestingModule } from '../testing-module.js';\nimport { TestSseConnection } from './test-sse-connection.js';\n\n/**\n * TestSseRequest\n *\n * Builder for a Server-Sent Events connection. `connect()` issues a GET through\n * `module.fetch()`, asserts a `text/event-stream` 200, and wraps the streaming\n * body in a {@link TestSseConnection}.\n *\n * @example\n * ```ts\n * const sse = await module.sse('/stream/events').connect();\n * await sse.assertEvent({ event: 'message', data: 'hello' });\n * ```\n */\nexport class TestSseRequest {\n private readonly requestHeaders = new Headers();\n private principal: TestPrincipal | null = null;\n private resolver: ActingAsResolver | null = null;\n\n constructor(\n private readonly path: string,\n private readonly module: TestingModule,\n ) {}\n\n /** Merge additional headers onto the SSE request. */\n withHeaders(headers: Record<string, string>): this {\n for (const [key, value] of Object.entries(headers)) {\n this.requestHeaders.set(key, value);\n }\n return this;\n }\n\n /** Authenticate the connection (see {@link TestHttpRequest.actingAs}). */\n actingAs(principal: TestPrincipal, resolver?: ActingAsResolver): this {\n this.principal = principal;\n this.resolver = resolver ?? null;\n return this;\n }\n\n /** Open the stream and return a live {@link TestSseConnection}. */\n async connect(): Promise<TestSseConnection> {\n await this.applyAuthentication();\n\n this.requestHeaders.set('Accept', 'text/event-stream');\n\n const url = new URL(this.path, 'http://localhost');\n const request = new Request(url.toString(), { headers: this.requestHeaders });\n\n const response = await this.module.fetch(request);\n\n expect(response.status, `Expected status 200, got ${response.status}`).toBe(200);\n\n const contentType = response.headers.get('content-type') ?? '';\n expect(\n contentType.includes('text/event-stream'),\n `Expected content-type \"text/event-stream\", got \"${contentType}\"`,\n ).toBe(true);\n\n return new TestSseConnection(response);\n }\n\n private async applyAuthentication(): Promise<void> {\n if (!this.principal) return;\n\n const resolver = this.resolver ?? this.module.getAuthResolver();\n if (!resolver) {\n throw new Error(\n 'actingAs() requires an auth resolver. Pass one explicitly or register ' +\n 'a default with module.setAuthResolver(resolver).',\n );\n }\n\n const headers = await resolver(this.module, this.principal);\n for (const [key, value] of headers.entries()) {\n this.requestHeaders.set(key, value);\n }\n }\n}\n","import { Context } from 'hono';\nimport {\n REQUEST_CONTEXT,\n type RequestContext,\n type Token,\n type Type,\n type VelaApplication,\n} from '@velajs/vela';\nimport type { Container } from '@velajs/vela/internal';\nimport { SeederRegistry, type ISeeder } from '@velajs/vela/seeder';\nimport { expect } from 'vitest';\nimport type { TestDatabase } from './db/test-database.js';\nimport { TestHttpClient } from './http/test-http-client.js';\nimport { TestSseRequest } from './sse/test-sse-request.js';\nimport { TestWsRequest } from './ws/test-ws-request.js';\n\n/** A test principal — an opaque object the auth resolver turns into headers. */\nexport type TestPrincipal = Record<string, unknown>;\n\n/**\n * Turns a principal into request headers (session cookie, bearer token, …).\n * The signature `(module, principal) => Promise<Headers>` is a cross-package\n * contract: sibling packages (e.g. `@velajs/better-auth/testing`) build a\n * resolver against it. Kept generic so `@velajs/testing` needs no auth deps.\n */\nexport type ActingAsResolver = (\n module: TestingModule,\n principal: TestPrincipal,\n) => Promise<Headers>;\n\ntype HonoApp = ReturnType<VelaApplication['getHonoApp']>;\n\n/**\n * TestingModule\n *\n * The compiled test harness. Beyond `get`/`createApplication`/`close`, it adds\n * Laravel-flavored ergonomics: a fluent HTTP client, SSE/WS builders, request-\n * scope execution, seeding, and database assertion wrappers.\n *\n * @example\n * ```ts\n * const module = await Test.createTestingModule({ imports: [AppModule] }).compile();\n * await module.http.post('/users').withBody({ name: 'A' }).send()\n * .then((r) => r.assertCreated());\n * ```\n */\nexport class TestingModule {\n private _http: TestHttpClient | null = null;\n private honoApp: HonoApp | null = null;\n private authResolver: ActingAsResolver | null = null;\n\n constructor(\n private readonly app: VelaApplication,\n private readonly container: Container,\n ) {}\n\n /** Resolve a provider from the root container. */\n get<T>(token: Token<T>): T {\n return this.container.resolve(token);\n }\n\n /** Build (once) and return the underlying application. */\n async createApplication(): Promise<VelaApplication> {\n await this.app.initRoutes();\n return this.app;\n }\n\n /** Lazy fluent HTTP client bound to this module. */\n get http(): TestHttpClient {\n this._http ??= new TestHttpClient(this);\n return this._http;\n }\n\n /** Start an SSE connection builder for `path`. */\n sse(path: string): TestSseRequest {\n return new TestSseRequest(path, this);\n }\n\n /** Start a WebSocket connection builder for `path` (needs a transport adapter). */\n ws(path: string): TestWsRequest {\n return new TestWsRequest(path, this);\n }\n\n /**\n * Drive a `Request` through the full Hono pipeline. The Hono app is built\n * once and reused across requests.\n */\n async fetch(request: Request, env?: unknown, ctx?: unknown): Promise<Response> {\n const hono = await this.ensureHono();\n return hono.fetch(request, env as never, ctx as never);\n }\n\n /**\n * Register a default auth resolver used by `actingAs(principal)` when no\n * resolver is passed explicitly.\n */\n setAuthResolver(resolver: ActingAsResolver): this {\n this.authResolver = resolver;\n return this;\n }\n\n /** The default auth resolver, if one was registered. */\n getAuthResolver(): ActingAsResolver | null {\n return this.authResolver;\n }\n\n /**\n * Run `callback` inside a request-scoped child container seeded with a mock\n * {@link RequestContext}, so REQUEST-scoped providers (and anything injecting\n * `REQUEST_CONTEXT`) resolve. The child is disposed afterwards.\n */\n async runInRequestScope<T>(callback: (container: Container) => T | Promise<T>): Promise<T> {\n const child = this.container.createChild();\n child.setRequestInstance(REQUEST_CONTEXT, this.createMockRequestContext());\n try {\n return await callback(child);\n } finally {\n await child.dispose();\n }\n }\n\n /**\n * Run the given `@Seeder()` classes, each in its own request scope. Throws if\n * a class is not a registered seeder. Requires `SeederModule` (or the seeders\n * themselves) to be present in the module graph.\n */\n async seed(...SeederClasses: Type<ISeeder>[]): Promise<void> {\n const registry = this.container.resolve(SeederRegistry);\n const known = new Set<unknown>(registry.list().map((s) => s.target));\n\n for (const SeederClass of SeederClasses) {\n if (!known.has(SeederClass)) {\n throw new Error(\n `Seeder \"${SeederClass.name}\" is not registered. Add it to a module's ` +\n 'providers or SeederModule.forRoot({ seeders: [...] }).',\n );\n }\n await this.runInRequestScope(async (child) => {\n const instance = child.resolve<ISeeder>(SeederClass);\n await instance.run();\n });\n }\n }\n\n /** Assert a row matching `where` exists in `table` (via a {@link TestDatabase}). */\n async assertDatabaseHas(\n db: TestDatabase,\n table: string,\n where: Record<string, unknown>,\n ): Promise<void> {\n const exists = await db.has(table, where);\n expect(exists, `Expected ${table} to have a row matching ${JSON.stringify(where)}`).toBe(true);\n }\n\n /** Assert no row matching `where` exists in `table`. */\n async assertDatabaseMissing(\n db: TestDatabase,\n table: string,\n where: Record<string, unknown>,\n ): Promise<void> {\n const exists = await db.has(table, where);\n expect(exists, `Expected ${table} NOT to have a row matching ${JSON.stringify(where)}`).toBe(\n false,\n );\n }\n\n /** Assert `table` has exactly `expected` rows. */\n async assertDatabaseCount(db: TestDatabase, table: string, expected: number): Promise<void> {\n const actual = await db.count(table);\n expect(actual, `Expected ${table} count ${expected}, got ${actual}`).toBe(expected);\n }\n\n /** Dispose the application. */\n async close(signal?: string): Promise<void> {\n await this.app.close(signal);\n }\n\n private async ensureHono(): Promise<HonoApp> {\n if (!this.honoApp) {\n const app = await this.createApplication();\n this.honoApp = app.getHonoApp();\n }\n return this.honoApp;\n }\n\n /**\n * Build a minimal, functional {@link RequestContext} for out-of-band request\n * scopes. Vela has no `createMockRouterContext`; a real (empty) Hono `Context`\n * backs the `hono` field so nothing dangles.\n */\n private createMockRequestContext(): RequestContext {\n const bag = new Map<string | symbol, unknown>();\n const request = new Request('http://localhost/');\n const hono = new Context(request);\n return {\n id: crypto.randomUUID(),\n receivedAt: new Date(),\n request,\n hono,\n set(key, value) {\n bag.set(key, value);\n },\n get<V>(key: string | symbol): V | undefined {\n return bag.get(key) as V | undefined;\n },\n has(key) {\n return bag.has(key);\n },\n };\n }\n}\n","import {\n DiscoveryService,\n REQUEST_CONTEXT,\n Scope,\n type ModuleOptions,\n type ProviderOptions,\n type Token,\n type Type,\n} from '@velajs/vela';\nimport {\n APP_FILTER,\n APP_GUARD,\n APP_INTERCEPTOR,\n APP_MIDDLEWARE,\n APP_PIPE,\n Container,\n MetadataRegistry,\n ModuleLoader,\n ModuleRef,\n RouteManager,\n VelaApplication,\n bindAppProviders,\n} from '@velajs/vela/internal';\nimport { TestingModule } from './testing-module.js';\n\ninterface OverrideEntry {\n token: Token;\n provider: ProviderOptions;\n}\n\nexport class OverrideBy {\n constructor(\n private readonly builder: TestingModuleBuilder,\n private readonly token: Token,\n ) {}\n\n useValue(value: unknown): TestingModuleBuilder {\n this.builder['addOverride']({\n token: this.token,\n provider: { provide: this.token, useValue: value },\n });\n return this.builder;\n }\n\n useClass(cls: Type): TestingModuleBuilder {\n this.builder['addOverride']({\n token: this.token,\n provider: { provide: this.token, useClass: cls },\n });\n return this.builder;\n }\n\n useFactory(options: {\n factory: (...args: unknown[]) => unknown;\n inject?: Token[];\n }): TestingModuleBuilder {\n this.builder['addOverride']({\n token: this.token,\n provider: {\n provide: this.token,\n useFactory: options.factory,\n inject: options.inject,\n },\n });\n return this.builder;\n }\n}\n\nexport class TestingModuleBuilder {\n private overrides: OverrideEntry[] = [];\n\n constructor(private readonly metadata: ModuleOptions) {}\n\n overrideProvider(token: Token): OverrideBy {\n return new OverrideBy(this, token);\n }\n\n overrideGuard(guard: Type): OverrideBy {\n return new OverrideBy(this, guard);\n }\n\n overridePipe(pipe: Type): OverrideBy {\n return new OverrideBy(this, pipe);\n }\n\n overrideInterceptor(interceptor: Type): OverrideBy {\n return new OverrideBy(this, interceptor);\n }\n\n overrideFilter(filter: Type): OverrideBy {\n return new OverrideBy(this, filter);\n }\n\n private addOverride(entry: OverrideEntry): void {\n const idx = this.overrides.findIndex((o) => o.token === entry.token);\n if (idx !== -1) {\n this.overrides[idx] = entry;\n } else {\n this.overrides.push(entry);\n }\n }\n\n async compile(): Promise<TestingModule> {\n class TestRootModule {}\n MetadataRegistry.setModuleOptions(TestRootModule, {\n imports: this.metadata.imports,\n providers: this.metadata.providers,\n controllers: this.metadata.controllers,\n exports: this.metadata.exports,\n });\n\n const container = new Container();\n\n // Replicate bootstrap()'s global token setup so request-pipeline tests\n // can resolve REQUEST_CONTEXT, ModuleRef, and the APP_* sentinel tokens\n // from any module scope. Without this, guards / decorators that inject\n // REQUEST_CONTEXT through ExecutionContext fail with \"no provider\"\n // when the request enters the pipeline. See vela/src/factory/bootstrap.ts.\n container.register({ provide: Container, useValue: container });\n container.markGlobalToken(Container);\n\n container.register({\n provide: ModuleRef,\n useFactory: (c: Container) => new ModuleRef(c),\n inject: [Container],\n });\n container.markGlobalToken(ModuleRef);\n\n // Decorator-driven discovery — global so any provider can inject it, and so\n // callOnApplicationBootstrap() reuses this instance instead of self-building\n // one. Mirrors vela/src/factory/bootstrap.ts.\n container.register({\n provide: DiscoveryService,\n useFactory: (c: Container) => new DiscoveryService(c),\n inject: [Container],\n });\n container.markGlobalToken(DiscoveryService);\n\n for (const t of [APP_GUARD, APP_PIPE, APP_INTERCEPTOR, APP_FILTER, APP_MIDDLEWARE]) {\n container.markGlobalToken(t);\n }\n\n // REQUEST_CONTEXT is seeded into each per-request child container by\n // RouteManager via setRequestInstance. Registering with a throw-factory\n // here ensures findRegistration succeeds (so the child's cached request\n // instance is returned) while surfacing a clear error if the token is\n // ever resolved outside the request path.\n container.register({\n provide: REQUEST_CONTEXT,\n scope: Scope.REQUEST,\n useFactory: () => {\n throw new Error(\n 'REQUEST_CONTEXT can only be resolved inside a request — ' +\n 'it is seeded by RouteManager when the request enters the pipeline.',\n );\n },\n });\n container.markGlobalToken(REQUEST_CONTEXT);\n\n // Pre-register at root so `moduleRef.get(token)` and framework-internal\n // resolves (instantiate(guard, container) with no requestingModuleId) see\n // the override immediately.\n for (const override of this.overrides) {\n container.register(override.provider);\n }\n\n const routeManager = new RouteManager(container);\n\n const loader = new ModuleLoader(container, routeManager);\n loader.load(TestRootModule);\n\n // Force-apply overrides into every module bucket that already holds the\n // token (plus root). Without this, controller constructor-injection (which\n // passes requestingModuleId to findRegistration) finds the module's own\n // registration first and never consults the root override. The default\n // 'all-existing' buckets replace every non-root bucket holding the token\n // and re-register at root — the supported form of the old private loop.\n for (const override of this.overrides) {\n container.replaceProvider(override.provider);\n }\n\n bindAppProviders(routeManager, container, loader);\n\n const app = new VelaApplication(container, routeManager);\n const instances = await loader.resolveAllInstances();\n app.setInstances(instances);\n\n await app.callOnModuleInit();\n await app.callOnApplicationBootstrap();\n await app.initRoutes();\n\n return new TestingModule(app, container);\n }\n}\n","import type { ModuleOptions } from '@velajs/vela';\nimport { TestingModuleBuilder } from './testing-module.builder.js';\n\nexport const Test = {\n createTestingModule(metadata: ModuleOptions): TestingModuleBuilder {\n return new TestingModuleBuilder(metadata);\n },\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { expect } from "vitest";
|
|
2
|
+
//#region src/ws/test-ws-request.ts
|
|
3
|
+
let registeredConnector = null;
|
|
4
|
+
/**
|
|
5
|
+
* Register the transport that {@link TestWsRequest.connect} uses. Called by a
|
|
6
|
+
* platform adapter's side-effect import (e.g. `@velajs/testing/websocket-node`).
|
|
7
|
+
*/
|
|
8
|
+
function registerWsConnector(connector) {
|
|
9
|
+
registeredConnector = connector;
|
|
10
|
+
}
|
|
11
|
+
/** The currently registered WebSocket connector, if any. */
|
|
12
|
+
function getWsConnector() {
|
|
13
|
+
return registeredConnector;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* TestWsRequest
|
|
17
|
+
*
|
|
18
|
+
* Builder for a WebSocket connection. `connect()` requires a transport adapter
|
|
19
|
+
* to be registered; without one it throws a clear, actionable error.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* import '@velajs/testing/websocket-node';
|
|
24
|
+
* const ws = await module.ws('/ws/chat').connect();
|
|
25
|
+
* ws.send('hi');
|
|
26
|
+
* await ws.assertMessage('echo:hi');
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
var TestWsRequest = class {
|
|
30
|
+
path;
|
|
31
|
+
module;
|
|
32
|
+
requestHeaders = new Headers();
|
|
33
|
+
principal = null;
|
|
34
|
+
resolver = null;
|
|
35
|
+
constructor(path, module) {
|
|
36
|
+
this.path = path;
|
|
37
|
+
this.module = module;
|
|
38
|
+
}
|
|
39
|
+
/** Merge additional headers onto the upgrade request. */
|
|
40
|
+
withHeaders(headers) {
|
|
41
|
+
for (const [key, value] of Object.entries(headers)) this.requestHeaders.set(key, value);
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
/** Authenticate the connection (see {@link TestHttpRequest.actingAs}). */
|
|
45
|
+
actingAs(principal, resolver) {
|
|
46
|
+
this.principal = principal;
|
|
47
|
+
this.resolver = resolver ?? null;
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
/** Open the socket via the registered transport adapter. */
|
|
51
|
+
async connect() {
|
|
52
|
+
const connector = getWsConnector();
|
|
53
|
+
if (!connector) throw new Error("WebSocket connect() requires a transport adapter. Import \"@velajs/testing/websocket-node\" (Node). Cloudflare Durable-Object WebSockets are exercised via @velajs/cloudflare + the workerd pool, not the core harness.");
|
|
54
|
+
await this.applyAuthentication();
|
|
55
|
+
return connector(this.module, this.path, this.requestHeaders);
|
|
56
|
+
}
|
|
57
|
+
async applyAuthentication() {
|
|
58
|
+
if (!this.principal) return;
|
|
59
|
+
const resolver = this.resolver ?? this.module.getAuthResolver();
|
|
60
|
+
if (!resolver) throw new Error("actingAs() requires an auth resolver. Pass one explicitly or register a default with module.setAuthResolver(resolver).");
|
|
61
|
+
const headers = await resolver(this.module, this.principal);
|
|
62
|
+
for (const [key, value] of headers.entries()) this.requestHeaders.set(key, value);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/ws/test-ws-connection.ts
|
|
67
|
+
/**
|
|
68
|
+
* TestWsConnection
|
|
69
|
+
*
|
|
70
|
+
* Wraps a live `WebSocket` with queue-based wait/assert helpers. Transport is
|
|
71
|
+
* supplied by the caller — the core harness never opens a socket itself (see
|
|
72
|
+
* `@velajs/testing/websocket-node`).
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* const ws = await module.ws('/ws/chat').connect();
|
|
77
|
+
* ws.send('hi');
|
|
78
|
+
* await ws.assertMessage('echo:hi');
|
|
79
|
+
* ws.close();
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
var TestWsConnection = class {
|
|
83
|
+
ws;
|
|
84
|
+
cleanup;
|
|
85
|
+
messageQueue = [];
|
|
86
|
+
messageWaiters = [];
|
|
87
|
+
closeEvent = null;
|
|
88
|
+
closeWaiters = [];
|
|
89
|
+
constructor(ws, cleanup) {
|
|
90
|
+
this.ws = ws;
|
|
91
|
+
this.cleanup = cleanup;
|
|
92
|
+
this.ws.addEventListener("message", (event) => {
|
|
93
|
+
const data = event.data;
|
|
94
|
+
if (this.messageWaiters.length > 0) this.messageWaiters.shift()(data);
|
|
95
|
+
else this.messageQueue.push(data);
|
|
96
|
+
});
|
|
97
|
+
this.ws.addEventListener("close", (event) => {
|
|
98
|
+
this.closeEvent = {
|
|
99
|
+
code: event.code,
|
|
100
|
+
reason: event.reason
|
|
101
|
+
};
|
|
102
|
+
for (const waiter of this.closeWaiters) waiter(this.closeEvent);
|
|
103
|
+
this.closeWaiters = [];
|
|
104
|
+
this.cleanup?.();
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/** The raw `WebSocket`. */
|
|
108
|
+
get raw() {
|
|
109
|
+
return this.ws;
|
|
110
|
+
}
|
|
111
|
+
/** Send a message. */
|
|
112
|
+
send(data) {
|
|
113
|
+
this.ws.send(data);
|
|
114
|
+
}
|
|
115
|
+
/** Close the connection. */
|
|
116
|
+
close(code, reason) {
|
|
117
|
+
this.ws.close(code, reason);
|
|
118
|
+
}
|
|
119
|
+
/** Wait for the next message (rejects after `timeout` ms). */
|
|
120
|
+
async waitForMessage(timeout = 5e3) {
|
|
121
|
+
if (this.messageQueue.length > 0) return this.messageQueue.shift();
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
const waiter = (data) => {
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
resolve(data);
|
|
126
|
+
};
|
|
127
|
+
const timer = setTimeout(() => {
|
|
128
|
+
const index = this.messageWaiters.indexOf(waiter);
|
|
129
|
+
if (index !== -1) this.messageWaiters.splice(index, 1);
|
|
130
|
+
reject(/* @__PURE__ */ new Error(`WebSocket: no message received within ${timeout}ms`));
|
|
131
|
+
}, timeout);
|
|
132
|
+
this.messageWaiters.push(waiter);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/** Wait for the connection to close (rejects after `timeout` ms). */
|
|
136
|
+
async waitForClose(timeout = 5e3) {
|
|
137
|
+
if (this.closeEvent) return this.closeEvent;
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
const waiter = (event) => {
|
|
140
|
+
clearTimeout(timer);
|
|
141
|
+
resolve(event);
|
|
142
|
+
};
|
|
143
|
+
const timer = setTimeout(() => {
|
|
144
|
+
const index = this.closeWaiters.indexOf(waiter);
|
|
145
|
+
if (index !== -1) this.closeWaiters.splice(index, 1);
|
|
146
|
+
reject(/* @__PURE__ */ new Error(`WebSocket: connection did not close within ${timeout}ms`));
|
|
147
|
+
}, timeout);
|
|
148
|
+
this.closeWaiters.push(waiter);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/** Assert the next message equals `expected`. */
|
|
152
|
+
async assertMessage(expected, timeout = 5e3) {
|
|
153
|
+
const data = await this.waitForMessage(timeout);
|
|
154
|
+
const message = typeof data === "string" ? data : "[ArrayBuffer]";
|
|
155
|
+
expect(message, `Expected WebSocket message "${expected}", got "${message}"`).toBe(expected);
|
|
156
|
+
}
|
|
157
|
+
/** Assert the connection closes, optionally with `expectedCode`. */
|
|
158
|
+
async assertClosed(expectedCode, timeout = 5e3) {
|
|
159
|
+
const event = await this.waitForClose(timeout);
|
|
160
|
+
if (expectedCode !== void 0) expect(event.code, `Expected close code ${expectedCode}, got ${event.code}`).toBe(expectedCode);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
//#endregion
|
|
164
|
+
export { registerWsConnector as i, TestWsRequest as n, getWsConnector as r, TestWsConnection as t };
|
|
165
|
+
|
|
166
|
+
//# sourceMappingURL=test-ws-connection-BHlEKwQz.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-ws-connection-BHlEKwQz.js","names":[],"sources":["../src/ws/test-ws-request.ts","../src/ws/test-ws-connection.ts"],"sourcesContent":["// Adapted from @stratal/testing (MIT, © Temitayo Fadojutimi). Vela's WebSocket\n// transport is pluggable and NOT reachable through a plain in-memory fetch, so\n// the actual upgrade is delegated to a registered connector — provided by\n// `@velajs/testing/websocket-node` for Node. The core harness never promises a\n// universal `connect()`.\nimport type { ActingAsResolver, TestPrincipal, TestingModule } from '../testing-module.js';\nimport type { TestWsConnection } from './test-ws-connection.js';\n\n/**\n * A transport adapter that performs the WebSocket upgrade and returns a live\n * connection. Registered by a platform package (e.g. websocket-node).\n */\nexport type WsConnector = (\n module: TestingModule,\n path: string,\n headers: Headers,\n) => Promise<TestWsConnection>;\n\nlet registeredConnector: WsConnector | null = null;\n\n/**\n * Register the transport that {@link TestWsRequest.connect} uses. Called by a\n * platform adapter's side-effect import (e.g. `@velajs/testing/websocket-node`).\n */\nexport function registerWsConnector(connector: WsConnector): void {\n registeredConnector = connector;\n}\n\n/** The currently registered WebSocket connector, if any. */\nexport function getWsConnector(): WsConnector | null {\n return registeredConnector;\n}\n\n/**\n * TestWsRequest\n *\n * Builder for a WebSocket connection. `connect()` requires a transport adapter\n * to be registered; without one it throws a clear, actionable error.\n *\n * @example\n * ```ts\n * import '@velajs/testing/websocket-node';\n * const ws = await module.ws('/ws/chat').connect();\n * ws.send('hi');\n * await ws.assertMessage('echo:hi');\n * ```\n */\nexport class TestWsRequest {\n private readonly requestHeaders = new Headers();\n private principal: TestPrincipal | null = null;\n private resolver: ActingAsResolver | null = null;\n\n constructor(\n private readonly path: string,\n private readonly module: TestingModule,\n ) {}\n\n /** Merge additional headers onto the upgrade request. */\n withHeaders(headers: Record<string, string>): this {\n for (const [key, value] of Object.entries(headers)) {\n this.requestHeaders.set(key, value);\n }\n return this;\n }\n\n /** Authenticate the connection (see {@link TestHttpRequest.actingAs}). */\n actingAs(principal: TestPrincipal, resolver?: ActingAsResolver): this {\n this.principal = principal;\n this.resolver = resolver ?? null;\n return this;\n }\n\n /** Open the socket via the registered transport adapter. */\n async connect(): Promise<TestWsConnection> {\n const connector = getWsConnector();\n if (!connector) {\n throw new Error(\n 'WebSocket connect() requires a transport adapter. Import ' +\n '\"@velajs/testing/websocket-node\" (Node). Cloudflare Durable-Object ' +\n 'WebSockets are exercised via @velajs/cloudflare + the workerd pool, ' +\n 'not the core harness.',\n );\n }\n\n await this.applyAuthentication();\n\n return connector(this.module, this.path, this.requestHeaders);\n }\n\n private async applyAuthentication(): Promise<void> {\n if (!this.principal) return;\n\n const resolver = this.resolver ?? this.module.getAuthResolver();\n if (!resolver) {\n throw new Error(\n 'actingAs() requires an auth resolver. Pass one explicitly or register ' +\n 'a default with module.setAuthResolver(resolver).',\n );\n }\n\n const headers = await resolver(this.module, this.principal);\n for (const [key, value] of headers.entries()) {\n this.requestHeaders.set(key, value);\n }\n }\n}\n","// Ported from @stratal/testing (MIT, © Temitayo Fadojutimi). Web-standard\n// `WebSocket` wrapper; the optional `cleanup` hook lets a transport adapter\n// (e.g. websocket-node) tear down its server when the socket closes.\nimport { expect } from 'vitest';\n\n/**\n * TestWsConnection\n *\n * Wraps a live `WebSocket` with queue-based wait/assert helpers. Transport is\n * supplied by the caller — the core harness never opens a socket itself (see\n * `@velajs/testing/websocket-node`).\n *\n * @example\n * ```ts\n * const ws = await module.ws('/ws/chat').connect();\n * ws.send('hi');\n * await ws.assertMessage('echo:hi');\n * ws.close();\n * ```\n */\nexport class TestWsConnection {\n private readonly messageQueue: (string | ArrayBuffer)[] = [];\n private messageWaiters: ((data: string | ArrayBuffer) => void)[] = [];\n private closeEvent: { code?: number; reason?: string } | null = null;\n private closeWaiters: ((event: { code?: number; reason?: string }) => void)[] = [];\n\n constructor(\n private readonly ws: WebSocket,\n private readonly cleanup?: () => void | Promise<void>,\n ) {\n this.ws.addEventListener('message', (event: MessageEvent) => {\n const data = event.data as string | ArrayBuffer;\n if (this.messageWaiters.length > 0) {\n this.messageWaiters.shift()!(data);\n } else {\n this.messageQueue.push(data);\n }\n });\n\n this.ws.addEventListener('close', (event: CloseEvent) => {\n this.closeEvent = { code: event.code, reason: event.reason };\n for (const waiter of this.closeWaiters) {\n waiter(this.closeEvent);\n }\n this.closeWaiters = [];\n void this.cleanup?.();\n });\n }\n\n /** The raw `WebSocket`. */\n get raw(): WebSocket {\n return this.ws;\n }\n\n /** Send a message. */\n send(data: string | ArrayBufferLike | ArrayBufferView): void {\n // The runtime WebSocket.send accepts all of these; the cast bridges the\n // slight type differences between DOM/undici and Bun's WebSocket typings.\n this.ws.send(data as Parameters<WebSocket['send']>[0]);\n }\n\n /** Close the connection. */\n close(code?: number, reason?: string): void {\n this.ws.close(code, reason);\n }\n\n /** Wait for the next message (rejects after `timeout` ms). */\n async waitForMessage(timeout = 5000): Promise<string | ArrayBuffer> {\n if (this.messageQueue.length > 0) {\n return this.messageQueue.shift()!;\n }\n\n return new Promise<string | ArrayBuffer>((resolve, reject) => {\n const waiter = (data: string | ArrayBuffer): void => {\n clearTimeout(timer);\n resolve(data);\n };\n\n const timer = setTimeout(() => {\n const index = this.messageWaiters.indexOf(waiter);\n if (index !== -1) this.messageWaiters.splice(index, 1);\n reject(new Error(`WebSocket: no message received within ${timeout}ms`));\n }, timeout);\n\n this.messageWaiters.push(waiter);\n });\n }\n\n /** Wait for the connection to close (rejects after `timeout` ms). */\n async waitForClose(timeout = 5000): Promise<{ code?: number; reason?: string }> {\n if (this.closeEvent) {\n return this.closeEvent;\n }\n\n return new Promise<{ code?: number; reason?: string }>((resolve, reject) => {\n const waiter = (event: { code?: number; reason?: string }): void => {\n clearTimeout(timer);\n resolve(event);\n };\n\n const timer = setTimeout(() => {\n const index = this.closeWaiters.indexOf(waiter);\n if (index !== -1) this.closeWaiters.splice(index, 1);\n reject(new Error(`WebSocket: connection did not close within ${timeout}ms`));\n }, timeout);\n\n this.closeWaiters.push(waiter);\n });\n }\n\n /** Assert the next message equals `expected`. */\n async assertMessage(expected: string, timeout = 5000): Promise<void> {\n const data = await this.waitForMessage(timeout);\n const message = typeof data === 'string' ? data : '[ArrayBuffer]';\n expect(message, `Expected WebSocket message \"${expected}\", got \"${message}\"`).toBe(expected);\n }\n\n /** Assert the connection closes, optionally with `expectedCode`. */\n async assertClosed(expectedCode?: number, timeout = 5000): Promise<void> {\n const event = await this.waitForClose(timeout);\n if (expectedCode !== undefined) {\n expect(event.code, `Expected close code ${expectedCode}, got ${event.code}`).toBe(\n expectedCode,\n );\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export {};
|
|
1
|
+
export { };
|
|
@@ -1,109 +1,74 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Node transport adapter for `module.ws(path).connect()`. Importing this module
|
|
5
|
-
* (for its side effect) registers a WebSocket connector that:
|
|
6
|
-
* 1. builds the app's Hono instance and a `@hono/node-ws` upgrade factory,
|
|
7
|
-
* 2. registers every `@WebSocketGateway` via `@velajs/vela/websocket-node`,
|
|
8
|
-
* 3. serves it on an ephemeral loopback port, and
|
|
9
|
-
* 4. opens a real client `WebSocket` against it.
|
|
10
|
-
*
|
|
11
|
-
* `@hono/node-ws` and `@hono/node-server` are OPTIONAL peers, imported
|
|
12
|
-
* dynamically — this module loads without them and only errs at `connect()`
|
|
13
|
-
* time with an actionable message. Their types are declared locally so the
|
|
14
|
-
* package typechecks without the peers installed. Cloudflare Durable-Object
|
|
15
|
-
* WebSockets are exercised via `@velajs/cloudflare` + the workerd pool, not
|
|
16
|
-
* this adapter.
|
|
17
|
-
*/ import { TestWsConnection } from "../ws/test-ws-connection.js";
|
|
18
|
-
import { registerWsConnector } from "../ws/test-ws-request.js";
|
|
19
|
-
// One backing HTTP server per module; gateways are registered exactly once.
|
|
20
|
-
const serversByModule = new WeakMap();
|
|
1
|
+
import { i as registerWsConnector, t as TestWsConnection } from "../test-ws-connection-BHlEKwQz.js";
|
|
2
|
+
//#region src/websocket-node/index.ts
|
|
3
|
+
const serversByModule = /* @__PURE__ */ new WeakMap();
|
|
21
4
|
async function importOptional(specifier) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
});
|
|
28
|
-
}
|
|
5
|
+
try {
|
|
6
|
+
return await import(specifier);
|
|
7
|
+
} catch (error) {
|
|
8
|
+
throw new Error(`[@velajs/testing/websocket-node] "${specifier}" is required for WebSocket connect() on Node but is not installed. Install the optional peers: \`npm i -D @hono/node-ws @hono/node-server\`.`, { cause: error });
|
|
9
|
+
}
|
|
29
10
|
}
|
|
30
11
|
async function ensureServer(module) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
port
|
|
60
|
-
};
|
|
61
|
-
})();
|
|
62
|
-
serversByModule.set(module, started);
|
|
63
|
-
return started;
|
|
12
|
+
const existing = serversByModule.get(module);
|
|
13
|
+
if (existing) return existing;
|
|
14
|
+
const started = (async () => {
|
|
15
|
+
const { serve } = await importOptional("@hono/node-server");
|
|
16
|
+
const { createNodeWebSocket } = await importOptional("@hono/node-ws");
|
|
17
|
+
const { registerWebSocketGateways } = await importOptional("@velajs/vela/websocket-node");
|
|
18
|
+
const app = await module.createApplication();
|
|
19
|
+
const hono = app.getHonoApp();
|
|
20
|
+
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app: hono });
|
|
21
|
+
registerWebSocketGateways(app, upgradeWebSocket);
|
|
22
|
+
const { server, port } = await new Promise((resolve) => {
|
|
23
|
+
const s = serve({
|
|
24
|
+
fetch: hono.fetch,
|
|
25
|
+
port: 0
|
|
26
|
+
}, (info) => {
|
|
27
|
+
resolve({
|
|
28
|
+
server: s,
|
|
29
|
+
port: info.port
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
injectWebSocket(server);
|
|
34
|
+
server.unref?.();
|
|
35
|
+
return { port };
|
|
36
|
+
})();
|
|
37
|
+
serversByModule.set(module, started);
|
|
38
|
+
return started;
|
|
64
39
|
}
|
|
65
40
|
async function openClient(url, headers) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const wsModule = await import(wsSpecifier);
|
|
76
|
-
return new wsModule.default(url, undefined, {
|
|
77
|
-
headers: Object.fromEntries(headerEntries)
|
|
78
|
-
});
|
|
79
|
-
} catch {
|
|
80
|
-
// `ws` not installed — proceed with the global client (headers dropped).
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
if (typeof WebSocket === 'undefined') {
|
|
84
|
-
throw new Error('[@velajs/testing/websocket-node] No global WebSocket. Use Node >=22 or ' + 'install the `ws` package.');
|
|
85
|
-
}
|
|
86
|
-
return new WebSocket(url);
|
|
41
|
+
const headerEntries = [...headers.entries()];
|
|
42
|
+
if (headerEntries.length > 0) {
|
|
43
|
+
const wsSpecifier = "ws";
|
|
44
|
+
try {
|
|
45
|
+
return new (await (import(wsSpecifier))).default(url, void 0, { headers: Object.fromEntries(headerEntries) });
|
|
46
|
+
} catch {}
|
|
47
|
+
}
|
|
48
|
+
if (typeof WebSocket === "undefined") throw new Error("[@velajs/testing/websocket-node] No global WebSocket. Use Node >=22 or install the `ws` package.");
|
|
49
|
+
return new WebSocket(url);
|
|
87
50
|
}
|
|
88
|
-
registerWsConnector(async (module, path, headers)=>{
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return new TestWsConnection(ws);
|
|
51
|
+
registerWsConnector(async (module, path, headers) => {
|
|
52
|
+
const { port } = await ensureServer(module);
|
|
53
|
+
const ws = await openClient(`ws://127.0.0.1:${port}${path}`, headers);
|
|
54
|
+
await new Promise((resolve, reject) => {
|
|
55
|
+
const cleanup = () => {
|
|
56
|
+
ws.removeEventListener("open", onOpen);
|
|
57
|
+
ws.removeEventListener("error", onError);
|
|
58
|
+
};
|
|
59
|
+
const onOpen = () => {
|
|
60
|
+
cleanup();
|
|
61
|
+
resolve();
|
|
62
|
+
};
|
|
63
|
+
const onError = (event) => {
|
|
64
|
+
cleanup();
|
|
65
|
+
reject(/* @__PURE__ */ new Error(`WebSocket failed to open: ${String(event)}`));
|
|
66
|
+
};
|
|
67
|
+
ws.addEventListener("open", onOpen);
|
|
68
|
+
ws.addEventListener("error", onError);
|
|
69
|
+
});
|
|
70
|
+
return new TestWsConnection(ws);
|
|
109
71
|
});
|
|
72
|
+
//#endregion
|
|
73
|
+
|
|
74
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/websocket-node/index.ts"],"sourcesContent":["/**\n * `@velajs/testing/websocket-node`\n *\n * Node transport adapter for `module.ws(path).connect()`. Importing this module\n * (for its side effect) registers a WebSocket connector that:\n * 1. builds the app's Hono instance and a `@hono/node-ws` upgrade factory,\n * 2. registers every `@WebSocketGateway` via `@velajs/vela/websocket-node`,\n * 3. serves it on an ephemeral loopback port, and\n * 4. opens a real client `WebSocket` against it.\n *\n * `@hono/node-ws` and `@hono/node-server` are OPTIONAL peers, imported\n * dynamically — this module loads without them and only errs at `connect()`\n * time with an actionable message. Their types are declared locally so the\n * package typechecks without the peers installed. Cloudflare Durable-Object\n * WebSockets are exercised via `@velajs/cloudflare` + the workerd pool, not\n * this adapter.\n */\nimport type { VelaApplication } from '@velajs/vela';\nimport type { UpgradeWebSocket } from 'hono/ws';\nimport type { TestingModule } from '../testing-module.js';\nimport { TestWsConnection } from '../ws/test-ws-connection.js';\nimport { registerWsConnector } from '../ws/test-ws-request.js';\n\n// Minimal shapes for the optional peers (avoids `typeof import(...)` which would\n// fail to resolve when the peers are not installed).\ninterface NodeServerModule {\n serve: (\n options: { fetch: unknown; port: number },\n onListening?: (info: { port: number }) => void,\n ) => unknown;\n}\ninterface NodeWsModule {\n createNodeWebSocket: (options: { app: unknown }) => {\n injectWebSocket: (server: unknown) => void;\n upgradeWebSocket: UpgradeWebSocket;\n };\n}\ninterface VelaWsNodeModule {\n registerWebSocketGateways: (app: VelaApplication, upgradeWebSocket: UpgradeWebSocket) => void;\n}\ninterface WsPackage {\n default: new (\n url: string,\n protocols?: unknown,\n options?: { headers: Record<string, string> },\n ) => WebSocket;\n}\n\ninterface RunningServer {\n port: number;\n}\n\n// One backing HTTP server per module; gateways are registered exactly once.\nconst serversByModule = new WeakMap<TestingModule, Promise<RunningServer>>();\n\nasync function importOptional<T>(specifier: string): Promise<T> {\n try {\n return (await import(specifier)) as T;\n } catch (error) {\n throw new Error(\n `[@velajs/testing/websocket-node] \"${specifier}\" is required for WebSocket ` +\n 'connect() on Node but is not installed. Install the optional peers: ' +\n '`npm i -D @hono/node-ws @hono/node-server`.',\n { cause: error },\n );\n }\n}\n\nasync function ensureServer(module: TestingModule): Promise<RunningServer> {\n const existing = serversByModule.get(module);\n if (existing) return existing;\n\n const started = (async (): Promise<RunningServer> => {\n const { serve } = await importOptional<NodeServerModule>('@hono/node-server');\n const { createNodeWebSocket } = await importOptional<NodeWsModule>('@hono/node-ws');\n const { registerWebSocketGateways } = await importOptional<VelaWsNodeModule>(\n '@velajs/vela/websocket-node',\n );\n\n const app = await module.createApplication();\n const hono = app.getHonoApp();\n\n const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app: hono });\n registerWebSocketGateways(app, upgradeWebSocket);\n\n const { server, port } = await new Promise<{ server: unknown; port: number }>((resolve) => {\n const s = serve({ fetch: hono.fetch, port: 0 }, (info) => {\n resolve({ server: s, port: info.port });\n });\n });\n\n injectWebSocket(server);\n // Never let the test server keep the process alive; the runtime reclaims it\n // on exit. Tests don't need an explicit server teardown hook this way.\n (server as { unref?: () => void }).unref?.();\n\n return { port };\n })();\n\n serversByModule.set(module, started);\n return started;\n}\n\nasync function openClient(url: string, headers: Headers): Promise<WebSocket> {\n const headerEntries = [...headers.entries()];\n\n // The standard WebSocket client cannot set arbitrary upgrade headers. When\n // headers are needed (e.g. auth cookies), fall back to the `ws` package which\n // supports them; otherwise use the runtime's global WebSocket.\n if (headerEntries.length > 0) {\n const wsSpecifier = 'ws';\n try {\n const wsModule = (await import(wsSpecifier)) as WsPackage;\n return new wsModule.default(url, undefined, {\n headers: Object.fromEntries(headerEntries),\n });\n } catch {\n // `ws` not installed — proceed with the global client (headers dropped).\n }\n }\n\n if (typeof WebSocket === 'undefined') {\n throw new Error(\n '[@velajs/testing/websocket-node] No global WebSocket. Use Node >=22 or ' +\n 'install the `ws` package.',\n );\n }\n return new WebSocket(url);\n}\n\nregisterWsConnector(async (module, path, headers) => {\n const { port } = await ensureServer(module);\n const url = `ws://127.0.0.1:${port}${path}`;\n const ws = await openClient(url, headers);\n\n await new Promise<void>((resolve, reject) => {\n const cleanup = (): void => {\n ws.removeEventListener('open', onOpen);\n ws.removeEventListener('error', onError as EventListener);\n };\n const onOpen = (): void => {\n cleanup();\n resolve();\n };\n const onError = (event: unknown): void => {\n cleanup();\n reject(new Error(`WebSocket failed to open: ${String(event)}`));\n };\n ws.addEventListener('open', onOpen);\n ws.addEventListener('error', onError as EventListener);\n });\n\n return new TestWsConnection(ws);\n});\n"],"mappings":";;AAqDA,MAAM,kCAAkB,IAAI,QAA+C;AAE3E,eAAe,eAAkB,WAA+B;CAC9D,IAAI;EACF,OAAQ,MAAM,OAAO;CACvB,SAAS,OAAO;EACd,MAAM,IAAI,MACR,qCAAqC,UAAU,gJAG/C,EAAE,OAAO,MAAM,CACjB;CACF;AACF;AAEA,eAAe,aAAa,QAA+C;CACzE,MAAM,WAAW,gBAAgB,IAAI,MAAM;CAC3C,IAAI,UAAU,OAAO;CAErB,MAAM,WAAW,YAAoC;EACnD,MAAM,EAAE,UAAU,MAAM,eAAiC,mBAAmB;EAC5E,MAAM,EAAE,wBAAwB,MAAM,eAA6B,eAAe;EAClF,MAAM,EAAE,8BAA8B,MAAM,eAC1C,6BACF;EAEA,MAAM,MAAM,MAAM,OAAO,kBAAkB;EAC3C,MAAM,OAAO,IAAI,WAAW;EAE5B,MAAM,EAAE,iBAAiB,qBAAqB,oBAAoB,EAAE,KAAK,KAAK,CAAC;EAC/E,0BAA0B,KAAK,gBAAgB;EAE/C,MAAM,EAAE,QAAQ,SAAS,MAAM,IAAI,SAA4C,YAAY;GACzF,MAAM,IAAI,MAAM;IAAE,OAAO,KAAK;IAAO,MAAM;GAAE,IAAI,SAAS;IACxD,QAAQ;KAAE,QAAQ;KAAG,MAAM,KAAK;IAAK,CAAC;GACxC,CAAC;EACH,CAAC;EAED,gBAAgB,MAAM;EAGtB,OAAmC,QAAQ;EAE3C,OAAO,EAAE,KAAK;CAChB,EAAA,CAAG;CAEH,gBAAgB,IAAI,QAAQ,OAAO;CACnC,OAAO;AACT;AAEA,eAAe,WAAW,KAAa,SAAsC;CAC3E,MAAM,gBAAgB,CAAC,GAAG,QAAQ,QAAQ,CAAC;CAK3C,IAAI,cAAc,SAAS,GAAG;EAC5B,MAAM,cAAc;EACpB,IAAI;GAEF,OAAO,KAAI,OADa,OAAO,cAAA,CACX,QAAQ,KAAK,KAAA,GAAW,EAC1C,SAAS,OAAO,YAAY,aAAa,EAC3C,CAAC;EACH,QAAQ,CAER;CACF;CAEA,IAAI,OAAO,cAAc,aACvB,MAAM,IAAI,MACR,kGAEF;CAEF,OAAO,IAAI,UAAU,GAAG;AAC1B;AAEA,oBAAoB,OAAO,QAAQ,MAAM,YAAY;CACnD,MAAM,EAAE,SAAS,MAAM,aAAa,MAAM;CAE1C,MAAM,KAAK,MAAM,WAAW,kBADE,OAAO,QACJ,OAAO;CAExC,MAAM,IAAI,SAAe,SAAS,WAAW;EAC3C,MAAM,gBAAsB;GAC1B,GAAG,oBAAoB,QAAQ,MAAM;GACrC,GAAG,oBAAoB,SAAS,OAAwB;EAC1D;EACA,MAAM,eAAqB;GACzB,QAAQ;GACR,QAAQ;EACV;EACA,MAAM,WAAW,UAAyB;GACxC,QAAQ;GACR,uBAAO,IAAI,MAAM,6BAA6B,OAAO,KAAK,GAAG,CAAC;EAChE;EACA,GAAG,iBAAiB,QAAQ,MAAM;EAClC,GAAG,iBAAiB,SAAS,OAAwB;CACvD,CAAC;CAED,OAAO,IAAI,iBAAiB,EAAE;AAChC,CAAC"}
|