diff --git a/packages/dev-test/package.json b/packages/dev-test/package.json index c836614..339306b 100644 --- a/packages/dev-test/package.json +++ b/packages/dev-test/package.json @@ -15,7 +15,7 @@ }, "sideEffects": false, "type": "module", - "version": "0.86.7", + "version": "0.86.8", "main": "./index.js", "exports": { "./globals.d.ts": "./src/globals.d.ts" diff --git a/packages/dev-test/src/env/browser.spec.ts b/packages/dev-test/src/env/browser.spec.ts index 47f0018..690ff88 100644 --- a/packages/dev-test/src/env/browser.spec.ts +++ b/packages/dev-test/src/env/browser.spec.ts @@ -1,6 +1,9 @@ // Copyright 2017-2026 @pezkuwi/dev-test authors & contributors // SPDX-License-Identifier: Apache-2.0 +import { createHash } from 'node:crypto'; +import { createServer } from 'node:http'; + import { browser } from './browser.js'; const all = browser(); @@ -18,6 +21,7 @@ describe('browser', () => { it('contains the top-level objects', () => { expect(all.document).toBeDefined(); expect(all.navigator).toBeDefined(); + expect(location.href).toBe('http://localhost/'); }); it('contains HTML*Element', () => { @@ -61,4 +65,49 @@ describe('browser', () => { expect(() => window.dispatchEvent(new CustomEvent('custom', { detail: 1 }))).not.toThrow(); expect(() => window.dispatchEvent(new Event('plain'))).not.toThrow(); }); + + it('opens a WebSocket and exchanges a message', async () => { + // the smallest server that completes the RFC 6455 handshake and sends one text frame + const server = createServer(); + // an upgraded socket leaves the server's tracking, so it is closed here + const sockets: { destroy: () => void }[] = []; + + server.on('upgrade', (req, socket) => { + sockets.push(socket); + + const accept = createHash('sha1') + .update(`${String(req.headers['sec-websocket-key'])}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`) + .digest('base64'); + + socket.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${accept}\r\n\r\n`); + socket.write(Buffer.from([0x81, 0x02, 0x68, 0x69])); + }); + + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + + const { port } = server.address() as { port: number }; + const ws = new WebSocket(`ws://127.0.0.1:${port}`); + + try { + const received = await new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error('no message within 5s')), 5_000); + + ws.onmessage = (event) => { + clearTimeout(timer); + resolve(String(event.data)); + }; + + ws.onerror = () => { + clearTimeout(timer); + reject(new Error('WebSocket error')); + }; + }); + + expect(received).toBe('hi'); + } finally { + ws.close(); + sockets.forEach((socket) => socket.destroy()); + await new Promise((resolve) => server.close(resolve)); + } + }); }); diff --git a/packages/dev-test/src/env/browser.ts b/packages/dev-test/src/env/browser.ts index fda9a8b..07d079d 100644 --- a/packages/dev-test/src/env/browser.ts +++ b/packages/dev-test/src/env/browser.ts @@ -9,8 +9,13 @@ 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. + * + * WebSocket follows from that: Node's own dispatches its events through + * Node's EventTarget, which rejects the JSDOM Event that replaced Node's, so + * no open, message or error ever reaches a listener and a connection never + * comes up. JSDOM's WebSocket dispatches events from its own realm. */ -export const BROWSER_OVERRIDES: readonly string[] = ['CustomEvent', 'Event']; +export const BROWSER_OVERRIDES: readonly string[] = ['CustomEvent', 'Event', 'WebSocket']; /** * JSDOM does no layout, so no element ever changes size and the callback @@ -62,6 +67,13 @@ function events (window: DOMWindow) { }; } +/** Network, see BROWSER_OVERRIDES for why WebSocket is JSDOM's */ +function network (window: DOMWindow) { + return { + WebSocket: window.WebSocket + }; +} + /** Styles, geometry and the observers */ function layout (window: DOMWindow) { return { @@ -90,6 +102,7 @@ export function browser () { ...nodes(window), ...events(window), ...layout(window), + ...network(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 @@ -168,6 +181,7 @@ export function browser () { crypto: window.crypto, document: window.document, localStorage: window.localStorage, + location: window.location, navigator: window.navigator, // window... window diff --git a/packages/dev-test/src/packageInfo.ts b/packages/dev-test/src/packageInfo.ts index d67254e..5ba5961 100644 --- a/packages/dev-test/src/packageInfo.ts +++ b/packages/dev-test/src/packageInfo.ts @@ -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.7' }; +export const packageInfo = { name: '@pezkuwi/dev-test', path: 'auto', type: 'auto', version: '0.86.8' }; diff --git a/packages/dev-ts/package.json b/packages/dev-ts/package.json index e63fa69..5679996 100644 --- a/packages/dev-ts/package.json +++ b/packages/dev-ts/package.json @@ -15,7 +15,7 @@ }, "sideEffects": false, "type": "module", - "version": "0.86.7", + "version": "0.86.8", "main": "./index.js", "exports": { "./globals.d.ts": "./src/globals.d.ts" diff --git a/packages/dev-ts/src/packageInfo.ts b/packages/dev-ts/src/packageInfo.ts index b6cff06..1f9f3ca 100644 --- a/packages/dev-ts/src/packageInfo.ts +++ b/packages/dev-ts/src/packageInfo.ts @@ -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.7' }; +export const packageInfo = { name: '@pezkuwi/dev-ts', path: 'auto', type: 'auto', version: '0.86.8' }; diff --git a/packages/dev/package.json b/packages/dev/package.json index 6b1d0de..26e898f 100644 --- a/packages/dev/package.json +++ b/packages/dev/package.json @@ -15,7 +15,7 @@ }, "sideEffects": false, "type": "module", - "version": "0.86.7", + "version": "0.86.8", "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.7", - "@pezkuwi/dev-ts": "^0.86.7", + "@pezkuwi/dev-test": "^0.86.8", + "@pezkuwi/dev-ts": "^0.86.8", "@rollup/plugin-alias": "^5.1.1", "@rollup/plugin-commonjs": "^29.0.3", "@rollup/plugin-dynamic-import-vars": "^2.1.5", diff --git a/packages/dev/scripts/pezkuwi-dev-build-ts.mjs b/packages/dev/scripts/pezkuwi-dev-build-ts.mjs index c1a1ff6..428494a 100755 --- a/packages/dev/scripts/pezkuwi-dev-build-ts.mjs +++ b/packages/dev/scripts/pezkuwi-dev-build-ts.mjs @@ -4,6 +4,7 @@ import JSON5 from 'json5'; import fs from 'node:fs'; +import { builtinModules } from 'node:module'; import path from 'node:path'; import process from 'node:process'; import ts from 'typescript'; @@ -32,8 +33,9 @@ const TARGET_TSES = ts.ScriptTarget.ES2022; const TARGET_NODE = '>=18'; const IGNORE_IMPORTS = [ - // node (new-style) - ...['assert', 'child_process', 'crypto', 'fs', 'module', 'os', 'path', 'process', 'readline', 'test', 'url', 'util'].map((m) => `node:${m}`), + // node (new-style): every builtin, none of them is a package.json dependency. + // Some are listed with the prefix already (node:test, node:sqlite). + ...builtinModules.map((m) => m.startsWith('node:') ? m : `node:${m}`), // other '@testing-library/react', 'react', 'react-native', 'styled-components' diff --git a/packages/dev/src/packageInfo.ts b/packages/dev/src/packageInfo.ts index e138bc1..51a0879 100644 --- a/packages/dev/src/packageInfo.ts +++ b/packages/dev/src/packageInfo.ts @@ -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.7' }; +export const packageInfo = { name: '@pezkuwi/dev', path: 'auto', type: 'auto', version: '0.86.8' }; diff --git a/yarn.lock b/yarn.lock index a7720ca..08fd13d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1189,7 +1189,7 @@ __metadata: languageName: node linkType: hard -"@pezkuwi/dev-test@npm:^0.86.7, @pezkuwi/dev-test@workspace:packages/dev-test": +"@pezkuwi/dev-test@npm:^0.86.8, @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.7, @pezkuwi/dev-ts@workspace:packages/dev-ts": +"@pezkuwi/dev-ts@npm:^0.86.8, @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.7" - "@pezkuwi/dev-ts": "npm:^0.86.7" + "@pezkuwi/dev-test": "npm:^0.86.8" + "@pezkuwi/dev-ts": "npm:^0.86.8" "@rollup/plugin-alias": "npm:^5.1.1" "@rollup/plugin-commonjs": "npm:^29.0.3" "@rollup/plugin-dynamic-import-vars": "npm:^2.1.5"