@stacksjs/testing 0.75.226 → 0.75.230
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 +147 -0
- package/dist/browser.js +6 -0
- package/dist/disposable-database.d.ts +42 -0
- package/dist/disposable-database.js +2 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +11 -2
- package/dist/isolated.d.ts +41 -0
- package/dist/isolated.js +11 -0
- package/package.json +17 -11
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Poll `read` until it returns something truthy, and return that. Throws
|
|
3
|
+
* after `timeoutMs` (default 12 s) with `label` in the message.
|
|
4
|
+
*/
|
|
5
|
+
export declare function waitFor<T>(read: () => Promise<T> | T, timeoutMs?: unknown, options?: { intervalMs?: number, label?: string }): Promise<NonNullable<T>>;
|
|
6
|
+
/** Open a session on a DevTools WebSocket URL (`ws://127.0.0.1:9222/devtools/page/...`). */
|
|
7
|
+
export declare function connect(url: string, options?: CdpOptions): Promise<Cdp>;
|
|
8
|
+
/** The Chrome to drive: `CHROME_PATH`, else the first installed one found. Null when there is none. */
|
|
9
|
+
export declare function findChrome(): string | null;
|
|
10
|
+
/** True when this Chrome would be killed by GoogleUpdater (it still carries the updater's keys). */
|
|
11
|
+
export declare function chromeHasUpdater(executable: string): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* A private clone of a macOS Chrome that GoogleUpdater leaves alone (see the
|
|
14
|
+
* module docs): `cp -cR`, the `KS*` keys deleted from `Info.plist`, its own
|
|
15
|
+
* `CFBundleIdentifier`, `codesign --force --deep --sign -`. Kept in
|
|
16
|
+
* `directory` (default a per-version folder in the temp directory) and
|
|
17
|
+
* reused while its version matches the source's.
|
|
18
|
+
*/
|
|
19
|
+
export declare function prepareMacChrome(options?: { source?: string, directory?: string }): PreparedChrome;
|
|
20
|
+
/**
|
|
21
|
+
* Start a private headless Chrome and open a session on its page, with the
|
|
22
|
+
* `Page`, `Runtime`, `Network` and `Log` domains enabled.
|
|
23
|
+
*/
|
|
24
|
+
export declare function launch(options?: LaunchOptions): Promise<LaunchedChrome>;
|
|
25
|
+
/**
|
|
26
|
+
* Wait until the document has loaded and no request has been in flight for
|
|
27
|
+
* `quietMs` (event streams excluded, see `Cdp`). Resolves true once settled,
|
|
28
|
+
* false at the timeout (a page that never goes quiet is often the finding);
|
|
29
|
+
* rejects only if Chrome goes away.
|
|
30
|
+
*/
|
|
31
|
+
export declare function settle(cdp: Cdp, options?: SettleOptions): Promise<boolean>;
|
|
32
|
+
/** Navigate and wait for the page to settle. Resolves with whether it settled. */
|
|
33
|
+
export declare function navigate(cdp: Cdp, url: string, options?: SettleOptions): Promise<boolean>;
|
|
34
|
+
/** Count how often each entry appears, keeping those seen more than once. */
|
|
35
|
+
export declare function duplicateRequests(requests: readonly string[]): Record<string, number>;
|
|
36
|
+
/**
|
|
37
|
+
* Load `url` once and report what it cost and what went wrong: the same API
|
|
38
|
+
* request made more than once in one load, failed same-origin requests, and
|
|
39
|
+
* console errors. Never throws for a page problem: a launch or navigation
|
|
40
|
+
* failure is reported in `errors`.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* const report = await auditPage('http://localhost:3000/dashboard', {
|
|
44
|
+
* beforeNavigate: cdp => cdp.send('Page.addScriptToEvaluateOnNewDocument', { source: `localStorage.setItem('token', ${JSON.stringify(token)})` }),
|
|
45
|
+
* })
|
|
46
|
+
* expect(report.duplicates).toEqual({})
|
|
47
|
+
* expect(report.failed).toEqual([])
|
|
48
|
+
*/
|
|
49
|
+
export declare function auditPage(url: string, options?: PageAuditOptions): Promise<PageAuditReport>;
|
|
50
|
+
/** One line per page: `/path: 12 api | dup 2x GET /api/me | failed 404 /api/x | errors ...`. */
|
|
51
|
+
export declare function formatPageAudit(report: PageAuditReport): string;
|
|
52
|
+
/** True when the report found nothing: no duplicates, failures or errors, and the page settled. */
|
|
53
|
+
export declare function pageAuditClean(report: PageAuditReport): boolean;
|
|
54
|
+
export declare interface CdpOptions {
|
|
55
|
+
commandTimeoutMs?: number
|
|
56
|
+
}
|
|
57
|
+
export declare interface PreparedChrome {
|
|
58
|
+
executable: string
|
|
59
|
+
app: string
|
|
60
|
+
reused: boolean
|
|
61
|
+
}
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Launching
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
export declare interface Viewport {
|
|
66
|
+
width: number
|
|
67
|
+
height: number
|
|
68
|
+
deviceScaleFactor?: number
|
|
69
|
+
mobile?: boolean
|
|
70
|
+
}
|
|
71
|
+
export declare interface LaunchOptions extends CdpOptions {
|
|
72
|
+
chromePath?: string
|
|
73
|
+
stableMacChrome?: boolean
|
|
74
|
+
profile?: string
|
|
75
|
+
headless?: boolean
|
|
76
|
+
viewport?: Viewport | false
|
|
77
|
+
args?: string[]
|
|
78
|
+
quietLiveReload?: string[] | false
|
|
79
|
+
acceptDialogs?: boolean
|
|
80
|
+
userAgent?: boolean | string
|
|
81
|
+
startTimeoutMs?: number
|
|
82
|
+
}
|
|
83
|
+
export declare interface LaunchedChrome {
|
|
84
|
+
cdp: Cdp
|
|
85
|
+
close: () => Promise<void>
|
|
86
|
+
target: string
|
|
87
|
+
port: number
|
|
88
|
+
chromePath: string
|
|
89
|
+
}
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Settling and navigating
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
export declare interface SettleOptions {
|
|
94
|
+
quietMs?: number
|
|
95
|
+
timeoutMs?: number
|
|
96
|
+
}
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Page audit
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
export declare interface PageAuditOptions extends SettleOptions {
|
|
101
|
+
cdp?: Cdp
|
|
102
|
+
launch?: LaunchOptions
|
|
103
|
+
viewport?: Viewport
|
|
104
|
+
isApiRequest?: (url: URL, method: string) => boolean
|
|
105
|
+
beforeNavigate?: (cdp: Cdp) => Promise<void> | void
|
|
106
|
+
}
|
|
107
|
+
export declare interface PageAuditReport {
|
|
108
|
+
url: string
|
|
109
|
+
apiRequests: string[]
|
|
110
|
+
duplicates: Record<string, number>
|
|
111
|
+
failed: string[]
|
|
112
|
+
errors: string[]
|
|
113
|
+
settled: boolean
|
|
114
|
+
durationMs: number
|
|
115
|
+
}
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// The protocol session
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
declare type Listener = (params: any) => void;
|
|
120
|
+
/**
|
|
121
|
+
* One DevTools Protocol session.
|
|
122
|
+
*
|
|
123
|
+
* Besides commands and events it keeps two things a test usually wants:
|
|
124
|
+
*
|
|
125
|
+
* - `errors`: uncaught exceptions, `console.error` calls, error log entries
|
|
126
|
+
* (favicon misses excluded) and failed requests (cancellations excluded);
|
|
127
|
+
* - `inFlight`: the requests still loading, so a stalled page can say what it
|
|
128
|
+
* is waiting on. An event stream (`text/event-stream`: messages,
|
|
129
|
+
* notifications, live reload) stays open for the life of the page, so it
|
|
130
|
+
* leaves `inFlight` as soon as its response starts; counting it would keep
|
|
131
|
+
* every wait for a quiet network running to its timeout.
|
|
132
|
+
*
|
|
133
|
+
* Enable the `Network`, `Runtime` and `Log` domains for those to fill;
|
|
134
|
+
* `launch()` does.
|
|
135
|
+
*/
|
|
136
|
+
export declare class Cdp {
|
|
137
|
+
errors: string[];
|
|
138
|
+
inFlight: unknown;
|
|
139
|
+
closed: boolean;
|
|
140
|
+
constructor(socket: WebSocket, options?: CdpOptions);
|
|
141
|
+
on(method: string, listener: Listener): () => void;
|
|
142
|
+
off(method: string, listener: Listener): void;
|
|
143
|
+
once<T = any>(method: string, match?: (params: T) => boolean, timeoutMs?: unknown): Promise<T>;
|
|
144
|
+
send<T = any>(method: string, params?: Record<string, unknown>): Promise<T>;
|
|
145
|
+
evaluate<T = any>(expression: string): Promise<T>;
|
|
146
|
+
disconnect(): void;
|
|
147
|
+
}
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import{existsSync as b,mkdirSync as A,mkdtempSync as T,readFileSync as y,renameSync as q,rmSync as v}from"fs";import{tmpdir as R}from"os";import{join as p}from"path";import m from"process";async function x(e,t=12000,r={}){let n=Date.now(),i=await e();while(!i&&Date.now()-n<t)await Bun.sleep(r.intervalMs??100),i=await e();if(!i)throw Error(`Timed out after ${t}ms${r.label?` waiting for ${r.label}`:""}`);return i}class E{socket;id=0;pending=new Map;listeners=new Map;commandTimeoutMs;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}this.track(t.method,t.params??{});for(let r of[...this.listeners.get(t.method)??[]])try{r(t.params??{})}catch(n){this.errors.push(`Listener for ${t.method} threw: ${n?.message??String(n)}`)}}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,i)=>{if(this.closed)return i(Error("Chrome closed the connection"));let a=setTimeout(()=>{c(),clearInterval(l),i(Error(`Timed out after ${r}ms waiting for ${e}`))},r),l=setInterval(()=>{if(!this.closed)return;clearTimeout(a),clearInterval(l),c(),i(Error("Chrome closed the connection"))},100),c=this.on(e,(u)=>{if(!t(u))return;clearTimeout(a),clearInterval(l),c(),n(u)})})}send(e,t={}){let r=++this.id;return new Promise((n,i)=>{if(this.closed)return i(Error("Chrome closed the connection"));let a=setTimeout(()=>{this.pending.delete(r),i(Error(`Chrome command timed out: ${e}`))},this.commandTimeoutMs);this.pending.set(r,{resolve:n,reject:i,timer:a}),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 D(e,t={}){let r=new WebSocket(e);return await new Promise((n,i)=>{r.addEventListener("open",()=>n(),{once:!0}),r.addEventListener("error",()=>i(Error(`Could not connect to Chrome at ${e}`)),{once:!0})}),new E(r,t)}var M=["/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"],L=["google-chrome","google-chrome-stable","chromium","chromium-browser","chrome"],k=["KSProductID","KSUpdateURL","KSChannelID","KSVersion"],P="dev.stacks.testing.chrome";function I(){if(m.env.CHROME_PATH)return m.env.CHROME_PATH;if(m.platform==="darwin"){let e=M.find((t)=>b(t));if(e)return e}for(let e of L){let t=Bun.which(e);if(t)return t}return null}function O(e){let t=/^(.*?\.app)\/Contents\/MacOS\/[^/]+$/.exec(e);return t?t[1]:null}function w(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 N(e){let t=O(e);if(!t||m.platform!=="darwin")return!1;let r=p(t,"Contents/Info.plist");return b(r)&&k.some((n)=>w(r,`Print :${n}`).ok)}function F(e={}){if(m.platform!=="darwin")throw Error("prepareMacChrome is for macOS; elsewhere launch Chrome as installed");let t=e.source??M[0],r=O(t);if(!r||!b(r))throw Error(`No Chrome app bundle at ${t}`);let n=p(r,"Contents/Info.plist"),i=w(n,"Print :CFBundleShortVersionString").out||"unknown",a=w(n,"Print :CFBundleExecutable").out||"Google Chrome",l=e.directory??p(R(),`stacks-chrome-${i.replace(/[^\w.-]/g,"_")}`),c=p(l,"Chrome.app"),u=p(c,"Contents/MacOS",a),h=p(c,"Contents/Info.plist");if(b(u)&&w(h,"Print :CFBundleIdentifier").out===P&&w(h,"Print :CFBundleShortVersionString").out===i)return{executable:u,app:c,reused:!0};A(l,{recursive:!0});let g=T(p(l,"staging-")),o=p(g,"Chrome.app"),f=(s,d)=>{let C=Bun.spawnSync(s,{stdout:"pipe",stderr:"pipe"});if(C.exitCode!==0)throw Error(`${d} failed: ${C.stderr.toString().trim()}`)};try{f(["cp","-cR",r,o],"Cloning Chrome");let s=p(o,"Contents/Info.plist");for(let d of k)w(s,`Delete :${d}`);f(["/usr/libexec/PlistBuddy","-c",`Set :CFBundleIdentifier ${P}`,s],"Renaming the Chrome bundle"),f(["codesign","--force","--deep","--sign","-",o],"Re-signing Chrome"),v(c,{recursive:!0,force:!0}),q(o,c)}finally{v(g,{recursive:!0,force:!0})}return{executable:u,app:c,reused:!1}}function B(e){let t=e.chromePath??m.env.CHROME_PATH;if(t)return t;let r=I();if(!r)throw Error("No Chrome found: install Google Chrome or Chromium, or set CHROME_PATH");if((e.stableMacChrome??!0)&&m.platform==="darwin"&&N(r))return F({source:r}).executable;return r}async function _(e={}){let t=B(e);if(!b(t))throw Error(`Chrome was not found at ${t}`);let r=e.profile||T(p(R(),"stacks-browser-"));v(p(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"}),i,a=!1,l=async()=>{if(a)return;if(a=!0,i?.disconnect(),n.kill(),await n.exited,!e.profile)v(r,{recursive:!0,force:!0})};try{let c=p(r,"DevToolsActivePort");await x(()=>b(c)&&y(c,"utf8").includes(`
|
|
3
|
+
`),e.startTimeoutMs??30000,{label:"Chrome to start"});let u=Number(y(c,"utf8").split(`
|
|
4
|
+
`)[0]),h=await x(async()=>{let s=await fetch(`http://127.0.0.1:${u}/json/list`).then((d)=>d.json()).catch(()=>[]);return s.length?s:null},12000,{label:"a page target"}),g=h.find((s)=>s.type==="page"&&s.url==="about:blank")||h.find((s)=>s.type==="page");if(!g)throw Error("Chrome did not expose a page target");i=await D(g.webSocketDebuggerUrl,e);let o=i;if(await Promise.all([o.send("Page.enable"),o.send("Runtime.enable"),o.send("Network.enable"),o.send("Log.enable"),...e.viewport===!1?[]:[o.send("Emulation.setDeviceMetricsOverride",{deviceScaleFactor:1,mobile:!1,width:1440,height:1000,...e.viewport})]]),e.userAgent!==!1){let s=typeof e.userAgent==="string"?e.userAgent:String((await o.send("Browser.getVersion")).userAgent||"").replace("HeadlessChrome","Chrome");if(s)await o.send("Network.setUserAgentOverride",{userAgent:s})}let f=e.quietLiveReload===!1?[]:e.quietLiveReload??["*/_stx/hmr*"];if(f.length)o.on("Fetch.requestPaused",(s)=>{o.send("Fetch.fulfillRequest",{requestId:s.requestId,responseCode:200,responseHeaders:[{name:"Content-Type",value:"text/event-stream"}],body:Buffer.from(`retry: 86400000
|
|
5
|
+
|
|
6
|
+
`).toString("base64")}).catch(()=>{})}),await o.send("Fetch.enable",{patterns:f.map((s)=>({urlPattern:s}))});if(e.acceptDialogs!==!1)o.on("Page.javascriptDialogOpening",(s)=>{o.errors.push(`Dialog (${s.type}) on ${s.url}: ${s.message||"(no message)"}`),o.send("Page.handleJavaScriptDialog",{accept:!0}).catch(()=>{})});return{cdp:o,close:l,target:g.webSocketDebuggerUrl,port:u,chromePath:t}}catch(c){throw await l(),c}}async function H(e,t={}){let r=t.quietMs??1000,n=t.timeoutMs??15000,i=Date.now(),a=0;while(Date.now()-i<n){if(e.closed)throw Error("Chrome closed the connection");if(e.inFlight.size>0||await e.evaluate("document.readyState !== 'complete'").catch(()=>!0))a=0;else if(!a)a=Date.now();else if(Date.now()-a>=r)return!0;await Bun.sleep(100)}return!1}async function U(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 H(e,r)}var j=(e)=>e.pathname.startsWith("/api/")&&!e.pathname.endsWith("/events");function S(e){let t={};for(let r of e)t[r]=(t[r]||0)+1;return Object.fromEntries(Object.entries(t).filter(([,r])=>r>1))}async function J(e,t={}){let r=Date.now(),n=new URL(e).origin,i=t.isApiRequest??j,a=[],l=[],c=null,u=t.cdp,h=[],g=u?.errors.length??0;try{if(!u)c=await _(t.launch),u=c.cdp;let o=u;if(h.push(o.on("Network.requestWillBeSent",(s)=>{let d=new URL(s.request.url);if(d.origin===n&&i(d,s.request.method))a.push(`${s.request.method} ${d.pathname}${d.search}`)})),h.push(o.on("Network.responseReceived",(s)=>{let d=new URL(s.response.url);if(d.origin===n&&s.response.status>=400)l.push(`${s.response.status} ${d.pathname}`)})),t.viewport)await o.send("Emulation.setDeviceMetricsOverride",{deviceScaleFactor:1,mobile:!1,...t.viewport});await t.beforeNavigate?.(o);let f=await U(o,e,t);return{url:e,apiRequests:a,duplicates:S(a),failed:l,errors:o.errors.slice(g),settled:f,durationMs:Date.now()-r}}catch(o){return{url:e,apiRequests:a,duplicates:S(a),failed:l,errors:[...u?.errors.slice(g)??[],`audit: ${o?.message??String(o)}`],settled:!1,durationMs:Date.now()-r}}finally{for(let o of h)o();await c?.close().catch(()=>{})}}function z(e){let t=e.url;try{let n=new URL(e.url);t=`${n.pathname}${n.search}`}catch{}let r=Object.entries(e.duplicates).map(([n,i])=>`${i}x ${n}`);return`${t}: ${e.apiRequests.length} api${r.length?` | dup ${r.join(", ")}`:""}${e.failed.length?` | failed ${e.failed.join(", ")}`:""}${e.errors.length?` | errors ${e.errors.slice(0,3).join(" ; ")}`:""}${e.settled?"":" | never settled"}`}function Y(e){return e.settled&&!Object.keys(e.duplicates).length&&!e.failed.length&&!e.errors.length}export{E as Cdp,J as auditPage,N as chromeHasUpdater,D as connect,S as duplicateRequests,I as findChrome,z as formatPageAudit,_ as launch,U as navigate,Y as pageAuditClean,F as prepareMacChrome,H as settle,x as waitFor};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** The Postgres server URL disposable databases are created on, if one is configured. */
|
|
2
|
+
export declare function disposablePostgresUrl(options?: Pick<DisposableDatabaseOptions, 'url'>): string | undefined;
|
|
3
|
+
/** Throws unless `url` is a Postgres URL for a server on this machine. */
|
|
4
|
+
export declare function assertLocalPostgres(url: string): URL;
|
|
5
|
+
/**
|
|
6
|
+
* Create a disposable database and return it with a `drop()` (also an
|
|
7
|
+
* `await using` disposer). Prefer `withDisposableDatabase`, which cannot
|
|
8
|
+
* forget to drop it.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createDisposableDatabase(dialect: DisposableDialect, options?: DisposableDatabaseOptions): Promise<CreatedDisposableDatabase>;
|
|
11
|
+
/**
|
|
12
|
+
* Run `fn` against a fresh database, then drop it, even when `fn` throws.
|
|
13
|
+
* A failure to drop is reported only when `fn` itself succeeded, so the
|
|
14
|
+
* test's own error is never hidden behind a cleanup error.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* for (const dialect of ['sqlite', 'postgres'] as const) {
|
|
18
|
+
* test.skipIf(dialect === 'postgres' && !disposablePostgresUrl())(`${dialect} checkout flow`, async () => {
|
|
19
|
+
* await withDisposableDatabase(dialect, async (database) => {
|
|
20
|
+
* const result = await runIsolated('tests/fixtures/checkout-flow.ts', { env: { APP_ENV: 'test', ...database.env }, timeoutMs: 45_000 })
|
|
21
|
+
* assertIsolatedOk(result, 'checkout flow OK')
|
|
22
|
+
* })
|
|
23
|
+
* }, 50_000)
|
|
24
|
+
* }
|
|
25
|
+
*/
|
|
26
|
+
export declare function withDisposableDatabase<T>(dialect: DisposableDialect, fn: (database: DisposableDatabase) => Promise<T> | T, options?: DisposableDatabaseOptions): Promise<T>;
|
|
27
|
+
export declare interface DisposableDatabaseOptions {
|
|
28
|
+
url?: string
|
|
29
|
+
prefix?: string
|
|
30
|
+
}
|
|
31
|
+
export declare interface DisposableDatabase {
|
|
32
|
+
dialect: DisposableDialect
|
|
33
|
+
name: string
|
|
34
|
+
directory: string
|
|
35
|
+
path?: string
|
|
36
|
+
url?: string
|
|
37
|
+
env: Record<string, string>
|
|
38
|
+
}
|
|
39
|
+
export declare interface CreatedDisposableDatabase extends DisposableDatabase, AsyncDisposable {
|
|
40
|
+
drop: () => Promise<void>
|
|
41
|
+
}
|
|
42
|
+
export type DisposableDialect = 'sqlite' | 'postgres';
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import{mkdtempSync as u,rmSync as i}from"fs";import{tmpdir as f}from"os";import{join as b}from"path";import m from"process";var{SQL:d}=globalThis.Bun;var g=new Set(["localhost","127.0.0.1","[::1]","::1"]);function S(e={}){return e.url||m.env.STACKS_TEST_POSTGRES_URL||void 0}function w(e){let s;try{s=new URL(e)}catch{throw Error("A disposable Postgres database needs a postgres:// URL")}if(!["postgres:","postgresql:"].includes(s.protocol))throw Error(`A disposable Postgres database needs a postgres:// URL, got ${s.protocol}`);if(!g.has(s.hostname))throw Error(`Disposable databases are only created on a local PostgreSQL server (localhost, 127.0.0.1, ::1), not "${s.hostname}"`);return s}function T(e){return`${(e.toLowerCase().replace(/[^a-z0-9_]/g,"_").replace(/^[^a-z_]+/,"")||"stacks_test").slice(0,38)}_${crypto.randomUUID().replaceAll("-","").slice(0,24)}`}async function h(e,s={}){if(e!=="sqlite"&&e!=="postgres")throw Error(`Disposable databases are sqlite or postgres, not "${String(e)}"`);let r=T(s.prefix??"stacks_test"),t=u(b(f(),`${r.slice(0,20)}-`)),n=!1;if(e==="sqlite"){let o=b(t,`${r}.sqlite`),a=async()=>{if(n)return;n=!0,i(t,{recursive:!0,force:!0})};return{dialect:e,name:r,directory:t,path:o,env:{DB_CONNECTION:"sqlite",DB_DATABASE_PATH:o},drop:a,[Symbol.asyncDispose]:a}}try{let o=S(s);if(!o)throw Error("A disposable Postgres database needs a server: pass options.url or set STACKS_TEST_POSTGRES_URL");let a=w(o),p=new d(a.href);try{await p.unsafe(`CREATE DATABASE "${r}"`)}finally{await p.close()}let c=new URL(a.href);c.pathname=`/${r}`;let l=async()=>{if(n)return;n=!0;let D=new d(a.href);try{await D.unsafe(`DROP DATABASE IF EXISTS "${r}" WITH (FORCE)`)}finally{await D.close(),i(t,{recursive:!0,force:!0})}};return{dialect:e,name:r,directory:t,url:c.href,env:{DB_CONNECTION:"postgres",DB_HOST:a.hostname.replace(/^\[|\]$/g,""),DB_PORT:a.port||"5432",DB_DATABASE:r,DB_USERNAME:decodeURIComponent(a.username),DB_PASSWORD:decodeURIComponent(a.password)},drop:l,[Symbol.asyncDispose]:l}}catch(o){throw i(t,{recursive:!0,force:!0}),o}}async function O(e,s,r={}){let t=await h(e,r),n;try{n=await s(t)}catch(o){throw await t.drop().catch(()=>{}),o}return await t.drop(),n}export{w as assertLocalPostgres,h as createDisposableDatabase,S as disposablePostgresUrl,O as withDisposableDatabase};
|
package/dist/index.d.ts
CHANGED
|
@@ -27,4 +27,8 @@ export * from './events';
|
|
|
27
27
|
// Mail fakes (stacksjs/stacks#2581), on top of the email package's capture driver.
|
|
28
28
|
export * from './mail';
|
|
29
29
|
export * from './time';
|
|
30
|
+
// Child-process isolation and throwaway databases. Bun and node only, no
|
|
31
|
+
// framework import, so both are safe at the root.
|
|
32
|
+
export * from './isolated';
|
|
33
|
+
export * from './disposable-database';
|
|
30
34
|
export * from 'bun:test';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import
|
|
3
|
-
`).join(""),
|
|
2
|
+
import D from"process";function pe(){D.env.NODE_ENV="test",D.env.APP_ENV="test"}var E=new Set(["headers","query","body","formData","actingAs"]);function L(e){if(!e)return"";let t=new URLSearchParams;for(let[r,o]of Object.entries(e)){if(o===void 0||o===null)continue;if(Array.isArray(o)){for(let a of o)t.append(r,String(a));continue}t.append(r,String(o))}let n=t.toString();return n?`?${n}`:""}function B(e,t,n){let r=Object.keys(e).filter((o)=>!E.has(o));if(r.length===0)return;throw TypeError(`http.${t.toLowerCase()}('${n}', \u2026) got unknown option(s) ${r.map((o)=>`'${o}'`).join(", ")}. Did you mean to send a body? Wrap it: { body: { ${r[0]}: \u2026 } }. `+`Valid options are ${[...E].join(", ")}.`)}async function j(){let e=await import("@stacksjs/router");return e.serverResponse}function U(e){let t={status:e.status,ok:e.ok,headers:e.headers,text:()=>e.text(),json:async()=>await e.clone().json(),assertStatus(n){if(this.status!==n)throw Error(`Expected status ${n}, got ${this.status}`);return t},async assertJson(n){let r=await e.clone().json();for(let[o,a]of Object.entries(n))if(JSON.stringify(r[o])!==JSON.stringify(a))throw Error(`assertJson: expected ${o}=${JSON.stringify(a)}, got ${JSON.stringify(r[o])}`);return t},assertHeader(n,r){let o=e.headers.get(n);if(o===null)throw Error(`assertHeader: missing header '${n}'`);if(r!==void 0){if(!(typeof r==="string"?o===r:r.test(o)))throw Error(`assertHeader: expected ${n}=${String(r)}, got '${o}'`)}return t}};return t}function x(e="http://localhost"){let t=null,n={},r=null;async function o(){if(!t)return null;if(r)return r;let{Auth:s}=await import("@stacksjs/auth"),u=Number(t.id);if(!Number.isFinite(u))throw TypeError(`actingAs() needs a user with a numeric id; got ${JSON.stringify(t.id)}`);let c=await s.loginUsingId(u);if(!c)throw Error(`actingAs() could not authenticate user ${u}: no such user. Create the row first (e.g. via a factory) so a token can be issued against it.`);return r=String(c.token),r}async function a(s){let u={Accept:"application/json",...n,...s.headers};if(s.body!==void 0&&s.formData===void 0)u["Content-Type"]="application/json";let c=await o();if(c&&!u.Authorization&&!u.authorization)u.Authorization=`Bearer ${c}`;return u}function l(s){if(s.formData!==void 0){if(s.body!==void 0)throw TypeError("A request carries either `body` or `formData`, not both.");let u=new FormData;for(let[c,p]of Object.entries(s.formData))u.append(c,p);return u}if(s.body===void 0)return;return typeof s.body==="string"?s.body:JSON.stringify(s.body)}async function d(s,u,c={}){let p=await j(),f=u.startsWith("http")?u:`${e}${u}`,y=L(c.query),I=y&&f.includes("?")?`${f}&${y.slice(1)}`:`${f}${y}`,_={method:s,headers:await a(c),body:l(c)},N=await p(new Request(I,_));return U(N)}let i={actingAs(s){return t=s,r=null,i},withHeaders(s){return n={...n,...s},i},request:(s,u,c)=>d(s,u,c),get:(s)=>d("GET",s),post:(s,u)=>d("POST",s,{body:u}),put:(s,u)=>d("PUT",s,{body:u}),patch:(s,u)=>d("PATCH",s,{body:u}),delete:(s,u)=>d("DELETE",s,{body:u})};return i}function fe(e,t){return x(t).actingAs(e)}var me=Object.freeze(["GET","POST","PUT","PATCH","DELETE","HEAD","OPTIONS"].reduce((e,t)=>{let n=async(r,o={})=>{B(o,t,r);let{actingAs:a,...l}=o,d=x();if(a)d.actingAs(a);return d.request(t,r,l)};return{...e,[t.toLowerCase()]:n}},{}));import v from"process";var H=60000;function M(e){let t=[],n="",r=null,o=!1,a=!1;for(let l of e){if(o){n+=l,o=!1;continue}if(l==="\\"){o=!0,a=!0;continue}if(r){if(l===r)r=null;else n+=l;continue}if(l==='"'||l==="'"){r=l,a=!0;continue}if(/\s/.test(l)){if(a)t.push(n),n="",a=!1;continue}n+=l,a=!0}if(r)throw Error(`Unterminated ${r} in command: ${e}`);if(a)t.push(n);return t}function he(e){let t=typeof e==="string"?M(e):[...e],n,r={},o=v.cwd(),a=H;async function l(){if(t.length===0)throw Error("command() needs something to run");let i=Bun.spawn(["./buddy",...t],{cwd:o,env:{...v.env,...r},stdin:n===void 0?"ignore":new TextEncoder().encode(n),stdout:"pipe",stderr:"pipe"}),s=!1,u=setTimeout(()=>{s=!0,i.kill()},a);try{let[c,p,f]=await Promise.all([new Response(i.stdout).text(),new Response(i.stderr).text(),i.exited]);return{exitCode:f,stdout:c,stderr:p,output:c+p,timedOut:s}}finally{clearTimeout(u)}}let d={withInput(i){return n=i.map((s)=>`${s}
|
|
3
|
+
`).join(""),d},withEnv(i){return r={...r,...i},d},withCwd(i){return o=i,d},withTimeout(i){if(!Number.isFinite(i)||i<=0)throw TypeError(`withTimeout expects a positive number of milliseconds, got ${i}`);return a=i,d},then(i,s){return l().then(i,s)}};return d}import{afterEach as J}from"bun:test";import{events as w}from"@stacksjs/events";var m=null,h=[],k=(e,t)=>{h.push({type:String(e),payload:t})};function Re(){if(m)return;m=[...w.all.entries()],w.all.clear(),h=[],w.on("*",k)}function De(){return m!==null}function G(e){if(!m)throw Error('getDispatchedEvents() was called without eventFake(). Nothing is recording, so an empty result would look like "the event did not fire".');return e===void 0?[...h]:h.filter((t)=>t.type===e)}function Ee(e){return G(e).length>0}function z(){if(!m)return;w.off("*",k),w.all.clear();for(let[e,t]of m)w.all.set(e,t);m=null,h=[]}J(()=>{z()});import{afterEach as Q}from"bun:test";import{CaptureEmailDriver as b,mail as O}from"@stacksjs/email";var g=null;function Oe(){if(g!==null)return;g=O.fake(),b.clear()}function Se(){return g!==null}function S(){if(g===null)throw Error('sentEmails() was called without mailFake(). Nothing is capturing, so an empty result would look like "no email was sent".');return[...b.all()]}function Ae(){return S().at(-1)}function Pe(e){let t=(n)=>{if(n===void 0||n===null)return!1;return(Array.isArray(n)?n:[n]).some((o)=>(typeof o==="string"?o:o?.address)===e)};return S().filter((n)=>t(n.to)||t(n.cc)||t(n.bcc))}function V(){if(g===null)return;O.restoreDriver(g),g=null,b.clear()}Q(()=>{V()});import{setSystemTime as A}from"bun:test";function K(e){let t=e instanceof Date?e:new Date(e);if(Number.isNaN(t.getTime()))throw TypeError(`Not a valid time: ${JSON.stringify(e)}`);return t}function W(e=new Date){let t=K(e);return A(t),t}function Fe(e){return W(e)}function qe(){A()}import{mkdtempSync as X,rmSync as Y,writeFileSync as P}from"fs";import{tmpdir as Z}from"os";import{join as T}from"path";import C from"process";var ee=2000;function te(e,t={}){let n=t.mode&&t.mode!=="auto"?t.mode:/\.(?:test|spec)\.[cm]?[jt]sx?$/.test(e)?"test":"run";return[C.execPath,...t.config?[`--config=${t.config}`]:[],...t.envFile===!1?["--no-env-file"]:[],...n==="test"?["test"]:[],e,...t.args??[]]}function ne(e){let t=e.inheritEnv===!1?{}:{...C.env};for(let[n,r]of Object.entries(e.env??{}))if(r===void 0)delete t[n];else t[n]=r;return Object.fromEntries(Object.entries(t).filter((n)=>n[1]!==void 0))}async function je(e,t={}){if(!e||typeof e!=="string")throw TypeError("runIsolated needs a file to run");let n=t.timeoutMs??60000;if(!Number.isFinite(n)||n<=0)throw RangeError("timeoutMs must be a positive number of milliseconds");let r=null,o;if(t.preload){r=X(T(Z(),"stacks-isolated-"));let i=T(r,"noop-preload.ts");P(i,`export {}
|
|
4
|
+
`);let s=JSON.stringify(t.preload.length?t.preload:[i]);o=T(r,"bunfig.toml"),P(o,`preload = ${s}
|
|
5
|
+
|
|
6
|
+
[test]
|
|
7
|
+
preload = ${s}
|
|
8
|
+
`)}let a=te(e,{...t,config:o}),l=Date.now(),d=!1;try{let i=Bun.spawn(a,{cwd:t.cwd,env:ne(t),stdin:t.stdin!=null?new Blob([t.stdin]):"ignore",stdout:"pipe",stderr:"pipe"}),s,u=setTimeout(()=>{d=!0,i.kill("SIGTERM"),s=setTimeout(()=>i.kill("SIGKILL"),ee)},n);try{let[c,p,f]=await Promise.all([i.exited,new Response(i.stdout).text(),new Response(i.stderr).text()]);return{code:i.signalCode?null:c,signal:i.signalCode==null?null:String(i.signalCode),stdout:p,stderr:f,timedOut:d,durationMs:Date.now()-l,command:a}}finally{if(clearTimeout(u),clearTimeout(s),i.exitCode===null&&!i.signalCode)i.kill("SIGKILL")}}finally{if(r)Y(r,{recursive:!0,force:!0})}}function Ue(e,t){let n=`
|
|
9
|
+
--- stdout ---
|
|
10
|
+
${e.stdout}
|
|
11
|
+
--- stderr ---
|
|
12
|
+
${e.stderr}`;if(e.timedOut)throw Error(`${e.command.slice(1).join(" ")} timed out after ${e.durationMs}ms${n}`);if(e.code!==0)throw Error(`${e.command.slice(1).join(" ")} exited ${e.code??e.signal}${n}`);if(t!=null&&!(typeof t==="string"?e.stdout.includes(t):t.test(e.stdout)))throw Error(`${e.command.slice(1).join(" ")} did not print ${String(t)}${n}`);return e}import{mkdtempSync as re,rmSync as R}from"fs";import{tmpdir as se}from"os";import{join as F}from"path";import oe from"process";var{SQL:q}=globalThis.Bun;var ie=new Set(["localhost","127.0.0.1","[::1]","::1"]);function ae(e={}){return e.url||oe.env.STACKS_TEST_POSTGRES_URL||void 0}function ue(e){let t;try{t=new URL(e)}catch{throw Error("A disposable Postgres database needs a postgres:// URL")}if(!["postgres:","postgresql:"].includes(t.protocol))throw Error(`A disposable Postgres database needs a postgres:// URL, got ${t.protocol}`);if(!ie.has(t.hostname))throw Error(`Disposable databases are only created on a local PostgreSQL server (localhost, 127.0.0.1, ::1), not "${t.hostname}"`);return t}function le(e){return`${(e.toLowerCase().replace(/[^a-z0-9_]/g,"_").replace(/^[^a-z_]+/,"")||"stacks_test").slice(0,38)}_${crypto.randomUUID().replaceAll("-","").slice(0,24)}`}async function de(e,t={}){if(e!=="sqlite"&&e!=="postgres")throw Error(`Disposable databases are sqlite or postgres, not "${String(e)}"`);let n=le(t.prefix??"stacks_test"),r=re(F(se(),`${n.slice(0,20)}-`)),o=!1;if(e==="sqlite"){let a=F(r,`${n}.sqlite`),l=async()=>{if(o)return;o=!0,R(r,{recursive:!0,force:!0})};return{dialect:e,name:n,directory:r,path:a,env:{DB_CONNECTION:"sqlite",DB_DATABASE_PATH:a},drop:l,[Symbol.asyncDispose]:l}}try{let a=ae(t);if(!a)throw Error("A disposable Postgres database needs a server: pass options.url or set STACKS_TEST_POSTGRES_URL");let l=ue(a),d=new q(l.href);try{await d.unsafe(`CREATE DATABASE "${n}"`)}finally{await d.close()}let i=new URL(l.href);i.pathname=`/${n}`;let s=async()=>{if(o)return;o=!0;let u=new q(l.href);try{await u.unsafe(`DROP DATABASE IF EXISTS "${n}" WITH (FORCE)`)}finally{await u.close(),R(r,{recursive:!0,force:!0})}};return{dialect:e,name:n,directory:r,url:i.href,env:{DB_CONNECTION:"postgres",DB_HOST:l.hostname.replace(/^\[|\]$/g,""),DB_PORT:l.port||"5432",DB_DATABASE:n,DB_USERNAME:decodeURIComponent(l.username),DB_PASSWORD:decodeURIComponent(l.password)},drop:s,[Symbol.asyncDispose]:s}}catch(a){throw R(r,{recursive:!0,force:!0}),a}}async function Ve(e,t,n={}){let r=await de(e,n),o;try{o=await t(r)}catch(a){throw await r.drop().catch(()=>{}),a}return await r.drop(),o}export*from"bun:test";export{fe as actingAs,Ue as assertIsolatedOk,ue as assertLocalPostgres,B as assertRequestOptions,he as command,de as createDisposableDatabase,ae as disposablePostgresUrl,Pe as emailsTo,Re as eventFake,De as eventsAreFaked,x as featureTest,W as freezeTime,G as getDispatchedEvents,Ee as hasDispatchedEvent,me as http,te as isolatedCommand,Ae as lastEmail,Oe as mailFake,Se as mailIsFaked,L as queryString,z as restoreEvents,V as restoreMail,je as runIsolated,S as sentEmails,pe as setupTestEnvironment,M as splitCommand,Fe as travelTo,qe as useRealTime,Ve as withDisposableDatabase};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** The full Bun command line `runIsolated` would spawn for these options. */
|
|
2
|
+
export declare function isolatedCommand(file: string, options?: Pick<RunIsolatedOptions, 'mode' | 'args' | 'envFile'> & { config?: string }): string[];
|
|
3
|
+
/**
|
|
4
|
+
* Run `file` in a fresh Bun process and collect what it printed.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const result = await runIsolated('tests/fixtures/billing-flow.ts', {
|
|
8
|
+
* env: { APP_ENV: 'test', DB_CONNECTION: 'sqlite', DB_DATABASE_PATH: path },
|
|
9
|
+
* timeoutMs: 45_000,
|
|
10
|
+
* })
|
|
11
|
+
* expect(result.code, result.stdout + result.stderr).toBe(0)
|
|
12
|
+
*/
|
|
13
|
+
export declare function runIsolated(file: string, options?: RunIsolatedOptions): Promise<IsolatedResult>;
|
|
14
|
+
/**
|
|
15
|
+
* Throw, with everything the child printed, unless it exited 0 (and printed
|
|
16
|
+
* `expect` to stdout, when given). Returns the result for further checks.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* assertIsolatedOk(await runIsolated('tests/fixtures/flow.ts'), 'flow OK')
|
|
20
|
+
*/
|
|
21
|
+
export declare function assertIsolatedOk(result: IsolatedResult, expect?: string | RegExp): IsolatedResult;
|
|
22
|
+
export declare interface RunIsolatedOptions {
|
|
23
|
+
env?: Record<string, string | undefined>
|
|
24
|
+
inheritEnv?: boolean
|
|
25
|
+
timeoutMs?: number
|
|
26
|
+
args?: string[]
|
|
27
|
+
cwd?: string
|
|
28
|
+
mode?: 'auto' | 'test' | 'run'
|
|
29
|
+
preload?: string[]
|
|
30
|
+
envFile?: boolean
|
|
31
|
+
stdin?: string
|
|
32
|
+
}
|
|
33
|
+
export declare interface IsolatedResult {
|
|
34
|
+
code: number | null
|
|
35
|
+
signal: string | null
|
|
36
|
+
stdout: string
|
|
37
|
+
stderr: string
|
|
38
|
+
timedOut: boolean
|
|
39
|
+
durationMs: number
|
|
40
|
+
command: string[]
|
|
41
|
+
}
|
package/dist/isolated.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import{mkdtempSync as R,rmSync as y,writeFileSync as u}from"fs";import{tmpdir as I}from"os";import{join as d}from"path";import c from"process";var v=2000;function b(t,e={}){let n=e.mode&&e.mode!=="auto"?e.mode:/\.(?:test|spec)\.[cm]?[jt]sx?$/.test(t)?"test":"run";return[c.execPath,...e.config?[`--config=${e.config}`]:[],...e.envFile===!1?["--no-env-file"]:[],...n==="test"?["test"]:[],t,...e.args??[]]}function h(t){let e=t.inheritEnv===!1?{}:{...c.env};for(let[n,r]of Object.entries(t.env??{}))if(r===void 0)delete e[n];else e[n]=r;return Object.fromEntries(Object.entries(e).filter((n)=>n[1]!==void 0))}async function k(t,e={}){if(!t||typeof t!=="string")throw TypeError("runIsolated needs a file to run");let n=e.timeoutMs??60000;if(!Number.isFinite(n)||n<=0)throw RangeError("timeoutMs must be a positive number of milliseconds");let r=null,s;if(e.preload){r=R(d(I(),"stacks-isolated-"));let i=d(r,"noop-preload.ts");u(i,`export {}
|
|
3
|
+
`);let o=JSON.stringify(e.preload.length?e.preload:[i]);s=d(r,"bunfig.toml"),u(s,`preload = ${o}
|
|
4
|
+
|
|
5
|
+
[test]
|
|
6
|
+
preload = ${o}
|
|
7
|
+
`)}let l=b(t,{...e,config:s}),m=Date.now(),a=!1;try{let i=Bun.spawn(l,{cwd:e.cwd,env:h(e),stdin:e.stdin!=null?new Blob([e.stdin]):"ignore",stdout:"pipe",stderr:"pipe"}),o,g=setTimeout(()=>{a=!0,i.kill("SIGTERM"),o=setTimeout(()=>i.kill("SIGKILL"),v)},n);try{let[f,p,w]=await Promise.all([i.exited,new Response(i.stdout).text(),new Response(i.stderr).text()]);return{code:i.signalCode?null:f,signal:i.signalCode==null?null:String(i.signalCode),stdout:p,stderr:w,timedOut:a,durationMs:Date.now()-m,command:l}}finally{if(clearTimeout(g),clearTimeout(o),i.exitCode===null&&!i.signalCode)i.kill("SIGKILL")}}finally{if(r)y(r,{recursive:!0,force:!0})}}function M(t,e){let n=`
|
|
8
|
+
--- stdout ---
|
|
9
|
+
${t.stdout}
|
|
10
|
+
--- stderr ---
|
|
11
|
+
${t.stderr}`;if(t.timedOut)throw Error(`${t.command.slice(1).join(" ")} timed out after ${t.durationMs}ms${n}`);if(t.code!==0)throw Error(`${t.command.slice(1).join(" ")} exited ${t.code??t.signal}${n}`);if(e!=null&&!(typeof e==="string"?t.stdout.includes(e):e.test(t.stdout)))throw Error(`${t.command.slice(1).join(" ")} did not print ${String(e)}${n}`);return t}export{M as assertIsolatedOk,b as isolatedCommand,k as runIsolated};
|
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.
|
|
5
|
+
"version": "0.75.230",
|
|
6
6
|
"description": "The Stacks way of testing.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -42,6 +42,12 @@
|
|
|
42
42
|
"import": "./dist/database.js",
|
|
43
43
|
"default": "./dist/database.js"
|
|
44
44
|
},
|
|
45
|
+
"./browser": {
|
|
46
|
+
"types": "./dist/browser.d.ts",
|
|
47
|
+
"bun": "./dist/browser.js",
|
|
48
|
+
"import": "./dist/browser.js",
|
|
49
|
+
"default": "./dist/browser.js"
|
|
50
|
+
},
|
|
45
51
|
"./dynamodb": {
|
|
46
52
|
"types": "./dist/dynamodb.d.ts",
|
|
47
53
|
"bun": "./dist/dynamodb.js",
|
|
@@ -73,16 +79,16 @@
|
|
|
73
79
|
"prepublishOnly": "bun run build"
|
|
74
80
|
},
|
|
75
81
|
"dependencies": {
|
|
76
|
-
"@stacksjs/auth": "0.75.
|
|
77
|
-
"@stacksjs/cache": "0.75.
|
|
78
|
-
"@stacksjs/config": "0.75.
|
|
79
|
-
"@stacksjs/database": "0.75.
|
|
80
|
-
"@stacksjs/email": "0.75.
|
|
81
|
-
"@stacksjs/events": "0.75.
|
|
82
|
-
"@stacksjs/path": "0.75.
|
|
83
|
-
"@stacksjs/router": "0.75.
|
|
84
|
-
"@stacksjs/storage": "0.75.
|
|
85
|
-
"@stacksjs/strings": "0.75.
|
|
82
|
+
"@stacksjs/auth": "0.75.230",
|
|
83
|
+
"@stacksjs/cache": "0.75.230",
|
|
84
|
+
"@stacksjs/config": "0.75.230",
|
|
85
|
+
"@stacksjs/database": "0.75.230",
|
|
86
|
+
"@stacksjs/email": "0.75.230",
|
|
87
|
+
"@stacksjs/events": "0.75.230",
|
|
88
|
+
"@stacksjs/path": "0.75.230",
|
|
89
|
+
"@stacksjs/router": "0.75.230",
|
|
90
|
+
"@stacksjs/storage": "0.75.230",
|
|
91
|
+
"@stacksjs/strings": "0.75.230",
|
|
86
92
|
"@stacksjs/ts-cloud": "^0.16.58",
|
|
87
93
|
"very-happy-dom": "^0.3.5"
|
|
88
94
|
},
|