Update tests to prefer RTL over Enzyme (#10247

* Update tests to prefer RTL over Enzyme

* Strict types
This commit is contained in:
Michael Telatyński 2023-02-28 08:58:23 +00:00 committed by GitHub
parent dd6fc124d7
commit f40d15388c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1095 additions and 632 deletions

View file

@ -60,7 +60,7 @@ export default class PlayPauseButton extends React.PureComponent<IProps> {
return (
<AccessibleTooltipButton
data-test-id="play-pause-button"
data-testid="play-pause-button"
className={classes}
title={isPlaying ? _t("Pause") : _t("Play")}
onClick={this.onClick}

View file

@ -15,20 +15,16 @@ limitations under the License.
*/
import React from "react";
// eslint-disable-next-line deprecate/import
import { mount, ReactWrapper } from "enzyme";
import { mocked } from "jest-mock";
import { logger } from "matrix-js-sdk/src/logger";
import { act } from "react-dom/test-utils";
import { fireEvent, render, RenderResult } from "@testing-library/react";
import RecordingPlayback, { PlaybackLayout } from "../../../../src/components/views/audio_messages/RecordingPlayback";
import { Playback } from "../../../../src/audio/Playback";
import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext";
import { createAudioContext } from "../../../../src/audio/compat";
import { findByTestId, flushPromises } from "../../../test-utils";
import PlaybackWaveform from "../../../../src/components/views/audio_messages/PlaybackWaveform";
import SeekBar from "../../../../src/components/views/audio_messages/SeekBar";
import PlaybackClock from "../../../../src/components/views/audio_messages/PlaybackClock";
import { flushPromises } from "../../../test-utils";
import { IRoomState } from "../../../../src/components/structures/RoomView";
jest.mock("../../../../src/audio/compat", () => ({
createAudioContext: jest.fn(),
@ -57,12 +53,13 @@ describe("<RecordingPlayback />", () => {
const mockChannelData = new Float32Array();
const defaultRoom = { roomId: "!room:server.org", timelineRenderingType: TimelineRenderingType.File };
const defaultRoom = { roomId: "!room:server.org", timelineRenderingType: TimelineRenderingType.File } as IRoomState;
const getComponent = (props: React.ComponentProps<typeof RecordingPlayback>, room = defaultRoom) =>
mount(<RecordingPlayback {...props} />, {
wrappingComponent: RoomContext.Provider,
wrappingComponentProps: { value: room },
});
render(
<RoomContext.Provider value={room}>
<RecordingPlayback {...props} />
</RoomContext.Provider>,
);
beforeEach(() => {
jest.spyOn(logger, "error").mockRestore();
@ -71,7 +68,7 @@ describe("<RecordingPlayback />", () => {
mocked(createAudioContext).mockReturnValue(mockAudioContext as unknown as AudioContext);
});
const getPlayButton = (component: ReactWrapper) => findByTestId(component, "play-pause-button").at(0);
const getPlayButton = (component: RenderResult) => component.getByTestId("play-pause-button");
it("renders recording playback", () => {
const playback = new Playback(new ArrayBuffer(8));
@ -82,15 +79,16 @@ describe("<RecordingPlayback />", () => {
it("disables play button while playback is decoding", async () => {
const playback = new Playback(new ArrayBuffer(8));
const component = getComponent({ playback });
expect(getPlayButton(component).props().disabled).toBeTruthy();
expect(getPlayButton(component)).toHaveAttribute("disabled");
expect(getPlayButton(component)).toHaveAttribute("aria-disabled", "true");
});
it("enables play button when playback is finished decoding", async () => {
const playback = new Playback(new ArrayBuffer(8));
const component = getComponent({ playback });
await flushPromises();
component.setProps({});
expect(getPlayButton(component).props().disabled).toBeFalsy();
expect(getPlayButton(component)).not.toHaveAttribute("disabled");
expect(getPlayButton(component)).not.toHaveAttribute("aria-disabled", "true");
});
it("displays error when playback decoding fails", async () => {
@ -101,7 +99,7 @@ describe("<RecordingPlayback />", () => {
const playback = new Playback(new ArrayBuffer(8));
const component = getComponent({ playback });
await flushPromises();
expect(component.find(".text-warning").length).toBeFalsy();
expect(component.container.querySelector(".text-warning")).toBeDefined();
});
it("displays pre-prepared playback with correct playback phase", async () => {
@ -109,8 +107,9 @@ describe("<RecordingPlayback />", () => {
await playback.prepare();
const component = getComponent({ playback });
// playback already decoded, button is not disabled
expect(getPlayButton(component).props().disabled).toBeFalsy();
expect(component.find(".text-warning").length).toBeFalsy();
expect(getPlayButton(component)).not.toHaveAttribute("disabled");
expect(getPlayButton(component)).not.toHaveAttribute("aria-disabled", "true");
expect(component.container.querySelector(".text-warning")).toBeFalsy();
});
it("toggles playback on play pause button click", async () => {
@ -119,9 +118,7 @@ describe("<RecordingPlayback />", () => {
await playback.prepare();
const component = getComponent({ playback });
act(() => {
getPlayButton(component).simulate("click");
});
fireEvent.click(getPlayButton(component));
expect(playback.toggle).toHaveBeenCalled();
});
@ -131,9 +128,9 @@ describe("<RecordingPlayback />", () => {
const playback = new Playback(new ArrayBuffer(8));
const component = getComponent({ playback, layout: PlaybackLayout.Composer });
expect(component.find(PlaybackClock).length).toBeTruthy();
expect(component.find(PlaybackWaveform).length).toBeTruthy();
expect(component.find(SeekBar).length).toBeFalsy();
expect(component.container.querySelector(".mx_Clock")).toBeDefined();
expect(component.container.querySelector(".mx_Waveform")).toBeDefined();
expect(component.container.querySelector(".mx_SeekBar")).toBeFalsy();
});
});
@ -142,18 +139,18 @@ describe("<RecordingPlayback />", () => {
const playback = new Playback(new ArrayBuffer(8));
const component = getComponent({ playback, layout: PlaybackLayout.Timeline });
expect(component.find(PlaybackClock).length).toBeTruthy();
expect(component.find(PlaybackWaveform).length).toBeTruthy();
expect(component.find(SeekBar).length).toBeTruthy();
expect(component.container.querySelector(".mx_Clock")).toBeDefined();
expect(component.container.querySelector(".mx_Waveform")).toBeDefined();
expect(component.container.querySelector(".mx_SeekBar")).toBeDefined();
});
it("should be the default", () => {
const playback = new Playback(new ArrayBuffer(8));
const component = getComponent({ playback }); // no layout set for test
expect(component.find(PlaybackClock).length).toBeTruthy();
expect(component.find(PlaybackWaveform).length).toBeTruthy();
expect(component.find(SeekBar).length).toBeTruthy();
expect(component.container.querySelector(".mx_Clock")).toBeDefined();
expect(component.container.querySelector(".mx_Waveform")).toBeDefined();
expect(component.container.querySelector(".mx_SeekBar")).toBeDefined();
});
});
});

View file

@ -17,9 +17,7 @@ limitations under the License.
*/
import React from "react";
import { act } from "react-dom/test-utils";
// eslint-disable-next-line deprecate/import
import { mount, ReactWrapper } from "enzyme";
import { fireEvent, render, RenderResult } from "@testing-library/react";
import InteractiveAuthComponent from "../../../../src/components/structures/InteractiveAuth";
import { flushPromises, getMockClientWithEventEmitter, unmockClientPeg } from "../../../test-utils";
@ -34,7 +32,7 @@ describe("InteractiveAuthComponent", function () {
makeRequest: jest.fn().mockResolvedValue(undefined),
onAuthFinished: jest.fn(),
};
const getComponent = (props = {}) => mount(<InteractiveAuthComponent {...defaultProps} {...props} />);
const getComponent = (props = {}) => render(<InteractiveAuthComponent {...defaultProps} {...props} />);
beforeEach(function () {
jest.clearAllMocks();
@ -44,9 +42,10 @@ describe("InteractiveAuthComponent", function () {
unmockClientPeg();
});
const getSubmitButton = (wrapper: ReactWrapper) => wrapper.find('AccessibleButton[kind="primary"]').at(0);
const getRegistrationTokenInput = (wrapper: ReactWrapper) =>
wrapper.find('input[name="registrationTokenField"]').at(0);
const getSubmitButton = ({ container }: RenderResult) =>
container.querySelector(".mx_AccessibleButton_kind_primary");
const getRegistrationTokenInput = ({ container }: RenderResult) =>
container.querySelector('input[name="registrationTokenField"]');
it("Should successfully complete a registration token flow", async () => {
const onAuthFinished = jest.fn();
@ -61,28 +60,25 @@ describe("InteractiveAuthComponent", function () {
const registrationTokenNode = getRegistrationTokenInput(wrapper);
const submitNode = getSubmitButton(wrapper);
const formNode = wrapper.find("form").at(0);
const formNode = wrapper.container.querySelector("form");
expect(registrationTokenNode).toBeTruthy();
expect(submitNode).toBeTruthy();
expect(formNode).toBeTruthy();
// submit should be disabled
expect(submitNode.props().disabled).toBe(true);
expect(submitNode).toHaveAttribute("disabled");
expect(submitNode).toHaveAttribute("aria-disabled", "true");
// put something in the registration token box
act(() => {
registrationTokenNode.simulate("change", { target: { value: "s3kr3t" } });
wrapper.setProps({});
});
fireEvent.change(registrationTokenNode!, { target: { value: "s3kr3t" } });
expect(getRegistrationTokenInput(wrapper).props().value).toEqual("s3kr3t");
expect(getSubmitButton(wrapper).props().disabled).toBe(false);
expect(getRegistrationTokenInput(wrapper)).toHaveValue("s3kr3t");
expect(submitNode).not.toHaveAttribute("disabled");
expect(submitNode).not.toHaveAttribute("aria-disabled", "true");
// hit enter; that should trigger a request
act(() => {
formNode.simulate("submit");
});
fireEvent.submit(formNode!);
// wait for auth request to resolve
await flushPromises();

View file

@ -15,8 +15,7 @@ limitations under the License.
*/
import React from "react";
import { act } from "react-dom/test-utils";
import { fireEvent, render, RenderResult } from "@testing-library/react";
import { act, fireEvent, render, RenderResult } from "@testing-library/react";
import { MatrixClient, MatrixEvent, Room, RoomMember, getBeaconInfoIdentifier } from "matrix-js-sdk/src/matrix";
import * as maplibregl from "maplibre-gl";
import { mocked } from "jest-mock";

View file

@ -18,6 +18,7 @@ import React from "react";
// eslint-disable-next-line deprecate/import
import { mount, ReactWrapper } from "enzyme";
import { mocked } from "jest-mock";
// eslint-disable-next-line deprecate/import
import { act } from "react-dom/test-utils";
import { Room } from "matrix-js-sdk/src/matrix";

View file

@ -15,8 +15,7 @@ limitations under the License.
*/
import React, { ComponentProps } from "react";
// eslint-disable-next-line deprecate/import
import { mount, ReactWrapper } from "enzyme";
import { render, RenderResult } from "@testing-library/react";
import { MatrixEvent, RoomMember } from "matrix-js-sdk/src/matrix";
import {
@ -124,8 +123,8 @@ describe("EventListSummary", function () {
events: [],
children: [],
};
const renderComponent = (props = {}): ReactWrapper => {
return mount(
const renderComponent = (props = {}): RenderResult => {
return render(
<MatrixClientContext.Provider value={mockClient}>
<EventListSummary {...defaultProps} {...props} />
</MatrixClientContext.Provider>,
@ -150,13 +149,11 @@ describe("EventListSummary", function () {
threshold: 3,
};
const wrapper = renderComponent(props); // matrix cli context wrapper
const { container } = renderComponent(props); // matrix cli context wrapper
expect(wrapper.find("GenericEventListSummary").props().children).toEqual([
<div className="event_tile" key="event0">
Expanded membership
</div>,
]);
const children = container.querySelector(".mx_GenericEventListSummary_unstyledList")!.children;
expect(children).toHaveLength(1);
expect(children[0]).toHaveTextContent("Expanded membership");
});
it("renders expanded events if there are less than props.threshold", function () {
@ -172,16 +169,12 @@ describe("EventListSummary", function () {
threshold: 3,
};
const wrapper = renderComponent(props); // matrix cli context wrapper
const { container } = renderComponent(props); // matrix cli context wrapper
expect(wrapper.find("GenericEventListSummary").props().children).toEqual([
<div className="event_tile" key="event0">
Expanded membership
</div>,
<div className="event_tile" key="event1">
Expanded membership
</div>,
]);
const children = container.querySelector(".mx_GenericEventListSummary_unstyledList")!.children;
expect(children).toHaveLength(2);
expect(children[0]).toHaveTextContent("Expanded membership");
expect(children[1]).toHaveTextContent("Expanded membership");
});
it("renders collapsed events if events.length = props.threshold", function () {
@ -198,11 +191,9 @@ describe("EventListSummary", function () {
threshold: 3,
};
const wrapper = renderComponent(props);
const summary = wrapper.find(".mx_GenericEventListSummary_summary");
const summaryText = summary.text();
expect(summaryText).toBe("user_1 joined and left and joined");
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
expect(summary).toHaveTextContent("user_1 joined and left and joined");
});
it("truncates long join,leave repetitions", function () {
@ -230,11 +221,9 @@ describe("EventListSummary", function () {
threshold: 3,
};
const wrapper = renderComponent(props);
const summary = wrapper.find(".mx_GenericEventListSummary_summary");
const summaryText = summary.text();
expect(summaryText).toBe("user_1 joined and left 7 times");
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
expect(summary).toHaveTextContent("user_1 joined and left 7 times");
});
it("truncates long join,leave repetitions between other events", function () {
@ -274,11 +263,9 @@ describe("EventListSummary", function () {
threshold: 3,
};
const wrapper = renderComponent(props);
const summary = wrapper.find(".mx_GenericEventListSummary_summary");
const summaryText = summary.text();
expect(summaryText).toBe("user_1 was unbanned, joined and left 7 times and was invited");
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
expect(summary).toHaveTextContent("user_1 was unbanned, joined and left 7 times and was invited");
});
it("truncates multiple sequences of repetitions with other events between", function () {
@ -320,11 +307,9 @@ describe("EventListSummary", function () {
threshold: 3,
};
const wrapper = renderComponent(props);
const summary = wrapper.find(".mx_GenericEventListSummary_summary");
const summaryText = summary.text();
expect(summaryText).toBe(
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
expect(summary).toHaveTextContent(
"user_1 was unbanned, joined and left 2 times, was banned, " + "joined and left 3 times and was invited",
);
});
@ -374,11 +359,11 @@ describe("EventListSummary", function () {
threshold: 3,
};
const wrapper = renderComponent(props);
const summary = wrapper.find(".mx_GenericEventListSummary_summary");
const summaryText = summary.text();
expect(summaryText).toBe("user_1 and one other were unbanned, joined and left 2 times and were banned");
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
expect(summary).toHaveTextContent(
"user_1 and one other were unbanned, joined and left 2 times and were banned",
);
});
it("handles many users following the same sequence of memberships", function () {
@ -406,11 +391,11 @@ describe("EventListSummary", function () {
threshold: 3,
};
const wrapper = renderComponent(props);
const summary = wrapper.find(".mx_GenericEventListSummary_summary");
const summaryText = summary.text();
expect(summaryText).toBe("user_0 and 19 others were unbanned, joined and left 2 times and were banned");
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
expect(summary).toHaveTextContent(
"user_0 and 19 others were unbanned, joined and left 2 times and were banned",
);
});
it("correctly orders sequences of transitions by the order of their first event", function () {
@ -450,11 +435,9 @@ describe("EventListSummary", function () {
threshold: 3,
};
const wrapper = renderComponent(props);
const summary = wrapper.find(".mx_GenericEventListSummary_summary");
const summaryText = summary.text();
expect(summaryText).toBe(
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
expect(summary).toHaveTextContent(
"user_2 was unbanned and joined and left 2 times, user_1 was unbanned, " +
"joined and left 2 times and was banned",
);
@ -520,11 +503,9 @@ describe("EventListSummary", function () {
threshold: 3,
};
const wrapper = renderComponent(props);
const summary = wrapper.find(".mx_GenericEventListSummary_summary");
const summaryText = summary.text();
expect(summaryText).toBe(
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
expect(summary).toHaveTextContent(
"user_1 was invited, was banned, joined, rejected their invitation, left, " +
"had their invitation withdrawn, was unbanned, was removed, left and was removed",
);
@ -563,11 +544,11 @@ describe("EventListSummary", function () {
threshold: 3,
};
const wrapper = renderComponent(props);
const summary = wrapper.find(".mx_GenericEventListSummary_summary");
const summaryText = summary.text();
expect(summaryText).toBe("user_1 and one other rejected their invitations and had their invitations withdrawn");
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
expect(summary).toHaveTextContent(
"user_1 and one other rejected their invitations and had their invitations withdrawn",
);
});
it("handles invitation plurals correctly when there are multiple invites", function () {
@ -591,11 +572,9 @@ describe("EventListSummary", function () {
threshold: 1, // threshold = 1 to force collapse
};
const wrapper = renderComponent(props);
const summary = wrapper.find(".mx_GenericEventListSummary_summary");
const summaryText = summary.text();
expect(summaryText).toBe("user_1 rejected their invitation 2 times");
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
expect(summary).toHaveTextContent("user_1 rejected their invitation 2 times");
});
it('handles a summary length = 2, with no "others"', function () {
@ -613,11 +592,9 @@ describe("EventListSummary", function () {
threshold: 3,
};
const wrapper = renderComponent(props);
const summary = wrapper.find(".mx_GenericEventListSummary_summary");
const summaryText = summary.text();
expect(summaryText).toBe("user_1 and user_2 joined 2 times");
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
expect(summary).toHaveTextContent("user_1 and user_2 joined 2 times");
});
it('handles a summary length = 2, with 1 "other"', function () {
@ -634,11 +611,9 @@ describe("EventListSummary", function () {
threshold: 3,
};
const wrapper = renderComponent(props);
const summary = wrapper.find(".mx_GenericEventListSummary_summary");
const summaryText = summary.text();
expect(summaryText).toBe("user_1, user_2 and one other joined");
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
expect(summary).toHaveTextContent("user_1, user_2 and one other joined");
});
it('handles a summary length = 2, with many "others"', function () {
@ -651,11 +626,9 @@ describe("EventListSummary", function () {
threshold: 3,
};
const wrapper = renderComponent(props);
const summary = wrapper.find(".mx_GenericEventListSummary_summary");
const summaryText = summary.text();
expect(summaryText).toBe("user_0, user_1 and 18 others joined");
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
expect(summary).toHaveTextContent("user_0, user_1 and 18 others joined");
});
it("should not blindly group 3pid invites and treat them as distinct users instead", () => {
@ -703,10 +676,8 @@ describe("EventListSummary", function () {
threshold: 3,
};
const wrapper = renderComponent(props);
const summary = wrapper.find(".mx_GenericEventListSummary_summary");
const summaryText = summary.text();
expect(summaryText).toBe("n...@d... was invited 2 times, d...@w... was invited");
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
expect(summary).toHaveTextContent("n...@d... was invited 2 times, d...@w... was invited");
});
});

View file

@ -15,15 +15,14 @@ limitations under the License.
*/
import React from "react";
// eslint-disable-next-line deprecate/import
import { mount, ReactWrapper } from "enzyme";
import { fireEvent, render, RenderResult } from "@testing-library/react";
import { Room } from "matrix-js-sdk/src/models/room";
import { M_POLL_KIND_DISCLOSED, M_POLL_KIND_UNDISCLOSED, M_POLL_START } from "matrix-js-sdk/src/@types/polls";
import { PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { M_TEXT } from "matrix-js-sdk/src/@types/extensible_events";
import { findById, getMockClientWithEventEmitter } from "../../../test-utils";
import { getMockClientWithEventEmitter } from "../../../test-utils";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import PollCreateDialog from "../../../../src/components/views/elements/PollCreateDialog";
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
@ -51,40 +50,41 @@ describe("PollCreateDialog", () => {
});
it("renders a blank poll", () => {
const dialog = mount(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />, {
wrappingComponent: MatrixClientContext.Provider,
wrappingComponentProps: { value: mockClient },
});
expect(dialog.html()).toMatchSnapshot();
const dialog = render(
<MatrixClientContext.Provider value={mockClient}>
<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />
</MatrixClientContext.Provider>,
);
expect(dialog.asFragment()).toMatchSnapshot();
});
it("autofocuses the poll topic on mount", () => {
const dialog = mount(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
expect(findById(dialog, "poll-topic-input").at(0).props().autoFocus).toEqual(true);
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
expect(dialog.container.querySelector("#poll-topic-input")).toHaveFocus();
});
it("autofocuses the new poll option field after clicking add option button", () => {
const dialog = mount(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
expect(findById(dialog, "poll-topic-input").at(0).props().autoFocus).toEqual(true);
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
expect(dialog.container.querySelector("#poll-topic-input")).toHaveFocus();
dialog.find("div.mx_PollCreateDialog_addOption").simulate("click");
fireEvent.click(dialog.container.querySelector("div.mx_PollCreateDialog_addOption")!);
expect(findById(dialog, "poll-topic-input").at(0).props().autoFocus).toEqual(false);
expect(findById(dialog, "pollcreate_option_1").at(0).props().autoFocus).toEqual(false);
expect(findById(dialog, "pollcreate_option_2").at(0).props().autoFocus).toEqual(true);
expect(dialog.container.querySelector("#poll-topic-input")).not.toHaveFocus();
expect(dialog.container.querySelector("#pollcreate_option_1")).not.toHaveFocus();
expect(dialog.container.querySelector("#pollcreate_option_2")).toHaveFocus();
});
it("renders a question and some options", () => {
const dialog = mount(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
expect(submitIsDisabled(dialog)).toBe(true);
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
expectSubmitToBeDisabled(dialog, true);
// When I set some values in the boxes
changeValue(dialog, "Question or topic", "How many turnips is the optimal number?");
changeValue(dialog, "Option 1", "As many as my neighbour");
changeValue(dialog, "Option 2", "The question is meaningless");
dialog.find("div.mx_PollCreateDialog_addOption").simulate("click");
fireEvent.click(dialog.container.querySelector("div.mx_PollCreateDialog_addOption")!);
changeValue(dialog, "Option 3", "Mu");
expect(dialog.html()).toMatchSnapshot();
expect(dialog.asFragment()).toMatchSnapshot();
});
it("renders info from a previous event", () => {
@ -92,23 +92,23 @@ describe("PollCreateDialog", () => {
PollStartEvent.from("Poll Q", ["Answer 1", "Answer 2"], M_POLL_KIND_DISCLOSED).serialize(),
);
const dialog = mount(
const dialog = render(
<PollCreateDialog room={createRoom()} onFinished={jest.fn()} editingMxEvent={previousEvent} />,
);
expect(submitIsDisabled(dialog)).toBe(false);
expect(dialog.html()).toMatchSnapshot();
expectSubmitToBeDisabled(dialog, false);
expect(dialog.asFragment()).toMatchSnapshot();
});
it("doesn't allow submitting until there are options", () => {
const dialog = mount(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
expect(submitIsDisabled(dialog)).toBe(true);
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
expectSubmitToBeDisabled(dialog, true);
});
it("does allow submitting when there are options and a question", () => {
// Given a dialog with no info in (which I am unable to submit)
const dialog = mount(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
expect(submitIsDisabled(dialog)).toBe(true);
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
expectSubmitToBeDisabled(dialog, true);
// When I set some values in the boxes
changeValue(dialog, "Question or topic", "Q");
@ -116,28 +116,30 @@ describe("PollCreateDialog", () => {
changeValue(dialog, "Option 2", "A2");
// Then I am able to submit
expect(submitIsDisabled(dialog)).toBe(false);
expectSubmitToBeDisabled(dialog, false);
});
it("shows the open poll description at first", () => {
const dialog = mount(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
expect(dialog.find("select").prop("value")).toEqual(M_POLL_KIND_DISCLOSED.name);
expect(dialog.find("p").text()).toEqual("Voters see results as soon as they have voted");
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
expect(dialog.container.querySelector("select")).toHaveValue(M_POLL_KIND_DISCLOSED.name);
expect(dialog.container.querySelector("p")).toHaveTextContent("Voters see results as soon as they have voted");
});
it("shows the closed poll description if we choose it", () => {
const dialog = mount(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
changeKind(dialog, M_POLL_KIND_UNDISCLOSED.name);
expect(dialog.find("select").prop("value")).toEqual(M_POLL_KIND_UNDISCLOSED.name);
expect(dialog.find("p").text()).toEqual("Results are only revealed when you end the poll");
expect(dialog.container.querySelector("select")).toHaveValue(M_POLL_KIND_UNDISCLOSED.name);
expect(dialog.container.querySelector("p")).toHaveTextContent(
"Results are only revealed when you end the poll",
);
});
it("shows the open poll description if we choose it", () => {
const dialog = mount(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
changeKind(dialog, M_POLL_KIND_UNDISCLOSED.name);
changeKind(dialog, M_POLL_KIND_DISCLOSED.name);
expect(dialog.find("select").prop("value")).toEqual(M_POLL_KIND_DISCLOSED.name);
expect(dialog.find("p").text()).toEqual("Voters see results as soon as they have voted");
expect(dialog.container.querySelector("select")).toHaveValue(M_POLL_KIND_DISCLOSED.name);
expect(dialog.container.querySelector("p")).toHaveTextContent("Voters see results as soon as they have voted");
});
it("shows the closed poll description when editing a closed poll", () => {
@ -146,32 +148,34 @@ describe("PollCreateDialog", () => {
);
previousEvent.event.event_id = "$prevEventId";
const dialog = mount(
const dialog = render(
<PollCreateDialog room={createRoom()} onFinished={jest.fn()} editingMxEvent={previousEvent} />,
);
expect(dialog.find("select").prop("value")).toEqual(M_POLL_KIND_UNDISCLOSED.name);
expect(dialog.find("p").text()).toEqual("Results are only revealed when you end the poll");
expect(dialog.container.querySelector("select")).toHaveValue(M_POLL_KIND_UNDISCLOSED.name);
expect(dialog.container.querySelector("p")).toHaveTextContent(
"Results are only revealed when you end the poll",
);
});
it("displays a spinner after submitting", () => {
const dialog = mount(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
changeValue(dialog, "Question or topic", "Q");
changeValue(dialog, "Option 1", "A1");
changeValue(dialog, "Option 2", "A2");
expect(dialog.find("Spinner").length).toBe(0);
expect(dialog.container.querySelector(".mx_Spinner")).toBeFalsy();
dialog.find("button").simulate("click");
expect(dialog.find("Spinner").length).toBe(1);
fireEvent.click(dialog.container.querySelector("button")!);
expect(dialog.container.querySelector(".mx_Spinner")).toBeDefined();
});
it("sends a poll create event when submitted", () => {
const dialog = mount(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
changeValue(dialog, "Question or topic", "Q");
changeValue(dialog, "Option 1", "A1");
changeValue(dialog, "Option 2", "A2");
dialog.find("button").simulate("click");
fireEvent.click(dialog.container.querySelector("button")!);
const [, , eventType, sentEventContent] = mockClient.sendEvent.mock.calls[0];
expect(M_POLL_START.matches(eventType)).toBeTruthy();
expect(sentEventContent).toEqual({
@ -206,14 +210,14 @@ describe("PollCreateDialog", () => {
);
previousEvent.event.event_id = "$prevEventId";
const dialog = mount(
const dialog = render(
<PollCreateDialog room={createRoom()} onFinished={jest.fn()} editingMxEvent={previousEvent} />,
);
changeValue(dialog, "Question or topic", "Poll Q updated");
changeValue(dialog, "Option 2", "Answer 2 updated");
changeKind(dialog, M_POLL_KIND_UNDISCLOSED.name);
dialog.find("button").simulate("click");
fireEvent.click(dialog.container.querySelector("button")!);
const [, , eventType, sentEventContent] = mockClient.sendEvent.mock.calls[0];
expect(M_POLL_START.matches(eventType)).toBeTruthy();
@ -255,12 +259,12 @@ describe("PollCreateDialog", () => {
);
previousEvent.event.event_id = "$prevEventId";
const dialog = mount(
const dialog = render(
<PollCreateDialog room={createRoom()} onFinished={jest.fn()} editingMxEvent={previousEvent} />,
);
changeValue(dialog, "Question or topic", "Poll Q updated");
dialog.find("button").simulate("click");
fireEvent.click(dialog.container.querySelector("button")!);
const [, , eventType, sentEventContent] = mockClient.sendEvent.mock.calls[0];
expect(M_POLL_START.matches(eventType)).toBeTruthy();
@ -273,14 +277,20 @@ function createRoom(): Room {
return new Room("roomid", MatrixClientPeg.get(), "@name:example.com", {});
}
function changeValue(wrapper: ReactWrapper, labelText: string, value: string) {
wrapper.find(`input[label="${labelText}"]`).simulate("change", { target: { value: value } });
function changeValue(wrapper: RenderResult, labelText: string, value: string) {
fireEvent.change(wrapper.container.querySelector(`input[label="${labelText}"]`)!, {
target: { value: value },
});
}
function changeKind(wrapper: ReactWrapper, value: string) {
wrapper.find("select").simulate("change", { target: { value: value } });
function changeKind(wrapper: RenderResult, value: string) {
fireEvent.change(wrapper.container.querySelector("select")!, { target: { value: value } });
}
function submitIsDisabled(wrapper: ReactWrapper) {
return wrapper.find('button[type="submit"]').prop("aria-disabled") === true;
function expectSubmitToBeDisabled(wrapper: RenderResult, disabled: boolean) {
if (disabled) {
expect(wrapper.container.querySelector('button[type="submit"]')).toHaveAttribute("aria-disabled", "true");
} else {
expect(wrapper.container.querySelector('button[type="submit"]')).not.toHaveAttribute("aria-disabled", "true");
}
}

View file

@ -1,7 +1,560 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PollCreateDialog renders a blank poll 1`] = `"<div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div><div data-focus-lock-disabled="false" role="dialog" aria-labelledby="mx_CompoundDialog_title" aria-describedby="mx_CompoundDialog_content" class="mx_CompoundDialog mx_ScrollableBaseDialog"><div class="mx_CompoundDialog_header"><h1>Create poll</h1><div aria-label="Close dialog" role="button" tabindex="0" class="mx_AccessibleButton mx_CompoundDialog_cancelButton"></div></div><form class="mx_CompoundDialog_form"><div class="mx_CompoundDialog_content"><div class="mx_PollCreateDialog"><h2>Poll type</h2><div class="mx_Field mx_Field_select"><select type="text" id="mx_Field_1"><option value="org.matrix.msc3381.poll.disclosed">Open poll</option><option value="org.matrix.msc3381.poll.undisclosed">Closed poll</option></select><label for="mx_Field_1"></label></div><p>Voters see results as soon as they have voted</p><h2>What is your poll question or topic?</h2><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="poll-topic-input" maxlength="340" label="Question or topic" placeholder="Write something…" type="text" value=""><label for="poll-topic-input">Question or topic</label></div><h2>Create options</h2><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_0" maxlength="340" label="Option 1" placeholder="Write an option" type="text" value=""><label for="pollcreate_option_0">Option 1</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_1" maxlength="340" label="Option 2" placeholder="Write an option" type="text" value=""><label for="pollcreate_option_1">Option 2</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_addOption mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary">Add option</div></div></div><div class="mx_CompoundDialog_footer"><div role="button" tabindex="0" class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline">Cancel</div><button type="submit" role="button" tabindex="0" aria-disabled="true" disabled="" class="mx_AccessibleButton mx_Dialog_nonDialogButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary mx_AccessibleButton_disabled">Create Poll</button></div></form></div><div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div>"`;
exports[`PollCreateDialog renders a blank poll 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_CompoundDialog_content"
aria-labelledby="mx_CompoundDialog_title"
class="mx_CompoundDialog mx_ScrollableBaseDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_CompoundDialog_header"
>
<h1>
Create poll
</h1>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_CompoundDialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<form
class="mx_CompoundDialog_form"
>
<div
class="mx_CompoundDialog_content"
>
<div
class="mx_PollCreateDialog"
>
<h2>
Poll type
</h2>
<div
class="mx_Field mx_Field_select"
>
<select
id="mx_Field_1"
type="text"
>
<option
value="org.matrix.msc3381.poll.disclosed"
>
Open poll
</option>
<option
value="org.matrix.msc3381.poll.undisclosed"
>
Closed poll
</option>
</select>
<label
for="mx_Field_1"
/>
</div>
<p>
Voters see results as soon as they have voted
</p>
<h2>
What is your poll question or topic?
</h2>
<div
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
>
<input
id="poll-topic-input"
label="Question or topic"
maxlength="340"
placeholder="Write something…"
type="text"
value=""
/>
<label
for="poll-topic-input"
>
Question or topic
</label>
</div>
<h2>
Create options
</h2>
<div
class="mx_PollCreateDialog_option"
>
<div
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
>
<input
id="pollcreate_option_0"
label="Option 1"
maxlength="340"
placeholder="Write an option"
type="text"
value=""
/>
<label
for="pollcreate_option_0"
>
Option 1
</label>
</div>
<div
class="mx_AccessibleButton mx_PollCreateDialog_removeOption"
role="button"
tabindex="0"
/>
</div>
<div
class="mx_PollCreateDialog_option"
>
<div
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
>
<input
id="pollcreate_option_1"
label="Option 2"
maxlength="340"
placeholder="Write an option"
type="text"
value=""
/>
<label
for="pollcreate_option_1"
>
Option 2
</label>
</div>
<div
class="mx_AccessibleButton mx_PollCreateDialog_removeOption"
role="button"
tabindex="0"
/>
</div>
<div
class="mx_AccessibleButton mx_PollCreateDialog_addOption mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary"
role="button"
tabindex="0"
>
Add option
</div>
</div>
</div>
<div
class="mx_CompoundDialog_footer"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
role="button"
tabindex="0"
>
Cancel
</div>
<button
aria-disabled="true"
class="mx_AccessibleButton mx_Dialog_nonDialogButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary mx_AccessibleButton_disabled"
disabled=""
role="button"
tabindex="0"
type="submit"
>
Create Poll
</button>
</div>
</form>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`PollCreateDialog renders a question and some options 1`] = `"<div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div><div data-focus-lock-disabled="false" role="dialog" aria-labelledby="mx_CompoundDialog_title" aria-describedby="mx_CompoundDialog_content" class="mx_CompoundDialog mx_ScrollableBaseDialog"><div class="mx_CompoundDialog_header"><h1>Create poll</h1><div aria-label="Close dialog" role="button" tabindex="0" class="mx_AccessibleButton mx_CompoundDialog_cancelButton"></div></div><form class="mx_CompoundDialog_form"><div class="mx_CompoundDialog_content"><div class="mx_PollCreateDialog"><h2>Poll type</h2><div class="mx_Field mx_Field_select"><select type="text" id="mx_Field_4"><option value="org.matrix.msc3381.poll.disclosed">Open poll</option><option value="org.matrix.msc3381.poll.undisclosed">Closed poll</option></select><label for="mx_Field_4"></label></div><p>Voters see results as soon as they have voted</p><h2>What is your poll question or topic?</h2><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="poll-topic-input" maxlength="340" label="Question or topic" placeholder="Write something…" type="text" value="How many turnips is the optimal number?"><label for="poll-topic-input">Question or topic</label></div><h2>Create options</h2><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_0" maxlength="340" label="Option 1" placeholder="Write an option" type="text" value="As many as my neighbour"><label for="pollcreate_option_0">Option 1</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_1" maxlength="340" label="Option 2" placeholder="Write an option" type="text" value="The question is meaningless"><label for="pollcreate_option_1">Option 2</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_2" maxlength="340" label="Option 3" placeholder="Write an option" type="text" value="Mu"><label for="pollcreate_option_2">Option 3</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_addOption mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary">Add option</div></div></div><div class="mx_CompoundDialog_footer"><div role="button" tabindex="0" class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline">Cancel</div><button type="submit" role="button" tabindex="0" class="mx_AccessibleButton mx_Dialog_nonDialogButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary">Create Poll</button></div></form></div><div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div>"`;
exports[`PollCreateDialog renders a question and some options 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_CompoundDialog_content"
aria-labelledby="mx_CompoundDialog_title"
class="mx_CompoundDialog mx_ScrollableBaseDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_CompoundDialog_header"
>
<h1>
Create poll
</h1>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_CompoundDialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<form
class="mx_CompoundDialog_form"
>
<div
class="mx_CompoundDialog_content"
>
<div
class="mx_PollCreateDialog"
>
<h2>
Poll type
</h2>
<div
class="mx_Field mx_Field_select"
>
<select
id="mx_Field_4"
type="text"
>
<option
value="org.matrix.msc3381.poll.disclosed"
>
Open poll
</option>
<option
value="org.matrix.msc3381.poll.undisclosed"
>
Closed poll
</option>
</select>
<label
for="mx_Field_4"
/>
</div>
<p>
Voters see results as soon as they have voted
</p>
<h2>
What is your poll question or topic?
</h2>
<div
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
>
<input
id="poll-topic-input"
label="Question or topic"
maxlength="340"
placeholder="Write something…"
type="text"
value="How many turnips is the optimal number?"
/>
<label
for="poll-topic-input"
>
Question or topic
</label>
</div>
<h2>
Create options
</h2>
<div
class="mx_PollCreateDialog_option"
>
<div
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
>
<input
id="pollcreate_option_0"
label="Option 1"
maxlength="340"
placeholder="Write an option"
type="text"
value="As many as my neighbour"
/>
<label
for="pollcreate_option_0"
>
Option 1
</label>
</div>
<div
class="mx_AccessibleButton mx_PollCreateDialog_removeOption"
role="button"
tabindex="0"
/>
</div>
<div
class="mx_PollCreateDialog_option"
>
<div
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
>
<input
id="pollcreate_option_1"
label="Option 2"
maxlength="340"
placeholder="Write an option"
type="text"
value="The question is meaningless"
/>
<label
for="pollcreate_option_1"
>
Option 2
</label>
</div>
<div
class="mx_AccessibleButton mx_PollCreateDialog_removeOption"
role="button"
tabindex="0"
/>
</div>
<div
class="mx_PollCreateDialog_option"
>
<div
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
>
<input
id="pollcreate_option_2"
label="Option 3"
maxlength="340"
placeholder="Write an option"
type="text"
value="Mu"
/>
<label
for="pollcreate_option_2"
>
Option 3
</label>
</div>
<div
class="mx_AccessibleButton mx_PollCreateDialog_removeOption"
role="button"
tabindex="0"
/>
</div>
<div
class="mx_AccessibleButton mx_PollCreateDialog_addOption mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary"
role="button"
tabindex="0"
>
Add option
</div>
</div>
</div>
<div
class="mx_CompoundDialog_footer"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
role="button"
tabindex="0"
>
Cancel
</div>
<button
class="mx_AccessibleButton mx_Dialog_nonDialogButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
type="submit"
>
Create Poll
</button>
</div>
</form>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`PollCreateDialog renders info from a previous event 1`] = `"<div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div><div data-focus-lock-disabled="false" role="dialog" aria-labelledby="mx_CompoundDialog_title" aria-describedby="mx_CompoundDialog_content" class="mx_CompoundDialog mx_ScrollableBaseDialog"><div class="mx_CompoundDialog_header"><h1>Edit poll</h1><div aria-label="Close dialog" role="button" tabindex="0" class="mx_AccessibleButton mx_CompoundDialog_cancelButton"></div></div><form class="mx_CompoundDialog_form"><div class="mx_CompoundDialog_content"><div class="mx_PollCreateDialog"><h2>Poll type</h2><div class="mx_Field mx_Field_select"><select type="text" id="mx_Field_5"><option value="org.matrix.msc3381.poll.disclosed">Open poll</option><option value="org.matrix.msc3381.poll.undisclosed">Closed poll</option></select><label for="mx_Field_5"></label></div><p>Voters see results as soon as they have voted</p><h2>What is your poll question or topic?</h2><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="poll-topic-input" maxlength="340" label="Question or topic" placeholder="Write something…" type="text" value="Poll Q"><label for="poll-topic-input">Question or topic</label></div><h2>Create options</h2><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_0" maxlength="340" label="Option 1" placeholder="Write an option" type="text" value="Answer 1"><label for="pollcreate_option_0">Option 1</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_1" maxlength="340" label="Option 2" placeholder="Write an option" type="text" value="Answer 2"><label for="pollcreate_option_1">Option 2</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_addOption mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary">Add option</div></div></div><div class="mx_CompoundDialog_footer"><div role="button" tabindex="0" class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline">Cancel</div><button type="submit" role="button" tabindex="0" class="mx_AccessibleButton mx_Dialog_nonDialogButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary">Done</button></div></form></div><div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div>"`;
exports[`PollCreateDialog renders info from a previous event 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_CompoundDialog_content"
aria-labelledby="mx_CompoundDialog_title"
class="mx_CompoundDialog mx_ScrollableBaseDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_CompoundDialog_header"
>
<h1>
Edit poll
</h1>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_CompoundDialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<form
class="mx_CompoundDialog_form"
>
<div
class="mx_CompoundDialog_content"
>
<div
class="mx_PollCreateDialog"
>
<h2>
Poll type
</h2>
<div
class="mx_Field mx_Field_select"
>
<select
id="mx_Field_5"
type="text"
>
<option
value="org.matrix.msc3381.poll.disclosed"
>
Open poll
</option>
<option
value="org.matrix.msc3381.poll.undisclosed"
>
Closed poll
</option>
</select>
<label
for="mx_Field_5"
/>
</div>
<p>
Voters see results as soon as they have voted
</p>
<h2>
What is your poll question or topic?
</h2>
<div
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
>
<input
id="poll-topic-input"
label="Question or topic"
maxlength="340"
placeholder="Write something…"
type="text"
value="Poll Q"
/>
<label
for="poll-topic-input"
>
Question or topic
</label>
</div>
<h2>
Create options
</h2>
<div
class="mx_PollCreateDialog_option"
>
<div
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
>
<input
id="pollcreate_option_0"
label="Option 1"
maxlength="340"
placeholder="Write an option"
type="text"
value="Answer 1"
/>
<label
for="pollcreate_option_0"
>
Option 1
</label>
</div>
<div
class="mx_AccessibleButton mx_PollCreateDialog_removeOption"
role="button"
tabindex="0"
/>
</div>
<div
class="mx_PollCreateDialog_option"
>
<div
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
>
<input
id="pollcreate_option_1"
label="Option 2"
maxlength="340"
placeholder="Write an option"
type="text"
value="Answer 2"
/>
<label
for="pollcreate_option_1"
>
Option 2
</label>
</div>
<div
class="mx_AccessibleButton mx_PollCreateDialog_removeOption"
role="button"
tabindex="0"
/>
</div>
<div
class="mx_AccessibleButton mx_PollCreateDialog_addOption mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary"
role="button"
tabindex="0"
>
Add option
</div>
</div>
</div>
<div
class="mx_CompoundDialog_footer"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
role="button"
tabindex="0"
>
Cancel
</div>
<button
class="mx_AccessibleButton mx_Dialog_nonDialogButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
type="submit"
>
Done
</button>
</div>
</form>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;

View file

@ -15,9 +15,7 @@ limitations under the License.
*/
import React, { ComponentProps } from "react";
// eslint-disable-next-line deprecate/import
import { mount } from "enzyme";
import { act } from "react-dom/test-utils";
import { act, fireEvent, render } from "@testing-library/react";
import * as maplibregl from "maplibre-gl";
import { BeaconEvent, getBeaconInfoIdentifier, RelationType, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix";
import { Relations } from "matrix-js-sdk/src/models/relations";
@ -36,7 +34,6 @@ import { MediaEventHelper } from "../../../../src/utils/MediaEventHelper";
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
import Modal from "../../../../src/Modal";
import { TILE_SERVER_WK_KEY } from "../../../../src/utils/WellKnownUtils";
import { MapError } from "../../../../src/components/views/location/MapError";
import * as mapUtilHooks from "../../../../src/utils/location/useMap";
import { LocationShareError } from "../../../../src/utils/location";
@ -75,10 +72,11 @@ describe("<MBeaconBody />", () => {
};
const getComponent = (props = {}) =>
mount(<MBeaconBody {...defaultProps} {...props} />, {
wrappingComponent: MatrixClientContext.Provider,
wrappingComponentProps: { value: mockClient },
});
render(
<MatrixClientContext.Provider value={mockClient}>
<MBeaconBody {...defaultProps} {...props} />
</MatrixClientContext.Provider>,
);
const modalSpy = jest.spyOn(Modal, "createDialog").mockReturnValue({
finished: Promise.resolve([true]),
@ -94,7 +92,7 @@ describe("<MBeaconBody />", () => {
const beaconInfoEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: false }, "$alice-room1-1");
makeRoomWithStateEvents([beaconInfoEvent], { roomId, mockClient });
const component = getComponent({ mxEvent: beaconInfoEvent });
expect(component.text()).toEqual("Live location ended");
expect(component.container).toHaveTextContent("Live location ended");
});
it("renders stopped beacon UI for an expired beacon", () => {
@ -107,7 +105,7 @@ describe("<MBeaconBody />", () => {
);
makeRoomWithStateEvents([beaconInfoEvent], { roomId, mockClient });
const component = getComponent({ mxEvent: beaconInfoEvent });
expect(component.text()).toEqual("Live location ended");
expect(component.container).toHaveTextContent("Live location ended");
});
it("renders loading beacon UI for a beacon that has not started yet", () => {
@ -120,7 +118,7 @@ describe("<MBeaconBody />", () => {
);
makeRoomWithStateEvents([beaconInfoEvent], { roomId, mockClient });
const component = getComponent({ mxEvent: beaconInfoEvent });
expect(component.text()).toEqual("Loading live location…");
expect(component.container).toHaveTextContent("Loading live location…");
});
it("does not open maximised map when on click when beacon is stopped", () => {
@ -133,9 +131,7 @@ describe("<MBeaconBody />", () => {
);
makeRoomWithStateEvents([beaconInfoEvent], { roomId, mockClient });
const component = getComponent({ mxEvent: beaconInfoEvent });
act(() => {
component.find(".mx_MBeaconBody_map").at(0).simulate("click");
});
fireEvent.click(component.container.querySelector(".mx_MBeaconBody_map")!);
expect(modalSpy).not.toHaveBeenCalled();
});
@ -155,7 +151,7 @@ describe("<MBeaconBody />", () => {
const component = getComponent({ mxEvent: aliceBeaconInfo1 });
// beacon1 has been superceded by beacon2
expect(component.text()).toEqual("Live location ended");
expect(component.container).toHaveTextContent("Live location ended");
});
it("renders stopped UI when a beacon event is replaced", () => {
@ -179,10 +175,8 @@ describe("<MBeaconBody />", () => {
beaconInstance.update(aliceBeaconInfo2);
});
component.setProps({});
// beacon1 has been superceded by beacon2
expect(component.text()).toEqual("Live location ended");
expect(component.container).toHaveTextContent("Live location ended");
});
};
@ -202,10 +196,8 @@ describe("<MBeaconBody />", () => {
beaconInstance.emit(BeaconEvent.LivenessChange, false, beaconInstance);
});
component.setProps({});
// stopped UI
expect(component.text()).toEqual("Live location ended");
expect(component.container).toHaveTextContent("Live location ended");
});
});
@ -227,16 +219,14 @@ describe("<MBeaconBody />", () => {
makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient });
const component = getComponent({ mxEvent: aliceBeaconInfo });
expect(component.text()).toEqual("Loading live location…");
expect(component.container).toHaveTextContent("Loading live location…");
});
it("does nothing on click when a beacon has no location", () => {
makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient });
const component = getComponent({ mxEvent: aliceBeaconInfo });
act(() => {
component.find(".mx_MBeaconBody_map").at(0).simulate("click");
});
fireEvent.click(component.container.querySelector(".mx_MBeaconBody_map")!);
expect(modalSpy).not.toHaveBeenCalled();
});
@ -247,7 +237,7 @@ describe("<MBeaconBody />", () => {
beaconInstance.addLocations([location1]);
const component = getComponent({ mxEvent: aliceBeaconInfo });
expect(component.find("Map").length).toBeTruthy;
expect(component.container.querySelector(".maplibregl-canvas-container")).toBeDefined();
});
it("opens maximised map view on click when beacon has a live location", () => {
@ -256,9 +246,7 @@ describe("<MBeaconBody />", () => {
beaconInstance.addLocations([location1]);
const component = getComponent({ mxEvent: aliceBeaconInfo });
act(() => {
component.find("Map").simulate("click");
});
fireEvent.click(component.container.querySelector(".mx_Map")!);
// opens modal
expect(modalSpy).toHaveBeenCalled();
@ -268,44 +256,18 @@ describe("<MBeaconBody />", () => {
makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient });
const component = getComponent({ mxEvent: aliceBeaconInfo });
act(() => {
component.find(".mx_MBeaconBody_map").at(0).simulate("click");
});
fireEvent.click(component.container.querySelector(".mx_MBeaconBody_map")!);
expect(modalSpy).not.toHaveBeenCalled();
});
it("renders a live beacon with a location correctly", () => {
const room = makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient });
const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo))!;
beaconInstance.addLocations([location1]);
const component = getComponent({ mxEvent: aliceBeaconInfo });
expect(component.find("Map").length).toBeTruthy;
});
it("opens maximised map view on click when beacon has a live location", () => {
const room = makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient });
const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo))!;
beaconInstance.addLocations([location1]);
const component = getComponent({ mxEvent: aliceBeaconInfo });
act(() => {
component.find("Map").simulate("click");
});
// opens modal
expect(modalSpy).toHaveBeenCalled();
});
it("updates latest location", () => {
const room = makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient });
const component = getComponent({ mxEvent: aliceBeaconInfo });
getComponent({ mxEvent: aliceBeaconInfo });
const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo))!;
act(() => {
beaconInstance.addLocations([location1]);
component.setProps({});
});
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 51, lon: 41 });
@ -313,7 +275,6 @@ describe("<MBeaconBody />", () => {
act(() => {
beaconInstance.addLocations([location2]);
component.setProps({});
});
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 52, lon: 42 });
@ -455,7 +416,7 @@ describe("<MBeaconBody />", () => {
makeRoomWithBeacons(roomId, mockClient, [beaconInfoEvent], [location1]);
const component = getComponent({ mxEvent: beaconInfoEvent });
expect(component.find(MapError)).toMatchSnapshot();
expect(component.getByTestId("map-rendering-error")).toMatchSnapshot();
});
// test that statuses display as expected with a map display error

View file

@ -21,6 +21,7 @@ import { LocationAssetType } from "matrix-js-sdk/src/@types/location";
import { ClientEvent, RoomMember } from "matrix-js-sdk/src/matrix";
import * as maplibregl from "maplibre-gl";
import { logger } from "matrix-js-sdk/src/logger";
// eslint-disable-next-line deprecate/import
import { act } from "react-dom/test-utils";
import { SyncState } from "matrix-js-sdk/src/sync";

View file

@ -1,35 +1,22 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<MBeaconBody /> when map display is not configured renders maps unavailable error for a live beacon with location 1`] = `
<MapError
className="mx_MBeaconBody_mapError mx_MBeaconBody_mapErrorInteractive"
error="MapStyleUrlNotConfigured"
isMinimised={true}
onClick={[Function]}
<div
class="mx_MapError mx_MBeaconBody_mapError mx_MBeaconBody_mapErrorInteractive mx_MapError_isMinimised"
data-testid="map-rendering-error"
>
<div
className="mx_MapError mx_MBeaconBody_mapError mx_MBeaconBody_mapErrorInteractive mx_MapError_isMinimised"
data-testid="map-rendering-error"
onClick={[Function]}
class="mx_MapError_icon"
/>
<h3
class="mx_Heading_h3 mx_MapError_heading"
>
<div
className="mx_MapError_icon"
/>
<Heading
className="mx_MapError_heading"
size="h3"
>
<h3
className="mx_Heading_h3 mx_MapError_heading"
>
Unable to load map
</h3>
</Heading>
<p
className="mx_MapError_message"
>
This homeserver is not configured to display maps.
</p>
</div>
</MapError>
Unable to load map
</h3>
<p
class="mx_MapError_message"
>
This homeserver is not configured to display maps.
</p>
</div>
`;

View file

@ -16,6 +16,7 @@ limitations under the License.
*/
import React, { Component } from "react";
// eslint-disable-next-line deprecate/import
import ReactTestUtils from "react-dom/test-utils";
import ReactDOM from "react-dom";
import { Room } from "matrix-js-sdk/src/models/room";

View file

@ -15,9 +15,7 @@ limitations under the License.
*/
import React from "react";
// eslint-disable-next-line deprecate/import
import { mount, ReactWrapper } from "enzyme";
import { render, screen, act, fireEvent, waitFor, getByRole } from "@testing-library/react";
import { render, screen, act, fireEvent, waitFor, getByRole, RenderResult } from "@testing-library/react";
import { mocked, Mocked } from "jest-mock";
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
import { Room } from "matrix-js-sdk/src/models/room";
@ -60,291 +58,7 @@ import WidgetUtils from "../../../../src/utils/WidgetUtils";
import { ElementWidgetActions } from "../../../../src/stores/widgets/ElementWidgetActions";
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../../src/MediaDeviceHandler";
describe("RoomHeader (Enzyme)", () => {
it("shows the room avatar in a room with only ourselves", () => {
// When we render a non-DM room with 1 person in it
const room = createRoom({ name: "X Room", isDm: false, userIds: [] });
const rendered = mountHeader(room);
// Then the room's avatar is the initial of its name
const initial = findSpan(rendered, ".mx_BaseAvatar_initial");
expect(initial.text()).toEqual("X");
// And there is no image avatar (because it's not set on this room)
const image = findImg(rendered, ".mx_BaseAvatar_image");
expect(image.prop("src")).toEqual("data:image/png;base64,00");
});
it("shows the room avatar in a room with 2 people", () => {
// When we render a non-DM room with 2 people in it
const room = createRoom({ name: "Y Room", isDm: false, userIds: ["other"] });
const rendered = mountHeader(room);
// Then the room's avatar is the initial of its name
const initial = findSpan(rendered, ".mx_BaseAvatar_initial");
expect(initial.text()).toEqual("Y");
// And there is no image avatar (because it's not set on this room)
const image = findImg(rendered, ".mx_BaseAvatar_image");
expect(image.prop("src")).toEqual("data:image/png;base64,00");
});
it("shows the room avatar in a room with >2 people", () => {
// When we render a non-DM room with 3 people in it
const room = createRoom({ name: "Z Room", isDm: false, userIds: ["other1", "other2"] });
const rendered = mountHeader(room);
// Then the room's avatar is the initial of its name
const initial = findSpan(rendered, ".mx_BaseAvatar_initial");
expect(initial.text()).toEqual("Z");
// And there is no image avatar (because it's not set on this room)
const image = findImg(rendered, ".mx_BaseAvatar_image");
expect(image.prop("src")).toEqual("data:image/png;base64,00");
});
it("shows the room avatar in a DM with only ourselves", () => {
// When we render a non-DM room with 1 person in it
const room = createRoom({ name: "Z Room", isDm: true, userIds: [] });
const rendered = mountHeader(room);
// Then the room's avatar is the initial of its name
const initial = findSpan(rendered, ".mx_BaseAvatar_initial");
expect(initial.text()).toEqual("Z");
// And there is no image avatar (because it's not set on this room)
const image = findImg(rendered, ".mx_BaseAvatar_image");
expect(image.prop("src")).toEqual("data:image/png;base64,00");
});
it("shows the user avatar in a DM with 2 people", () => {
// Note: this is the interesting case - this is the ONLY
// time we should use the user's avatar.
// When we render a DM room with only 2 people in it
const room = createRoom({ name: "Y Room", isDm: true, userIds: ["other"] });
const rendered = mountHeader(room);
// Then we use the other user's avatar as our room's image avatar
const image = findImg(rendered, ".mx_BaseAvatar_image");
expect(image.prop("src")).toEqual("http://this.is.a.url/example.org/other");
// And there is no initial avatar
expect(rendered.find(".mx_BaseAvatar_initial")).toHaveLength(0);
});
it("shows the room avatar in a DM with >2 people", () => {
// When we render a DM room with 3 people in it
const room = createRoom({
name: "Z Room",
isDm: true,
userIds: ["other1", "other2"],
});
const rendered = mountHeader(room);
// Then the room's avatar is the initial of its name
const initial = findSpan(rendered, ".mx_BaseAvatar_initial");
expect(initial.text()).toEqual("Z");
// And there is no image avatar (because it's not set on this room)
const image = findImg(rendered, ".mx_BaseAvatar_image");
expect(image.prop("src")).toEqual("data:image/png;base64,00");
});
it("renders call buttons normally", () => {
const room = createRoom({ name: "Room", isDm: false, userIds: ["other"] });
const wrapper = mountHeader(room);
expect(wrapper.find('[aria-label="Voice call"]').hostNodes()).toHaveLength(1);
expect(wrapper.find('[aria-label="Video call"]').hostNodes()).toHaveLength(1);
});
it("hides call buttons when the room is tombstoned", () => {
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
const wrapper = mountHeader(
room,
{},
{
tombstone: mkEvent({
event: true,
type: "m.room.tombstone",
room: room.roomId,
user: "@user1:server",
skey: "",
content: {},
ts: Date.now(),
}),
},
);
expect(wrapper.find('[aria-label="Voice call"]').hostNodes()).toHaveLength(0);
expect(wrapper.find('[aria-label="Video call"]').hostNodes()).toHaveLength(0);
});
it("should render buttons if not passing showButtons (default true)", () => {
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
const wrapper = mountHeader(room);
expect(wrapper.find(".mx_RoomHeader_button")).not.toHaveLength(0);
});
it("should not render buttons if passing showButtons = false", () => {
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
const wrapper = mountHeader(room, { showButtons: false });
expect(wrapper.find(".mx_RoomHeader_button")).toHaveLength(0);
});
it("should render the room options context menu if not passing enableRoomOptionsMenu (default true)", () => {
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
const wrapper = mountHeader(room);
expect(wrapper.find(".mx_RoomHeader_name.mx_AccessibleButton")).toHaveLength(1);
});
it("should not render the room options context menu if passing enableRoomOptionsMenu = false", () => {
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
const wrapper = mountHeader(room, { enableRoomOptionsMenu: false });
expect(wrapper.find(".mx_RoomHeader_name.mx_AccessibleButton")).toHaveLength(0);
});
});
interface IRoomCreationInfo {
name: string;
isDm: boolean;
userIds: string[];
}
function createRoom(info: IRoomCreationInfo) {
stubClient();
const client: MatrixClient = MatrixClientPeg.get();
const roomId = "!1234567890:domain";
const userId = client.getUserId()!;
if (info.isDm) {
client.getAccountData = (eventType) => {
expect(eventType).toEqual("m.direct");
return mkDirectEvent(roomId, userId, info.userIds);
};
}
DMRoomMap.makeShared().start();
const room = new Room(roomId, client, userId, {
pendingEventOrdering: PendingEventOrdering.Detached,
});
const otherJoinEvents: MatrixEvent[] = [];
for (const otherUserId of info.userIds) {
otherJoinEvents.push(mkJoinEvent(roomId, otherUserId));
}
room.currentState.setStateEvents([
mkCreationEvent(roomId, userId),
mkNameEvent(roomId, userId, info.name),
mkJoinEvent(roomId, userId),
...otherJoinEvents,
]);
room.recalculate();
return room;
}
function mountHeader(room: Room, propsOverride = {}, roomContext?: Partial<IRoomState>): ReactWrapper {
const props: RoomHeaderProps = {
room,
inRoom: true,
onSearchClick: () => {},
onInviteClick: null,
onForgetClick: () => {},
onAppsClick: () => {},
e2eStatus: E2EStatus.Normal,
appsShown: true,
searchInfo: {
searchId: Math.random(),
promise: new Promise<ISearchResults>(() => {}),
term: "",
scope: SearchScope.Room,
count: 0,
},
viewingCall: false,
activeCall: null,
...propsOverride,
};
return mount(
<RoomContext.Provider value={{ ...roomContext, room } as IRoomState}>
<RoomHeader {...props} />
</RoomContext.Provider>,
);
}
function mkCreationEvent(roomId: string, userId: string): MatrixEvent {
return mkEvent({
event: true,
type: "m.room.create",
room: roomId,
user: userId,
content: {
creator: userId,
room_version: "5",
predecessor: {
room_id: "!prevroom",
event_id: "$someevent",
},
},
});
}
function mkNameEvent(roomId: string, userId: string, name: string): MatrixEvent {
return mkEvent({
event: true,
type: "m.room.name",
room: roomId,
user: userId,
content: { name },
});
}
function mkJoinEvent(roomId: string, userId: string) {
const ret = mkEvent({
event: true,
type: "m.room.member",
room: roomId,
user: userId,
content: {
membership: "join",
avatar_url: "mxc://example.org/" + userId,
},
});
ret.event.state_key = userId;
return ret;
}
function mkDirectEvent(roomId: string, userId: string, otherUsers: string[]): MatrixEvent {
const content: Record<string, string[]> = {};
for (const otherUserId of otherUsers) {
content[otherUserId] = [roomId];
}
return mkEvent({
event: true,
type: "m.direct",
room: roomId,
user: userId,
content,
});
}
function findSpan(wrapper: ReactWrapper, selector: string): ReactWrapper {
const els = wrapper.find(selector).hostNodes();
expect(els).toHaveLength(1);
return els.at(0);
}
function findImg(wrapper: ReactWrapper, selector: string): ReactWrapper {
const els = wrapper.find(selector).hostNodes();
expect(els).toHaveLength(1);
return els.at(0);
}
describe("RoomHeader (React Testing Library)", () => {
describe("RoomHeader", () => {
let client: Mocked<MatrixClient>;
let room: Room;
let alice: RoomMember;
@ -397,6 +111,9 @@ describe("RoomHeader (React Testing Library)", () => {
[MediaDeviceKindEnum.VideoInput]: [],
[MediaDeviceKindEnum.AudioOutput]: [],
});
DMRoomMap.makeShared();
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(carol.userId);
});
afterEach(async () => {
@ -879,4 +596,274 @@ describe("RoomHeader (React Testing Library)", () => {
expect(screen.queryByRole("button", { name: /invite/i })).toBeNull();
});
it("shows the room avatar in a room with only ourselves", () => {
// When we render a non-DM room with 1 person in it
const room = createRoom({ name: "X Room", isDm: false, userIds: [] });
const rendered = mountHeader(room);
// Then the room's avatar is the initial of its name
const initial = rendered.container.querySelector(".mx_BaseAvatar_initial");
expect(initial).toHaveTextContent("X");
// And there is no image avatar (because it's not set on this room)
const image = rendered.container.querySelector(".mx_BaseAvatar_image");
expect(image).toHaveAttribute("src", "data:image/png;base64,00");
});
it("shows the room avatar in a room with 2 people", () => {
// When we render a non-DM room with 2 people in it
const room = createRoom({ name: "Y Room", isDm: false, userIds: ["other"] });
const rendered = mountHeader(room);
// Then the room's avatar is the initial of its name
const initial = rendered.container.querySelector(".mx_BaseAvatar_initial");
expect(initial).toHaveTextContent("Y");
// And there is no image avatar (because it's not set on this room)
const image = rendered.container.querySelector(".mx_BaseAvatar_image");
expect(image).toHaveAttribute("src", "data:image/png;base64,00");
});
it("shows the room avatar in a room with >2 people", () => {
// When we render a non-DM room with 3 people in it
const room = createRoom({ name: "Z Room", isDm: false, userIds: ["other1", "other2"] });
const rendered = mountHeader(room);
// Then the room's avatar is the initial of its name
const initial = rendered.container.querySelector(".mx_BaseAvatar_initial");
expect(initial).toHaveTextContent("Z");
// And there is no image avatar (because it's not set on this room)
const image = rendered.container.querySelector(".mx_BaseAvatar_image");
expect(image).toHaveAttribute("src", "data:image/png;base64,00");
});
it("shows the room avatar in a DM with only ourselves", () => {
// When we render a non-DM room with 1 person in it
const room = createRoom({ name: "Z Room", isDm: true, userIds: [] });
const rendered = mountHeader(room);
// Then the room's avatar is the initial of its name
const initial = rendered.container.querySelector(".mx_BaseAvatar_initial");
expect(initial).toHaveTextContent("Z");
// And there is no image avatar (because it's not set on this room)
const image = rendered.container.querySelector(".mx_BaseAvatar_image");
expect(image).toHaveAttribute("src", "data:image/png;base64,00");
});
it("shows the user avatar in a DM with 2 people", () => {
// Note: this is the interesting case - this is the ONLY
// time we should use the user's avatar.
// When we render a DM room with only 2 people in it
const room = createRoom({ name: "Y Room", isDm: true, userIds: ["other"] });
const rendered = mountHeader(room);
// Then we use the other user's avatar as our room's image avatar
const image = rendered.container.querySelector(".mx_BaseAvatar_image");
expect(image).toHaveAttribute("src", "http://this.is.a.url/example.org/other");
// And there is no initial avatar
expect(rendered.container.querySelector(".mx_BaseAvatar_initial")).toBeFalsy();
});
it("shows the room avatar in a DM with >2 people", () => {
// When we render a DM room with 3 people in it
const room = createRoom({
name: "Z Room",
isDm: true,
userIds: ["other1", "other2"],
});
const rendered = mountHeader(room);
// Then the room's avatar is the initial of its name
const initial = rendered.container.querySelector(".mx_BaseAvatar_initial");
expect(initial).toHaveTextContent("Z");
// And there is no image avatar (because it's not set on this room)
const image = rendered.container.querySelector(".mx_BaseAvatar_image");
expect(image).toHaveAttribute("src", "data:image/png;base64,00");
});
it("renders call buttons normally", () => {
const room = createRoom({ name: "Room", isDm: false, userIds: ["other"] });
const wrapper = mountHeader(room);
expect(wrapper.container.querySelector('[aria-label="Voice call"]')).toBeDefined();
expect(wrapper.container.querySelector('[aria-label="Video call"]')).toBeDefined();
});
it("hides call buttons when the room is tombstoned", () => {
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
const wrapper = mountHeader(
room,
{},
{
tombstone: mkEvent({
event: true,
type: "m.room.tombstone",
room: room.roomId,
user: "@user1:server",
skey: "",
content: {},
ts: Date.now(),
}),
},
);
expect(wrapper.container.querySelector('[aria-label="Voice call"]')).toBeFalsy();
expect(wrapper.container.querySelector('[aria-label="Video call"]')).toBeFalsy();
});
it("should render buttons if not passing showButtons (default true)", () => {
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
const wrapper = mountHeader(room);
expect(wrapper.container.querySelector(".mx_RoomHeader_button")).toBeDefined();
});
it("should not render buttons if passing showButtons = false", () => {
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
const wrapper = mountHeader(room, { showButtons: false });
expect(wrapper.container.querySelector(".mx_RoomHeader_button")).toBeFalsy();
});
it("should render the room options context menu if not passing enableRoomOptionsMenu (default true)", () => {
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
const wrapper = mountHeader(room);
expect(wrapper.container.querySelector(".mx_RoomHeader_name.mx_AccessibleButton")).toBeDefined();
});
it("should not render the room options context menu if passing enableRoomOptionsMenu = false", () => {
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
const wrapper = mountHeader(room, { enableRoomOptionsMenu: false });
expect(wrapper.container.querySelector(".mx_RoomHeader_name.mx_AccessibleButton")).toBeFalsy();
});
});
interface IRoomCreationInfo {
name: string;
isDm: boolean;
userIds: string[];
}
function createRoom(info: IRoomCreationInfo) {
stubClient();
const client: MatrixClient = MatrixClientPeg.get();
const roomId = "!1234567890:domain";
const userId = client.getUserId()!;
if (info.isDm) {
client.getAccountData = (eventType) => {
expect(eventType).toEqual("m.direct");
return mkDirectEvent(roomId, userId, info.userIds);
};
}
DMRoomMap.makeShared().start();
const room = new Room(roomId, client, userId, {
pendingEventOrdering: PendingEventOrdering.Detached,
});
const otherJoinEvents: MatrixEvent[] = [];
for (const otherUserId of info.userIds) {
otherJoinEvents.push(mkJoinEvent(roomId, otherUserId));
}
room.currentState.setStateEvents([
mkCreationEvent(roomId, userId),
mkNameEvent(roomId, userId, info.name),
mkJoinEvent(roomId, userId),
...otherJoinEvents,
]);
room.recalculate();
return room;
}
function mountHeader(room: Room, propsOverride = {}, roomContext?: Partial<IRoomState>): RenderResult {
const props: RoomHeaderProps = {
room,
inRoom: true,
onSearchClick: () => {},
onInviteClick: null,
onForgetClick: () => {},
onAppsClick: () => {},
e2eStatus: E2EStatus.Normal,
appsShown: true,
searchInfo: {
searchId: Math.random(),
promise: new Promise<ISearchResults>(() => {}),
term: "",
scope: SearchScope.Room,
count: 0,
},
viewingCall: false,
activeCall: null,
...propsOverride,
};
return render(
<RoomContext.Provider value={{ ...roomContext, room } as IRoomState}>
<RoomHeader {...props} />
</RoomContext.Provider>,
);
}
function mkCreationEvent(roomId: string, userId: string): MatrixEvent {
return mkEvent({
event: true,
type: "m.room.create",
room: roomId,
user: userId,
content: {
creator: userId,
room_version: "5",
predecessor: {
room_id: "!prevroom",
event_id: "$someevent",
},
},
});
}
function mkNameEvent(roomId: string, userId: string, name: string): MatrixEvent {
return mkEvent({
event: true,
type: "m.room.name",
room: roomId,
user: userId,
content: { name },
});
}
function mkJoinEvent(roomId: string, userId: string) {
const ret = mkEvent({
event: true,
type: "m.room.member",
room: roomId,
user: userId,
content: {
membership: "join",
avatar_url: "mxc://example.org/" + userId,
},
});
ret.event.state_key = userId;
return ret;
}
function mkDirectEvent(roomId: string, userId: string, otherUsers: string[]): MatrixEvent {
const content: Record<string, string[]> = {};
for (const otherUserId of otherUsers) {
content[otherUserId] = [roomId];
}
return mkEvent({
event: true,
type: "m.direct",
room: roomId,
user: userId,
content,
});
}

View file

@ -14,26 +14,36 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
// eslint-disable-next-line deprecate/import
import { mount, ReactWrapper } from "enzyme";
import React, { createRef, RefObject } from "react";
import { render } from "@testing-library/react";
import { MatrixClient, MsgType, Room } from "matrix-js-sdk/src/matrix";
import { mocked } from "jest-mock";
import VoiceRecordComposerTile from "../../../../src/components/views/rooms/VoiceRecordComposerTile";
import { VoiceRecording } from "../../../../src/audio/VoiceRecording";
import { doMaybeLocalRoomAction } from "../../../../src/utils/local-room";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { IUpload } from "../../../../src/audio/VoiceMessageRecording";
import { IUpload, VoiceMessageRecording } from "../../../../src/audio/VoiceMessageRecording";
import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks";
import { VoiceRecordingStore } from "../../../../src/stores/VoiceRecordingStore";
import { PlaybackClock } from "../../../../src/audio/PlaybackClock";
jest.mock("../../../../src/utils/local-room", () => ({
doMaybeLocalRoomAction: jest.fn(),
}));
jest.mock("../../../../src/stores/VoiceRecordingStore", () => ({
VoiceRecordingStore: {
getVoiceRecordingId: jest.fn().mockReturnValue("voice-recording-id"),
instance: {
getActiveRecording: jest.fn(),
disposeRecording: jest.fn(),
},
},
}));
describe("<VoiceRecordComposerTile/>", () => {
let voiceRecordComposerTile: ReactWrapper<VoiceRecordComposerTile>;
let mockRecorder: VoiceRecording;
let voiceRecordComposerTile: RefObject<VoiceRecordComposerTile>;
let mockRecorder: VoiceMessageRecording;
let mockUpload: IUpload;
let mockClient: MatrixClient;
const roomId = "!room:example.com";
@ -47,26 +57,42 @@ describe("<VoiceRecordComposerTile/>", () => {
const room = {
roomId,
} as unknown as Room;
voiceRecordComposerTile = createRef();
const props = {
room,
ref: voiceRecordComposerTile,
permalinkCreator: new RoomPermalinkCreator(room),
};
mockUpload = {
mxc: "mxc://example.com/voice",
};
mockRecorder = {
on: jest.fn(),
off: jest.fn(),
stop: jest.fn(),
upload: () => Promise.resolve(mockUpload),
durationSeconds: 1337,
contentType: "audio/ogg",
getPlayback: () => ({
on: jest.fn(),
off: jest.fn(),
prepare: jest.fn().mockResolvedValue(void 0),
clockInfo: {
timeSeconds: 0,
liveData: {
onUpdate: jest.fn(),
},
} as unknown as PlaybackClock,
waveform: [1.4, 2.5, 3.6],
waveformData: {
onUpdate: jest.fn(),
},
thumbnailWaveform: [1.4, 2.5, 3.6],
}),
} as unknown as VoiceRecording;
voiceRecordComposerTile = mount(<VoiceRecordComposerTile {...props} />);
voiceRecordComposerTile.setState({
recorder: mockRecorder,
});
} as unknown as VoiceMessageRecording;
mocked(VoiceRecordingStore.instance.getActiveRecording).mockReturnValue(mockRecorder);
render(<VoiceRecordComposerTile {...props} />);
mocked(doMaybeLocalRoomAction).mockImplementation(
<T,>(roomId: string, fn: (actualRoomId: string) => Promise<T>, _client?: MatrixClient) => {
@ -77,7 +103,7 @@ describe("<VoiceRecordComposerTile/>", () => {
describe("send", () => {
it("should send the voice recording", async () => {
await (voiceRecordComposerTile.instance() as VoiceRecordComposerTile).send();
await voiceRecordComposerTile.current!.send();
expect(mockClient.sendMessage).toHaveBeenCalledWith(roomId, {
"body": "Voice message",
"file": undefined,

View file

@ -16,8 +16,7 @@ limitations under the License.
import React from "react";
import { mocked } from "jest-mock";
import { act, Simulate } from "react-dom/test-utils";
import { fireEvent, render, RenderResult } from "@testing-library/react";
import { act, fireEvent, render, RenderResult } from "@testing-library/react";
import { EventType, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import { GuestAccess, HistoryVisibility, JoinRule } from "matrix-js-sdk/src/@types/partials";
@ -163,8 +162,8 @@ describe("<SpaceSettingsVisibilityTab />", () => {
(mockMatrixClient.sendStateEvent as jest.Mock).mockRejectedValue({});
const component = getComponent({ space });
await toggleGuestAccessSection(component);
await act(async () => {
Simulate.click(getGuestAccessToggle(component)!);
await act(() => {
fireEvent.click(getGuestAccessToggle(component)!);
});
expect(getErrorMessage(component)).toEqual("Failed to update the guest access of this space");
@ -213,14 +212,14 @@ describe("<SpaceSettingsVisibilityTab />", () => {
(mockMatrixClient.sendStateEvent as jest.Mock).mockRejectedValue({});
const component = getComponent({ space });
await act(async () => {
Simulate.click(getHistoryVisibilityToggle(component)!);
await act(() => {
fireEvent.click(getHistoryVisibilityToggle(component)!);
});
expect(getErrorMessage(component)).toEqual("Failed to update the history visibility of this space");
});
it("disables room preview toggle when history visability changes are not allowed", () => {
it("disables room preview toggle when history visibility changes are not allowed", () => {
const space = makeMockSpace(mockMatrixClient, joinRule, guestRule, historyRule);
(space.currentState.maySendStateEvent as jest.Mock).mockReturnValue(false);
const component = getComponent({ space });

View file

@ -14,10 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// eslint-disable-next-line deprecate/import
import { ReactWrapper } from "enzyme";
import { act } from "react-dom/test-utils";
import { act as actRTL, fireEvent, RenderResult } from "@testing-library/react";
import { act, fireEvent, RenderResult } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
export const addTextToComposer = (container: HTMLElement, text: string) =>
@ -34,23 +31,8 @@ export const addTextToComposer = (container: HTMLElement, text: string) =>
fireEvent.paste(container.querySelector('[role="textbox"]')!, pasteEvent);
});
export const addTextToComposerEnzyme = (wrapper: ReactWrapper, text: string) =>
act(() => {
// couldn't get input event on contenteditable to work
// paste works without illegal private method access
const pasteEvent: Partial<ClipboardEvent> = {
clipboardData: {
types: [],
files: [],
getData: (type: string) => (type === "text/plain" ? text : undefined),
} as unknown as DataTransfer,
};
wrapper.find('[role="textbox"]').simulate("paste", pasteEvent);
wrapper.update();
});
export const addTextToComposerRTL = async (renderResult: RenderResult, text: string): Promise<void> => {
await actRTL(async () => {
await act(async () => {
await userEvent.click(renderResult.getByLabelText("Send a message…"));
await userEvent.keyboard(text);
});

View file

@ -96,7 +96,7 @@ export function createTestClient(): MatrixClient {
getUserId: jest.fn().mockReturnValue("@userId:matrix.org"),
getSafeUserId: jest.fn().mockReturnValue("@userId:matrix.org"),
getUserIdLocalpart: jest.fn().mockResolvedValue("userId"),
getUser: jest.fn().mockReturnValue({ on: jest.fn() }),
getUser: jest.fn().mockReturnValue({ on: jest.fn(), off: jest.fn() }),
getDevice: jest.fn(),
getDeviceId: jest.fn().mockReturnValue("ABCDEFGHI"),
getStoredCrossSigningForUser: jest.fn(),
@ -455,6 +455,7 @@ export function mkRoomMember(roomId: string, userId: string, membership = "join"
getAvatarUrl: () => {},
getMxcAvatarUrl: () => {},
getDMInviter: () => {},
off: () => {},
} as unknown as RoomMember;
}

View file

@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// eslint-disable-next-line deprecate/import
import { ReactWrapper } from "enzyme";
import EventEmitter from "events";
import { ActionPayload } from "../../src/dispatcher/payloads";
@ -128,17 +126,6 @@ export function untilEmission(
});
}
export const findByAttr = (attr: string) => (component: ReactWrapper, value: string) =>
component.find(`[${attr}="${value}"]`);
export const findByTestId = findByAttr("data-test-id");
export const findById = findByAttr("id");
export const findByAriaLabel = findByAttr("aria-label");
const findByTagAndAttr = (attr: string) => (component: ReactWrapper, value: string, tag: string) =>
component.find(`${tag}[${attr}="${value}"]`);
export const findByTagAndTestId = findByTagAndAttr("data-test-id");
export const flushPromises = async () => await new Promise((resolve) => window.setTimeout(resolve));
// with jest's modern fake timers process.nextTick is also mocked,

View file

@ -30,7 +30,7 @@ describe("DMRoomMap", () => {
"user@example.com": [roomId1, roomId2],
"@user:example.com": [roomId1, roomId3, roomId4],
"@user2:example.com": [] as string[],
} satisfies IContent;
} as IContent;
let client: Mocked<MatrixClient>;
let dmRoomMap: DMRoomMap;

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { renderToString } from "react-dom/server";
import { render } from "@testing-library/react";
import {
IContent,
MatrixClient,
@ -227,6 +227,10 @@ describe("export", function () {
return matrixEvents;
}
function renderToString(elem: JSX.Element): string {
return render(elem).container.outerHTML;
}
it("checks if the export format is valid", function () {
function isValidFormat(format: string): boolean {
const options: string[] = Object.values(ExportFormat);
@ -255,7 +259,7 @@ describe("export", function () {
},
setProgressText,
);
const imageRegex = /<img.+ src="mxc:\/\/test.org" alt="image.png"\/>/;
const imageRegex = /<img.+ src="mxc:\/\/test.org" alt="image\.png"\/?>/;
expect(imageRegex.test(renderToString(exporter.getEventTile(mkImageEvent(), true)))).toBeTruthy();
});