twd-relay 0.3.0 → 1.1.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/README.md +73 -3
- package/dist/browser.cjs.js +1 -1
- package/dist/browser.d.ts +7 -0
- package/dist/browser.es.js +279 -108
- package/dist/cli.js +380 -302
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +24 -3
- package/dist/index.es.js +1 -1
- package/dist/relay-CK82tv0w.cjs +1 -0
- package/dist/relay-NDt-wlXz.js +148 -0
- package/dist/vite.cjs.js +1 -1
- package/dist/vite.es.js +2 -2
- package/package.json +7 -5
- package/dist/createTwdRelay-22etCW_8.js +0 -123
- package/dist/createTwdRelay-VRmjHboP.cjs +0 -1
package/README.md
CHANGED
|
@@ -18,9 +18,20 @@ Your app runs tests in the browser with twd-js. twd-relay adds a relay server an
|
|
|
18
18
|
|
|
19
19
|
1. Browser connects → sends `{ type: 'hello', role: 'browser' }`
|
|
20
20
|
2. Client connects → sends `{ type: 'hello', role: 'client' }`
|
|
21
|
-
3. Client sends `{ type: 'run', scope: 'all' }` → relay forwards to browser
|
|
21
|
+
3. Client sends `{ type: 'run', scope: 'all' }` (optionally with `testNames` to filter) → relay forwards to browser
|
|
22
22
|
4. Browser runs tests and streams events → relay broadcasts to clients
|
|
23
|
-
5. `
|
|
23
|
+
5. Browser sends `{ type: 'heartbeat' }` every 3s during a run (relay consumes these, never forwarded to clients)
|
|
24
|
+
6. `run:complete` clears the run lock (and the send-run script exits)
|
|
25
|
+
|
|
26
|
+
### Heartbeat & frozen-tab recovery
|
|
27
|
+
|
|
28
|
+
During an active test run the browser sends a heartbeat every 3 seconds. The relay tracks the last heartbeat time and checks every 10 seconds. If no heartbeat arrives for **120 seconds** during an active run, the relay considers the run dead (browser tab frozen by the OS), resets the run lock, and broadcasts to all clients:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{ "type": "run:abandoned", "reason": "heartbeat_timeout" }
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The CLI prints a clear message — `Run abandoned — browser tab appears frozen. Refresh the browser tab and retry.` — and exits with code 1. This is especially useful for AI agent workflows, where the agent gets an actionable signal instead of a silent 180s timeout followed by a cryptic `RUN_IN_PROGRESS` error.
|
|
24
35
|
|
|
25
36
|
---
|
|
26
37
|
|
|
@@ -57,6 +68,32 @@ const client = createBrowserClient({
|
|
|
57
68
|
client.connect();
|
|
58
69
|
```
|
|
59
70
|
|
|
71
|
+
Once connected, the browser client sets a colored favicon and prefixes `document.title` so you can spot the active TWD tab at a glance:
|
|
72
|
+
|
|
73
|
+
| Favicon | Title prefix | State |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| Blue | `[TWD]` | Connected, idle |
|
|
76
|
+
| Orange | `[TWD ...]` | Tests running |
|
|
77
|
+
| Green | `[TWD ✓]` | Last run passed |
|
|
78
|
+
| Red | `[TWD ✗]` | Last run had failures |
|
|
79
|
+
|
|
80
|
+
On disconnect or eviction (another tab taking over), the original favicon and title are restored.
|
|
81
|
+
|
|
82
|
+
### Aborting throttled runs
|
|
83
|
+
|
|
84
|
+
Chrome aggressively throttles timers in backgrounded tabs, which can stretch a 1-second test run to 30+ seconds. To avoid AI/CI hangs, the browser client monitors per-test wall-clock time. If any single test runs longer than 5 seconds (configurable), the browser emits `run:aborted`, the CLI prints a clear error with recovery guidance, and the run ends with exit code 1.
|
|
85
|
+
|
|
86
|
+
Override the threshold with `--max-test-duration <ms>` on `twd-relay run`, or pass `maxTestDurationMs` to `createBrowserClient`. Set it to `0` to disable detection entirely:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
twd-relay run --max-test-duration 15000 # raise to 15s for heavy multistep tests
|
|
90
|
+
twd-relay run --max-test-duration 0 # disable detection
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The default of 5 s is deliberate: real TWD tests are typically sub-second, and 5 s is a strong throttling signal. Heavy legitimate tests (complex multistep forms, many API calls) may need to raise this.
|
|
94
|
+
|
|
95
|
+
Recovery when an abort fires: foreground the TWD tab (identified by the `[TWD …]` title prefix set by the favicon indicator) and retry. For unattended runs (CI, agents), prefer `twd-cli`: it drives a headless browser where the tab is always focused and throttling doesn't apply.
|
|
96
|
+
|
|
60
97
|
**3. Open your app in a browser** — the page connects to the relay as “browser”.
|
|
61
98
|
|
|
62
99
|
**4. Trigger a run** — something must connect as a **client** and send `run`:
|
|
@@ -114,7 +151,40 @@ Then in your app you can omit the URL; the client defaults to `ws(s)://<current
|
|
|
114
151
|
| `twd-relay/browser` | Browser client: `createBrowserClient(options)` |
|
|
115
152
|
| `twd-relay/vite` | Vite plugin: `twdRemote(options)` |
|
|
116
153
|
|
|
117
|
-
CLI: `twd-relay` (or `npx twd-relay`)
|
|
154
|
+
CLI: `twd-relay` (or `npx twd-relay`) — two subcommands:
|
|
155
|
+
|
|
156
|
+
- `twd-relay serve` (default) — start the standalone relay
|
|
157
|
+
- `twd-relay run` — connect to a relay and trigger a test run
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## CLI `run` command
|
|
162
|
+
|
|
163
|
+
Connect to an existing relay, trigger tests, stream output, and exit with 0 (all pass) or 1 (failures).
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
# Run all tests (connects to Vite dev server on port 5173 by default)
|
|
167
|
+
twd-relay run
|
|
168
|
+
|
|
169
|
+
# Run on a different port
|
|
170
|
+
twd-relay run --port 9876
|
|
171
|
+
|
|
172
|
+
# Run specific tests by name (substring match, case-insensitive)
|
|
173
|
+
twd-relay run --test "should show error"
|
|
174
|
+
|
|
175
|
+
# Multiple filters — runs tests matching any of them
|
|
176
|
+
twd-relay run --test "login" --test "signup"
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
| Flag | Description | Default |
|
|
180
|
+
|------|-------------|---------|
|
|
181
|
+
| `--port <port>` | Relay port | 5173 |
|
|
182
|
+
| `--host <host>` | Relay host | localhost |
|
|
183
|
+
| `--path <path>` | WebSocket path | `/__twd/ws` |
|
|
184
|
+
| `--timeout <ms>` | Timeout | 180000 |
|
|
185
|
+
| `--test <name>` | Filter tests by name substring (repeatable) | — |
|
|
186
|
+
|
|
187
|
+
When `--test` is used and no tests match, the CLI prints the available test names so you can correct the filter.
|
|
118
188
|
|
|
119
189
|
---
|
|
120
190
|
|
package/dist/browser.cjs.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
var z=Object.create;var R=Object.defineProperty;var U=Object.getOwnPropertyDescriptor;var q=Object.getOwnPropertyNames;var H=Object.getPrototypeOf,G=Object.prototype.hasOwnProperty;var V=(e,r,s,c)=>{if(r&&typeof r=="object"||typeof r=="function")for(let n of q(r))!G.call(e,n)&&n!==s&&R(e,n,{get:()=>r[n],enumerable:!(c=U(r,n))||c.enumerable});return e};var X=(e,r,s)=>(s=e!=null?z(H(e)):{},V(r||!e||!e.__esModule?R(s,"default",{value:e,enumerable:!0}):s,e));Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});var M=e=>`data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect x='3' y='4' width='26' height='24' rx='4' fill='${e}'/><circle cx='7' cy='9' r='1' fill='white'/><circle cx='11' cy='9' r='1' fill='white'/><circle cx='15' cy='9' r='1' fill='white'/><path d='M9 20l4 4 10-10' stroke='white' stroke-width='3.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/></svg>`,K={connected:M("%234A90D9"),running:M("%23F5A623"),pass:M("%237ED321"),fail:M("%23D0021B")},Q={connected:"[TWD] ",running:"[TWD ...] ",pass:"[TWD ✓] ",fail:"[TWD ✗] "};function Y(e){let r=null,s=null,c=!1,n=null;function b(){if(n&&n.isConnected)return n;const i=e.querySelector("link[rel='icon']");if(i)n=i;else{const f=e.createElement("link");f.rel="icon",e.head.appendChild(f),n=f}return n}return{save(){if(s!==null)return;const i=e.querySelector("link[rel='icon']");c=i!==null,r=i?.href??null,s=e.title,n=i},restore(){s!==null&&(e.title=s),!c&&n&&n.isConnected?n.remove():c&&r!==null&&n&&n.setAttribute("href",r),r=null,s=null,c=!1,n=null},set(i){b().setAttribute("href",K[i]);const f=s??e.title;e.title=Q[i]+f}}}function Z(e){const r=e.now??(()=>performance.now()),s=e.thresholdMs;let c=null,n=null,b=!1;return{onTestStart(i){c=r(),n=i},onTestEnd(){if(c===null||n===null)return null;const i=n,f=r()-c;return c=null,n=null,s<=0||f<=s?null:{testName:i,durationMs:f}},checkThreshold(){if(s<=0||c===null||n===null)return null;const i=r()-c;return i<=s?null:{testName:n,durationMs:i}},markAborted(){b=!0},isAborted(){return b}}}function ee(e){return`${window.location.protocol==="https:"?"wss:":"ws:"}//${window.location.host}${e}`}function E(e,r){if(!e.parent)return"";const s=r.get(e.parent);return s?s.name:""}function te(e){const r=e?.url??ee(e?.path??"/__twd/ws"),s=e?.reconnect??!0,c=e?.reconnectInterval??2e3,n=e?.log??!1,b=e?.maxTestDurationMs??5e3,i="[twd-relay]";function f(...o){n&&console.info(i,...o)}function _(...o){console.warn(i,...o)}const w=Y(document);let u=null,k=!1,N=null;function d(o){u&&u.readyState===WebSocket.OPEN&&u.send(JSON.stringify(o))}function g(){window.dispatchEvent(new CustomEvent("twd:state-change"))}async function I(o,m={}){const a=Z({thresholdMs:typeof m.maxTestDurationMs=="number"?m.maxTestDurationMs:b});let h=0,S=0,W=0;const C=performance.now();let x;function D(y){a.isAborted()||(a.markAborted(),d({type:"run:aborted",reason:"throttled",durationMs:y.durationMs,testName:y.testName}),d({type:"run:complete",passed:h,failed:S,skipped:W,duration:performance.now()-C}),w.set("fail"),g(),clearInterval(x))}x=setInterval(()=>{d({type:"heartbeat"});const y=a.checkThreshold();y&&D(y)},3e3),w.set("running");try{const y=window.__TWD_STATE__;if(!y){_("TWD not initialized — make sure twd-js is loaded before running tests"),d({type:"error",code:"NO_TWD",message:"TWD not initialized"});return}const T=y.handlers;let v;if(o&&o.length>0){const t=o.map(p=>p.toLowerCase()),l=[];for(const[,p]of T)if(p.type==="test"){const A=p.name.toLowerCase();t.some(j=>A.includes(j))&&l.push(p.id)}if(l.length===0){const p=Array.from(T.values()).filter(A=>A.type==="test").map(A=>A.name);d({type:"run:start",testCount:0}),d({type:"error",code:"NO_MATCH",message:`No tests matched: ${JSON.stringify(o)}. Available tests: ${JSON.stringify(p)}`}),d({type:"run:complete",passed:0,failed:0,skipped:0,duration:0}),w.set("pass");return}v=l}d({type:"run:start",testCount:v?v.length:Array.from(T.values()).filter(t=>t.type==="test").length});const F={onStart(t){a.isAborted()||(a.onTestStart(t.name),t.status="running",g(),d({type:"test:start",id:t.id,name:t.name,suite:E(t,T)}))},onPass(t){const l=a.onTestEnd();if(!a.isAborted()){if(l){D(l);return}h++,t.status="pass",g(),d({type:"test:pass",id:t.id,name:t.name,suite:E(t,T),duration:performance.now()-C})}},onFail(t,l){const p=a.onTestEnd();if(!a.isAborted()){if(p){D(p);return}S++,t.status="fail",t.logs=[l.message],g(),d({type:"test:fail",id:t.id,name:t.name,suite:E(t,T),error:l.message,duration:performance.now()-C})}},onSkip(t){const l=a.onTestEnd();if(!a.isAborted()){if(l){D(l);return}W++,t.status="skip",g(),d({type:"test:skip",id:t.id,name:t.name,suite:E(t,T)})}},onSuiteStart(t){a.isAborted()||(t.status="running",g())},onSuiteEnd(t){a.isAborted()||(t.status="idle",g())}};try{const{TestRunner:t}=await import("twd-js/runner"),l=new t(F);v?await l.runByIds(v):await l.runAll()}catch(t){const l=t instanceof Error?t.message:String(t);_("Runner error:",l),d({type:"error",code:"RUNNER_ERROR",message:l}),S++,a.onTestEnd()}const J=performance.now()-C;a.isAborted()?w.set("fail"):(d({type:"run:complete",passed:h,failed:S,skipped:W,duration:J}),w.set(S>0?"fail":"pass")),g()}finally{clearInterval(x)}}function L(){const o=window.__TWD_STATE__;if(!o){d({type:"error",code:"NO_TWD",message:"TWD not initialized"});return}const m=o.handlers,a=[];for(const[,h]of m)h.type==="test"&&a.push({id:h.id,name:h.name,suite:E(h,m),status:h.status??"idle"});d({type:"status:result",tests:a})}function $(o){let m;try{m=JSON.parse(o.data)}catch{return}m.type==="run"?(f("Received run command — running tests..."),I(Array.isArray(m.testNames)?m.testNames:void 0,m)):m.type==="status"&&L()}function P(){s&&!k&&(f(`Reconnecting in ${c}ms...`),N=setTimeout(()=>{O()},c))}function O(){u&&(u.readyState===WebSocket.OPEN||u.readyState===WebSocket.CONNECTING)||(k=!1,f("Connecting to",r),u=new WebSocket(r),u.addEventListener("open",()=>{d({type:"hello",role:"browser"}),w.save(),w.set("connected"),f("Connected to relay — ready to receive run/status commands")}),u.addEventListener("message",$),u.addEventListener("close",o=>{if(u=null,w.restore(),o.reason==="Replaced by new browser"){_("Another browser instance connected — this instance will not reconnect");return}k||f("Disconnected",o.code?`(code ${o.code})`:"",o.reason||""),P()}),u.addEventListener("error",()=>{}))}function B(){k=!0,N&&(clearTimeout(N),N=null),u&&(u.close(1e3,"Client disconnecting"),u=null)}return{connect:O,disconnect:B,get connected(){return u!==null&&u.readyState===WebSocket.OPEN}}}exports.createBrowserClient=te;
|
package/dist/browser.d.ts
CHANGED
|
@@ -15,6 +15,13 @@ export declare interface BrowserClientOptions {
|
|
|
15
15
|
reconnectInterval?: number;
|
|
16
16
|
/** Enable console logging. Default: false */
|
|
17
17
|
log?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Maximum wall-clock ms any single test may run before the browser
|
|
20
|
+
* aborts the run with reason 'throttled'. Typically triggered when the
|
|
21
|
+
* tab is backgrounded and Chrome throttles timers. Default: 5000.
|
|
22
|
+
* Set to 0 to disable detection.
|
|
23
|
+
*/
|
|
24
|
+
maxTestDurationMs?: number;
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
export declare function createBrowserClient(options?: BrowserClientOptions): BrowserClient;
|
package/dist/browser.es.js
CHANGED
|
@@ -1,141 +1,312 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
var M = (t) => `data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect x='3' y='4' width='26' height='24' rx='4' fill='${t}'/><circle cx='7' cy='9' r='1' fill='white'/><circle cx='11' cy='9' r='1' fill='white'/><circle cx='15' cy='9' r='1' fill='white'/><path d='M9 20l4 4 10-10' stroke='white' stroke-width='3.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/></svg>`, z = {
|
|
2
|
+
connected: M("%234A90D9"),
|
|
3
|
+
running: M("%23F5A623"),
|
|
4
|
+
pass: M("%237ED321"),
|
|
5
|
+
fail: M("%23D0021B")
|
|
6
|
+
}, U = {
|
|
7
|
+
connected: "[TWD] ",
|
|
8
|
+
running: "[TWD ...] ",
|
|
9
|
+
pass: "[TWD ✓] ",
|
|
10
|
+
fail: "[TWD ✗] "
|
|
11
|
+
};
|
|
12
|
+
function j(t) {
|
|
13
|
+
let f = null, c = null, u = !1, n = null;
|
|
14
|
+
function b() {
|
|
15
|
+
if (n && n.isConnected) return n;
|
|
16
|
+
const s = t.querySelector("link[rel='icon']");
|
|
17
|
+
if (s) n = s;
|
|
18
|
+
else {
|
|
19
|
+
const d = t.createElement("link");
|
|
20
|
+
d.rel = "icon", t.head.appendChild(d), n = d;
|
|
21
|
+
}
|
|
22
|
+
return n;
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
save() {
|
|
26
|
+
if (c !== null) return;
|
|
27
|
+
const s = t.querySelector("link[rel='icon']");
|
|
28
|
+
u = s !== null, f = s?.href ?? null, c = t.title, n = s;
|
|
29
|
+
},
|
|
30
|
+
restore() {
|
|
31
|
+
c !== null && (t.title = c), !u && n && n.isConnected ? n.remove() : u && f !== null && n && n.setAttribute("href", f), f = null, c = null, u = !1, n = null;
|
|
32
|
+
},
|
|
33
|
+
set(s) {
|
|
34
|
+
b().setAttribute("href", z[s]);
|
|
35
|
+
const d = c ?? t.title;
|
|
36
|
+
t.title = U[s] + d;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
3
39
|
}
|
|
4
|
-
function
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
return
|
|
40
|
+
function q(t) {
|
|
41
|
+
const f = t.now ?? (() => performance.now()), c = t.thresholdMs;
|
|
42
|
+
let u = null, n = null, b = !1;
|
|
43
|
+
return {
|
|
44
|
+
onTestStart(s) {
|
|
45
|
+
u = f(), n = s;
|
|
46
|
+
},
|
|
47
|
+
onTestEnd() {
|
|
48
|
+
if (u === null || n === null) return null;
|
|
49
|
+
const s = n, d = f() - u;
|
|
50
|
+
return u = null, n = null, c <= 0 || d <= c ? null : {
|
|
51
|
+
testName: s,
|
|
52
|
+
durationMs: d
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
checkThreshold() {
|
|
56
|
+
if (c <= 0 || u === null || n === null) return null;
|
|
57
|
+
const s = f() - u;
|
|
58
|
+
return s <= c ? null : {
|
|
59
|
+
testName: n,
|
|
60
|
+
durationMs: s
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
markAborted() {
|
|
64
|
+
b = !0;
|
|
65
|
+
},
|
|
66
|
+
isAborted() {
|
|
67
|
+
return b;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function H(t) {
|
|
72
|
+
return `${window.location.protocol === "https:" ? "wss:" : "ws:"}//${window.location.host}${t}`;
|
|
8
73
|
}
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
74
|
+
function E(t, f) {
|
|
75
|
+
if (!t.parent) return "";
|
|
76
|
+
const c = f.get(t.parent);
|
|
77
|
+
return c ? c.name : "";
|
|
78
|
+
}
|
|
79
|
+
function G(t) {
|
|
80
|
+
const f = t?.url ?? H(t?.path ?? "/__twd/ws"), c = t?.reconnect ?? !0, u = t?.reconnectInterval ?? 2e3, n = t?.log ?? !1, b = t?.maxTestDurationMs ?? 5e3, s = "[twd-relay]";
|
|
81
|
+
function d(...r) {
|
|
82
|
+
n && console.info(s, ...r);
|
|
13
83
|
}
|
|
14
|
-
function
|
|
15
|
-
console.warn(
|
|
84
|
+
function _(...r) {
|
|
85
|
+
console.warn(s, ...r);
|
|
16
86
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
87
|
+
const w = j(document);
|
|
88
|
+
let a = null, k = !1, N = null;
|
|
89
|
+
function l(r) {
|
|
90
|
+
a && a.readyState === WebSocket.OPEN && a.send(JSON.stringify(r));
|
|
20
91
|
}
|
|
21
|
-
function
|
|
92
|
+
function g() {
|
|
22
93
|
window.dispatchEvent(new CustomEvent("twd:state-change"));
|
|
23
94
|
}
|
|
24
|
-
async function
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
95
|
+
async function I(r, m = {}) {
|
|
96
|
+
const o = q({ thresholdMs: typeof m.maxTestDurationMs == "number" ? m.maxTestDurationMs : b });
|
|
97
|
+
let h = 0, S = 0, x = 0;
|
|
98
|
+
const C = performance.now();
|
|
99
|
+
let W;
|
|
100
|
+
function D(y) {
|
|
101
|
+
o.isAborted() || (o.markAborted(), l({
|
|
102
|
+
type: "run:aborted",
|
|
103
|
+
reason: "throttled",
|
|
104
|
+
durationMs: y.durationMs,
|
|
105
|
+
testName: y.testName
|
|
106
|
+
}), l({
|
|
107
|
+
type: "run:complete",
|
|
108
|
+
passed: h,
|
|
109
|
+
failed: S,
|
|
110
|
+
skipped: x,
|
|
111
|
+
duration: performance.now() - C
|
|
112
|
+
}), w.set("fail"), g(), clearInterval(W));
|
|
29
113
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
onPass(e) {
|
|
43
|
-
a++, e.status = "pass", i(), o({
|
|
44
|
-
type: "test:pass",
|
|
45
|
-
id: e.id,
|
|
46
|
-
name: e.name,
|
|
47
|
-
suite: d(e, r),
|
|
48
|
-
duration: performance.now() - y
|
|
49
|
-
});
|
|
50
|
-
},
|
|
51
|
-
onFail(e, c) {
|
|
52
|
-
T++, e.status = "fail", e.logs = [c.message], i(), o({
|
|
53
|
-
type: "test:fail",
|
|
54
|
-
id: e.id,
|
|
55
|
-
name: e.name,
|
|
56
|
-
suite: d(e, r),
|
|
57
|
-
error: c.message,
|
|
58
|
-
duration: performance.now() - y
|
|
59
|
-
});
|
|
60
|
-
},
|
|
61
|
-
onSkip(e) {
|
|
62
|
-
_++, e.status = "skip", i(), o({
|
|
63
|
-
type: "test:skip",
|
|
64
|
-
id: e.id,
|
|
65
|
-
name: e.name,
|
|
66
|
-
suite: d(e, r)
|
|
114
|
+
W = setInterval(() => {
|
|
115
|
+
l({ type: "heartbeat" });
|
|
116
|
+
const y = o.checkThreshold();
|
|
117
|
+
y && D(y);
|
|
118
|
+
}, 3e3), w.set("running");
|
|
119
|
+
try {
|
|
120
|
+
const y = window.__TWD_STATE__;
|
|
121
|
+
if (!y) {
|
|
122
|
+
_("TWD not initialized — make sure twd-js is loaded before running tests"), l({
|
|
123
|
+
type: "error",
|
|
124
|
+
code: "NO_TWD",
|
|
125
|
+
message: "TWD not initialized"
|
|
67
126
|
});
|
|
68
|
-
|
|
69
|
-
onSuiteStart(e) {
|
|
70
|
-
e.status = "running", i();
|
|
71
|
-
},
|
|
72
|
-
onSuiteEnd(e) {
|
|
73
|
-
e.status = "idle", i();
|
|
127
|
+
return;
|
|
74
128
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
129
|
+
const T = y.handlers;
|
|
130
|
+
let v;
|
|
131
|
+
if (r && r.length > 0) {
|
|
132
|
+
const e = r.map((p) => p.toLowerCase()), i = [];
|
|
133
|
+
for (const [, p] of T) if (p.type === "test") {
|
|
134
|
+
const A = p.name.toLowerCase();
|
|
135
|
+
e.some((J) => A.includes(J)) && i.push(p.id);
|
|
136
|
+
}
|
|
137
|
+
if (i.length === 0) {
|
|
138
|
+
const p = Array.from(T.values()).filter((A) => A.type === "test").map((A) => A.name);
|
|
139
|
+
l({
|
|
140
|
+
type: "run:start",
|
|
141
|
+
testCount: 0
|
|
142
|
+
}), l({
|
|
143
|
+
type: "error",
|
|
144
|
+
code: "NO_MATCH",
|
|
145
|
+
message: `No tests matched: ${JSON.stringify(r)}. Available tests: ${JSON.stringify(p)}`
|
|
146
|
+
}), l({
|
|
147
|
+
type: "run:complete",
|
|
148
|
+
passed: 0,
|
|
149
|
+
failed: 0,
|
|
150
|
+
skipped: 0,
|
|
151
|
+
duration: 0
|
|
152
|
+
}), w.set("pass");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
v = i;
|
|
156
|
+
}
|
|
157
|
+
l({
|
|
158
|
+
type: "run:start",
|
|
159
|
+
testCount: v ? v.length : Array.from(T.values()).filter((e) => e.type === "test").length
|
|
160
|
+
});
|
|
161
|
+
const F = {
|
|
162
|
+
onStart(e) {
|
|
163
|
+
o.isAborted() || (o.onTestStart(e.name), e.status = "running", g(), l({
|
|
164
|
+
type: "test:start",
|
|
165
|
+
id: e.id,
|
|
166
|
+
name: e.name,
|
|
167
|
+
suite: E(e, T)
|
|
168
|
+
}));
|
|
169
|
+
},
|
|
170
|
+
onPass(e) {
|
|
171
|
+
const i = o.onTestEnd();
|
|
172
|
+
if (!o.isAborted()) {
|
|
173
|
+
if (i) {
|
|
174
|
+
D(i);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
h++, e.status = "pass", g(), l({
|
|
178
|
+
type: "test:pass",
|
|
179
|
+
id: e.id,
|
|
180
|
+
name: e.name,
|
|
181
|
+
suite: E(e, T),
|
|
182
|
+
duration: performance.now() - C
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
onFail(e, i) {
|
|
187
|
+
const p = o.onTestEnd();
|
|
188
|
+
if (!o.isAborted()) {
|
|
189
|
+
if (p) {
|
|
190
|
+
D(p);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
S++, e.status = "fail", e.logs = [i.message], g(), l({
|
|
194
|
+
type: "test:fail",
|
|
195
|
+
id: e.id,
|
|
196
|
+
name: e.name,
|
|
197
|
+
suite: E(e, T),
|
|
198
|
+
error: i.message,
|
|
199
|
+
duration: performance.now() - C
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
onSkip(e) {
|
|
204
|
+
const i = o.onTestEnd();
|
|
205
|
+
if (!o.isAborted()) {
|
|
206
|
+
if (i) {
|
|
207
|
+
D(i);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
x++, e.status = "skip", g(), l({
|
|
211
|
+
type: "test:skip",
|
|
212
|
+
id: e.id,
|
|
213
|
+
name: e.name,
|
|
214
|
+
suite: E(e, T)
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
onSuiteStart(e) {
|
|
219
|
+
o.isAborted() || (e.status = "running", g());
|
|
220
|
+
},
|
|
221
|
+
onSuiteEnd(e) {
|
|
222
|
+
o.isAborted() || (e.status = "idle", g());
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
try {
|
|
226
|
+
const { TestRunner: e } = await import("twd-js/runner"), i = new e(F);
|
|
227
|
+
v ? await i.runByIds(v) : await i.runAll();
|
|
228
|
+
} catch (e) {
|
|
229
|
+
const i = e instanceof Error ? e.message : String(e);
|
|
230
|
+
_("Runner error:", i), l({
|
|
231
|
+
type: "error",
|
|
232
|
+
code: "RUNNER_ERROR",
|
|
233
|
+
message: i
|
|
234
|
+
}), S++, o.onTestEnd();
|
|
235
|
+
}
|
|
236
|
+
const B = performance.now() - C;
|
|
237
|
+
o.isAborted() ? w.set("fail") : (l({
|
|
238
|
+
type: "run:complete",
|
|
239
|
+
passed: h,
|
|
240
|
+
failed: S,
|
|
241
|
+
skipped: x,
|
|
242
|
+
duration: B
|
|
243
|
+
}), w.set(S > 0 ? "fail" : "pass")), g();
|
|
244
|
+
} finally {
|
|
245
|
+
clearInterval(W);
|
|
82
246
|
}
|
|
83
|
-
const D = performance.now() - y;
|
|
84
|
-
o({ type: "run:complete", passed: a, failed: T, skipped: _, duration: D }), i();
|
|
85
247
|
}
|
|
86
|
-
function
|
|
87
|
-
const
|
|
88
|
-
if (!
|
|
89
|
-
|
|
248
|
+
function O() {
|
|
249
|
+
const r = window.__TWD_STATE__;
|
|
250
|
+
if (!r) {
|
|
251
|
+
l({
|
|
252
|
+
type: "error",
|
|
253
|
+
code: "NO_TWD",
|
|
254
|
+
message: "TWD not initialized"
|
|
255
|
+
});
|
|
90
256
|
return;
|
|
91
257
|
}
|
|
92
|
-
const
|
|
93
|
-
for (const [,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
258
|
+
const m = r.handlers, o = [];
|
|
259
|
+
for (const [, h] of m) h.type === "test" && o.push({
|
|
260
|
+
id: h.id,
|
|
261
|
+
name: h.name,
|
|
262
|
+
suite: E(h, m),
|
|
263
|
+
status: h.status ?? "idle"
|
|
264
|
+
});
|
|
265
|
+
l({
|
|
266
|
+
type: "status:result",
|
|
267
|
+
tests: o
|
|
268
|
+
});
|
|
101
269
|
}
|
|
102
|
-
function
|
|
103
|
-
let
|
|
270
|
+
function L(r) {
|
|
271
|
+
let m;
|
|
104
272
|
try {
|
|
105
|
-
|
|
273
|
+
m = JSON.parse(r.data);
|
|
106
274
|
} catch {
|
|
107
275
|
return;
|
|
108
276
|
}
|
|
109
|
-
|
|
277
|
+
m.type === "run" ? (d("Received run command — running tests..."), I(Array.isArray(m.testNames) ? m.testNames : void 0, m)) : m.type === "status" && O();
|
|
110
278
|
}
|
|
111
|
-
function
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
},
|
|
279
|
+
function $() {
|
|
280
|
+
c && !k && (d(`Reconnecting in ${u}ms...`), N = setTimeout(() => {
|
|
281
|
+
R();
|
|
282
|
+
}, u));
|
|
115
283
|
}
|
|
116
|
-
function
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
284
|
+
function R() {
|
|
285
|
+
a && (a.readyState === WebSocket.OPEN || a.readyState === WebSocket.CONNECTING) || (k = !1, d("Connecting to", f), a = new WebSocket(f), a.addEventListener("open", () => {
|
|
286
|
+
l({
|
|
287
|
+
type: "hello",
|
|
288
|
+
role: "browser"
|
|
289
|
+
}), w.save(), w.set("connected"), d("Connected to relay — ready to receive run/status commands");
|
|
290
|
+
}), a.addEventListener("message", L), a.addEventListener("close", (r) => {
|
|
291
|
+
if (a = null, w.restore(), r.reason === "Replaced by new browser") {
|
|
292
|
+
_("Another browser instance connected — this instance will not reconnect");
|
|
122
293
|
return;
|
|
123
294
|
}
|
|
124
|
-
|
|
125
|
-
}),
|
|
295
|
+
k || d("Disconnected", r.code ? `(code ${r.code})` : "", r.reason || ""), $();
|
|
296
|
+
}), a.addEventListener("error", () => {
|
|
126
297
|
}));
|
|
127
298
|
}
|
|
128
|
-
function
|
|
129
|
-
|
|
299
|
+
function P() {
|
|
300
|
+
k = !0, N && (clearTimeout(N), N = null), a && (a.close(1e3, "Client disconnecting"), a = null);
|
|
130
301
|
}
|
|
131
302
|
return {
|
|
132
|
-
connect:
|
|
133
|
-
disconnect:
|
|
303
|
+
connect: R,
|
|
304
|
+
disconnect: P,
|
|
134
305
|
get connected() {
|
|
135
|
-
return
|
|
306
|
+
return a !== null && a.readyState === WebSocket.OPEN;
|
|
136
307
|
}
|
|
137
308
|
};
|
|
138
309
|
}
|
|
139
310
|
export {
|
|
140
|
-
|
|
311
|
+
G as createBrowserClient
|
|
141
312
|
};
|