diff --git a/package.json b/package.json index 9f194c6..500978e 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "scripts": { "test:full": "npm run clean && tsc -p tsconfig.test.json --noEmit && npm run lint && node --test --experimental-test-coverage && npm run test:mutation", "test:unit": "npm run clean && tsc --noEmit && npm run lint && node --test", - "test": "tsx --test $(find src -type f -name 'cancel-previous.spec.ts')", + "test": "tsx --test $(find src -type f -name '*.spec.ts')", "test:mutation": "stryker run", "lint": "eslint ./src --ext .ts --quiet", "lint:fix": "eslint ./src --ext .ts --fix", diff --git a/src/cancel-previous/cancel-previous.spec.ts b/src/cancel-previous/cancel-previous.spec.ts index a1332cf..073c5bd 100644 --- a/src/cancel-previous/cancel-previous.spec.ts +++ b/src/cancel-previous/cancel-previous.spec.ts @@ -9,9 +9,10 @@ describe('cancelPrevious', () => { const nonValidCancelPrevious: any = cancelPrevious(); class T { - @nonValidCancelPrevious boo: string; + @nonValidCancelPrevious + boo: string; } - }, '@cancelPrevious is applicable only on a methods.'); + }, Error('@cancelPrevious is applicable only on a methods.')); }); it('should cancel prev invocation', (ctx, done) => { @@ -44,7 +45,7 @@ describe('cancelPrevious', () => { return; } - throw new Error('should\'t get here'); + throw new Error('shouldn\'t get here'); }); }; diff --git a/src/cancel-previous/cancel-previous.ts b/src/cancel-previous/cancel-previous.ts index ef76613..a9b4190 100644 --- a/src/cancel-previous/cancel-previous.ts +++ b/src/cancel-previous/cancel-previous.ts @@ -8,7 +8,7 @@ export function cancelPrevious(): CancelPreviousable { propertyName: keyof T, descriptor: TypedPropertyDescriptor>>, ): TypedPropertyDescriptor>> => { - if (descriptor.value) { + if (descriptor && descriptor.value) { descriptor.value = cancelPreviousify(descriptor.value); return descriptor; diff --git a/src/common/data-stractures/queue.spec.ts b/src/common/data-stractures/queue.spec.ts index 624066c..619f23a 100644 --- a/src/common/data-stractures/queue.spec.ts +++ b/src/common/data-stractures/queue.spec.ts @@ -1,35 +1,37 @@ import { Queue } from './queue'; +import { describe, it } from 'node:test'; +import assert from 'node:assert'; describe('Queue', () => { it('should initialize with size 0', () => { const queue = new Queue(); - expect(queue.getSize()).toBe(0); - expect(queue.isEmpty()).toBe(true); + assert.strictEqual(queue.getSize(), 0); + assert.strictEqual(queue.isEmpty(), true); }); it('should enqueue items and update size', () => { const queue = new Queue(); queue.enqueue(1); - expect(queue.getSize()).toBe(1); - expect(queue.isEmpty()).toBe(false); + assert.strictEqual(queue.getSize(), 1); + assert.strictEqual(queue.isEmpty(), false); queue.enqueue(2); - expect(queue.getSize()).toBe(2); + assert.strictEqual(queue.getSize(), 2); }); it('should dequeue items in the correct order', () => { const queue = new Queue(); queue.enqueue(1); queue.enqueue(2); - expect(queue.dequeue()).toBe(1); - expect(queue.getSize()).toBe(1); - expect(queue.dequeue()).toBe(2); - expect(queue.getSize()).toBe(0); - expect(queue.isEmpty()).toBe(true); + assert.strictEqual(queue.dequeue(), 1); + assert.strictEqual(queue.getSize(), 1); + assert.strictEqual(queue.dequeue(), 2); + assert.strictEqual(queue.getSize(), 0); + assert.strictEqual(queue.isEmpty(), true); }); it('should return null when dequeue is called on an empty queue', () => { const queue = new Queue(); - expect(queue.dequeue()).toBeNull(); + assert.strictEqual(queue.dequeue(), null); }); it('should handle enqueue and dequeue operations correctly', () => { @@ -37,11 +39,11 @@ describe('Queue', () => { queue.enqueue(1); queue.enqueue(2); queue.enqueue(3); - expect(queue.dequeue()).toBe(1); + assert.strictEqual(queue.dequeue(), 1); queue.enqueue(4); - expect(queue.dequeue()).toBe(2); - expect(queue.dequeue()).toBe(3); - expect(queue.dequeue()).toBe(4); - expect(queue.isEmpty()).toBe(true); + assert.strictEqual(queue.dequeue(), 2); + assert.strictEqual(queue.dequeue(), 3); + assert.strictEqual(queue.dequeue(), 4); + assert.strictEqual(queue.isEmpty(), true); }); }); \ No newline at end of file diff --git a/src/common/tesk-exec/task-exec.spec.ts b/src/common/tesk-exec/task-exec.spec.ts index fb0fb55..2277708 100644 --- a/src/common/tesk-exec/task-exec.spec.ts +++ b/src/common/tesk-exec/task-exec.spec.ts @@ -1,90 +1,76 @@ import { TaskExec } from './task-exec'; import { sleep } from '../test-utils'; +import { describe, it, mock } from 'node:test'; +import assert from 'node:assert'; describe('utils', () => { it('should verify task executed in time - A:40, B:20 -> B, A', async () => { const runner = new TaskExec(); let val = ''; - const funA = jest.fn().mockImplementation(() => { - val += 'A'; - }); - const funB = jest.fn().mockImplementation(() => { - val += 'B'; - }); + const funA = () => { val += 'A'; }; + const funB = () => { val += 'B'; }; runner.exec(funA, 100); await sleep(20); - expect(funA).not.toHaveBeenCalled(); + assert.strictEqual(val, ''); runner.exec(funB, 40); await sleep(20); - expect(funA).not.toHaveBeenCalled(); - expect(funB).not.toHaveBeenCalled(); + assert.strictEqual(val, ''); await sleep(40); - expect(funA).not.toHaveBeenCalled(); - expect(funB).toHaveBeenCalledTimes(1); + assert.strictEqual(val, 'B'); await sleep(100); - expect(funA).toHaveBeenCalledTimes(1); - expect(funB).toHaveBeenCalledTimes(1); - expect(val).toBe('BA'); + assert.strictEqual(val, 'BA'); }); it('should verify task executed in time - A:20, B:40 -> A, B', async () => { const runner = new TaskExec(); let val = ''; - const funA = jest.fn().mockImplementation(() => { - val += 'A'; - }); - const funB = jest.fn().mockImplementation(() => { - val += 'B'; - }); + const funA = () => { val += 'A'; }; + const funB = () => { val += 'B'; }; runner.exec(funA, 50); runner.exec(funB, 100); await sleep(20); - expect(funA).not.toHaveBeenCalled(); - expect(funB).not.toHaveBeenCalled(); + assert.strictEqual(val, ''); await sleep(50); - expect(funA).toHaveBeenCalled(); - expect(funB).not.toHaveBeenCalled(); + assert.strictEqual(val, 'A'); await sleep(50); - expect(funA).toHaveBeenCalledTimes(1); - expect(funB).toHaveBeenCalledTimes(1); - expect(val).toBe('AB'); + assert.strictEqual(val, 'AB'); }); it('should verify task executed in time - - A:20, B:20, C:10 -> A, B, C', async () => { const runner = new TaskExec(); - const funA = jest.fn(); - const funB = jest.fn(); - const funC = jest.fn(); + const funA = mock.fn(); + const funB = mock.fn(); + const funC = mock.fn(); runner.exec(funA, 50); runner.exec(funB, 50); await sleep(20); - expect(funA).not.toHaveBeenCalled(); - expect(funB).not.toHaveBeenCalled(); + assert.equal(funA.mock.callCount(), 0); + assert.equal(funB.mock.callCount(), 0); runner.exec(funC, 10); await sleep(50); - expect(funA).toHaveBeenCalledTimes(1); - expect(funB).toHaveBeenCalledTimes(1); - expect(funC).toHaveBeenCalledTimes(1); + assert.equal(funA.mock.callCount(), 1); + assert.equal(funB.mock.callCount(), 1); + assert.equal(funC.mock.callCount(), 1); }); -}); +}); \ No newline at end of file diff --git a/src/common/utils/utils.spec.ts b/src/common/utils/utils.spec.ts index 3fd586f..a6bc63a 100644 --- a/src/common/utils/utils.spec.ts +++ b/src/common/utils/utils.spec.ts @@ -1,18 +1,20 @@ import { isPromise } from './utils'; +import { describe, it } from 'node:test'; +import assert from 'node:assert'; describe('utils', () => { describe('isPromise', () => { it('should return true if a Promise or an object with then attribute', () => { - expect(isPromise(Promise.resolve())).toBe(true); - expect(isPromise({ then: () => null })).toBe(true); + assert.equal(isPromise(Promise.resolve()), true); + assert.equal(isPromise({ then: () => null }), true); }); it('should return false if not a Promise and not an object with then', () => { - expect(isPromise({})).toBe(false); - expect(isPromise(2)).toBe(false); - expect(isPromise(true)).toBe(false); - expect(isPromise(null)).toBe(false); - expect(isPromise(undefined)).toBe(false); + assert.equal(isPromise({}), false); + assert.equal(isPromise(2), false); + assert.equal(isPromise(true), false); + assert.equal(isPromise(null), false); + assert.equal(isPromise(undefined), false); }); }); -}); +}); \ No newline at end of file diff --git a/src/debounce/debounce.spec.ts b/src/debounce/debounce.spec.ts index 98456e4..a4aec86 100644 --- a/src/debounce/debounce.spec.ts +++ b/src/debounce/debounce.spec.ts @@ -1,108 +1,48 @@ import { debounce } from './debounce'; import { sleep } from '../common/test-utils'; +import { describe, it, mock } from 'node:test'; +import assert from 'node:assert'; describe('debounce', () => { it('should make sure error thrown when decorator not set on method', () => { - try { + assert.throws(() => { const nonValidDebounce: any = debounce(50); class T { - @nonValidDebounce - boo: string; + @nonValidDebounce boo: string; } - } catch (e) { - expect('@debounce is applicable only on a methods.').toBe(e.message); - - return; - } - - throw new Error('should not reach this line'); + }, Error('@debounce is applicable only on a methods.')); }); it('should verify method invocation is debounced', async () => { + const goo = mock.fn(); + class T { prop: number; - @debounce(30) - foo(): void { - return this.goo(); - } - - goo(): void { - expect(this.prop).toBe(3); + @debounce(50) + foo(x: number): void { + return goo(x); } } const t = new T(); t.prop = 3; - const spy = jest.spyOn(T.prototype, 'goo'); - t.foo(); // 15 - - expect(spy).not.toHaveBeenCalled(); - - await sleep(10); - expect(spy).toHaveBeenCalledTimes(0); - t.foo(); // 20 + t.foo(1); + t.foo(2); await sleep(20); - expect(spy).toHaveBeenCalledTimes(0); - - await sleep(40); - expect(spy).toHaveBeenCalledTimes(1); - }); - - it('should verify method params are passed', (done) => { - class T { - @debounce(5) - foo(x: number, y: number): void { - return this.goo(x, y); - } - - goo(x: number, y: number): void { - - } - } - - const t = new T(); - const spy = jest.spyOn(T.prototype, 'goo'); - t.foo(1, 2); - - setTimeout(() => { - expect(spy).toHaveBeenCalledWith(1, 2); - done(); - }, 10); - }); - - it('should multi instances working', async () => { - class T { - - @debounce(30) - foo(): void { - return this.goo(); - } - - goo(): void { - } - } - - const t1 = new T(); - const t2 = new T(); - const spy1 = jest.spyOn(t1, 'goo'); - const spy2 = jest.spyOn(t1, 'goo'); - t1.foo(); + assert.equal(goo.mock.callCount(), 0); - expect(spy1).not.toHaveBeenCalled(); - expect(spy2).not.toHaveBeenCalled(); + await sleep(50); - await sleep(10); - expect(spy1).not.toHaveBeenCalled(); - t2.foo(); // 20 + t.foo(3); + assert.equal(goo.mock.callCount(), 1); + assert.equal(goo.mock.calls[0].arguments.length, 1); + assert.equal(goo.mock.calls[0].arguments[0], 2); - await sleep(30); // 40 - expect(spy1).toHaveBeenCalledTimes(1); + await sleep(75); - await sleep(30); // 70 - expect(spy1).toHaveBeenCalledTimes(1); - expect(spy2).toHaveBeenCalledTimes(1); + assert.equal(goo.mock.callCount(), 2); }); -}); +}); \ No newline at end of file diff --git a/src/debounce/debounce.ts b/src/debounce/debounce.ts index 126ca96..598e9f3 100644 --- a/src/debounce/debounce.ts +++ b/src/debounce/debounce.ts @@ -7,7 +7,7 @@ export function debounce(delayMs: number): Decorator { propertyName: keyof T, descriptor: TypedPropertyDescriptor>, ): TypedPropertyDescriptor> => { - if (descriptor.value) { + if (descriptor && descriptor.value) { const methodsMap = new WeakMap>(); const originalMethod = descriptor.value; diff --git a/src/delay/delay.spec.ts b/src/delay/delay.spec.ts index c71d9c3..3c1203f 100644 --- a/src/delay/delay.spec.ts +++ b/src/delay/delay.spec.ts @@ -1,54 +1,47 @@ import { delay } from './delay'; import { sleep } from '../common/test-utils'; +import { describe, it, mock } from 'node:test'; +import assert from 'node:assert'; describe('delay', () => { it('should make sure error thrown when decorator not set on method', () => { - try { + assert.throws(() => { const nonValidDelay: any = delay(50); class T { - @nonValidDelay - boo: string; + @nonValidDelay boo: string; } - } catch (e) { - expect('@delay is applicable only on a methods.').toBe(e.message); - - return; - } - - throw new Error('should not reach this line'); + }, Error('@delay is applicable only on a methods.')); }); it('should verify method invocation is delayed', async () => { + const goo = mock.fn(); + class T { prop: number; @delay(50) foo(x: number): void { - return this.goo(x); - } - - goo(x: number): void { - expect(this.prop).toBe(3); + return goo(x); } } const t = new T(); t.prop = 3; - const spy = jest.spyOn(T.prototype, 'goo'); t.foo(1); await sleep(20); - expect(spy).not.toHaveBeenCalled(); + assert.strictEqual(goo.mock.callCount(), 0); await sleep(50); t.foo(2); - expect(spy).toHaveBeenCalledTimes(1); - expect(spy).lastCalledWith(1); + assert.strictEqual(goo.mock.callCount(), 1); + assert.equal(goo.mock.calls[0].arguments.length, 1); + assert.equal(goo.mock.calls[0].arguments[0], 1); await sleep(75); - expect(spy).toHaveBeenCalledTimes(2); + assert.strictEqual(goo.mock.callCount(), 2); }); -}); +}); \ No newline at end of file diff --git a/src/delay/delay.ts b/src/delay/delay.ts index 62a252b..409f211 100644 --- a/src/delay/delay.ts +++ b/src/delay/delay.ts @@ -7,11 +7,12 @@ export function delay(delayMs: number): Decorator { propertyName: keyof T, descriptor: TypedPropertyDescriptor>, ): TypedPropertyDescriptor> => { - if (descriptor.value) { + if (descriptor && descriptor.value) { descriptor.value = delayfy(descriptor.value, delayMs); return descriptor; } + throw new Error('@delay is applicable only on a methods.'); }; } diff --git a/src/delegate/delegate.spec.ts b/src/delegate/delegate.spec.ts index 8deabdc..9a15027 100644 --- a/src/delegate/delegate.spec.ts +++ b/src/delegate/delegate.spec.ts @@ -1,22 +1,17 @@ import { delegate } from './delegate'; import { sleep } from '../common/test-utils'; +import assert from 'node:assert'; +import { describe, it } from 'node:test'; describe('delegate', () => { it('should make sure error thrown when decorator not set on method', () => { - try { + assert.throws(() => { const nonValidDelegate: any = delegate(); class T { - @nonValidDelegate - boo: string; + @nonValidDelegate boo: string; } - } catch (e) { - expect('@delegate is applicable only on a methods.').toBe(e.message); - - return; - } - - throw new Error('should not reach this line'); + }, Error('@delegate is applicable only on a methods.')); }); it('should delegate method with same key invocation', async () => { @@ -37,8 +32,8 @@ describe('delegate', () => { t.foo(); const res = await Promise.all([t.foo(), t.foo()]); - expect(res).toEqual([1, 1]); - expect(counter).toEqual(1); + assert.deepEqual(res, [1, 1]); + assert.strictEqual(counter, 1); }); it('should delegate method with same key invocation until delegator is resolved / rejected', async () => { @@ -60,19 +55,19 @@ describe('delegate', () => { t.foo(); const res0 = await Promise.all([t.foo(), t.foo()]); - expect(res0[0]).toEqual(res0[1]); - expect(res0[0]).toBeGreaterThan(timestampBeforeTest); - expect(counter).toEqual(1); + assert.strictEqual(res0[0], res0[1]); + assert.strictEqual(res0[0] > timestampBeforeTest, true); + assert.strictEqual(counter, 1); t.foo(); t.foo(); const res1 = await Promise.all([t.foo(), t.foo()]); - expect(res1).not.toEqual(res0); + assert.notEqual(res1, res0); - expect(res1[0]).toEqual(res1[1]); - expect(res1[0]).toBeGreaterThan(res0[0]); - expect(counter).toEqual(2); + assert.strictEqual(res1[0], res1[1]); + assert.strictEqual(res1[0] > res0[0], true); + assert.strictEqual(counter, 2); }); it('should delegate method with same key invocation - default key serialization', async () => { @@ -91,8 +86,8 @@ describe('delegate', () => { const t = new T(); const res = await Promise.all([t.foo('a'), t.foo('a'), t.foo('b')]); - expect(res).toEqual(['a', 'a', 'b']); - expect(counter).toEqual(2); + assert.deepEqual(res, ['a', 'a', 'b']); + assert.strictEqual(counter, 2); }); it('should delegate method with same key invocation - default key serialization - many args', async () => { @@ -115,8 +110,8 @@ describe('delegate', () => { t.foo(1, 1, 1, 2), t.foo(1, 1, 1, 1), ]); - expect(res).toEqual([4, 5, 4]); - expect(counter).toEqual(2); + assert.deepEqual(res, [4, 5, 4]); + assert.strictEqual(counter, 2); }); it('should delegate method with same key invocation - custom serialization', async () => { @@ -135,8 +130,8 @@ describe('delegate', () => { const t = new T(); const res = await Promise.all([t.foo(1, 1), t.foo(2, 1), t.foo(1, 1)]); - expect(res).toEqual([2, 3, 2]); - expect(counter).toEqual(2); + assert.deepEqual(res, [2, 3, 2]); + assert.equal(counter, 2); }); it('should have the correct context', async () => { @@ -153,6 +148,6 @@ describe('delegate', () => { const result = await Example.ex1(); - expect(result).toEqual(2); + assert.equal(result, 2); }); }); diff --git a/src/delegate/delegate.ts b/src/delegate/delegate.ts index 29b0988..2d21e59 100644 --- a/src/delegate/delegate.ts +++ b/src/delegate/delegate.ts @@ -8,7 +8,7 @@ export function delegate(keyResolver?: (...args: any[]) => str propertyName: keyof T, descriptor: TypedPropertyDescriptor>, ): TypedPropertyDescriptor> => { - if (descriptor.value) { + if (descriptor && descriptor.value) { descriptor.value = delegatify(descriptor.value, keyResolver); return descriptor; diff --git a/src/exec-time/exec-time.spec.ts b/src/exec-time/exec-time.spec.ts index ecd2c2f..ab621e9 100644 --- a/src/exec-time/exec-time.spec.ts +++ b/src/exec-time/exec-time.spec.ts @@ -1,27 +1,22 @@ import { execTime } from './exec-time'; import { ExactTimeReportData } from './exec-time.model'; import { sleep } from '../common/test-utils'; +import { describe, it, mock } from 'node:test'; +import assert from 'node:assert'; describe('exec-time', () => { it('should make sure error thrown when decorator not set on method', () => { - try { + assert.throws(() => { const nonValidExecTime: any = execTime(); class T { - @nonValidExecTime - boo: string; + @nonValidExecTime boo: string; } - } catch (e) { - expect('@execTime is applicable only on a methods.').toBe(e.message); - - return; - } - - throw new Error('should not reach this line'); + }, Error('@execTime is applicable only on a methods.')); }); it('should make sure that the reporter is called with the correct data when decorated method is sync', () => { - const reporter = jest.fn(); + const reporter = mock.fn(); class T { @execTime(reporter) @@ -33,22 +28,21 @@ describe('exec-time', () => { const t = new T(); t.foo('a'); - expect(reporter).toHaveBeenCalledTimes(1); - const args: ExactTimeReportData = reporter.mock.calls[0][0]; - expect(args.args).toEqual(['a']); - expect(args.result).toEqual('ab'); - expect(args.execTime).toBeGreaterThanOrEqual(0); - expect(args.execTime).toBeLessThan(10); + assert.equal(reporter.mock.callCount(), 1); + const args: ExactTimeReportData = reporter.mock.calls[0].arguments[0]; + assert.deepEqual(args.args, ['a']); + assert.equal(args.result, 'ab'); + assert(args.execTime >= 0); + assert(args.execTime < 10); }); it('should make sure that the reporter is called with the correct data when decorated method is async', async () => { - const reporter = jest.fn(); + const reporter = mock.fn(); class T { @execTime(reporter) async foo(x: string): Promise { await sleep(10); - return Promise.resolve(`${x}b`); } } @@ -56,16 +50,18 @@ describe('exec-time', () => { const t = new T(); await t.foo('a'); - expect(reporter).toHaveBeenCalledTimes(1); - const args: ExactTimeReportData = reporter.mock.calls[0][0]; - expect(args.args).toEqual(['a']); - expect(args.result).toEqual('ab'); - expect(args.execTime).toBeGreaterThanOrEqual(8); - expect(args.execTime).toBeLessThan(20); + assert.equal(reporter.mock.callCount(), 1); + const args: ExactTimeReportData = reporter.mock.calls[0].arguments[0]; + assert.deepEqual(args.args, ['a']); + assert.equal(args.result, 'ab'); + assert(args.execTime >= 8); + assert(args.execTime < 20); }); it('should make sure that the console.log is being called by default', async () => { - const logSpy = jest.spyOn(global.console, 'info'); + const logSpy = mock.fn(console.log); + const slog = console.log; + console.info = logSpy; class T { @execTime() @@ -76,20 +72,19 @@ describe('exec-time', () => { const t = new T(); await t.foo('a'); - expect(logSpy).toHaveBeenCalledTimes(1); - const clogSpyArgs = logSpy.mock.calls[0][0]; - expect(clogSpyArgs).toBeGreaterThanOrEqual(0); - logSpy.mockRestore(); + assert.equal(logSpy.mock.callCount(), 1); + const clogSpyArgs = logSpy.mock.calls[0].arguments[0]; + assert(clogSpyArgs >= 0); + console.info = slog; }); - it('should make sure that the reporter is called when provided as sting', async () => { + it('should make sure that the reporter is called when provided as string', async () => { class T { - goo = jest.fn(); + goo = mock.fn(); @execTime('goo') async foo(x: string): Promise { await sleep(10); - return Promise.resolve(`${x}b`); } } @@ -97,11 +92,11 @@ describe('exec-time', () => { const t = new T(); await t.foo('a'); - expect(t.goo).toHaveBeenCalledTimes(1); - const args: ExactTimeReportData = t.goo.mock.calls[0][0]; - expect(args.args).toEqual(['a']); - expect(args.result).toEqual('ab'); - expect(args.execTime).toBeGreaterThanOrEqual(8); - expect(args.execTime).toBeLessThan(20); + assert.equal(t.goo.mock.callCount(), 1); + const args: ExactTimeReportData = t.goo.mock.calls[0].arguments[0]; + assert.deepEqual(args.args, ['a']); + assert.equal(args.result, 'ab'); + assert(args.execTime >= 8); + assert(args.execTime < 20); }); -}); +}); \ No newline at end of file diff --git a/src/exec-time/exec-time.ts b/src/exec-time/exec-time.ts index 876308b..7fdfe8e 100644 --- a/src/exec-time/exec-time.ts +++ b/src/exec-time/exec-time.ts @@ -8,7 +8,7 @@ export function execTime(arg?: ReportFunction | string): ExactTimeRepor propertyName: keyof T, descriptor: TypedPropertyDescriptor>, ): TypedPropertyDescriptor> => { - if (descriptor.value) { + if (descriptor && descriptor.value) { descriptor.value = execTimify(descriptor.value, arg); return descriptor; diff --git a/src/memoize-async/memoize-async.spec.ts b/src/memoize-async/memoize-async.spec.ts index 5ffab26..9752068 100644 --- a/src/memoize-async/memoize-async.spec.ts +++ b/src/memoize-async/memoize-async.spec.ts @@ -1,9 +1,13 @@ import { memoizeAsync } from './memoize-async'; import { AsyncCache } from './memoize-async.model'; import { sleep } from '../common/test-utils'; +import { describe, it, mock } from 'node:test'; +import assert from 'node:assert'; -describe('memozie-async', () => { +describe('memoize-async', () => { it('should verify memoize async caching original method', async () => { + const spy = mock.fn((x: number, y: number) => Promise.resolve(x + y)); + class T { prop: number; @@ -13,169 +17,135 @@ describe('memozie-async', () => { } goo(x: number, y: number): Promise { - expect(this.prop).toBe(3); - - return Promise.resolve(x + y); + assert.equal(this.prop, 3); + return spy(x, y); } } - return new Promise((res) => { - const t = new T(); - t.prop = 3; - const spy = jest.spyOn(T.prototype, 'goo'); - const resp1 = t.foo(1, 2); - const resp2 = t.foo(1, 2); - - setTimeout(() => { - expect(spy).toHaveBeenCalledWith(1, 2); - expect(spy).toHaveBeenCalledTimes(1); - - const resp01 = t.foo(1, 3); - - setTimeout(() => { - expect(spy).toHaveBeenCalledWith(1, 3); - expect(spy).toHaveBeenCalledTimes(2); - }, 0); + const t = new T(); + t.prop = 3; - setTimeout(async () => { - const resp3 = t.foo(1, 2); + const resp1 = await t.foo(1, 2); + const resp2 = await t.foo(1, 2); + const resp4 = await t.foo(1, 3); - setTimeout(async () => { - expect(spy).toHaveBeenCalledWith(1, 2); + assert.equal(spy.mock.callCount(), 2); + assert.deepEqual(spy.mock.calls[0].arguments, [1, 2]); + assert.deepEqual(spy.mock.calls[1].arguments, [1, 3]); - expect(spy).toHaveBeenCalledTimes(3); + await sleep(20); + const resp3 = await t.foo(1, 2); - expect(await resp1).toBe(3); - expect(await resp2).toBe(3); - expect(await resp3).toBe(3); - expect(await resp01).toBe(4); - res(null); - }, 0); - }, 20); - }, 0); - }); + assert.equal(spy.mock.callCount(), 3); + assert.equal(resp1, 3); + assert.equal(resp2, 3); + assert.equal(resp3, 3); + assert.equal(resp4, 4); }); it('should verify memoize key foo', async () => { - const mapper = jest.fn((x: string, y: string) => `${x}_${y}`); + const mapper = mock.fn((x: string, y: string) => `${x}_${y}`); + const spyFooWithMapper = mock.fn((x: string, y: string) => Promise.resolve(x + y)); class T { + // eslint-disable-next-line class-methods-use-this @memoizeAsync({ expirationTimeMs: 10, keyResolver: mapper }) fooWithMapper(x: string, y: string): Promise { - return this.goo(x, y); - } - - goo(x: string, y: string): Promise { - return Promise.resolve(x + y); + return spyFooWithMapper(x, y); } } - return new Promise((resolve) => { - const t = new T(); - const spyFooWithMapper = jest.spyOn(T.prototype, 'goo'); + const t = new T(); - t.fooWithMapper('x', 'y'); - t.fooWithMapper('x', 'y'); - setTimeout(() => { - expect(mapper.mock.calls.length).toBe(2); - expect(spyFooWithMapper).toHaveBeenCalledTimes(1); - expect(spyFooWithMapper).toHaveBeenCalledWith('x', 'y'); - resolve(null); - }, 0); - }); + await t.fooWithMapper('x', 'y'); + await t.fooWithMapper('x', 'y'); + + assert.equal(mapper.mock.callCount(), 2); + assert.equal(spyFooWithMapper.mock.callCount(), 1); + assert.deepEqual(spyFooWithMapper.mock.calls[0].arguments, ['x', 'y']); }); it('should verify memoize key foo as string - target method', async () => { + const spyFooWithMapper = mock.fn((x: string, y: string) => Promise.resolve(x + y)); + const spyMapper = mock.fn((x: string, y: string) => `${x}_${y}`); + class T { - foo(x: string, y: string): string { - return `${x}_${y}`; - } + foo = spyMapper; + // eslint-disable-next-line class-methods-use-this @memoizeAsync({ expirationTimeMs: 10, keyResolver: 'foo' }) fooWithMapper(x: string, y: string): Promise { - return this.goo(x, y); - } - - goo(x: string, y: string): Promise { - return Promise.resolve(x + y); + return spyFooWithMapper(x, y); } } - return new Promise((resolve) => { + const prom = new Promise((resolve) => { const t = new T(); - const spyFooWithMapper = jest.spyOn(T.prototype, 'goo'); - const mapper = jest.spyOn(T.prototype, 'foo'); t.fooWithMapper('x', 'y'); t.fooWithMapper('x', 'y'); setTimeout(() => { - expect(mapper).toHaveBeenCalledTimes(2); - expect(spyFooWithMapper).toHaveBeenCalledTimes(1); - expect(spyFooWithMapper).toHaveBeenCalledWith('x', 'y'); - expect(mapper).toHaveBeenCalledWith('x', 'y'); + assert.equal(spyMapper.mock.callCount(), 2); + assert.equal(spyFooWithMapper.mock.callCount(), 1); + + assert(spyMapper.mock.calls[0].arguments[0] === 'x'); + assert(spyMapper.mock.calls[0].arguments[1] === 'y'); + + assert(spyFooWithMapper.mock.calls[0].arguments[0] === 'x'); + assert(spyFooWithMapper.mock.calls[0].arguments[1] === 'y'); + resolve(null); }, 0); }); + + await prom; }); it('should make sure error thrown when decorator not set on method', () => { - try { + assert.throws(() => { const nonValidMemoizeAsync: any = memoizeAsync(50); class T { - @nonValidMemoizeAsync - boo: string; + @nonValidMemoizeAsync boo: string; } - } catch (e) { - expect('@memoizeAsync is applicable only on a methods.').toBe(e.message); - - return; - } - - throw new Error('should not reach this line'); + }, Error('@memoizeAsync is applicable only on a methods.')); }); - it('should make sure that when promise is rejected it is removed from the cache', (done) => { + it('should make sure that when promise is rejected it is removed from the cache', async () => { + const err = new Error('rejected'); + const spy = mock.fn(() => Promise.reject(err)); + class T { @memoizeAsync(20) - foo(): Promise { - return this.goo(); - } - - goo(): Promise { - return Promise.reject(new Error('rejected')); + async foo(): Promise { + return spy(); } } const t = new T(); - const spy = jest.spyOn(T.prototype, 'goo'); + await assert.rejects(async () => { + await t.foo(); + }, err); - t.foo() - .catch((e) => { - expect(e.message).toBe('rejected'); - }); + await sleep(20); - setTimeout(() => { - t.foo().catch((e) => { - expect(e.message).toBe('rejected'); - }); + await assert.rejects(async () => { + await t.foo(); + }, err); - setTimeout(() => { - expect(spy).toHaveBeenCalledTimes(2); - done(); - }, 0); - }, 20); + assert.equal(spy.mock.callCount(), 2); }); - it('should use provided cache', (done) => { + it('should use provided cache', (_, done) => { const cache = new Map(); + const spy = mock.fn(); class T { @memoizeAsync({ expirationTimeMs: 30, cache }) foo(): Promise { - return this.goo(); + return spy(); } goo(): Promise { @@ -183,21 +153,19 @@ describe('memozie-async', () => { } } - const spy = jest.spyOn(T.prototype, 'goo'); - const t = new T(); t.foo(); setTimeout(() => { t.foo(); setTimeout(() => { - expect(spy).toHaveBeenCalledTimes(1); + assert.equal(spy.mock.callCount(), 1); cache.delete('[]'); t.foo(); setTimeout(() => { - expect(spy).toHaveBeenCalledTimes(2); + assert.equal(spy.mock.callCount(), 2); done(); }, 0); }, 0); @@ -221,8 +189,8 @@ describe('memozie-async', () => { const one = t.foo(); const two = t.goo(); - expect(await one).toBe(1); - expect(await two).toBe(2); + assert.equal(await one, 1); + assert.equal(await two, 2); }); it('should verify that by default the cache is never cleaned', async () => { @@ -238,14 +206,15 @@ describe('memozie-async', () => { const t = new T(); await t.foo(); - expect(cache.size).toEqual(1); + assert.equal(cache.size, 1); await sleep(50); - expect(cache.size).toEqual(1); + assert.equal(cache.size, 1); }); it('should verify usage of async cache', async () => { const map = new Map(); + const spy = mock.fn(() => Promise.resolve(1)); const cache: AsyncCache = { delete: async (p1: string) => { @@ -264,35 +233,31 @@ describe('memozie-async', () => { cache, }) foo(): Promise { - return this.goo(); - } - - goo(): Promise { - return Promise.resolve(1); + return spy(); } } - return new Promise((resolve) => { - const spy = jest.spyOn(T.prototype, 'goo'); - + const prom = new Promise((resolve) => { const t = new T(); t.foo(); setTimeout(() => { t.foo(); setTimeout(() => { - expect(spy).toHaveBeenCalledTimes(1); + assert.equal(spy.mock.callCount(), 1); cache.delete('[]'); t.foo(); setTimeout(() => { - expect(spy).toHaveBeenCalledTimes(2); + assert.equal(spy.mock.callCount(), 2); resolve(null); }, 0); }, 0); }, 10); }); + + await prom; }); it('should throw exception when async has method throws an exception', async () => { @@ -303,43 +268,7 @@ describe('memozie-async', () => { map.delete(p1); }, get: async (p1: string) => map.get(p1), - has: async (p1: string) => Promise.reject(new Error('error')), - set: async (p1: string, p2: number) => { - map.set(p1, p2); - }, - }; - - class T { - @memoizeAsync({ - expirationTimeMs: 30, - cache, - }) - foo(): Promise { - return Promise.resolve(1); - } - } - - const t = new T(); - try { - await t.foo(); - } catch (e) { - expect(e.message).toBe('error'); - - return; - } - - throw new Error('shouldn\'t get to here'); - }); - - it('should throw exception when async get method throws an exception', async () => { - const map = new Map(); - - const cache: AsyncCache = { - delete: async (p1: string) => { - map.delete(p1); - }, - get: async (p1: string) => Promise.reject(new Error('error')), - has: async (p1: string) => Promise.resolve(true), + has: async (_: string) => Promise.reject(new Error('error')), set: async (p1: string, p2: number) => { map.set(p1, p2); }, @@ -356,26 +285,20 @@ describe('memozie-async', () => { } const t = new T(); - try { + await assert.rejects(async () => { await t.foo(); - } catch (e) { - expect(e.message).toBe('error'); - - return; - } - - throw new Error('shouldn\'t get to here'); + }, Error('error')); }); - it('should throw exception when async get method throws an exception', async () => { + it('should throw exception when async get method throwing an exception', async () => { const map = new Map(); const cache: AsyncCache = { delete: async (p1: string) => { map.delete(p1); }, - get: async (p1: string) => Promise.reject(new Error('error')), - has: async (p1: string) => true, + get: async (_: string) => Promise.reject(new Error('error')), + has: async (_: string) => true, set: async (p1: string, p2: number) => { map.set(p1, p2); }, @@ -392,15 +315,9 @@ describe('memozie-async', () => { } const t = new T(); - try { + await assert.rejects(async () => { await t.foo(); - } catch (e) { - expect(e.message).toBe('error'); - - return; - } - - throw new Error('shouldn\'t get to here'); + }, Error('error')); }); it('should throw exception when async set method throws an exception', async () => { @@ -412,7 +329,7 @@ describe('memozie-async', () => { }, get: async (p1: string) => map.get(p1), has: async (p1: string) => map.has(p1), - set: async (p1: string, p2: number) => new Promise((resolve, reject) => { + set: async (_: string, __: number) => new Promise((___, reject) => { setTimeout(() => { reject(new Error('error')); }); @@ -430,15 +347,9 @@ describe('memozie-async', () => { } const t = new T(); - try { + await assert.rejects(async () => { await t.foo(); - } catch (e) { - expect(e.message).toBe('error'); - - return; - } - - throw new Error('shouldn\'t get to here'); + }, Error('error')); }); it('should throw exception when original method is broken', async () => { @@ -466,14 +377,8 @@ describe('memozie-async', () => { } const t = new T(); - try { + await assert.rejects(async () => { await t.foo(); - } catch (e) { - expect(e.message).toBe('error'); - - return; - } - - throw new Error('shouldn\'t get to here'); + }, Error('error')); }); }); diff --git a/src/memoize-async/memoize-async.ts b/src/memoize-async/memoize-async.ts index 33de957..d806a51 100644 --- a/src/memoize-async/memoize-async.ts +++ b/src/memoize-async/memoize-async.ts @@ -11,7 +11,7 @@ export function memoizeAsync(input?: AsyncMemoizeConfig propertyName: keyof T, descriptor: TypedPropertyDescriptor>, ): TypedPropertyDescriptor> => { - if (descriptor.value) { + if (descriptor && descriptor.value) { descriptor.value = memoizeAsyncify(descriptor.value, input as any); return descriptor; diff --git a/src/memoize/memoize.spec.ts b/src/memoize/memoize.spec.ts index 422de26..87a621e 100644 --- a/src/memoize/memoize.spec.ts +++ b/src/memoize/memoize.spec.ts @@ -1,148 +1,129 @@ import { memoize } from './memoize'; import { sleep } from '../common/test-utils'; +import { describe, it, mock } from 'node:test'; +import assert from 'node:assert'; declare const window: any; describe('memozie', () => { - it('should verify memoize caching original method', (done) => { + it('should verify memoize caching original method', (ctx, done) => { + const spy = mock.fn((x: number, y: number) => x + y); + class T { prop: 3; @memoize(10) foo(x: number, y: number): number { - return this.goo(x, y); - } - - goo(x: number, y: number): number { - expect(this.prop).toBe(3); - - return x + y; + return spy(x, y); } } const t = new T(); t.prop = 3; - const spy = jest.spyOn(T.prototype, 'goo'); + const resp1 = t.foo(1, 2); const resp2 = t.foo(1, 2); const resp4 = t.foo(1, 3); - expect(spy).toHaveBeenCalledTimes(2); - expect(spy.mock.calls[0][0]).toBe(1); - expect(spy.mock.calls[0][1]).toBe(2); - expect(spy.mock.calls[1][0]).toBe(1); - expect(spy.mock.calls[1][1]).toBe(3); + assert.equal(spy.mock.callCount(), 2); + assert.equal(spy.mock.calls[0].arguments[0], 1); + assert.equal(spy.mock.calls[0].arguments[1], 2); + assert.equal(spy.mock.calls[1].arguments[0], 1); + assert.equal(spy.mock.calls[1].arguments[1], 3); setTimeout(async () => { const resp3 = t.foo(1, 2); - expect(spy).toHaveBeenCalledTimes(3); - expect(resp1).toBe(3); - expect(resp2).toBe(3); - expect(resp3).toBe(3); - expect(resp4).toBe(4); + assert.equal(spy.mock.callCount(), 3); + assert.equal(resp1, 3); + assert.equal(resp2, 3); + assert.equal(resp3, 3); + assert.equal(resp4, 4); done(); }, 20); }); it('should make sure error thrown when decorator not set on method', () => { - try { + assert.throws(() => { const nonValidMemoize: any = memoize(50); class T { - @nonValidMemoize - boo: string; + @nonValidMemoize boo: string; } - } catch (e) { - expect('@memoize is applicable only on a methods.').toBe(e.message); - - return; - } - - throw new Error('should not reach this line'); + }, Error('@memoize is applicable only on a methods.')); }); - it('should use provided cache', (done) => { + it('should use provided cache', (ctx, done) => { const cache = new Map(); + const spy = mock.fn(() => 1); class T { @memoize({ expirationTimeMs: 30, cache }) foo(): number { - return this.goo(); - } - - goo(): number { - return 1; + return spy(); } } - const spy = jest.spyOn(T.prototype, 'goo'); const t = new T(); t.foo(); setTimeout(() => { t.foo(); - expect(spy).toHaveBeenCalledTimes(1); + assert.equal(spy.mock.callCount(), 1); cache.delete('[]'); t.foo(); - expect(spy).toHaveBeenCalledTimes(2); + assert.equal(spy.mock.callCount(), 2); done(); }, 10); }); it('should verify memoize key foo as function', async () => { - const mapper = jest.fn((x: string, y: string) => `${x}_${y}`); + const mapper = mock.fn((x: string, y: string) => `${x}_${y}`); + const spyFooWithMapper = mock.fn(); class T { + @memoize({ expirationTimeMs: 10, keyResolver: mapper }) fooWithMapper(x: string, y: string): string { - return this.goo(x, y); - } - - goo(x: string, y: string): string { - return x + y; + return spyFooWithMapper(x, y); } } const t = new T(); - const spyFooWithMapper = jest.spyOn(T.prototype, 'goo'); + t.fooWithMapper('x', 'y'); t.fooWithMapper('x', 'y'); - expect(mapper.mock.calls.length).toBe(2); - expect(spyFooWithMapper).toHaveBeenCalledTimes(1); - expect(spyFooWithMapper).toHaveBeenCalledWith('x', 'y'); + assert.equal(mapper.mock.callCount(), 2); + assert.equal(spyFooWithMapper.mock.callCount(), 1); + assert.deepEqual(spyFooWithMapper.mock.calls[0].arguments, ['x', 'y']); }); it('should verify memoize key foo as string - method name', async () => { + const spyFooWithMapper = mock.fn((x: string, y: string) => x + y); + const spyMapper = mock.fn((x: string, y: string) => `${x}_${y}`); + class T { - foo(x: string, y: string): string { - return `${x}_${y}`; - } + foo = spyMapper; @memoize({ expirationTimeMs: 10, keyResolver: 'foo' }) fooWithInnerMapper(x: string, y: string): string { - return this.goo(x, y); - } - - goo(x: string, y: string): string { - return x + y; + return spyFooWithMapper(x, y); } } const t = new T(); - const spyFooWithMapper = jest.spyOn(T.prototype, 'goo'); - const spyMapper = jest.spyOn(T.prototype, 'foo'); t.fooWithInnerMapper('x', 'y'); t.fooWithInnerMapper('x', 'y'); - expect(spyMapper).toHaveBeenCalledTimes(2); - expect(spyFooWithMapper).toHaveBeenCalledTimes(1); - expect(spyFooWithMapper).toHaveBeenCalledWith('x', 'y'); + assert.equal(spyMapper.mock.callCount(), 2); + assert.equal(spyFooWithMapper.mock.callCount(), 1); + assert(spyFooWithMapper.mock.calls[0].arguments[0] === 'x'); + assert(spyFooWithMapper.mock.calls[0].arguments[1] === 'y'); }); it('should verify that by default the cache is never cleaned', async () => { @@ -158,9 +139,9 @@ describe('memozie', () => { const t = new T(); t.foo(); - expect(cache.size).toEqual(1); + assert.equal(cache.size, 1); await sleep(50); - expect(cache.size).toEqual(1); + assert.equal(cache.size, 1); }); }); diff --git a/src/memoize/memoize.ts b/src/memoize/memoize.ts index 8fbc14d..4da0e41 100644 --- a/src/memoize/memoize.ts +++ b/src/memoize/memoize.ts @@ -11,11 +11,12 @@ export function memoize(input?: MemoizeConfig | number): propertyName: keyof T, descriptor: TypedPropertyDescriptor>, ): TypedPropertyDescriptor> => { - if (descriptor.value) { + if (descriptor && descriptor.value) { descriptor.value = memoizify(descriptor.value, input as any); return descriptor; } + throw new Error('@memoize is applicable only on a methods.'); }; } diff --git a/src/multi-dispatch/multi-dispatch.spec.ts b/src/multi-dispatch/multi-dispatch.spec.ts index 46d6f8b..5f6f384 100644 --- a/src/multi-dispatch/multi-dispatch.spec.ts +++ b/src/multi-dispatch/multi-dispatch.spec.ts @@ -1,22 +1,17 @@ import { multiDispatch } from './multi-dispatch'; import { sleep } from '../common/utils/utils'; +import { describe, it } from 'node:test'; +import assert from 'node:assert'; -describe('retry', () => { +describe('multi-dispatch', () => { it('should make sure error thrown when decorator not set on method', () => { - try { + assert.throws(() => { const nonValidMultiDispatch: any = multiDispatch(50); class T { - @nonValidMultiDispatch - boo: string; + @nonValidMultiDispatch boo: string; } - } catch (e) { - expect('@multiDispatch is applicable only on a methods.').toBe(e.message); - - return; - } - - throw new Error('should not reach this line'); + }, Error('@multiDispatch is applicable only on a methods.')); }); it('should dispatch twice and resolve', async () => { @@ -37,8 +32,8 @@ describe('retry', () => { const t = new T(); const res = await t.foo(); - expect(t.counter).toEqual(2); - expect(res).toEqual('yes'); + assert.strictEqual(t.counter, 2); + assert.strictEqual(res, 'yes'); }); it('should get last error if all rejected', async () => { @@ -60,13 +55,13 @@ describe('retry', () => { } const t = new T(); - try { + await assert.rejects(async () => { await t.foo(); - throw new Error('should not reach here'); - } catch (e) { - expect(t.counter).toEqual(2); - expect(e.message).toEqual('slowest'); - } + }, (err: Error) => { + assert.strictEqual(t.counter, 2); + assert.strictEqual(err.message, 'slowest'); + return true; + }); }); it('should dispatch twice return faster', async () => { @@ -79,7 +74,6 @@ describe('retry', () => { if (this.counter === 1) { await sleep(100); - return Promise.resolve('slow'); } @@ -90,7 +84,7 @@ describe('retry', () => { const t = new T(); const res = await t.foo(); - expect(t.counter).toEqual(2); - expect(res).toEqual('fast'); + assert.strictEqual(t.counter, 2); + assert.strictEqual(res, 'fast'); }); }); diff --git a/src/multi-dispatch/multi-dispatch.ts b/src/multi-dispatch/multi-dispatch.ts index f95634a..6fc40ee 100644 --- a/src/multi-dispatch/multi-dispatch.ts +++ b/src/multi-dispatch/multi-dispatch.ts @@ -8,7 +8,7 @@ export function multiDispatch(dispatchesAmount: number): MultiDispatcha propertyName: keyof T, descriptor: TypedPropertyDescriptor>, ): TypedPropertyDescriptor> => { - if (descriptor.value) { + if (descriptor && descriptor.value) { descriptor.value = multiDispatchify(descriptor.value, dispatchesAmount); return descriptor; diff --git a/src/on-error/on-error.spec.ts b/src/on-error/on-error.spec.ts index da82d7b..3bb357c 100644 --- a/src/on-error/on-error.spec.ts +++ b/src/on-error/on-error.spec.ts @@ -1,88 +1,84 @@ import { onError } from './on-error'; +import { describe, it, mock } from 'node:test'; +import assert from 'node:assert'; describe('onError', () => { it('should make sure error thrown when decorator not set on method', () => { - try { + assert.throws(() => { const nonValidOnError: any = onError({ func: null }); class T { - @nonValidOnError - boo: string; + @nonValidOnError boo: string; } - } catch (e) { - expect('@onError is applicable only on a methods.').toBe(e.message); - - return; - } - - throw new Error('should not reach this line'); + }, Error('@onError is applicable only on a methods.')); }); it('should verify onError called on exception, when as string', () => { + const spyGoo = mock.fn((_: number) => { + throw new Error('error'); + }); + const spyOnError = mock.fn((e: Error, args: any[]): void => { + assert.equal(e.message, 'error'); + assert.deepEqual(args, [1]); + + }); + class T { prop = 3; @onError({ func: 'onError' }) foo(x: number): any { - return this.goo(x); - } - - goo(x: number): any { - throw new Error('arr'); + return spyGoo(x); } - onError(e: Error, args: any[]): void { - expect(e.message).toBe('arr'); - expect(args).toEqual([1]); - expect(this.prop).toBe(3); - } + onError = (...args: any[]) => { + assert.equal(this.prop, 3); + spyOnError.apply(this, args); + }; } const t = new T(); - const spyGoo = jest.spyOn(T.prototype, 'goo'); - const spyAfter = jest.spyOn(T.prototype, 'onError'); t.foo(1); - expect(spyGoo).toHaveBeenCalledTimes(1); - expect(spyGoo).toHaveBeenCalledWith(1); - expect(spyAfter).toHaveBeenCalledTimes(1); + assert.equal(spyGoo.mock.callCount(), 1); + assert.equal(spyGoo.mock.calls[0].arguments[0], 1); + assert.equal(spyGoo.mock.calls[0].arguments.length, 1); + assert.equal(spyOnError.mock.callCount(), 1); }); it('should verify onError called on exception, when as function', () => { - const onErrorFunc = jest.fn((e: Error, args: any[]): void => { - expect(e.message).toBe('arr'); - expect(args).toEqual([1]); + const onErrorFunc = mock.fn((e: Error, args: any[]): void => { + assert.equal(e.message, 'arr'); + assert.deepEqual(args, [1]); + }); + const spyGoo = mock.fn((_: number) => { + throw new Error('arr'); }); class T { @onError({ func: onErrorFunc }) foo(x: number): any { - return this.goo(x); - } - - goo(x: number): any { - throw new Error('arr'); + return spyGoo(x); } } const t = new T(); - const spyGoo = jest.spyOn(T.prototype, 'goo'); t.foo(1); - expect(spyGoo).toHaveBeenCalledTimes(1); - expect(spyGoo).toHaveBeenCalledWith(1); - expect(onErrorFunc).toHaveBeenCalledTimes(1); + assert.equal(spyGoo.mock.callCount(), 1); + assert.deepEqual(spyGoo.mock.calls[0].arguments, [1]); + assert.equal(onErrorFunc.mock.callCount(), 1); }); it('should verify onError called on exception, when function is async', async () => { - const onErrorFunc = jest.fn(async (e: Error, args: any[]): Promise => { - expect(e.message).toBe('error'); - expect(args).toEqual([1]); + const onErrorFunc = mock.fn(async (e: Error, args: any[]): Promise => { + assert.equal(e.message, 'error'); + assert.deepEqual(args, [1]); }); class T { @onError({ func: onErrorFunc }) - foo(x: number): Promise { + foo(_: number): Promise { return Promise.reject(new Error('error')); } } @@ -90,11 +86,11 @@ describe('onError', () => { const t = new T(); await t.foo(1); - expect(onErrorFunc).toHaveBeenCalledTimes(1); + assert.equal(onErrorFunc.mock.callCount(), 1); }); it('should verify onError was not called when no error, and the function is async', async () => { - const onErrorFunc = jest.fn(async (): Promise => { + const onErrorFunc = mock.fn(async (): Promise => { }); class T { @@ -107,18 +103,18 @@ describe('onError', () => { const t = new T(); await t.foo(); - expect(onErrorFunc).not.toHaveBeenCalled(); + assert.equal(onErrorFunc.mock.callCount(), 0); }); it('should verify onError called on exception, when function is sync', () => { - const onErrorFunc = jest.fn(async (e: Error, args: any[]): Promise => { - expect(e.message).toBe('arr'); - expect(args).toEqual([1]); + const onErrorFunc = mock.fn((e: Error, args: any[]): void => { + assert.equal(e.message, 'arr'); + assert.deepEqual(args, [1]); }); class T { @onError({ func: onErrorFunc }) - foo(x: number): any { + foo(_: number): any { throw new Error('arr'); } } @@ -126,6 +122,6 @@ describe('onError', () => { const t = new T(); t.foo(1); - expect(onErrorFunc).toHaveBeenCalledTimes(1); + assert.equal(onErrorFunc.mock.callCount(), 1); }); }); diff --git a/src/on-error/on-error.ts b/src/on-error/on-error.ts index dfc596e..babbb48 100644 --- a/src/on-error/on-error.ts +++ b/src/on-error/on-error.ts @@ -8,7 +8,7 @@ export function onError(config: OnErrorConfig): OnErrorable { propertyName: keyof T, descriptor: TypedPropertyDescriptor>, ): TypedPropertyDescriptor> => { - if (descriptor.value) { + if (descriptor && descriptor.value) { descriptor.value = onErrorify(descriptor.value, config); return descriptor; diff --git a/src/rate-limit/rate-limit.spec.ts b/src/rate-limit/rate-limit.spec.ts index 8a9f532..6b7b7c3 100644 --- a/src/rate-limit/rate-limit.spec.ts +++ b/src/rate-limit/rate-limit.spec.ts @@ -2,6 +2,8 @@ import { rateLimit } from './rate-limit'; import { sleep } from '../common/test-utils'; import { RateLimitAsyncCounter } from './rate-limit.model'; import { SimpleRateLimitCounter } from './simple-rate-limit-counter'; +import { describe, it } from 'node:test'; +import assert from 'node:assert'; export class AsyncSimpleRateLimitCounter implements RateLimitAsyncCounter { counterMap = new Map(); @@ -48,27 +50,20 @@ describe('rate-limit', () => { } it('should make sure error thrown when decorator not set on method', () => { - try { + assert.throws(() => { const nonValidRateLimit: any = rateLimit({ allowedCalls: 1, timeSpanMs: 1000, }); class TT { - @nonValidRateLimit - boo: string; + @nonValidRateLimit boo: string; } - } catch (e) { - expect('@rateLimit is applicable only on a method.').toBe(e.message); - - return; - } - - throw new Error('should not reach this line'); + }, Error('@rateLimit is applicable only on a method.')); }); it('should make sure error on async and sync counters', () => { - try { + assert.throws(() => { class TT { @rateLimit({ allowedCalls: 1, @@ -76,39 +71,31 @@ describe('rate-limit', () => { rateLimitAsyncCounter: {} as any, rateLimitCounter: {} as any, }) - foo() { - } + foo() {} } - - throw new Error('should not reach this line'); - } catch (e) { - expect('You cant provide both rateLimitAsyncCounter and rateLimitCounter.').toBe(e.message); - } + }, Error('You can\'t provide both rateLimitAsyncCounter and rateLimitCounter.')); }); it('should verify sync limit is working as expected', async () => { const t = new T(); t.foo(); await sleep(50); - expect(t.counter).toEqual(1); + assert.equal(t.counter, 1); t.foo(); await sleep(50); - expect(t.counter).toEqual(2); + assert.equal(t.counter, 2); await sleep(50); - try { + assert.throws(() => { t.foo(); - throw new Error('should not get to this line'); - } catch (e) { - expect(e.message).toEqual('You have acceded the amount of allowed calls'); - } + }, Error('You have acceded the amount of allowed calls')); await sleep(80); const resp = t.foo(); - expect(t.counter).toEqual(3); - expect(resp).toEqual(3); + assert.equal(t.counter, 3); + assert.equal(resp, 3); }); it('should verify sync limit is working as expected with custom Rate Limiter', async () => { @@ -129,33 +116,30 @@ describe('rate-limit', () => { const t = new TT(); t.foo(); - expect(countMap.size).toEqual(1); + assert.equal(countMap.size, 1); await sleep(50); - expect(t.counter).toEqual(1); + assert.equal(t.counter, 1); t.foo(); - expect(countMap.size).toEqual(1); - expect(countMap.get('__rateLimit__')).toEqual(2); + assert.equal(countMap.size, 1); + assert.equal(countMap.get('__rateLimit__'), 2); await sleep(50); - expect(t.counter).toEqual(2); + assert.equal(t.counter, 2); await sleep(50); - try { + assert.throws(() => { t.foo(); - throw new Error('should not get to this line'); - } catch (e) { - expect(e.message).toEqual('You have acceded the amount of allowed calls'); - } + }, Error('You have acceded the amount of allowed calls')); await sleep(80); - expect(countMap.get('__rateLimit__')).toEqual(1); + assert.equal(countMap.get('__rateLimit__'), 1); t.foo(); - expect(countMap.get('__rateLimit__')).toEqual(2); + assert.equal(countMap.get('__rateLimit__'), 2); - expect(t.counter).toEqual(3); + assert.equal(t.counter, 3); await sleep(220); - expect(countMap.size).toEqual(0); + assert.equal(countMap.size, 0); }); it('should verify async limit is working as expected with custom Rate Limiter', async () => { @@ -171,7 +155,6 @@ describe('rate-limit', () => { }) async foo() { this.counter += 1; - return this.counter; } } @@ -181,30 +164,27 @@ describe('rate-limit', () => { await t1.foo(); await sleep(50); - expect(t1.counter).toEqual(1); + assert.equal(t1.counter, 1); await t2.foo(); await sleep(50); - expect(t1.counter).toEqual(1); - expect(t2.counter).toEqual(1); + assert.equal(t1.counter, 1); + assert.equal(t2.counter, 1); await sleep(50); - try { + await assert.rejects(async () => { await t1.foo(); - throw new Error('should not get to this line'); - } catch (e) { - expect(e.message).toEqual('You have acceded the amount of allowed calls'); - } + }, Error('You have acceded the amount of allowed calls')); await sleep(80); const resp1 = await t1.foo(); const resp2 = await t2.foo(); - expect(t1.counter).toEqual(2); - expect(resp1).toEqual(2); - expect(resp2).toEqual(2); + assert.equal(t1.counter, 2); + assert.equal(resp1, 2); + assert.equal(resp2, 2); - expect(counter.counterMap.has('__rateLimit__')).toEqual(true); + assert.equal(counter.counterMap.has('__rateLimit__'), true); }); it('should invoke provider mapper provided as function', async () => { @@ -214,7 +194,7 @@ describe('rate-limit', () => { timeSpanMs: 200, keyResolver: (x: string) => x, }) - foo(x: string) { + foo(_: string) { } } @@ -224,15 +204,15 @@ describe('rate-limit', () => { t.foo('b'); await sleep(50); - try { + assert.throws(() => { t.foo('a'); - throw new Error('should not get to this line'); - } catch (e) { - expect(e.message).toEqual('You have acceded the amount of allowed calls'); - } + }, Error('You have acceded the amount of allowed calls')); await sleep(120); - t.foo('a'); + + assert.doesNotThrow(() => { + t.foo('a'); + }); }); it('should invoke provider mapper provided as string', async () => { @@ -242,7 +222,7 @@ describe('rate-limit', () => { timeSpanMs: 200, keyResolver: 'goo', }) - foo(x: string) { + foo(_: string) { } goo(x: string) { @@ -256,15 +236,14 @@ describe('rate-limit', () => { t.foo('b'); await sleep(50); - try { + assert.throws(() => { t.foo('a'); - throw new Error('should not get to this line'); - } catch (e) { - expect(e.message).toEqual('You have acceded the amount of allowed calls'); - } + }, Error('You have acceded the amount of allowed calls')); await sleep(120); - t.foo('a'); + assert.doesNotThrow(() => { + t.foo('a'); + }); }); it('should invoke custom handler when exceeds the amount of allowed calls', async () => { @@ -284,11 +263,8 @@ describe('rate-limit', () => { t.foo(); await sleep(20); - try { + assert.throws(() => { t.foo(); - throw new Error('should not get to this line'); - } catch (e) { - expect(e.message).toEqual('blarg'); - } + }, Error('blarg')); }); }); diff --git a/src/rate-limit/rate-limit.ts b/src/rate-limit/rate-limit.ts index 4de1cd3..c550c38 100644 --- a/src/rate-limit/rate-limit.ts +++ b/src/rate-limit/rate-limit.ts @@ -8,7 +8,7 @@ export function rateLimit(config: RateLimitConfigs): RateLimit propertyName: keyof T, descriptor: TypedPropertyDescriptor>, ): TypedPropertyDescriptor> => { - if (descriptor.value) { + if (descriptor && descriptor.value) { descriptor.value = rateLimitify(descriptor.value, config); return descriptor; diff --git a/src/rate-limit/rate-limitify.ts b/src/rate-limit/rate-limitify.ts index 79e6270..1559592 100644 --- a/src/rate-limit/rate-limitify.ts +++ b/src/rate-limit/rate-limitify.ts @@ -56,7 +56,7 @@ export function rateLimitify( config: RateLimitConfigs, ): Method { if (config.rateLimitAsyncCounter && config.rateLimitCounter) { - throw new Error('You cant provide both rateLimitAsyncCounter and rateLimitCounter.'); + throw new Error('You can\'t provide both rateLimitAsyncCounter and rateLimitCounter.'); } const taskExec = new TaskExec(); diff --git a/src/readonly/readonly.spec.ts b/src/readonly/readonly.spec.ts index d099d83..d4df7da 100644 --- a/src/readonly/readonly.spec.ts +++ b/src/readonly/readonly.spec.ts @@ -1,24 +1,18 @@ import { readonly } from './readonly'; +import { describe, it } from 'node:test'; +import assert from 'node:assert'; + +class T { + @readonly() prop = 2; +} describe('readonly', () => { it('should verify readonly throws exception when trying to write to it', () => { - class T { - @readonly() - prop = 2; - } - const t = new T(); - expect(t.prop).toBe(2); - - try { - t.prop = 4; - } catch (e) { - expect(e.message).toBe('Cannot assign to read only property \'prop\' of object \'#\''); - - return; - } - - throw new Error('shouldn\'t reach this line'); + assert(t.prop === 2); + assert.throws(() => { + t.prop = 3; + }, Error('Cannot assign to read only property \'prop\' of object \'#\'')); }); -}); +}); \ No newline at end of file diff --git a/src/readonly/readonly.ts b/src/readonly/readonly.ts index d97c3a9..82d0ed3 100644 --- a/src/readonly/readonly.ts +++ b/src/readonly/readonly.ts @@ -1,7 +1,19 @@ export function readonly(): any { - return (target: T, key: keyof T, descriptor: PropertyDescriptor): PropertyDescriptor => { - descriptor.writable = false; + return (target: T, propertyKey: keyof T): void => { + let value = target[propertyKey]; - return descriptor; + const getter = () => value; + const setter = (newValue: any) => { + if (value !== undefined) { + throw new Error(`Cannot assign to read only property '${propertyKey as string}' of object '#<${target.constructor.name}>'`); + } + + value = newValue; + }; + + Object.defineProperty(target, propertyKey, { + get: getter, + set: setter, + }); }; -} +} \ No newline at end of file diff --git a/src/refreshable/refreshable.spec.ts b/src/refreshable/refreshable.spec.ts index 3272931..aca5efa 100644 --- a/src/refreshable/refreshable.spec.ts +++ b/src/refreshable/refreshable.spec.ts @@ -1,19 +1,19 @@ import { refreshable } from './refreshable'; import { sleep } from '../common/test-utils'; +import { describe, it, afterEach, mock } from 'node:test'; +import assert from 'node:assert'; describe('refreshable', () => { const originalSetInterval = global.setInterval; - const unrefMock = jest.fn(); + const unrefMock = mock.fn(); function useFakeSetInterval() { - global.setInterval = jest.fn(() => ({ unref: unrefMock })); + global.setInterval = mock.fn(() => ({ unref: unrefMock })); } - function restoreSetInterval() { + afterEach(() => { global.setInterval = originalSetInterval; - } - - afterEach(restoreSetInterval); + }); it('should call unref on setInterval', async () => { useFakeSetInterval(); @@ -24,7 +24,20 @@ describe('refreshable', () => { const t = { prop: 0 } as { prop: number }; foo(t, 'prop'); await sleep(10); - expect(unrefMock).toHaveBeenCalled(); + assert.equal(unrefMock.mock.callCount(), 1); + }); + + it('validate unref is not a function, the refreshable logic still works', async () => { + const dataProviderMock = mock.fn(() => Promise.resolve(0)); + global.setInterval = mock.fn(() => ({ unref: 'string' })); + const foo = refreshable({ + dataProvider: dataProviderMock, + intervalMs: 50, + }); + const t = { prop: 0 } as { prop: number }; + foo(t, 'prop'); + await sleep(10); + assert.equal(dataProviderMock.mock.callCount(), 1); }); it('should populate refreshable property', async () => { @@ -61,19 +74,19 @@ describe('refreshable', () => { await sleep(10); - expect(t.prop).toBe(0); - expect(t.proop).toBe(0); + assert.equal(t.prop, 0); + assert.equal(t.proop, 0); await sleep(60); - expect(t.prop).toBe(1); - expect(t.proop).toBe(1); + assert.equal(t.prop, 1); + assert.equal(t.proop, 1); t.prop = null; t.proop = 100; await sleep(50); - expect(t.prop).toBe(1); - expect(t.proop).toBe(2); + assert.equal(t.prop, 1); + assert.equal(t.proop, 2); }); }); diff --git a/src/retry/retry.spec.ts b/src/retry/retry.spec.ts index bf10919..f8e7beb 100644 --- a/src/retry/retry.spec.ts +++ b/src/retry/retry.spec.ts @@ -1,23 +1,18 @@ import { retry } from './retry'; import { sleep } from '../common/test-utils'; import { retryfy } from './retryfy'; +import { describe, it, mock } from 'node:test'; +import assert from 'node:assert'; describe('retry', () => { it('should make sure error thrown when decorator not set on method', () => { - try { + assert.throws(() => { const nonValidRetry: any = retry(50); class T { - @nonValidRetry - boo: string; + @nonValidRetry boo: string; } - } catch (e) { - expect('@retry is applicable only on a methods.').toBe(e.message); - - return; - } - - throw new Error('should not reach this line'); + }, Error('@retry is applicable only on a methods.')); }); it('should retry twice', async () => { @@ -31,18 +26,16 @@ describe('retry', () => { foo(): Promise { if (this.counter === 0) { this.counter += 1; - return Promise.reject(new Error('no')); } - return Promise.resolve('yes'); } } const t = new T(); const res = await t.foo(); - expect(t.counter).toEqual(1); - expect(res).toEqual('yes'); + assert.strictEqual(t.counter, 1); + assert.strictEqual(res, 'yes'); }); it('should throw exception after retries', async () => { @@ -60,13 +53,10 @@ describe('retry', () => { } const t = new T(); - try { + await assert.rejects(async () => { await t.foo(); - throw new Error('should not get hear'); - } catch (e) { - expect(t.counter).toEqual(3); - expect(e.message).toEqual('no'); - } + }, Error('no')); + assert.strictEqual(t.counter, 3); }); it('should wait according to configured delay', async () => { @@ -91,11 +81,11 @@ describe('retry', () => { const t = new T(); t.foo(); await sleep(25); - expect(t.counter).toEqual(1); + assert.strictEqual(t.counter, 1); await sleep(50); - expect(t.counter).toEqual(2); + assert.strictEqual(t.counter, 2); await sleep(75); - expect(t.counter).toEqual(3); + assert.strictEqual(t.counter, 3); }); it('should wait according to retries array', async () => { @@ -117,11 +107,11 @@ describe('retry', () => { const t = new T(); t.foo(); await sleep(25); - expect(t.counter).toEqual(1); + assert.strictEqual(t.counter, 1); await sleep(50); - expect(t.counter).toEqual(2); + assert.strictEqual(t.counter, 2); await sleep(150); - expect(t.counter).toEqual(3); + assert.strictEqual(t.counter, 3); }); it('should wait according to retries object with delaysArray', async () => { @@ -145,11 +135,11 @@ describe('retry', () => { const t = new T(); t.foo(); await sleep(25); - expect(t.counter).toEqual(1); + assert.strictEqual(t.counter, 1); await sleep(50); - expect(t.counter).toEqual(2); + assert.strictEqual(t.counter, 2); await sleep(150); - expect(t.counter).toEqual(3); + assert.strictEqual(t.counter, 3); }); it('should wait 1 sec by default', async () => { @@ -171,20 +161,23 @@ describe('retry', () => { const t = new T(); const prom = t.foo(); await sleep(500); - expect(t.counter).toEqual(1); + assert.strictEqual(t.counter, 1); await sleep(600); - expect(t.counter).toEqual(2); + assert.strictEqual(t.counter, 2); - expect(await prom).toEqual('yes'); - expect(t.counter).toEqual(2); + const res = await prom; + assert.strictEqual(res, 'yes'); + assert.strictEqual(t.counter, 2); }); - it('should throw error when invalid input', async () => { - expect(retryfy.bind(this, '6')).toThrow('invalid input'); + it('should throw error when invalid input', () => { + assert.throws(() => { + (retryfy as any)('6'); + }, new Error('invalid input')); }); it('should invoke onRetry', async () => { - const onRetry = jest.fn(); + const onRetry = mock.fn(); class T { counter = 0; @@ -219,7 +212,7 @@ describe('retry', () => { return Promise.resolve(); } - retry(e, c): void { + retry(): void { this.decCounter += 1; } } @@ -229,20 +222,20 @@ describe('retry', () => { await t.goo(); await sleep(100); - expect(onRetry).toHaveBeenCalledTimes(2); + assert.equal(onRetry.mock.callCount(), 2); const argsFirstCall = onRetry.mock.calls[0]; - expect(argsFirstCall[0].message).toEqual('no 1'); - expect(argsFirstCall[1]).toEqual(0); + assert.equal(argsFirstCall.arguments[0].message, 'no 1'); + assert.equal(argsFirstCall.arguments[1], 0); const argsSecondCall = onRetry.mock.calls[1]; - expect(argsSecondCall[0].message).toEqual('no 2'); - expect(argsSecondCall[1]).toEqual(1); + assert.equal(argsSecondCall.arguments[0].message, 'no 2'); + assert.equal(argsSecondCall.arguments[1], 1); - expect(t.decCounter).toEqual(3); + assert.equal(t.decCounter, 3); }); - it('should throw error when provided both retires and delaysArray', () => { - try { + it('should throw error when provided both retries and delaysArray', () => { + assert.throws(() => { class T { @retry({ retries: 3, @@ -252,21 +245,13 @@ describe('retry', () => { return Promise.resolve(); } } - } catch (e) { - expect('You can not provide both retries and delaysArray').toBe(e.message); - - return; - } - - throw new Error('should not reach this line'); + }, new Error('You can not provide both retries and delaysArray')); }); it('should fill the delays with 1000ms by default', async () => { class T { counter = 0; - decCounter = 0; - @retry({ retries: 1, }) @@ -285,8 +270,8 @@ describe('retry', () => { t.foo(); await sleep(500); - expect(t.counter).toEqual(1); + assert.strictEqual(t.counter, 1); await sleep(600); - expect(t.counter).toEqual(2); + assert.strictEqual(t.counter, 2); }); }); diff --git a/src/retry/retry.ts b/src/retry/retry.ts index c5b1132..f0bbf6b 100644 --- a/src/retry/retry.ts +++ b/src/retry/retry.ts @@ -8,7 +8,7 @@ export function retry(input: RetryInput): Retryable { propertyName: keyof T, descriptor: TypedPropertyDescriptor>, ): TypedPropertyDescriptor> => { - if (descriptor.value) { + if (descriptor && descriptor.value) { descriptor.value = retryfy(descriptor.value, input); return descriptor; diff --git a/src/throttle-async/throttle-async.spec.ts b/src/throttle-async/throttle-async.spec.ts index ac01d82..42c8dc1 100644 --- a/src/throttle-async/throttle-async.spec.ts +++ b/src/throttle-async/throttle-async.spec.ts @@ -1,22 +1,17 @@ import { sleep } from '../common/test-utils'; import { throttleAsync } from './throttle-async'; +import { describe, it } from 'node:test'; +import assert from 'node:assert'; describe('throttle-async', () => { it('should make sure error thrown when decorator not set on method', () => { - try { + assert.throws(() => { const nonThrottleAsync: any = throttleAsync(50); class T { - @nonThrottleAsync - boo: string; + @nonThrottleAsync boo: string; } - } catch (e) { - expect('@throttleAsync is applicable only on a methods.').toBe(e.message); - - return; - } - - throw new Error('should not reach this line'); + }, Error('@throttleAsync is applicable only on a methods.')); }); it('should verify method invocation is throttled 1', async () => { @@ -35,23 +30,23 @@ describe('throttle-async', () => { return new Promise(async (resolve) => { const t = new T(); - expect(t.prop).toEqual(0); + assert.strictEqual(t.prop, 0); t.foo('a').then((res) => { - expect(res).toEqual('a'); + assert.strictEqual(res, 'a'); }); - expect(t.prop).toEqual(1); + assert.strictEqual(t.prop, 1); t.foo('b').then((res) => { - expect(res).toEqual('b'); + assert.strictEqual(res, 'b'); resolve(null); }); - expect(t.prop).toEqual(1); + assert.strictEqual(t.prop, 1); await sleep(20); - expect(t.prop).toEqual(1); + assert.strictEqual(t.prop, 1); await sleep(50); - expect(t.prop).toEqual(2); + assert.strictEqual(t.prop, 2); }); }); @@ -72,13 +67,13 @@ describe('throttle-async', () => { const t = new T(); t.foo().then((res) => { - expect(res).toEqual(2); - expect(t.prop).toEqual(2); + assert.strictEqual(res, 2); + assert.strictEqual(t.prop, 2); }); t.foo().then((res) => { - expect(res).toEqual(2); - expect(t.prop).toEqual(2); + assert.strictEqual(res, 2); + assert.strictEqual(t.prop, 2); resolve(null); }); }); @@ -109,12 +104,12 @@ describe('throttle-async', () => { throw new Error('should get to this point'); }) .catch((e: Error) => { - expect(e.message).toEqual('blarg'); + assert.strictEqual(e.message, 'blarg'); }); t.foo('b') .then((res) => { - expect(res).toEqual('b'); + assert.strictEqual(res, 'b'); resolve(null); }); }); @@ -136,17 +131,17 @@ describe('throttle-async', () => { const t = new T(); t.foo('a'); - expect(t.prop).toEqual(1); + assert.strictEqual(t.prop, 1); t.foo('b'); - expect(t.prop).toEqual(2); + assert.strictEqual(t.prop, 2); await sleep(30); t.foo('c'); - expect(t.prop).toEqual(3); + assert.strictEqual(t.prop, 3); const val = await t.foo('d'); - expect(t.prop).toEqual(4); - expect(val).toEqual('d'); + assert.strictEqual(t.prop, 4); + assert.strictEqual(val, 'd'); }); it('should validate methods invoked between times', async () => { @@ -176,7 +171,6 @@ describe('throttle-async', () => { await t.foo('k'); const seconds = (new Date().getTime() - start.getTime()) / 1000; - expect(seconds).toBeGreaterThanOrEqual(0.5); - expect(seconds).toBeLessThan(0.6); + assert(seconds >= 0.5 && seconds < 0.6); }); -}); +}); \ No newline at end of file diff --git a/src/throttle-async/throttle-async.ts b/src/throttle-async/throttle-async.ts index 2acbe12..f01b924 100644 --- a/src/throttle-async/throttle-async.ts +++ b/src/throttle-async/throttle-async.ts @@ -7,7 +7,7 @@ export function throttleAsync(parallelCalls?: number): Decorat propertyName: keyof T, descriptor: TypedPropertyDescriptor>, ): TypedPropertyDescriptor> => { - if (descriptor.value) { + if (descriptor && descriptor.value) { descriptor.value = throttleAsyncify(descriptor.value, parallelCalls); return descriptor; diff --git a/src/throttle/throttle.spec.ts b/src/throttle/throttle.spec.ts index 1d424d7..ca81f23 100644 --- a/src/throttle/throttle.spec.ts +++ b/src/throttle/throttle.spec.ts @@ -1,56 +1,48 @@ import { throttle } from './throttle'; import { sleep } from '../common/test-utils'; +import { describe, it, mock } from 'node:test'; +import assert from 'node:assert'; describe('throttle', () => { it('should make sure error thrown when decorator not set on method', () => { - try { + assert.throws(() => { const nonValidThrottle: any = throttle(50); class T { - @nonValidThrottle - boo: string; + @nonValidThrottle boo: string; } - } catch (e) { - expect('@throttle is applicable only on a methods.').toBe(e.message); - - return; - } - - throw new Error('should not reach this line'); + }, Error('@throttle is applicable only on a methods.')); }); it('should verify method invocation is throttled', async () => { + const spy = mock.fn(); + class T { prop: number; @throttle(20) foo(x: number): void { - return this.goo(x); - } - - goo(x: number): void { - expect(this.prop).toBe(3); + return spy(x); } } const t = new T(); t.prop = 3; - const spy = jest.spyOn(T.prototype, 'goo'); + t.foo(1); - expect(spy).toHaveBeenCalledTimes(1); - expect(spy).toHaveBeenCalledWith(1); + assert.equal(spy.mock.callCount(), 1); + assert.equal(spy.mock.calls[0].arguments[0], 1); await sleep(10); t.foo(2); - expect(spy).toHaveBeenCalledTimes(1); - expect(spy).lastCalledWith(1); + assert.equal(spy.mock.callCount(), 1); + assert.equal(spy.mock.calls[0].arguments[0], 1); await sleep(30); t.foo(3); - - expect(spy).toHaveBeenCalledTimes(2); - expect(spy).lastCalledWith(3); + assert.equal(spy.mock.callCount(), 2); + assert.equal(spy.mock.calls[1].arguments[0], 3); }); }); diff --git a/src/throttle/throttle.ts b/src/throttle/throttle.ts index 7a0e773..a883fab 100644 --- a/src/throttle/throttle.ts +++ b/src/throttle/throttle.ts @@ -7,7 +7,7 @@ export function throttle(delayMs: number): Decorator { propertyName: keyof T, descriptor: TypedPropertyDescriptor>, ): TypedPropertyDescriptor> => { - if (descriptor.value) { + if (descriptor && descriptor.value) { descriptor.value = throttlify(descriptor.value, delayMs); return descriptor; diff --git a/src/timeout/timeout.spec.ts b/src/timeout/timeout.spec.ts index ecad23b..d6b4dfa 100644 --- a/src/timeout/timeout.spec.ts +++ b/src/timeout/timeout.spec.ts @@ -1,23 +1,18 @@ import { timeout } from './timeout'; import { sleep } from '../common/utils/utils'; import { TimeoutError } from './timeout.index'; +import { describe, it } from 'node:test'; +import assert from 'node:assert'; describe('timeout', () => { it('should make sure error thrown when decorator not set on method', () => { - try { + assert.throws(() => { const nonValidTimeout: any = timeout(50); class T { - @nonValidTimeout - boo: string; + @nonValidTimeout boo: string; } - } catch (e) { - expect('@timeout is applicable only on a methods.').toBe(e.message); - - return; - } - - throw new Error('should not reach this line'); + }, Error('@timeout is applicable only on a methods.')); }); it('should throw timeout exception after provided ms', async () => { @@ -26,35 +21,36 @@ describe('timeout', () => { class T { @timeout(ms) async foo(): Promise { - await sleep(100); + await sleep(70); } } const t = new T(); - try { + await assert.rejects(async () => { await t.foo(); - throw new Error('should not reach here'); - } catch (e) { - expect(e.message).toEqual(`timeout occurred after ${ms}`); - expect(e instanceof TimeoutError).toBeTruthy(); - } + }, (e: TimeoutError) => { + assert(e instanceof TimeoutError); + assert.equal(e.message, `timeout occurred after ${ms}`); + + return true; + }); }); - it('should no throw timeout exception after provided ms', async () => { + it('should not throw timeout exception after provided ms', async () => { const ms = 100; class T { @timeout(ms) async foo(): Promise { await sleep(50); - - return Promise.resolve(1); + return 1; } } const t = new T(); - expect(await t.foo()).toEqual(1); + const result = await t.foo(); + assert.equal(result, 1); }); it('should catch original exception after the method throw an error within provided ms', async () => { @@ -63,7 +59,7 @@ describe('timeout', () => { class T { @timeout(ms) async foo() { - await Promise.reject(1); + await Promise.reject(new Error('original error')); await sleep(100); } } @@ -73,7 +69,7 @@ describe('timeout', () => { try { await t.foo(); } catch (e) { - expect(e).toEqual(1); + assert.strictEqual(e.message, 'original error'); } }); @@ -93,7 +89,7 @@ describe('timeout', () => { try { await t.foo(); } catch (e) { - expect(e).toBeInstanceOf(TimeoutError); + assert(e instanceof TimeoutError); } }); }); diff --git a/src/timeout/timeout.ts b/src/timeout/timeout.ts index a7b5fd5..b7c3938 100644 --- a/src/timeout/timeout.ts +++ b/src/timeout/timeout.ts @@ -8,7 +8,7 @@ export function timeout(ms: number): Timeoutable { propertyName: keyof T, descriptor: TypedPropertyDescriptor>, ): TypedPropertyDescriptor> => { - if (descriptor.value) { + if (descriptor && descriptor.value) { descriptor.value = timeoutify(descriptor.value, ms); return descriptor; diff --git a/stryker.conf.js b/stryker.conf.js index b175b90..6c77220 100644 --- a/stryker.conf.js +++ b/stryker.conf.js @@ -3,7 +3,7 @@ module.exports = { packageManager: 'npm', reporters: ['progress', 'html', 'dashboard'], coverageAnalysis: 'off', - mutate: ['src/before/**/*.ts', '!src/**/*.spec.ts'], + mutate: ['src/**/*.ts', '!src/**/*.spec.ts'], command: 'npm run test', thresholds: { break: 95