@stacksjs/testing 0.75.233 → 0.75.236

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.d.ts CHANGED
@@ -81,6 +81,7 @@ export declare function pageAuditClean(report: PageAuditReport): boolean;
81
81
  export declare const LIVE_RELOAD_PATTERNS: readonly string[];
82
82
  export declare interface CdpOptions {
83
83
  commandTimeoutMs?: number
84
+ connectTimeoutMs?: number
84
85
  }
85
86
  /** One `Fetch.enable` pattern, as the protocol spells it. */
86
87
  export declare interface FetchPattern {
@@ -124,6 +125,22 @@ export declare interface LaunchOptions extends CdpOptions {
124
125
  userAgent?: boolean | string
125
126
  startTimeoutMs?: number
126
127
  }
128
+ export declare interface ChromeLaunchDiagnostics {
129
+ pid: number | null
130
+ chromePath: string
131
+ profile: string
132
+ temporaryProfile: boolean
133
+ elapsedMs: number
134
+ exitCodeBeforeCleanup: number | null
135
+ signalCodeBeforeCleanup: string | number | null
136
+ exitCode: number | null
137
+ signalCode: string | number | null
138
+ portFileExisted: boolean
139
+ portFileContents: string | null
140
+ stderrTail: string
141
+ stderrTruncated: boolean
142
+ cleanupError: string | null
143
+ }
127
144
  export declare interface LaunchedChrome {
128
145
  cdp: Cdp
129
146
  close: () => Promise<void>
@@ -226,3 +243,7 @@ export declare class Cdp {
226
243
  evaluate<T = any>(expression: string): Promise<T>;
227
244
  disconnect(): void;
228
245
  }
246
+ export declare class ChromeLaunchError extends Error {
247
+ public readonly diagnostics: ChromeLaunchDiagnostics;
248
+ constructor(cause: unknown, diagnostics: ChromeLaunchDiagnostics);
249
+ }
package/dist/browser.js CHANGED
@@ -1,6 +1,8 @@
1
1
  // @bun
2
- import{existsSync as C,mkdirSync as U,mkdtempSync as M,readFileSync as q,renameSync as B,rmSync as R}from"fs";import{tmpdir as L}from"os";import{join as m}from"path";import b from"process";async function S(e,t=12000,r={}){let n=Date.now(),s=await e();while(!s&&Date.now()-n<t)await Bun.sleep(r.intervalMs??100),s=await e();if(!s)throw Error(`Timed out after ${t}ms${r.label?` waiting for ${r.label}`:""}`);return s}var _=new Set(["Fetch.continueRequest","Fetch.fulfillRequest","Fetch.failRequest","Fetch.continueResponse"]);function H(e,t){let r="";for(let n=0;n<e.length;n++){let s=e[n];if(s==="\\"&&n+1<e.length)r+=T(e[++n]);else if(s==="*")r+="[\\s\\S]*";else if(s==="?")r+="[\\s\\S]";else r+=T(s)}return new RegExp(`^${r}$`).test(t)}function T(e){return e.replace(/[.*+?^${}()|[\]\\/]/g,"\\$&")}function F(e,t){let r=t.responseStatusCode!=null||t.responseErrorReason!=null?"Response":"Request";if((e.requestStage??"Request")!==r)return!1;if(e.resourceType&&e.resourceType!==t.resourceType)return!1;return H(e.urlPattern??"*",t.request?.url??"")}class O{socket;id=0;pending=new Map;listeners=new Map;commandTimeoutMs;interceptors=[];ownFetch=null;answered=new Map;errors=[];inFlight=new Map;closed=!1;constructor(e,t={}){this.socket=e;this.commandTimeoutMs=t.commandTimeoutMs??30000,e.addEventListener("close",()=>{this.closed=!0;for(let[r,n]of this.pending)clearTimeout(n.timer),n.reject(Error("Chrome closed the connection")),this.pending.delete(r)}),e.addEventListener("message",(r)=>this.receive(String(r.data)))}receive(e){let t;try{t=JSON.parse(e)}catch{return}if(t.id){let r=this.pending.get(t.id);if(!r)return;if(this.pending.delete(t.id),clearTimeout(r.timer),t.error)r.reject(Error(t.error.message));else r.resolve(t.result);return}if(this.track(t.method,t.params??{}),t.method==="Fetch.requestPaused"&&!this.routePaused(t.params??{}))return;this.dispatch(t.method,t.params??{})}dispatch(e,t){for(let r of[...this.listeners.get(e)??[]])try{r(t)}catch(n){this.errors.push(`Listener for ${e} threw: ${n?.message??String(n)}`)}}routePaused(e){this.remember(e.requestId,!1);let t=this.interceptors.filter((s)=>s.patterns.some((i)=>F(i,e)));for(let s of t)try{let i=s.handler(e,this);if(i&&typeof i.catch==="function")i.catch((a)=>this.errors.push(`Fetch handler threw: ${a?.message??String(a)}`))}catch(i){this.errors.push(`Fetch handler threw: ${i?.message??String(i)}`)}let r=(this.listeners.get("Fetch.requestPaused")?.size??0)>0,n=this.ownFetch?this.ownFetch.patterns.some((s)=>F(s,e)):!1;if(!t.length&&!n&&!r)this.send("Fetch.continueRequest",{requestId:e.requestId}).catch(()=>{});return r&&(n||!t.length)}remember(e,t){if(this.answered.delete(e),this.answered.set(e,t),this.answered.size>2000){for(let[r]of this.answered)if(this.answered.delete(r),this.answered.size<=1000)break}}async applyFetch(){let e=[...this.interceptors.flatMap((t)=>t.patterns),...this.ownFetch?.patterns??[]];if(!e.length)return await this.command("Fetch.disable");return await this.command("Fetch.enable",{...this.ownFetch?.params,patterns:e})}async intercept(e,t){if(!e.length)throw TypeError("intercept needs at least one pattern");let r={patterns:e.map((s)=>({...s})),handler:t};this.interceptors.push(r);try{await this.applyFetch()}catch(s){throw this.interceptors=this.interceptors.filter((i)=>i!==r),s}let n=!1;return async()=>{if(n)return;if(n=!0,this.interceptors=this.interceptors.filter((s)=>s!==r),!this.closed)await this.applyFetch()}}track(e,t){if(e==="Network.requestWillBeSent")this.inFlight.set(t.requestId,`${t.request?.method} ${t.request?.url}`);else if(e==="Network.loadingFinished"||e==="Network.loadingFailed")this.inFlight.delete(t.requestId);else if(e==="Network.responseReceived"&&t.response?.mimeType==="text/event-stream")this.inFlight.delete(t.requestId);if(e==="Runtime.exceptionThrown")this.errors.push(t.exceptionDetails?.exception?.description||t.exceptionDetails?.text||"Runtime exception");else if(e==="Runtime.consoleAPICalled"&&t.type==="error")this.errors.push((t.args||[]).map((r)=>r.value??r.description??"").join(" "));else if(e==="Log.entryAdded"&&t.entry?.level==="error"&&!String(t.entry.text).includes("favicon"))this.errors.push(t.entry.text);else if(e==="Network.loadingFailed"&&!t.canceled)this.errors.push(`Network: ${t.errorText||"request failed"}`)}on(e,t){let r=this.listeners.get(e);if(!r)this.listeners.set(e,r=new Set);return r.add(t),()=>this.off(e,t)}off(e,t){this.listeners.get(e)?.delete(t)}once(e,t=()=>!0,r=this.commandTimeoutMs){return new Promise((n,s)=>{if(this.closed)return s(Error("Chrome closed the connection"));let i=setTimeout(()=>{c(),clearInterval(a),s(Error(`Timed out after ${r}ms waiting for ${e}`))},r),a=setInterval(()=>{if(!this.closed)return;clearTimeout(i),clearInterval(a),c(),s(Error("Chrome closed the connection"))},100),c=this.on(e,(p)=>{if(!t(p))return;clearTimeout(i),clearInterval(a),c(),n(p)})})}send(e,t={}){if(e==="Fetch.enable"){let{patterns:r,...n}=t;return this.ownFetch={patterns:r?.length?r.map((s)=>({...s})):[{urlPattern:"*"}],params:n},this.closed?this.command(e,t):this.applyFetch()}if(e==="Fetch.disable")return this.ownFetch=null,this.closed?this.command(e,t):this.applyFetch();if(_.has(e)&&typeof t.requestId==="string"){if(this.answered.get(t.requestId)===!0)return Promise.resolve({});if(this.answered.has(t.requestId))this.remember(t.requestId,!0)}return this.command(e,t)}command(e,t={}){let r=++this.id;return new Promise((n,s)=>{if(this.closed)return s(Error("Chrome closed the connection"));let i=setTimeout(()=>{this.pending.delete(r),s(Error(`Chrome command timed out: ${e}`))},this.commandTimeoutMs);this.pending.set(r,{resolve:n,reject:s,timer:i}),this.socket.send(JSON.stringify({id:r,method:e,params:t}))})}async evaluate(e){let t=await this.send("Runtime.evaluate",{expression:e,awaitPromise:!0,returnByValue:!0});if(t.exceptionDetails)throw Error(t.exceptionDetails.exception?.description||t.exceptionDetails.text);return t.result?.value}disconnect(){try{this.socket.close()}catch{}}}async function j(e,t={}){let r=new WebSocket(e);return await new Promise((n,s)=>{r.addEventListener("open",()=>n(),{once:!0}),r.addEventListener("error",()=>s(Error(`Could not connect to Chrome at ${e}`)),{once:!0})}),new O(r,t)}var D=["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome","/Applications/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing","/Applications/Chromium.app/Contents/MacOS/Chromium","/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary"],V=["google-chrome","google-chrome-stable","chromium","chromium-browser","chrome"],I=["KSProductID","KSUpdateURL","KSChannelID","KSVersion"],E="dev.stacks.testing.chrome";function W(){if(b.env.CHROME_PATH)return b.env.CHROME_PATH;if(b.platform==="darwin"){let e=D.find((t)=>C(t));if(e)return e}for(let e of V){let t=Bun.which(e);if(t)return t}return null}function N(e){let t=/^(.*?\.app)\/Contents\/MacOS\/[^/]+$/.exec(e);return t?t[1]:null}function P(e,t){let r=Bun.spawnSync(["/usr/libexec/PlistBuddy","-c",t,e],{stdout:"pipe",stderr:"pipe"});return{ok:r.exitCode===0,out:r.stdout.toString().trim()}}function G(e){let t=N(e);if(!t||b.platform!=="darwin")return!1;let r=m(t,"Contents/Info.plist");return C(r)&&I.some((n)=>P(r,`Print :${n}`).ok)}function K(e={}){if(b.platform!=="darwin")throw Error("prepareMacChrome is for macOS; elsewhere launch Chrome as installed");let t=e.source??D[0],r=N(t);if(!r||!C(r))throw Error(`No Chrome app bundle at ${t}`);let n=m(r,"Contents/Info.plist"),s=P(n,"Print :CFBundleShortVersionString").out||"unknown",i=P(n,"Print :CFBundleExecutable").out||"Google Chrome",a=e.directory??m(L(),`stacks-chrome-${s.replace(/[^\w.-]/g,"_")}`),c=m(a,"Chrome.app"),p=m(c,"Contents/MacOS",i),h=m(c,"Contents/Info.plist");if(C(p)&&P(h,"Print :CFBundleIdentifier").out===E&&P(h,"Print :CFBundleShortVersionString").out===s)return{executable:p,app:c,reused:!0};U(a,{recursive:!0});let g=M(m(a,"staging-")),l=m(g,"Chrome.app"),o=(f,y)=>{let d=Bun.spawnSync(f,{stdout:"pipe",stderr:"pipe"});if(d.exitCode!==0)throw Error(`${y} failed: ${d.stderr.toString().trim()}`)};try{o(["cp","-cR",r,l],"Cloning Chrome");let f=m(l,"Contents/Info.plist");for(let y of I)P(f,`Delete :${y}`);o(["/usr/libexec/PlistBuddy","-c",`Set :CFBundleIdentifier ${E}`,f],"Renaming the Chrome bundle"),o(["codesign","--force","--deep","--sign","-",l],"Re-signing Chrome"),R(c,{recursive:!0,force:!0}),B(l,c)}finally{R(g,{recursive:!0,force:!0})}return{executable:p,app:c,reused:!1}}var z=["*/_stx/hmr*"];async function J(e,t=z){if(!t.length)return async()=>{};return await e.intercept(t.map((r)=>({urlPattern:r})),(r,n)=>{n.send("Fetch.fulfillRequest",{requestId:r.requestId,responseCode:200,responseHeaders:[{name:"Content-Type",value:"text/event-stream"}],body:Buffer.from(`retry: 86400000
2
+ import{closeSync as W,existsSync as E,mkdirSync as K,mkdtempSync as _,openSync as z,readSync as J,renameSync as Y,rmSync as F}from"fs";import{tmpdir as U}from"os";import{join as C}from"path";import x from"process";async function A(e,t=12000,r={}){let n=Date.now(),s=await e();while(!s&&Date.now()-n<t)await Bun.sleep(r.intervalMs??100),s=await e();if(!s)throw Error(`Timed out after ${t}ms${r.label?` waiting for ${r.label}`:""}`);return s}var Q=new Set(["Fetch.continueRequest","Fetch.fulfillRequest","Fetch.failRequest","Fetch.continueResponse"]);function X(e,t){let r="";for(let n=0;n<e.length;n++){let s=e[n];if(s==="\\"&&n+1<e.length)r+=D(e[++n]);else if(s==="*")r+="[\\s\\S]*";else if(s==="?")r+="[\\s\\S]";else r+=D(s)}return new RegExp(`^${r}$`).test(t)}function D(e){return e.replace(/[.*+?^${}()|[\]\\/]/g,"\\$&")}function I(e,t){let r=t.responseStatusCode!=null||t.responseErrorReason!=null?"Response":"Request";if((e.requestStage??"Request")!==r)return!1;if(e.resourceType&&e.resourceType!==t.resourceType)return!1;return X(e.urlPattern??"*",t.request?.url??"")}class H{socket;id=0;pending=new Map;listeners=new Map;commandTimeoutMs;interceptors=[];ownFetch=null;answered=new Map;errors=[];inFlight=new Map;closed=!1;constructor(e,t={}){this.socket=e;this.commandTimeoutMs=t.commandTimeoutMs??30000,e.addEventListener("close",()=>this.markClosed()),e.addEventListener("message",(r)=>this.receive(String(r.data)))}markClosed(){this.closed=!0;for(let[e,t]of this.pending)clearTimeout(t.timer),t.reject(Error("Chrome closed the connection")),this.pending.delete(e)}receive(e){let t;try{t=JSON.parse(e)}catch{return}if(t.id){let r=this.pending.get(t.id);if(!r)return;if(this.pending.delete(t.id),clearTimeout(r.timer),t.error)r.reject(Error(t.error.message));else r.resolve(t.result);return}if(this.track(t.method,t.params??{}),t.method==="Fetch.requestPaused"&&!this.routePaused(t.params??{}))return;this.dispatch(t.method,t.params??{})}dispatch(e,t){for(let r of[...this.listeners.get(e)??[]])try{r(t)}catch(n){this.errors.push(`Listener for ${e} threw: ${n?.message??String(n)}`)}}routePaused(e){this.remember(e.requestId,!1);let t=this.interceptors.filter((s)=>s.patterns.some((o)=>I(o,e)));for(let s of t)try{let o=s.handler(e,this);if(o&&typeof o.catch==="function")o.catch((i)=>this.errors.push(`Fetch handler threw: ${i?.message??String(i)}`))}catch(o){this.errors.push(`Fetch handler threw: ${o?.message??String(o)}`)}let r=(this.listeners.get("Fetch.requestPaused")?.size??0)>0,n=this.ownFetch?this.ownFetch.patterns.some((s)=>I(s,e)):!1;if(!t.length&&!n&&!r)this.send("Fetch.continueRequest",{requestId:e.requestId}).catch(()=>{});return r&&(n||!t.length)}remember(e,t){if(this.answered.delete(e),this.answered.set(e,t),this.answered.size>2000){for(let[r]of this.answered)if(this.answered.delete(r),this.answered.size<=1000)break}}async applyFetch(){let e=[...this.interceptors.flatMap((t)=>t.patterns),...this.ownFetch?.patterns??[]];if(!e.length)return await this.command("Fetch.disable");return await this.command("Fetch.enable",{...this.ownFetch?.params,patterns:e})}async intercept(e,t){if(!e.length)throw TypeError("intercept needs at least one pattern");let r={patterns:e.map((s)=>({...s})),handler:t};this.interceptors.push(r);try{await this.applyFetch()}catch(s){throw this.interceptors=this.interceptors.filter((o)=>o!==r),s}let n=!1;return async()=>{if(n)return;if(n=!0,this.interceptors=this.interceptors.filter((s)=>s!==r),!this.closed)await this.applyFetch()}}track(e,t){if(e==="Network.requestWillBeSent")this.inFlight.set(t.requestId,`${t.request?.method} ${t.request?.url}`);else if(e==="Network.loadingFinished"||e==="Network.loadingFailed")this.inFlight.delete(t.requestId);else if(e==="Network.responseReceived"&&t.response?.mimeType==="text/event-stream")this.inFlight.delete(t.requestId);if(e==="Runtime.exceptionThrown")this.errors.push(t.exceptionDetails?.exception?.description||t.exceptionDetails?.text||"Runtime exception");else if(e==="Runtime.consoleAPICalled"&&t.type==="error")this.errors.push((t.args||[]).map((r)=>r.value??r.description??"").join(" "));else if(e==="Log.entryAdded"&&t.entry?.level==="error"&&!String(t.entry.text).includes("favicon"))this.errors.push(t.entry.text);else if(e==="Network.loadingFailed"&&!t.canceled)this.errors.push(`Network: ${t.errorText||"request failed"}`)}on(e,t){let r=this.listeners.get(e);if(!r)this.listeners.set(e,r=new Set);return r.add(t),()=>this.off(e,t)}off(e,t){this.listeners.get(e)?.delete(t)}once(e,t=()=>!0,r=this.commandTimeoutMs){return new Promise((n,s)=>{if(this.closed)return s(Error("Chrome closed the connection"));let o=setTimeout(()=>{c(),clearInterval(i),s(Error(`Timed out after ${r}ms waiting for ${e}`))},r),i=setInterval(()=>{if(!this.closed)return;clearTimeout(o),clearInterval(i),c(),s(Error("Chrome closed the connection"))},100),c=this.on(e,(g)=>{if(!t(g))return;clearTimeout(o),clearInterval(i),c(),n(g)})})}send(e,t={}){if(e==="Fetch.enable"){let{patterns:r,...n}=t;return this.ownFetch={patterns:r?.length?r.map((s)=>({...s})):[{urlPattern:"*"}],params:n},this.closed?this.command(e,t):this.applyFetch()}if(e==="Fetch.disable")return this.ownFetch=null,this.closed?this.command(e,t):this.applyFetch();if(Q.has(e)&&typeof t.requestId==="string"){if(this.answered.get(t.requestId)===!0)return Promise.resolve({});if(this.answered.has(t.requestId))this.remember(t.requestId,!0)}return this.command(e,t)}command(e,t={}){let r=++this.id;return new Promise((n,s)=>{if(this.closed)return s(Error("Chrome closed the connection"));let o=setTimeout(()=>{this.pending.delete(r),s(Error(`Chrome command timed out: ${e}`))},this.commandTimeoutMs);this.pending.set(r,{resolve:n,reject:s,timer:o}),this.socket.send(JSON.stringify({id:r,method:e,params:t}))})}async evaluate(e){let t=await this.send("Runtime.evaluate",{expression:e,awaitPromise:!0,returnByValue:!0});if(t.exceptionDetails)throw Error(t.exceptionDetails.exception?.description||t.exceptionDetails.text);return t.result?.value}disconnect(){this.markClosed();try{this.socket.close()}catch{}}}async function Z(e,t={}){let r=new WebSocket(e);return await new Promise((n,s)=>{let o=(h)=>{if(clearTimeout(g),r.removeEventListener("open",i),r.removeEventListener("error",c),r.removeEventListener("close",c),h){try{r.close()}catch{}s(h)}else n()},i=()=>o(),c=()=>o(Error(`Could not connect to Chrome at ${e}`)),g=setTimeout(()=>o(Error(`Timed out connecting to Chrome at ${e}`)),t.connectTimeoutMs??30000);r.addEventListener("open",i,{once:!0}),r.addEventListener("error",c,{once:!0}),r.addEventListener("close",c,{once:!0})}),new H(r,t)}var j=["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome","/Applications/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing","/Applications/Chromium.app/Contents/MacOS/Chromium","/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary"],ee=["google-chrome","google-chrome-stable","chromium","chromium-browser","chrome"],G=["KSProductID","KSUpdateURL","KSChannelID","KSVersion"],O="dev.stacks.testing.chrome";function te(){if(x.env.CHROME_PATH)return x.env.CHROME_PATH;if(x.platform==="darwin"){let e=j.find((t)=>E(t));if(e)return e}for(let e of ee){let t=Bun.which(e);if(t)return t}return null}function V(e){let t=/^(.*?\.app)\/Contents\/MacOS\/[^/]+$/.exec(e);return t?t[1]:null}function S(e,t){let r=Bun.spawnSync(["/usr/libexec/PlistBuddy","-c",t,e],{stdout:"pipe",stderr:"pipe"});return{ok:r.exitCode===0,out:r.stdout.toString().trim()}}function re(e){let t=V(e);if(!t||x.platform!=="darwin")return!1;let r=C(t,"Contents/Info.plist");return E(r)&&G.some((n)=>S(r,`Print :${n}`).ok)}function ne(e={}){if(x.platform!=="darwin")throw Error("prepareMacChrome is for macOS; elsewhere launch Chrome as installed");let t=e.source??j[0],r=V(t);if(!r||!E(r))throw Error(`No Chrome app bundle at ${t}`);let n=C(r,"Contents/Info.plist"),s=S(n,"Print :CFBundleShortVersionString").out||"unknown",o=S(n,"Print :CFBundleExecutable").out||"Google Chrome",i=e.directory??C(U(),`stacks-chrome-${s.replace(/[^\w.-]/g,"_")}`),c=C(i,"Chrome.app"),g=C(c,"Contents/MacOS",o),h=C(c,"Contents/Info.plist");if(E(g)&&S(h,"Print :CFBundleIdentifier").out===O&&S(h,"Print :CFBundleShortVersionString").out===s)return{executable:g,app:c,reused:!0};K(i,{recursive:!0});let y=_(C(i,"staging-")),b=C(y,"Chrome.app"),P=(w,f)=>{let u=Bun.spawnSync(w,{stdout:"pipe",stderr:"pipe"});if(u.exitCode!==0)throw Error(`${f} failed: ${u.stderr.toString().trim()}`)};try{P(["cp","-cR",r,b],"Cloning Chrome");let w=C(b,"Contents/Info.plist");for(let f of G)S(w,`Delete :${f}`);P(["/usr/libexec/PlistBuddy","-c",`Set :CFBundleIdentifier ${O}`,w],"Renaming the Chrome bundle"),P(["codesign","--force","--deep","--sign","-",b],"Re-signing Chrome"),F(c,{recursive:!0,force:!0}),Y(b,c)}finally{F(y,{recursive:!0,force:!0})}return{executable:g,app:c,reused:!1}}class k extends Error{diagnostics;constructor(e,t){super(`${e instanceof Error?e.message:String(e)}
3
+ Chrome startup: ${JSON.stringify(t)}`,{cause:e});this.diagnostics=t;this.name="ChromeLaunchError"}}async function q(e,t){let r;try{return await Promise.race([e.then(()=>!0),new Promise((n)=>{r=setTimeout(()=>n(!1),t)})])}finally{clearTimeout(r)}}function M(e){let t;try{t=z(e,"r");let r=Buffer.alloc(1024);return r.subarray(0,J(t,r,0,r.length,0)).toString("utf8")}catch{return null}finally{if(t!==void 0)try{W(t)}catch{}}}var se=["*/_stx/hmr*"];async function oe(e,t=se){if(!t.length)return async()=>{};return await e.intercept(t.map((r)=>({urlPattern:r})),(r,n)=>{n.send("Fetch.fulfillRequest",{requestId:r.requestId,responseCode:200,responseHeaders:[{name:"Content-Type",value:"text/event-stream"}],body:Buffer.from(`retry: 86400000
3
4
 
4
- `).toString("base64")}).catch(()=>{})})}function Y(e){let t=e.chromePath??b.env.CHROME_PATH;if(t)return t;let r=W();if(!r)throw Error("No Chrome found: install Google Chrome or Chromium, or set CHROME_PATH");if((e.stableMacChrome??!0)&&b.platform==="darwin"&&G(r))return K({source:r}).executable;return r}async function Q(e={}){let t=Y(e);if(!C(t))throw Error(`Chrome was not found at ${t}`);let r=e.profile||M(m(L(),"stacks-browser-"));R(m(r,"DevToolsActivePort"),{force:!0});let n=Bun.spawn([t,...e.headless===!1?[]:["--headless=new"],"--remote-debugging-port=0",`--user-data-dir=${r}`,"--no-first-run","--no-default-browser-check","--disable-background-networking","--disable-component-update","--disable-extensions","--hide-scrollbars","--use-mock-keychain",...e.args??[],"about:blank"],{stdout:"ignore",stderr:"ignore"}),s,i=!1,a=async()=>{if(i)return;if(i=!0,s?.disconnect(),n.kill(),await n.exited,!e.profile)R(r,{recursive:!0,force:!0})};try{let c=m(r,"DevToolsActivePort");await S(()=>C(c)&&q(c,"utf8").includes(`
5
- `),e.startTimeoutMs??30000,{label:"Chrome to start"});let p=Number(q(c,"utf8").split(`
6
- `)[0]),h=await S(async()=>{let o=await fetch(`http://127.0.0.1:${p}/json/list`).then((f)=>f.json()).catch(()=>[]);return o.length?o:null},12000,{label:"a page target"}),g=h.find((o)=>o.type==="page"&&o.url==="about:blank")||h.find((o)=>o.type==="page");if(!g)throw Error("Chrome did not expose a page target");s=await j(g.webSocketDebuggerUrl,e);let l=s;if(await Promise.all([l.send("Page.enable"),l.send("Runtime.enable"),l.send("Network.enable"),l.send("Log.enable"),...e.viewport===!1?[]:[l.send("Emulation.setDeviceMetricsOverride",{deviceScaleFactor:1,mobile:!1,width:1440,height:1000,...e.viewport})]]),e.userAgent!==!1){let o=typeof e.userAgent==="string"?e.userAgent:String((await l.send("Browser.getVersion")).userAgent||"").replace("HeadlessChrome","Chrome");if(o)await l.send("Network.setUserAgentOverride",{userAgent:o})}if(e.quietLiveReload!==!1)await J(l,e.quietLiveReload);if(e.acceptDialogs!==!1)l.on("Page.javascriptDialogOpening",(o)=>{l.errors.push(`Dialog (${o.type}) on ${o.url}: ${o.message||"(no message)"}`),l.send("Page.handleJavaScriptDialog",{accept:!0}).catch(()=>{})});return{cdp:l,close:a,target:g.webSocketDebuggerUrl,port:p,chromePath:t}}catch(c){throw await a(),c}}async function X(e,t={}){let r=t.quietMs??1000,n=t.timeoutMs??15000,s=Date.now(),i=0;while(Date.now()-s<n){if(e.closed)throw Error("Chrome closed the connection");if(e.inFlight.size>0||await e.evaluate("document.readyState !== 'complete'").catch(()=>!0))i=0;else if(!i)i=Date.now();else if(Date.now()-i>=r)return!0;await Bun.sleep(100)}return!1}async function Z(e,t,r={}){let n=await e.send("Page.navigate",{url:t});if(n?.errorText)throw Error(`Could not open ${t}: ${n.errorText}`);return await X(e,r)}var ee=(e)=>e.pathname.startsWith("/api/")&&!e.pathname.endsWith("/events");function te(e){let t={};for(let r of e)t[r]=(t[r]||0)+1;return Object.fromEntries(Object.entries(t).filter(([,r])=>r>1))}function k(e){try{let t=new URL(e);return`${t.origin}${t.pathname}`}catch{return e.replace(/[?#].*$/,"")}}function re(e,t,r="navigation"){let n=[{url:e,apiRequests:[]}],s=!1;for(let i of t){let a=n.at(-1);if(i.type==="request"){a.apiRequests.push(i.request);continue}let c=r==="navigation"&&s&&(i.sameDocument?k(i.url)!==k(a.url):!0);if(s=!0,c)n.push({url:i.url,apiRequests:[]});else a.url=i.url}return n.map((i)=>({...i,duplicates:te(i.apiRequests)}))}function ne(e){let t={};for(let r of e)for(let[n,s]of Object.entries(r.duplicates))t[n]=Math.max(t[n]??0,s);return t}async function ce(e,t={}){let r=Date.now(),n=new URL(e).origin,s=t.isApiRequest??ee,i=[],a=[],c=[],p=null,h=t.cdp,g=[],l=h?.errors.length??0,o,f=e,y=async(d,x=[])=>{let u=re(e,a,t.pageLoads),w=f;if(h&&!h.closed){let v=await h.evaluate("location.href").catch(()=>null);if(v&&v!=="about:blank"&&!v.startsWith("chrome-error:"))w=v}return{url:e,finalUrl:w,apiRequests:i,duplicates:ne(u),loads:u,failed:c,errors:[...h?.errors.slice(l)??[],...x],settled:d,durationMs:Date.now()-r}};try{if(!h)p=await Q(t.launch),h=p.cdp;let d=h;if(g.push(d.on("Network.requestWillBeSent",(u)=>{let w=new URL(u.request.url);if(w.origin===n&&s(w,u.request.method)){let v=`${u.request.method} ${w.pathname}${w.search}`;i.push(v),a.push({type:"request",request:v})}})),g.push(d.on("Network.responseReceived",(u)=>{let w=new URL(u.response.url);if(w.origin===n&&u.response.status>=400)c.push(`${u.response.status} ${w.pathname}`)})),g.push(d.on("Page.frameNavigated",(u)=>{if(u.frame?.parentId)return;o=u.frame?.id??o,f=u.frame?.unreachableUrl||`${u.frame?.url??f}${u.frame?.urlFragment??""}`,a.push({type:"navigated",url:f,sameDocument:!1})})),g.push(d.on("Page.navigatedWithinDocument",(u)=>{if(o&&u.frameId!==o)return;f=u.url,a.push({type:"navigated",url:u.url,sameDocument:!0})})),await d.send("Page.enable"),o=(await d.send("Page.getFrameTree").catch(()=>null))?.frameTree?.frame?.id,t.viewport)await d.send("Emulation.setDeviceMetricsOverride",{deviceScaleFactor:1,mobile:!1,...t.viewport});await t.beforeNavigate?.(d);let x=await Z(d,e,t);return await y(x)}catch(d){return await y(!1,[`audit: ${d?.message??String(d)}`])}finally{for(let d of g)d();await p?.close().catch(()=>{})}}function A(e){try{let t=new URL(e);return`${t.pathname}${t.search}`}catch{return e}}function ue(e){let t=A(e.url),r=e.finalUrl?A(e.finalUrl):t,n=Object.entries(e.duplicates).map(([s,i])=>`${i}x ${s}`);return`${t}${r!==t?` -> ${r}`:""}: ${e.apiRequests.length} api${n.length?` | dup ${n.join(", ")}`:""}${e.failed.length?` | failed ${e.failed.join(", ")}`:""}${e.errors.length?` | errors ${e.errors.slice(0,3).join(" ; ")}`:""}${e.settled?"":" | never settled"}`}function le(e){return e.settled&&!Object.keys(e.duplicates).length&&!e.failed.length&&!e.errors.length}export{O as Cdp,z as LIVE_RELOAD_PATTERNS,ce as auditPage,G as chromeHasUpdater,j as connect,te as duplicateRequests,W as findChrome,ue as formatPageAudit,Q as launch,ne as loadDuplicates,H as matchesUrlPattern,Z as navigate,le as pageAuditClean,F as pausedRequestMatches,K as prepareMacChrome,J as quietLiveReload,X as settle,re as splitPageLoads,S as waitFor};
5
+ `).toString("base64")}).catch(()=>{})})}function ie(e){let t=e.chromePath??x.env.CHROME_PATH;if(t)return t;let r=te();if(!r)throw Error("No Chrome found: install Google Chrome or Chromium, or set CHROME_PATH");if((e.stableMacChrome??!0)&&x.platform==="darwin"&&re(r))return ne({source:r}).executable;return r}async function ae(e={}){let t=e.startTimeoutMs??30000;if(!Number.isFinite(t)||t<=0)throw RangeError("startTimeoutMs must be positive and finite");let r=ie(e),n=Date.now();if(!E(r))throw Error(`Chrome was not found at ${r}`);let s=e.profile||_(C(U(),"stacks-browser-"));F(C(s,"DevToolsActivePort"),{force:!0});let o=C(s,"DevToolsActivePort"),i=(()=>{try{return Bun.spawn([r,...e.headless===!1?[]:["--headless=new"],"--remote-debugging-port=0",`--user-data-dir=${s}`,"--no-first-run","--no-default-browser-check","--disable-background-networking","--disable-component-update","--disable-extensions","--hide-scrollbars","--use-mock-keychain",...e.args??[],"about:blank"],{stdin:"ignore",stdout:"ignore",stderr:"pipe"})}catch(p){let a=null;try{if(!e.profile)F(s,{recursive:!0,force:!0})}catch(l){a=String(l)}throw new k(p,{pid:null,chromePath:r,profile:s,temporaryProfile:!e.profile,elapsedMs:Date.now()-n,exitCodeBeforeCleanup:null,signalCodeBeforeCleanup:null,exitCode:null,signalCode:null,portFileExisted:!1,portFileContents:null,stderrTail:"",stderrTruncated:!1,cleanupError:a})}})(),c="",g=!1,h,y=Promise.resolve(),b,P,w=()=>P??=(async()=>{b?.disconnect();let p=!1,a=[];try{if(i.exitCode===null&&i.signalCode===null)try{i.kill("SIGTERM")}catch{}if(!await q(i.exited,2000)){try{i.kill("SIGKILL")}catch{}if(!await q(i.exited,2000))throw Error(`Chrome process ${i.pid} did not exit after SIGKILL`)}p=!0}catch(l){a.push(String(l)),p=i.exitCode!==null||i.signalCode!==null}finally{try{if(!await q(y,100)&&h){if(!await q(h.cancel(),100))throw Error("Chrome stderr cancellation did not finish")}if(!await q(y,100))throw Error("Chrome stderr reader did not finish")}catch(l){a.push(String(l))}finally{if(p&&!e.profile)try{F(s,{recursive:!0,force:!0})}catch(l){a.push(String(l))}}}if(a.length)throw Error(`Chrome cleanup failed: ${a.join("; ")}`)})(),f=()=>{let p=t-(Date.now()-n);if(p<=0)throw Error(`Timed out after ${t}ms starting Chrome`);if(i.exitCode!==null||i.signalCode!==null)throw Error("Chrome exited during startup");return p},u=setTimeout(()=>b?.disconnect(),Math.max(1,t-(Date.now()-n)));try{h=i.stderr.getReader();let p=h;y=(async()=>{let d=new TextDecoder;for(;;){let{value:T,done:R}=await p.read(),L=d.decode(T,{stream:!R});if(g||=c.length+L.length>16384,c=(c+L).slice(-16384),R)return}})().catch((d)=>{c=`${c}
6
+ [stderr read failed: ${String(d)}]`.slice(-16384)}),await A(()=>(f(),M(o)?.includes(`
7
+ `)),f(),{label:"Chrome to start"});let a=Number(M(o)?.split(`
8
+ `)[0]);if(!Number.isInteger(a)||a<1||a>65535)throw Error("Chrome wrote an invalid DevToolsActivePort");let l=await A(async()=>{let d=await fetch(`http://127.0.0.1:${a}/json/list`,{signal:AbortSignal.timeout(Math.min(1000,f()))}).then((T)=>T.json()).catch(()=>[]);return Array.isArray(d)&&d.length?d:null},Math.min(12000,f()),{label:"a page target"}),v=l.find((d)=>d.type==="page"&&d.url==="about:blank")||l.find((d)=>d.type==="page");if(!v)throw Error("Chrome did not expose a page target");b=await Z(v.webSocketDebuggerUrl,{...e,connectTimeoutMs:Math.min(e.connectTimeoutMs??30000,f())}),f();let m=b;if(await Promise.all([m.send("Page.enable"),m.send("Runtime.enable"),m.send("Network.enable"),m.send("Log.enable"),...e.viewport===!1?[]:[m.send("Emulation.setDeviceMetricsOverride",{deviceScaleFactor:1,mobile:!1,width:1440,height:1000,...e.viewport})]]),e.userAgent!==!1){let d=typeof e.userAgent==="string"?e.userAgent:String((await m.send("Browser.getVersion")).userAgent||"").replace("HeadlessChrome","Chrome");if(d)await m.send("Network.setUserAgentOverride",{userAgent:d})}if(e.quietLiveReload!==!1)await oe(m,e.quietLiveReload);if(e.acceptDialogs!==!1)m.on("Page.javascriptDialogOpening",(d)=>{m.errors.push(`Dialog (${d.type}) on ${d.url}: ${d.message||"(no message)"}`),m.send("Page.handleJavaScriptDialog",{accept:!0}).catch(()=>{})});return f(),clearTimeout(u),{cdp:m,close:w,target:v.webSocketDebuggerUrl,port:a,chromePath:r}}catch(p){clearTimeout(u);let a=Date.now()-n,{exitCode:l,signalCode:v}=i,m=E(o),d=M(o),T=null;try{await w()}catch(R){T=R instanceof Error?R.message:String(R)}throw new k(p,{pid:i.pid,chromePath:r,profile:s,temporaryProfile:!e.profile,elapsedMs:a,exitCodeBeforeCleanup:l,signalCodeBeforeCleanup:v,exitCode:i.exitCode,signalCode:i.signalCode,portFileExisted:m,portFileContents:d,stderrTail:c,stderrTruncated:g,cleanupError:T})}}async function ce(e,t={}){let r=t.quietMs??1000,n=t.timeoutMs??15000,s=Date.now(),o=0;while(Date.now()-s<n){if(e.closed)throw Error("Chrome closed the connection");if(e.inFlight.size>0||await e.evaluate("document.readyState !== 'complete'").catch(()=>!0))o=0;else if(!o)o=Date.now();else if(Date.now()-o>=r)return!0;await Bun.sleep(100)}return!1}async function le(e,t,r={}){let n=await e.send("Page.navigate",{url:t});if(n?.errorText)throw Error(`Could not open ${t}: ${n.errorText}`);return await ce(e,r)}var ue=(e)=>e.pathname.startsWith("/api/")&&!e.pathname.endsWith("/events");function de(e){let t={};for(let r of e)t[r]=(t[r]||0)+1;return Object.fromEntries(Object.entries(t).filter(([,r])=>r>1))}function N(e){try{let t=new URL(e);return`${t.origin}${t.pathname}`}catch{return e.replace(/[?#].*$/,"")}}function he(e,t,r="navigation"){let n=[{url:e,apiRequests:[]}],s=!1;for(let o of t){let i=n.at(-1);if(o.type==="request"){i.apiRequests.push(o.request);continue}let c=r==="navigation"&&s&&(o.sameDocument?N(o.url)!==N(i.url):!0);if(s=!0,c)n.push({url:o.url,apiRequests:[]});else i.url=o.url}return n.map((o)=>({...o,duplicates:de(o.apiRequests)}))}function pe(e){let t={};for(let r of e)for(let[n,s]of Object.entries(r.duplicates))t[n]=Math.max(t[n]??0,s);return t}async function ve(e,t={}){let r=Date.now(),n=new URL(e).origin,s=t.isApiRequest??ue,o=[],i=[],c=[],g=null,h=t.cdp,y=[],b=h?.errors.length??0,P,w=e,f=async(u,p=[])=>{let a=he(e,i,t.pageLoads),l=w;if(h&&!h.closed){let v=await h.evaluate("location.href").catch(()=>null);if(v&&v!=="about:blank"&&!v.startsWith("chrome-error:"))l=v}return{url:e,finalUrl:l,apiRequests:o,duplicates:pe(a),loads:a,failed:c,errors:[...h?.errors.slice(b)??[],...p],settled:u,durationMs:Date.now()-r}};try{if(!h)g=await ae(t.launch),h=g.cdp;let u=h;if(y.push(u.on("Network.requestWillBeSent",(a)=>{let l=new URL(a.request.url);if(l.origin===n&&s(l,a.request.method)){let v=`${a.request.method} ${l.pathname}${l.search}`;o.push(v),i.push({type:"request",request:v})}})),y.push(u.on("Network.responseReceived",(a)=>{let l=new URL(a.response.url);if(l.origin===n&&a.response.status>=400)c.push(`${a.response.status} ${l.pathname}`)})),y.push(u.on("Page.frameNavigated",(a)=>{if(a.frame?.parentId)return;P=a.frame?.id??P,w=a.frame?.unreachableUrl||`${a.frame?.url??w}${a.frame?.urlFragment??""}`,i.push({type:"navigated",url:w,sameDocument:!1})})),y.push(u.on("Page.navigatedWithinDocument",(a)=>{if(P&&a.frameId!==P)return;w=a.url,i.push({type:"navigated",url:a.url,sameDocument:!0})})),await u.send("Page.enable"),P=(await u.send("Page.getFrameTree").catch(()=>null))?.frameTree?.frame?.id,t.viewport)await u.send("Emulation.setDeviceMetricsOverride",{deviceScaleFactor:1,mobile:!1,...t.viewport});await t.beforeNavigate?.(u);let p=await le(u,e,t);return await f(p)}catch(u){return await f(!1,[`audit: ${u?.message??String(u)}`])}finally{for(let u of y)u();await g?.close().catch(()=>{})}}function B(e){try{let t=new URL(e);return`${t.pathname}${t.search}`}catch{return e}}function ye(e){let t=B(e.url),r=e.finalUrl?B(e.finalUrl):t,n=Object.entries(e.duplicates).map(([s,o])=>`${o}x ${s}`);return`${t}${r!==t?` -> ${r}`:""}: ${e.apiRequests.length} api${n.length?` | dup ${n.join(", ")}`:""}${e.failed.length?` | failed ${e.failed.join(", ")}`:""}${e.errors.length?` | errors ${e.errors.slice(0,3).join(" ; ")}`:""}${e.settled?"":" | never settled"}`}function Ce(e){return e.settled&&!Object.keys(e.duplicates).length&&!e.failed.length&&!e.errors.length}export{H as Cdp,k as ChromeLaunchError,se as LIVE_RELOAD_PATTERNS,ve as auditPage,re as chromeHasUpdater,Z as connect,de as duplicateRequests,te as findChrome,ye as formatPageAudit,ae as launch,pe as loadDuplicates,X as matchesUrlPattern,le as navigate,Ce as pageAuditClean,I as pausedRequestMatches,ne as prepareMacChrome,oe as quietLiveReload,ce as settle,he as splitPageLoads,A as waitFor};
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/testing",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.233",
5
+ "version": "0.75.236",
6
6
  "description": "The Stacks way of testing.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -79,16 +79,16 @@
79
79
  "prepublishOnly": "bun run build"
80
80
  },
81
81
  "dependencies": {
82
- "@stacksjs/auth": "0.75.233",
83
- "@stacksjs/cache": "0.75.233",
84
- "@stacksjs/config": "0.75.233",
85
- "@stacksjs/database": "0.75.233",
86
- "@stacksjs/email": "0.75.233",
87
- "@stacksjs/events": "0.75.233",
88
- "@stacksjs/path": "0.75.233",
89
- "@stacksjs/router": "0.75.233",
90
- "@stacksjs/storage": "0.75.233",
91
- "@stacksjs/strings": "0.75.233",
82
+ "@stacksjs/auth": "0.75.236",
83
+ "@stacksjs/cache": "0.75.236",
84
+ "@stacksjs/config": "0.75.236",
85
+ "@stacksjs/database": "0.75.236",
86
+ "@stacksjs/email": "0.75.236",
87
+ "@stacksjs/events": "0.75.236",
88
+ "@stacksjs/path": "0.75.236",
89
+ "@stacksjs/router": "0.75.236",
90
+ "@stacksjs/storage": "0.75.236",
91
+ "@stacksjs/strings": "0.75.236",
92
92
  "@stacksjs/ts-cloud": "^0.16.58",
93
93
  "very-happy-dom": "^0.3.5"
94
94
  },