twd-relay 1.1.0 → 1.1.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/README.md CHANGED
@@ -81,16 +81,16 @@ On disconnect or eviction (another tab taking over), the original favicon and ti
81
81
 
82
82
  ### Aborting throttled runs
83
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.
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 10 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
85
 
86
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
87
 
88
88
  ```bash
89
- twd-relay run --max-test-duration 15000 # raise to 15s for heavy multistep tests
89
+ twd-relay run --max-test-duration 20000 # raise to 20s for heavy multistep tests
90
90
  twd-relay run --max-test-duration 0 # disable detection
91
91
  ```
92
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.
93
+ The default of 10 s is chosen to sit above the Testing Library default `findBy*` timeout (3 s). A legitimately failing test with one or two missed selectors still completes under the threshold, while throttled runs where tests typically cluster in the 10–30 s range — trip the abort reliably.
94
94
 
95
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
96
 
@@ -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;
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??1e4,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
@@ -18,7 +18,7 @@ export declare interface BrowserClientOptions {
18
18
  /**
19
19
  * Maximum wall-clock ms any single test may run before the browser
20
20
  * aborts the run with reason 'throttled'. Typically triggered when the
21
- * tab is backgrounded and Chrome throttles timers. Default: 5000.
21
+ * tab is backgrounded and Chrome throttles timers. Default: 10000.
22
22
  * Set to 0 to disable detection.
23
23
  */
24
24
  maxTestDurationMs?: number;
@@ -77,7 +77,7 @@ function E(t, f) {
77
77
  return c ? c.name : "";
78
78
  }
79
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]";
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 ?? 1e4, s = "[twd-relay]";
81
81
  function d(...r) {
82
82
  n && console.info(s, ...r);
83
83
  }
package/dist/cli.js CHANGED
@@ -323,7 +323,7 @@ Options for run:
323
323
  --test <name> Filter tests by name substring (repeatable)
324
324
  --max-test-duration <ms> Abort if any single test exceeds this many
325
325
  ms (default from browser client, typically
326
- 5000; 0 disables)
326
+ 10000; 0 disables)
327
327
 
328
328
  Examples:
329
329
  twd-relay # start relay on port 9876
package/dist/index.d.ts CHANGED
@@ -47,7 +47,7 @@ export declare interface RunCommand {
47
47
  testNames?: string[];
48
48
  /** Max wall-clock ms any single test may run before the browser aborts
49
49
  * the run with reason 'throttled'. 0 disables detection. Omit to let
50
- * the browser use its own default (5000). */
50
+ * the browser use its own default (10000). */
51
51
  maxTestDurationMs?: number;
52
52
  }
53
53
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twd-relay",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
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",