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
Make function (actor) => Promise a first-class Performable #23
Conversation
…rom step decoration
Hey, thanks for this. To answer your question, yes After our conversation (#21) I'm thinking of making some more changes it this space too. Here are my initial thoughts:
So instead of: export class Start implements Task {
static withATodoListContaining(items: string[]) {
return new Start(items);
}
@step('{0} starts with a Todo List containing #items')
performAs(actor: PerformsTasks): PromiseLike<void> {
return actor.attemptsTo(
Open.browserOn('/examples/angularjs/'),
...this.items.map(item => AddATodoItem.called(item))
);
}
constructor(private items: string[]) {
}
private addAll(items: string[]): Task[] {
return items.map(item => AddATodoItem.called(item));
}
} we'd have: @reported
export class Start implements Task {
static withATodoListContaining = (items: string[]) => new Start(items);
performAs = (actor: PerformsTasks) => actor.attemptsTo(
Open.browserOn('/examples/angularjs/'),
...this.addAll(this.items)
);
toString = () => '{0} starts with a Todo List containing #items';
constructor(private items: string[]) {
}
} Those changes, together with favouring arrow functions over the more traditional syntax would help with:
Which I think are both worthwhile. On the "funcitonal note"; I'm not sure how I feel about the First of all, I like the below style, because it both accomplishes your original goal and should help the IDEs understand the code in the non-TypeScript scenario: const PayWithCreditCard = {
number(creditCardNumber: string) {
return describeStep(
actor => actor.attemptsTo( /*...*/ ),
`{0} pays with a credit card number ${creditCardNumber}`,
);
},
}; We could take it even further: const PayWithCreditCard = {
number: (creditCardNumber: string) => describeStep(
actor => actor.attemptsTo( /*...*/ ),
`{0} pays with a credit card number ${creditCardNumber}`,
),
}; Or to make it read even nicer: const PayWithCreditCard = { number: (creditCardNumber: string) =>
aTaskWhere(`{0} pays with a credit card number ${creditCardNumber}`,
actor => actor.attemptsTo( /*...*/ )
),
}; Or even skip passing the functions, and pass lists of things to do instead: const PayWithCreditCard = { number: (creditCardNumber: string) =>
aTaskWhere(`{0} pays with a credit card number ${creditCardNumber}`,
task1,
task2,
task3
),
}; Now, since there are no annotations available to us in the JavaScript land, could the For example: @reported // that would be in serenity core, so annotations are allowed
class ReportedTask implements Task {
constructor(private description: string, private tasks: Task[]) {
}
performAs = (actor: PerformsTasks) => actor.attemptsTo(...this.tasks);
toString = () => this.description;
} we could even call it @reported
class Tasks implements Task {
constructor(private description: string, private tasks: Task[]) {
}
performAs = (actor: PerformsTasks) => actor.attemptsTo(...this.tasks);
toString = () => this.description;
} and: export function aTaskWhere(description: string, ...tasks: Task[]) {
return new Tasks(description, tasks);
} Would that work? It's late though, so I might be missing something :-) What I'm proposing is not really a functional interface, more of a builder to help with encouraging task composition, which also seems to accomplish the goal of working in JavaScript, JavaScript/Flow and TypeScript environments. Come to think about it, it works even for single-element "todo lists", such as: const Submit = aTaskWhere(`{0} submits the form`,
Click.on(Form.Submit_Button)
); What do you think? |
I like these ideas and I think everything you mentioned is doable on top of this PR. So that
const Submit = aTask(
Click.on(Form.Submit_Button)
).where(
`{0} submits the form`)
); Something like export function aTask(...tasks: Attemptable[]): Describable {
return toDescribable(compose(...tasks));
}
export function toDescribable(performable: FunctionalPerformable): Describable {
return extend<Describable>(performable, {
where: (descriptionTemplate) => describeStep(performable, descriptionTemplate)
});
}
export function compose(...tasks: Attemptable[]) : FunctionalPerformable {
return actor => actor.attemptsTo(...tasks);
}
export interface Describable extends FunctionalPerformable {
where(descriptionTemplate: string): FunctionalPerformable;
} I will give it a try this evening. I'm no longer sure if fluent task builder from #21 will add much value on top of this API. It's quite powerful already. Fluent stuff was more meant to abstract away Task construction. Maybe now it can be focused only on constructing a DSL for a Task and not on composition and annotation? |
@jan-molak Now functional tasks look even more compact const PayWithCreditCard = {
number: (creditCardNumber: string) => aTask()
.where(`{0} pays with a credit card number ${creditCardNumber}`)
};
const PlayAChord = {
called: (chord: Chord) =>
actor => PlayAnInstrument.as(actor).play(chord),
};
const playThe = chords =>
chords.map(chord => PlayAChord.called(chord));
const PerformASong = (musicSheet: MusicSheet) => ({
performAs: aTask(...playThe(musicSheet.chords)),
}); Autocompletion in IntelliJ also works well with aTask factory and all the chaining. So it should be possible to annotate with types even dynamically constructed fluent interfaces. |
- `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
Apologies for making the PR conflict with the |
No problem. It has to be refined anyway. |
@jan-molak
So why not call everything an Activity? And make certain Activities implement RequiresAbility interface if they depend on Ability. That would greatly simplify mental model for me. And finally
|
Hey @InvictusMB, it's great to hear from you again!
The idea behind the
Yes, that was my first impulse too, but it becomes more complicated when we start sharing
The difference is that an Because of this split, we can limit the the actions of some of the stage crew members, such as the Photographer, to only capture screenshots (or the HTML of the page, etc.) when the state of the system changes, so when an interaction occurs.
I'm not sure if we can achieve the above behaviour of the stage crew members if all the activities looked the same?
Interesting, could you please give an example of what you have in mind?
True, it might.
Yeah, I guess it could. Although it could also become:
And then we can have questions that require the actor to be able to see something on the screen, to read their notes, to check the last received HTTP response and so on. |
But should we know the type? Relying on feature detection makes more sense to me than using types. Especially in JS world.
Symbols work quite well there. They are unique and immutable so that you can get data back only if you have a reference to the symbol you used to set a property. I used this technique to attach metadata to functional Tasks.
I would argue with this using the same appeal to portability. I might say that for some domains I would want to treat a particular Task as an atomic transaction. I might also want that to differ on per-scenario basis. So that in scope of FillPaymentDetails scenario I'm interested in all Interactions with each form field. But in a bigger picture when I consider MakePurchase scenario I want a FillPaymentDetails task to be atomic and not report internal steps. So from my perspective an Activity might be a Task in one context but become an Interaction when I move up in abstraction levels.
For example, Memoize interaction that requires TakeNotes Ability. While TakeNotes Ability may store Notes in memory or may persist them in a file or DB. But then we could take it further. Imagine a Task Pay.with(PayPalAccount) that fills the PayPal form. But in a different context I can as well consider this an ability. So when I want to have a MakePayment interaction that would require a Pay ability I could create an Which makes differences between Task, Interaction and Ability appear and disappear depending on context and abstraction level we want to use them in. |
After a few iterations on Screen Play pattern I have the following understanding:
So from automation engineer perspective I want to have this:
Which might look like this: function MakePurchase(product) {
return actor => actor.attemtsTo(
GoTo(OffersPage),
Select(product),
UseAbility.to(Pay),
TakeNotes(getPurchaseId)
);
} And ultimately the scenario: Actor.whoCan(Pay.with(PayPalAccount))
.attemtsTo(MakePurchase(aBook)) |
That's right
That's correct, I'd also add that a Task doesn't necessarily have to cause a change in the system state. It could ask a question, for example.
That's a nice definition :-) Yes, you can also think about the Ability as a client to some external interface of the system. So
An Interaction, yes, a Task - not necessarily.
Did you mean "Ability" or "Activity"?
function MakePurchase(product) {
return actor => actor.attemtsTo(
GoTo(OffersPage),
Select(product),
UseAbility.to(Pay), // (1)
TakeNotes(getPurchaseId) // (2)
);
}
Actor.whoCan(Pay.with(PayPalAccount))
.attemptsTo(MakePurchase(aBook)) Yup, that looks cool. By the way, with the recent introduction of Lerna, you could create the functional Screenplay as a new npm module that could be used instead of the OO version. |
In this context I did mean Activity. Which would become an Ability if used in context of
That's the crucial point of my reasoning. UseAbility.to(Pay) will assert that an Actor was configured with an instance of Pay Activity as in
Yes, getPurchaseId could be a Question to execute on system.
Abstracting from a current codebase but applying a formal logic, a Task requires a particular Ability if any of underlying Interactions require that Ability, doesn't it?
Good point. I will try to do it that way. |
I'm not sure if I can fully visualise how that would work just yet, it might be easier to discuss it over a prototype perhaps?
Yes, a Task will have a transitive dependency on Ability or several Abilities, but it can be via both Interactions and Questions.
Cool! It would be nice to make the mechanism pluggable eventually. |
…ng instead of @step affects: serenity-js As per our conversation in #23, ActivityType turned out to not add much value, so it's now removed. As RecordedScenes and RecordedActivities now contain information about the place where they've been invoked, it should be possible to implement a better filtering mechanism based on paths.
Closes #21, #22
Concerns:
step
name for decorator a legacy?