@gjsify/unit 0.25.1 → 0.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/esm/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import"./_virtual/_rolldown/runtime.js";import{spy as e}from"./spy.js";import"@girs/gjs";import t from"node:assert";import{quitMainLoop as n}from"@gjsify/utils/main-loop";const runtimeGlobals=()=>globalThis,r=typeof runtimeGlobals().process?.versions?.gjs==`string`?runtimeGlobals().imports?.mainloop:void 0;let i=0,a=0,o=0,s=0;const c=[],noteStrayFailure=e=>{++a,c.push({suite:d,message:e}),p.push({suite:d,test:`<stray assertion outside any it()>`,message:e})};let l=``,u=0,d=``,p=[],m={testTimeout:5e3,suiteTimeout:3e4,runTimeout:12e4},h=new Map;var TimeoutError=class extends Error{constructor(e,t){super(`Timeout: "${e}" exceeded ${t}ms`),this.name=`TimeoutError`}};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)}),a=Promise.resolve(e());a.catch(()=>{});try{return await Promise.race([a,i])}finally{clearTimeout(r)}}const configure=e=>{m={...m,...e}};function applyEnvOverrides(){try{let e=runtimeGlobals().process?.env;if(!e)return;let t=parseInt(e.GJSIFY_TEST_TIMEOUT,10);!isNaN(t)&&t>=0&&(m.testTimeout=t);let n=parseInt(e.GJSIFY_SUITE_TIMEOUT,10);!isNaN(n)&&n>=0&&(m.suiteTimeout=n);let r=parseInt(e.GJSIFY_RUN_TIMEOUT,10);!isNaN(r)&&r>=0&&(m.runTimeout=r)}catch{}}const g=`\x1B[31m`,_=`\x1B[32m`,v=`\x1B[34m`,y=`\x1B[90m`,b=`\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`,x=typeof runtimeGlobals().process?.versions?.gjs!=`string`&&runtimeGlobals().document!==void 0?console.log:globalThis.print||console.log;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 S=[],C=[];function envBag(){return globalThis.process?.env}const w={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;S.push({key:e,had:e in n,prev:n[e]}),n[e]=t},unstubAllGlobals(){let e=globalThis;for(let t=S.length-1;t>=0;t--){let n=S[t];n.had?e[n.key]=n.prev:delete e[n.key]}S.length=0},stubEnv(e,t){let n=envBag();n&&(C.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=C.length-1;t>=0;t--){let n=C[t];n.had?e[n.key]=n.prev:delete e[n.key]}C.length=0}}};var T=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,s===0&&noteStrayFailure(t),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{spy as e}from"./spy.js";import"@girs/gjs";import t from"node:assert";import{quitMainLoop as n}from"@gjsify/utils/main-loop";const runtimeGlobals=()=>globalThis,r=typeof runtimeGlobals().process?.versions?.gjs==`string`?runtimeGlobals().imports?.mainloop:void 0;let i=0,a=0,o=0,s=0,c=0;const l=[],noteStrayFailure=e=>{++a,l.push({suite:p,message:e}),m.push({suite:p,test:`<stray assertion outside any it()>`,message:e})};let u=``,d=0,p=``,m=[],h={testTimeout:5e3,suiteTimeout:3e4,runTimeout:12e4},g=new Map;var TimeoutError=class extends Error{constructor(e,t){super(`Timeout: "${e}" exceeded ${t}ms`),this.name=`TimeoutError`}};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)}),a=Promise.resolve(e());a.catch(()=>{});try{return await Promise.race([a,i])}finally{clearTimeout(r)}}const configure=e=>{h={...h,...e}};function applyEnvOverrides(){try{let e=runtimeGlobals().process?.env;if(!e)return;let t=parseInt(e.GJSIFY_TEST_TIMEOUT,10);!isNaN(t)&&t>=0&&(h.testTimeout=t);let n=parseInt(e.GJSIFY_SUITE_TIMEOUT,10);!isNaN(n)&&n>=0&&(h.suiteTimeout=n);let r=parseInt(e.GJSIFY_RUN_TIMEOUT,10);!isNaN(r)&&r>=0&&(h.runTimeout=r)}catch{}}const _=`\x1B[31m`,v=`\x1B[32m`,y=`\x1B[34m`,b=`\x1B[90m`,x=`\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`,S=typeof runtimeGlobals().process?.versions?.gjs!=`string`&&runtimeGlobals().document!==void 0?console.log:globalThis.print||console.log;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 C=[],w=[];function envBag(){return globalThis.process?.env}const T={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;C.push({key:e,had:e in n,prev:n[e]}),n[e]=t},unstubAllGlobals(){let e=globalThis;for(let t=C.length-1;t>=0;t--){let n=C[t];n.had?e[n.key]=n.prev:delete e[n.key]}C.length=0},stubEnv(e,t){let n=envBag();n&&(w.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=w.length-1;t>=0;t--){let n=w[t];n.had?e[n.key]=n.prev:delete e[n.key]}w.length=0}}};var E=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,c===0&&noteStrayFailure(t),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){this.triggerResult(this.actualValue==e,` Expected values to match using ==
3
3
  Expected: ${formatValue(e)} (${typeof e})\n Actual: ${formatValue(this.actualValue)} (${typeof this.actualValue})`)}toStrictEqual(e){let n=!0,r=``;try{t.deepStrictEqual(this.actualValue,e)}catch(e){n=!1,r=e.message||``}this.triggerResult(n,` Expected values to be deeply strictly equal
4
4
  Expected: ${JSON.stringify(e)}\n Actual: ${JSON.stringify(this.actualValue)}`+(r?`\n ${r}`:``))}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 n=this.mockCalls().some(n=>{try{return t.deepStrictEqual(n,e),!0}catch{return!1}});this.triggerResult(n,` 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,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,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,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??m.suiteTimeout;x(`
9
- `+e);let i=d;d=e;let o=now();try{await withTimeout(t,r,`describe: ${e}`)}catch(e){if(e instanceof TimeoutError)++a,x(` ${g}⏱ Suite timed out: ${e.message}${b}`);else throw e}d=i;let s=now()-o;x(` ${y}↳ ${formatDuration(s)}${b}`),E=null,D=null};describe.skip=async function(e,t){++o,x(`\n${v}- ${e} (skipped)${b}`)};const hasDisplay=()=>{let e=runtimeGlobals().process?.env;if(e)return!!(e.DISPLAY||e.WAYLAND_DISPLAY);try{let e=runtimeGlobals().imports?.gi?.GLib;if(e)return!!(e.getenv(`DISPLAY`)||e.getenv(`WAYLAND_DISPLAY`))}catch{}return!1},runtimeMatch=async function(e,t){if(e.includes(`Display`))return{matched:hasDisplay()};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}=await runtimeMatch(e,t);if(!r){++o;return}x(`\nOn ${e.join(`, `)}${t?` `+t:``}`),await n()};let E,D;const beforeEach=function(e){E=e},afterEach=function(e){D=e},it=async function(e,t,n){let r=h.get(e);if(r!==void 0){++o,x(` ${v}-${b} ${y}${e} (skipped: ${r})${b}`);return}let i=typeof n==`number`?n:n?.timeout??m.testTimeout,c=now();++s;try{typeof E==`function`&&await E(),await withTimeout(t,i,e),typeof D==`function`&&await D();let n=now()-c;x(` ${_}✔${b} ${y}${e} (${formatDuration(n)})${b}`)}catch(t){let n=now()-c;++a,p.push({suite:d,test:e,message:t.message??String(t)});let r=t instanceof TimeoutError?`⏱`:`❌`;x(` ${g}${r}${b} ${y}${e} (${formatDuration(n)})${b}`),x(`${g}${t.message}${b}`),t.stack&&x(t.stack)}finally{--s}};it.skip=async function(e,t){++o,x(` ${v}-${b} ${y}${e} (skipped)${b}`)};const expect=function(e,t){return++i,new T(e,!0)},failAssertion=e=>{throw e.__testFailureCounted=!0,s===0&&noteStrayFailure(e?.message??String(e)),e},assert=function(e,n){++i;try{t(e,n)}catch(e){failAssertion(e)}};assert.strictEqual=function(e,n,r){++i;try{t.strictEqual(e,n,r)}catch(e){failAssertion(e)}},assert.throws=function(e,...n){++i;let r;try{e()}catch(e){r=e}try{t.throws(()=>{if(r)throw r},n[0],n[1])}catch(e){failAssertion(e)}},assert.deepStrictEqual=function(e,n,r){++i;try{t.deepStrictEqual(e,n,r)}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:i-a,failed:a,total:i,errors:p},t.documentElement.dataset.testsDone=`true`)},printResult=()=>{let e=u>0?now()-u:0,t=e>0?` ${y}(${formatDuration(e)})`:``,n=l?`[${l}] `:``;if(o&&x(`\n${v}✔ ${o} ignored test${o>1?`s`:``}${b}`),c.length){x(`\n${g}⚠ ${c.length} assertion${c.length>1?`s`:``} fired outside any it() (leaked from a settled test)${b}`);for(let e of c)x(` ${g}↳ ${e.message.trim().split(`
10
- `)[0]}${b}`)}x(a?`\n${g}❌ ${n}${a} of ${i} tests failed${t}${b}`:`\n${_}✔ ${n}${i} completed${t}${b}`)},getRuntime=async()=>{if(l&&l!==`Unknown`)return l;if(globalThis.Deno?.version?.deno)return`Deno `+globalThis.Deno?.version?.deno;{let e=globalThis.process;if(!e)try{e=await import(`node:process`)}catch{}if(e?.versions?.gjs)return l=`Gjs `+e.versions.gjs,l;if(e?.versions?.node)return l=`Node.js `+e.versions.node,l}return runtimeGlobals().document===void 0?l||`Unknown`:(l=`Browser`,l)},printRuntime=async()=>{let e=await getRuntime();x(`\nRunning on ${e}`)},run=async(e,t)=>{if(applyEnvOverrides(),u=now(),h=new Map,t&&(typeof t==`number`?m.runTimeout=t:(t.timeout!==void 0&&(m.runTimeout=t.timeout),t.testTimeout!==void 0&&(m.testTimeout=t.testTimeout),t.suiteTimeout!==void 0&&(m.suiteTimeout=t.suiteTimeout),t.skip&&(h=new Map(Object.entries(t.skip))))),printRuntime().then(async()=>{try{await withTimeout(()=>runTests(e),m.runTimeout,`entire test run`)}catch(e){if(e instanceof TimeoutError)x(`\n${g}⏱ ${e.message}${b}`),++a;else throw e}}).then(async()=>{if(printResult(),browserSignalDone(),x(),n(),r?.quit(),!r){let e=+(a>0);try{(globalThis.process||await import(`node:process`)).exit(e)}catch{}}}),r?.run(),r){let e=+(a>0);try{runtimeGlobals().imports?.system?.exit(e)}catch{}}};var O={run,assert,expect,it,afterEach,beforeEach,on,describe,configure,print:x,vi:w};export{afterEach,assert,beforeEach,configure,O as default,describe,expect,formatValue,it,on,x as print,run,e as spy,w as vi};
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,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,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,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??h.suiteTimeout;S(`
9
+ `+e);let i=p;p=e;let o=now();try{await withTimeout(t,r,`describe: ${e}`)}catch(e){if(e instanceof TimeoutError)++a,S(` ${_}⏱ Suite timed out: ${e.message}${x}`);else throw e}p=i;let s=now()-o;S(` ${b}↳ ${formatDuration(s)}${x}`),D=null,O=null};describe.skip=async function(e,t){++o,S(`\n${y}- ${e} (skipped)${x}`)};const hasDisplay=()=>{let e=runtimeGlobals().process?.env;if(e)return!!(e.DISPLAY||e.WAYLAND_DISPLAY);let t=runtimeGlobals().imports?.gi?.GLib;return t?!!(t.getenv(`DISPLAY`)||t.getenv(`WAYLAND_DISPLAY`)):!1},runtimeMatch=async function(e,t){if(e.includes(`Display`))return{matched:hasDisplay()};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}=await runtimeMatch(e,t);if(!r){++o;return}S(`\nOn ${e.join(`, `)}${t?` `+t:``}`),await n()};let D,O;const beforeEach=function(e){D=e},afterEach=function(e){O=e},it=async function(e,t,n){let r=g.get(e);if(r!==void 0){++o,S(` ${y}-${x} ${b}${e} (skipped: ${r})${x}`);return}let i=typeof n==`number`?n:n?.timeout??h.testTimeout,s=now();++c;try{typeof D==`function`&&await D(),await withTimeout(t,i,e),typeof O==`function`&&await O();let n=now()-s;S(` ${v}✔${x} ${b}${e} (${formatDuration(n)})${x}`)}catch(t){let n=now()-s;++a,m.push({suite:p,test:e,message:t.message??String(t)});let r=t instanceof TimeoutError?`⏱`:`❌`;S(` ${_}${r}${x} ${b}${e} (${formatDuration(n)})${x}`),S(`${_}${t.message}${x}`),t.stack&&S(t.stack)}finally{--c}};it.skip=async function(e,t){++o,S(` ${y}-${x} ${b}${e} (skipped)${x}`)};const getTestCounters=()=>({overall:i,failed:a,ignored:o,xfail:s});it.failing=async function(e,t,n){let r=h.testTimeout,i=now();++c;let o=!1;try{typeof D==`function`&&await D(),await withTimeout(t,r,e),typeof O==`function`&&await O()}catch{o=!0}finally{--c}let l=now()-i;if(o){++s,S(` ${y}✗${x} ${b}${e} (expected failure — ${n})${x}`);return}++a,m.push({suite:p,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}`}),S(` ${_}❌${x} ${b}${e} (${formatDuration(l)})${x}`),S(`${_}it.failing passed unexpectedly — remove the marker. Reason it carried: ${n}${x}`)};const expect=function(e,t){return++i,new E(e,!0)},failAssertion=e=>{throw e.__testFailureCounted=!0,c===0&&noteStrayFailure(e?.message??String(e)),e},assert=function(e,n){++i;try{t(e,n)}catch(e){failAssertion(e)}};assert.strictEqual=function(e,n,r){++i;try{t.strictEqual(e,n,r)}catch(e){failAssertion(e)}},assert.throws=function(e,...n){++i;let r;try{e()}catch(e){r=e}try{t.throws(()=>{if(r)throw r},n[0],n[1])}catch(e){failAssertion(e)}},assert.deepStrictEqual=function(e,n,r){++i;try{t.deepStrictEqual(e,n,r)}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:i-a,failed:a,total:i,errors:m},t.documentElement.dataset.testsDone=`true`)},printResult=()=>{let e=d>0?now()-d:0,t=e>0?` ${b}(${formatDuration(e)})`:``,n=u?`[${u}] `:``;if(o&&S(`\n${y}✔ ${o} ignored test${o>1?`s`:``}${x}`),s&&S(`\n${y}✗ ${s} expected failure${s>1?`s`:``} (it.failing — upstream defects)${x}`),l.length){S(`\n${_}⚠ ${l.length} assertion${l.length>1?`s`:``} fired outside any it() (leaked from a settled test)${x}`);for(let e of l)S(` ${_}↳ ${e.message.trim().split(`
10
+ `)[0]}${x}`)}S(a?`\n${_}❌ ${n}${a} of ${i} tests failed${t}${x}`:`\n${v}✔ ${n}${i} completed${t}${x}`)},getRuntime=async()=>{if(u&&u!==`Unknown`)return u;if(globalThis.Deno?.version?.deno)return`Deno `+globalThis.Deno?.version?.deno;{let e=globalThis.process;if(!e)try{e=await import(`node:process`)}catch{}if(e?.versions?.gjs)return u=`Gjs `+e.versions.gjs,u;if(e?.versions?.node)return u=`Node.js `+e.versions.node,u}return runtimeGlobals().document===void 0?u||`Unknown`:(u=`Browser`,u)},printRuntime=async()=>{let e=await getRuntime();S(`\nRunning on ${e}`)},run=async(e,t)=>{if(applyEnvOverrides(),d=now(),g=new Map,s=0,t&&(typeof t==`number`?h.runTimeout=t:(t.timeout!==void 0&&(h.runTimeout=t.timeout),t.testTimeout!==void 0&&(h.testTimeout=t.testTimeout),t.suiteTimeout!==void 0&&(h.suiteTimeout=t.suiteTimeout),t.skip&&(g=new Map(Object.entries(t.skip))))),printRuntime().then(async()=>{try{await withTimeout(()=>runTests(e),h.runTimeout,`entire test run`)}catch(e){if(e instanceof TimeoutError)S(`\n${_}⏱ ${e.message}${x}`),++a;else throw e}}).then(async()=>{if(printResult(),browserSignalDone(),S(),n(),r?.quit(),!r){let e=+(a>0);try{(globalThis.process||await import(`node:process`)).exit(e)}catch{}}}),r?.run(),r){let e=+(a>0);runtimeGlobals().imports?.system?.exit(e)}};var k={run,assert,expect,it,afterEach,beforeEach,on,describe,configure,print:S,vi:T};export{afterEach,assert,beforeEach,configure,k as default,describe,expect,formatValue,getTestCounters,it,on,S as print,run,e as spy,T as vi};
@@ -100,6 +100,48 @@ export declare const it: {
100
100
  timeout?: number;
101
101
  } | number): Promise<void>;
102
102
  skip(expectation: string, _callback?: () => void | Promise<void>): Promise<void>;
103
+ failing(expectation: string, callback: () => void | Promise<void>, reason: string): Promise<void>;
104
+ };
105
+ /**
106
+ * An EXPECTED failure — a test that asserts the correct behaviour against a
107
+ * defect we cannot fix from here (an upstream bug, a platform gap).
108
+ *
109
+ * This is categorically NOT `it.skip`, and the difference is the whole point:
110
+ *
111
+ * - a skip stops running the code, so it hides forever and nothing ever tells
112
+ * you the day the bug is fixed;
113
+ * - `it.failing` RUNS the test, tolerates the failure it was told to expect —
114
+ * and **fails the suite the moment the test starts passing**, because that
115
+ * means the marker has outlived its cause and must be removed.
116
+ *
117
+ * So it is self-retiring: it keeps the suite honest today without turning the
118
+ * gate off, and it turns itself into a task the moment upstream lands the fix.
119
+ * The assertion is never weakened — the test still asserts the spec-correct
120
+ * behaviour, which is what makes the pass-detection meaningful.
121
+ *
122
+ * `reason` is mandatory and should name the upstream defect and where it is
123
+ * tracked, so the next reader does not have to re-derive why this is here.
124
+ *
125
+ * ```ts
126
+ * await it.failing(
127
+ * 'send/receive empty string',
128
+ * async () => { … },
129
+ * 'GStreamer webrtcdatachannel sends a zero-length buffer for the ' +
130
+ * 'STRING_EMPTY PPID; RFC 8831 §6.6 requires one zero byte. Not ' +
131
+ * 'reachable from JS — needs an upstream fix.',
132
+ * );
133
+ * ```
134
+ */
135
+ /**
136
+ * Live view of the run counters. Exposed so `it.failing`'s own spec can assert
137
+ * on what the CI gate reads (the counters) instead of scraping printed text —
138
+ * the summary's wording is free to change, its accounting is not.
139
+ */
140
+ export declare const getTestCounters: () => {
141
+ overall: number;
142
+ failed: number;
143
+ ignored: number;
144
+ xfail: number;
103
145
  };
104
146
  export declare const expect: (actualValue: unknown, _message?: string) => MatcherFactory;
105
147
  export declare const assert: {
@@ -133,6 +175,7 @@ declare const _default: {
133
175
  timeout?: number;
134
176
  } | number): Promise<void>;
135
177
  skip(expectation: string, _callback?: () => void | Promise<void>): Promise<void>;
178
+ failing(expectation: string, callback: () => void | Promise<void>, reason: string): Promise<void>;
136
179
  };
137
180
  afterEach: (callback?: Callback) => void;
138
181
  beforeEach: (callback?: Callback) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/unit",
3
- "version": "0.25.1",
3
+ "version": "0.26.1",
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,21 +49,21 @@
49
49
  "devDependencies": {
50
50
  "@girs/gjs": "^4.1.0",
51
51
  "@girs/glib-2.0": "^4.1.0",
52
- "@gjsify/cli": "^0.25.1",
52
+ "@gjsify/cli": "^0.26.1",
53
53
  "@types/node": "^25.9.2",
54
54
  "typescript": "^6.0.3"
55
55
  },
56
56
  "dependencies": {
57
- "@gjsify/abort-controller": "^0.25.1",
58
- "@gjsify/assert": "^0.25.1",
59
- "@gjsify/buffer": "^0.25.1",
60
- "@gjsify/dom-elements": "^0.25.1",
61
- "@gjsify/dom-exception": "^0.25.1",
62
- "@gjsify/node-globals": "^0.25.1",
63
- "@gjsify/process": "^0.25.1",
64
- "@gjsify/utils": "^0.25.1",
65
- "@gjsify/web-globals": "^0.25.1",
66
- "@gjsify/web-streams": "^0.25.1"
57
+ "@gjsify/abort-controller": "^0.26.1",
58
+ "@gjsify/assert": "^0.26.1",
59
+ "@gjsify/buffer": "^0.26.1",
60
+ "@gjsify/dom-elements": "^0.26.1",
61
+ "@gjsify/dom-exception": "^0.26.1",
62
+ "@gjsify/node-globals": "^0.26.1",
63
+ "@gjsify/process": "^0.26.1",
64
+ "@gjsify/utils": "^0.26.1",
65
+ "@gjsify/web-globals": "^0.26.1",
66
+ "@gjsify/web-streams": "^0.26.1"
67
67
  },
68
68
  "gjsify": {
69
69
  "tier": 1