Room list: add custom section creation (#33155)
* feat: add creation section dialog * feat: add in skip list a method to change filters * feat: add helper to creation section * feat: add custom sections data to Settings * feat: add custom section to room list store v3 * feat: update header and room list item vms * feat: add toast to room list vm * feat: add new translation * chore: move util functions of room list specs * test: add custom section playwright tests * chore: call loadCustomSections in RoomListStoreV3 ctor
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* 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, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import React from "react";
|
||||
|
||||
import { CreateSectionDialog } from "../../../../../src/components/views/dialogs/CreateSectionDialog";
|
||||
|
||||
describe("CreateSectionDialog", () => {
|
||||
const onFinished: jest.Mock = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
function renderComponent(): void {
|
||||
render(<CreateSectionDialog onFinished={onFinished} />);
|
||||
}
|
||||
|
||||
it("renders the dialog", () => {
|
||||
const { container } = render(<CreateSectionDialog onFinished={onFinished} />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("has the create section button disabled when the input is empty", () => {
|
||||
renderComponent();
|
||||
const createButton = screen.getByRole("button", { name: "Create section" });
|
||||
expect(createButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it("calls onFinished with true and the section name when create section is clicked", async () => {
|
||||
renderComponent();
|
||||
const input = screen.getByRole("textbox");
|
||||
await userEvent.type(input, "My section");
|
||||
const createButton = screen.getByRole("button", { name: "Create section" });
|
||||
await userEvent.click(createButton);
|
||||
expect(onFinished).toHaveBeenCalledWith(true, "My section");
|
||||
});
|
||||
|
||||
it("calls onFinished with false when the dialog is cancelled", async () => {
|
||||
renderComponent();
|
||||
const cancelButton = screen.getByRole("button", { name: "Cancel" });
|
||||
await userEvent.click(cancelButton);
|
||||
expect(onFinished).toHaveBeenCalledWith(false, "");
|
||||
});
|
||||
|
||||
it("calls onFinished with true and the section name when the form is submitted", async () => {
|
||||
renderComponent();
|
||||
const input = screen.getByRole("textbox");
|
||||
await userEvent.type(input, "My section");
|
||||
await userEvent.keyboard("{Enter}");
|
||||
expect(onFinished).toHaveBeenCalledWith(true, "My section");
|
||||
});
|
||||
});
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`CreateSectionDialog renders the dialog 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_CreateSectionDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
>
|
||||
<h1
|
||||
class="mx_Heading_h3 mx_Dialog_title"
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
Create a section
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_CreateSectionDialog_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-6x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-md-semibold_6v6n8_55"
|
||||
>
|
||||
Sections are only for you
|
||||
</span>
|
||||
<form
|
||||
class="_root_19upo_16 mx_CreateSectionDialog_form"
|
||||
>
|
||||
<div
|
||||
class="_field_19upo_26"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="radix-_r_0_"
|
||||
>
|
||||
|
||||
Section name
|
||||
</label>
|
||||
<input
|
||||
class="_control_sqdq4_10"
|
||||
id="radix-_r_0_"
|
||||
name="sectionName"
|
||||
required=""
|
||||
title=""
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<span
|
||||
class="mx_Dialog_buttons_row"
|
||||
>
|
||||
<button
|
||||
data-testid="dialog-cancel-button"
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="mx_Dialog_primary"
|
||||
data-testid="dialog-primary-button"
|
||||
disabled=""
|
||||
type="button"
|
||||
>
|
||||
Create section
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -14,9 +14,11 @@ import type { RoomNotificationState } from "../../../../src/stores/notifications
|
||||
import {
|
||||
CHATS_TAG,
|
||||
LISTS_UPDATE_EVENT,
|
||||
SECTION_CREATED_EVENT,
|
||||
RoomListStoreV3Class,
|
||||
type Section,
|
||||
} from "../../../../src/stores/room-list-v3/RoomListStoreV3";
|
||||
import * as sectionModule from "../../../../src/stores/room-list-v3/section";
|
||||
import { AsyncStoreWithClient } from "../../../../src/stores/AsyncStoreWithClient";
|
||||
import { RecencySorter } from "../../../../src/stores/room-list-v3/skip-list/sorters/RecencySorter";
|
||||
import { mkEvent, mkMessage, mkSpace, mkStubRoom, stubClient, upsertRoomStateEvents } from "../../../test-utils";
|
||||
@@ -830,6 +832,7 @@ describe("RoomListStoreV3", () => {
|
||||
function enableSections(): void {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string) => {
|
||||
if (setting === "feature_room_list_sections") return true;
|
||||
if (setting === "RoomList.OrderedCustomSections") return [];
|
||||
return false;
|
||||
});
|
||||
}
|
||||
@@ -1007,6 +1010,84 @@ describe("RoomListStoreV3", () => {
|
||||
const favSection = findSection(sections, DefaultTagID.Favourite)!;
|
||||
expect(favSection.rooms).toContain(rooms[3]);
|
||||
});
|
||||
|
||||
describe("createSection", () => {
|
||||
it("emits SECTION_CREATED_EVENT and LISTS_UPDATE_EVENT when section is created", async () => {
|
||||
enableSections();
|
||||
getClientAndRooms();
|
||||
jest.spyOn(sectionModule, "createSection").mockResolvedValue(true);
|
||||
|
||||
const store = new RoomListStoreV3Class(dispatcher);
|
||||
await store.start();
|
||||
|
||||
const sectionCreatedListener = jest.fn();
|
||||
const listsUpdateListener = jest.fn();
|
||||
store.on(SECTION_CREATED_EVENT, sectionCreatedListener);
|
||||
store.on(LISTS_UPDATE_EVENT, listsUpdateListener);
|
||||
|
||||
await store.createSection();
|
||||
|
||||
expect(sectionCreatedListener).toHaveBeenCalled();
|
||||
expect(listsUpdateListener).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not emit when section creation is cancelled", async () => {
|
||||
enableSections();
|
||||
getClientAndRooms();
|
||||
jest.spyOn(sectionModule, "createSection").mockResolvedValue(false);
|
||||
|
||||
const store = new RoomListStoreV3Class(dispatcher);
|
||||
await store.start();
|
||||
|
||||
const sectionCreatedListener = jest.fn();
|
||||
store.on(SECTION_CREATED_EVENT, sectionCreatedListener);
|
||||
|
||||
await store.createSection();
|
||||
|
||||
expect(sectionCreatedListener).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("updates sections when RoomList.OrderedCustomSections setting changes", async () => {
|
||||
enableSections();
|
||||
const { rooms } = getClientAndRooms();
|
||||
|
||||
let settingsWatcher: (settingName: string) => void = () => {};
|
||||
jest.spyOn(SettingsStore, "watchSetting").mockImplementation((settingName, _roomId, callback) => {
|
||||
if (settingName === "RoomList.OrderedCustomSections") settingsWatcher = callback as () => void;
|
||||
return "watcher-id";
|
||||
});
|
||||
|
||||
const customTag = "element.io.section.custom";
|
||||
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string) => {
|
||||
if (setting === "feature_room_list_sections") return true;
|
||||
if (setting === "RoomList.OrderedCustomSections") return [];
|
||||
return false;
|
||||
});
|
||||
|
||||
const store = new RoomListStoreV3Class(dispatcher);
|
||||
await store.start();
|
||||
|
||||
// Initial state: 3 sections (Favourite, Chats, LowPriority)
|
||||
expect(store.getSortedRoomsInActiveSpace().sections).toHaveLength(3);
|
||||
|
||||
// Mark a room with the custom tag and update the settings
|
||||
rooms[0].tags = { [customTag]: { order: 0 } };
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string) => {
|
||||
if (setting === "feature_room_list_sections") return true;
|
||||
if (setting === "RoomList.OrderedCustomSections") return [customTag];
|
||||
return false;
|
||||
});
|
||||
|
||||
// Trigger the settings watcher
|
||||
settingsWatcher("RoomList.OrderedCustomSections");
|
||||
|
||||
// Now there should be 4 sections (Favourite, custom, Chats, LowPriority)
|
||||
expect(store.getSortedRoomsInActiveSpace().sections).toHaveLength(4);
|
||||
const customSection = findSection(store.getSortedRoomsInActiveSpace().sections, customTag)!;
|
||||
expect(customSection.rooms).toContain(rooms[0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Muted rooms", () => {
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* 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 Modal from "../../../../src/Modal";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
import { createSection } from "../../../../src/stores/room-list-v3/section";
|
||||
import { CreateSectionDialog } from "../../../../src/components/views/dialogs/CreateSectionDialog";
|
||||
|
||||
describe("createSection", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(null);
|
||||
jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[false, "", false],
|
||||
[true, "", false],
|
||||
[true, "My Section", true],
|
||||
])("returns %s when shouldCreate=%s and name='%s'", async (shouldCreate, name, expected) => {
|
||||
jest.spyOn(Modal, "createDialog").mockReturnValue({
|
||||
finished: Promise.resolve([shouldCreate, name]),
|
||||
close: jest.fn(),
|
||||
} as any);
|
||||
|
||||
const result = await createSection();
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it("opens the CreateSectionDialog", async () => {
|
||||
const createDialogSpy = jest.spyOn(Modal, "createDialog").mockReturnValue({
|
||||
finished: Promise.resolve([false, ""]),
|
||||
close: jest.fn(),
|
||||
} as any);
|
||||
|
||||
await createSection();
|
||||
expect(createDialogSpy).toHaveBeenCalledWith(CreateSectionDialog);
|
||||
});
|
||||
|
||||
it("saves section data and ordered sections at ACCOUNT level when confirmed", async () => {
|
||||
const existingTag = "element.io.section.existing";
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
||||
if (setting === "RoomList.OrderedCustomSections") return [existingTag];
|
||||
return null;
|
||||
});
|
||||
jest.spyOn(Modal, "createDialog").mockReturnValue({
|
||||
finished: Promise.resolve([true, "My Section"]),
|
||||
close: jest.fn(),
|
||||
} as any);
|
||||
const setValueSpy = jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
|
||||
|
||||
await createSection();
|
||||
|
||||
const customDataCall = setValueSpy.mock.calls.find(([name]) => name === "RoomList.CustomSectionData");
|
||||
const savedSection = Object.values(customDataCall![3] as Record<string, { tag: string; name: string }>)[0];
|
||||
expect(savedSection.name).toBe("My Section");
|
||||
expect(savedSection.tag).toMatch(/^element\.io\.section\./);
|
||||
|
||||
const orderedCall = setValueSpy.mock.calls.find(([name]) => name === "RoomList.OrderedCustomSections");
|
||||
const savedOrder = orderedCall![3] as string[];
|
||||
expect(savedOrder[0]).toBe(existingTag);
|
||||
expect(savedOrder[1]).toMatch(/^element\.io\.section\./);
|
||||
});
|
||||
});
|
||||
@@ -18,6 +18,9 @@ import { getMockedRooms } from "./getMockedRooms";
|
||||
import SpaceStore from "../../../../../src/stores/spaces/SpaceStore";
|
||||
import { MetaSpace } from "../../../../../src/stores/spaces";
|
||||
import { RoomNotificationStateStore } from "../../../../../src/stores/notifications/RoomNotificationStateStore";
|
||||
import { FavouriteFilter } from "../../../../../src/stores/room-list-v3/skip-list/filters/FavouriteFilter";
|
||||
import { FilterEnum } from "../../../../../src/stores/room-list-v3/skip-list/filters";
|
||||
import { DefaultTagID } from "../../../../../src/stores/room-list-v3/skip-list/tag";
|
||||
|
||||
describe("RoomSkipList", () => {
|
||||
function generateSkipList(roomCount?: number): {
|
||||
@@ -99,6 +102,26 @@ describe("RoomSkipList", () => {
|
||||
expect(() => skipList.addNewRoom(room)).toThrow("Can't add room to skiplist");
|
||||
});
|
||||
|
||||
it("Filters are applied to existing nodes when useNewFilters is called", () => {
|
||||
const { skipList, rooms } = generateSkipList(10);
|
||||
|
||||
// Mark some rooms as favourite
|
||||
const favouriteRooms = [rooms[2], rooms[5], rooms[8]];
|
||||
for (const room of favouriteRooms) {
|
||||
room.tags = { [DefaultTagID.Favourite]: { order: 0 } };
|
||||
}
|
||||
|
||||
// No filters yet — all rooms are in the list
|
||||
expect(skipList.size).toEqual(10);
|
||||
|
||||
// Apply the favourite filter
|
||||
skipList.useNewFilters([new FavouriteFilter()]);
|
||||
|
||||
// Only favourite rooms should be returned when filtering by favourite
|
||||
const filteredRooms = Array.from(skipList.getRoomsInActiveSpace([FilterEnum.FavouriteFilter]));
|
||||
expect(filteredRooms).toHaveLength(favouriteRooms.length);
|
||||
});
|
||||
|
||||
it("Re-sort works when sorter is swapped", () => {
|
||||
const { skipList, rooms, sorter } = generateSkipList();
|
||||
const sortedByRecency = [...rooms].sort((a, b) => sorter.comparator(a, b));
|
||||
|
||||
@@ -61,6 +61,7 @@ describe("RoomListHeaderViewModel", () => {
|
||||
if (settingName === "RoomList.preferredSorting") return SortingAlgorithm.Recency;
|
||||
if (settingName === "feature_video_rooms") return true;
|
||||
if (settingName === "feature_element_call_video_rooms") return true;
|
||||
if (settingName === "RoomList.OrderedCustomSections") return [];
|
||||
return false;
|
||||
});
|
||||
});
|
||||
@@ -159,6 +160,23 @@ describe("RoomListHeaderViewModel", () => {
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
expect(vm.getSnapshot().isMessagePreviewEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[true, true, false],
|
||||
[false, false, true],
|
||||
])(
|
||||
"when feature_room_list_sections is %s: canCreateSection=%s, useComposeIcon=%s",
|
||||
(featureEnabled, expectedCanCreateSection, expectedUseComposeIcon) => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => {
|
||||
if (settingName === "feature_room_list_sections") return featureEnabled;
|
||||
return false;
|
||||
});
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
expect(vm.getSnapshot().canCreateSection).toBe(expectedCanCreateSection);
|
||||
expect(vm.getSnapshot().useComposeIcon).toBe(expectedUseComposeIcon);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("event listeners", () => {
|
||||
@@ -296,6 +314,13 @@ describe("RoomListHeaderViewModel", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should call createSection on RoomListStoreV3 when createSection is called", () => {
|
||||
const createSectionSpy = jest.spyOn(RoomListStoreV3.instance, "createSection").mockResolvedValue();
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm.createSection();
|
||||
expect(createSectionSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should toggle message preview from enabled to disabled", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => {
|
||||
if (settingName === "RoomList.showMessagePreview") return true;
|
||||
|
||||
@@ -29,6 +29,7 @@ import { Action } from "../../../src/dispatcher/actions";
|
||||
import { CallStore } from "../../../src/stores/CallStore";
|
||||
import { CallEvent, type Call } from "../../../src/models/Call";
|
||||
import { RoomListItemViewModel } from "../../../src/viewmodels/room-list/RoomListItemViewModel";
|
||||
import RoomListStoreV3 from "../../../src/stores/room-list-v3/RoomListStoreV3";
|
||||
|
||||
jest.mock("../../../src/viewmodels/room-list/utils", () => ({
|
||||
hasAccessToOptionsMenu: jest.fn().mockReturnValue(true),
|
||||
@@ -75,6 +76,7 @@ describe("RoomListItemViewModel", () => {
|
||||
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
||||
if (setting === "RoomList.showMessagePreview") return false;
|
||||
if (setting === "RoomList.OrderedCustomSections") return [];
|
||||
return false;
|
||||
});
|
||||
jest.spyOn(SettingsStore, "watchSetting").mockImplementation(() => "watcher-id");
|
||||
@@ -501,6 +503,20 @@ describe("RoomListItemViewModel", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("canMoveToSection", () => {
|
||||
it.each([
|
||||
[true, true],
|
||||
[false, false],
|
||||
])("should be %s when feature_room_list_sections is %s", (featureEnabled, expected) => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
||||
if (setting === "feature_room_list_sections") return featureEnabled;
|
||||
return false;
|
||||
});
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
expect(viewModel.getSnapshot().canMoveToSection).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Actions", () => {
|
||||
it("should dispatch view room action on openRoom", () => {
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
@@ -572,6 +588,13 @@ describe("RoomListItemViewModel", () => {
|
||||
room_id: "!room:server",
|
||||
});
|
||||
});
|
||||
|
||||
it("should call createSection on RoomListStoreV3 when onCreateSection is called", () => {
|
||||
const createSectionSpy = jest.spyOn(RoomListStoreV3.instance, "createSection").mockResolvedValue();
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
viewModel.onCreateSection();
|
||||
expect(createSectionSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Cleanup", () => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { mocked } from "jest-mock";
|
||||
import { waitFor } from "jest-matrix-react";
|
||||
|
||||
import { createTestClient, flushPromises, mkStubRoom, stubClient } from "../../test-utils";
|
||||
import { createTestClient, flushPromises, flushPromisesWithFakeTimers, mkStubRoom, stubClient } from "../../test-utils";
|
||||
import RoomListStoreV3, { CHATS_TAG, RoomListStoreV3Event } from "../../../src/stores/room-list-v3/RoomListStoreV3";
|
||||
import SpaceStore from "../../../src/stores/spaces/SpaceStore";
|
||||
import { FilterEnum } from "../../../src/stores/room-list-v3/skip-list/filters";
|
||||
@@ -589,6 +589,14 @@ describe("RoomListViewModel", () => {
|
||||
});
|
||||
|
||||
describe("Cleanup", () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("should dispose all room item view models on dispose", () => {
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
@@ -604,6 +612,51 @@ describe("RoomListViewModel", () => {
|
||||
expect(disposeSpy2).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("Toast", () => {
|
||||
it("should show toast when SectionCreated event fires", () => {
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
RoomListStoreV3.instance.emit(RoomListStoreV3Event.SectionCreated);
|
||||
expect(viewModel.getSnapshot().toast).toBe("section_created");
|
||||
});
|
||||
|
||||
it("should clear toast when closeToast is called", () => {
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
RoomListStoreV3.instance.emit(RoomListStoreV3Event.SectionCreated);
|
||||
expect(viewModel.getSnapshot().toast).toBe("section_created");
|
||||
|
||||
viewModel.closeToast();
|
||||
expect(viewModel.getSnapshot().toast).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should auto-close toast after 15 seconds", () => {
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
RoomListStoreV3.instance.emit(RoomListStoreV3Event.SectionCreated);
|
||||
expect(viewModel.getSnapshot().toast).toBe("section_created");
|
||||
|
||||
jest.advanceTimersByTime(15 * 1000);
|
||||
expect(viewModel.getSnapshot().toast).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should reset the auto-close timer when a new section is created", () => {
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
RoomListStoreV3.instance.emit(RoomListStoreV3Event.SectionCreated);
|
||||
jest.advanceTimersByTime(10 * 1000);
|
||||
|
||||
// Second section created — resets the timer
|
||||
RoomListStoreV3.instance.emit(RoomListStoreV3Event.SectionCreated);
|
||||
jest.advanceTimersByTime(10 * 1000);
|
||||
|
||||
// Toast should still be visible (only 10s since last emit)
|
||||
expect(viewModel.getSnapshot().toast).toBe("section_created");
|
||||
|
||||
jest.advanceTimersByTime(5 * 1000);
|
||||
expect(viewModel.getSnapshot().toast).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sections (feature_room_list_sections)", () => {
|
||||
let favRoom1: Room;
|
||||
let favRoom2: Room;
|
||||
@@ -922,7 +975,7 @@ describe("RoomListViewModel", () => {
|
||||
action: Action.ActiveRoomChanged,
|
||||
newRoomId: "!fav1:server",
|
||||
});
|
||||
await flushPromises();
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
expect(viewModel.getSnapshot().roomListState.activeRoomIndex).toBe(0);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user