@gjsify/unit 0.38.1 → 0.39.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/index.js +8 -6
- package/lib/types/index.d.ts +84 -12
- package/package.json +13 -13
package/lib/esm/index.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import"./_virtual/_rolldown/runtime.js";import{canRealizeGl as e,canRealizeSurface as t}from"./capabilities.js";import{spy as n}from"./spy.js";import"@girs/gjs";import r from"node:assert";import{runtimeName as i,runtimeVersion as a}from"@gjsify/runtime";import{hostOs as o}from"@gjsify/utils/core";import{quitMainLoop as s}from"@gjsify/utils/main-loop";const runtimeGlobals=()=>globalThis,c=typeof runtimeGlobals().process?.versions?.gjs==`string`?runtimeGlobals().imports?.mainloop:void 0;let l=0,u=0,d=0,p=0,m=0;
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{canRealizeGl as e,canRealizeSurface as t}from"./capabilities.js";import{spy as n}from"./spy.js";import"@girs/gjs";import r from"node:assert";import{runtimeName as i,runtimeVersion as a}from"@gjsify/runtime";import{hostOs as o}from"@gjsify/utils/core";import{quitMainLoop as s}from"@gjsify/utils/main-loop";const runtimeGlobals=()=>globalThis,c=typeof runtimeGlobals().process?.versions?.gjs==`string`?runtimeGlobals().imports?.mainloop:void 0;let l=0,u=0,d=0,p=0;const m=new Map,axisRecord=e=>{let t=m.get(e);return t||(t={matched:0,ignored:0,tests:0},m.set(e,t)),t};let h=0;const g=[],_=[],noteWarning=e=>{_.push({suite:S,message:e})};let v=!0;const probeCwd=()=>{let e=runtimeGlobals().process?.cwd;if(typeof e!=`function`)return!0;try{return e(),!0}catch{return!1}},noteIfCwdDestroyed=e=>{if(!v||probeCwd())return;v=!1,++u;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.";w.push({suite:S,test:e,message:t}),P(` ${k}❌ ${e} — ${t}${N}`)},noteStrayFailure=e=>{++u,g.push({suite:S,message:e}),w.push({suite:S,test:`<stray assertion outside any it()>`,message:e})},y=[],noteThrownAssertion=e=>{let t=y[y.length-1];t&&e instanceof Error&&t.add(e)},forgetThrownAssertion=e=>{if(e instanceof Error)for(let t of y)t.delete(e)};let b=``,x=0,S=``,C=``,w=[],T={testTimeout:5e3,suiteTimeout:3e4,runTimeout:12e4},E=new Map;var TimeoutError=class extends Error{constructor(e,t){super(`Timeout: "${e}" exceeded ${t}ms`),this.name=`TimeoutError`}};const D=[];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(()=>{}),D.push(a);try{let t=Promise.resolve(e());return t.catch(()=>{}),await Promise.race([t,i,o])}finally{clearTimeout(r);let e=D.lastIndexOf(a);e!==-1&&D.splice(e,1)}}let O=!1;const installUncaughtHooks=()=>{if(O)return;let e=runtimeGlobals().process;if(typeof e?.on!=`function`||typeof e.versions?.gjs==`string`)return;O=!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=h>0?` during "${C}"`:``;noteWarning(`${t}: ${e}${r} — absorbed by a listener the spec installed itself`);return}let a=D[D.length-1];a?a(n):noteStrayFailure(n?.message??String(n))};e.on(`uncaughtException`,handle(`uncaughtException`)),e.on(`unhandledRejection`,handle(`unhandledRejection`))},configure=e=>{T={...T,...e}};function applyEnvOverrides(){try{let e=runtimeGlobals().process?.env;if(!e)return;let t=parseInt(e.GJSIFY_TEST_TIMEOUT,10);!isNaN(t)&&t>=0&&(T.testTimeout=t);let n=parseInt(e.GJSIFY_SUITE_TIMEOUT,10);!isNaN(n)&&n>=0&&(T.suiteTimeout=n);let r=parseInt(e.GJSIFY_RUN_TIMEOUT,10);!isNaN(r)&&r>=0&&(T.runTimeout=r)}catch{}}const k=`\x1B[31m`,A=`\x1B[32m`,j=`\x1B[34m`,M=`\x1B[90m`,N=`\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`,P=typeof runtimeGlobals().process?.versions?.gjs!=`string`&&runtimeGlobals().document!==void 0?console.log:globalThis.print||console.log;function isStructurallyEqual(e,t){try{return r.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 F=[],I=[];function envBag(){return globalThis.process?.env}const L={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;F.push({key:e,had:e in n,prev:n[e]}),n[e]=t},unstubAllGlobals(){let e=globalThis;for(let t=F.length-1;t>=0;t--){let n=F[t];n.had?e[n.key]=n.prev:delete e[n.key]}F.length=0},stubEnv(e,t){let n=envBag();n&&(I.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=I.length-1;t>=0;t--){let n=I[t];n.had?e[n.key]=n.prev:delete e[n.key]}I.length=0}}};var R=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,h===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{r.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)
|
|
5
5
|
Expected: ${formatValue(e)}\n Actual: ${formatValue(this.actualValue)}`)}mockCalls(){return this.actualValue?.mock?.calls??[]}toHaveBeenCalled(){this.triggerResult(this.mockCalls().length>0,` Expected mock function to have been called`)}toHaveBeenCalledTimes(e){let t=this.mockCalls().length;this.triggerResult(t===e,` Expected mock to have been called ${e} time(s)\n Actual: ${t} time(s)`)}toHaveBeenCalledWith(...e){let t=this.mockCalls().some(t=>{try{return r.deepStrictEqual(t,e),!0}catch{return!1}});this.triggerResult(t,` Expected mock to have been called with ${formatValue(e)}`)}toEqualArray(e){let t=this.actualValue,n=Array.isArray(t)&&Array.isArray(e)&&t.length===e.length;for(let r=0;r<t.length&&(n=t[r]==e[r],n);r++);this.triggerResult(n,` Expected array items to match using ==
|
|
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
|
-
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??
|
|
9
|
-
`+e);let i=
|
|
10
|
-
`),
|
|
11
|
-
`)[0]}${
|
|
12
|
-
`)[0]}${
|
|
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??T.suiteTimeout;P(`
|
|
9
|
+
`+e);let i=S;S=e;let a=now();try{await withTimeout(t,r,`describe: ${e}`)}catch(t){if(t instanceof TimeoutError)++u,w.push({suite:e,test:`<suite timed out>`,message:t.message}),P(` ${k}⏱ Suite timed out: ${t.message}${N}`);else throw t}S=i;let o=now()-a;P(` ${M}↳ ${formatDuration(o)}${N}`),z=null,B=null};describe.skip=async function(e,t){++d,P(`\n${j}- ${e} (skipped)${N}`)};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(o(),displayEnv()),hasGl=()=>e(o(),displayEnv()),runtimeMatch=async function(e,t){if(e.includes(`Display`))return{matched:hasDisplay(),runtime:`Display`};if(e.includes(`Gl`))return{matched: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){++d;for(let t of e)++axisRecord(t).ignored;return}P(`\nOn ${e.join(`, `)}${t?` `+t:``}`);let a=l;await n();let o=axisRecord(i??e[0]);++o.matched,o.tests+=l-a};let z,B;const beforeEach=function(e){z=e},afterEach=function(e){B=e},it=async function(e,t,n){let r=E.get(e);if(r!==void 0){++d,P(` ${j}-${N} ${M}${e} (skipped: ${r})${N}`);return}let i=typeof n==`number`?n:n?.timeout??T.testTimeout,a=now();++h,C=e;let o=new Set;y.push(o);let s,c=!1;try{typeof z==`function`&&await z(),await withTimeout(t,i,e),typeof B==`function`&&await B()}catch(e){c=!0,s=e,e instanceof Error&&o.delete(e)}finally{--h,y.pop(),noteIfCwdDestroyed(e)}let l=now()-a,p=s instanceof TimeoutError?[...o]:[];if(!c){P(` ${A}✔${N} ${M}${e} (${formatDuration(l)})${N}`);return}++u;let m=[];c&&m.push(s?.message??String(s));for(let e of p)m.push(`${e.message}\n ${M}↳ 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.${N}`);let g=m.join(`
|
|
10
|
+
`),_=p[0]??s,v=c&&s instanceof TimeoutError&&p.length===0?`⏱`:`❌`;w.push({suite:S,test:e,message:g}),P(` ${k}${v}${N} ${M}${e} (${formatDuration(l)})${N}`),P(`${k}${g}${N}`),_?.stack&&P(_.stack)};it.skip=async function(e,t){++d,P(` ${j}-${N} ${M}${e} (skipped)${N}`)};const getTestCounters=()=>({overall:l,failed:u,ignored:d,xfail:p,warnings:_.length}),getAxisLedger=()=>{let e={};for(let[t,n]of m)e[t]={...n};return e};it.failing=async function(e,t,n,r){let i=typeof r==`number`?r:r?.timeout??T.testTimeout;if(typeof r==`object`&&r?.when===!1)return it(e,t,{timeout:i});let a=now();++h,y.push(new Set);let o=!1;try{typeof z==`function`&&await z(),await withTimeout(t,i,e),typeof B==`function`&&await B()}catch{o=!0}finally{--h,y.pop()}let s=now()-a;if(o){++p,P(` ${j}✗${N} ${M}${e} (expected failure — ${n})${N}`);return}++u,w.push({suite:S,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}`}),P(` ${k}❌${N} ${M}${e} (${formatDuration(s)})${N}`),P(`${k}it.failing passed unexpectedly — remove the marker. Reason it carried: ${n}${N}`)};const expect=function(e,t){return++l,new R(e,!0)},failAssertion=e=>{throw e.__testFailureCounted=!0,h===0?noteStrayFailure(e?.message??String(e)):noteThrownAssertion(e),e},assert=function(e,t){++l;try{r(e,t)}catch(e){failAssertion(e)}};assert.strictEqual=function(e,t,n){++l;try{r.strictEqual(e,t,n)}catch(e){failAssertion(e)}},assert.throws=function(e,...t){++l;let n;try{e()}catch(e){n=e}try{r.throws(()=>{if(n)throw n},t[0],t[1])}catch(e){failAssertion(e)}},assert.deepStrictEqual=function(e,t,n){++l;try{r.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:l-u,failed:u,total:l,errors:w},t.documentElement.dataset.testsDone=`true`)},failUnexercisedAxes=async e=>{for(let t of e){let{matched:e}=await runtimeMatch([t]);if(!e)continue;let n=m.get(t);if(n&&n.tests>0)continue;++u;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`}`;w.push({suite:`<axis declaration>`,test:t,message:r}),P(`\n${k}❌ ${r}${N}`)}},printResult=()=>{let e=x>0?now()-x:0,t=e>0?` ${M}(${formatDuration(e)})`:``,n=b?`[${b}] `:``;if(d&&P(`\n${j}✔ ${d} ignored test${d>1?`s`:``}${N}`),m.size){P(`\n${j}⊞ axes exercised${N}`);for(let[e,t]of m)P(` ${j}↳ ${e}: ${t.tests} test${t.tests===1?``:`s`} from ${t.matched} gate${t.matched===1?``:`s`}${t.ignored?`, ${t.ignored} stood down`:``}${N}`)}if(p&&P(`\n${j}✗ ${p} expected failure${p>1?`s`:``} (it.failing — upstream defects)${N}`),_.length){P(`\n${j}⚠ ${_.length} warning${_.length>1?`s`:``} (not counted — nothing is claimed about these)${N}`);for(let e of _)P(` ${j}↳ ${e.message.trim().split(`
|
|
11
|
+
`)[0]}${N}`)}if(g.length){P(`\n${k}⚠ ${g.length} assertion${g.length>1?`s`:``} fired outside any it() (leaked from a settled test)${N}`);for(let e of g)P(` ${k}↳ ${e.message.trim().split(`
|
|
12
|
+
`)[0]}${N}`)}u?(printFailureRecap(n),P(`\n${k}❌ ${n}${u} of ${l} tests failed${t}${N}`)):P(`\n${A}✔ ${n}${l} completed${t}${N}`)},printFailureRecap=e=>{for(let t of formatFailureRecap(w,u,e))P(t);if(envVar(`GITHUB_ACTIONS`))for(let t of formatFailureAnnotations(w,e))P(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
|
+
`)[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${k}✖ ${n}failed test${e.length===1?``:`s`}${N}`];for(let t of e){let e=t.message.trim().split(`
|
|
14
|
+
`)[0];r.push(` ${k}✖ ${t.suite} › ${t.test}${N}${M} — ${e}${N}`)}return e.length!==t&&r.push(` ${k}✖ ${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${N}`),r},V={GJS:`Gjs`,"Node.js":`Node.js`,Bun:`Bun`,Deno:`Deno`},getRuntime=async()=>{if(b&&b!==`Unknown`)return b;let e=V[i];return e?(b=a?`${e} ${a}`:e,b):runtimeGlobals().document===void 0?b||`Unknown`:(b=`Browser`,b)},printRuntime=async()=>{let e=await getRuntime();P(`\nRunning on ${e}`)},run=async(e,t)=>{applyEnvOverrides(),installUncaughtHooks(),x=now(),E=new Map,p=0,_.length=0,m.clear(),v=probeCwd();let n=[];if(t&&(typeof t==`number`?T.runTimeout=t:(t.timeout!==void 0&&(T.runTimeout=t.timeout),t.testTimeout!==void 0&&(T.testTimeout=t.testTimeout),t.suiteTimeout!==void 0&&(T.suiteTimeout=t.suiteTimeout),t.skip&&(E=new Map(Object.entries(t.skip))),t.requireAxes&&(n=t.requireAxes))),printRuntime().then(async()=>{try{await withTimeout(()=>runTests(e),T.runTimeout,`entire test run`)}catch(e){if(e instanceof TimeoutError)P(`\n${k}⏱ ${e.message}${N}`),++u,w.push({suite:`<test run>`,test:`<run timed out>`,message:e.message});else throw e}}).then(()=>failUnexercisedAxes(n)).then(async()=>{if(printResult(),browserSignalDone(),P(),s(),c?.quit(),!c){let e=+(u>0);try{(globalThis.process||await import(`node:process`)).exit(e)}catch{}}}),c?.run(),c){let e=+(u>0);runtimeGlobals().imports?.system?.exit(e)}};var H={run,assert,expect,it,afterEach,beforeEach,on,describe,configure,print:P,vi:L};export{afterEach,assert,beforeEach,configure,H as default,describe,expect,formatFailureAnnotations,formatFailureRecap,formatValue,getAxisLedger,getTestCounters,it,on,P as print,run,n as spy,L as vi};
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
import '@girs/gjs';
|
|
2
2
|
export * from './spy.js';
|
|
3
3
|
import nodeAssert from 'node:assert';
|
|
4
|
+
/**
|
|
5
|
+
* What each `on()` gate actually DID, per axis it named.
|
|
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 the exit code reads `countTestsFailed` alone. So an axis
|
|
12
|
+
* that stopped running leaves nothing behind — strictly worse than a deleted test
|
|
13
|
+
* file, which at least shows up as a file that is gone.
|
|
14
|
+
*
|
|
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.
|
|
18
|
+
*/
|
|
19
|
+
export interface AxisRecord {
|
|
20
|
+
/** Gated blocks that matched this host and ran. */
|
|
21
|
+
matched: number;
|
|
22
|
+
/** Gated blocks that named this axis and stood down. */
|
|
23
|
+
ignored: number;
|
|
24
|
+
/** Tests the matched blocks executed. */
|
|
25
|
+
tests: number;
|
|
26
|
+
}
|
|
4
27
|
export interface TimeoutConfig {
|
|
5
28
|
/** Per-it() timeout in ms. Default: 5000. 0 = disabled. */
|
|
6
29
|
testTimeout: number;
|
|
@@ -23,6 +46,25 @@ export type Callback = () => void | Promise<void>;
|
|
|
23
46
|
* `'Display'`, which made each wrong on a different OS. See `hasDisplay`/`hasGl`.
|
|
24
47
|
*/
|
|
25
48
|
export type Runtime = 'Gjs' | 'Deno' | 'Bun' | 'Node.js' | 'Unknown' | 'Browser' | 'Display' | 'Gl';
|
|
49
|
+
export interface RunOptions {
|
|
50
|
+
/** Wall-clock budget for the whole run. */
|
|
51
|
+
timeout?: number;
|
|
52
|
+
/** Per-`it()` budget. */
|
|
53
|
+
testTimeout?: number;
|
|
54
|
+
/** Per-`describe()` budget. */
|
|
55
|
+
suiteTimeout?: number;
|
|
56
|
+
/** `expectation` → reason; skips that test and prints the reason. */
|
|
57
|
+
skip?: Record<string, string>;
|
|
58
|
+
/**
|
|
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`).
|
|
65
|
+
*/
|
|
66
|
+
requireAxes?: readonly Runtime[];
|
|
67
|
+
}
|
|
26
68
|
export declare const print: (...data: any[]) => void;
|
|
27
69
|
/**
|
|
28
70
|
* Render any value for an assertion failure message WITHOUT throwing.
|
|
@@ -124,6 +166,14 @@ export declare const getTestCounters: () => {
|
|
|
124
166
|
xfail: number;
|
|
125
167
|
warnings: number;
|
|
126
168
|
};
|
|
169
|
+
/**
|
|
170
|
+
* Snapshot of what each `on()` gate did, keyed by the axis it named.
|
|
171
|
+
*
|
|
172
|
+
* The read side of `RunOptions.requireAxes` — a copy, so a caller cannot mutate the
|
|
173
|
+
* runner's tally. Same seam and same reason as `getTestCounters()`: assert against
|
|
174
|
+
* the numbers the verdict is computed from, not against printed text.
|
|
175
|
+
*/
|
|
176
|
+
export declare const getAxisLedger: () => Record<string, AxisRecord>;
|
|
127
177
|
export declare const expect: (actualValue: unknown, _message?: string) => MatcherFactory;
|
|
128
178
|
export declare const assert: {
|
|
129
179
|
(success: unknown, message?: string | Error): void;
|
|
@@ -131,19 +181,41 @@ export declare const assert: {
|
|
|
131
181
|
throws(promiseFn: () => unknown, args_0?: nodeAssert.AssertPredicate, args_1?: string): void;
|
|
132
182
|
deepStrictEqual<T>(actual: unknown, expected: T, message?: string | Error): asserts actual is T;
|
|
133
183
|
};
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
184
|
+
/**
|
|
185
|
+
* The failures as GitHub Actions `::error::` workflow commands.
|
|
186
|
+
*
|
|
187
|
+
* Separate from the human recap because the constraints differ: a command must start
|
|
188
|
+
* at column 0, carry no SGR codes (they would be printed literally in the annotation),
|
|
189
|
+
* and encode newlines as `%0A`. Emitting a coloured recap line here would put escape
|
|
190
|
+
* sequences on the summary page.
|
|
191
|
+
*
|
|
192
|
+
* Capped, because Actions renders at most ten annotations per step and this runner is
|
|
193
|
+
* launched once per runtime per shard — a 200-failure leg would spend the whole budget
|
|
194
|
+
* on one shard and push every other leg's first failure off the page. The count is
|
|
195
|
+
* stated when it truncates, so the cap can never read as "that was all of them".
|
|
196
|
+
*/
|
|
197
|
+
export declare const formatFailureAnnotations: (entries: ReadonlyArray<{
|
|
198
|
+
suite: string;
|
|
199
|
+
test: string;
|
|
200
|
+
message: string;
|
|
201
|
+
}>, rtTag?: string, max?: number) => string[];
|
|
202
|
+
/**
|
|
203
|
+
* The recap's LINES, as a pure function of the ledger and the tally.
|
|
204
|
+
*
|
|
205
|
+
* Split out so a spec can drive the shipping formatter: `printFailureRecap` reads
|
|
206
|
+
* module-level state and writes to stdout, neither of which a test can reach — and a
|
|
207
|
+
* reporter nothing tests is how the gap in #1159 lasted this long. The colour codes
|
|
208
|
+
* are applied here too, so assertions see the shipped strings rather than a parallel
|
|
209
|
+
* spelling of them.
|
|
210
|
+
*/
|
|
211
|
+
export declare const formatFailureRecap: (entries: ReadonlyArray<{
|
|
212
|
+
suite: string;
|
|
213
|
+
test: string;
|
|
214
|
+
message: string;
|
|
215
|
+
}>, countFailed: number, rtTag?: string) => string[];
|
|
216
|
+
export declare const run: (namespaces: Namespaces, options?: RunOptions | number) => Promise<void>;
|
|
140
217
|
declare const _default: {
|
|
141
|
-
run: (namespaces: Namespaces, options?:
|
|
142
|
-
timeout?: number;
|
|
143
|
-
testTimeout?: number;
|
|
144
|
-
suiteTimeout?: number;
|
|
145
|
-
skip?: Record<string, string>;
|
|
146
|
-
} | number) => Promise<void>;
|
|
218
|
+
run: (namespaces: Namespaces, options?: RunOptions | number) => Promise<void>;
|
|
147
219
|
assert: {
|
|
148
220
|
(success: unknown, message?: string | Error): void;
|
|
149
221
|
strictEqual<T>(actual: unknown, expected: T, message?: string | Error): asserts actual is T;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/unit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.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",
|
|
@@ -49,22 +49,22 @@
|
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@girs/gjs": "^4.1.0",
|
|
51
51
|
"@girs/glib-2.0": "^4.1.0",
|
|
52
|
-
"@gjsify/cli": "^0.
|
|
52
|
+
"@gjsify/cli": "^0.39.0",
|
|
53
53
|
"@types/node": "^25.9.2",
|
|
54
54
|
"typescript": "^6.0.3"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@gjsify/abort-controller": "^0.
|
|
58
|
-
"@gjsify/assert": "^0.
|
|
59
|
-
"@gjsify/buffer": "^0.
|
|
60
|
-
"@gjsify/dom-elements": "^0.
|
|
61
|
-
"@gjsify/dom-exception": "^0.
|
|
62
|
-
"@gjsify/node-globals": "^0.
|
|
63
|
-
"@gjsify/process": "^0.
|
|
64
|
-
"@gjsify/runtime": "^0.
|
|
65
|
-
"@gjsify/utils": "^0.
|
|
66
|
-
"@gjsify/web-globals": "^0.
|
|
67
|
-
"@gjsify/web-streams": "^0.
|
|
57
|
+
"@gjsify/abort-controller": "^0.39.0",
|
|
58
|
+
"@gjsify/assert": "^0.39.0",
|
|
59
|
+
"@gjsify/buffer": "^0.39.0",
|
|
60
|
+
"@gjsify/dom-elements": "^0.39.0",
|
|
61
|
+
"@gjsify/dom-exception": "^0.39.0",
|
|
62
|
+
"@gjsify/node-globals": "^0.39.0",
|
|
63
|
+
"@gjsify/process": "^0.39.0",
|
|
64
|
+
"@gjsify/runtime": "^0.39.0",
|
|
65
|
+
"@gjsify/utils": "^0.39.0",
|
|
66
|
+
"@gjsify/web-globals": "^0.39.0",
|
|
67
|
+
"@gjsify/web-streams": "^0.39.0"
|
|
68
68
|
},
|
|
69
69
|
"gjsify": {
|
|
70
70
|
"tier": 1,
|