UNPKG

@mojojs/core

Version:

Real-time web framework

340 lines 11.8 kB
import assert from 'node:assert/strict'; import { on } from 'node:events'; import { MockUserAgent } from './mock.js'; import DOM from '@mojojs/dom'; import { jsonPointer } from '@mojojs/util'; import yaml from 'js-yaml'; import StackUtils from 'stack-utils'; // Helper function to add required `TestUserAgent` assert methods // that are missing in a `TAP` instance. This is as an intended side effect // to ensure compatibility with `TestUserAgent` class below function addNodeAssertMethods(tapInstance) { tapInstance.strictEqual = tapInstance.equal.bind(tapInstance); tapInstance.notStrictEqual = tapInstance.not.bind(tapInstance); tapInstance.doesNotMatch = tapInstance.notMatch.bind(tapInstance); tapInstance.deepStrictEqual = tapInstance.same.bind(tapInstance); } /** * Test user-agent class. */ export class TestUserAgent extends MockUserAgent { constructor(options = {}) { super(options); /** * Current HTTP response content. */ this.body = Buffer.from(''); this._assert = undefined; this._dom = undefined; this._finished = undefined; this._messages = undefined; this._res = undefined; this._stack = new StackUtils(); this._ws = undefined; if (options.tap !== undefined) this._prepareTap(options.tap); } /** * Delegate assertion to test framework currently in use. */ assert(name, args, msg, skip) { const test = this._assert ?? assert; test[name](...args, msg, { stack: this._stack.captureString(10, skip).replaceAll('file://', '') }); } /** * Check response content for exact match. */ bodyIs(body) { this.assert('strictEqual', [this.body.toString(), body], 'body is equal', this.bodyIs); return this; } /** * Opposite of `bodyIs`. */ bodyIsnt(body) { this.assert('notStrictEqual', [this.body.toString(), body], 'body is not equal', this.bodyIsnt); return this; } /** * Check response content for similar match. */ bodyLike(regex) { this.assert('match', [this.body.toString(), regex], 'body is similar', this.bodyLike); return this; } /** * Opposite of `bodyLike`. */ bodyUnlike(regex) { this.assert('doesNotMatch', [this.body.toString(), regex], 'body is not similar', this.bodyUnlike); return this; } /** * Close WebSocket connection gracefully. */ async closeOk(code, reason) { this.ws.close(code, reason); await this._waitFinished(); this.assert('ok', [true], 'closed WebSocket', this.closeOk); } /** * Wait for WebSocket connection to be closed gracefully and check status. */ async closedOk(code) { await this._waitFinished(); const finished = this._finished == null ? null : this._finished[0]; this.assert('strictEqual', [finished, code], `WebSocket closed with status ${code}`, this.closedOk); } /** * Perform a `DELETE` request and check for transport errors. */ async deleteOk(url, options) { return await this._requestOk(this.deleteOk, 'DELETE', url, options); } /** * Checks for existence of the CSS selectors first matching HTML/XML element. */ elementExists(selector) { const elements = this._html.find(selector); this.assert('ok', [elements.length > 0], `element for selector "${selector}" exists`, this.elementExists); return this; } /** * Opposite of `elementExists`. */ elementExistsNot(selector) { const elements = this._html.find(selector); this.assert('ok', [elements.length === 0], `no element for selector "${selector}"`, this.elementExistsNot); return this; } /** * Perform a `GET` request and check for transport errors. */ async getOk(url, options) { return await this._requestOk(this.getOk, 'GET', url, options); } /** * Perform a `HEAD` request and check for transport errors. */ async headOk(url, options) { return await this._requestOk(this.headOk, 'HEAD', url, options); } /** * Check if response header exists. */ headerExists(name) { this.assert('ok', [this.res.get(name) !== null], `header "${name}" exists`, this.headerExists); return this; } /** * Opposite of `headerExists`. */ headerExistsNot(name) { this.assert('ok', [this.res.get(name) === null], `no "${name}" header`, this.headerExistsNot); return this; } /** * Check response header for exact match. */ headerIs(name, value) { this.assert('strictEqual', [this.res.get(name), value], `${name}: ${value}`, this.headerIs); return this; } /** * Opposite of `headerIs`. */ headerIsnt(name, value) { this.assert('notStrictEqual', [this.res.get(name), value], `not ${name}: ${value}`, this.headerIsnt); return this; } /** * Check response header for similar match. */ headerLike(name, regex) { this.assert('match', [this.res.get(name), regex], `${name} is similar`, this.headerLike); return this; } /** * Check if JSON response contains a value that can be identified using the given JSON Pointer. */ jsonHas(pointer) { const has = jsonPointer(JSON.parse(this.body.toString()), pointer) !== undefined; this.assert('ok', [has], `has value for JSON Pointer "${pointer}" (JSON)`, this.jsonHas); return this; } /** * Check the value extracted from JSON response using the given JSON Pointerr, which defaults to the root value if it * is omitted. */ jsonIs(value, pointer = '') { const expected = jsonPointer(JSON.parse(this.body.toString()), pointer); this.assert('deepStrictEqual', [expected, value], `exact match for JSON Pointer "${pointer}" (JSON)`, this.jsonIs); return this; } /** * Wait for next WebSocket message to arrive. */ async messageOk() { if (this._messages === undefined) throw new Error('No active WebSocket connection'); const message = (await this._messages.next()).value[0]; this.assert('ok', [true], 'message received', this.messageOk); return message; } /** * Create a new test user-agent. */ static async newTestUserAgent(app, options, serverOptions) { app.exceptionFormat = 'txt'; return await new TestUserAgent(options).start(app, serverOptions); } /** * Perform a `OPTIONS` request and check for transport errors. */ async optionsOk(url, options) { return await this._requestOk(this.optionsOk, 'OPTIONS', url, options); } /** * Perform a `PATCH` request and check for transport errors. */ async patchOk(url, options) { return await this._requestOk(this.patchOk, 'PATCH', url, options); } /** * Perform a `POST` request and check for transport errors. */ async postOk(url, options) { return await this._requestOk(this.postOk, 'POST', url, options); } /** * Perform a `PUT` request and check for transport errors. */ async putOk(url, options) { return await this._requestOk(this.putOk, 'PUT', url, options); } /** * Current HTTP response. */ get res() { const res = this._res; if (res === undefined) throw new Error('No active HTTP response'); return res; } /** * Send message or frame via WebSocket. */ async sendOk(message) { await this.ws.send(message); this.assert('ok', [true], 'send message', this.sendOk); } /** * Check response status for exact match. */ statusIs(status) { this.assert('strictEqual', [this.res.statusCode, status], `response status is ${status}`, this.statusIs); return this; } /** * Checks text content of the CSS selectors first matching HTML/XML element for similar match. */ textLike(selector, regex) { this.assert('match', [this._html.at(selector)?.text() ?? '', regex], `similar match for selector "${selector}"`, this.textLike); return this; } /** * Checks text content of the CSS selectors first matching HTML/XML element for no match. */ textUnlike(selector, regex) { this.assert('doesNotMatch', [this._html.at(selector)?.text() ?? '', regex], `no similar match for selector "${selector}"`, this.textLike); return this; } /** * Check response `Content-Type` header for exact match. */ typeIs(value) { this.assert('strictEqual', [this.res.type, value], `Content-Type: ${value}`, this.typeIs); return this; } /** * Check response `Content-Type` header for similar match. */ typeLike(regex) { this.assert('match', [this.res.type, regex], 'Content-Type is similar', this.typeLike); return this; } /** * Open a WebSocket connection with transparent handshake. */ async websocketOk(url, options) { const ws = (this._ws = await this.websocket(url, options)); this._res = ws.handshake ?? undefined; this._finished = null; ws.on('close', (...args) => (this._finished = args)); this._messages = on(this.ws, 'message'); this.assert('ok', [true], `WebSocket handshake with ${url.toString()}`, this.websocketOk); } /** * Active WebSocket connection. */ get ws() { const ws = this._ws; if (ws === undefined) throw new Error('No active WebSocket connection'); return ws; } /** * Check if YAML response contains a value that can be identified using the given JSON Pointer. */ yamlHas(pointer) { const has = jsonPointer(yaml.load(this.body.toString()), pointer) !== undefined; this.assert('ok', [has], `has value for JSON Pointer "${pointer}" (YAML)`, this.jsonHas); return this; } /** * Check the value extracted from YAML response using the given JSON Pointerr, which defaults to the root value if it * is omitted. */ yamlIs(value, pointer = '') { const expected = jsonPointer(yaml.load(this.body.toString()), pointer); this.assert('deepStrictEqual', [expected, value], `exact match for JSON Pointer "${pointer}" (YAML)`, this.yamlIs); return this; } get _html() { if (this._dom === undefined) this._dom = new DOM(this.body.toString()); return this._dom; } _prepareTap(tap) { addNodeAssertMethods(tap); this._assert = tap; const subtests = [tap]; const assert = this._assert; assert.beforeEach(async (t) => { addNodeAssertMethods(t); subtests.push(t); this._assert = t; }); assert.afterEach(async () => { subtests.pop(); this._assert = subtests[subtests.length - 1]; }); } async _requestOk(skip, method, url, options) { this._res = await this.request({ method, url, ...options }); this._dom = undefined; this.body = await this.res.buffer(); this.assert('ok', [true], `${method.toUpperCase()} request for ${url.toString()}`, skip); return this; } async _waitFinished() { if (this._finished != null) return; return await new Promise(resolve => { this.ws.once('close', () => { resolve(); }); }); } } //# sourceMappingURL=test.js.map