From 4c80762e22440d3edf05e78cdf652d2689110235 Mon Sep 17 00:00:00 2001 From: Kerry Date: Wed, 27 Jul 2022 17:10:20 +0200 Subject: [PATCH] test typescriptification - timeline-window, scheduler, etc (#2539) * spec/unit/user.spec.js -> spec/unit/user.spec.ts * fix ts in user.spec * renamed: spec/unit/timeline-window.spec.js -> spec/unit/timeline-window.spec.ts * overdo it fixing types in timeline-window.spec * renamed spec/unit/sync-accumulator.spec.js spec/unit/sync-accumulator.spec.ts * fix ts in sync-accumalator.spec * spec/unit/scheduler.spec.js -> spec/unit/scheduler.spec.ts * fix ts in scheduler.spec * missed types in timeline-window spec --- .../{scheduler.spec.js => scheduler.spec.ts} | 157 +++--- ...lator.spec.js => sync-accumulator.spec.ts} | 38 +- spec/unit/timeline-window.spec.js | 456 ------------------ spec/unit/timeline-window.spec.ts | 439 +++++++++++++++++ spec/unit/{user.spec.js => user.spec.ts} | 30 +- 5 files changed, 559 insertions(+), 561 deletions(-) rename spec/unit/{scheduler.spec.js => scheduler.spec.ts} (77%) rename spec/unit/{sync-accumulator.spec.js => sync-accumulator.spec.ts} (94%) delete mode 100644 spec/unit/timeline-window.spec.js create mode 100644 spec/unit/timeline-window.spec.ts rename spec/unit/{user.spec.js => user.spec.ts} (71%) diff --git a/spec/unit/scheduler.spec.js b/spec/unit/scheduler.spec.ts similarity index 77% rename from spec/unit/scheduler.spec.js rename to spec/unit/scheduler.spec.ts index eb54fd5a62f..6a02111f403 100644 --- a/spec/unit/scheduler.spec.js +++ b/spec/unit/scheduler.spec.ts @@ -45,8 +45,8 @@ describe("MatrixScheduler", function() { queueFn = function() { return "one_big_queue"; }; - const deferA = defer(); - const deferB = defer(); + const deferA = defer>(); + const deferB = defer>(); let yieldedA = false; scheduler.setProcessFunction(function(event) { if (yieldedA) { @@ -70,84 +70,84 @@ describe("MatrixScheduler", function() { }); it("should invoke the retryFn on failure and wait the amount of time specified", - async function() { - const waitTimeMs = 1500; - const retryDefer = defer(); - retryFn = function() { - retryDefer.resolve(); - return waitTimeMs; - }; - queueFn = function() { - return "yep"; - }; + async function() { + const waitTimeMs = 1500; + const retryDefer = defer(); + retryFn = function() { + retryDefer.resolve(); + return waitTimeMs; + }; + queueFn = function() { + return "yep"; + }; - let procCount = 0; - scheduler.setProcessFunction(function(ev) { - procCount += 1; - if (procCount === 1) { - expect(ev).toEqual(eventA); - return deferred.promise; - } else if (procCount === 2) { + let procCount = 0; + scheduler.setProcessFunction(function(ev) { + procCount += 1; + if (procCount === 1) { + expect(ev).toEqual(eventA); + return deferred.promise; + } else if (procCount === 2) { // don't care about this deferred - return new Promise(); - } - expect(procCount).toBeLessThan(3); - }); + return new Promise(() => {}); + } + expect(procCount).toBeLessThan(3); + }); - scheduler.queueEvent(eventA); - // as queueing doesn't start processing synchronously anymore (see commit bbdb5ac) - // wait just long enough before it does - await Promise.resolve(); - expect(procCount).toEqual(1); - deferred.reject({}); - await retryDefer.promise; - expect(procCount).toEqual(1); - jest.advanceTimersByTime(waitTimeMs); - await Promise.resolve(); - expect(procCount).toEqual(2); - }); + scheduler.queueEvent(eventA); + // as queueing doesn't start processing synchronously anymore (see commit bbdb5ac) + // wait just long enough before it does + await Promise.resolve(); + expect(procCount).toEqual(1); + deferred.reject({}); + await retryDefer.promise; + expect(procCount).toEqual(1); + jest.advanceTimersByTime(waitTimeMs); + await Promise.resolve(); + expect(procCount).toEqual(2); + }); it("should give up if the retryFn on failure returns -1 and try the next event", - async function() { + async function() { // Queue A & B. // Reject A and return -1 on retry. // Expect B to be tried next and the promise for A to be rejected. - retryFn = function() { - return -1; - }; - queueFn = function() { - return "yep"; - }; + retryFn = function() { + return -1; + }; + queueFn = function() { + return "yep"; + }; - const deferA = defer(); - const deferB = defer(); - let procCount = 0; - scheduler.setProcessFunction(function(ev) { - procCount += 1; - if (procCount === 1) { - expect(ev).toEqual(eventA); - return deferA.promise; - } else if (procCount === 2) { - expect(ev).toEqual(eventB); - return deferB.promise; - } - expect(procCount).toBeLessThan(3); - }); + const deferA = defer(); + const deferB = defer(); + let procCount = 0; + scheduler.setProcessFunction(function(ev) { + procCount += 1; + if (procCount === 1) { + expect(ev).toEqual(eventA); + return deferA.promise; + } else if (procCount === 2) { + expect(ev).toEqual(eventB); + return deferB.promise; + } + expect(procCount).toBeLessThan(3); + }); - const globalA = scheduler.queueEvent(eventA); - scheduler.queueEvent(eventB); - // as queueing doesn't start processing synchronously anymore (see commit bbdb5ac) - // wait just long enough before it does - await Promise.resolve(); - expect(procCount).toEqual(1); - deferA.reject({}); - try { - await globalA; - } catch (err) { + const globalA = scheduler.queueEvent(eventA); + scheduler.queueEvent(eventB); + // as queueing doesn't start processing synchronously anymore (see commit bbdb5ac) + // wait just long enough before it does await Promise.resolve(); - expect(procCount).toEqual(2); - } - }); + expect(procCount).toEqual(1); + deferA.reject({}); + try { + await globalA; + } catch (err) { + await Promise.resolve(); + expect(procCount).toEqual(2); + } + }); it("should treat each queue separately", function(done) { // Queue messages A B C D. @@ -175,7 +175,7 @@ describe("MatrixScheduler", function() { const expectOrder = [ eventA.getId(), eventB.getId(), eventD.getId(), ]; - const deferA = defer(); + const deferA = defer(); scheduler.setProcessFunction(function(event) { const id = expectOrder.shift(); expect(id).toEqual(event.getId()); @@ -191,7 +191,7 @@ describe("MatrixScheduler", function() { // wait a bit then resolve A and we should get D (not C) next. setTimeout(function() { - deferA.resolve({}); + deferA.resolve(); }, 1000); jest.advanceTimersByTime(1000); }); @@ -336,28 +336,29 @@ describe("MatrixScheduler", function() { errcode: "M_LIMIT_EXCEEDED", retry_after_ms: 5000, }), ); - expect(res >= 500).toBe(true, "Didn't wait long enough."); + expect(res >= 500).toBe(true); }); it("should give up after 5 attempts", function() { const res = MatrixScheduler.RETRY_BACKOFF_RATELIMIT( - eventA, 5, {}, + eventA, 5, new MatrixError({}), ); - expect(res).toBe(-1, "Didn't give up."); + expect(res).toBe(-1); }); it("should do exponential backoff", function() { + const error = new MatrixError({}); expect(MatrixScheduler.RETRY_BACKOFF_RATELIMIT( - eventA, 1, {}, + eventA, 1, error, )).toEqual(2000); expect(MatrixScheduler.RETRY_BACKOFF_RATELIMIT( - eventA, 2, {}, + eventA, 2, error, )).toEqual(4000); expect(MatrixScheduler.RETRY_BACKOFF_RATELIMIT( - eventA, 3, {}, + eventA, 3, error, )).toEqual(8000); expect(MatrixScheduler.RETRY_BACKOFF_RATELIMIT( - eventA, 4, {}, + eventA, 4, error, )).toEqual(16000); }); }); diff --git a/spec/unit/sync-accumulator.spec.js b/spec/unit/sync-accumulator.spec.ts similarity index 94% rename from spec/unit/sync-accumulator.spec.js rename to spec/unit/sync-accumulator.spec.ts index 5fe9a3611b0..645efbfbba4 100644 --- a/spec/unit/sync-accumulator.spec.js +++ b/spec/unit/sync-accumulator.spec.ts @@ -1,6 +1,6 @@ /* Copyright 2017 Vector Creations Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 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. @@ -384,6 +384,10 @@ describe("SyncAccumulator", function() { }; } + afterEach(() => { + jest.spyOn(global.Date, 'now').mockRestore(); + }); + it("should copy summary properties", function() { sa.accumulate(createSyncResponseWithSummary({ "m.heroes": ["@alice:bar"], @@ -413,25 +417,19 @@ describe("SyncAccumulator", function() { const delta = 1000; const startingTs = 1000; - const oldDateNow = Date.now; - try { - Date.now = jest.fn(); - Date.now.mockReturnValue(startingTs); - - sa.accumulate(RES_WITH_AGE); - - Date.now.mockReturnValue(startingTs + delta); - - const output = sa.getJSON(); - expect(output.roomsData.join["!foo:bar"].timeline.events[0].unsigned.age).toEqual( - RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0].unsigned.age + delta, - ); - expect(Object.keys(output.roomsData.join["!foo:bar"].timeline.events[0])).toEqual( - Object.keys(RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0]), - ); - } finally { - Date.now = oldDateNow; - } + jest.spyOn(global.Date, 'now').mockReturnValue(startingTs); + + sa.accumulate(RES_WITH_AGE); + + jest.spyOn(global.Date, 'now').mockReturnValue(startingTs + delta); + + const output = sa.getJSON(); + expect(output.roomsData.join["!foo:bar"].timeline.events[0].unsigned.age).toEqual( + RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0].unsigned.age + delta, + ); + expect(Object.keys(output.roomsData.join["!foo:bar"].timeline.events[0])).toEqual( + Object.keys(RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0]), + ); }); it("should mangle age without adding extra keys", () => { diff --git a/spec/unit/timeline-window.spec.js b/spec/unit/timeline-window.spec.js deleted file mode 100644 index 4fc78234446..00000000000 --- a/spec/unit/timeline-window.spec.js +++ /dev/null @@ -1,456 +0,0 @@ -import { EventTimeline } from "../../src/models/event-timeline"; -import { TimelineIndex, TimelineWindow } from "../../src/timeline-window"; -import * as utils from "../test-utils/test-utils"; - -const ROOM_ID = "roomId"; -const USER_ID = "userId"; - -/* - * create a timeline with a bunch (default 3) events. - * baseIndex is 1 by default. - */ -function createTimeline(numEvents, baseIndex) { - if (numEvents === undefined) { - numEvents = 3; - } - if (baseIndex === undefined) { - baseIndex = 1; - } - - // XXX: this is a horrid hack - const timelineSet = { room: { roomId: ROOM_ID } }; - timelineSet.room.getUnfilteredTimelineSet = function() { - return timelineSet; - }; - - const timeline = new EventTimeline(timelineSet); - - // add the events after the baseIndex first - addEventsToTimeline(timeline, numEvents - baseIndex, false); - - // then add those before the baseIndex - addEventsToTimeline(timeline, baseIndex, true); - - expect(timeline.getBaseIndex()).toEqual(baseIndex); - return timeline; -} - -function addEventsToTimeline(timeline, numEvents, toStartOfTimeline) { - for (let i = 0; i < numEvents; i++) { - timeline.addEvent( - utils.mkMessage({ - room: ROOM_ID, user: USER_ID, - event: true, - }), - { toStartOfTimeline }, - ); - } -} - -/* - * create a pair of linked timelines - */ -function createLinkedTimelines() { - const tl1 = createTimeline(); - const tl2 = createTimeline(); - tl1.setNeighbouringTimeline(tl2, EventTimeline.FORWARDS); - tl2.setNeighbouringTimeline(tl1, EventTimeline.BACKWARDS); - return [tl1, tl2]; -} - -describe("TimelineIndex", function() { - describe("minIndex", function() { - it("should return the min index relative to BaseIndex", function() { - const timelineIndex = new TimelineIndex(createTimeline(), 0); - expect(timelineIndex.minIndex()).toEqual(-1); - }); - }); - - describe("maxIndex", function() { - it("should return the max index relative to BaseIndex", function() { - const timelineIndex = new TimelineIndex(createTimeline(), 0); - expect(timelineIndex.maxIndex()).toEqual(2); - }); - }); - - describe("advance", function() { - it("should advance up to the end of the timeline", function() { - const timelineIndex = new TimelineIndex(createTimeline(), 0); - const result = timelineIndex.advance(3); - expect(result).toEqual(2); - expect(timelineIndex.index).toEqual(2); - }); - - it("should retreat back to the start of the timeline", function() { - const timelineIndex = new TimelineIndex(createTimeline(), 0); - const result = timelineIndex.advance(-2); - expect(result).toEqual(-1); - expect(timelineIndex.index).toEqual(-1); - }); - - it("should advance into the next timeline", function() { - const timelines = createLinkedTimelines(); - const tl1 = timelines[0]; - const tl2 = timelines[1]; - - // initialise the index pointing at the end of the first timeline - const timelineIndex = new TimelineIndex(tl1, 2); - - const result = timelineIndex.advance(1); - expect(result).toEqual(1); - expect(timelineIndex.timeline).toBe(tl2); - - // we expect the index to be the zero (ie, the same as the - // BaseIndex), because the BaseIndex points at the second event, - // and we've advanced past the first. - expect(timelineIndex.index).toEqual(0); - }); - - it("should retreat into the previous timeline", function() { - const timelines = createLinkedTimelines(); - const tl1 = timelines[0]; - const tl2 = timelines[1]; - - // initialise the index pointing at the start of the second - // timeline - const timelineIndex = new TimelineIndex(tl2, -1); - - const result = timelineIndex.advance(-1); - expect(result).toEqual(-1); - expect(timelineIndex.timeline).toBe(tl1); - expect(timelineIndex.index).toEqual(1); - }); - }); - - describe("retreat", function() { - it("should retreat up to the start of the timeline", function() { - const timelineIndex = new TimelineIndex(createTimeline(), 0); - const result = timelineIndex.retreat(2); - expect(result).toEqual(1); - expect(timelineIndex.index).toEqual(-1); - }); - }); -}); - -describe("TimelineWindow", function() { - /** - * create a dummy eventTimelineSet and client, and a TimelineWindow - * attached to them. - */ - let timelineSet; - let client; - function createWindow(timeline, opts) { - timelineSet = { getTimelineForEvent: () => null }; - client = {}; - client.getEventTimeline = function(timelineSet0, eventId0) { - expect(timelineSet0).toBe(timelineSet); - return Promise.resolve(timeline); - }; - - return new TimelineWindow(client, timelineSet, opts); - } - - describe("load", function() { - it("should initialise from the live timeline", function() { - const liveTimeline = createTimeline(); - const room = {}; - room.getLiveTimeline = function() { - return liveTimeline; - }; - - const timelineWindow = new TimelineWindow(undefined, room); - return timelineWindow.load(undefined, 2).then(function() { - const expectedEvents = liveTimeline.getEvents().slice(1); - expect(timelineWindow.getEvents()).toEqual(expectedEvents); - }); - }); - - it("should initialise from a specific event", function() { - const timeline = createTimeline(); - const eventId = timeline.getEvents()[1].getId(); - - const timelineSet = { getTimelineForEvent: () => null }; - const client = {}; - client.getEventTimeline = function(timelineSet0, eventId0) { - expect(timelineSet0).toBe(timelineSet); - expect(eventId0).toEqual(eventId); - return Promise.resolve(timeline); - }; - - const timelineWindow = new TimelineWindow(client, timelineSet); - return timelineWindow.load(eventId, 3).then(function() { - const expectedEvents = timeline.getEvents(); - expect(timelineWindow.getEvents()).toEqual(expectedEvents); - }); - }); - - it("canPaginate should return false until load has returned", function() { - const timeline = createTimeline(); - timeline.setPaginationToken("toktok1", EventTimeline.BACKWARDS); - timeline.setPaginationToken("toktok2", EventTimeline.FORWARDS); - - const eventId = timeline.getEvents()[1].getId(); - - const timelineSet = { getTimelineForEvent: () => null }; - const client = {}; - - const timelineWindow = new TimelineWindow(client, timelineSet); - - client.getEventTimeline = function(timelineSet0, eventId0) { - expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)) - .toBe(false); - expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)) - .toBe(false); - return Promise.resolve(timeline); - }; - - return timelineWindow.load(eventId, 3).then(function() { - const expectedEvents = timeline.getEvents(); - expect(timelineWindow.getEvents()).toEqual(expectedEvents); - expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)) - .toBe(true); - expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)) - .toBe(true); - }); - }); - }); - - describe("pagination", function() { - it("should be able to advance across the initial timeline", function() { - const timeline = createTimeline(); - const eventId = timeline.getEvents()[1].getId(); - const timelineWindow = createWindow(timeline); - - return timelineWindow.load(eventId, 1).then(function() { - const expectedEvents = [timeline.getEvents()[1]]; - expect(timelineWindow.getEvents()).toEqual(expectedEvents); - - expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)) - .toBe(true); - expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)) - .toBe(true); - - return timelineWindow.paginate(EventTimeline.FORWARDS, 2); - }).then(function(success) { - expect(success).toBe(true); - const expectedEvents = timeline.getEvents().slice(1); - expect(timelineWindow.getEvents()).toEqual(expectedEvents); - - expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)) - .toBe(true); - expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)) - .toBe(false); - - return timelineWindow.paginate(EventTimeline.FORWARDS, 2); - }).then(function(success) { - expect(success).toBe(false); - - return timelineWindow.paginate(EventTimeline.BACKWARDS, 2); - }).then(function(success) { - expect(success).toBe(true); - const expectedEvents = timeline.getEvents(); - expect(timelineWindow.getEvents()).toEqual(expectedEvents); - - expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)) - .toBe(false); - expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)) - .toBe(false); - return timelineWindow.paginate(EventTimeline.BACKWARDS, 2); - }).then(function(success) { - expect(success).toBe(false); - }); - }); - - it("should advance into next timeline", function() { - const tls = createLinkedTimelines(); - const eventId = tls[0].getEvents()[1].getId(); - const timelineWindow = createWindow(tls[0], { windowLimit: 5 }); - - return timelineWindow.load(eventId, 3).then(function() { - const expectedEvents = tls[0].getEvents(); - expect(timelineWindow.getEvents()).toEqual(expectedEvents); - - expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)) - .toBe(false); - expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)) - .toBe(true); - - return timelineWindow.paginate(EventTimeline.FORWARDS, 2); - }).then(function(success) { - expect(success).toBe(true); - const expectedEvents = tls[0].getEvents() - .concat(tls[1].getEvents().slice(0, 2)); - expect(timelineWindow.getEvents()).toEqual(expectedEvents); - - expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)) - .toBe(false); - expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)) - .toBe(true); - - return timelineWindow.paginate(EventTimeline.FORWARDS, 2); - }).then(function(success) { - expect(success).toBe(true); - // the windowLimit should have made us drop an event from - // tls[0] - const expectedEvents = tls[0].getEvents().slice(1) - .concat(tls[1].getEvents()); - expect(timelineWindow.getEvents()).toEqual(expectedEvents); - - expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)) - .toBe(true); - expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)) - .toBe(false); - return timelineWindow.paginate(EventTimeline.FORWARDS, 2); - }).then(function(success) { - expect(success).toBe(false); - }); - }); - - it("should retreat into previous timeline", function() { - const tls = createLinkedTimelines(); - const eventId = tls[1].getEvents()[1].getId(); - const timelineWindow = createWindow(tls[1], { windowLimit: 5 }); - - return timelineWindow.load(eventId, 3).then(function() { - const expectedEvents = tls[1].getEvents(); - expect(timelineWindow.getEvents()).toEqual(expectedEvents); - - expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)) - .toBe(true); - expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)) - .toBe(false); - - return timelineWindow.paginate(EventTimeline.BACKWARDS, 2); - }).then(function(success) { - expect(success).toBe(true); - const expectedEvents = tls[0].getEvents().slice(1, 3) - .concat(tls[1].getEvents()); - expect(timelineWindow.getEvents()).toEqual(expectedEvents); - - expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)) - .toBe(true); - expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)) - .toBe(false); - - return timelineWindow.paginate(EventTimeline.BACKWARDS, 2); - }).then(function(success) { - expect(success).toBe(true); - // the windowLimit should have made us drop an event from - // tls[1] - const expectedEvents = tls[0].getEvents() - .concat(tls[1].getEvents().slice(0, 2)); - expect(timelineWindow.getEvents()).toEqual(expectedEvents); - - expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)) - .toBe(false); - expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)) - .toBe(true); - return timelineWindow.paginate(EventTimeline.BACKWARDS, 2); - }).then(function(success) { - expect(success).toBe(false); - }); - }); - - it("should make forward pagination requests", function() { - const timeline = createTimeline(); - timeline.setPaginationToken("toktok", EventTimeline.FORWARDS); - - const timelineWindow = createWindow(timeline, { windowLimit: 5 }); - const eventId = timeline.getEvents()[1].getId(); - - client.paginateEventTimeline = function(timeline0, opts) { - expect(timeline0).toBe(timeline); - expect(opts.backwards).toBe(false); - expect(opts.limit).toEqual(2); - - addEventsToTimeline(timeline, 3, false); - return Promise.resolve(true); - }; - - return timelineWindow.load(eventId, 3).then(function() { - const expectedEvents = timeline.getEvents(); - expect(timelineWindow.getEvents()).toEqual(expectedEvents); - - expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)) - .toBe(false); - expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)) - .toBe(true); - return timelineWindow.paginate(EventTimeline.FORWARDS, 2); - }).then(function(success) { - expect(success).toBe(true); - const expectedEvents = timeline.getEvents().slice(0, 5); - expect(timelineWindow.getEvents()).toEqual(expectedEvents); - }); - }); - - it("should make backward pagination requests", function() { - const timeline = createTimeline(); - timeline.setPaginationToken("toktok", EventTimeline.BACKWARDS); - - const timelineWindow = createWindow(timeline, { windowLimit: 5 }); - const eventId = timeline.getEvents()[1].getId(); - - client.paginateEventTimeline = function(timeline0, opts) { - expect(timeline0).toBe(timeline); - expect(opts.backwards).toBe(true); - expect(opts.limit).toEqual(2); - - addEventsToTimeline(timeline, 3, true); - return Promise.resolve(true); - }; - - return timelineWindow.load(eventId, 3).then(function() { - const expectedEvents = timeline.getEvents(); - expect(timelineWindow.getEvents()).toEqual(expectedEvents); - - expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)) - .toBe(true); - expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)) - .toBe(false); - return timelineWindow.paginate(EventTimeline.BACKWARDS, 2); - }).then(function(success) { - expect(success).toBe(true); - const expectedEvents = timeline.getEvents().slice(1, 6); - expect(timelineWindow.getEvents()).toEqual(expectedEvents); - }); - }); - - it("should limit the number of unsuccessful pagination requests", function() { - const timeline = createTimeline(); - timeline.setPaginationToken("toktok", EventTimeline.FORWARDS); - - const timelineWindow = createWindow(timeline, { windowLimit: 5 }); - const eventId = timeline.getEvents()[1].getId(); - - let paginateCount = 0; - client.paginateEventTimeline = function(timeline0, opts) { - expect(timeline0).toBe(timeline); - expect(opts.backwards).toBe(false); - expect(opts.limit).toEqual(2); - paginateCount += 1; - return Promise.resolve(true); - }; - - return timelineWindow.load(eventId, 3).then(function() { - const expectedEvents = timeline.getEvents(); - expect(timelineWindow.getEvents()).toEqual(expectedEvents); - - expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)) - .toBe(false); - expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)) - .toBe(true); - return timelineWindow.paginate(EventTimeline.FORWARDS, 2, true, 3); - }).then(function(success) { - expect(success).toBe(false); - expect(paginateCount).toEqual(3); - const expectedEvents = timeline.getEvents().slice(0, 3); - expect(timelineWindow.getEvents()).toEqual(expectedEvents); - - expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)) - .toBe(false); - expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)) - .toBe(true); - }); - }); - }); -}); diff --git a/spec/unit/timeline-window.spec.ts b/spec/unit/timeline-window.spec.ts new file mode 100644 index 00000000000..b8c84c422f7 --- /dev/null +++ b/spec/unit/timeline-window.spec.ts @@ -0,0 +1,439 @@ +/* +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 { MockedObject } from 'jest-mock'; + +import { MatrixClient } from "../../src/client"; +import { EventTimelineSet } from "../../src/models/event-timeline-set"; +import { Room } from "../../src/models/room"; +import { EventTimeline } from "../../src/models/event-timeline"; +import { TimelineIndex, TimelineWindow } from "../../src/timeline-window"; +import { mkMessage } from "../test-utils/test-utils"; + +const ROOM_ID = "roomId"; +const USER_ID = "userId"; +const mockClient = { + getEventTimeline: jest.fn(), + paginateEventTimeline: jest.fn(), +} as unknown as MockedObject; + +/* + * create a timeline with a bunch (default 3) events. + * baseIndex is 1 by default. + */ +function createTimeline(numEvents = 3, baseIndex = 1): EventTimeline { + const room = new Room(ROOM_ID, mockClient, USER_ID); + const timelineSet = new EventTimelineSet(room); + jest.spyOn(timelineSet.room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet); + + const timeline = new EventTimeline(timelineSet); + + // add the events after the baseIndex first + addEventsToTimeline(timeline, numEvents - baseIndex, false); + + // then add those before the baseIndex + addEventsToTimeline(timeline, baseIndex, true); + + expect(timeline.getBaseIndex()).toEqual(baseIndex); + return timeline; +} + +function addEventsToTimeline(timeline: EventTimeline, numEvents: number, toStartOfTimeline: boolean) { + for (let i = 0; i < numEvents; i++) { + timeline.addEvent( + mkMessage({ + room: ROOM_ID, user: USER_ID, + event: true, + }), + { toStartOfTimeline }, + ); + } +} + +/* + * create a pair of linked timelines + */ +function createLinkedTimelines(): [EventTimeline, EventTimeline] { + const tl1 = createTimeline(); + const tl2 = createTimeline(); + tl1.setNeighbouringTimeline(tl2, EventTimeline.FORWARDS); + tl2.setNeighbouringTimeline(tl1, EventTimeline.BACKWARDS); + return [tl1, tl2]; +} + +describe("TimelineIndex", function() { + beforeEach(() => { + jest.clearAllMocks(); + mockClient.getEventTimeline.mockResolvedValue(undefined); + }); + + describe("minIndex", function() { + it("should return the min index relative to BaseIndex", function() { + const timelineIndex = new TimelineIndex(createTimeline(), 0); + expect(timelineIndex.minIndex()).toEqual(-1); + }); + }); + + describe("maxIndex", function() { + it("should return the max index relative to BaseIndex", function() { + const timelineIndex = new TimelineIndex(createTimeline(), 0); + expect(timelineIndex.maxIndex()).toEqual(2); + }); + }); + + describe("advance", function() { + it("should advance up to the end of the timeline", function() { + const timelineIndex = new TimelineIndex(createTimeline(), 0); + const result = timelineIndex.advance(3); + expect(result).toEqual(2); + expect(timelineIndex.index).toEqual(2); + }); + + it("should retreat back to the start of the timeline", function() { + const timelineIndex = new TimelineIndex(createTimeline(), 0); + const result = timelineIndex.advance(-2); + expect(result).toEqual(-1); + expect(timelineIndex.index).toEqual(-1); + }); + + it("should advance into the next timeline", function() { + const timelines = createLinkedTimelines(); + const tl1 = timelines[0]; + const tl2 = timelines[1]; + + // initialise the index pointing at the end of the first timeline + const timelineIndex = new TimelineIndex(tl1, 2); + + const result = timelineIndex.advance(1); + expect(result).toEqual(1); + expect(timelineIndex.timeline).toBe(tl2); + + // we expect the index to be the zero (ie, the same as the + // BaseIndex), because the BaseIndex points at the second event, + // and we've advanced past the first. + expect(timelineIndex.index).toEqual(0); + }); + + it("should retreat into the previous timeline", function() { + const timelines = createLinkedTimelines(); + const tl1 = timelines[0]; + const tl2 = timelines[1]; + + // initialise the index pointing at the start of the second + // timeline + const timelineIndex = new TimelineIndex(tl2, -1); + + const result = timelineIndex.advance(-1); + expect(result).toEqual(-1); + expect(timelineIndex.timeline).toBe(tl1); + expect(timelineIndex.index).toEqual(1); + }); + }); + + describe("retreat", function() { + it("should retreat up to the start of the timeline", function() { + const timelineIndex = new TimelineIndex(createTimeline(), 0); + const result = timelineIndex.retreat(2); + expect(result).toEqual(1); + expect(timelineIndex.index).toEqual(-1); + }); + }); +}); + +describe("TimelineWindow", function() { + /** + * create a dummy eventTimelineSet and client, and a TimelineWindow + * attached to them. + */ + function createWindow(timeline: EventTimeline, opts?: { + windowLimit?: number; + }): [TimelineWindow, EventTimelineSet] { + const timelineSet = { getTimelineForEvent: () => null } as unknown as EventTimelineSet; + mockClient.getEventTimeline.mockResolvedValue(timeline); + + return [new TimelineWindow(mockClient, timelineSet, opts), timelineSet]; + } + + beforeEach(() => { + jest.clearAllMocks(); + mockClient.getEventTimeline.mockResolvedValue(undefined); + mockClient.paginateEventTimeline.mockReturnValue(undefined); + }); + + describe("load", function() { + it("should initialise from the live timeline", async function() { + const liveTimeline = createTimeline(); + const room = new Room(ROOM_ID, mockClient, USER_ID); + const timelineSet = new EventTimelineSet(room); + jest.spyOn(timelineSet, 'getLiveTimeline').mockReturnValue(liveTimeline); + + const timelineWindow = new TimelineWindow(mockClient, timelineSet); + await timelineWindow.load(undefined, 2); + + expect(timelineSet.getLiveTimeline).toHaveBeenCalled(); + + const expectedEvents = liveTimeline.getEvents().slice(1); + expect(timelineWindow.getEvents()).toEqual(expectedEvents); + }); + + it("should initialise from a specific event", async function() { + const timeline = createTimeline(); + const eventId = timeline.getEvents()[1].getId(); + + const timelineSet = { getTimelineForEvent: () => null } as unknown as EventTimelineSet; + mockClient.getEventTimeline.mockResolvedValue(timeline); + + const timelineWindow = new TimelineWindow(mockClient, timelineSet); + await timelineWindow.load(eventId, 3); + expect(mockClient.getEventTimeline).toHaveBeenCalledWith(timelineSet, eventId); + const expectedEvents = timeline.getEvents(); + expect(timelineWindow.getEvents()).toEqual(expectedEvents); + }); + + it("canPaginate should return false until load has returned", async function() { + const timeline = createTimeline(); + timeline.setPaginationToken("toktok1", EventTimeline.BACKWARDS); + timeline.setPaginationToken("toktok2", EventTimeline.FORWARDS); + + const eventId = timeline.getEvents()[1].getId(); + + const timelineSet = { getTimelineForEvent: () => null } as unknown as EventTimelineSet; + mockClient.getEventTimeline.mockResolvedValue(timeline); + + const timelineWindow = new TimelineWindow(mockClient, timelineSet); + + const timelineWindowLoadPromise = timelineWindow.load(eventId, 3); + + // cannot paginate before load is complete + expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)).toBe(false); + expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)).toBe(false); + + // wait for load + await timelineWindowLoadPromise; + const expectedEvents = timeline.getEvents(); + expect(timelineWindow.getEvents()).toEqual(expectedEvents); + + // can paginate now + expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)) + .toBe(true); + expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)) + .toBe(true); + }); + }); + + describe("pagination", function() { + it("should be able to advance across the initial timeline", async function() { + const timeline = createTimeline(); + const eventId = timeline.getEvents()[1].getId(); + const [timelineWindow] = createWindow(timeline); + + await timelineWindow.load(eventId, 1); + + const expectedEvents = [timeline.getEvents()[1]]; + expect(timelineWindow.getEvents()).toEqual(expectedEvents); + + expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)) + .toBe(true); + expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)) + .toBe(true); + + expect(await timelineWindow.paginate(EventTimeline.FORWARDS, 2)).toBe(true); + const expectedEventsAfterPagination = timeline.getEvents().slice(1); + expect(timelineWindow.getEvents()).toEqual(expectedEventsAfterPagination); + + expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)) + .toBe(true); + expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)) + .toBe(false); + + // cant paginate forward anymore + expect(await timelineWindow.paginate(EventTimeline.FORWARDS, 2)).toBe(false); + + // paginate back again + expect(await timelineWindow.paginate(EventTimeline.BACKWARDS, 2)).toBe(true); + + const expectedEvents3 = timeline.getEvents(); + expect(timelineWindow.getEvents()).toEqual(expectedEvents3); + + expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)) + .toBe(false); + expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)) + .toBe(false); + expect(await timelineWindow.paginate(EventTimeline.BACKWARDS, 2)).toBe(false); + }); + + it("should advance into next timeline", async function() { + const tls = createLinkedTimelines(); + const eventId = tls[0].getEvents()[1].getId(); + const [timelineWindow] = createWindow(tls[0], { windowLimit: 5 }); + + await timelineWindow.load(eventId, 3); + const expectedEvents = tls[0].getEvents(); + expect(timelineWindow.getEvents()).toEqual(expectedEvents); + + expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)) + .toBe(false); + expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)) + .toBe(true); + + expect(await timelineWindow.paginate(EventTimeline.FORWARDS, 2)).toBe(true); + const expectedEvents2 = tls[0].getEvents() + .concat(tls[1].getEvents().slice(0, 2)); + expect(timelineWindow.getEvents()).toEqual(expectedEvents2); + + expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)) + .toBe(false); + expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)) + .toBe(true); + + expect(await timelineWindow.paginate(EventTimeline.FORWARDS, 2)).toBe(true); + + // the windowLimit should have made us drop an event from + // tls[0] + const expectedEvents3 = tls[0].getEvents().slice(1) + .concat(tls[1].getEvents()); + expect(timelineWindow.getEvents()).toEqual(expectedEvents3); + + expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)) + .toBe(true); + expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)) + .toBe(false); + expect(await timelineWindow.paginate(EventTimeline.FORWARDS, 2)).toBe(false); + }); + + it("should retreat into previous timeline", async function() { + const tls = createLinkedTimelines(); + const eventId = tls[1].getEvents()[1].getId(); + const [timelineWindow] = createWindow(tls[1], { windowLimit: 5 }); + + await timelineWindow.load(eventId, 3); + + const expectedEvents = tls[1].getEvents(); + expect(timelineWindow.getEvents()).toEqual(expectedEvents); + + expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)) + .toBe(true); + expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)) + .toBe(false); + + expect(await timelineWindow.paginate(EventTimeline.BACKWARDS, 2)).toBe(true); + const expectedEvents2 = tls[0].getEvents().slice(1, 3) + .concat(tls[1].getEvents()); + expect(timelineWindow.getEvents()).toEqual(expectedEvents2); + + expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)) + .toBe(true); + expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)) + .toBe(false); + + expect(await timelineWindow.paginate(EventTimeline.BACKWARDS, 2)).toBe(true); + // the windowLimit should have made us drop an event from + // tls[1] + const expectedEvents3 = tls[0].getEvents() + .concat(tls[1].getEvents().slice(0, 2)); + expect(timelineWindow.getEvents()).toEqual(expectedEvents3); + + expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)) + .toBe(false); + expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)) + .toBe(true); + expect(await timelineWindow.paginate(EventTimeline.BACKWARDS, 2)).toBe(false); + }); + + it("should make forward pagination requests", async function() { + const timeline = createTimeline(); + timeline.setPaginationToken("toktok", EventTimeline.FORWARDS); + + const [timelineWindow] = createWindow(timeline, { windowLimit: 5 }); + const eventId = timeline.getEvents()[1].getId(); + + mockClient.paginateEventTimeline.mockImplementation(async (_t, _opts) => { + addEventsToTimeline(timeline, 3, false); + return true; + }); + + await timelineWindow.load(eventId, 3); + const expectedEvents = timeline.getEvents(); + expect(timelineWindow.getEvents()).toEqual(expectedEvents); + expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)).toBe(false); + expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)).toBe(true); + + expect(await timelineWindow.paginate(EventTimeline.FORWARDS, 2)).toBe(true); + expect(mockClient.paginateEventTimeline).toHaveBeenCalledWith(timeline, { backwards: false, limit: 2 }); + const expectedEvents2 = timeline.getEvents().slice(0, 5); + expect(timelineWindow.getEvents()).toEqual(expectedEvents2); + }); + + it("should make backward pagination requests", async function() { + const timeline = createTimeline(); + timeline.setPaginationToken("toktok", EventTimeline.BACKWARDS); + + const [timelineWindow] = createWindow(timeline, { windowLimit: 5 }); + const eventId = timeline.getEvents()[1].getId(); + + mockClient.paginateEventTimeline.mockImplementation(async (_t, _opts) => { + addEventsToTimeline(timeline, 3, true); + return true; + }); + + await timelineWindow.load(eventId, 3); + const expectedEvents = timeline.getEvents(); + expect(timelineWindow.getEvents()).toEqual(expectedEvents); + + expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)).toBe(true); + expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)).toBe(false); + + expect(await timelineWindow.paginate(EventTimeline.BACKWARDS, 2)).toBe(true); + expect(mockClient.paginateEventTimeline).toHaveBeenCalledWith(timeline, { backwards: true, limit: 2 }); + + const expectedEvents2 = timeline.getEvents().slice(1, 6); + expect(timelineWindow.getEvents()).toEqual(expectedEvents2); + }); + + it("should limit the number of unsuccessful pagination requests", async function() { + const timeline = createTimeline(); + timeline.setPaginationToken("toktok", EventTimeline.FORWARDS); + + const [timelineWindow] = createWindow(timeline, { windowLimit: 5 }); + const eventId = timeline.getEvents()[1].getId(); + + mockClient.paginateEventTimeline.mockImplementation(async (_t, _opts) => { + return true; + }); + + await timelineWindow.load(eventId, 3); + const expectedEvents = timeline.getEvents(); + expect(timelineWindow.getEvents()).toEqual(expectedEvents); + + expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)) + .toBe(false); + expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)) + .toBe(true); + expect(await timelineWindow.paginate(EventTimeline.FORWARDS, 2, true, 3)).toBe(false); + + expect(mockClient.paginateEventTimeline).toHaveBeenCalledWith(timeline, { backwards: false, limit: 2 }); + + expect(mockClient.paginateEventTimeline).toHaveBeenCalledTimes(3); + const expectedEvents2 = timeline.getEvents().slice(0, 3); + expect(timelineWindow.getEvents()).toEqual(expectedEvents2); + + expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)) + .toBe(false); + expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)) + .toBe(true); + }); + }); +}); diff --git a/spec/unit/user.spec.js b/spec/unit/user.spec.ts similarity index 71% rename from spec/unit/user.spec.js rename to spec/unit/user.spec.ts index babe6e4d716..42cf42150f7 100644 --- a/spec/unit/user.spec.js +++ b/spec/unit/user.spec.ts @@ -1,16 +1,32 @@ -import { User } from "../../src/models/user"; -import * as utils from "../test-utils/test-utils"; +/* +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 { User, UserEvent } from "../../src/models/user"; +import { mkEvent } from "../test-utils/test-utils"; describe("User", function() { const userId = "@alice:bar"; - let user; + let user: User; beforeEach(function() { user = new User(userId); }); describe("setPresenceEvent", function() { - const event = utils.mkEvent({ + const event = mkEvent({ type: "m.presence", content: { presence: "online", user_id: userId, @@ -22,7 +38,7 @@ describe("User", function() { it("should emit 'User.displayName' if the display name changes", function() { let emitCount = 0; - user.on("User.displayName", function(ev, usr) { + user.on(UserEvent.DisplayName, function(ev, usr) { emitCount += 1; }); user.setPresenceEvent(event); @@ -33,7 +49,7 @@ describe("User", function() { it("should emit 'User.avatarUrl' if the avatar URL changes", function() { let emitCount = 0; - user.on("User.avatarUrl", function(ev, usr) { + user.on(UserEvent.AvatarUrl, function(ev, usr) { emitCount += 1; }); user.setPresenceEvent(event); @@ -44,7 +60,7 @@ describe("User", function() { it("should emit 'User.presence' if the presence changes", function() { let emitCount = 0; - user.on("User.presence", function(ev, usr) { + user.on(UserEvent.Presence, function(ev, usr) { emitCount += 1; }); user.setPresenceEvent(event);