diff --git a/CHANGELOG.md b/CHANGELOG.md index 3408405..6acfdc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - Removed the `__sdk.js` developer file from the respoitory. +- Fixed the types for runAirtableScript. ## [0.0.2] - 2025-01-09 diff --git a/README.md b/README.md index 185bcec..58b5855 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,47 @@ const result = await runAirtableScript({ You can pass one of `us`, `friendly`, `european`, or `iso`. +### Mocking fetch requests + +Airtable scripts can either use `fetch`, or in extensions `remoteFetchAsync` to make HTTP requests. You can mock these requests using the `fetchMock` setting: + +```js +const result = await runAirtableScript({ + script: myScript, + base: baseFixture, + fetchMock: (url, request) => { + return { + status: 200, + body: JSON.stringify({ message: 'Hello, world!' }), + } + }, +}) +``` + +### Mocking user inputs + +You can mock any `input` from either an automation input or user interaction using the `mockInput` setting: + +```js +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](https://airtable.com/developers/scripting/api/input) are available to be mocked. Check out the [input.test.ts](./test/input.test.ts) file for examples. + ### Results The results from calling `runAirtableScript` are an object with several properties: diff --git a/src/environment/console-aggregator.ts b/src/environment/console-aggregator.ts index c079366..0294b45 100644 --- a/src/environment/console-aggregator.ts +++ b/src/environment/console-aggregator.ts @@ -13,6 +13,9 @@ type ConsoleAggregator = { /** * Returns a console object that aggregates all messages logged to it. * Used to override the global console object in tests. + * + * The _getMessages method is called after a test is run to pass the + * messages to the test runner. */ const consoleAggregator = (): ConsoleAggregator => { const consoleMessages: ConsoleMessage[] = [] diff --git a/src/environment/index.ts b/src/environment/index.ts index 58105dd..f7ffed2 100644 --- a/src/environment/index.ts +++ b/src/environment/index.ts @@ -12,15 +12,13 @@ import { OUTPUT_CLEAR } from './output-clear' type StrictGlobal = { runAirtableScript: (options: RunScriptOptions) => Promise - MutationTypes: typeof MutationTypes | undefined - OUTPUT_CLEAR: typeof OUTPUT_CLEAR | undefined + MutationTypes: typeof MutationTypes + OUTPUT_CLEAR: typeof OUTPUT_CLEAR } export type AirtableScriptGlobal = Required export class AirtableScriptEnvironment extends NodeEnvironment { - declare global: StrictGlobal & NodeEnvironment['global'] - constructor(config: JestEnvironmentConfig, _context: EnvironmentContext) { super(config, _context) this.global.runAirtableScript = runAirtableScript diff --git a/src/environment/run-airtable-script.ts b/src/environment/run-airtable-script.ts index d9bc8ab..6b5b14e 100644 --- a/src/environment/run-airtable-script.ts +++ b/src/environment/run-airtable-script.ts @@ -11,6 +11,8 @@ import { type DefaultDateLocale = 'friendly' | 'us' | 'european' | 'iso' +type Output = [string, number, boolean] | { key: string; value: string }[] + type RunScriptOptions = { script: string base: { base: unknown } | unknown @@ -24,7 +26,7 @@ type RunScriptOptions = { } type RunScriptResult = { - output: unknown[] + output: Output mutations: Mutation[] console: ConsoleMessage[] } @@ -47,6 +49,9 @@ type RunContext = { let sdkScript: string | null = null +/** + * Runs a given Airtable script against a base fixture. Full definition is in src/environment/index.ts + */ const runAirtableScript = async ({ script, base, @@ -77,8 +82,8 @@ const runAirtableScript = async ({ __defaultDateLocale: defaultDateLocale, console: consoleAggregator(), } - vm.createContext(context) + vm.createContext(context) vm.runInContext(sdkScript, context) // We need to run the script in an async function so that we can use await // directly inside the script. @@ -90,7 +95,7 @@ const runAirtableScript = async ({ ) return { - output: context.__output || [], + output: (context.__output as Output) || [], mutations: context.__mutations || [], console: context.console._getMessages(), } diff --git a/src/environment/sdk/globals/output.ts b/src/environment/sdk/globals/output.ts index 8e20d2c..625ac30 100644 --- a/src/environment/sdk/globals/output.ts +++ b/src/environment/sdk/globals/output.ts @@ -20,12 +20,18 @@ type ExtensionOutput = { // @ts-ignore globalThis.__output = [] +/** + * The output object if a script is being used within an automation. + */ const automationOutput: AutomationOutput = { set: (key, value) => { __output.push({ key, value }) }, } +/** + * The output object if a script is being used within an extension. + */ const extensionOutput: ExtensionOutput = { /** * Displays the given text on-screen. @@ -88,6 +94,7 @@ const extensionOutput: ExtensionOutput = { }, } +// Use one of the two outputs based on the context (extension or automation) const output: AutomationOutput | ExtensionOutput = globalThis.__inAutomation ? automationOutput : extensionOutput diff --git a/src/environment/sdk/lib/pascal-case.ts b/src/environment/sdk/lib/pascal-case.ts index 4fca71a..f66e0f5 100644 --- a/src/environment/sdk/lib/pascal-case.ts +++ b/src/environment/sdk/lib/pascal-case.ts @@ -1,5 +1,6 @@ /** - * A pascal case utility function. + * A pascal case utility function. Used for schema IDs, which always start with + * a three-letter lower-case prefix like tblTableId or fldFieldId. */ const pascalCase = (str: string): string => str diff --git a/src/index.ts b/src/index.ts index 91720e2..ac69b1b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,37 @@ export { AirtableScriptEnvironment as default } from './environment' import type { AirtableScriptGlobal } from './environment' +import type { + RunScriptOptions, + RunScriptResult, +} from './environment/run-airtable-script' declare global { - const runAirtableScript: AirtableScriptGlobal['runAirtableScript'] + /** + * Runs a given Airtable script against a base fixture. Returns the output, console, and base + * mutations that the script generated. + * + * @param {RunScriptOptions} options + * @param {string} options.script The script to run, as a string. + * @param {Base} options.base The base fixture to run the script against. Generate this using + * the Test Fixture Generator extension. + * @param {boolean} [options.inAutomation=false] Whether the script is running in an automation. Defaults to false. + * @param {DefaultCursor | false} [options.defaultCursor=false] The default cursor to use for the script. Defaults to false. + * @param {Collaborator | false} [options.currentUser=false] The current user to use for the script. Defaults to false. + * @param {unknown} [options.mockInput=undefined] The mock input for the script. See the README for more information. + * @param {Function | false} [options.mockFetch=false] A function that mocks any fetch requests. + * @param {DefaultDateLocale} [options.defaultDateLocale='us'] The date format to use when a date field uses the "local" date format. Defaults to 'us'. + * @returns {Promise} + */ + const runAirtableScript: ( + options: RunScriptOptions + ) => Promise + /** + * An object containing the different types of mutations that can be tracked in a script. + */ const MutationTypes: AirtableScriptGlobal['MutationTypes'] + /** + * A special string that is used to denote that a call to output.clear() was made in the script. + */ const OUTPUT_CLEAR: AirtableScriptGlobal['OUTPUT_CLEAR'] } diff --git a/test/input.test.ts b/test/input.test.ts index 61dbbb9..286a83c 100644 --- a/test/input.test.ts +++ b/test/input.test.ts @@ -15,10 +15,12 @@ describe('Input', () => { config: () => ({ [key]: 'tbl123' }), }, }) - expect(results.output[0].key).toEqual('config') - expect(results.output[0].value).toEqual( - JSON.stringify({ [key]: 'tbl123' }) - ) + if (typeof results.output[0] === 'object') { + expect(results.output[0].key).toEqual('config') + expect(results.output[0].value).toEqual( + JSON.stringify({ [key]: 'tbl123' }) + ) + } }) }) describe('extension script', () => {