Skip to content

Commit 6889a1f

Browse files
committed
Modify WidgetMenuContext call (apptile, extensioncard, widgetcard), test and stories
1 parent 32d0432 commit 6889a1f

File tree

8 files changed

+292
-135
lines changed

8 files changed

+292
-135
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<testExecutions version="1">
2+
<file path="/home/marc/Workspace/element-web/packages/shared-components/src/right-panel/WidgetContextMenu/WidgetContextMenuView.test.tsx">
3+
<testCase name="&lt;WidgetContextMenuView /&gt; renders widget contextmenu with all options" duration="93"/>
4+
<testCase name="&lt;WidgetContextMenuView /&gt; renders widget contextmenu without only basic modification" duration="26"/>
5+
<testCase name="&lt;WidgetContextMenuView /&gt; should attach vm methods" duration="201"/>
6+
</file>
7+
</testExecutions>
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import React, { type JSX } from "react";
9+
import { fn } from "storybook/test";
10+
11+
import type { Meta, StoryFn } from "@storybook/react-vite";
12+
import { useMockedViewModel } from "../../useMockedViewModel";
13+
import { WidgetContextMenuAction, WidgetContextMenuSnapshot, WidgetContextMenuView } from "./WidgetContextMenuView";
14+
import { IconButton } from "@vector-im/compound-web";
15+
import TriggerIcon from "@vector-im/compound-design-tokens/assets/web/icons/overflow-horizontal";
16+
17+
type WidgetContextMenuViewModelProps = WidgetContextMenuSnapshot & WidgetContextMenuAction;
18+
19+
const WidgetContextMenuViewWrapper = ({
20+
onStreamAudioClick,
21+
onEditClick,
22+
onSnapshotClick,
23+
onDeleteClick,
24+
onRevokeClick,
25+
onFinished,
26+
onMoveButton,
27+
...rest
28+
}: WidgetContextMenuViewModelProps): JSX.Element => {
29+
const vm = useMockedViewModel(rest, {
30+
onStreamAudioClick,
31+
onEditClick,
32+
onSnapshotClick,
33+
onDeleteClick,
34+
onRevokeClick,
35+
onFinished,
36+
onMoveButton,
37+
});
38+
return <WidgetContextMenuView vm={vm} />;
39+
};
40+
41+
export default {
42+
title: "RightPanel/WidgetContextMenuView",
43+
component: WidgetContextMenuViewWrapper,
44+
tags: ["autodocs"],
45+
args: {
46+
showStreamAudioStreamButton: true,
47+
showEditButton: true,
48+
showRevokeButton: true,
49+
showDeleteButton: true,
50+
showSnapshotButton: true,
51+
showMoveButtons: [true, true],
52+
canModify: true,
53+
widgetMessaging: undefined,
54+
isMenuOpened: true,
55+
trigger: (
56+
<IconButton size="24px">
57+
<TriggerIcon />
58+
</IconButton>
59+
),
60+
onStreamAudioClick: fn(),
61+
onEditClick: fn(),
62+
onSnapshotClick: fn(),
63+
onDeleteClick: fn(),
64+
onRevokeClick: fn(),
65+
onFinished: fn(),
66+
onMoveButton: fn(),
67+
},
68+
} as Meta<typeof WidgetContextMenuViewWrapper>;
69+
70+
const Template: StoryFn<typeof WidgetContextMenuViewWrapper> = (args) => <WidgetContextMenuViewWrapper {...args} />;
71+
72+
export const Default = Template.bind({});
73+
74+
export const OnlyBasicModification = Template.bind({});
75+
OnlyBasicModification.args = {
76+
showSnapshotButton: false,
77+
showMoveButtons: [false, false],
78+
showStreamAudioStreamButton: false,
79+
showEditButton: false,
80+
};

packages/shared-components/src/right-panel/WidgetContextMenu/WidgetContextMenuView.test.tsx

Lines changed: 84 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -7,86 +7,104 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
77
Please see LICENSE files in the repository root for full details.
88
*/
99

10-
import React, { type JSX, type ComponentProps } from "react";
10+
import React from "react";
1111
import { screen, render } from "jest-matrix-react";
1212
import userEvent from "@testing-library/user-event";
13-
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
14-
import { MatrixWidgetType } from "matrix-widget-api";
15-
import {
16-
type ApprovalOpts,
17-
type WidgetInfo,
18-
WidgetLifecycle,
19-
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle";
20-
21-
import { WidgetContextMenu } from "../../../../../src/components/views/context_menus/WidgetContextMenu";
22-
import { type IApp } from "../../../../../src/stores/WidgetStore";
23-
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
24-
import WidgetUtils from "../../../../../src/utils/WidgetUtils";
25-
import { ModuleRunner } from "../../../../../src/modules/ModuleRunner";
26-
import SettingsStore from "../../../../../src/settings/SettingsStore";
27-
28-
describe("<WidgetContextMenu />", () => {
29-
const widgetId = "w1";
30-
const eventId = "e1";
31-
const roomId = "r1";
32-
const userId = "@user-id:server";
33-
34-
const app: IApp = {
35-
id: widgetId,
36-
eventId,
37-
roomId,
38-
type: MatrixWidgetType.Custom,
39-
url: "https://example.com",
40-
name: "Example 1",
41-
creatorUserId: userId,
42-
avatar_url: undefined,
43-
};
13+
import { WidgetContextMenuAction, WidgetContextMenuSnapshot, WidgetContextMenuView } from "./WidgetContextMenuView";
14+
import * as stories from "./WidgetContextMenuView.stories.tsx";
15+
import { MockViewModel } from "../../viewmodel/MockViewModel.ts";
16+
import { IconButton } from "@vector-im/compound-web";
17+
import { composeStories } from "@storybook/react-vite";
18+
import TriggerIcon from "@vector-im/compound-design-tokens/assets/web/icons/overflow-horizontal";
19+
20+
const { Default, OnlyBasicModification } = composeStories(stories);
4421

45-
let mockClient: MatrixClient;
22+
describe("<WidgetContextMenuView />", () => {
4623

47-
let onFinished: () => void;
24+
afterEach(() => {
25+
jest.resetAllMocks();
26+
});
4827

49-
beforeEach(() => {
50-
onFinished = jest.fn();
51-
jest.spyOn(WidgetUtils, "canUserModifyWidgets").mockReturnValue(true);
5228

53-
mockClient = {
54-
getUserId: jest.fn().mockReturnValue(userId),
55-
} as unknown as MatrixClient;
29+
it("renders widget contextmenu with all options", () => {
30+
const { container } = render(<Default />);
31+
expect(container).toMatchSnapshot();
5632
});
5733

58-
afterEach(() => {
59-
jest.restoreAllMocks();
34+
it("renders widget contextmenu without only basic modification", () => {
35+
const { container } = render(<OnlyBasicModification />);
36+
expect(container).toMatchSnapshot();
6037
});
6138

62-
function getComponent(props: Partial<ComponentProps<typeof WidgetContextMenu>> = {}): JSX.Element {
63-
return (
64-
<MatrixClientContext.Provider value={mockClient}>
65-
<WidgetContextMenu app={app} onFinished={onFinished} {...props} />
66-
</MatrixClientContext.Provider>
67-
);
68-
}
39+
const onKeyDown = jest.fn();
40+
const togglePlay = jest.fn();
41+
const onSeekbarChange = jest.fn();
42+
43+
const onStreamAudioClick = jest.fn();
44+
const onEditClick = jest.fn();
45+
const onSnapshotClick = jest.fn();
46+
const onDeleteClick = jest.fn();
47+
const onRevokeClick = jest.fn();
48+
const onFinished = jest.fn();
49+
const onMoveButton = jest.fn();
50+
class WidgetContextMenuViewModel
51+
extends MockViewModel<WidgetContextMenuSnapshot>
52+
implements WidgetContextMenuAction
53+
{
54+
public onKeyDown = onKeyDown;
55+
public togglePlay = togglePlay;
56+
public onSeekbarChange = onSeekbarChange;
57+
58+
public onStreamAudioClick = onStreamAudioClick;
59+
public onEditClick = onEditClick;
60+
public onSnapshotClick = onSnapshotClick;
61+
public onDeleteClick = onDeleteClick;
62+
public onRevokeClick = onRevokeClick;
63+
public onFinished = onFinished;
64+
public onMoveButton = onMoveButton;
65+
};
6966

70-
it("renders revoke button", async () => {
71-
const { rerender } = render(getComponent());
67+
const defaultValue: WidgetContextMenuSnapshot = {
68+
showStreamAudioStreamButton: true,
69+
showEditButton: true,
70+
showRevokeButton: true,
71+
showDeleteButton: true,
72+
showSnapshotButton: true,
73+
showMoveButtons: [true, true],
74+
canModify: true,
75+
widgetMessaging: undefined,
76+
isMenuOpened: true,
77+
trigger: (
78+
<IconButton size="24px">
79+
<TriggerIcon />
80+
</IconButton>
81+
),
82+
};
7283

73-
const revokeButton = screen.getByLabelText("Revoke permissions");
74-
expect(revokeButton).toBeInTheDocument();
84+
it("should attach vm methods", async () => {
85+
const vm = new WidgetContextMenuViewModel(defaultValue);
7586

76-
jest.spyOn(ModuleRunner.instance, "invoke").mockImplementation((lifecycleEvent, opts, widgetInfo) => {
77-
if (lifecycleEvent === WidgetLifecycle.PreLoadRequest && (widgetInfo as WidgetInfo).id === widgetId) {
78-
(opts as ApprovalOpts).approved = true;
79-
}
80-
});
87+
render(<WidgetContextMenuView vm={vm} />);
8188

82-
rerender(getComponent());
83-
expect(revokeButton).not.toBeInTheDocument();
84-
});
89+
await userEvent.click(screen.getByRole("menuitem", { name: "Start audio stream" }));
90+
expect(onStreamAudioClick).toHaveBeenCalled();
91+
92+
await userEvent.click(screen.getByRole("menuitem", { name: "Edit" }));
93+
expect(onEditClick).toHaveBeenCalled();
94+
95+
await userEvent.click(screen.getByRole("menuitem", { name: "Take a picture" }));
96+
expect(onSnapshotClick).toHaveBeenCalled();
97+
98+
await userEvent.click(screen.getByRole("menuitem", { name: "Revoke permissions" }));
99+
expect(onRevokeClick).toHaveBeenCalled();
100+
101+
await userEvent.click(screen.getByRole("menuitem", { name: "Remove for everyone" }));
102+
expect(onDeleteClick).toHaveBeenCalled();
103+
104+
await userEvent.click(screen.getByRole("menuitem", { name: "Move left" }));
105+
expect(onMoveButton).toHaveBeenCalledWith(-1);
85106

86-
it("revokes permissions", async () => {
87-
render(getComponent());
88-
await userEvent.click(screen.getByLabelText("Revoke permissions"));
89-
expect(onFinished).toHaveBeenCalled();
90-
expect(SettingsStore.getValue("allowedWidgets", roomId)[eventId]).toBe(false);
107+
await userEvent.click(screen.getByRole("menuitem", { name: "Move right" }));
108+
expect(onMoveButton).toHaveBeenCalledWith(1);
91109
});
92110
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2+
3+
exports[`<WidgetContextMenuView /> renders widget contextmenu with all options 1`] = `
4+
<div
5+
aria-hidden="true"
6+
data-aria-hidden="true"
7+
>
8+
<button
9+
aria-controls="radix-_r_1_"
10+
aria-disabled="false"
11+
aria-expanded="true"
12+
aria-haspopup="menu"
13+
class="_icon-button_1pz9o_8"
14+
data-kind="primary"
15+
data-state="open"
16+
id="radix-_r_0_"
17+
role="button"
18+
style="--cpd-icon-button-size: 24px;"
19+
tabindex="0"
20+
type="button"
21+
>
22+
<div
23+
class="_indicator-icon_zr2a0_17"
24+
style="--cpd-icon-button-size: 100%;"
25+
>
26+
<svg
27+
fill="currentColor"
28+
height="1em"
29+
viewBox="0 0 24 24"
30+
width="1em"
31+
xmlns="http://www.w3.org/2000/svg"
32+
>
33+
<path
34+
d="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
35+
/>
36+
</svg>
37+
</div>
38+
</button>
39+
</div>
40+
`;
41+
42+
exports[`<WidgetContextMenuView /> renders widget contextmenu without only basic modification 1`] = `
43+
<div
44+
aria-hidden="true"
45+
data-aria-hidden="true"
46+
>
47+
<button
48+
aria-controls="radix-_r_b_"
49+
aria-disabled="false"
50+
aria-expanded="true"
51+
aria-haspopup="menu"
52+
class="_icon-button_1pz9o_8"
53+
data-kind="primary"
54+
data-state="open"
55+
id="radix-_r_a_"
56+
role="button"
57+
style="--cpd-icon-button-size: 24px;"
58+
tabindex="0"
59+
type="button"
60+
>
61+
<div
62+
class="_indicator-icon_zr2a0_17"
63+
style="--cpd-icon-button-size: 100%;"
64+
>
65+
<svg
66+
fill="currentColor"
67+
height="1em"
68+
viewBox="0 0 24 24"
69+
width="1em"
70+
xmlns="http://www.w3.org/2000/svg"
71+
>
72+
<path
73+
d="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
74+
/>
75+
</svg>
76+
</div>
77+
</button>
78+
</div>
79+
`;

0 commit comments

Comments
 (0)