Merge remote-tracking branch 'origin/develop' into feat/emoji-picker-rich-text-mode

This commit is contained in:
Florian Duros 2022-12-06 16:38:46 +01:00
commit 1bd560d350
No known key found for this signature in database
GPG key ID: 9700AA5870258A0B
39 changed files with 805 additions and 125 deletions

View file

@ -35,6 +35,7 @@ import SettingsStore from "../src/settings/SettingsStore";
import { SettingLevel } from "../src/settings/SettingLevel";
import { getMockClientWithEventEmitter, mockPlatformPeg } from "./test-utils";
import { UIFeature } from "../src/settings/UIFeature";
import { isBulkUnverifiedDeviceReminderSnoozed } from "../src/utils/device/snoozeBulkUnverifiedDeviceReminder";
// don't litter test console with logs
jest.mock("matrix-js-sdk/src/logger");
@ -48,6 +49,10 @@ jest.mock("../src/SecurityManager", () => ({
isSecretStorageBeingAccessed: jest.fn(), accessSecretStorage: jest.fn(),
}));
jest.mock("../src/utils/device/snoozeBulkUnverifiedDeviceReminder", () => ({
isBulkUnverifiedDeviceReminderSnoozed: jest.fn(),
}));
const userId = '@user:server';
const deviceId = 'my-device-id';
const mockDispatcher = mocked(dis);
@ -95,6 +100,7 @@ describe('DeviceListener', () => {
});
jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(mockClient);
jest.spyOn(SettingsStore, 'getValue').mockReturnValue(false);
mocked(isBulkUnverifiedDeviceReminderSnoozed).mockClear().mockReturnValue(false);
});
const createAndStart = async (): Promise<DeviceListener> => {
@ -451,6 +457,23 @@ describe('DeviceListener', () => {
expect(BulkUnverifiedSessionsToast.showToast).not.toHaveBeenCalled();
});
it('hides toast when reminder is snoozed', async () => {
mocked(isBulkUnverifiedDeviceReminderSnoozed).mockReturnValue(true);
// currentDevice, device2 are verified, device3 is unverified
mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => {
switch (deviceId) {
case currentDevice.deviceId:
case device2.deviceId:
return deviceTrustVerified;
default:
return deviceTrustUnverified;
}
});
await createAndStart();
expect(BulkUnverifiedSessionsToast.showToast).not.toHaveBeenCalled();
expect(BulkUnverifiedSessionsToast.hideToast).toHaveBeenCalled();
});
it('shows toast with unverified devices at app start', async () => {
// currentDevice, device2 are verified, device3 is unverified
mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => {

View file

@ -38,6 +38,7 @@ describe("<ForgotPassword>", () => {
let client: MatrixClient;
let serverConfig: ValidatedServerConfig;
let onComplete: () => void;
let onLoginClick: () => void;
let renderResult: RenderResult;
let restoreConsole: () => void;
@ -49,9 +50,16 @@ describe("<ForgotPassword>", () => {
});
};
const submitForm = async (submitLabel: string): Promise<void> => {
const clickButton = async (label: string): Promise<void> => {
await act(async () => {
await userEvent.click(screen.getByText(submitLabel), { delay: null });
await userEvent.click(screen.getByText(label), { delay: null });
});
};
const itShouldCloseTheDialogAndShowThePasswordInput = (): void => {
it("should close the dialog and show the password input", () => {
expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument();
expect(screen.getByText("Reset your password")).toBeInTheDocument();
});
};
@ -70,6 +78,7 @@ describe("<ForgotPassword>", () => {
serverConfig.hsName = "example.com";
onComplete = jest.fn();
onLoginClick = jest.fn();
jest.spyOn(AutoDiscoveryUtils, "validateServerConfigWithStaticUrls").mockResolvedValue(serverConfig);
jest.spyOn(AutoDiscoveryUtils, "authComponentStateForError");
@ -94,6 +103,7 @@ describe("<ForgotPassword>", () => {
renderResult = render(<ForgotPassword
serverConfig={serverConfig}
onComplete={onComplete}
onLoginClick={onLoginClick}
/>);
});
@ -108,6 +118,7 @@ describe("<ForgotPassword>", () => {
renderResult.rerender(<ForgotPassword
serverConfig={serverConfig}
onComplete={onComplete}
onLoginClick={onLoginClick}
/>);
});
@ -116,6 +127,16 @@ describe("<ForgotPassword>", () => {
});
});
describe("when clicking »Sign in instead«", () => {
beforeEach(async () => {
await clickButton("Sign in instead");
});
it("should call onLoginClick()", () => {
expect(onLoginClick).toHaveBeenCalled();
});
});
describe("when entering a non-email value", () => {
beforeEach(async () => {
await typeIntoField("Email address", "not en email");
@ -132,7 +153,7 @@ describe("<ForgotPassword>", () => {
mocked(client).requestPasswordEmailToken.mockRejectedValue({
errcode: "M_THREEPID_NOT_FOUND",
});
await submitForm("Send email");
await clickButton("Send email");
});
it("should show an email not found message", () => {
@ -146,7 +167,7 @@ describe("<ForgotPassword>", () => {
mocked(client).requestPasswordEmailToken.mockRejectedValue({
name: "ConnectionError",
});
await submitForm("Send email");
await clickButton("Send email");
});
it("should show an info about that", () => {
@ -166,7 +187,7 @@ describe("<ForgotPassword>", () => {
serverIsAlive: false,
serverDeadError: "server down",
});
await submitForm("Send email");
await clickButton("Send email");
});
it("should show the server error", () => {
@ -180,7 +201,7 @@ describe("<ForgotPassword>", () => {
mocked(client).requestPasswordEmailToken.mockResolvedValue({
sid: testSid,
});
await submitForm("Send email");
await clickButton("Send email");
});
it("should send the mail and show the check email view", () => {
@ -193,6 +214,16 @@ describe("<ForgotPassword>", () => {
expect(screen.getByText(testEmail)).toBeInTheDocument();
});
describe("when clicking re-enter email", () => {
beforeEach(async () => {
await clickButton("Re-enter email address");
});
it("go back to the email input", () => {
expect(screen.queryByText("Enter your email to reset password")).toBeInTheDocument();
});
});
describe("when clicking resend email", () => {
beforeEach(async () => {
await userEvent.click(screen.getByText("Resend"), { delay: null });
@ -212,7 +243,7 @@ describe("<ForgotPassword>", () => {
describe("when clicking next", () => {
beforeEach(async () => {
await submitForm("Next");
await clickButton("Next");
});
it("should show the password input view", () => {
@ -246,7 +277,7 @@ describe("<ForgotPassword>", () => {
retry_after_ms: (13 * 60 + 37) * 1000,
},
});
await submitForm("Reset password");
await clickButton("Reset password");
});
it("should show the rate limit error message", () => {
@ -258,7 +289,7 @@ describe("<ForgotPassword>", () => {
describe("and submitting it", () => {
beforeEach(async () => {
await submitForm("Reset password");
await clickButton("Reset password");
// double flush promises for the modal to appear
await flushPromisesWithFakeTimers();
await flushPromisesWithFakeTimers();
@ -284,6 +315,46 @@ describe("<ForgotPassword>", () => {
expect(screen.getByText(testEmail)).toBeInTheDocument();
});
describe("and dismissing the dialog by clicking the background", () => {
beforeEach(async () => {
await act(async () => {
await userEvent.click(screen.getByTestId("dialog-background"), { delay: null });
});
// double flush promises for the modal to disappear
await flushPromisesWithFakeTimers();
await flushPromisesWithFakeTimers();
});
itShouldCloseTheDialogAndShowThePasswordInput();
});
describe("and dismissing the dialog", () => {
beforeEach(async () => {
await act(async () => {
await userEvent.click(screen.getByLabelText("Close dialog"), { delay: null });
});
// double flush promises for the modal to disappear
await flushPromisesWithFakeTimers();
await flushPromisesWithFakeTimers();
});
itShouldCloseTheDialogAndShowThePasswordInput();
});
describe("when clicking re-enter email", () => {
beforeEach(async () => {
await clickButton("Re-enter email address");
// double flush promises for the modal to disappear
await flushPromisesWithFakeTimers();
await flushPromisesWithFakeTimers();
});
it("should close the dialog and go back to the email input", () => {
expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument();
expect(screen.queryByText("Enter your email to reset password")).toBeInTheDocument();
});
});
describe("when validating the link from the mail", () => {
beforeEach(async () => {
mocked(client.setPassword).mockResolvedValue({});

View file

@ -20,6 +20,7 @@ import React from "react";
import {
StatelessNotificationBadge,
} from "../../../../../src/components/views/rooms/NotificationBadge/StatelessNotificationBadge";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { NotificationColor } from "../../../../../src/stores/notifications/NotificationColor";
describe("NotificationBadge", () => {
@ -45,5 +46,19 @@ describe("NotificationBadge", () => {
fireEvent.mouseLeave(container.firstChild);
expect(cb).toHaveBeenCalledTimes(3);
});
it("hides the bold icon when the settings is set", () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => {
return name === "feature_hidebold";
});
const { container } = render(<StatelessNotificationBadge
symbol=""
color={NotificationColor.Bold}
count={1}
/>);
expect(container.firstChild).toBeNull();
});
});
});

View file

@ -32,7 +32,7 @@ import RoomListStore, { RoomListStoreClass } from "../../../../src/stores/room-l
import RoomListLayoutStore from "../../../../src/stores/room-list/RoomListLayoutStore";
import RoomList from "../../../../src/components/views/rooms/RoomList";
import RoomSublist from "../../../../src/components/views/rooms/RoomSublist";
import RoomTile from "../../../../src/components/views/rooms/RoomTile";
import { RoomTile } from "../../../../src/components/views/rooms/RoomTile";
import { getMockClientWithEventEmitter, mockClientMethodsUser } from '../../../test-utils';
import ResizeNotifier from '../../../../src/utils/ResizeNotifier';

View file

@ -15,12 +15,13 @@ limitations under the License.
*/
import React from "react";
import { render, screen, act } from "@testing-library/react";
import { render, screen, act, RenderResult } from "@testing-library/react";
import { mocked, Mocked } from "jest-mock";
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { Widget } from "matrix-widget-api";
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
import type { ClientWidgetApi } from "matrix-widget-api";
@ -30,6 +31,7 @@ import {
MockedCall,
useMockedCalls,
setupAsyncStoreWithClient,
filterConsole,
} from "../../../test-utils";
import { CallStore } from "../../../../src/stores/CallStore";
import RoomTile from "../../../../src/components/views/rooms/RoomTile";
@ -39,38 +41,79 @@ import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import PlatformPeg from "../../../../src/PlatformPeg";
import BasePlatform from "../../../../src/BasePlatform";
import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore";
import { VoiceBroadcastInfoState } from "../../../../src/voice-broadcast";
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
describe("RoomTile", () => {
jest.spyOn(PlatformPeg, "get")
.mockReturnValue({ overrideBrowserShortcuts: () => false } as unknown as BasePlatform);
useMockedCalls();
const setUpVoiceBroadcast = (state: VoiceBroadcastInfoState): void => {
voiceBroadcastInfoEvent = mkVoiceBroadcastInfoStateEvent(
room.roomId,
state,
client.getUserId(),
client.getDeviceId(),
);
act(() => {
room.currentState.setStateEvents([voiceBroadcastInfoEvent]);
});
};
const renderRoomTile = (): void => {
renderResult = render(
<RoomTile
room={room}
showMessagePreview={false}
isMinimized={false}
tag={DefaultTagID.Untagged}
/>,
);
};
let client: Mocked<MatrixClient>;
let restoreConsole: () => void;
let voiceBroadcastInfoEvent: MatrixEvent;
let room: Room;
let renderResult: RenderResult;
beforeEach(() => {
restoreConsole = filterConsole(
// irrelevant for this test
"Room !1:example.org does not have an m.room.create event",
);
stubClient();
client = mocked(MatrixClientPeg.get());
DMRoomMap.makeShared();
room = new Room("!1:example.org", client, "@alice:example.org", {
pendingEventOrdering: PendingEventOrdering.Detached,
});
client.getRoom.mockImplementation(roomId => roomId === room.roomId ? room : null);
client.getRooms.mockReturnValue([room]);
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
renderRoomTile();
});
afterEach(() => {
restoreConsole();
jest.clearAllMocks();
});
describe("call subtitle", () => {
let room: Room;
it("should render the room", () => {
expect(renderResult.container).toMatchSnapshot();
});
describe("when a call starts", () => {
let call: MockedCall;
let widget: Widget;
beforeEach(() => {
room = new Room("!1:example.org", client, "@alice:example.org", {
pendingEventOrdering: PendingEventOrdering.Detached,
});
client.getRoom.mockImplementation(roomId => roomId === room.roomId ? room : null);
client.getRooms.mockReturnValue([room]);
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
setupAsyncStoreWithClient(CallStore.instance, client);
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);
@ -83,18 +126,10 @@ describe("RoomTile", () => {
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
stop: () => {},
} as unknown as ClientWidgetApi);
render(
<RoomTile
room={room}
showMessagePreview={false}
isMinimized={false}
tag={DefaultTagID.Untagged}
/>,
);
});
afterEach(() => {
renderResult.unmount();
call.destroy();
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
@ -147,5 +182,45 @@ describe("RoomTile", () => {
act(() => { call.participants = new Map(); });
expect(screen.queryByLabelText(/participant/)).toBe(null);
});
describe("and a live broadcast starts", () => {
beforeEach(() => {
setUpVoiceBroadcast(VoiceBroadcastInfoState.Started);
});
it("should still render the call subtitle", () => {
expect(screen.queryByText("Video")).toBeInTheDocument();
expect(screen.queryByText("Live")).not.toBeInTheDocument();
});
});
});
describe("when a live voice broadcast starts", () => {
beforeEach(() => {
setUpVoiceBroadcast(VoiceBroadcastInfoState.Started);
});
it("should render the »Live« subtitle", () => {
expect(screen.queryByText("Live")).toBeInTheDocument();
});
describe("and the broadcast stops", () => {
beforeEach(() => {
const stopEvent = mkVoiceBroadcastInfoStateEvent(
room.roomId,
VoiceBroadcastInfoState.Stopped,
client.getUserId(),
client.getDeviceId(),
voiceBroadcastInfoEvent,
);
act(() => {
room.currentState.setStateEvents([stopEvent]);
});
});
it("should not render the »Live« subtitle", () => {
expect(screen.queryByText("Live")).not.toBeInTheDocument();
});
});
});
});

View file

@ -0,0 +1,81 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RoomTile should render the room 1`] = `
<div>
<div
aria-label="!1:example.org Unread messages."
aria-selected="false"
class="mx_AccessibleButton mx_RoomTile"
role="treeitem"
tabindex="-1"
>
<div
class="mx_DecoratedRoomAvatar"
>
<span
class="mx_BaseAvatar"
role="presentation"
>
<span
aria-hidden="true"
class="mx_BaseAvatar_initial"
style="font-size: 20.8px; width: 32px; line-height: 32px;"
>
!
</span>
<img
alt=""
aria-hidden="true"
class="mx_BaseAvatar_image"
data-testid="avatar-img"
src="data:image/png;base64,00"
style="width: 32px; height: 32px;"
/>
</span>
</div>
<div
class="mx_RoomTile_titleContainer"
>
<div
class="mx_RoomTile_title mx_RoomTile_titleHasUnreadEvents"
tabindex="-1"
title="!1:example.org"
>
<span
dir="auto"
>
!1:example.org
</span>
</div>
</div>
<div
aria-hidden="true"
class="mx_RoomTile_badgeContainer"
>
<div
class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_dot"
>
<span
class="mx_NotificationBadge_count"
/>
</div>
</div>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="Room options"
class="mx_AccessibleButton mx_RoomTile_menuButton"
role="button"
tabindex="0"
/>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="Notification options"
class="mx_AccessibleButton mx_RoomTile_notificationsButton"
role="button"
tabindex="-1"
/>
</div>
</div>
`;

View file

@ -38,6 +38,7 @@ jest.mock('../../../../src/settings/SettingsStore', () => ({
setValue: jest.fn(),
getValue: jest.fn(),
monitorSetting: jest.fn(),
watchSetting: jest.fn(),
}));
jest.mock('../../../../src/dispatcher/dispatcher', () => ({

View file

@ -25,6 +25,7 @@ import { TestSdkContext } from "../TestSdkContext";
jest.mock("../../src/settings/SettingsStore", () => ({
getValue: jest.fn(),
monitorSetting: jest.fn(),
watchSetting: jest.fn(),
}));
describe("TypingStore", () => {

View file

@ -39,7 +39,7 @@ export const filterConsole = (...ignoreList: string[]): () => void => {
return;
}
originalFunction(data);
originalFunction(...data);
};
}

View file

@ -42,6 +42,7 @@ jest.mock('../../src/Modal', () => ({
jest.mock('../../src/settings/SettingsStore', () => ({
getValue: jest.fn(),
monitorSetting: jest.fn(),
watchSetting: jest.fn(),
}));
const mockPromptBeforeInviteUnknownUsers = (value: boolean) => {

View file

@ -0,0 +1,98 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { logger } from "matrix-js-sdk/src/logger";
import {
isBulkUnverifiedDeviceReminderSnoozed,
snoozeBulkUnverifiedDeviceReminder,
} from "../../../src/utils/device/snoozeBulkUnverifiedDeviceReminder";
const SNOOZE_KEY = 'mx_snooze_bulk_unverified_device_nag';
describe('snooze bulk unverified device nag', () => {
const localStorageSetSpy = jest.spyOn(localStorage.__proto__, 'setItem');
const localStorageGetSpy = jest.spyOn(localStorage.__proto__, 'getItem');
const localStorageRemoveSpy = jest.spyOn(localStorage.__proto__, 'removeItem');
// 14.03.2022 16:15
const now = 1647270879403;
beforeEach(() => {
localStorageSetSpy.mockClear().mockImplementation(() => {});
localStorageGetSpy.mockClear().mockReturnValue(null);
localStorageRemoveSpy.mockClear().mockImplementation(() => {});
jest.spyOn(Date, 'now').mockReturnValue(now);
});
afterAll(() => {
jest.restoreAllMocks();
});
describe('snoozeBulkUnverifiedDeviceReminder()', () => {
it('sets the current time in local storage', () => {
snoozeBulkUnverifiedDeviceReminder();
expect(localStorageSetSpy).toHaveBeenCalledWith(SNOOZE_KEY, now.toString());
});
it('catches an error from localstorage', () => {
const loggerErrorSpy = jest.spyOn(logger, 'error');
localStorageSetSpy.mockImplementation(() => { throw new Error('oups'); });
snoozeBulkUnverifiedDeviceReminder();
expect(loggerErrorSpy).toHaveBeenCalled();
});
});
describe('isBulkUnverifiedDeviceReminderSnoozed()', () => {
it('returns false when there is no snooze in storage', () => {
const result = isBulkUnverifiedDeviceReminderSnoozed();
expect(localStorageGetSpy).toHaveBeenCalledWith(SNOOZE_KEY);
expect(result).toBe(false);
});
it('catches an error from localstorage and returns false', () => {
const loggerErrorSpy = jest.spyOn(logger, 'error');
localStorageGetSpy.mockImplementation(() => { throw new Error('oups'); });
const result = isBulkUnverifiedDeviceReminderSnoozed();
expect(result).toBe(false);
expect(loggerErrorSpy).toHaveBeenCalled();
});
it('returns false when snooze timestamp in storage is not a number', () => {
localStorageGetSpy.mockReturnValue('test');
const result = isBulkUnverifiedDeviceReminderSnoozed();
expect(result).toBe(false);
});
it('returns false when snooze timestamp in storage is over a week ago', () => {
const msDay = 1000 * 60 * 60 * 24;
// snoozed 8 days ago
localStorageGetSpy.mockReturnValue(now - (msDay * 8));
const result = isBulkUnverifiedDeviceReminderSnoozed();
expect(result).toBe(false);
});
it('returns true when snooze timestamp in storage is less than a week ago', () => {
const msDay = 1000 * 60 * 60 * 24;
// snoozed 8 days ago
localStorageGetSpy.mockReturnValue(now - (msDay * 6));
const result = isBulkUnverifiedDeviceReminderSnoozed();
expect(result).toBe(true);
});
});
});