From 141685da676dd6a5ba8a23a6c6e07d1007c0d301 Mon Sep 17 00:00:00 2001
From: Frederik Bolding
Date: Tue, 4 Feb 2025 14:23:52 +0100
Subject: [PATCH 01/12] test: Add basic tests for SnapUIRenderer
---
.../snap-ui-renderer.test.js.snap | 243 ++++++++++++++++++
.../snap-ui-renderer/snap-ui-renderer.test.js | 183 ++++++++++++-
.../app/snaps/snap-ui-renderer/utils.test.ts | 15 ++
3 files changed, 431 insertions(+), 10 deletions(-)
create mode 100644 ui/components/app/snaps/snap-ui-renderer/__snapshots__/snap-ui-renderer.test.js.snap
create mode 100644 ui/components/app/snaps/snap-ui-renderer/utils.test.ts
diff --git a/ui/components/app/snaps/snap-ui-renderer/__snapshots__/snap-ui-renderer.test.js.snap b/ui/components/app/snaps/snap-ui-renderer/__snapshots__/snap-ui-renderer.test.js.snap
new file mode 100644
index 000000000000..f2d49704a43f
--- /dev/null
+++ b/ui/components/app/snaps/snap-ui-renderer/__snapshots__/snap-ui-renderer.test.js.snap
@@ -0,0 +1,243 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SnapUIRenderer renders basic UI 1`] = `
+
+`;
+
+exports[`SnapUIRenderer renders footers 1`] = `
+
+`;
+
+exports[`SnapUIRenderer renders loading state 1`] = `
+
+`;
+
+exports[`SnapUIRenderer supports interactive inputs 1`] = `
+
+`;
+
+exports[`SnapUIRenderer supports the contentBackgroundColor prop 1`] = `
+
+`;
+
+exports[`SnapUIRenderer supports the onCancel prop 1`] = `
+
+`;
diff --git a/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.test.js b/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.test.js
index e6707f84e8c1..f87dcac0607a 100644
--- a/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.test.js
+++ b/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.test.js
@@ -1,15 +1,178 @@
-import { JSXElementStruct } from '@metamask/snaps-sdk/jsx';
-import { COMPONENT_MAPPING } from './components';
+import React from 'react';
+import {
+ Box,
+ Text,
+ Container,
+ Footer,
+ Button,
+ Input,
+} from '@metamask/snaps-sdk/jsx';
+import configureMockStore from 'redux-mock-store';
+import mockState from '../../../../../test/data/mock-state.json';
+import { renderWithProvider } from '../../../../../test/lib/render-helpers';
+import { SnapUIRenderer } from './snap-ui-renderer';
+import { fireEvent } from '@testing-library/react';
+import thunk from 'redux-thunk';
+import * as backgroundConnection from '../../../../store/background-connection';
+import { BackgroundColor } from '../../../../helpers/constants/design-system';
-const EXCLUDED_COMPONENTS = ['Option', 'Radio', 'SelectorOption'];
+jest.mock('../../../../store/background-connection', () => ({
+ ...jest.requireActual('../../../../store/background-connection'),
+ submitRequestToBackground: jest.fn(),
+}));
-describe('Snap UI mapping', () => {
- it('supports all exposed components', () => {
- const elements = JSXElementStruct.schema
- .map((struct) => JSON.parse(struct.schema.type.type))
- .filter((key) => !EXCLUDED_COMPONENTS.includes(key));
- expect(Object.keys(COMPONENT_MAPPING).sort()).toStrictEqual(
- elements.sort(),
+const { submitRequestToBackground } = jest.mocked(backgroundConnection);
+
+const MOCK_SNAP_ID = 'npm:@metamask/test-snap-bip44';
+const MOCK_INTERFACE_ID = 'interfaceId';
+
+function renderInterface(
+ content,
+ {
+ useFooter = false,
+ useDelineator = false,
+ onCancel,
+ contentBackgroundColor,
+ } = {},
+) {
+ const mockStore = configureMockStore([thunk])({
+ ...mockState,
+ metamask: {
+ ...mockState.metamask,
+ interfaces: {
+ [MOCK_INTERFACE_ID]: {
+ snapId: MOCK_SNAP_ID,
+ content,
+ state: {},
+ context: null,
+ contentType: null,
+ },
+ },
+ },
+ });
+ return renderWithProvider(
+ ,
+ mockStore,
+ );
+}
+
+describe('SnapUIRenderer', () => {
+ it('renders loading state', () => {
+ const { container } = renderInterface(null);
+
+ expect(container.getElementsByClassName('pulse-loader').length).toBe(1);
+ expect(container).toMatchSnapshot();
+ });
+
+ it('renders basic UI', () => {
+ const { container, getByText } = renderInterface(
+ Box({ children: Text({ children: 'Hello world!' }) }),
+ );
+
+ expect(getByText('Hello world!')).toBeDefined();
+ expect(container).toMatchSnapshot();
+ });
+
+ it('renders footers', () => {
+ const { container, getByText } = renderInterface(
+ Container({
+ children: [
+ Box({ children: Text({ children: 'Hello world!' }) }),
+ Footer({ children: Button({ children: 'Foo' }) }),
+ ],
+ }),
+ { useFooter: true },
+ );
+
+ expect(getByText('Foo')).toBeDefined();
+ expect(container).toMatchSnapshot();
+ });
+
+ it('supports the onCancel prop', () => {
+ const onCancel = jest.fn();
+ const { container, getByText } = renderInterface(
+ Container({
+ children: [
+ Box({ children: Text({ children: 'Hello world!' }) }),
+ Footer({ children: Button({ children: 'Foo' }) }),
+ ],
+ }),
+ { useFooter: true, onCancel },
);
+
+ const button = getByText('Cancel');
+ expect(button).toBeDefined();
+ expect(container).toMatchSnapshot();
+
+ fireEvent.click(button);
+ expect(onCancel).toHaveBeenCalled();
+ });
+
+ it('supports the contentBackgroundColor prop', () => {
+ const { container, getByText } = renderInterface(
+ Container({
+ children: [
+ Box({ children: Text({ children: 'Hello world!' }) }),
+ Footer({ children: Button({ children: 'Foo' }) }),
+ ],
+ }),
+ {
+ useFooter: true,
+ contentBackgroundColor: BackgroundColor.backgroundDefault,
+ },
+ );
+
+ expect(container.getElementsByClassName('mm-box--background-color-background-alternative').length).toBe(1);
+ expect(container).toMatchSnapshot();
+ });
+
+ it('supports interactive inputs', () => {
+ const { container, getByText, getByRole } = renderInterface(
+ Container({
+ children: [
+ Box({ children: Input({ name: 'input' }) }),
+ Footer({ children: Button({ children: 'Foo' }) }),
+ ],
+ }),
+ { useFooter: true },
+ );
+
+ const input = getByRole('textbox');
+ fireEvent.change(input, { target: { value: 'a' } });
+
+ expect(submitRequestToBackground).toHaveBeenNthCalledWith(
+ 1,
+ 'updateInterfaceState',
+ [MOCK_INTERFACE_ID, { input: 'a' }],
+ );
+ expect(submitRequestToBackground).toHaveBeenNthCalledWith(
+ 2,
+ 'handleSnapRequest',
+ [
+ {
+ handler: 'onUserInput',
+ origin: '',
+ request: {
+ jsonrpc: '2.0',
+ method: ' ',
+ params: {
+ context: null,
+ event: { name: 'input', type: 'InputChangeEvent', value: 'a' },
+ id: MOCK_INTERFACE_ID,
+ },
+ },
+ snapId: MOCK_SNAP_ID,
+ },
+ ],
+ );
+
+ expect(container).toMatchSnapshot();
});
});
diff --git a/ui/components/app/snaps/snap-ui-renderer/utils.test.ts b/ui/components/app/snaps/snap-ui-renderer/utils.test.ts
new file mode 100644
index 000000000000..1e12355cdffd
--- /dev/null
+++ b/ui/components/app/snaps/snap-ui-renderer/utils.test.ts
@@ -0,0 +1,15 @@
+import { JSXElementStruct, Box, Text } from '@metamask/snaps-sdk/jsx';
+import { COMPONENT_MAPPING } from './components';
+
+const EXCLUDED_COMPONENTS = ['Option', 'Radio', 'SelectorOption'];
+
+describe('Snap UI mapping', () => {
+ it('supports all exposed components', () => {
+ const elements = JSXElementStruct.schema
+ .map((struct) => JSON.parse(struct.schema.type.type))
+ .filter((key) => !EXCLUDED_COMPONENTS.includes(key));
+ expect(Object.keys(COMPONENT_MAPPING).sort()).toStrictEqual(
+ elements.sort(),
+ );
+ });
+});
From 17ab173ed712ee08f6bc7055530edd7630de0768 Mon Sep 17 00:00:00 2001
From: Frederik Bolding
Date: Tue, 4 Feb 2025 14:29:43 +0100
Subject: [PATCH 02/12] Fix lint
---
.../snap-ui-renderer/snap-ui-renderer.test.js | 18 +++++++++++-------
.../app/snaps/snap-ui-renderer/utils.test.ts | 2 +-
2 files changed, 12 insertions(+), 8 deletions(-)
diff --git a/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.test.js b/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.test.js
index f87dcac0607a..6ce5edfe8287 100644
--- a/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.test.js
+++ b/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.test.js
@@ -8,13 +8,13 @@ import {
Input,
} from '@metamask/snaps-sdk/jsx';
import configureMockStore from 'redux-mock-store';
-import mockState from '../../../../../test/data/mock-state.json';
-import { renderWithProvider } from '../../../../../test/lib/render-helpers';
-import { SnapUIRenderer } from './snap-ui-renderer';
import { fireEvent } from '@testing-library/react';
import thunk from 'redux-thunk';
+import { renderWithProvider } from '../../../../../test/lib/render-helpers';
+import mockState from '../../../../../test/data/mock-state.json';
import * as backgroundConnection from '../../../../store/background-connection';
import { BackgroundColor } from '../../../../helpers/constants/design-system';
+import { SnapUIRenderer } from './snap-ui-renderer';
jest.mock('../../../../store/background-connection', () => ({
...jest.requireActual('../../../../store/background-connection'),
@@ -67,7 +67,7 @@ describe('SnapUIRenderer', () => {
it('renders loading state', () => {
const { container } = renderInterface(null);
- expect(container.getElementsByClassName('pulse-loader').length).toBe(1);
+ expect(container.getElementsByClassName('pulse-loader')).toHaveLength(1);
expect(container).toMatchSnapshot();
});
@@ -116,7 +116,7 @@ describe('SnapUIRenderer', () => {
});
it('supports the contentBackgroundColor prop', () => {
- const { container, getByText } = renderInterface(
+ const { container } = renderInterface(
Container({
children: [
Box({ children: Text({ children: 'Hello world!' }) }),
@@ -129,12 +129,16 @@ describe('SnapUIRenderer', () => {
},
);
- expect(container.getElementsByClassName('mm-box--background-color-background-alternative').length).toBe(1);
+ expect(
+ container.getElementsByClassName(
+ 'mm-box--background-color-background-alternative',
+ ),
+ ).toHaveLength(1);
expect(container).toMatchSnapshot();
});
it('supports interactive inputs', () => {
- const { container, getByText, getByRole } = renderInterface(
+ const { container, getByRole } = renderInterface(
Container({
children: [
Box({ children: Input({ name: 'input' }) }),
diff --git a/ui/components/app/snaps/snap-ui-renderer/utils.test.ts b/ui/components/app/snaps/snap-ui-renderer/utils.test.ts
index 1e12355cdffd..e6707f84e8c1 100644
--- a/ui/components/app/snaps/snap-ui-renderer/utils.test.ts
+++ b/ui/components/app/snaps/snap-ui-renderer/utils.test.ts
@@ -1,4 +1,4 @@
-import { JSXElementStruct, Box, Text } from '@metamask/snaps-sdk/jsx';
+import { JSXElementStruct } from '@metamask/snaps-sdk/jsx';
import { COMPONENT_MAPPING } from './components';
const EXCLUDED_COMPONENTS = ['Option', 'Radio', 'SelectorOption'];
From 96ea098e05292f07f95acc27810248dfd654e132 Mon Sep 17 00:00:00 2001
From: Frederik Bolding
Date: Tue, 4 Feb 2025 15:13:29 +0100
Subject: [PATCH 03/12] Fix types
---
ui/components/app/snaps/snap-ui-renderer/utils.test.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/ui/components/app/snaps/snap-ui-renderer/utils.test.ts b/ui/components/app/snaps/snap-ui-renderer/utils.test.ts
index e6707f84e8c1..47c3444f7cab 100644
--- a/ui/components/app/snaps/snap-ui-renderer/utils.test.ts
+++ b/ui/components/app/snaps/snap-ui-renderer/utils.test.ts
@@ -1,3 +1,4 @@
+import type { Struct } from '@metamask/superstruct';
import { JSXElementStruct } from '@metamask/snaps-sdk/jsx';
import { COMPONENT_MAPPING } from './components';
@@ -5,7 +6,7 @@ const EXCLUDED_COMPONENTS = ['Option', 'Radio', 'SelectorOption'];
describe('Snap UI mapping', () => {
it('supports all exposed components', () => {
- const elements = JSXElementStruct.schema
+ const elements = (JSXElementStruct.schema as Struct[])
.map((struct) => JSON.parse(struct.schema.type.type))
.filter((key) => !EXCLUDED_COMPONENTS.includes(key));
expect(Object.keys(COMPONENT_MAPPING).sort()).toStrictEqual(
From 51ae20ec70176e3f65254b1cb5b21d692a2da61e Mon Sep 17 00:00:00 2001
From: Frederik Bolding
Date: Tue, 4 Feb 2025 16:04:25 +0100
Subject: [PATCH 04/12] Add more tests
---
.../snap-ui-renderer.test.js.snap | 91 +++++++++++++++----
.../snap-ui-renderer/snap-ui-renderer.test.js | 42 +++++++--
.../app/snaps/snap-ui-renderer/utils.test.ts | 6 +-
3 files changed, 113 insertions(+), 26 deletions(-)
diff --git a/ui/components/app/snaps/snap-ui-renderer/__snapshots__/snap-ui-renderer.test.js.snap b/ui/components/app/snaps/snap-ui-renderer/__snapshots__/snap-ui-renderer.test.js.snap
index f2d49704a43f..8841e164185a 100644
--- a/ui/components/app/snaps/snap-ui-renderer/__snapshots__/snap-ui-renderer.test.js.snap
+++ b/ui/components/app/snaps/snap-ui-renderer/__snapshots__/snap-ui-renderer.test.js.snap
@@ -1,5 +1,39 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`SnapUIRenderer prefills interactive inputs with existing state 1`] = `
+
+`;
+
exports[`SnapUIRenderer renders basic UI 1`] = `
`;
-exports[`SnapUIRenderer supports interactive inputs 1`] = `
+exports[`SnapUIRenderer supports container backgrounds 1`] = `
`;
@@ -80,6 +84,10 @@ exports[`SnapUIRenderer re-focuses input after re-render 1`] = `
+
`;
@@ -130,6 +138,10 @@ exports[`SnapUIRenderer re-renders when the interface changes 1`] = `
+
`;
@@ -180,6 +192,10 @@ exports[`SnapUIRenderer re-syncs state when the interface changes 1`] = `
+
`;
@@ -203,6 +219,10 @@ exports[`SnapUIRenderer renders basic UI 1`] = `
+
`;
@@ -247,6 +267,10 @@ exports[`SnapUIRenderer renders footers 1`] = `
+
`;
@@ -314,6 +338,10 @@ exports[`SnapUIRenderer supports container backgrounds 1`] = `
+
`;
@@ -360,6 +388,10 @@ exports[`SnapUIRenderer supports forms 1`] = `
+
`;
@@ -394,6 +426,10 @@ exports[`SnapUIRenderer supports interactive inputs 1`] = `
+
`;
@@ -438,6 +474,10 @@ exports[`SnapUIRenderer supports the contentBackgroundColor prop 1`] = `
+
`;
@@ -491,6 +531,10 @@ exports[`SnapUIRenderer supports the onCancel prop 1`] = `
+
`;
diff --git a/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.js b/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.js
index 9f60a3e0769c..a4ccfb85de68 100644
--- a/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.js
+++ b/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.js
@@ -1,4 +1,4 @@
-import React, { memo, useMemo } from 'react';
+import React, { memo, useMemo, useRef } from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { Container } from '@metamask/snaps-sdk/jsx';
@@ -20,6 +20,15 @@ import {
import { useI18nContext } from '../../../../hooks/useI18nContext';
import { mapToExtensionCompatibleColor, mapToTemplate } from './utils';
+// Component for tracking the number of re-renders
+// DO NOT USE IN PRODUCTION
+const PerformanceTracker = () => {
+ const rendersRef = useRef(0);
+ rendersRef.current++;
+
+ return ;
+};
+
// Component that maps Snaps UI JSON format to MetaMask Template Renderer format
const SnapUIRendererComponent = ({
snapId,
@@ -33,6 +42,7 @@ const SnapUIRendererComponent = ({
useFooter = false,
onCancel,
contentBackgroundColor,
+ PERF_DEBUG,
}) => {
const t = useI18nContext();
@@ -115,6 +125,7 @@ const SnapUIRendererComponent = ({
}}
>
+ {PERF_DEBUG && }
);
@@ -137,4 +148,5 @@ SnapUIRendererComponent.propTypes = {
useFooter: PropTypes.bool,
onCancel: PropTypes.func,
contentBackgroundColor: PropTypes.string,
+ PERF_DEBUG: PropTypes.bool, // DO NOT USE THIS IN PRODUCTION
};
diff --git a/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.test.js b/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.test.js
index 6bad3363481b..d4ca72430d09 100644
--- a/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.test.js
+++ b/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.test.js
@@ -9,7 +9,6 @@ import {
Form,
} from '@metamask/snaps-sdk/jsx';
import { fireEvent, waitFor } from '@testing-library/react';
-import thunk from 'redux-thunk';
import { renderWithProvider } from '../../../../../test/lib/render-helpers';
import mockState from '../../../../../test/data/mock-state.json';
import * as backgroundConnection from '../../../../store/background-connection';
@@ -88,11 +87,20 @@ function renderInterface(
useFooter={useFooter}
onCancel={onCancel}
contentBackgroundColor={contentBackgroundColor}
+ PERF_DEBUG={true}
/>,
store,
);
- return { ...result, updateInterface };
+ const getRenderCount = () =>
+ parseInt(
+ result
+ .getByTestId('performance')
+ .getAttribute('data-renders'),
+ 10,
+ );
+
+ return { ...result, updateInterface, getRenderCount };
}
describe('SnapUIRenderer', () => {
@@ -108,11 +116,12 @@ describe('SnapUIRenderer', () => {
});
it('renders basic UI', () => {
- const { container, getByText } = renderInterface(
+ const { container, getByText, getRenderCount } = renderInterface(
Box({ children: Text({ children: 'Hello world!' }) }),
);
expect(getByText('Hello world!')).toBeDefined();
+ expect(getRenderCount()).toBe(1);
expect(container).toMatchSnapshot();
});
@@ -344,7 +353,7 @@ describe('SnapUIRenderer', () => {
});
it('re-renders when the interface changes', () => {
- const { container, getAllByRole, getByRole, updateInterface } =
+ const { container, getAllByRole, updateInterface, getRenderCount } =
renderInterface(Box({ children: Input({ name: 'input' }) }));
const inputs = getAllByRole('textbox');
@@ -357,11 +366,13 @@ describe('SnapUIRenderer', () => {
const inputsAfterRerender = getAllByRole('textbox');
expect(inputsAfterRerender.length).toBe(2);
+ expect(getRenderCount()).toBe(2)
+
expect(container).toMatchSnapshot();
});
it('re-syncs state when the interface changes', () => {
- const { container, getAllByRole, getByRole, updateInterface } =
+ const { container, getAllByRole, getRenderCount, updateInterface } =
renderInterface(Box({ children: Input({ name: 'input' }) }));
updateInterface(
@@ -373,11 +384,13 @@ describe('SnapUIRenderer', () => {
expect(inputsAfterRerender[0].value).toStrictEqual('bar');
expect(inputsAfterRerender[1].value).toStrictEqual('foo');
+ expect(getRenderCount()).toBe(2)
+
expect(container).toMatchSnapshot();
});
it('re-focuses input after re-render', async () => {
- const { container, getAllByRole, getByRole, updateInterface } =
+ const { container, getAllByRole, getByRole, updateInterface, getRenderCount } =
renderInterface(Box({ children: Input({ name: 'input' }) }));
const input = getByRole('textbox');
@@ -393,6 +406,8 @@ describe('SnapUIRenderer', () => {
await waitFor(() => expect(inputs[0]).toHaveFocus());
+ expect(getRenderCount()).toBe(2)
+
expect(container).toMatchSnapshot();
});
});
From 52cfd4b8ca04d252c1c7696876559e27072b1bc6 Mon Sep 17 00:00:00 2001
From: Frederik Bolding
Date: Wed, 5 Feb 2025 12:55:15 +0100
Subject: [PATCH 09/12] Fix lint
---
.../snap-ui-renderer/snap-ui-renderer.js | 2 +-
.../snap-ui-renderer/snap-ui-renderer.test.js | 45 +++++++++++--------
2 files changed, 27 insertions(+), 20 deletions(-)
diff --git a/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.js b/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.js
index a4ccfb85de68..2852d35e24c5 100644
--- a/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.js
+++ b/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.js
@@ -24,7 +24,7 @@ import { mapToExtensionCompatibleColor, mapToTemplate } from './utils';
// DO NOT USE IN PRODUCTION
const PerformanceTracker = () => {
const rendersRef = useRef(0);
- rendersRef.current++;
+ rendersRef.current += 1;
return ;
};
diff --git a/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.test.js b/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.test.js
index d4ca72430d09..a85b203560d0 100644
--- a/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.test.js
+++ b/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.test.js
@@ -13,8 +13,8 @@ import { renderWithProvider } from '../../../../../test/lib/render-helpers';
import mockState from '../../../../../test/data/mock-state.json';
import * as backgroundConnection from '../../../../store/background-connection';
import { BackgroundColor } from '../../../../helpers/constants/design-system';
-import { SnapUIRenderer } from './snap-ui-renderer';
import configureStore from '../../../../store/store';
+import { SnapUIRenderer } from './snap-ui-renderer';
jest.mock('../../../../store/background-connection', () => ({
...jest.requireActual('../../../../store/background-connection'),
@@ -52,12 +52,12 @@ function renderInterface(
},
});
- const reducer = (state, action) => {
+ const reducer = (storeState, action) => {
if (action.type === 'updateInterface') {
return {
- ...state,
+ ...storeState,
metamask: {
- ...state.metamask,
+ ...storeState.metamask,
interfaces: {
[MOCK_INTERFACE_ID]: {
snapId: MOCK_SNAP_ID,
@@ -70,13 +70,17 @@ function renderInterface(
},
};
}
- return state;
+ return storeState;
};
store.replaceReducer(reducer);
- const updateInterface = (content, state = null) => {
- store.dispatch({ type: 'updateInterface', content, state });
+ const updateInterface = (newContent, newState = null) => {
+ store.dispatch({
+ type: 'updateInterface',
+ content: newContent,
+ state: newState,
+ });
};
const result = renderWithProvider(
@@ -87,16 +91,14 @@ function renderInterface(
useFooter={useFooter}
onCancel={onCancel}
contentBackgroundColor={contentBackgroundColor}
- PERF_DEBUG={true}
+ PERF_DEBUG
/>,
store,
);
const getRenderCount = () =>
parseInt(
- result
- .getByTestId('performance')
- .getAttribute('data-renders'),
+ result.getByTestId('performance').getAttribute('data-renders'),
10,
);
@@ -357,16 +359,16 @@ describe('SnapUIRenderer', () => {
renderInterface(Box({ children: Input({ name: 'input' }) }));
const inputs = getAllByRole('textbox');
- expect(inputs.length).toBe(1);
+ expect(inputs).toHaveLength(1);
updateInterface(
Box({ children: [Input({ name: 'input' }), Input({ name: 'input2' })] }),
);
const inputsAfterRerender = getAllByRole('textbox');
- expect(inputsAfterRerender.length).toBe(2);
+ expect(inputsAfterRerender).toHaveLength(2);
- expect(getRenderCount()).toBe(2)
+ expect(getRenderCount()).toBe(2);
expect(container).toMatchSnapshot();
});
@@ -384,14 +386,19 @@ describe('SnapUIRenderer', () => {
expect(inputsAfterRerender[0].value).toStrictEqual('bar');
expect(inputsAfterRerender[1].value).toStrictEqual('foo');
- expect(getRenderCount()).toBe(2)
+ expect(getRenderCount()).toBe(2);
expect(container).toMatchSnapshot();
});
it('re-focuses input after re-render', async () => {
- const { container, getAllByRole, getByRole, updateInterface, getRenderCount } =
- renderInterface(Box({ children: Input({ name: 'input' }) }));
+ const {
+ container,
+ getAllByRole,
+ getByRole,
+ updateInterface,
+ getRenderCount,
+ } = renderInterface(Box({ children: Input({ name: 'input' }) }));
const input = getByRole('textbox');
input.focus();
@@ -402,11 +409,11 @@ describe('SnapUIRenderer', () => {
);
const inputs = getAllByRole('textbox');
- expect(inputs.length).toBe(2);
+ expect(inputs).toHaveLength(2);
await waitFor(() => expect(inputs[0]).toHaveFocus());
- expect(getRenderCount()).toBe(2)
+ expect(getRenderCount()).toBe(2);
expect(container).toMatchSnapshot();
});
From b23ae638c721ff2322000402bd135fb4f8fe2a2c Mon Sep 17 00:00:00 2001
From: Frederik Bolding
Date: Wed, 5 Feb 2025 14:49:49 +0100
Subject: [PATCH 10/12] Add field test
---
.../snap-ui-renderer.test.js.snap | 78 +++++++++++
.../snap-ui-renderer/snap-ui-renderer.test.js | 131 ++++++++++++++++++
2 files changed, 209 insertions(+)
diff --git a/ui/components/app/snaps/snap-ui-renderer/__snapshots__/snap-ui-renderer.test.js.snap b/ui/components/app/snaps/snap-ui-renderer/__snapshots__/snap-ui-renderer.test.js.snap
index 872671ba644f..dedcba203543 100644
--- a/ui/components/app/snaps/snap-ui-renderer/__snapshots__/snap-ui-renderer.test.js.snap
+++ b/ui/components/app/snaps/snap-ui-renderer/__snapshots__/snap-ui-renderer.test.js.snap
@@ -396,6 +396,84 @@ exports[`SnapUIRenderer supports forms 1`] = `
`;
+exports[`SnapUIRenderer supports forms with fields 1`] = `
+
+`;
+
exports[`SnapUIRenderer supports interactive inputs 1`] = `
{
expect(container).toMatchSnapshot();
});
+ it('supports forms with fields', () => {
+ const { container, getByRole } = renderInterface(
+ Box({
+ children: Form({
+ name: 'form',
+ children: [
+ Field({ label: 'My Input', children: Input({ name: 'input' }) }),
+ Field({ label: 'Checkbox', children: Checkbox({ name: 'checkbox' }) }),
+ Button({ type: 'submit', name: 'submit', children: 'Submit' }),
+ ],
+ }),
+ }),
+ );
+
+ const input = getByRole('textbox');
+ fireEvent.change(input, { target: { value: 'abc' } });
+
+ expect(submitRequestToBackground).toHaveBeenNthCalledWith(
+ 1,
+ 'updateInterfaceState',
+ [MOCK_INTERFACE_ID, { form: { input: 'abc' } }],
+ );
+
+ expect(submitRequestToBackground).toHaveBeenNthCalledWith(
+ 2,
+ 'handleSnapRequest',
+ [
+ {
+ handler: 'onUserInput',
+ origin: '',
+ request: {
+ jsonrpc: '2.0',
+ method: ' ',
+ params: {
+ context: null,
+ event: { name: 'input', type: 'InputChangeEvent', value: 'abc' },
+ id: MOCK_INTERFACE_ID,
+ },
+ },
+ snapId: MOCK_SNAP_ID,
+ },
+ ],
+ );
+
+ const checkbox = getByRole('checkbox');
+ fireEvent.click(checkbox);
+
+ expect(submitRequestToBackground).toHaveBeenNthCalledWith(
+ 3,
+ 'updateInterfaceState',
+ [MOCK_INTERFACE_ID, { form: { checkbox: true, input: 'abc' } }],
+ );
+
+ expect(submitRequestToBackground).toHaveBeenNthCalledWith(
+ 4,
+ 'handleSnapRequest',
+ [
+ {
+ handler: 'onUserInput',
+ origin: '',
+ request: {
+ jsonrpc: '2.0',
+ method: ' ',
+ params: {
+ context: null,
+ event: { name: 'checkbox', type: 'InputChangeEvent', value: true },
+ id: MOCK_INTERFACE_ID,
+ },
+ },
+ snapId: MOCK_SNAP_ID,
+ },
+ ],
+ );
+
+ const button = getByRole('button');
+ fireEvent.click(button);
+
+ expect(submitRequestToBackground).toHaveBeenNthCalledWith(
+ 5,
+ 'handleSnapRequest',
+ [
+ {
+ handler: 'onUserInput',
+ origin: '',
+ request: {
+ jsonrpc: '2.0',
+ method: ' ',
+ params: {
+ context: null,
+ event: { name: 'submit', type: 'ButtonClickEvent' },
+ id: MOCK_INTERFACE_ID,
+ },
+ },
+ snapId: MOCK_SNAP_ID,
+ },
+ ],
+ );
+
+ expect(submitRequestToBackground).toHaveBeenNthCalledWith(
+ 6,
+ 'handleSnapRequest',
+ [
+ {
+ handler: 'onUserInput',
+ origin: '',
+ request: {
+ jsonrpc: '2.0',
+ method: ' ',
+ params: {
+ context: null,
+ event: {
+ name: 'form',
+ type: 'FormSubmitEvent',
+ value: {
+ checkbox: true,
+ input: 'abc',
+ },
+ },
+ id: MOCK_INTERFACE_ID,
+ },
+ },
+ snapId: MOCK_SNAP_ID,
+ },
+ ],
+ );
+
+ expect(container).toMatchSnapshot();
+ });
+
it('re-renders when the interface changes', () => {
const { container, getAllByRole, updateInterface, getRenderCount } =
renderInterface(Box({ children: Input({ name: 'input' }) }));
From 3ed45d991cd00c2de98506e0091b24583b35596a Mon Sep 17 00:00:00 2001
From: Frederik Bolding
Date: Wed, 5 Feb 2025 15:29:16 +0100
Subject: [PATCH 11/12] Update snapshot
---
.../__snapshots__/snap-ui-renderer.test.js.snap | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/ui/components/app/snaps/snap-ui-renderer/__snapshots__/snap-ui-renderer.test.js.snap b/ui/components/app/snaps/snap-ui-renderer/__snapshots__/snap-ui-renderer.test.js.snap
index dedcba203543..f0cc98e97313 100644
--- a/ui/components/app/snaps/snap-ui-renderer/__snapshots__/snap-ui-renderer.test.js.snap
+++ b/ui/components/app/snaps/snap-ui-renderer/__snapshots__/snap-ui-renderer.test.js.snap
@@ -450,9 +450,13 @@ exports[`SnapUIRenderer supports forms with fields 1`] = `
class="mm-checkbox__input-wrapper"
>
+
From 305a5522f620caf2f8728a780cb42a6a3eed3170 Mon Sep 17 00:00:00 2001
From: Frederik Bolding
Date: Fri, 7 Feb 2025 11:55:36 +0100
Subject: [PATCH 12/12] Add file input test
---
.../snap-ui-renderer.test.js.snap | 63 ++++++++
.../snap-ui-renderer/snap-ui-renderer.test.js | 146 +++++++++++++++++-
2 files changed, 207 insertions(+), 2 deletions(-)
diff --git a/ui/components/app/snaps/snap-ui-renderer/__snapshots__/snap-ui-renderer.test.js.snap b/ui/components/app/snaps/snap-ui-renderer/__snapshots__/snap-ui-renderer.test.js.snap
index f0cc98e97313..42d019e2a1f0 100644
--- a/ui/components/app/snaps/snap-ui-renderer/__snapshots__/snap-ui-renderer.test.js.snap
+++ b/ui/components/app/snaps/snap-ui-renderer/__snapshots__/snap-ui-renderer.test.js.snap
@@ -346,6 +346,69 @@ exports[`SnapUIRenderer supports container backgrounds 1`] = `
`;
+exports[`SnapUIRenderer supports file inputs 1`] = `
+
+`;
+
exports[`SnapUIRenderer supports forms 1`] = `
{
name: 'form',
children: [
Field({ label: 'My Input', children: Input({ name: 'input' }) }),
- Field({ label: 'Checkbox', children: Checkbox({ name: 'checkbox' }) }),
+ Field({
+ label: 'Checkbox',
+ children: Checkbox({ name: 'checkbox' }),
+ }),
Button({ type: 'submit', name: 'submit', children: 'Submit' }),
],
}),
@@ -421,7 +426,11 @@ describe('SnapUIRenderer', () => {
method: ' ',
params: {
context: null,
- event: { name: 'checkbox', type: 'InputChangeEvent', value: true },
+ event: {
+ name: 'checkbox',
+ type: 'InputChangeEvent',
+ value: true,
+ },
id: MOCK_INTERFACE_ID,
},
},
@@ -548,4 +557,137 @@ describe('SnapUIRenderer', () => {
expect(container).toMatchSnapshot();
});
+
+ it('supports file inputs', async () => {
+ const { container, getByRole } = renderInterface(
+ Box({
+ children: Form({
+ name: 'form',
+ children: [
+ Field({
+ label: 'My Input',
+ children: FileInput({ name: 'input' }),
+ }),
+ Button({ type: 'submit', name: 'submit', children: 'Submit' }),
+ ],
+ }),
+ }),
+ );
+
+ const file = new File(['foo'], 'foo.svg', { type: 'image/svg' });
+
+ // JSDOM doesn't support array buffer so we overwrite it
+ file.arrayBuffer = async () => {
+ return new Uint8Array([102, 111, 111]);
+ };
+
+ const input = container.querySelector('#input');
+ await userEvent.upload(input, file);
+
+ expect(submitRequestToBackground).toHaveBeenNthCalledWith(
+ 1,
+ 'updateInterfaceState',
+ [
+ MOCK_INTERFACE_ID,
+ {
+ form: {
+ input: {
+ contentType: 'image/svg',
+ contents: 'Zm9v',
+ name: 'foo.svg',
+ size: 3,
+ },
+ },
+ },
+ ],
+ );
+
+ expect(submitRequestToBackground).toHaveBeenNthCalledWith(
+ 2,
+ 'handleSnapRequest',
+ [
+ {
+ handler: 'onUserInput',
+ origin: '',
+ request: {
+ jsonrpc: '2.0',
+ method: ' ',
+ params: {
+ context: null,
+ event: {
+ name: 'input',
+ type: 'FileUploadEvent',
+ file: {
+ contentType: 'image/svg',
+ contents: 'Zm9v',
+ name: 'foo.svg',
+ size: 3,
+ },
+ },
+ id: MOCK_INTERFACE_ID,
+ },
+ },
+ snapId: MOCK_SNAP_ID,
+ },
+ ],
+ );
+
+ const button = getByRole('button');
+ fireEvent.click(button);
+
+ expect(submitRequestToBackground).toHaveBeenNthCalledWith(
+ 5,
+ 'handleSnapRequest',
+ [
+ {
+ handler: 'onUserInput',
+ origin: '',
+ request: {
+ jsonrpc: '2.0',
+ method: ' ',
+ params: {
+ context: null,
+ event: { name: 'submit', type: 'ButtonClickEvent' },
+ id: MOCK_INTERFACE_ID,
+ },
+ },
+ snapId: MOCK_SNAP_ID,
+ },
+ ],
+ );
+
+ expect(submitRequestToBackground).toHaveBeenNthCalledWith(
+ 6,
+ 'handleSnapRequest',
+ [
+ {
+ handler: 'onUserInput',
+ origin: '',
+ request: {
+ jsonrpc: '2.0',
+ method: ' ',
+ params: {
+ context: null,
+ event: {
+ name: 'form',
+ type: 'FormSubmitEvent',
+ value: {
+ input: {
+ contentType: 'image/svg',
+ contents: 'Zm9v',
+ name: 'foo.svg',
+ size: 3,
+ },
+ },
+ },
+ id: MOCK_INTERFACE_ID,
+ },
+ },
+ snapId: MOCK_SNAP_ID,
+ },
+ ],
+ );
+
+ expect(container).toMatchSnapshot();
+ });
});