Merge pull request #20 from pezkuwichain/feat/test-module-mocks

run-test: module mocks flag; dev-test: fuller browser env, toContain; release 0.86.6
This commit is contained in:
SatoshiQaziMuhammed authored and GitHub committed 2026-10-10 11:05:59 +04:00
commit c83f0adb5b
16 files changed
+216 -15

No files matched your search

+1 -1
View File
@@ -30,7 +30,7 @@
"docs": "pezkuwi-dev-build-docs",
"lint": "pezkuwi-dev-run-lint",
"postinstall": "./packages/dev/scripts/pezkuwi-dev-yarn-only.mjs",
"test": "yarn build && pezkuwi-dev-run-test --dev-build --env browser"
"test": "yarn build && pezkuwi-dev-run-test --dev-build --experimental-test-module-mocks --env browser"
},
"devDependencies": {
"@pezkuwi/dev": "workspace:packages/dev",
+1 -1
View File
@@ -15,7 +15,7 @@
},
"sideEffects": false,
"type": "module",
"version": "0.86.5",
"version": "0.86.6",
"main": "./index.js",
"exports": {
"./globals.d.ts": "./src/globals.d.ts"
+38
View File
@@ -23,4 +23,42 @@ describe('browser', () => {
it('contains HTML*Element', () => {
expect(typeof all.HTMLElement).toBe('function');
});
it('contains the DOM node hierarchy', () => {
const div = document.createElement('div');
expect(div).toBeInstanceOf(Element);
expect(div).toBeInstanceOf(Node);
expect(document.createTextNode('text')).toBeInstanceOf(Text);
expect(document.createDocumentFragment()).toBeInstanceOf(DocumentFragment);
expect(document.createElementNS('http://www.w3.org/2000/svg', 'svg')).toBeInstanceOf(SVGElement);
expect(div.attachShadow({ mode: 'open' })).toBeInstanceOf(ShadowRoot);
expect(document).toBeInstanceOf(Document);
});
it('contains the UI event constructors', () => {
const div = document.createElement('div');
expect(() => div.dispatchEvent(new MouseEvent('click'))).not.toThrow();
expect(() => div.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }))).not.toThrow();
expect(new FocusEvent('focus')).toBeInstanceOf(UIEvent);
});
it('contains getComputedStyle and the observers', () => {
const div = document.createElement('div');
div.style.display = 'none';
document.body.appendChild(div);
expect(getComputedStyle(div).display).toBe('none');
expect(() => new MutationObserver(() => undefined).observe(div, { childList: true })).not.toThrow();
expect(() => new ResizeObserver(() => undefined).observe(div)).not.toThrow();
div.remove();
});
it('dispatches the global CustomEvent and Event on window', () => {
expect(() => window.dispatchEvent(new CustomEvent('custom', { detail: 1 }))).not.toThrow();
expect(() => window.dispatchEvent(new Event('plain'))).not.toThrow();
});
});
+75
View File
@@ -1,8 +1,80 @@
// Copyright 2017-2026 @pezkuwi/dev-test authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DOMWindow } from 'jsdom';
import { JSDOM } from 'jsdom';
/**
* Constructors that Node defines as well. A DOM node only accepts an event
* created in its own realm, so in the browser environment the JSDOM ones
* replace Node's, as they do under jest-environment-jsdom.
*/
export const BROWSER_OVERRIDES: readonly string[] = ['CustomEvent', 'Event'];
/**
* JSDOM does no layout, so no element ever changes size and the callback
* never runs. JSDOM has no ResizeObserver of its own.
*/
class ResizeObserver {
observe (): void {
// nothing is ever resized
}
unobserve (): void {
// nothing is being watched
}
disconnect (): void {
// nothing is being watched
}
}
/** The DOM node hierarchy, which libraries check with instanceof */
function nodes (window: DOMWindow) {
return {
CharacterData: window.CharacterData,
Comment: window.Comment,
Document: window.Document,
DocumentFragment: window.DocumentFragment,
Element: window.Element,
Node: window.Node,
NodeList: window.NodeList,
SVGElement: window.SVGElement,
ShadowRoot: window.ShadowRoot,
Text: window.Text
};
}
/** Event constructors. Node has its own Event and CustomEvent, see BROWSER_OVERRIDES */
function events (window: DOMWindow) {
return {
CompositionEvent: window.CompositionEvent,
CustomEvent: window.CustomEvent,
Event: window.Event,
FocusEvent: window.FocusEvent,
InputEvent: window.InputEvent,
KeyboardEvent: window.KeyboardEvent,
MouseEvent: window.MouseEvent,
TouchEvent: window.TouchEvent,
UIEvent: window.UIEvent,
WheelEvent: window.WheelEvent
};
}
/** Styles, geometry and the observers */
function layout (window: DOMWindow) {
return {
// present on the JSDOM window, absent from its DOMWindow type
DOMRect: window['DOMRect'] as typeof DOMRect,
DOMRectReadOnly: window['DOMRectReadOnly'] as typeof DOMRectReadOnly,
MutationObserver: window.MutationObserver,
MutationRecord: window.MutationRecord,
ResizeObserver,
getComputedStyle: window.getComputedStyle.bind(window)
};
}
/**
* Export a very basic JSDom environment - this is just enough so we have
* @testing-environment/react tests passing in this repo
@@ -15,6 +87,9 @@ export function browser () {
const { window } = new JSDOM('', { url: 'http://localhost' });
return {
...nodes(window),
...events(window),
...layout(window),
// 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
+24
View File
@@ -255,4 +255,28 @@ describe('expect', () => {
expect(() => expect([1, 2]).not.toEqual(expect.arrayContaining([2]))).toThrow();
});
});
describe('.toContain', () => {
it('finds a substring in a string', () => {
expect('voter-alice').toContain('alice');
expect(() => expect('voter-alice').toContain('bob')).toThrow();
});
it('finds the item itself in an array or other iterable', () => {
const item = { a: 1 };
expect([1, item]).toContain(item);
expect(new Set(['a'])).toContain('a');
expect(() => expect([{ a: 1 }]).toContain({ a: 1 })).toThrow();
});
it('rejects a value that is neither a string nor an iterable', () => {
expect(() => expect(123).toContain(1)).toThrow();
});
it('supports .not', () => {
expect('voter-alice').not.toContain('bob');
expect(() => expect([1, 2]).not.toContain(2)).toThrow();
});
});
});
+36
View File
@@ -144,6 +144,40 @@ function assertMatchObj (value: unknown, check: object): void {
.forEach(([key, other]) => assertMatch((value as Record<string, unknown>)[key], other));
}
/**
* @internal
*
* Whether a string contains the substring, or another iterable the item
*/
function contains (value: unknown, item: unknown): boolean {
if (typeof value === 'string') {
return typeof item === 'string' && value.includes(item);
}
assert.ok(typeof (value as Iterable<unknown> | null | undefined)?.[Symbol.iterator] === 'function', `Expected a string or an iterable, found ${typeof value}`);
return [...(value as Iterable<unknown>)].includes(item);
}
/**
* @internal
*
* A helper for toContain: a string contains the substring, any other
* iterable contains the item itself (not a deep-equal copy)
*/
function assertContains (value: unknown, item: unknown): void {
assert.ok(contains(value, item), `Expected ${String(value)} to contain ${String(item)}`);
}
/**
* @internal
*
* The inverse of assertContains
*/
function assertNotContains (value: unknown, item: unknown): void {
assert.ok(!contains(value, item), `Expected ${String(value)} not to contain ${String(item)}`);
}
/**
* @internal
*
@@ -269,6 +303,7 @@ export function expect () {
toBeDefined: () => assert.ok(value === undefined),
toBeNull: (value: unknown) => assert.ok(value !== null),
toBeUndefined: () => assert.ok(value !== undefined),
toContain: (item: unknown) => assertNotContains(value, item),
toEqual: (other: unknown) => containsMatcher(other)
? assert.throws(() => assertEqualMatch(value, other))
: assert.notDeepEqual(value, other),
@@ -287,6 +322,7 @@ export function expect () {
toBeNull: (value: unknown) => assert.ok(value === null),
toBeTruthy: () => assert.ok(value),
toBeUndefined: () => assert.ok(value === undefined),
toContain: (item: unknown) => assertContains(value, item),
toEqual: (other: unknown) => containsMatcher(other)
? assertEqualMatch(value, other)
: assert.deepEqual(value, other),
+6 -2
View File
@@ -1,7 +1,7 @@
// Copyright 2017-2026 @pezkuwi/dev-test authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { browser } from './browser.js';
import { browser, BROWSER_OVERRIDES } from './browser.js';
import { expect } from './expect.js';
import { jest } from './jest.js';
import { lifecycle } from './lifecycle.js';
@@ -15,7 +15,11 @@ export function exposeEnv (isBrowser: boolean): void {
env && Object
.entries(env())
.forEach(([key, fn]) => {
globalThis[key as 'undefined'] ??= fn;
if (isBrowser && BROWSER_OVERRIDES.includes(key)) {
(globalThis as Record<string, unknown>)[key] = fn;
} else {
globalThis[key as 'undefined'] ??= fn;
}
});
});
}
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2017-2026 @pezkuwi/dev-test authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { mock } from 'node:test';
describe('mock.module', () => {
it('replaces a module loaded after the mock is registered', async () => {
mock.module('node:os', { namedExports: { platform: () => 'mocked' } });
const { platform } = await import('node:os');
expect(platform()).toBe('mocked');
});
});
+1 -1
View File
@@ -3,4 +3,4 @@
// Do not edit, auto-generated by @pezkuwi/dev
export const packageInfo = { name: '@pezkuwi/dev-test', path: 'auto', type: 'auto', version: '0.86.5' };
export const packageInfo = { name: '@pezkuwi/dev-test', path: 'auto', type: 'auto', version: '0.86.6' };
+1 -1
View File
@@ -15,7 +15,7 @@
},
"sideEffects": false,
"type": "module",
"version": "0.86.5",
"version": "0.86.6",
"main": "./index.js",
"exports": {
"./globals.d.ts": "./src/globals.d.ts"
+1 -1
View File
@@ -3,4 +3,4 @@
// Do not edit, auto-generated by @pezkuwi/dev
export const packageInfo = { name: '@pezkuwi/dev-ts', path: 'auto', type: 'auto', version: '0.86.5' };
export const packageInfo = { name: '@pezkuwi/dev-ts', path: 'auto', type: 'auto', version: '0.86.6' };
+5
View File
@@ -252,6 +252,11 @@ This script runs test files in the repository, filtering by file extensions and
- **`--logfile <file>`**:
Specifies a log file to capture test output.
- **`--experimental-test-module-mocks`**:
Passed to Node.js, enabling `mock.module` from `node:test`. Node only swaps a module that has not
been loaded yet, so a spec registers its mocks first and then loads the code under test with a
dynamic `import()`.
- **`--import <module>`**:
Imports the specified module.
+3 -3
View File
@@ -15,7 +15,7 @@
},
"sideEffects": false,
"type": "module",
"version": "0.86.5",
"version": "0.86.6",
"bin": {
"pezkuwi-ci-ghact-build": "./scripts/pezkuwi-ci-ghact-build.mjs",
"pezkuwi-ci-ghact-docs": "./scripts/pezkuwi-ci-ghact-docs.mjs",
@@ -52,8 +52,8 @@
"dependencies": {
"@eslint/compat": "^2.1.1",
"@eslint/js": "^10.0.1",
"@pezkuwi/dev-test": "^0.86.5",
"@pezkuwi/dev-ts": "^0.86.5",
"@pezkuwi/dev-test": "^0.86.6",
"@pezkuwi/dev-ts": "^0.86.6",
"@rollup/plugin-alias": "^5.1.1",
"@rollup/plugin-commonjs": "^29.0.3",
"@rollup/plugin-dynamic-import-vars": "^2.1.5",
@@ -58,6 +58,11 @@ for (let i = 0; i < args.length; i++) {
cmd.push(args[++i]);
break;
// node flags with no params
case '--experimental-test-module-mocks':
nodeFlags.push(args[i]);
break;
// node flags that could have additional params
case '--import':
case '--loader':
+1 -1
View File
@@ -3,4 +3,4 @@
// Do not edit, auto-generated by @pezkuwi/dev
export const packageInfo = { name: '@pezkuwi/dev', path: 'auto', type: 'auto', version: '0.86.5' };
export const packageInfo = { name: '@pezkuwi/dev', path: 'auto', type: 'auto', version: '0.86.6' };
+4 -4
View File
@@ -1189,7 +1189,7 @@ __metadata:
languageName: node
linkType: hard
"@pezkuwi/dev-test@npm:^0.86.5, @pezkuwi/dev-test@workspace:packages/dev-test":
"@pezkuwi/dev-test@npm:^0.86.6, @pezkuwi/dev-test@workspace:packages/dev-test":
version: 0.0.0-use.local
resolution: "@pezkuwi/dev-test@workspace:packages/dev-test"
dependencies:
@@ -1205,7 +1205,7 @@ __metadata:
languageName: unknown
linkType: soft
"@pezkuwi/dev-ts@npm:^0.86.5, @pezkuwi/dev-ts@workspace:packages/dev-ts":
"@pezkuwi/dev-ts@npm:^0.86.6, @pezkuwi/dev-ts@workspace:packages/dev-ts":
version: 0.0.0-use.local
resolution: "@pezkuwi/dev-ts@workspace:packages/dev-ts"
dependencies:
@@ -1221,8 +1221,8 @@ __metadata:
dependencies:
"@eslint/compat": "npm:^2.1.1"
"@eslint/js": "npm:^10.0.1"
"@pezkuwi/dev-test": "npm:^0.86.5"
"@pezkuwi/dev-ts": "npm:^0.86.5"
"@pezkuwi/dev-test": "npm:^0.86.6"
"@pezkuwi/dev-ts": "npm:^0.86.6"
"@rollup/plugin-alias": "npm:^5.1.1"
"@rollup/plugin-commonjs": "npm:^29.0.3"
"@rollup/plugin-dynamic-import-vars": "npm:^2.1.5"