#User-Agent
The mojo.js toolkit contains a full featured HTTP and WebSocket user-agent. And while its primary purpose is integration testing of web applications, it can also be used for many other things.
import {UserAgent} from '@mojojs/core';
const ua = new UserAgent();
const res = await ua.get('https://mojolicious.org');
const content = await res.text();
The API is heavily inspired by the Fetch Standard and should feel familar if you've
used fetch
before.
#User-Agent Options
The user-agent can be initialized with a few options, but none of them are required.
const ua = new UserAgent({
// Base URL to be used to resolve all relative request URLs with
baseURL: 'http://127.0.0.1:3000',
// Maximum number of redirects to follow, default to none
maxRedirects: 5,
// Name of user-agent to send with `User-Agent` header
name: 'mojoUA/1.0'
});
#Request Config
Every request is represented by a config object that contains various properties to describe every part of the HTTP request.
const res = await ua.request({
// HTTP method for request
method: 'GET',
// URL of request target as a string or URL object, may be be relative to `ua.baseURL`
url: new URL('https://mojolicious.org'),
// Headers to include in request
headers: {Accept: '*/*', Authorization: 'token 123456789abcdef'},
// Object with key/value pairs to be sent with the query string
query: {fieldA: 'first value', fieldB: 'second value'},
// Request body as a string, `Buffer` or `stream.Readable` object
body: 'Some content to send with request',
// Data structure to be sent in JSON format, or for WebSockets a `true` value to enable JSON mode
json: {hello: ['world']},
// Data structure to be sent in YAML format
yaml: {hello: ['world']},
// Object with key/value pairs to be sent in `application/x-www-form-urlencoded` format
form: {fieldA: 'first value', fieldB: 'second value'},
// Object with key/value pairs and a file upload to be sent in `multipart/form-data` format
formData: {fieldA: 'first value', fieldB: 'second value', fieldC: {content: 'Hello Mojo!', filename: 'test.txt'}},
// Basic authentication
auth: 'user:password',
// Disable TLS certificate validation
insecure: true,
// Override the trusted CA certificates (defaults to CAs curated by Mozilla that ship with Node)
ca: ['...', '...'],
// Server name for the SNI (Server Name Indication) TLS extension
servername: 'localhost',
// Path to UNIX domain socket (UNIX only)
socketPath: '/var/lib/run/myapp.sock',
// An `AbortSignal` to abort the request
signal: controller.signal,
// Alternative `http.Agent` object to use, for keep-alive or SOCKS proxy support with `proxy-agent`
// (this API is likely to change in mojo.js 2.0)
agent: new http.Agent({keepAlive: true})
});
The request
method returns a Promise
that resolves with a response object, right after the response
status line and headers have been received. But before any data from the response body has been read, which can be
handled in a separate step later on. Be aware: The agent API is likely to change in mojo.js 2.0 to improve
compatibility with fetch based backends.
#Request Shortcuts
Since every request includes at least method
and url
values, there are HTTP method specific shortcuts you can use
instead of request
.
const res = await ua.delete('https://mojolicious.org');
const res = await ua.get('https://mojolicious.org');
const res = await ua.head('https://mojolicious.org');
const res = await ua.options('https://mojolicious.org');
const res = await ua.patch('https://mojolicious.org');
const res = await ua.post('https://mojolicious.org');
const res = await ua.put('https://mojolicious.org');
All remaining config values can be passed with a second argument to any one of the shortcut methods.
const res = await ua.post('/search', {form: {q: 'mojo'}});
#Response Headers
Status line information and response headers are available right away with the response object.
// Status code and message
const statusCode = res.statusCode;
const statusMessage = res.statusMessage;
// Headers
const contentType = res.get('Content-Type');
// 2xx
const isSuccess = res.isSuccess;
// 3xx
const isRedirect = res.isRedirect;
// 4xx
const isClientError = res.isClientError;
// 5xx
const isServerError = res.isServerError;
// 4xx or 5xx
const isError = res.isError;
#Response Body
The reponse body can be received in various formats. Most of them will result once again in a new Promise
, resolving
to different results however.
// String
const text = await res.text();
// Buffer
const buffer = await res.buffer();
// Pipe content to `stream.Writable` object
await res.pipe(process.stdout);
// Parsed JSON
const data = await res.json();
// Parsed YAML
const data = await res.yaml();
// Parsed HTML via `@mojojs/dom`
const dom = await res.html();
const title = dom.at('title').text();
// Parsed XML via `@mojojs/dom`
const dom = await res.xml();
// Async iterator
const parts = [];
for await (const chunk of res) {
parts.push(chunk);
}
const buffer = Buffer.concat(parts);
For HTML and XML parsing @mojojs/dom will be used. Making it very easy to extract information from documents with just a CSS selector and almost no code at all.
#Cookies
By default a tough-cookie based cookie jar will be used for state keeping, and you can reconfigure it however you like.
ua.cookieJar.storage.allowSpecialUseDomain = true;
Of course you can also just disable cookies completely.
const ua = new UserAgent({cookieJar: null});
#Timeouts
You can use an AbortController
to make sure a request does not take longer than a certain amount of time. Once
aborted the promise returned by ua.get()
will reject.
const controller = new AbortController();
const signal = controller.signal;
setTimeout(() => controller.abort(), 3000);
const res = await ua.get('https://mojojs.org', {signal});
#WebSockets
For WebSocket handshakes there are also quite a few options available.
const ws = await ua.websocket('wss://mojolicious.org', {
// Headers to include in handshake
headers: {Accept: '*/*', Authorization: 'token 123456789abcdef'},
// Object with key/value pairs to be sent with the query string
query: {fieldA: 'first value', fieldB: 'second value'},
// Enable JSON mode (encoding and decoding all messages automatically)
json: true,
// Basic authentication
auth: 'user:password',
// WebSocket subprotocols
protocols: ['foo', 'bar'],
// Path to UNIX domain socket (UNIX only)
socketPath: '/var/lib/run/myapp.sock'
});
You can choose between multiple API styles.
// Async iterator
const ws = await ua.websocket('/ws');
await ws.send('something');
for await (const message of ws) {
console.log(message);
}
// Events (this API is likely to change in mojo.js 2.0)
const ws = await ua.websocket('/ws');
await ws.send('something');
ws.on('message', message => {
console.log(message);
});
With support for ping
and pong
frames. Be aware: The event based API is likely to change in mojo.js 2.0 to
imporve browser compatibility.
// Handshake with authentication headers (this API is likely to change in mojo.js 2.0)
const ws = await ua.websocket('/ws', {headers: {Authorization: 'token 123456789abcdef'}});
ws.on('ping', data => {
ws.pong(data);
});
Cookies from the cookie jar will of course also be available for the handshake, so you can rely on them for things like authentication.
#Hooks
Hooks can be used to extend the user-agent and run code for every HTTP request or WebSocket handshake.
// Add a header to every HTTP request
ua.addHook('request', async (ua, config) => {
config.headers['X-Bender'] = 'Bite my shiny metal ass!';
});
// Add a query parameter to every WebSocket handshake
ua.addHook('websocket', async (ua, config) => {
config.url.searchParams.append('hello', 'mojo');
});
#Testing
For web application testing there is also a more specialised subclass available that adds various test methods using assert to integrate seamlessly into most testing frameworks.
import {TestUserAgent} from '@mojojs/core';
const ua = new TestUserAgent({baseURL: 'https://mojolicious.org'});
(await ua.getOk('/')).statusIs(200).headerLike('Content-Type', /html/).bodyLike(/Mojolicious/);
tap subtests are also supported, and scope changes can be managed automatically
with the tap
option.
import {TestUserAgent} from '@mojojs/core';
import t from 'tap';
t.test('Mojolicious', async t => {
const ua = new TestUserAgent({baseURL: 'https://mojolicious.org', tap: t});
await t.test('Index', async t => {
(await ua.getOk('/')).statusIs(200).bodyLike(/Mojolicious/);
});
});
And to test mojo.js web applications there is no need to mock anything. The test user-agent can automatically start and manage a web server listening to a random port for you.
import {app} from '../index.js';
import t from 'tap';
t.test('Example application', async t => {
const ua = await app.newTestUserAgent({tap: t});
await t.test('Index', async t => {
(await ua.getOk('/')).statusIs(200).bodyLike(/mojo.js/);
});
await ua.stop();
});
There are test alternatives for all HTTP method shortcuts.
await ua.deleteOk('/foo');
await ua.getOk('/foo', {headers: {Host: 'mojolicious.org'}});
await ua.headOk('/foo', {headers: {Accept: '*/*'}});
await ua.optionsOk('/foo', {auth: 'kraih:s3cret'});
await ua.patchOk('/foo', {formData: {role: 'admin'}});
await ua.postOk('/foo', {body: Buffer.from('Hello Mojo!')});
await ua.putOk('/foo', {json: {hello: 'world'}});
await ua.websocketOk('/ws', {protocols: ['test/1', 'test/2']});
All test methods return the user-agent object again to allow for easy method chaining and all state is stored inside the user-agent object.
// Status tests
(await ua.getOk('/foo'))
.statusIs(200);
// Header tests
(await ua.getOk('/foo'))
.typeIs('text/html')
.typeLike(/html/)
.headerIs('Content-Type', 'text/html')
.headerLike('Content-Type', /html/)
.headerExists('Content-Type')
.headerExistsNot('X-Test');
// Body tests
(await ua.getOk('/foo'))
.bodyIs('Hello World!')
.bodyLike(/Hello/)
.bodyUnlike(/Bye/);
// JSON tests
(await ua.getOk('/foo'))
.jsonIs({hello: 'world'})
.jsonIs('world', '/hello');
// YAML tests
(await ua.getOk('/foo'))
.yamlIs({hello: 'world'})
.yamlIs('world', '/hello');
// HTML tests
(await ua.getOk('/foo'))
.elementExists('head > title')
.elementExistsNot('body #error')
.textLike('head > title', /Welcome/)
.textUnlike('head > title', /Bye/);
Testing WebSockets is almost as easy, but all operations are async and have to return a Promise
.
await ua.websocketOk('/echo');
await ua.sendOk('hello');
assert.equal(await ua.messageOk(), 'echo: hello');
await ua.closeOk(4000);
await ua.closedOk(4000);
If you're using TypeScript we also support explicit resource management with await using
for a little less
boilerplate.
import {app} from '../index.js';
import t from 'tap';
t.test('Example application', async t => {
await using ua = await app.newTestUserAgent({tap: t});
await t.test('Index', async t => {
(await ua.getOk('/')).statusIs(200).bodyLike(/mojo.js/);
});
});
And while the test user-agent is very efficient for testing backend services, for frontend testing we recommend combining it with playwright.
#Introspection
You can set the MOJO_CLIENT_DEBUG
environment variable to get some advanced diagnostics information printed to
stderr
.
$ MOJO_CLIENT_DEBUG=1 node myapp.js
-- Client >>> Server
GET / HTTP/1.1\x0d
Accept-Encoding: gzip\x0d
Host: 127.0.0.1:3000\x0d
Connection: close\x0d
\x0d
-- Client <<< Server
HTTP/1.1 200 OK\x0d
Content-Type: text/plain; charset=utf-8\x0d
Content-Length: 12\x0d
Date: Mon, 02 May 2022 23:32:34 GMT\x0d
Connection: close\x0d
\x0d
Hello World!
Be aware: The exact output format is likely to change over time as more protocols are supported.
#More
A lot more documentation and examples by many different authors can be found in the mojo.js wiki.
#Support
If you have any questions the documentation might not yet answer, don't hesitate to ask in the Forum, or on IRC.