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
Consuming SerenityJS API from es5/6 #22
Comments
Interesting idea, thanks! Could you please share what factors prevent you from using TypeScript (via ts-node)? |
@jan-molak |
- `Performable` is really an `Activity` that an `Actor` can perform, therefore it should be called as such. - What used to be an `Activity` is now a `RecordedActivity`. `RecordedActivity` is just a "tiny type", that captures the name of the real `Activity` and works alongside with `Outcome` to capture the result and other meta data related to the performance. - To make the model consistent, `Scene` is now a `RecordedScene` - To help distinguish `RecordedActivity` related to a `Task` and `Interaction`, the `@step()` annotation can now take an optional second arugment `ActivityType.Task` or `ActivityType.Interaction`. A `@step` that doesn't specify its type is considerd to be of type `Task` by default as 90% of time you'll be writing Tasks not Interactions. - The distinction between a `RecordedTask` and `RecordedInteraction` will help to make the configuration of the `Photographer` more fine-grained, so that we could for example tell it to only capture screenshots of interactions rather than all the tasks. This will also help to enable the granularity needed for #18. - `See` and `CompareNotes` are now `Interactions` rather than generic `Activities` (pretending to be `Tasks`), as performing an assertion is also an interaction with the system. No need for special treatment of those two classes. - The `Interaction` interface is also corrected to allow for `Actors` that `UseAbilities` but also `AnswerQuestions` Those changes should not affect the consumers of the Serenity/JS APIs and will help tackle the tech debt before it spreads ;-) Enables #18, #22, #23
affects: @serenity-js/cucumber-2, serenity-js - Instead of using the @step annotation it's enough for a task to define a toString() method, which will be used to determine its description #22 - RecordedTask and RecordedActivity are now replaced by RecordedActivity, which can also define the location where it was invoked to support #18 - A more functional-style DSL provided to make prototyping faster, for example, to create a task it's enough to write: task_where('{0} proceeds to checkout') #21 ISSUES CLOSED: #21, #22
Does this implementation solve the problem? You should be able to use either the ES6-style implementation of a task: class Follow {
static the = (personOfInterest: string) => new Follow(personOfInterest);
performAs(actor: PerformsTasks): PromiseLike<void> {
// ...
}
toString = () => `{0} follows the ${ this.personOfInterest }`;
constructor(personOfInterest) {
this.personOfInterest = personOfInterest;
}
} which can be passed to the actor to perform: alice.attemptsTo(Follow.the('white rabbit')) Or alternatively, you can define the task as follows: const follow_the = (person_of_interest: string) => task_where(`{0} follows the ${person_of_interest}`); and then: alice.attemptsTo(follow_the('white rabbit')) which is closer to the functional interface we talked about in #21 as it allows for tasks to be composed as well: const follow_the = (person_of_interest: string) => task_where(`{0} follows the ${person_of_interest}`,
task_where('{0} notices the rabbit'),
task_where('{0} decides to follow the rabbit'),
// ... and so on. You can also have Click.on(...) and other tasks and interactions here.
); Looking forward to hearing your thoughts. |
@jan-molak class Follow {
static the = (personOfInterest: string) => new Follow(personOfInterest);
performAs(actor: PerformsTasks): PromiseLike<void> {
// ...
}
toString = () => `{0} follows the ${ this.personOfInterest }`;
constructor(personOfInterest) {
this.personOfInterest = personOfInterest;
}
} to const Follow = {
the(personOfInterest : string) {
return task_where(`{0} follows the ${personOfInterest }`);
}
} wouldn't require any change in the consuming code. Also I believe |
…ken instead of {0} affects: serenity-js See @InvictusMB suggestion in #22
Thanks for your feedback! I like the suggestion regarding the const Follow = {
the: (personOfInterest : string) => task_where(`#actor follows the ${ personOfInterest }`);
} Does this solve the original issue of being able to use Serenity/JS with Flow? |
@jan-molak I also meant Just for reference: Airbnb naming convention I don't know any widely used JS style guides with snake case naming but there is at least one research claiming that it's better for readability in some cases. |
Yeah, that's a good point; My thinking around it was that task_where(`#actor books a ticket to from ${ origin } to ${ destination }`) vs taskWhere(`#actor books a ticket to from ${ origin } to ${ destination }`) Having said that, those functions would be used with camelCased tasks, which might (?) make it look a bit awkward: task_where(`#actor books a ticket to from ${ origin } to ${ destination }`,
SelectOriginAirport.of(origin),
SelectDestinationAirport.of(destination),
// ...
) A nice way to do it would be to have a static |
This is an interesting topic. My first guesses were 2 ways that I would go in C#. Namely:
But apparently However, the answer is much simpler. In TS a class can be used anywhere an interface can be. So just just converting Task interface to an abstract class should be sufficient. export abstract class Task extends Activity {
abstract performAs(actor: PerformsTasks): PromiseLike<void>;
static where(description: string, ...activities: Activity[]): Task {
return new AnonymousTask(description, activities);
}
} And then I had another round on this "interface vs abstract type" question but in context of TS. And so far I see no reason to use There are similiar concerns raised for Flow typing here and there. Which makes me believe that the "interface" concept in TS is only a legacy from flawed type systems in .Net and Java worlds where it was the only way to express a contract. But it seems redundant when you have a powerful type system like TypeScript or Flow. |
Intriguing, thanks for researching this topic so thoroughly! So it seems like a TypeScript class can I wonder if there could be any unwanted side effects of converting screenplay interfaces (Task/Interaction/Question) to abstract classes? If an abstract class is identical to an interface as far as the type definition is concerned, no client code would need to change. |
I would expect no side effects from this change. At least as long as the latest TypeScript versions are used. The only difference is that types and therefore interfaces leave no traces after compilation while abstract class always produces some backing code. This shows some really curious semantics in TS. Also there is one gotcha. Imagine this example: abstract class Foo {
abstract foo(): void;
}
interface Bar extends Foo {
bar(): void;
}
class Baz implements Bar {
foo() { }
bar() { }
} So far everything is fine and the code compiles. But if you add a private member to abstract class Foo, class Baz will no longer satisfy an interface. Unless it explicitly inherits Foo. And Baz is not allowed to have its own private member of the same name. The fact that private members can influence a class interface type makes absolutely no sense to me. |
…tion, ...sub-tasks)\` affects: serenity-js Thanks to @InvictusMB for suggesting the TypeScript trick enabling the \`Task.where\` syntax in #22
…_yarn/serenity-js/core-2.0.1-alpha.98 Bump @serenity-js/core from 2.0.1-alpha.90 to 2.0.1-alpha.98
Currently SerenityJS kind of forces you to use TypeScript which is not always feasible. It would be nice to expose an API that can be consumed from es5/6 code without an extra boilerplate.
There are two things that make me sad:
performAs
method istoo Java likeredundant.The following steps could address those issues:
Expose a helper that will decorate a TaskProvide a task description viatoString
method instead of a decoratorI have converted getting started tutorial to es6 with Babel as a proof of concept. It required me to create a helper to reduce a boilerplate and wrap plain functions into SerenityJS compatible Task.
Task definitions ended up like this:
Which was still far from perfect but a good place to start if you are limited to es5/6.
The text was updated successfully, but these errors were encountered: