Products · Build a dot

Testing

Test a dot's pages with testPage, without Dash or the simulator. Fake the Dash calls they make and check what happened.

testPage runs one page in a plain test, with no Dash and no simulator. You call its mutations and actions with tap, then check what happened. It works with any test runner. These examples use Vitest:

Terminal
npm install --save-dev vitest

A test for the cart page of a Freetown cookshop:

pages/cart/controller.test.ts
import { testPage } from '@monimesl/dot-runtime/testing';
import { describe, expect, it } from 'vitest';

import cart from './controller';

describe('cart', () => {
  it('adds jollof and goes to checkout', async () => {
    const page = await testPage(cart, { options: { from: 'menu' } });

    await page.tap('add');
    await page.tap('setQuantity', { value: 3 });
    await page.tap('checkout');

    expect(page.state).toMatchObject({ items: 1, quantity: 3 });
    expect(page.navigations).toEqual([
      { action: 'push', page: 'checkout', options: { items: '1' } },
    ]);
  });
});

Import @monimesl/dot-runtime/testing before the controller, so definePage, createEvent and env exist when it loads. env starts empty. Fill in what the page reads:

TypeScript
Object.assign(env, { API_URL: 'https://api.example.test' });

Open, tap and check

testPage(page, settings) opens the page as Dash would, running onLoad and then onShow. Its settings are all optional:

  • options: the page's launch options.
  • host: fakes for the Dash calls it makes. See Fake Dash calls.
  • results: by page id, the value a page opened with pushForResult hands back, as in { 'pick-genre': 'afrobeats' }. A page you leave out gives undefined, as if the person went back.

Drive the page with tap(name, detail?) for its mutations and actions, and with show(), hide(), refresh() and unload() for its hooks. Then check:

  • state: the page's state now.
  • navigations: the pages it opened and closed.
  • hostCalls: the Dash calls it made.
  • result: what it gave back with backWithResult.

The testPage reference has every setting and member.

When a mutation or action throws, tap rejects with that error, and so does tapping a name the page doesn't have:

TypeScript
await expect(page.tap('pay')).rejects.toThrow('Payment declined');

Fake Dash calls

host maps each Dash call to a function, and what the function returns is the call's result. The name drops this.: this.dash.ui.alert becomes dash.ui.alert. A call without a fake fails with unsupported.

TypeScript
const page = await testPage(cancelOrder, {
  host: {
    'dash.ui.alert': () => ({ action: 'cancel' }),
    'dash.network.http.request': () => ({
      ok: true,
      status: 200,
      statusText: 'OK',
      headers: { 'content-type': 'application/json' },
      body: '{"cancelled":true}',
    }),
  },
});
await page.tap('cancel');

expect(page.hostCalls.map((call) => call.method)).toEqual([
  'dash.ui.alert',
  'dash.network.http.request',
]);

Write a body as text, even when the page reads JSON. dash.utils and dash.supports don't need fakes.

A test draws no page, so this.captureView() gives an empty picture. It still saves that picture with Dash, so fake dash.storage.file.write and dash.storage.file.open.

Make a call fail

Throw a DashError with the code you want:

TypeScript
import { DashError } from '@monimesl/dot-runtime';

const page = await testPage(send, {
  host: {
    'dash.contacts.pickPhone': () => {
      throw new DashError('denied', 'The person said no');
    },
  },
});
await page.tap('chooseCousin');

expect(page.state.typeNumber).toBe(true);

To test a closed picker, use the code cancelled.

Test taps with data

tap sends an event without data. When a handler reads data-* attributes from event.data, send the whole event with dispatch:

TypeScript
await page.dispatch('addPhoto', createEvent('click', {}, { source: 'camera' }));

Test a mutation on its own

A mutation is a plain function. Call it with a state and an event:

TypeScript
expect(cart.mutations?.add?.({ items: 1, quantity: 1 }, createEvent('tap'))).toEqual({ items: 2 });

Run it in the simulator too

testPage doesn't check that state is plain data, and it doesn't draw the page. Run the dot in the simulator as well. The simulator keeps Dash's limits, and holds calls back while its tab is hidden, as Dash does while the person is in another app.

Keep reading