# TestDriver

**Kind:** Class

A class designed to facilitate the automated testing of Airtable Extensions
outside of a production Extensions environment. Each instance creates a simulated
`Base` which is distinct from any other Base created in this way.
Custom Extensions can be instantiated using an instance of this class; see `the `Container` method`.

The example code for this class's methods is written in terms of a
non-existent Airtable Extension called `MyCustomExtension`. Each example includes a
description of the presumed behavior for that Extension. Consumers of this library
will work with their own Extensions whose behavior differs from these examples, so
their tests will be distinct in this regard.

## Properties

### `base`

Type: `Base`

The simulated `Base` associated with this instance.

### `cursor`

Type: `Cursor`

The `Cursor` instance associated with this instance's Base.

### `globalConfig`

Type: `GlobalConfig`

A simulated `GlobalConfig` instance. This always starts empty.

### `session`

Type: `Session`

A `Session` instance. This will correspond to the first
collaborator in your fixture data.

## Methods

### `Container(__namedParameters)`

A React Component which may be used to wrap Extension Components, enabling
them to run outside of a production Extensions environment.

**Parameters:**
- `__namedParameters` (`Object`)

**Returns:** `Element`

```js
import TestDriver from '@airtable/blocks-testing';
// Given MyCustomExtension, an Airtable Extension which defines a React Component...
const MyCustomExtension = require('../src/my_custom_extension');
// And given myFixtureData, a data structure describing the initial
// state of a simulated Airtable Base...
const myFixtureData = require('./my_fixture_data');

const testDriver = new TestDriver(myFixtureData);

render(
    <testDriver.Container>
        <MyCustomExtension />
    </testDriver.Container>
);
```

### `deleteFieldAsync(tableIdOrName, fieldIdOrName)`

Destroy a `Field` in the simulated `Base`.

**Parameters:**
- `tableIdOrName` (`TableId | string`)
- `fieldIdOrName` (`FieldId | string`)

**Returns:** `Promise<void>`

```js
import TestDriver from '@airtable/blocks-testing';
// Given MyCustomExtension, an Airtable Extension which displays the names of all
// the Fields in the active Table...
import MyCustomExtension from '../src/my_custom_extension';
// And given myFixtureData, a data structure describing an Airtable
// Base which contains a Table named "Table One" with three Fields...
import myFixtureData from './my_fixture_data';
import {render, screen} from '@testing-library/react';

const testDriver = new TestDriver(myFixtureData);
let items, itemTexts;

render(
    <testDriver.Container>
        <MyCustomExtension />
    </testDriver.Container>
);

// Verify that MyExtension initially displays all three Fields
items = screen.getAllByRole('listitem');
itemTexts = items.map((el) => el.textContent);
expect(itemTexts).toEqual(['1st field', '2nd field', '3rd field']);

// Simulate the destruction of the Field named "2nd field"
await testDriver.deleteFieldAsync('Table One', '2nd field');

// Verify that MyExtension correctly updates to describe the two remaining
// Fields
items = screen.getAllByRole('listitem');
itemTexts = items.map((el) => el.textContent);
expect(itemTexts).toEqual(['1st field', '3rd field']);
```

### `deleteTable(tableIdOrName)`

Destroy a `Table` in the simulated `Base`.

**Parameters:**
- `tableIdOrName` (`TableId | string`)

**Returns:** `void`

```js
import TestDriver from '@airtable/blocks-testing';
// Given MyCustomExtension, an Airtable Extension which displays the names of all
// the Tables in the Base...
import MyCustomExtension from '../src/my_custom_extension';
// And given myFixtureData, a data structure describing an Airtable
// Base which contains three Tables...
import myFixtureData from './my_fixture_data';
import {render, screen} from '@testing-library/react';

const testDriver = new TestDriver(myFixtureData);
let items, itemTexts;

render(
    <testDriver.Container>
        <MyCustomExtension />
    </testDriver.Container>
);

// Verify that MyExtension initially displays all three Tables
items = screen.getAllByRole('listitem');
itemTexts = items.map((el) => el.textContent);
expect(itemTexts).toEqual(['1st table', '2nd table', '3rd table']);

// Simulate the destruction of the Table named "2nd table"
testDriver.deleteTable('2nd table');

// Verify that MyExtension correctly updates to describe the two remaining
// Table
items = screen.getAllByRole('listitem');
itemTexts = items.map((el) => el.textContent);
expect(itemTexts).toEqual(['1st table', '3rd table']);
```

### `deleteViewAsync(tableIdOrName, viewIdOrName)`

Destroy a `View` in the simulated `Base`.

**Parameters:**
- `tableIdOrName` (`TableId | string`)
- `viewIdOrName` (`ViewId | string`)

**Returns:** `Promise<void>`

```js
import TestDriver from '@airtable/blocks-testing';
// Given MyCustomExtension, an Airtable Extension which displays the names of all
// the Views in the active Table...
import MyCustomExtension from '../src/my_custom_extension';
// And given myFixtureData, a data structure describing an Airtable
// Base which contains a Table named "Table One" with three Views...
import myFixtureData from './my_fixture_data';
import {render, screen} from '@testing-library/react';

const testDriver = new TestDriver(myFixtureData);
let items, itemTexts;

render(
    <testDriver.Container>
        <MyCustomExtension />
    </testDriver.Container>
);

// Verify that MyExtension initially displays all three Views
items = screen.getAllByRole('listitem');
itemTexts = items.map((el) => el.textContent);
expect(itemTexts).toEqual(['1st view', '2nd view', '3rd view']);

// Simulate the destruction of the Field named "2nd view"
await testDriver.deleteViewAsync('Table One', '2nd view');

// Verify that MyExtension correctly updates to describe the two remaining
// Views
items = screen.getAllByRole('listitem');
itemTexts = items.map((el) => el.textContent);
expect(itemTexts).toEqual(['1st view', '3rd view']);
```

### `setActiveCursorModels(tableAndOrView)`

Update the active `Table` and/or the active `View` of the
Extension's `Cursor`. Either `table` or `view` must be specified.

**Parameters:**
- `tableAndOrView` (`TableAndOrView`)

**Returns:** `void`

```js
testDriver.setActiveCursorModels({view: 'My grid view'});
```

```js
import TestDriver from '@airtable/blocks-testing';
// Given MyCustomExtension, an Airtable Extension which displays the names of the
// active Table...
import MyCustomExtension from '../src/my_custom_extension';
// And given myFixtureData, a data structure describing an Airtable
// Base which contains two Tables...
import myFixtureData from './my_fixture_data';
import {render, screen} from '@testing-library/react';

const testDriver = new TestDriver(myFixtureData);
let heading;

render(
    <testDriver.Container>
        <MyCustomExtension />
    </testDriver.Container>
);

// Verify that MyExtension initially displays the first Table
heading = screen.getByRole('heading');
expect(heading.textContent).toBe('First table');

// Simulate the end user selecting the second Table from the Airtable
// user interface
testDriver.setActiveCursorModels(({table: 'Second table'});

// Verify that MyExtension correctly updates to describe the newly-selected
// Table
heading = screen.getByRole('heading');
expect(heading.textContent).toBe('Second table');
```

### `simulateExpandedRecordSelection(pickRecord)`

Specify the outcome of a request for the user to select a record in the
UI created by `expandRecordPickerAsync`.

**Parameters:**
- `pickRecord` (`PickRecord`)

**Returns:** `void`

```js
import TestDriver from '@airtable/blocks-testing';
// Given MyCustomExtension, an Airtable Extension which prompts the end user to
// select a Record and displays the name of the Record they selected...
import MyCustomExtension from '../src/my_custom_extension';
// And given myFixtureData, a data structure describing the initial
// state of a simulated Airtable Base...
import myFixtureData from './my_fixture_data';
import {render, screen} from '@testing-library/react';
import userEvent from '@testing-library/user-event';

const testDriver = new TestDriver(myFixtureData);

testDriver.simulateExpandedRecordSelection((tableId, recordIds) => {
    return recordIds[1];
});

render(
    <testDriver.Container>
        <MyCustomExtension />
    </testDriver.Container>
);

// Simulate a user clicking on a button in MyCustomExtension labeled with the
// text "Choose record". If MyCustomExtension reacts to this event by invoking
// the SDK's `expandRecordPickerAsync`, then it will receive the second
// available record due to the function that is provided to
// `simulateExpandedRecordSelection` above.
const button = screen.getByRole('button', {name: 'Choose record'});
userEvent.click(button);

// Verify that MyCustomExtension correctly responds to the simulated user's
// input
const heading = await waitFor(() => screen.getByRole('heading'));
expect(heading.textContent)
  .toBe('You selected the record named "Number Two"');
```

### `simulatePermissionCheck(check)`

Specify the outcome of internal permission checks. This influences the
behavior of not only explicit permission checks from Extensions code but also
the outcome of model operations such as `createRecordsAsync`.

**Parameters:**
- `check` (`object`)

**Returns:** `void`

```js
import TestDriver from '@airtable/blocks-testing';
// Given MyCustomExtension, an Airtable Extension which displays a button labeled
// "Add" and which disables that button for users who lack "write"
// permissions to the Base...
import MyCustomExtension from '../src/my_custom_extension';
// And given myFixtureData, a data structure describing the initial
// state of a simulated Airtable Base...
import myFixtureData from './my_fixture_data';
import {render, screen} from '@testing-library/react';

const testDriver = new TestDriver(myFixtureData);

// Configure the test driver to reject all "create record" mutations.
testDriver.simulatePermissionCheck((mutation) => {
    return mutation.type !== 'createMultipleRecords';
});

render(
    <testDriver.Container>
        <MyCustomExtension />
    </testDriver.Container>
);

// Verify that MyCustomExtension recognizes that the current user may not
// create Records and that disables the corresponding aspect of the user
// interface.
const button = screen.getByRole('button', {name: 'Add'});
expect(button.disabled).toBe(true);
```

### `unwatch(key, fn)`

De-register a function which was previously registered with `watch`. See `WatchableKeysAndArgs` for the available keys.

**Parameters:**
- `key` (`Key`)
- `fn` (`object`)

**Returns:** `void`

```js
import TestDriver from '@airtable/blocks-testing';
// Given MyCustomExtension, an Airtable Extension which enters "full screen" mode
// in response to certain interactions...
const MyCustomExtension = require('../src/my_custom_extension');
// And given myFixtureData, a data structure describing the initial
// state of a simulated Airtable Base...
const myFixtureData = require('./my_fixture_data');

let testDriver;
let enterCount;
let increment = () => {
  enterCount += 1;
});

// Configure the test runner to create a TestDriver instance before
// every test and to listen for requests to enter "full screen" mode
beforeEach(() => {
  testDriver = new TestDriver(myFixtureData);
  enterCount = 0;
  testDriver.watch('enterFullscreen', increment);
});

// Configure the test runner to remove the event listener after every
// test. (The next test will have a new instance of TestDriver with its
// own event handler, so this one is no longer necessary.)
afterEach(() => {
  testDriver.unwatch('enterFullscreen', increment);
});

// (include tests using the `testDriver` and `enterCount` variables
// here)
```

### `userSelectRecords(recordIds)`

Simulate a user visually selecting a set of `Records` in
the active `Table`. This operation is unrelated to an Extension's
programmatic "selection" of records via, e.g. `Table.selectRecords`. To deselect all records, invoke this method with
an empty array.

**Parameters:**
- `recordIds` (`Array<RecordId>`)

**Returns:** `void`

```js
import TestDriver from '@airtable/blocks-testing';
// Given MyCustomExtension, an Airtable Extension which displays the number of
// Records that an end user has selected in the active Table...
import MyCustomExtension from '../src/my_custom_extension';
// And given myFixtureData, a data structure describing the initial
// state of a simulated Airtable Base...
import myFixtureData from './my_fixture_data';
import {render, screen} from '@testing-library/react';

const testDriver = new TestDriver(myFixtureData);

render(
    <testDriver.Container>
        <MyCustomExtension />
    </testDriver.Container>
);

// Retrieve all the Records present in the first Table in the Base
const records = await testDriver.base.tables[0].selectRecordsAsync();

// Simulate an end-user selecting the second and fourth Record
testDriver.userSelectRecords([records[1].id, records[3].id]);

// Verify that MyCustomExtension correctly responds to the simulated user's
// input
const heading = await waitFor(() => screen.getByRole('heading'));
expect(heading.textContent).toBe('2 records selected');
```

### `watch(key, fn)`

Register a function to be invoked in response to a given internal event.
See `WatchableKeysAndArgs` for the available keys and the values
which are included when they are emitted.

**Parameters:**
- `key` (`Key`)
- `fn` (`object`)

**Returns:** `void`

```js
import TestDriver from '@airtable/blocks-testing';
// Given MyCustomExtension, an Airtable Extension which presents the user with one
// button for each available Record, and which responds to button clicks
// by expanding the Record in the Airtable user interface...
import MyCustomExtension from '../src/my_custom_extension';
// And given myFixtureData, a data structure describing an Airtable
// Base which contains a Table with three records...
import myFixtureData from './my_fixture_data';
import {render, screen} from '@testing-library/react';
import userEvent from '@testing-library/user-event';

const testDriver = new TestDriver(myFixtureData);

// Keep track of every time MyCustomExtension attempts to expand a Record in
// the Airtable user interface
let expandedRecordIds = [];
testDriver.watch('expandRecord', ({recordId}) => {
  expendedRecordIds.push(recordId);
});

render(
    <testDriver.Container>
        <MyCustomExtension />
    </testDriver.Container>
);

// Verify that MyCustomExtension does not expand any Records prior to user
// interaction
expect(expandedRecords).toEqual([]);

// Simulate a user clicking on the second button in MyCustomExtension, which
// is expected to correspond to the second Record in the simulated Base
const buttons = screen.getAllByRole('button');
userEvent.click(buttons[1]);

// Verify that MyCustomExtension correctly expanded the second Record in the
// Airtable user interface
expect(expandedRecords).toEqual(['rec2']);
```
