diff --git a/codecov.yaml b/codecov.yaml index e1289344..f08dc9b2 100644 --- a/codecov.yaml +++ b/codecov.yaml @@ -13,7 +13,6 @@ coverage: informational: true patch: default: - # Encourage (but don't enforce) 80% coverage on all lines that a PR + # Enforce 80% coverage on all lines that a PR # touches target: 80% - informational: true diff --git a/src/reactions/RaisedHandIndicator.test.tsx b/src/reactions/RaisedHandIndicator.test.tsx index fedd8ec2..62e3ffb5 100644 --- a/src/reactions/RaisedHandIndicator.test.tsx +++ b/src/reactions/RaisedHandIndicator.test.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { describe, expect, test } from "vitest"; +import { beforeEach, describe, expect, test, vi } from "vitest"; import { render, configure } from "@testing-library/react"; import { RaisedHandIndicator } from "./RaisedHandIndicator"; @@ -15,6 +15,13 @@ configure({ }); describe("RaisedHandIndicator", () => { + const fixedTime = new Date("2025-01-01T12:00:00.000Z"); + + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(fixedTime); + }); + test("renders nothing when no hand has been raised", () => { const { container } = render(); expect(container.firstChild).toBeNull(); diff --git a/src/reactions/__snapshots__/RaisedHandIndicator.test.tsx.snap b/src/reactions/__snapshots__/RaisedHandIndicator.test.tsx.snap index ab6fafa3..43c3f928 100644 --- a/src/reactions/__snapshots__/RaisedHandIndicator.test.tsx.snap +++ b/src/reactions/__snapshots__/RaisedHandIndicator.test.tsx.snap @@ -15,7 +15,7 @@ exports[`RaisedHandIndicator > renders a smaller indicator when miniature is spe

- 00:01 + 00:00

`; @@ -35,7 +35,7 @@ exports[`RaisedHandIndicator > renders an indicator when a hand has been raised

- 00:01 + 00:00

`; @@ -55,7 +55,7 @@ exports[`RaisedHandIndicator > renders an indicator when a hand has been raised

- 01:01 + 01:00

`; diff --git a/src/state/ObservableScope.test.ts b/src/state/ObservableScope.test.ts index 99f2b424..31728f39 100644 --- a/src/state/ObservableScope.test.ts +++ b/src/state/ObservableScope.test.ts @@ -5,9 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { BehaviorSubject, combineLatest, Subject } from "rxjs"; import { logger } from "matrix-js-sdk/lib/logger"; +import { sleep } from "matrix-js-sdk/lib/utils"; import { Epoch, @@ -102,3 +103,137 @@ describe("Epoch", () => { s$.complete(); }); }); + +describe("Reconcile", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("should wait clean up before processing next", async () => { + vi.useFakeTimers(); + const scope = new ObservableScope(); + const behavior$ = new BehaviorSubject(0); + + const setup = vi.fn().mockImplementation(async () => await sleep(100)); + const cleanup = vi + .fn() + .mockImplementation(async (n: number) => await sleep(100)); + scope.reconcile(behavior$, async (value) => { + await setup(); + return async (): Promise => { + await cleanup(value); + }; + }); + // Let the initial setup process + await vi.advanceTimersByTimeAsync(120); + expect(setup).toHaveBeenCalledTimes(1); + expect(cleanup).toHaveBeenCalledTimes(0); + + // Send next value + behavior$.next(1); + await vi.advanceTimersByTimeAsync(50); + // Should not have started setup for 1 yet + expect(setup).toHaveBeenCalledTimes(1); + expect(cleanup).toHaveBeenCalledTimes(1); + expect(cleanup).toHaveBeenCalledWith(0); + + // Let cleanup finish + await vi.advanceTimersByTimeAsync(50); + // Now setup for 1 should have started + expect(setup).toHaveBeenCalledTimes(2); + }); + + it("should skip intermediates values that are not setup", async () => { + vi.useFakeTimers(); + const scope = new ObservableScope(); + const behavior$ = new BehaviorSubject(0); + + const setup = vi + .fn() + .mockImplementation(async (n: number) => await sleep(100)); + + const cleanupLock = Promise.withResolvers(); + const cleanup = vi + .fn() + .mockImplementation(async (n: number) => await cleanupLock.promise); + + scope.reconcile(behavior$, async (value) => { + await setup(value); + return async (): Promise => { + await cleanup(value); + }; + }); + // Let the initial setup process (0) + await vi.advanceTimersByTimeAsync(120); + + // Send 4 next values quickly + behavior$.next(1); + behavior$.next(2); + behavior$.next(3); + behavior$.next(4); + + await vi.advanceTimersByTimeAsync(3000); + // should have only called cleanup for 0 + expect(cleanup).toHaveBeenCalledTimes(1); + expect(cleanup).toHaveBeenCalledWith(0); + // Let cleanup finish + cleanupLock.resolve(undefined); + await vi.advanceTimersByTimeAsync(120); + + // Now setup for 4 should have started, skipping 1,2,3 + expect(setup).toHaveBeenCalledTimes(2); + expect(setup).toHaveBeenCalledWith(4); + expect(setup).not.toHaveBeenCalledWith(1); + expect(setup).not.toHaveBeenCalledWith(2); + expect(setup).not.toHaveBeenCalledWith(3); + }); + + it("should wait for setup to complete before starting cleanup", async () => { + vi.useFakeTimers(); + const scope = new ObservableScope(); + const behavior$ = new BehaviorSubject(0); + + const setup = vi + .fn() + .mockImplementation(async (n: number) => await sleep(3000)); + + const cleanupLock = Promise.withResolvers(); + const cleanup = vi + .fn() + .mockImplementation(async (n: number) => await cleanupLock.promise); + + scope.reconcile(behavior$, async (value) => { + await setup(value); + return async (): Promise => { + await cleanup(value); + }; + }); + + await vi.advanceTimersByTimeAsync(500); + // Setup for 0 should be in progress + expect(setup).toHaveBeenCalledTimes(1); + + behavior$.next(1); + await vi.advanceTimersByTimeAsync(500); + + // Should not have started setup for 1 yet + expect(setup).not.toHaveBeenCalledWith(1); + // Should not have called cleanup yet, because the setup for 0 is not done + expect(cleanup).toHaveBeenCalledTimes(0); + + // Let setup for 0 finish + await vi.advanceTimersByTimeAsync(2500 + 100); + // Now cleanup for 0 should have started + expect(cleanup).toHaveBeenCalledTimes(1); + expect(cleanup).toHaveBeenCalledWith(0); + + cleanupLock.resolve(undefined); + await vi.advanceTimersByTimeAsync(100); + // Now setup for 1 should have started + expect(setup).toHaveBeenCalledWith(1); + }); +});