Last active
November 7, 2025 19:15
-
-
Save jeansymolanza/5cb2a742bd0a270cd68d4df8ec4c130e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Yep—what’s biting you is how Angular’s test runtime (zone.js + jest-preset-angular) wraps/binds class methods. When you access `instance.method`, you often get a wrapped/bound function that’s not the same function object your decorator mutated, so your custom flag is missing → `undefined`. | |
| You’ve got two solid paths: | |
| # Option A — Test against the prototype (unwrapped method) | |
| Assert on the function stored on the class prototype, not on the instance. | |
| carbon.decorator.ts | |
| ```ts | |
| export function CarbonOnToggleSearch() { | |
| return (_target: any, _propertyKey: string, descriptor: PropertyDescriptor): void => { | |
| (descriptor.value as any)._taggedForCarbonOnToggleSearch = true; | |
| }; | |
| } | |
| export function CarbonOnRefresh() { | |
| return (_target: any, _propertyKey: string, descriptor: PropertyDescriptor): void => { | |
| (descriptor.value as any)._taggedForCarbonOnRefresh = true; | |
| }; | |
| } | |
| export function CarbonOnRestore() { | |
| return (_target: any, _propertyKey: string, descriptor: PropertyDescriptor): void => { | |
| (descriptor.value as any)._taggedForCarbonOnRestore = true; | |
| }; | |
| } | |
| ``` | |
| carbon.decorator.spec.ts | |
| ```ts | |
| import { CarbonOnToggleSearch, CarbonOnRefresh, CarbonOnRestore } from './carbon.decorator'; | |
| class Fixture { | |
| @CarbonOnToggleSearch() | |
| toggleSearch() {} | |
| @CarbonOnRefresh() | |
| refresh() {} | |
| @CarbonOnRestore() | |
| restore() {} | |
| } | |
| describe('Carbon decorators', () => { | |
| it('marks toggleSearch on the prototype function', () => { | |
| const fn = Object.getOwnPropertyDescriptor(Fixture.prototype, 'toggleSearch')!.value as any; | |
| expect(fn._taggedForCarbonOnToggleSearch).toBe(true); | |
| }); | |
| it('marks refresh on the prototype function', () => { | |
| const fn = Object.getOwnPropertyDescriptor(Fixture.prototype, 'refresh')!.value as any; | |
| expect(fn._taggedForCarbonOnRefresh).toBe(true); | |
| }); | |
| it('marks restore on the prototype function', () => { | |
| const fn = Object.getOwnPropertyDescriptor(Fixture.prototype, 'restore')!.value as any; | |
| expect(fn._taggedForCarbonOnRestore).toBe(true); | |
| }); | |
| }); | |
| ``` | |
| Why this works: the decorator mutates `descriptor.value` on the prototype. Prototype lookup returns the original, unwrapped function, so your flags are present. | |
| # Option B — Refactor to metadata (safer with wrappers/AOT) | |
| Use `reflect-metadata` and attach a stable metadata key. This survives wrapping/binding. | |
| carbon.decorator.ts | |
| ```ts | |
| import 'reflect-metadata'; | |
| export const CARBON_TAGS = { | |
| toggleSearch: 'carbon:toggleSearch', | |
| refresh: 'carbon:refresh', | |
| restore: 'carbon:restore', | |
| } as const; | |
| function makeCarbonDecorator(key: string) { | |
| return function () { | |
| return (_target: any, propertyKey: string, descriptor: PropertyDescriptor): void => { | |
| Reflect.defineMetadata(key, true, descriptor.value); | |
| }; | |
| }; | |
| } | |
| export const CarbonOnToggleSearch = makeCarbonDecorator(CARBON_TAGS.toggleSearch); | |
| export const CarbonOnRefresh = makeCarbonDecorator(CARBON_TAGS.refresh); | |
| export const CarbonOnRestore = makeCarbonDecorator(CARBON_TAGS.restore); | |
| ``` | |
| carbon.decorator.spec.ts | |
| ```ts | |
| import 'reflect-metadata'; | |
| import { CARBON_TAGS, CarbonOnToggleSearch, CarbonOnRefresh, CarbonOnRestore } from './carbon.decorator'; | |
| class Fixture { | |
| @CarbonOnToggleSearch() | |
| toggleSearch() {} | |
| @CarbonOnRefresh() | |
| refresh() {} | |
| @CarbonOnRestore() | |
| restore() {} | |
| } | |
| describe('Carbon decorators via metadata', () => { | |
| it('sets metadata for toggleSearch', () => { | |
| const fn = Object.getOwnPropertyDescriptor(Fixture.prototype, 'toggleSearch')!.value as Function; | |
| expect(Reflect.getMetadata(CARBON_TAGS.toggleSearch, fn)).toBe(true); | |
| }); | |
| it('sets metadata for refresh', () => { | |
| const fn = Object.getOwnPropertyDescriptor(Fixture.prototype, 'refresh')!.value as Function; | |
| expect(Reflect.getMetadata(CARBON_TAGS.refresh, fn)).toBe(true); | |
| }); | |
| it('sets metadata for restore', () => { | |
| const fn = Object.getOwnPropertyDescriptor(Fixture.prototype, 'restore')!.value as Function; | |
| expect(Reflect.getMetadata(CARBON_TAGS.restore, fn)).toBe(true); | |
| }); | |
| }); | |
| ``` | |
| # Jest/TS config checklist for Angular + decorators | |
| * `jest-preset-angular` configured. | |
| * `ts-jest` uses your spec tsconfig that has: | |
| ```json | |
| { | |
| "compilerOptions": { | |
| "experimentalDecorators": true, | |
| "emitDecoratorMetadata": false | |
| } | |
| } | |
| ``` | |
| * Add `reflect-metadata` once in your Jest setup file if using Option B: | |
| ```ts | |
| import 'reflect-metadata'; | |
| ``` | |
| * Ensure your decorators are always used with parentheses: `@CarbonOnRefresh()`. | |
| # If you still prefer instance-level assertions | |
| Angular’s wrappers can strip function properties. If you must assert on an instance, read from the prototype and compare identities: | |
| ```ts | |
| const inst = new Fixture(); | |
| const fromProto = Object.getOwnPropertyDescriptor(Fixture.prototype, 'refresh')!.value; | |
| const fromInstance = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(inst), 'refresh')!.value; | |
| expect(fromInstance).toBe(fromProto); | |
| expect((fromProto as any)._taggedForCarbonOnRefresh).toBe(true); | |
| ``` | |
| If that equality fails, something is wrapping the method; stick with prototype or metadata. | |
| Want me to adapt this to your repo layout and `jest.config.*` so it runs green out of the box? |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment