@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.
- package/README.md +66 -14
- package/build/cjs/_virtual/_rollupPluginBabelHelpers.js +15 -0
- package/build/cjs/admin/factories.js +18 -0
- package/build/cjs/api-version.js +2 -2
- package/build/cjs/checkout/factories.js +0 -5
- package/build/cjs/index.js +209 -86
- package/build/cjs/point-of-sale/factories.js +6 -1
- package/build/esm/_virtual/_rollupPluginBabelHelpers.mjs +10 -0
- package/build/esm/admin/factories.mjs +18 -0
- package/build/esm/api-version.mjs +2 -2
- package/build/esm/checkout/factories.mjs +0 -5
- package/build/esm/index.mjs +208 -87
- package/build/esm/point-of-sale/factories.mjs +6 -1
- package/build/esnext/_virtual/_rollupPluginBabelHelpers.esnext +10 -0
- package/build/esnext/admin/factories.esnext +18 -0
- package/build/esnext/api-version.esnext +2 -2
- package/build/esnext/checkout/factories.esnext +0 -5
- package/build/esnext/index.esnext +208 -87
- package/build/esnext/point-of-sale/factories.esnext +6 -1
- package/build/ts/admin/factories.d.ts.map +1 -1
- package/build/ts/checkout/factories.d.ts.map +1 -1
- package/build/ts/disposable.d.ts +20 -0
- package/build/ts/index.d.ts +62 -14
- package/build/ts/index.d.ts.map +1 -1
- package/build/ts/point-of-sale/factories.d.ts.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/admin/README.md +6 -0
- package/src/admin/factories.ts +22 -0
- package/src/checkout/factories.ts +0 -1
- package/src/disposable.d.ts +20 -0
- package/src/index.ts +214 -119
- package/src/point-of-sale/factories.ts +8 -1
- package/src/tests/admin-factories.test.ts +25 -1
- package/src/tests/getExtension.test.ts +93 -1
- package/src/tests/setUpExtension.test.ts +52 -0
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ This testing library provides strongly typed mocks of the extension API--like th
|
|
|
14
14
|
## 📋 Recommendations
|
|
15
15
|
|
|
16
16
|
- **TypeScript** — we recommend TypeScript to enforce API compliance against mock objects
|
|
17
|
+
- **Node.js ≥ 22.0.0** and **TypeScript ≥ 5.2** — to use ([Explicit Resource Management](https://github.com/tc39/proposal-explicit-resource-management))
|
|
17
18
|
- **@testing-library/preact** — if your extension uses [Preact](https://preactjs.com/), we recommend installing [`@testing-library/preact`](https://preactjs.com/guide/v10/preact-testing-library/) for its `fireEvent` and `waitFor` helpers
|
|
18
19
|
|
|
19
20
|
## 📦 Installation
|
|
@@ -72,7 +73,7 @@ Your extension's own `package.json` (inside `extensions/my-extension/`) already
|
|
|
72
73
|
"devDependencies": {
|
|
73
74
|
"@shopify/ui-extensions-tester": "latest",
|
|
74
75
|
"@testing-library/preact": "^3.2.0",
|
|
75
|
-
"typescript": "^5.
|
|
76
|
+
"typescript": "^5.2.0",
|
|
76
77
|
"vitest": "^3.0.0"
|
|
77
78
|
}
|
|
78
79
|
}
|
|
@@ -144,7 +145,34 @@ The path must match the target you pass to `getExtension()`.
|
|
|
144
145
|
|
|
145
146
|
## 🏊♀️ Getting started
|
|
146
147
|
|
|
147
|
-
Every test file follows the same pattern:
|
|
148
|
+
Every test file follows the same pattern:
|
|
149
|
+
create an extension harness, set it up before
|
|
150
|
+
each test, and tear it down after.
|
|
151
|
+
|
|
152
|
+
### Quick start with `using` (Node ≥ 22.0.0)
|
|
153
|
+
|
|
154
|
+
If your runtime supports
|
|
155
|
+
[Explicit Resource Management](https://github.com/tc39/proposal-explicit-resource-management),
|
|
156
|
+
use `setUpExtension` for zero-boilerplate
|
|
157
|
+
setup and automatic teardown:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import {setUpExtension} from '@shopify/ui-extensions-tester';
|
|
161
|
+
|
|
162
|
+
test('rendering the extension', async () => {
|
|
163
|
+
using extension = setUpExtension(
|
|
164
|
+
'purchase.checkout.block.render',
|
|
165
|
+
);
|
|
166
|
+
await extension.render();
|
|
167
|
+
// tearDown() is called automatically at the end of the block
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Classic setup
|
|
172
|
+
|
|
173
|
+
Alternatively, create the harness once and
|
|
174
|
+
manage the lifecycle with `beforeEach` /
|
|
175
|
+
`afterEach`:
|
|
148
176
|
|
|
149
177
|
```ts
|
|
150
178
|
import {getExtension} from '@shopify/ui-extensions-tester';
|
|
@@ -339,25 +367,31 @@ Each surface exports some helpers:
|
|
|
339
367
|
|
|
340
368
|
Exports from `@shopify/ui-extensions-tester`:
|
|
341
369
|
|
|
342
|
-
### `
|
|
370
|
+
### `setUpExtension(target, options?)`
|
|
371
|
+
|
|
372
|
+
Sets up an extension for testing and returns a
|
|
373
|
+
disposable object that supports automatic
|
|
374
|
+
teardown with the `using` keyword:
|
|
375
|
+
|
|
376
|
+
```ts
|
|
377
|
+
test('example', async () => {
|
|
378
|
+
using extension = setUpExtension(
|
|
379
|
+
'purchase.checkout.block.render',
|
|
380
|
+
);
|
|
381
|
+
await extension.render();
|
|
382
|
+
// tearDown() called automatically
|
|
383
|
+
});
|
|
384
|
+
```
|
|
343
385
|
|
|
344
|
-
|
|
386
|
+
It reads `shopify.extension.toml`, finds the module for the given target, and provides helpers to mock the environment and render the extension. It locates `shopify.extension.toml` by walking up from the calling test file's directory, and falls back to searching `extensions/` under the current working directory. Results are cached: calling `getExtension` twice with the same target and the same resolved TOML returns the same instance.
|
|
345
387
|
|
|
346
388
|
| Option | Type | Default | Description |
|
|
347
389
|
| ----------------- | -------- | ----------------------------- | ---------------------------------------------------------- |
|
|
348
390
|
| `configSearchDir` | `string` | calling test file's directory | Directory to start searching for `shopify.extension.toml`. |
|
|
349
391
|
|
|
350
|
-
By default
|
|
392
|
+
By default, it walks up from the test file's directory to find `shopify.extension.toml`.
|
|
351
393
|
|
|
352
|
-
**Returns** an `Extension
|
|
353
|
-
|
|
354
|
-
#### `extension.setUp()`
|
|
355
|
-
|
|
356
|
-
Sets up an extension environment for testing. Creates a mock `shopify` global with some defaults.
|
|
357
|
-
|
|
358
|
-
#### `extension.tearDown()`
|
|
359
|
-
|
|
360
|
-
Tears down the extension environment. Resets the `shopify` global and clears `document.body`.
|
|
394
|
+
**Returns** an `Extension` object with the following members:
|
|
361
395
|
|
|
362
396
|
#### `extension.render()`
|
|
363
397
|
|
|
@@ -367,6 +401,8 @@ Imports and executes the extension module's default export, rendering the extens
|
|
|
367
401
|
|
|
368
402
|
A mock `shopify` global, typed correctly for the target under test. You can mutate any property.
|
|
369
403
|
|
|
404
|
+
When testing `admin.app.intent.render`, the mock `shopify.intents` object also includes `response.ok()`, `response.error()`, and `response.closed()`.
|
|
405
|
+
|
|
370
406
|
#### `extension.fetch`
|
|
371
407
|
|
|
372
408
|
A mock `fetch()` function installed as `globalThis.fetch` during `setUp()` and removed during `tearDown()`.
|
|
@@ -402,6 +438,22 @@ extension.navigation.currentEntry =
|
|
|
402
438
|
|
|
403
439
|
Assigning to `extension.navigation` also updates `globalThis.navigation`, so extension code that calls `navigation.navigate()` directly will use the mock.
|
|
404
440
|
|
|
441
|
+
### `getExtension(target, options?)`
|
|
442
|
+
|
|
443
|
+
> ⚠️ Prefer [`setUpExtension`](#setupextensiontarget-options) on Node ≥ 22.0.0. Use `getExtension` only if your runtime does not support [Explicit Resource Management](https://github.com/tc39/proposal-explicit-resource-management).
|
|
444
|
+
|
|
445
|
+
Accepts the same arguments as `setUpExtension`. You must call `extension.setUp()` and `extension.tearDown()` explicitly.
|
|
446
|
+
|
|
447
|
+
**Returns** an `Extension` object with the following additional members:
|
|
448
|
+
|
|
449
|
+
#### `extension.setUp()`
|
|
450
|
+
|
|
451
|
+
Sets up an extension environment for testing. Creates a mock `shopify` global with some defaults.
|
|
452
|
+
|
|
453
|
+
#### `extension.tearDown()`
|
|
454
|
+
|
|
455
|
+
Tears down the extension environment. Resets the `shopify` global and clears `document.body`.
|
|
456
|
+
|
|
405
457
|
### `createNavigationHistoryEntry(options?)`
|
|
406
458
|
|
|
407
459
|
Creates a [`NavigationHistoryEntry`](https://developer.mozilla.org/en-US/docs/Web/API/NavigationHistoryEntry) for mocking `navigation.currentEntry` or other navigation values.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
function _classPrivateFieldLooseBase(e, t) {
|
|
6
|
+
if (!{}.hasOwnProperty.call(e, t)) throw new TypeError("attempted to use private field on non-instance");
|
|
7
|
+
return e;
|
|
8
|
+
}
|
|
9
|
+
var id = 0;
|
|
10
|
+
function _classPrivateFieldLooseKey(e) {
|
|
11
|
+
return "__private_" + id++ + "_" + e;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
exports.classPrivateFieldLooseBase = _classPrivateFieldLooseBase;
|
|
15
|
+
exports.classPrivateFieldLooseKey = _classPrivateFieldLooseKey;
|
|
@@ -38,6 +38,13 @@ function createConfigApp() {
|
|
|
38
38
|
applicationUrl: 'https://mock-app.test'
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
+
function createIntentResponseApi() {
|
|
42
|
+
return {
|
|
43
|
+
ok: async () => {},
|
|
44
|
+
error: async () => {},
|
|
45
|
+
closed: async () => {}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
41
48
|
function createMockStandardApi(target) {
|
|
42
49
|
return {
|
|
43
50
|
extension: {
|
|
@@ -70,6 +77,15 @@ function createMockStandardRenderingApi(target) {
|
|
|
70
77
|
})
|
|
71
78
|
};
|
|
72
79
|
}
|
|
80
|
+
function createAppIntentRenderMock(target) {
|
|
81
|
+
return {
|
|
82
|
+
...createMockStandardRenderingApi(target),
|
|
83
|
+
intents: {
|
|
84
|
+
...createMockStandardApi(target).intents,
|
|
85
|
+
response: createIntentResponseApi()
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
73
89
|
function createMockBlockApi(target) {
|
|
74
90
|
return {
|
|
75
91
|
...createMockStandardRenderingApi(target),
|
|
@@ -214,6 +230,8 @@ const adminMockFactories = {
|
|
|
214
230
|
// Runnable targets
|
|
215
231
|
'admin.customers.segmentation-templates.data': createCustomerSegmentTemplateMock,
|
|
216
232
|
'admin.app.tools.data': createMockStandardApi,
|
|
233
|
+
// App render targets
|
|
234
|
+
'admin.app.intent.render': createAppIntentRenderMock,
|
|
217
235
|
// Block targets
|
|
218
236
|
'admin.product-details.block.render': createMockBlockApi,
|
|
219
237
|
'admin.order-details.block.render': createMockBlockApi,
|
package/build/cjs/api-version.js
CHANGED
|
@@ -5,14 +5,14 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
5
5
|
/**
|
|
6
6
|
* The API version supported by this version of the library.
|
|
7
7
|
*
|
|
8
|
-
* At build time, `"2026.4.0
|
|
8
|
+
* At build time, `"2026.4.0"` is replaced by rollup with the
|
|
9
9
|
* raw NPM version string from package.json (e.g. `"2026.4.0-rc.1"`).
|
|
10
10
|
*
|
|
11
11
|
* When running from source (e.g. in tests), the placeholder is still
|
|
12
12
|
* present, so we fall back to reading package.json via require.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
const npmVersion = "2026.4.0
|
|
15
|
+
const npmVersion = "2026.4.0";
|
|
16
16
|
function npmVersionToApiVersion(version) {
|
|
17
17
|
const [year, minor] = version.split('.');
|
|
18
18
|
return `${year}-${minor.padStart(2, '0')}`;
|
|
@@ -115,11 +115,6 @@ function createMockStandardApi(target) {
|
|
|
115
115
|
}),
|
|
116
116
|
selectedPaymentOptions: signals.createSubscribableSignalLike([]),
|
|
117
117
|
settings: signals.createSubscribableSignalLike({}),
|
|
118
|
-
ui: {
|
|
119
|
-
overlay: {
|
|
120
|
-
close: () => {}
|
|
121
|
-
}
|
|
122
|
-
},
|
|
123
118
|
version: '0.0.0',
|
|
124
119
|
customerPrivacy: signals.createSubscribableSignalLike({
|
|
125
120
|
allowedProcessing: {
|
package/build/cjs/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
+
var _rollupPluginBabelHelpers = require('./_virtual/_rollupPluginBabelHelpers.js');
|
|
5
6
|
var fs = require('fs');
|
|
6
7
|
var path = require('path');
|
|
7
8
|
var targets = require('./targets.js');
|
|
@@ -31,6 +32,8 @@ function _interopNamespace(e) {
|
|
|
31
32
|
var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
|
|
32
33
|
var path__namespace = /*#__PURE__*/_interopNamespace(path);
|
|
33
34
|
|
|
35
|
+
var _dispose;
|
|
36
|
+
|
|
34
37
|
/**
|
|
35
38
|
* Makes all properties in the API deeply mutable so tests can
|
|
36
39
|
* override any value through the `extension.shopify` proxy:
|
|
@@ -39,6 +42,168 @@ var path__namespace = /*#__PURE__*/_interopNamespace(path);
|
|
|
39
42
|
* extension.shopify.i18n.translate = (key) => myTranslations[key];
|
|
40
43
|
*/
|
|
41
44
|
|
|
45
|
+
/**
|
|
46
|
+
* `Symbol.dispose` for runtimes that support it, with a polyfill
|
|
47
|
+
* fallback so the library works on older Node versions too.
|
|
48
|
+
*/
|
|
49
|
+
const SymbolDispose = (_dispose = Symbol.dispose) !== null && _dispose !== void 0 ? _dispose : Symbol.for('Symbol.dispose');
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Members shared by both {@link ExtensionHarness} (returned by
|
|
53
|
+
* `getExtension`) and {@link DisposableExtensionHarness} (returned
|
|
54
|
+
* by `setUpExtension`).
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Returned by `getExtension`. The caller is responsible for calling
|
|
59
|
+
* `setUp()` before each test and `tearDown()` after.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Returned by `setUpExtension`. Already set up — tears down
|
|
64
|
+
* automatically via `Symbol.dispose` (the `using` keyword):
|
|
65
|
+
*
|
|
66
|
+
* ```ts
|
|
67
|
+
* using extension = setUpExtension('purchase.checkout.block.render');
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
var _target = /*#__PURE__*/_rollupPluginBabelHelpers.classPrivateFieldLooseKey("target");
|
|
71
|
+
var _resolvedModule = /*#__PURE__*/_rollupPluginBabelHelpers.classPrivateFieldLooseKey("resolvedModule");
|
|
72
|
+
var _modulePath = /*#__PURE__*/_rollupPluginBabelHelpers.classPrivateFieldLooseKey("modulePath");
|
|
73
|
+
var _checkout = /*#__PURE__*/_rollupPluginBabelHelpers.classPrivateFieldLooseKey("checkout");
|
|
74
|
+
var _networkAccess = /*#__PURE__*/_rollupPluginBabelHelpers.classPrivateFieldLooseKey("networkAccess");
|
|
75
|
+
var _apiAccess = /*#__PURE__*/_rollupPluginBabelHelpers.classPrivateFieldLooseKey("apiAccess");
|
|
76
|
+
var _fetchImpl = /*#__PURE__*/_rollupPluginBabelHelpers.classPrivateFieldLooseKey("fetchImpl");
|
|
77
|
+
var _previousFetch = /*#__PURE__*/_rollupPluginBabelHelpers.classPrivateFieldLooseKey("previousFetch");
|
|
78
|
+
var _navigationImpl = /*#__PURE__*/_rollupPluginBabelHelpers.classPrivateFieldLooseKey("navigationImpl");
|
|
79
|
+
var _previousNavigation = /*#__PURE__*/_rollupPluginBabelHelpers.classPrivateFieldLooseKey("previousNavigation");
|
|
80
|
+
class Extension {
|
|
81
|
+
constructor(target, options) {
|
|
82
|
+
var _options$configSearch;
|
|
83
|
+
Object.defineProperty(this, _target, {
|
|
84
|
+
writable: true,
|
|
85
|
+
value: void 0
|
|
86
|
+
});
|
|
87
|
+
Object.defineProperty(this, _resolvedModule, {
|
|
88
|
+
writable: true,
|
|
89
|
+
value: void 0
|
|
90
|
+
});
|
|
91
|
+
Object.defineProperty(this, _modulePath, {
|
|
92
|
+
writable: true,
|
|
93
|
+
value: void 0
|
|
94
|
+
});
|
|
95
|
+
Object.defineProperty(this, _checkout, {
|
|
96
|
+
writable: true,
|
|
97
|
+
value: void 0
|
|
98
|
+
});
|
|
99
|
+
Object.defineProperty(this, _networkAccess, {
|
|
100
|
+
writable: true,
|
|
101
|
+
value: void 0
|
|
102
|
+
});
|
|
103
|
+
Object.defineProperty(this, _apiAccess, {
|
|
104
|
+
writable: true,
|
|
105
|
+
value: void 0
|
|
106
|
+
});
|
|
107
|
+
Object.defineProperty(this, _fetchImpl, {
|
|
108
|
+
writable: true,
|
|
109
|
+
value: void 0
|
|
110
|
+
});
|
|
111
|
+
Object.defineProperty(this, _previousFetch, {
|
|
112
|
+
writable: true,
|
|
113
|
+
value: void 0
|
|
114
|
+
});
|
|
115
|
+
Object.defineProperty(this, _navigationImpl, {
|
|
116
|
+
writable: true,
|
|
117
|
+
value: navigation.createMockNavigation()
|
|
118
|
+
});
|
|
119
|
+
Object.defineProperty(this, _previousNavigation, {
|
|
120
|
+
writable: true,
|
|
121
|
+
value: void 0
|
|
122
|
+
});
|
|
123
|
+
const configSearchDir = (_options$configSearch = options === null || options === void 0 ? void 0 : options.configSearchDir) !== null && _options$configSearch !== void 0 ? _options$configSearch : path__namespace.dirname(getCallerFile());
|
|
124
|
+
const tomlPath = findToml(configSearchDir);
|
|
125
|
+
const tomlDir = path__namespace.dirname(tomlPath);
|
|
126
|
+
const tomlContent = fs__namespace.readFileSync(tomlPath, 'utf-8');
|
|
127
|
+
validateApiVersion(tomlContent);
|
|
128
|
+
const modulePath = parseTargetModule(tomlContent, target);
|
|
129
|
+
_rollupPluginBabelHelpers.classPrivateFieldLooseBase(this, _target)[_target] = target;
|
|
130
|
+
_rollupPluginBabelHelpers.classPrivateFieldLooseBase(this, _modulePath)[_modulePath] = modulePath;
|
|
131
|
+
_rollupPluginBabelHelpers.classPrivateFieldLooseBase(this, _resolvedModule)[_resolvedModule] = path__namespace.resolve(tomlDir, modulePath);
|
|
132
|
+
_rollupPluginBabelHelpers.classPrivateFieldLooseBase(this, _checkout)[_checkout] = targets.isCheckoutTarget(target);
|
|
133
|
+
_rollupPluginBabelHelpers.classPrivateFieldLooseBase(this, _networkAccess)[_networkAccess] = _rollupPluginBabelHelpers.classPrivateFieldLooseBase(this, _checkout)[_checkout] && parseNetworkAccess(tomlContent);
|
|
134
|
+
_rollupPluginBabelHelpers.classPrivateFieldLooseBase(this, _apiAccess)[_apiAccess] = _rollupPluginBabelHelpers.classPrivateFieldLooseBase(this, _checkout)[_checkout] && parseApiAccess(tomlContent);
|
|
135
|
+
}
|
|
136
|
+
setUp() {
|
|
137
|
+
fetchPolyfills.installFetchPolyfills();
|
|
138
|
+
_rollupPluginBabelHelpers.classPrivateFieldLooseBase(this, _fetchImpl)[_fetchImpl] = _rollupPluginBabelHelpers.classPrivateFieldLooseBase(this, _checkout)[_checkout] && !_rollupPluginBabelHelpers.classPrivateFieldLooseBase(this, _networkAccess)[_networkAccess] && !_rollupPluginBabelHelpers.classPrivateFieldLooseBase(this, _apiAccess)[_apiAccess] ? async () => {
|
|
139
|
+
// Checkout is the only surface that currently enforces
|
|
140
|
+
// fetch capabilities.
|
|
141
|
+
throw new Error('fetch() is not available. Add network_access = true or ' + 'api_access = true to [extensions.capabilities] in shopify.extension.toml.');
|
|
142
|
+
} : async () => new Response();
|
|
143
|
+
_rollupPluginBabelHelpers.classPrivateFieldLooseBase(this, _previousFetch)[_previousFetch] = globalThis.fetch;
|
|
144
|
+
_rollupPluginBabelHelpers.classPrivateFieldLooseBase(this, _previousNavigation)[_previousNavigation] = globalThis.navigation;
|
|
145
|
+
_rollupPluginBabelHelpers.classPrivateFieldLooseBase(this, _navigationImpl)[_navigationImpl] = navigation.createMockNavigation();
|
|
146
|
+
globalThis.shopify = deepWritableProxy(targetApis.createMockTargetApi(_rollupPluginBabelHelpers.classPrivateFieldLooseBase(this, _target)[_target]));
|
|
147
|
+
globalThis.fetch = _rollupPluginBabelHelpers.classPrivateFieldLooseBase(this, _fetchImpl)[_fetchImpl];
|
|
148
|
+
globalThis.navigation = _rollupPluginBabelHelpers.classPrivateFieldLooseBase(this, _navigationImpl)[_navigationImpl];
|
|
149
|
+
}
|
|
150
|
+
get shopify() {
|
|
151
|
+
if (!globalThis.shopify) {
|
|
152
|
+
throw new Error('You must call extension.setUp() before accessing extension.shopify.');
|
|
153
|
+
}
|
|
154
|
+
return globalThis.shopify;
|
|
155
|
+
}
|
|
156
|
+
get fetch() {
|
|
157
|
+
return _rollupPluginBabelHelpers.classPrivateFieldLooseBase(this, _fetchImpl)[_fetchImpl];
|
|
158
|
+
}
|
|
159
|
+
set fetch(fn) {
|
|
160
|
+
_rollupPluginBabelHelpers.classPrivateFieldLooseBase(this, _fetchImpl)[_fetchImpl] = fn;
|
|
161
|
+
globalThis.fetch = fn;
|
|
162
|
+
}
|
|
163
|
+
get navigation() {
|
|
164
|
+
return _rollupPluginBabelHelpers.classPrivateFieldLooseBase(this, _navigationImpl)[_navigationImpl];
|
|
165
|
+
}
|
|
166
|
+
set navigation(obj) {
|
|
167
|
+
_rollupPluginBabelHelpers.classPrivateFieldLooseBase(this, _navigationImpl)[_navigationImpl] = obj;
|
|
168
|
+
globalThis.navigation = obj;
|
|
169
|
+
}
|
|
170
|
+
async render() {
|
|
171
|
+
const mod = await (function (t) { return Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require(t)); }); })(_rollupPluginBabelHelpers.classPrivateFieldLooseBase(this, _resolvedModule)[_resolvedModule]);
|
|
172
|
+
const renderFn = mod.default;
|
|
173
|
+
if (typeof renderFn !== 'function') {
|
|
174
|
+
throw new Error(`Expected default export of ${_rollupPluginBabelHelpers.classPrivateFieldLooseBase(this, _modulePath)[_modulePath]} to be a function, got ${typeof renderFn}`);
|
|
175
|
+
}
|
|
176
|
+
await renderFn();
|
|
177
|
+
}
|
|
178
|
+
tearDown() {
|
|
179
|
+
// Dynamically import preact to unmount cleanly without requiring
|
|
180
|
+
// the test file to depend on preact directly.
|
|
181
|
+
try {
|
|
182
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
183
|
+
const {
|
|
184
|
+
render
|
|
185
|
+
} = require('preact');
|
|
186
|
+
render(null, document.body);
|
|
187
|
+
} catch {
|
|
188
|
+
// Fallback if preact isn't available
|
|
189
|
+
document.body.innerHTML = '';
|
|
190
|
+
}
|
|
191
|
+
delete globalThis.shopify;
|
|
192
|
+
if (_rollupPluginBabelHelpers.classPrivateFieldLooseBase(this, _previousFetch)[_previousFetch] === undefined) {
|
|
193
|
+
delete globalThis.fetch;
|
|
194
|
+
} else {
|
|
195
|
+
globalThis.fetch = _rollupPluginBabelHelpers.classPrivateFieldLooseBase(this, _previousFetch)[_previousFetch];
|
|
196
|
+
}
|
|
197
|
+
if (_rollupPluginBabelHelpers.classPrivateFieldLooseBase(this, _previousNavigation)[_previousNavigation] === undefined) {
|
|
198
|
+
delete globalThis.navigation;
|
|
199
|
+
} else {
|
|
200
|
+
globalThis.navigation = _rollupPluginBabelHelpers.classPrivateFieldLooseBase(this, _previousNavigation)[_previousNavigation];
|
|
201
|
+
}
|
|
202
|
+
fetchPolyfills.uninstallFetchPolyfills();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const extensionCache = new Map();
|
|
206
|
+
|
|
42
207
|
/**
|
|
43
208
|
* Returns an extension test harness for the given target.
|
|
44
209
|
*
|
|
@@ -55,92 +220,46 @@ var path__namespace = /*#__PURE__*/_interopNamespace(path);
|
|
|
55
220
|
* `shopify.extension.toml`. Defaults to the calling test file's directory.
|
|
56
221
|
*/
|
|
57
222
|
function getExtension(target, options) {
|
|
58
|
-
var _options$
|
|
59
|
-
const
|
|
60
|
-
const tomlPath = findToml(
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
let navigationImpl = navigation.createMockNavigation();
|
|
72
|
-
let previousNavigation;
|
|
73
|
-
const ext = {
|
|
74
|
-
setUp() {
|
|
75
|
-
fetchPolyfills.installFetchPolyfills();
|
|
76
|
-
fetchImpl = checkout && !networkAccess && !apiAccess ? async () => {
|
|
77
|
-
// Checkout is the only surface that currently enforces
|
|
78
|
-
// fetch capabilities.
|
|
79
|
-
throw new Error('fetch() is not available. Add network_access = true or ' + 'api_access = true to [extensions.capabilities] in shopify.extension.toml.');
|
|
80
|
-
} : async () => new Response();
|
|
81
|
-
previousFetch = globalThis.fetch;
|
|
82
|
-
previousNavigation = globalThis.navigation;
|
|
83
|
-
globalThis.shopify = deepWritableProxy(targetApis.createMockTargetApi(target));
|
|
84
|
-
globalThis.fetch = fetchImpl;
|
|
85
|
-
globalThis.navigation = navigationImpl;
|
|
86
|
-
},
|
|
87
|
-
get shopify() {
|
|
88
|
-
if (!globalThis.shopify) {
|
|
89
|
-
throw new Error('You must call extension.setUp() before accessing extension.shopify.');
|
|
90
|
-
}
|
|
91
|
-
return globalThis.shopify;
|
|
92
|
-
},
|
|
93
|
-
get fetch() {
|
|
94
|
-
return fetchImpl;
|
|
95
|
-
},
|
|
96
|
-
set fetch(fn) {
|
|
97
|
-
fetchImpl = fn;
|
|
98
|
-
globalThis.fetch = fn;
|
|
99
|
-
},
|
|
100
|
-
get navigation() {
|
|
101
|
-
return navigationImpl;
|
|
102
|
-
},
|
|
103
|
-
set navigation(obj) {
|
|
104
|
-
navigationImpl = obj;
|
|
105
|
-
globalThis.navigation = obj;
|
|
106
|
-
},
|
|
107
|
-
async render() {
|
|
108
|
-
const mod = await (function (t) { return Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require(t)); }); })(resolvedModule);
|
|
109
|
-
const renderFn = mod.default;
|
|
110
|
-
if (typeof renderFn !== 'function') {
|
|
111
|
-
throw new Error(`Expected default export of ${modulePath} to be a function, got ${typeof renderFn}`);
|
|
112
|
-
}
|
|
113
|
-
await renderFn();
|
|
114
|
-
},
|
|
115
|
-
tearDown() {
|
|
116
|
-
// Dynamically import preact to unmount cleanly without requiring
|
|
117
|
-
// the test file to depend on preact directly.
|
|
118
|
-
try {
|
|
119
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
120
|
-
const {
|
|
121
|
-
render
|
|
122
|
-
} = require('preact');
|
|
123
|
-
render(null, document.body);
|
|
124
|
-
} catch {
|
|
125
|
-
// Fallback if preact isn't available
|
|
126
|
-
document.body.innerHTML = '';
|
|
127
|
-
}
|
|
128
|
-
delete globalThis.shopify;
|
|
129
|
-
if (previousFetch === undefined) {
|
|
130
|
-
delete globalThis.fetch;
|
|
131
|
-
} else {
|
|
132
|
-
globalThis.fetch = previousFetch;
|
|
133
|
-
}
|
|
134
|
-
if (previousNavigation === undefined) {
|
|
135
|
-
delete globalThis.navigation;
|
|
136
|
-
} else {
|
|
137
|
-
globalThis.navigation = previousNavigation;
|
|
138
|
-
}
|
|
139
|
-
fetchPolyfills.uninstallFetchPolyfills();
|
|
140
|
-
}
|
|
141
|
-
};
|
|
223
|
+
var _options$configSearch2;
|
|
224
|
+
const resolvedConfigSearchDir = (_options$configSearch2 = options === null || options === void 0 ? void 0 : options.configSearchDir) !== null && _options$configSearch2 !== void 0 ? _options$configSearch2 : path__namespace.dirname(getCallerFile());
|
|
225
|
+
const tomlPath = findToml(resolvedConfigSearchDir);
|
|
226
|
+
const tomlMtimeMs = fs__namespace.statSync(tomlPath).mtimeMs;
|
|
227
|
+
const cacheKey = JSON.stringify([target, tomlPath, tomlMtimeMs]);
|
|
228
|
+
const cached = extensionCache.get(cacheKey);
|
|
229
|
+
if (cached) {
|
|
230
|
+
return cached;
|
|
231
|
+
}
|
|
232
|
+
const ext = new Extension(target, {
|
|
233
|
+
configSearchDir: resolvedConfigSearchDir
|
|
234
|
+
});
|
|
235
|
+
extensionCache.set(cacheKey, ext);
|
|
142
236
|
return ext;
|
|
143
237
|
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Sets up an extension for testing and returns a disposable object
|
|
241
|
+
* that supports automatic teardown with the `using` keyword:
|
|
242
|
+
*
|
|
243
|
+
* ```ts
|
|
244
|
+
* test('rendering the extension', async () => {
|
|
245
|
+
* using extension = setUpExtension(
|
|
246
|
+
* 'purchase.checkout.block.render',
|
|
247
|
+
* );
|
|
248
|
+
* await extension.render();
|
|
249
|
+
* // tearDown() is called automatically at the end of the block
|
|
250
|
+
* });
|
|
251
|
+
* ```
|
|
252
|
+
*
|
|
253
|
+
* @param target - The extension target to mock.
|
|
254
|
+
* @param options - Optional configuration (same as {@link getExtension}).
|
|
255
|
+
*/
|
|
256
|
+
function setUpExtension(target, options) {
|
|
257
|
+
const extension = getExtension(target, options);
|
|
258
|
+
extension.setUp();
|
|
259
|
+
return Object.assign(extension, {
|
|
260
|
+
[SymbolDispose]: () => extension.tearDown()
|
|
261
|
+
});
|
|
262
|
+
}
|
|
144
263
|
function validateApiVersion(toml) {
|
|
145
264
|
const match = toml.match(/^\s*api_version\s*=\s*"([^"]+)"/m);
|
|
146
265
|
const tomlVersion = match === null || match === void 0 ? void 0 : match[1];
|
|
@@ -208,8 +327,10 @@ function getCallerFile() {
|
|
|
208
327
|
const err = new Error();
|
|
209
328
|
let callerFile = '';
|
|
210
329
|
Error.prepareStackTrace = (_err, stack) => {
|
|
211
|
-
//
|
|
212
|
-
|
|
330
|
+
// Walk the stack, skipping all frames that originate from this
|
|
331
|
+
// package file. This works regardless of whether the caller is
|
|
332
|
+
// getExtension() or setUpExtension() → getExtension().
|
|
333
|
+
for (let i = 1; i < stack.length; i++) {
|
|
213
334
|
const fileName = stack[i].getFileName();
|
|
214
335
|
if (fileName && fileName !== thisPackageFilePath) {
|
|
215
336
|
callerFile = fileName;
|
|
@@ -341,4 +462,6 @@ function deepWritableProxy(obj) {
|
|
|
341
462
|
}
|
|
342
463
|
|
|
343
464
|
exports.createNavigationHistoryEntry = navigation.createNavigationHistoryEntry;
|
|
465
|
+
exports.SymbolDispose = SymbolDispose;
|
|
344
466
|
exports.getExtension = getExtension;
|
|
467
|
+
exports.setUpExtension = setUpExtension;
|
|
@@ -57,9 +57,14 @@ function createTransaction() {
|
|
|
57
57
|
lineItems: []
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
|
+
function createConnectivityApiContent() {
|
|
61
|
+
return {
|
|
62
|
+
current: signals.createReadonlySignalLike(createConnectivityState())
|
|
63
|
+
};
|
|
64
|
+
}
|
|
60
65
|
function createMockBaseEventData() {
|
|
61
66
|
return {
|
|
62
|
-
connectivity:
|
|
67
|
+
connectivity: createConnectivityApiContent(),
|
|
63
68
|
device: {
|
|
64
69
|
name: 'Mock POS Device',
|
|
65
70
|
deviceId: 1,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
function _classPrivateFieldLooseBase(e, t) {
|
|
2
|
+
if (!{}.hasOwnProperty.call(e, t)) throw new TypeError("attempted to use private field on non-instance");
|
|
3
|
+
return e;
|
|
4
|
+
}
|
|
5
|
+
var id = 0;
|
|
6
|
+
function _classPrivateFieldLooseKey(e) {
|
|
7
|
+
return "__private_" + id++ + "_" + e;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export { _classPrivateFieldLooseBase as classPrivateFieldLooseBase, _classPrivateFieldLooseKey as classPrivateFieldLooseKey };
|
|
@@ -34,6 +34,13 @@ function createConfigApp() {
|
|
|
34
34
|
applicationUrl: 'https://mock-app.test'
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
+
function createIntentResponseApi() {
|
|
38
|
+
return {
|
|
39
|
+
ok: async () => {},
|
|
40
|
+
error: async () => {},
|
|
41
|
+
closed: async () => {}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
37
44
|
function createMockStandardApi(target) {
|
|
38
45
|
return {
|
|
39
46
|
extension: {
|
|
@@ -66,6 +73,15 @@ function createMockStandardRenderingApi(target) {
|
|
|
66
73
|
})
|
|
67
74
|
};
|
|
68
75
|
}
|
|
76
|
+
function createAppIntentRenderMock(target) {
|
|
77
|
+
return {
|
|
78
|
+
...createMockStandardRenderingApi(target),
|
|
79
|
+
intents: {
|
|
80
|
+
...createMockStandardApi(target).intents,
|
|
81
|
+
response: createIntentResponseApi()
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
69
85
|
function createMockBlockApi(target) {
|
|
70
86
|
return {
|
|
71
87
|
...createMockStandardRenderingApi(target),
|
|
@@ -210,6 +226,8 @@ const adminMockFactories = {
|
|
|
210
226
|
// Runnable targets
|
|
211
227
|
'admin.customers.segmentation-templates.data': createCustomerSegmentTemplateMock,
|
|
212
228
|
'admin.app.tools.data': createMockStandardApi,
|
|
229
|
+
// App render targets
|
|
230
|
+
'admin.app.intent.render': createAppIntentRenderMock,
|
|
213
231
|
// Block targets
|
|
214
232
|
'admin.product-details.block.render': createMockBlockApi,
|
|
215
233
|
'admin.order-details.block.render': createMockBlockApi,
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The API version supported by this version of the library.
|
|
3
3
|
*
|
|
4
|
-
* At build time, `"2026.4.0
|
|
4
|
+
* At build time, `"2026.4.0"` is replaced by rollup with the
|
|
5
5
|
* raw NPM version string from package.json (e.g. `"2026.4.0-rc.1"`).
|
|
6
6
|
*
|
|
7
7
|
* When running from source (e.g. in tests), the placeholder is still
|
|
8
8
|
* present, so we fall back to reading package.json via require.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
const npmVersion = "2026.4.0
|
|
11
|
+
const npmVersion = "2026.4.0";
|
|
12
12
|
function npmVersionToApiVersion(version) {
|
|
13
13
|
const [year, minor] = version.split('.');
|
|
14
14
|
return `${year}-${minor.padStart(2, '0')}`;
|
|
@@ -111,11 +111,6 @@ function createMockStandardApi(target) {
|
|
|
111
111
|
}),
|
|
112
112
|
selectedPaymentOptions: createSubscribableSignalLike([]),
|
|
113
113
|
settings: createSubscribableSignalLike({}),
|
|
114
|
-
ui: {
|
|
115
|
-
overlay: {
|
|
116
|
-
close: () => {}
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
114
|
version: '0.0.0',
|
|
120
115
|
customerPrivacy: createSubscribableSignalLike({
|
|
121
116
|
allowedProcessing: {
|