Room list: add robustness to custom section loading (#33475)
* refactor: move all access to custom sections settings into `section.ts` * fix: add robustness when getting the order list of custom sections * fix: add robustness when getting the custom section data * fix: ignore malformed section but don't erase them * fix: remove useless await operator * test: add more tests
This commit is contained in:
@@ -833,6 +833,7 @@ describe("RoomListStoreV3", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string) => {
|
||||
if (setting === "feature_room_list_sections") return true;
|
||||
if (setting === "RoomList.OrderedCustomSections") return [];
|
||||
if (setting === "RoomList.CustomSectionData") return {};
|
||||
return false;
|
||||
});
|
||||
}
|
||||
@@ -1093,6 +1094,7 @@ describe("RoomListStoreV3", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string) => {
|
||||
if (setting === "feature_room_list_sections") return true;
|
||||
if (setting === "RoomList.OrderedCustomSections") return [];
|
||||
if (setting === "RoomList.CustomSectionData") return {};
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -1107,11 +1109,13 @@ describe("RoomListStoreV3", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string) => {
|
||||
if (setting === "feature_room_list_sections") return true;
|
||||
if (setting === "RoomList.OrderedCustomSections") return [customTag];
|
||||
if (setting === "RoomList.CustomSectionData")
|
||||
return { [customTag]: { tag: customTag, name: "Custom" } };
|
||||
return false;
|
||||
});
|
||||
|
||||
// Trigger the settings watcher
|
||||
settingsWatcher("RoomList.OrderedCustomSections");
|
||||
await Promise.resolve(settingsWatcher("RoomList.OrderedCustomSections"));
|
||||
|
||||
// Now there should be 4 sections (Favourite, custom, Chats, LowPriority)
|
||||
expect(store.getSortedRoomsInActiveSpace().sections).toHaveLength(4);
|
||||
|
||||
@@ -8,12 +8,14 @@
|
||||
import Modal from "../../../../src/Modal";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
import {
|
||||
CHATS_TAG,
|
||||
CUSTOM_SECTION_TAG_PREFIX,
|
||||
createSection,
|
||||
editSection,
|
||||
deleteSection,
|
||||
getCustomSectionData,
|
||||
getOrderedCustomSections,
|
||||
isDefaultSectionTag,
|
||||
CHATS_TAG,
|
||||
CUSTOM_SECTION_TAG_PREFIX,
|
||||
isSectionTag,
|
||||
} from "../../../../src/stores/room-list-v3/section";
|
||||
import { CreateSectionDialog } from "../../../../src/components/views/dialogs/CreateSectionDialog";
|
||||
@@ -25,6 +27,68 @@ describe("section", () => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("getCustomSectionData", () => {
|
||||
const validTag = "element.io.section.valid";
|
||||
const invalidTag = "element.io.section.invalid";
|
||||
const validEntry = { tag: validTag, name: "Valid" };
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it.each([null, false, 42, "string", []] as const)("returns an empty object when the raw value is %p", (raw) => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(raw as any);
|
||||
expect(getCustomSectionData()).toEqual({});
|
||||
});
|
||||
|
||||
it("returns valid entries and drops invalid ones", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue({
|
||||
[validTag]: validEntry,
|
||||
[invalidTag]: { tag: "element.io.section.mismatch", name: "Bad" },
|
||||
});
|
||||
expect(getCustomSectionData()).toEqual({ [validTag]: validEntry });
|
||||
});
|
||||
|
||||
it("drops entries that fail the isValidCustomSection check", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue({
|
||||
"element.io.section.null-val": null,
|
||||
"element.io.section.str-val": "not-an-object",
|
||||
"element.io.section.bad-tag": { tag: "not-a-custom-tag", name: "Bad" },
|
||||
"element.io.section.bad-name": { tag: "element.io.section.bad-name", name: 42 },
|
||||
});
|
||||
expect(getCustomSectionData()).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getOrderedCustomSections", () => {
|
||||
const tag = "element.io.section.abc";
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it("returns an empty array when the raw value is not an array", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
||||
if (setting === "RoomList.OrderedCustomSections") return "not-an-array";
|
||||
return null;
|
||||
});
|
||||
|
||||
const result = getOrderedCustomSections();
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("removes unknown sections and saves the cleaned list", () => {
|
||||
const knownTag = "element.io.section.known";
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
||||
if (setting === "RoomList.CustomSectionData") return { [knownTag]: { tag: knownTag, name: "Known" } };
|
||||
if (setting === "RoomList.OrderedCustomSections") return [knownTag, tag];
|
||||
return null;
|
||||
});
|
||||
|
||||
expect(getOrderedCustomSections()).toEqual([knownTag]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createSection", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(null);
|
||||
@@ -69,6 +133,8 @@ describe("section", () => {
|
||||
const existingTag = "element.io.section.existing";
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
||||
if (setting === "RoomList.OrderedCustomSections") return [existingTag];
|
||||
if (setting === "RoomList.CustomSectionData")
|
||||
return { [existingTag]: { tag: existingTag, name: "Existing" } };
|
||||
return null;
|
||||
});
|
||||
jest.spyOn(Modal, "createDialog").mockReturnValue({
|
||||
@@ -79,15 +145,16 @@ describe("section", () => {
|
||||
|
||||
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\./);
|
||||
|
||||
const newTag = savedOrder[1];
|
||||
const customDataCall = setValueSpy.mock.calls.find(([name]) => name === "RoomList.CustomSectionData");
|
||||
const savedSection = (customDataCall![3] as Record<string, { tag: string; name: string }>)[newTag];
|
||||
expect(savedSection.name).toBe("My Section");
|
||||
expect(savedSection.tag).toBe(newTag);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -100,6 +167,12 @@ describe("section", () => {
|
||||
jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it("does nothing if the tag is not a custom section tag", async () => {
|
||||
const createDialogSpy = jest.spyOn(Modal, "createDialog");
|
||||
await editSection("m.favourite");
|
||||
expect(createDialogSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does nothing if the section does not exist", async () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue({});
|
||||
const createDialogSpy = jest.spyOn(Modal, "createDialog");
|
||||
@@ -157,13 +230,20 @@ describe("section", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
||||
if (setting === "RoomList.CustomSectionData") return { [tag]: { tag, name: "My Section" } };
|
||||
if (setting === "RoomList.CustomSectionData")
|
||||
return { [tag]: { tag, name: "My Section" }, [otherTag]: { tag: otherTag, name: "Other Section" } };
|
||||
if (setting === "RoomList.OrderedCustomSections") return [otherTag, tag];
|
||||
return null;
|
||||
});
|
||||
jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it("does nothing if the tag is not a custom section tag", async () => {
|
||||
const createDialogSpy = jest.spyOn(Modal, "createDialog");
|
||||
await deleteSection("m.favourite", false);
|
||||
expect(createDialogSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does nothing if the section does not exist", async () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue({});
|
||||
const createDialogSpy = jest.spyOn(Modal, "createDialog");
|
||||
|
||||
@@ -80,8 +80,10 @@ describe("RoomListItemViewModel", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
||||
if (setting === "RoomList.showMessagePreview") return false;
|
||||
if (setting === "RoomList.OrderedCustomSections") return [];
|
||||
if (setting === "RoomList.CustomSectionData") return {};
|
||||
return false;
|
||||
});
|
||||
jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
|
||||
jest.spyOn(SettingsStore, "watchSetting").mockImplementation(() => "watcher-id");
|
||||
|
||||
jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue(null);
|
||||
|
||||
@@ -154,6 +154,19 @@ describe("RoomListSectionHeaderViewModel", () => {
|
||||
|
||||
expect(vm.getSnapshot().title).toBe("My Section");
|
||||
});
|
||||
|
||||
it("should not update title when tag is not a custom section tag", () => {
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag: "m.favourite",
|
||||
title: "Favourites",
|
||||
spaceId: "!space:server",
|
||||
onToggleExpanded,
|
||||
});
|
||||
|
||||
watchCallback();
|
||||
|
||||
expect(vm.getSnapshot().title).toBe("Favourites");
|
||||
});
|
||||
});
|
||||
|
||||
describe("editSection", () => {
|
||||
|
||||
Reference in New Issue
Block a user