From 6e5c8e0427278c725499a346eaa0cd8187f5530b Mon Sep 17 00:00:00 2001 From: Janne Savolainen Date: Wed, 6 Jul 2022 16:48:00 +0300 Subject: [PATCH] Force update after thirty days since update was downloaded (#5776) --- ...e-since-update-was-downloaded.test.ts.snap | 708 ++++++++++++++++++ ...g-time-since-update-was-downloaded.test.ts | 148 ++++ ...e-modal-root-frame-component.injectable.ts | 36 + .../force-update-modal.module.scss | 25 + .../force-update-modal/force-update-modal.tsx | 69 ++ .../install-update-countdown.injectable.ts | 27 + ...seconds-after-install-starts.injectable.ts | 12 + ...ter-update-must-be-installed.injectable.ts | 14 + ...-since-update-was-downloaded.injectable.ts | 30 + .../countdown/countdown-state.injectable.ts | 53 ++ .../components/countdown/countdown.test.tsx | 142 ++++ .../components/countdown/countdown.tsx | 16 + 12 files changed, 1280 insertions(+) create mode 100644 src/behaviours/application-update/__snapshots__/force-user-to-update-when-too-long-time-since-update-was-downloaded.test.ts.snap create mode 100644 src/behaviours/application-update/force-user-to-update-when-too-long-time-since-update-was-downloaded.test.ts create mode 100644 src/renderer/application-update/force-update-modal/force-update-modal-root-frame-component.injectable.ts create mode 100644 src/renderer/application-update/force-update-modal/force-update-modal.module.scss create mode 100644 src/renderer/application-update/force-update-modal/force-update-modal.tsx create mode 100644 src/renderer/application-update/force-update-modal/install-update-countdown.injectable.ts create mode 100644 src/renderer/application-update/force-update-modal/seconds-after-install-starts.injectable.ts create mode 100644 src/renderer/application-update/force-update-modal/time-after-update-must-be-installed.injectable.ts create mode 100644 src/renderer/application-update/force-update-modal/time-since-update-was-downloaded.injectable.ts create mode 100644 src/renderer/components/countdown/countdown-state.injectable.ts create mode 100644 src/renderer/components/countdown/countdown.test.tsx create mode 100644 src/renderer/components/countdown/countdown.tsx diff --git a/src/behaviours/application-update/__snapshots__/force-user-to-update-when-too-long-time-since-update-was-downloaded.test.ts.snap b/src/behaviours/application-update/__snapshots__/force-user-to-update-when-too-long-time-since-update-was-downloaded.test.ts.snap new file mode 100644 index 0000000000..186c470d12 --- /dev/null +++ b/src/behaviours/application-update/__snapshots__/force-user-to-update-when-too-long-time-since-update-was-downloaded.test.ts.snap @@ -0,0 +1,708 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`force user to update when too long since update was downloaded when application is started given checking for updates and it resolves, when update was downloaded renders 1`] = ` + +
+
+
+
+ + + home + + + + + arrow_back + + + + + arrow_forward + + + +
+
+
+
+
+
+
+ +
+
+

+ Welcome to OpenLens 5! +

+

+ To get you started we have auto-detected your clusters in your + + kubeconfig file and added them to the catalog, your centralized + + view for managing all your cloud-native resources. +
+
+ If you have any questions or feedback, please join our + + Lens Community slack channel + + . +

+ +
+
+
+
+
+
+
+
+ + + arrow_left + + +
+
+ 0 +
+
+ + + arrow_right + + +
+
+
+
+
+
+
+
+
+ +`; + +exports[`force user to update when too long since update was downloaded when application is started given checking for updates and it resolves, when update was downloaded when enough time passes to consider that update must be installed renders 1`] = ` + +
+
+
+
+ + + home + + + + + arrow_back + + + + + arrow_forward + + + +
+
+
+
+
+
+
+ +
+
+

+ Welcome to OpenLens 5! +

+

+ To get you started we have auto-detected your clusters in your + + kubeconfig file and added them to the catalog, your centralized + + view for managing all your cloud-native resources. +
+
+ If you have any questions or feedback, please join our + + Lens Community slack channel + + . +

+ +
+
+
+
+
+
+
+
+ + + arrow_left + + +
+
+ 0 +
+
+ + + arrow_right + + +
+
+
+
+
+
+
+
+
+ + +`; + +exports[`force user to update when too long since update was downloaded when application is started given checking for updates and it resolves, when update was downloaded when not enough time passes to consider that update must be installed renders 1`] = ` + +
+
+
+
+ + + home + + + + + arrow_back + + + + + arrow_forward + + + +
+
+
+
+
+
+
+ +
+
+

+ Welcome to OpenLens 5! +

+

+ To get you started we have auto-detected your clusters in your + + kubeconfig file and added them to the catalog, your centralized + + view for managing all your cloud-native resources. +
+
+ If you have any questions or feedback, please join our + + Lens Community slack channel + + . +

+ +
+
+
+
+
+
+
+
+ + + arrow_left + + +
+
+ 0 +
+
+ + + arrow_right + + +
+
+
+
+
+
+
+
+
+ +`; diff --git a/src/behaviours/application-update/force-user-to-update-when-too-long-time-since-update-was-downloaded.test.ts b/src/behaviours/application-update/force-user-to-update-when-too-long-time-since-update-was-downloaded.test.ts new file mode 100644 index 0000000000..0a28239097 --- /dev/null +++ b/src/behaviours/application-update/force-user-to-update-when-too-long-time-since-update-was-downloaded.test.ts @@ -0,0 +1,148 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import type { CheckForPlatformUpdates } from "../../main/application-update/check-for-platform-updates/check-for-platform-updates.injectable"; +import checkForPlatformUpdatesInjectable from "../../main/application-update/check-for-platform-updates/check-for-platform-updates.injectable"; +import type { DownloadPlatformUpdate } from "../../main/application-update/download-platform-update/download-platform-update.injectable"; +import downloadPlatformUpdateInjectable from "../../main/application-update/download-platform-update/download-platform-update.injectable"; +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; +import type { DiContainer } from "@ogre-tools/injectable"; +import processCheckingForUpdatesInjectable from "../../main/application-update/check-for-updates/process-checking-for-updates.injectable"; +import type { RenderResult } from "@testing-library/react"; +import { fireEvent } from "@testing-library/react"; +import { advanceFakeTime, useFakeTime } from "../../common/test-utils/use-fake-time"; +import quitAndInstallUpdateInjectable from "../../main/application-update/quit-and-install-update.injectable"; +import timeAfterUpdateMustBeInstalledInjectable from "../../renderer/application-update/force-update-modal/time-after-update-must-be-installed.injectable"; +import secondsAfterInstallStartsInjectable from "../../renderer/application-update/force-update-modal/seconds-after-install-starts.injectable"; + +const TIME_AFTER_UPDATE_MUST_BE_INSTALLED = 1000; +const TIME_AFTER_INSTALL_STARTS = 5 * 1000; + +describe("force user to update when too long since update was downloaded", () => { + let applicationBuilder: ApplicationBuilder; + let checkForPlatformUpdatesMock: AsyncFnMock; + let downloadPlatformUpdateMock: AsyncFnMock; + let mainDi: DiContainer; + let quitAndInstallUpdateMock: jest.Mock; + + beforeEach(() => { + useFakeTime("2015-10-21T07:28:00Z"); + + applicationBuilder = getApplicationBuilder(); + + applicationBuilder.beforeApplicationStart(({ mainDi, rendererDi }) => { + checkForPlatformUpdatesMock = asyncFn(); + + mainDi.override(checkForPlatformUpdatesInjectable, () => checkForPlatformUpdatesMock); + + downloadPlatformUpdateMock = asyncFn(); + + mainDi.override(downloadPlatformUpdateInjectable, () => downloadPlatformUpdateMock); + + quitAndInstallUpdateMock = jest.fn(); + + mainDi.override(quitAndInstallUpdateInjectable, () => quitAndInstallUpdateMock); + + rendererDi.override(timeAfterUpdateMustBeInstalledInjectable, () => TIME_AFTER_UPDATE_MUST_BE_INSTALLED); + + rendererDi.override(secondsAfterInstallStartsInjectable, () => TIME_AFTER_INSTALL_STARTS / 1000); + }); + + mainDi = applicationBuilder.dis.mainDi; + }); + + describe("when application is started", () => { + let rendered: RenderResult; + + beforeEach(async () => { + rendered = await applicationBuilder.render(); + }); + + describe("given checking for updates and it resolves, when update was downloaded", () => { + beforeEach(async () => { + const processCheckingForUpdates = mainDi.inject( + processCheckingForUpdatesInjectable, + ); + + processCheckingForUpdates("irrelevant"); + + await checkForPlatformUpdatesMock.resolve({ + updateWasDiscovered: true, + version: "42.0.0", + }); + + await downloadPlatformUpdateMock.resolve({ + downloadWasSuccessful: true, + }); + }); + + it("does not show modal yet", () => { + expect(rendered.queryByTestId("must-update-immediately")).not.toBeInTheDocument(); + }); + + describe("when not enough time passes to consider that update must be installed", () => { + beforeEach(() => { + advanceFakeTime(TIME_AFTER_UPDATE_MUST_BE_INSTALLED - 1); + }); + + it("does not show modal yet", () => { + expect(rendered.queryByTestId("must-update-immediately")).not.toBeInTheDocument(); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + describe("when enough time passes to consider that update must be installed", () => { + beforeEach(() => { + advanceFakeTime(TIME_AFTER_UPDATE_MUST_BE_INSTALLED); + }); + + it("shows modal to inform about forced update", () => { + expect(rendered.getByTestId("must-update-immediately")).toBeInTheDocument(); + }); + + it("when selected to update now, restarts the application to update", () => { + fireEvent.click(rendered.getByTestId("update-now-from-must-update-immediately-modal")); + + expect(quitAndInstallUpdateMock).toHaveBeenCalled(); + }); + + it("shows countdown for automatic update", () => { + expect(rendered.getByTestId("countdown-to-automatic-update")).toHaveTextContent("5"); + }); + + it("when some time passes, updates the countdown for automatic update", () => { + advanceFakeTime(1000); + + expect(rendered.getByTestId("countdown-to-automatic-update")).toHaveTextContent("4"); + }); + + it("when not enough time passes for automatic update, does not restart the application yet", () => { + advanceFakeTime(TIME_AFTER_INSTALL_STARTS - 1); + + expect(quitAndInstallUpdateMock).not.toHaveBeenCalled(); + }); + + it("when enough time passes for automatically update, restarts the application to update", () => { + advanceFakeTime(TIME_AFTER_INSTALL_STARTS); + + expect(quitAndInstallUpdateMock).toHaveBeenCalled(); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + }); + }); + }); +}); diff --git a/src/renderer/application-update/force-update-modal/force-update-modal-root-frame-component.injectable.ts b/src/renderer/application-update/force-update-modal/force-update-modal-root-frame-component.injectable.ts new file mode 100644 index 0000000000..d8871b2b59 --- /dev/null +++ b/src/renderer/application-update/force-update-modal/force-update-modal-root-frame-component.injectable.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { rootFrameChildComponentInjectionToken } from "../../frames/root-frame/root-frame-child-component-injection-token"; +import { ForceUpdateModal } from "./force-update-modal"; +import timeSinceUpdateWasDownloadedInjectable from "./time-since-update-was-downloaded.injectable"; +import updateDownloadedDateTimeInjectable from "../../../common/application-update/update-downloaded-date-time/update-downloaded-date-time.injectable"; +import timeAfterUpdateMustBeInstalledInjectable from "./time-after-update-must-be-installed.injectable"; + +const forceUpdateModalRootFrameComponentInjectable = getInjectable({ + id: "force-update-modal-root-frame-component", + + instantiate: (di) => { + const timeSinceUpdateWasDownloaded = di.inject(timeSinceUpdateWasDownloadedInjectable); + const updateDownloadedDateTime = di.inject(updateDownloadedDateTimeInjectable); + const timeWhenUpdateMustBeInstalled = di.inject(timeAfterUpdateMustBeInstalledInjectable); + + return { + id: "force-update-modal", + Component: ForceUpdateModal, + + shouldRender: computed( + () => + !!updateDownloadedDateTime.value.get() && + timeSinceUpdateWasDownloaded.get() >= timeWhenUpdateMustBeInstalled, + ), + }; + }, + + injectionToken: rootFrameChildComponentInjectionToken, +}); + +export default forceUpdateModalRootFrameComponentInjectable; diff --git a/src/renderer/application-update/force-update-modal/force-update-modal.module.scss b/src/renderer/application-update/force-update-modal/force-update-modal.module.scss new file mode 100644 index 0000000000..268a4a2af7 --- /dev/null +++ b/src/renderer/application-update/force-update-modal/force-update-modal.module.scss @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +$baseline: 8px; + +.ForceUpdateModal { + background-color: white; + border-radius: 1 * $baseline; + padding: 3 * $baseline; + width: 50 * $baseline; + + .header { + margin-bottom: 2 * $baseline; + } + + .content { + margin-bottom: 2 * $baseline; + } + + .footer { + display: flex; + justify-content: center; + } +} diff --git a/src/renderer/application-update/force-update-modal/force-update-modal.tsx b/src/renderer/application-update/force-update-modal/force-update-modal.tsx new file mode 100644 index 0000000000..22c322be82 --- /dev/null +++ b/src/renderer/application-update/force-update-modal/force-update-modal.tsx @@ -0,0 +1,69 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { withInjectables } from "@ogre-tools/injectable-react"; +import React from "react"; +import restartAndInstallUpdateInjectable from "../../components/update-button/restart-and-install-update.injectable"; +import { Countdown } from "../../components/countdown/countdown"; +import type { IComputedValue } from "mobx"; +import { observer } from "mobx-react"; +import installUpdateCountdownInjectable from "./install-update-countdown.injectable"; +import { Dialog } from "../../components/dialog"; +import { Button } from "../../components/button"; +import styles from "./force-update-modal.module.scss"; + +interface Dependencies { + restartAndInstallUpdate: () => void; + secondsTill: IComputedValue; +} + +const NonInjectedForceUpdateModal = observer( + ({ restartAndInstallUpdate, secondsTill }: Dependencies) => ( + +
+
+

Please update

+
+ +
+

+ An update to Lens Desktop is required to continue using the application. +

+
+ +
+ +
+
+
+ ), +); + +export const ForceUpdateModal = withInjectables( + NonInjectedForceUpdateModal, + + { + getProps: (di, props) => ({ + restartAndInstallUpdate: di.inject(restartAndInstallUpdateInjectable), + secondsTill: di.inject(installUpdateCountdownInjectable), + ...props, + }), + }, +); diff --git a/src/renderer/application-update/force-update-modal/install-update-countdown.injectable.ts b/src/renderer/application-update/force-update-modal/install-update-countdown.injectable.ts new file mode 100644 index 0000000000..eb1ee53371 --- /dev/null +++ b/src/renderer/application-update/force-update-modal/install-update-countdown.injectable.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import countdownStateInjectable from "../../components/countdown/countdown-state.injectable"; +import secondsAfterInstallStartsInjectable from "./seconds-after-install-starts.injectable"; +import restartAndInstallUpdateInjectable from "../../components/update-button/restart-and-install-update.injectable"; + +const installUpdateCountdownInjectable = getInjectable({ + id: "install-update-countdown", + + instantiate: (di) => { + const secondsAfterInstallStarts = di.inject(secondsAfterInstallStartsInjectable); + const restartAndInstallUpdate = di.inject(restartAndInstallUpdateInjectable); + + return di.inject(countdownStateInjectable, { + startFrom: secondsAfterInstallStarts, + + onZero: () => { + restartAndInstallUpdate(); + }, + }); + }, +}); + +export default installUpdateCountdownInjectable; diff --git a/src/renderer/application-update/force-update-modal/seconds-after-install-starts.injectable.ts b/src/renderer/application-update/force-update-modal/seconds-after-install-starts.injectable.ts new file mode 100644 index 0000000000..aec4c29f14 --- /dev/null +++ b/src/renderer/application-update/force-update-modal/seconds-after-install-starts.injectable.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; + +const secondsAfterInstallStartsInjectable = getInjectable({ + id: "seconds-after-install-starts", + instantiate: () => 90, +}); + +export default secondsAfterInstallStartsInjectable; diff --git a/src/renderer/application-update/force-update-modal/time-after-update-must-be-installed.injectable.ts b/src/renderer/application-update/force-update-modal/time-after-update-must-be-installed.injectable.ts new file mode 100644 index 0000000000..c2bdb3cc83 --- /dev/null +++ b/src/renderer/application-update/force-update-modal/time-after-update-must-be-installed.injectable.ts @@ -0,0 +1,14 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; + +const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000; + +const timeAfterUpdateMustBeInstalledInjectable = getInjectable({ + id: "time-after-update-must-be-installed", + instantiate: () => THIRTY_DAYS, +}); + +export default timeAfterUpdateMustBeInstalledInjectable; diff --git a/src/renderer/application-update/force-update-modal/time-since-update-was-downloaded.injectable.ts b/src/renderer/application-update/force-update-modal/time-since-update-was-downloaded.injectable.ts new file mode 100644 index 0000000000..e89657a72c --- /dev/null +++ b/src/renderer/application-update/force-update-modal/time-since-update-was-downloaded.injectable.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import assert from "assert"; +import { computed } from "mobx"; +import moment from "moment"; +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 timeSinceUpdateWasDownloadedInjectable = getInjectable({ + id: "time-since-update-was-downloaded", + + instantiate: (di) => { + const updateDownloadedDateTime = di.inject(updateDownloadedDateTimeInjectable); + + return computed(() => { + const currentTimestamp = reactiveNow(); + + const downloadedAt = updateDownloadedDateTime.value.get(); + + assert(downloadedAt); + + return currentTimestamp - (moment(downloadedAt).unix() * 1000); + }); + }, +}); + +export default timeSinceUpdateWasDownloadedInjectable; diff --git a/src/renderer/components/countdown/countdown-state.injectable.ts b/src/renderer/components/countdown/countdown-state.injectable.ts new file mode 100644 index 0000000000..6cd290b4cf --- /dev/null +++ b/src/renderer/components/countdown/countdown-state.injectable.ts @@ -0,0 +1,53 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { + computed, + observable, + onBecomeObserved, + onBecomeUnobserved, + runInAction, +} from "mobx"; + +const countdownStateInjectable = getInjectable({ + id: "countdown-state", + + instantiate: ( + di, + { startFrom, onZero }: { startFrom: number; onZero: () => void }, + ) => { + const state = observable.box(startFrom); + + let intervalId: NodeJS.Timer | undefined; + + const stop = () => { + clearInterval(intervalId); + }; + + const start = () => { + intervalId = setInterval(() => { + const secondsLeft = state.get() - 1; + + runInAction(() => { + state.set(secondsLeft); + }); + + if (secondsLeft === 0) { + stop(); + onZero(); + } + }, 1000); + }; + + onBecomeObserved(state, start); + onBecomeUnobserved(state, stop); + + return computed(() => state.get()); + }, + + lifecycle: lifecycleEnum.transient, +}); + +export default countdownStateInjectable; diff --git a/src/renderer/components/countdown/countdown.test.tsx b/src/renderer/components/countdown/countdown.test.tsx new file mode 100644 index 0000000000..9bb08f486b --- /dev/null +++ b/src/renderer/components/countdown/countdown.test.tsx @@ -0,0 +1,142 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { DiContainer } from "@ogre-tools/injectable"; +import { createContainer } from "@ogre-tools/injectable"; +import countdownStateInjectable from "./countdown-state.injectable"; +import type { DiRender } from "../test-utils/renderFor"; +import { renderFor } from "../test-utils/renderFor"; +import { Countdown } from "./countdown"; +import React from "react"; +import type { RenderResult } from "@testing-library/react"; +import { advanceFakeTime, useFakeTime } from "../../../common/test-utils/use-fake-time"; +import type { IComputedValue } from "mobx"; +import { observe } from "mobx"; +import { noop } from "../../../common/utils"; + +describe("countdown", () => { + let di: DiContainer; + let render: DiRender; + + beforeEach(() => { + useFakeTime("2015-10-21T07:28:00Z"); + + di = createContainer("irrelevant"); + + render = renderFor(di); + + di.register(countdownStateInjectable); + }); + + describe("when rendering countdown", () => { + let rendered: RenderResult; + let onZeroMock: jest.Mock; + + beforeEach(() => { + onZeroMock = jest.fn(); + + const secondsTill = di.inject(countdownStateInjectable, { + startFrom: 42, + onZero: onZeroMock, + }); + + rendered = render( + , + ); + }); + + it("renders with initial seconds", () => { + expect(rendered.container).toHaveTextContent("42"); + }); + + describe("when time passes", () => { + beforeEach(() => { + advanceFakeTime(1000); + }); + + it("updates the seconds", () => { + expect(rendered.container).toHaveTextContent("41"); + }); + + it("does not call callback yet", () => { + expect(onZeroMock).not.toHaveBeenCalled(); + }); + }); + + it("when just not enough time passes to fulfill the countdown, does not call the callback yet", () => { + advanceFakeTime(41 * 1000); + + expect(onZeroMock).not.toHaveBeenCalled(); + }); + + describe("when time passes enough to fulfill the countdown", () => { + beforeEach(() => { + advanceFakeTime(42 * 1000); + }); + + it("shows zero as seconds", () => { + expect(rendered.container).toHaveTextContent("0"); + }); + + it("calls the callback", () => { + expect(onZeroMock).toHaveBeenCalled(); + }); + + describe("when time passes even more", () => { + beforeEach(() => { + onZeroMock.mockClear(); + + advanceFakeTime(1000); + }); + + it("does not update the countdown anymore", () => { + expect(rendered.container).toHaveTextContent("0"); + }); + + it("does not call the callback", () => { + expect(onZeroMock).not.toHaveBeenCalled(); + }); + }); + }); + }); + + describe("given observed", () => { + let onZeroMock: jest.Mock; + let unobserve: () => void; + let secondsTill: IComputedValue; + + beforeEach(() => { + onZeroMock = jest.fn(); + + secondsTill = di.inject(countdownStateInjectable, { + startFrom: 1, + onZero: onZeroMock, + }); + + unobserve = observe(secondsTill, noop); + }); + + describe("given unobserved, when enough time passes so that it would fulfill the countdown", () => { + beforeEach(() => { + onZeroMock.mockClear(); + + unobserve(); + + advanceFakeTime(1000); + }); + + it("does not call callback yet", () => { + expect(onZeroMock).not.toHaveBeenCalled(); + }); + + it("given observed again, when time passes to fulfill the countdown, calls the callback", () => { + observe(secondsTill, noop); + + advanceFakeTime(1000); + + expect(onZeroMock).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/src/renderer/components/countdown/countdown.tsx b/src/renderer/components/countdown/countdown.tsx new file mode 100644 index 0000000000..82b0d60e92 --- /dev/null +++ b/src/renderer/components/countdown/countdown.tsx @@ -0,0 +1,16 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { IComputedValue } from "mobx"; +import { observer } from "mobx-react"; +import type { HTMLAttributes } from "react"; +import React from "react"; + +interface CountdownProps extends HTMLAttributes { + secondsTill: IComputedValue; +} + +export const Countdown = observer(({ secondsTill, ...props }: CountdownProps) => ( + {secondsTill.get()} +));