# Testing (/docs/products/dash-dots/developers/testing)

`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](https://vitest.dev):

```bash
npm install --save-dev vitest
```

A test for the cart page of a Freetown cookshop:

```ts title="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:

```ts
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](#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](/docs/products/dash-dots/reference/pages-and-dot#testpage) 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:

```ts
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`.

```ts
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:

```ts
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](/docs/products/dash-dots/developers/views#data-attributes) from `event.data`, send the whole event with `dispatch`:

```ts
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:

```ts
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](/docs/products/dash-dots/developers/pages-and-state#state), and it doesn't draw the page. Run the dot in the simulator as well. The simulator keeps Dash's [limits](/docs/products/dash-dots/developers/network-and-files#limits), and [holds calls back](/docs/products/dash-dots/developers/dash-apis#when-dash-holds-a-call-back) while its tab is hidden, as Dash does while the person is in another app.
