diff --git a/packages/snaps-jest/src/helpers.test.tsx b/packages/snaps-jest/src/helpers.test.tsx
index 6f7ccf69ba..998325c349 100644
--- a/packages/snaps-jest/src/helpers.test.tsx
+++ b/packages/snaps-jest/src/helpers.test.tsx
@@ -411,6 +411,7 @@ describe('installSnap', () => {
selectFromRadioGroup: expect.any(Function),
selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
+ waitForUpdate: expect.any(Function),
ok: expect.any(Function),
cancel: expect.any(Function),
});
@@ -473,6 +474,7 @@ describe('installSnap', () => {
selectFromRadioGroup: expect.any(Function),
selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
+ waitForUpdate: expect.any(Function),
ok: expect.any(Function),
cancel: expect.any(Function),
});
@@ -535,6 +537,7 @@ describe('installSnap', () => {
selectFromRadioGroup: expect.any(Function),
selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
+ waitForUpdate: expect.any(Function),
ok: expect.any(Function),
});
diff --git a/packages/snaps-simulation/package.json b/packages/snaps-simulation/package.json
index f9948c9f39..8c573f5276 100644
--- a/packages/snaps-simulation/package.json
+++ b/packages/snaps-simulation/package.json
@@ -70,6 +70,7 @@
"@metamask/superstruct": "^3.1.0",
"@metamask/utils": "^10.0.0",
"@reduxjs/toolkit": "^1.9.5",
+ "fast-deep-equal": "^3.1.3",
"mime": "^3.0.0",
"readable-stream": "^3.6.2",
"redux-saga": "^1.2.3"
diff --git a/packages/snaps-simulation/src/controllers.ts b/packages/snaps-simulation/src/controllers.ts
index 0877480b7d..b67989d539 100644
--- a/packages/snaps-simulation/src/controllers.ts
+++ b/packages/snaps-simulation/src/controllers.ts
@@ -15,6 +15,7 @@ import type {
ExecutionServiceActions,
SnapInterfaceControllerActions,
SnapInterfaceControllerAllowedActions,
+ SnapInterfaceControllerStateChangeEvent,
} from '@metamask/snaps-controllers';
import {
caveatSpecifications as snapsCaveatsSpecifications,
@@ -38,9 +39,12 @@ export type RootControllerAllowedActions =
| ExecutionServiceActions
| SubjectMetadataControllerActions;
+export type RootControllerAllowedEvents =
+ SnapInterfaceControllerStateChangeEvent;
+
export type RootControllerMessenger = ControllerMessenger<
RootControllerAllowedActions,
- any
+ RootControllerAllowedEvents
>;
export type GetControllersOptions = {
diff --git a/packages/snaps-simulation/src/helpers.test.tsx b/packages/snaps-simulation/src/helpers.test.tsx
index 53368ef92a..7dc369ef8e 100644
--- a/packages/snaps-simulation/src/helpers.test.tsx
+++ b/packages/snaps-simulation/src/helpers.test.tsx
@@ -145,6 +145,7 @@ describe('helpers', () => {
selectFromRadioGroup: expect.any(Function),
selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
+ waitForUpdate: expect.any(Function),
ok: expect.any(Function),
cancel: expect.any(Function),
});
@@ -207,6 +208,7 @@ describe('helpers', () => {
selectFromRadioGroup: expect.any(Function),
selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
+ waitForUpdate: expect.any(Function),
ok: expect.any(Function),
cancel: expect.any(Function),
});
@@ -269,6 +271,7 @@ describe('helpers', () => {
selectFromRadioGroup: expect.any(Function),
selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
+ waitForUpdate: expect.any(Function),
ok: expect.any(Function),
});
diff --git a/packages/snaps-simulation/src/interface.test.tsx b/packages/snaps-simulation/src/interface.test.tsx
index 0ed7bc866e..02368bfaed 100644
--- a/packages/snaps-simulation/src/interface.test.tsx
+++ b/packages/snaps-simulation/src/interface.test.tsx
@@ -50,6 +50,7 @@ import {
typeInField,
uploadFile,
selectFromSelector,
+ waitForUpdate,
} from './interface';
import type { RunSagaFunction } from './store';
import { createStore, resolveInterface, setInterface } from './store';
@@ -90,6 +91,7 @@ describe('getInterfaceResponse', () => {
selectFromRadioGroup: jest.fn(),
selectFromSelector: jest.fn(),
uploadFile: jest.fn(),
+ waitForUpdate: jest.fn(),
};
it('returns an `ok` function that resolves the user interface with `null` for alert dialogs', async () => {
@@ -111,6 +113,7 @@ describe('getInterfaceResponse', () => {
selectFromRadioGroup: expect.any(Function),
selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
+ waitForUpdate: expect.any(Function),
ok: expect.any(Function),
});
@@ -138,6 +141,7 @@ describe('getInterfaceResponse', () => {
selectFromRadioGroup: expect.any(Function),
selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
+ waitForUpdate: expect.any(Function),
ok: expect.any(Function),
cancel: expect.any(Function),
});
@@ -166,6 +170,7 @@ describe('getInterfaceResponse', () => {
selectFromRadioGroup: expect.any(Function),
selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
+ waitForUpdate: expect.any(Function),
ok: expect.any(Function),
cancel: expect.any(Function),
});
@@ -194,6 +199,7 @@ describe('getInterfaceResponse', () => {
selectFromRadioGroup: expect.any(Function),
selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
+ waitForUpdate: expect.any(Function),
ok: expect.any(Function),
cancel: expect.any(Function),
});
@@ -222,6 +228,7 @@ describe('getInterfaceResponse', () => {
selectFromRadioGroup: expect.any(Function),
selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
+ waitForUpdate: expect.any(Function),
ok: expect.any(Function),
cancel: expect.any(Function),
});
@@ -250,6 +257,7 @@ describe('getInterfaceResponse', () => {
selectFromRadioGroup: expect.any(Function),
selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
+ waitForUpdate: expect.any(Function),
ok: expect.any(Function),
cancel: expect.any(Function),
});
@@ -296,6 +304,7 @@ describe('getInterfaceResponse', () => {
selectInDropdown: expect.any(Function),
selectFromRadioGroup: expect.any(Function),
selectFromSelector: expect.any(Function),
+ waitForUpdate: expect.any(Function),
uploadFile: expect.any(Function),
});
});
@@ -336,6 +345,7 @@ describe('getInterfaceResponse', () => {
selectFromRadioGroup: expect.any(Function),
selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
+ waitForUpdate: expect.any(Function),
cancel: expect.any(Function),
});
});
@@ -370,6 +380,7 @@ describe('getInterfaceResponse', () => {
selectFromRadioGroup: expect.any(Function),
selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
+ waitForUpdate: expect.any(Function),
cancel: expect.any(Function),
ok: expect.any(Function),
});
@@ -1250,6 +1261,7 @@ describe('getInterface', () => {
selectFromRadioGroup: expect.any(Function),
selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
+ waitForUpdate: expect.any(Function),
ok: expect.any(Function),
});
});
@@ -1280,6 +1292,7 @@ describe('getInterface', () => {
selectFromRadioGroup: expect.any(Function),
selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
+ waitForUpdate: expect.any(Function),
ok: expect.any(Function),
});
});
@@ -1468,6 +1481,41 @@ describe('getInterface', () => {
},
);
});
+
+ it('waits for the interface content to update when `waitForUpdate` is called', async () => {
+ jest.spyOn(rootControllerMessenger, 'call');
+ const { store, runSaga } = createStore(getMockOptions());
+
+ const content = (
+
+
+
+ );
+ const id = await interfaceController.createInterface(MOCK_SNAP_ID, content);
+ const type = DialogType.Alert;
+ const ui = { type: DIALOG_APPROVAL_TYPES[type], id };
+
+ store.dispatch(setInterface(ui));
+
+ const result = await runSaga(
+ getInterface,
+ runSaga,
+ MOCK_SNAP_ID,
+ rootControllerMessenger,
+ ).toPromise();
+
+ const promise = result.waitForUpdate();
+
+ await interfaceController.updateInterface(
+ MOCK_SNAP_ID,
+ id,
+ Hello world!,
+ );
+
+ const newInterface = await promise;
+
+ expect(newInterface.content.type).toBe('Text');
+ });
});
describe('selectFromRadioGroup', () => {
@@ -1761,3 +1809,40 @@ describe('selectFromSelector', () => {
);
});
});
+
+describe('waitForUpdate', () => {
+ const rootControllerMessenger = getRootControllerMessenger();
+ const controllerMessenger = getRestrictedSnapInterfaceControllerMessenger(
+ rootControllerMessenger,
+ );
+
+ const interfaceController = new SnapInterfaceController({
+ messenger: controllerMessenger,
+ });
+
+ it('waits for the interface content to update', async () => {
+ const content = ;
+
+ const interfaceId = await interfaceController.createInterface(
+ MOCK_SNAP_ID,
+ content,
+ );
+
+ const promise = waitForUpdate(
+ rootControllerMessenger,
+ MOCK_SNAP_ID,
+ interfaceId,
+ content,
+ );
+
+ await interfaceController.updateInterface(
+ MOCK_SNAP_ID,
+ interfaceId,
+ Hello world!,
+ );
+
+ const newInterface = await promise;
+
+ expect(newInterface.content.type).toBe('Text');
+ });
+});
diff --git a/packages/snaps-simulation/src/interface.ts b/packages/snaps-simulation/src/interface.ts
index 6933857f83..1c9aee7c28 100644
--- a/packages/snaps-simulation/src/interface.ts
+++ b/packages/snaps-simulation/src/interface.ts
@@ -1,3 +1,4 @@
+import type { SnapInterfaceControllerState } from '@metamask/snaps-controllers';
import type { DialogApprovalTypes } from '@metamask/snaps-rpc-methods';
import { DIALOG_APPROVAL_TYPES } from '@metamask/snaps-rpc-methods';
import type {
@@ -19,6 +20,7 @@ import {
} from '@metamask/snaps-utils';
import { assertExhaustive, hasProperty } from '@metamask/utils';
import type { PayloadAction } from '@reduxjs/toolkit';
+import deepEqual from 'fast-deep-equal';
import { type SagaIterator } from 'redux-saga';
import { call, put, select, take } from 'redux-saga/effects';
@@ -26,7 +28,12 @@ import type { RootControllerMessenger } from './controllers';
import { getFileSize, getFileToUpload } from './files';
import type { Interface, RunSagaFunction } from './store';
import { getCurrentInterface, resolveInterface, setInterface } from './store';
-import type { FileOptions, SnapInterface, SnapInterfaceActions } from './types';
+import type {
+ FileOptions,
+ SnapHandlerInterface,
+ SnapInterface,
+ SnapInterfaceActions,
+} from './types';
/**
* The maximum file size that can be uploaded.
@@ -752,6 +759,48 @@ export async function selectFromSelector(
});
}
+/**
+ * Wait for an interface to be updated.
+ *
+ * @param controllerMessenger - The controller messenger used to call actions.
+ * @param snapId - The Snap ID.
+ * @param id - The interface ID.
+ * @param originalContent - The original interface content.
+ * @returns A promise that resolves to the updated interface.
+ */
+export async function waitForUpdate(
+ controllerMessenger: RootControllerMessenger,
+ snapId: SnapId,
+ id: string,
+ originalContent: JSXElement,
+) {
+ return new Promise((resolve) => {
+ const listener = (state: SnapInterfaceControllerState) => {
+ const currentInterface = state.interfaces[id];
+ const newContent = currentInterface?.content;
+
+ if (!deepEqual(originalContent, newContent)) {
+ controllerMessenger.unsubscribe(
+ 'SnapInterfaceController:stateChange',
+ listener,
+ );
+
+ const actions = getInterfaceActions(snapId, controllerMessenger, {
+ content: newContent,
+ id,
+ });
+
+ resolve({ ...actions, content: newContent });
+ }
+ };
+
+ controllerMessenger.subscribe(
+ 'SnapInterfaceController:stateChange',
+ listener,
+ );
+ });
+}
+
/**
* Get a formatted file size.
*
@@ -923,6 +972,9 @@ export function getInterfaceActions(
options,
);
},
+
+ waitForUpdate: async () =>
+ waitForUpdate(controllerMessenger, snapId, id, content),
};
}
diff --git a/packages/snaps-simulation/src/request.test.tsx b/packages/snaps-simulation/src/request.test.tsx
index 60e3025c4e..67adb77b60 100644
--- a/packages/snaps-simulation/src/request.test.tsx
+++ b/packages/snaps-simulation/src/request.test.tsx
@@ -115,6 +115,7 @@ describe('handleRequest', () => {
selectInDropdown: expect.any(Function),
typeInField: expect.any(Function),
uploadFile: expect.any(Function),
+ waitForUpdate: expect.any(Function),
});
await closeServer();
@@ -346,6 +347,7 @@ describe('getInterfaceApi', () => {
selectFromRadioGroup: expect.any(Function),
selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
+ waitForUpdate: expect.any(Function),
});
});
@@ -379,6 +381,7 @@ describe('getInterfaceApi', () => {
selectFromRadioGroup: expect.any(Function),
selectFromSelector: expect.any(Function),
uploadFile: expect.any(Function),
+ waitForUpdate: expect.any(Function),
});
});
diff --git a/packages/snaps-simulation/src/types.ts b/packages/snaps-simulation/src/types.ts
index 5bb52f557c..0c1c119c34 100644
--- a/packages/snaps-simulation/src/types.ts
+++ b/packages/snaps-simulation/src/types.ts
@@ -173,6 +173,11 @@ export type SnapInterfaceActions = {
file: string | Uint8Array,
options?: FileOptions,
): Promise;
+
+ /**
+ * Wait for the interface to be updated.
+ */
+ waitForUpdate: () => Promise;
};
/**
diff --git a/yarn.lock b/yarn.lock
index 5eb2d26aed..e956661fc8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6145,6 +6145,7 @@ __metadata:
eslint-plugin-prettier: "npm:^4.2.1"
eslint-plugin-promise: "npm:^6.1.1"
express: "npm:^4.18.2"
+ fast-deep-equal: "npm:^3.1.3"
jest: "npm:^29.0.2"
jest-it-up: "npm:^2.0.0"
jest-silent-reporter: "npm:^0.6.0"