Initial rebrand: @polkadot -> @pezkuwi (3 packages)

- Package namespace: @polkadot/dev -> @pezkuwi/dev
- Repository: polkadot-js/dev -> pezkuwichain/pezkuwi-dev
- Author: Pezkuwi Team <team@pezkuwichain.io>

Packages:
- @pezkuwi/dev (build tools, linting, CI scripts)
- @pezkuwi/dev-test (test runner)
- @pezkuwi/dev-ts (TypeScript build)

Upstream: polkadot-js/dev v0.83.3
This commit is contained in:
2026-01-05 14:22:47 +03:00
commit 8d28b36f9c
135 changed files with 19232 additions and 0 deletions
View File
+32
View File
@@ -0,0 +1,32 @@
# @pezkuwi/dev-test
This is a very basic Jest-compatible environment that could be used alongside tests. The need for this came from replacing Jest with `node --test` without rewriting all assertions.
It provides the following -
1. Browser `window`, `document`, `navigator` (see usage for browser-specific path)
2. `jest` functions, specifically `spyOn` (not comprehensive, some will error, some witll noop)
3. `expect` functions (not comprehensive, caters for specific polkadot-js usage)
## Usage
On thing to note is that `node:test` is still rapidly evolving - this includes the APIs and features. As such this requires at least Node 18.14, however 18.15+ is recommended.
The entry points are different based on the environment you would like to operate in. For a browser-like environment,
```
node --require @pezkuwi/dev-test/browser ...
```
or for a basic describe/expect/jest-only global environment
```
node --require @pezkuwi/dev-test/node ...
```
The `...` above indicates any additional Node options, for instance a full command could be -
```
node --require @pezkuwi/dev-test/node --test something.test.js
```
+30
View File
@@ -0,0 +1,30 @@
{
"author": "Jaco Greeff <jacogr@gmail.com>",
"bugs": "https://github.com/pezkuwi/dev/issues",
"description": "A basic test-functions-as-global library on top of node:test",
"engines": {
"node": ">=18.14"
},
"homepage": "https://github.com/pezkuwi/dev/tree/master/packages/dev-test#readme",
"license": "Apache-2.0",
"name": "@pezkuwi/dev-test",
"repository": {
"directory": "packages/dev-test",
"type": "git",
"url": "https://github.com/pezkuwi/dev.git"
},
"sideEffects": false,
"type": "module",
"version": "0.84.2",
"main": "./index.js",
"exports": {
"./globals.d.ts": "./src/globals.d.ts"
},
"dependencies": {
"jsdom": "^24.0.0",
"tslib": "^2.7.0"
},
"devDependencies": {
"@types/jsdom": "^21.1.6"
}
}
+6
View File
@@ -0,0 +1,6 @@
// Copyright 2017-2025 @polkadot/dev-test authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { exposeEnv } from './env/index.js';
exposeEnv(true);
+26
View File
@@ -0,0 +1,26 @@
// Copyright 2017-2025 @polkadot/dev-test authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { browser } from './browser.js';
const all = browser();
describe('browser', () => {
it('contains window', () => {
expect(all.window).toBeDefined();
});
it('contains a crypto implementation', () => {
expect(all.crypto).toBeTruthy();
expect(typeof all.crypto.getRandomValues).toBe('function');
});
it('contains the top-level objects', () => {
expect(all.document).toBeDefined();
expect(all.navigator).toBeDefined();
});
it('contains HTML*Element', () => {
expect(typeof all.HTMLElement).toBe('function');
});
});
+100
View File
@@ -0,0 +1,100 @@
// Copyright 2017-2025 @polkadot/dev-test authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { JSDOM } from 'jsdom';
/**
* Export a very basic JSDom environment - this is just enough so we have
* @testing-environment/react tests passing in this repo
*
* FIXME: This approach is actually _explicitly_ discouraged by JSDOM - when
* using window you should run the tests inside that context, instead of just
* blindly relying on the globals as we do here
*/
export function browser () {
const { window } = new JSDOM('', { url: 'http://localhost' });
return {
// All HTML Elements that are defined on the JSDOM window object.
// (we copied as-is from the types definition). We cannot get this
// via Object.keys(window).filter(...) so we have to specify explicitly
HTMLAnchorElement: window.HTMLAnchorElement,
HTMLAreaElement: window.HTMLAreaElement,
HTMLAudioElement: window.HTMLAudioElement,
HTMLBRElement: window.HTMLBRElement,
HTMLBaseElement: window.HTMLBaseElement,
HTMLBodyElement: window.HTMLBodyElement,
HTMLButtonElement: window.HTMLButtonElement,
HTMLCanvasElement: window.HTMLCanvasElement,
HTMLDListElement: window.HTMLDListElement,
HTMLDataElement: window.HTMLDataElement,
HTMLDataListElement: window.HTMLDataListElement,
HTMLDetailsElement: window.HTMLDetailsElement,
HTMLDialogElement: window.HTMLDialogElement,
HTMLDirectoryElement: window.HTMLDirectoryElement,
HTMLDivElement: window.HTMLDivElement,
HTMLElement: window.HTMLElement,
HTMLEmbedElement: window.HTMLEmbedElement,
HTMLFieldSetElement: window.HTMLFieldSetElement,
HTMLFontElement: window.HTMLFontElement,
HTMLFormElement: window.HTMLFormElement,
HTMLFrameElement: window.HTMLFrameElement,
HTMLFrameSetElement: window.HTMLFrameSetElement,
HTMLHRElement: window.HTMLHRElement,
HTMLHeadElement: window.HTMLHeadElement,
HTMLHeadingElement: window.HTMLHeadingElement,
HTMLHtmlElement: window.HTMLHtmlElement,
HTMLIFrameElement: window.HTMLIFrameElement,
HTMLImageElement: window.HTMLImageElement,
HTMLInputElement: window.HTMLInputElement,
HTMLLIElement: window.HTMLLIElement,
HTMLLabelElement: window.HTMLLabelElement,
HTMLLegendElement: window.HTMLLegendElement,
HTMLLinkElement: window.HTMLLinkElement,
HTMLMapElement: window.HTMLMapElement,
HTMLMarqueeElement: window.HTMLMarqueeElement,
HTMLMediaElement: window.HTMLMediaElement,
HTMLMenuElement: window.HTMLMenuElement,
HTMLMetaElement: window.HTMLMetaElement,
HTMLMeterElement: window.HTMLMeterElement,
HTMLModElement: window.HTMLModElement,
HTMLOListElement: window.HTMLOListElement,
HTMLObjectElement: window.HTMLObjectElement,
HTMLOptGroupElement: window.HTMLOptGroupElement,
HTMLOptionElement: window.HTMLOptionElement,
HTMLOutputElement: window.HTMLOutputElement,
HTMLParagraphElement: window.HTMLParagraphElement,
HTMLParamElement: window.HTMLParamElement,
HTMLPictureElement: window.HTMLPictureElement,
HTMLPreElement: window.HTMLPreElement,
HTMLProgressElement: window.HTMLProgressElement,
HTMLQuoteElement: window.HTMLQuoteElement,
HTMLScriptElement: window.HTMLScriptElement,
HTMLSelectElement: window.HTMLSelectElement,
HTMLSlotElement: window.HTMLSlotElement,
HTMLSourceElement: window.HTMLSourceElement,
HTMLSpanElement: window.HTMLSpanElement,
HTMLStyleElement: window.HTMLStyleElement,
HTMLTableCaptionElement: window.HTMLTableCaptionElement,
HTMLTableCellElement: window.HTMLTableCellElement,
HTMLTableColElement: window.HTMLTableColElement,
HTMLTableElement: window.HTMLTableElement,
HTMLTableRowElement: window.HTMLTableRowElement,
HTMLTableSectionElement: window.HTMLTableSectionElement,
HTMLTemplateElement: window.HTMLTemplateElement,
HTMLTextAreaElement: window.HTMLTextAreaElement,
HTMLTimeElement: window.HTMLTimeElement,
HTMLTitleElement: window.HTMLTitleElement,
HTMLTrackElement: window.HTMLTrackElement,
HTMLUListElement: window.HTMLUListElement,
HTMLUnknownElement: window.HTMLUnknownElement,
HTMLVideoElement: window.HTMLVideoElement,
// normal service resumes, the base top-level names
crypto: window.crypto,
document: window.document,
localStorage: window.localStorage,
navigator: window.navigator,
// window...
window
};
}
+222
View File
@@ -0,0 +1,222 @@
// Copyright 2017-2025 @polkadot/dev-test authors & contributors
// SPDX-License-Identifier: Apache-2.0
describe('expect', () => {
it('has been decorated', () => {
expect(expect(true).not).toBeDefined();
});
it('throws on unimplemented', () => {
expect(
() => expect(true).not.toHaveReturnedWith()
).toThrow('expect(...).not.toHaveReturnedWith has not been implemented');
});
it('throws on unimplemented (with alternative)', () => {
expect(
() => expect(true).not.toBeFalsy()
).toThrow('expect(...).not.toBeFalsy has not been implemented (Use expect(...).toBeTruthy instead)');
});
describe('rejects', () => {
it('matches a rejection via .toThrow', async () => {
await expect(
Promise.reject(new Error('this is a rejection message'))
).rejects.toThrow(/rejection/);
});
});
describe('.toBeDefined', () => {
it('does not throw on null', () => {
expect(null).toBeDefined();
});
it('throws on undefined', () => {
expect(
() => expect(undefined).toBeDefined()
).toThrow();
});
it('.not does not throw on undefined', () => {
expect(undefined).not.toBeDefined();
});
});
describe('.toThrow', () => {
const thrower = () => {
throw new Error('some error');
};
it('matches error with empty throw', () => {
expect(thrower).toThrow();
});
it('matches error with exact message', () => {
expect(thrower).toThrow('some error');
});
it('matches error with regex message', () => {
expect(thrower).toThrow(/me er/);
});
it('handles .not correctly (no throw, empty message)', () => {
expect(() => undefined).not.toThrow();
});
it('handles .not correctly (no throw, regex match)', () => {
expect(() => undefined).not.toThrow(/me er/);
});
it('handles .not correctly (throw, string match)', () => {
expect(() => undefined).not.toThrow('no match');
});
it('handles .not correctly (throw, regex match)', () => {
expect(() => undefined).not.toThrow(/no match/);
});
});
describe('.toMatch', () => {
it('fails matching when non-object passed in', () => {
expect(
() => expect(undefined).toMatch(/match/)
).toThrow(/Expected string/);
});
it('fails matching when non-matching string passed in', () => {
expect(
() => expect('some').toMatch(/match/)
).toThrow(/did not match/);
});
it('matches string passed', () => {
expect(
() => expect('matching').toMatch(/match/)
).not.toThrow();
});
});
describe('.toMatchObject', () => {
it('fails matching when non-object passed in', () => {
expect(
() => expect(undefined).toMatchObject({ foo: 'bar' })
).toThrow(/Expected object/);
});
it('matches empty object', () => {
expect({
a: 'foo',
b: 'bar'
}).toMatchObject({});
});
it('matches object with some fields', () => {
expect({
a: 'foo',
b: 'bar',
c: 123,
d: [456, 789]
}).toMatchObject({
a: 'foo',
c: 123,
d: [456, 789]
});
});
it('matches an object with some expect.stringMatching supplied', () => {
expect({
a: 'foo bar',
b: 'baz',
c: 'zaz'
}).toMatchObject({
a: expect.stringMatching(/o b/),
b: expect.stringMatching('baz'),
c: 'zaz'
});
});
it('matches an object with expect.any supplied', () => {
expect({
a: 123,
b: Boolean(true),
c: 'foo'
}).toMatchObject({
a: expect.any(Number),
b: expect.any(Boolean),
c: 'foo'
});
});
it('does not match an object with non instance value for expect.any', () => {
expect(
() => expect({
a: true,
b: 'foo'
}).toMatchObject({
a: expect.any(Number),
b: 'foo'
})
).toThrow(/not an instance of Number/);
});
it('matches an object with expect.anything supplied', () => {
expect({
a: 123,
b: 'foo'
}).toMatchObject({
a: expect.anything(),
b: 'foo'
});
});
it('does not match an object with undefined value for expect.anything', () => {
expect(
() => expect({
b: 'foo'
}).toMatchObject({
a: expect.anything(),
b: 'foo'
})
).toThrow(/non-nullish/);
});
it('does not match an object with non-array value', () => {
expect(
() => expect({
a: 'foo',
b: 'bar'
}).toMatchObject({
a: 'foo',
b: [123, 456]
})
).toThrow(/Expected array value/);
});
it('allows for deep matching', () => {
expect({
a: 123,
b: {
c: 456,
d: {
e: 'foo',
f: 'bar',
g: {
h: [789, { z: 'baz' }]
}
}
}
}).toMatchObject({
a: 123,
b: {
c: expect.any(Number),
d: {
f: 'bar',
g: {
h: [expect.any(Number), { z: 'baz' }]
}
}
}
});
});
});
});
+248
View File
@@ -0,0 +1,248 @@
// Copyright 2017-2025 @polkadot/dev-test authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { AnyFn, WithMock } from '../types.js';
import { strict as assert } from 'node:assert';
import { enhanceObj, stubObj } from '../util.js';
type AssertMatchFn = (value: unknown) => void;
type Mocked = Partial<WithMock<AnyFn>>;
// logged via Object.keys(expect).sort()
const EXPECT_KEYS = ['addEqualityTesters', 'addSnapshotSerializer', 'any', 'anything', 'arrayContaining', 'assertions', 'closeTo', 'extend', 'extractExpectedAssertionsErrors', 'getState', 'hasAssertions', 'not', 'objectContaining', 'setState', 'stringContaining', 'stringMatching', 'toMatchInlineSnapshot', 'toMatchSnapshot', 'toThrowErrorMatchingInlineSnapshot', 'toThrowErrorMatchingSnapshot'] as const;
// logged via Object.keys(expect(0)).sort()
const EXPECT_KEYS_FN = ['lastCalledWith', 'lastReturnedWith', 'not', 'nthCalledWith', 'nthReturnedWith', 'rejects', 'resolves', 'toBe', 'toBeCalled', 'toBeCalledTimes', 'toBeCalledWith', 'toBeCloseTo', 'toBeDefined', 'toBeFalsy', 'toBeGreaterThan', 'toBeGreaterThanOrEqual', 'toBeInstanceOf', 'toBeLessThan', 'toBeLessThanOrEqual', 'toBeNaN', 'toBeNull', 'toBeTruthy', 'toBeUndefined', 'toContain', 'toContainEqual', 'toEqual', 'toHaveBeenCalled', 'toHaveBeenCalledTimes', 'toHaveBeenCalledWith', 'toHaveBeenLastCalledWith', 'toHaveBeenNthCalledWith', 'toHaveLastReturnedWith', 'toHaveLength', 'toHaveNthReturnedWith', 'toHaveProperty', 'toHaveReturned', 'toHaveReturnedTimes', 'toHaveReturnedWith', 'toMatch', 'toMatchInlineSnapshot', 'toMatchObject', 'toMatchSnapshot', 'toReturn', 'toReturnTimes', 'toReturnWith', 'toStrictEqual', 'toThrow', 'toThrowError', 'toThrowErrorMatchingInlineSnapshot', 'toThrowErrorMatchingSnapshot'] as const;
const stubExpect = stubObj('expect', EXPECT_KEYS);
const stubExpectFn = stubObj('expect(...)', EXPECT_KEYS_FN, {
toThrowError: 'expect(...).toThrow'
});
const stubExpectFnRejects = stubObj('expect(...).rejects', EXPECT_KEYS_FN, {
toThrowError: 'expect(...).rejects.toThrow'
});
const stubExpectFnResolves = stubObj('expect(...).resolves', EXPECT_KEYS_FN);
const stubExpectFnNot = stubObj('expect(...).not', EXPECT_KEYS_FN, {
toBeFalsy: 'expect(...).toBeTruthy',
toBeTruthy: 'expect(...).toBeFalsy',
toThrowError: 'expect(...).not.toThrow'
});
/**
* @internal
*
* A helper that wraps a matching function in an ExpectMatcher. This is (currently)
* only used/checked for in the calledWith* helpers
*
* TODO We don't use it in polkadot-js, but a useful enhancement could be for
* any of the to* expectations to detect and use those. An example of useful code
* in that case:
*
* ```js
* expect({
* a: 'blah',
* b: 3
* }).toEqual(
* expect.objectContaining({ b: 3 })
* )
* ```
*
* An example of matcher use can be seen in the isCalledWith loops
*/
class Matcher {
assertMatch: AssertMatchFn;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor (assertFn: (value: any, check: any) => void, check?: unknown) {
this.assertMatch = (value) => assertFn(value, check);
}
}
/**
* @internal
*
* Asserts that the input value is non-nullish
*/
function assertNonNullish (value: unknown): void {
assert.ok(value !== null && value !== undefined, `Expected non-nullish value, found ${value as string}`);
}
/**
* @internal
*
* A helper that checks a single call arguments, which may include the
* use of matchers. This is used in finding any call or checking a specific
* call
*/
function assertCallHasArgs (call: { arguments: unknown[] } | undefined, args: unknown[]): void {
assert.ok(call && args.length === call.arguments?.length, 'Number of arguments does not match');
args.forEach((arg, i) => assertMatch(call.arguments[i], arg));
}
/**
* @internal
*
* A helper that checks for the first instance of a match on the actual call
* arguments (this extracts the toHaveBeenCalledWith logic)
*/
function assertSomeCallHasArgs (value: Mocked | undefined, args: unknown[]) {
assert.ok(value?.mock?.calls.some((call) => {
try {
assertCallHasArgs(call, args);
return true;
} catch {
return false;
}
}), 'No call found matching arguments');
}
/**
* @internal
*
* Asserts that the value is either (equal deep) or matches the matcher (if supplied)
*/
function assertMatch (value: unknown, check: unknown): void {
check instanceof Matcher
? check.assertMatch(value)
: Array.isArray(check)
? assertMatchArr(value, check)
: check && typeof check === 'object'
? assertMatchObj(value, check)
: assert.deepStrictEqual(value, check);
}
/**
* @internal
*
* A helper to match the supplied array check against the resulting array
*
* @param {unknown} value
* @param {unknown[]} check
*/
function assertMatchArr (value: unknown, check: unknown[]): void {
assert.ok(value && Array.isArray(value), `Expected array value, found ${typeof value}`);
assert.ok(value.length === check.length, `Expected array with ${check.length} entries, found ${value.length}`);
check.forEach((other, i) => assertMatch(value[i], other));
}
/**
* @internal
*
* A helper to match the supplied fields against the resulting object
*/
function assertMatchObj (value: unknown, check: object): void {
assert.ok(value && typeof value === 'object', `Expected object value, found ${typeof value}`);
Object
.entries(check)
.forEach(([key, other]) => assertMatch((value as Record<string, unknown>)[key], other));
}
/**
* @internal
*
* A helper to match a string value against another string or regex
*/
function assertMatchStr (value: unknown, check: string | RegExp): void {
assert.ok(typeof value === 'string', `Expected string value, found ${typeof value}`);
typeof check === 'string'
? assert.strictEqual(value, check)
: assert.match(value, check);
}
/**
* @internal
*
* A helper to check the type of a specific value as used in the expect.any(Clazz) matcher
*
* @see https://github.com/facebook/jest/blob/a49c88610e49a3242576160740a32a2fe11161e1/packages/expect/src/asymmetricMatchers.ts#L103-L133
*/
// eslint-disable-next-line @typescript-eslint/ban-types
function assertInstanceOf (value: unknown, Clazz: Function): void {
assert.ok(
(Clazz === Array && Array.isArray(value)) ||
(Clazz === BigInt && typeof value === 'bigint') ||
(Clazz === Boolean && typeof value === 'boolean') ||
(Clazz === Function && typeof value === 'function') ||
(Clazz === Number && typeof value === 'number') ||
(Clazz === Object && typeof value === 'object') ||
(Clazz === String && typeof value === 'string') ||
(Clazz === Symbol && typeof value === 'symbol') ||
(value instanceof Clazz),
`${value as string} is not an instance of ${Clazz.name}`
);
}
/**
* @internal
*
* A helper to ensure that the supplied string/array does include the checker string.
*
* @param {string | unknown[]} value
* @param {string} check
*/
// eslint-disable-next-line @typescript-eslint/ban-types
function assertIncludes (value: string | unknown[], [check, Clazz]: [string, Function]): void {
assertInstanceOf(value, Clazz);
assert.ok(value?.includes(check), `${value as string} does not include ${check}`);
}
/**
* Sets up the shimmed expect(...) function, including all .to* and .not.to*
* functions. This is not comprehensive, rather is contains what we need to
* make all polkadot-js usages pass
**/
export function expect () {
const rootMatchers = {
// eslint-disable-next-line @typescript-eslint/ban-types
any: (Clazz: Function) => new Matcher(assertInstanceOf, Clazz),
anything: () => new Matcher(assertNonNullish),
arrayContaining: (check: string) => new Matcher(assertIncludes, [check, Array]),
objectContaining: (check: object) => new Matcher(assertMatchObj, check),
stringContaining: (check: string) => new Matcher(assertIncludes, [check, String]),
stringMatching: (check: string | RegExp) => new Matcher(assertMatchStr, check)
};
return {
expect: enhanceObj(enhanceObj((value: unknown) =>
enhanceObj({
not: enhanceObj({
toBe: (other: unknown) => assert.notStrictEqual(value, other),
toBeDefined: () => assert.ok(value === undefined),
toBeNull: (value: unknown) => assert.ok(value !== null),
toBeUndefined: () => assert.ok(value !== undefined),
toEqual: (other: unknown) => assert.notDeepEqual(value, other),
toHaveBeenCalled: () => assert.ok(!(value as Mocked | undefined)?.mock?.calls.length),
toThrow: (message?: RegExp | Error | string) => assert.doesNotThrow(value as () => unknown, message && { message } as Error)
}, stubExpectFnNot),
rejects: enhanceObj({
toThrow: (message?: RegExp | Error | string) => assert.rejects(value as Promise<unknown>, message && { message } as Error)
}, stubExpectFnRejects),
resolves: enhanceObj({}, stubExpectFnResolves),
toBe: (other: unknown) => assert.strictEqual(value, other),
toBeDefined: () => assert.ok(value !== undefined),
toBeFalsy: () => assert.ok(!value),
// eslint-disable-next-line @typescript-eslint/ban-types
toBeInstanceOf: (Clazz: Function) => assertInstanceOf(value, Clazz),
toBeNull: (value: unknown) => assert.ok(value === null),
toBeTruthy: () => assert.ok(value),
toBeUndefined: () => assert.ok(value === undefined),
toEqual: (other: unknown) => assert.deepEqual(value, other),
toHaveBeenCalled: () => assert.ok((value as Mocked | undefined)?.mock?.calls.length),
toHaveBeenCalledTimes: (count: number) => assert.equal((value as Mocked | undefined)?.mock?.calls.length, count),
toHaveBeenCalledWith: (...args: unknown[]) => assertSomeCallHasArgs((value as Mocked | undefined), args),
toHaveBeenLastCalledWith: (...args: unknown[]) => assertCallHasArgs((value as Mocked | undefined)?.mock?.calls.at(-1), args),
toHaveLength: (length: number) => assert.equal((value as unknown[] | undefined)?.length, length),
toMatch: (check: string | RegExp) => assertMatchStr(value, check),
toMatchObject: (check: object) => assertMatchObj(value, check),
toThrow: (message?: RegExp | Error | string) => assert.throws(value as () => unknown, message && { message } as Error)
}, stubExpectFn), rootMatchers), stubExpect)
};
}
+21
View File
@@ -0,0 +1,21 @@
// Copyright 2017-2025 @polkadot/dev-test authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { browser } from './browser.js';
import { expect } from './expect.js';
import { jest } from './jest.js';
import { lifecycle } from './lifecycle.js';
import { suite } from './suite.js';
/**
* Exposes the jest-y environment via globals.
*/
export function exposeEnv (isBrowser: boolean): void {
[expect, jest, lifecycle, suite, isBrowser && browser].forEach((env) => {
env && Object
.entries(env())
.forEach(([key, fn]) => {
globalThis[key as 'undefined'] ??= fn;
});
});
}
+168
View File
@@ -0,0 +1,168 @@
// Copyright 2017-2025 @polkadot/dev-test authors & contributors
// SPDX-License-Identifier: Apache-2.0
describe('jest', () => {
it('has been enhanced', () => {
expect(jest.setTimeout).toBeDefined();
});
describe('.fn', () => {
it('works on .toHaveBeenCalled', () => {
const mock = jest.fn(() => 3);
expect(mock).not.toHaveBeenCalled();
expect(mock()).toBe(3);
expect(mock).toHaveBeenCalled();
});
it('works on .toHaveBeenCalledTimes', () => {
const mock = jest.fn(() => 3);
expect(mock()).toBe(3);
expect(mock()).toBe(3);
expect(mock).toHaveBeenCalledTimes(2);
});
it('works with .toHaveBeenCalledWith', () => {
const sum = jest.fn((a: number, b: number) => a + b);
expect(sum(1, 2)).toBe(3);
expect(sum).toHaveBeenCalledWith(1, 2);
expect(sum(2, 3)).toBe(5);
expect(sum(4, 5)).toBe(9);
expect(sum).toHaveBeenCalledWith(1, 2);
expect(sum).toHaveBeenCalledWith(2, 3);
expect(sum).toHaveBeenCalledWith(4, 5);
expect(sum).toHaveBeenLastCalledWith(4, 5);
});
it('works with .toHaveBeenCalledWith & expect.objectContaining', () => {
const test = jest.fn((a: unknown, b: unknown) => !!a && !!b);
test({ a: 123, b: 'test' }, null);
expect(test).toHaveBeenLastCalledWith({ a: 123, b: 'test' }, null);
expect(test).toHaveBeenLastCalledWith(expect.objectContaining({}), null);
expect(test).toHaveBeenLastCalledWith(expect.objectContaining({ a: 123 }), null);
expect(test).toHaveBeenLastCalledWith(expect.objectContaining({ b: 'test' }), null);
});
it('allows .mockImplementation', () => {
const mock = jest.fn(() => 3);
expect(mock()).toBe(3);
mock.mockImplementation(() => 4);
expect(mock()).toBe(4);
expect(mock()).toBe(4);
});
it('allows .mockImplementationOnce', () => {
const mock = jest.fn(() => 3);
expect(mock()).toBe(3);
mock.mockImplementationOnce(() => 4);
expect(mock()).toBe(4);
expect(mock()).toBe(3);
});
it('allows resets', () => {
const mock = jest.fn(() => 3);
expect(mock).not.toHaveBeenCalled();
expect(mock()).toBe(3);
expect(mock).toHaveBeenCalled();
mock.mockReset();
expect(mock).not.toHaveBeenCalled();
expect(mock()).toBe(3);
expect(mock).toHaveBeenCalled();
});
});
describe('.spyOn', () => {
it('works on .toHaveBeenCalled', () => {
const obj = {
add: (a: number, b: number) => a + b
};
const spy = jest.spyOn(obj, 'add');
expect(spy).not.toHaveBeenCalled();
expect(obj.add(1, 2)).toBe(3);
expect(spy).toHaveBeenCalledTimes(1);
});
it('allows .mockImplementation', () => {
const obj = {
add: (a: number, b: number) => a + b
};
const spy = jest.spyOn(obj, 'add');
expect(obj.add(1, 2)).toBe(3);
expect(spy).toHaveBeenCalledTimes(1);
spy.mockImplementation(() => 4);
expect(obj.add(1, 2)).toBe(4);
expect(spy).toHaveBeenCalledTimes(2);
expect(obj.add(1, 2)).toBe(4);
expect(spy).toHaveBeenCalledTimes(3);
});
it('allows .mockImplementationOnce', () => {
const obj = {
add: (a: number, b: number) => a + b
};
const spy = jest.spyOn(obj, 'add');
expect(obj.add(1, 2)).toBe(3);
expect(spy).toHaveBeenCalledTimes(1);
spy.mockImplementationOnce(() => 4);
expect(obj.add(1, 2)).toBe(4);
expect(spy).toHaveBeenCalledTimes(2);
expect(obj.add(1, 2)).toBe(3);
expect(spy).toHaveBeenCalledTimes(3);
});
it('allows resets', () => {
const obj = {
add: (a: number, b: number) => a + b
};
const spy = jest.spyOn(obj, 'add');
expect(spy).not.toHaveBeenCalled();
expect(obj.add(1, 2)).toBe(3);
expect(spy).toHaveBeenCalledTimes(1);
spy.mockReset();
expect(spy).not.toHaveBeenCalled();
expect(obj.add(1, 2)).toBe(3);
expect(spy).toHaveBeenCalledTimes(1);
});
it('allows restores', () => {
const obj = {
add: (a: number, b: number) => a + b
};
const spy = jest.spyOn(obj, 'add');
expect(spy).not.toHaveBeenCalled();
expect(obj.add(1, 2)).toBe(3);
expect(spy).toHaveBeenCalledTimes(1);
spy.mockRestore();
});
});
});
+68
View File
@@ -0,0 +1,68 @@
// Copyright 2017-2025 @polkadot/dev-test authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { AnyFn, WithMock } from '../types.js';
import { mock } from 'node:test';
import { enhanceObj, stubObj, warnObj } from '../util.js';
// logged via Object.keys(jest).sort()
const JEST_KEYS_STUB = ['advanceTimersByTime', 'advanceTimersToNextTimer', 'autoMockOff', 'autoMockOn', 'clearAllMocks', 'clearAllTimers', 'createMockFromModule', 'deepUnmock', 'disableAutomock', 'doMock', 'dontMock', 'enableAutomock', 'fn', 'genMockFromModule', 'getRealSystemTime', 'getSeed', 'getTimerCount', 'isEnvironmentTornDown', 'isMockFunction', 'isolateModules', 'isolateModulesAsync', 'mock', 'mocked', 'now', 'replaceProperty', 'requireActual', 'requireMock', 'resetAllMocks', 'resetModules', 'restoreAllMocks', 'retryTimes', 'runAllImmediates', 'runAllTicks', 'runAllTimers', 'runOnlyPendingTimers', 'setMock', 'setSystemTime', 'setTimeout', 'spyOn', 'unmock', 'unstable_mockModule', 'useFakeTimers', 'useRealTimers'] as const;
const JEST_KEYS_WARN = ['setTimeout'] as const;
// logged via Object.keys(jest.fn()).sort()
const MOCK_KEYS_STUB = ['_isMockFunction', 'getMockImplementation', 'getMockName', 'mock', 'mockClear', 'mockImplementation', 'mockImplementationOnce', 'mockName', 'mockRejectedValue', 'mockRejectedValueOnce', 'mockReset', 'mockResolvedValue', 'mockResolvedValueOnce', 'mockRestore', 'mockReturnThis', 'mockReturnValue', 'mockReturnValueOnce', 'withImplementation'] as const;
const jestStub = stubObj('jest', JEST_KEYS_STUB);
const jestWarn = warnObj('jest', JEST_KEYS_WARN);
const mockStub = stubObj('jest.fn()', MOCK_KEYS_STUB);
/**
* @internal
*
* This adds the mockReset and mockRestore functionality to any
* spy or mock function
**/
function extendMock <F extends AnyFn> (mocked: WithMock<F>) {
// We use the node:test mock here for casting below - however we
// don't want this in any method signature since this is a private
// types export, which could get us in "some" trouble
//
// Effectively the casts below ensure that our WithMock<*> aligns
// on a high-level to what we use via private type...
const spy = (mocked as unknown as ReturnType<typeof mock['fn']>);
return enhanceObj(enhanceObj(mocked, {
mockImplementation: <F extends AnyFn> (fn: F): void => {
spy.mock.mockImplementation(fn);
},
mockImplementationOnce: <F extends AnyFn> (fn: F): void => {
spy.mock.mockImplementationOnce(fn);
},
mockReset: (): void => {
spy.mock.resetCalls();
},
mockRestore: (): void => {
spy.mock.restore();
}
}), mockStub);
}
/**
* Sets up the jest object. This is certainly not extensive, and probably
* not quite meant to be (never say never). Rather this adds the functionality
* that we use in the polkadot-js projects.
**/
export function jest () {
return {
jest: enhanceObj(enhanceObj({
fn: <F extends AnyFn> (fn?: F) => extendMock<F>(mock.fn(fn)),
restoreAllMocks: () => {
mock.reset();
},
spyOn: <F extends AnyFn> (obj: object, key: string) => extendMock<F>(mock.method(obj, key as keyof typeof obj))
}, jestWarn), jestStub)
};
}
+18
View File
@@ -0,0 +1,18 @@
// Copyright 2017-2025 @polkadot/dev-test authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { after, afterEach, before, beforeEach } from 'node:test';
/**
* This ensures that the before/after functions are exposed
**/
export function lifecycle () {
return {
after,
afterAll: after,
afterEach,
before,
beforeAll: before,
beforeEach
};
}
+61
View File
@@ -0,0 +1,61 @@
// Copyright 2017-2025 @polkadot/dev-test authors & contributors
// SPDX-License-Identifier: Apache-2.0
describe('describe()', () => {
// eslint-disable-next-line jest/no-focused-tests
describe.only('.only', () => {
it('runs this one', () => {
expect(true).toBe(true);
});
});
describe('.skip', () => {
// eslint-disable-next-line jest/no-disabled-tests
describe.skip('.only (.skip)', () => {
it('skips inside .only', () => {
expect(true).toBe(true);
throw new Error('FATAL: This should not run');
});
});
});
});
describe('it()', () => {
it('has been enhanced', () => {
expect(it.todo).toBeDefined();
});
it('allows promises', async () => {
expect(await Promise.resolve(true)).toBe(true);
});
describe('.only', () => {
// eslint-disable-next-line jest/no-focused-tests
it.only('runs this test when .only is used', () => {
expect(true).toBe(true);
});
// eslint-disable-next-line jest/no-disabled-tests
it.skip('skips when .skip is used', () => {
expect(true).toBe(true);
throw new Error('FATAL: This should not run');
});
});
describe('.skip', () => {
// eslint-disable-next-line jest/no-disabled-tests
it.skip('skips when .skip is used', () => {
expect(true).toBe(true);
throw new Error('FATAL: This should not run');
});
});
describe('.todo', () => {
it.todo('marks as a todo when .todo is used', () => {
expect(true).toBe(true);
});
});
});
+48
View File
@@ -0,0 +1,48 @@
// Copyright 2017-2025 @polkadot/dev-test authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { describe, it } from 'node:test';
import { enhanceObj } from '../util.js';
interface WrapOpts {
only?: boolean;
skip?: boolean;
todo?: boolean;
}
type WrapFn = (name: string, options: { only?: boolean; skip?: boolean; timeout?: number; todo?: boolean; }, fn: () => void | Promise<void>) => void | Promise<void>;
const MINUTE = 60 * 1000;
/**
* @internal
*
* Wraps either describe or it with relevant .only, .skip, .todo & .each helpers,
* shimming it into a Jest-compatible environment.
*
* @param {} fn
*/
function createWrapper <T extends WrapFn> (fn: T, defaultTimeout: number) {
const wrap = (opts: WrapOpts) => (name: string, exec: () => void | Promise<void>, timeout?: number) => fn(name, { ...opts, timeout: (timeout || defaultTimeout) }, exec) as unknown as void;
// Ensure that we have consistent helpers on the function. These are not consistently
// applied accross all node:test versions, latest has all, so always apply ours.
// Instead of node:test options for e.g. timeout, we provide a Jest-compatible signature
return enhanceObj(wrap({}), {
only: wrap({ only: true }),
skip: wrap({ skip: true }),
todo: wrap({ todo: true })
});
}
/**
* This ensures that the describe and it functions match our actual usages.
* This includes .only, .skip and .todo helpers (.each is not applied)
**/
export function suite () {
return {
describe: createWrapper(describe, 60 * MINUTE),
it: createWrapper(it, 2 * MINUTE)
};
}
+4
View File
@@ -0,0 +1,4 @@
// Copyright 2017-2025 @polkadot/dev-test authors & contributors
// SPDX-License-Identifier: Apache-2.0
module.exports = {};
+32
View File
@@ -0,0 +1,32 @@
// Copyright 2017-2025 @polkadot/dev-test authors & contributors
// SPDX-License-Identifier: Apache-2.0
/* eslint-disable no-var */
import type { expect } from './env/expect.js';
import type { jest } from './env/jest.js';
import type { lifecycle } from './env/lifecycle.js';
import type { suite } from './env/suite.js';
type Expect = ReturnType<typeof expect>;
type Jest = ReturnType<typeof jest>;
type Lifecycle = ReturnType<typeof lifecycle>;
type Suite = ReturnType<typeof suite>;
declare global {
var after: Lifecycle['after'];
var afterAll: Lifecycle['afterAll'];
var afterEach: Lifecycle['afterEach'];
var before: Lifecycle['before'];
var beforeAll: Lifecycle['beforeAll'];
var beforeEach: Lifecycle['beforeEach'];
var describe: Suite['describe'];
var expect: Expect['expect'];
var it: Suite['it'];
var jest: Jest['jest'];
}
export {};
+4
View File
@@ -0,0 +1,4 @@
// Copyright 2017-2025 @polkadot/dev-test authors & contributors
// SPDX-License-Identifier: Apache-2.0
throw new Error('Use node --require @polkadot/dev-test/{node, browser} depending on the required environment');
+6
View File
@@ -0,0 +1,6 @@
// Copyright 2017-2025 @polkadot/dev-test authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { exposeEnv } from './env/index.js';
exposeEnv(false);
+6
View File
@@ -0,0 +1,6 @@
// Copyright 2017-2025 @polkadot/dev-test authors & contributors
// SPDX-License-Identifier: Apache-2.0
// Do not edit, auto-generated by @polkadot/dev
export const packageInfo = { name: '@polkadot/dev-test', path: 'auto', type: 'auto', version: '0.84.2' };
+28
View File
@@ -0,0 +1,28 @@
// Copyright 2017-2025 @polkadot/dev-test authors & contributors
// SPDX-License-Identifier: Apache-2.0
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyFn = (...args: any[]) => any;
export type BaseObj = Record<string, unknown>;
// eslint-disable-next-line @typescript-eslint/ban-types
export type BaseFn = Function;
export type StubFn = (...args: unknown[]) => unknown;
// These basically needs to align with the ReturnType<typeof node:test:mock['fn']>
// functions at least for the functionality that we are using: accessing calls &
// managing the mock interface with resets and restores
export type WithMock<F extends AnyFn> = F & {
mock: {
calls: {
arguments: unknown[];
}[];
mockImplementation: (fn: AnyFn) => void;
mockImplementationOnce: (fn: AnyFn) => void;
resetCalls: () => void;
restore: () => void;
}
}
+74
View File
@@ -0,0 +1,74 @@
// Copyright 2017-2025 @polkadot/dev-test authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { enhanceObj, stubObj, warnObj } from './util.js';
describe('enhanceObj', () => {
it('extends objects with non-existing values', () => {
const test = enhanceObj(
enhanceObj(
{ a: () => 1 },
{ b: () => 2 }
),
{ c: () => 3 }
);
expect(test.a()).toBe(1);
expect(test.b()).toBe(2);
expect(test.c()).toBe(3);
});
it('does not override existing values', () => {
const test = enhanceObj(
enhanceObj(
{ a: 0, b: () => 1 },
{ a: () => 0, b: () => 2 }
),
{ c: () => 2 }
);
expect(test.a).toBe(0);
expect(test.b()).toBe(1);
expect(test.c()).toBe(2);
});
});
describe('stubObj', () => {
it('has entries throwing for unimplemented values', () => {
const test = stubObj('obj', ['a', 'b'] as const);
expect(
() => test.b()
).toThrow('obj.b has not been implemented');
});
it('has entries throwing for unimplemented values (w/ alternatives)', () => {
const test = stubObj('obj', ['a', 'b'] as const, { b: 'obj.a' });
expect(
() => test.b()
).toThrow('obj.b has not been implemented (Use obj.a instead)');
});
});
describe('warnObj', () => {
let spy: ReturnType<typeof jest.spyOn>;
beforeEach(() => {
spy = jest.spyOn(console, 'warn');
});
afterEach(() => {
spy.mockRestore();
});
it('has entries warning on unimplemented', () => {
const test = warnObj('obj', ['a', 'b'] as const);
test.b();
expect(spy).toHaveBeenCalledWith('obj.b has been implemented as a noop');
});
});
+57
View File
@@ -0,0 +1,57 @@
// Copyright 2017-2025 @polkadot/dev-test authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BaseFn, BaseObj, StubFn } from './types.js';
/**
* Extends an existing object with the additional function if they
* are not already existing.
*/
export function enhanceObj <T extends BaseObj | BaseFn, X> (obj: T, extra: X) {
Object
.entries(extra as Record<string, unknown>)
.forEach(([key, value]) => {
(obj as Record<string, unknown>)[key] ??= value;
});
return obj as T & Omit<X, keyof T>;
}
/**
* @internal
*
* A helper to create a stub object based wite the stub creator supplied
*/
function createStub <N extends readonly string[]> (keys: N, creator: (key: string) => StubFn) {
return keys.reduce<Record<string, StubFn>>((obj, key) => {
obj[key] ??= creator(key);
return obj;
}, {}) as unknown as { [K in N[number]]: StubFn };
}
/**
* Extends a given object with the named functions if they do not
* already exist on the object.
*
* @type {StubObjFn}
*/
export function stubObj <N extends readonly string[]> (objName: string, keys: N, alts?: Record<string, string>) {
return createStub(keys, (key) => () => {
const alt = alts?.[key];
throw new Error(`${objName}.${key} has not been implemented${alt ? ` (Use ${alt} instead)` : ''}`);
});
}
/**
* Extends a given object with the named functions if they do not
* already exist on the object.
*
* @type {StubObjFn}
*/
export function warnObj <N extends readonly string[]> (objName: string, keys: N) {
return createStub(keys, (key) => () => {
console.warn(`${objName}.${key} has been implemented as a noop`);
});
}
+16
View File
@@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": "..",
"outDir": "./build",
"rootDir": "./src"
},
"exclude": [
"**/mod.ts",
"src/**/*.spec.ts"
],
"include": [
"src/**/*"
],
"references": []
}
+16
View File
@@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": "..",
"outDir": "./build",
"rootDir": "./src",
"emitDeclarationOnly": false,
"noEmit": true
},
"include": [
"src/**/*.spec.ts"
],
"references": [
{ "path": "../dev-test/tsconfig.build.json" }
]
}