diff --git a/src/behaviours/application-update/analytics-for-installing-update.test.ts b/src/behaviours/application-update/analytics-for-installing-update.test.ts index c33a3f92af..018d2361da 100644 --- a/src/behaviours/application-update/analytics-for-installing-update.test.ts +++ b/src/behaviours/application-update/analytics-for-installing-update.test.ts @@ -18,6 +18,7 @@ import downloadPlatformUpdateInjectable from "../../main/application-update/down import quitAndInstallUpdateInjectable from "../../main/application-update/quit-and-install-update.injectable"; import appVersionInjectable from "../../common/get-configuration-file-model/app-version/app-version.injectable"; import periodicalCheckForUpdatesInjectable from "../../main/application-update/periodical-check-for-updates/periodical-check-for-updates.injectable"; +import { advanceFakeTime, useFakeTime } from "../../common/test-utils/use-fake-time"; describe("analytics for installing update", () => { let applicationBuilder: ApplicationBuilder; @@ -27,9 +28,7 @@ describe("analytics for installing update", () => { let mainDi: DiContainer; beforeEach(async () => { - jest.useFakeTimers(); - - global.Date.now = () => new Date("2015-10-21T07:28:00Z").getTime(); + useFakeTime("2015-10-21T07:28:00Z"); applicationBuilder = getApplicationBuilder(); @@ -84,14 +83,14 @@ describe("analytics for installing update", () => { it("when enough time passes to check for updates again, sends event to analytics for being checked periodically", () => { analyticsListenerMock.mockClear(); - jest.advanceTimersByTime(1000 * 60 * 60 * 2); + advanceFakeTime(1000 * 60 * 60 * 2); expect(analyticsListenerMock).toHaveBeenCalledWith({ name: "app", action: "checking-for-updates", params: { - currentDateTime: "2015-10-21T07:28:00Z", + currentDateTime: "2015-10-21T09:28:00Z", source: "periodic", }, }); diff --git a/src/behaviours/application-update/installing-update-using-topbar-button.test.tsx b/src/behaviours/application-update/installing-update-using-topbar-button.test.tsx index f2b9a58dd1..c639798fac 100644 --- a/src/behaviours/application-update/installing-update-using-topbar-button.test.tsx +++ b/src/behaviours/application-update/installing-update-using-topbar-button.test.tsx @@ -16,6 +16,7 @@ import type { ApplicationBuilder } from "../../renderer/components/test-utils/ge import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import processCheckingForUpdatesInjectable from "../../main/application-update/check-for-updates/process-checking-for-updates.injectable"; import quitAndInstallUpdateInjectable from "../../main/application-update/quit-and-install-update.injectable"; +import { advanceFakeTime, useFakeTime } from "../../common/test-utils/use-fake-time"; function daysToMilliseconds(days: number) { return Math.round(days * 24 * 60 * 60 * 1000); @@ -28,7 +29,7 @@ describe("encourage user to update when sufficient time passed since update was let quitAndInstallUpdateMock: jest.MockedFunction<() => void>; beforeEach(() => { - jest.useFakeTimers(); + useFakeTime("2015-10-21T07:28:00Z"); applicationBuilder = getApplicationBuilder(); @@ -128,13 +129,13 @@ describe("encourage user to update when sufficient time passed since update was }); it("given just enough time passes for medium update encouragement, has medium emotional indication in the button", () => { - jest.advanceTimersByTime(daysToMilliseconds(22)); + advanceFakeTime(daysToMilliseconds(22)); expect(button).toHaveAttribute("data-warning-level", "medium"); }); it("given just enough time passes for severe update encouragement, has severe emotional indication in the button", () => { - jest.advanceTimersByTime(daysToMilliseconds(26)); + advanceFakeTime(daysToMilliseconds(26)); expect(button).toHaveAttribute("data-warning-level", "high"); }); diff --git a/src/behaviours/application-update/installing-update.test.ts b/src/behaviours/application-update/installing-update.test.ts index c81f2954ed..666dbc1e4c 100644 --- a/src/behaviours/application-update/installing-update.test.ts +++ b/src/behaviours/application-update/installing-update.test.ts @@ -16,6 +16,7 @@ import type { DownloadPlatformUpdate } from "../../main/application-update/downl import downloadPlatformUpdateInjectable from "../../main/application-update/download-platform-update/download-platform-update.injectable"; import setUpdateOnQuitInjectable from "../../main/electron-app/features/set-update-on-quit.injectable"; import processCheckingForUpdatesInjectable from "../../main/application-update/check-for-updates/process-checking-for-updates.injectable"; +import { advanceFakeTime, useFakeTime } from "../../common/test-utils/use-fake-time"; describe("installing update", () => { let applicationBuilder: ApplicationBuilder; @@ -25,7 +26,7 @@ describe("installing update", () => { let setUpdateOnQuitMock: jest.Mock; beforeEach(() => { - jest.useFakeTimers(); + useFakeTime("2015-10-21T07:28:00Z"); applicationBuilder = getApplicationBuilder(); @@ -114,8 +115,8 @@ describe("installing update", () => { expect(rendered.baseElement).toMatchSnapshot(); }); - it.skip("when 5 seconds elapses, clears the notification to the user", () => { - jest.advanceTimersByTime(6000); + it("when 5 seconds elapses, clears the notification to the user", () => { + advanceFakeTime(6000); expect(rendered.getByTestId("app-update-idle")).toBeInTheDocument(); }); diff --git a/src/behaviours/application-update/periodical-checking-of-updates.test.ts b/src/behaviours/application-update/periodical-checking-of-updates.test.ts index e81c002e34..37c9d4e92b 100644 --- a/src/behaviours/application-update/periodical-checking-of-updates.test.ts +++ b/src/behaviours/application-update/periodical-checking-of-updates.test.ts @@ -11,6 +11,7 @@ import type { AsyncFnMock } from "@async-fn/jest"; import asyncFn from "@async-fn/jest"; import processCheckingForUpdatesInjectable from "../../main/application-update/check-for-updates/process-checking-for-updates.injectable"; import periodicalCheckForUpdatesInjectable from "../../main/application-update/periodical-check-for-updates/periodical-check-for-updates.injectable"; +import { advanceFakeTime, useFakeTime } from "../../common/test-utils/use-fake-time"; const ENOUGH_TIME = 1000 * 60 * 60 * 2; @@ -19,7 +20,7 @@ describe("periodical checking of updates", () => { let processCheckingForUpdatesMock: AsyncFnMock<() => Promise>; beforeEach(() => { - jest.useFakeTimers(); + useFakeTime("2015-10-21T07:28:00Z"); applicationBuilder = getApplicationBuilder(); @@ -59,7 +60,7 @@ describe("periodical checking of updates", () => { it("when just not enough time passes, does not check for updates again automatically yet", () => { processCheckingForUpdatesMock.mockClear(); - jest.advanceTimersByTime(ENOUGH_TIME - 1); + advanceFakeTime(ENOUGH_TIME - 1); expect(processCheckingForUpdatesMock).not.toHaveBeenCalled(); }); @@ -67,7 +68,7 @@ describe("periodical checking of updates", () => { it("when just enough time passes, checks for updates again automatically", () => { processCheckingForUpdatesMock.mockClear(); - jest.advanceTimersByTime(ENOUGH_TIME); + advanceFakeTime(ENOUGH_TIME); expect(processCheckingForUpdatesMock).toHaveBeenCalled(); }); diff --git a/src/behaviours/cluster/sidebar-and-tab-navigation-for-core.test.tsx b/src/behaviours/cluster/sidebar-and-tab-navigation-for-core.test.tsx index 5258a51d88..96b4a6b2d7 100644 --- a/src/behaviours/cluster/sidebar-and-tab-navigation-for-core.test.tsx +++ b/src/behaviours/cluster/sidebar-and-tab-navigation-for-core.test.tsx @@ -22,6 +22,7 @@ import pathExistsInjectable from "../../common/fs/path-exists.injectable"; import readJsonFileInjectable from "../../common/fs/read-json-file.injectable"; import { navigateToRouteInjectionToken } from "../../common/front-end-routing/navigate-to-route-injection-token"; import sidebarStorageInjectable from "../../renderer/components/layout/sidebar-storage/sidebar-storage.injectable"; +import { advanceFakeTime, useFakeTime } from "../../common/test-utils/use-fake-time"; describe("cluster - sidebar and tab navigation for core", () => { let applicationBuilder: ApplicationBuilder; @@ -29,7 +30,7 @@ describe("cluster - sidebar and tab navigation for core", () => { let rendered: RenderResult; beforeEach(() => { - jest.useFakeTimers(); + useFakeTime("2015-10-21T07:28:00Z"); applicationBuilder = getApplicationBuilder(); rendererDi = applicationBuilder.dis.rendererDi; @@ -262,7 +263,7 @@ describe("cluster - sidebar and tab navigation for core", () => { }); it("when not enough time passes, does not store state for expanded sidebar items to file system yet", async () => { - jest.advanceTimersByTime(250 - 1); + advanceFakeTime(250 - 1); const pathExistsFake = rendererDi.inject(pathExistsInjectable); @@ -274,7 +275,7 @@ describe("cluster - sidebar and tab navigation for core", () => { }); it("when enough time passes, stores state for expanded sidebar items to file system", async () => { - jest.advanceTimersByTime(250); + advanceFakeTime(250); const readJsonFileFake = rendererDi.inject(readJsonFileInjectable); diff --git a/src/behaviours/cluster/sidebar-and-tab-navigation-for-extensions.test.tsx b/src/behaviours/cluster/sidebar-and-tab-navigation-for-extensions.test.tsx index 0f625965b8..75389c6631 100644 --- a/src/behaviours/cluster/sidebar-and-tab-navigation-for-extensions.test.tsx +++ b/src/behaviours/cluster/sidebar-and-tab-navigation-for-extensions.test.tsx @@ -18,6 +18,7 @@ import { navigateToRouteInjectionToken } from "../../common/front-end-routing/na import assert from "assert"; import type { FakeExtensionData } from "../../renderer/components/test-utils/get-renderer-extension-fake"; import { getRendererExtensionFakeFor } from "../../renderer/components/test-utils/get-renderer-extension-fake"; +import { advanceFakeTime, useFakeTime } from "../../common/test-utils/use-fake-time"; describe("cluster - sidebar and tab navigation for extensions", () => { let applicationBuilder: ApplicationBuilder; @@ -25,7 +26,7 @@ describe("cluster - sidebar and tab navigation for extensions", () => { let rendered: RenderResult; beforeEach(() => { - jest.useFakeTimers(); + useFakeTime("2015-10-21T07:28:00Z"); applicationBuilder = getApplicationBuilder(); rendererDi = applicationBuilder.dis.rendererDi; @@ -278,7 +279,7 @@ describe("cluster - sidebar and tab navigation for extensions", () => { }); it("when not enough time passes, does not store state for expanded sidebar items to file system yet", async () => { - jest.advanceTimersByTime(250 - 1); + advanceFakeTime(250 - 1); const pathExistsFake = rendererDi.inject(pathExistsInjectable); @@ -290,7 +291,7 @@ describe("cluster - sidebar and tab navigation for extensions", () => { }); it("when enough time passes, stores state for expanded sidebar items to file system", async () => { - jest.advanceTimersByTime(250); + advanceFakeTime(250); const readJsonFileFake = rendererDi.inject(readJsonFileInjectable); diff --git a/src/behaviours/quitting-and-restarting-the-app/quitting-the-app-using-application-menu.test.ts b/src/behaviours/quitting-and-restarting-the-app/quitting-the-app-using-application-menu.test.ts index 5f96bb06fb..19360d77bd 100644 --- a/src/behaviours/quitting-and-restarting-the-app/quitting-the-app-using-application-menu.test.ts +++ b/src/behaviours/quitting-and-restarting-the-app/quitting-the-app-using-application-menu.test.ts @@ -10,6 +10,7 @@ import { lensWindowInjectionToken } from "../../main/start-main-application/lens import exitAppInjectable from "../../main/electron-app/features/exit-app.injectable"; import clusterManagerInjectable from "../../main/cluster-manager.injectable"; import stopServicesAndExitAppInjectable from "../../main/stop-services-and-exit-app.injectable"; +import { advanceFakeTime, useFakeTime } from "../../common/test-utils/use-fake-time"; describe("quitting the app using application menu", () => { describe("given application has started", () => { @@ -18,7 +19,7 @@ describe("quitting the app using application menu", () => { let exitAppMock: jest.Mock; beforeEach(async () => { - jest.useFakeTimers(); + useFakeTime("2015-10-21T07:28:00Z"); applicationBuilder = getApplicationBuilder().beforeApplicationStart( ({ mainDi }) => { @@ -71,14 +72,14 @@ describe("quitting the app using application menu", () => { }); it("after insufficient time passes, does not terminate application yet", () => { - jest.advanceTimersByTime(999); + advanceFakeTime(999); expect(exitAppMock).not.toHaveBeenCalled(); }); describe("after sufficient time passes", () => { beforeEach(() => { - jest.advanceTimersByTime(1000); + advanceFakeTime(1000); }); it("terminates application", () => { diff --git a/src/common/test-utils/use-fake-time.ts b/src/common/test-utils/use-fake-time.ts new file mode 100644 index 0000000000..77cd385b4b --- /dev/null +++ b/src/common/test-utils/use-fake-time.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { act } from "@testing-library/react"; + +let usingFakeTime = false; + +export const advanceFakeTime = (milliseconds: number) => { + if (!usingFakeTime) { + throw new Error("Tried to advance fake time but it was not enabled. Call useFakeTime() first."); + } + + act(() => { + jest.advanceTimersByTime(milliseconds); + }); +}; + +export const useFakeTime = (dateTime: string) => { + usingFakeTime = true; + + jest.useFakeTimers(); + + jest.setSystemTime(new Date(dateTime)); +}; diff --git a/src/common/utils/reactive-now/reactive-now.test.tsx b/src/common/utils/reactive-now/reactive-now.test.tsx new file mode 100644 index 0000000000..d2deb951b7 --- /dev/null +++ b/src/common/utils/reactive-now/reactive-now.test.tsx @@ -0,0 +1,70 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { RenderResult } from "@testing-library/react"; +import { render } from "@testing-library/react"; +import type { IComputedValue } from "mobx"; +import { computed, observe } from "mobx"; +import React from "react"; +import { observer } from "mobx-react"; +import { advanceFakeTime, useFakeTime } from "../../test-utils/use-fake-time"; +import { reactiveNow } from "./reactive-now"; + +describe("reactiveNow", () => { + let someComputed: IComputedValue; + + beforeEach(() => { + useFakeTime("2015-10-21T07:28:00Z"); + + someComputed = computed(() => { + const currentTimestamp = reactiveNow(); + + return currentTimestamp > new Date("2015-10-21T07:28:00Z").getTime(); + }); + }); + + describe("react-context", () => { + let rendered: RenderResult; + + beforeEach(() => { + const TestComponent = observer( + ({ someComputed }: { someComputed: IComputedValue }) => ( +
{someComputed.get() ? "true" : "false"}
+ ), + ); + + rendered = render(); + }); + + it("given time passes, works", () => { + advanceFakeTime(1000); + + expect(rendered.container.textContent).toBe("true"); + }); + + it("does not share the state from previous test", () => { + expect(rendered.container.textContent).toBe("false"); + }); + }); + + describe("non-react-context", () => { + let actual: boolean; + + beforeEach(() => { + observe(someComputed, (changed) => { + actual = changed.newValue as boolean; + }, true); + }); + + it("given time passes, works", () => { + advanceFakeTime(1000); + + expect(actual).toBe(true); + }); + + it("does not share the state from previous test", () => { + expect(actual).toBe(false); + }); + }); +}); diff --git a/src/common/utils/reactive-now/reactive-now.ts b/src/common/utils/reactive-now/reactive-now.ts new file mode 100644 index 0000000000..febac37010 --- /dev/null +++ b/src/common/utils/reactive-now/reactive-now.ts @@ -0,0 +1,60 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { _isComputingDerivation } from "mobx"; +import type { IResource } from "mobx-utils"; +import { fromResource } from "mobx-utils"; + +// Note: This file is copy-pasted from mobx-utils to fix very specific issue. +// TODO: Remove this file once https://github.com/mobxjs/mobx-utils/issues/306 is fixed. +const tickers: Record> = {}; + +export function reactiveNow(interval?: number | "frame") { + if (interval === void 0) { interval = 1000; } + + if (!_isComputingDerivation()) { + // See #40 + return Date.now(); + } + + // Note: This is the kludge until https://github.com/mobxjs/mobx-utils/issues/306 is fixed + const synchronizationIsEnabled = !process.env.JEST_WORKER_ID; + + if (!tickers[interval] || !synchronizationIsEnabled) { + if (typeof interval === "number") + tickers[interval] = createIntervalTicker(interval); + else + tickers[interval] = createAnimationFrameTicker(); + } + + return tickers[interval].current(); +} + +function createIntervalTicker(interval: number) { + let subscriptionHandle: NodeJS.Timer; + + return fromResource(function (sink) { + sink(Date.now()); + subscriptionHandle = setInterval(function () { return sink(Date.now()); }, interval); + }, function () { + clearInterval(subscriptionHandle); + }, Date.now()); +} + +function createAnimationFrameTicker() { + const frameBasedTicker = fromResource(function (sink) { + sink(Date.now()); + + function scheduleTick() { + window.requestAnimationFrame(function () { + sink(Date.now()); + if (frameBasedTicker.isAlive()) + scheduleTick(); + }); + } + scheduleTick(); + }, function () { }, Date.now()); + + return frameBasedTicker; +} diff --git a/src/renderer/components/duration/reactive-duration.tsx b/src/renderer/components/duration/reactive-duration.tsx index 4430315b38..ce38b82509 100644 --- a/src/renderer/components/duration/reactive-duration.tsx +++ b/src/renderer/components/duration/reactive-duration.tsx @@ -4,9 +4,9 @@ */ import { observer } from "mobx-react"; -import { now } from "mobx-utils"; import React from "react"; import { formatDuration } from "../../utils"; +import { reactiveNow } from "../../../common/utils/reactive-now/reactive-now"; export interface ReactiveDurationProps { timestamp: string | undefined; @@ -42,7 +42,7 @@ export const ReactiveDuration = observer(({ timestamp, compact = true }: Reactiv return ( <> - {formatDuration(now(computeUpdateInterval(timestampSeconds)) - timestampSeconds, compact)} + {formatDuration(reactiveNow(computeUpdateInterval(timestampSeconds)) - timestampSeconds, compact)} ); }); diff --git a/src/renderer/components/status-bar/auto-update-component.tsx b/src/renderer/components/status-bar/auto-update-component.tsx index 5a0b7e8acf..0f6e69c086 100644 --- a/src/renderer/components/status-bar/auto-update-component.tsx +++ b/src/renderer/components/status-bar/auto-update-component.tsx @@ -15,7 +15,7 @@ import type { UpdateIsBeingDownloaded } from "../../../common/application-update import updateIsBeingDownloadedInjectable from "../../../common/application-update/update-is-being-downloaded/update-is-being-downloaded.injectable"; import type { UpdatesAreBeingDiscovered } from "../../../common/application-update/updates-are-being-discovered/updates-are-being-discovered.injectable"; import updatesAreBeingDiscoveredInjectable from "../../../common/application-update/updates-are-being-discovered/updates-are-being-discovered.injectable"; -import { now as reactiveDateNow } from "mobx-utils"; +import { reactiveNow } from "../../../common/utils/reactive-now/reactive-now"; interface Dependencies { progressOfUpdateDownload: ProgressOfUpdateDownload; @@ -32,10 +32,10 @@ interface EndNoteProps { const EndNote = observer(({ version, note }: EndNoteProps) => { const [start] = useState(Date.now()); - if (start + 5000 <= reactiveDateNow()) { + if (start + 5000 <= reactiveNow()) { return idle(); } - + return note(version ?? ""); }); diff --git a/src/renderer/components/update-button/update-warning-level.injectable.ts b/src/renderer/components/update-button/update-warning-level.injectable.ts index 7db021c472..cc963ab27d 100644 --- a/src/renderer/components/update-button/update-warning-level.injectable.ts +++ b/src/renderer/components/update-button/update-warning-level.injectable.ts @@ -4,8 +4,8 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import { computed } from "mobx"; -import { now as reactiveDateNow } from "mobx-utils"; import updateDownloadedDateTimeInjectable from "../../../common/application-update/update-downloaded-date-time/update-downloaded-date-time.injectable"; +import { reactiveNow } from "../../../common/utils/reactive-now/reactive-now"; const updateWarningLevelInjectable = getInjectable({ id: "update-warning-level", @@ -23,7 +23,7 @@ const updateWarningLevelInjectable = getInjectable({ const ONE_DAY = 1000 * 60 * 60 * 24; const downloadedAtTimestamp = new Date(downloadedAt).getTime(); - const currentDateTimeTimestamp = reactiveDateNow(ONE_DAY); + const currentDateTimeTimestamp = reactiveNow(ONE_DAY); const elapsedTime = currentDateTimeTimestamp - downloadedAtTimestamp;