mirror of
https://github.com/element-hq/element-web
synced 2024-11-25 02:35:48 +03:00
Implement new unreachable state and fix broken string ref (#11748)
* Fix string ref issue * Implement unreachable state * Fix eslint failure * Fix i18n * Fix i18n again * Write cypress test * Write jest test * Write more jest tests * Update method name * Use unstable prefix * Always use prefix This is never to going to be in the spec so always use the io.element prefix * Update tests * Remove redundant code from cypress test * Use unstable prefix * Refactor code * Remove supressOnHover prop * Remove sub-text label * Join lines * Remove blank line * Add jsdoc
This commit is contained in:
parent
6849afd9fc
commit
90419bdffd
9 changed files with 234 additions and 133 deletions
64
cypress/e2e/presence/presence.spec.ts
Normal file
64
cypress/e2e/presence/presence.spec.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
|
|
||||||
|
describe("Presence tests", () => {
|
||||||
|
let homeserver: HomeserverInstance;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.startHomeserver("default").then((data) => {
|
||||||
|
homeserver = data;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cy.stopHomeserver(homeserver);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders unreachable presence state correctly", () => {
|
||||||
|
cy.initTestUser(homeserver, "Janet");
|
||||||
|
cy.getBot(homeserver, { displayName: "Bob" }).then((bob) => {
|
||||||
|
cy.intercept("GET", "**/sync*", (req) => {
|
||||||
|
req.continue((res) => {
|
||||||
|
res.body.presence = {
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
type: "m.presence",
|
||||||
|
sender: bob.getUserId(),
|
||||||
|
content: {
|
||||||
|
presence: "io.element.unreachable",
|
||||||
|
currently_active: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
cy.createRoom({ name: "My Room", invite: [bob.getUserId()] }).then((roomId) => {
|
||||||
|
cy.viewRoomById(roomId);
|
||||||
|
});
|
||||||
|
cy.findByRole("button", { name: "Room info" }).click();
|
||||||
|
cy.get(".mx_RightPanel").within(() => {
|
||||||
|
cy.contains("People").click();
|
||||||
|
});
|
||||||
|
cy.get(".mx_EntityTile_unreachable")
|
||||||
|
.should("contain.text", "Bob")
|
||||||
|
.should("contain.text", "User's server unreachable");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -46,11 +46,11 @@ limitations under the License.
|
||||||
background-color: $header-panel-text-primary-color;
|
background-color: $header-panel-text-primary-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EntityTile .mx_PresenceLabel {
|
.mx_EntityTile:not(.mx_EntityTile_unreachable) .mx_PresenceLabel {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EntityTile:not(.mx_EntityTile_noHover):hover .mx_PresenceLabel {
|
.mx_EntityTile:hover .mx_PresenceLabel {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,7 +106,9 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EntityTile_unknown .mx_EntityTile_avatar,
|
.mx_EntityTile_unknown .mx_EntityTile_avatar,
|
||||||
.mx_EntityTile_unknown .mx_EntityTile_name {
|
.mx_EntityTile_unknown .mx_EntityTile_name,
|
||||||
|
.mx_EntityTile_unreachable .mx_EntityTile_avatar,
|
||||||
|
.mx_EntityTile_unreachable .mx_EntityTile_name {
|
||||||
opacity: 0.25;
|
opacity: 0.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -264,8 +264,7 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
|
||||||
<BaseAvatar url={require("../../../../res/img/ellipsis.svg").default} name="..." size="36px" />
|
<BaseAvatar url={require("../../../../res/img/ellipsis.svg").default} name="..." size="36px" />
|
||||||
}
|
}
|
||||||
name={text}
|
name={text}
|
||||||
presenceState="online"
|
showPresence={false}
|
||||||
suppressOnHover={true}
|
|
||||||
onClick={() => setTruncateAt(totalCount)}
|
onClick={() => setTruncateAt(totalCount)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -35,12 +35,13 @@ const PowerLabel: Record<PowerStatus, TranslationKey> = {
|
||||||
[PowerStatus.Moderator]: _td("power_level|mod"),
|
[PowerStatus.Moderator]: _td("power_level|mod"),
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PresenceState = "offline" | "online" | "unavailable";
|
export type PresenceState = "offline" | "online" | "unavailable" | "io.element.unreachable";
|
||||||
|
|
||||||
const PRESENCE_CLASS: Record<PresenceState, string> = {
|
const PRESENCE_CLASS: Record<PresenceState, string> = {
|
||||||
offline: "mx_EntityTile_offline",
|
"offline": "mx_EntityTile_offline",
|
||||||
online: "mx_EntityTile_online",
|
"online": "mx_EntityTile_online",
|
||||||
unavailable: "mx_EntityTile_unavailable",
|
"unavailable": "mx_EntityTile_unavailable",
|
||||||
|
"io.element.unreachable": "mx_EntityTile_unreachable",
|
||||||
};
|
};
|
||||||
|
|
||||||
function presenceClassForMember(presenceState?: PresenceState, lastActiveAgo?: number, showPresence?: boolean): string {
|
function presenceClassForMember(presenceState?: PresenceState, lastActiveAgo?: number, showPresence?: boolean): string {
|
||||||
|
@ -75,7 +76,6 @@ interface IProps {
|
||||||
presenceCurrentlyActive?: boolean;
|
presenceCurrentlyActive?: boolean;
|
||||||
showInviteButton: boolean;
|
showInviteButton: boolean;
|
||||||
onClick(): void;
|
onClick(): void;
|
||||||
suppressOnHover: boolean;
|
|
||||||
showPresence: boolean;
|
showPresence: boolean;
|
||||||
subtextLabel?: string;
|
subtextLabel?: string;
|
||||||
e2eStatus?: E2EState;
|
e2eStatus?: E2EState;
|
||||||
|
@ -93,7 +93,6 @@ export default class EntityTile extends React.PureComponent<IProps, IState> {
|
||||||
presenceLastActiveAgo: 0,
|
presenceLastActiveAgo: 0,
|
||||||
presenceLastTs: 0,
|
presenceLastTs: 0,
|
||||||
showInviteButton: false,
|
showInviteButton: false,
|
||||||
suppressOnHover: false,
|
|
||||||
showPresence: true,
|
showPresence: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -105,10 +104,27 @@ export default class EntityTile extends React.PureComponent<IProps, IState> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the PresenceLabel component if needed
|
||||||
|
* @returns The PresenceLabel component if we need to render it, undefined otherwise
|
||||||
|
*/
|
||||||
|
private getPresenceLabel(): JSX.Element | undefined {
|
||||||
|
if (!this.props.showPresence) return;
|
||||||
|
const activeAgo = this.props.presenceLastActiveAgo
|
||||||
|
? Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)
|
||||||
|
: -1;
|
||||||
|
return (
|
||||||
|
<PresenceLabel
|
||||||
|
activeAgo={activeAgo}
|
||||||
|
currentlyActive={this.props.presenceCurrentlyActive}
|
||||||
|
presenceState={this.props.presenceState}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
const mainClassNames: Record<string, boolean> = {
|
const mainClassNames: Record<string, boolean> = {
|
||||||
mx_EntityTile: true,
|
mx_EntityTile: true,
|
||||||
mx_EntityTile_noHover: !!this.props.suppressOnHover,
|
|
||||||
};
|
};
|
||||||
if (this.props.className) mainClassNames[this.props.className] = true;
|
if (this.props.className) mainClassNames[this.props.className] = true;
|
||||||
|
|
||||||
|
@ -119,43 +135,13 @@ export default class EntityTile extends React.PureComponent<IProps, IState> {
|
||||||
);
|
);
|
||||||
mainClassNames[presenceClass] = true;
|
mainClassNames[presenceClass] = true;
|
||||||
|
|
||||||
let nameEl;
|
|
||||||
const name = this.props.nameJSX || this.props.name;
|
const name = this.props.nameJSX || this.props.name;
|
||||||
|
const nameAndPresence = (
|
||||||
if (!this.props.suppressOnHover) {
|
|
||||||
const activeAgo = this.props.presenceLastActiveAgo
|
|
||||||
? Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)
|
|
||||||
: -1;
|
|
||||||
|
|
||||||
let presenceLabel: JSX.Element | undefined;
|
|
||||||
if (this.props.showPresence) {
|
|
||||||
presenceLabel = (
|
|
||||||
<PresenceLabel
|
|
||||||
activeAgo={activeAgo}
|
|
||||||
currentlyActive={this.props.presenceCurrentlyActive}
|
|
||||||
presenceState={this.props.presenceState}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (this.props.subtextLabel) {
|
|
||||||
presenceLabel = <span className="mx_EntityTile_subtext">{this.props.subtextLabel}</span>;
|
|
||||||
}
|
|
||||||
nameEl = (
|
|
||||||
<div className="mx_EntityTile_details">
|
<div className="mx_EntityTile_details">
|
||||||
<div className="mx_EntityTile_name">{name}</div>
|
<div className="mx_EntityTile_name">{name}</div>
|
||||||
{presenceLabel}
|
{this.getPresenceLabel()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (this.props.subtextLabel) {
|
|
||||||
nameEl = (
|
|
||||||
<div className="mx_EntityTile_details">
|
|
||||||
<div className="mx_EntityTile_name">{name}</div>
|
|
||||||
<span className="mx_EntityTile_subtext">{this.props.subtextLabel}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
nameEl = <div className="mx_EntityTile_name">{name}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let inviteButton;
|
let inviteButton;
|
||||||
if (this.props.showInviteButton) {
|
if (this.props.showInviteButton) {
|
||||||
|
@ -198,7 +184,7 @@ export default class EntityTile extends React.PureComponent<IProps, IState> {
|
||||||
{av}
|
{av}
|
||||||
{e2eIcon}
|
{e2eIcon}
|
||||||
</div>
|
</div>
|
||||||
{nameEl}
|
{nameAndPresence}
|
||||||
{powerLabel}
|
{powerLabel}
|
||||||
{inviteButton}
|
{inviteButton}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
|
|
@ -81,6 +81,7 @@ export default class MemberList extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
public static contextType = SDKContext;
|
public static contextType = SDKContext;
|
||||||
public context!: React.ContextType<typeof SDKContext>;
|
public context!: React.ContextType<typeof SDKContext>;
|
||||||
|
private tiles: Map<string, MemberTile> = new Map();
|
||||||
|
|
||||||
public constructor(props: IProps, context: React.ContextType<typeof SDKContext>) {
|
public constructor(props: IProps, context: React.ContextType<typeof SDKContext>) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -154,7 +155,7 @@ export default class MemberList extends React.Component<IProps, IState> {
|
||||||
// Attach a SINGLE listener for global presence changes then locate the
|
// Attach a SINGLE listener for global presence changes then locate the
|
||||||
// member tile and re-render it. This is more efficient than every tile
|
// member tile and re-render it. This is more efficient than every tile
|
||||||
// ever attaching their own listener.
|
// ever attaching their own listener.
|
||||||
const tile = this.refs[user.userId];
|
const tile = this.tiles.get(user.userId);
|
||||||
if (tile) {
|
if (tile) {
|
||||||
this.updateList(); // reorder the membership list
|
this.updateList(); // reorder the membership list
|
||||||
}
|
}
|
||||||
|
@ -245,8 +246,7 @@ export default class MemberList extends React.Component<IProps, IState> {
|
||||||
<BaseAvatar url={require("../../../../res/img/ellipsis.svg").default} name="..." size="36px" />
|
<BaseAvatar url={require("../../../../res/img/ellipsis.svg").default} name="..." size="36px" />
|
||||||
}
|
}
|
||||||
name={text}
|
name={text}
|
||||||
presenceState="online"
|
showPresence={false}
|
||||||
suppressOnHover={true}
|
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -307,14 +307,24 @@ export default class MemberList extends React.Component<IProps, IState> {
|
||||||
return members.map((m) => {
|
return members.map((m) => {
|
||||||
if (m instanceof RoomMember) {
|
if (m instanceof RoomMember) {
|
||||||
// Is a Matrix invite
|
// Is a Matrix invite
|
||||||
return <MemberTile key={m.userId} member={m} ref={m.userId} showPresence={this.showPresence} />;
|
return (
|
||||||
|
<MemberTile
|
||||||
|
key={m.userId}
|
||||||
|
member={m}
|
||||||
|
ref={(tile) => {
|
||||||
|
if (tile) this.tiles.set(m.userId, tile);
|
||||||
|
else this.tiles.delete(m.userId);
|
||||||
|
}}
|
||||||
|
showPresence={this.showPresence}
|
||||||
|
/>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Is a 3pid invite
|
// Is a 3pid invite
|
||||||
return (
|
return (
|
||||||
<EntityTile
|
<EntityTile
|
||||||
key={m.getStateKey()}
|
key={m.getStateKey()}
|
||||||
name={m.getContent().display_name}
|
name={m.getContent().display_name}
|
||||||
suppressOnHover={true}
|
showPresence={false}
|
||||||
onClick={() => this.onPending3pidInviteClick(m)}
|
onClick={() => this.onPending3pidInviteClick(m)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -44,6 +44,8 @@ export default class PresenceLabel extends React.Component<IProps> {
|
||||||
// the 'active ago' ends up being 0.
|
// the 'active ago' ends up being 0.
|
||||||
if (presence && BUSY_PRESENCE_NAME.matches(presence)) return _t("presence|busy");
|
if (presence && BUSY_PRESENCE_NAME.matches(presence)) return _t("presence|busy");
|
||||||
|
|
||||||
|
if (presence === "io.element.unreachable") return _t("presence|unreachable");
|
||||||
|
|
||||||
if (!currentlyActive && activeAgo !== undefined && activeAgo > 0) {
|
if (!currentlyActive && activeAgo !== undefined && activeAgo > 0) {
|
||||||
const duration = formatDuration(activeAgo);
|
const duration = formatDuration(activeAgo);
|
||||||
if (presence === "online") return _t("presence|online_for", { duration: duration });
|
if (presence === "online") return _t("presence|online_for", { duration: duration });
|
||||||
|
|
|
@ -1740,7 +1740,8 @@
|
||||||
"online": "Online",
|
"online": "Online",
|
||||||
"online_for": "Online for %(duration)s",
|
"online_for": "Online for %(duration)s",
|
||||||
"unknown": "Unknown",
|
"unknown": "Unknown",
|
||||||
"unknown_for": "Unknown for %(duration)s"
|
"unknown_for": "Unknown for %(duration)s",
|
||||||
|
"unreachable": "User's server unreachable"
|
||||||
},
|
},
|
||||||
"quick_settings": {
|
"quick_settings": {
|
||||||
"all_settings": "All settings",
|
"all_settings": "All settings",
|
||||||
|
|
|
@ -16,8 +16,8 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { act, render, RenderResult } from "@testing-library/react";
|
import { act, render, RenderResult, screen } from "@testing-library/react";
|
||||||
import { Room, MatrixClient, RoomState, RoomMember, User } from "matrix-js-sdk/src/matrix";
|
import { Room, MatrixClient, RoomState, RoomMember, User, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
import { compare } from "matrix-js-sdk/src/utils";
|
import { compare } from "matrix-js-sdk/src/utils";
|
||||||
|
|
||||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||||
|
@ -137,8 +137,7 @@ describe("MemberList", () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe.each([false, true])("does order members correctly (presence %s)", (enablePresence) => {
|
function renderMemberList(enablePresence: boolean): void {
|
||||||
beforeEach(function () {
|
|
||||||
TestUtils.stubClient();
|
TestUtils.stubClient();
|
||||||
client = MatrixClientPeg.safeGet();
|
client = MatrixClientPeg.safeGet();
|
||||||
client.hasLazyLoadMembersEnabled = () => false;
|
client.hasLazyLoadMembersEnabled = () => false;
|
||||||
|
@ -156,7 +155,7 @@ describe("MemberList", () => {
|
||||||
const adminUser = new RoomMember(memberListRoom.roomId, `@admin${i}:localhost`);
|
const adminUser = new RoomMember(memberListRoom.roomId, `@admin${i}:localhost`);
|
||||||
adminUser.membership = "join";
|
adminUser.membership = "join";
|
||||||
adminUser.powerLevel = 100;
|
adminUser.powerLevel = 100;
|
||||||
adminUser.user = new User(adminUser.userId);
|
adminUser.user = User.createUser(adminUser.userId, client);
|
||||||
adminUser.user.currentlyActive = true;
|
adminUser.user.currentlyActive = true;
|
||||||
adminUser.user.presence = "online";
|
adminUser.user.presence = "online";
|
||||||
adminUser.user.lastPresenceTs = 1000;
|
adminUser.user.lastPresenceTs = 1000;
|
||||||
|
@ -166,7 +165,7 @@ describe("MemberList", () => {
|
||||||
const moderatorUser = new RoomMember(memberListRoom.roomId, `@moderator${i}:localhost`);
|
const moderatorUser = new RoomMember(memberListRoom.roomId, `@moderator${i}:localhost`);
|
||||||
moderatorUser.membership = "join";
|
moderatorUser.membership = "join";
|
||||||
moderatorUser.powerLevel = 50;
|
moderatorUser.powerLevel = 50;
|
||||||
moderatorUser.user = new User(moderatorUser.userId);
|
moderatorUser.user = User.createUser(moderatorUser.userId, client);
|
||||||
moderatorUser.user.currentlyActive = true;
|
moderatorUser.user.currentlyActive = true;
|
||||||
moderatorUser.user.presence = "online";
|
moderatorUser.user.presence = "online";
|
||||||
moderatorUser.user.lastPresenceTs = 1000;
|
moderatorUser.user.lastPresenceTs = 1000;
|
||||||
|
@ -176,7 +175,7 @@ describe("MemberList", () => {
|
||||||
const defaultUser = new RoomMember(memberListRoom.roomId, `@default${i}:localhost`);
|
const defaultUser = new RoomMember(memberListRoom.roomId, `@default${i}:localhost`);
|
||||||
defaultUser.membership = "join";
|
defaultUser.membership = "join";
|
||||||
defaultUser.powerLevel = 0;
|
defaultUser.powerLevel = 0;
|
||||||
defaultUser.user = new User(defaultUser.userId);
|
defaultUser.user = User.createUser(defaultUser.userId, client);
|
||||||
defaultUser.user.currentlyActive = true;
|
defaultUser.user.currentlyActive = true;
|
||||||
defaultUser.user.presence = "online";
|
defaultUser.user.presence = "online";
|
||||||
defaultUser.user.lastPresenceTs = 1000;
|
defaultUser.user.lastPresenceTs = 1000;
|
||||||
|
@ -215,6 +214,11 @@ describe("MemberList", () => {
|
||||||
/>
|
/>
|
||||||
</SDKContext.Provider>,
|
</SDKContext.Provider>,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe.each([false, true])("does order members correctly (presence %s)", (enablePresence) => {
|
||||||
|
beforeEach(function () {
|
||||||
|
renderMemberList(enablePresence);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("does order members correctly", () => {
|
describe("does order members correctly", () => {
|
||||||
|
@ -308,4 +312,24 @@ describe("MemberList", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("memberlist is rendered correctly", () => {
|
||||||
|
beforeEach(function () {
|
||||||
|
renderMemberList(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("memberlist is re-rendered on unreachable presence event", async () => {
|
||||||
|
defaultUsers[0].user?.setPresenceEvent(
|
||||||
|
new MatrixEvent({
|
||||||
|
type: "m.presence",
|
||||||
|
sender: defaultUsers[0].userId,
|
||||||
|
content: {
|
||||||
|
presence: "io.element.unreachable",
|
||||||
|
currently_active: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(await screen.findByText(/User's server unreachable/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -32,4 +32,17 @@ describe("<PresenceLabel/>", () => {
|
||||||
</DocumentFragment>
|
</DocumentFragment>
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should render 'Unreachable' for presence=unreachable", () => {
|
||||||
|
const { asFragment } = render(<PresenceLabel presenceState="io.element.unreachable" />);
|
||||||
|
expect(asFragment()).toMatchInlineSnapshot(`
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="mx_PresenceLabel"
|
||||||
|
>
|
||||||
|
User's server unreachable
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue