UNPKG

wrapme

Version:

Functions to wrap other functions and methods and to change/enhance their behavior, functionality or usage

1,198 lines (944 loc) 41.7 kB
import * as api from '../src'; // eslint-disable-next-line no-duplicate-imports import { AnyFunction, AnyObject, FieldGetter, FieldSetter, HandlerParam, Wrapper, WrapSettings } from '../src'; describe('API', function testSuite() { interface TestObj extends AnyObject { a: number; b: number; c?: unknown; inc(value?: number): number; dec(value?: number): number; add(...valueList: unknown[]): number; reset(): number; } const getterLog: unknown[] = []; const setterLog: unknown[] = []; let testObj: TestObj; let testValue: number; let undef: undefined; function callReset(param: HandlerParam): number { const obj = param.targetObj; obj?.reset(); return obj ? obj.a : 0; } function changeArgSet(param: HandlerParam): number { const { arg } = param; if (arg.length) { arg[0] = -arg[0]; arg.push(arg.length); } return param.run(); } function changeFirstArg(param: HandlerParam): number { // eslint-disable-next-line @typescript-eslint/restrict-plus-operands return param.run( (param.arg[0] || 0) + 10 ); } function changeResult(param: HandlerParam): number { return (param.run() as number) + 10; } function runApply(param: HandlerParam): unknown { const firstArg = param.arg0; return firstArg && typeof firstArg === 'object' ? param.runApply(...(firstArg as {data: unknown[]}).data) : param.runApply(); } function saveResult(param: HandlerParam): number { const value = (param.save.value as number) || 1; const result = param.run(value); param.save.value = result; return result; } function skipNegative(param: HandlerParam): number | undefined { const { arg } = param; if (! arg.length || arg[0] >= 0) { return param.run(); } return undef; } function useResult(param: HandlerParam): number { return param.result * 2; } function logFieldAccess(param: HandlerParam): unknown { const { byGet, bySet, settings } = param; let result; if (byGet) { result = settings.before || settings.listen ? param.result : (param.get as FieldGetter)(); getterLog.push(result); } else if (bySet) { result = param.arg0; setterLog.push(result); } return result; } function resetFieldAccessLog(): void { getterLog.length = 0; setterLog.length = 0; } function prepare(): void { testObj = { a: 0, b: 1, inc(value?: number): number { this.a += typeof value === 'number' ? value : 1; return this.a; }, dec(value?: number): number { this.a -= typeof value === 'number' ? value : 1; return this.a; }, add(...valueList: number[]): number { for (let i = 0, len = valueList.length; i < len; i++) { this.a += valueList[i]; } return this.a; }, reset(): number { return (this.a = 0); } }; resetFieldAccessLog(); testValue = 0; } beforeEach(prepare); function testFunction(value: unknown): number { if (typeof value === 'number') { testValue += value; } return testValue; } function changeAndCheckField(field: string, value: unknown, expected?: unknown): void { testObj[field] = value; expect( testObj[field] ) .toEqual( arguments.length > 2 ? expected : value ); // eslint-disable-line multiline-ternary } function checkInc(expected?: number, value?: number): void { if (arguments.length > 1) { expect( testObj.inc(value) ) .toBe( expected ); } else { expect( testObj.inc() ) .toBe( expected ); } expect( testObj.a ) .toBe( expected ); } // eslint-disable-next-line max-params function checkFunc(func: Wrapper, argList?: unknown[] | null, expected?: unknown, expectedTestValue?: unknown): void { expect( func( ...(argList || []) ) ) .toBe( expected ); if (arguments.length > 3) { expect( testValue ) .toBe( expectedTestValue ); } } type ParamCheck = (param: AnyObject) => void; function getHandlerParamCheck(field: string, expected: unknown, run?: boolean): ParamCheck { return function check(param: AnyObject): void { expect( param[field] ) .toBe( expected ); if (run) { param.run(); } }; } describe('wrap', function wrapTestSuite() { const { wrap } = api; describe('wrap method', function methodWrapTestSuite() { describe('wrap(targetObject, method, handler)', function baseMethodWrapTestSuite() { it('should replace specified method of target object and return function that restores original method', () => { const { inc } = testObj; const unwrap = wrap(testObj, 'inc', changeFirstArg); // eslint-disable-next-line @typescript-eslint/unbound-method expect( testObj.inc ) .not.toBe( inc ); unwrap(); // eslint-disable-next-line @typescript-eslint/unbound-method expect( testObj.inc ) .toBe( inc ); }); it('should save result of original method', () => { wrap(testObj, 'inc', saveResult); expect( testObj.inc() ) .toBe( 1 ); expect( testObj.inc() ) .toBe( 2 ); expect( testObj.inc() ) .toBe( 4 ); expect( testObj.inc(100) ) .toBe( 8 ); }); describe('replace specified method of target object', () => { it('should change first argument of method', () => { wrap(testObj, 'inc', changeFirstArg); checkInc(10); checkInc(25, 5); }); it('should change argument list', () => { wrap(testObj, 'add', changeArgSet); expect( testObj.add(4, 3, 8) ) .toBe( 10 ); expect( testObj.a ) .toBe( 10 ); }); it('should change result of method', () => { wrap(testObj, 'inc', changeResult); expect( testObj.inc() ) .toBe( 11 ); expect( testObj.a ) .toBe( 1 ); expect( testObj.inc(5) ) .toBe( 16 ); expect( testObj.a ) .toBe( 6 ); }); it('handler should not call original method for some condition', () => { wrap(testObj, 'inc', skipNegative); checkInc(1); checkInc(9, 8); expect( testObj.inc(-5) ) .toBe( undef ); expect( testObj.a ) .toBe( 9 ); checkInc(10); expect( testObj.inc(-3) ) .toBe( undef ); expect( testObj.a ) .toBe( 10 ); }); }); describe('handler parameter', () => { // eslint-disable-next-line max-params function checkHandlerParam(method: 'inc' | 'dec' | 'add' | 'reset', field: string, expected: unknown, settings?: WrapSettings): void { wrap(testObj, method, getHandlerParamCheck(field, expected), settings); testObj[method](); } it('"context" field should be reference to target object', () => { checkHandlerParam('inc', 'context', testObj); checkHandlerParam('dec', 'context', testObj); }); it('"context" field should be reference to specified function context', () => { const newTarget = {a: 100}; wrap(testObj, 'inc', getHandlerParamCheck('context', newTarget, true)); expect( testObj.a ) .toBe( 0 ); testObj.inc.call(newTarget); expect( newTarget.a ) .toBe( 101 ); expect( testObj.a ) .toBe( 0 ); }); it('should have "method" field with name of replaced method', () => { checkHandlerParam('inc', 'method', 'inc'); checkHandlerParam('dec', 'method', 'dec'); checkHandlerParam('reset', 'method', 'reset'); }); it('should have "number" field whose value is a number of call', () => { const expected: unknown[] = []; const log: unknown[] = []; function handler(param: HandlerParam): void { log.push( param.number ); } wrap(testObj, 'inc', handler, {after: true}); for (let i = 0; i < 8; i++) { expected.push(i + 1); expect( testObj.inc() ) .toBe( i + 1 ); } expect( log ) .toEqual( expected ); }); it('"settings" field should have value that is passed to "wrap"', () => { const settings = {before: false, data: 1}; checkHandlerParam('inc', 'settings', settings, settings); }); it('"target" field should be reference to original method', () => { /* eslint-disable @typescript-eslint/unbound-method */ checkHandlerParam('inc', 'target', testObj.inc); checkHandlerParam('dec', 'target', testObj.dec); checkHandlerParam('reset', 'target', testObj.reset); /* eslint-enable @typescript-eslint/unbound-method */ }); it('"targetObj" field should be reference to target object', () => { checkHandlerParam('inc', 'targetObj', testObj); }); it('should have "runApply" method that can be used to pass array of arguments', () => { function checkAdd(argList: unknown[], expected: number): void { expect( testObj.add(...argList) ) .toBe( expected ); testObj.reset(); } wrap(testObj, 'add', runApply); checkAdd([1, 2, 3, 4], 10); checkAdd([1, 10, 100], 111); checkAdd([ {data: [5]} ], 5); checkAdd([ {data: [7, 8, 3, -4]} ], 7); checkAdd([ {data: [ [7, 8, 3, -4] ]} ], 14); // eslint-disable-next-line array-bracket-spacing checkAdd([ {data: [ [5, 6, 1], [20, 30], 100 ]} ], 12); }); it('should have "value" field witch contains last result returned by wrapping function', () => { const log: unknown[] = []; function handler(param: HandlerParam): void { log.push(param.value); } wrap(testObj, 'inc', handler, {listen: true}); testObj.inc(); testObj.inc(4); testObj.inc(-2); expect( testObj.a ) .toBe( 3 ); expect( log ) .toEqual( [undef, 1, 5] ); }); }); }); describe('wrap(targetObject, method, handler, {after: true})', function afterMethodWrapTestSuite() { it('should call original method after handler', () => { checkInc(7, 7); wrap(testObj, 'inc', callReset, {after: true}); for (let i = 0; i < 11; i++) { checkInc(1); } testObj.add(50, 4); expect( testObj.a ) .toBe( 55 ); }); }); describe('wrap(targetObject, method, handler, {before: true})', function beforeMethodWrapTestSuite() { it('should call original method before handler', () => { wrap(testObj, 'inc', useResult, {before: true}); expect( testObj.inc() ) .toBe( 2 ); expect( testObj.a ) .toBe( 1 ); expect( testObj.inc() ) .toBe( 4 ); expect( testObj.a ) .toBe( 2 ); expect( testObj.inc(3) ) .toBe( 10 ); expect( testObj.a ) .toBe( 5 ); }); it('should change settings in handler and call original method after handler', () => { wrap( testObj, 'inc', function handler(param: HandlerParam) { param.settings.after = true; return param.result * 100; }, {before: true} ); checkInc(2); checkInc(4); checkInc(6); checkInc(12, 3); }); }); describe('wrap(targetObject, method, handler, {listen: true})', function listenMethodWrapTestSuite() { it('should call original method before handler and original\'s result should be returned from wrapper', () => { wrap(testObj, 'inc', useResult, {listen: true}); expect( testObj.inc() ) .toBe( 1 ); expect( testObj.a ) .toBe( 1 ); expect( testObj.inc() ) .toBe( 2 ); expect( testObj.a ) .toBe( 2 ); expect( testObj.inc(3) ) .toBe( 5 ); expect( testObj.a ) .toBe( 5 ); }); it('should change settings in handler and handler\'s result should be returned from wrapper', () => { wrap( testObj, 'inc', function handler(param: HandlerParam) { param.settings.listen = false; return param.result * 100; }, {listen: true} ); expect( testObj.inc(2) ) .toBe( 200 ); expect( testObj.a ) .toBe( 2 ); }); }); describe('wrap(targetObject, method, handler, {bind: true})', function bindMethodWrapTestSuite() { it('should bind created method to target object', () => { // eslint-disable-next-line @typescript-eslint/unbound-method const originalInc = testObj.inc; wrap(testObj, 'inc', changeFirstArg, {bind: true}); // eslint-disable-next-line @typescript-eslint/unbound-method const newInc = testObj.inc; expect( newInc ) .not.toBe( originalInc ); expect( newInc() ) .toBe( 10 ); expect( testObj.a ) .toBe( 10 ); expect( newInc() ) .toBe( 20 ); expect( testObj.a ) .toBe( 20 ); expect( newInc(7) ) .toBe( 37 ); expect( testObj.a ) .toBe( 37 ); }); }); describe('wrap(targetObject, method, handler, {context: someObj})', function contextMethodWrapTestSuite() { it('should call handler with specified context', () => { const logger = { log: [], handle(param: HandlerParam): number { (this.log as number[]).push(param.arg[0] || 0); return param.run(); } }; // eslint-disable-next-line @typescript-eslint/unbound-method wrap(testObj, 'inc', logger.handle, {context: logger}); checkInc(1); checkInc(2); checkInc(10, 8); checkInc(11); checkInc(7, -4); expect( logger.log ) .toEqual( [0, 0, 8, 0, -4] ); }); }); describe('wrap(targetObject, method, handler, {data: someValue})', function dataMethodWrapTestSuite() { it('"settings.data" should be available as "data" field in handler parameter', () => { interface TestParam extends HandlerParam { data: {log: Array<number | string>}; } function handler(param: TestParam): number { param.data.log.push(param.arg[0] || 'no'); return param.run(); } const data = { log: [] }; wrap(testObj, 'inc', handler, {data}); checkInc(1); checkInc(2); checkInc(5, 3); checkInc(4, -1); checkInc(5); expect( data.log ) .toEqual( ['no', 'no', 3, -1, 'no'] ); }); }); }); describe('wrap function', function functionWrapTestSuite() { describe('wrap(targetFunction, handler)', function baseFunctionWrapTestSuite() { it('should return new function that calls specified handler', () => { const wrapper = wrap(testFunction, changeFirstArg); expect( wrapper ) .not.toBe( testFunction ); checkFunc(wrapper, [], 10, 10); checkFunc(wrapper, [-5], 15, 15); checkFunc(wrapper, [4], 29, 29); checkFunc(wrapper, [], 39, 39); }); it('handler should have access to context object that is used to call wrapper function', () => { interface ContextObject { counter: number; value: number; update(val?: unknown): number; change?(val?: unknown): unknown; } interface TestParam extends HandlerParam { context: ContextObject; } function handler(param: TestParam): number { param.context.counter++; return param.run(); } const obj: ContextObject = { counter: 0, value: 0, update(val?: unknown) { if (val && typeof val === 'number') { this.value += val; } return this.value; } }; // eslint-disable-next-line @typescript-eslint/unbound-method obj.change = wrap(obj.update, handler); obj.change(); obj.change(3); obj.change(-1); obj.change(); obj.change(7); expect( obj.counter ) .toBe( 5 ); expect( obj.value ) .toBe( 9 ); }); it('handler should not call original function for some condition', () => { const wrapper = wrap(testFunction, skipNegative); checkFunc(wrapper, [], 0, 0); checkFunc(wrapper, [-5], undef, 0); checkFunc(wrapper, [7], 7, 7); checkFunc(wrapper, [], 7, 7); checkFunc(wrapper, [-3], undef, 7); checkFunc(wrapper, [3], 10, 10); }); }); describe('wrap(targetFunction, handler, settings)', function settingsFunctionWrapTestSuite() { it('should call original function after handler', () => { let counter = 0; function handler(): void { counter++; } const wrapper = wrap(testFunction, handler, {after: true}); checkFunc(wrapper, [4], 4); checkFunc(wrapper, [-6], -2); checkFunc(wrapper, [4], 2); checkFunc(wrapper, [], 2); checkFunc(wrapper, [7], 9, 9); expect( counter ) .toBe( 5 ); }); it('should call original function before handler', () => { let counter = 0; function handler(param: HandlerParam): unknown { counter++; return param.result; } const wrapper = wrap(testFunction, handler, {before: true}); checkFunc(wrapper, [2], 2); checkFunc(wrapper, [-5], -3); checkFunc(wrapper, [], -3); checkFunc(wrapper, [7], 4); checkFunc(wrapper, [3], 7, 7); expect( counter ) .toBe( 5 ); }); it('should call handler with specified context', () => { const watcher = { counter: 0, watch(param: HandlerParam): unknown { this.counter++; return param.run(); } }; // eslint-disable-next-line @typescript-eslint/unbound-method const wrapper = wrap(testFunction, watcher.watch, {context: watcher}); checkFunc(wrapper, [10], 10); checkFunc(wrapper, [-7], 3); checkFunc(wrapper, [1], 4); checkFunc(wrapper, [-2], 2, 2); expect( watcher.counter ) .toBe( 4 ); }); }); }); describe('wrap field', function fieldWrapTestSuite() { function changeAndCheckA(...argList: [unknown, unknown?]): void { return changeAndCheckField('a', ...argList); } function changeAndCheckC(...argList: [unknown, unknown?]): void { return changeAndCheckField('c', ...argList); } it('should change descriptor of specified field of target object, preserve value of field and return function that restores original descriptor', () => { const descr = Object.getOwnPropertyDescriptor(testObj, 'b'); const unwrap = wrap(testObj, 'b', logFieldAccess); expect( Object.getOwnPropertyDescriptor(testObj, 'b') ) .not.toBe( descr ); testObj.b = testObj.b + 3; const c = testObj.b - 1; testObj.b *= c; expect( getterLog ) .toEqual( [1, 1, 1] ); expect( setterLog ) .toEqual( [4, 0] ); unwrap(); resetFieldAccessLog(); expect( testObj.b ) .toBe( 1 ); expect( Object.getOwnPropertyDescriptor(testObj, 'b') ) .toEqual( descr ); testObj.b = -4; expect( getterLog.length ) .toBe( 0 ); expect( setterLog.length ) .toBe( 0 ); }); it("should change descriptor of specified field of target object, accept changes for field's value and return function that restores original descriptor", () => { const descr = Object.getOwnPropertyDescriptor(testObj, 'b'); const unwrap = wrap(testObj, 'b', logFieldAccess, {listen: true}); expect( Object.getOwnPropertyDescriptor(testObj, 'b') ) .not.toBe( descr ); testObj.b = testObj.b + 3; const c = testObj.b - 1; testObj.b *= c; expect( getterLog ) .toEqual( [1, 4, 4] ); expect( setterLog ) .toEqual( [4, 12] ); unwrap(); resetFieldAccessLog(); expect( testObj.b ) .toBe( 12 ); (descr as PropertyDescriptor).value = testObj.b; expect( Object.getOwnPropertyDescriptor(testObj, 'b') ) .toEqual( descr ); testObj.b = -4; expect( getterLog.length ) .toBe( 0 ); expect( setterLog.length ) .toBe( 0 ); }); it('should add new field in target object', () => { expect( 'c' in testObj ) .toBe( false ); expect( Object.getOwnPropertyDescriptor(testObj, 'c') ) .toBeUndefined(); const unwrap = wrap(testObj, 'c', changeFirstArg); expect( 'c' in testObj ) .toBe( true ); expect( Object.getOwnPropertyDescriptor(testObj, 'c') ) .toBeDefined(); expect( testObj.c ) .toBeUndefined(); changeAndCheckC(1, 11); changeAndCheckC(-7, 3); unwrap(); expect( 'c' in testObj ) .toBe( true ); expect( Object.getOwnPropertyDescriptor(testObj, 'c') ) .toBeDefined(); expect( testObj.c ) .toBe( 3 ); changeAndCheckC(5); changeAndCheckC(-2); }); it("should save field's getter and setter, use them and return function that restores original descriptor", () => { let value = ''; function get(): {value: string; words: number} { const trimmed = value.replace(/^\s+|\s+$/g, ''); return { value, words: trimmed ? trimmed.split(/\s+/).length : 0 }; } function set(val: string): void { value = val.toLowerCase(); } const descr = { get, set, enumerable: true, configurable: true }; Object.defineProperty(testObj, 'c', descr); const unwrap = wrap(testObj, 'c', logFieldAccess, {listen: true}); const newDescr = Object.getOwnPropertyDescriptor(testObj, 'c'); // eslint-disable-next-line @typescript-eslint/unbound-method expect( (newDescr as PropertyDescriptor).get ) .not.toBe( descr.get ); // eslint-disable-next-line @typescript-eslint/unbound-method expect( (newDescr as PropertyDescriptor).set ) .not.toBe( descr.set ); const testStr = ' Test FIELD wrapPing '; testObj.c = testStr; const { c } = testObj; expect( value ) .toBe( testStr.toLowerCase() ); expect( c ) .toEqual( {value, words: 3} ); expect( getterLog ) .toEqual( [c] ); expect( setterLog ) .toEqual( [testStr] ); unwrap(); resetFieldAccessLog(); expect( Object.getOwnPropertyDescriptor(testObj, 'c') ) .toEqual( descr ); expect( testObj.c ) .toEqual( c ); changeAndCheckC('', {value: '', words: 0}); changeAndCheckC(' ', {value: ' ', words: 0}); changeAndCheckC('new TEST', {value: 'new test', words: 2}); expect( getterLog.length ) .toBe( 0 ); expect( setterLog.length ) .toBe( 0 ); }); it("handler should control field's getter and setter", () => { function handler(param: HandlerParam): string { let result = ''; if (param.byGet) { result = ((param.get as FieldGetter)() || '') as string; result += `:${result.length}`; } else if (param.bySet) { result = (param.set as FieldSetter)(param.arg0.toLowerCase()) as string; } return result; } const unwrap = wrap(testObj, 'c', handler); expect( 'c' in testObj ) .toBe( true ); expect( Object.getOwnPropertyDescriptor(testObj, 'c') ) .toBeDefined(); expect( testObj.c ) .toBe( ':0' ); changeAndCheckC('Some String', 'some string:11'); changeAndCheckC('TEST', 'test:4'); unwrap(); expect( 'c' in testObj ) .toBe( true ); expect( Object.getOwnPropertyDescriptor(testObj, 'c') ) .toBeDefined(); expect( testObj.c ) .toBe( 'test:4' ); changeAndCheckC('Abc...xyZ'); changeAndCheckC('The Last Chance!'); }); it('should use getter and setter provided in settings', () => { function get(): number { return testObj.b; } function set(val: unknown): void { const value = Number(val); if (value) { testObj.b += value; } } const unwrap = wrap(testObj, 'c', logFieldAccess, {listen: true, get, set}); const descr = Object.getOwnPropertyDescriptor(testObj, 'c'); // eslint-disable-next-line @typescript-eslint/unbound-method expect( (descr as PropertyDescriptor).get ) .not.toBe( get ); // eslint-disable-next-line @typescript-eslint/unbound-method expect( (descr as PropertyDescriptor).set ) .not.toBe( set ); expect( testObj.c ) .toBe( testObj.b ); changeAndCheckC(1, 2); changeAndCheckC(-3, -1); changeAndCheckC(null, -1); changeAndCheckC(4, 3); changeAndCheckC('zero', 3); changeAndCheckC(0, 3); expect( testObj.c ) .toBe( testObj.b ); expect( getterLog ) .toEqual( [1, 2, -1, -1, 3, 3, 3, 3] ); expect( setterLog ) .toEqual( [1, -3, null, 4, 'zero', 0] ); unwrap(); expect( 'c' in testObj ) .toBe( true ); expect( Object.getOwnPropertyDescriptor(testObj, 'c') ) .toBeDefined(); expect( testObj.c ) .toBe( 3 ); changeAndCheckC(18); changeAndCheckC('omega'); expect( testObj.b ) .toBe( 3 ); }); it('should make read-only field', () => { const unwrap = wrap(testObj, 'a', logFieldAccess, {listen: true, set: false}); expect( testObj.a ) .toBe( 0 ); // eslint-disable-next-line no-return-assign expect(() => testObj.a = 11) .toThrow(); expect(() => testObj.inc(7)) .toThrow(); expect( getterLog ) .toEqual( [0, 0] ); expect( setterLog.length ) .toBe( 0 ); unwrap(); changeAndCheckA(8); testObj.inc(3); expect( testObj.a ) .toBe( 11 ); }); it('should make write-only field', () => { const unwrap = wrap(testObj, 'a', logFieldAccess, {listen: true, get: false}); changeAndCheckA(3, undef); changeAndCheckA(false, undef); changeAndCheckA('end', undef); expect( getterLog.length ) .toBe( 0 ); expect( setterLog ) .toEqual( [3, false, 'end'] ); unwrap(); expect( testObj.a ) .toBe( undef ); changeAndCheckA(true); changeAndCheckA(null); }); it('should wrap method as usual field', () => { /* eslint-disable @typescript-eslint/unbound-method */ const { inc } = testObj; const log: unknown[] = []; function handler(param: HandlerParam): AnyFunction { if (param.bySet) { log.push(param.arg0); } return param.byUnwrap ? inc : testObj.dec; } const unwrap = wrap(testObj, 'inc', handler, {get: true, set: () => undef}); expect( testObj.inc ) .toBe( testObj.dec ); testObj.inc(); testObj.inc = testObj.reset; testObj.inc(7); testObj.inc = testObj.add; expect( testObj.a ) .toBe( -8 ); expect( log ) .toEqual( [testObj.reset, testObj.add] ); unwrap(); expect( testObj.inc ) .toBe( inc ); testObj.inc(4); expect( testObj.a ) .toBe( -4 ); /* eslint-enable @typescript-eslint/unbound-method */ }); }); }); describe('intercept', function interceptTestSuite() { const { intercept } = api; it('should replace specified fields and methods and return function that restores original fields and methods', () => { const watcher = { counter: 0, watch(param: HandlerParam): unknown { if (! param.byUnwrap) { this.counter++; } return param.run(); } }; // eslint-disable-next-line @typescript-eslint/unbound-method const unwrap = intercept(testObj, ['inc', 'dec', 'add', 'reset', 'b', 'c'], watcher.watch, {context: watcher}); testObj.inc(); testObj.add(3, 4, 7); testObj.dec(5); testObj.c = testObj.b; testObj.b += 3; expect( testObj.a ) .toBe( 10 ); expect( testObj.b ) .toBe( 4 ); expect( testObj.c ) .toBe( 1 ); expect( watcher.counter ) .toBe( 9 ); testObj.reset(); testObj.b = testObj.a; testObj.inc(7); testObj.c = 4; expect( testObj.a ) .toBe( 7 ); expect( testObj.b ) .toBe( 0 ); expect( testObj.c ) .toBe( 4 ); expect( watcher.counter ) .toBe( 15 ); unwrap(); testObj.dec(4); testObj.add(8, -2); testObj.b = testObj.c as number; expect( testObj.a ) .toBe( 9 ); expect( testObj.b ) .toBe( 4 ); expect( testObj.c ) .toBe( 4 ); expect( watcher.counter ) .toBe( 15 ); }); it('should replace specified method and return function that restores original method', () => { function check(method: string | string[]): void { const unwrap = intercept(testObj, method, skipNegative); expect( testObj.inc(1) ) .toBe( 1 ); expect( testObj.inc(-5) ) .toBe( undef ); expect( testObj.inc(2) ) .toBe( 3 ); expect( testObj.inc(-1) ) .toBe( undef ); expect( testObj.a ) .toBe( 3 ); unwrap(); checkInc(1, -2); prepare(); } check(['inc']); check('inc'); }); it('should replace specified field and return function that restores original field', () => { function check(list?: boolean): void { const value = 3; const unwrap = intercept( testObj, list ? ['a'] : 'a', logFieldAccess, { listen: true, get: () => value, set: () => undef } ); expect( testObj.a ) .toBe( value ); expect( testObj.inc(1) ) .toBe( value); expect( testObj.dec(2) ) .toBe( value ); expect( testObj.add(3, -4, 10) ) .toBe( value ); expect( testObj.a ) .toBe( value ); expect( getterLog ) .toEqual( Array(10).fill(value) ); expect( setterLog ) .toEqual( [4, 1, 6, -1, 13] ); unwrap(); checkInc(value + 7, 7); prepare(); } check(); check(true); }); it('should return new function that calls specified handler', () => { const wrapper = intercept(testFunction, changeFirstArg); expect( wrapper ) .not.toBe( testFunction ); checkFunc(wrapper, [-2], 8, 8); }); }); });