diff --git a/src/utils/diagnostics/stopwatch.ts b/src/utils/diagnostics/stopwatch.ts index ad0e9a4..9138328 100644 --- a/src/utils/diagnostics/stopwatch.ts +++ b/src/utils/diagnostics/stopwatch.ts @@ -1,66 +1,140 @@ +/** + * A callback type for when a {@link Stopwatch} is started. + */ interface StartCallback { - (currentDate: Date, stopwatch: Stopwatch): void; + /** + * @param date - The date when the {@link Stopwatch} was started. + * @param stopwatch - The {@link Stopwatch} instance. + */ + (date: Date, stopwatch: Stopwatch): void; } +/** + * A callback type for when a {@link Stopwatch} is stopped. + */ interface StopCallback { - (elapsedMilliseconds: number, currentDate: Date, stopwatch: Stopwatch): void; + /** + * @param elapsedTime - The elapsed time in milliseconds. + * @param date - The date when the {@link Stopwatch} was stopped. + * @param stopwatch - The {@link Stopwatch} instance. + */ + (elapsedTime: number, date: Date, stopwatch: Stopwatch): void; } -export default class Stopwatch { - #initialDate = 0; - #isRunning = false; - #elapsedMilliseconds = 0; - #onStart: StartCallback = null; - #onStop: StopCallback = null; +/** + * A class for measuring elapsed time. + */ +export class Stopwatch { + /** + * Indicates whether the stopwatch is currently running. + */ + private _isRunning: boolean; - public constructor(onStart?: StartCallback, onStop?: StopCallback) { - this.#onStart = onStart; - this.#onStop = onStop; + /** + * The time when stopwatch was started. + */ + private _startTime: number; + + /** + * The elapsed time in milliseconds since the stopwatch was started. + */ + private _elapsedTime: number; + + /** + * A callback function that will be called when the stopwatch is started. + */ + private readonly _onStart?: StartCallback; + + /** + * A callback function that will be called when the stopwatch is stopped. + */ + private readonly _onStop?: StopCallback; + + /** + * Creates a new instance of {@link Stopwatch}. + * + * @param onStart - A callback function that will be called when the stopwatch is started. + * @param onStop - A callback function that will be called when the stopwatch is stopped. + */ + constructor(onStart?: StartCallback, onStop?: StopCallback) { + this._isRunning = false; + this._startTime = 0; + this._elapsedTime = 0; + this._onStart = onStart; + this._onStop = onStop; } - public get elapsedMilliseconds(): number { - return this.#isRunning - ? (this.#elapsedMilliseconds + new Date().valueOf() - this.#initialDate) - : this.#elapsedMilliseconds; + /** + * Gets the elapsed time in milliseconds since the stopwatch was started. + */ + get elapsedMilliseconds(): number { + return this._elapsedTime + (this._isRunning ? Date.now() - this._startTime : 0); } - public get isRunning(): boolean { - return this.#isRunning; + /** + * Gets a value indicating whether the stopwatch is currently running. + */ + get isRunning(): boolean { + return this._isRunning; } - public start(): boolean { - if (!this.#isRunning) { - const currentDate = new Date(); - this.#initialDate = currentDate.valueOf(); - this.#isRunning = true; - this.#onStart?.(currentDate, this); - return true; + /** + * Starts the stopwatch. + * + * @returns `true` if the stopwatch was successfully started; `false` if it was already running. + */ + start(): boolean { + if (this._isRunning) { + return false; } - return false; + + this._startTime = Date.now(); + this._isRunning = true; + this._onStart?.(new Date(), this); + return true; } - public stop(): boolean { - if (this.#isRunning) { - const currentDate = new Date(); - this.#elapsedMilliseconds += currentDate.valueOf() - this.#initialDate; - this.#isRunning = false; - this.#onStop?.(this.#elapsedMilliseconds, currentDate, this); - return true; + /** + * Stops the stopwatch. + * + * @returns `true` if the stopwatch was successfully stopped; `false` if it was already stopped. + */ + stop(): boolean { + if (!this._isRunning) { + return false; } - return false; + + this._elapsedTime += Date.now() - this._startTime; + this._isRunning = false; + this._onStop?.(this._elapsedTime, new Date(), this); + return true; } - public reset(): void { + /** + * Resets the stopwatch. + */ + reset(): void { this.stop(); - this.#elapsedMilliseconds = 0; + this._elapsedTime = 0; } - public restart(): void { + /** + * Restarts the stopwatch. + */ + restart(): void { this.reset(); this.start(); } - public static startNew(onStart?: StartCallback, onStop?: StopCallback): Stopwatch { + /** + * Creates a new instance of {@link Stopwatch} and starts it. + * + * @param onStart - A callback function that will be called when the stopwatch is started. + * @param onStop - A callback function that will be called when the stopwatch is stopped. + * + * @returns The newly created and started stopwatch. + */ + static startNew(onStart?: StartCallback, onStop?: StopCallback): Stopwatch { const stopwatch = new Stopwatch(onStart, onStop); stopwatch.start(); return stopwatch; diff --git a/tests/unit/utils/diagnostics/stopwatch.spec.ts b/tests/unit/utils/diagnostics/stopwatch.spec.ts new file mode 100644 index 0000000..73f9612 --- /dev/null +++ b/tests/unit/utils/diagnostics/stopwatch.spec.ts @@ -0,0 +1,179 @@ +import { Stopwatch } from "@/utils/diagnostics/stopwatch"; + +describe("Stopwatch", () => { + beforeEach(() => { + jest.useFakeTimers(); + jest.setSystemTime(0); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + describe("constructor", () => { + test("initializes with default values", () => { + const stopwatch = new Stopwatch(); + + expect(stopwatch.isRunning).toBe(false); + expect(stopwatch.elapsedMilliseconds).toBe(0); + }); + }); + + describe("start", () => { + test("starts stopwatch", () => { + const onStart = jest.fn(); + const onStop = jest.fn(); + const stopwatch = new Stopwatch(onStart, onStop); + + expect(stopwatch.start()).toBe(true); + expect(stopwatch.isRunning).toBe(true); + expect(onStart).toHaveBeenCalledTimes(1); + expect(onStart).toHaveBeenCalledWith(new Date(0), stopwatch); + expect(onStop).not.toHaveBeenCalled(); + }); + + test("doesn't start if stopwatch is already running", () => { + const onStart = jest.fn(); + const stopwatch = Stopwatch.startNew(onStart); + + expect(stopwatch.start()).toBe(false); + expect(onStart).toHaveBeenCalledTimes(1); + }); + }); + + describe("stop", () => { + test("stops stopwatch", () => { + const onStart = jest.fn(); + const onStop = jest.fn(); + const stopwatch = Stopwatch.startNew(onStart, onStop); + + jest.advanceTimersByTime(1000); + + expect(stopwatch.stop()).toBe(true); + expect(stopwatch.isRunning).toBe(false); + expect(onStart).toHaveBeenCalledTimes(1); + expect(onStart).toHaveBeenCalledWith(new Date(0), stopwatch); + expect(onStop).toHaveBeenCalledTimes(1); + expect(onStop).toHaveBeenCalledWith(1000, new Date(1000), stopwatch); + }); + + test("doesn't stop if stopwatch is already stopped", () => { + const onStop = jest.fn(); + const stopwatch = new Stopwatch(undefined, onStop); + + expect(stopwatch.stop()).toBe(false); + expect(onStop).not.toBeCalled(); + }); + }); + + describe("elapsedMilliseconds", () => { + test("measures elapsed time while stopwatch is running", () => { + const stopwatch = Stopwatch.startNew(); + + expect(stopwatch.elapsedMilliseconds).toBe(0); + + jest.advanceTimersByTime(1000); + expect(stopwatch.elapsedMilliseconds).toBe(1000); + + jest.advanceTimersByTime(1000); + expect(stopwatch.elapsedMilliseconds).toBe(2000); + }); + + test("measures elapsed time correctly when stopwatch is stopped", () => { + const stopwatch = Stopwatch.startNew(); + + expect(stopwatch.elapsedMilliseconds).toBe(0); + + jest.advanceTimersByTime(1000); + expect(stopwatch.elapsedMilliseconds).toBe(1000); + + jest.advanceTimersByTime(1000); + expect(stopwatch.elapsedMilliseconds).toBe(2000); + + stopwatch.stop(); + jest.advanceTimersByTime(1000); + expect(stopwatch.elapsedMilliseconds).toBe(2000); + }); + + test("returns 0 if the stopwatch was never started", () => { + const stopwatch = new Stopwatch(); + + expect(stopwatch.elapsedMilliseconds).toBe(0); + }); + }); + + describe("reset", () => { + test("resets stopwatch correctly while it's running", () => { + const stopwatch = Stopwatch.startNew(); + + jest.advanceTimersByTime(1000); + stopwatch.reset(); + + expect(stopwatch.elapsedMilliseconds).toBe(0); + expect(stopwatch.isRunning).toBe(false); + }); + + test("resets stopwatch correctly when it's stopped", () => { + const stopwatch = Stopwatch.startNew(); + + jest.advanceTimersByTime(1000); + stopwatch.stop(); + stopwatch.reset(); + + expect(stopwatch.elapsedMilliseconds).toBe(0); + expect(stopwatch.isRunning).toBe(false); + }); + + test("does nothing if the stopwatch was never started", () => { + const stopwatch = new Stopwatch(); + stopwatch.reset(); + + expect(stopwatch.elapsedMilliseconds).toBe(0); + expect(stopwatch.isRunning).toBe(false); + }); + }); + + describe("restart", () => { + test("restarts stopwatch correctly while it's running", () => { + const stopwatch = Stopwatch.startNew(); + + jest.advanceTimersByTime(1000); + stopwatch.restart(); + + expect(stopwatch.elapsedMilliseconds).toBe(0); + expect(stopwatch.isRunning).toBe(true); + }); + + test("restarts stopwatch correctly when it's stopped", () => { + const stopwatch = Stopwatch.startNew(); + + jest.advanceTimersByTime(1000); + stopwatch.stop(); + stopwatch.restart(); + + expect(stopwatch.elapsedMilliseconds).toBe(0); + expect(stopwatch.isRunning).toBe(true); + }); + + test("starts the stopwatch if it was never started", () => { + const stopwatch = new Stopwatch(); + stopwatch.restart(); + + expect(stopwatch.elapsedMilliseconds).toBe(0); + expect(stopwatch.isRunning).toBe(true); + }); + }); + + describe("startNew", () => { + test("starts new stopwatch correctly", () => { + const onStart = jest.fn(); + const onStop = jest.fn(); + const stopwatch = Stopwatch.startNew(onStart, onStop); + + expect(stopwatch.isRunning).toBe(true); + expect(onStart).toHaveBeenCalledTimes(1); + expect(onStart).toHaveBeenCalledWith(new Date(0), stopwatch); + expect(onStop).not.toHaveBeenCalled(); + }); + }); +});