@velajs/testing 0.4.0 → 0.5.0
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/dist/db/test-database.d.ts +27 -0
- package/dist/db/test-database.js +19 -0
- package/dist/http/path-utils.d.ts +11 -0
- package/dist/http/path-utils.js +37 -0
- package/dist/http/test-http-client.d.ts +37 -0
- package/dist/http/test-http-client.js +61 -0
- package/dist/http/test-http-request.d.ts +46 -0
- package/dist/http/test-http-request.js +87 -0
- package/dist/http/test-response.d.ts +76 -0
- package/dist/http/test-response.js +166 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +11 -0
- package/dist/sse/test-sse-connection.d.ts +46 -0
- package/dist/sse/test-sse-connection.js +196 -0
- package/dist/sse/test-sse-request.d.ts +30 -0
- package/dist/sse/test-sse-request.js +62 -0
- package/dist/testing-module.d.ts +78 -1
- package/dist/testing-module.js +127 -4
- package/dist/websocket-node/index.d.ts +1 -0
- package/dist/websocket-node/index.js +109 -0
- package/dist/ws/test-ws-connection.d.ts +41 -0
- package/dist/ws/test-ws-connection.js +105 -0
- package/dist/ws/test-ws-request.d.ts +43 -0
- package/dist/ws/test-ws-request.js +69 -0
- package/package.json +20 -3
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Ported from @stratal/testing (MIT, © Temitayo Fadojutimi). Web-standard
|
|
2
|
+
// `WebSocket` wrapper; the optional `cleanup` hook lets a transport adapter
|
|
3
|
+
// (e.g. websocket-node) tear down its server when the socket closes.
|
|
4
|
+
import { expect } from "vitest";
|
|
5
|
+
/**
|
|
6
|
+
* TestWsConnection
|
|
7
|
+
*
|
|
8
|
+
* Wraps a live `WebSocket` with queue-based wait/assert helpers. Transport is
|
|
9
|
+
* supplied by the caller — the core harness never opens a socket itself (see
|
|
10
|
+
* `@velajs/testing/websocket-node`).
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const ws = await module.ws('/ws/chat').connect();
|
|
15
|
+
* ws.send('hi');
|
|
16
|
+
* await ws.assertMessage('echo:hi');
|
|
17
|
+
* ws.close();
|
|
18
|
+
* ```
|
|
19
|
+
*/ export class TestWsConnection {
|
|
20
|
+
ws;
|
|
21
|
+
cleanup;
|
|
22
|
+
messageQueue = [];
|
|
23
|
+
messageWaiters = [];
|
|
24
|
+
closeEvent = null;
|
|
25
|
+
closeWaiters = [];
|
|
26
|
+
constructor(ws, cleanup){
|
|
27
|
+
this.ws = ws;
|
|
28
|
+
this.cleanup = cleanup;
|
|
29
|
+
this.ws.addEventListener('message', (event)=>{
|
|
30
|
+
const data = event.data;
|
|
31
|
+
if (this.messageWaiters.length > 0) {
|
|
32
|
+
this.messageWaiters.shift()(data);
|
|
33
|
+
} else {
|
|
34
|
+
this.messageQueue.push(data);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
this.ws.addEventListener('close', (event)=>{
|
|
38
|
+
this.closeEvent = {
|
|
39
|
+
code: event.code,
|
|
40
|
+
reason: event.reason
|
|
41
|
+
};
|
|
42
|
+
for (const waiter of this.closeWaiters){
|
|
43
|
+
waiter(this.closeEvent);
|
|
44
|
+
}
|
|
45
|
+
this.closeWaiters = [];
|
|
46
|
+
void this.cleanup?.();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/** The raw `WebSocket`. */ get raw() {
|
|
50
|
+
return this.ws;
|
|
51
|
+
}
|
|
52
|
+
/** Send a message. */ send(data) {
|
|
53
|
+
// The runtime WebSocket.send accepts all of these; the cast bridges the
|
|
54
|
+
// slight type differences between DOM/undici and Bun's WebSocket typings.
|
|
55
|
+
this.ws.send(data);
|
|
56
|
+
}
|
|
57
|
+
/** Close the connection. */ close(code, reason) {
|
|
58
|
+
this.ws.close(code, reason);
|
|
59
|
+
}
|
|
60
|
+
/** Wait for the next message (rejects after `timeout` ms). */ async waitForMessage(timeout = 5000) {
|
|
61
|
+
if (this.messageQueue.length > 0) {
|
|
62
|
+
return this.messageQueue.shift();
|
|
63
|
+
}
|
|
64
|
+
return new Promise((resolve, reject)=>{
|
|
65
|
+
const waiter = (data)=>{
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
resolve(data);
|
|
68
|
+
};
|
|
69
|
+
const timer = setTimeout(()=>{
|
|
70
|
+
const index = this.messageWaiters.indexOf(waiter);
|
|
71
|
+
if (index !== -1) this.messageWaiters.splice(index, 1);
|
|
72
|
+
reject(new Error(`WebSocket: no message received within ${timeout}ms`));
|
|
73
|
+
}, timeout);
|
|
74
|
+
this.messageWaiters.push(waiter);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** Wait for the connection to close (rejects after `timeout` ms). */ async waitForClose(timeout = 5000) {
|
|
78
|
+
if (this.closeEvent) {
|
|
79
|
+
return this.closeEvent;
|
|
80
|
+
}
|
|
81
|
+
return new Promise((resolve, reject)=>{
|
|
82
|
+
const waiter = (event)=>{
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
resolve(event);
|
|
85
|
+
};
|
|
86
|
+
const timer = setTimeout(()=>{
|
|
87
|
+
const index = this.closeWaiters.indexOf(waiter);
|
|
88
|
+
if (index !== -1) this.closeWaiters.splice(index, 1);
|
|
89
|
+
reject(new Error(`WebSocket: connection did not close within ${timeout}ms`));
|
|
90
|
+
}, timeout);
|
|
91
|
+
this.closeWaiters.push(waiter);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/** Assert the next message equals `expected`. */ async assertMessage(expected, timeout = 5000) {
|
|
95
|
+
const data = await this.waitForMessage(timeout);
|
|
96
|
+
const message = typeof data === 'string' ? data : '[ArrayBuffer]';
|
|
97
|
+
expect(message, `Expected WebSocket message "${expected}", got "${message}"`).toBe(expected);
|
|
98
|
+
}
|
|
99
|
+
/** Assert the connection closes, optionally with `expectedCode`. */ async assertClosed(expectedCode, timeout = 5000) {
|
|
100
|
+
const event = await this.waitForClose(timeout);
|
|
101
|
+
if (expectedCode !== undefined) {
|
|
102
|
+
expect(event.code, `Expected close code ${expectedCode}, got ${event.code}`).toBe(expectedCode);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ActingAsResolver, TestPrincipal, TestingModule } from '../testing-module.js';
|
|
2
|
+
import type { TestWsConnection } from './test-ws-connection.js';
|
|
3
|
+
/**
|
|
4
|
+
* A transport adapter that performs the WebSocket upgrade and returns a live
|
|
5
|
+
* connection. Registered by a platform package (e.g. websocket-node).
|
|
6
|
+
*/
|
|
7
|
+
export type WsConnector = (module: TestingModule, path: string, headers: Headers) => Promise<TestWsConnection>;
|
|
8
|
+
/**
|
|
9
|
+
* Register the transport that {@link TestWsRequest.connect} uses. Called by a
|
|
10
|
+
* platform adapter's side-effect import (e.g. `@velajs/testing/websocket-node`).
|
|
11
|
+
*/
|
|
12
|
+
export declare function registerWsConnector(connector: WsConnector): void;
|
|
13
|
+
/** The currently registered WebSocket connector, if any. */
|
|
14
|
+
export declare function getWsConnector(): WsConnector | null;
|
|
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
|
+
export declare class TestWsRequest {
|
|
30
|
+
private readonly path;
|
|
31
|
+
private readonly module;
|
|
32
|
+
private readonly requestHeaders;
|
|
33
|
+
private principal;
|
|
34
|
+
private resolver;
|
|
35
|
+
constructor(path: string, module: TestingModule);
|
|
36
|
+
/** Merge additional headers onto the upgrade request. */
|
|
37
|
+
withHeaders(headers: Record<string, string>): this;
|
|
38
|
+
/** Authenticate the connection (see {@link TestHttpRequest.actingAs}). */
|
|
39
|
+
actingAs(principal: TestPrincipal, resolver?: ActingAsResolver): this;
|
|
40
|
+
/** Open the socket via the registered transport adapter. */
|
|
41
|
+
connect(): Promise<TestWsConnection>;
|
|
42
|
+
private applyAuthentication;
|
|
43
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Adapted from @stratal/testing (MIT, © Temitayo Fadojutimi). Vela's WebSocket
|
|
2
|
+
// transport is pluggable and NOT reachable through a plain in-memory fetch, so
|
|
3
|
+
// the actual upgrade is delegated to a registered connector — provided by
|
|
4
|
+
// `@velajs/testing/websocket-node` for Node. The core harness never promises a
|
|
5
|
+
// universal `connect()`.
|
|
6
|
+
let registeredConnector = null;
|
|
7
|
+
/**
|
|
8
|
+
* Register the transport that {@link TestWsRequest.connect} uses. Called by a
|
|
9
|
+
* platform adapter's side-effect import (e.g. `@velajs/testing/websocket-node`).
|
|
10
|
+
*/ export function registerWsConnector(connector) {
|
|
11
|
+
registeredConnector = connector;
|
|
12
|
+
}
|
|
13
|
+
/** The currently registered WebSocket connector, if any. */ export function getWsConnector() {
|
|
14
|
+
return registeredConnector;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* TestWsRequest
|
|
18
|
+
*
|
|
19
|
+
* Builder for a WebSocket connection. `connect()` requires a transport adapter
|
|
20
|
+
* to be registered; without one it throws a clear, actionable error.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* import '@velajs/testing/websocket-node';
|
|
25
|
+
* const ws = await module.ws('/ws/chat').connect();
|
|
26
|
+
* ws.send('hi');
|
|
27
|
+
* await ws.assertMessage('echo:hi');
|
|
28
|
+
* ```
|
|
29
|
+
*/ export class TestWsRequest {
|
|
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. */ withHeaders(headers) {
|
|
40
|
+
for (const [key, value] of Object.entries(headers)){
|
|
41
|
+
this.requestHeaders.set(key, value);
|
|
42
|
+
}
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
/** Authenticate the connection (see {@link TestHttpRequest.actingAs}). */ 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. */ async connect() {
|
|
51
|
+
const connector = getWsConnector();
|
|
52
|
+
if (!connector) {
|
|
53
|
+
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
|
+
}
|
|
55
|
+
await this.applyAuthentication();
|
|
56
|
+
return connector(this.module, this.path, this.requestHeaders);
|
|
57
|
+
}
|
|
58
|
+
async applyAuthentication() {
|
|
59
|
+
if (!this.principal) return;
|
|
60
|
+
const resolver = this.resolver ?? this.module.getAuthResolver();
|
|
61
|
+
if (!resolver) {
|
|
62
|
+
throw new Error('actingAs() requires an auth resolver. Pass one explicitly or register ' + 'a default with module.setAuthResolver(resolver).');
|
|
63
|
+
}
|
|
64
|
+
const headers = await resolver(this.module, this.principal);
|
|
65
|
+
for (const [key, value] of headers.entries()){
|
|
66
|
+
this.requestHeaders.set(key, value);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velajs/testing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Testing utilities for Vela framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
".": {
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./websocket-node": {
|
|
14
|
+
"types": "./dist/websocket-node/index.d.ts",
|
|
15
|
+
"import": "./dist/websocket-node/index.js"
|
|
12
16
|
}
|
|
13
17
|
},
|
|
14
18
|
"files": [
|
|
@@ -17,7 +21,9 @@
|
|
|
17
21
|
"LICENSE",
|
|
18
22
|
"CHANGELOG.md"
|
|
19
23
|
],
|
|
20
|
-
"sideEffects":
|
|
24
|
+
"sideEffects": [
|
|
25
|
+
"./dist/websocket-node/index.js"
|
|
26
|
+
],
|
|
21
27
|
"keywords": [
|
|
22
28
|
"vela",
|
|
23
29
|
"testing",
|
|
@@ -42,7 +48,18 @@
|
|
|
42
48
|
},
|
|
43
49
|
"peerDependencies": {
|
|
44
50
|
"@velajs/vela": ">=1.11.0",
|
|
45
|
-
"hono": ">=4"
|
|
51
|
+
"hono": ">=4",
|
|
52
|
+
"vitest": ">=3",
|
|
53
|
+
"@hono/node-ws": ">=1",
|
|
54
|
+
"@hono/node-server": ">=1"
|
|
55
|
+
},
|
|
56
|
+
"peerDependenciesMeta": {
|
|
57
|
+
"@hono/node-ws": {
|
|
58
|
+
"optional": true
|
|
59
|
+
},
|
|
60
|
+
"@hono/node-server": {
|
|
61
|
+
"optional": true
|
|
62
|
+
}
|
|
46
63
|
},
|
|
47
64
|
"devDependencies": {
|
|
48
65
|
"@swc/cli": "^0.8.1",
|