Permalink
Browse files

fix(core): both the @step and Activity::toString can use an #actor to…

…ken instead of {0}

affects: serenity-js

See @InvictusMB suggestion in #22
  • Loading branch information...
jan-molak committed Apr 28, 2017
1 parent 605018e commit a1da9235a6142a7082ed6ba419abc92abb7025f0
@@ -87,20 +87,22 @@ describe ('When recording', () => {
describe('minimalist implementation', () => {
const follow_the = (person_of_interest: string) => task_where(`{0} follows the ${person_of_interest}`);
const Follow = {
the: (person_of_interest: string) => task_where(`{0} follows the ${person_of_interest}`),
};
it('notifies the Stage Manager when the activity starts and finishes', () => alice.attemptsTo(follow_the('white rabbit')).then(() => {
it('notifies the Stage Manager when the activity starts and finishes', () => alice.attemptsTo(Follow.the('white rabbit')).then(() => {
const entries = stage_manager.readTheJournal();
expect(entries).to.have.lengthOf(2);
expect(entries[ 0 ].value).to.deep.equal(entries[ 1 ].value.subject);
}));
it('notifies the Stage Manager of Activity\'s invocation location', () => alice.attemptsTo(follow_the('white rabbit')).then(() => {
it('notifies the Stage Manager of Activity\'s invocation location', () => alice.attemptsTo(Follow.the('white rabbit')).then(() => {
const entries = stage_manager.readTheJournal();
expect(entries[ 0 ].value).to.be.recorded.as('Alice follows the white rabbit').calledAt({
line: 99,
line: 101,
column: 97,
path: '/recording.spec.ts',
});
@@ -1,21 +1,17 @@
import { ActivityDescription } from '../../../../src/serenity/recording/activity_description';
import { given } from 'mocha-testdata';
import { describe_as } from '../../../../src/serenity/recording/activity_description';
import { Actor, PerformsTasks, Task } from '../../../../src/serenity/screenplay';
import expect = require('../../../expect');
describe('ActivityDescription', () => {
describe('Description of an Activity', () => {
class PayWithCreditCard implements Task {
static number(creditCardNumber: string) {
return new PayWithCreditCard(creditCardNumber);
}
expiringOn(date: string) {
this.expiryDate = date.split('/');
return this;
}
constructor(private cardNumber: string, private expiryDate?: string[]) {}
performAs(actor: PerformsTasks): Promise<void> {
@@ -24,46 +20,36 @@ describe('ActivityDescription', () => {
);
}
lastFourDigits() {
return this.cardNumber.slice(-4);
}
}
describe('generates a step from an activity so that the template of its name', () => {
toString = () => describe_as('#actor pays with a credit card ending #lastFourDigits', this);
it('uses public methods defined on the activity', () => {
const step = new ActivityDescription('Pays with a credit card ending "#lastFourDigits"')
.interpolateWith(PayWithCreditCard.number('4111 1111 1111 1234').expiringOn('2020/12'), []);
expect(step.name).to.equal('Pays with a credit card ending "1234"');
});
it('uses member fields defined on the activity', () => {
const step = new ActivityDescription('Pays with a credit number "#cardNumber"')
.interpolateWith(PayWithCreditCard.number('4111 1111 1111 1234').expiringOn('2020/12'), []);
expect(step.name).to.equal('Pays with a credit number "4111 1111 1111 1234"');
});
private lastFourDigits = () => this.cardNumber.slice(-4);
}
it('uses lists of strings', () => {
const step = new ActivityDescription('Pays with a credit card expiring on "#expiryDate"')
.interpolateWith(PayWithCreditCard.number('4111 1111 1111 1234').expiringOn('2020/12'), []);
given(
[ describe_as('name: {0}', 'John'), 'name: John' ],
[ describe_as('{1}, {0}; age: {2}', 'John', 'Smith', 47), 'Smith, John; age: 47' ],
[ describe_as('{0} selects a product', Actor.named('Emma')), 'Emma selects a product' ],
[ describe_as('#actor selects a product', Actor.named('Emma')), 'Emma selects a product' ],
[ describe_as('#first #last', { first: 'Jan', last: 'Molak' }), 'Jan Molak' ],
[ describe_as('#first #last', { last: 'Bond' }), '#first Bond' ],
[ describe_as('products: #list', { list: ['apples', 'oranges'] }), 'products: apples, oranges' ],
).
it ('can be parametrised', (result, expected) => expect(result).to.equal(expected));
expect(step.name).to.equal('Pays with a credit card expiring on "2020, 12"');
});
it ('can include the properties of an Activity', () => {
it('uses arguments passed to the performAs method', () => {
const step = new ActivityDescription('{0} pays with a credit card')
.interpolateWith(PayWithCreditCard.number('4111 1111 1111 1234').expiringOn('2020/12'), [Actor.named('James')]);
const activity = PayWithCreditCard.number('4111 1111 1111 1234');
expect(step.name).to.equal('James pays with a credit card');
});
expect(activity.toString()).to.equal('#actor pays with a credit card ending 1234');
});
it('ignores the fields that are not defined', () => {
const step = new ActivityDescription('Pays with a credit card with PIN number #pinNumber')
.interpolateWith(PayWithCreditCard.number('4111 1111 1111 1234'), []);
it ('interpolates field tokens before taking argument tokens into consideration', () => {
const activity = {
products: () => ['apples', 'oranges'],
toString: () => describe_as('{0} adds #products to the basket', activity),
};
expect(step.name).to.equal('Pays with a credit card with PIN number #pinNumber');
});
expect(activity.toString()).to.equal('{0} adds apples, oranges to the basket');
});
});
@@ -1,42 +1,30 @@
import { RecordedActivity } from '../domain/model';
import { Activity } from '../screenplay/activities';
import { Actor } from '../screenplay/actor';
// todo: make this a function akin to string.format... describeAs('... #field', obj)
export class ActivityDescription {
private arg_token = /{(\d+)}/g;
private field_token = /#(\w+)/g;
export function describe_as(template: string, ...parameters: any[]): string {
constructor(private template: string) { }
const
argument_tokens = /{(\d+)}/g,
field_tokens = /#(\w+)/g,
actor_token = '#actor',
actor_name = parameters[0] instanceof Actor ? parameters[0] : actor_token;
// todo: remove
interpolateWith(activity: Activity, argumentsOfThePerformAsMethod: any[]): RecordedActivity {
const name = this.determineName(this.template, activity, argumentsOfThePerformAsMethod);
const interpolated = field_tokens.test(template)
? template.replace(field_tokens, using(parameters[0]))
: template.replace(argument_tokens, using(parameters));
return new RecordedActivity(name);
}
interpolatedWithArguments = (...args: Object[]) => this.template.replace(this.arg_token, this.using(args));
interpolatedWithFieldsOf = (source: Object) => this.template.replace(this.field_token, this.using(source));
toString = () => this.template;
return interpolated.replace(actor_token, actor_name);
}
private using(source: any) {
return (token: string, field: string|number) => {
switch ({}.toString.call(source[field])) {
case '[object Function]': return source[field]();
case '[object Array]': return source[field].join(', ');
case '[object Object]': return source[field].toString();
case '[object Undefined]': return token;
default: return source[field];
}
};
}
function using(source: any) {
return (token: string, field: string|number) => stringify(token, source[field]);
}
// todo: remove
private determineName(template: string, activity: Activity, args: any[]): string {
return template.
replace(this.field_token, this.using(activity)).
replace(this.arg_token, this.using(args));
function stringify(token: string, value: any): string {
switch ({}.toString.call(value)) {
case '[object Function]': return stringify(token, value());
case '[object Array]': return value.map(item => stringify(token, item)).join(', ');
case '[object Object]': return value.toString();
case '[object Undefined]': return token;
default: return value;
}
}
@@ -1 +1,2 @@
export * from './activity_description';
export * from './step_annotation';
@@ -1,5 +1,5 @@
import { Activity } from '../screenplay';
import { ActivityDescription } from './activity_description';
import { describe_as } from './activity_description';
export class Step {
@@ -9,7 +9,7 @@ export class Step {
if (this.usesOldDescriptionStyle(target)) {
target.toString = function() {
return new ActivityDescription(template).interpolatedWithFieldsOf(this);
return describe_as(template, this);
};
}
@@ -1,6 +1,6 @@
import { serenity } from '../..';
import { ActivityFinished, ActivityStarts, Outcome, RecordedActivity, Result, SourceLocation } from '../domain';
import { ActivityDescription } from '../recording/activity_description';
import { describe_as } from '../recording/activity_description';
import { StageManager } from '../stage/stage_manager';
import { Activity } from './activities';
@@ -101,7 +101,7 @@ class TrackedActivity implements Activity {
private location: Promise<SourceLocation>;
performAs(actor: PerformsTasks | UsesAbilities | AnswersQuestions): PromiseLike<void> {
const description = new ActivityDescription(this.activity.toString()).interpolatedWithArguments(actor);
const description = describe_as(this.activity.toString(), actor);
return this.location.then(location => {

0 comments on commit a1da923

Please sign in to comment.