Skip to content

[feat] Auto Create Spies for Property get/set #709

@Alanoll

Description

@Alanoll

Description

I was recently mocked a CanvasRenderingContext2D, and I used let obj = createSpyObject(CanvasRenderingContext2D); to create the Spy for me. It works great for all the methods on the class, but properties aren't spyable.

So while the test succeeds because the property values are assignable and readable, I can't spy that a property was set three times with three different colors, just inspect the property value for the last one.

Proposed solution

I did a quick experiment in my project and implemented this

export type SpyObjectEx<T> = T & { [P in keyof T]: T[P] extends UnknownFunction ? T[P] & CompatibleSpy<T[P]> : T[P] } & {
  /**
   * Casts to type without readonly properties
   */
  castToWritable(): Writable<T>

  propertySpy: {
    [P in keyof T]: T[P] extends UnknownFunction ? undefined : {
      'get': jasmine.Spy<(this: T) => P>
      'set': jasmine.Spy<(this: T, value: T[P]) => void>
    }
  }
};


export function installPropertySpies<T>(spy: SpyObject<T>): SpyObjectEx<T> {
  const propertySpy: any = {};
  Object.getOwnPropertyNames(spy).forEach((key) => {
    const descriptor = Object.getOwnPropertyDescriptor(spy, key);
    if (!descriptor)
      return;

    let propertySpies: {
      get?: jasmine.Spy<(this: T) => T[keyof T]> | undefined
      set?: jasmine.Spy<(this: T, value: T[keyof T]) => void> | undefined
    } = {
    };

    if (descriptor.get || descriptor.set) {
      if (descriptor.get) {
        propertySpies.get = spyOnProperty<T>(spy, key as any, 'get');
        propertySpies.get.and.callThrough();
      }

      if (descriptor.set) {
        propertySpies.set = spyOnProperty<T>(spy, key as any, 'set');
        propertySpies.set.and.callThrough();
      }
    }

    propertySpy[key] = propertySpies;
  });
  (spy as SpyObjectEx<T>).propertySpy = propertySpy;
  return spy as SpyObjectEx<T>;
}

I'm thinking it could be something added to either installProtoMethods<T> or createSpyObject<T>.
It shouldn't conflict with users use are providing properties via the template, since they're not defined as properties with getters/setters.

There is some leakage for the type casing, in that Typescript won't error if a user tried spy.propertySpy['methodName'], but it should throw a compiler error on spy.propertySpy['methodName'].and as the type should be undefined.

Alternatives considered

Leaving this in my project, since it appears to do what I need it to do. And someone could copy/paste this into their's if needed.

Do you want to create a pull request?

Yes

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions