From 2218a39cb8ad695dbe5fbebe7088844539743c70 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 22 Oct 2025 16:35:22 +0200 Subject: [PATCH 1/2] feat: replace `ViewWrapper` by `useMockedViewModel` --- .../shared-components/src/ViewWrapper.tsx | 52 ------------------- packages/shared-components/src/index.ts | 2 +- .../src/useMockedViewModel.ts | 25 +++++++++ 3 files changed, 26 insertions(+), 53 deletions(-) delete mode 100644 packages/shared-components/src/ViewWrapper.tsx create mode 100644 packages/shared-components/src/useMockedViewModel.ts diff --git a/packages/shared-components/src/ViewWrapper.tsx b/packages/shared-components/src/ViewWrapper.tsx deleted file mode 100644 index d0d445ea5c5..00000000000 --- a/packages/shared-components/src/ViewWrapper.tsx +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import React, { type JSX, useMemo, type ComponentType } from "react"; -import { omitBy, pickBy } from "lodash"; - -import { MockViewModel } from "./viewmodel/MockViewModel"; -import { type ViewModel } from "./viewmodel/ViewModel"; - -interface ViewWrapperProps { - /** - * The component to render, which should accept a `vm` prop of type `V`. - */ - Component: ComponentType<{ vm: V }>; - /** - * The props to pass to the component, which can include both snapshot data and actions. - */ - props: Record; -} - -/** - * A wrapper component that creates a view model instance and passes it to the specified component. - * This is useful for testing components in isolation with a mocked view model and allows to use primitive types in stories. - * - * Props is parsed and split into snapshot and actions. Where values that are functions (`typeof Function`) are considered actions and the rest is considered the snapshot. - * - * @example - * ```tsx - * props={Snapshot&Actions} Component={MyComponent} /> - * ``` - */ -export function ViewWrapper>({ - props, - Component, -}: Readonly>): JSX.Element { - const vm = useMemo(() => { - const isFunction = (value: any): value is typeof Function => typeof value === typeof Function; - const snapshot = omitBy(props, isFunction) as T; - const actions = pickBy(props, isFunction); - - const vm = new MockViewModel(snapshot); - Object.assign(vm, actions); - - return vm as unknown as V; - }, [props]); - - return ; -} diff --git a/packages/shared-components/src/index.ts b/packages/shared-components/src/index.ts index 226632dec48..6967c175fd3 100644 --- a/packages/shared-components/src/index.ts +++ b/packages/shared-components/src/index.ts @@ -28,5 +28,5 @@ export * from "./utils/numbers"; // MVVM export * from "./viewmodel"; -export * from "./ViewWrapper"; +export * from "./useMockedViewModel"; export * from "./useViewModel"; diff --git a/packages/shared-components/src/useMockedViewModel.ts b/packages/shared-components/src/useMockedViewModel.ts new file mode 100644 index 00000000000..6f130753519 --- /dev/null +++ b/packages/shared-components/src/useMockedViewModel.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { useMemo } from "react"; + +import { MockViewModel, type ViewModel } from "./viewmodel"; + +/** + * Hook helper to return a mocked view model created with the given snapshot and actions. + * This is useful for testing components in isolation with a mocked view model and allows to use primitive types in stories. + * + * @param snapshot + * @param actions + */ +export function useMockedViewModel(snapshot: S, actions: A): ViewModel & A { + return useMemo(() => { + const vm = new MockViewModel(snapshot); + Object.assign(vm, actions); + return vm as unknown as ViewModel & A; + }, [snapshot, actions]); +} From d83a045c209046d1e86c53695729692a0511e768 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 22 Oct 2025 16:35:41 +0200 Subject: [PATCH 2/2] feat: update existing story --- .../AudioPlayerView.stories.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.stories.tsx b/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.stories.tsx index 869e922dd4a..18782f25cbd 100644 --- a/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.stories.tsx +++ b/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.stories.tsx @@ -9,18 +9,18 @@ import React, { type JSX } from "react"; import { fn } from "storybook/test"; import type { Meta, StoryFn } from "@storybook/react-vite"; -import { - AudioPlayerView, - type AudioPlayerViewActions, - type AudioPlayerViewSnapshot, - type AudioPlayerViewModel, -} from "./AudioPlayerView"; -import { ViewWrapper } from "../../ViewWrapper"; +import { AudioPlayerView, type AudioPlayerViewActions, type AudioPlayerViewSnapshot } from "./AudioPlayerView"; +import { useMockedViewModel } from "../../useMockedViewModel"; type AudioPlayerProps = AudioPlayerViewSnapshot & AudioPlayerViewActions; -const AudioPlayerViewWrapper = (props: AudioPlayerProps): JSX.Element => ( - Component={AudioPlayerView} props={props} /> -); +const AudioPlayerViewWrapper = ({ togglePlay, onKeyDown, onSeekbarChange, ...rest }: AudioPlayerProps): JSX.Element => { + const vm = useMockedViewModel(rest, { + togglePlay, + onKeyDown, + onSeekbarChange, + }); + return ; +}; export default { title: "Audio/AudioPlayerView",