grist-widget-sdk 0.4.0 → 0.5.0

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 (37) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +79 -208
  3. package/dist/app.cjs +183 -65
  4. package/dist/app.cjs.map +1 -1
  5. package/dist/app.d.cts +5 -100
  6. package/dist/app.d.ts +5 -100
  7. package/dist/app.js +4 -2
  8. package/dist/{chunk-P6VTEKWO.js → chunk-LSALXIGM.js} +82 -10
  9. package/dist/chunk-LSALXIGM.js.map +1 -0
  10. package/dist/{chunk-O7NY46QY.js → chunk-MAPAWVQC.js} +88 -20
  11. package/dist/chunk-MAPAWVQC.js.map +1 -0
  12. package/dist/{chunk-B5ZZNLVS.js → chunk-OXA2VITC.js} +5 -1
  13. package/dist/chunk-OXA2VITC.js.map +1 -0
  14. package/dist/{use-grist-BK0eke9K.d.cts → define-widget-B1FIvI-h.d.cts} +179 -104
  15. package/dist/{use-grist-C5DMZ3wU.d.ts → define-widget-B9gEtJP0.d.ts} +179 -104
  16. package/dist/emulator/testing.cjs +2390 -2243
  17. package/dist/emulator/testing.cjs.map +1 -1
  18. package/dist/emulator/testing.d.cts +27 -9
  19. package/dist/emulator/testing.d.ts +27 -9
  20. package/dist/emulator/testing.js +74 -55
  21. package/dist/emulator/testing.js.map +1 -1
  22. package/dist/emulator.cjs +4 -0
  23. package/dist/emulator.cjs.map +1 -1
  24. package/dist/emulator.d.cts +3 -3
  25. package/dist/emulator.d.ts +3 -3
  26. package/dist/emulator.js +1 -1
  27. package/dist/index.cjs +133 -67
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +2 -2
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.js +5 -1
  32. package/dist/mount-grist-widget-CkB-3iG_.d.ts +137 -0
  33. package/dist/mount-grist-widget-Dtelqao_.d.cts +137 -0
  34. package/package.json +1 -1
  35. package/dist/chunk-B5ZZNLVS.js.map +0 -1
  36. package/dist/chunk-O7NY46QY.js.map +0 -1
  37. package/dist/chunk-P6VTEKWO.js.map +0 -1
@@ -1,9 +1,10 @@
1
1
  import { ReactElement, ComponentType } from 'react';
2
2
  import { RenderOptions, RenderResult } from '@testing-library/react';
3
3
  export { RenderOptions, RenderResult, act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
4
+ import { GristWidget, UseGristOptions } from '../index.cjs';
4
5
  import { CreateGristEmulatorOptions, GristEmulator, EmulatorEvent } from '../emulator.cjs';
5
6
  export { p as presets } from '../templates-CGmRIQBV.cjs';
6
- import { UseGristOptions } from '../index.cjs';
7
+ import { GristWidgetEntry } from '../app.cjs';
7
8
  import { GristReplicaDocument } from '../emulator.cjs';
8
9
 
9
10
  /**
@@ -21,6 +22,19 @@ type RenderWithGristOptions = RenderOptions & {
21
22
  emulator?: CreateGristEmulatorOptions;
22
23
  /** Skip mounting the emulator; you bring your own. Useful for shared fixtures. */
23
24
  existing?: GristEmulator;
25
+ /**
26
+ * The widget's declaration (`defineGristWidget()`) or its `GRIST_OPTIONS`.
27
+ * Given, the test runs the widget as `mountGristWidget()` does inside
28
+ * Grist (provider, boundary, alerts), against a table with a column per
29
+ * declared column, every one mapped, and the access it asks for granted.
30
+ */
31
+ widget?: GristWidget | UseGristOptions;
32
+ /**
33
+ * With `widget`: the table's rows, under the declared names, as typed
34
+ * reads give them (`{ Title: "Ship", Due: new Date(...) }`). The first is
35
+ * selected. Defaults to one row of sample values.
36
+ */
37
+ rows?: ReadonlyArray<Record<string, unknown>>;
24
38
  };
25
39
  type RenderWithGristResult = RenderResult & {
26
40
  emulator: GristEmulator;
@@ -30,8 +44,10 @@ declare function renderWithGrist(ui: ReactElement, options?: RenderWithGristOpti
30
44
  type SmokeTestWidgetOptions = {
31
45
  /** The widget, as `src/main.tsx` passes it to `mountGristWidget()`. */
32
46
  App: ComponentType;
33
- /** The widget's `GRIST_OPTIONS`. */
34
- options: UseGristOptions;
47
+ /** The widget's `GRIST_OPTIONS` (or pass `widget`). */
48
+ options?: UseGristOptions;
49
+ /** The widget's declaration, from `defineGristWidget()` (or pass `options`). */
50
+ widget?: GristWidget;
35
51
  /**
36
52
  * The Grist document to run against. Defaults to one table with a column
37
53
  * per column the widget declares, typed after the first type it accepts,
@@ -49,20 +65,22 @@ type SmokeTestWidgetOptions = {
49
65
  * rejects with the widget's own error if it throws, or with what the page
50
66
  * showed instead if it never gets there. Then unmounts.
51
67
  *
52
- * A widget's whole smoke test:
68
+ * A widget's whole smoke test, given its entry module as `mountGristWidget()`
69
+ * takes it (the widget, and its `widget` declaration or `GRIST_OPTIONS`):
53
70
  *
54
71
  * ```tsx
55
72
  * import { it } from "vitest"
56
73
  * import { smokeTestWidget } from "grist-widget-sdk/emulator/testing"
57
- * import App, { GRIST_OPTIONS } from "./App"
74
+ * import * as entry from "./App"
58
75
  *
59
- * it("renders inside Grist", () => smokeTestWidget({ App, options: GRIST_OPTIONS }))
76
+ * it("renders inside Grist", () => smokeTestWidget(entry))
60
77
  * ```
61
78
  *
62
- * It only reads `GRIST_OPTIONS`, so it stays valid as the widget changes,
63
- * and it improves with each SDK release.
79
+ * It only reads the declaration, so it stays valid as the widget changes,
80
+ * and it improves with each SDK release. `smokeTestWidget({ App, options })`
81
+ * and `smokeTestWidget({ App, widget })` work too.
64
82
  */
65
- declare function smokeTestWidget({ App, options, document, timeout, }: SmokeTestWidgetOptions): Promise<void>;
83
+ declare function smokeTestWidget(input: SmokeTestWidgetOptions | GristWidgetEntry, extra?: Pick<SmokeTestWidgetOptions, "document" | "timeout">): Promise<void>;
66
84
 
67
85
  /**
68
86
  * Browser APIs jsdom lacks but widget UI commonly uses, as inert stand-ins:
@@ -1,9 +1,10 @@
1
1
  import { ReactElement, ComponentType } from 'react';
2
2
  import { RenderOptions, RenderResult } from '@testing-library/react';
3
3
  export { RenderOptions, RenderResult, act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
4
+ import { GristWidget, UseGristOptions } from '../index.js';
4
5
  import { CreateGristEmulatorOptions, GristEmulator, EmulatorEvent } from '../emulator.js';
5
6
  export { p as presets } from '../templates-Cq94qOcf.js';
6
- import { UseGristOptions } from '../index.js';
7
+ import { GristWidgetEntry } from '../app.js';
7
8
  import { GristReplicaDocument } from '../emulator.js';
8
9
 
9
10
  /**
@@ -21,6 +22,19 @@ type RenderWithGristOptions = RenderOptions & {
21
22
  emulator?: CreateGristEmulatorOptions;
22
23
  /** Skip mounting the emulator; you bring your own. Useful for shared fixtures. */
23
24
  existing?: GristEmulator;
25
+ /**
26
+ * The widget's declaration (`defineGristWidget()`) or its `GRIST_OPTIONS`.
27
+ * Given, the test runs the widget as `mountGristWidget()` does inside
28
+ * Grist (provider, boundary, alerts), against a table with a column per
29
+ * declared column, every one mapped, and the access it asks for granted.
30
+ */
31
+ widget?: GristWidget | UseGristOptions;
32
+ /**
33
+ * With `widget`: the table's rows, under the declared names, as typed
34
+ * reads give them (`{ Title: "Ship", Due: new Date(...) }`). The first is
35
+ * selected. Defaults to one row of sample values.
36
+ */
37
+ rows?: ReadonlyArray<Record<string, unknown>>;
24
38
  };
25
39
  type RenderWithGristResult = RenderResult & {
26
40
  emulator: GristEmulator;
@@ -30,8 +44,10 @@ declare function renderWithGrist(ui: ReactElement, options?: RenderWithGristOpti
30
44
  type SmokeTestWidgetOptions = {
31
45
  /** The widget, as `src/main.tsx` passes it to `mountGristWidget()`. */
32
46
  App: ComponentType;
33
- /** The widget's `GRIST_OPTIONS`. */
34
- options: UseGristOptions;
47
+ /** The widget's `GRIST_OPTIONS` (or pass `widget`). */
48
+ options?: UseGristOptions;
49
+ /** The widget's declaration, from `defineGristWidget()` (or pass `options`). */
50
+ widget?: GristWidget;
35
51
  /**
36
52
  * The Grist document to run against. Defaults to one table with a column
37
53
  * per column the widget declares, typed after the first type it accepts,
@@ -49,20 +65,22 @@ type SmokeTestWidgetOptions = {
49
65
  * rejects with the widget's own error if it throws, or with what the page
50
66
  * showed instead if it never gets there. Then unmounts.
51
67
  *
52
- * A widget's whole smoke test:
68
+ * A widget's whole smoke test, given its entry module as `mountGristWidget()`
69
+ * takes it (the widget, and its `widget` declaration or `GRIST_OPTIONS`):
53
70
  *
54
71
  * ```tsx
55
72
  * import { it } from "vitest"
56
73
  * import { smokeTestWidget } from "grist-widget-sdk/emulator/testing"
57
- * import App, { GRIST_OPTIONS } from "./App"
74
+ * import * as entry from "./App"
58
75
  *
59
- * it("renders inside Grist", () => smokeTestWidget({ App, options: GRIST_OPTIONS }))
76
+ * it("renders inside Grist", () => smokeTestWidget(entry))
60
77
  * ```
61
78
  *
62
- * It only reads `GRIST_OPTIONS`, so it stays valid as the widget changes,
63
- * and it improves with each SDK release.
79
+ * It only reads the declaration, so it stays valid as the widget changes,
80
+ * and it improves with each SDK release. `smokeTestWidget({ App, options })`
81
+ * and `smokeTestWidget({ App, widget })` work too.
64
82
  */
65
- declare function smokeTestWidget({ App, options, document, timeout, }: SmokeTestWidgetOptions): Promise<void>;
83
+ declare function smokeTestWidget(input: SmokeTestWidgetOptions | GristWidgetEntry, extra?: Pick<SmokeTestWidgetOptions, "document" | "timeout">): Promise<void>;
66
84
 
67
85
  /**
68
86
  * Browser APIs jsdom lacks but widget UI commonly uses, as inert stand-ins:
@@ -1,22 +1,82 @@
1
1
  import {
2
- GristWidgetApp
3
- } from "../chunk-P6VTEKWO.js";
2
+ GristWidgetApp,
3
+ entryProps
4
+ } from "../chunk-LSALXIGM.js";
4
5
  import {
6
+ encodeGristCell,
7
+ isGristWidget,
5
8
  resetGristForTests
6
- } from "../chunk-O7NY46QY.js";
9
+ } from "../chunk-MAPAWVQC.js";
7
10
  import {
8
11
  createGristEmulator,
9
12
  presets
10
- } from "../chunk-B5ZZNLVS.js";
13
+ } from "../chunk-OXA2VITC.js";
11
14
  import "../chunk-4ZGVKGDF.js";
12
15
 
13
16
  // src/emulator/testing/render-with-grist.tsx
14
17
  import { render } from "@testing-library/react";
18
+
19
+ // src/emulator/testing/declared-document.ts
20
+ function declaredColumns(options) {
21
+ return (options.columns ?? []).map(
22
+ (column) => typeof column === "string" ? { name: column, type: "Any", declared: void 0, allowMultiple: false } : { name: column.name, type: firstType(column.type), declared: column.type, allowMultiple: column.allowMultiple === true }
23
+ );
24
+ }
25
+ function firstType(type) {
26
+ return (type ?? "Any").split(",")[0].trim() || "Any";
27
+ }
28
+ function mapEveryColumn(columns) {
29
+ return Object.fromEntries(columns.map((c) => [c.name, c.allowMultiple ? [c.name] : c.name]));
30
+ }
31
+ function declaredDocument(columns, rows) {
32
+ const given = rows ?? [Object.fromEntries(columns.map((c) => [c.name, sampleValue(c.type)]))];
33
+ return {
34
+ generatedAt: "1970-01-01T00:00:00.000Z",
35
+ docName: "Test document",
36
+ mode: "schema+data",
37
+ tables: {
38
+ Table1: {
39
+ columns: Object.fromEntries(columns.map((c) => [c.name, { type: c.type }])),
40
+ rows: given.map((row, i) => ({
41
+ id: typeof row.id === "number" ? row.id : i + 1,
42
+ ...Object.fromEntries(columns.map((c) => [c.name, encodeGristCell(c.declared, row[c.name] ?? null)]))
43
+ }))
44
+ }
45
+ }
46
+ };
47
+ }
48
+ function sampleValue(type) {
49
+ switch (type) {
50
+ case "Numeric":
51
+ case "Int":
52
+ return 42;
53
+ case "Bool":
54
+ return true;
55
+ case "Date":
56
+ case "DateTime":
57
+ return 1767225600;
58
+ // 2026-01-01, as Grist stores it: epoch seconds
59
+ default:
60
+ return "Sample";
61
+ }
62
+ }
63
+
64
+ // src/emulator/testing/render-with-grist.tsx
65
+ import { jsx } from "react/jsx-runtime";
15
66
  function renderWithGrist(ui, options = {}) {
16
67
  fakeIframeContext();
17
68
  resetGristForTests();
18
- const emulator = options.existing ?? createGristEmulator(options.emulator ?? { document: presets.blank() });
19
- const result = render(ui, options);
69
+ const widgetOptions = options.widget ? isGristWidget(options.widget) ? options.widget.options : options.widget : void 0;
70
+ const columns = widgetOptions ? declaredColumns(widgetOptions) : [];
71
+ const emulatorOptions = widgetOptions && !options.emulator?.document ? {
72
+ ...options.emulator,
73
+ document: declaredDocument(columns, options.rows),
74
+ widgetInteraction: options.emulator?.widgetInteraction ?? { access_level: widgetOptions.requiredAccess ?? "read table" }
75
+ } : options.emulator ?? { document: presets.blank() };
76
+ const emulator = options.existing ?? createGristEmulator(emulatorOptions);
77
+ if (widgetOptions && columns.length > 0) emulator.setColumnMappings(mapEveryColumn(columns));
78
+ const Widget = () => ui;
79
+ const result = render(widgetOptions ? /* @__PURE__ */ jsx(GristWidgetApp, { App: Widget, options: widgetOptions }) : ui, options);
20
80
  const originalUnmount = result.unmount;
21
81
  result.unmount = () => {
22
82
  originalUnmount();
@@ -69,21 +129,19 @@ function installJsdomStubs() {
69
129
  }
70
130
 
71
131
  // src/emulator/testing/smoke-test-widget.tsx
72
- import { jsx } from "react/jsx-runtime";
73
- async function smokeTestWidget({
74
- App,
75
- options,
76
- document,
77
- timeout = 1e4
78
- }) {
132
+ import { jsx as jsx2 } from "react/jsx-runtime";
133
+ async function smokeTestWidget(input, extra = {}) {
134
+ const { App, options: given, widget, document, timeout = 1e4 } = "App" in input ? input : { ...entryProps(input), ...extra };
79
135
  installJsdomStubs();
136
+ const options = given ?? widget?.options;
137
+ if (!options) throw new Error("smokeTestWidget: pass the widget's `widget` (defineGristWidget) or its `options` (GRIST_OPTIONS).");
80
138
  const crash = { error: void 0 };
81
139
  function Probe() {
82
- return /* @__PURE__ */ jsx(CatchRenderError, { crash, children: /* @__PURE__ */ jsx("div", { "data-grist-smoke-test": "", children: /* @__PURE__ */ jsx(App, {}) }) });
140
+ return /* @__PURE__ */ jsx2(CatchRenderError, { crash, children: /* @__PURE__ */ jsx2("div", { "data-grist-smoke-test": "", children: /* @__PURE__ */ jsx2(App, {}) }) });
83
141
  }
84
142
  const columns = declaredColumns(options);
85
- const gristDocument = document ?? smokeTestDocument(columns);
86
- const result = renderWithGrist(/* @__PURE__ */ jsx(GristWidgetApp, { App: Probe, options }), {
143
+ const gristDocument = document ?? declaredDocument(columns);
144
+ const result = renderWithGrist(/* @__PURE__ */ jsx2(GristWidgetApp, { App: Probe, options }), {
87
145
  emulator: {
88
146
  document: gristDocument,
89
147
  // Granted as the user would in Grist's widget panel: the access the
@@ -128,45 +186,6 @@ var CatchRenderError = class extends Component {
128
186
  return this.state.failed ? null : this.props.children;
129
187
  }
130
188
  };
131
- function declaredColumns(options) {
132
- return (options.columns ?? []).map(
133
- (column) => typeof column === "string" ? { name: column, type: "Any", allowMultiple: false } : { name: column.name, type: firstType(column.type), allowMultiple: column.allowMultiple === true }
134
- );
135
- }
136
- function firstType(type) {
137
- return (type ?? "Any").split(",")[0].trim() || "Any";
138
- }
139
- function mapEveryColumn(columns) {
140
- return Object.fromEntries(columns.map((c) => [c.name, c.allowMultiple ? [c.name] : c.name]));
141
- }
142
- function smokeTestDocument(columns) {
143
- return {
144
- generatedAt: "1970-01-01T00:00:00.000Z",
145
- docName: "Smoke test",
146
- mode: "schema+data",
147
- tables: {
148
- Table1: {
149
- columns: Object.fromEntries(columns.map((c) => [c.name, { type: c.type }])),
150
- rows: [{ id: 1, ...Object.fromEntries(columns.map((c) => [c.name, sampleValue(c.type)])) }]
151
- }
152
- }
153
- };
154
- }
155
- function sampleValue(type) {
156
- switch (type) {
157
- case "Numeric":
158
- case "Int":
159
- return 42;
160
- case "Bool":
161
- return true;
162
- case "Date":
163
- case "DateTime":
164
- return 1767225600;
165
- // 2026-01-01, as Grist stores it: epoch seconds
166
- default:
167
- return "Sample";
168
- }
169
- }
170
189
 
171
190
  // src/emulator/testing/matchers.ts
172
191
  function actionsOf(emulator) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/emulator/testing/render-with-grist.tsx","../../src/emulator/testing/smoke-test-widget.tsx","../../src/emulator/testing/jsdom-stubs.ts","../../src/emulator/testing/matchers.ts","../../src/emulator/testing/index.ts"],"sourcesContent":["/**\n * `renderWithGrist(ui, options)` — drop-in replacement for\n * `@testing-library/react`'s `render(ui)` that boots a Grist emulator\n * (inline transport) and exposes it to the test.\n *\n * The emulator is mounted as `window.grist`. The SDK hooks (`useGrist`,\n * `useGristReady`, etc.) detect it via the same path they use in production.\n *\n * Cleanup is automatic when the test's container unmounts.\n */\n\nimport type { ReactElement } from \"react\"\n\nimport { render, type RenderOptions, type RenderResult } from \"@testing-library/react\"\n\nimport { resetGristForTests } from \"../../sdk/use-grist\"\nimport { createGristEmulator } from \"../create-grist-emulator\"\nimport type { CreateGristEmulatorOptions, GristEmulator } from \"../create-grist-emulator\"\nimport { presets } from \"../templates\"\n\nexport type RenderWithGristOptions = RenderOptions & {\n emulator?: CreateGristEmulatorOptions\n /** Skip mounting the emulator; you bring your own. Useful for shared fixtures. */\n existing?: GristEmulator\n}\n\nexport type RenderWithGristResult = RenderResult & {\n emulator: GristEmulator\n}\n\nexport function renderWithGrist(\n ui: ReactElement,\n options: RenderWithGristOptions = {}\n): RenderWithGristResult {\n // Make `window.self !== window.top` evaluate to `true` so\n // `useGristAvailability` reports the widget as embedded. We only do this in\n // jsdom; real browsers already enforce this distinction for iframes.\n fakeIframeContext()\n\n resetGristForTests()\n\n const emulator =\n options.existing ??\n createGristEmulator(options.emulator ?? { document: presets.blank() })\n\n const result = render(ui, options)\n const originalUnmount = result.unmount\n result.unmount = () => {\n originalUnmount()\n if (!options.existing) emulator.dispose()\n resetGristForTests()\n }\n return Object.assign(result, { emulator })\n}\n\nfunction fakeIframeContext() {\n if (typeof window === \"undefined\") return\n if (window.self !== window.top) return // already iframe-like\n try {\n Object.defineProperty(window, \"top\", {\n configurable: true,\n get: () => ({}),\n })\n } catch {\n // Some jsdom versions block redefinition; fail silently — tests using\n // `useGristAvailability` will need to bypass it explicitly in that case.\n }\n}\n","import { Component, type ComponentType, type ReactNode } from \"react\"\nimport { waitFor } from \"@testing-library/react\"\n\nimport { GristWidgetApp } from \"../../app/mount-grist-widget\"\nimport type { GristColumnToMap, GristReplicaDocument, GristWidgetColumnMap, UseGristOptions } from \"../../sdk\"\nimport { installJsdomStubs } from \"./jsdom-stubs\"\nimport { renderWithGrist } from \"./render-with-grist\"\n\nexport type SmokeTestWidgetOptions = {\n /** The widget, as `src/main.tsx` passes it to `mountGristWidget()`. */\n App: ComponentType\n /** The widget's `GRIST_OPTIONS`. */\n options: UseGristOptions\n /**\n * The Grist document to run against. Defaults to one table with a column\n * per column the widget declares, typed after the first type it accepts,\n * and one row of sample values.\n */\n document?: GristReplicaDocument\n /** How long to wait for the widget to render, in ms. Defaults to 10 000. */\n timeout?: number\n}\n\n/**\n * Mounts the widget the way `mountGristWidget()` does inside Grist, against\n * an emulated Grist document with every column it declares mapped and the\n * access it requests granted, and waits until it renders past\n * `GristBoundary`. Resolves once it has,\n * rejects with the widget's own error if it throws, or with what the page\n * showed instead if it never gets there. Then unmounts.\n *\n * A widget's whole smoke test:\n *\n * ```tsx\n * import { it } from \"vitest\"\n * import { smokeTestWidget } from \"grist-widget-sdk/emulator/testing\"\n * import App, { GRIST_OPTIONS } from \"./App\"\n *\n * it(\"renders inside Grist\", () => smokeTestWidget({ App, options: GRIST_OPTIONS }))\n * ```\n *\n * It only reads `GRIST_OPTIONS`, so it stays valid as the widget changes,\n * and it improves with each SDK release.\n */\nexport async function smokeTestWidget({\n App,\n options,\n document,\n timeout = 10_000,\n}: SmokeTestWidgetOptions): Promise<void> {\n installJsdomStubs()\n\n const crash: { error: unknown } = { error: undefined }\n function Probe() {\n return (\n <CatchRenderError crash={crash}>\n <div data-grist-smoke-test=\"\">\n <App />\n </div>\n </CatchRenderError>\n )\n }\n\n const columns = declaredColumns(options)\n const gristDocument = document ?? smokeTestDocument(columns)\n const result = renderWithGrist(<GristWidgetApp App={Probe} options={options} />, {\n emulator: {\n document: gristDocument,\n // Granted as the user would in Grist's widget panel: the access the\n // widget asks for, unless the given document says otherwise.\n widgetInteraction: gristDocument.widgetInteraction ?? { access_level: options.requiredAccess ?? \"read table\" },\n },\n })\n try {\n if (columns.length > 0) result.emulator.setColumnMappings(mapEveryColumn(columns))\n\n try {\n await waitFor(\n () => {\n if (crash.error !== undefined) return\n const probe = result.container.querySelector(\"[data-grist-smoke-test]\")\n if (!probe || !probe.hasChildNodes()) throw new Error(\"not rendered yet\")\n },\n { timeout }\n )\n } catch {\n const shown = result.container.textContent?.trim().replace(/\\s+/g, \" \") || \"nothing\"\n throw new Error(\n `smokeTestWidget: the widget didn't render past GristBoundary within ${timeout} ms. ` +\n `The page showed: \"${shown.slice(0, 300)}\".`\n )\n }\n if (crash.error !== undefined) throw crash.error\n } finally {\n result.unmount()\n }\n}\n\n/** Records the widget's render error, so the test fails with it rather than a timeout. */\nclass CatchRenderError extends Component<{ crash: { error: unknown }; children: ReactNode }, { failed: boolean }> {\n state = { failed: false }\n\n static getDerivedStateFromError() {\n return { failed: true }\n }\n\n componentDidCatch(error: unknown) {\n this.props.crash.error = error\n }\n\n render() {\n return this.state.failed ? null : this.props.children\n }\n}\n\ntype Column = { name: string; type: string; allowMultiple: boolean }\n\nfunction declaredColumns(options: UseGristOptions): Column[] {\n return (options.columns ?? []).map((column: string | GristColumnToMap) =>\n typeof column === \"string\"\n ? { name: column, type: \"Any\", allowMultiple: false }\n : { name: column.name, type: firstType(column.type), allowMultiple: column.allowMultiple === true }\n )\n}\n\n/** The first Grist type a column accepts: `\"Text,Ref,Choice\"` gives `\"Text\"`. */\nfunction firstType(type: string | undefined): string {\n return (type ?? \"Any\").split(\",\")[0].trim() || \"Any\"\n}\n\nfunction mapEveryColumn(columns: Column[]): GristWidgetColumnMap {\n return Object.fromEntries(columns.map((c) => [c.name, c.allowMultiple ? [c.name] : c.name]))\n}\n\n/** One table with a column per declared column, and one row of sample values. */\nfunction smokeTestDocument(columns: Column[]): GristReplicaDocument {\n return {\n generatedAt: \"1970-01-01T00:00:00.000Z\",\n docName: \"Smoke test\",\n mode: \"schema+data\",\n tables: {\n Table1: {\n columns: Object.fromEntries(columns.map((c) => [c.name, { type: c.type }])),\n rows: [{ id: 1, ...Object.fromEntries(columns.map((c) => [c.name, sampleValue(c.type)])) }],\n },\n },\n }\n}\n\nfunction sampleValue(type: string): unknown {\n switch (type) {\n case \"Numeric\":\n case \"Int\":\n return 42\n case \"Bool\":\n return true\n case \"Date\":\n case \"DateTime\":\n return 1767225600 // 2026-01-01, as Grist stores it: epoch seconds\n default:\n return \"Sample\"\n }\n}\n","/**\n * Browser APIs jsdom lacks but widget UI commonly uses, as inert stand-ins:\n * `ResizeObserver` (charts, virtualized lists, popovers) and\n * `window.matchMedia` (the theme, responsive hooks; it reports a light,\n * wide screen). Only installs what's missing, so a test's own mock wins.\n *\n * {@link smokeTestWidget} calls it. For a widget's other tests, call it at\n * the top of the test file or in a Vitest setup file.\n */\nexport function installJsdomStubs(): void {\n if (typeof window === \"undefined\") return\n\n globalThis.ResizeObserver ??= class ResizeObserver {\n observe() {}\n unobserve() {}\n disconnect() {}\n }\n\n window.matchMedia ??= (query: string) =>\n ({\n matches: false,\n media: query,\n onchange: null,\n addEventListener() {},\n removeEventListener() {},\n addListener() {},\n removeListener() {},\n dispatchEvent: () => false,\n }) as MediaQueryList\n}\n","/**\n * Vitest helpers to assert what the widget did to the document.\n *\n * Usage:\n *\n * const { emulator } = renderWithGrist(<MyWidget />)\n * await userEvent.click(screen.getByText(\"Add\"))\n * expect(actionsOf(emulator)).toContainEqual([\"AddRecord\", \"Tasks\", null, { title: \"X\" }])\n */\n\nimport type { GristEmulator } from \"../create-grist-emulator\"\nimport type { EmulatorEvent } from \"../core/event-bus\"\n\nexport function actionsOf(emulator: GristEmulator): unknown[][] {\n return emulator.bus\n .history()\n .filter((e: EmulatorEvent): e is Extract<EmulatorEvent, { type: \"user-action\" }> => e.type === \"user-action\")\n .map((e) => e.action as unknown[])\n}\n\nexport function eventsOf(emulator: GristEmulator): readonly EmulatorEvent[] {\n return emulator.bus.history()\n}\n\n/** Bus event kinds aligned with grist host callbacks (see inline transport). */\nexport type WaitForEventKind = \"ready\" | \"record\" | \"records\" | \"options\" | \"theme\" | \"cursor\"\n\nexport type WaitForEventOptions = {\n timeoutMs?: number\n}\n\nfunction predicateForKind(kind: WaitForEventKind): (event: EmulatorEvent) => boolean {\n switch (kind) {\n case \"ready\":\n return (e) => e.type === \"ready\"\n case \"record\":\n case \"cursor\":\n return (e) => e.type === \"cursor-change\"\n case \"records\":\n return (e) => e.type === \"data-change\"\n case \"options\":\n return (e) => e.type === \"options-change\"\n case \"theme\":\n return (e) => e.type === \"theme-change\"\n }\n}\n\nfunction waitForNextEvent(\n emulator: GristEmulator,\n predicate: (event: EmulatorEvent) => boolean,\n timeoutMs: number,\n label?: string\n): Promise<EmulatorEvent> {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n off()\n const hint = label ? ` (kind: ${label})` : \"\"\n const recent = emulator.bus\n .history()\n .slice(-5)\n .map((e) => e.type)\n .join(\", \")\n reject(\n new Error(\n `waitForEvent timed out after ${timeoutMs}ms${hint}.` +\n (recent ? ` Recent bus events: ${recent}.` : \" No bus events yet.\")\n )\n )\n }, timeoutMs)\n const off = emulator.bus.on((event) => {\n if (predicate(event)) {\n clearTimeout(timer)\n off()\n resolve(event)\n }\n })\n })\n}\n\nexport function waitForEvent(\n emulator: GristEmulator,\n kind: WaitForEventKind,\n options?: WaitForEventOptions\n): Promise<EmulatorEvent>\nexport function waitForEvent(\n emulator: GristEmulator,\n predicate: (event: EmulatorEvent) => boolean,\n timeoutMs?: number\n): Promise<EmulatorEvent>\nexport function waitForEvent(\n emulator: GristEmulator,\n kindOrPredicate: WaitForEventKind | ((event: EmulatorEvent) => boolean),\n timeoutOrOptions: number | WaitForEventOptions = 1000\n): Promise<EmulatorEvent> {\n if (typeof kindOrPredicate === \"string\") {\n const opts = typeof timeoutOrOptions === \"object\" ? timeoutOrOptions : {}\n const timeoutMs = opts.timeoutMs ?? 1000\n return waitForNextEvent(emulator, predicateForKind(kindOrPredicate), timeoutMs, kindOrPredicate)\n }\n const timeoutMs = typeof timeoutOrOptions === \"number\" ? timeoutOrOptions : 1000\n return waitForNextEvent(emulator, kindOrPredicate, timeoutMs)\n}\n","/**\n * Test entry point — `grist-widget-sdk/emulator/testing`.\n *\n * Bundles `renderWithGrist`, matchers, document presets, and the most-used\n * @testing-library/react primitives so a typical test file only needs one\n * import line:\n *\n * import {\n * renderWithGrist, presets, screen, fireEvent, waitFor, actionsOf,\n * } from \"grist-widget-sdk/emulator/testing\"\n */\n\nexport { renderWithGrist } from \"./render-with-grist\"\nexport type {\n RenderWithGristOptions,\n RenderWithGristResult,\n} from \"./render-with-grist\"\n\nexport { smokeTestWidget, type SmokeTestWidgetOptions } from \"./smoke-test-widget\"\nexport { installJsdomStubs } from \"./jsdom-stubs\"\n\nexport {\n actionsOf,\n eventsOf,\n waitForEvent,\n type WaitForEventKind,\n type WaitForEventOptions,\n} from \"./matchers\"\n\n// Document fixtures (re-export from the emulator entry for convenience).\nexport { presets } from \"../templates\"\n\n// @testing-library/react convenience re-exports — see AGENTS.md\nexport {\n act,\n cleanup,\n fireEvent,\n render,\n screen,\n waitFor,\n within,\n} from \"@testing-library/react\"\nexport type { RenderOptions, RenderResult } from \"@testing-library/react\"\n"],"mappings":"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","names":["timeoutMs","render","waitFor"]}
1
+ {"version":3,"sources":["../../src/emulator/testing/render-with-grist.tsx","../../src/emulator/testing/declared-document.ts","../../src/emulator/testing/smoke-test-widget.tsx","../../src/emulator/testing/jsdom-stubs.ts","../../src/emulator/testing/matchers.ts","../../src/emulator/testing/index.ts"],"sourcesContent":["/**\n * `renderWithGrist(ui, options)` — drop-in replacement for\n * `@testing-library/react`'s `render(ui)` that boots a Grist emulator\n * (inline transport) and exposes it to the test.\n *\n * The emulator is mounted as `window.grist`. The SDK hooks (`useGrist`,\n * `useGristReady`, etc.) detect it via the same path they use in production.\n *\n * Cleanup is automatic when the test's container unmounts.\n */\n\nimport type { ReactElement } from \"react\"\n\nimport { render, type RenderOptions, type RenderResult } from \"@testing-library/react\"\n\nimport { GristWidgetApp } from \"../../app/mount-grist-widget\"\nimport type { GristWidget, UseGristOptions } from \"../../sdk\"\nimport { isGristWidget } from \"../../sdk/define-widget\"\nimport { resetGristForTests } from \"../../sdk/use-grist\"\nimport { declaredColumns, declaredDocument, mapEveryColumn } from \"./declared-document\"\nimport { createGristEmulator } from \"../create-grist-emulator\"\nimport type { CreateGristEmulatorOptions, GristEmulator } from \"../create-grist-emulator\"\nimport { presets } from \"../templates\"\n\nexport type RenderWithGristOptions = RenderOptions & {\n emulator?: CreateGristEmulatorOptions\n /** Skip mounting the emulator; you bring your own. Useful for shared fixtures. */\n existing?: GristEmulator\n /**\n * The widget's declaration (`defineGristWidget()`) or its `GRIST_OPTIONS`.\n * Given, the test runs the widget as `mountGristWidget()` does inside\n * Grist (provider, boundary, alerts), against a table with a column per\n * declared column, every one mapped, and the access it asks for granted.\n */\n widget?: GristWidget | UseGristOptions\n /**\n * With `widget`: the table's rows, under the declared names, as typed\n * reads give them (`{ Title: \"Ship\", Due: new Date(...) }`). The first is\n * selected. Defaults to one row of sample values.\n */\n rows?: ReadonlyArray<Record<string, unknown>>\n}\n\nexport type RenderWithGristResult = RenderResult & {\n emulator: GristEmulator\n}\n\nexport function renderWithGrist(\n ui: ReactElement,\n options: RenderWithGristOptions = {}\n): RenderWithGristResult {\n // Make `window.self !== window.top` evaluate to `true` so\n // `useGristAvailability` reports the widget as embedded. We only do this in\n // jsdom; real browsers already enforce this distinction for iframes.\n fakeIframeContext()\n\n resetGristForTests()\n\n const widgetOptions = options.widget ? (isGristWidget(options.widget) ? options.widget.options : options.widget) : undefined\n const columns = widgetOptions ? declaredColumns(widgetOptions) : []\n const emulatorOptions: CreateGristEmulatorOptions =\n widgetOptions && !options.emulator?.document\n ? {\n ...options.emulator,\n document: declaredDocument(columns, options.rows),\n widgetInteraction: options.emulator?.widgetInteraction ?? { access_level: widgetOptions.requiredAccess ?? \"read table\" },\n }\n : (options.emulator ?? { document: presets.blank() })\n const emulator = options.existing ?? createGristEmulator(emulatorOptions)\n if (widgetOptions && columns.length > 0) emulator.setColumnMappings(mapEveryColumn(columns))\n\n const Widget = () => ui\n const result = render(widgetOptions ? <GristWidgetApp App={Widget} options={widgetOptions} /> : ui, options)\n const originalUnmount = result.unmount\n result.unmount = () => {\n originalUnmount()\n if (!options.existing) emulator.dispose()\n resetGristForTests()\n }\n return Object.assign(result, { emulator })\n}\n\nfunction fakeIframeContext() {\n if (typeof window === \"undefined\") return\n if (window.self !== window.top) return // already iframe-like\n try {\n Object.defineProperty(window, \"top\", {\n configurable: true,\n get: () => ({}),\n })\n } catch {\n // Some jsdom versions block redefinition; fail silently — tests using\n // `useGristAvailability` will need to bypass it explicitly in that case.\n }\n}\n","/**\n * A test document from a widget's declared columns: one table, `Table1`,\n * with a column per declared column (typed after the first type it accepts)\n * and the given rows, each value stored as Grist stores it; and the mappings\n * that map every declared column to its namesake. Shared by\n * `smokeTestWidget` and `renderWithGrist({ widget })`.\n */\n\nimport { encodeGristCell } from \"../../sdk/runtime/cells\"\nimport type { GristColumnToMap, GristReplicaDocument, GristWidgetColumnMap, UseGristOptions } from \"../../sdk\"\n\nexport type Column = { name: string; type: string; declared: string | undefined; allowMultiple: boolean }\n\nexport function declaredColumns(options: UseGristOptions): Column[] {\n return (options.columns ?? []).map((column: string | GristColumnToMap) =>\n typeof column === \"string\"\n ? { name: column, type: \"Any\", declared: undefined, allowMultiple: false }\n : { name: column.name, type: firstType(column.type), declared: column.type, allowMultiple: column.allowMultiple === true }\n )\n}\n\n/** The first Grist type a column accepts: `\"Text,Ref,Choice\"` gives `\"Text\"`. */\nfunction firstType(type: string | undefined): string {\n return (type ?? \"Any\").split(\",\")[0].trim() || \"Any\"\n}\n\nexport function mapEveryColumn(columns: Column[]): GristWidgetColumnMap {\n return Object.fromEntries(columns.map((c) => [c.name, c.allowMultiple ? [c.name] : c.name]))\n}\n\n/**\n * One table with a column per declared column, and `rows` (under the declared\n * names, as typed reads give them: a `Date`, a `string[]`…), or one row of\n * sample values.\n */\nexport function declaredDocument(columns: Column[], rows?: ReadonlyArray<Record<string, unknown>>): GristReplicaDocument {\n const given = rows ?? [Object.fromEntries(columns.map((c) => [c.name, sampleValue(c.type)]))]\n return {\n generatedAt: \"1970-01-01T00:00:00.000Z\",\n docName: \"Test document\",\n mode: \"schema+data\",\n tables: {\n Table1: {\n columns: Object.fromEntries(columns.map((c) => [c.name, { type: c.type }])),\n rows: given.map((row, i) => ({\n id: typeof row.id === \"number\" ? row.id : i + 1,\n ...Object.fromEntries(columns.map((c) => [c.name, encodeGristCell(c.declared, row[c.name] ?? null)])),\n })),\n },\n },\n }\n}\n\nfunction sampleValue(type: string): unknown {\n switch (type) {\n case \"Numeric\":\n case \"Int\":\n return 42\n case \"Bool\":\n return true\n case \"Date\":\n case \"DateTime\":\n return 1767225600 // 2026-01-01, as Grist stores it: epoch seconds\n default:\n return \"Sample\"\n }\n}\n","import { Component, type ComponentType, type ReactNode } from \"react\"\nimport { waitFor } from \"@testing-library/react\"\n\nimport { entryProps, GristWidgetApp, type GristWidgetEntry } from \"../../app/mount-grist-widget\"\nimport type { GristReplicaDocument, GristWidget, UseGristOptions } from \"../../sdk\"\nimport { declaredColumns, declaredDocument, mapEveryColumn } from \"./declared-document\"\nimport { installJsdomStubs } from \"./jsdom-stubs\"\nimport { renderWithGrist } from \"./render-with-grist\"\n\nexport type SmokeTestWidgetOptions = {\n /** The widget, as `src/main.tsx` passes it to `mountGristWidget()`. */\n App: ComponentType\n /** The widget's `GRIST_OPTIONS` (or pass `widget`). */\n options?: UseGristOptions\n /** The widget's declaration, from `defineGristWidget()` (or pass `options`). */\n widget?: GristWidget\n /**\n * The Grist document to run against. Defaults to one table with a column\n * per column the widget declares, typed after the first type it accepts,\n * and one row of sample values.\n */\n document?: GristReplicaDocument\n /** How long to wait for the widget to render, in ms. Defaults to 10 000. */\n timeout?: number\n}\n\n/**\n * Mounts the widget the way `mountGristWidget()` does inside Grist, against\n * an emulated Grist document with every column it declares mapped and the\n * access it requests granted, and waits until it renders past\n * `GristBoundary`. Resolves once it has,\n * rejects with the widget's own error if it throws, or with what the page\n * showed instead if it never gets there. Then unmounts.\n *\n * A widget's whole smoke test, given its entry module as `mountGristWidget()`\n * takes it (the widget, and its `widget` declaration or `GRIST_OPTIONS`):\n *\n * ```tsx\n * import { it } from \"vitest\"\n * import { smokeTestWidget } from \"grist-widget-sdk/emulator/testing\"\n * import * as entry from \"./App\"\n *\n * it(\"renders inside Grist\", () => smokeTestWidget(entry))\n * ```\n *\n * It only reads the declaration, so it stays valid as the widget changes,\n * and it improves with each SDK release. `smokeTestWidget({ App, options })`\n * and `smokeTestWidget({ App, widget })` work too.\n */\nexport async function smokeTestWidget(\n input: SmokeTestWidgetOptions | GristWidgetEntry,\n extra: Pick<SmokeTestWidgetOptions, \"document\" | \"timeout\"> = {}\n): Promise<void> {\n const { App, options: given, widget, document, timeout = 10_000 } =\n \"App\" in input ? input : { ...entryProps(input), ...extra }\n installJsdomStubs()\n const options = given ?? widget?.options\n if (!options) throw new Error(\"smokeTestWidget: pass the widget's `widget` (defineGristWidget) or its `options` (GRIST_OPTIONS).\")\n\n const crash: { error: unknown } = { error: undefined }\n function Probe() {\n return (\n <CatchRenderError crash={crash}>\n <div data-grist-smoke-test=\"\">\n <App />\n </div>\n </CatchRenderError>\n )\n }\n\n const columns = declaredColumns(options)\n const gristDocument = document ?? declaredDocument(columns)\n const result = renderWithGrist(<GristWidgetApp App={Probe} options={options} />, {\n emulator: {\n document: gristDocument,\n // Granted as the user would in Grist's widget panel: the access the\n // widget asks for, unless the given document says otherwise.\n widgetInteraction: gristDocument.widgetInteraction ?? { access_level: options.requiredAccess ?? \"read table\" },\n },\n })\n try {\n if (columns.length > 0) result.emulator.setColumnMappings(mapEveryColumn(columns))\n\n try {\n await waitFor(\n () => {\n if (crash.error !== undefined) return\n const probe = result.container.querySelector(\"[data-grist-smoke-test]\")\n if (!probe || !probe.hasChildNodes()) throw new Error(\"not rendered yet\")\n },\n { timeout }\n )\n } catch {\n const shown = result.container.textContent?.trim().replace(/\\s+/g, \" \") || \"nothing\"\n throw new Error(\n `smokeTestWidget: the widget didn't render past GristBoundary within ${timeout} ms. ` +\n `The page showed: \"${shown.slice(0, 300)}\".`\n )\n }\n if (crash.error !== undefined) throw crash.error\n } finally {\n result.unmount()\n }\n}\n\n/** Records the widget's render error, so the test fails with it rather than a timeout. */\nclass CatchRenderError extends Component<{ crash: { error: unknown }; children: ReactNode }, { failed: boolean }> {\n state = { failed: false }\n\n static getDerivedStateFromError() {\n return { failed: true }\n }\n\n componentDidCatch(error: unknown) {\n this.props.crash.error = error\n }\n\n render() {\n return this.state.failed ? null : this.props.children\n }\n}\n","/**\n * Browser APIs jsdom lacks but widget UI commonly uses, as inert stand-ins:\n * `ResizeObserver` (charts, virtualized lists, popovers) and\n * `window.matchMedia` (the theme, responsive hooks; it reports a light,\n * wide screen). Only installs what's missing, so a test's own mock wins.\n *\n * {@link smokeTestWidget} calls it. For a widget's other tests, call it at\n * the top of the test file or in a Vitest setup file.\n */\nexport function installJsdomStubs(): void {\n if (typeof window === \"undefined\") return\n\n globalThis.ResizeObserver ??= class ResizeObserver {\n observe() {}\n unobserve() {}\n disconnect() {}\n }\n\n window.matchMedia ??= (query: string) =>\n ({\n matches: false,\n media: query,\n onchange: null,\n addEventListener() {},\n removeEventListener() {},\n addListener() {},\n removeListener() {},\n dispatchEvent: () => false,\n }) as MediaQueryList\n}\n","/**\n * Vitest helpers to assert what the widget did to the document.\n *\n * Usage:\n *\n * const { emulator } = renderWithGrist(<MyWidget />)\n * await userEvent.click(screen.getByText(\"Add\"))\n * expect(actionsOf(emulator)).toContainEqual([\"AddRecord\", \"Tasks\", null, { title: \"X\" }])\n */\n\nimport type { GristEmulator } from \"../create-grist-emulator\"\nimport type { EmulatorEvent } from \"../core/event-bus\"\n\nexport function actionsOf(emulator: GristEmulator): unknown[][] {\n return emulator.bus\n .history()\n .filter((e: EmulatorEvent): e is Extract<EmulatorEvent, { type: \"user-action\" }> => e.type === \"user-action\")\n .map((e) => e.action as unknown[])\n}\n\nexport function eventsOf(emulator: GristEmulator): readonly EmulatorEvent[] {\n return emulator.bus.history()\n}\n\n/** Bus event kinds aligned with grist host callbacks (see inline transport). */\nexport type WaitForEventKind = \"ready\" | \"record\" | \"records\" | \"options\" | \"theme\" | \"cursor\"\n\nexport type WaitForEventOptions = {\n timeoutMs?: number\n}\n\nfunction predicateForKind(kind: WaitForEventKind): (event: EmulatorEvent) => boolean {\n switch (kind) {\n case \"ready\":\n return (e) => e.type === \"ready\"\n case \"record\":\n case \"cursor\":\n return (e) => e.type === \"cursor-change\"\n case \"records\":\n return (e) => e.type === \"data-change\"\n case \"options\":\n return (e) => e.type === \"options-change\"\n case \"theme\":\n return (e) => e.type === \"theme-change\"\n }\n}\n\nfunction waitForNextEvent(\n emulator: GristEmulator,\n predicate: (event: EmulatorEvent) => boolean,\n timeoutMs: number,\n label?: string\n): Promise<EmulatorEvent> {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n off()\n const hint = label ? ` (kind: ${label})` : \"\"\n const recent = emulator.bus\n .history()\n .slice(-5)\n .map((e) => e.type)\n .join(\", \")\n reject(\n new Error(\n `waitForEvent timed out after ${timeoutMs}ms${hint}.` +\n (recent ? ` Recent bus events: ${recent}.` : \" No bus events yet.\")\n )\n )\n }, timeoutMs)\n const off = emulator.bus.on((event) => {\n if (predicate(event)) {\n clearTimeout(timer)\n off()\n resolve(event)\n }\n })\n })\n}\n\nexport function waitForEvent(\n emulator: GristEmulator,\n kind: WaitForEventKind,\n options?: WaitForEventOptions\n): Promise<EmulatorEvent>\nexport function waitForEvent(\n emulator: GristEmulator,\n predicate: (event: EmulatorEvent) => boolean,\n timeoutMs?: number\n): Promise<EmulatorEvent>\nexport function waitForEvent(\n emulator: GristEmulator,\n kindOrPredicate: WaitForEventKind | ((event: EmulatorEvent) => boolean),\n timeoutOrOptions: number | WaitForEventOptions = 1000\n): Promise<EmulatorEvent> {\n if (typeof kindOrPredicate === \"string\") {\n const opts = typeof timeoutOrOptions === \"object\" ? timeoutOrOptions : {}\n const timeoutMs = opts.timeoutMs ?? 1000\n return waitForNextEvent(emulator, predicateForKind(kindOrPredicate), timeoutMs, kindOrPredicate)\n }\n const timeoutMs = typeof timeoutOrOptions === \"number\" ? timeoutOrOptions : 1000\n return waitForNextEvent(emulator, kindOrPredicate, timeoutMs)\n}\n","/**\n * Test entry point — `grist-widget-sdk/emulator/testing`.\n *\n * Bundles `renderWithGrist`, matchers, document presets, and the most-used\n * @testing-library/react primitives so a typical test file only needs one\n * import line:\n *\n * import {\n * renderWithGrist, presets, screen, fireEvent, waitFor, actionsOf,\n * } from \"grist-widget-sdk/emulator/testing\"\n */\n\nexport { renderWithGrist } from \"./render-with-grist\"\nexport type {\n RenderWithGristOptions,\n RenderWithGristResult,\n} from \"./render-with-grist\"\n\nexport { smokeTestWidget, type SmokeTestWidgetOptions } from \"./smoke-test-widget\"\nexport { installJsdomStubs } from \"./jsdom-stubs\"\n\nexport {\n actionsOf,\n eventsOf,\n waitForEvent,\n type WaitForEventKind,\n type WaitForEventOptions,\n} from \"./matchers\"\n\n// Document fixtures (re-export from the emulator entry for convenience).\nexport { presets } from \"../templates\"\n\n// @testing-library/react convenience re-exports — see AGENTS.md\nexport {\n act,\n cleanup,\n fireEvent,\n render,\n screen,\n waitFor,\n within,\n} from \"@testing-library/react\"\nexport type { RenderOptions, RenderResult } from \"@testing-library/react\"\n"],"mappings":"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","names":["jsx","timeoutMs","render","waitFor"]}
package/dist/emulator.cjs CHANGED
@@ -729,6 +729,7 @@ function normalizeColumnarForTable(table, columns) {
729
729
  }
730
730
  function normalizeCellValueForType(value, type) {
731
731
  if (value === null || value === void 0 || typeof type !== "string") return value;
732
+ if (isListColumnType(type) && Array.isArray(value) && value[0] !== "L") return ["E", "KeyError"];
732
733
  if (type.startsWith("Ref:")) return normalizeRefValue(value);
733
734
  if (type.startsWith("RefList:")) return normalizeRefListValue(value);
734
735
  return value;
@@ -746,6 +747,9 @@ function extractRefRowId(value) {
746
747
  if (fromTuple !== void 0 && fromTuple > 0) return Math.trunc(fromTuple);
747
748
  return toPositiveInt(value);
748
749
  }
750
+ function isListColumnType(type) {
751
+ return type === "ChoiceList" || type === "Attachments" || type.startsWith("RefList:");
752
+ }
749
753
  function normalizeRefValue(value) {
750
754
  return extractRefRowId(value);
751
755
  }