feat: show call participants in room list (Discord-style)
Docker / Docker Buildx (push) Has been cancelled
Build Debian package / Build package (release) Has been cancelled
Build and Deploy / prepare (release) Has been cancelled
Deploy release / Deploy to Cloudflare Pages (release) Has been cancelled
Build and Deploy / Trigger Pro pipeline (release) Has been cancelled
Build and Deploy / Windows arm64 (release) Has been cancelled
Build and Deploy / Windows x64 (release) Has been cancelled
Build and Deploy / macOS (release) Has been cancelled
Build and Deploy / Linux amd64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / Linux arm64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / ${{ needs.prepare.outputs.deploy == 'true' && 'Deploy' || 'Deploy (dry-run)' }} (release) Has been cancelled
Build and Deploy / Deploy builds to ESS (release) Has been cancelled

This commit is contained in:
sorB
2026-05-10 14:25:35 +02:00
parent b797925316
commit 3da363517f
4610 changed files with 827237 additions and 1 deletions
@@ -0,0 +1,95 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { act, fireEvent, render } from "jest-matrix-react";
import CurrentDeviceSection from "../../../../../../src/components/views/settings/devices/CurrentDeviceSection";
import { DeviceType } from "../../../../../../src/utils/device/parseUserAgent";
describe("<CurrentDeviceSection />", () => {
const deviceId = "alices_device";
const alicesVerifiedDevice = {
device_id: deviceId,
isVerified: false,
deviceType: DeviceType.Unknown,
};
const alicesUnverifiedDevice = {
device_id: deviceId,
isVerified: false,
deviceType: DeviceType.Unknown,
};
const defaultProps = {
device: alicesVerifiedDevice,
onVerifyCurrentDevice: jest.fn(),
onSignOutCurrentDevice: jest.fn(),
saveDeviceName: jest.fn(),
isLoading: false,
isSigningOut: false,
otherSessionsCount: 1,
setPushNotifications: jest.fn(),
};
const getComponent = (props = {}): React.ReactElement => <CurrentDeviceSection {...defaultProps} {...props} />;
it("renders spinner while device is loading", () => {
const { container } = render(getComponent({ device: undefined, isLoading: true }));
expect(container.getElementsByClassName("mx_Spinner").length).toBeTruthy();
});
it("handles when device is falsy", async () => {
const { container } = render(getComponent({ device: undefined }));
expect(container).toMatchSnapshot();
});
it("renders device and correct security card when device is verified", () => {
const { container } = render(getComponent());
expect(container).toMatchSnapshot();
});
it("renders device and correct security card when device is unverified", () => {
const { container } = render(getComponent({ device: alicesUnverifiedDevice }));
expect(container).toMatchSnapshot();
});
it("displays device details on main tile click", () => {
const { getByTestId, container } = render(getComponent({ device: alicesUnverifiedDevice }));
act(() => {
fireEvent.click(getByTestId(`device-tile-${alicesUnverifiedDevice.device_id}`));
});
expect(container.getElementsByClassName("mx_DeviceDetails").length).toBeTruthy();
act(() => {
fireEvent.click(getByTestId(`device-tile-${alicesUnverifiedDevice.device_id}`));
});
// device details are hidden
expect(container.getElementsByClassName("mx_DeviceDetails").length).toBeFalsy();
});
it("displays device details on toggle click", () => {
const { container, getByTestId } = render(getComponent({ device: alicesUnverifiedDevice }));
act(() => {
fireEvent.click(getByTestId("current-session-toggle-details"));
});
expect(container.getElementsByClassName("mx_DeviceDetails")).toMatchSnapshot();
act(() => {
fireEvent.click(getByTestId("current-session-toggle-details"));
});
// device details are hidden
expect(container.getElementsByClassName("mx_DeviceDetails").length).toBeFalsy();
});
});
@@ -0,0 +1,141 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { fireEvent, render, type RenderResult } from "jest-matrix-react";
import { DeviceDetailHeading } from "../../../../../../src/components/views/settings/devices/DeviceDetailHeading";
import { flushPromisesWithFakeTimers } from "../../../../../test-utils";
import { DeviceType } from "../../../../../../src/utils/device/parseUserAgent";
jest.useFakeTimers();
describe("<DeviceDetailHeading />", () => {
const device = {
device_id: "123",
display_name: "My device",
isVerified: true,
deviceType: DeviceType.Unknown,
};
const defaultProps = {
device,
saveDeviceName: jest.fn(),
};
const getComponent = (props = {}) => <DeviceDetailHeading {...defaultProps} {...props} />;
const setInputValue = (getByTestId: RenderResult["getByTestId"], value: string) => {
const input = getByTestId("device-rename-input");
fireEvent.change(input, { target: { value } });
};
it("renders device name", () => {
const { container } = render(getComponent());
expect({ container }).toMatchSnapshot();
});
it("renders device id as fallback when device has no display name", () => {
const { getByText } = render(
getComponent({
device: { ...device, display_name: undefined },
}),
);
expect(getByText(device.device_id)).toBeTruthy();
});
it("displays name edit form on rename button click", () => {
const { getByTestId, container } = render(getComponent());
fireEvent.click(getByTestId("device-heading-rename-cta"));
expect({ container }).toMatchSnapshot();
});
it("cancelling edit switches back to original display", () => {
const { getByTestId, container } = render(getComponent());
// start editing
fireEvent.click(getByTestId("device-heading-rename-cta"));
// stop editing
fireEvent.click(getByTestId("device-rename-cancel-cta"));
expect(container.getElementsByClassName("mx_DeviceDetailHeading").length).toBe(1);
});
it("clicking submit updates device name with edited value", () => {
const saveDeviceName = jest.fn();
const { getByTestId } = render(getComponent({ saveDeviceName }));
// start editing
fireEvent.click(getByTestId("device-heading-rename-cta"));
setInputValue(getByTestId, "new device name");
fireEvent.click(getByTestId("device-rename-submit-cta"));
expect(saveDeviceName).toHaveBeenCalledWith("new device name");
});
it("disables form while device name is saving", () => {
const { getByTestId, container } = render(getComponent());
// start editing
fireEvent.click(getByTestId("device-heading-rename-cta"));
setInputValue(getByTestId, "new device name");
fireEvent.click(getByTestId("device-rename-submit-cta"));
// buttons disabled
expect(getByTestId("device-rename-cancel-cta").getAttribute("aria-disabled")).toEqual("true");
expect(getByTestId("device-rename-submit-cta").getAttribute("aria-disabled")).toEqual("true");
expect(container.getElementsByClassName("mx_Spinner").length).toBeTruthy();
});
it("toggles out of editing mode when device name is saved successfully", async () => {
const { getByTestId, findByTestId } = render(getComponent());
// start editing
fireEvent.click(getByTestId("device-heading-rename-cta"));
setInputValue(getByTestId, "new device name");
fireEvent.click(getByTestId("device-rename-submit-cta"));
await flushPromisesWithFakeTimers();
// read mode displayed
await expect(findByTestId("device-detail-heading")).resolves.toBeTruthy();
});
it("displays error when device name fails to save", async () => {
const saveDeviceName = jest.fn().mockRejectedValueOnce("oups").mockResolvedValue({});
const { getByTestId, queryByText, findByText, container } = render(getComponent({ saveDeviceName }));
// start editing
fireEvent.click(getByTestId("device-heading-rename-cta"));
setInputValue(getByTestId, "new device name");
fireEvent.click(getByTestId("device-rename-submit-cta"));
// flush promise
await flushPromisesWithFakeTimers();
// then tick for render
await flushPromisesWithFakeTimers();
// error message displayed
await expect(findByText("Failed to set session name")).resolves.toBeTruthy();
// spinner removed
expect(container.getElementsByClassName("mx_Spinner").length).toBeFalsy();
// try again
fireEvent.click(getByTestId("device-rename-submit-cta"));
// error message cleared
expect(queryByText("Failed to set display name")).toBeFalsy();
});
});
@@ -0,0 +1,187 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React, { type ComponentProps } from "react";
import { fireEvent, render } from "jest-matrix-react";
import { PUSHER_ENABLED } from "matrix-js-sdk/src/matrix";
import DeviceDetails from "../../../../../../src/components/views/settings/devices/DeviceDetails";
import { mkPusher } from "../../../../../test-utils/test-utils";
import { DeviceType } from "../../../../../../src/utils/device/parseUserAgent";
describe("<DeviceDetails />", () => {
const baseDevice = {
device_id: "my-device",
isVerified: false,
deviceType: DeviceType.Unknown,
};
const defaultProps: ComponentProps<typeof DeviceDetails> = {
device: baseDevice,
isSigningOut: false,
onSignOutDevice: jest.fn(),
saveDeviceName: jest.fn(),
setPushNotifications: jest.fn(),
supportsMSC3881: true,
};
const getComponent = (props = {}) => <DeviceDetails {...defaultProps} {...props} />;
// 14.03.2022 16:15
const now = 1647270879403;
jest.useFakeTimers();
beforeEach(() => {
jest.setSystemTime(now);
});
it("renders device without metadata", () => {
const { container } = render(getComponent());
expect(container).toMatchSnapshot();
});
it("renders device with metadata", () => {
const device = {
...baseDevice,
display_name: "My Device",
last_seen_ip: "123.456.789",
last_seen_ts: now - 60000000,
appName: "Element Web",
client: "Firefox 100",
deviceModel: "Iphone X",
deviceOperatingSystem: "Windows 95",
};
const { container } = render(getComponent({ device }));
expect(container).toMatchSnapshot();
});
it("renders a verified device", () => {
const device = {
...baseDevice,
isVerified: true,
};
const { container } = render(getComponent({ device }));
expect(container).toMatchSnapshot();
});
it("disables sign out button while sign out is pending", () => {
const device = {
...baseDevice,
};
const { getByTestId } = render(getComponent({ device, isSigningOut: true }));
expect(getByTestId("device-detail-sign-out-cta").getAttribute("aria-disabled")).toEqual("true");
});
it("renders the push notification section when a pusher exists", () => {
const device = {
...baseDevice,
};
const pusher = mkPusher({
device_id: device.device_id,
});
const { getByTestId } = render(
getComponent({
device,
pusher,
isSigningOut: true,
}),
);
expect(getByTestId("device-detail-push-notification")).toBeTruthy();
});
it("hides the push notification section when no pusher", () => {
const device = {
...baseDevice,
};
const { getByTestId } = render(
getComponent({
device,
pusher: null,
isSigningOut: true,
}),
);
expect(() => getByTestId("device-detail-push-notification")).toThrow();
});
it("disables the checkbox when there is no server support", () => {
const device = {
...baseDevice,
};
const pusher = mkPusher({
device_id: device.device_id,
[PUSHER_ENABLED.name]: false,
});
const { getByTestId } = render(
getComponent({
device,
pusher,
isSigningOut: true,
supportsMSC3881: false,
}),
);
const checkbox = getByTestId("device-detail-push-notification-checkbox");
expect(checkbox.getAttribute("aria-disabled")).toEqual("true");
expect(checkbox.getAttribute("aria-checked")).toEqual("false");
});
it("changes the pusher status when clicked", () => {
const device = {
...baseDevice,
};
const enabled = false;
const pusher = mkPusher({
device_id: device.device_id,
[PUSHER_ENABLED.name]: enabled,
});
const { getByTestId } = render(
getComponent({
device,
pusher,
isSigningOut: true,
}),
);
const checkbox = getByTestId("device-detail-push-notification-checkbox");
fireEvent.click(checkbox);
expect(defaultProps.setPushNotifications).toHaveBeenCalledWith(device.device_id, !enabled);
});
it("changes the local notifications settings status when clicked", () => {
const device = {
...baseDevice,
};
const enabled = false;
const { getByTestId } = render(
getComponent({
device,
localNotificationSettings: {
is_silenced: !enabled,
},
isSigningOut: true,
}),
);
const checkbox = getByTestId("device-detail-push-notification-checkbox");
fireEvent.click(checkbox);
expect(defaultProps.setPushNotifications).toHaveBeenCalledWith(device.device_id, !enabled);
});
});
@@ -0,0 +1,38 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { fireEvent, render } from "jest-matrix-react";
import { DeviceExpandDetailsButton } from "../../../../../../src/components/views/settings/devices/DeviceExpandDetailsButton";
describe("<DeviceExpandDetailsButton />", () => {
const defaultProps = {
isExpanded: false,
onClick: jest.fn(),
};
const getComponent = (props = {}) => <DeviceExpandDetailsButton {...defaultProps} {...props} />;
it("renders when not expanded", () => {
const { container } = render(getComponent());
expect({ container }).toMatchSnapshot();
});
it("renders when expanded", () => {
const { container } = render(getComponent({ isExpanded: true }));
expect({ container }).toMatchSnapshot();
});
it("calls onClick", () => {
const onClick = jest.fn();
const { getByTestId } = render(getComponent({ "data-testid": "test", onClick }));
fireEvent.click(getByTestId("test"));
expect(onClick).toHaveBeenCalled();
});
});
@@ -0,0 +1,37 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { render } from "jest-matrix-react";
import React from "react";
import DeviceSecurityCard from "../../../../../../src/components/views/settings/devices/DeviceSecurityCard";
import { DeviceSecurityVariation } from "../../../../../../src/components/views/settings/devices/types";
describe("<DeviceSecurityCard />", () => {
const defaultProps = {
variation: DeviceSecurityVariation.Verified,
heading: "Verified session",
description: "nice",
};
const getComponent = (props = {}): React.ReactElement => <DeviceSecurityCard {...defaultProps} {...props} />;
it("renders basic card", () => {
const { container } = render(getComponent());
expect(container).toMatchSnapshot();
});
it("renders with children", () => {
const { container } = render(
getComponent({
children: <div>hey</div>,
variation: DeviceSecurityVariation.Unverified,
}),
);
expect(container).toMatchSnapshot();
});
});
@@ -0,0 +1,124 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render } from "jest-matrix-react";
import { type IMyDevice } from "matrix-js-sdk/src/matrix";
import DeviceTile from "../../../../../../src/components/views/settings/devices/DeviceTile";
import { DeviceType } from "../../../../../../src/utils/device/parseUserAgent";
describe("<DeviceTile />", () => {
const defaultProps = {
device: {
device_id: "123",
isVerified: false,
deviceType: DeviceType.Unknown,
},
};
const getComponent = (props = {}) => <DeviceTile {...defaultProps} {...props} />;
// 14.03.2022 16:15
const now = 1647270879403;
jest.useFakeTimers();
beforeEach(() => {
jest.setSystemTime(now);
});
it("renders a device with no metadata", () => {
const { container } = render(getComponent());
expect(container).toMatchSnapshot();
});
it("applies interactive class when tile has click handler", () => {
const onClick = jest.fn();
const { getByTestId } = render(getComponent({ onClick }));
expect(getByTestId("device-tile-123").className.includes("mx_DeviceTile_interactive")).toBeTruthy();
});
it("renders a verified device with no metadata", () => {
const { container } = render(getComponent());
expect(container).toMatchSnapshot();
});
it("renders display name with a tooltip", () => {
const device: IMyDevice = {
device_id: "123",
display_name: "My device",
};
const { container } = render(getComponent({ device }));
expect(container).toMatchSnapshot();
});
it("renders last seen ip metadata", () => {
const device: IMyDevice = {
device_id: "123",
display_name: "My device",
last_seen_ip: "1.2.3.4",
};
const { getByTestId } = render(getComponent({ device }));
expect(getByTestId("device-metadata-lastSeenIp").textContent).toEqual(device.last_seen_ip);
});
it("separates metadata with a dot", () => {
const device: IMyDevice = {
device_id: "123",
last_seen_ip: "1.2.3.4",
last_seen_ts: now - 60000,
};
const { container } = render(getComponent({ device }));
expect(container).toMatchSnapshot();
});
describe("Last activity", () => {
const MS_DAY = 24 * 60 * 60 * 1000;
it("renders with day of week and time when last activity is less than 6 days ago", () => {
const device: IMyDevice = {
device_id: "123",
last_seen_ip: "1.2.3.4",
last_seen_ts: now - MS_DAY * 3,
};
const { getByTestId } = render(getComponent({ device }));
expect(getByTestId("device-metadata-lastActivity").textContent).toEqual("Last activity Fri 15:14");
});
it("renders with month and date when last activity is more than 6 days ago", () => {
const device: IMyDevice = {
device_id: "123",
last_seen_ip: "1.2.3.4",
last_seen_ts: now - MS_DAY * 8,
};
const { getByTestId } = render(getComponent({ device }));
expect(getByTestId("device-metadata-lastActivity").textContent).toEqual("Last activity Mar 6");
});
it("renders with month, date, year when activity is in a different calendar year", () => {
const device: IMyDevice = {
device_id: "123",
last_seen_ip: "1.2.3.4",
last_seen_ts: new Date("2021-12-29").getTime(),
};
const { getByTestId } = render(getComponent({ device }));
expect(getByTestId("device-metadata-lastActivity").textContent).toEqual("Last activity Dec 29, 2021");
});
it("renders with inactive notice when last activity was more than 90 days ago", () => {
const device: IMyDevice = {
device_id: "123",
last_seen_ip: "1.2.3.4",
last_seen_ts: now - MS_DAY * 100,
};
const { getByTestId, queryByTestId } = render(getComponent({ device }));
expect(getByTestId("device-metadata-inactive").textContent).toEqual("Inactive for 90+ days (Dec 4, 2021)");
// last activity and verification not shown when inactive
expect(queryByTestId("device-metadata-lastActivity")).toBeFalsy();
expect(queryByTestId("device-metadata-verificationStatus")).toBeFalsy();
});
});
});
@@ -0,0 +1,65 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { render } from "jest-matrix-react";
import React from "react";
import { DeviceTypeIcon } from "../../../../../../src/components/views/settings/devices/DeviceTypeIcon";
import { DeviceType } from "../../../../../../src/utils/device/parseUserAgent";
describe("<DeviceTypeIcon />", () => {
const defaultProps = {
isVerified: false,
isSelected: false,
};
const getComponent = (props = {}) => <DeviceTypeIcon {...defaultProps} {...props} />;
it("renders an unverified device", () => {
const { container } = render(getComponent());
expect(container).toMatchSnapshot();
});
it("renders a verified device", () => {
const { container } = render(getComponent({ isVerified: true }));
expect(container).toMatchSnapshot();
});
it("renders correctly when selected", () => {
const { container } = render(getComponent({ isSelected: true }));
expect(container).toMatchSnapshot();
});
it("renders an unknown device icon when no device type given", () => {
const { getByLabelText } = render(getComponent());
expect(getByLabelText("Unknown session type")).toBeTruthy();
});
it("renders a desktop device type", () => {
const deviceType = DeviceType.Desktop;
const { getByLabelText } = render(getComponent({ deviceType }));
expect(getByLabelText("Desktop session")).toBeTruthy();
});
it("renders a web device type", () => {
const deviceType = DeviceType.Web;
const { getByLabelText } = render(getComponent({ deviceType }));
expect(getByLabelText("Web session")).toBeTruthy();
});
it("renders a mobile device type", () => {
const deviceType = DeviceType.Mobile;
const { getByLabelText } = render(getComponent({ deviceType }));
expect(getByLabelText("Mobile session")).toBeTruthy();
});
it("renders an unknown device type", () => {
const deviceType = DeviceType.Unknown;
const { getByLabelText } = render(getComponent({ deviceType }));
expect(getByLabelText("Unknown session type")).toBeTruthy();
});
});
@@ -0,0 +1,88 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { render } from "jest-matrix-react";
import React from "react";
import {
DeviceVerificationStatusCard,
type DeviceVerificationStatusCardProps,
} from "../../../../../../src/components/views/settings/devices/DeviceVerificationStatusCard";
import { type ExtendedDevice } from "../../../../../../src/components/views/settings/devices/types";
import { DeviceType } from "../../../../../../src/utils/device/parseUserAgent";
describe("<DeviceVerificationStatusCard />", () => {
const deviceId = "test-device";
const unverifiedDevice: ExtendedDevice = {
device_id: deviceId,
isVerified: false,
deviceType: DeviceType.Unknown,
};
const verifiedDevice: ExtendedDevice = {
...unverifiedDevice,
isVerified: true,
};
const unverifiableDevice: ExtendedDevice = {
...unverifiedDevice,
isVerified: null,
};
const defaultProps = {
device: unverifiedDevice,
onVerifyDevice: jest.fn(),
};
const getComponent = (props: Partial<DeviceVerificationStatusCardProps> = {}) => (
<DeviceVerificationStatusCard {...defaultProps} {...props} />
);
const verifyButtonTestId = `verification-status-button-${deviceId}`;
describe("for the current device", () => {
// current device uses different copy
it("renders an unverified device", () => {
const { getByText } = render(getComponent({ isCurrentDevice: true }));
expect(getByText("Verify your current session for enhanced secure messaging.")).toBeTruthy();
});
it("renders an unverifiable device", () => {
const { getByText } = render(
getComponent({
device: unverifiableDevice,
isCurrentDevice: true,
}),
);
expect(getByText("This session doesn't support encryption and thus can't be verified.")).toBeTruthy();
});
it("renders a verified device", () => {
const { getByText } = render(
getComponent({
device: verifiedDevice,
isCurrentDevice: true,
}),
);
expect(getByText("Your current session is ready for secure messaging.")).toBeTruthy();
});
});
it("renders an unverified device", () => {
const { container } = render(getComponent());
expect(container).toMatchSnapshot();
});
it("renders an unverifiable device", () => {
const { container, queryByTestId } = render(getComponent({ device: unverifiableDevice }));
expect(container).toMatchSnapshot();
expect(queryByTestId(verifyButtonTestId)).toBeFalsy();
});
it("renders a verified device", () => {
const { container, queryByTestId } = render(getComponent({ device: verifiedDevice }));
expect(container).toMatchSnapshot();
expect(queryByTestId(verifyButtonTestId)).toBeFalsy();
});
});
@@ -0,0 +1,231 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React, { type ComponentProps } from "react";
import { act, fireEvent, render } from "jest-matrix-react";
import { FilteredDeviceList } from "../../../../../../src/components/views/settings/devices/FilteredDeviceList";
import { DeviceSecurityVariation } from "../../../../../../src/components/views/settings/devices/types";
import { flushPromises, mockPlatformPeg } from "../../../../../test-utils";
import { DeviceType } from "../../../../../../src/utils/device/parseUserAgent";
mockPlatformPeg();
const MS_DAY = 86400000;
describe("<FilteredDeviceList />", () => {
// 14.03.2022 16:15
const now = 1647270879403;
jest.spyOn(global.Date, "now").mockReturnValue(now);
const newDevice = {
device_id: "new",
last_seen_ts: Date.now() - 500,
last_seen_ip: "123.456.789",
display_name: "My Device",
isVerified: true,
deviceType: DeviceType.Unknown,
};
const unverifiedNoMetadata = {
device_id: "unverified-no-metadata",
isVerified: false,
deviceType: DeviceType.Unknown,
};
const verifiedNoMetadata = {
device_id: "verified-no-metadata",
isVerified: true,
deviceType: DeviceType.Unknown,
};
const hundredDaysOld = {
device_id: "100-days-old",
isVerified: true,
last_seen_ts: Date.now() - MS_DAY * 100,
deviceType: DeviceType.Unknown,
};
const hundredDaysOldUnverified = {
device_id: "unverified-100-days-old",
isVerified: false,
last_seen_ts: Date.now() - MS_DAY * 100,
deviceType: DeviceType.Unknown,
};
const defaultProps: ComponentProps<typeof FilteredDeviceList> = {
onFilterChange: jest.fn(),
onDeviceExpandToggle: jest.fn(),
onSignOutDevices: jest.fn(),
saveDeviceName: jest.fn(),
setPushNotifications: jest.fn(),
setSelectedDeviceIds: jest.fn(),
localNotificationSettings: new Map(),
expandedDeviceIds: [],
signingOutDeviceIds: [],
selectedDeviceIds: [],
devices: {
[unverifiedNoMetadata.device_id]: unverifiedNoMetadata,
[verifiedNoMetadata.device_id]: verifiedNoMetadata,
[newDevice.device_id]: newDevice,
[hundredDaysOld.device_id]: hundredDaysOld,
[hundredDaysOldUnverified.device_id]: hundredDaysOldUnverified,
},
pushers: [],
supportsMSC3881: true,
};
const getComponent = (props = {}) => <FilteredDeviceList {...defaultProps} {...props} />;
afterAll(() => {
jest.spyOn(global.Date, "now").mockRestore();
});
it("renders devices in correct order", () => {
const { container } = render(getComponent());
const tiles = container.querySelectorAll(".mx_DeviceTile");
expect(tiles[0].getAttribute("data-testid")).toEqual(`device-tile-${newDevice.device_id}`);
expect(tiles[1].getAttribute("data-testid")).toEqual(`device-tile-${hundredDaysOld.device_id}`);
expect(tiles[2].getAttribute("data-testid")).toEqual(`device-tile-${hundredDaysOldUnverified.device_id}`);
expect(tiles[3].getAttribute("data-testid")).toEqual(`device-tile-${unverifiedNoMetadata.device_id}`);
expect(tiles[4].getAttribute("data-testid")).toEqual(`device-tile-${verifiedNoMetadata.device_id}`);
});
it("updates list order when devices change", () => {
const updatedOldDevice = { ...hundredDaysOld, last_seen_ts: new Date().getTime() };
const updatedDevices = {
[hundredDaysOld.device_id]: updatedOldDevice,
[newDevice.device_id]: newDevice,
};
const { container, rerender } = render(getComponent());
rerender(getComponent({ devices: updatedDevices }));
const tiles = container.querySelectorAll(".mx_DeviceTile");
expect(tiles.length).toBe(2);
expect(tiles[0].getAttribute("data-testid")).toEqual(`device-tile-${hundredDaysOld.device_id}`);
expect(tiles[1].getAttribute("data-testid")).toEqual(`device-tile-${newDevice.device_id}`);
});
it("displays no results message when there are no devices", () => {
const { container } = render(getComponent({ devices: {} }));
expect(container.getElementsByClassName("mx_FilteredDeviceList_noResults")).toMatchSnapshot();
});
describe("filtering", () => {
const setFilter = async (container: HTMLElement, option: DeviceSecurityVariation | string) => {
const dropdown = container.querySelector('[aria-label="Filter devices"]');
fireEvent.click(dropdown as Element);
// tick to let dropdown render
await flushPromises();
fireEvent.click(container.querySelector(`#device-list-filter__${option}`) as Element);
};
it("does not display filter description when filter is falsy", () => {
const { container } = render(getComponent({ filter: undefined }));
const tiles = container.querySelectorAll(".mx_DeviceTile");
expect(container.getElementsByClassName("mx_FilteredDeviceList_securityCard").length).toBeFalsy();
expect(tiles.length).toEqual(5);
});
it("updates filter when prop changes", () => {
const { container, rerender } = render(getComponent({ filter: DeviceSecurityVariation.Verified }));
const tiles = container.querySelectorAll(".mx_DeviceTile");
expect(tiles.length).toEqual(3);
expect(tiles[0].getAttribute("data-testid")).toEqual(`device-tile-${newDevice.device_id}`);
expect(tiles[1].getAttribute("data-testid")).toEqual(`device-tile-${hundredDaysOld.device_id}`);
expect(tiles[2].getAttribute("data-testid")).toEqual(`device-tile-${verifiedNoMetadata.device_id}`);
rerender(getComponent({ filter: DeviceSecurityVariation.Inactive }));
const rerenderedTiles = container.querySelectorAll(".mx_DeviceTile");
expect(rerenderedTiles.length).toEqual(2);
expect(rerenderedTiles[0].getAttribute("data-testid")).toEqual(`device-tile-${hundredDaysOld.device_id}`);
expect(rerenderedTiles[1].getAttribute("data-testid")).toEqual(
`device-tile-${hundredDaysOldUnverified.device_id}`,
);
});
it("calls onFilterChange handler", async () => {
const onFilterChange = jest.fn();
const { container } = render(getComponent({ onFilterChange }));
await setFilter(container, DeviceSecurityVariation.Verified);
expect(onFilterChange).toHaveBeenCalledWith(DeviceSecurityVariation.Verified);
});
it("calls onFilterChange handler correctly when setting filter to All", async () => {
const onFilterChange = jest.fn();
const { container } = render(getComponent({ onFilterChange, filter: DeviceSecurityVariation.Verified }));
await setFilter(container, "ALL");
// filter is cleared
expect(onFilterChange).toHaveBeenCalledWith(undefined);
});
it.each([
[DeviceSecurityVariation.Verified, [newDevice, hundredDaysOld, verifiedNoMetadata]],
[DeviceSecurityVariation.Unverified, [hundredDaysOldUnverified, unverifiedNoMetadata]],
[DeviceSecurityVariation.Inactive, [hundredDaysOld, hundredDaysOldUnverified]],
])("filters correctly for %s", (filter, expectedDevices) => {
const { container } = render(getComponent({ filter }));
expect(container.getElementsByClassName("mx_FilteredDeviceList_securityCard")).toMatchSnapshot();
const tileDeviceIds = [...container.querySelectorAll(".mx_DeviceTile")].map((tile) =>
tile.getAttribute("data-testid"),
);
expect(tileDeviceIds).toEqual(expectedDevices.map((device) => `device-tile-${device.device_id}`));
});
it.each([
[DeviceSecurityVariation.Verified],
[DeviceSecurityVariation.Unverified],
[DeviceSecurityVariation.Inactive],
])("renders no results correctly for %s", (filter) => {
const { container } = render(getComponent({ filter, devices: {} }));
expect(container.getElementsByClassName("mx_FilteredDeviceList_securityCard").length).toBeFalsy();
expect(container.getElementsByClassName("mx_FilteredDeviceList_noResults")).toMatchSnapshot();
});
it("clears filter from no results message", () => {
const onFilterChange = jest.fn();
const { getByTestId } = render(
getComponent({
onFilterChange,
filter: DeviceSecurityVariation.Verified,
devices: {
[unverifiedNoMetadata.device_id]: unverifiedNoMetadata,
},
}),
);
act(() => {
fireEvent.click(getByTestId("devices-clear-filter-btn"));
});
expect(onFilterChange).toHaveBeenCalledWith(undefined);
});
});
describe("device details", () => {
it("renders expanded devices with device details", () => {
const expandedDeviceIds = [newDevice.device_id, hundredDaysOld.device_id];
const { container, getByTestId } = render(getComponent({ expandedDeviceIds }));
expect(container.getElementsByClassName("mx_DeviceDetails").length).toBeTruthy();
expect(getByTestId(`device-detail-${newDevice.device_id}`)).toBeTruthy();
expect(getByTestId(`device-detail-${hundredDaysOld.device_id}`)).toBeTruthy();
});
it("clicking toggle calls onDeviceExpandToggle", () => {
const onDeviceExpandToggle = jest.fn();
const { getByTestId } = render(getComponent({ onDeviceExpandToggle }));
act(() => {
const tile = getByTestId(`device-tile-${hundredDaysOld.device_id}`);
const toggle = tile.querySelector('[aria-label="Show details"]');
fireEvent.click(toggle as Element);
});
expect(onDeviceExpandToggle).toHaveBeenCalledWith(hundredDaysOld.device_id);
});
});
});
@@ -0,0 +1,46 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { fireEvent, render } from "jest-matrix-react";
import React from "react";
import FilteredDeviceListHeader from "../../../../../../src/components/views/settings/devices/FilteredDeviceListHeader";
describe("<FilteredDeviceListHeader />", () => {
const defaultProps = {
selectedDeviceCount: 0,
isAllSelected: false,
toggleSelectAll: jest.fn(),
children: <div>test</div>,
["data-testid"]: "test123",
};
const getComponent = (props = {}) => <FilteredDeviceListHeader {...defaultProps} {...props} />;
it("renders correctly when no devices are selected", () => {
const { container } = render(getComponent());
expect(container).toMatchSnapshot();
});
it("renders correctly when all devices are selected", () => {
const { container } = render(getComponent({ isAllSelected: true }));
expect(container).toMatchSnapshot();
});
it("renders correctly when some devices are selected", () => {
const { getByText } = render(getComponent({ selectedDeviceCount: 2 }));
expect(getByText("2 sessions selected")).toBeTruthy();
});
it("clicking checkbox toggles selection", () => {
const toggleSelectAll = jest.fn();
const { getByTestId } = render(getComponent({ toggleSelectAll }));
fireEvent.click(getByTestId("device-select-all-checkbox"));
expect(toggleSelectAll).toHaveBeenCalled();
});
});
@@ -0,0 +1,288 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { cleanup, render, waitFor } from "jest-matrix-react";
import { mocked, type MockedObject } from "jest-mock";
import React, { createRef, type RefObject } from "react";
import {
ClientRendezvousFailureReason,
MSC4108FailureReason,
MSC4108SignInWithQR,
RendezvousError,
} from "matrix-js-sdk/src/rendezvous";
import { HTTPError, type MatrixClient, MatrixHttpApi } from "matrix-js-sdk/src/matrix";
import LoginWithQR, { LoginWithQRFailureReason } from "../../../../../../src/components/views/auth/LoginWithQR";
import { Click, Mode, Phase } from "../../../../../../src/components/views/auth/LoginWithQR-types";
jest.mock("matrix-js-sdk/src/rendezvous/transports");
jest.mock("matrix-js-sdk/src/rendezvous/channels");
jest.mock("matrix-js-sdk/src/rendezvous/channels/MSC4108SecureChannel.ts");
const mockedFlow = jest.fn();
jest.mock("../../../../../../src/components/views/auth/LoginWithQRFlow", () => (props: Record<string, any>) => {
mockedFlow(props);
return <div />;
});
function makeClient() {
const cli = mocked({
getUser: jest.fn(),
isGuest: jest.fn().mockReturnValue(false),
isUserIgnored: jest.fn(),
getUserId: jest.fn(),
on: jest.fn(),
isSynapseAdministrator: jest.fn().mockResolvedValue(false),
isRoomEncrypted: jest.fn().mockReturnValue(false),
mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
doesServerSupportUnstableFeature: jest.fn().mockReturnValue(true),
removeListener: jest.fn(),
requestLoginToken: jest.fn(),
currentState: {
on: jest.fn(),
},
getClientWellKnown: jest.fn().mockReturnValue({}),
getCrypto: jest.fn().mockReturnValue({}),
getDomain: jest.fn(),
} as unknown as MatrixClient);
cli.http = new MatrixHttpApi(cli, {
baseUrl: "https://server/",
prefix: "prefix",
onlyData: true,
}) as any;
return cli;
}
function unresolvedPromise<T>(): Promise<T> {
return new Promise(() => {});
}
describe("<LoginWithQR />", () => {
let client!: MockedObject<MatrixClient>;
const defaultProps = {
legacy: true,
mode: Mode.Show,
onFinished: jest.fn(),
} as const;
beforeEach(() => {
mockedFlow.mockReset();
jest.resetAllMocks();
client = makeClient();
});
afterEach(() => {
client = makeClient();
jest.clearAllMocks();
jest.useRealTimers();
cleanup();
});
describe("MSC4108", () => {
const getComponent = (props: {
client: MatrixClient;
onFinished?: () => void;
ref?: RefObject<LoginWithQR | null>;
}) => <LoginWithQR {...defaultProps} {...props} />;
test("render QR then back", async () => {
const onFinished = jest.fn();
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockReturnValue(unresolvedPromise());
jest.spyOn(MSC4108SignInWithQR.prototype, "generateCode");
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols");
jest.spyOn(MSC4108SignInWithQR.prototype, "cancel");
const ref = createRef<LoginWithQR>();
render(getComponent({ client, onFinished, ref }));
await waitFor(() =>
expect(mockedFlow).toHaveBeenLastCalledWith({
phase: Phase.ShowingQR,
onClick: expect.any(Function),
}),
);
const rendezvous = ref.current!.state.rendezvous!;
expect(rendezvous.generateCode).toHaveBeenCalled();
expect(rendezvous.negotiateProtocols).toHaveBeenCalled();
// back
const onClick = mockedFlow.mock.calls[0][0].onClick;
await onClick(Click.Back);
expect(onFinished).toHaveBeenCalledWith(false);
expect(rendezvous.cancel).toHaveBeenCalledWith(MSC4108FailureReason.UserCancelled);
});
test("should open a new channel if expires before qr scan", async () => {
const onFinished = jest.fn();
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockReturnValue(unresolvedPromise());
const ref = createRef<LoginWithQR>();
render(getComponent({ client, onFinished, ref }));
await waitFor(() =>
expect(mockedFlow).toHaveBeenLastCalledWith({
phase: Phase.ShowingQR,
onClick: expect.any(Function),
}),
);
const rendezvous = ref.current!.state.rendezvous!;
expect(rendezvous.generateCode).toHaveBeenCalled();
expect(rendezvous.negotiateProtocols).toHaveBeenCalled();
// Expire the channel
rendezvous.onFailure!(ClientRendezvousFailureReason.Expired);
await jest.runAllTimersAsync();
await waitFor(() => expect(ref.current!.state.rendezvous).toBeDefined());
expect(ref.current!.state.rendezvous).not.toBe(rendezvous);
});
test("failed to connect", async () => {
render(getComponent({ client }));
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockResolvedValue({});
jest.spyOn(MSC4108SignInWithQR.prototype, "deviceAuthorizationGrant").mockRejectedValue(
new HTTPError("Internal Server Error", 500),
);
const fn = jest.spyOn(MSC4108SignInWithQR.prototype, "cancel");
await waitFor(() => expect(fn).toHaveBeenLastCalledWith(ClientRendezvousFailureReason.Unknown));
});
test("should show error if check code doesn't match", async () => {
jest.spyOn(global.window, "open");
render(getComponent({ client }));
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockResolvedValue({});
jest.spyOn(MSC4108SignInWithQR.prototype, "deviceAuthorizationGrant").mockResolvedValue({
verificationUri: "mock-verification-uri",
});
await waitFor(() =>
expect(mockedFlow).toHaveBeenLastCalledWith({
phase: Phase.OutOfBandConfirmation,
onClick: expect.any(Function),
}),
);
const onClick = mockedFlow.mock.calls[0][0].onClick;
await onClick(Click.Approve, "12");
await waitFor(() =>
expect(mockedFlow).toHaveBeenLastCalledWith({
phase: Phase.OutOfBandConfirmation,
failureReason: LoginWithQRFailureReason.CheckCodeMismatch,
onClick: expect.any(Function),
}),
);
});
test("reciprocates login", async () => {
const ref = createRef<LoginWithQR>();
jest.spyOn(global.window, "open");
render(getComponent({ client, ref }));
jest.spyOn(MSC4108SignInWithQR.prototype, "shareSecrets").mockResolvedValue({});
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockResolvedValue({});
jest.spyOn(MSC4108SignInWithQR.prototype, "deviceAuthorizationGrant").mockResolvedValue({
verificationUri: "mock-verification-uri",
});
await waitFor(() =>
expect(mockedFlow).toHaveBeenLastCalledWith({
phase: Phase.OutOfBandConfirmation,
onClick: expect.any(Function),
}),
);
const onClick = mockedFlow.mock.calls[0][0].onClick;
await onClick(Click.Approve);
await waitFor(() =>
expect(mockedFlow).toHaveBeenLastCalledWith({
phase: Phase.WaitingForDevice,
onClick: expect.any(Function),
}),
);
expect(global.window.open).toHaveBeenCalledWith("mock-verification-uri", "_blank");
const rendezvous = ref.current!.state.rendezvous!;
expect(rendezvous.shareSecrets).toHaveBeenCalled();
});
test("handles errors during protocol negotiation", async () => {
const ref = createRef<LoginWithQR>();
render(getComponent({ client, ref }));
jest.spyOn(MSC4108SignInWithQR.prototype, "cancel").mockResolvedValue();
const err = new RendezvousError("Unknown Failure", MSC4108FailureReason.UnsupportedProtocol);
// @ts-ignore work-around for lazy mocks
err.code = MSC4108FailureReason.UnsupportedProtocol;
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockRejectedValue(err);
await waitFor(() =>
expect(mockedFlow).toHaveBeenLastCalledWith(
expect.objectContaining({
phase: Phase.ShowingQR,
}),
),
);
await waitFor(() => {
const rendezvous = ref.current!.state.rendezvous!;
expect(rendezvous.cancel).toHaveBeenCalledWith(MSC4108FailureReason.UnsupportedProtocol);
});
});
test("handles errors during reciprocation", async () => {
render(getComponent({ client }));
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockResolvedValue({});
jest.spyOn(MSC4108SignInWithQR.prototype, "deviceAuthorizationGrant").mockResolvedValue({});
await waitFor(() =>
expect(mockedFlow).toHaveBeenLastCalledWith({
phase: Phase.OutOfBandConfirmation,
onClick: expect.any(Function),
}),
);
jest.spyOn(MSC4108SignInWithQR.prototype, "shareSecrets").mockRejectedValue(
new HTTPError("Internal Server Error", 500),
);
const onClick = mockedFlow.mock.calls[0][0].onClick;
await onClick(Click.Approve);
await waitFor(() =>
expect(mockedFlow).toHaveBeenLastCalledWith(
expect.objectContaining({
phase: Phase.Error,
failureReason: ClientRendezvousFailureReason.Unknown,
}),
),
);
});
test("handles user cancelling during reciprocation", async () => {
const ref = createRef<LoginWithQR>();
render(getComponent({ client, ref }));
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockResolvedValue({});
jest.spyOn(MSC4108SignInWithQR.prototype, "deviceAuthorizationGrant").mockResolvedValue({});
jest.spyOn(MSC4108SignInWithQR.prototype, "deviceAuthorizationGrant").mockResolvedValue({});
await waitFor(() =>
expect(mockedFlow).toHaveBeenLastCalledWith({
phase: Phase.OutOfBandConfirmation,
onClick: expect.any(Function),
}),
);
jest.spyOn(MSC4108SignInWithQR.prototype, "cancel").mockResolvedValue();
const onClick = mockedFlow.mock.calls[0][0].onClick;
await onClick(Click.Cancel);
const rendezvous = ref.current!.state.rendezvous!;
expect(rendezvous.cancel).toHaveBeenCalledWith(MSC4108FailureReason.UserCancelled);
});
});
});
@@ -0,0 +1,94 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { cleanup, fireEvent, render, screen, waitFor } from "jest-matrix-react";
import React from "react";
import { ClientRendezvousFailureReason, MSC4108FailureReason } from "matrix-js-sdk/src/rendezvous";
import LoginWithQRFlow from "../../../../../../src/components/views/auth/LoginWithQRFlow";
import { LoginWithQRFailureReason, type FailureReason } from "../../../../../../src/components/views/auth/LoginWithQR";
import { Click, Phase } from "../../../../../../src/components/views/auth/LoginWithQR-types";
describe("<LoginWithQRFlow />", () => {
const onClick = jest.fn();
const defaultProps = {
onClick,
};
const getComponent = (props: {
phase: Phase;
onClick?: () => Promise<void>;
failureReason?: FailureReason;
code?: Uint8Array;
}) => <LoginWithQRFlow {...defaultProps} {...props} />;
beforeEach(() => {});
afterEach(() => {
onClick.mockReset();
cleanup();
});
it("renders spinner while loading", async () => {
const { container } = render(getComponent({ phase: Phase.Loading }));
expect(container).toMatchSnapshot();
});
it("renders spinner whilst QR generating", async () => {
const { container } = render(getComponent({ phase: Phase.ShowingQR }));
expect(screen.getAllByTestId("spinner")).toHaveLength(1);
expect(container).toMatchSnapshot();
});
it("renders QR code", async () => {
const { container } = render(
getComponent({ phase: Phase.ShowingQR, code: new TextEncoder().encode("mock-code") }),
);
// QR code is rendered async so we wait for it:
await waitFor(() => screen.getAllByAltText("QR Code").length === 1);
expect(container).toMatchSnapshot();
});
it("renders spinner while signing in", async () => {
const { container } = render(getComponent({ phase: Phase.WaitingForDevice }));
expect(screen.getAllByTestId("cancel-button")).toHaveLength(1);
expect(container).toMatchSnapshot();
fireEvent.click(screen.getByTestId("cancel-button"));
expect(onClick).toHaveBeenCalledWith(Click.Cancel, undefined);
});
it("renders spinner while verifying", async () => {
const { container } = render(getComponent({ phase: Phase.Verifying }));
expect(container).toMatchSnapshot();
});
it("renders check code confirmation", async () => {
const { container } = render(getComponent({ phase: Phase.OutOfBandConfirmation }));
expect(container).toMatchSnapshot();
});
describe("errors", () => {
for (const failureReason of [
...Object.values(MSC4108FailureReason),
...Object.values(LoginWithQRFailureReason),
...Object.values(ClientRendezvousFailureReason),
]) {
it(`renders ${failureReason}`, async () => {
const { container } = render(
getComponent({
phase: Phase.Error,
failureReason,
}),
);
expect(screen.getAllByTestId("cancellation-message")).toHaveLength(1);
expect(container).toMatchSnapshot();
});
}
});
});
@@ -0,0 +1,87 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { render } from "jest-matrix-react";
import { mocked } from "jest-mock";
import { type IClientWellKnown, type IServerVersions, type MatrixClient } from "matrix-js-sdk/src/matrix";
import React from "react";
import fetchMock from "@fetch-mock/jest";
import LoginWithQRSection from "../../../../../../src/components/views/settings/devices/LoginWithQRSection";
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
function makeClient(wellKnown: IClientWellKnown) {
const crypto = mocked({
supportsSecretsForQrLogin: jest.fn().mockReturnValue(true),
isCrossSigningReady: jest.fn().mockReturnValue(true),
});
return mocked({
getUser: jest.fn(),
isGuest: jest.fn().mockReturnValue(false),
isUserIgnored: jest.fn(),
getUserId: jest.fn(),
on: jest.fn(),
isSynapseAdministrator: jest.fn().mockResolvedValue(false),
isRoomEncrypted: jest.fn().mockReturnValue(false),
mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
removeListener: jest.fn(),
currentState: {
on: jest.fn(),
},
getClientWellKnown: jest.fn().mockReturnValue(wellKnown),
getCrypto: jest.fn().mockReturnValue(crypto),
} as unknown as MatrixClient);
}
function makeVersions(unstableFeatures: Record<string, boolean>): IServerVersions {
return {
versions: [],
unstable_features: unstableFeatures,
};
}
describe("<LoginWithQRSection />", () => {
beforeAll(() => {
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(makeClient({}));
});
describe("MSC4108", () => {
describe("MSC4108", () => {
const defaultProps = {
onShowQr: () => {},
versions: makeVersions({ "org.matrix.msc4108": true }),
};
const getComponent = (props = {}) => <LoginWithQRSection {...defaultProps} {...props} />;
let client: MatrixClient;
beforeEach(() => {
client = makeClient({});
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(client);
});
test("no homeserver support", async () => {
const { container } = render(getComponent({ versions: makeVersions({ "org.matrix.msc4108": false }) }));
expect(container.textContent).toContain("Not supported by your account provider");
});
test("no support in crypto", async () => {
client.getCrypto()!.exportSecretsBundle = undefined;
const { container } = render(getComponent({ client }));
expect(container.textContent).toContain("Not supported by your account provider");
});
test("failed to connect", async () => {
fetchMock.catch(500);
const { container } = render(getComponent({ client }));
expect(container.textContent).toContain("Not supported by your account provider");
});
});
});
});
@@ -0,0 +1,108 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { act, fireEvent, render } from "jest-matrix-react";
import SecurityRecommendations from "../../../../../../src/components/views/settings/devices/SecurityRecommendations";
import { DeviceSecurityVariation } from "../../../../../../src/components/views/settings/devices/types";
const MS_DAY = 24 * 60 * 60 * 1000;
describe("<SecurityRecommendations />", () => {
const unverifiedNoMetadata = { device_id: "unverified-no-metadata", isVerified: false };
const verifiedNoMetadata = { device_id: "verified-no-metadata", isVerified: true };
const hundredDaysOld = { device_id: "100-days-old", isVerified: true, last_seen_ts: Date.now() - MS_DAY * 100 };
const hundredDaysOldUnverified = {
device_id: "unverified-100-days-old",
isVerified: false,
last_seen_ts: Date.now() - MS_DAY * 100,
};
const defaultProps = {
devices: {},
goToFilteredList: jest.fn(),
currentDeviceId: "abc123",
};
const getComponent = (props = {}) => <SecurityRecommendations {...defaultProps} {...props} />;
it("renders null when no devices", () => {
const { container } = render(getComponent());
expect(container.firstChild).toBeNull();
});
it("renders unverified devices section when user has unverified devices", () => {
const devices = {
[unverifiedNoMetadata.device_id]: unverifiedNoMetadata,
[verifiedNoMetadata.device_id]: verifiedNoMetadata,
[hundredDaysOldUnverified.device_id]: hundredDaysOldUnverified,
};
const { container } = render(getComponent({ devices }));
expect(container).toMatchSnapshot();
});
it("does not render unverified devices section when only the current device is unverified", () => {
const devices = {
[unverifiedNoMetadata.device_id]: unverifiedNoMetadata,
[verifiedNoMetadata.device_id]: verifiedNoMetadata,
};
const { container } = render(getComponent({ devices, currentDeviceId: unverifiedNoMetadata.device_id }));
// nothing to render
expect(container.firstChild).toBeFalsy();
});
it("renders inactive devices section when user has inactive devices", () => {
const devices = {
[verifiedNoMetadata.device_id]: verifiedNoMetadata,
[hundredDaysOldUnverified.device_id]: hundredDaysOldUnverified,
};
const { container } = render(getComponent({ devices }));
expect(container).toMatchSnapshot();
});
it("renders both cards when user has both unverified and inactive devices", () => {
const devices = {
[verifiedNoMetadata.device_id]: verifiedNoMetadata,
[hundredDaysOld.device_id]: hundredDaysOld,
[unverifiedNoMetadata.device_id]: unverifiedNoMetadata,
};
const { container } = render(getComponent({ devices }));
expect(container).toMatchSnapshot();
});
it("clicking view all unverified devices button works", () => {
const goToFilteredList = jest.fn();
const devices = {
[verifiedNoMetadata.device_id]: verifiedNoMetadata,
[hundredDaysOld.device_id]: hundredDaysOld,
[unverifiedNoMetadata.device_id]: unverifiedNoMetadata,
};
const { getByTestId } = render(getComponent({ devices, goToFilteredList }));
act(() => {
fireEvent.click(getByTestId("unverified-devices-cta"));
});
expect(goToFilteredList).toHaveBeenCalledWith(DeviceSecurityVariation.Unverified);
});
it("clicking view all inactive devices button works", () => {
const goToFilteredList = jest.fn();
const devices = {
[verifiedNoMetadata.device_id]: verifiedNoMetadata,
[hundredDaysOld.device_id]: hundredDaysOld,
[unverifiedNoMetadata.device_id]: unverifiedNoMetadata,
};
const { getByTestId } = render(getComponent({ devices, goToFilteredList }));
act(() => {
fireEvent.click(getByTestId("inactive-devices-cta"));
});
expect(goToFilteredList).toHaveBeenCalledWith(DeviceSecurityVariation.Inactive);
});
});
@@ -0,0 +1,83 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { act, fireEvent, render } from "jest-matrix-react";
import React from "react";
import SelectableDeviceTile from "../../../../../../src/components/views/settings/devices/SelectableDeviceTile";
import { DeviceType } from "../../../../../../src/utils/device/parseUserAgent";
describe("<SelectableDeviceTile />", () => {
const device = {
display_name: "My Device",
device_id: "my-device",
last_seen_ip: "123.456.789",
isVerified: false,
deviceType: DeviceType.Unknown,
};
const defaultProps = {
onSelect: jest.fn(),
onClick: jest.fn(),
device,
children: <div>test</div>,
isSelected: false,
};
const getComponent = (props = {}) => <SelectableDeviceTile {...defaultProps} {...props} />;
it("renders unselected device tile with checkbox", () => {
const { container } = render(getComponent());
expect(container).toMatchSnapshot();
});
it("renders selected tile", () => {
const { container } = render(getComponent({ isSelected: true }));
expect(container.querySelector(`#device-tile-checkbox-${device.device_id}`)).toMatchSnapshot();
});
it("calls onSelect on checkbox click", () => {
const onSelect = jest.fn();
const { container } = render(getComponent({ onSelect }));
act(() => {
fireEvent.click(container.querySelector(`#device-tile-checkbox-${device.device_id}`)!);
});
expect(onSelect).toHaveBeenCalled();
});
it("calls onClick on device tile info click", () => {
const onClick = jest.fn();
const { getByText } = render(getComponent({ onClick }));
act(() => {
fireEvent.click(getByText(device.display_name));
});
expect(onClick).toHaveBeenCalled();
});
it("does not call onClick when clicking device tiles actions", () => {
const onClick = jest.fn();
const onDeviceActionClick = jest.fn();
const children = (
<button onClick={onDeviceActionClick} data-testid="device-action-button">
test
</button>
);
const { getByTestId } = render(getComponent({ onClick, children }));
act(() => {
fireEvent.click(getByTestId("device-action-button"));
});
// action click handler called
expect(onDeviceActionClick).toHaveBeenCalled();
// main click handler not called
expect(onClick).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,549 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`<CurrentDeviceSection /> displays device details on toggle click 1`] = `
HTMLCollection [
<div
class="mx_DeviceDetails mx_CurrentDeviceSection_deviceDetails"
data-testid="device-detail-alices_device"
>
<section
class="mx_DeviceDetails_section"
>
<div
class="mx_DeviceDetailHeading"
data-testid="device-detail-heading"
>
<h4
class="mx_Heading_h4"
>
alices_device
</h4>
<div
class="mx_AccessibleButton mx_DeviceDetailHeading_renameCta mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
data-testid="device-heading-rename-cta"
role="button"
tabindex="0"
>
Rename
</div>
</div>
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Unverified"
>
<svg
fill="currentColor"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Unverified session
</p>
<p
class="mx_DeviceSecurityCard_description"
>
Verify your current session for enhanced secure messaging.
<button
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
Learn more
</button>
</p>
<div
class="mx_DeviceSecurityCard_actions"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
data-testid="verification-status-button-alices_device"
role="button"
tabindex="0"
>
Verify session
</div>
</div>
</div>
</div>
</section>
<section
class="mx_DeviceDetails_section"
>
<p
class="mx_DeviceDetails_sectionHeading"
>
Session details
</p>
<table
class="mx_DeviceDetails_metadataTable"
data-testid="device-detail-metadata-session"
>
<tbody>
<tr>
<td
class="mxDeviceDetails_metadataLabel"
>
Session ID
</td>
<td
class="mxDeviceDetails_metadataValue"
>
alices_device
</td>
</tr>
</tbody>
</table>
</section>
<section
class="mx_DeviceDetails_section"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger_inline"
data-testid="device-detail-sign-out-cta"
role="button"
tabindex="0"
>
<span
class="mx_DeviceDetails_signOutButtonContent"
>
Remove this session
</span>
</div>
</section>
</div>,
]
`;
exports[`<CurrentDeviceSection /> handles when device is falsy 1`] = `
<div>
<div
class="mx_SettingsSubsection"
data-testid="current-session-section"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Current session
</h2>
<div
aria-disabled="true"
aria-expanded="false"
aria-haspopup="true"
aria-label="Options"
class="mx_AccessibleButton mx_AccessibleButton_disabled"
data-testid="current-session-menu"
disabled=""
role="button"
tabindex="0"
>
<svg
class="mx_KebabContextMenu_icon"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
/>
</svg>
</div>
</div>
<div
class="mx_SettingsSubsection_content"
/>
</div>
</div>
`;
exports[`<CurrentDeviceSection /> renders device and correct security card when device is unverified 1`] = `
<div>
<div
class="mx_SettingsSubsection"
data-testid="current-session-section"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Current session
</h2>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="Options"
class="mx_AccessibleButton"
data-testid="current-session-menu"
role="button"
tabindex="0"
>
<svg
class="mx_KebabContextMenu_icon"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
/>
</svg>
</div>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div
class="mx_DeviceTile mx_DeviceTile_interactive"
data-testid="device-tile-alices_device"
>
<div
class="mx_DeviceTypeIcon"
>
<div
class="mx_DeviceTypeIcon_deviceIconWrapper"
>
<svg
aria-label="Unknown session type"
class="mx_DeviceTypeIcon_deviceIcon"
fill="currentColor"
height="1em"
role="img"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3.5 20q-.625 0-1.062-.437A1.45 1.45 0 0 1 2 18.5q0-.625.438-1.062A1.45 1.45 0 0 1 3.5 17H4V6q0-.824.588-1.412A1.93 1.93 0 0 1 6 4h14q.424 0 .712.287Q21 4.576 21 5t-.288.713A.97.97 0 0 1 20 6H6v11h4.5q.624 0 1.063.438.437.437.437 1.062t-.437 1.063A1.45 1.45 0 0 1 10.5 20zM15 20a.97.97 0 0 1-.713-.288A.97.97 0 0 1 14 19V9q0-.424.287-.713A.97.97 0 0 1 15 8h6q.424 0 .712.287Q22 8.576 22 9v10q0 .424-.288.712A.97.97 0 0 1 21 20zm1-3h4v-7h-4z"
/>
</svg>
</div>
<svg
aria-label="Unverified"
class="mx_DeviceTypeIcon_verificationIcon unverified"
fill="currentColor"
height="1em"
role="img"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<div
class="mx_DeviceTile_info"
>
<h3
class="mx_Heading_h4"
>
alices_device
</h3>
<div
class="mx_DeviceTile_metadata"
>
<span
data-testid="device-metadata-isVerified"
>
Unverified
</span>
·
<span
data-testid="device-metadata-deviceId"
>
alices_device
</span>
</div>
</div>
<div
class="mx_DeviceTile_actions"
>
<div
aria-label="Show details"
class="mx_AccessibleButton mx_DeviceExpandDetailsButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_icon"
data-testid="current-session-toggle-details"
role="button"
tabindex="0"
>
<svg
class="mx_DeviceExpandDetailsButton_icon"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
</div>
</div>
</div>
<br />
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Unverified"
>
<svg
fill="currentColor"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Unverified session
</p>
<p
class="mx_DeviceSecurityCard_description"
>
Verify your current session for enhanced secure messaging.
<button
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
Learn more
</button>
</p>
<div
class="mx_DeviceSecurityCard_actions"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
data-testid="verification-status-button-alices_device"
role="button"
tabindex="0"
>
Verify session
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`<CurrentDeviceSection /> renders device and correct security card when device is verified 1`] = `
<div>
<div
class="mx_SettingsSubsection"
data-testid="current-session-section"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Current session
</h2>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="Options"
class="mx_AccessibleButton"
data-testid="current-session-menu"
role="button"
tabindex="0"
>
<svg
class="mx_KebabContextMenu_icon"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
/>
</svg>
</div>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div
class="mx_DeviceTile mx_DeviceTile_interactive"
data-testid="device-tile-alices_device"
>
<div
class="mx_DeviceTypeIcon"
>
<div
class="mx_DeviceTypeIcon_deviceIconWrapper"
>
<svg
aria-label="Unknown session type"
class="mx_DeviceTypeIcon_deviceIcon"
fill="currentColor"
height="1em"
role="img"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3.5 20q-.625 0-1.062-.437A1.45 1.45 0 0 1 2 18.5q0-.625.438-1.062A1.45 1.45 0 0 1 3.5 17H4V6q0-.824.588-1.412A1.93 1.93 0 0 1 6 4h14q.424 0 .712.287Q21 4.576 21 5t-.288.713A.97.97 0 0 1 20 6H6v11h4.5q.624 0 1.063.438.437.437.437 1.062t-.437 1.063A1.45 1.45 0 0 1 10.5 20zM15 20a.97.97 0 0 1-.713-.288A.97.97 0 0 1 14 19V9q0-.424.287-.713A.97.97 0 0 1 15 8h6q.424 0 .712.287Q22 8.576 22 9v10q0 .424-.288.712A.97.97 0 0 1 21 20zm1-3h4v-7h-4z"
/>
</svg>
</div>
<svg
aria-label="Unverified"
class="mx_DeviceTypeIcon_verificationIcon unverified"
fill="currentColor"
height="1em"
role="img"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<div
class="mx_DeviceTile_info"
>
<h3
class="mx_Heading_h4"
>
alices_device
</h3>
<div
class="mx_DeviceTile_metadata"
>
<span
data-testid="device-metadata-isVerified"
>
Unverified
</span>
·
<span
data-testid="device-metadata-deviceId"
>
alices_device
</span>
</div>
</div>
<div
class="mx_DeviceTile_actions"
>
<div
aria-label="Show details"
class="mx_AccessibleButton mx_DeviceExpandDetailsButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_icon"
data-testid="current-session-toggle-details"
role="button"
tabindex="0"
>
<svg
class="mx_DeviceExpandDetailsButton_icon"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
</div>
</div>
</div>
<br />
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Unverified"
>
<svg
fill="currentColor"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Unverified session
</p>
<p
class="mx_DeviceSecurityCard_description"
>
Verify your current session for enhanced secure messaging.
<button
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
Learn more
</button>
</p>
<div
class="mx_DeviceSecurityCard_actions"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
data-testid="verification-status-button-alices_device"
role="button"
tabindex="0"
>
Verify session
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
@@ -0,0 +1,97 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`<DeviceDetailHeading /> displays name edit form on rename button click 1`] = `
{
"container": <div>
<form
aria-disabled="false"
class="mx_DeviceDetailHeading_renameForm"
method="post"
>
<p
class="mx_DeviceDetailHeading_renameFormHeading"
id="device-rename-123"
>
Rename session
</p>
<div>
<div
class="mx_Field mx_Field_input mx_DeviceDetailHeading_renameFormInput"
>
<input
aria-describedby="device-rename-description-123"
aria-labelledby="device-rename-123"
autocomplete="off"
data-testid="device-rename-input"
id="mx_Field_1"
maxlength="100"
type="text"
value="My device"
/>
<label
for="mx_Field_1"
/>
</div>
<span
class="mx_Caption"
id="device-rename-description-123"
>
Please be aware that session names are also visible to people you communicate with.
<button
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
Learn more
</button>
</span>
</div>
<div
class="mx_DeviceDetailHeading_renameFormButtons"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
data-testid="device-rename-submit-cta"
role="button"
tabindex="0"
>
Save
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary"
data-testid="device-rename-cancel-cta"
role="button"
tabindex="0"
>
Cancel
</div>
</div>
</form>
</div>,
}
`;
exports[`<DeviceDetailHeading /> renders device name 1`] = `
{
"container": <div>
<div
class="mx_DeviceDetailHeading"
data-testid="device-detail-heading"
>
<h4
class="mx_Heading_h4"
>
My device
</h4>
<div
class="mx_AccessibleButton mx_DeviceDetailHeading_renameCta mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
data-testid="device-heading-rename-cta"
role="button"
tabindex="0"
>
Rename
</div>
</div>
</div>,
}
`;
@@ -0,0 +1,451 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`<DeviceDetails /> renders a verified device 1`] = `
<div>
<div
class="mx_DeviceDetails"
data-testid="device-detail-my-device"
>
<section
class="mx_DeviceDetails_section"
>
<div
class="mx_DeviceDetailHeading"
data-testid="device-detail-heading"
>
<h4
class="mx_Heading_h4"
>
my-device
</h4>
<div
class="mx_AccessibleButton mx_DeviceDetailHeading_renameCta mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
data-testid="device-heading-rename-cta"
role="button"
tabindex="0"
>
Rename
</div>
</div>
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Verified"
>
<svg
fill="currentColor"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M11.106 2.447a2 2 0 0 1 1.789 0l6 3A2 2 0 0 1 20 7.237V12c0 6.742-5.773 9.246-7.51 9.846-.32.111-.66.111-.98 0C9.774 21.246 4 18.743 4 12V7.236a2 2 0 0 1 1.105-1.789zm4.601 6.846a1 1 0 0 0-1.414 0L11 12.586l-1.293-1.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4a1 1 0 0 0 0-1.414"
fill-rule="evenodd"
/>
</svg>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Verified session
</p>
<p
class="mx_DeviceSecurityCard_description"
>
This session is ready for secure messaging.
<button
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
Learn more
</button>
</p>
</div>
</div>
</section>
<section
class="mx_DeviceDetails_section"
>
<p
class="mx_DeviceDetails_sectionHeading"
>
Session details
</p>
<table
class="mx_DeviceDetails_metadataTable"
data-testid="device-detail-metadata-session"
>
<tbody>
<tr>
<td
class="mxDeviceDetails_metadataLabel"
>
Session ID
</td>
<td
class="mxDeviceDetails_metadataValue"
>
my-device
</td>
</tr>
</tbody>
</table>
</section>
<section
class="mx_DeviceDetails_section"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger_inline"
data-testid="device-detail-sign-out-cta"
role="button"
tabindex="0"
>
<span
class="mx_DeviceDetails_signOutButtonContent"
>
Remove this session
</span>
</div>
</section>
</div>
</div>
`;
exports[`<DeviceDetails /> renders device with metadata 1`] = `
<div>
<div
class="mx_DeviceDetails"
data-testid="device-detail-my-device"
>
<section
class="mx_DeviceDetails_section"
>
<div
class="mx_DeviceDetailHeading"
data-testid="device-detail-heading"
>
<h4
class="mx_Heading_h4"
>
My Device
</h4>
<div
class="mx_AccessibleButton mx_DeviceDetailHeading_renameCta mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
data-testid="device-heading-rename-cta"
role="button"
tabindex="0"
>
Rename
</div>
</div>
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Unverified"
>
<svg
fill="currentColor"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Unverified session
</p>
<p
class="mx_DeviceSecurityCard_description"
>
Verify or remove this session for best security and reliability.
<button
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
Learn more
</button>
</p>
</div>
</div>
</section>
<section
class="mx_DeviceDetails_section"
>
<p
class="mx_DeviceDetails_sectionHeading"
>
Session details
</p>
<table
class="mx_DeviceDetails_metadataTable"
data-testid="device-detail-metadata-session"
>
<tbody>
<tr>
<td
class="mxDeviceDetails_metadataLabel"
>
Session ID
</td>
<td
class="mxDeviceDetails_metadataValue"
>
my-device
</td>
</tr>
<tr>
<td
class="mxDeviceDetails_metadataLabel"
>
Last activity
</td>
<td
class="mxDeviceDetails_metadataValue"
>
Sun 22:34
</td>
</tr>
</tbody>
</table>
<table
class="mx_DeviceDetails_metadataTable"
data-testid="device-detail-metadata-application"
>
<thead>
<tr>
<th>
Application
</th>
</tr>
</thead>
<tbody>
<tr>
<td
class="mxDeviceDetails_metadataLabel"
>
Name
</td>
<td
class="mxDeviceDetails_metadataValue"
>
Element Web
</td>
</tr>
</tbody>
</table>
<table
class="mx_DeviceDetails_metadataTable"
data-testid="device-detail-metadata-device"
>
<thead>
<tr>
<th>
Device
</th>
</tr>
</thead>
<tbody>
<tr>
<td
class="mxDeviceDetails_metadataLabel"
>
Model
</td>
<td
class="mxDeviceDetails_metadataValue"
>
Iphone X
</td>
</tr>
<tr>
<td
class="mxDeviceDetails_metadataLabel"
>
Operating system
</td>
<td
class="mxDeviceDetails_metadataValue"
>
Windows 95
</td>
</tr>
<tr>
<td
class="mxDeviceDetails_metadataLabel"
>
Browser
</td>
<td
class="mxDeviceDetails_metadataValue"
>
Firefox 100
</td>
</tr>
<tr>
<td
class="mxDeviceDetails_metadataLabel"
>
IP address
</td>
<td
class="mxDeviceDetails_metadataValue"
>
123.456.789
</td>
</tr>
</tbody>
</table>
</section>
<section
class="mx_DeviceDetails_section"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger_inline"
data-testid="device-detail-sign-out-cta"
role="button"
tabindex="0"
>
<span
class="mx_DeviceDetails_signOutButtonContent"
>
Remove this session
</span>
</div>
</section>
</div>
</div>
`;
exports[`<DeviceDetails /> renders device without metadata 1`] = `
<div>
<div
class="mx_DeviceDetails"
data-testid="device-detail-my-device"
>
<section
class="mx_DeviceDetails_section"
>
<div
class="mx_DeviceDetailHeading"
data-testid="device-detail-heading"
>
<h4
class="mx_Heading_h4"
>
my-device
</h4>
<div
class="mx_AccessibleButton mx_DeviceDetailHeading_renameCta mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
data-testid="device-heading-rename-cta"
role="button"
tabindex="0"
>
Rename
</div>
</div>
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Unverified"
>
<svg
fill="currentColor"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Unverified session
</p>
<p
class="mx_DeviceSecurityCard_description"
>
Verify or remove this session for best security and reliability.
<button
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
Learn more
</button>
</p>
</div>
</div>
</section>
<section
class="mx_DeviceDetails_section"
>
<p
class="mx_DeviceDetails_sectionHeading"
>
Session details
</p>
<table
class="mx_DeviceDetails_metadataTable"
data-testid="device-detail-metadata-session"
>
<tbody>
<tr>
<td
class="mxDeviceDetails_metadataLabel"
>
Session ID
</td>
<td
class="mxDeviceDetails_metadataValue"
>
my-device
</td>
</tr>
</tbody>
</table>
</section>
<section
class="mx_DeviceDetails_section"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger_inline"
data-testid="device-detail-sign-out-cta"
role="button"
tabindex="0"
>
<span
class="mx_DeviceDetails_signOutButtonContent"
>
Remove this session
</span>
</div>
</section>
</div>
</div>
`;
@@ -0,0 +1,53 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`<DeviceExpandDetailsButton /> renders when expanded 1`] = `
{
"container": <div>
<div
aria-label="Hide details"
class="mx_AccessibleButton mx_DeviceExpandDetailsButton mx_DeviceExpandDetailsButton_expanded mx_AccessibleButton_hasKind mx_AccessibleButton_kind_icon"
role="button"
tabindex="0"
>
<svg
class="mx_DeviceExpandDetailsButton_icon"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
</div>
</div>,
}
`;
exports[`<DeviceExpandDetailsButton /> renders when not expanded 1`] = `
{
"container": <div>
<div
aria-label="Show details"
class="mx_AccessibleButton mx_DeviceExpandDetailsButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_icon"
role="button"
tabindex="0"
>
<svg
class="mx_DeviceExpandDetailsButton_icon"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
</div>
</div>,
}
`;
@@ -0,0 +1,86 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`<DeviceSecurityCard /> renders basic card 1`] = `
<div>
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Verified"
>
<svg
fill="currentColor"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M11.106 2.447a2 2 0 0 1 1.789 0l6 3A2 2 0 0 1 20 7.237V12c0 6.742-5.773 9.246-7.51 9.846-.32.111-.66.111-.98 0C9.774 21.246 4 18.743 4 12V7.236a2 2 0 0 1 1.105-1.789zm4.601 6.846a1 1 0 0 0-1.414 0L11 12.586l-1.293-1.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4a1 1 0 0 0 0-1.414"
fill-rule="evenodd"
/>
</svg>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Verified session
</p>
<p
class="mx_DeviceSecurityCard_description"
>
nice
</p>
</div>
</div>
</div>
`;
exports[`<DeviceSecurityCard /> renders with children 1`] = `
<div>
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Unverified"
>
<svg
fill="currentColor"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Verified session
</p>
<p
class="mx_DeviceSecurityCard_description"
>
nice
</p>
<div
class="mx_DeviceSecurityCard_actions"
>
<div>
hey
</div>
</div>
</div>
</div>
</div>
`;
@@ -0,0 +1,305 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`<DeviceTile /> renders a device with no metadata 1`] = `
<div>
<div
class="mx_DeviceTile"
data-testid="device-tile-123"
>
<div
class="mx_DeviceTypeIcon"
>
<div
class="mx_DeviceTypeIcon_deviceIconWrapper"
>
<svg
aria-label="Unknown session type"
class="mx_DeviceTypeIcon_deviceIcon"
fill="currentColor"
height="1em"
role="img"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3.5 20q-.625 0-1.062-.437A1.45 1.45 0 0 1 2 18.5q0-.625.438-1.062A1.45 1.45 0 0 1 3.5 17H4V6q0-.824.588-1.412A1.93 1.93 0 0 1 6 4h14q.424 0 .712.287Q21 4.576 21 5t-.288.713A.97.97 0 0 1 20 6H6v11h4.5q.624 0 1.063.438.437.437.437 1.062t-.437 1.063A1.45 1.45 0 0 1 10.5 20zM15 20a.97.97 0 0 1-.713-.288A.97.97 0 0 1 14 19V9q0-.424.287-.713A.97.97 0 0 1 15 8h6q.424 0 .712.287Q22 8.576 22 9v10q0 .424-.288.712A.97.97 0 0 1 21 20zm1-3h4v-7h-4z"
/>
</svg>
</div>
<svg
aria-label="Unverified"
class="mx_DeviceTypeIcon_verificationIcon unverified"
fill="currentColor"
height="1em"
role="img"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<div
class="mx_DeviceTile_info"
>
<h3
class="mx_Heading_h4"
>
123
</h3>
<div
class="mx_DeviceTile_metadata"
>
<span
data-testid="device-metadata-isVerified"
>
Unverified
</span>
·
<span
data-testid="device-metadata-deviceId"
>
123
</span>
</div>
</div>
<div
class="mx_DeviceTile_actions"
/>
</div>
</div>
`;
exports[`<DeviceTile /> renders a verified device with no metadata 1`] = `
<div>
<div
class="mx_DeviceTile"
data-testid="device-tile-123"
>
<div
class="mx_DeviceTypeIcon"
>
<div
class="mx_DeviceTypeIcon_deviceIconWrapper"
>
<svg
aria-label="Unknown session type"
class="mx_DeviceTypeIcon_deviceIcon"
fill="currentColor"
height="1em"
role="img"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3.5 20q-.625 0-1.062-.437A1.45 1.45 0 0 1 2 18.5q0-.625.438-1.062A1.45 1.45 0 0 1 3.5 17H4V6q0-.824.588-1.412A1.93 1.93 0 0 1 6 4h14q.424 0 .712.287Q21 4.576 21 5t-.288.713A.97.97 0 0 1 20 6H6v11h4.5q.624 0 1.063.438.437.437.437 1.062t-.437 1.063A1.45 1.45 0 0 1 10.5 20zM15 20a.97.97 0 0 1-.713-.288A.97.97 0 0 1 14 19V9q0-.424.287-.713A.97.97 0 0 1 15 8h6q.424 0 .712.287Q22 8.576 22 9v10q0 .424-.288.712A.97.97 0 0 1 21 20zm1-3h4v-7h-4z"
/>
</svg>
</div>
<svg
aria-label="Unverified"
class="mx_DeviceTypeIcon_verificationIcon unverified"
fill="currentColor"
height="1em"
role="img"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<div
class="mx_DeviceTile_info"
>
<h3
class="mx_Heading_h4"
>
123
</h3>
<div
class="mx_DeviceTile_metadata"
>
<span
data-testid="device-metadata-isVerified"
>
Unverified
</span>
·
<span
data-testid="device-metadata-deviceId"
>
123
</span>
</div>
</div>
<div
class="mx_DeviceTile_actions"
/>
</div>
</div>
`;
exports[`<DeviceTile /> renders display name with a tooltip 1`] = `
<div>
<div
class="mx_DeviceTile"
data-testid="device-tile-123"
>
<div
class="mx_DeviceTypeIcon"
>
<div
class="mx_DeviceTypeIcon_deviceIconWrapper"
>
<svg
aria-label="Unknown session type"
class="mx_DeviceTypeIcon_deviceIcon"
fill="currentColor"
height="1em"
role="img"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3.5 20q-.625 0-1.062-.437A1.45 1.45 0 0 1 2 18.5q0-.625.438-1.062A1.45 1.45 0 0 1 3.5 17H4V6q0-.824.588-1.412A1.93 1.93 0 0 1 6 4h14q.424 0 .712.287Q21 4.576 21 5t-.288.713A.97.97 0 0 1 20 6H6v11h4.5q.624 0 1.063.438.437.437.437 1.062t-.437 1.063A1.45 1.45 0 0 1 10.5 20zM15 20a.97.97 0 0 1-.713-.288A.97.97 0 0 1 14 19V9q0-.424.287-.713A.97.97 0 0 1 15 8h6q.424 0 .712.287Q22 8.576 22 9v10q0 .424-.288.712A.97.97 0 0 1 21 20zm1-3h4v-7h-4z"
/>
</svg>
</div>
<svg
aria-label="Unverified"
class="mx_DeviceTypeIcon_verificationIcon unverified"
fill="currentColor"
height="1em"
role="img"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<div
class="mx_DeviceTile_info"
>
<h3
class="mx_Heading_h4"
>
My device
</h3>
<div
class="mx_DeviceTile_metadata"
>
<span
data-testid="device-metadata-isVerified"
>
Unverified
</span>
·
<span
data-testid="device-metadata-deviceId"
>
123
</span>
</div>
</div>
<div
class="mx_DeviceTile_actions"
/>
</div>
</div>
`;
exports[`<DeviceTile /> separates metadata with a dot 1`] = `
<div>
<div
class="mx_DeviceTile"
data-testid="device-tile-123"
>
<div
class="mx_DeviceTypeIcon"
>
<div
class="mx_DeviceTypeIcon_deviceIconWrapper"
>
<svg
aria-label="Unknown session type"
class="mx_DeviceTypeIcon_deviceIcon"
fill="currentColor"
height="1em"
role="img"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3.5 20q-.625 0-1.062-.437A1.45 1.45 0 0 1 2 18.5q0-.625.438-1.062A1.45 1.45 0 0 1 3.5 17H4V6q0-.824.588-1.412A1.93 1.93 0 0 1 6 4h14q.424 0 .712.287Q21 4.576 21 5t-.288.713A.97.97 0 0 1 20 6H6v11h4.5q.624 0 1.063.438.437.437.437 1.062t-.437 1.063A1.45 1.45 0 0 1 10.5 20zM15 20a.97.97 0 0 1-.713-.288A.97.97 0 0 1 14 19V9q0-.424.287-.713A.97.97 0 0 1 15 8h6q.424 0 .712.287Q22 8.576 22 9v10q0 .424-.288.712A.97.97 0 0 1 21 20zm1-3h4v-7h-4z"
/>
</svg>
</div>
<svg
aria-label="Unverified"
class="mx_DeviceTypeIcon_verificationIcon unverified"
fill="currentColor"
height="1em"
role="img"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<div
class="mx_DeviceTile_info"
>
<h3
class="mx_Heading_h4"
>
123
</h3>
<div
class="mx_DeviceTile_metadata"
>
<span
data-testid="device-metadata-isVerified"
>
Unverified
</span>
·
<span
data-testid="device-metadata-lastActivity"
>
Last activity 15:13
</span>
·
<span
data-testid="device-metadata-lastSeenIp"
>
1.2.3.4
</span>
·
<span
data-testid="device-metadata-deviceId"
>
123
</span>
</div>
</div>
<div
class="mx_DeviceTile_actions"
/>
</div>
</div>
`;
@@ -0,0 +1,126 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`<DeviceTypeIcon /> renders a verified device 1`] = `
<div>
<div
class="mx_DeviceTypeIcon"
>
<div
class="mx_DeviceTypeIcon_deviceIconWrapper"
>
<svg
aria-label="Unknown session type"
class="mx_DeviceTypeIcon_deviceIcon"
fill="currentColor"
height="1em"
role="img"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3.5 20q-.625 0-1.062-.437A1.45 1.45 0 0 1 2 18.5q0-.625.438-1.062A1.45 1.45 0 0 1 3.5 17H4V6q0-.824.588-1.412A1.93 1.93 0 0 1 6 4h14q.424 0 .712.287Q21 4.576 21 5t-.288.713A.97.97 0 0 1 20 6H6v11h4.5q.624 0 1.063.438.437.437.437 1.062t-.437 1.063A1.45 1.45 0 0 1 10.5 20zM15 20a.97.97 0 0 1-.713-.288A.97.97 0 0 1 14 19V9q0-.424.287-.713A.97.97 0 0 1 15 8h6q.424 0 .712.287Q22 8.576 22 9v10q0 .424-.288.712A.97.97 0 0 1 21 20zm1-3h4v-7h-4z"
/>
</svg>
</div>
<svg
aria-label="Verified"
class="mx_DeviceTypeIcon_verificationIcon verified"
fill="currentColor"
height="1em"
role="img"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M11.106 2.447a2 2 0 0 1 1.789 0l6 3A2 2 0 0 1 20 7.237V12c0 6.742-5.773 9.246-7.51 9.846-.32.111-.66.111-.98 0C9.774 21.246 4 18.743 4 12V7.236a2 2 0 0 1 1.105-1.789zm4.601 6.846a1 1 0 0 0-1.414 0L11 12.586l-1.293-1.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4a1 1 0 0 0 0-1.414"
fill-rule="evenodd"
/>
</svg>
</div>
</div>
`;
exports[`<DeviceTypeIcon /> renders an unverified device 1`] = `
<div>
<div
class="mx_DeviceTypeIcon"
>
<div
class="mx_DeviceTypeIcon_deviceIconWrapper"
>
<svg
aria-label="Unknown session type"
class="mx_DeviceTypeIcon_deviceIcon"
fill="currentColor"
height="1em"
role="img"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3.5 20q-.625 0-1.062-.437A1.45 1.45 0 0 1 2 18.5q0-.625.438-1.062A1.45 1.45 0 0 1 3.5 17H4V6q0-.824.588-1.412A1.93 1.93 0 0 1 6 4h14q.424 0 .712.287Q21 4.576 21 5t-.288.713A.97.97 0 0 1 20 6H6v11h4.5q.624 0 1.063.438.437.437.437 1.062t-.437 1.063A1.45 1.45 0 0 1 10.5 20zM15 20a.97.97 0 0 1-.713-.288A.97.97 0 0 1 14 19V9q0-.424.287-.713A.97.97 0 0 1 15 8h6q.424 0 .712.287Q22 8.576 22 9v10q0 .424-.288.712A.97.97 0 0 1 21 20zm1-3h4v-7h-4z"
/>
</svg>
</div>
<svg
aria-label="Unverified"
class="mx_DeviceTypeIcon_verificationIcon unverified"
fill="currentColor"
height="1em"
role="img"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
</div>
`;
exports[`<DeviceTypeIcon /> renders correctly when selected 1`] = `
<div>
<div
class="mx_DeviceTypeIcon mx_DeviceTypeIcon_selected"
>
<div
class="mx_DeviceTypeIcon_deviceIconWrapper"
>
<svg
aria-label="Unknown session type"
class="mx_DeviceTypeIcon_deviceIcon"
fill="currentColor"
height="1em"
role="img"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3.5 20q-.625 0-1.062-.437A1.45 1.45 0 0 1 2 18.5q0-.625.438-1.062A1.45 1.45 0 0 1 3.5 17H4V6q0-.824.588-1.412A1.93 1.93 0 0 1 6 4h14q.424 0 .712.287Q21 4.576 21 5t-.288.713A.97.97 0 0 1 20 6H6v11h4.5q.624 0 1.063.438.437.437.437 1.062t-.437 1.063A1.45 1.45 0 0 1 10.5 20zM15 20a.97.97 0 0 1-.713-.288A.97.97 0 0 1 14 19V9q0-.424.287-.713A.97.97 0 0 1 15 8h6q.424 0 .712.287Q22 8.576 22 9v10q0 .424-.288.712A.97.97 0 0 1 21 20zm1-3h4v-7h-4z"
/>
</svg>
</div>
<svg
aria-label="Unverified"
class="mx_DeviceTypeIcon_verificationIcon unverified"
fill="currentColor"
height="1em"
role="img"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
</div>
`;
@@ -0,0 +1,150 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`<DeviceVerificationStatusCard /> renders a verified device 1`] = `
<div>
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Verified"
>
<svg
fill="currentColor"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M11.106 2.447a2 2 0 0 1 1.789 0l6 3A2 2 0 0 1 20 7.237V12c0 6.742-5.773 9.246-7.51 9.846-.32.111-.66.111-.98 0C9.774 21.246 4 18.743 4 12V7.236a2 2 0 0 1 1.105-1.789zm4.601 6.846a1 1 0 0 0-1.414 0L11 12.586l-1.293-1.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4a1 1 0 0 0 0-1.414"
fill-rule="evenodd"
/>
</svg>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Verified session
</p>
<p
class="mx_DeviceSecurityCard_description"
>
This session is ready for secure messaging.
<button
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
Learn more
</button>
</p>
</div>
</div>
</div>
`;
exports[`<DeviceVerificationStatusCard /> renders an unverifiable device 1`] = `
<div>
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Unverified"
>
<svg
fill="currentColor"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Unverified session
</p>
<p
class="mx_DeviceSecurityCard_description"
>
This session doesn't support encryption and thus can't be verified.
<button
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
Learn more
</button>
</p>
</div>
</div>
</div>
`;
exports[`<DeviceVerificationStatusCard /> renders an unverified device 1`] = `
<div>
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Unverified"
>
<svg
fill="currentColor"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Unverified session
</p>
<p
class="mx_DeviceSecurityCard_description"
>
Verify or remove this session for best security and reliability.
<button
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
Learn more
</button>
</p>
<div
class="mx_DeviceSecurityCard_actions"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
data-testid="verification-status-button-test-device"
role="button"
tabindex="0"
>
Verify session
</div>
</div>
</div>
</div>
</div>
`;
@@ -0,0 +1,216 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`<FilteredDeviceList /> displays no results message when there are no devices 1`] = `
HTMLCollection [
<div
class="mx_FilteredDeviceList_noResults"
>
No sessions found.
</div>,
]
`;
exports[`<FilteredDeviceList /> filtering filters correctly for Inactive 1`] = `
HTMLCollection [
<div
class="mx_FilteredDeviceList_securityCard"
>
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Inactive"
>
<div
height="16"
width="16"
/>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Inactive sessions
</p>
<p
class="mx_DeviceSecurityCard_description"
>
<span>
Consider removing old sessions (90 days or older) you don't use anymore.
<button
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
Learn more
</button>
</span>
</p>
</div>
</div>
</div>,
]
`;
exports[`<FilteredDeviceList /> filtering filters correctly for Unverified 1`] = `
HTMLCollection [
<div
class="mx_FilteredDeviceList_securityCard"
>
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Unverified"
>
<svg
fill="currentColor"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Unverified sessions
</p>
<p
class="mx_DeviceSecurityCard_description"
>
<span>
Verify your sessions for enhanced secure messaging or remove from those you don't recognize or use anymore.
<button
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
Learn more
</button>
</span>
</p>
</div>
</div>
</div>,
]
`;
exports[`<FilteredDeviceList /> filtering filters correctly for Verified 1`] = `
HTMLCollection [
<div
class="mx_FilteredDeviceList_securityCard"
>
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Verified"
>
<svg
fill="currentColor"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M11.106 2.447a2 2 0 0 1 1.789 0l6 3A2 2 0 0 1 20 7.237V12c0 6.742-5.773 9.246-7.51 9.846-.32.111-.66.111-.98 0C9.774 21.246 4 18.743 4 12V7.236a2 2 0 0 1 1.105-1.789zm4.601 6.846a1 1 0 0 0-1.414 0L11 12.586l-1.293-1.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4a1 1 0 0 0 0-1.414"
fill-rule="evenodd"
/>
</svg>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Verified sessions
</p>
<p
class="mx_DeviceSecurityCard_description"
>
<span>
For best security, remove any session that you don't recognize or use anymore.
<button
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
Learn more
</button>
</span>
</p>
</div>
</div>
</div>,
]
`;
exports[`<FilteredDeviceList /> filtering renders no results correctly for Inactive 1`] = `
HTMLCollection [
<div
class="mx_FilteredDeviceList_noResults"
>
No inactive sessions found.
 
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
data-testid="devices-clear-filter-btn"
role="button"
tabindex="0"
>
Show all
</div>
</div>,
]
`;
exports[`<FilteredDeviceList /> filtering renders no results correctly for Unverified 1`] = `
HTMLCollection [
<div
class="mx_FilteredDeviceList_noResults"
>
No unverified sessions found.
 
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
data-testid="devices-clear-filter-btn"
role="button"
tabindex="0"
>
Show all
</div>
</div>,
]
`;
exports[`<FilteredDeviceList /> filtering renders no results correctly for Verified 1`] = `
HTMLCollection [
<div
class="mx_FilteredDeviceList_noResults"
>
No verified sessions found.
 
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
data-testid="devices-clear-filter-btn"
role="button"
tabindex="0"
>
Show all
</div>
</div>,
]
`;
@@ -0,0 +1,132 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`<FilteredDeviceListHeader /> renders correctly when all devices are selected 1`] = `
<div>
<div
class="mx_FilteredDeviceListHeader"
data-testid="test123"
>
<span
tabindex="0"
>
<form
class="_root_19upo_16"
>
<div
class="_inline-field_19upo_32"
>
<div
class="_inline-field-control_19upo_44"
>
<div
class="_container_153f2_10"
>
<input
aria-label="Deselect all"
aria-labelledby="_r_9_"
checked=""
class="_input_153f2_18"
data-testid="device-select-all-checkbox"
id="device-select-all-checkbox"
type="checkbox"
/>
<div
class="_ui_153f2_19"
>
<svg
aria-hidden="true"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
</div>
</div>
</div>
<div
class="_inline-field-body_19upo_38"
/>
</div>
</form>
</span>
<span
class="mx_FilteredDeviceListHeader_label"
>
Sessions
</span>
<div>
test
</div>
</div>
</div>
`;
exports[`<FilteredDeviceListHeader /> renders correctly when no devices are selected 1`] = `
<div>
<div
class="mx_FilteredDeviceListHeader"
data-testid="test123"
>
<span
tabindex="0"
>
<form
class="_root_19upo_16"
>
<div
class="_inline-field_19upo_32"
>
<div
class="_inline-field-control_19upo_44"
>
<div
class="_container_153f2_10"
>
<input
aria-label="Select all"
aria-labelledby="_r_0_"
class="_input_153f2_18"
data-testid="device-select-all-checkbox"
id="device-select-all-checkbox"
type="checkbox"
/>
<div
class="_ui_153f2_19"
>
<svg
aria-hidden="true"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
</div>
</div>
</div>
<div
class="_inline-field-body_19upo_38"
/>
</div>
</form>
</span>
<span
class="mx_FilteredDeviceListHeader_label"
>
Sessions
</span>
<div>
test
</div>
</div>
</div>
`;
@@ -0,0 +1,397 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`<SecurityRecommendations /> renders both cards when user has both unverified and inactive devices 1`] = `
<div>
<div
class="mx_SettingsSubsection"
data-testid="security-recommendations-section"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Security recommendations
</h2>
</div>
<div
class="mx_SettingsSubsection_description"
>
<div
class="mx_SettingsSubsection_text"
>
Improve your account security by following these recommendations.
</div>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Unverified"
>
<svg
fill="currentColor"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Unverified sessions
</p>
<p
class="mx_DeviceSecurityCard_description"
>
Verify your sessions for enhanced secure messaging or remove from those you don't recognize or use anymore.
<button
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
Learn more
</button>
</p>
<div
class="mx_DeviceSecurityCard_actions"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
data-testid="unverified-devices-cta"
role="button"
tabindex="0"
>
View all (1)
</div>
</div>
</div>
</div>
<div
class="mx_SecurityRecommendations_spacing"
/>
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Inactive"
>
<div
height="16"
width="16"
/>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Inactive sessions
</p>
<p
class="mx_DeviceSecurityCard_description"
>
Consider removing old sessions (90 days or older) you don't use anymore.
<button
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
Learn more
</button>
</p>
<div
class="mx_DeviceSecurityCard_actions"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
data-testid="inactive-devices-cta"
role="button"
tabindex="0"
>
View all (1)
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`<SecurityRecommendations /> renders inactive devices section when user has inactive devices 1`] = `
<div>
<div
class="mx_SettingsSubsection"
data-testid="security-recommendations-section"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Security recommendations
</h2>
</div>
<div
class="mx_SettingsSubsection_description"
>
<div
class="mx_SettingsSubsection_text"
>
Improve your account security by following these recommendations.
</div>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Unverified"
>
<svg
fill="currentColor"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Unverified sessions
</p>
<p
class="mx_DeviceSecurityCard_description"
>
Verify your sessions for enhanced secure messaging or remove from those you don't recognize or use anymore.
<button
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
Learn more
</button>
</p>
<div
class="mx_DeviceSecurityCard_actions"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
data-testid="unverified-devices-cta"
role="button"
tabindex="0"
>
View all (1)
</div>
</div>
</div>
</div>
<div
class="mx_SecurityRecommendations_spacing"
/>
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Inactive"
>
<div
height="16"
width="16"
/>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Inactive sessions
</p>
<p
class="mx_DeviceSecurityCard_description"
>
Consider removing old sessions (90 days or older) you don't use anymore.
<button
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
Learn more
</button>
</p>
<div
class="mx_DeviceSecurityCard_actions"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
data-testid="inactive-devices-cta"
role="button"
tabindex="0"
>
View all (1)
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`<SecurityRecommendations /> renders unverified devices section when user has unverified devices 1`] = `
<div>
<div
class="mx_SettingsSubsection"
data-testid="security-recommendations-section"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Security recommendations
</h2>
</div>
<div
class="mx_SettingsSubsection_description"
>
<div
class="mx_SettingsSubsection_text"
>
Improve your account security by following these recommendations.
</div>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Unverified"
>
<svg
fill="currentColor"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Unverified sessions
</p>
<p
class="mx_DeviceSecurityCard_description"
>
Verify your sessions for enhanced secure messaging or remove from those you don't recognize or use anymore.
<button
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
Learn more
</button>
</p>
<div
class="mx_DeviceSecurityCard_actions"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
data-testid="unverified-devices-cta"
role="button"
tabindex="0"
>
View all (2)
</div>
</div>
</div>
</div>
<div
class="mx_SecurityRecommendations_spacing"
/>
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Inactive"
>
<div
height="16"
width="16"
/>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Inactive sessions
</p>
<p
class="mx_DeviceSecurityCard_description"
>
Consider removing old sessions (90 days or older) you don't use anymore.
<button
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
Learn more
</button>
</p>
<div
class="mx_DeviceSecurityCard_actions"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
data-testid="inactive-devices-cta"
role="button"
tabindex="0"
>
View all (1)
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
@@ -0,0 +1,145 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`<SelectableDeviceTile /> renders selected tile 1`] = `
<input
checked=""
class="_input_153f2_18"
data-testid="device-tile-checkbox-my-device"
id="device-tile-checkbox-my-device"
type="checkbox"
/>
`;
exports[`<SelectableDeviceTile /> renders unselected device tile with checkbox 1`] = `
<div>
<div
class="mx_SelectableDeviceTile"
>
<form
class="_root_19upo_16"
>
<div
class="_inline-field_19upo_32 mx_SelectableDeviceTile_checkbox"
>
<div
class="_inline-field-control_19upo_44"
>
<div
class="_container_153f2_10"
>
<input
class="_input_153f2_18"
data-testid="device-tile-checkbox-my-device"
id="device-tile-checkbox-my-device"
type="checkbox"
/>
<div
class="_ui_153f2_19"
>
<svg
aria-hidden="true"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
</div>
</div>
</div>
<div
class="_inline-field-body_19upo_38"
>
<label
class="_label_19upo_59"
for="device-tile-checkbox-my-device"
>
<div
class="mx_DeviceTile mx_DeviceTile_interactive"
data-testid="device-tile-my-device"
>
<div
class="mx_DeviceTypeIcon"
>
<div
class="mx_DeviceTypeIcon_deviceIconWrapper"
>
<svg
aria-label="Unknown session type"
class="mx_DeviceTypeIcon_deviceIcon"
fill="currentColor"
height="1em"
role="img"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3.5 20q-.625 0-1.062-.437A1.45 1.45 0 0 1 2 18.5q0-.625.438-1.062A1.45 1.45 0 0 1 3.5 17H4V6q0-.824.588-1.412A1.93 1.93 0 0 1 6 4h14q.424 0 .712.287Q21 4.576 21 5t-.288.713A.97.97 0 0 1 20 6H6v11h4.5q.624 0 1.063.438.437.437.437 1.062t-.437 1.063A1.45 1.45 0 0 1 10.5 20zM15 20a.97.97 0 0 1-.713-.288A.97.97 0 0 1 14 19V9q0-.424.287-.713A.97.97 0 0 1 15 8h6q.424 0 .712.287Q22 8.576 22 9v10q0 .424-.288.712A.97.97 0 0 1 21 20zm1-3h4v-7h-4z"
/>
</svg>
</div>
<svg
aria-label="Unverified"
class="mx_DeviceTypeIcon_verificationIcon unverified"
fill="currentColor"
height="1em"
role="img"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<div
class="mx_DeviceTile_info"
>
<h3
class="mx_Heading_h4"
>
My Device
</h3>
<div
class="mx_DeviceTile_metadata"
>
<span
data-testid="device-metadata-isVerified"
>
Unverified
</span>
·
<span
data-testid="device-metadata-lastSeenIp"
>
123.456.789
</span>
·
<span
data-testid="device-metadata-deviceId"
>
my-device
</span>
</div>
</div>
<div
class="mx_DeviceTile_actions"
>
<div>
test
</div>
</div>
</div>
</label>
</div>
</div>
</form>
</div>
</div>
`;
@@ -0,0 +1,34 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`deleteDevices() opens interactive auth dialog when delete fails with 401 1`] = `
{
"m.login.sso": {
"1": {
"body": "Confirm removing these devices by using Single Sign On to prove your identity.",
"continueKind": "primary",
"continueText": "Single Sign On",
"title": "Use Single Sign On to continue",
},
"2": {
"body": "Click the button below to confirm removing these devices.",
"continueKind": "danger",
"continueText": "Remove devices",
"title": "Confirm removing these devices",
},
},
"org.matrix.login.sso": {
"1": {
"body": "Confirm removing these devices by using Single Sign On to prove your identity.",
"continueKind": "primary",
"continueText": "Single Sign On",
"title": "Use Single Sign On to continue",
},
"2": {
"body": "Click the button below to confirm removing these devices.",
"continueKind": "danger",
"continueText": "Remove devices",
"title": "Confirm removing these devices",
},
},
}
`;
@@ -0,0 +1,94 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { MatrixError, type UIAFlow } from "matrix-js-sdk/src/matrix";
import { deleteDevicesWithInteractiveAuth } from "../../../../../../src/components/views/settings/devices/deleteDevices";
import Modal from "../../../../../../src/Modal";
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../../../test-utils";
describe("deleteDevices()", () => {
const userId = "@alice:server.org";
const deviceIds = ["device_1", "device_2"];
const mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
deleteMultipleDevices: jest.fn(),
});
const modalSpy = jest.spyOn(Modal, "createDialog") as jest.SpyInstance;
const interactiveAuthError = new MatrixError({ flows: [] as UIAFlow[] }, 401);
beforeEach(() => {
jest.clearAllMocks();
});
it("deletes devices and calls onFinished when interactive auth is not required", async () => {
mockClient.deleteMultipleDevices.mockResolvedValue({});
const onFinished = jest.fn();
await deleteDevicesWithInteractiveAuth(mockClient, deviceIds, onFinished);
expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith(deviceIds, undefined);
expect(onFinished).toHaveBeenCalledWith(true);
// didnt open modal
expect(modalSpy).not.toHaveBeenCalled();
});
it("throws without opening auth dialog when delete fails with a non-401 status code", async () => {
const error = new Error("");
// @ts-ignore
error.httpStatus = 404;
mockClient.deleteMultipleDevices.mockRejectedValue(error);
const onFinished = jest.fn();
await expect(deleteDevicesWithInteractiveAuth(mockClient, deviceIds, onFinished)).rejects.toThrow(error);
expect(onFinished).not.toHaveBeenCalled();
// didnt open modal
expect(modalSpy).not.toHaveBeenCalled();
});
it("throws without opening auth dialog when delete fails without data.flows", async () => {
const error = new Error("");
// @ts-ignore
error.httpStatus = 401;
// @ts-ignore
error.data = {};
mockClient.deleteMultipleDevices.mockRejectedValue(error);
const onFinished = jest.fn();
await expect(deleteDevicesWithInteractiveAuth(mockClient, deviceIds, onFinished)).rejects.toThrow(error);
expect(onFinished).not.toHaveBeenCalled();
// didnt open modal
expect(modalSpy).not.toHaveBeenCalled();
});
it("opens interactive auth dialog when delete fails with 401", async () => {
mockClient.deleteMultipleDevices.mockRejectedValue(interactiveAuthError);
const onFinished = jest.fn();
await deleteDevicesWithInteractiveAuth(mockClient, deviceIds, onFinished);
expect(onFinished).not.toHaveBeenCalled();
// opened modal
expect(modalSpy).toHaveBeenCalled();
const { title, authData, aestheticsForStagePhases } = modalSpy.mock.calls[0][1]!;
// modal opened as expected
expect(title).toEqual("Authentication");
expect(authData).toEqual(interactiveAuthError.data);
expect(aestheticsForStagePhases).toMatchSnapshot();
});
});
@@ -0,0 +1,81 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { filterDevicesBySecurityRecommendation } from "../../../../../../src/components/views/settings/devices/filter";
import { DeviceSecurityVariation } from "../../../../../../src/components/views/settings/devices/types";
import { DeviceType } from "../../../../../../src/utils/device/parseUserAgent";
const MS_DAY = 86400000;
describe("filterDevicesBySecurityRecommendation()", () => {
const unverifiedNoMetadata = {
device_id: "unverified-no-metadata",
isVerified: false,
deviceType: DeviceType.Unknown,
};
const verifiedNoMetadata = {
device_id: "verified-no-metadata",
isVerified: true,
deviceType: DeviceType.Unknown,
};
const hundredDaysOld = {
device_id: "100-days-old",
isVerified: true,
last_seen_ts: Date.now() - MS_DAY * 100,
deviceType: DeviceType.Unknown,
};
const hundredDaysOldUnverified = {
device_id: "unverified-100-days-old",
isVerified: false,
last_seen_ts: Date.now() - MS_DAY * 100,
deviceType: DeviceType.Unknown,
};
const fiftyDaysOld = {
device_id: "50-days-old",
isVerified: true,
last_seen_ts: Date.now() - MS_DAY * 50,
deviceType: DeviceType.Unknown,
};
const devices = [unverifiedNoMetadata, verifiedNoMetadata, hundredDaysOld, hundredDaysOldUnverified, fiftyDaysOld];
it("returns all devices when no securityRecommendations are passed", () => {
expect(filterDevicesBySecurityRecommendation(devices, [])).toBe(devices);
});
it("returns devices older than 90 days as inactive", () => {
expect(filterDevicesBySecurityRecommendation(devices, [DeviceSecurityVariation.Inactive])).toEqual([
// devices without ts metadata are not filtered as inactive
hundredDaysOld,
hundredDaysOldUnverified,
]);
});
it("returns correct devices for verified filter", () => {
expect(filterDevicesBySecurityRecommendation(devices, [DeviceSecurityVariation.Verified])).toEqual([
verifiedNoMetadata,
hundredDaysOld,
fiftyDaysOld,
]);
});
it("returns correct devices for unverified filter", () => {
expect(filterDevicesBySecurityRecommendation(devices, [DeviceSecurityVariation.Unverified])).toEqual([
unverifiedNoMetadata,
hundredDaysOldUnverified,
]);
});
it("returns correct devices for combined verified and inactive filters", () => {
expect(
filterDevicesBySecurityRecommendation(devices, [
DeviceSecurityVariation.Unverified,
DeviceSecurityVariation.Inactive,
]),
).toEqual([hundredDaysOldUnverified]);
});
});