@mojojs/core
Version:
Real-time web framework
340 lines • 11.8 kB
JavaScript
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