@shopify/ui-extensions-tester 2026.4.0-rc.2 → 2026.4.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 (36) hide show
  1. package/README.md +66 -14
  2. package/build/cjs/_virtual/_rollupPluginBabelHelpers.js +15 -0
  3. package/build/cjs/admin/factories.js +18 -0
  4. package/build/cjs/api-version.js +2 -2
  5. package/build/cjs/checkout/factories.js +0 -5
  6. package/build/cjs/index.js +209 -86
  7. package/build/cjs/point-of-sale/factories.js +6 -1
  8. package/build/esm/_virtual/_rollupPluginBabelHelpers.mjs +10 -0
  9. package/build/esm/admin/factories.mjs +18 -0
  10. package/build/esm/api-version.mjs +2 -2
  11. package/build/esm/checkout/factories.mjs +0 -5
  12. package/build/esm/index.mjs +208 -87
  13. package/build/esm/point-of-sale/factories.mjs +6 -1
  14. package/build/esnext/_virtual/_rollupPluginBabelHelpers.esnext +10 -0
  15. package/build/esnext/admin/factories.esnext +18 -0
  16. package/build/esnext/api-version.esnext +2 -2
  17. package/build/esnext/checkout/factories.esnext +0 -5
  18. package/build/esnext/index.esnext +208 -87
  19. package/build/esnext/point-of-sale/factories.esnext +6 -1
  20. package/build/ts/admin/factories.d.ts.map +1 -1
  21. package/build/ts/checkout/factories.d.ts.map +1 -1
  22. package/build/ts/disposable.d.ts +20 -0
  23. package/build/ts/index.d.ts +62 -14
  24. package/build/ts/index.d.ts.map +1 -1
  25. package/build/ts/point-of-sale/factories.d.ts.map +1 -1
  26. package/build/tsconfig.tsbuildinfo +1 -1
  27. package/package.json +2 -2
  28. package/src/admin/README.md +6 -0
  29. package/src/admin/factories.ts +22 -0
  30. package/src/checkout/factories.ts +0 -1
  31. package/src/disposable.d.ts +20 -0
  32. package/src/index.ts +214 -119
  33. package/src/point-of-sale/factories.ts +8 -1
  34. package/src/tests/admin-factories.test.ts +25 -1
  35. package/src/tests/getExtension.test.ts +93 -1
  36. package/src/tests/setUpExtension.test.ts +52 -0
@@ -1,3 +1,4 @@
1
+ import { classPrivateFieldLooseBase as _classPrivateFieldLooseBase, classPrivateFieldLooseKey as _classPrivateFieldLooseKey } from './_virtual/_rollupPluginBabelHelpers.esnext';
1
2
  import * as fs from 'fs';
2
3
  import * as path from 'path';
3
4
  import { isCheckoutTarget } from './targets.esnext';
@@ -7,6 +8,8 @@ export { createNavigationHistoryEntry } from './navigation.esnext';
7
8
  import { API_VERSION } from './api-version.esnext';
8
9
  import { installFetchPolyfills, uninstallFetchPolyfills } from './fetch-polyfills.esnext';
9
10
 
11
+ var _dispose;
12
+
10
13
  /**
11
14
  * Makes all properties in the API deeply mutable so tests can
12
15
  * override any value through the `extension.shopify` proxy:
@@ -15,6 +18,168 @@ import { installFetchPolyfills, uninstallFetchPolyfills } from './fetch-polyfill
15
18
  * extension.shopify.i18n.translate = (key) => myTranslations[key];
16
19
  */
17
20
 
21
+ /**
22
+ * `Symbol.dispose` for runtimes that support it, with a polyfill
23
+ * fallback so the library works on older Node versions too.
24
+ */
25
+ const SymbolDispose = (_dispose = Symbol.dispose) !== null && _dispose !== void 0 ? _dispose : Symbol.for('Symbol.dispose');
26
+
27
+ /**
28
+ * Members shared by both {@link ExtensionHarness} (returned by
29
+ * `getExtension`) and {@link DisposableExtensionHarness} (returned
30
+ * by `setUpExtension`).
31
+ */
32
+
33
+ /**
34
+ * Returned by `getExtension`. The caller is responsible for calling
35
+ * `setUp()` before each test and `tearDown()` after.
36
+ */
37
+
38
+ /**
39
+ * Returned by `setUpExtension`. Already set up — tears down
40
+ * automatically via `Symbol.dispose` (the `using` keyword):
41
+ *
42
+ * ```ts
43
+ * using extension = setUpExtension('purchase.checkout.block.render');
44
+ * ```
45
+ */
46
+ var _target = /*#__PURE__*/_classPrivateFieldLooseKey("target");
47
+ var _resolvedModule = /*#__PURE__*/_classPrivateFieldLooseKey("resolvedModule");
48
+ var _modulePath = /*#__PURE__*/_classPrivateFieldLooseKey("modulePath");
49
+ var _checkout = /*#__PURE__*/_classPrivateFieldLooseKey("checkout");
50
+ var _networkAccess = /*#__PURE__*/_classPrivateFieldLooseKey("networkAccess");
51
+ var _apiAccess = /*#__PURE__*/_classPrivateFieldLooseKey("apiAccess");
52
+ var _fetchImpl = /*#__PURE__*/_classPrivateFieldLooseKey("fetchImpl");
53
+ var _previousFetch = /*#__PURE__*/_classPrivateFieldLooseKey("previousFetch");
54
+ var _navigationImpl = /*#__PURE__*/_classPrivateFieldLooseKey("navigationImpl");
55
+ var _previousNavigation = /*#__PURE__*/_classPrivateFieldLooseKey("previousNavigation");
56
+ class Extension {
57
+ constructor(target, options) {
58
+ var _options$configSearch;
59
+ Object.defineProperty(this, _target, {
60
+ writable: true,
61
+ value: void 0
62
+ });
63
+ Object.defineProperty(this, _resolvedModule, {
64
+ writable: true,
65
+ value: void 0
66
+ });
67
+ Object.defineProperty(this, _modulePath, {
68
+ writable: true,
69
+ value: void 0
70
+ });
71
+ Object.defineProperty(this, _checkout, {
72
+ writable: true,
73
+ value: void 0
74
+ });
75
+ Object.defineProperty(this, _networkAccess, {
76
+ writable: true,
77
+ value: void 0
78
+ });
79
+ Object.defineProperty(this, _apiAccess, {
80
+ writable: true,
81
+ value: void 0
82
+ });
83
+ Object.defineProperty(this, _fetchImpl, {
84
+ writable: true,
85
+ value: void 0
86
+ });
87
+ Object.defineProperty(this, _previousFetch, {
88
+ writable: true,
89
+ value: void 0
90
+ });
91
+ Object.defineProperty(this, _navigationImpl, {
92
+ writable: true,
93
+ value: createMockNavigation()
94
+ });
95
+ Object.defineProperty(this, _previousNavigation, {
96
+ writable: true,
97
+ value: void 0
98
+ });
99
+ const configSearchDir = (_options$configSearch = options === null || options === void 0 ? void 0 : options.configSearchDir) !== null && _options$configSearch !== void 0 ? _options$configSearch : path.dirname(getCallerFile());
100
+ const tomlPath = findToml(configSearchDir);
101
+ const tomlDir = path.dirname(tomlPath);
102
+ const tomlContent = fs.readFileSync(tomlPath, 'utf-8');
103
+ validateApiVersion(tomlContent);
104
+ const modulePath = parseTargetModule(tomlContent, target);
105
+ _classPrivateFieldLooseBase(this, _target)[_target] = target;
106
+ _classPrivateFieldLooseBase(this, _modulePath)[_modulePath] = modulePath;
107
+ _classPrivateFieldLooseBase(this, _resolvedModule)[_resolvedModule] = path.resolve(tomlDir, modulePath);
108
+ _classPrivateFieldLooseBase(this, _checkout)[_checkout] = isCheckoutTarget(target);
109
+ _classPrivateFieldLooseBase(this, _networkAccess)[_networkAccess] = _classPrivateFieldLooseBase(this, _checkout)[_checkout] && parseNetworkAccess(tomlContent);
110
+ _classPrivateFieldLooseBase(this, _apiAccess)[_apiAccess] = _classPrivateFieldLooseBase(this, _checkout)[_checkout] && parseApiAccess(tomlContent);
111
+ }
112
+ setUp() {
113
+ installFetchPolyfills();
114
+ _classPrivateFieldLooseBase(this, _fetchImpl)[_fetchImpl] = _classPrivateFieldLooseBase(this, _checkout)[_checkout] && !_classPrivateFieldLooseBase(this, _networkAccess)[_networkAccess] && !_classPrivateFieldLooseBase(this, _apiAccess)[_apiAccess] ? async () => {
115
+ // Checkout is the only surface that currently enforces
116
+ // fetch capabilities.
117
+ throw new Error('fetch() is not available. Add network_access = true or ' + 'api_access = true to [extensions.capabilities] in shopify.extension.toml.');
118
+ } : async () => new Response();
119
+ _classPrivateFieldLooseBase(this, _previousFetch)[_previousFetch] = globalThis.fetch;
120
+ _classPrivateFieldLooseBase(this, _previousNavigation)[_previousNavigation] = globalThis.navigation;
121
+ _classPrivateFieldLooseBase(this, _navigationImpl)[_navigationImpl] = createMockNavigation();
122
+ globalThis.shopify = deepWritableProxy(createMockTargetApi(_classPrivateFieldLooseBase(this, _target)[_target]));
123
+ globalThis.fetch = _classPrivateFieldLooseBase(this, _fetchImpl)[_fetchImpl];
124
+ globalThis.navigation = _classPrivateFieldLooseBase(this, _navigationImpl)[_navigationImpl];
125
+ }
126
+ get shopify() {
127
+ if (!globalThis.shopify) {
128
+ throw new Error('You must call extension.setUp() before accessing extension.shopify.');
129
+ }
130
+ return globalThis.shopify;
131
+ }
132
+ get fetch() {
133
+ return _classPrivateFieldLooseBase(this, _fetchImpl)[_fetchImpl];
134
+ }
135
+ set fetch(fn) {
136
+ _classPrivateFieldLooseBase(this, _fetchImpl)[_fetchImpl] = fn;
137
+ globalThis.fetch = fn;
138
+ }
139
+ get navigation() {
140
+ return _classPrivateFieldLooseBase(this, _navigationImpl)[_navigationImpl];
141
+ }
142
+ set navigation(obj) {
143
+ _classPrivateFieldLooseBase(this, _navigationImpl)[_navigationImpl] = obj;
144
+ globalThis.navigation = obj;
145
+ }
146
+ async render() {
147
+ const mod = await import(_classPrivateFieldLooseBase(this, _resolvedModule)[_resolvedModule]);
148
+ const renderFn = mod.default;
149
+ if (typeof renderFn !== 'function') {
150
+ throw new Error(`Expected default export of ${_classPrivateFieldLooseBase(this, _modulePath)[_modulePath]} to be a function, got ${typeof renderFn}`);
151
+ }
152
+ await renderFn();
153
+ }
154
+ tearDown() {
155
+ // Dynamically import preact to unmount cleanly without requiring
156
+ // the test file to depend on preact directly.
157
+ try {
158
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
159
+ const {
160
+ render
161
+ } = require('preact');
162
+ render(null, document.body);
163
+ } catch {
164
+ // Fallback if preact isn't available
165
+ document.body.innerHTML = '';
166
+ }
167
+ delete globalThis.shopify;
168
+ if (_classPrivateFieldLooseBase(this, _previousFetch)[_previousFetch] === undefined) {
169
+ delete globalThis.fetch;
170
+ } else {
171
+ globalThis.fetch = _classPrivateFieldLooseBase(this, _previousFetch)[_previousFetch];
172
+ }
173
+ if (_classPrivateFieldLooseBase(this, _previousNavigation)[_previousNavigation] === undefined) {
174
+ delete globalThis.navigation;
175
+ } else {
176
+ globalThis.navigation = _classPrivateFieldLooseBase(this, _previousNavigation)[_previousNavigation];
177
+ }
178
+ uninstallFetchPolyfills();
179
+ }
180
+ }
181
+ const extensionCache = new Map();
182
+
18
183
  /**
19
184
  * Returns an extension test harness for the given target.
20
185
  *
@@ -31,92 +196,46 @@ import { installFetchPolyfills, uninstallFetchPolyfills } from './fetch-polyfill
31
196
  * `shopify.extension.toml`. Defaults to the calling test file's directory.
32
197
  */
33
198
  function getExtension(target, options) {
34
- var _options$configSearch;
35
- const configSearchDir = (_options$configSearch = options === null || options === void 0 ? void 0 : options.configSearchDir) !== null && _options$configSearch !== void 0 ? _options$configSearch : path.dirname(getCallerFile());
36
- const tomlPath = findToml(configSearchDir);
37
- const tomlDir = path.dirname(tomlPath);
38
- const tomlContent = fs.readFileSync(tomlPath, 'utf-8');
39
- validateApiVersion(tomlContent);
40
- const modulePath = parseTargetModule(tomlContent, target);
41
- const resolvedModule = path.resolve(tomlDir, modulePath);
42
- const checkout = isCheckoutTarget(target);
43
- const networkAccess = checkout && parseNetworkAccess(tomlContent);
44
- const apiAccess = checkout && parseApiAccess(tomlContent);
45
- let fetchImpl;
46
- let previousFetch;
47
- let navigationImpl = createMockNavigation();
48
- let previousNavigation;
49
- const ext = {
50
- setUp() {
51
- installFetchPolyfills();
52
- fetchImpl = checkout && !networkAccess && !apiAccess ? async () => {
53
- // Checkout is the only surface that currently enforces
54
- // fetch capabilities.
55
- throw new Error('fetch() is not available. Add network_access = true or ' + 'api_access = true to [extensions.capabilities] in shopify.extension.toml.');
56
- } : async () => new Response();
57
- previousFetch = globalThis.fetch;
58
- previousNavigation = globalThis.navigation;
59
- globalThis.shopify = deepWritableProxy(createMockTargetApi(target));
60
- globalThis.fetch = fetchImpl;
61
- globalThis.navigation = navigationImpl;
62
- },
63
- get shopify() {
64
- if (!globalThis.shopify) {
65
- throw new Error('You must call extension.setUp() before accessing extension.shopify.');
66
- }
67
- return globalThis.shopify;
68
- },
69
- get fetch() {
70
- return fetchImpl;
71
- },
72
- set fetch(fn) {
73
- fetchImpl = fn;
74
- globalThis.fetch = fn;
75
- },
76
- get navigation() {
77
- return navigationImpl;
78
- },
79
- set navigation(obj) {
80
- navigationImpl = obj;
81
- globalThis.navigation = obj;
82
- },
83
- async render() {
84
- const mod = await import(resolvedModule);
85
- const renderFn = mod.default;
86
- if (typeof renderFn !== 'function') {
87
- throw new Error(`Expected default export of ${modulePath} to be a function, got ${typeof renderFn}`);
88
- }
89
- await renderFn();
90
- },
91
- tearDown() {
92
- // Dynamically import preact to unmount cleanly without requiring
93
- // the test file to depend on preact directly.
94
- try {
95
- // eslint-disable-next-line @typescript-eslint/no-var-requires
96
- const {
97
- render
98
- } = require('preact');
99
- render(null, document.body);
100
- } catch {
101
- // Fallback if preact isn't available
102
- document.body.innerHTML = '';
103
- }
104
- delete globalThis.shopify;
105
- if (previousFetch === undefined) {
106
- delete globalThis.fetch;
107
- } else {
108
- globalThis.fetch = previousFetch;
109
- }
110
- if (previousNavigation === undefined) {
111
- delete globalThis.navigation;
112
- } else {
113
- globalThis.navigation = previousNavigation;
114
- }
115
- uninstallFetchPolyfills();
116
- }
117
- };
199
+ var _options$configSearch2;
200
+ const resolvedConfigSearchDir = (_options$configSearch2 = options === null || options === void 0 ? void 0 : options.configSearchDir) !== null && _options$configSearch2 !== void 0 ? _options$configSearch2 : path.dirname(getCallerFile());
201
+ const tomlPath = findToml(resolvedConfigSearchDir);
202
+ const tomlMtimeMs = fs.statSync(tomlPath).mtimeMs;
203
+ const cacheKey = JSON.stringify([target, tomlPath, tomlMtimeMs]);
204
+ const cached = extensionCache.get(cacheKey);
205
+ if (cached) {
206
+ return cached;
207
+ }
208
+ const ext = new Extension(target, {
209
+ configSearchDir: resolvedConfigSearchDir
210
+ });
211
+ extensionCache.set(cacheKey, ext);
118
212
  return ext;
119
213
  }
214
+
215
+ /**
216
+ * Sets up an extension for testing and returns a disposable object
217
+ * that supports automatic teardown with the `using` keyword:
218
+ *
219
+ * ```ts
220
+ * test('rendering the extension', async () => {
221
+ * using extension = setUpExtension(
222
+ * 'purchase.checkout.block.render',
223
+ * );
224
+ * await extension.render();
225
+ * // tearDown() is called automatically at the end of the block
226
+ * });
227
+ * ```
228
+ *
229
+ * @param target - The extension target to mock.
230
+ * @param options - Optional configuration (same as {@link getExtension}).
231
+ */
232
+ function setUpExtension(target, options) {
233
+ const extension = getExtension(target, options);
234
+ extension.setUp();
235
+ return Object.assign(extension, {
236
+ [SymbolDispose]: () => extension.tearDown()
237
+ });
238
+ }
120
239
  function validateApiVersion(toml) {
121
240
  const match = toml.match(/^\s*api_version\s*=\s*"([^"]+)"/m);
122
241
  const tomlVersion = match === null || match === void 0 ? void 0 : match[1];
@@ -184,8 +303,10 @@ function getCallerFile() {
184
303
  const err = new Error();
185
304
  let callerFile = '';
186
305
  Error.prepareStackTrace = (_err, stack) => {
187
- // stack[0] is getCallerFile, stack[1] is getExtension, stack[2] is the caller
188
- for (let i = 2; i < stack.length; i++) {
306
+ // Walk the stack, skipping all frames that originate from this
307
+ // package file. This works regardless of whether the caller is
308
+ // getExtension() or setUpExtension() → getExtension().
309
+ for (let i = 1; i < stack.length; i++) {
189
310
  const fileName = stack[i].getFileName();
190
311
  if (fileName && fileName !== thisPackageFilePath) {
191
312
  callerFile = fileName;
@@ -316,4 +437,4 @@ function deepWritableProxy(obj) {
316
437
  });
317
438
  }
318
439
 
319
- export { getExtension };
440
+ export { SymbolDispose, getExtension, setUpExtension };
@@ -53,9 +53,14 @@ function createTransaction() {
53
53
  lineItems: []
54
54
  };
55
55
  }
56
+ function createConnectivityApiContent() {
57
+ return {
58
+ current: createReadonlySignalLike(createConnectivityState())
59
+ };
60
+ }
56
61
  function createMockBaseEventData() {
57
62
  return {
58
- connectivity: createConnectivityState(),
63
+ connectivity: createConnectivityApiContent(),
59
64
  device: {
60
65
  name: 'Mock POS Device',
61
66
  deviceId: 1,
@@ -1 +1 @@
1
- {"version":3,"file":"factories.d.ts","sourceRoot":"","sources":["../../../src/admin/factories.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,gBAAgB,EAMjB,MAAM,8BAA8B,CAAC;AAKtC;;;;;;GAMG;AACH,MAAM,MAAM,oBAAoB,CAAC,CAAC,SAAS,eAAe,IACxD,gBAAgB,CAAC,CAAC,CAAC,SAAS;IAAC,GAAG,EAAE,MAAM,GAAG,CAAA;CAAC,GACxC,IAAI,CAAC,GAAG,EAAE,KAAK,MAAM,EAAE,CAAC,GACxB,KAAK,CAAC;AAqBZ,wBAAgB,aAAa,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAQ7D;AAuTD;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,SAAS,eAAe,EAChE,MAAM,EAAE,CAAC,GACR,oBAAoB,CAAC,CAAC,CAAC,CASzB"}
1
+ {"version":3,"file":"factories.d.ts","sourceRoot":"","sources":["../../../src/admin/factories.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,gBAAgB,EAMjB,MAAM,8BAA8B,CAAC;AAKtC;;;;;;GAMG;AACH,MAAM,MAAM,oBAAoB,CAAC,CAAC,SAAS,eAAe,IACxD,gBAAgB,CAAC,CAAC,CAAC,SAAS;IAAC,GAAG,EAAE,MAAM,GAAG,CAAA;CAAC,GACxC,IAAI,CAAC,GAAG,EAAE,KAAK,MAAM,EAAE,CAAC,GACxB,KAAK,CAAC;AAsBZ,wBAAgB,aAAa,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAQ7D;AA4UD;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,SAAS,eAAe,EAChE,MAAM,EAAE,CAAC,GACR,oBAAoB,CAAC,CAAC,CAAC,CASzB"}
@@ -1 +1 @@
1
- {"version":3,"file":"factories.d.ts","sourceRoot":"","sources":["../../../src/checkout/factories.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EAahB,MAAM,iCAAiC,CAAC;AAiczC;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAAC,CAAC,SAAS,eAAe,EACnE,MAAM,EAAE,CAAC,GACR,eAAe,CAAC,CAAC,CAAC,CASpB"}
1
+ {"version":3,"file":"factories.d.ts","sourceRoot":"","sources":["../../../src/checkout/factories.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EAahB,MAAM,iCAAiC,CAAC;AAgczC;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAAC,CAAC,SAAS,eAAe,EACnE,MAAM,EAAE,CAAC,GACR,eAAe,CAAC,CAAC,CAAC,CASpB"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Polyfill type declarations for the Explicit Resource Management proposal
3
+ * (https://github.com/tc39/proposal-explicit-resource-management).
4
+ *
5
+ * TypeScript >= 5.2 ships these in `lib.esnext.disposable.d.ts`.
6
+ * For older versions we declare them here so that consumers can write
7
+ * `using extension = setUpExtension(...)` with full type safety.
8
+ *
9
+ * These declarations are additive — if `Symbol.dispose` already exists
10
+ * in the type system (TS >= 5.2 with `lib: ["esnext"]`), the interfaces
11
+ * merge harmlessly.
12
+ */
13
+
14
+ interface SymbolConstructor {
15
+ readonly dispose: unique symbol;
16
+ }
17
+
18
+ interface Disposable {
19
+ [Symbol.dispose](): void;
20
+ }
@@ -1,3 +1,4 @@
1
+ /// <reference path="./disposable.d.ts" />
1
2
  import type { AnyExtensionTarget, ApiForTarget } from './targets';
2
3
  import { type Navigation } from './navigation';
3
4
  export type { AnyExtensionTarget, ApiForTarget } from './targets';
@@ -13,19 +14,17 @@ export type { Navigation, NavigationHistoryEntry, NavigationNavigateOptions, } f
13
14
  type Mutable<T> = T extends (...args: any[]) => any ? T : T extends object ? {
14
15
  -readonly [K in keyof T]: Mutable<T[K]>;
15
16
  } : T;
16
- interface Extension<T extends AnyExtensionTarget> {
17
- /**
18
- * Sets up an extension environment for testing.
19
- *
20
- * For example, it creates a mock `shopify` global with some defaults.
21
- */
22
- setUp(): void;
23
- /**
24
- * Tears down the extension environment.
25
- *
26
- * For example, it resets the `shopify` global and clears `document.body`.
27
- */
28
- tearDown(): void;
17
+ /**
18
+ * `Symbol.dispose` for runtimes that support it, with a polyfill
19
+ * fallback so the library works on older Node versions too.
20
+ */
21
+ export declare const SymbolDispose: typeof Symbol.dispose;
22
+ /**
23
+ * Members shared by both {@link ExtensionHarness} (returned by
24
+ * `getExtension`) and {@link DisposableExtensionHarness} (returned
25
+ * by `setUpExtension`).
26
+ */
27
+ interface BaseExtensionHarness<T extends AnyExtensionTarget> {
29
28
  /**
30
29
  * Imports and executes the extension module's default export,
31
30
  * rendering the extension into `document.body`.
@@ -69,6 +68,35 @@ interface Extension<T extends AnyExtensionTarget> {
69
68
  */
70
69
  navigation: Navigation;
71
70
  }
71
+ /**
72
+ * Returned by `getExtension`. The caller is responsible for calling
73
+ * `setUp()` before each test and `tearDown()` after.
74
+ */
75
+ interface ExtensionHarness<T extends AnyExtensionTarget> extends BaseExtensionHarness<T> {
76
+ /**
77
+ * Sets up an extension environment for testing.
78
+ *
79
+ * For example, it creates a mock `shopify` global with some defaults.
80
+ */
81
+ setUp(): void;
82
+ /**
83
+ * Tears down the extension environment.
84
+ *
85
+ * For example, it resets the `shopify` global and clears `document.body`.
86
+ */
87
+ tearDown(): void;
88
+ }
89
+ /**
90
+ * Returned by `setUpExtension`. Already set up — tears down
91
+ * automatically via `Symbol.dispose` (the `using` keyword):
92
+ *
93
+ * ```ts
94
+ * using extension = setUpExtension('purchase.checkout.block.render');
95
+ * ```
96
+ */
97
+ interface DisposableExtensionHarness<T extends AnyExtensionTarget> extends BaseExtensionHarness<T> {
98
+ [Symbol.dispose](): void;
99
+ }
72
100
  /**
73
101
  * Returns an extension test harness for the given target.
74
102
  *
@@ -86,5 +114,25 @@ interface Extension<T extends AnyExtensionTarget> {
86
114
  */
87
115
  export declare function getExtension<T extends AnyExtensionTarget>(target: T, options?: {
88
116
  configSearchDir?: string;
89
- }): Extension<T>;
117
+ }): ExtensionHarness<T>;
118
+ /**
119
+ * Sets up an extension for testing and returns a disposable object
120
+ * that supports automatic teardown with the `using` keyword:
121
+ *
122
+ * ```ts
123
+ * test('rendering the extension', async () => {
124
+ * using extension = setUpExtension(
125
+ * 'purchase.checkout.block.render',
126
+ * );
127
+ * await extension.render();
128
+ * // tearDown() is called automatically at the end of the block
129
+ * });
130
+ * ```
131
+ *
132
+ * @param target - The extension target to mock.
133
+ * @param options - Optional configuration (same as {@link getExtension}).
134
+ */
135
+ export declare function setUpExtension<T extends AnyExtensionTarget>(target: T, options?: {
136
+ configSearchDir?: string;
137
+ }): DisposableExtensionHarness<T>;
90
138
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,kBAAkB,EAAE,YAAY,EAAC,MAAM,WAAW,CAAC;AAGhE,OAAO,EAAuB,KAAK,UAAU,EAAC,MAAM,cAAc,CAAC;AAOnE,YAAY,EAAC,kBAAkB,EAAE,YAAY,EAAC,MAAM,WAAW,CAAC;AAChE,OAAO,EAAC,4BAA4B,EAAC,MAAM,cAAc,CAAC;AAC1D,YAAY,EACV,UAAU,EACV,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,cAAc,CAAC;AAEtB;;;;;;GAMG;AACH,KAAK,OAAO,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,GAC/C,CAAC,GACD,CAAC,SAAS,MAAM,GAChB;IAAC,CAAC,UAAU,CAAC,IAAI,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAC,GACzC,CAAC,CAAC;AAEN,UAAU,SAAS,CAAC,CAAC,SAAS,kBAAkB;IAC9C;;;;OAIG;IACH,KAAK,IAAI,IAAI,CAAC;IAEd;;;;OAIG;IACH,QAAQ,IAAI,IAAI,CAAC;IAEjB;;;OAGG;IACH,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAExB;;;;;;;;OAQG;IACH,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;IAElC;;;;;;;;;;;OAWG;IACH,KAAK,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAE/B;;;;;;;;;;;;OAYG;IACH,UAAU,EAAE,UAAU,CAAC;CACxB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,kBAAkB,EACvD,MAAM,EAAE,CAAC,EACT,OAAO,CAAC,EAAE;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAC,GACnC,SAAS,CAAC,CAAC,CAAC,CA4Gd"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,kBAAkB,EAAE,YAAY,EAAC,MAAM,WAAW,CAAC;AAGhE,OAAO,EAAuB,KAAK,UAAU,EAAC,MAAM,cAAc,CAAC;AAOnE,YAAY,EAAC,kBAAkB,EAAE,YAAY,EAAC,MAAM,WAAW,CAAC;AAChE,OAAO,EAAC,4BAA4B,EAAC,MAAM,cAAc,CAAC;AAC1D,YAAY,EACV,UAAU,EACV,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,cAAc,CAAC;AAEtB;;;;;;GAMG;AACH,KAAK,OAAO,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,GAC/C,CAAC,GACD,CAAC,SAAS,MAAM,GAChB;IAAC,CAAC,UAAU,CAAC,IAAI,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAC,GACzC,CAAC,CAAC;AAEN;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,OAAO,MAAM,CAAC,OACc,CAAC;AAEzD;;;;GAIG;AACH,UAAU,oBAAoB,CAAC,CAAC,SAAS,kBAAkB;IACzD;;;OAGG;IACH,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAExB;;;;;;;;OAQG;IACH,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;IAElC;;;;;;;;;;;OAWG;IACH,KAAK,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAE/B;;;;;;;;;;;;OAYG;IACH,UAAU,EAAE,UAAU,CAAC;CACxB;AAED;;;GAGG;AACH,UAAU,gBAAgB,CAAC,CAAC,SAAS,kBAAkB,CACrD,SAAQ,oBAAoB,CAAC,CAAC,CAAC;IAC/B;;;;OAIG;IACH,KAAK,IAAI,IAAI,CAAC;IAEd;;;;OAIG;IACH,QAAQ,IAAI,IAAI,CAAC;CAClB;AAED;;;;;;;GAOG;AACH,UAAU,0BAA0B,CAAC,CAAC,SAAS,kBAAkB,CAC/D,SAAQ,oBAAoB,CAAC,CAAC,CAAC;IAC/B,CAAC,aAAa,CAAC,IAAI,IAAI,CAAC;CACzB;AA6HD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,kBAAkB,EACvD,MAAM,EAAE,CAAC,EACT,OAAO,CAAC,EAAE;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAC,GACnC,gBAAgB,CAAC,CAAC,CAAC,CAiBrB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,kBAAkB,EACzD,MAAM,EAAE,CAAC,EACT,OAAO,CAAC,EAAE;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAC,GACnC,0BAA0B,CAAC,CAAC,CAAC,CAO/B"}
@@ -1 +1 @@
1
- {"version":3,"file":"factories.d.ts","sourceRoot":"","sources":["../../../src/point-of-sale/factories.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,gBAAgB,EAuBjB,MAAM,sCAAsC,CAAC;AAM9C;;;;;;GAMG;AACH,KAAK,kBAAkB,CAAC,CAAC,SAAS,eAAe,IAC/C,gBAAgB,CAAC,CAAC,CAAC,SAAS;IAAC,GAAG,EAAE,MAAM,GAAG,CAAA;CAAC,GACxC,GAAG,GACH,gBAAgB,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,GAAG,GAClD,CAAC,GACD,KAAK,CAAC;AAwgBZ;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,SAAS,eAAe,EAC9D,MAAM,EAAE,CAAC,GACR,kBAAkB,CAAC,CAAC,CAAC,CASvB"}
1
+ {"version":3,"file":"factories.d.ts","sourceRoot":"","sources":["../../../src/point-of-sale/factories.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,gBAAgB,EAwBjB,MAAM,sCAAsC,CAAC;AAM9C;;;;;;GAMG;AACH,KAAK,kBAAkB,CAAC,CAAC,SAAS,eAAe,IAC/C,gBAAgB,CAAC,CAAC,CAAC,SAAS;IAAC,GAAG,EAAE,MAAM,GAAG,CAAA;CAAC,GACxC,GAAG,GACH,gBAAgB,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,GAAG,GAClD,CAAC,GACD,KAAK,CAAC;AA8gBZ;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,SAAS,eAAe,EAC9D,MAAM,EAAE,CAAC,GACR,kBAAkB,CAAC,CAAC,CAAC,CASvB"}