Skip to content

Instantly share code, notes, and snippets.

@jeansymolanza
Last active November 7, 2025 19:15
Show Gist options
  • Select an option

  • Save jeansymolanza/5cb2a742bd0a270cd68d4df8ec4c130e to your computer and use it in GitHub Desktop.

Select an option

Save jeansymolanza/5cb2a742bd0a270cd68d4df8ec4c130e to your computer and use it in GitHub Desktop.
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