/* 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 React from "react"; import { mocked } from "jest-mock"; import { render, screen } from "@testing-library/react"; import { Room } from "matrix-js-sdk/src/models/room"; import { ISearchResults } from "matrix-js-sdk/src/@types/search"; import { defer } from "matrix-js-sdk/src/utils"; import { SearchResult } from "matrix-js-sdk/src/models/search-result"; import { IEvent, MatrixEvent } from "matrix-js-sdk/src/models/event"; import { EventType } from "matrix-js-sdk/src/@types/event"; import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { RoomSearchView } from "../../../src/components/structures/RoomSearchView"; import { SearchScope } from "../../../src/components/views/rooms/SearchBar"; import ResizeNotifier from "../../../src/utils/ResizeNotifier"; import { RoomPermalinkCreator } from "../../../src/utils/permalinks/Permalinks"; import { stubClient } from "../../test-utils"; import MatrixClientContext from "../../../src/contexts/MatrixClientContext"; import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; import { searchPagination } from "../../../src/Searching"; jest.mock("../../../src/Searching", () => ({ searchPagination: jest.fn(), })); describe("", () => { const eventMapper = (obj: Partial) => new MatrixEvent(obj); const resizeNotifier = new ResizeNotifier(); let client: MatrixClient; let room: Room; let permalinkCreator: RoomPermalinkCreator; beforeEach(async () => { stubClient(); client = MatrixClientPeg.get(); client.supportsThreads = jest.fn().mockReturnValue(true); room = new Room("!room:server", client, client.getUserId()!); mocked(client.getRoom).mockReturnValue(room); permalinkCreator = new RoomPermalinkCreator(room, room.roomId); jest.spyOn(Element.prototype, "clientHeight", "get").mockReturnValue(100); }); afterEach(async () => { jest.restoreAllMocks(); }); it("should show a spinner before the promise resolves", async () => { const deferred = defer(); render( , ); await screen.findByTestId("messagePanelSearchSpinner"); }); it("should render results when the promise resolves", async () => { render( ({ results: [ SearchResult.fromJson( { rank: 1, result: { room_id: room.roomId, event_id: "$2", sender: client.getUserId()!, origin_server_ts: 1, content: { body: "Foo Test Bar", msgtype: "m.text" }, type: EventType.RoomMessage, }, context: { profile_info: {}, events_before: [ { room_id: room.roomId, event_id: "$1", sender: client.getUserId()!, origin_server_ts: 1, content: { body: "Before", msgtype: "m.text" }, type: EventType.RoomMessage, }, ], events_after: [ { room_id: room.roomId, event_id: "$3", sender: client.getUserId()!, origin_server_ts: 1, content: { body: "After", msgtype: "m.text" }, type: EventType.RoomMessage, }, ], }, }, eventMapper, ), ], highlights: [], count: 1, })} resizeNotifier={resizeNotifier} permalinkCreator={permalinkCreator} className="someClass" onUpdate={jest.fn()} /> , ); await screen.findByText("Before"); await screen.findByText("Foo Test Bar"); await screen.findByText("After"); }); it("should highlight words correctly", async () => { render( ({ results: [ SearchResult.fromJson( { rank: 1, result: { room_id: room.roomId, event_id: "$2", sender: client.getUserId()!, origin_server_ts: 1, content: { body: "Foo Test Bar", msgtype: "m.text" }, type: EventType.RoomMessage, }, context: { profile_info: {}, events_before: [], events_after: [], }, }, eventMapper, ), ], highlights: ["test"], count: 1, })} resizeNotifier={resizeNotifier} permalinkCreator={permalinkCreator} className="someClass" onUpdate={jest.fn()} /> , ); const text = await screen.findByText("Test"); expect(text).toHaveClass("mx_EventTile_searchHighlight"); }); it("should show spinner above results when backpaginating", async () => { const searchResults: ISearchResults = { results: [ SearchResult.fromJson( { rank: 1, result: { room_id: room.roomId, event_id: "$2", sender: client.getUserId()!, origin_server_ts: 1, content: { body: "Foo Test Bar", msgtype: "m.text" }, type: EventType.RoomMessage, }, context: { profile_info: {}, events_before: [], events_after: [], }, }, eventMapper, ), ], highlights: ["test"], next_batch: "next_batch", count: 2, }; mocked(searchPagination).mockResolvedValue({ ...searchResults, results: [ ...searchResults.results, SearchResult.fromJson( { rank: 1, result: { room_id: room.roomId, event_id: "$4", sender: client.getUserId()!, origin_server_ts: 4, content: { body: "Potato", msgtype: "m.text" }, type: EventType.RoomMessage, }, context: { profile_info: {}, events_before: [], events_after: [], }, }, eventMapper, ), ], next_batch: undefined, }); render( , ); await screen.findByRole("progressbar"); await screen.findByText("Potato"); expect(screen.queryByRole("progressbar")).toBeFalsy(); }); it("should handle resolutions after unmounting sanely", async () => { const deferred = defer(); const { unmount } = render( , ); unmount(); deferred.resolve({ results: [], highlights: [], }); }); it("should handle rejections after unmounting sanely", async () => { const deferred = defer(); const { unmount } = render( , ); unmount(); deferred.reject({ results: [], highlights: [], }); }); it("should show modal if error is encountered", async () => { const deferred = defer(); render( , ); deferred.reject(new Error("Some error")); await screen.findByText("Search failed"); await screen.findByText("Some error"); }); it("should combine search results when the query is present in multiple sucessive messages", async () => { const searchResults: ISearchResults = { results: [ SearchResult.fromJson( { rank: 1, result: { room_id: room.roomId, event_id: "$4", sender: client.getUserId() ?? "", origin_server_ts: 1, content: { body: "Foo2", msgtype: "m.text" }, type: EventType.RoomMessage, }, context: { profile_info: {}, events_before: [ { room_id: room.roomId, event_id: "$3", sender: client.getUserId() ?? "", origin_server_ts: 1, content: { body: "Between", msgtype: "m.text" }, type: EventType.RoomMessage, }, ], events_after: [ { room_id: room.roomId, event_id: "$5", sender: client.getUserId() ?? "", origin_server_ts: 1, content: { body: "After", msgtype: "m.text" }, type: EventType.RoomMessage, }, ], }, }, eventMapper, ), SearchResult.fromJson( { rank: 1, result: { room_id: room.roomId, event_id: "$2", sender: client.getUserId() ?? "", origin_server_ts: 1, content: { body: "Foo", msgtype: "m.text" }, type: EventType.RoomMessage, }, context: { profile_info: {}, events_before: [ { room_id: room.roomId, event_id: "$1", sender: client.getUserId() ?? "", origin_server_ts: 1, content: { body: "Before", msgtype: "m.text" }, type: EventType.RoomMessage, }, ], events_after: [ { room_id: room.roomId, event_id: "$3", sender: client.getUserId() ?? "", origin_server_ts: 1, content: { body: "Between", msgtype: "m.text" }, type: EventType.RoomMessage, }, ], }, }, eventMapper, ), ], highlights: [], next_batch: "", count: 1, }; render( , ); const beforeNode = await screen.findByText("Before"); const fooNode = await screen.findByText("Foo"); const betweenNode = await screen.findByText("Between"); const foo2Node = await screen.findByText("Foo2"); const afterNode = await screen.findByText("After"); expect((await screen.findAllByText("Between")).length).toBe(1); expect(beforeNode.compareDocumentPosition(fooNode) == Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); expect(fooNode.compareDocumentPosition(betweenNode) == Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); expect(betweenNode.compareDocumentPosition(foo2Node) == Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); expect(foo2Node.compareDocumentPosition(afterNode) == Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); }); });