@gjsify/unit 0.53.0 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/esm/gl.js ADDED
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";import{registerGlProbe as e}from"./index.js";const errorMessage=e=>e?.message??String(e),loadGlProbeGi=async()=>{let[e,t]=await Promise.all([import(`gi://Gtk?version=4.0`),import(`gi://Gdk?version=4.0`)]);if(!e.default||!t.default)throw Error(`no GTK 4 reachable from this runtime`);return{Gtk:e.default,Gdk:t.default}},realizeGlContext=async()=>{try{let{Gtk:e,Gdk:t}=await loadGlProbeGi();if(!e.init_check())throw Error(`Gtk.init_check() could not open the display`);let n=t.Display.get_default();if(!n)throw Error(`GDK has no default display`);return n.create_gl_context().realize(),t.GLContext.clear_current(),{ok:!0,failure:``}}catch(e){return{ok:!1,failure:errorMessage(e)}}};e(realizeGlContext);
package/lib/esm/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import"./_virtual/_rolldown/runtime.js";import{canRealizeGl as e,canRealizeSurface as t}from"./capabilities.js";import{createHeartbeat as n}from"./heartbeat.js";import{spy as r}from"./spy.js";import"@girs/gjs";import i from"node:assert";import{runtimeName as a,runtimeVersion as o}from"@gjsify/runtime";import{hostOs as s}from"@gjsify/utils/core";import{quitMainLoop as c}from"@gjsify/utils/main-loop";const runtimeGlobals=()=>globalThis,l=typeof runtimeGlobals().process?.versions?.gjs==`string`?runtimeGlobals().imports?.mainloop:void 0;let u=0,d=0,p=0,m=0,h=0,g=!1,_=0;const v=new Map,axisRecord=e=>{let t=v.get(e);return t||(t={matched:0,ignored:0,tests:0},v.set(e,t)),t};let y=0;const b=[],x=[],noteWarning=e=>{x.push({suite:E,message:e})},errorMessage=e=>e?.message??String(e);let S=!0;const probeCwd=()=>{let e=runtimeGlobals().process?.cwd;if(typeof e!=`function`)return!0;try{return e(),!0}catch{return!1}},noteIfCwdDestroyed=e=>{if(!S||probeCwd())return;S=!1,++m;let t="this test left the process in a deleted working directory — every later `process.cwd()`, including one inside a spawned child, now fails with ENOENT. A spec that `chdir`s into a temp dir must chdir BACK before removing it, and the whole run shares one process.";O.push({suite:E,test:e,message:t}),z(` ${P}❌ ${e} — ${t}${R}`)},noteStrayFailure=e=>{++m,b.push({suite:E,message:e}),O.push({suite:E,test:`<stray assertion outside any it()>`,message:e})},C=[],noteThrownAssertion=e=>{let t=C[C.length-1];t&&e instanceof Error&&t.add(e)},forgetThrownAssertion=e=>{if(e instanceof Error)for(let t of C)t.delete(e)};let w=``,T=0,E=``,D=``,O=[],k={testTimeout:5e3,suiteTimeout:3e4,runTimeout:12e4},A=n(void 0,0),j=new Map;var TimeoutError=class extends Error{constructor(e,t){super(`Timeout: "${e}" exceeded ${t}ms`),this.name=`TimeoutError`}};const M=[];async function withTimeout(e,t,n){if(t<=0)return e();let r,i=new Promise((e,i)=>{r=setTimeout(()=>i(new TimeoutError(n,t)),t)});i.catch(()=>{});let a,o=new Promise((e,t)=>{a=t});o.catch(()=>{}),M.push(a);try{let t=Promise.resolve(e());return t.catch(()=>{}),await Promise.race([t,i,o])}finally{clearTimeout(r);let e=M.lastIndexOf(a);e!==-1&&M.splice(e,1)}}let N=!1;const installUncaughtHooks=()=>{if(N)return;let e=runtimeGlobals().process;if(typeof e?.on!=`function`||typeof e.versions?.gjs==`string`)return;N=!0;let handle=t=>n=>{let r=n?.__testFailureCounted===!0,i=(e.listenerCount?.(t)??1)-1;if(!r&&i>0){let e=n?.message??String(n),r=y>0?` during "${D}"`:``;noteWarning(`${t}: ${e}${r} — absorbed by a listener the spec installed itself`);return}let a=M[M.length-1];a?a(n):noteStrayFailure(n?.message??String(n))};e.on(`uncaughtException`,handle(`uncaughtException`)),e.on(`unhandledRejection`,handle(`unhandledRejection`))},configure=e=>{k={...k,...e}};function applyEnvOverrides(){try{let e=runtimeGlobals().process?.env;if(!e)return;let t=parseInt(e.GJSIFY_TEST_TIMEOUT,10);!isNaN(t)&&t>=0&&(k.testTimeout=t);let n=parseInt(e.GJSIFY_SUITE_TIMEOUT,10);!isNaN(n)&&n>=0&&(k.suiteTimeout=n);let r=parseInt(e.GJSIFY_RUN_TIMEOUT,10);!isNaN(r)&&r>=0&&(k.runTimeout=r)}catch{}}const P=`\x1B[31m`,F=`\x1B[32m`,I=`\x1B[34m`,L=`\x1B[90m`,R=`\x1B[39m`,now=()=>runtimeGlobals().performance?.now?.()??Date.now(),formatDuration=e=>e>=1e3?`${(e/1e3).toFixed(2)}s`:e>=100?`${Math.round(e)}ms`:`${e.toFixed(1)}ms`,z=typeof runtimeGlobals().process?.versions?.gjs!=`string`&&runtimeGlobals().document!==void 0?console.log:globalThis.print||console.log;function isStructurallyEqual(e,t){try{return i.deepStrictEqual(e,t),!0}catch{return!1}}function formatValue(e){switch(typeof e){case`symbol`:return e.toString();case`bigint`:return`${e}n`;case`string`:return e;case`function`:return e.name?`[Function ${e.name}]`:`[Function (anonymous)]`;case`object`:if(e===null)return`null`;try{return JSON.stringify(e)??String(e)}catch{return Object.prototype.toString.call(e)}default:return String(e)}}function matchesObject(e,t){return Object.is(e,t)?!0:typeof t!=`object`||!t?e===t:typeof e!=`object`||!e?!1:Array.isArray(t)?!Array.isArray(e)||e.length!==t.length?!1:t.every((t,n)=>matchesObject(e[n],t)):Array.isArray(e)?!1:Object.keys(t).every(n=>matchesObject(e[n],t[n]))}const B=[],V=[];function envBag(){return globalThis.process?.env}const H={fn(e){let t=[],f=function(...n){return t.push(n),e?e.apply(this,n):void 0};return f.mock={calls:t},f},stubGlobal(e,t){let n=globalThis;B.push({key:e,had:e in n,prev:n[e]}),n[e]=t},unstubAllGlobals(){let e=globalThis;for(let t=B.length-1;t>=0;t--){let n=B[t];n.had?e[n.key]=n.prev:delete e[n.key]}B.length=0},stubEnv(e,t){let n=envBag();n&&(V.push({key:e,had:e in n,prev:n[e]}),t===void 0?delete n[e]:n[e]=t)},unstubAllEnvs(){let e=envBag();if(e){for(let t=V.length-1;t>=0;t--){let n=V[t];n.had?e[n.key]=n.prev:delete e[n.key]}V.length=0}}};var U=class MatcherFactory{actualValue;positive;not;constructor(e,t,n){this.actualValue=e,this.positive=t,n?this.not=n:this.not=new MatcherFactory(e,!t,this)}triggerResult(e,t){if(e&&!this.positive||!e&&this.positive){let e=Error(t);throw e.__testFailureCounted=!0,y===0?noteStrayFailure(t):noteThrownAssertion(e),e}}to(e){this.triggerResult(e(this.actualValue),` Expected callback to validate`)}toBe(e){this.triggerResult(this.actualValue===e,` Expected values to match using ===
1
+ import"./_virtual/_rolldown/runtime.js";import{canRealizeGl as e,canRealizeSurface as t}from"./capabilities.js";import{spy as n}from"./spy.js";import{createHeartbeat as r}from"./heartbeat.js";import"@girs/gjs";import i from"node:assert";import{runtimeName as a,runtimeVersion as o}from"@gjsify/runtime";import{hostOs as s}from"@gjsify/utils/core";import{quitMainLoop as c}from"@gjsify/utils/main-loop";const runtimeGlobals=()=>globalThis,l=typeof runtimeGlobals().process?.versions?.gjs==`string`?runtimeGlobals().imports?.mainloop:void 0;let u=0,d=0,p=0,m=0,h=0,g=!1,_=0;const v=new Map,axisRecord=e=>{let t=v.get(e);return t||(t={matched:0,ignored:0,tests:0},v.set(e,t)),t};let y=0;const b=[],x=[],noteWarning=e=>{x.push({suite:E,message:e})},errorMessage=e=>e?.message??String(e);let S=!0;const probeCwd=()=>{let e=runtimeGlobals().process?.cwd;if(typeof e!=`function`)return!0;try{return e(),!0}catch{return!1}},noteIfCwdDestroyed=e=>{if(!S||probeCwd())return;S=!1,++m;let t="this test left the process in a deleted working directory — every later `process.cwd()`, including one inside a spawned child, now fails with ENOENT. A spec that `chdir`s into a temp dir must chdir BACK before removing it, and the whole run shares one process.";O.push({suite:E,test:e,message:t}),z(` ${P}❌ ${e} — ${t}${R}`)},noteStrayFailure=e=>{++m,b.push({suite:E,message:e}),O.push({suite:E,test:`<stray assertion outside any it()>`,message:e})},C=[],noteThrownAssertion=e=>{let t=C[C.length-1];t&&e instanceof Error&&t.add(e)},forgetThrownAssertion=e=>{if(e instanceof Error)for(let t of C)t.delete(e)};let w=``,T=0,E=``,D=``,O=[],k={testTimeout:5e3,suiteTimeout:3e4,runTimeout:12e4},A=r(void 0,0),j=new Map;var TimeoutError=class extends Error{constructor(e,t){super(`Timeout: "${e}" exceeded ${t}ms`),this.name=`TimeoutError`}};const M=[];async function withTimeout(e,t,n){if(t<=0)return e();let r,i=new Promise((e,i)=>{r=setTimeout(()=>i(new TimeoutError(n,t)),t)});i.catch(()=>{});let a,o=new Promise((e,t)=>{a=t});o.catch(()=>{}),M.push(a);try{let t=Promise.resolve(e());return t.catch(()=>{}),await Promise.race([t,i,o])}finally{clearTimeout(r);let e=M.lastIndexOf(a);e!==-1&&M.splice(e,1)}}let N=!1;const installUncaughtHooks=()=>{if(N)return;let e=runtimeGlobals().process;if(typeof e?.on!=`function`||typeof e.versions?.gjs==`string`)return;N=!0;let handle=t=>n=>{let r=n?.__testFailureCounted===!0,i=(e.listenerCount?.(t)??1)-1;if(!r&&i>0){let e=n?.message??String(n),r=y>0?` during "${D}"`:``;noteWarning(`${t}: ${e}${r} — absorbed by a listener the spec installed itself`);return}let a=M[M.length-1];a?a(n):noteStrayFailure(n?.message??String(n))};e.on(`uncaughtException`,handle(`uncaughtException`)),e.on(`unhandledRejection`,handle(`unhandledRejection`))},configure=e=>{k={...k,...e}};function applyEnvOverrides(){try{let e=runtimeGlobals().process?.env;if(!e)return;let t=parseInt(e.GJSIFY_TEST_TIMEOUT,10);!isNaN(t)&&t>=0&&(k.testTimeout=t);let n=parseInt(e.GJSIFY_SUITE_TIMEOUT,10);!isNaN(n)&&n>=0&&(k.suiteTimeout=n);let r=parseInt(e.GJSIFY_RUN_TIMEOUT,10);!isNaN(r)&&r>=0&&(k.runTimeout=r)}catch{}}const P=`\x1B[31m`,F=`\x1B[32m`,I=`\x1B[34m`,L=`\x1B[90m`,R=`\x1B[39m`,now=()=>runtimeGlobals().performance?.now?.()??Date.now(),formatDuration=e=>e>=1e3?`${(e/1e3).toFixed(2)}s`:e>=100?`${Math.round(e)}ms`:`${e.toFixed(1)}ms`,z=typeof runtimeGlobals().process?.versions?.gjs!=`string`&&runtimeGlobals().document!==void 0?console.log:globalThis.print||console.log;function isStructurallyEqual(e,t){try{return i.deepStrictEqual(e,t),!0}catch{return!1}}function formatValue(e){switch(typeof e){case`symbol`:return e.toString();case`bigint`:return`${e}n`;case`string`:return e;case`function`:return e.name?`[Function ${e.name}]`:`[Function (anonymous)]`;case`object`:if(e===null)return`null`;try{return JSON.stringify(e)??String(e)}catch{return Object.prototype.toString.call(e)}default:return String(e)}}function matchesObject(e,t){return Object.is(e,t)?!0:typeof t!=`object`||!t?e===t:typeof e!=`object`||!e?!1:Array.isArray(t)?!Array.isArray(e)||e.length!==t.length?!1:t.every((t,n)=>matchesObject(e[n],t)):Array.isArray(e)?!1:Object.keys(t).every(n=>matchesObject(e[n],t[n]))}const B=[],V=[];function envBag(){return globalThis.process?.env}const H={fn(e){let t=[],f=function(...n){return t.push(n),e?e.apply(this,n):void 0};return f.mock={calls:t},f},stubGlobal(e,t){let n=globalThis;B.push({key:e,had:e in n,prev:n[e]}),n[e]=t},unstubAllGlobals(){let e=globalThis;for(let t=B.length-1;t>=0;t--){let n=B[t];n.had?e[n.key]=n.prev:delete e[n.key]}B.length=0},stubEnv(e,t){let n=envBag();n&&(V.push({key:e,had:e in n,prev:n[e]}),t===void 0?delete n[e]:n[e]=t)},unstubAllEnvs(){let e=envBag();if(e){for(let t=V.length-1;t>=0;t--){let n=V[t];n.had?e[n.key]=n.prev:delete e[n.key]}V.length=0}}};var U=class MatcherFactory{actualValue;positive;not;constructor(e,t,n){this.actualValue=e,this.positive=t,n?this.not=n:this.not=new MatcherFactory(e,!t,this)}triggerResult(e,t){if(e&&!this.positive||!e&&this.positive){let e=Error(t);throw e.__testFailureCounted=!0,y===0?noteStrayFailure(t):noteThrownAssertion(e),e}}to(e){this.triggerResult(e(this.actualValue),` Expected callback to validate`)}toBe(e){this.triggerResult(this.actualValue===e,` Expected values to match using ===
2
2
  Expected: ${formatValue(e)} (${typeof e})\n Actual: ${formatValue(this.actualValue)} (${typeof this.actualValue})`)}toEqual(e){let t=this.actualValue==e,n=``;!t&&isStructurallyEqual(this.actualValue,e)&&(n="\n The two values are structurally equal but are not the same reference.\n `toEqual` compares with `==` — use `toStrictEqual` for deep equality."),this.triggerResult(t,` Expected values to match using ==
3
3
  Expected: ${formatValue(e)} (${typeof e})\n Actual: ${formatValue(this.actualValue)} (${typeof this.actualValue})${n}`)}toStrictEqual(e){let t=!0,n=``;try{i.deepStrictEqual(this.actualValue,e)}catch(e){t=!1,n=e.message||``}this.triggerResult(t,` Expected values to be deeply strictly equal
4
4
  Expected: ${JSON.stringify(e)}\n Actual: ${JSON.stringify(this.actualValue)}`+(n?`\n ${n}`:``))}toMatchObject(e){this.triggerResult(matchesObject(this.actualValue,e),` Expected value to match object (partial deep match)
@@ -6,9 +6,9 @@ import"./_virtual/_rolldown/runtime.js";import{canRealizeGl as e,canRealizeSurfa
6
6
  Expected: ${formatValue(e)} (${typeof e})\n Actual: ${formatValue(this.actualValue)} (${typeof this.actualValue})`)}toBeInstanceOf(e){this.triggerResult(this.actualValue instanceof e,` Expected value to be instance of ${e.name||e}\n Actual: ${this.actualValue?.constructor?.name||typeof this.actualValue}`)}toHaveLength(e){let t=this.actualValue?.length;this.triggerResult(t===e,` Expected length: ${e}\n Actual length: ${t}`)}toMatch(e){let t=this.actualValue;if(typeof t.match!=`function`)throw Error(`You can not use toMatch on type ${typeof this.actualValue}`);this.triggerResult(!!t.match(e),` Expected values to match using regular expression
7
7
  Expression: `+formatValue(e)+`
8
8
  Actual: `+formatValue(this.actualValue))}toBeDefined(){this.triggerResult(this.actualValue!==void 0,` Expected value to be defined`)}toBeUndefined(){this.triggerResult(this.actualValue===void 0,` Expected value to be undefined`)}toBeNull(){this.triggerResult(this.actualValue===null,` Expected value to be null`)}toBeTruthy(){this.triggerResult(this.actualValue,` Expected value to be truthy`)}toBeFalsy(){this.triggerResult(!this.actualValue,` Expected value to be falsy`)}toBeNaN(){this.triggerResult(Number.isNaN(this.actualValue),` Expected value to be NaN\n Actual: ${formatValue(this.actualValue)}`)}toContain(e){let t=this.actualValue,n;n=typeof t==`string`?t.includes(String(e)):t instanceof Array?t.indexOf(e)!==-1:!1,this.triggerResult(n,` Expected `+formatValue(t)+` to contain `+formatValue(e))}toBeLessThan(e){this.triggerResult(this.actualValue<e,` Expected `+formatValue(this.actualValue)+` to be less than `+e)}toBeGreaterThan(e){this.triggerResult(this.actualValue>e,` Expected `+formatValue(this.actualValue)+` to be greater than `+e)}toBeGreaterThanOrEqual(e){this.triggerResult(this.actualValue>=e,` Expected ${formatValue(this.actualValue)} to be greater than or equal to ${e}`)}toBeLessThanOrEqual(e){this.triggerResult(this.actualValue<=e,` Expected ${formatValue(this.actualValue)} to be less than or equal to ${e}`)}toBeCloseTo(e,t){let n=10**t;this.triggerResult(Math.round(this.actualValue*n)/n===Math.round(e*n)/n,` Expected `+formatValue(this.actualValue)+` with precision `+t+` to be close to `+e)}toThrow(e){let t=``,n=!1,r=!0,i=!0,a=this.actualValue;try{a(),n=!1}catch(a){t=a?.message||``,n=!0,forgetThrownAssertion(a),typeof e==`function`?r=a instanceof e:typeof e==`string`?i=t.includes(e):e instanceof RegExp&&(i=e.test(t))}let o=this.actualValue?.name||typeof this.actualValue==`function`?`[anonymous function]`:String(this.actualValue);this.triggerResult(n,` Expected ${o} to ${this.positive?`throw`:`not throw`} an exception ${!this.positive&&t?`, but an error with the message "${t}" was thrown`:``}`),typeof e==`function`?this.triggerResult(r,` Expected Error type '${e.name}', but the error is not an instance of it`):e!==void 0&&this.triggerResult(i,` Expected error message to match ${e}\n Actual message: "${t}"`)}async toReject(e){let t=!1,n=``,r=!0,i=!0;try{await this.actualValue,t=!1}catch(a){t=!0,forgetThrownAssertion(a),n=a?.message||String(a),typeof e==`function`?r=a instanceof e:typeof e==`string`?i=n.includes(e):e instanceof RegExp&&(i=e.test(n))}this.triggerResult(t,` Expected promise to ${this.positive?`reject`:`resolve`}${!this.positive&&n?`, but it rejected with "${n}"`:``}`),t&&typeof e==`function`?this.triggerResult(r,` Expected rejection type '${e.name}', but the error is not an instance of it`):t&&e!==void 0&&this.triggerResult(i,` Expected rejection message to match ${e}\n Actual message: "${n}"`)}async toResolve(){let e=!1,t=``;try{await this.actualValue,e=!0}catch(n){e=!1,forgetThrownAssertion(n),t=n?.message||String(n)}this.triggerResult(e,` Expected promise to ${this.positive?`resolve`:`reject`}${e?``:`, but it rejected with "${t}"`}`)}get rejects(){return{toThrow:e=>this.toReject(e),toReject:e=>this.toReject(e)}}get resolves(){return{toResolve:()=>this.toResolve()}}};const describe=async function(e,t,n){let r=typeof n==`number`?n:n?.timeout??k.suiteTimeout;z(`
9
- `+e);let i=E;E=e;let a=now();K.push({before:[],after:[]});try{await withTimeout(t,r,`describe: ${e}`)}catch(t){if(t instanceof TimeoutError)++m,O.push({suite:e,test:`<suite timed out>`,message:t.message}),z(` ${P}⏱ Suite timed out: ${t.message}${R}`);else throw t}finally{K.pop()}E=i;let o=now()-a;z(` ${L}↳ ${formatDuration(o)}${R}`)};describe.skip=async function(e,t){++h,z(`\n${I}- ${e} (skipped)${R}`)};const envVar=e=>{let t=runtimeGlobals().process?.env;if(t)return t[e];let n=runtimeGlobals().imports?.gi?.GLib;return n?n.getenv(e)??void 0:void 0},displayEnv=()=>({DISPLAY:envVar(`DISPLAY`),WAYLAND_DISPLAY:envVar(`WAYLAND_DISPLAY`)}),hasDisplay=()=>t(s(),displayEnv());let W,G=``;const loadGlProbeGi=async()=>{let[e,t]=await Promise.all([import(`gi://Gtk?version=4.0`),import(`gi://Gdk?version=4.0`)]);if(!e.default||!t.default)throw Error(`no GTK 4 reachable from this runtime`);return{Gtk:e.default,Gdk:t.default}},realizeGlContext=async()=>{if(W!==void 0)return W;try{let{Gtk:e,Gdk:t}=await loadGlProbeGi();if(!e.init_check())throw Error(`Gtk.init_check() could not open the display`);let n=t.Display.get_default();if(!n)throw Error(`GDK has no default display`);n.create_gl_context().realize(),t.GLContext.clear_current(),W=!0}catch(e){W=!1,G=errorMessage(e),noteWarning(`on('Gl') skipped: a display exists but no GL context realizes — ${G}`)}return W},hasGl=()=>e(s(),displayEnv(),realizeGlContext),runtimeMatch=async function(e,t){if(e.includes(`Display`))return{matched:hasDisplay(),runtime:`Display`};if(e.includes(`Gl`))return{matched:await hasGl(),runtime:`Gl`};let n=await getRuntime(),r=e.find(e=>n.includes(e));return!r||typeof t==`string`&&!n.includes(t)?{matched:!1}:{matched:!0,runtime:r,version:t}},on=async function(e,t,n){typeof e==`string`&&(e=[e]),typeof t==`function`&&(n=t,t=void 0);let{matched:r,runtime:i}=await runtimeMatch(e,t);if(!r){++h;for(let t of e)++axisRecord(t).ignored;return}z(`\nOn ${e.join(`, `)}${t?` `+t:``}`);let a=d;await n();let o=axisRecord(i??e[0]);++o.matched,o.tests+=d-a},K=[{before:[],after:[]}],currentHookFrame=()=>K[K.length-1],beforeEach=function(e){typeof e==`function`&&currentHookFrame().before.push(e)},afterEach=function(e){typeof e==`function`&&currentHookFrame().after.push(e)},runBeforeEachHooks=async()=>{for(let e of K)for(let t of e.before)await t()},runAfterEachHooks=async()=>{for(let e=K.length-1;e>=0;--e){let t=K[e];for(let e=t.after.length-1;e>=0;--e)await t.after[e]()}},it=async function(e,t,n){let r=j.get(e);if(r!==void 0){++h,z(` ${I}-${R} ${L}${e} (skipped: ${r})${R}`);return}let i=typeof n==`number`?n:n?.timeout??k.testTimeout,a=now();++d,++y,D=e;let o=new Set;C.push(o);let s,c=!1;try{await runBeforeEachHooks(),A.noteInFlight(E?`${E} › ${e}`:e,i),await withTimeout(t,i,e)}catch(e){c=!0,s=e,e instanceof Error&&o.delete(e)}finally{try{await runAfterEachHooks()}catch(t){c?noteWarning(`afterEach also threw while "${e}" was already failing: ${errorMessage(t)}`):(c=!0,s=t,t instanceof Error&&o.delete(t))}--y,C.pop(),noteIfCwdDestroyed(e),A.noteSettled()}let l=now()-a,u=s instanceof TimeoutError?[...o]:[];if(!c){z(` ${F}✔${R} ${L}${e} (${formatDuration(l)})${R}`);return}++p;let m=[];c&&m.push(s?.message??String(s));for(let e of u)m.push(`${e.message}\n ${L}↳ thrown from a callback OUTSIDE this test's awaited chain, so it could not reach the test boundary. Reject the promise from that callback (or await a helper that does) to surface it directly.${R}`);let g=m.join(`
10
- `),_=u[0]??s,v=c&&s instanceof TimeoutError&&u.length===0?`⏱`:`❌`;O.push({suite:E,test:e,message:g}),z(` ${P}${v}${R} ${L}${e} (${formatDuration(l)})${R}`),z(`${P}${g}${R}`),_?.stack&&z(_.stack)};it.skip=async function(e,t){++h,z(` ${I}-${R} ${L}${e} (skipped)${R}`)};const getTestCounters=()=>({assertions:u,tests:d,failed:p,failedOutsideTests:m,ignored:h,xfail:_,warnings:x.length}),getAxisLedger=()=>{let e={};for(let[t,n]of v)e[t]={...n};return e};it.failing=async function(e,t,n,r){let i=typeof r==`number`?r:r?.timeout??k.testTimeout;if(typeof r==`object`&&r?.when===!1)return it(e,t,{timeout:i});let a=now();++d,++y,C.push(new Set);let o=!1;try{await runBeforeEachHooks(),await withTimeout(t,i,e)}catch{o=!0}finally{try{await runAfterEachHooks()}catch(t){noteWarning(`afterEach threw after the expected failure "${e}": ${errorMessage(t)}`)}--y,C.pop()}let s=now()-a;if(o){++_,z(` ${I}✗${R} ${L}${e} (expected failure — ${n})${R}`);return}++p,O.push({suite:E,test:e,message:`it.failing("${e}") PASSED — the expected failure no longer happens, so this marker is stale and must be removed (turn it back into a plain it()). It was marked expected-failing because: ${n}`}),z(` ${P}❌${R} ${L}${e} (${formatDuration(s)})${R}`),z(`${P}it.failing passed unexpectedly — remove the marker. Reason it carried: ${n}${R}`)};const expect=function(e,t){return++u,new U(e,!0)},failAssertion=e=>{throw e.__testFailureCounted=!0,y===0?noteStrayFailure(e?.message??String(e)):noteThrownAssertion(e),e},assert=function(e,t){++u;try{i(e,t)}catch(e){failAssertion(e)}};assert.strictEqual=function(e,t,n){++u;try{i.strictEqual(e,t,n)}catch(e){failAssertion(e)}},assert.throws=function(e,...t){++u;let n;try{e()}catch(e){n=e}try{i.throws(()=>{if(n)throw n},t[0],t[1])}catch(e){failAssertion(e)}},assert.deepStrictEqual=function(e,t,n){++u;try{i.deepStrictEqual(e,t,n)}catch(e){failAssertion(e)}};const runTests=async function(e){for(let t in e){let n=e[t];typeof n==`function`?await n():typeof n==`object`&&await runTests(n)}},browserSignalDone=()=>{let e=runtimeGlobals(),t=e.document;t&&(e.__gjsify_test_results={passed:d-p,failed:p+m,total:d,assertions:u,errors:O},t.documentElement.dataset.testsDone=`true`)},failUnexercisedAxes=async e=>{for(let t of e){let{matched:e}=await runtimeMatch([t]);if(!e)continue;let n=v.get(t);if(n&&n.tests>0)continue;++m;let r=`declared axis '${t}' ran on this host but exercised nothing — ${n?`${n.matched} gate(s) matched but executed no test, ${n.ignored} stood down`:`no on() gate named it`}`;O.push({suite:`<axis declaration>`,test:t,message:r}),z(`\n${P}❌ ${r}${R}`)}},failUnmetExpectedAxes=async()=>{let e=(envVar(`GJSIFY_TEST_EXPECT_AXES`)??``).split(`,`).map(e=>e.trim()).filter(Boolean);for(let t of e){if(!v.has(t))continue;let{matched:e}=await runtimeMatch([t]);if(e)continue;++m;let n=`axis '${t}' is expected on this host (GJSIFY_TEST_EXPECT_AXES) but stood down${t===`Gl`&&G?` — ${G}`:``}`;O.push({suite:`<axis expectation>`,test:t,message:n}),z(`\n${P}❌ ${n}${R}`)}},formatFailureVerdict=e=>{let t=[];if(e.failed&&t.push(`${e.failed} of ${e.tests} tests failed`),e.outside){let n=e.outside===1?``:`s`;t.push(e.failed?`${e.outside} failure${n} outside any test`:`${e.outside} failure${n} outside any test, ${e.tests} tests passed`)}return t.join(`, and `)},exitCodeFor=(e,t)=>e>0||t?1:0,printResult=()=>{let e=T>0?now()-T:0,t=e>0?` ${L}(${formatDuration(e)})`:``,n=w?`[${w}] `:``;if(h&&z(`\n${I}✔ ${h} ignored test${h>1?`s`:``}${R}`),v.size){z(`\n${I}⊞ axes exercised${R}`);for(let[e,t]of v)z(` ${I}↳ ${e}: ${t.tests} test${t.tests===1?``:`s`} from ${t.matched} gate${t.matched===1?``:`s`}${t.ignored?`, ${t.ignored} stood down`:``}${R}`)}if(_&&z(`\n${I}✗ ${_} expected failure${_>1?`s`:``} (it.failing — upstream defects)${R}`),x.length){z(`\n${I}⚠ ${x.length} warning${x.length>1?`s`:``} (not counted — nothing is claimed about these)${R}`);for(let e of x)z(` ${I}↳ ${e.message.trim().split(`
9
+ `+e);let i=E;E=e;let a=now();q.push({before:[],after:[]});try{await withTimeout(t,r,`describe: ${e}`)}catch(t){if(t instanceof TimeoutError)++m,O.push({suite:e,test:`<suite timed out>`,message:t.message}),z(` ${P}⏱ Suite timed out: ${t.message}${R}`);else throw t}finally{q.pop()}E=i;let o=now()-a;z(` ${L}↳ ${formatDuration(o)}${R}`)};describe.skip=async function(e,t){++h,z(`\n${I}- ${e} (skipped)${R}`)};const envVar=e=>{let t=runtimeGlobals().process?.env;if(t)return t[e];let n=runtimeGlobals().imports?.gi?.GLib;return n?n.getenv(e)??void 0:void 0},displayEnv=()=>({DISPLAY:envVar(`DISPLAY`),WAYLAND_DISPLAY:envVar(`WAYLAND_DISPLAY`)}),hasDisplay=()=>t(s(),displayEnv());let W;const registerGlProbe=e=>{W=e};let G,K=``;const realizeGlContext=async()=>{if(G!==void 0)return G;if(!W)return G=!1,K=`no on('Gl') prober registered — import '@gjsify/unit/gl' to use this axis`,G;let{ok:e,failure:t}=await W();return G=e,K=t,e||noteWarning(`on('Gl') skipped: a display exists but no GL context realizes — ${t}`),G},hasGl=()=>e(s(),displayEnv(),realizeGlContext),runtimeMatch=async function(e,t){if(e.includes(`Display`))return{matched:hasDisplay(),runtime:`Display`};if(e.includes(`Gl`))return{matched:await hasGl(),runtime:`Gl`};let n=await getRuntime(),r=e.find(e=>n.includes(e));return!r||typeof t==`string`&&!n.includes(t)?{matched:!1}:{matched:!0,runtime:r,version:t}},on=async function(e,t,n){typeof e==`string`&&(e=[e]),typeof t==`function`&&(n=t,t=void 0);let{matched:r,runtime:i}=await runtimeMatch(e,t);if(!r){++h;for(let t of e)++axisRecord(t).ignored;return}z(`\nOn ${e.join(`, `)}${t?` `+t:``}`);let a=d;await n();let o=axisRecord(i??e[0]);++o.matched,o.tests+=d-a},q=[{before:[],after:[]}],currentHookFrame=()=>q[q.length-1],beforeEach=function(e){typeof e==`function`&&currentHookFrame().before.push(e)},afterEach=function(e){typeof e==`function`&&currentHookFrame().after.push(e)},runBeforeEachHooks=async()=>{for(let e of q)for(let t of e.before)await t()},runAfterEachHooks=async()=>{for(let e=q.length-1;e>=0;--e){let t=q[e];for(let e=t.after.length-1;e>=0;--e)await t.after[e]()}},it=async function(e,t,n){let r=j.get(e);if(r!==void 0){++h,z(` ${I}-${R} ${L}${e} (skipped: ${r})${R}`);return}let i=typeof n==`number`?n:n?.timeout??k.testTimeout,a=now();++d,++y,D=e;let o=new Set;C.push(o);let s,c=!1;try{await runBeforeEachHooks(),A.noteInFlight(E?`${E} › ${e}`:e,i),await withTimeout(t,i,e)}catch(e){c=!0,s=e,e instanceof Error&&o.delete(e)}finally{try{await runAfterEachHooks()}catch(t){c?noteWarning(`afterEach also threw while "${e}" was already failing: ${errorMessage(t)}`):(c=!0,s=t,t instanceof Error&&o.delete(t))}--y,C.pop(),noteIfCwdDestroyed(e),A.noteSettled()}let l=now()-a,u=s instanceof TimeoutError?[...o]:[];if(!c){z(` ${F}✔${R} ${L}${e} (${formatDuration(l)})${R}`);return}++p;let m=[];c&&m.push(s?.message??String(s));for(let e of u)m.push(`${e.message}\n ${L}↳ thrown from a callback OUTSIDE this test's awaited chain, so it could not reach the test boundary. Reject the promise from that callback (or await a helper that does) to surface it directly.${R}`);let g=m.join(`
10
+ `),_=u[0]??s,v=c&&s instanceof TimeoutError&&u.length===0?`⏱`:`❌`;O.push({suite:E,test:e,message:g}),z(` ${P}${v}${R} ${L}${e} (${formatDuration(l)})${R}`),z(`${P}${g}${R}`),_?.stack&&z(_.stack)};it.skip=async function(e,t){++h,z(` ${I}-${R} ${L}${e} (skipped)${R}`)};const getTestCounters=()=>({assertions:u,tests:d,failed:p,failedOutsideTests:m,ignored:h,xfail:_,warnings:x.length}),getAxisLedger=()=>{let e={};for(let[t,n]of v)e[t]={...n};return e};it.failing=async function(e,t,n,r){let i=typeof r==`number`?r:r?.timeout??k.testTimeout;if(typeof r==`object`&&r?.when===!1)return it(e,t,{timeout:i});let a=now();++d,++y,C.push(new Set);let o=!1;try{await runBeforeEachHooks(),await withTimeout(t,i,e)}catch{o=!0}finally{try{await runAfterEachHooks()}catch(t){noteWarning(`afterEach threw after the expected failure "${e}": ${errorMessage(t)}`)}--y,C.pop()}let s=now()-a;if(o){++_,z(` ${I}✗${R} ${L}${e} (expected failure — ${n})${R}`);return}++p,O.push({suite:E,test:e,message:`it.failing("${e}") PASSED — the expected failure no longer happens, so this marker is stale and must be removed (turn it back into a plain it()). It was marked expected-failing because: ${n}`}),z(` ${P}❌${R} ${L}${e} (${formatDuration(s)})${R}`),z(`${P}it.failing passed unexpectedly — remove the marker. Reason it carried: ${n}${R}`)};const expect=function(e,t){return++u,new U(e,!0)},failAssertion=e=>{throw e.__testFailureCounted=!0,y===0?noteStrayFailure(e?.message??String(e)):noteThrownAssertion(e),e},assert=function(e,t){++u;try{i(e,t)}catch(e){failAssertion(e)}};assert.strictEqual=function(e,t,n){++u;try{i.strictEqual(e,t,n)}catch(e){failAssertion(e)}},assert.throws=function(e,...t){++u;let n;try{e()}catch(e){n=e}try{i.throws(()=>{if(n)throw n},t[0],t[1])}catch(e){failAssertion(e)}},assert.deepStrictEqual=function(e,t,n){++u;try{i.deepStrictEqual(e,t,n)}catch(e){failAssertion(e)}};const runTests=async function(e){for(let t in e){let n=e[t];typeof n==`function`?await n():typeof n==`object`&&await runTests(n)}},browserSignalDone=()=>{let e=runtimeGlobals(),t=e.document;t&&(e.__gjsify_test_results={passed:d-p,failed:p+m,total:d,assertions:u,errors:O},t.documentElement.dataset.testsDone=`true`)},failUnexercisedAxes=async e=>{for(let t of e){let{matched:e}=await runtimeMatch([t]);if(!e)continue;let n=v.get(t);if(n&&n.tests>0)continue;++m;let r=`declared axis '${t}' ran on this host but exercised nothing — ${n?`${n.matched} gate(s) matched but executed no test, ${n.ignored} stood down`:`no on() gate named it`}`;O.push({suite:`<axis declaration>`,test:t,message:r}),z(`\n${P}❌ ${r}${R}`)}},failUnmetExpectedAxes=async()=>{let e=(envVar(`GJSIFY_TEST_EXPECT_AXES`)??``).split(`,`).map(e=>e.trim()).filter(Boolean);for(let t of e){if(!v.has(t))continue;let{matched:e}=await runtimeMatch([t]);if(e)continue;++m;let n=`axis '${t}' is expected on this host (GJSIFY_TEST_EXPECT_AXES) but stood down${t===`Gl`&&K?` — ${K}`:``}`;O.push({suite:`<axis expectation>`,test:t,message:n}),z(`\n${P}❌ ${n}${R}`)}},formatFailureVerdict=e=>{let t=[];if(e.failed&&t.push(`${e.failed} of ${e.tests} tests failed`),e.outside){let n=e.outside===1?``:`s`;t.push(e.failed?`${e.outside} failure${n} outside any test`:`${e.outside} failure${n} outside any test, ${e.tests} tests passed`)}return t.join(`, and `)},exitCodeFor=(e,t)=>e>0||t?1:0,flushed=e=>Promise.all(e.map(e=>new Promise(t=>e.write(``,()=>t())))).then(()=>void 0),printResult=()=>{let e=T>0?now()-T:0,t=e>0?` ${L}(${formatDuration(e)})`:``,n=w?`[${w}] `:``;if(h&&z(`\n${I}✔ ${h} ignored test${h>1?`s`:``}${R}`),v.size){z(`\n${I}⊞ axes exercised${R}`);for(let[e,t]of v)z(` ${I}↳ ${e}: ${t.tests} test${t.tests===1?``:`s`} from ${t.matched} gate${t.matched===1?``:`s`}${t.ignored?`, ${t.ignored} stood down`:``}${R}`)}if(_&&z(`\n${I}✗ ${_} expected failure${_>1?`s`:``} (it.failing — upstream defects)${R}`),x.length){z(`\n${I}⚠ ${x.length} warning${x.length>1?`s`:``} (not counted — nothing is claimed about these)${R}`);for(let e of x)z(` ${I}↳ ${e.message.trim().split(`
11
11
  `)[0]}${R}`)}if(b.length){z(`\n${P}⚠ ${b.length} assertion${b.length>1?`s`:``} fired outside any it() (leaked from a settled test)${R}`);for(let e of b)z(` ${P}↳ ${e.message.trim().split(`
12
12
  `)[0]}${R}`)}if(p||m){printFailureRecap(n);let e=formatFailureVerdict({failed:p,outside:m,tests:d});z(`\n${P}❌ ${n}${e}${countsSuffix()}${t}${R}`)}else z(g?`\n${P}❌ ${n}${d} test${d===1?``:`s`} passed, then a suite body threw — the run is INCOMPLETE${countsSuffix()}${t}${R}`:`\n${F}✔ ${n}${d} test${d===1?``:`s`} passed${countsSuffix()}${t}${R}`)},countsSuffix=()=>{let e=[`${u} assertion${u===1?``:`s`}`];return h&&e.push(`${h} ignored`),` ${L}· ${e.join(` · `)}${R}`},printFailureRecap=e=>{for(let t of formatFailureRecap(O,p+m,e))z(t);if(envVar(`GITHUB_ACTIONS`))for(let t of formatFailureAnnotations(O,e))z(t)},formatFailureAnnotations=(e,t=``,n=10)=>{let escape=e=>e.replace(/%/g,`%25`).replace(/\r/g,`%0D`).replace(/\n/g,`%0A`),r=e.slice(0,n).map(e=>`::error title=${escape(`${t}${e.suite} › ${e.test}`)}::${escape(e.message.trim().split(`
13
13
  `)[0])}`);return e.length>n&&r.push(`::error title=${escape(`${t}more failures`)}::${e.length-n} further failure(s) — see the ✖ recap in the log`),r},formatFailureRecap=(e,t,n=``)=>{let r=[`\n${P}✖ ${n}failed test${e.length===1?``:`s`}${R}`];for(let t of e){let e=t.message.trim().split(`
14
- `)[0];r.push(` ${P}✖ ${t.suite} › ${t.test}${R}${L} — ${e}${R}`)}return e.length!==t&&r.push(` ${P}✖ ${t} failure${t===1?``:`s`} counted but ${e.length} named — a failure path is raising the tally without recording itself, so this list is INCOMPLETE${R}`),r},q={GJS:`Gjs`,"Node.js":`Node.js`,Bun:`Bun`,Deno:`Deno`},getRuntime=async()=>{if(w&&w!==`Unknown`)return w;let e=q[a];return e?(w=o?`${e} ${o}`:e,w):runtimeGlobals().document===void 0?w||`Unknown`:(w=`Browser`,w)},printRuntime=async()=>{let e=await getRuntime();z(`\nRunning on ${e}`)},run=async(e,t)=>{applyEnvOverrides(),installUncaughtHooks(),T=now(),j=new Map,_=0,x.length=0,v.clear(),S=probeCwd(),g=!1;let r=[];if(t&&(typeof t==`number`?k.runTimeout=t:(t.timeout!==void 0&&(k.runTimeout=t.timeout),t.testTimeout!==void 0&&(k.testTimeout=t.testTimeout),t.suiteTimeout!==void 0&&(k.suiteTimeout=t.suiteTimeout),t.skip&&(j=new Map(Object.entries(t.skip))),t.requireAxes&&(r=t.requireAxes))),A=n(envBag(),k.runTimeout),printRuntime().then(async()=>{try{await withTimeout(()=>runTests(e),k.runTimeout,`entire test run`)}catch(e){if(e instanceof TimeoutError)z(`\n${P}⏱ ${e.message}${R}`),++m,O.push({suite:`<test run>`,test:`<run timed out>`,message:e.message});else throw e}}).then(()=>failUnexercisedAxes(r)).then(()=>failUnmetExpectedAxes()).catch(e=>{g=!0;let t=e instanceof Error?e.stack??e.message:String(e);z(`\n${P}❌ a suite body threw, so the run is INCOMPLETE and every later suite was skipped${E?` (in "${E}")`:``} — ${t}${R}`)}).then(async()=>{if(printResult(),browserSignalDone(),z(),A.stop(),c(),l?.quit(),!l){let e=exitCodeFor(p+m,g);try{(globalThis.process||await import(`node:process`)).exit(e)}catch{}}}),l?.run(),l){let e=exitCodeFor(p+m,g);runtimeGlobals().imports?.system?.exit(e)}};var J={run,assert,expect,it,afterEach,beforeEach,on,describe,configure,print:z,vi:H};export{afterEach,assert,beforeEach,configure,J as default,describe,exitCodeFor,expect,formatFailureAnnotations,formatFailureRecap,formatFailureVerdict,formatValue,getAxisLedger,getTestCounters,it,on,z as print,run,r as spy,H as vi};
14
+ `)[0];r.push(` ${P}✖ ${t.suite} › ${t.test}${R}${L} — ${e}${R}`)}return e.length!==t&&r.push(` ${P}✖ ${t} failure${t===1?``:`s`} counted but ${e.length} named — a failure path is raising the tally without recording itself, so this list is INCOMPLETE${R}`),r},J={GJS:`Gjs`,"Node.js":`Node.js`,Bun:`Bun`,Deno:`Deno`},getRuntime=async()=>{if(w&&w!==`Unknown`)return w;let e=J[a];return e?(w=o?`${e} ${o}`:e,w):runtimeGlobals().document===void 0?w||`Unknown`:(w=`Browser`,w)},printRuntime=async()=>{let e=await getRuntime();z(`\nRunning on ${e}`)},run=async(e,t)=>{applyEnvOverrides(),installUncaughtHooks(),T=now(),j=new Map,_=0,x.length=0,v.clear(),S=probeCwd(),g=!1;let n=[];if(t&&(typeof t==`number`?k.runTimeout=t:(t.timeout!==void 0&&(k.runTimeout=t.timeout),t.testTimeout!==void 0&&(k.testTimeout=t.testTimeout),t.suiteTimeout!==void 0&&(k.suiteTimeout=t.suiteTimeout),t.skip&&(j=new Map(Object.entries(t.skip))),t.requireAxes&&(n=t.requireAxes))),A=r(envBag(),k.runTimeout),printRuntime().then(async()=>{try{await withTimeout(()=>runTests(e),k.runTimeout,`entire test run`)}catch(e){if(e instanceof TimeoutError)z(`\n${P}⏱ ${e.message}${R}`),++m,O.push({suite:`<test run>`,test:`<run timed out>`,message:e.message});else throw e}}).then(()=>failUnexercisedAxes(n)).then(()=>failUnmetExpectedAxes()).catch(e=>{g=!0;let t=e instanceof Error?e.stack??e.message:String(e);z(`\n${P}❌ a suite body threw, so the run is INCOMPLETE and every later suite was skipped${E?` (in "${E}")`:``} — ${t}${R}`)}).then(async()=>{if(printResult(),browserSignalDone(),z(),A.stop(),c(),l?.quit(),!l){let e=exitCodeFor(p+m,g);try{let t=globalThis.process||await import(`node:process`);await flushed([t.stdout,t.stderr]),t.exit(e)}catch{}}}),l?.run(),l){let e=exitCodeFor(p+m,g);runtimeGlobals().imports?.system?.exit(e)}};var Y={run,assert,expect,it,afterEach,beforeEach,on,describe,configure,print:z,vi:H};export{afterEach,assert,beforeEach,configure,Y as default,describe,exitCodeFor,expect,flushed,formatFailureAnnotations,formatFailureRecap,formatFailureVerdict,formatValue,getAxisLedger,getTestCounters,it,on,z as print,registerGlProbe,run,n as spy,H as vi};
@@ -0,0 +1 @@
1
+ export {};
@@ -4,17 +4,13 @@ import nodeAssert from 'node:assert';
4
4
  /**
5
5
  * What each `on()` gate actually DID, per axis it named.
6
6
  *
7
- * `on()` answers "is this host on that axis" and returns silently when it is not,
8
- * which is correct — `on('Deno', …)` is supposed to contribute nothing under Node.
9
- * The gap is that a gate which SHOULD have fired and did not is indistinguishable
10
- * from one that correctly stood down: a miss only does `++countTestsIgnored`, the
11
- * count is printed, and neither failure counter moves. So an axis that stopped
12
- * running leaves nothing behind — strictly worse than a deleted test
13
- * file, which at least shows up as a file that is gone.
7
+ * `on()` answers "is this host on that axis" and returns silently when it is not, which is correct.
8
+ * The gap is that a gate which SHOULD have fired and did not is indistinguishable from one that
9
+ * correctly stood down: a miss only does `++countTestsIgnored`, and neither failure counter moves —
10
+ * strictly worse than a deleted test file, which at least shows up as gone.
14
11
  *
15
- * `tests` is the delta the matched blocks actually executed, not the number of
16
- * `it()` calls they contain: a block that matched and then registered nothing is
17
- * the exact silent shape being measured, and it must not look like coverage.
12
+ * `tests` is the delta the matched blocks actually executed, not the number of `it()` calls they
13
+ * contain: a block that matched and then registered nothing must not look like coverage.
18
14
  */
19
15
  export interface AxisRecord {
20
16
  /** Gated blocks that matched this host and ran. */
@@ -40,10 +36,10 @@ export type Callback = () => void | Promise<void>;
40
36
  /**
41
37
  * What `on()` gates on: a runtime IDENTITY, or a host CAPABILITY.
42
38
  *
43
- * `'Display'` and `'Gl'` are capabilities and deliberately separate — a gate must
44
- * state the thing it actually requires. They were one name until the WebGL suites
45
- * (which need a realizable GL context) and any plain GTK-surface test shared
46
- * `'Display'`, which made each wrong on a different OS. See `hasDisplay`/`hasGl`.
39
+ * `'Display'` and `'Gl'` are capabilities and deliberately separate — a gate must state the thing
40
+ * it actually requires. Sharing one name made each wrong on a different OS: the WebGL suites (a
41
+ * realizable GL context) and any plain GTK-surface test both wanted `'Display'`.
42
+ * See `hasDisplay`/`hasGl`.
47
43
  */
48
44
  export type Runtime = 'Gjs' | 'Deno' | 'Bun' | 'Node.js' | 'Unknown' | 'Browser' | 'Display' | 'Gl';
49
45
  export interface RunOptions {
@@ -56,21 +52,18 @@ export interface RunOptions {
56
52
  /** `expectation` → reason; skips that test and prints the reason. */
57
53
  skip?: Record<string, string>;
58
54
  /**
59
- * Axes this entry claims to exercise — checked, not decorative.
60
- *
61
- * Each named axis that THIS host matches must have executed at least one test
62
- * through an `on()` gate, or the run fails. An axis the host does not match is
63
- * skipped, so one built entry can declare every axis it serves and each leg is
64
- * held only to its own (see `failUnexercisedAxes`).
55
+ * Axes this entry claims to exercise — checked, not decorative. Each named axis that THIS
56
+ * host matches must have executed at least one test through an `on()` gate, or the run fails.
57
+ * An axis the host does not match is skipped, so one built entry can declare every axis it
58
+ * serves and each leg is held only to its own (see `failUnexercisedAxes`).
65
59
  */
66
60
  requireAxes?: readonly Runtime[];
67
61
  }
68
62
  export declare const print: (...data: any[]) => void;
69
63
  /**
70
- * Render any value for an assertion failure message WITHOUT throwing.
71
- * Template-literal / `+` interpolation throws a TypeError on `symbol` and `bigint`
72
- * operands, which would mask the real assertion result, so matchers route operands
73
- * through this rather than interpolating them.
64
+ * Render any value for an assertion failure message WITHOUT throwing. Template-literal / `+`
65
+ * interpolation throws a TypeError on `symbol` and `bigint` operands, which would mask the real
66
+ * assertion result, so matchers route operands through this rather than interpolating them.
74
67
  */
75
68
  export declare function formatValue(value: unknown): string;
76
69
  type AnyFn = (...args: any[]) => any;
@@ -140,6 +133,14 @@ export declare const describe: {
140
133
  } | number): Promise<void>;
141
134
  skip(moduleName: string, _callback?: Callback): Promise<void>;
142
135
  };
136
+ /**
137
+ * `@gjsify/unit/gl`'s one call into this module: hands over the GTK/GDK probe so `on('Gl', …)`
138
+ * can use it without `index.ts` ever naming a GUI typelib itself.
139
+ */
140
+ export declare const registerGlProbe: (prober: () => Promise<{
141
+ ok: boolean;
142
+ failure: string;
143
+ }>) => void;
143
144
  /** Run `callback` only on the named runtime(s): `on('Deno', () => { it(…) })`. */
144
145
  export declare const on: (onRuntime: Runtime | Runtime[], version: string | Callback, callback?: Callback) => Promise<void>;
145
146
  export declare const beforeEach: (callback?: Callback) => void;
@@ -175,9 +176,9 @@ export declare const getTestCounters: () => {
175
176
  /**
176
177
  * Snapshot of what each `on()` gate did, keyed by the axis it named.
177
178
  *
178
- * The read side of `RunOptions.requireAxes` — a copy, so a caller cannot mutate the
179
- * runner's tally. Same seam and same reason as `getTestCounters()`: assert against
180
- * the numbers the verdict is computed from, not against printed text.
179
+ * The read side of `RunOptions.requireAxes` — a copy, so a caller cannot mutate the runner's tally.
180
+ * Same seam and same reason as `getTestCounters()`: assert against the numbers the verdict is
181
+ * computed from, not against printed text.
181
182
  */
182
183
  export declare const getAxisLedger: () => Record<string, AxisRecord>;
183
184
  export declare const expect: (actualValue: unknown, _message?: string) => MatcherFactory;
@@ -187,46 +188,61 @@ export declare const assert: {
187
188
  throws(promiseFn: () => unknown, args_0?: nodeAssert.AssertPredicate, args_1?: string): void;
188
189
  deepStrictEqual<T>(actual: unknown, expected: T, message?: string | Error): asserts actual is T;
189
190
  };
190
- /**
191
- * The process exit code, as a pure function of the two things that decide it.
192
- *
193
- * Extracted for the same reason `formatFailureRecap` is: the two exit sites read
194
- * module state and call `process.exit`, neither of which a spec can reach — and this
195
- * rule is exactly where the regression lived. `bodyThrew` with a zero tally used to
196
- * answer 0, so a run that dropped eight of nine suites reported success.
197
- */
198
191
  /**
199
192
  * The red summary's sentence: what failed, in terms that can be true together.
200
193
  *
201
- * TWO CLAUSES, because they are two different facts and one ratio cannot carry
202
- * both. `N of M tests failed` is only true of failures a TEST owns; a stray
203
- * assertion, a suite timeout and an unexercised axis raise the numerator without
204
- * raising the denominator, and the arithmetic then says something impossible —
205
- * measured before the split: `3 of 2 tests failed`, and with no real tests at
206
- * all, `2 of 0`. A line whose whole job is to be quoted cannot print that.
194
+ * TWO CLAUSES, because they are two different facts and one ratio cannot carry both. `N of M tests
195
+ * failed` is only true of failures a TEST owns; a stray assertion, a suite timeout and an
196
+ * unexercised axis raise the numerator without raising the denominator, and the arithmetic then
197
+ * says something impossible — measured before the split: `3 of 2 tests failed`, and with no real
198
+ * tests at all, `2 of 0`. A line whose whole job is to be quoted cannot print that.
207
199
  *
208
- * Exported and pure for `failure-recap.spec.ts`'s reason: the wording is free to
209
- * change, the accounting is not, and a spec asserting on printed colour would
210
- * pin the wrong half.
200
+ * Exported and pure for `failure-recap.spec.ts`'s reason: the wording is free to change, the
201
+ * accounting is not, and a spec asserting on printed colour would pin the wrong half.
211
202
  */
212
203
  export declare const formatFailureVerdict: (counts: {
213
204
  failed: number;
214
205
  outside: number;
215
206
  tests: number;
216
207
  }) => string;
208
+ /**
209
+ * The process exit code, as a pure function of the two things that decide it.
210
+ *
211
+ * Extracted for the same reason `formatFailureRecap` is: the two exit sites read module state and
212
+ * call `process.exit`, neither of which a spec can reach — and this rule is exactly where the
213
+ * regression lived. `bodyThrew` with a zero tally answered 0, so a run that dropped eight of nine
214
+ * suites reported success.
215
+ */
217
216
  export declare const exitCodeFor: (failed: number, bodyThrew: boolean) => number;
217
+ /** The one member of a Node `Writable` that `flushed` needs. */
218
+ export interface FlushableStream {
219
+ write(chunk: string, callback: (error?: Error | null) => void): unknown;
220
+ }
221
+ /**
222
+ * Resolve once everything already written to each stream has reached the OS.
223
+ *
224
+ * `process.exit()` DISCARDS output still queued in a stream, and on macOS a stdout PIPE is
225
+ * asynchronous in Node (synchronous on Linux, and a file is synchronous everywhere). Every CI log
226
+ * is a pipe. Measured on #1999: the react-native suite exited 1 on macOS with the log cut off
227
+ * after a passing case — the recap that named the failure was in the queue `process.exit` threw
228
+ * away, so the report said "exited with code 1" and nothing else. A slow reader reproduces it
229
+ * locally on every run; a file redirect never does, which is why it read as a crash.
230
+ *
231
+ * An empty write completes after every write queued before it. Its error argument is ignored: a
232
+ * reader that went away cannot be flushed to, and the exit must still happen.
233
+ */
234
+ export declare const flushed: (streams: readonly FlushableStream[]) => Promise<void>;
218
235
  /**
219
236
  * The failures as GitHub Actions `::error::` workflow commands.
220
237
  *
221
- * Separate from the human recap because the constraints differ: a command must start
222
- * at column 0, carry no SGR codes (they would be printed literally in the annotation),
223
- * and encode newlines as `%0A`. Emitting a coloured recap line here would put escape
224
- * sequences on the summary page.
238
+ * Separate from the human recap because the constraints differ: a command must start at column 0,
239
+ * carry no SGR codes (they would be printed literally in the annotation), and encode newlines as
240
+ * `%0A`. Emitting a coloured recap line here would put escape sequences on the summary page.
225
241
  *
226
- * Capped, because Actions renders at most ten annotations per step and this runner is
227
- * launched once per runtime per shard — a 200-failure leg would spend the whole budget
228
- * on one shard and push every other leg's first failure off the page. The count is
229
- * stated when it truncates, so the cap can never read as "that was all of them".
242
+ * Capped, because Actions renders at most ten annotations per step and this runner is launched once
243
+ * per runtime per shard — a 200-failure leg would spend the whole budget on one shard and push
244
+ * every other leg's first failure off the page. The count is stated when it truncates, so the cap
245
+ * can never read as "that was all of them".
230
246
  */
231
247
  export declare const formatFailureAnnotations: (entries: ReadonlyArray<{
232
248
  suite: string;
@@ -236,11 +252,10 @@ export declare const formatFailureAnnotations: (entries: ReadonlyArray<{
236
252
  /**
237
253
  * The recap's LINES, as a pure function of the ledger and the tally.
238
254
  *
239
- * Split out so a spec can drive the shipping formatter: `printFailureRecap` reads
240
- * module-level state and writes to stdout, neither of which a test can reach — and a
241
- * reporter nothing tests is how the gap in #1159 lasted this long. The colour codes
242
- * are applied here too, so assertions see the shipped strings rather than a parallel
243
- * spelling of them.
255
+ * Split out so a spec can drive the shipping formatter: `printFailureRecap` reads module-level
256
+ * state and writes to stdout, neither of which a test can reach — and a reporter nothing tests is
257
+ * how the gap in #1159 lasted this long. The colour codes are applied here too, so assertions see
258
+ * the shipped strings rather than a parallel spelling of them.
244
259
  */
245
260
  export declare const formatFailureRecap: (entries: ReadonlyArray<{
246
261
  suite: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/unit",
3
- "version": "0.53.0",
3
+ "version": "0.55.0",
4
4
  "description": "A BDD-style testing framework for Gjs",
5
5
  "module": "lib/esm/index.js",
6
6
  "types": "lib/types/index.d.ts",
@@ -12,8 +12,15 @@
12
12
  ".": {
13
13
  "types": "./lib/types/index.d.ts",
14
14
  "default": "./lib/esm/index.js"
15
+ },
16
+ "./gl": {
17
+ "types": "./lib/types/gl.d.ts",
18
+ "default": "./lib/esm/gl.js"
15
19
  }
16
20
  },
21
+ "sideEffects": [
22
+ "./lib/esm/gl.js"
23
+ ],
17
24
  "scripts": {
18
25
  "clear": "gjsify clear lib tsconfig.tsbuildinfo tsconfig.types.tsbuildinfo test.gjs.mjs test.node.mjs",
19
26
  "check": "gjsify tsc --noEmit",
@@ -50,22 +57,22 @@
50
57
  "devDependencies": {
51
58
  "@girs/gjs": "5.4.0",
52
59
  "@girs/glib-2.0": "5.4.0",
53
- "@gjsify/cli": "^0.53.0",
60
+ "@gjsify/cli": "^0.55.0",
54
61
  "@types/node": "^25.9.2",
55
62
  "typescript": "^6.0.3"
56
63
  },
57
64
  "dependencies": {
58
- "@gjsify/abort-controller": "^0.53.0",
59
- "@gjsify/assert": "^0.53.0",
60
- "@gjsify/buffer": "^0.53.0",
61
- "@gjsify/dom-elements": "^0.53.0",
62
- "@gjsify/dom-exception": "^0.53.0",
63
- "@gjsify/node-globals": "^0.53.0",
64
- "@gjsify/process": "^0.53.0",
65
- "@gjsify/runtime": "^0.53.0",
66
- "@gjsify/utils": "^0.53.0",
67
- "@gjsify/web-globals": "^0.53.0",
68
- "@gjsify/web-streams": "^0.53.0"
65
+ "@gjsify/abort-controller": "^0.55.0",
66
+ "@gjsify/assert": "^0.55.0",
67
+ "@gjsify/buffer": "^0.55.0",
68
+ "@gjsify/dom-elements": "^0.55.0",
69
+ "@gjsify/dom-exception": "^0.55.0",
70
+ "@gjsify/node-globals": "^0.55.0",
71
+ "@gjsify/process": "^0.55.0",
72
+ "@gjsify/runtime": "^0.55.0",
73
+ "@gjsify/utils": "^0.55.0",
74
+ "@gjsify/web-globals": "^0.55.0",
75
+ "@gjsify/web-streams": "^0.55.0"
69
76
  },
70
77
  "gjsify": {
71
78
  "tier": 1,