twd-relay 0.3.0 → 1.0.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/browser.cjs.js +1 -1
- package/dist/browser.es.js +131 -78
- package/dist/cli.js +311 -298
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.es.js +1 -1
- package/dist/relay-C8UTSEbG.js +132 -0
- package/dist/relay-CpUORBd7.cjs +1 -0
- package/dist/vite.cjs.js +1 -1
- package/dist/vite.es.js +2 -2
- package/package.json +6 -5
- package/dist/createTwdRelay-22etCW_8.js +0 -123
- package/dist/createTwdRelay-VRmjHboP.cjs +0 -1
package/dist/browser.cjs.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
var I=Object.create;var O=Object.defineProperty;var J=Object.getOwnPropertyDescriptor;var M=Object.getOwnPropertyNames;var z=Object.getPrototypeOf,B=Object.prototype.hasOwnProperty;var j=(t,o,l,p)=>{if(o&&typeof o=="object"||typeof o=="function")for(let u of M(o))!B.call(t,u)&&u!==l&&O(t,u,{get:()=>o[u],enumerable:!(p=J(o,u))||p.enumerable});return t};var U=(t,o,l)=>(l=t!=null?I(z(t)):{},j(o||!t||!t.__esModule?O(l,"default",{value:t,enumerable:!0}):l,t));Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function x(t){return`${window.location.protocol==="https:"?"wss:":"ws:"}//${window.location.host}${t}`}function g(t,o){if(!t.parent)return"";const l=o.get(t.parent);return l?l.name:""}function F(t){const o=t?.url??x(t?.path??"/__twd/ws"),l=t?.reconnect??!0,p=t?.reconnectInterval??2e3,u=t?.log??!1,C="[twd-relay]";function w(...n){u&&console.info(C,...n)}function N(...n){console.warn(C,...n)}let r=null,h=!1,S=null;function s(n){r&&r.readyState===WebSocket.OPEN&&r.send(JSON.stringify(n))}function f(){window.dispatchEvent(new CustomEvent("twd:state-change"))}async function R(n){const i=window.__TWD_STATE__;if(!i){N("TWD not initialized — make sure twd-js is loaded before running tests"),s({type:"error",code:"NO_TWD",message:"TWD not initialized"});return}const d=i.handlers;let c;if(n&&n.length>0){const e=n.map(m=>m.toLowerCase()),a=[];for(const[,m]of d)if(m.type==="test"){const y=m.name.toLowerCase();e.some(P=>y.includes(P))&&a.push(m.id)}if(a.length===0){const m=Array.from(d.values()).filter(y=>y.type==="test").map(y=>y.name);s({type:"run:start",testCount:0}),s({type:"error",code:"NO_MATCH",message:`No tests matched: ${JSON.stringify(n)}. Available tests: ${JSON.stringify(m)}`}),s({type:"run:complete",passed:0,failed:0,skipped:0,duration:0});return}c=a}s({type:"run:start",testCount:c?c.length:Array.from(d.values()).filter(e=>e.type==="test").length});let _=0,b=0,v=0;const T=performance.now(),L={onStart(e){e.status="running",f(),s({type:"test:start",id:e.id,name:e.name,suite:g(e,d)})},onPass(e){_++,e.status="pass",f(),s({type:"test:pass",id:e.id,name:e.name,suite:g(e,d),duration:performance.now()-T})},onFail(e,a){b++,e.status="fail",e.logs=[a.message],f(),s({type:"test:fail",id:e.id,name:e.name,suite:g(e,d),error:a.message,duration:performance.now()-T})},onSkip(e){v++,e.status="skip",f(),s({type:"test:skip",id:e.id,name:e.name,suite:g(e,d)})},onSuiteStart(e){e.status="running",f()},onSuiteEnd(e){e.status="idle",f()}};try{const{TestRunner:e}=await import("twd-js/runner"),a=new e(L);c?await a.runByIds(c):await a.runAll()}catch(e){const a=e instanceof Error?e.message:String(e);N("Runner error:",a),s({type:"error",code:"RUNNER_ERROR",message:a})}const $=performance.now()-T;s({type:"run:complete",passed:_,failed:b,skipped:v,duration:$}),f()}function W(){const n=window.__TWD_STATE__;if(!n){s({type:"error",code:"NO_TWD",message:"TWD not initialized"});return}const i=n.handlers,d=[];for(const[,c]of i)c.type==="test"&&d.push({id:c.id,name:c.name,suite:g(c,i),status:c.status??"idle"});s({type:"status:result",tests:d})}function k(n){let i;try{i=JSON.parse(n.data)}catch{return}i.type==="run"?(w("Received run command — running tests..."),R(Array.isArray(i.testNames)?i.testNames:void 0)):i.type==="status"&&W()}function A(){l&&!h&&(w(`Reconnecting in ${p}ms...`),S=setTimeout(()=>{E()},p))}function E(){r&&(r.readyState===WebSocket.OPEN||r.readyState===WebSocket.CONNECTING)||(h=!1,w("Connecting to",o),r=new WebSocket(o),r.addEventListener("open",()=>{s({type:"hello",role:"browser"}),w("Connected to relay — ready to receive run/status commands")}),r.addEventListener("message",k),r.addEventListener("close",n=>{if(r=null,n.reason==="Replaced by new browser"){N("Another browser instance connected — this instance will not reconnect");return}h||w("Disconnected",n.code?`(code ${n.code})`:"",n.reason||""),A()}),r.addEventListener("error",()=>{}))}function D(){h=!0,S&&(clearTimeout(S),S=null),r&&(r.close(1e3,"Client disconnecting"),r=null)}return{connect:E,disconnect:D,get connected(){return r!==null&&r.readyState===WebSocket.OPEN}}}exports.createBrowserClient=F;
|
package/dist/browser.es.js
CHANGED
|
@@ -1,141 +1,194 @@
|
|
|
1
|
-
function
|
|
2
|
-
return `${window.location.protocol === "https:" ? "wss:" : "ws:"}//${window.location.host}${
|
|
1
|
+
function I(i) {
|
|
2
|
+
return `${window.location.protocol === "https:" ? "wss:" : "ws:"}//${window.location.host}${i}`;
|
|
3
3
|
}
|
|
4
|
-
function
|
|
5
|
-
if (!
|
|
6
|
-
const
|
|
7
|
-
return
|
|
4
|
+
function m(i, p) {
|
|
5
|
+
if (!i.parent) return "";
|
|
6
|
+
const w = p.get(i.parent);
|
|
7
|
+
return w ? w.name : "";
|
|
8
8
|
}
|
|
9
|
-
function
|
|
10
|
-
const
|
|
11
|
-
function
|
|
12
|
-
|
|
9
|
+
function P(i) {
|
|
10
|
+
const p = i?.url ?? I(i?.path ?? "/__twd/ws"), w = i?.reconnect ?? !0, N = i?.reconnectInterval ?? 2e3, v = i?.log ?? !1, T = "[twd-relay]";
|
|
11
|
+
function u(...t) {
|
|
12
|
+
v && console.info(T, ...t);
|
|
13
13
|
}
|
|
14
|
-
function
|
|
15
|
-
console.warn(
|
|
14
|
+
function h(...t) {
|
|
15
|
+
console.warn(T, ...t);
|
|
16
16
|
}
|
|
17
|
-
let
|
|
18
|
-
function
|
|
19
|
-
|
|
17
|
+
let n = null, y = !1, g = null;
|
|
18
|
+
function r(t) {
|
|
19
|
+
n && n.readyState === WebSocket.OPEN && n.send(JSON.stringify(t));
|
|
20
20
|
}
|
|
21
|
-
function
|
|
21
|
+
function l() {
|
|
22
22
|
window.dispatchEvent(new CustomEvent("twd:state-change"));
|
|
23
23
|
}
|
|
24
|
-
async function
|
|
25
|
-
const
|
|
26
|
-
if (!
|
|
27
|
-
|
|
24
|
+
async function O(t) {
|
|
25
|
+
const o = window.__TWD_STATE__;
|
|
26
|
+
if (!o) {
|
|
27
|
+
h("TWD not initialized — make sure twd-js is loaded before running tests"), r({
|
|
28
|
+
type: "error",
|
|
29
|
+
code: "NO_TWD",
|
|
30
|
+
message: "TWD not initialized"
|
|
31
|
+
});
|
|
28
32
|
return;
|
|
29
33
|
}
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
const c = o.handlers;
|
|
35
|
+
let a;
|
|
36
|
+
if (t && t.length > 0) {
|
|
37
|
+
const e = t.map((d) => d.toLowerCase()), s = [];
|
|
38
|
+
for (const [, d] of c) if (d.type === "test") {
|
|
39
|
+
const f = d.name.toLowerCase();
|
|
40
|
+
e.some(($) => f.includes($)) && s.push(d.id);
|
|
41
|
+
}
|
|
42
|
+
if (s.length === 0) {
|
|
43
|
+
const d = Array.from(c.values()).filter((f) => f.type === "test").map((f) => f.name);
|
|
44
|
+
r({
|
|
45
|
+
type: "run:start",
|
|
46
|
+
testCount: 0
|
|
47
|
+
}), r({
|
|
48
|
+
type: "error",
|
|
49
|
+
code: "NO_MATCH",
|
|
50
|
+
message: `No tests matched: ${JSON.stringify(t)}. Available tests: ${JSON.stringify(d)}`
|
|
51
|
+
}), r({
|
|
52
|
+
type: "run:complete",
|
|
53
|
+
passed: 0,
|
|
54
|
+
failed: 0,
|
|
55
|
+
skipped: 0,
|
|
56
|
+
duration: 0
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
a = s;
|
|
61
|
+
}
|
|
62
|
+
r({
|
|
63
|
+
type: "run:start",
|
|
64
|
+
testCount: a ? a.length : Array.from(c.values()).filter((e) => e.type === "test").length
|
|
65
|
+
});
|
|
66
|
+
let E = 0, _ = 0, b = 0;
|
|
67
|
+
const S = performance.now(), D = {
|
|
34
68
|
onStart(e) {
|
|
35
|
-
e.status = "running",
|
|
69
|
+
e.status = "running", l(), r({
|
|
36
70
|
type: "test:start",
|
|
37
71
|
id: e.id,
|
|
38
72
|
name: e.name,
|
|
39
|
-
suite:
|
|
73
|
+
suite: m(e, c)
|
|
40
74
|
});
|
|
41
75
|
},
|
|
42
76
|
onPass(e) {
|
|
43
|
-
|
|
77
|
+
E++, e.status = "pass", l(), r({
|
|
44
78
|
type: "test:pass",
|
|
45
79
|
id: e.id,
|
|
46
80
|
name: e.name,
|
|
47
|
-
suite:
|
|
48
|
-
duration: performance.now() -
|
|
81
|
+
suite: m(e, c),
|
|
82
|
+
duration: performance.now() - S
|
|
49
83
|
});
|
|
50
84
|
},
|
|
51
|
-
onFail(e,
|
|
52
|
-
|
|
85
|
+
onFail(e, s) {
|
|
86
|
+
_++, e.status = "fail", e.logs = [s.message], l(), r({
|
|
53
87
|
type: "test:fail",
|
|
54
88
|
id: e.id,
|
|
55
89
|
name: e.name,
|
|
56
|
-
suite:
|
|
57
|
-
error:
|
|
58
|
-
duration: performance.now() -
|
|
90
|
+
suite: m(e, c),
|
|
91
|
+
error: s.message,
|
|
92
|
+
duration: performance.now() - S
|
|
59
93
|
});
|
|
60
94
|
},
|
|
61
95
|
onSkip(e) {
|
|
62
|
-
|
|
96
|
+
b++, e.status = "skip", l(), r({
|
|
63
97
|
type: "test:skip",
|
|
64
98
|
id: e.id,
|
|
65
99
|
name: e.name,
|
|
66
|
-
suite:
|
|
100
|
+
suite: m(e, c)
|
|
67
101
|
});
|
|
68
102
|
},
|
|
69
103
|
onSuiteStart(e) {
|
|
70
|
-
e.status = "running",
|
|
104
|
+
e.status = "running", l();
|
|
71
105
|
},
|
|
72
106
|
onSuiteEnd(e) {
|
|
73
|
-
e.status = "idle",
|
|
107
|
+
e.status = "idle", l();
|
|
74
108
|
}
|
|
75
109
|
};
|
|
76
110
|
try {
|
|
77
|
-
const { TestRunner: e } = await import("twd-js/runner");
|
|
78
|
-
await
|
|
111
|
+
const { TestRunner: e } = await import("twd-js/runner"), s = new e(D);
|
|
112
|
+
a ? await s.runByIds(a) : await s.runAll();
|
|
79
113
|
} catch (e) {
|
|
80
|
-
const
|
|
81
|
-
|
|
114
|
+
const s = e instanceof Error ? e.message : String(e);
|
|
115
|
+
h("Runner error:", s), r({
|
|
116
|
+
type: "error",
|
|
117
|
+
code: "RUNNER_ERROR",
|
|
118
|
+
message: s
|
|
119
|
+
});
|
|
82
120
|
}
|
|
83
|
-
const
|
|
84
|
-
|
|
121
|
+
const L = performance.now() - S;
|
|
122
|
+
r({
|
|
123
|
+
type: "run:complete",
|
|
124
|
+
passed: E,
|
|
125
|
+
failed: _,
|
|
126
|
+
skipped: b,
|
|
127
|
+
duration: L
|
|
128
|
+
}), l();
|
|
85
129
|
}
|
|
86
130
|
function R() {
|
|
87
|
-
const
|
|
88
|
-
if (!
|
|
89
|
-
|
|
131
|
+
const t = window.__TWD_STATE__;
|
|
132
|
+
if (!t) {
|
|
133
|
+
r({
|
|
134
|
+
type: "error",
|
|
135
|
+
code: "NO_TWD",
|
|
136
|
+
message: "TWD not initialized"
|
|
137
|
+
});
|
|
90
138
|
return;
|
|
91
139
|
}
|
|
92
|
-
const
|
|
93
|
-
for (const [, a] of
|
|
94
|
-
a.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
140
|
+
const o = t.handlers, c = [];
|
|
141
|
+
for (const [, a] of o) a.type === "test" && c.push({
|
|
142
|
+
id: a.id,
|
|
143
|
+
name: a.name,
|
|
144
|
+
suite: m(a, o),
|
|
145
|
+
status: a.status ?? "idle"
|
|
146
|
+
});
|
|
147
|
+
r({
|
|
148
|
+
type: "status:result",
|
|
149
|
+
tests: c
|
|
150
|
+
});
|
|
101
151
|
}
|
|
102
|
-
function W(
|
|
103
|
-
let
|
|
152
|
+
function W(t) {
|
|
153
|
+
let o;
|
|
104
154
|
try {
|
|
105
|
-
|
|
155
|
+
o = JSON.parse(t.data);
|
|
106
156
|
} catch {
|
|
107
157
|
return;
|
|
108
158
|
}
|
|
109
|
-
|
|
159
|
+
o.type === "run" ? (u("Received run command — running tests..."), O(Array.isArray(o.testNames) ? o.testNames : void 0)) : o.type === "status" && R();
|
|
110
160
|
}
|
|
111
|
-
function
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
},
|
|
161
|
+
function k() {
|
|
162
|
+
w && !y && (u(`Reconnecting in ${N}ms...`), g = setTimeout(() => {
|
|
163
|
+
C();
|
|
164
|
+
}, N));
|
|
115
165
|
}
|
|
116
|
-
function
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
166
|
+
function C() {
|
|
167
|
+
n && (n.readyState === WebSocket.OPEN || n.readyState === WebSocket.CONNECTING) || (y = !1, u("Connecting to", p), n = new WebSocket(p), n.addEventListener("open", () => {
|
|
168
|
+
r({
|
|
169
|
+
type: "hello",
|
|
170
|
+
role: "browser"
|
|
171
|
+
}), u("Connected to relay — ready to receive run/status commands");
|
|
172
|
+
}), n.addEventListener("message", W), n.addEventListener("close", (t) => {
|
|
173
|
+
if (n = null, t.reason === "Replaced by new browser") {
|
|
174
|
+
h("Another browser instance connected — this instance will not reconnect");
|
|
122
175
|
return;
|
|
123
176
|
}
|
|
124
|
-
|
|
125
|
-
}),
|
|
177
|
+
y || u("Disconnected", t.code ? `(code ${t.code})` : "", t.reason || ""), k();
|
|
178
|
+
}), n.addEventListener("error", () => {
|
|
126
179
|
}));
|
|
127
180
|
}
|
|
128
|
-
function
|
|
129
|
-
|
|
181
|
+
function A() {
|
|
182
|
+
y = !0, g && (clearTimeout(g), g = null), n && (n.close(1e3, "Client disconnecting"), n = null);
|
|
130
183
|
}
|
|
131
184
|
return {
|
|
132
|
-
connect:
|
|
133
|
-
disconnect:
|
|
185
|
+
connect: C,
|
|
186
|
+
disconnect: A,
|
|
134
187
|
get connected() {
|
|
135
|
-
return
|
|
188
|
+
return n !== null && n.readyState === WebSocket.OPEN;
|
|
136
189
|
}
|
|
137
190
|
};
|
|
138
191
|
}
|
|
139
192
|
export {
|
|
140
|
-
|
|
193
|
+
P as createBrowserClient
|
|
141
194
|
};
|
package/dist/cli.js
CHANGED
|
@@ -1,259 +1,261 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createServer } from "http";
|
|
3
|
-
import WebSocket
|
|
4
|
-
|
|
3
|
+
import WebSocket, { WebSocket as WebSocket$1, WebSocketServer } from "ws";
|
|
4
|
+
//#region src/relay/createTwdRelay.ts
|
|
5
|
+
var DEFAULT_PATH = "/__twd/ws";
|
|
5
6
|
function createTwdRelay(server, options) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
},
|
|
161
|
-
get browserConnected() {
|
|
162
|
-
return browser !== null && browser.readyState === WebSocket.OPEN;
|
|
163
|
-
},
|
|
164
|
-
get clientCount() {
|
|
165
|
-
return clients.size;
|
|
166
|
-
}
|
|
167
|
-
};
|
|
7
|
+
const path = options?.path ?? DEFAULT_PATH;
|
|
8
|
+
const onError = options?.onError;
|
|
9
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
10
|
+
let browser = null;
|
|
11
|
+
const clients = /* @__PURE__ */ new Set();
|
|
12
|
+
let runInProgress = false;
|
|
13
|
+
function sendError(ws, code, message) {
|
|
14
|
+
const msg = {
|
|
15
|
+
type: "error",
|
|
16
|
+
code,
|
|
17
|
+
message
|
|
18
|
+
};
|
|
19
|
+
if (ws.readyState === WebSocket$1.OPEN) ws.send(JSON.stringify(msg));
|
|
20
|
+
}
|
|
21
|
+
function broadcastToClients(data) {
|
|
22
|
+
for (const client of clients) if (client.readyState === WebSocket$1.OPEN) client.send(data);
|
|
23
|
+
}
|
|
24
|
+
function sendConnectedStatus(ws) {
|
|
25
|
+
const msg = {
|
|
26
|
+
type: "connected",
|
|
27
|
+
browser: browser !== null && browser.readyState === WebSocket$1.OPEN
|
|
28
|
+
};
|
|
29
|
+
if (ws.readyState === WebSocket$1.OPEN) ws.send(JSON.stringify(msg));
|
|
30
|
+
}
|
|
31
|
+
function notifyBrowserDisconnected() {
|
|
32
|
+
broadcastToClients(JSON.stringify({
|
|
33
|
+
type: "connected",
|
|
34
|
+
browser: false
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
function handleBrowserMessage(data) {
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = JSON.parse(data);
|
|
41
|
+
} catch {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (parsed.type === "run:complete") runInProgress = false;
|
|
45
|
+
broadcastToClients(data);
|
|
46
|
+
}
|
|
47
|
+
function handleClientMessage(ws, data) {
|
|
48
|
+
let parsed;
|
|
49
|
+
try {
|
|
50
|
+
parsed = JSON.parse(data);
|
|
51
|
+
} catch {
|
|
52
|
+
sendError(ws, "INVALID_MESSAGE", "Invalid JSON");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!parsed.type) {
|
|
56
|
+
sendError(ws, "INVALID_MESSAGE", "Missing \"type\" field");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (parsed.type === "run") {
|
|
60
|
+
if (!browser || browser.readyState !== WebSocket$1.OPEN) {
|
|
61
|
+
sendError(ws, "NO_BROWSER", "No browser connected");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (runInProgress) {
|
|
65
|
+
sendError(ws, "RUN_IN_PROGRESS", "A test run is already in progress");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
runInProgress = true;
|
|
69
|
+
browser.send(data);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (parsed.type === "status") {
|
|
73
|
+
if (!browser || browser.readyState !== WebSocket$1.OPEN) {
|
|
74
|
+
sendError(ws, "NO_BROWSER", "No browser connected");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
browser.send(data);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
sendError(ws, "UNKNOWN_COMMAND", `Unknown command: ${parsed.type}`);
|
|
81
|
+
}
|
|
82
|
+
function handleConnection(ws) {
|
|
83
|
+
let identified = false;
|
|
84
|
+
const identifyHandler = (raw) => {
|
|
85
|
+
const data = typeof raw === "string" ? raw : raw.toString();
|
|
86
|
+
if (!identified) {
|
|
87
|
+
let parsed;
|
|
88
|
+
try {
|
|
89
|
+
parsed = JSON.parse(data);
|
|
90
|
+
} catch {
|
|
91
|
+
sendError(ws, "INVALID_MESSAGE", "Invalid JSON");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (parsed.type !== "hello" || parsed.role !== "browser" && parsed.role !== "client") {
|
|
95
|
+
sendError(ws, "INVALID_MESSAGE", "First message must be a hello with role \"browser\" or \"client\"");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
identified = true;
|
|
99
|
+
if (parsed.role === "browser") {
|
|
100
|
+
if (browser && browser.readyState === WebSocket$1.OPEN) browser.close(1e3, "Replaced by new browser");
|
|
101
|
+
browser = ws;
|
|
102
|
+
runInProgress = false;
|
|
103
|
+
ws.on("close", () => {
|
|
104
|
+
if (browser === ws) {
|
|
105
|
+
browser = null;
|
|
106
|
+
runInProgress = false;
|
|
107
|
+
notifyBrowserDisconnected();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
broadcastToClients(JSON.stringify({
|
|
111
|
+
type: "connected",
|
|
112
|
+
browser: true
|
|
113
|
+
}));
|
|
114
|
+
ws.on("message", (raw) => {
|
|
115
|
+
handleBrowserMessage(typeof raw === "string" ? raw : raw.toString());
|
|
116
|
+
});
|
|
117
|
+
} else {
|
|
118
|
+
clients.add(ws);
|
|
119
|
+
ws.on("close", () => {
|
|
120
|
+
clients.delete(ws);
|
|
121
|
+
});
|
|
122
|
+
sendConnectedStatus(ws);
|
|
123
|
+
ws.on("message", (raw) => {
|
|
124
|
+
handleClientMessage(ws, typeof raw === "string" ? raw : raw.toString());
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
ws.removeListener("message", identifyHandler);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
ws.on("message", identifyHandler);
|
|
131
|
+
ws.on("error", (err) => {
|
|
132
|
+
if (onError) onError(err);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const upgradeHandler = (request, socket, head) => {
|
|
136
|
+
if (new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`).pathname === path) wss.handleUpgrade(request, socket, head, (ws) => {
|
|
137
|
+
handleConnection(ws);
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
server.on("upgrade", upgradeHandler);
|
|
141
|
+
wss.on("error", (err) => {
|
|
142
|
+
if (onError) onError(err);
|
|
143
|
+
});
|
|
144
|
+
return {
|
|
145
|
+
close() {
|
|
146
|
+
server.removeListener("upgrade", upgradeHandler);
|
|
147
|
+
for (const client of clients) client.close(1e3, "Relay shutting down");
|
|
148
|
+
clients.clear();
|
|
149
|
+
if (browser && browser.readyState === WebSocket$1.OPEN) browser.close(1e3, "Relay shutting down");
|
|
150
|
+
browser = null;
|
|
151
|
+
runInProgress = false;
|
|
152
|
+
wss.close();
|
|
153
|
+
},
|
|
154
|
+
get browserConnected() {
|
|
155
|
+
return browser !== null && browser.readyState === WebSocket$1.OPEN;
|
|
156
|
+
},
|
|
157
|
+
get clientCount() {
|
|
158
|
+
return clients.size;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
168
161
|
}
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/cli/run.ts
|
|
169
164
|
function run(options) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
165
|
+
const { port, timeout, path, host, testNames } = options;
|
|
166
|
+
const url = `ws://${host}:${port}${path}`;
|
|
167
|
+
console.log(`Connecting to ${url}...`);
|
|
168
|
+
const ws = new WebSocket(url);
|
|
169
|
+
let runSent = false;
|
|
170
|
+
let runComplete = false;
|
|
171
|
+
let failed = false;
|
|
172
|
+
const timer = setTimeout(() => {
|
|
173
|
+
console.error(`\nTimeout: no run:complete received within ${timeout / 1e3}s`);
|
|
174
|
+
ws.close();
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}, timeout);
|
|
177
|
+
ws.on("open", () => {
|
|
178
|
+
ws.send(JSON.stringify({
|
|
179
|
+
type: "hello",
|
|
180
|
+
role: "client"
|
|
181
|
+
}));
|
|
182
|
+
});
|
|
183
|
+
ws.on("message", (data) => {
|
|
184
|
+
const msg = JSON.parse(data.toString());
|
|
185
|
+
switch (msg.type) {
|
|
186
|
+
case "connected":
|
|
187
|
+
if (msg.browser && !runSent) {
|
|
188
|
+
runSent = true;
|
|
189
|
+
console.log("Browser connected, triggering test run...\n");
|
|
190
|
+
const runMsg = {
|
|
191
|
+
type: "run",
|
|
192
|
+
scope: "all"
|
|
193
|
+
};
|
|
194
|
+
if (testNames?.length) runMsg.testNames = testNames;
|
|
195
|
+
ws.send(JSON.stringify(runMsg));
|
|
196
|
+
} else if (!msg.browser) console.log("Waiting for browser to connect...");
|
|
197
|
+
break;
|
|
198
|
+
case "run:start":
|
|
199
|
+
console.log(`Running ${msg.testCount} test(s)...\n`);
|
|
200
|
+
break;
|
|
201
|
+
case "test:start":
|
|
202
|
+
console.log(` RUN: ${msg.suite} > ${msg.name}`);
|
|
203
|
+
break;
|
|
204
|
+
case "test:pass":
|
|
205
|
+
console.log(` PASS: ${msg.suite} > ${msg.name} (${msg.duration}ms)`);
|
|
206
|
+
break;
|
|
207
|
+
case "test:fail":
|
|
208
|
+
failed = true;
|
|
209
|
+
console.log(` FAIL: ${msg.suite} > ${msg.name} (${msg.duration}ms)`);
|
|
210
|
+
if (msg.error) console.log(` Error: ${msg.error}`);
|
|
211
|
+
break;
|
|
212
|
+
case "test:skip":
|
|
213
|
+
console.log(` SKIP: ${msg.suite} > ${msg.name}`);
|
|
214
|
+
break;
|
|
215
|
+
case "run:complete": {
|
|
216
|
+
const duration = (msg.duration / 1e3).toFixed(1);
|
|
217
|
+
console.log(`\n--- Run complete ---`);
|
|
218
|
+
console.log(`Passed: ${msg.passed} | Failed: ${msg.failed} | Skipped: ${msg.skipped}`);
|
|
219
|
+
console.log(`Duration: ${duration}s`);
|
|
220
|
+
runComplete = true;
|
|
221
|
+
clearTimeout(timer);
|
|
222
|
+
ws.close();
|
|
223
|
+
process.exit(failed || msg.failed > 0 ? 1 : 0);
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
case "error":
|
|
227
|
+
console.error(`Error [${msg.code}]: ${msg.message}`);
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
ws.on("close", () => {
|
|
232
|
+
clearTimeout(timer);
|
|
233
|
+
if (!runComplete) {
|
|
234
|
+
console.error("Connection closed before run completed");
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
ws.on("error", (err) => {
|
|
239
|
+
clearTimeout(timer);
|
|
240
|
+
console.error(`Connection error: ${err.message}`);
|
|
241
|
+
process.exit(1);
|
|
242
|
+
});
|
|
247
243
|
}
|
|
248
|
-
|
|
249
|
-
|
|
244
|
+
//#endregion
|
|
245
|
+
//#region src/cli/standalone.ts
|
|
246
|
+
var args = process.argv.slice(2);
|
|
247
|
+
var subcommand = args.find((a) => !a.startsWith("--"));
|
|
250
248
|
function parseFlag(name) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
249
|
+
const idx = args.indexOf(name);
|
|
250
|
+
if (idx !== -1 && args[idx + 1]) return args[idx + 1];
|
|
251
|
+
}
|
|
252
|
+
function parseFlagAll(name) {
|
|
253
|
+
const values = [];
|
|
254
|
+
for (let i = 0; i < args.length; i++) if (args[i] === name && args[i + 1] && !args[i + 1].startsWith("--")) values.push(args[i + 1]);
|
|
255
|
+
return values;
|
|
254
256
|
}
|
|
255
257
|
function printHelp() {
|
|
256
|
-
|
|
258
|
+
console.log(`Usage: twd-relay [command] [options]
|
|
257
259
|
|
|
258
260
|
Commands:
|
|
259
261
|
(none), serve Start the standalone relay server (default)
|
|
@@ -268,6 +270,7 @@ Options for run:
|
|
|
268
270
|
--host <host> Relay host to connect to (default: localhost)
|
|
269
271
|
--path <path> WebSocket path (default: /__twd/ws)
|
|
270
272
|
--timeout <ms> Timeout in ms (default: 180000)
|
|
273
|
+
--test <name> Filter tests by name substring (repeatable)
|
|
271
274
|
|
|
272
275
|
Examples:
|
|
273
276
|
twd-relay # start relay on port 9876
|
|
@@ -275,61 +278,71 @@ Examples:
|
|
|
275
278
|
twd-relay run # trigger run via Vite dev server on 5173
|
|
276
279
|
twd-relay run --port 9876 # trigger run on custom port
|
|
277
280
|
twd-relay run --host 192.168.1.10 --path /app/__twd/ws
|
|
278
|
-
twd-relay run --timeout 30000 # custom timeout
|
|
281
|
+
twd-relay run --timeout 30000 # custom timeout
|
|
282
|
+
twd-relay run --test "login" # run tests matching "login"
|
|
283
|
+
twd-relay run --test "login" --test "signup" # run multiple`);
|
|
279
284
|
}
|
|
280
285
|
if (args.includes("--help") || args.includes("-h")) {
|
|
281
|
-
|
|
282
|
-
|
|
286
|
+
printHelp();
|
|
287
|
+
process.exit(0);
|
|
283
288
|
}
|
|
284
289
|
if (subcommand === "run") {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
290
|
+
const portStr = parseFlag("--port");
|
|
291
|
+
const timeoutStr = parseFlag("--timeout");
|
|
292
|
+
const pathFlag = parseFlag("--path") ?? "/__twd/ws";
|
|
293
|
+
const hostFlag = parseFlag("--host") ?? "localhost";
|
|
294
|
+
const port = portStr ? parseInt(portStr, 10) : 5173;
|
|
295
|
+
if (isNaN(port)) {
|
|
296
|
+
console.error("Invalid port number:", portStr);
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
const timeout = timeoutStr ? parseInt(timeoutStr, 10) : 18e4;
|
|
300
|
+
if (isNaN(timeout)) {
|
|
301
|
+
console.error("Invalid timeout value:", timeoutStr);
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
const testNames = parseFlagAll("--test");
|
|
305
|
+
run({
|
|
306
|
+
port,
|
|
307
|
+
timeout,
|
|
308
|
+
path: pathFlag,
|
|
309
|
+
host: hostFlag,
|
|
310
|
+
testNames: testNames.length > 0 ? testNames : void 0
|
|
311
|
+
});
|
|
300
312
|
} else if (!subcommand || subcommand === "serve") {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
313
|
+
const portStr = parseFlag("--port");
|
|
314
|
+
const pathFlag = parseFlag("--path") ?? "/__twd/ws";
|
|
315
|
+
let port = 9876;
|
|
316
|
+
if (portStr) {
|
|
317
|
+
port = parseInt(portStr, 10);
|
|
318
|
+
if (isNaN(port)) {
|
|
319
|
+
console.error("Invalid port number:", portStr);
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const server = createServer();
|
|
324
|
+
const relay = createTwdRelay(server, {
|
|
325
|
+
path: pathFlag,
|
|
326
|
+
onError(err) {
|
|
327
|
+
console.error("[twd-relay] Error:", err.message);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
server.listen(port, () => {
|
|
331
|
+
console.log(`TWD Relay running on ws://localhost:${port}${pathFlag}`);
|
|
332
|
+
console.log("Waiting for connections...");
|
|
333
|
+
});
|
|
334
|
+
function shutdown() {
|
|
335
|
+
console.log("\nShutting down...");
|
|
336
|
+
relay.close();
|
|
337
|
+
server.close(() => {
|
|
338
|
+
process.exit(0);
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
process.on("SIGINT", shutdown);
|
|
342
|
+
process.on("SIGTERM", shutdown);
|
|
331
343
|
} else {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
344
|
+
console.error(`Unknown command: ${subcommand}`);
|
|
345
|
+
printHelp();
|
|
346
|
+
process.exit(1);
|
|
335
347
|
}
|
|
348
|
+
//#endregion
|
package/dist/index.cjs.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./relay-CpUORBd7.cjs");exports.createTwdRelay=e.createTwdRelay;
|
package/dist/index.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export declare type InboundMessage = HelloMessage | Command | BrowserEvent;
|
|
|
28
28
|
export declare interface RunCommand {
|
|
29
29
|
type: 'run';
|
|
30
30
|
scope: 'all';
|
|
31
|
+
testNames?: string[];
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
export declare interface RunCompleteEvent {
|
|
@@ -78,7 +79,7 @@ export declare interface TestStartEvent {
|
|
|
78
79
|
suite: string;
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
export declare type TwdErrorCode = 'NO_BROWSER' | 'RUN_IN_PROGRESS' | 'UNKNOWN_COMMAND' | 'INVALID_MESSAGE';
|
|
82
|
+
export declare type TwdErrorCode = 'NO_BROWSER' | 'RUN_IN_PROGRESS' | 'UNKNOWN_COMMAND' | 'INVALID_MESSAGE' | 'NO_MATCH';
|
|
82
83
|
|
|
83
84
|
export declare interface TwdErrorMessage {
|
|
84
85
|
type: 'error';
|
package/dist/index.es.js
CHANGED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { WebSocket as s, WebSocketServer as I } from "ws";
|
|
2
|
+
var P = "/__twd/ws";
|
|
3
|
+
function _(y, p) {
|
|
4
|
+
const g = p?.path ?? P, f = p?.onError, u = new I({ noServer: !0 });
|
|
5
|
+
let t = null;
|
|
6
|
+
const a = /* @__PURE__ */ new Set();
|
|
7
|
+
let c = !1;
|
|
8
|
+
function i(e, n, r) {
|
|
9
|
+
const o = {
|
|
10
|
+
type: "error",
|
|
11
|
+
code: n,
|
|
12
|
+
message: r
|
|
13
|
+
};
|
|
14
|
+
e.readyState === s.OPEN && e.send(JSON.stringify(o));
|
|
15
|
+
}
|
|
16
|
+
function S(e) {
|
|
17
|
+
for (const n of a) n.readyState === s.OPEN && n.send(e);
|
|
18
|
+
}
|
|
19
|
+
function O(e) {
|
|
20
|
+
const n = {
|
|
21
|
+
type: "connected",
|
|
22
|
+
browser: t !== null && t.readyState === s.OPEN
|
|
23
|
+
};
|
|
24
|
+
e.readyState === s.OPEN && e.send(JSON.stringify(n));
|
|
25
|
+
}
|
|
26
|
+
function E() {
|
|
27
|
+
S(JSON.stringify({
|
|
28
|
+
type: "connected",
|
|
29
|
+
browser: !1
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
function h(e) {
|
|
33
|
+
let n;
|
|
34
|
+
try {
|
|
35
|
+
n = JSON.parse(e);
|
|
36
|
+
} catch {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
n.type === "run:complete" && (c = !1), S(e);
|
|
40
|
+
}
|
|
41
|
+
function b(e, n) {
|
|
42
|
+
let r;
|
|
43
|
+
try {
|
|
44
|
+
r = JSON.parse(n);
|
|
45
|
+
} catch {
|
|
46
|
+
i(e, "INVALID_MESSAGE", "Invalid JSON");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (!r.type) {
|
|
50
|
+
i(e, "INVALID_MESSAGE", 'Missing "type" field');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (r.type === "run") {
|
|
54
|
+
if (!t || t.readyState !== s.OPEN) {
|
|
55
|
+
i(e, "NO_BROWSER", "No browser connected");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (c) {
|
|
59
|
+
i(e, "RUN_IN_PROGRESS", "A test run is already in progress");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
c = !0, t.send(n);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (r.type === "status") {
|
|
66
|
+
if (!t || t.readyState !== s.OPEN) {
|
|
67
|
+
i(e, "NO_BROWSER", "No browser connected");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
t.send(n);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
i(e, "UNKNOWN_COMMAND", `Unknown command: ${r.type}`);
|
|
74
|
+
}
|
|
75
|
+
function m(e) {
|
|
76
|
+
let n = !1;
|
|
77
|
+
const r = (o) => {
|
|
78
|
+
const A = typeof o == "string" ? o : o.toString();
|
|
79
|
+
if (!n) {
|
|
80
|
+
let d;
|
|
81
|
+
try {
|
|
82
|
+
d = JSON.parse(A);
|
|
83
|
+
} catch {
|
|
84
|
+
i(e, "INVALID_MESSAGE", "Invalid JSON");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (d.type !== "hello" || d.role !== "browser" && d.role !== "client") {
|
|
88
|
+
i(e, "INVALID_MESSAGE", 'First message must be a hello with role "browser" or "client"');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
n = !0, d.role === "browser" ? (t && t.readyState === s.OPEN && t.close(1e3, "Replaced by new browser"), t = e, c = !1, e.on("close", () => {
|
|
92
|
+
t === e && (t = null, c = !1, E());
|
|
93
|
+
}), S(JSON.stringify({
|
|
94
|
+
type: "connected",
|
|
95
|
+
browser: !0
|
|
96
|
+
})), e.on("message", (l) => {
|
|
97
|
+
h(typeof l == "string" ? l : l.toString());
|
|
98
|
+
})) : (a.add(e), e.on("close", () => {
|
|
99
|
+
a.delete(e);
|
|
100
|
+
}), O(e), e.on("message", (l) => {
|
|
101
|
+
b(e, typeof l == "string" ? l : l.toString());
|
|
102
|
+
})), e.removeListener("message", r);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
e.on("message", r), e.on("error", (o) => {
|
|
106
|
+
f && f(o);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const N = (e, n, r) => {
|
|
110
|
+
new URL(e.url ?? "/", `http://${e.headers.host ?? "localhost"}`).pathname === g && u.handleUpgrade(e, n, r, (o) => {
|
|
111
|
+
m(o);
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
return y.on("upgrade", N), u.on("error", (e) => {
|
|
115
|
+
f && f(e);
|
|
116
|
+
}), {
|
|
117
|
+
close() {
|
|
118
|
+
y.removeListener("upgrade", N);
|
|
119
|
+
for (const e of a) e.close(1e3, "Relay shutting down");
|
|
120
|
+
a.clear(), t && t.readyState === s.OPEN && t.close(1e3, "Relay shutting down"), t = null, c = !1, u.close();
|
|
121
|
+
},
|
|
122
|
+
get browserConnected() {
|
|
123
|
+
return t !== null && t.readyState === s.OPEN;
|
|
124
|
+
},
|
|
125
|
+
get clientCount() {
|
|
126
|
+
return a.size;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
export {
|
|
131
|
+
_ as t
|
|
132
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
let o=require("ws");var R="/__twd/ws";function W(y,N){const p=N?.path??R,f=N?.onError,u=new o.WebSocketServer({noServer:!0});let t=null;const l=new Set;let a=!1;function c(e,n,r){const s={type:"error",code:n,message:r};e.readyState===o.WebSocket.OPEN&&e.send(JSON.stringify(s))}function S(e){for(const n of l)n.readyState===o.WebSocket.OPEN&&n.send(e)}function b(e){const n={type:"connected",browser:t!==null&&t.readyState===o.WebSocket.OPEN};e.readyState===o.WebSocket.OPEN&&e.send(JSON.stringify(n))}function O(){S(JSON.stringify({type:"connected",browser:!1}))}function E(e){let n;try{n=JSON.parse(e)}catch{return}n.type==="run:complete"&&(a=!1),S(e)}function h(e,n){let r;try{r=JSON.parse(n)}catch{c(e,"INVALID_MESSAGE","Invalid JSON");return}if(!r.type){c(e,"INVALID_MESSAGE",'Missing "type" field');return}if(r.type==="run"){if(!t||t.readyState!==o.WebSocket.OPEN){c(e,"NO_BROWSER","No browser connected");return}if(a){c(e,"RUN_IN_PROGRESS","A test run is already in progress");return}a=!0,t.send(n);return}if(r.type==="status"){if(!t||t.readyState!==o.WebSocket.OPEN){c(e,"NO_BROWSER","No browser connected");return}t.send(n);return}c(e,"UNKNOWN_COMMAND",`Unknown command: ${r.type}`)}function m(e){let n=!1;const r=s=>{const P=typeof s=="string"?s:s.toString();if(!n){let d;try{d=JSON.parse(P)}catch{c(e,"INVALID_MESSAGE","Invalid JSON");return}if(d.type!=="hello"||d.role!=="browser"&&d.role!=="client"){c(e,"INVALID_MESSAGE",'First message must be a hello with role "browser" or "client"');return}n=!0,d.role==="browser"?(t&&t.readyState===o.WebSocket.OPEN&&t.close(1e3,"Replaced by new browser"),t=e,a=!1,e.on("close",()=>{t===e&&(t=null,a=!1,O())}),S(JSON.stringify({type:"connected",browser:!0})),e.on("message",i=>{E(typeof i=="string"?i:i.toString())})):(l.add(e),e.on("close",()=>{l.delete(e)}),b(e),e.on("message",i=>{h(e,typeof i=="string"?i:i.toString())})),e.removeListener("message",r)}};e.on("message",r),e.on("error",s=>{f&&f(s)})}const g=(e,n,r)=>{new URL(e.url??"/",`http://${e.headers.host??"localhost"}`).pathname===p&&u.handleUpgrade(e,n,r,s=>{m(s)})};return y.on("upgrade",g),u.on("error",e=>{f&&f(e)}),{close(){y.removeListener("upgrade",g);for(const e of l)e.close(1e3,"Relay shutting down");l.clear(),t&&t.readyState===o.WebSocket.OPEN&&t.close(1e3,"Relay shutting down"),t=null,a=!1,u.close()},get browserConnected(){return t!==null&&t.readyState===o.WebSocket.OPEN},get clientCount(){return l.size}}}Object.defineProperty(exports,"createTwdRelay",{enumerable:!0,get:function(){return W}});
|
package/dist/vite.cjs.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const l=require("./relay-CpUORBd7.cjs");function n(r){let t="/";return{name:"twd-relay",configResolved(e){t=e.base},configureServer(e){if(!e.httpServer)return;const o=r?.path??t.replace(/\/$/,"")+"/__twd/ws",a=l.createTwdRelay(e.httpServer,{path:o});e.httpServer.on("close",()=>a.close())}}}exports.twdRemote=n;
|
package/dist/vite.es.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { t as n } from "./relay-C8UTSEbG.js";
|
|
2
2
|
function l(r) {
|
|
3
3
|
let t = "/";
|
|
4
4
|
return {
|
|
@@ -8,7 +8,7 @@ function l(r) {
|
|
|
8
8
|
},
|
|
9
9
|
configureServer(e) {
|
|
10
10
|
if (!e.httpServer) return;
|
|
11
|
-
const o = r?.path ?? t.replace(/\/$/, "") + "/__twd/ws", a =
|
|
11
|
+
const o = r?.path ?? t.replace(/\/$/, "") + "/__twd/ws", a = n(e.httpServer, { path: o });
|
|
12
12
|
e.httpServer.on("close", () => a.close());
|
|
13
13
|
}
|
|
14
14
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "twd-relay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "WebSocket relay for TWD — enables AI agents and external tools to run and observe in-browser tests",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "BRIKEV",
|
|
@@ -60,14 +60,15 @@
|
|
|
60
60
|
}
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"ws": "^8.
|
|
63
|
+
"ws": "^8.20.0"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@types/ws": "^8.18.1",
|
|
67
|
-
"@vitest/coverage-v8": "^4.0
|
|
67
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
68
|
+
"esbuild": "^0.27.4",
|
|
68
69
|
"typescript": "^5.9.3",
|
|
69
|
-
"vite": "^
|
|
70
|
+
"vite": "^8.0.1",
|
|
70
71
|
"vite-plugin-dts": "^4.5.4",
|
|
71
|
-
"vitest": "^4.0
|
|
72
|
+
"vitest": "^4.1.0"
|
|
72
73
|
}
|
|
73
74
|
}
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { WebSocketServer as P, WebSocket as o } from "ws";
|
|
2
|
-
const R = "/__twd/ws";
|
|
3
|
-
function J(p, N) {
|
|
4
|
-
const E = N?.path ?? R, f = N?.onError, u = new P({ noServer: !0 });
|
|
5
|
-
let t = null;
|
|
6
|
-
const c = /* @__PURE__ */ new Set();
|
|
7
|
-
let a = !1;
|
|
8
|
-
function s(e, n, r) {
|
|
9
|
-
const i = { type: "error", code: n, message: r };
|
|
10
|
-
e.readyState === o.OPEN && e.send(JSON.stringify(i));
|
|
11
|
-
}
|
|
12
|
-
function S(e) {
|
|
13
|
-
for (const n of c)
|
|
14
|
-
n.readyState === o.OPEN && n.send(e);
|
|
15
|
-
}
|
|
16
|
-
function h(e) {
|
|
17
|
-
const n = { type: "connected", browser: t !== null && t.readyState === o.OPEN };
|
|
18
|
-
e.readyState === o.OPEN && e.send(JSON.stringify(n));
|
|
19
|
-
}
|
|
20
|
-
function m() {
|
|
21
|
-
S(JSON.stringify({ type: "connected", browser: !1 }));
|
|
22
|
-
}
|
|
23
|
-
function b(e) {
|
|
24
|
-
let n;
|
|
25
|
-
try {
|
|
26
|
-
n = JSON.parse(e);
|
|
27
|
-
} catch {
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
n.type === "run:complete" && (a = !1), S(e);
|
|
31
|
-
}
|
|
32
|
-
function A(e, n) {
|
|
33
|
-
let r;
|
|
34
|
-
try {
|
|
35
|
-
r = JSON.parse(n);
|
|
36
|
-
} catch {
|
|
37
|
-
s(e, "INVALID_MESSAGE", "Invalid JSON");
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
if (!r.type) {
|
|
41
|
-
s(e, "INVALID_MESSAGE", 'Missing "type" field');
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
if (r.type === "run") {
|
|
45
|
-
if (!t || t.readyState !== o.OPEN) {
|
|
46
|
-
s(e, "NO_BROWSER", "No browser connected");
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
if (a) {
|
|
50
|
-
s(e, "RUN_IN_PROGRESS", "A test run is already in progress");
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
a = !0, t.send(n);
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
if (r.type === "status") {
|
|
57
|
-
if (!t || t.readyState !== o.OPEN) {
|
|
58
|
-
s(e, "NO_BROWSER", "No browser connected");
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
t.send(n);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
s(e, "UNKNOWN_COMMAND", `Unknown command: ${r.type}`);
|
|
65
|
-
}
|
|
66
|
-
function I(e) {
|
|
67
|
-
let n = !1;
|
|
68
|
-
const r = (i) => {
|
|
69
|
-
const y = typeof i == "string" ? i : i.toString();
|
|
70
|
-
if (!n) {
|
|
71
|
-
let d;
|
|
72
|
-
try {
|
|
73
|
-
d = JSON.parse(y);
|
|
74
|
-
} catch {
|
|
75
|
-
s(e, "INVALID_MESSAGE", "Invalid JSON");
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
if (d.type !== "hello" || d.role !== "browser" && d.role !== "client") {
|
|
79
|
-
s(e, "INVALID_MESSAGE", 'First message must be a hello with role "browser" or "client"');
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
n = !0, d.role === "browser" ? (t && t.readyState === o.OPEN && t.close(1e3, "Replaced by new browser"), t = e, a = !1, e.on("close", () => {
|
|
83
|
-
t === e && (t = null, a = !1, m());
|
|
84
|
-
}), S(JSON.stringify({ type: "connected", browser: !0 })), e.on("message", (l) => {
|
|
85
|
-
const g = typeof l == "string" ? l : l.toString();
|
|
86
|
-
b(g);
|
|
87
|
-
})) : (c.add(e), e.on("close", () => {
|
|
88
|
-
c.delete(e);
|
|
89
|
-
}), h(e), e.on("message", (l) => {
|
|
90
|
-
const g = typeof l == "string" ? l : l.toString();
|
|
91
|
-
A(e, g);
|
|
92
|
-
})), e.removeListener("message", r);
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
e.on("message", r), e.on("error", (i) => {
|
|
96
|
-
f && f(i);
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
const O = (e, n, r) => {
|
|
100
|
-
new URL(e.url ?? "/", `http://${e.headers.host ?? "localhost"}`).pathname === E && u.handleUpgrade(e, n, r, (y) => {
|
|
101
|
-
I(y);
|
|
102
|
-
});
|
|
103
|
-
};
|
|
104
|
-
return p.on("upgrade", O), u.on("error", (e) => {
|
|
105
|
-
f && f(e);
|
|
106
|
-
}), {
|
|
107
|
-
close() {
|
|
108
|
-
p.removeListener("upgrade", O);
|
|
109
|
-
for (const e of c)
|
|
110
|
-
e.close(1e3, "Relay shutting down");
|
|
111
|
-
c.clear(), t && t.readyState === o.OPEN && t.close(1e3, "Relay shutting down"), t = null, a = !1, u.close();
|
|
112
|
-
},
|
|
113
|
-
get browserConnected() {
|
|
114
|
-
return t !== null && t.readyState === o.OPEN;
|
|
115
|
-
},
|
|
116
|
-
get clientCount() {
|
|
117
|
-
return c.size;
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
export {
|
|
122
|
-
J as c
|
|
123
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"use strict";const o=require("ws"),I="/__twd/ws";function P(N,p){const b=p?.path??I,f=p?.onError,S=new o.WebSocketServer({noServer:!0});let t=null;const l=new Set;let a=!1;function s(e,n,r){const c={type:"error",code:n,message:r};e.readyState===o.WebSocket.OPEN&&e.send(JSON.stringify(c))}function u(e){for(const n of l)n.readyState===o.WebSocket.OPEN&&n.send(e)}function E(e){const n={type:"connected",browser:t!==null&&t.readyState===o.WebSocket.OPEN};e.readyState===o.WebSocket.OPEN&&e.send(JSON.stringify(n))}function h(){u(JSON.stringify({type:"connected",browser:!1}))}function m(e){let n;try{n=JSON.parse(e)}catch{return}n.type==="run:complete"&&(a=!1),u(e)}function W(e,n){let r;try{r=JSON.parse(n)}catch{s(e,"INVALID_MESSAGE","Invalid JSON");return}if(!r.type){s(e,"INVALID_MESSAGE",'Missing "type" field');return}if(r.type==="run"){if(!t||t.readyState!==o.WebSocket.OPEN){s(e,"NO_BROWSER","No browser connected");return}if(a){s(e,"RUN_IN_PROGRESS","A test run is already in progress");return}a=!0,t.send(n);return}if(r.type==="status"){if(!t||t.readyState!==o.WebSocket.OPEN){s(e,"NO_BROWSER","No browser connected");return}t.send(n);return}s(e,"UNKNOWN_COMMAND",`Unknown command: ${r.type}`)}function A(e){let n=!1;const r=c=>{const y=typeof c=="string"?c:c.toString();if(!n){let d;try{d=JSON.parse(y)}catch{s(e,"INVALID_MESSAGE","Invalid JSON");return}if(d.type!=="hello"||d.role!=="browser"&&d.role!=="client"){s(e,"INVALID_MESSAGE",'First message must be a hello with role "browser" or "client"');return}n=!0,d.role==="browser"?(t&&t.readyState===o.WebSocket.OPEN&&t.close(1e3,"Replaced by new browser"),t=e,a=!1,e.on("close",()=>{t===e&&(t=null,a=!1,h())}),u(JSON.stringify({type:"connected",browser:!0})),e.on("message",i=>{const g=typeof i=="string"?i:i.toString();m(g)})):(l.add(e),e.on("close",()=>{l.delete(e)}),E(e),e.on("message",i=>{const g=typeof i=="string"?i:i.toString();W(e,g)})),e.removeListener("message",r)}};e.on("message",r),e.on("error",c=>{f&&f(c)})}const O=(e,n,r)=>{new URL(e.url??"/",`http://${e.headers.host??"localhost"}`).pathname===b&&S.handleUpgrade(e,n,r,y=>{A(y)})};return N.on("upgrade",O),S.on("error",e=>{f&&f(e)}),{close(){N.removeListener("upgrade",O);for(const e of l)e.close(1e3,"Relay shutting down");l.clear(),t&&t.readyState===o.WebSocket.OPEN&&t.close(1e3,"Relay shutting down"),t=null,a=!1,S.close()},get browserConnected(){return t!==null&&t.readyState===o.WebSocket.OPEN},get clientCount(){return l.size}}}exports.c=P;
|