From ab13fc4d789f5f44b42b3a73898cbb7adb8cd53b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Fri, 16 Jan 2026 14:08:48 -0300 Subject: [PATCH 1/2] feat: pass `pluginProps` to wrapper --- README.rst | 8 ++++++-- src/plugins/PluginSlot.jsx | 1 + src/plugins/PluginSlot.test.jsx | 14 ++++++++++---- src/plugins/data/utils.jsx | 5 +++-- src/plugins/data/utils.test.jsx | 7 ++++--- 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/README.rst b/README.rst index 7b6208bc..ca392666 100644 --- a/README.rst +++ b/README.rst @@ -251,15 +251,16 @@ Wrap '''' Unlike Modify, the Wrap operation adds a React component around the widget, and a single widget can receive more than -one wrap operation. Each wrapper function takes in a ``component`` and ``id`` prop. +one wrap operation. Each wrapper function takes in a ``component``, ``id`` and ``pluginProps`` prop. .. code-block:: - const wrapWidget = ({ component, idx }) => ( + const wrapWidget = ({ component, idx, pluginProps }) => (

This is a wrapper component that is placed around the widget.

{component}

With this wrapper, you can add anything before or after the widget.

+

You can use the pluginProps to pass in any additional props to the wrapper: {pluginProps.prop1}

); @@ -272,6 +273,9 @@ one wrap operation. Each wrapper function takes in a ``component`` and ``id`` pr { op: PLUGIN_OPERATIONS.Wrap, widgetId: 'default_contents', + pluginProps: { + prop1: 'prop1', + }, wrapper: wrapWidget, } diff --git a/src/plugins/PluginSlot.jsx b/src/plugins/PluginSlot.jsx index 99e53d8e..44bf6120 100644 --- a/src/plugins/PluginSlot.jsx +++ b/src/plugins/PluginSlot.jsx @@ -93,6 +93,7 @@ function BasePluginSlot({ wrapComponent( () => container, pluginConfig.wrappers, + pluginProps, ), ); } else { diff --git a/src/plugins/PluginSlot.test.jsx b/src/plugins/PluginSlot.test.jsx index e18244ac..9bcac990 100644 --- a/src/plugins/PluginSlot.test.jsx +++ b/src/plugins/PluginSlot.test.jsx @@ -3,7 +3,7 @@ import React from 'react'; import '@testing-library/jest-dom'; import classNames from 'classnames'; -import { render, fireEvent } from '@testing-library/react'; +import { render, fireEvent, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { logError } from '@edx/frontend-platform/logging'; import { IntlProvider } from '@edx/frontend-platform/i18n'; @@ -78,7 +78,7 @@ function DefaultContents({ className, onClick, ...rest }) { ); } -function PluginSlotWrapper({ slotOptions, children }) { +function PluginSlotWrapper({ slotOptions, children, pluginProps }) { return ( {children} @@ -182,9 +183,12 @@ describe('PluginSlot', () => { { op: PLUGIN_OPERATIONS.Wrap, widgetId: 'default_contents', - wrapper: ({ component }) => ( + wrapper: ({ component, pluginProps }) => (
{component} +
+ {pluginProps.prop1} +
), }, @@ -192,10 +196,12 @@ describe('PluginSlot', () => { keepDefault: true, }); - const { getByTestId } = render(); + const { getByTestId } = render(); const customWrapper = getByTestId('custom-wrapper'); const defaultContent = getByTestId('default_contents'); expect(customWrapper).toContainElement(defaultContent); + const pluginProps = within(customWrapper).getByTestId('custom-wrapper-prop'); + expect(pluginProps).toHaveTextContent('prop1'); }); it('should not render a widget if the Hide operation is applied to it', () => { diff --git a/src/plugins/data/utils.jsx b/src/plugins/data/utils.jsx index 68e4e3be..c79d0d71 100644 --- a/src/plugins/data/utils.jsx +++ b/src/plugins/data/utils.jsx @@ -82,13 +82,14 @@ export const organizePlugins = (defaultContents, plugins) => { * * @param {Function} renderComponent - Function that returns JSX (i.e. React Component) * @param {Array} wrappers - Array of components that each use a "component" prop to render the wrapped contents + * @params {object} pluginProps - Props defined in the PluginSlot * @returns {React.ReactElement} - The plugin component wrapped by any number of wrappers provided. */ -export const wrapComponent = (renderComponent, wrappers) => wrappers.reduce( +export const wrapComponent = (renderComponent, wrappers, pluginProps) => wrappers.reduce( // Disabled lint because currently we don't have a unique identifier for this // The "component" and "wrapper" are both functions // eslint-disable-next-line react/no-array-index-key - (component, wrapper, idx) => React.createElement(wrapper, { component, key: idx }), + (component, wrapper, idx) => React.createElement(wrapper, { component, key: idx, pluginProps }), renderComponent(), ); diff --git a/src/plugins/data/utils.test.jsx b/src/plugins/data/utils.test.jsx index b93c4163..930ee1f2 100644 --- a/src/plugins/data/utils.test.jsx +++ b/src/plugins/data/utils.test.jsx @@ -22,10 +22,10 @@ const mockIsAdminWrapper = ({ widget }) => { return isAdmin ? widget : null; }; -const makeMockElementWrapper = (testId = 0) => function MockElementWrapper({ component }) { +const makeMockElementWrapper = (testId = 0) => function MockElementWrapper({ component, pluginProps }) { return (
- This is a wrapper. + This is a wrapper with {pluginProps?.prop1}. {component}
); @@ -181,7 +181,7 @@ describe('organizePlugins', () => { describe('wrapComponent', () => { describe('when provided with a single wrapper in an array', () => { it('should wrap the provided component', () => { - const wrappedComponent = wrapComponent(mockRenderWidget, [makeMockElementWrapper()]); + const wrappedComponent = wrapComponent(mockRenderWidget, [makeMockElementWrapper()], { prop1: 'prop1' }); const { getByTestId } = render(wrappedComponent); @@ -189,6 +189,7 @@ describe('wrapComponent', () => { const widget = getByTestId('widget'); expect(wrapper).toContainElement(widget); + expect(wrapper).toHaveTextContent('This is a wrapper with prop1.'); }); }); describe('when provided with multiple wrappers in an array', () => { From 15bb3db167721a31783a8acdd0cbc4cd19d460de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Fri, 23 Jan 2026 15:47:21 -0300 Subject: [PATCH 2/2] test: add more tests related to `pluginProps` --- src/plugins/PluginSlot.test.jsx | 35 +++++++++++++++++++++++++++++---- src/plugins/data/utils.test.jsx | 14 ++++++++++++- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/plugins/PluginSlot.test.jsx b/src/plugins/PluginSlot.test.jsx index 9bcac990..567b9800 100644 --- a/src/plugins/PluginSlot.test.jsx +++ b/src/plugins/PluginSlot.test.jsx @@ -186,8 +186,8 @@ describe('PluginSlot', () => { wrapper: ({ component, pluginProps }) => (
{component} -
- {pluginProps.prop1} +
+ {pluginProps?.prop1 && `This is a wrapper with ${pluginProps?.prop1}.`}
), @@ -200,8 +200,35 @@ describe('PluginSlot', () => { const customWrapper = getByTestId('custom-wrapper'); const defaultContent = getByTestId('default_contents'); expect(customWrapper).toContainElement(defaultContent); - const pluginProps = within(customWrapper).getByTestId('custom-wrapper-prop'); - expect(pluginProps).toHaveTextContent('prop1'); + const pluginProps = within(customWrapper).getByTestId('custom-wrapper-props'); + expect(pluginProps).toHaveTextContent('This is a wrapper with prop1.'); + }); + + it('should wrap a Plugin when using the "wrap" operation without passing props', () => { + usePluginSlot.mockReturnValueOnce({ + plugins: [ + { + op: PLUGIN_OPERATIONS.Wrap, + widgetId: 'default_contents', + wrapper: ({ component, pluginProps }) => ( +
+ {component} +
+ {`This is a wrapper without props: ${JSON.stringify(pluginProps)}`} +
+
+ ), + }, + ], + keepDefault: true, + }); + + const { getByTestId } = render(); + const customWrapper = getByTestId('custom-wrapper'); + const defaultContent = getByTestId('default_contents'); + expect(customWrapper).toContainElement(defaultContent); + const pluginProps = within(customWrapper).getByTestId('custom-wrapper-no-props'); + expect(pluginProps).toHaveTextContent('This is a wrapper without props: {}'); }); it('should not render a widget if the Hide operation is applied to it', () => { diff --git a/src/plugins/data/utils.test.jsx b/src/plugins/data/utils.test.jsx index 930ee1f2..602cebd5 100644 --- a/src/plugins/data/utils.test.jsx +++ b/src/plugins/data/utils.test.jsx @@ -25,7 +25,7 @@ const mockIsAdminWrapper = ({ widget }) => { const makeMockElementWrapper = (testId = 0) => function MockElementWrapper({ component, pluginProps }) { return (
- This is a wrapper with {pluginProps?.prop1}. + {pluginProps?.prop1 && `This is a wrapper with ${pluginProps?.prop1}.`} {component}
); @@ -191,6 +191,18 @@ describe('wrapComponent', () => { expect(wrapper).toContainElement(widget); expect(wrapper).toHaveTextContent('This is a wrapper with prop1.'); }); + + it('should wrap the provided component without passing props', () => { + const wrappedComponent = wrapComponent(mockRenderWidget, [makeMockElementWrapper()]); + + const { getByTestId } = render(wrappedComponent); + + const wrapper = getByTestId('wrapper0'); + const widget = getByTestId('widget'); + + expect(wrapper).toContainElement(widget); + expect(wrapper).not.toHaveTextContent('This is a wrapper with prop1.'); + }); }); describe('when provided with multiple wrappers in an array', () => { it('should wrap starting with the first wrapper in the array', () => {