472 lines
16 KiB
TypeScript
472 lines
16 KiB
TypeScript
import { beforeEach, describe, expect, mock, test } from 'bun:test'
|
|
import { after, before, instead } from '.'
|
|
|
|
describe('Patcher', () => {
|
|
let originalMethod: (...args: any[]) => string
|
|
let obj: { method: typeof originalMethod }
|
|
|
|
beforeEach(() => {
|
|
originalMethod = mock(s => s ?? 'original')
|
|
obj = {
|
|
method: originalMethod,
|
|
}
|
|
})
|
|
|
|
describe('before', () => {
|
|
test('should execute callback before original method', () => {
|
|
const beforeCallback = mock(args => args)
|
|
before(obj, 'method', beforeCallback)
|
|
|
|
obj.method('arg1', 'arg2')
|
|
|
|
expect(beforeCallback).toHaveBeenCalledWith(['arg1', 'arg2'])
|
|
expect(originalMethod).toHaveBeenCalled()
|
|
})
|
|
|
|
test('should allow modifying arguments', () => {
|
|
before(obj, 'method', args => {
|
|
args[0] = 'modified'
|
|
return args
|
|
})
|
|
|
|
obj.method('original')
|
|
|
|
expect(originalMethod).toHaveBeenCalledWith('modified')
|
|
})
|
|
|
|
test('should unpatch correctly', () => {
|
|
const beforeCallback = mock(args => {
|
|
args[0] = 'modified'
|
|
return args
|
|
})
|
|
const unpatch = before(obj, 'method', beforeCallback)
|
|
|
|
// Test that patch is working
|
|
obj.method('original')
|
|
expect(originalMethod).toHaveBeenCalledWith('modified')
|
|
|
|
// Unpatch
|
|
unpatch()
|
|
|
|
// Test that patch is removed
|
|
obj.method('original')
|
|
expect(originalMethod).toHaveBeenCalledWith('original')
|
|
expect(beforeCallback).toHaveBeenCalledTimes(1) // Only called before unpatch
|
|
})
|
|
|
|
test('should handle multiple before hooks and unpatch correctly', () => {
|
|
const firstHook = mock(args => {
|
|
args[0] = 'first'
|
|
return args
|
|
})
|
|
const secondHook = mock(args => {
|
|
args[0] = `${args[0]}-second`
|
|
return args
|
|
})
|
|
|
|
const unpatch2 = before(obj, 'method', secondHook)
|
|
const unpatch1 = before(obj, 'method', firstHook)
|
|
|
|
obj.method('original')
|
|
expect(originalMethod).toHaveBeenCalledWith('first-second')
|
|
|
|
// Unpatch second hook
|
|
unpatch2()
|
|
|
|
obj.method('original')
|
|
expect(originalMethod).toHaveBeenCalledWith('first')
|
|
|
|
// Unpatch first hook
|
|
unpatch1()
|
|
|
|
obj.method('original')
|
|
expect(originalMethod).toHaveBeenCalledWith('original')
|
|
})
|
|
|
|
test('should not crash when unpatch is called multiple times', () => {
|
|
const beforeCallback = mock(args => args)
|
|
const unpatch = before(obj, 'method', beforeCallback)
|
|
|
|
unpatch()
|
|
expect(() => unpatch()).not.toThrow()
|
|
expect(() => unpatch()).not.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('after', () => {
|
|
test('should execute callback after original method', () => {
|
|
const afterCallback = mock(() => 'original')
|
|
after(obj, 'method', afterCallback)
|
|
|
|
const result = obj.method()
|
|
|
|
expect(afterCallback).toHaveBeenCalledWith('original')
|
|
expect(result).toBe('original')
|
|
})
|
|
|
|
test('should allow modifying return value', () => {
|
|
after(obj, 'method', () => 'modified')
|
|
|
|
const result = obj.method()
|
|
|
|
expect(result).toBe('modified')
|
|
})
|
|
|
|
test('should unpatch correctly', () => {
|
|
const afterCallback = mock(() => 'modified')
|
|
const unpatch = after(obj, 'method', afterCallback)
|
|
|
|
// Test that patch is working
|
|
let result = obj.method()
|
|
expect(result).toBe('modified')
|
|
expect(afterCallback).toHaveBeenCalledWith('original')
|
|
|
|
// Unpatch
|
|
unpatch()
|
|
|
|
// Test that patch is removed
|
|
result = obj.method()
|
|
expect(result).toBe('original')
|
|
expect(afterCallback).toHaveBeenCalledTimes(1) // Only called before unpatch
|
|
})
|
|
|
|
test('should handle multiple after hooks and unpatch correctly', () => {
|
|
const firstHook = mock(result => `${result}-first`)
|
|
const secondHook = mock(result => `${result}-second`)
|
|
|
|
const unpatch1 = after(obj, 'method', firstHook)
|
|
const unpatch2 = after(obj, 'method', secondHook)
|
|
|
|
let result = obj.method()
|
|
expect(result).toBe('original-second-first')
|
|
|
|
// Unpatch second hook (most recent)
|
|
unpatch2()
|
|
result = obj.method()
|
|
expect(result).toBe('original-first')
|
|
|
|
// Unpatch first hook
|
|
unpatch1()
|
|
result = obj.method()
|
|
expect(result).toBe('original')
|
|
})
|
|
|
|
test('should not crash when unpatch is called multiple times', () => {
|
|
const afterCallback = mock(result => result)
|
|
const unpatch = after(obj, 'method', afterCallback)
|
|
|
|
unpatch()
|
|
expect(() => unpatch()).not.toThrow()
|
|
expect(() => unpatch()).not.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('instead', () => {
|
|
test('should replace original method', () => {
|
|
const replacement = mock(() => 'replaced')
|
|
instead(obj, 'method', replacement)
|
|
|
|
const result = obj.method('arg1')
|
|
|
|
expect(replacement).toHaveBeenCalledWith(
|
|
['arg1'],
|
|
expect.any(Function),
|
|
)
|
|
expect(originalMethod).not.toHaveBeenCalled()
|
|
expect(result).toBe('replaced')
|
|
})
|
|
|
|
test('should provide access to original method', () => {
|
|
instead(obj, 'method', (_args, original) => {
|
|
return `${original()} modified`
|
|
})
|
|
|
|
const result = obj.method()
|
|
|
|
expect(result).toBe('original modified')
|
|
})
|
|
|
|
test('should unpatch correctly', () => {
|
|
const replacement = mock(() => 'replaced')
|
|
const unpatch = instead(obj, 'method', replacement)
|
|
|
|
// Test that patch is working
|
|
let result = obj.method()
|
|
expect(result).toBe('replaced')
|
|
expect(originalMethod).not.toHaveBeenCalled()
|
|
|
|
// Unpatch
|
|
unpatch()
|
|
|
|
// Test that patch is removed
|
|
result = obj.method()
|
|
expect(result).toBe('original')
|
|
expect(originalMethod).toHaveBeenCalled()
|
|
expect(replacement).toHaveBeenCalledTimes(1) // Only called before unpatch
|
|
})
|
|
|
|
test('should handle multiple instead hooks and unpatch correctly', () => {
|
|
const firstHook = mock((_args, original) => `first-${original()}`)
|
|
const secondHook = mock((_args, original) => `second-${original()}`)
|
|
|
|
const unpatch1 = instead(obj, 'method', firstHook)
|
|
const unpatch2 = instead(obj, 'method', secondHook)
|
|
|
|
let result = obj.method()
|
|
expect(result).toBe('second-first-original')
|
|
|
|
// Unpatch second hook (most recent)
|
|
unpatch2()
|
|
result = obj.method()
|
|
expect(result).toBe('first-original')
|
|
|
|
// Unpatch first hook
|
|
unpatch1()
|
|
result = obj.method()
|
|
expect(result).toBe('original')
|
|
})
|
|
|
|
test('should not crash when unpatch is called multiple times', () => {
|
|
const replacement = mock(() => 'replaced')
|
|
const unpatch = instead(obj, 'method', replacement)
|
|
|
|
unpatch()
|
|
expect(() => unpatch()).not.toThrow()
|
|
expect(() => unpatch()).not.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('mixed hooks', () => {
|
|
test('should handle before + after', () => {
|
|
const beforeHook = mock(args => {
|
|
args[0] = 'modified'
|
|
return args
|
|
})
|
|
const afterHook = mock(result => `${result}-after`)
|
|
|
|
const unpatchBefore = before(obj, 'method', beforeHook)
|
|
const unpatchAfter = after(obj, 'method', afterHook)
|
|
|
|
let result = obj.method('original')
|
|
expect(result).toBe('modified-after')
|
|
expect(beforeHook).toHaveBeenCalledWith(['modified'])
|
|
expect(afterHook).toHaveBeenCalledWith('modified')
|
|
|
|
// Unpatch after hook
|
|
unpatchAfter()
|
|
result = obj.method('original')
|
|
expect(result).toBe('modified')
|
|
|
|
// Unpatch before hook
|
|
unpatchBefore()
|
|
result = obj.method('original')
|
|
expect(result).toBe('original')
|
|
})
|
|
test('should handle instead + before', () => {
|
|
const insteadHook = mock(
|
|
(args, original) => `${original(...args)}-instead`,
|
|
)
|
|
const beforeHook = mock(args => {
|
|
args[0] = 'modified'
|
|
return args
|
|
})
|
|
|
|
const unpatchInstead = instead(obj, 'method', insteadHook)
|
|
const unpatchBefore = before(obj, 'method', beforeHook)
|
|
|
|
let result = obj.method('original')
|
|
expect(result).toBe('modified-instead')
|
|
expect(insteadHook).toHaveBeenCalledWith(
|
|
['modified'],
|
|
expect.any(Function),
|
|
)
|
|
expect(beforeHook).toHaveBeenCalledWith(['modified'])
|
|
|
|
// Unpatch before hook
|
|
unpatchBefore()
|
|
result = obj.method('original')
|
|
expect(result).toBe('original-instead')
|
|
|
|
// Unpatch instead hook
|
|
unpatchInstead()
|
|
result = obj.method('original')
|
|
expect(result).toBe('original')
|
|
})
|
|
|
|
test('should handle instead + after', () => {
|
|
const insteadHook = mock(() => 'oops')
|
|
const afterHook = mock(result => `${result}-after`)
|
|
|
|
const unpatchInstead = instead(obj, 'method', insteadHook)
|
|
const unpatchAfter = after(obj, 'method', afterHook)
|
|
|
|
let result = obj.method('original')
|
|
expect(result).toBe('oops-after')
|
|
expect(insteadHook).toHaveBeenCalledWith(
|
|
['original'],
|
|
expect.any(Function),
|
|
)
|
|
expect(afterHook).toHaveBeenCalledWith('oops')
|
|
|
|
// Unpatch after hook
|
|
unpatchAfter()
|
|
result = obj.method('original')
|
|
expect(result).toBe('oops')
|
|
|
|
// Unpatch instead hook
|
|
unpatchInstead()
|
|
result = obj.method('original')
|
|
expect(result).toBe('original')
|
|
})
|
|
|
|
test('should handle instead + before + after', () => {
|
|
const insteadHook = mock(
|
|
(args, original) => `${original(...args)}-instead`,
|
|
)
|
|
|
|
const beforeHook = mock(args => {
|
|
args[0] = 'modified'
|
|
return args
|
|
})
|
|
|
|
const afterHook = mock(result => `${result}-after`)
|
|
|
|
const unpatchInstead = instead(obj, 'method', insteadHook)
|
|
const unpatchBefore = before(obj, 'method', beforeHook)
|
|
const unpatchAfter = after(obj, 'method', afterHook)
|
|
|
|
let result = obj.method('original')
|
|
expect(result).toBe('modified-instead-after')
|
|
expect(insteadHook).toHaveBeenCalledWith(
|
|
['modified'],
|
|
expect.any(Function),
|
|
)
|
|
expect(beforeHook).toHaveBeenCalledWith(['modified'])
|
|
expect(afterHook).toHaveBeenCalledWith('modified-instead')
|
|
|
|
// Unpatch after hook
|
|
unpatchAfter()
|
|
result = obj.method('original')
|
|
expect(result).toBe('modified-instead')
|
|
|
|
// Unpatch before hook
|
|
unpatchBefore()
|
|
result = obj.method('original')
|
|
expect(result).toBe('original-instead')
|
|
|
|
// Unpatch instead hook
|
|
unpatchInstead()
|
|
result = obj.method('original')
|
|
expect(result).toBe('original')
|
|
})
|
|
|
|
test('should handle before + after + instead', () => {
|
|
const beforeHook = mock(args => {
|
|
args[0] = 'before-modified'
|
|
return args
|
|
})
|
|
|
|
const afterHook = mock(result => `${result}-after`)
|
|
|
|
const insteadHook = mock(
|
|
(args, original) => `${original(...args)}-instead`,
|
|
)
|
|
|
|
const unpatchBefore = before(obj, 'method', beforeHook)
|
|
const unpatchAfter = after(obj, 'method', afterHook)
|
|
const unpatchInstead = instead(obj, 'method', insteadHook)
|
|
|
|
let result = obj.method('original')
|
|
expect(result).toBe('before-modified-instead-after')
|
|
expect(beforeHook).toHaveBeenCalledWith(['before-modified'])
|
|
expect(afterHook).toHaveBeenCalledWith('before-modified-instead')
|
|
expect(insteadHook).toHaveBeenCalledWith(
|
|
['before-modified'],
|
|
expect.any(Function),
|
|
)
|
|
|
|
// Unpatch after hook
|
|
unpatchAfter()
|
|
result = obj.method('original')
|
|
expect(result).toBe('before-modified-instead')
|
|
|
|
// Unpatch before hook
|
|
unpatchBefore()
|
|
result = obj.method('original')
|
|
expect(result).toBe('original-instead')
|
|
|
|
// Unpatch instead hook
|
|
unpatchInstead()
|
|
result = obj.method('original')
|
|
expect(result).toBe('original')
|
|
})
|
|
|
|
test('should restore original method when all hooks are unpatched', () => {
|
|
const originalRef = obj.method
|
|
|
|
const unpatch1 = before(obj, 'method', args => args)
|
|
const unpatch2 = after(obj, 'method', result => result)
|
|
|
|
// Method should be patched
|
|
expect(obj.method).not.toBe(originalRef)
|
|
|
|
unpatch1()
|
|
// Still patched because after hook remains
|
|
expect(obj.method).not.toBe(originalRef)
|
|
|
|
unpatch2()
|
|
// Should be restored to original
|
|
expect(obj.method).toBe(originalRef)
|
|
})
|
|
})
|
|
|
|
// describe('error handling', () => {
|
|
// test('should throw error for invalid parent', () => {
|
|
// expect(() => before(null as any, 'method', () => [])).toThrow()
|
|
// expect(() => before(undefined as any, 'method', () => [])).toThrow()
|
|
// expect(() => before('string' as any, 'method', () => [])).toThrow()
|
|
// })
|
|
|
|
// test('should throw error for non-existent property', () => {
|
|
// expect(() => before(obj, 'nonExistent' as any, () => [])).toThrow(
|
|
// "Property 'nonExistent' does not exist on parent",
|
|
// )
|
|
// })
|
|
|
|
// test('should throw error for non-function property', () => {
|
|
// const objWithNonFunction = { prop: 'not a function' }
|
|
// expect(() =>
|
|
// before(objWithNonFunction, 'prop' as any, () => []),
|
|
// ).toThrow("Property 'prop' is not a function")
|
|
// })
|
|
|
|
// test('should throw error for invalid hook', () => {
|
|
// expect(() => before(obj, 'method', null as any)).toThrow()
|
|
// expect(() => before(obj, 'method', 'string' as any)).toThrow()
|
|
// })
|
|
// })
|
|
|
|
describe('constructor patching', () => {
|
|
test('should patch constructor functions', () => {
|
|
class TestClass {
|
|
value: string
|
|
constructor(value: string) {
|
|
this.value = value
|
|
}
|
|
}
|
|
|
|
const obj = { TestClass }
|
|
|
|
const beforeHook = mock(args => {
|
|
args[0] = 'modified'
|
|
return args
|
|
})
|
|
|
|
before(obj, 'TestClass', beforeHook)
|
|
|
|
const instance = new obj.TestClass('original')
|
|
expect(instance.value).toBe('modified')
|
|
expect(beforeHook).toHaveBeenCalledWith(['modified'])
|
|
})
|
|
})
|
|
})
|