diff --git a/packages/examples/packages/interactive-ui/snap.manifest.json b/packages/examples/packages/interactive-ui/snap.manifest.json
index 6581945413..583e934906 100644
--- a/packages/examples/packages/interactive-ui/snap.manifest.json
+++ b/packages/examples/packages/interactive-ui/snap.manifest.json
@@ -7,7 +7,7 @@
"url": "https://github.com/MetaMask/snaps.git"
},
"source": {
- "shasum": "uOuMArCEwmOmCl0Bl1WnRRm+DKcq0Y+O+5n8Z1KBMr8=",
+ "shasum": "WSCjxt5olWIenXrxEpjc90jeiv5odFCZ1PQ67OwzgBk=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
diff --git a/packages/examples/packages/interactive-ui/src/components/InteractiveForm.tsx b/packages/examples/packages/interactive-ui/src/components/InteractiveForm.tsx
index b1e85a3700..bea2545f9e 100644
--- a/packages/examples/packages/interactive-ui/src/components/InteractiveForm.tsx
+++ b/packages/examples/packages/interactive-ui/src/components/InteractiveForm.tsx
@@ -1,5 +1,8 @@
import type { SnapComponent } from '@metamask/snaps-sdk/jsx';
import {
+ Card,
+ Selector,
+ SelectorOption,
Radio,
RadioGroup,
Button,
@@ -36,6 +39,11 @@ export type InteractiveFormState = {
* The value of the example checkbox.
*/
'example-checkbox': boolean;
+
+ /**
+ * The value of the example Selector.
+ */
+ 'example-selector': string;
};
export const InteractiveForm: SnapComponent = () => {
@@ -63,9 +71,28 @@ export const InteractiveForm: SnapComponent = () => {
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
diff --git a/packages/examples/packages/interactive-ui/src/components/Result.tsx b/packages/examples/packages/interactive-ui/src/components/Result.tsx
index 9e210aea5a..3d75d4dff4 100644
--- a/packages/examples/packages/interactive-ui/src/components/Result.tsx
+++ b/packages/examples/packages/interactive-ui/src/components/Result.tsx
@@ -17,7 +17,9 @@ export const Result: SnapComponent = ({ values }) => {
))}
-
+
+
+
);
};
diff --git a/packages/examples/packages/interactive-ui/src/index.test.tsx b/packages/examples/packages/interactive-ui/src/index.test.tsx
index a15951e19b..5304ee8c77 100644
--- a/packages/examples/packages/interactive-ui/src/index.test.tsx
+++ b/packages/examples/packages/interactive-ui/src/index.test.tsx
@@ -45,6 +45,8 @@ describe('onRpcRequest', () => {
await formScreen.selectFromRadioGroup('example-radiogroup', 'option3');
+ await formScreen.selectFromSelector('example-selector', 'option2');
+
await formScreen.clickElement('example-checkbox');
await formScreen.clickElement('submit');
@@ -59,6 +61,7 @@ describe('onRpcRequest', () => {
'example-dropdown': 'option3',
'example-radiogroup': 'option3',
'example-checkbox': true,
+ 'example-selector': 'option2',
}}
/>,
);
@@ -90,6 +93,7 @@ describe('onRpcRequest', () => {
'example-dropdown': 'option1',
'example-radiogroup': 'option1',
'example-checkbox': false,
+ 'example-selector': 'option1',
}}
/>,
);
@@ -116,6 +120,8 @@ describe('onHomePage', () => {
await formScreen.selectFromRadioGroup('example-radiogroup', 'option3');
+ await formScreen.selectFromSelector('example-selector', 'option2');
+
await formScreen.clickElement('submit');
const resultScreen = response.getInterface();
@@ -127,6 +133,7 @@ describe('onHomePage', () => {
'example-dropdown': 'option3',
'example-radiogroup': 'option3',
'example-checkbox': false,
+ 'example-selector': 'option2',
}}
/>,
);
diff --git a/packages/snaps-jest/src/helpers.test.tsx b/packages/snaps-jest/src/helpers.test.tsx
index f24364cb48..1867def9d9 100644
--- a/packages/snaps-jest/src/helpers.test.tsx
+++ b/packages/snaps-jest/src/helpers.test.tsx
@@ -409,6 +409,7 @@ describe('installSnap', () => {
typeInField: expect.any(Function),
selectInDropdown: expect.any(Function),
selectFromRadioGroup: expect.any(Function),
+ selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
ok: expect.any(Function),
cancel: expect.any(Function),
@@ -470,6 +471,7 @@ describe('installSnap', () => {
typeInField: expect.any(Function),
selectInDropdown: expect.any(Function),
selectFromRadioGroup: expect.any(Function),
+ selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
ok: expect.any(Function),
cancel: expect.any(Function),
@@ -531,6 +533,7 @@ describe('installSnap', () => {
typeInField: expect.any(Function),
selectInDropdown: expect.any(Function),
selectFromRadioGroup: expect.any(Function),
+ selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
ok: expect.any(Function),
});
diff --git a/packages/snaps-jest/src/internals/request.test.tsx b/packages/snaps-jest/src/internals/request.test.tsx
index 488dda1b72..2c80da79ca 100644
--- a/packages/snaps-jest/src/internals/request.test.tsx
+++ b/packages/snaps-jest/src/internals/request.test.tsx
@@ -1,7 +1,15 @@
import { SnapInterfaceController } from '@metamask/snaps-controllers';
import type { SnapId } from '@metamask/snaps-sdk';
import { UserInputEventType, button, input, text } from '@metamask/snaps-sdk';
-import { Dropdown, Option, Radio, RadioGroup } from '@metamask/snaps-sdk/jsx';
+import {
+ Card,
+ Dropdown,
+ Option,
+ Radio,
+ RadioGroup,
+ Selector,
+ SelectorOption,
+} from '@metamask/snaps-sdk/jsx';
import { getJsxElementFromComponent, HandlerType } from '@metamask/snaps-utils';
import { MOCK_SNAP_ID } from '@metamask/snaps-utils/test-utils';
@@ -273,6 +281,7 @@ describe('getInterfaceApi', () => {
typeInField: expect.any(Function),
selectInDropdown: expect.any(Function),
selectFromRadioGroup: expect.any(Function),
+ selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
});
});
@@ -305,6 +314,7 @@ describe('getInterfaceApi', () => {
typeInField: expect.any(Function),
selectInDropdown: expect.any(Function),
selectFromRadioGroup: expect.any(Function),
+ selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
});
});
@@ -517,4 +527,61 @@ describe('getInterfaceApi', () => {
},
);
});
+
+ it('sends the request to the snap when using `selectInSelector`', async () => {
+ const controllerMessenger = getRootControllerMessenger();
+
+ jest.spyOn(controllerMessenger, 'call');
+
+ // eslint-disable-next-line no-new
+ new SnapInterfaceController({
+ messenger:
+ getRestrictedSnapInterfaceControllerMessenger(controllerMessenger),
+ });
+
+ const content = (
+
+
+
+
+
+
+
+
+ );
+
+ const getInterface = await getInterfaceApi(
+ { content },
+ MOCK_SNAP_ID,
+ controllerMessenger,
+ );
+
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const snapInterface = getInterface!();
+
+ await snapInterface.selectFromSelector('foo', 'option2');
+
+ expect(controllerMessenger.call).toHaveBeenNthCalledWith(
+ 6,
+ 'ExecutionService:handleRpcRequest',
+ MOCK_SNAP_ID,
+ {
+ origin: '',
+ handler: HandlerType.OnUserInput,
+ request: {
+ jsonrpc: '2.0',
+ method: ' ',
+ params: {
+ event: {
+ type: UserInputEventType.InputChangeEvent,
+ name: 'foo',
+ value: 'option2',
+ },
+ id: expect.any(String),
+ context: null,
+ },
+ },
+ },
+ );
+ });
});
diff --git a/packages/snaps-jest/src/internals/simulation/interface.test.tsx b/packages/snaps-jest/src/internals/simulation/interface.test.tsx
index d3be3c69b4..a511cab366 100644
--- a/packages/snaps-jest/src/internals/simulation/interface.test.tsx
+++ b/packages/snaps-jest/src/internals/simulation/interface.test.tsx
@@ -24,6 +24,9 @@ import {
Form,
Container,
Footer,
+ SelectorOption,
+ Card,
+ Selector,
} from '@metamask/snaps-sdk/jsx';
import {
getJsxElementFromComponent,
@@ -59,6 +62,7 @@ import {
selectFromRadioGroup,
typeInField,
uploadFile,
+ selectFromSelector,
} from './interface';
import type { RunSagaFunction } from './store';
import { createStore, resolveInterface, setInterface } from './store';
@@ -83,6 +87,7 @@ describe('getInterfaceResponse', () => {
typeInField: jest.fn(),
selectInDropdown: jest.fn(),
selectFromRadioGroup: jest.fn(),
+ selectFromSelector: jest.fn(),
uploadFile: jest.fn(),
};
@@ -103,6 +108,7 @@ describe('getInterfaceResponse', () => {
typeInField: expect.any(Function),
selectInDropdown: expect.any(Function),
selectFromRadioGroup: expect.any(Function),
+ selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
ok: expect.any(Function),
});
@@ -129,6 +135,7 @@ describe('getInterfaceResponse', () => {
typeInField: expect.any(Function),
selectInDropdown: expect.any(Function),
selectFromRadioGroup: expect.any(Function),
+ selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
ok: expect.any(Function),
cancel: expect.any(Function),
@@ -156,6 +163,7 @@ describe('getInterfaceResponse', () => {
typeInField: expect.any(Function),
selectInDropdown: expect.any(Function),
selectFromRadioGroup: expect.any(Function),
+ selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
ok: expect.any(Function),
cancel: expect.any(Function),
@@ -183,6 +191,7 @@ describe('getInterfaceResponse', () => {
typeInField: expect.any(Function),
selectInDropdown: expect.any(Function),
selectFromRadioGroup: expect.any(Function),
+ selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
ok: expect.any(Function),
cancel: expect.any(Function),
@@ -210,6 +219,7 @@ describe('getInterfaceResponse', () => {
typeInField: expect.any(Function),
selectInDropdown: expect.any(Function),
selectFromRadioGroup: expect.any(Function),
+ selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
ok: expect.any(Function),
cancel: expect.any(Function),
@@ -237,6 +247,7 @@ describe('getInterfaceResponse', () => {
typeInField: expect.any(Function),
selectInDropdown: expect.any(Function),
selectFromRadioGroup: expect.any(Function),
+ selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
ok: expect.any(Function),
cancel: expect.any(Function),
@@ -283,6 +294,7 @@ describe('getInterfaceResponse', () => {
typeInField: expect.any(Function),
selectInDropdown: expect.any(Function),
selectFromRadioGroup: expect.any(Function),
+ selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
});
});
@@ -321,6 +333,7 @@ describe('getInterfaceResponse', () => {
typeInField: expect.any(Function),
selectInDropdown: expect.any(Function),
selectFromRadioGroup: expect.any(Function),
+ selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
cancel: expect.any(Function),
});
@@ -354,6 +367,7 @@ describe('getInterfaceResponse', () => {
typeInField: expect.any(Function),
selectInDropdown: expect.any(Function),
selectFromRadioGroup: expect.any(Function),
+ selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
cancel: expect.any(Function),
ok: expect.any(Function),
@@ -1143,6 +1157,7 @@ describe('getInterface', () => {
typeInField: expect.any(Function),
selectInDropdown: expect.any(Function),
selectFromRadioGroup: expect.any(Function),
+ selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
ok: expect.any(Function),
});
@@ -1172,6 +1187,7 @@ describe('getInterface', () => {
typeInField: expect.any(Function),
selectInDropdown: expect.any(Function),
selectFromRadioGroup: expect.any(Function),
+ selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
ok: expect.any(Function),
});
@@ -1504,3 +1520,153 @@ describe('selectFromRadioGroup', () => {
);
});
});
+
+describe('selectFromSelector', () => {
+ const rootControllerMessenger = getRootControllerMessenger();
+ const controllerMessenger = getRestrictedSnapInterfaceControllerMessenger(
+ rootControllerMessenger,
+ );
+
+ const interfaceController = new SnapInterfaceController({
+ messenger: controllerMessenger,
+ });
+
+ const handleRpcRequestMock = jest.fn();
+
+ rootControllerMessenger.registerActionHandler(
+ 'ExecutionService:handleRpcRequest',
+ handleRpcRequestMock,
+ );
+
+ it('updates the interface state and sends an InputChangeEvent', async () => {
+ jest.spyOn(rootControllerMessenger, 'call');
+
+ const content = (
+
+
+
+
+
+
+
+
+ );
+
+ const interfaceId = await interfaceController.createInterface(
+ MOCK_SNAP_ID,
+ content,
+ );
+
+ await selectFromSelector(
+ rootControllerMessenger,
+ interfaceId,
+ content,
+ MOCK_SNAP_ID,
+ 'foo',
+ 'option2',
+ );
+
+ expect(rootControllerMessenger.call).toHaveBeenCalledWith(
+ 'SnapInterfaceController:updateInterfaceState',
+ interfaceId,
+ { foo: 'option2' },
+ );
+
+ expect(handleRpcRequestMock).toHaveBeenCalledWith(MOCK_SNAP_ID, {
+ origin: '',
+ handler: HandlerType.OnUserInput,
+ request: {
+ jsonrpc: '2.0',
+ method: ' ',
+ params: {
+ event: {
+ type: UserInputEventType.InputChangeEvent,
+ name: 'foo',
+ value: 'option2',
+ },
+ id: interfaceId,
+ context: null,
+ },
+ },
+ });
+ });
+
+ it('throws if chosen option does not exist', async () => {
+ const content = (
+
+
+
+
+
+
+
+
+ );
+
+ const interfaceId = await interfaceController.createInterface(
+ MOCK_SNAP_ID,
+ content,
+ );
+
+ await expect(
+ selectFromSelector(
+ rootControllerMessenger,
+ interfaceId,
+ content,
+ MOCK_SNAP_ID,
+ 'foo',
+ 'option3',
+ ),
+ ).rejects.toThrow(
+ 'The Selector with the name "foo" does not contain "option3"',
+ );
+ });
+
+ it('throws if there is no Selector in the interface', async () => {
+ const content = (
+
+ Foo
+
+ );
+
+ const interfaceId = await interfaceController.createInterface(
+ MOCK_SNAP_ID,
+ content,
+ );
+
+ await expect(
+ selectFromSelector(
+ rootControllerMessenger,
+ interfaceId,
+ content,
+ MOCK_SNAP_ID,
+ 'bar',
+ 'baz',
+ ),
+ ).rejects.toThrow(
+ 'Could not find an element in the interface with the name "bar".',
+ );
+ });
+
+ it('throws if the element is not a Selector', async () => {
+ const content = ;
+
+ const interfaceId = await interfaceController.createInterface(
+ MOCK_SNAP_ID,
+ content,
+ );
+
+ await expect(
+ selectFromSelector(
+ rootControllerMessenger,
+ interfaceId,
+ content,
+ MOCK_SNAP_ID,
+ 'foo',
+ 'baz',
+ ),
+ ).rejects.toThrow(
+ 'Expected an element of type "Selector", but found "Input".',
+ );
+ });
+});
diff --git a/packages/snaps-jest/src/internals/simulation/interface.ts b/packages/snaps-jest/src/internals/simulation/interface.ts
index f11666a864..cc1023ee35 100644
--- a/packages/snaps-jest/src/internals/simulation/interface.ts
+++ b/packages/snaps-jest/src/internals/simulation/interface.ts
@@ -664,6 +664,80 @@ export async function selectFromRadioGroup(
});
}
+/**
+ * Choose an option with value from Selector interface element.
+ *
+ * @param controllerMessenger - The controller messenger used to call actions.
+ * @param id - The interface ID.
+ * @param content - The interface Components.
+ * @param snapId - The Snap ID.
+ * @param name - The element name.
+ * @param value - The value to type in the element.
+ */
+export async function selectFromSelector(
+ controllerMessenger: RootControllerMessenger,
+ id: string,
+ content: JSXElement,
+ snapId: SnapId,
+ name: string,
+ value: string,
+) {
+ const result = getElement(content, name);
+
+ assert(
+ result !== undefined,
+ `Could not find an element in the interface with the name "${name}".`,
+ );
+
+ assert(
+ result.element.type === 'Selector',
+ `Expected an element of type "Selector", but found "${result.element.type}".`,
+ );
+
+ const options = getJsxChildren(result.element) as JSXElement[];
+ const selectedOption = options.find(
+ (option) =>
+ hasProperty(option.props, 'value') && option.props.value === value,
+ );
+
+ assert(
+ selectedOption !== undefined,
+ `The Selector with the name "${name}" does not contain "${value}".`,
+ );
+
+ const { state, context } = controllerMessenger.call(
+ 'SnapInterfaceController:getInterface',
+ snapId,
+ id,
+ );
+
+ const newState = mergeValue(state, name, value, result.form);
+
+ controllerMessenger.call(
+ 'SnapInterfaceController:updateInterfaceState',
+ id,
+ newState,
+ );
+
+ await controllerMessenger.call('ExecutionService:handleRpcRequest', snapId, {
+ origin: '',
+ handler: HandlerType.OnUserInput,
+ request: {
+ jsonrpc: '2.0',
+ method: ' ',
+ params: {
+ event: {
+ type: UserInputEventType.InputChangeEvent,
+ name: result.element.props.name,
+ value,
+ },
+ id,
+ context,
+ },
+ },
+ });
+}
+
/**
* Get a formatted file size.
*
@@ -809,6 +883,17 @@ export function getInterfaceActions(
);
},
+ selectFromSelector: async (name: string, value: string) => {
+ await selectFromSelector(
+ controllerMessenger,
+ id,
+ content,
+ snapId,
+ name,
+ value,
+ );
+ },
+
uploadFile: async (
name: string,
file: string | Uint8Array,
diff --git a/packages/snaps-jest/src/types/types.ts b/packages/snaps-jest/src/types/types.ts
index a5476de548..0d3eedd3ae 100644
--- a/packages/snaps-jest/src/types/types.ts
+++ b/packages/snaps-jest/src/types/types.ts
@@ -129,6 +129,14 @@ export type SnapInterfaceActions = {
*/
selectFromRadioGroup(name: string, value: string): Promise;
+ /**
+ * Choose an option with a value from Selector component.
+ *
+ * @param name - The element name to type in.
+ * @param value - The value to type.
+ */
+ selectFromSelector(name: string, value: string): Promise;
+
/**
* Upload a file.
*