Skip to content

Commit d7315b2

Browse files
authored
Support state objects requiring other state objects (#137)
2 parents 1dc7a1a + 01dba4e commit d7315b2

File tree

4 files changed

+373
-12
lines changed

4 files changed

+373
-12
lines changed

cucumber-tsflow-specs/features/custom-context-objects.feature

Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,350 @@ Feature: Custom context objects
5555
Then it passes
5656
And the output contains "The state is 'initial value'"
5757
And the output contains "The state is 'step value'"
58+
59+
Scenario: Custom context objects can depend on other custom context objects two levels deep
60+
Given a file named "features/a.feature" with:
61+
"""feature
62+
Feature: some feature
63+
Scenario: scenario a
64+
Given the state is "initial value"
65+
When I set the state to "step value"
66+
Then the state is "step value"
67+
"""
68+
And a file named "support/level-one-state.ts" with:
69+
"""ts
70+
import {binding} from 'cucumber-tsflow';
71+
import {LevelTwoState} from './level-two-state';
72+
73+
@binding([LevelTwoState])
74+
export class LevelOneState {
75+
constructor(public levelTwoState: LevelTwoState) {
76+
}
77+
}
78+
"""
79+
And a file named "support/level-two-state.ts" with:
80+
"""ts
81+
export class LevelTwoState {
82+
public value: string = "initial value";
83+
}
84+
"""
85+
And a file named "step_definitions/one.ts" with:
86+
"""ts
87+
import {LevelTwoState} from '../support/level-two-state';
88+
import {binding, when} from 'cucumber-tsflow';
89+
90+
@binding([LevelTwoState])
91+
class Steps {
92+
public constructor(private readonly levelTwoState: LevelTwoState) {
93+
}
94+
95+
@when("I set the state to {string}")
96+
public setState(newValue: string) {
97+
this.levelTwoState.value = newValue;
98+
}
99+
}
100+
101+
export = Steps;
102+
"""
103+
And a file named "step_definitions/two.ts" with:
104+
"""ts
105+
import {LevelOneState} from '../support/level-one-state';
106+
import {binding, then} from 'cucumber-tsflow';
107+
import * as assert from 'node:assert';
108+
109+
@binding([LevelOneState])
110+
class Steps {
111+
public constructor(private readonly levelOneState: LevelOneState) {}
112+
113+
@then("the state is {string}")
114+
public checkValue(value: string) {
115+
console.log(`The state is '${this.levelOneState.levelTwoState.value}'`);
116+
assert.equal(this.levelOneState.levelTwoState.value, value, "State value does not match");
117+
}
118+
}
119+
120+
export = Steps;
121+
"""
122+
When I run cucumber-js
123+
Then it passes
124+
And the output contains "The state is 'initial value'"
125+
And the output contains "The state is 'step value'"
126+
127+
Scenario: Custom context objects can depend on other custom context objects three levels deep
128+
Given a file named "features/a.feature" with:
129+
"""feature
130+
Feature: some feature
131+
Scenario: scenario a
132+
Given the state is "initial value"
133+
When I set the state to "step value"
134+
Then the state is "step value"
135+
"""
136+
And a file named "support/level-one-state.ts" with:
137+
"""ts
138+
import {binding} from 'cucumber-tsflow';
139+
import {LevelTwoState} from './level-two-state';
140+
141+
@binding([LevelTwoState])
142+
export class LevelOneState {
143+
constructor(public levelTwoState: LevelTwoState) {
144+
}
145+
}
146+
"""
147+
And a file named "support/level-two-state.ts" with:
148+
"""ts
149+
import {binding} from 'cucumber-tsflow';
150+
import {LevelThreeState} from './level-three-state';
151+
152+
@binding([LevelThreeState])
153+
export class LevelTwoState {
154+
constructor(public levelThreeState: LevelThreeState) {
155+
}
156+
}
157+
"""
158+
And a file named "support/level-three-state.ts" with:
159+
"""ts
160+
export class LevelThreeState {
161+
public value: string = "initial value";
162+
}
163+
"""
164+
And a file named "step_definitions/one.ts" with:
165+
"""ts
166+
import {LevelThreeState} from '../support/level-three-state';
167+
import {binding, when} from 'cucumber-tsflow';
168+
169+
@binding([LevelThreeState])
170+
class Steps {
171+
public constructor(private readonly levelThreeState: LevelThreeState) {
172+
}
173+
174+
@when("I set the state to {string}")
175+
public setState(newValue: string) {
176+
this.levelThreeState.value = newValue;
177+
}
178+
}
179+
180+
export = Steps;
181+
"""
182+
And a file named "step_definitions/two.ts" with:
183+
"""ts
184+
import {LevelOneState} from '../support/level-one-state';
185+
import {binding, then} from 'cucumber-tsflow';
186+
import * as assert from 'node:assert';
187+
188+
@binding([LevelOneState])
189+
class Steps {
190+
public constructor(private readonly levelOneState: LevelOneState) {}
191+
192+
@then("the state is {string}")
193+
public checkValue(value: string) {
194+
console.log(`The state is '${this.levelOneState.levelTwoState.levelThreeState.value}'`);
195+
assert.equal(this.levelOneState.levelTwoState.levelThreeState.value, value, "State value does not match");
196+
}
197+
}
198+
199+
export = Steps;
200+
"""
201+
When I run cucumber-js
202+
Then it passes
203+
And the output contains "The state is 'initial value'"
204+
And the output contains "The state is 'step value'"
205+
206+
Scenario: Circular dependencies are explicitly communicated to the developer
207+
Given a file named "features/a.feature" with:
208+
"""feature
209+
Feature: some feature
210+
Scenario: scenario a
211+
Given the state is "initial value"
212+
When I set the state to "step value"
213+
Then the state is "step value"
214+
"""
215+
And a file named "support/state-one.ts" with:
216+
"""ts
217+
import {binding} from 'cucumber-tsflow';
218+
import {StateTwo} from './state-two';
219+
220+
@binding([StateTwo])
221+
export class StateOne {
222+
constructor(public stateTwo: StateTwo) {
223+
}
224+
}
225+
"""
226+
And a file named "support/state-two.ts" with:
227+
"""ts
228+
import {StateOne} from './state-one';
229+
import {binding} from 'cucumber-tsflow';
230+
231+
@binding([StateOne])
232+
export class StateTwo {
233+
public value: string = "initial value";
234+
constructor(public stateOne: StateOne) {
235+
}
236+
}
237+
"""
238+
And a file named "step_definitions/one.ts" with:
239+
"""ts
240+
import {StateTwo} from '../support/state-two';
241+
import {binding, when} from 'cucumber-tsflow';
242+
243+
@binding([StateTwo])
244+
class Steps {
245+
public constructor(private readonly stateTwo: StateTwo) {
246+
}
247+
248+
@when("I set the state to {string}")
249+
public setState(newValue: string) {
250+
this.stateTwo.value = newValue;
251+
}
252+
}
253+
254+
export = Steps;
255+
"""
256+
And a file named "step_definitions/two.ts" with:
257+
"""ts
258+
import {StateOne} from '../support/state-one';
259+
import {binding, then} from 'cucumber-tsflow';
260+
import * as assert from 'node:assert';
261+
262+
@binding([StateOne])
263+
class Steps {
264+
public constructor(private readonly stateOne: StateOne) {}
265+
266+
@then("the state is {string}")
267+
public checkValue(value: string) {
268+
console.log(`The state is '${this.stateOne.stateTwo.value}'`);
269+
assert.equal(this.stateOne.stateTwo.value, value, "State value does not match");
270+
}
271+
}
272+
273+
export = Steps;
274+
"""
275+
When I run cucumber-js
276+
Then it fails
277+
And the error output contains text:
278+
"""
279+
Undefined context type at index 0 for StateOne, do you possibly have a circular dependency?
280+
"""
281+
282+
283+
Scenario: Circular dependencies within the same file are vaguely communicated to the developer
284+
Given a file named "features/a.feature" with:
285+
"""feature
286+
Feature: some feature
287+
Scenario: scenario a
288+
Given the state is "initial value"
289+
When I set the state to "step value"
290+
Then the state is "step value"
291+
"""
292+
And a file named "support/state.ts" with:
293+
"""ts
294+
import {binding} from 'cucumber-tsflow';
295+
296+
export class StateOne {
297+
constructor(public stateTwo: StateTwo) { }
298+
}
299+
300+
@binding([StateOne])
301+
export class StateTwo {
302+
public value: string = "initial value";
303+
constructor(public stateOne: StateOne) { }
304+
}
305+
306+
exports.StateOne = binding([StateTwo])(StateOne);
307+
"""
308+
And a file named "step_definitions/one.ts" with:
309+
"""ts
310+
import {StateTwo} from '../support/state';
311+
import {binding, when} from 'cucumber-tsflow';
312+
313+
@binding([StateTwo])
314+
class StepsOne {
315+
public constructor(private readonly stateTwo: StateTwo) {
316+
}
317+
318+
@when("I set the state to {string}")
319+
public setState(newValue: string) {
320+
this.stateTwo.value = newValue;
321+
}
322+
}
323+
324+
export = StepsOne;
325+
"""
326+
And a file named "step_definitions/two.ts" with:
327+
"""ts
328+
import {StateOne} from '../support/state';
329+
import {binding, then} from 'cucumber-tsflow';
330+
import * as assert from 'node:assert';
331+
332+
@binding([StateOne])
333+
class StepsTwo {
334+
public constructor(private readonly stateOne: StateOne) {}
335+
336+
@then("the state is {string}")
337+
public checkValue(value: string) {
338+
console.log(`The state is '${this.stateOne.stateTwo.value}'`);
339+
assert.equal(this.stateOne.stateTwo.value, value, "State value does not match");
340+
}
341+
}
342+
343+
export = StepsTwo;
344+
"""
345+
When I run cucumber-js
346+
Then it fails
347+
And the error output contains text:
348+
"""
349+
Undefined context type at index 0 for StepsTwo, do you possibly have a circular dependency?
350+
"""
351+
352+
Scenario: In-file circular dependencies are thrown as maximum call stack exceeded errors
353+
Given a file named "features/a.feature" with:
354+
"""feature
355+
Feature: some feature
356+
Scenario: scenario a
357+
Given the state is "initial value"
358+
When I set the state to "step value"
359+
Then the state is "step value"
360+
"""
361+
And a file named "support/circular.ts" with:
362+
"""ts
363+
import {binding} from 'cucumber-tsflow';
364+
365+
export class StateOne {
366+
constructor(public stateTwo: StateTwo) { }
367+
}
368+
369+
@binding([StateOne])
370+
export class StateTwo {
371+
public value: string = "initial value";
372+
constructor(public stateOne: StateOne) { }
373+
}
374+
375+
exports.StateOne = binding([StateTwo])(StateOne);
376+
"""
377+
And a file named "step_definitions/one.ts" with:
378+
"""ts
379+
import {StateTwo} from '../support/circular';
380+
import * as assert from 'node:assert';
381+
import {binding, when, then} from 'cucumber-tsflow';
382+
383+
@binding([StateTwo])
384+
class Steps {
385+
public constructor(private readonly stateTwo: StateTwo) {
386+
}
387+
388+
@when("I set the state to {string}")
389+
public setState(newValue: string) {
390+
this.stateTwo.value = newValue;
391+
}
392+
393+
@then("the state is {string}")
394+
public checkValue(value: string) {
395+
console.log(`The state is '${this.stateTwo.value}'`);
396+
assert.equal(this.stateTwo.value, value, "State value does not match");
397+
}
398+
}
399+
400+
export = Steps;
401+
"""
402+
When I run cucumber-js
403+
Then it fails
404+
And the output contains "RangeError: Maximum call stack size exceeded"

cucumber-tsflow/src/binding-decorator.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@ export function binding(requiredContextTypes?: ContextType[]): TypeDecorator {
6868
requiredContextTypes
6969
);
7070

71+
if (Array.isArray(requiredContextTypes)) {
72+
for (const i in requiredContextTypes) {
73+
if (typeof requiredContextTypes[i] === 'undefined') {
74+
throw new Error(`Undefined context type at index ${i} for ${target.name}, do you possibly have a circular dependency?`);
75+
}
76+
}
77+
}
78+
7179
const allBindings: StepBinding[] = [
7280
...bindingRegistry.getStepBindingsForTarget(target),
7381
...bindingRegistry.getStepBindingsForTarget(target.prototype),

0 commit comments

Comments
 (0)