Please review USDR’s general guidelines for software & data, too: https://policies.usdigitalresponse.org/data-and-software-guidelines
A Jest environment for Airtable Scripts.
You need to install both this package and @airtable/blocks to run tests. The @airtable/blocks
package is required by the base snapshots generated by Airtable's Testing Fixture extension.
npm install --save-dev jest-airtable-script @airtable/blocks
This repository falls under U.S. Digital Response’s Code of Conduct, and we will hold all participants in issues, pull requests, discussions, and other spaces related to this project to that Code of Conduct. Please see CODE_OF_CONDUCT.md for the full code.
Add the following to your jest.config.js
file:
{
testEnvironment: 'jest-environment-airtable-script'
}
Warning
You should generate fixture data from a copy of your base that has non-sensitive test data. You can generate fake records using the Random record generator extension.
Airtable scripts are always run within the context of a base. To test your scripts locally, you will need to generate an object that stores all the tables, fields, views, and records in your base. You can do this by using the Test Fixture Generator extension.
Important
The Test Fixture Generator extension has a bug that means you need to edit the output and can't just paste it into a new file.
To fix the file generated by the Test Fixture Generator extension, you need to remove the mockSdkWithFixtureData
function and export the fixture data object directly:
import { FieldType, ViewType } from '@airtable/blocks/models'
import { mockSdkWithFixtureData } from '@airtable/blocks/testing'
export default mockSdkWithFixtureData({ ...data })
Remove all references to mockSdkWithFixtureData
so that you just export the fixture data object:
import { FieldType, ViewType } from '@airtable/blocks/models'
export default { ...data }
Then you can import the fixture data in your test files:
import baseFixture from './base-fixture.js'
You will probably want to load your Airtable scripts from a separate file for testing. Here is an example of how you might do that:
// my-script.js
const table = base.getTable('My Table')
output.text(table.name)
// my-script.test.js
import fs from 'fs'
import baseFixture from './base-fixture.js'
describe('My Script', () => {
const myScript = fs.readFileSync(__dirname + '/my-script.js', 'utf8')
it('should output the table name', async () => {
const result = await runAirtableScript({
script: myScript,
base: baseFixture,
})
expect(result.output[0]).toEqual('My Table')
})
})
The Test Fixtures extension automatically changes IDs for tables, fields, views, and records to strings like fldFirstName
for a "First name" field. As this envrionment is mostly designed for developing scripts that run in multiple bases, IDs should not be depended on anyhow, and table or field names should be used instead.
If you really need to use IDs, you can use the global __isAirtableScriptTestEnvironment
variable to conditionally use IDs in your script:
const table = base.getTable(
globalThis.__isAirtableScriptTestEnvironment ? 'tblMyTable' : 'My Table'
)
When a script is running in the test environment, the global globalThis.__isAirtableScriptTestEnvironment
is set to true
. This can be used to conditionally run code that should only be executed in the test environment.
The Date, Last updated time, and Created time fields all support multiple date formats, including "Local," which can change depending on the client. This can make it hard to test the results of a call to a records' getCellValueAsString
method. To make this easier, you can set the defaultDateLocale
option when calling runAirtableScript
to specify the date format you want to use. This will set the date format for all date fields in the base.
const result = await runAirtableScript({
script: myScript,
base: baseFixture,
defaultDateLocale: 'us',
})
You can pass one of us
, friendly
, european
, or iso
.
Airtable scripts can either use fetch
, or in extensions remoteFetchAsync
to make HTTP requests. You can mock these requests using the fetchMock
setting:
const result = await runAirtableScript({
script: myScript,
base: baseFixture,
fetchMock: (url, request) => {
return {
status: 200,
body: JSON.stringify({ message: 'Hello, world!' }),
}
},
})
You can mock any input
from either an automation input or user interaction using the mockInput
setting:
const results = await runAirtableScript({
script: `
const text = await input.textAsync('Select a table')
output.inspect(text)
`,
base: randomRecords,
mockInput: {
// @ts-ignore
textAsync: (label) => {
if (label === 'Select a table') {
return 'text123'
}
},
},
})
Every input method for extensions or automations are available to be mocked. Check out the input.test.ts file for examples.
The results from calling runAirtableScript
are an object with several properties:
output
: An array of all the calls to the built-inoutput
object.mutations
: An array of all the changes made to the base, including creating, updating, and deleting tables, fields, and records.console
: An array of all the calls to theconsole
object, likeconsole.log
.
The output
property is an array of all the calls to the built-in output object. Different types of output methods return different types of items.
To persist data for test purposes, if your script calls output.clear()
, we instead just add a new record in the output array with the contnets of the global OUTPUT_CLEAR
variable. To test this, you can check for that variable:
it('outputs one text, then clears, then another text', async () => {
const { output } = await runAirtableScript({
script: `
output.text('first output')
output.clear()
output.text('second output')
`,
base: randomRecords,
})
expect(output).toEqual(['first output', OUTPUT_CLEAR, 'second output'])
})
Any changes you make to the base are recorded and returned in the mutations
property. The following types of mutations are supported and are defined in the global MutationTypes
object:
SET_MULTIPLE_RECORD_CELL_VALUES
DELETE_RECORD
CREATE_RECORD
CREATE_FIELD
UPDATE_FIELD_CONFIG
UPDATE_FIELD_DESCRIPTION
UPDATE_FIELD_NAME
CREATE_TABLE
Each mutation has one of the above type
properties and an args
property that contains the arguments passed to the method that caused the mutation.
An example of using a mutation in a test:
it('creates a record', async () => {
const { mutations } = await runAirtableScript({
script: `
const table = base.getTable('tblTableA')
await table.createRecordAsync({
fldName: 'New Name',
})
`,
base: testBase,
})
expect(mutations[0].type).toEqual(MutationTypes.CREATE_RECORD)
expect(mutations[0].args.cellValuesByFieldId).toEqual({
fldName: 'New Name',
})
})
We overwrite the default console
object in Airtable scripts to capture all calls to console.log
, console.warn
, and console.error
. These are returned in the console
property of the results object. It is structured as an array of objects that include a type
property (one of log
, warn
, or error
) and a message
property with the message that was logged.
An example of using the console in a test:
it('logs a message', async () => {
const { console } = await runAirtableScript({
script: `
console.log('Hello, world!')
`,
base: testBase,
})
expect(console[0].type).toEqual('log')
expect(console[0].message).toEqual('Hello, world!')
})
The environment variable JEST_AIRTABLE_TS_DEV
should be set to true
so that the runScript
function pulls the compiled SDK mock from the ./src/environment/sdk/__sdk.js
file. This is already set to true
in the package.json
file.
Note that because of the unique way that the SDK is mocked, including needing a pretty broad set of global variables being set up, there are files where some eslint rules are ignored.
Copyright (C) 2022 U.S. Digital Response (USDR)
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this software except in compliance with the License. You may obtain a copy of the License at:
LICENSE
in this repository or http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.