Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/pos-intercept-capabilities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@shopify/ui-extensions': minor
'@shopify/ui-extensions-tester': minor
---

Add `.error`, `.warning`, and `.info` POS intercept severity values to the existing `shopify.capabilities` signal, with corresponding POS target mocks in `@shopify/ui-extensions-tester`.
8 changes: 8 additions & 0 deletions packages/ui-extensions-tester/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,14 @@ test('it handles an empty order', async () => {
});
```

POS target mocks include an empty `shopify.capabilities` signal. Set its value to test capability-dependent behavior:

```ts
extension.shopify.capabilities.value = [
'beforecheckout.error',
];
```

### 🖱️ Triggering events

To simulate how a user would interact with your UI extension, you can call [`dispatchEvent()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent) or use `fireEvent` from `@testing-library/preact`. When an event triggers an async state change (like a Preact re-render), wrap follow-up assertions in `await waitFor()` to wait for the DOM to settle:
Expand Down
10 changes: 10 additions & 0 deletions packages/ui-extensions-tester/src/point-of-sale/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ expect(tile.getAttribute('subheading')).toEqual(
);
```

## ✅ Mocking capabilities

POS target mocks include an empty capabilities signal by default. Replace its value with the capabilities needed by your test:

```ts
extension.shopify.capabilities.value = [
'beforecheckout.error',
];
```

## 💾 Mocking storage

POS storage is a typed key-value store:
Expand Down
2 changes: 2 additions & 0 deletions packages/ui-extensions-tester/src/point-of-sale/factories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ function createMockStandardApi<T extends RenderExtensionTarget>(
target,
},
i18n: createMockI18n(),
capabilities: createReadonlySignalLike([]),
locale: {current: createReadonlySignalLike('en-US')},
toast: {show: () => {}},
session: {
Expand Down Expand Up @@ -460,6 +461,7 @@ function createDataTargetMock<T extends ExtensionTarget>(
target,
},
i18n: createMockI18n(),
capabilities: createReadonlySignalLike([]),
session: {
currentSession: createSessionCurrentSession(),
staffMember: createReadonlySignalLike(createStaffMember()),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {getExtension} from '../index';

import {createTestSandbox, type TestSandbox} from './helpers';

describe('POS capabilities mocks', () => {
let sandbox: TestSandbox;

beforeEach(() => {
sandbox = createTestSandbox();
});

afterEach(() => {
sandbox.destroy();
});

it('provides an empty capabilities signal for standard targets', () => {
sandbox.placeToml({target: 'pos.home.tile.render'});
const extension = getExtension('pos.home.tile.render', {
configSearchDir: sandbox.tempDir,
});

extension.setUp();

expect(extension.shopify.capabilities.value).toStrictEqual([]);
});

it('provides an empty capabilities signal for data targets', () => {
sandbox.placeToml({target: 'pos.app.ready.data'});
const extension = getExtension('pos.app.ready.data', {
configSearchDir: sandbox.tempDir,
});

extension.setUp();

expect(extension.shopify.capabilities.value).toStrictEqual([]);
});

it('allows tests to configure granted capabilities', () => {
sandbox.placeToml({target: 'pos.app.ready.data'});
const extension = getExtension('pos.app.ready.data', {
configSearchDir: sandbox.tempDir,
});

extension.setUp();
extension.shopify.capabilities.value = [
'beforecheckout.error',
'beforecheckout.warning',
'beforecheckout.info',
];

expect(extension.shopify.capabilities.value).toStrictEqual([
'beforecheckout.error',
'beforecheckout.warning',
'beforecheckout.info',
]);
});
});

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions packages/ui-extensions/src/surfaces/point-of-sale/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ export type {DeviceApi, DeviceApiContent} from './api/device-api/device-api';

export type {LocaleApi, LocaleApiContent} from './api/locale-api/locale-api';

export type {
CapabilitiesApi,
Capability,
InterceptCapability,
} from './api/capabilities-api/capabilities-api';

export type {OrderApiContent, OrderApi} from './api/order-api/order-api';

export type {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type {ReadonlySignalLike} from '../../../../shared';
import type {DataTargetApi} from '../data-target-api/data-target-api';
import type {StandardApi} from '../standard/standard-api';
import type {Capability, InterceptCapability} from './capabilities-api';

function createSignal<T>(value: T): ReadonlySignalLike<T> {
return {
value,
subscribe: () => () => undefined,
};
}

describe('POS capabilities API', () => {
it('is included in standard target APIs', () => {
const capabilities: StandardApi<'pos.home.tile.render'>['capabilities'] =
createSignal<Capability[]>([]);

expect(capabilities.value).toStrictEqual([]);
});

it('is included in data target APIs', () => {
const capabilities: DataTargetApi<'pos.app.ready.data'>['capabilities'] =
createSignal<Capability[]>([]);

expect(capabilities.value).toStrictEqual([]);
});

it('accepts all capabilities implied by an error grant', () => {
const capabilities: InterceptCapability[] = [
'beforecheckout.error',
'beforecheckout.warning',
'beforecheckout.info',
];

expect(capabilities).toStrictEqual([
'beforecheckout.error',
'beforecheckout.warning',
'beforecheckout.info',
]);
});

it('accepts a warning grant and info without error', () => {
const capabilities: InterceptCapability[] = [
'beforecheckout.warning',
'beforecheckout.info',
];

expect(capabilities).not.toContain('beforecheckout.error');
});

it('accepts only info with an info grant', () => {
const capabilities: InterceptCapability[] = ['beforecheckout.info'];

expect(capabilities).not.toContain('beforecheckout.error');
expect(capabilities).not.toContain('beforecheckout.warning');
});

it('accepts an empty array when no intercept capabilities are granted', () => {
const capabilities: InterceptCapability[] = [];

expect(capabilities).toStrictEqual([]);
});

it('types intercept capabilities from event names and severity suffixes', () => {
const capabilities: InterceptCapability[] = [
// @ts-expect-error Event names must come from ShopifyInterceptMap.
'unsupported.error',
// @ts-expect-error Capability suffixes use `warning`, not `warn`.
'beforecheckout.warn',
];

expect(capabilities).toHaveLength(2);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type {ReadonlySignalLike} from '../../../../shared';
import type {ShopifyInterceptMap} from '../../events';

/**
* A granted validation severity for a POS intercept event. Event names are
* derived from `ShopifyInterceptMap`.
*
* Grants are cumulative. An `.error` grant includes `.warning` and `.info`,
* and a `.warning` grant includes `.info`.
*
* @publicDocs
*/
export type InterceptCapability = `${Extract<
keyof ShopifyInterceptMap,
string
>}.${'error' | 'warning' | 'info'}`;

/**
* A capability granted to a POS extension.
*
* @publicDocs
*/
export type Capability = InterceptCapability;

/**
* Provides the capabilities granted to a POS extension.
*
* @publicDocs
*/
export interface CapabilitiesApi {
/**
* The allowed capabilities of the extension, defined in your
* [`shopify.extension.toml`](/docs/api/pos-ui-extensions/{API_VERSION}/configuration) file.
*/
capabilities: ReadonlySignalLike<Capability[]>;
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {CapabilitiesApi} from '../capabilities-api/capabilities-api';
import {ReadonlyCartApi} from '../cart-api/cart-api';
import {ConnectivityApi} from '../connectivity-api/connectivity-api';
import {DeviceApi} from '../device-api/device-api';
Expand All @@ -19,6 +20,7 @@ export type DataTargetApi<T> = {
extensionPoint: T;
i18n: I18n;
} & ExtensionApi<T> &
CapabilitiesApi &
SessionApi &
StorageApi &
LocaleApi &
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {CameraApi} from '../camera-api/camera-api';
import {CapabilitiesApi} from '../capabilities-api/capabilities-api';
import {ConnectivityApi} from '../connectivity-api/connectivity-api';
import {DeviceApi} from '../device-api/device-api';
import {ExtensionApi} from '../extension-api/extension-api';
Expand All @@ -21,6 +22,7 @@ export type StandardApi<T> = {[key: string]: any} & {
extensionPoint: T;
i18n: I18n;
} & ExtensionApi<T> &
CapabilitiesApi &
LocaleApi &
ToastApi &
SessionApi &
Expand Down
Loading