Replace react-dom tests with react testing-library tests (#10260)

This commit is contained in:
Michael Telatynski 2023-03-01 15:59:27 +00:00 committed by GitHub
parent 5398db21ad
commit e5291c195d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 132 additions and 184 deletions

View file

@ -68,8 +68,7 @@ interface IState {
}
export default class MemberList extends React.Component<IProps, IState> {
// XXX: exported for tests
public showPresence = true;
private readonly showPresence: boolean;
private mounted = false;
public static contextType = SDKContext;
@ -260,32 +259,6 @@ export default class MemberList extends React.Component<IProps, IState> {
});
};
/**
* SHOULD ONLY BE USED BY TESTS
*/
public memberString(member: RoomMember): string {
if (!member) {
return "(null)";
} else {
const u = member.user;
return (
"(" +
member.name +
", " +
member.powerLevel +
", " +
(u ? u.lastActiveAgo : "<null>") +
", " +
(u ? u.getLastActiveTs() : "<null>") +
", " +
(u ? u.currentlyActive : "<null>") +
", " +
(u ? u.presence : "<null>") +
")"
);
}
}
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>, snapshot?: any): void {
if (prevProps.searchQuery !== this.props.searchQuery) {
this.updateListNow(false);

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import React from "react";
import { act, renderIntoDocument, Simulate } from "react-dom/test-utils";
import { fireEvent, render } from "@testing-library/react";
import { Alignment } from "../../../../src/components/views/elements/Tooltip";
import TooltipTarget from "../../../../src/components/views/elements/TooltipTarget";
@ -28,27 +28,19 @@ describe("<TooltipTarget />", () => {
"label": "test label",
"alignment": Alignment.Left,
"id": "test id",
"data-test-id": "test",
"data-testid": "test",
};
afterEach(() => {
// clean up renderer tooltips
const wrapper = document.querySelector(".mx_Tooltip_wrapper");
while (wrapper?.firstChild) {
wrapper.removeChild(wrapper.lastChild!);
}
});
const getComponent = (props = {}) => {
const wrapper = renderIntoDocument<HTMLSpanElement>(
const wrapper = render(
// wrap in element so renderIntoDocument can render functional component
<span>
<TooltipTarget {...defaultProps} {...props}>
<span>child</span>
</TooltipTarget>
</span>,
) as HTMLSpanElement;
return wrapper.querySelector("[data-test-id=test]");
);
return wrapper.getByTestId("test");
};
const getVisibleTooltip = () => document.querySelector(".mx_Tooltip.mx_Tooltip_visible");
@ -62,41 +54,29 @@ describe("<TooltipTarget />", () => {
const alignmentKeys = Object.keys(Alignment).filter((o: any) => isNaN(o));
it.each(alignmentKeys)("displays %s aligned tooltip on mouseover", async (alignment: any) => {
const wrapper = getComponent({ alignment: Alignment[alignment] })!;
act(() => {
Simulate.mouseOver(wrapper);
});
fireEvent.mouseOver(wrapper);
expect(getVisibleTooltip()).toMatchSnapshot();
});
it("hides tooltip on mouseleave", () => {
const wrapper = getComponent()!;
act(() => {
Simulate.mouseOver(wrapper);
});
fireEvent.mouseOver(wrapper);
expect(getVisibleTooltip()).toBeTruthy();
act(() => {
Simulate.mouseLeave(wrapper);
});
fireEvent.mouseLeave(wrapper);
expect(getVisibleTooltip()).toBeFalsy();
});
it("displays tooltip on focus", () => {
const wrapper = getComponent()!;
act(() => {
Simulate.focus(wrapper);
});
fireEvent.focus(wrapper);
expect(getVisibleTooltip()).toBeTruthy();
});
it("hides tooltip on blur", async () => {
const wrapper = getComponent()!;
act(() => {
Simulate.focus(wrapper);
});
fireEvent.focus(wrapper);
expect(getVisibleTooltip()).toBeTruthy();
act(() => {
Simulate.blur(wrapper);
});
fireEvent.blur(wrapper);
expect(getVisibleTooltip()).toBeFalsy();
});
});

View file

@ -95,7 +95,7 @@ exports[`<TooltipTarget /> renders container 1`] = `
<div
aria-describedby="test id"
class="test tooltipTargetClassName"
data-test-id="test"
data-testid="test"
tabindex="0"
>
<span>

View file

@ -15,10 +15,8 @@ See the License for the specific language governing permissions and
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 React from "react";
import { act, render, RenderResult } from "@testing-library/react";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { User } from "matrix-js-sdk/src/models/user";
@ -28,7 +26,6 @@ import { MatrixClient, RoomState } from "matrix-js-sdk/src/matrix";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import * as TestUtils from "../../../test-utils";
import MemberList from "../../../../src/components/views/rooms/MemberList";
import MemberTile from "../../../../src/components/views/rooms/MemberTile";
import { SDKContext } from "../../../../src/contexts/SDKContext";
import { TestSdkContext } from "../../../TestSdkContext";
@ -45,9 +42,8 @@ describe("MemberList", () => {
return room;
}
let parentDiv: HTMLDivElement;
let client: MatrixClient;
let root: Component;
let root: RenderResult;
let memberListRoom: Room;
let memberList: MemberList;
@ -55,14 +51,101 @@ describe("MemberList", () => {
let moderatorUsers: RoomMember[] = [];
let defaultUsers: RoomMember[] = [];
function memberString(member: RoomMember): string {
if (!member) {
return "(null)";
} else {
const u = member.user;
return (
"(" +
member.name +
", " +
member.powerLevel +
", " +
(u ? u.lastActiveAgo : "<null>") +
", " +
(u ? u.getLastActiveTs() : "<null>") +
", " +
(u ? u.currentlyActive : "<null>") +
", " +
(u ? u.presence : "<null>") +
")"
);
}
}
function expectOrderedByPresenceAndPowerLevel(memberTiles: NodeListOf<Element>, isPresenceEnabled: boolean) {
let prevMember: RoomMember | undefined;
for (const tile of memberTiles) {
const memberA = prevMember;
const memberB = memberListRoom.currentState.members[tile.getAttribute("title")!.split(" ")[0]];
prevMember = memberB; // just in case an expect fails, set this early
if (!memberA) {
continue;
}
console.log("COMPARING A VS B:", memberString(memberA), memberString(memberB));
const userA = memberA.user!;
const userB = memberB.user!;
let groupChange = false;
if (isPresenceEnabled) {
const convertPresence = (p: string) => (p === "unavailable" ? "online" : p);
const presenceIndex = (p: string) => {
const order = ["active", "online", "offline"];
const idx = order.indexOf(convertPresence(p));
return idx === -1 ? order.length : idx; // unknown states at the end
};
const idxA = presenceIndex(userA.currentlyActive ? "active" : userA.presence);
const idxB = presenceIndex(userB.currentlyActive ? "active" : userB.presence);
console.log("Comparing presence groups...");
expect(idxB).toBeGreaterThanOrEqual(idxA);
groupChange = idxA !== idxB;
} else {
console.log("Skipped presence groups");
}
if (!groupChange) {
console.log("Comparing power levels...");
expect(memberA.powerLevel).toBeGreaterThanOrEqual(memberB.powerLevel);
groupChange = memberA.powerLevel !== memberB.powerLevel;
} else {
console.log("Skipping power level check due to group change");
}
if (!groupChange) {
if (isPresenceEnabled) {
console.log("Comparing last active timestamp...");
expect(userB.getLastActiveTs()).toBeLessThanOrEqual(userA.getLastActiveTs());
groupChange = userA.getLastActiveTs() !== userB.getLastActiveTs();
} else {
console.log("Skipping last active timestamp");
}
} else {
console.log("Skipping last active timestamp check due to group change");
}
if (!groupChange) {
const nameA = memberA.name[0] === "@" ? memberA.name.slice(1) : memberA.name;
const nameB = memberB.name[0] === "@" ? memberB.name.slice(1) : memberB.name;
const nameCompare = compare(nameB, nameA);
console.log("Comparing name");
expect(nameCompare).toBeGreaterThanOrEqual(0);
} else {
console.log("Skipping name check due to group change");
}
}
}
describe.each([false, true])("does order members correctly (presence %s)", (enablePresence) => {
beforeEach(function () {
TestUtils.stubClient();
client = MatrixClientPeg.get();
client.hasLazyLoadMembersEnabled = () => false;
parentDiv = document.createElement("div");
document.body.appendChild(parentDiv);
// Make room
memberListRoom = createRoom();
expect(memberListRoom.roomId).toBeTruthy();
@ -123,7 +206,8 @@ describe("MemberList", () => {
};
const context = new TestSdkContext();
context.client = client;
root = ReactDOM.render(
context.memberListStore.isPresenceEnabled = jest.fn().mockReturnValue(enablePresence);
root = render(
<SDKContext.Provider value={context}>
<MemberList
searchQuery=""
@ -133,86 +217,9 @@ describe("MemberList", () => {
ref={gatherWrappedRef}
/>
</SDKContext.Provider>,
parentDiv,
) as unknown as Component;
);
});
afterEach(() => {
if (parentDiv) {
ReactDOM.unmountComponentAtNode(parentDiv);
parentDiv.remove();
}
});
function expectOrderedByPresenceAndPowerLevel(memberTiles: MemberTile[], isPresenceEnabled: boolean) {
let prevMember: RoomMember | undefined;
for (const tile of memberTiles) {
const memberA = prevMember;
const memberB = tile.props.member;
prevMember = memberB; // just in case an expect fails, set this early
if (!memberA) {
continue;
}
console.log("COMPARING A VS B:");
console.log(memberList.memberString(memberA));
console.log(memberList.memberString(memberB));
const userA = memberA.user!;
const userB = memberB.user!;
let groupChange = false;
if (isPresenceEnabled) {
const convertPresence = (p: string) => (p === "unavailable" ? "online" : p);
const presenceIndex = (p: string) => {
const order = ["active", "online", "offline"];
const idx = order.indexOf(convertPresence(p));
return idx === -1 ? order.length : idx; // unknown states at the end
};
const idxA = presenceIndex(userA.currentlyActive ? "active" : userA.presence);
const idxB = presenceIndex(userB.currentlyActive ? "active" : userB.presence);
console.log("Comparing presence groups...");
expect(idxB).toBeGreaterThanOrEqual(idxA);
groupChange = idxA !== idxB;
} else {
console.log("Skipped presence groups");
}
if (!groupChange) {
console.log("Comparing power levels...");
expect(memberA.powerLevel).toBeGreaterThanOrEqual(memberB.powerLevel);
groupChange = memberA.powerLevel !== memberB.powerLevel;
} else {
console.log("Skipping power level check due to group change");
}
if (!groupChange) {
if (isPresenceEnabled) {
console.log("Comparing last active timestamp...");
expect(userB.getLastActiveTs()).toBeLessThanOrEqual(userA.getLastActiveTs());
groupChange = userA.getLastActiveTs() !== userB.getLastActiveTs();
} else {
console.log("Skipping last active timestamp");
}
} else {
console.log("Skipping last active timestamp check due to group change");
}
if (!groupChange) {
const nameA = memberA.name[0] === "@" ? memberA.name.slice(1) : memberA.name;
const nameB = memberB.name[0] === "@" ? memberB.name.slice(1) : memberB.name;
const nameCompare = compare(nameB, nameA);
console.log("Comparing name");
expect(nameCompare).toBeGreaterThanOrEqual(0);
} else {
console.log("Skipping name check due to group change");
}
}
}
function itDoesOrderMembersCorrectly(enablePresence: boolean) {
describe("does order members correctly", () => {
// Note: even if presence is disabled, we still expect that the presence
// tests will pass. All expectOrderedByPresenceAndPowerLevel does is ensure
@ -221,7 +228,7 @@ describe("MemberList", () => {
// Each of the 4 tests here is done to prove that the member list can meet
// all 4 criteria independently. Together, they should work.
it("by presence state", () => {
it("by presence state", async () => {
// Intentionally pick users that will confuse the power level sorting
const activeUsers = [defaultUsers[0]];
const onlineUsers = [adminUsers[0]];
@ -240,25 +247,23 @@ describe("MemberList", () => {
});
// Bypass all the event listeners and skip to the good part
memberList.showPresence = enablePresence;
memberList.updateListNow();
await act(() => memberList.updateListNow(true));
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
const tiles = root.container.querySelectorAll(".mx_EntityTile");
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
});
it("by power level", () => {
it("by power level", async () => {
// We already have admin, moderator, and default users so leave them alone
// Bypass all the event listeners and skip to the good part
memberList.showPresence = enablePresence;
memberList.updateListNow();
await act(() => memberList.updateListNow(true));
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
const tiles = root.container.querySelectorAll(".mx_EntityTile");
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
});
it("by last active timestamp", () => {
it("by last active timestamp", async () => {
// Intentionally pick users that will confuse the power level sorting
// lastActiveAgoTs == lastPresenceTs - lastActiveAgo
const activeUsers = [defaultUsers[0]];
@ -281,14 +286,13 @@ describe("MemberList", () => {
});
// Bypass all the event listeners and skip to the good part
memberList.showPresence = enablePresence;
memberList.updateListNow();
await act(() => memberList.updateListNow(true));
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
const tiles = root.container.querySelectorAll(".mx_EntityTile");
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
});
it("by name", () => {
it("by name", async () => {
// Intentionally put everyone on the same level to force a name comparison
const allUsers = [...adminUsers, ...moderatorUsers, ...defaultUsers];
allUsers.forEach((u) => {
@ -300,20 +304,11 @@ describe("MemberList", () => {
});
// Bypass all the event listeners and skip to the good part
memberList.showPresence = enablePresence;
memberList.updateListNow();
await act(() => memberList.updateListNow(true));
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
const tiles = root.container.querySelectorAll(".mx_EntityTile");
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
});
});
}
describe("when presence is enabled", () => {
itDoesOrderMembersCorrectly(true);
});
describe("when presence is not enabled", () => {
itDoesOrderMembersCorrectly(false);
});
});