@baselift/blocks-testing 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +177 -0
  2. package/dist/cjs/error_utils.js +56 -0
  3. package/dist/cjs/index.js +24 -0
  4. package/dist/cjs/inject_mock_airtable_interface.js +6 -0
  5. package/dist/cjs/mock_airtable_interface.js +851 -0
  6. package/dist/cjs/private_utils.js +78 -0
  7. package/dist/cjs/test_driver.js +658 -0
  8. package/dist/cjs/test_mutations.js +37 -0
  9. package/dist/cjs/vacant_airtable_interface.js +367 -0
  10. package/dist/types/src/error_utils.d.ts +11 -0
  11. package/dist/types/src/error_utils.d.ts.map +1 -0
  12. package/dist/types/src/index.d.ts +4 -0
  13. package/dist/types/src/index.d.ts.map +1 -0
  14. package/dist/types/src/inject_mock_airtable_interface.d.ts +2 -0
  15. package/dist/types/src/inject_mock_airtable_interface.d.ts.map +1 -0
  16. package/dist/types/src/mock_airtable_interface.d.ts +237 -0
  17. package/dist/types/src/mock_airtable_interface.d.ts.map +1 -0
  18. package/dist/types/src/private_utils.d.ts +33 -0
  19. package/dist/types/src/private_utils.d.ts.map +1 -0
  20. package/dist/types/src/test_driver.d.ts +450 -0
  21. package/dist/types/src/test_driver.d.ts.map +1 -0
  22. package/dist/types/src/test_mutations.d.ts +43 -0
  23. package/dist/types/src/test_mutations.d.ts.map +1 -0
  24. package/dist/types/src/vacant_airtable_interface.d.ts +2 -0
  25. package/dist/types/src/vacant_airtable_interface.d.ts.map +1 -0
  26. package/dist/types/test/index_compatible.test.d.ts +8 -0
  27. package/dist/types/test/index_compatible.test.d.ts.map +1 -0
  28. package/dist/types/test/index_incompatible.test.d.ts +2 -0
  29. package/dist/types/test/index_incompatible.test.d.ts.map +1 -0
  30. package/dist/types/test/mock_airtable_interface.test.d.ts +2 -0
  31. package/dist/types/test/mock_airtable_interface.test.d.ts.map +1 -0
  32. package/dist/types/test/mutation_types.test.d.ts +2 -0
  33. package/dist/types/test/mutation_types.test.d.ts.map +1 -0
  34. package/dist/types/test/package_json.test.d.ts +2 -0
  35. package/dist/types/test/package_json.test.d.ts.map +1 -0
  36. package/dist/types/test/test_driver.test.d.ts +2 -0
  37. package/dist/types/test/test_driver.test.d.ts.map +1 -0
  38. package/dist/types/test/untestable_bindings.test.d.ts +2 -0
  39. package/dist/types/test/untestable_bindings.test.d.ts.map +1 -0
  40. package/package.json +120 -0
  41. package/types/globals.d.ts +6 -0
@@ -0,0 +1,658 @@
1
+ "use strict";
2
+
3
+ require("core-js/modules/es.symbol.js");
4
+ require("core-js/modules/es.symbol.description.js");
5
+ require("core-js/modules/es.array.from.js");
6
+ require("core-js/modules/es.array.iterator.js");
7
+ require("core-js/modules/es.array.slice.js");
8
+ require("core-js/modules/es.regexp.exec.js");
9
+ require("core-js/modules/es.regexp.to-string.js");
10
+ require("core-js/modules/web.dom-collections.iterator.js");
11
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
12
+ Object.defineProperty(exports, "__esModule", {
13
+ value: true
14
+ });
15
+ exports.default = void 0;
16
+ require("core-js/modules/es.array.filter.js");
17
+ require("core-js/modules/es.array.map.js");
18
+ require("core-js/modules/es.array.reduce.js");
19
+ require("core-js/modules/es.object.to-string.js");
20
+ var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
21
+ var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
22
+ var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
23
+ var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
24
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
25
+ var _react = _interopRequireDefault(require("react"));
26
+ var _unstable_testing_utils = require("@baselift/blocks/unstable_testing_utils");
27
+ var _ui = require("@baselift/blocks/ui");
28
+ var _error_utils = require("./error_utils");
29
+ var _mock_airtable_interface = _interopRequireDefault(require("./mock_airtable_interface"));
30
+ var _test_mutations = require("./test_mutations");
31
+ function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
32
+ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
33
+ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
34
+ /**
35
+ * An object describing a {@link Table}, a {@link View}, or both.
36
+ */
37
+ /**
38
+ * A class designed to facilitate the automated testing of Airtable Extensions
39
+ * outside of a production Extensions environment. Each instance creates a simulated
40
+ * {@link Base} which is distinct from any other Base created in this way.
41
+ * Custom Extensions can be instantiated using an instance of this class; see {@link
42
+ * Container|the `Container` method}.
43
+ *
44
+ * The example code for this class's methods is written in terms of a
45
+ * non-existent Airtable Extension called `MyCustomExtension`. Each example includes a
46
+ * description of the presumed behavior for that Extension. Consumers of this library
47
+ * will work with their own Extensions whose behavior differs from these examples, so
48
+ * their tests will be distinct in this regard.
49
+ *
50
+ * @docsPath testing/TestDriver
51
+ */
52
+ var TestDriver = exports.default = /*#__PURE__*/function () {
53
+ /**
54
+ * Create an instance of the test driver, initializing a simulated Airtable
55
+ * Base to take the state described by the provided fixture data.
56
+ */
57
+ function TestDriver(fixtureData) {
58
+ (0, _classCallCheck2.default)(this, TestDriver);
59
+ /** @internal */
60
+ (0, _defineProperty2.default)(this, "_airtableInterface", void 0);
61
+ /** @internal */
62
+ (0, _defineProperty2.default)(this, "_sdk", void 0);
63
+ this._airtableInterface = new _mock_airtable_interface.default(fixtureData);
64
+ this._sdk = new _unstable_testing_utils.Sdk(this._airtableInterface);
65
+ this.Container = this.Container.bind(this);
66
+ }
67
+
68
+ /**
69
+ * The simulated {@link Base} associated with this instance.
70
+ */
71
+ return (0, _createClass2.default)(TestDriver, [{
72
+ key: "base",
73
+ get: function get() {
74
+ return this._sdk.base;
75
+ }
76
+
77
+ /**
78
+ * The {@link Cursor} instance associated with this instance's Base.
79
+ */
80
+ }, {
81
+ key: "cursor",
82
+ get: function get() {
83
+ return this._sdk.cursor;
84
+ }
85
+
86
+ /**
87
+ * A {@link Session} instance. This will correspond to the first
88
+ * collaborator in your fixture data.
89
+ */
90
+ }, {
91
+ key: "session",
92
+ get: function get() {
93
+ return this._sdk.session;
94
+ }
95
+
96
+ /**
97
+ * A simulated {@link GlobalConfig} instance. This always starts empty.
98
+ */
99
+ }, {
100
+ key: "globalConfig",
101
+ get: function get() {
102
+ return this._sdk.globalConfig;
103
+ }
104
+
105
+ /**
106
+ * A React Component which may be used to wrap Extension Components, enabling
107
+ * them to run outside of a production Extensions environment.
108
+ *
109
+ * @example
110
+ * ```js
111
+ * import TestDriver from '@baselift/blocks-testing';
112
+ * // Given MyCustomExtension, an Airtable Extension which defines a React Component...
113
+ * const MyCustomExtension = require('../src/my_custom_extension');
114
+ * // And given myFixtureData, a data structure describing the initial
115
+ * // state of a simulated Airtable Base...
116
+ * const myFixtureData = require('./my_fixture_data');
117
+ *
118
+ * const testDriver = new TestDriver(myFixtureData);
119
+ *
120
+ * render(
121
+ * <testDriver.Container>
122
+ * <MyCustomExtension />
123
+ * </testDriver.Container>
124
+ * );
125
+ * ```
126
+ */
127
+ }, {
128
+ key: "Container",
129
+ value: function Container(_ref) {
130
+ var children = _ref.children;
131
+ var fallback = /*#__PURE__*/_react.default.createElement('div', null);
132
+ return /*#__PURE__*/_react.default.createElement(_react.default.Suspense, {
133
+ fallback: fallback
134
+ }, /*#__PURE__*/_react.default.createElement(_ui.BaseProvider, {
135
+ value: this.base
136
+ }, children));
137
+ }
138
+
139
+ /**
140
+ * Destroy a {@link Field} in the simulated {@link Base}.
141
+ *
142
+ * @example
143
+ * ```js
144
+ * import TestDriver from '@baselift/blocks-testing';
145
+ * // Given MyCustomExtension, an Airtable Extension which displays the names of all
146
+ * // the Fields in the active Table...
147
+ * import MyCustomExtension from '../src/my_custom_extension';
148
+ * // And given myFixtureData, a data structure describing an Airtable
149
+ * // Base which contains a Table named "Table One" with three Fields...
150
+ * import myFixtureData from './my_fixture_data';
151
+ * import {render, screen} from '@testing-library/react';
152
+ *
153
+ * const testDriver = new TestDriver(myFixtureData);
154
+ * let items, itemTexts;
155
+ *
156
+ * render(
157
+ * <testDriver.Container>
158
+ * <MyCustomExtension />
159
+ * </testDriver.Container>
160
+ * );
161
+ *
162
+ * // Verify that MyExtension initially displays all three Fields
163
+ * items = screen.getAllByRole('listitem');
164
+ * itemTexts = items.map((el) => el.textContent);
165
+ * expect(itemTexts).toEqual(['1st field', '2nd field', '3rd field']);
166
+ *
167
+ * // Simulate the destruction of the Field named "2nd field"
168
+ * await testDriver.deleteFieldAsync('Table One', '2nd field');
169
+ *
170
+ * // Verify that MyExtension correctly updates to describe the two remaining
171
+ * // Fields
172
+ * items = screen.getAllByRole('listitem');
173
+ * itemTexts = items.map((el) => el.textContent);
174
+ * expect(itemTexts).toEqual(['1st field', '3rd field']);
175
+ * ```
176
+ */
177
+ }, {
178
+ key: "deleteFieldAsync",
179
+ value: (function () {
180
+ var _deleteFieldAsync = (0, _asyncToGenerator2.default)(/*#__PURE__*/_regenerator.default.mark(function _callee(tableIdOrName, fieldIdOrName) {
181
+ var table, field;
182
+ return _regenerator.default.wrap(function (_context) {
183
+ while (1) switch (_context.prev = _context.next) {
184
+ case 0:
185
+ table = this.base.getTable(tableIdOrName);
186
+ field = table.getField(fieldIdOrName);
187
+ _context.next = 1;
188
+ return this._airtableInterface.applyMutationAsync({
189
+ type: _test_mutations.TestMutationTypes.DELETE_SINGLE_FIELD,
190
+ tableId: table.id,
191
+ id: field.id
192
+ });
193
+ case 1:
194
+ case "end":
195
+ return _context.stop();
196
+ }
197
+ }, _callee, this);
198
+ }));
199
+ function deleteFieldAsync(_x, _x2) {
200
+ return _deleteFieldAsync.apply(this, arguments);
201
+ }
202
+ return deleteFieldAsync;
203
+ }()
204
+ /**
205
+ * Destroy a {@link Table} in the simulated {@link Base}.
206
+ *
207
+ * @example
208
+ * ```js
209
+ * import TestDriver from '@baselift/blocks-testing';
210
+ * // Given MyCustomExtension, an Airtable Extension which displays the names of all
211
+ * // the Tables in the Base...
212
+ * import MyCustomExtension from '../src/my_custom_extension';
213
+ * // And given myFixtureData, a data structure describing an Airtable
214
+ * // Base which contains three Tables...
215
+ * import myFixtureData from './my_fixture_data';
216
+ * import {render, screen} from '@testing-library/react';
217
+ *
218
+ * const testDriver = new TestDriver(myFixtureData);
219
+ * let items, itemTexts;
220
+ *
221
+ * render(
222
+ * <testDriver.Container>
223
+ * <MyCustomExtension />
224
+ * </testDriver.Container>
225
+ * );
226
+ *
227
+ * // Verify that MyExtension initially displays all three Tables
228
+ * items = screen.getAllByRole('listitem');
229
+ * itemTexts = items.map((el) => el.textContent);
230
+ * expect(itemTexts).toEqual(['1st table', '2nd table', '3rd table']);
231
+ *
232
+ * // Simulate the destruction of the Table named "2nd table"
233
+ * testDriver.deleteTable('2nd table');
234
+ *
235
+ * // Verify that MyExtension correctly updates to describe the two remaining
236
+ * // Table
237
+ * items = screen.getAllByRole('listitem');
238
+ * itemTexts = items.map((el) => el.textContent);
239
+ * expect(itemTexts).toEqual(['1st table', '3rd table']);
240
+ * ```
241
+ */
242
+ )
243
+ }, {
244
+ key: "deleteTable",
245
+ value: function deleteTable(tableIdOrName) {
246
+ var table = this.base.getTable(tableIdOrName);
247
+ var newOrder = this.base.tables.filter(_ref2 => {
248
+ var id = _ref2.id;
249
+ return table.id !== id;
250
+ }).map(_ref3 => {
251
+ var id = _ref3.id;
252
+ return id;
253
+ });
254
+ (0, _error_utils.invariant)(newOrder.length > 0, 'Table with ID "%s" may not be deleted because it is the only Table present in the Base', table.id);
255
+ var updates = [{
256
+ path: ['tableOrder'],
257
+ value: newOrder
258
+ }, {
259
+ path: ['tablesById', table.id],
260
+ value: undefined
261
+ }];
262
+ if (table.id === this._airtableInterface.sdkInitData.baseData.activeTableId) {
263
+ updates.push({
264
+ path: ['activeTableId'],
265
+ value: newOrder[0]
266
+ });
267
+ }
268
+ this._airtableInterface.triggerModelUpdates(updates);
269
+ }
270
+
271
+ /**
272
+ * Destroy a {@link View} in the simulated {@link Base}.
273
+ *
274
+ * @example
275
+ * ```js
276
+ * import TestDriver from '@baselift/blocks-testing';
277
+ * // Given MyCustomExtension, an Airtable Extension which displays the names of all
278
+ * // the Views in the active Table...
279
+ * import MyCustomExtension from '../src/my_custom_extension';
280
+ * // And given myFixtureData, a data structure describing an Airtable
281
+ * // Base which contains a Table named "Table One" with three Views...
282
+ * import myFixtureData from './my_fixture_data';
283
+ * import {render, screen} from '@testing-library/react';
284
+ *
285
+ * const testDriver = new TestDriver(myFixtureData);
286
+ * let items, itemTexts;
287
+ *
288
+ * render(
289
+ * <testDriver.Container>
290
+ * <MyCustomExtension />
291
+ * </testDriver.Container>
292
+ * );
293
+ *
294
+ * // Verify that MyExtension initially displays all three Views
295
+ * items = screen.getAllByRole('listitem');
296
+ * itemTexts = items.map((el) => el.textContent);
297
+ * expect(itemTexts).toEqual(['1st view', '2nd view', '3rd view']);
298
+ *
299
+ * // Simulate the destruction of the Field named "2nd view"
300
+ * await testDriver.deleteViewAsync('Table One', '2nd view');
301
+ *
302
+ * // Verify that MyExtension correctly updates to describe the two remaining
303
+ * // Views
304
+ * items = screen.getAllByRole('listitem');
305
+ * itemTexts = items.map((el) => el.textContent);
306
+ * expect(itemTexts).toEqual(['1st view', '3rd view']);
307
+ * ```
308
+ */
309
+ }, {
310
+ key: "deleteViewAsync",
311
+ value: (function () {
312
+ var _deleteViewAsync = (0, _asyncToGenerator2.default)(/*#__PURE__*/_regenerator.default.mark(function _callee2(tableIdOrName, viewIdOrName) {
313
+ var table, view;
314
+ return _regenerator.default.wrap(function (_context2) {
315
+ while (1) switch (_context2.prev = _context2.next) {
316
+ case 0:
317
+ table = this.base.getTable(tableIdOrName);
318
+ view = table.getView(viewIdOrName);
319
+ _context2.next = 1;
320
+ return this._airtableInterface.applyMutationAsync({
321
+ type: _test_mutations.TestMutationTypes.DELETE_SINGLE_VIEW,
322
+ tableId: table.id,
323
+ id: view.id
324
+ });
325
+ case 1:
326
+ case "end":
327
+ return _context2.stop();
328
+ }
329
+ }, _callee2, this);
330
+ }));
331
+ function deleteViewAsync(_x3, _x4) {
332
+ return _deleteViewAsync.apply(this, arguments);
333
+ }
334
+ return deleteViewAsync;
335
+ }()
336
+ /**
337
+ * Update the active {@link Table} and/or the active {@link View} of the
338
+ * Extension's {@link Cursor}. Either `table` or `view` must be specified.
339
+ *
340
+ * @example
341
+ * ```js
342
+ * testDriver.setActiveCursorModels({view: 'My grid view'});
343
+ * ```
344
+ *
345
+ * @example
346
+ * ```js
347
+ * import TestDriver from '@baselift/blocks-testing';
348
+ * // Given MyCustomExtension, an Airtable Extension which displays the names of the
349
+ * // active Table...
350
+ * import MyCustomExtension from '../src/my_custom_extension';
351
+ * // And given myFixtureData, a data structure describing an Airtable
352
+ * // Base which contains two Tables...
353
+ * import myFixtureData from './my_fixture_data';
354
+ * import {render, screen} from '@testing-library/react';
355
+ *
356
+ * const testDriver = new TestDriver(myFixtureData);
357
+ * let heading;
358
+ *
359
+ * render(
360
+ * <testDriver.Container>
361
+ * <MyCustomExtension />
362
+ * </testDriver.Container>
363
+ * );
364
+ *
365
+ * // Verify that MyExtension initially displays the first Table
366
+ * heading = screen.getByRole('heading');
367
+ * expect(heading.textContent).toBe('First table');
368
+ *
369
+ * // Simulate the end user selecting the second Table from the Airtable
370
+ * // user interface
371
+ * testDriver.setActiveCursorModels(({table: 'Second table'});
372
+ *
373
+ * // Verify that MyExtension correctly updates to describe the newly-selected
374
+ * // Table
375
+ * heading = screen.getByRole('heading');
376
+ * expect(heading.textContent).toBe('Second table');
377
+ * ```
378
+ */
379
+ )
380
+ }, {
381
+ key: "setActiveCursorModels",
382
+ value: function setActiveCursorModels(tableAndOrView) {
383
+ var tableIdOrName = tableAndOrView.table,
384
+ viewIdOrName = tableAndOrView.view;
385
+ (0, _error_utils.invariant)(tableIdOrName || viewIdOrName, 'One of `table` or `view` must be specified.');
386
+ var tableId = tableIdOrName ? this.base.getTable(tableIdOrName).id : this.cursor.activeTableId;
387
+ (0, _error_utils.invariant)(typeof tableId === 'string', 'Cannot set cursor model when table is not loaded');
388
+ var viewId = viewIdOrName ? this.base.getTable(tableId).getView(viewIdOrName).id : this.cursor.activeViewId;
389
+ (0, _error_utils.invariant)(!viewId || this.base.getTable(tableId).getViewIfExists(viewId), 'Cannot change active table to "%s" because active view ("%s") belongs to another table', tableIdOrName, viewId);
390
+ this._airtableInterface.triggerModelUpdates([{
391
+ path: ['activeTableId'],
392
+ value: tableId
393
+ }]);
394
+ if (viewIdOrName) {
395
+ this._airtableInterface.triggerModelUpdates([{
396
+ path: ['tablesById', tableId, 'activeViewId'],
397
+ value: viewId
398
+ }]);
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Specify the outcome of internal permission checks. This influences the
404
+ * behavior of not only explicit permission checks from Extensions code but also
405
+ * the outcome of model operations such as {@link createRecordsAsync}.
406
+ *
407
+ * @example
408
+ * ```js
409
+ * import TestDriver from '@baselift/blocks-testing';
410
+ * // Given MyCustomExtension, an Airtable Extension which displays a button labeled
411
+ * // "Add" and which disables that button for users who lack "write"
412
+ * // permissions to the Base...
413
+ * import MyCustomExtension from '../src/my_custom_extension';
414
+ * // And given myFixtureData, a data structure describing the initial
415
+ * // state of a simulated Airtable Base...
416
+ * import myFixtureData from './my_fixture_data';
417
+ * import {render, screen} from '@testing-library/react';
418
+ *
419
+ * const testDriver = new TestDriver(myFixtureData);
420
+ *
421
+ * // Configure the test driver to reject all "create record" mutations.
422
+ * testDriver.simulatePermissionCheck((mutation) => {
423
+ * return mutation.type !== 'createMultipleRecords';
424
+ * });
425
+ *
426
+ * render(
427
+ * <testDriver.Container>
428
+ * <MyCustomExtension />
429
+ * </testDriver.Container>
430
+ * );
431
+ *
432
+ * // Verify that MyCustomExtension recognizes that the current user may not
433
+ * // create Records and that disables the corresponding aspect of the user
434
+ * // interface.
435
+ * const button = screen.getByRole('button', {name: 'Add'});
436
+ * expect(button.disabled).toBe(true);
437
+ * ```
438
+ */
439
+ }, {
440
+ key: "simulatePermissionCheck",
441
+ value: function simulatePermissionCheck(check) {
442
+ this._airtableInterface.setUserPermissionCheck(check);
443
+ }
444
+
445
+ /**
446
+ * Specify the outcome of a request for the user to select a record in the
447
+ * UI created by {@link expandRecordPickerAsync}.
448
+ *
449
+ * @example
450
+ * ```js
451
+ * import TestDriver from '@baselift/blocks-testing';
452
+ * // Given MyCustomExtension, an Airtable Extension which prompts the end user to
453
+ * // select a Record and displays the name of the Record they selected...
454
+ * import MyCustomExtension from '../src/my_custom_extension';
455
+ * // And given myFixtureData, a data structure describing the initial
456
+ * // state of a simulated Airtable Base...
457
+ * import myFixtureData from './my_fixture_data';
458
+ * import {render, screen} from '@testing-library/react';
459
+ * import userEvent from '@testing-library/user-event';
460
+ *
461
+ * const testDriver = new TestDriver(myFixtureData);
462
+ *
463
+ * testDriver.simulateExpandedRecordSelection((tableId, recordIds) => {
464
+ * return recordIds[1];
465
+ * });
466
+ *
467
+ * render(
468
+ * <testDriver.Container>
469
+ * <MyCustomExtension />
470
+ * </testDriver.Container>
471
+ * );
472
+ *
473
+ * // Simulate a user clicking on a button in MyCustomExtension labeled with the
474
+ * // text "Choose record". If MyCustomExtension reacts to this event by invoking
475
+ * // the SDK's `expandRecordPickerAsync`, then it will receive the second
476
+ * // available record due to the function that is provided to
477
+ * // `simulateExpandedRecordSelection` above.
478
+ * const button = screen.getByRole('button', {name: 'Choose record'});
479
+ * userEvent.click(button);
480
+ *
481
+ * // Verify that MyCustomExtension correctly responds to the simulated user's
482
+ * // input
483
+ * const heading = await waitFor(() => screen.getByRole('heading'));
484
+ * expect(heading.textContent)
485
+ * .toBe('You selected the record named "Number Two"');
486
+ * ```
487
+ */
488
+ }, {
489
+ key: "simulateExpandedRecordSelection",
490
+ value: function simulateExpandedRecordSelection(pickRecord) {
491
+ this._airtableInterface.setPickRecord(pickRecord);
492
+ }
493
+
494
+ /**
495
+ * Simulate a user visually selecting a set of {@link Record|Records} in
496
+ * the active {@link Table}. This operation is unrelated to an Extension's
497
+ * programmatic "selection" of records via, e.g. {@link
498
+ * Table.selectRecords}. To deselect all records, invoke this method with
499
+ * an empty array.
500
+ *
501
+ * @example
502
+ * ```js
503
+ * import TestDriver from '@baselift/blocks-testing';
504
+ * // Given MyCustomExtension, an Airtable Extension which displays the number of
505
+ * // Records that an end user has selected in the active Table...
506
+ * import MyCustomExtension from '../src/my_custom_extension';
507
+ * // And given myFixtureData, a data structure describing the initial
508
+ * // state of a simulated Airtable Base...
509
+ * import myFixtureData from './my_fixture_data';
510
+ * import {render, screen} from '@testing-library/react';
511
+ *
512
+ * const testDriver = new TestDriver(myFixtureData);
513
+ *
514
+ * render(
515
+ * <testDriver.Container>
516
+ * <MyCustomExtension />
517
+ * </testDriver.Container>
518
+ * );
519
+ *
520
+ * // Retrieve all the Records present in the first Table in the Base
521
+ * const records = await testDriver.base.tables[0].selectRecordsAsync();
522
+ *
523
+ * // Simulate an end-user selecting the second and fourth Record
524
+ * testDriver.userSelectRecords([records[1].id, records[3].id]);
525
+ *
526
+ * // Verify that MyCustomExtension correctly responds to the simulated user's
527
+ * // input
528
+ * const heading = await waitFor(() => screen.getByRole('heading'));
529
+ * expect(heading.textContent).toBe('2 records selected');
530
+ * ```
531
+ */
532
+ }, {
533
+ key: "userSelectRecords",
534
+ value: function userSelectRecords(recordIds) {
535
+ var baseData = this._airtableInterface.sdkInitData.baseData;
536
+ var activeTableId = baseData.activeTableId;
537
+ (0, _error_utils.invariant)(activeTableId, 'Cannot select records when no table is active');
538
+ var _iterator = _createForOfIteratorHelper(recordIds),
539
+ _step;
540
+ try {
541
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
542
+ var recordId = _step.value;
543
+ (0, _error_utils.invariant)(this._airtableInterface.hasRecord(activeTableId, recordId), 'Record with ID "%s" is not present in active table "%s"', recordId, activeTableId);
544
+ }
545
+ } catch (err) {
546
+ _iterator.e(err);
547
+ } finally {
548
+ _iterator.f();
549
+ }
550
+ var selectedRecordIdSet = recordIds.reduce((all, recordId) => {
551
+ all[recordId] = true;
552
+ return all;
553
+ }, {});
554
+ this._airtableInterface.triggerModelUpdates([{
555
+ path: ['cursorData', 'selectedRecordIdSet'],
556
+ value: selectedRecordIdSet
557
+ }]);
558
+ }
559
+
560
+ /**
561
+ * Register a function to be invoked in response to a given internal event.
562
+ * See {@link WatchableKeysAndArgs} for the available keys and the values
563
+ * which are included when they are emitted.
564
+ *
565
+ * @example
566
+ * ```js
567
+ * import TestDriver from '@baselift/blocks-testing';
568
+ * // Given MyCustomExtension, an Airtable Extension which presents the user with one
569
+ * // button for each available Record, and which responds to button clicks
570
+ * // by expanding the Record in the Airtable user interface...
571
+ * import MyCustomExtension from '../src/my_custom_extension';
572
+ * // And given myFixtureData, a data structure describing an Airtable
573
+ * // Base which contains a Table with three records...
574
+ * import myFixtureData from './my_fixture_data';
575
+ * import {render, screen} from '@testing-library/react';
576
+ * import userEvent from '@testing-library/user-event';
577
+ *
578
+ * const testDriver = new TestDriver(myFixtureData);
579
+ *
580
+ * // Keep track of every time MyCustomExtension attempts to expand a Record in
581
+ * // the Airtable user interface
582
+ * let expandedRecordIds = [];
583
+ * testDriver.watch('expandRecord', ({recordId}) => {
584
+ * expendedRecordIds.push(recordId);
585
+ * });
586
+ *
587
+ * render(
588
+ * <testDriver.Container>
589
+ * <MyCustomExtension />
590
+ * </testDriver.Container>
591
+ * );
592
+ *
593
+ * // Verify that MyCustomExtension does not expand any Records prior to user
594
+ * // interaction
595
+ * expect(expandedRecords).toEqual([]);
596
+ *
597
+ * // Simulate a user clicking on the second button in MyCustomExtension, which
598
+ * // is expected to correspond to the second Record in the simulated Base
599
+ * const buttons = screen.getAllByRole('button');
600
+ * userEvent.click(buttons[1]);
601
+ *
602
+ * // Verify that MyCustomExtension correctly expanded the second Record in the
603
+ * // Airtable user interface
604
+ * expect(expandedRecords).toEqual(['rec2']);
605
+ * ```
606
+ */
607
+ }, {
608
+ key: "watch",
609
+ value: function watch(key, fn) {
610
+ this._airtableInterface.on(key, fn);
611
+ }
612
+
613
+ /**
614
+ * De-register a function which was previously registered with {@link
615
+ * watch}. See {@link WatchableKeysAndArgs} for the available keys.
616
+ *
617
+ * @example
618
+ * ```js
619
+ * import TestDriver from '@baselift/blocks-testing';
620
+ * // Given MyCustomExtension, an Airtable Extension which enters "full screen" mode
621
+ * // in response to certain interactions...
622
+ * const MyCustomExtension = require('../src/my_custom_extension');
623
+ * // And given myFixtureData, a data structure describing the initial
624
+ * // state of a simulated Airtable Base...
625
+ * const myFixtureData = require('./my_fixture_data');
626
+ *
627
+ * let testDriver;
628
+ * let enterCount;
629
+ * let increment = () => {
630
+ * enterCount += 1;
631
+ * });
632
+ *
633
+ * // Configure the test runner to create a TestDriver instance before
634
+ * // every test and to listen for requests to enter "full screen" mode
635
+ * beforeEach(() => {
636
+ * testDriver = new TestDriver(myFixtureData);
637
+ * enterCount = 0;
638
+ * testDriver.watch('enterFullscreen', increment);
639
+ * });
640
+ *
641
+ * // Configure the test runner to remove the event listener after every
642
+ * // test. (The next test will have a new instance of TestDriver with its
643
+ * // own event handler, so this one is no longer necessary.)
644
+ * afterEach(() => {
645
+ * testDriver.unwatch('enterFullscreen', increment);
646
+ * });
647
+ *
648
+ * // (include tests using the `testDriver` and `enterCount` variables
649
+ * // here)
650
+ * ```
651
+ */
652
+ }, {
653
+ key: "unwatch",
654
+ value: function unwatch(key, fn) {
655
+ this._airtableInterface.off(key, fn);
656
+ }
657
+ }]);
658
+ }();