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.

  1. import {UserAgent} from '@mojojs/core';
  2. const ua = new UserAgent();
  3. const res = await ua.get('https://mojolicious.org');
  4. 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.

  1. const ua = new UserAgent({
  2. // Base URL to be used to resolve all relative request URLs with
  3. baseURL: 'http://127.0.0.1:3000',
  4. // Maximum number of redirects to follow, default to none
  5. maxRedirects: 5,
  6. // Name of user agent to send with `User-Agent` header
  7. name: 'mojoUA/1.0'
  8. });

Request Config

Every request is represented by a config object that contains various properties to describe every part of the HTTP request.

  1. const res = await ua.request({
  2. // HTTP method for request
  3. method: 'GET',
  4. // URL of request target as a string or URL object, may be be relative to `ua.baseURL`
  5. url: new URL('https://mojolicious.org'),
  6. // Headers to include in request
  7. headers: {Accept: '*/*', Authorization: 'token 123456789abcdef'},
  8. // Object with key/value pairs to be sent with the query string
  9. query: {fieldA: 'first value', fieldB: 'second value'},
  10. // Request body as a string, `Buffer` or `stream.Readable` object
  11. body: 'Some content to send with request',
  12. // Data structure to be send in JSON format, or for WebSockets a `true` value to enable JSON mode
  13. json: {hello: ['world']},
  14. // Data structure to be send in YAML format
  15. yaml: {hello: ['world']},
  16. // Object with key/value pairs to be sent in `application/x-www-form-urlencoded` format
  17. form: {fieldA: 'first value', fieldB: 'second value'},
  18. // Object with key/value pairs and a file upload to be sent in `multipart/form-data` format
  19. formData: {fieldA: 'first value', fieldB: 'second value', fieldC: {content: 'Hello Mojo!', filename: 'test.txt'}},
  20. // Basic authentication
  21. auth: 'user:password',
  22. // Disable TLS certificate validation
  23. insecure: true,
  24. // Override the trusted CA certificates (defaults to CAs curated by Mozilla that ship with Node)
  25. ca: ['...', '...'],
  26. // Server name for the SNI (Server Name Indication) TLS extension
  27. servername: 'localhost',
  28. // Alternative `http.Agent` object to use, for keep-alive or SOCKS proxy support with `proxy-agent`
  29. agent: new http.Agent({keepAlive: true})
  30. });

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.

Request Shortcuts

Since every request includes at least method and url values, there are HTTP method specific shortcuts you can use instead of request.

  1. const res = await ua.delete('https://mojolicious.org');
  2. const res = await ua.get('https://mojolicious.org');
  3. const res = await ua.head('https://mojolicious.org');
  4. const res = await ua.options('https://mojolicious.org');
  5. const res = await ua.patch('https://mojolicious.org');
  6. const res = await ua.post('https://mojolicious.org');
  7. 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.

  1. 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.

  1. // Status code and message
  2. const statusCode = res.statusCode;
  3. const statusMessage = res.statusMessage;
  4. // Headers
  5. const contentType = res.get('Content-Type');
  6. // 2xx
  7. const isSuccess = res.isSuccess;
  8. // 3xx
  9. const isRedirect = res.isRedirect;
  10. // 4xx
  11. const isClientError = res.isClientError;
  12. // 5xx
  13. const isServerError = res.isServerError;
  14. // 4xx or 5xx
  15. 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.

  1. // String
  2. const text = await res.text();
  3. // Buffer
  4. const buffer = await res.buffer();
  5. // Pipe content to `stream.Writable` object
  6. await res.pipe(process.stdout);
  7. // Parsed JSON
  8. const data = await res.json();
  9. // Parsed YAML
  10. const data = await res.yaml();
  11. // Parsed HTML via `@mojojs/dom`
  12. const dom = await res.html();
  13. const title = dom.at('title').text();
  14. // Parsed XML via `@mojojs/dom`
  15. const dom = await res.xml();
  16. // Async iterator
  17. const parts = [];
  18. for await (const chunk of res) {
  19. parts.push(chunk);
  20. }
  21. 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.

  1. ua.cookieJar.storage.allowSpecialUseDomain = true;

Of course you can also just disable cookies completely.

  1. const ua = new UserAgent({cookieJar: null});

WebSockets

For WebSocket handshakes there are also quite a few options available.

  1. const ws = await ua.websocket('wss://mojolicious.org', {
  2. // Headers to include in handshake
  3. headers: {Accept: '*/*', Authorization: 'token 123456789abcdef'},
  4. // Object with key/value pairs to be sent with the query string
  5. query: {fieldA: 'first value', fieldB: 'second value'},
  6. // Enable JSON mode (encoding and decoding all messages automatically)
  7. json: true,
  8. // Basic authentication
  9. auth: 'user:password',
  10. // WebSocket subprotocols
  11. protocols: ['foo', 'bar']
  12. });

You can choose between multiple API styles.

  1. // Events
  2. const ws = await ua.websocket('/ws');
  3. await ws.send('something');
  4. ws.on('message', message => {
  5. console.log(message);
  6. });
  7. // Async iterator
  8. const ws = await ua.websocket('/ws');
  9. await ws.send('something');
  10. for await (const message of ws) {
  11. console.log(message);
  12. }

With support for ping and pong frames.

  1. // Handshake with authentication headers
  2. const ws = await ua.websocket('/ws', {headers: {Authorization: 'token 123456789abcdef'}});
  3. ws.on('ping', data => {
  4. ws.pong(data);
  5. });

Cookies from the cookie jar will of course also be available for the handshake, so you can rely on them for things like authentication.

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.

  1. import {TestUserAgent} from '@mojojs/core';
  2. const ua = new TestUserAgent({baseURL: 'https://mojolicious.org'});
  3. (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.

  1. import {TestUserAgent} from '@mojojs/core';
  2. import t from 'tap';
  3. t.test('Mojolicious', async t => {
  4. const ua = new TestUserAgent({baseURL: 'https://mojolicious.org', tap: t});
  5. await t.test('Index', async t => {
  6. (await ua.getOk('/')).statusIs(200).bodyLike(/Mojolicious/);
  7. });
  8. });

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.

  1. import {app} from '../index.js';
  2. import t from 'tap';
  3. t.test('Example application', async t => {
  4. const ua = await app.newTestUserAgent({tap: t});
  5. await t.test('Index', async t => {
  6. (await ua.getOk('/')).statusIs(200).bodyLike(/mojo.js/);
  7. });
  8. await ua.stop();
  9. });

There are test alternatives for all HTTP method shortcuts.

  1. await ua.deleteOk('/foo');
  2. await ua.getOk('/foo', {headers: {Host: 'mojolicious.org'}});
  3. await ua.headOk('/foo', {headers: {Accept: '*/*'}});
  4. await ua.optionsOk('/foo', {auth: 'kraih:s3cret'});
  5. await ua.patchOk('/foo', {formData: {role: 'admin'}});
  6. await ua.postOk('/foo', {body: Buffer.from('Hello Mojo!')});
  7. await ua.putOk('/foo', {json: {hello: 'world'}});
  8. 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.

  1. // Status tests
  2. (await ua.getOk('/foo'))
  3. .statusIs(200);
  4. // Header tests
  5. (await ua.getOk('/foo'))
  6. .typeIs('text/html')
  7. .typeLike(/html/)
  8. .headerIs('Content-Type', 'text/html')
  9. .headerLike('Content-Type', /html/)
  10. .headerExists('Content-Type')
  11. .headerExistsNot('X-Test');
  12. // Body tests
  13. (await ua.getOk('/foo'))
  14. .bodyIs('Hello World!')
  15. .bodyLike(/Hello/)
  16. .bodyUnlike(/Bye/);
  17. // JSON tests
  18. (await ua.getOk('/foo'))
  19. .jsonIs({hello: 'world'})
  20. .jsonIs('world', '/hello');
  21. // YAML tests
  22. (await ua.getOk('/foo'))
  23. .yamlIs({hello: 'world'})
  24. .yamlIs('world', '/hello');
  25. // HTML tests
  26. (await ua.getOk('/foo'))
  27. .elementExists('head > title')
  28. .elementExistsNot('body #error')
  29. .textLike('head > title', /Welcome/);

Testing WebSockets is almost as easy, but all operations are async and have to return a Promise.

  1. await ua.websocketOk('/echo');
  2. await ua.sendOk('hello');
  3. assert.equal(await ua.messageOk(), 'echo: hello');
  4. await ua.closeOk(4000);
  5. await ua.closedOk(4000);

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.

  1. $ MOJO_CLIENT_DEBUG=1 node myapp.js
  2. -- Client >>> Server
  3. GET / HTTP/1.1\x0d
  4. Accept-Encoding: gzip\x0d
  5. Host: 127.0.0.1:3000\x0d
  6. Connection: close\x0d
  7. \x0d
  8. -- Client <<< Server
  9. HTTP/1.1 200 OK\x0d
  10. Content-Type: text/plain; charset=utf-8\x0d
  11. Content-Length: 12\x0d
  12. Date: Mon, 02 May 2022 23:32:34 GMT\x0d
  13. Connection: close\x0d
  14. \x0d
  15. Hello World!

Support

If you have any questions the documentation might not yet answer, don’t hesitate to ask in the Forum, on Matrix, or IRC.