@cepharum/contextual-gherkin 3.0.0-beta.5 → 3.0.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -751,7 +751,7 @@ declare module "@cepharum/contextual-gherkin" {
751
751
  * @param value value to expect in a matching element's attribute
752
752
  * @returns another context with all those elements of current one which have a matching value in named attribute
753
753
  */
754
- withAttribute( name: string, value: string|RegExp ): Context;
754
+ withAttribute( name: string, value: string|RegExp|boolean ): Context;
755
755
 
756
756
  /**
757
757
  * Limits set of matching elements to those with a named DOM property
@@ -768,7 +768,7 @@ declare module "@cepharum/contextual-gherkin" {
768
768
  * @param value value to expect in a matching element's DOM property, use callback invoked with found value to return truthy on a match
769
769
  * @returns another context with all those elements of current one which have a matching value in named DOM property
770
770
  */
771
- withProperty( name: string, value: string|RegExp|SelectorFn ): Context;
771
+ withProperty( name: string, value: string|RegExp|boolean|SelectorFn ): Context;
772
772
 
773
773
  /**
774
774
  * Limits set of matching elements to those with a named DOM property
@@ -1000,4 +1000,35 @@ declare module "@cepharum/contextual-gherkin" {
1000
1000
  type PlaywrightBrowserContextOptions = BrowserContextOptions;
1001
1001
  type PlaywrightBrowser = Browser;
1002
1002
  type PlaywrightLocator = Locator;
1003
+
1004
+ type Action = "click" | "hover" | "enter";
1005
+
1006
+ async function globalFind( cardinal: NumberAndWord ): Promise<void>;
1007
+ async function globalFindByAttribute( cardinal: NumberAndWord, name: string, value: string|RegExp ): Promise<void>;
1008
+ async function globalFindByProperty( cardinal: NumberAndWord, name: string, value: string|RegExp ): Promise<void>;
1009
+ async function globalFindByClass( cardinal: NumberAndWord, className: string, negated: boolean ): Promise<void>;
1010
+ async function globalFindByLabel( cardinal: NumberAndWord, label: string, partially: boolean ): Promise<void>;
1011
+ async function globalFindByTextContent( cardinal: NumberAndWord, text: string, partially: boolean ): Promise<void>;
1012
+
1013
+ async function globalActionByAttribute( cardinal: NumberAndWord, name: string, value: string|RegExp, action: Action, input?: string ): Promise<void>;
1014
+ async function globalActionByProperty( cardinal: NumberAndWord, name: string, value: string|RegExp, action: Action, input?: string ): Promise<void>;
1015
+ async function globalActionByClass( cardinal: NumberAndWord, className: string, negated?: boolean, action: Action, input?: string ): Promise<void>;
1016
+ async function globalActionByLabel( cardinal: NumberAndWord, label: string, partially: boolean, action: Action, input?: string ): Promise<void>;
1017
+ async function globalActionByTextContent( cardinal: NumberAndWord, text: string, partially: boolean, action: Action, input?: string ): Promise<void>;
1018
+
1019
+ async function localFind( phrase: ContextualWord, cardinal: NumberAndWord ): Promise<void>;
1020
+ async function localFindByAttribute( phrase: ContextualWord, cardinal: NumberAndWord, name: string, value: string|RegExp ): Promise<void>;
1021
+ async function localFindByProperty( phrase: ContextualWord, cardinal: NumberAndWord, name: string, value: string|RegExp ): Promise<void>;
1022
+ async function localFindByClass( phrase: ContextualWord, cardinal: NumberAndWord, className: string, negated: boolean ): Promise<void>;
1023
+ async function localFindByLabel( phrase: ContextualWord, cardinal: NumberAndWord, label: string, partially: boolean ): Promise<void>;
1024
+ async function localFindByTextContent( phrase: ContextualWord, cardinal: NumberAndWord, text: string, partially: boolean ): Promise<void>;
1025
+
1026
+ async function localAction( phrase: ContextualWord, action: Action, input?: string ): Promise<void>;
1027
+
1028
+ async function localStateByAttribute( phrase: ContextualWord, all: boolean, name: string, value: string|RegExp ): Promise<void>;
1029
+ async function localStateByProperty( phrase: ContextualWord, all: boolean, name: string, value: string|RegExp ): Promise<void>;
1030
+ async function localStateByClass( phrase: ContextualWord, all: boolean, className: string, negated: boolean ): Promise<void>;
1031
+ async function localStateByLabel( phrase: ContextualWord, label: string, partially: boolean ): Promise<void>;
1032
+ async function localStateByTextContent( phrase: ContextualWord, text: string, partially: boolean ): Promise<void>;
1033
+ async function localStateExists( phrase: ContextualWord, mustExist: boolean ): Promise<void>;
1003
1034
  }
package/index.js CHANGED
@@ -13,6 +13,8 @@ export * from "./lib/adapter/playwright.js";
13
13
  export * from "./lib/context/abstract.js";
14
14
  export * from "./lib/context/playwright.js";
15
15
 
16
+ export * from "./lib/step-helper.js";
17
+
16
18
  /**
17
19
  * Configures test controller to support phrases of contextual Gherkin.
18
20
  *
package/lib/api.js CHANGED
@@ -157,11 +157,12 @@ export class Api {
157
157
  * view in case you need a view with a dedicated session.
158
158
  *
159
159
  * @param {string} name name of view to provide
160
+ * @param {boolean} sharedSession if true, the optionally created view shares a session with current one, otherwise it runs in a different session
160
161
  */
161
- async getViewByName( name = "default" ) {
162
+ async getViewByName( name = "default", sharedSession = true ) {
162
163
  const view = this.#views.get( name );
163
164
 
164
- return view == null ? await this.createView( name ) : view;
165
+ return view == null ? await this.createView( name, sharedSession ) : view;
165
166
  }
166
167
 
167
168
  /**
@@ -173,12 +174,13 @@ export class Api {
173
174
  * view in case you need a view with a dedicated session.
174
175
  *
175
176
  * @param {string} name name of view to provide
177
+ * @param {boolean} sharedSession if true, the optionally created view shares a session with current one, otherwise it runs in a different session
176
178
  */
177
- async selectViewByName( name = "default" ) {
179
+ async selectViewByName( name = "default", sharedSession = true ) {
178
180
  const view = this.#views.get( name );
179
181
 
180
182
  if ( view == null ) {
181
- return await this.createView( name, true, true );
183
+ return await this.createView( name, sharedSession, true );
182
184
  }
183
185
 
184
186
  this.#currentViewName = name;
@@ -0,0 +1,579 @@
1
+ import { Api } from "./api.js";
2
+ import { stringMessageOperator as sOp } from "../lib/helper.js";
3
+
4
+ /* eslint-disable spaced-comment */
5
+
6
+ //#region common helpers
7
+
8
+ export const actAndTrack = async( cardinal, matches, action, input ) => {
9
+ const api = Api.access();
10
+ const target = matches.find( "$" + action, false );
11
+
12
+ await api.expect( target.locator, `none or multiple matching $${action} target for selected ${cardinal.word} to ${input == null ? action : `enter '${input}' into`}` )
13
+ .toHaveCount( 1 );
14
+
15
+ // detach matches first as interacting with them might change them in a way
16
+ // so that the original query isn't finding them anymore (e.g. changing
17
+ // label of a button on click)
18
+ const detached = await matches.detach( cardinal.cardinality );
19
+
20
+ if ( input == null ) {
21
+ await target.actions[action]();
22
+ } else {
23
+ await target.actions.fill( input );
24
+ }
25
+
26
+ await detached.track( cardinal.word, cardinal.cardinality );
27
+
28
+ const wait = Number( process.env.ACTION_WAIT ) || 100;
29
+ if ( wait > 0 ) {
30
+ await api.wait( wait );
31
+ }
32
+ };
33
+
34
+ //#endregion
35
+
36
+
37
+ //#region global querying helpers
38
+
39
+ /**
40
+ * Searches for type of elements with a certain label.
41
+ *
42
+ * @param {NumberAndWord} cardinal provides cardinal number and word
43
+ * @returns {Promise<void>} promises step passed
44
+ */
45
+ export async function globalFind( cardinal ) {
46
+ const api = Api.access();
47
+
48
+ await api
49
+ .find( cardinal.word )
50
+ .checkCardinalWord( cardinal );
51
+ }
52
+
53
+ /**
54
+ * Searches for element(s) matching a named attribute by value.
55
+ *
56
+ * @param {NumberAndWord} cardinal describes what to search
57
+ * @param {string|RegExp} name name of attribute to match
58
+ * @param {string|RegExp|boolean} value value of named attribute to match, boolean value to explicitly test for named attribute being truthy or falsy
59
+ * @return {Promise<void>} promises step passed
60
+ */
61
+ export async function globalFindByAttribute( cardinal, name, value ) {
62
+ const api = Api.access();
63
+
64
+ await api
65
+ .find( cardinal.word )
66
+ .withAttribute( name, value )
67
+ .checkCardinalWord( cardinal, true, `expected %amount with ${name} ${sOp( value )} ${value}` );
68
+ }
69
+
70
+ /**
71
+ * Searches for element(s) matching a named DOM property by value.
72
+ *
73
+ * @param {NumberAndWord} cardinal describes what to search
74
+ * @param {string} name name of DOM property to match
75
+ * @param {string|boolean} value value of named DOM property to match, or boolean value to explicitly test if property is truthy or falsy
76
+ * @return {Promise<void>} promises step passed
77
+ */
78
+ export async function globalFindByProperty( cardinal, name, value ) {
79
+ const api = Api.access();
80
+
81
+ let text;
82
+
83
+ if ( typeof value === "boolean" ) {
84
+ text = `${value ? "not " : ""}${name}`;
85
+ } else {
86
+ text = `with ${name} ${sOp( value )} ${value}`;
87
+ }
88
+
89
+ await api
90
+ .find( cardinal.word )
91
+ .withProperty( name, value )
92
+ .checkCardinalWord( cardinal, true, `expected %amount ${text}` );
93
+ }
94
+
95
+ /**
96
+ * Searches for element(s) with a named class set.
97
+ *
98
+ * @param {NumberAndWord} cardinal describes what to search
99
+ * @param {string} className name of class to test
100
+ * @param {boolean} negated inverts the assertion by means of requiring one or all elements of context not to have selected class
101
+ * @return {Promise<void>} promises step passed
102
+ */
103
+ export async function globalFindByClass( cardinal, className, negated ) {
104
+ const api = Api.access();
105
+ const matches = api
106
+ .find( cardinal.word )
107
+ .withListProperty( "classList", className, negated );
108
+
109
+ await matches.checkCardinalWord( cardinal, true, `expected %amount ${negated ? "not " : ""}marked as "${className}"` );
110
+ }
111
+
112
+ /**
113
+ * Searches for type of elements with a certain label.
114
+ *
115
+ * @param {NumberAndWord} cardinal provides cardinal number and word
116
+ * @param {string} label provides required label of matching element(s)
117
+ * @param {boolean} partially if true, provided text does not have to match whole content of matching subs
118
+ * @returns {Promise<void>} promises step passed
119
+ */
120
+ export async function globalFindByLabel( cardinal, label, partially ) {
121
+ const api = Api.access();
122
+
123
+ await api
124
+ .find( cardinal.word )
125
+ .filterBySubsWithText( "$label", "label", label, partially )
126
+ .checkCardinalWord( cardinal, true, `expected %amount ${partially ? "partially " : ""}labelled with "${label}"` );
127
+ }
128
+
129
+ /**
130
+ * Searches for type of elements with a certain label.
131
+ *
132
+ * @param {NumberAndWord} cardinal provides cardinal number and word
133
+ * @param {string} text provides text to be found in eventually matching element(s)
134
+ * @param {boolean} partially if true, provided text does not have to match whole content of matching subs
135
+ * @returns {Promise<void>} promises step passed
136
+ */
137
+ export async function globalFindByTextContent( cardinal, text, partially ) {
138
+ const api = Api.access();
139
+
140
+ await api
141
+ .find( cardinal.word )
142
+ .filterBySubsWithText( "$text", false, text, partially )
143
+ .checkCardinalWord( cardinal, true, `expected %amount ${partially ? "partially " : ""}reading "${text}"` );
144
+ }
145
+
146
+ //#endregion
147
+
148
+
149
+ //#region global action helpers
150
+
151
+ /**
152
+ * Searches for element matching a named attribute by value and performs
153
+ * selected action on it.
154
+ *
155
+ * @param {NumberAndWord} cardinal describes what to search
156
+ * @param {string|RegExp} name name of attribute to match
157
+ * @param {string|RegExp} value value of named attribute to match
158
+ * @param {'click'|'hover'|'enter'} action action to perform on found element
159
+ * @param {string} [input] text to enter into selected field (instead of clicking or hovering)
160
+ * @return {Promise<void>} promises step passed
161
+ */
162
+ export async function globalActionByAttribute( cardinal, name, value, action, input ) {
163
+ const api = Api.access();
164
+
165
+ if ( cardinal.cardinality !== "singular" ) {
166
+ await api.expect( false, `selector is not addressing a single ${cardinal.word} to ${input == null ? action : `enter '${input}' into`}` )
167
+ .toEqual( true );
168
+ }
169
+
170
+ const matches = api.find( cardinal.word ).withAttribute( name, value );
171
+
172
+ await api.expect( matches.locator, `no single matching ${cardinal.word} with attribute ${name} == "${value}" to ${input == null ? action : `enter '${input}' into`}` )
173
+ .toHaveCount( 1 );
174
+
175
+ await actAndTrack( cardinal, matches, action, input );
176
+ }
177
+
178
+ /**
179
+ * Searches for element matching a named attribute by value and performs
180
+ * selected action on it.
181
+ *
182
+ * @param {NumberAndWord} cardinal describes what to search
183
+ * @param {string|RegExp} className class required for match
184
+ * @param {boolean} negated inverts the assertion by means of requiring one or all elements of context not to have selected class
185
+ * @param {'click'|'hover'|'enter'} action action to perform on found element
186
+ * @param {string} [input] text to enter into selected field (instead of clicking or hovering)
187
+ * @return {Promise<void>} promises step passed
188
+ */
189
+ export async function globalActionByClass( cardinal, className, negated, action, input ) {
190
+ const api = Api.access();
191
+
192
+ if ( cardinal.cardinality !== "singular" ) {
193
+ await api.expect( false, `selector is not addressing a single ${cardinal.word} ${negated ? "not " : ""}marked as ${className} to ${input == null ? action : `enter '${input}' into`}` )
194
+ .toEqual( true );
195
+ }
196
+
197
+ const matches = api
198
+ .find( cardinal.word )
199
+ .withListProperty( "classList", className, negated );
200
+
201
+ await api.expect( matches.locator, `no single matching ${cardinal.word} ${negated ? "not " : ""}marked as ${className} to ${input == null ? action : `enter '${input}' into`}` )
202
+ .toHaveCount( 1 );
203
+
204
+ await actAndTrack( cardinal, matches, action, input );
205
+ }
206
+
207
+ /**
208
+ * Searches for element matching a named DOM property by value and performs
209
+ * selected action on it.
210
+ *
211
+ * @param {NumberAndWord} cardinal describes what to search
212
+ * @param {string|RegExp} name name of attribute to match
213
+ * @param {string|RegExp} value value of named attribute to match
214
+ * @param {'click'|'hover'|'enter'} action action to perform on found element
215
+ * @param {string} [input] text to enter into selected field (instead of clicking or hovering)
216
+ * @return {Promise<void>} promises step passed
217
+ */
218
+ export async function globalActionByProperty( cardinal, name, value, action, input ) {
219
+ const api = Api.access();
220
+
221
+ if ( cardinal.cardinality !== "singular" ) {
222
+ await api.expect( false, `selector is not addressing a single ${cardinal.word} with property ${name} == "${value}" to ${input == null ? action : `enter '${input}' into`}` )
223
+ .toEqual( true );
224
+ }
225
+
226
+ const matches = api.find( cardinal.word ).withProperty( name, value );
227
+
228
+ await api.expect( matches.locator, `no single matching ${cardinal.word} with property ${name} == "${value}" to ${input == null ? action : `enter '${input}' into`}` )
229
+ .toHaveCount( 1 );
230
+
231
+ await actAndTrack( cardinal, matches, action, input );
232
+ }
233
+
234
+ /**
235
+ * Searches for type of element with a certain label and performs selected
236
+ * action on it.
237
+ *
238
+ * @param {NumberAndWord} cardinal provides cardinal number and word
239
+ * @param {string} label provides required label of matching element(s)
240
+ * @param {boolean} partially if true, provided text does not have to match whole content of matching subs
241
+ * @param {'click'|'hover'|'enter'} action action to perform on found element
242
+ * @param {string} [input] text to enter into selected field (instead of clicking or hovering)
243
+ * @returns {Promise<void>} promises step passed
244
+ */
245
+ export async function globalActionByLabel( cardinal, label, partially, action, input ) {
246
+ const api = Api.access();
247
+
248
+ if ( cardinal.cardinality !== "singular" ) {
249
+ await api.expect( false, `selector is not addressing a single ${cardinal.word} with ${partially ? "partial " : ""}label "${label}" to ${input == null ? action : `enter '${input}' into`}` )
250
+ .toEqual( true );
251
+ }
252
+
253
+ const matches = api.find( cardinal.word ).filterBySubsWithText( "$label", "label", label, partially );
254
+
255
+ await api.expect( matches.locator, `no single matching ${cardinal.word} with ${partially ? "partial " : ""}label "${label}" to ${input == null ? action : `enter '${input}' into`}` )
256
+ .toHaveCount( 1 );
257
+
258
+ await actAndTrack( cardinal, matches, action, input );
259
+ }
260
+
261
+ /**
262
+ * Searches for type of element with given text content and performs selected
263
+ * action on it.
264
+ *
265
+ * @param {NumberAndWord} cardinal provides cardinal number and word
266
+ * @param {string} text provides text to be found in eventually matching element(s)
267
+ * @param {boolean} partially if true, provided text does not have to match whole content of matching subs
268
+ * @param {'click'|'hover'|'enter'} action action to perform on found element
269
+ * @param {string} [input] text to enter into selected field (instead of clicking or hovering)
270
+ * @returns {Promise<void>} promises step passed
271
+ */
272
+ export async function globalActionByTextContent( cardinal, text, partially, action, input ) {
273
+ const api = Api.access();
274
+
275
+ if ( cardinal.cardinality !== "singular" ) {
276
+ await api.expect( false, `selector is not addressing a single ${cardinal.word} ${partially ? "partially " : ""}reading "${text}" to ${input == null ? action : `enter '${input}' into`}` )
277
+ .toEqual( true );
278
+ }
279
+
280
+ const matches = api.find( cardinal.word ).filterBySubsWithText( "$text", false, text, partially );
281
+
282
+ await api.expect( matches.locator, `no single matching ${cardinal.word} ${partially ? "partially " : ""}reading "${text}" to ${input == null ? action : `enter '${input}' into`}` )
283
+ .toHaveCount( 1 );
284
+
285
+ await actAndTrack( cardinal, matches, action, input );
286
+ }
287
+
288
+ //#endregion
289
+
290
+
291
+ //#region local querying helpers
292
+
293
+ /**
294
+ * Finds elements in context of containing elements.
295
+ *
296
+ * @param {ContextualWord} phrase describes context of search
297
+ * @param {NumberAndWord} cardinal describes what to search
298
+ * @return {Promise<void>} promises step passed
299
+ */
300
+ export async function localFind( phrase, cardinal ) {
301
+ const api = Api.access();
302
+ const context = api.getContextFor( phrase );
303
+
304
+ await context
305
+ .find( cardinal.word )
306
+ .checkCardinalWord( cardinal );
307
+ }
308
+
309
+ /**
310
+ * Finds elements in context of containing elements matching a named attribute
311
+ * by value.
312
+ *
313
+ * @param {ContextualWord} phrase describes context of search
314
+ * @param {NumberAndWord} cardinal describes what to search
315
+ * @param {string|RegExp} name name of attribute to match
316
+ * @param {string|RegExp} value value of named attribute to match
317
+ * @return {Promise<void>} promises step passed
318
+ */
319
+ export async function localFindByAttribute( phrase, cardinal, name, value ) {
320
+ const api = Api.access();
321
+ const context = api.getContextFor( phrase );
322
+
323
+ await context
324
+ .find( cardinal.word )
325
+ .withAttribute( name, value )
326
+ .checkCardinalWord( cardinal, true, `expected %amount with ${name} ${sOp( value )} ${value}` );
327
+ }
328
+
329
+ /**
330
+ * Finds elements in context of containing elements matching a named DOM
331
+ * property by value.
332
+ *
333
+ * @param {ContextualWord} phrase describes context of search
334
+ * @param {NumberAndWord} cardinal describes what to search
335
+ * @param {string} name name of DOM property to match
336
+ * @param {string|boolean} value value of named DOM property to match, or boolean to explicitly test named property for being truthy or falsy
337
+ * @return {Promise<void>} promises step passed
338
+ */
339
+ export async function localFindByProperty( phrase, cardinal, name, value ) {
340
+ const api = Api.access();
341
+ const context = api.getContextFor( phrase );
342
+ let text;
343
+
344
+ if ( typeof value === "boolean" ) {
345
+ text = `${value ? "" : "not "}${name}`;
346
+ } else {
347
+ text = `with ${name} ${value instanceof RegExp ? "matching" : "equals"} ${value}`;
348
+ }
349
+
350
+ await context
351
+ .find( cardinal.word )
352
+ .withProperty( name, value )
353
+ .checkCardinalWord( cardinal, true, `expected %amount ${text}` );
354
+ }
355
+
356
+ /**
357
+ * Finds elements in context of containing elements with named class.
358
+ *
359
+ * @param {ContextualWord} phrase describes context of search
360
+ * @param {NumberAndWord} cardinal describes what to search
361
+ * @param {string} className name of class to check
362
+ * @param {boolean} negated inverts the assertion by means of requiring one or all elements of context not to have selected class
363
+ * @return {Promise<void>} promises step passed
364
+ */
365
+ export async function localFindByClass( phrase, cardinal, className, negated ) {
366
+ const api = Api.access();
367
+ const context = api.getContextFor( phrase );
368
+ const matches = context.find( cardinal.word );
369
+
370
+ await matches
371
+ .withListProperty( "classList", className, negated )
372
+ .checkCardinalWord( cardinal, true, `expected %amount ${negated ? "not " : ""}marked as "${className}"` );
373
+ }
374
+
375
+ /**
376
+ * Finds elements in context of containing elements with a given label.
377
+ *
378
+ * @param {ContextualWord} phrase describes context of search
379
+ * @param {NumberAndWord} cardinal describes what to search
380
+ * @param {string} label expected label of item(s) to find
381
+ * @param {boolean} partially if true, provided text does not have to match whole content of matching subs
382
+ * @return {Promise<void>} promises step passed
383
+ */
384
+ export async function localFindByLabel( phrase, cardinal, label, partially ) {
385
+ const api = Api.access();
386
+ const context = api.getContextFor( phrase );
387
+
388
+ await context
389
+ .find( cardinal.word )
390
+ .filterBySubsWithText( "$label", "label", label, partially )
391
+ .checkCardinalWord( cardinal, true, `expected %amount ${partially ? "partially " : ""}labelled with "${label}"` );
392
+ }
393
+
394
+ /**
395
+ * Finds elements in context of containing elements containing given text.
396
+ *
397
+ * @param {ContextualWord} phrase describes context of search
398
+ * @param {NumberAndWord} cardinal describes what to search
399
+ * @param {string} text provides expected text content of item(s) to find
400
+ * @param {boolean} partially if true, provided text does not have to match whole content of matching subs
401
+ * @return {Promise<void>} promises step passed
402
+ */
403
+ export async function localFindByTextContent( phrase, cardinal, text, partially ) {
404
+ const api = Api.access();
405
+ const context = api.getContextFor( phrase );
406
+
407
+ await context
408
+ .find( cardinal.word )
409
+ .filterBySubsWithText( "$text", false, text, partially )
410
+ .checkCardinalWord( cardinal, true, `expected %amount ${partially ? "partially " : ""}reading "${text}"` );
411
+ }
412
+
413
+ //#endregion
414
+
415
+
416
+ //#region local action helpers
417
+
418
+ /**
419
+ * Searches for type of element in current context and performs selected action
420
+ * on it.
421
+ *
422
+ * @param {ContextualWord} phrase description of element to be clicked.
423
+ * @param {'click'|'hover'|'enter'} action action to perform on found element
424
+ * @param {string} [input] text to enter into selected field (instead of clicking or hovering)
425
+ * @returns {Promise<void>} promises element clicked
426
+ */
427
+ export async function localAction( phrase, action, input ) {
428
+ const api = Api.access();
429
+ const context = api.getContextFor( phrase );
430
+
431
+ await api.expect( context.locator, `no single matching ${phrase.word} to ${input == null ? action : `enter '${input}' into`}` )
432
+ .toHaveCount( 1 );
433
+
434
+ const target = context.find( "$" + action, false );
435
+
436
+ await api.expect( target.locator, `no single matching $${action} target for selected ${phrase.word} to ${input == null ? action : `enter '${input}' into`}` )
437
+ .toHaveCount( 1 );
438
+
439
+ // detach matches first as interacting with them might change them in a way
440
+ // so that the original query isn't finding them anymore (e.g. changing
441
+ // label of a button on click)
442
+ const detached = await context.detach( phrase.targetCardinality );
443
+
444
+ if ( input == null ) {
445
+ await target.actions[action]();
446
+ } else {
447
+ await target.actions.fill( input );
448
+ }
449
+
450
+ if ( phrase.isRefining ) {
451
+ await detached.track( phrase.word, phrase.targetCardinality );
452
+ }
453
+
454
+ const wait = Number( process.env.ACTION_WAIT ) || 100;
455
+ if ( wait > 0 ) {
456
+ await api.wait( wait );
457
+ }
458
+ }
459
+
460
+ //#endregion
461
+
462
+
463
+ //#region local assertion helpers
464
+
465
+ /**
466
+ * Checks if some or all elements of an addressed match have a named attribute
467
+ * matching some expected value.
468
+ *
469
+ * @param {ContextualWord} phrase description of previous match to check
470
+ * @param {boolean} all if true, all matching elements have to match named attribute's expected value
471
+ * @param {string} name name of attribute to check with either matching element
472
+ * @param {string} value expected value of named attribute per matching element
473
+ * @returns {Promise<void>} promises check passed
474
+ */
475
+ export async function localStateByAttribute( phrase, all, name, value ) {
476
+ const api = Api.access();
477
+ const context = api.getContextFor( phrase );
478
+ const filtered = context.withAttribute( name, value );
479
+
480
+ await context.checkFilteredInContext( filtered, phrase, all );
481
+ }
482
+
483
+ /**
484
+ * Checks if all elements of an addressed match have a named property matching
485
+ * some expected value.
486
+ *
487
+ * @param {ContextualWord} phrase description of previous match to check
488
+ * @param {boolean} all if true, all matching elements have to match named attribute's expected value
489
+ * @param {string} name name of DOM property to check on either matching element
490
+ * @param {string|RegExp|boolean} value expected value of named property per matching element
491
+ * @returns {Promise<void>} promises check passed
492
+ */
493
+ export async function localStateByProperty( phrase, all, name, value ) {
494
+ const api = Api.access();
495
+ const context = api.getContextFor( phrase );
496
+ const filtered = context.withProperty( name, value );
497
+
498
+ await context.checkFilteredInContext( filtered, phrase, all );
499
+ }
500
+
501
+ /**
502
+ * Asserts if selected element(s) has/have given class or not.
503
+ *
504
+ * @param {ContextualWord} phrase describes context of search
505
+ * @param {boolean} all if true, all matching elements have to match named attribute's expected value
506
+ * @param {string} className name of class to check
507
+ * @param {boolean} negated inverts the assertion by means of requiring one or all elements of context not to have selected class
508
+ * @return {Promise<void>} promises step passed
509
+ */
510
+ export async function localStateByClass( phrase, all, className, negated ) {
511
+ const api = Api.access();
512
+ const context = api.getContextFor( phrase );
513
+ const filtered = context.withListProperty( "classList", className, negated );
514
+
515
+ await context.checkFilteredInContext( filtered, phrase, all );
516
+ }
517
+
518
+ /**
519
+ * Checks if an addressed match of a previously tracked search contains a given
520
+ * text.
521
+ *
522
+ * @param {ContextualWord} phrase description of previous match to check
523
+ * @param {string} text expected text found in elements of selected match
524
+ * @param {boolean} partially if true, provided text does not have to match whole content of matching subs
525
+ * @returns {Promise<void>} promises check passed
526
+ */
527
+ export async function localStateByLabel( phrase, text, partially ) {
528
+ const api = Api.access();
529
+ const context = api.getContextFor( phrase );
530
+ const cardinal = phrase.asCardinalWord( await context.matchCount() );
531
+
532
+ await context
533
+ .filterBySubsWithText( "$label", "label", text, partially )
534
+ .checkCardinalWord( cardinal, phrase.isRefining, `expected %amount labelled with "${text}"` );
535
+ }
536
+
537
+ /**
538
+ * Checks if an addressed match of a previously tracked search contains a given
539
+ * text.
540
+ *
541
+ * @param {ContextualWord} phrase description of previous match to check
542
+ * @param {string} text expected text found in elements of selected match
543
+ * @param {boolean} partially if true searched elements may partially contain provided text to be considered a match
544
+ * @returns {Promise<void>} promises check passed
545
+ */
546
+ export async function localStateByTextContent( phrase, text, partially ) {
547
+ const api = Api.access();
548
+ const context = api.getContextFor( phrase );
549
+ const cardinal = phrase.asCardinalWord( await context.matchCount() );
550
+
551
+ await context
552
+ .filterBySubsWithText( "$text", false, text, partially )
553
+ .checkCardinalWord( cardinal, phrase.isRefining, `expected %amount ${partially ? "partially " : ""}reading "${text}"` );
554
+ }
555
+
556
+ /**
557
+ * Checks if some contextual elements exist or are missing explicitly.
558
+ *
559
+ * @param {ContextualWord} phrase description of previous match to check
560
+ * @param {boolean} mustExist if true, described contextual element has to exist to pass, otherwise it has to be missing
561
+ * @returns {Promise<void>} promises check passed
562
+ */
563
+ export async function localStateExists( phrase, mustExist ) {
564
+ const api = Api.access();
565
+ const context = api.getContextFor( phrase );
566
+
567
+ if ( mustExist ) {
568
+ // FIXME might pass unintentionally if just one of several elements to be tested exists
569
+ await api.expect( context.locator ).not.toHaveCount( 0 );
570
+
571
+ if ( phrase.isRefining ) {
572
+ await context.track( phrase.word, phrase.targetCardinality );
573
+ }
574
+ } else {
575
+ await api.expect( context.locator ).toHaveCount( 0 );
576
+ }
577
+ }
578
+
579
+ //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cepharum/contextual-gherkin",
3
- "version": "3.0.0-beta.5",
3
+ "version": "3.0.0-beta.6",
4
4
  "description": "flexible step definitions for Gherkin",
5
5
  "author": "cepharum GmbH <thomas.urban@cepharum.de>",
6
6
  "homepage": "https://cepharum-foss.gitlab.io/contextual-gherkin/",