1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Force update after thirty days since update was downloaded (#5776)

This commit is contained in:
Janne Savolainen 2022-07-06 16:48:00 +03:00 committed by GitHub
parent eb6cc70143
commit 6e5c8e0427
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1280 additions and 0 deletions

View File

@ -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`] = `
<body>
<div>
<div
class="ClusterManager"
>
<div
class="topBar"
>
<div
class="items"
>
<i
class="Icon material interactive focusable"
data-testid="home-button"
tabindex="0"
>
<span
class="icon"
data-icon-name="home"
>
home
</span>
</i>
<i
class="Icon material interactive disabled focusable"
data-testid="history-back"
>
<span
class="icon"
data-icon-name="arrow_back"
>
arrow_back
</span>
</i>
<i
class="Icon material interactive disabled focusable"
data-testid="history-forward"
>
<span
class="icon"
data-icon-name="arrow_forward"
>
arrow_forward
</span>
</i>
<button
class="updateButton"
data-testid="update-button"
data-warning-level="light"
id="update-lens-button"
>
Update
<i
class="Icon icon material focusable"
>
<span
class="icon"
data-icon-name="arrow_drop_down"
>
arrow_drop_down
</span>
</i>
</button>
</div>
<div
class="items"
/>
</div>
<main>
<div
id="lens-views"
/>
<div
class="flex justify-center Welcome align-center"
data-testid="welcome-page"
>
<div
data-testid="welcome-banner-container"
style="width: 320px;"
>
<i
class="Icon logo svg focusable"
>
<span
class="icon"
/>
</i>
<div
class="flex justify-center"
>
<div
data-testid="welcome-text-container"
style="width: 320px;"
>
<h2>
Welcome to OpenLens 5!
</h2>
<p>
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.
<br />
<br />
If you have any questions or feedback, please join our
<a
class="link"
href="https://join.slack.com/t/k8slens/shared_invite/zt-wcl8jq3k-68R5Wcmk1o95MLBE5igUDQ"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
</a>
.
</p>
<ul
class="block"
data-testid="welcome-menu-container"
style="width: 320px;"
>
<li
class="flex grid-12"
>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="view_list"
>
view_list
</span>
</i>
<a
class="box col-10"
>
Browse Clusters in Catalog
</a>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="navigate_next"
>
navigate_next
</span>
</i>
</li>
</ul>
</div>
</div>
</div>
</div>
</main>
<div
class="HotbarMenu flex column"
>
<div
class="HotbarItems flex column gaps"
/>
<div
class="HotbarSelector"
>
<i
class="Icon Icon previous material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_left"
>
arrow_left
</span>
</i>
<div
class="HotbarIndex"
>
<div
class="badge Badge small clickable"
id="hotbarIndex"
>
0
</div>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_right"
>
arrow_right
</span>
</i>
</div>
</div>
<div
class="StatusBar"
>
<div
class="leftSide"
data-testid="status-bar-left"
/>
<div
class="rightSide"
data-testid="status-bar-right"
/>
</div>
</div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;
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`] = `
<body>
<div>
<div
class="ClusterManager"
>
<div
class="topBar"
>
<div
class="items"
>
<i
class="Icon material interactive focusable"
data-testid="home-button"
tabindex="0"
>
<span
class="icon"
data-icon-name="home"
>
home
</span>
</i>
<i
class="Icon material interactive disabled focusable"
data-testid="history-back"
>
<span
class="icon"
data-icon-name="arrow_back"
>
arrow_back
</span>
</i>
<i
class="Icon material interactive disabled focusable"
data-testid="history-forward"
>
<span
class="icon"
data-icon-name="arrow_forward"
>
arrow_forward
</span>
</i>
<button
class="updateButton"
data-testid="update-button"
data-warning-level="light"
id="update-lens-button"
>
Update
<i
class="Icon icon material focusable"
>
<span
class="icon"
data-icon-name="arrow_drop_down"
>
arrow_drop_down
</span>
</i>
</button>
</div>
<div
class="items"
/>
</div>
<main>
<div
id="lens-views"
/>
<div
class="flex justify-center Welcome align-center"
data-testid="welcome-page"
>
<div
data-testid="welcome-banner-container"
style="width: 320px;"
>
<i
class="Icon logo svg focusable"
>
<span
class="icon"
/>
</i>
<div
class="flex justify-center"
>
<div
data-testid="welcome-text-container"
style="width: 320px;"
>
<h2>
Welcome to OpenLens 5!
</h2>
<p>
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.
<br />
<br />
If you have any questions or feedback, please join our
<a
class="link"
href="https://join.slack.com/t/k8slens/shared_invite/zt-wcl8jq3k-68R5Wcmk1o95MLBE5igUDQ"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
</a>
.
</p>
<ul
class="block"
data-testid="welcome-menu-container"
style="width: 320px;"
>
<li
class="flex grid-12"
>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="view_list"
>
view_list
</span>
</i>
<a
class="box col-10"
>
Browse Clusters in Catalog
</a>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="navigate_next"
>
navigate_next
</span>
</i>
</li>
</ul>
</div>
</div>
</div>
</div>
</main>
<div
class="HotbarMenu flex column"
>
<div
class="HotbarItems flex column gaps"
/>
<div
class="HotbarSelector"
>
<i
class="Icon Icon previous material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_left"
>
arrow_left
</span>
</i>
<div
class="HotbarIndex"
>
<div
class="badge Badge small clickable"
id="hotbarIndex"
>
0
</div>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_right"
>
arrow_right
</span>
</i>
</div>
</div>
<div
class="StatusBar"
>
<div
class="leftSide"
data-testid="status-bar-left"
/>
<div
class="rightSide"
data-testid="status-bar-right"
/>
</div>
</div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
<div
class="Animate opacity-scale Dialog flex center modal pinned enter"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
class="box"
>
<div
class="ForceUpdateModal"
data-testid="must-update-immediately"
>
<div
class="header"
>
<h2>
Please update
</h2>
</div>
<div
class="content"
>
<p>
An update to Lens Desktop is required to continue using the application.
</p>
</div>
<div
class="footer"
>
<button
class="Button primary"
data-testid="update-now-from-must-update-immediately-modal"
type="button"
>
Update
(
<span
data-testid="countdown-to-automatic-update"
>
5
</span>
)
</button>
</div>
</div>
</div>
</div>
</body>
`;
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`] = `
<body>
<div>
<div
class="ClusterManager"
>
<div
class="topBar"
>
<div
class="items"
>
<i
class="Icon material interactive focusable"
data-testid="home-button"
tabindex="0"
>
<span
class="icon"
data-icon-name="home"
>
home
</span>
</i>
<i
class="Icon material interactive disabled focusable"
data-testid="history-back"
>
<span
class="icon"
data-icon-name="arrow_back"
>
arrow_back
</span>
</i>
<i
class="Icon material interactive disabled focusable"
data-testid="history-forward"
>
<span
class="icon"
data-icon-name="arrow_forward"
>
arrow_forward
</span>
</i>
<button
class="updateButton"
data-testid="update-button"
data-warning-level="light"
id="update-lens-button"
>
Update
<i
class="Icon icon material focusable"
>
<span
class="icon"
data-icon-name="arrow_drop_down"
>
arrow_drop_down
</span>
</i>
</button>
</div>
<div
class="items"
/>
</div>
<main>
<div
id="lens-views"
/>
<div
class="flex justify-center Welcome align-center"
data-testid="welcome-page"
>
<div
data-testid="welcome-banner-container"
style="width: 320px;"
>
<i
class="Icon logo svg focusable"
>
<span
class="icon"
/>
</i>
<div
class="flex justify-center"
>
<div
data-testid="welcome-text-container"
style="width: 320px;"
>
<h2>
Welcome to OpenLens 5!
</h2>
<p>
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.
<br />
<br />
If you have any questions or feedback, please join our
<a
class="link"
href="https://join.slack.com/t/k8slens/shared_invite/zt-wcl8jq3k-68R5Wcmk1o95MLBE5igUDQ"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
</a>
.
</p>
<ul
class="block"
data-testid="welcome-menu-container"
style="width: 320px;"
>
<li
class="flex grid-12"
>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="view_list"
>
view_list
</span>
</i>
<a
class="box col-10"
>
Browse Clusters in Catalog
</a>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="navigate_next"
>
navigate_next
</span>
</i>
</li>
</ul>
</div>
</div>
</div>
</div>
</main>
<div
class="HotbarMenu flex column"
>
<div
class="HotbarItems flex column gaps"
/>
<div
class="HotbarSelector"
>
<i
class="Icon Icon previous material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_left"
>
arrow_left
</span>
</i>
<div
class="HotbarIndex"
>
<div
class="badge Badge small clickable"
id="hotbarIndex"
>
0
</div>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_right"
>
arrow_right
</span>
</i>
</div>
</div>
<div
class="StatusBar"
>
<div
class="leftSide"
data-testid="status-bar-left"
/>
<div
class="rightSide"
data-testid="status-bar-right"
/>
</div>
</div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;

View File

@ -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<CheckForPlatformUpdates>;
let downloadPlatformUpdateMock: AsyncFnMock<DownloadPlatformUpdate>;
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();
});
});
});
});
});

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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<number>;
}
const NonInjectedForceUpdateModal = observer(
({ restartAndInstallUpdate, secondsTill }: Dependencies) => (
<Dialog isOpen={true} pinned>
<div
data-testid="must-update-immediately"
className={styles.ForceUpdateModal}
>
<div className={styles.header}>
<h2>Please update</h2>
</div>
<div className={styles.content}>
<p>
An update to Lens Desktop is required to continue using the application.
</p>
</div>
<div className={styles.footer}>
<Button
primary
data-testid="update-now-from-must-update-immediately-modal"
onClick={restartAndInstallUpdate}
label="Update"
>
{" "}
(
<Countdown
secondsTill={secondsTill}
data-testid="countdown-to-automatic-update"
/>
)
</Button>
</div>
</div>
</Dialog>
),
);
export const ForceUpdateModal = withInjectables<Dependencies>(
NonInjectedForceUpdateModal,
{
getProps: (di, props) => ({
restartAndInstallUpdate: di.inject(restartAndInstallUpdateInjectable),
secondsTill: di.inject(installUpdateCountdownInjectable),
...props,
}),
},
);

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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(
<Countdown secondsTill={secondsTill} />,
);
});
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<number>;
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();
});
});
});
});

View File

@ -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<HTMLSpanElement> {
secondsTill: IComputedValue<number>;
}
export const Countdown = observer(({ secondsTill, ...props }: CountdownProps) => (
<span {...props}>{secondsTill.get()}</span>
));