diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32d2226..f146338 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,13 @@ jobs: node-version: ${{ matrix.node-version }} - run: npm i - run: npm run ci + - name: Test Report + uses: dorny/test-reporter@v1 + if: success() || failure() + with: + name: JEST Tests + path: reports/jest-*.xml + reporter: jest-junit build: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index b297a49..f068944 100644 --- a/.gitignore +++ b/.gitignore @@ -130,4 +130,5 @@ dist .pnp.* dist -src/environment/sdk.js \ No newline at end of file +src/environment/sdk.js +reports \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0793446..6f49f5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.0.4] - 2025-01-13 + +### Changed + +- Fixed FS module not being available in the SDK. +- Clarified some types in the input SDK. +- General cleanup of some comments. + ## [0.0.3] - 2025-01-10 ### Changed diff --git a/README.md b/README.md index 58b5855..0ea1ec8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ **Please review USDR’s general guidelines for software & data, too: https://policies.usdigitalresponse.org/data-and-software-guidelines** -[![Code of Conduct](https://img.shields.io/badge/%E2%9D%A4-code%20of%20conduct-blue.svg?style=flat)](./CODE_OF_CONDUCT.md) +[![Code of Conduct](https://img.shields.io/badge/%E2%9D%A4-code%20of%20conduct-blue.svg?style=flat)](./CODE_OF_CONDUCT.md) ![Test and lint](https://github.com/usdigitalresponse/jest-environment-airtable-script/actions/workflows/ci.yml/badge.svg) # Jest Airtable Script diff --git a/eslint.config.mjs b/eslint.config.mjs index c51767d..8ce92f9 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -6,7 +6,7 @@ import tseslint from 'typescript-eslint' export default [ ...tseslint.config(eslint.configs.recommended, tseslint.configs.recommended), { - ignores: ['**/__sdk.js', '**/*.test.ts'], + ignores: ['**/sdk.js', '**/*.test.ts'], }, { rules: { diff --git a/package-lock.json b/package-lock.json index 84887ac..a514350 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,14 @@ { "name": "jest-environment-airtable-script", - "version": "0.0.2", + "version": "0.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "jest-environment-airtable-script", - "version": "0.0.2", + "version": "0.0.3", "license": "Apache-2.0", "dependencies": { - "camelize-ts": "^3.0.0", - "detect-ts-node": "^1.0.5", "luxon": "^3.5.0" }, "devDependencies": { @@ -34,6 +32,7 @@ "eslint": "^9.17.0", "husky": "^9.1.7", "jest": "^29.7.0", + "jest-junit": "^16.0.0", "npm-run-all": "^4.1.5", "npm-watch": "^0.13.0", "rollup": "^4.29.1", @@ -4563,14 +4562,6 @@ "node": ">=6" } }, - "node_modules/camelize-ts": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelize-ts/-/camelize-ts-3.0.0.tgz", - "integrity": "sha512-cgRwKKavoDKLTjO4FQTs3dRBePZp/2Y9Xpud0FhuCOTE86M2cniKN4CCXgRnsyXNMmQMifVHcv6SPaMtTx6ofQ==", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001690", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", @@ -5039,11 +5030,6 @@ "node": ">=8" } }, - "node_modules/detect-ts-node": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/detect-ts-node/-/detect-ts-node-1.0.5.tgz", - "integrity": "sha512-lWACfJ+H6jpxT1uuIQi2KAIkczeHJcM4rmfbAR86gfmAlrJpCVZbnKB0fiqmH8TGw4dm9xrptfwNObEsDdvsFg==" - }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -7033,6 +7019,21 @@ "fsevents": "^2.3.2" } }, + "node_modules/jest-junit": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-16.0.0.tgz", + "integrity": "sha512-A94mmw6NfJab4Fg/BlvVOUXzXgF0XIH6EmTgJ5NDPp4xoKq0Kr7sErb+4Xs9nZvu58pJojz5RFGpqnZYJTrRfQ==", + "dev": true, + "dependencies": { + "mkdirp": "^1.0.4", + "strip-ansi": "^6.0.1", + "uuid": "^8.3.2", + "xml": "^1.0.1" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/jest-leak-detector": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", @@ -7828,6 +7829,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -9878,6 +9891,15 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -10069,6 +10091,12 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "dev": true + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index c38a10b..c5bd603 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jest-environment-airtable-script", - "version": "0.0.3", + "version": "0.0.4", "description": "A jest environment for testing Airtable scripts in extensions and automations", "license": "Apache-2.0", "author": "", @@ -33,8 +33,9 @@ "build:sdk": "rollup -c rollup-sdk.config.mjs", "build:package": "rollup -c rollup.config.mjs", "jest": "JEST_AIRTABLE_TS_DEV=true jest", + "jest:ci": "JEST_AIRTABLE_TS_DEV=true jest --reporters=default --reporters=jest-junit", "test": "run-s build:sdk jest", - "ci": "run-s lint test", + "ci": "run-s lint build:sdk jest:ci", "watch": "npm-watch test", "lint": "eslint ./src", "prepare": "husky" @@ -60,6 +61,7 @@ "eslint": "^9.17.0", "husky": "^9.1.7", "jest": "^29.7.0", + "jest-junit": "^16.0.0", "npm-run-all": "^4.1.5", "npm-watch": "^0.13.0", "rollup": "^4.29.1", @@ -69,8 +71,15 @@ "typescript-eslint": "^8.19.0" }, "dependencies": { - "camelize-ts": "^3.0.0", - "detect-ts-node": "^1.0.5", "luxon": "^3.5.0" + }, + "jest-junit": { + "outputDirectory": "reports", + "outputName": "jest-junit.xml", + "ancestorSeparator": " › ", + "uniqueOutputName": "false", + "suiteNameTemplate": "{filepath}", + "classNameTemplate": "{classname}", + "titleTemplate": "{title}" } } diff --git a/src/environment/mutation-types.ts b/src/environment/mutation-types.ts index 615e1de..d6dcf72 100644 --- a/src/environment/mutation-types.ts +++ b/src/environment/mutation-types.ts @@ -3,7 +3,6 @@ * Taken from: * @see https://github.com/Airtable/blocks/blob/6c0a2ed709a34e28cb3e999fc6cc6406eaa3817b/packages/sdk/src/types/mutations.ts */ - const MutationTypes = Object.freeze({ SET_MULTIPLE_RECORD_CELL_VALUES: 'setMultipleRecordCellValues' as const, DELETE_RECORD: 'deleteRecord' as const, diff --git a/src/environment/sdk/globals/base/index.ts b/src/environment/sdk/globals/base/index.ts index 424d30f..4e081a2 100644 --- a/src/environment/sdk/globals/base/index.ts +++ b/src/environment/sdk/globals/base/index.ts @@ -6,7 +6,7 @@ import { pascalCase } from '../../lib/pascal-case' import { MutationTypes } from '../../../mutation-types' import { FixtureTable } from '../globals' -interface Base { +type Base = { id: string | null name: string | null activeCollaborators: Array | null diff --git a/src/environment/sdk/globals/globals.d.ts b/src/environment/sdk/globals/globals.d.ts index c37082a..51a2e58 100644 --- a/src/environment/sdk/globals/globals.d.ts +++ b/src/environment/sdk/globals/globals.d.ts @@ -5,6 +5,11 @@ import type { Mutation } from './mutations' import type { FieldType } from './base/field' import type { ViewType } from './base/view' +/** + * The "fixture" types are loose types used to defined base objects + * exported from the Test Fixture Generator extension. Since these + * are pretty inconsisstent, there are anumber of optional keys. + */ type FixtureField = { id: string name: string @@ -45,6 +50,9 @@ type FixtureBase = { collaborators: Collaborator[] } +/** + * Declare all the global variables that are available in the scripting environment. + */ declare global { var __currentUser: Collaborator | undefined var __inAutomation: boolean diff --git a/src/environment/sdk/globals/input.ts b/src/environment/sdk/globals/input.ts index 44ed900..0180c6b 100644 --- a/src/environment/sdk/globals/input.ts +++ b/src/environment/sdk/globals/input.ts @@ -25,7 +25,7 @@ type EmptyRecord = { [key: string]: unknown } -interface ExtensionInputConfig { +type ExtensionInputConfig = { (settings: { title: string; description?: string; items: unknown[] }): { [key: string]: Table | Field | string | number } @@ -53,8 +53,14 @@ type MockInput = { > ) => string tableAsync?: (label: string) => string - viewAsync?: (label: string, table: Table | string) => string - fieldAsync?: (label: string, table: Table | string) => string + viewAsync?: ( + label: string, + tableOrTableNameOrTableId: Table | string + ) => string + fieldAsync?: ( + label: string, + tableOrTableNameOrTableId: Table | string + ) => string recordAsync?: ( label: string, options: { @@ -114,6 +120,11 @@ type ExtensionInput = { config: ExtensionInputConfig } +/** + * Checks whether a mock input method exists. If not, throws an error. + * + * @param string The name of the input method. + */ const checkMockInput = (method: string): void => { if (!__mockInput) { throw new Error('mockInput is not defined') @@ -126,6 +137,12 @@ const checkMockInput = (method: string): void => { } const automationInput: AutomationInput = { + /** + * Automations only get one source of input: an object of config values. + * Returns an object with all input keys mapped to their corresponding values. + * + * @see https://airtable.com/developers/scripting/api/input#config + */ config: () => { checkMockInput('config') // @ts-ignore @@ -134,18 +151,37 @@ const automationInput: AutomationInput = { } const extensionInput: ExtensionInput = { + /** + * Prompts the user to enter text. It's similar to prompt() in normal JavaScript, but looks much nicer. + * + * @see https://airtable.com/developers/scripting/api/input#text-async + */ textAsync: (label) => new Promise((resolve) => { checkMockInput('textAsync') // @ts-ignore resolve((__mockInput as MockInput).textAsync(label)) }), + + /** + * Prompts the user to choose one from a list of several options. + +You can mix and match both string and object options. The function will return either the label string, or the value from the object if one is specified. + * + * @see https://airtable.com/developers/scripting/api/input#buttons-async + */ buttonsAsync: (label, options): Promise => new Promise((resolve) => { checkMockInput('buttonsAsync') // @ts-ignore resolve((__mockInput as MockInput).buttonsAsync(label, options)) }), + + /** + * Prompts the user to choose a table from a list of all tables in the base. + * + * @see https://airtable.com/developers/scripting/api/input#table-async + */ tableAsync: (label) => new Promise((resolve) => { checkMockInput('tableAsync') @@ -154,26 +190,50 @@ const extensionInput: ExtensionInput = { // @ts-ignore resolve(globalThis.base.getTable(tableId)) }), - viewAsync: (label, table) => + /** + * Prompts the user to choose a view belonging to the table specified by tableOrTableNameOrTableId + * + * @see https://airtable.com/developers/scripting/api/input#view-async + */ + viewAsync: (label, tableOrTableNameOrTableId) => new Promise((resolve) => { checkMockInput('viewAsync') - const tableObj = - // @ts-ignore - typeof table === 'string' ? globalThis.base.getTable(table) : table + const table = + typeof tableOrTableNameOrTableId === 'string' + ? // @ts-ignore + globalThis.base.getTable(tableOrTableNameOrTableId) + : tableOrTableNameOrTableId // @ts-ignore const viewId = (__mockInput as MockInput).viewAsync(label, table) - resolve(tableObj.getView(viewId)) + resolve(table.getView(viewId)) }), - fieldAsync: (label, table) => + /** + * Prompts the user to choose a field belonging to the table specified by tableOrTableNameOrTableId. + * + * @see https://airtable.com/developers/scripting/api/input#field-async + */ + fieldAsync: (label, tableOrTableNameOrTableId) => new Promise((resolve) => { checkMockInput('fieldAsync') - const tableObj = - // @ts-ignore - typeof table === 'string' ? globalThis.base.getTable(table) : table + const table = + typeof tableOrTableNameOrTableId === 'string' + ? // @ts-ignore + globalThis.base.getTable(tableOrTableNameOrTableId) + : tableOrTableNameOrTableId // @ts-ignore - const fieldId = (__mockInput as MockInput).fieldAsync(label, table) - resolve(tableObj.getField(fieldId)) + const fieldId = (__mockInput as MockInput).fieldAsync( + label, + tableOrTableNameOrTableId + ) + resolve(table.getField(fieldId)) }), + /** + * Expands a list of records in the Airtable UI, and prompts the user to pick one. + +If the user picks a record, the record instance is returned. If the user dismisses the picker, null is returned. If there are no records to pick from in the source, the picker is not shown and null is returned. + * + * @see https://airtable.com/developers/scripting/api/input#record-async + */ recordAsync: ( label, source: Table | View | Array | RecordQueryResult, @@ -207,7 +267,9 @@ const extensionInput: ExtensionInput = { }), /** * Prompts the user to import a file. + * * @todo need to document that the implementer is on their own for parsing + * @see https://airtable.com/developers/scripting/api/input#file-async */ fileAsync: ( label, @@ -222,6 +284,12 @@ const extensionInput: ExtensionInput = { // @ts-ignore resolve((__mockInput as MockInput).fileAsync(label, options)) }), + /** + * The extension input object exposes settings configurable through the settings + * button of the extension. + * + * @see https://airtable.com/developers/scripting/api/config + */ config: Object.assign( (settings: { title: string @@ -233,6 +301,11 @@ const extensionInput: ExtensionInput = { return (__mockInput as MockInput).config(settings, base) }, { + /** + * Defines a setting for a Table. + * + * @see https://airtable.com/developers/scripting/api/config#input-config-table + */ table: ( key: string, options?: @@ -249,6 +322,11 @@ const extensionInput: ExtensionInput = { description: options?.description, } }, + /** + * Defines a setting for a Field. + * + * @see https://airtable.com/developers/scripting/api/config#input-config-field + */ field: ( key: string, options?: @@ -265,6 +343,11 @@ const extensionInput: ExtensionInput = { description: options?.description, } }, + /** + * Defines a setting for a View. + * + * @see https://airtable.com/developers/scripting/api/config#input-config-view + */ view: ( key: string, options?: @@ -282,6 +365,11 @@ const extensionInput: ExtensionInput = { description: options?.description, } }, + /** + * Defines a setting for a text variable. + * + * @see https://airtable.com/developers/scripting/api/config#input-config-text + */ text: ( key: string, options?: @@ -298,6 +386,11 @@ const extensionInput: ExtensionInput = { description: options?.description, } }, + /** + * Defines a setting for a number variable. + * + * @see https://airtable.com/developers/scripting/api/config#input-config-number + */ number: ( key: string, options?: @@ -314,6 +407,12 @@ const extensionInput: ExtensionInput = { description: options?.description, } }, + /** + * Defines a setting for a select option. + * The value returned will be the string value of the currently selected option. + * + * @see https://airtable.com/developers/scripting/api/config#input-config-select + */ select: ( key: string, options: { diff --git a/src/environment/sdk/globals/session.ts b/src/environment/sdk/globals/session.ts index 9089069..a2f9ded 100644 --- a/src/environment/sdk/globals/session.ts +++ b/src/environment/sdk/globals/session.ts @@ -12,6 +12,11 @@ type Session = { } const session: Session = { + /** + * The user currently running the script, or null if the script is running in a publicly shared base. + * + * @see https://airtable.com/developers/scripting/api/session#current-user + */ get currentUser() { if (typeof __currentUser !== 'undefined') { return __currentUser diff --git a/src/index.ts b/src/index.ts index ac69b1b..16166fe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,11 @@ -export { AirtableScriptEnvironment as default } from './environment' - import type { AirtableScriptGlobal } from './environment' import type { RunScriptOptions, RunScriptResult, } from './environment/run-airtable-script' +export { AirtableScriptEnvironment as default } from './environment' + declare global { /** * Runs a given Airtable script against a base fixture. Returns the output, console, and base @@ -26,9 +26,11 @@ declare global { 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.