Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a matcher to test decorator applicability #97

Open
mrazauskas opened this issue Jan 15, 2024 · 4 comments · May be fixed by #100
Open

Add a matcher to test decorator applicability #97

mrazauskas opened this issue Jan 15, 2024 · 4 comments · May be fixed by #100
Labels
enhancement New feature or request
Milestone

Comments

@mrazauskas
Copy link
Member

References:

@tbroyer Thanks for starting the discussion on decorator applicability testing. If you have a moment, could I ask for your feedback on the following thoughts?

First of all, if it sounds interesting, you can simply migrate the type test to TSTyche and it will work as expected:

import { expect } from "tstyche";

class Base extends HTMLElement {
  foo() {}
}

const classDec = <T extends new (...args: any[]) => Base>(
  value: T,
  _context: ClassDecoratorContext<T>
) => {
  return class DecoratedClass extends value {};
};

(
  @classDec
  class extends Base {}
);

expect(
  (
    @classDec
    class {}
  )
).type.toRaiseError();

expect(
  (
    @classDec
    class extends HTMLElement {}
  )
).type.toRaiseError("Unable to resolve signature of class decorator");

expect(() => {
  @classDec
  abstract class Test extends Base {}
}).type.toRaiseError(1238);

If used without arguments, the .toRaiseError() matcher will match any and all semantic errors found in the expression. If substring(s) or error code(s) are provided, only those errors will be matched. To learn more, see documentation: https://tstyche.org/reference/expect-api#to-raise-error


All works, but the try..catch pattern feels somewhat strange. The thing is that TypeScript user does not expect errors while writing code. I mean, we are using TypeScript to get feedback in a form of errors with some guidance how to fix those. As the result, users expect to have working code, but not errors.

I have a feeling that testing tools should follow this path. Type tests should demonstrate how APIs can be used, instead of what errors are raised.

Hence .not.toHaveProperty() matcher was added recently to TSTyche. .not.toBeCallableWith() and .not.toBeConstructableWith() are coming soon (see #52). These are complicated. Some progress is done in private branches, not everything is visible in the PR.


What if the applicability of a decorator could be tested with a dedicated matcher? On surface it sounds easier to implement than .toBeCallableWith().

How this new matcher should it named? Perhaps simply: .toBeApplicableTo(). Description: "Checks if the source decorator function can be applied to the target class or class member."

The above test becomes:

import { expect } from "tstyche";

class Base extends HTMLElement {
  foo() {}
}

const classDec = <T extends new (...args: any[]) => Base>(
  value: T,
  _context: ClassDecoratorContext<T>
) => {
  return class DecoratedClass extends value {};
};

abstract class Test extends Base {}

expect(classDec).type.toBeApplicableTo(class extends Base {});

expect(classDec).type.not.toBeApplicableTo(class {});
expect(classDec).type.not.toBeApplicableTo(class extends HTMLElement {});
expect(classDec).type.not.toBeApplicableTo(Test);
@mrazauskas mrazauskas added the enhancement New feature or request label Jan 15, 2024
@mrazauskas mrazauskas added this to the TSTyche 1.0 milestone Jan 15, 2024
@tbroyer
Copy link

tbroyer commented Jan 15, 2024

I didn't know about TSTyche so thanks for the ping (and knowing that it works with TSTyche makes me hopeful it can be made to work with tsd too).

Regarding your toBeApplicableTo proposal, while it works great for class decorators, how about class member decorators? I don't think you can pass a getter or method as an argument, and how about auto-accessors? But maybe just passing the member name as a second argument? (but then, how about private members? would that work? imagine a decorator declared as function dec (value: undefined, context: ClassFieldDecoratorContext<Base, any> & { private: true }); how about static and non-static members with the same name?)

@mrazauskas
Copy link
Member Author

Thanks for good questions. To be honest I have zero experience with decorators, so implementing a matcher is a chance to learn something new.

As you propose, it sounds good to take second optional argument key: string | number | symbol:

expect(bound).type.toBeApplicableTo(Person, "greet");
expect(bound).type.toBeApplicableTo(Person, "#sleep");

I don’t see how else a private field like #x could be reached from outside.


Regarding tsd. I would say that de facto it is an abandoned project, unfortunately. Owners / maintainers do almost nothing with it for years. If you look at last commits, these are mostly new error codes added by contributors. The maintainers merge those without asking to included tests. And so on. Maybe some magic will happen with tsdjs/tsd#196, but it also looks rather forgotten.

@mrazauskas
Copy link
Member Author

Ah.. static and non-static members with the same name is great edge case. Looks simple on surface:

class Foo {
  static bar() {
    return "xyz";
  }

  bar() {
    return 123;
  }
}

expect(bound).type.toBeApplicableTo(Foo.bar); // static method
expect(bound).type.toBeApplicableTo(Foo, "bar"); // same as '.toBeApplicableTo(Foo.prototype.bar)'.

But it is allowed to have static #bar or private static bar as well. If same name is the only problem, a hint could be used to distinguish between instance and static fields:

expect(bound).type.toBeApplicableTo(Foo, "bar"); // the default hint is "instance"
expect(bound).type.toBeApplicableTo(Foo, "bar", "static");

@mrazauskas
Copy link
Member Author

mrazauskas commented Jan 15, 2024

What if this matcher would be a decorator?

import { expect } from "tstyche";
import { bound, collector, selector, unbound } from "../decorators.js";

@expect(collector).type.toBeApplicable  // looks good here
@expect(selector).type.not.toBeApplicable
class Fixture {
  #name: string;
  constructor(name: string) {
    this.#name = name;
  }

  @expect(bound).type.toBeApplicable  // works with any field kind
  @expect(unbound).type.not.toBeApplicable
  greet() {
    console.log(`Hello, my name is ${this.#name}.`);
  }
}

@mrazauskas mrazauskas linked a pull request Jan 16, 2024 that will close this issue
@mrazauskas mrazauskas modified the milestones: TSTyche 1.0, TSTyche 2.0 Jan 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants