Stopwatch refactoring

This commit is contained in:
Kir_Antipov 2022-12-17 13:18:35 +00:00
parent a33f82afe1
commit d1b07fe763
2 changed files with 290 additions and 37 deletions

View file

@ -1,66 +1,140 @@
/**
* A callback type for when a {@link Stopwatch} is started.
*/
interface StartCallback { 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 { 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; * A class for measuring elapsed time.
#isRunning = false; */
#elapsedMilliseconds = 0; export class Stopwatch {
#onStart: StartCallback = null; /**
#onStop: StopCallback = null; * Indicates whether the stopwatch is currently running.
*/
private _isRunning: boolean;
public constructor(onStart?: StartCallback, onStop?: StopCallback) { /**
this.#onStart = onStart; * The time when stopwatch was started.
this.#onStop = onStop; */
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 * Gets the elapsed time in milliseconds since the stopwatch was started.
? (this.#elapsedMilliseconds + new Date().valueOf() - this.#initialDate) */
: this.#elapsedMilliseconds; 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) { * Starts the stopwatch.
const currentDate = new Date(); *
this.#initialDate = currentDate.valueOf(); * @returns `true` if the stopwatch was successfully started; `false` if it was already running.
this.#isRunning = true; */
this.#onStart?.(currentDate, this); start(): boolean {
return true; if (this._isRunning) {
}
return false; return false;
} }
public stop(): boolean { this._startTime = Date.now();
if (this.#isRunning) { this._isRunning = true;
const currentDate = new Date(); this._onStart?.(new Date(), this);
this.#elapsedMilliseconds += currentDate.valueOf() - this.#initialDate;
this.#isRunning = false;
this.#onStop?.(this.#elapsedMilliseconds, currentDate, this);
return true; 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;
} }
public reset(): void { this._elapsedTime += Date.now() - this._startTime;
this._isRunning = false;
this._onStop?.(this._elapsedTime, new Date(), this);
return true;
}
/**
* Resets the stopwatch.
*/
reset(): void {
this.stop(); this.stop();
this.#elapsedMilliseconds = 0; this._elapsedTime = 0;
} }
public restart(): void { /**
* Restarts the stopwatch.
*/
restart(): void {
this.reset(); this.reset();
this.start(); 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); const stopwatch = new Stopwatch(onStart, onStop);
stopwatch.start(); stopwatch.start();
return stopwatch; return stopwatch;

View file

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