@cepharum/contextual-gherkin 3.1.0 → 4.0.1
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 +33 -8
- package/lib/context/playwright.js +59 -6
- package/lib/support/playwright/custom-selectors.js +112 -0
- package/llms.txt +137 -0
- package/package.json +1 -1
package/index.d.ts
CHANGED
|
@@ -690,18 +690,43 @@ declare module "@cepharum/contextual-gherkin" {
|
|
|
690
690
|
nth( index: number|null ): AbstractContext;
|
|
691
691
|
|
|
692
692
|
/**
|
|
693
|
-
*
|
|
694
|
-
*
|
|
693
|
+
* Limits current set of matching elements to those also matching the
|
|
694
|
+
* selector configured for a given element type, intersecting the current
|
|
695
|
+
* set rather than searching descendants.
|
|
695
696
|
*
|
|
696
|
-
* @
|
|
697
|
-
*
|
|
698
|
-
*
|
|
697
|
+
* @param typeOfElement type of element to intersect current matches with
|
|
698
|
+
* @param defaultSelector selector to use if none has been configured for the named type of elements
|
|
699
|
+
* @returns another context representing only those matching elements of current context that also match selector of given type
|
|
700
|
+
*/
|
|
701
|
+
filterByType( typeOfElement: string, defaultSelector?: SimpleSelector ): AbstractContext;
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Limits current set of matching elements to those also matching a given
|
|
705
|
+
* selector, intersecting the current set rather than searching descendants.
|
|
706
|
+
*
|
|
707
|
+
* @param selector selector string to intersect current matches with
|
|
708
|
+
* @returns another context representing only those matching elements of current context that also match given selector
|
|
709
|
+
*/
|
|
710
|
+
filterBySelector( selector: string ): AbstractContext;
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Limits current set of matching elements to those containing a descendant
|
|
714
|
+
* matching the selector configured for a given element type.
|
|
699
715
|
*
|
|
700
|
-
* @param typeOfElement type of element to
|
|
716
|
+
* @param typeOfElement type of element to look for in descendants
|
|
701
717
|
* @param defaultSelector selector to use if none has been configured for the named type of elements
|
|
702
|
-
* @returns another context representing only those matching elements of current context that
|
|
718
|
+
* @returns another context representing only those matching elements of current context that contain a descendant of given type
|
|
719
|
+
*/
|
|
720
|
+
filterBySubType( typeOfElement: string, defaultSelector?: SimpleSelector ): AbstractContext;
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Limits current set of matching elements to those containing a descendant
|
|
724
|
+
* matching a given selector.
|
|
725
|
+
*
|
|
726
|
+
* @param selector selector string to look for in descendants
|
|
727
|
+
* @returns another context representing only those matching elements of current context that contain a descendant matching given selector
|
|
703
728
|
*/
|
|
704
|
-
|
|
729
|
+
filterBySubSelector( selector: string ): AbstractContext;
|
|
705
730
|
|
|
706
731
|
/**
|
|
707
732
|
* Reduces current context's set of matching elements to those marked as
|
|
@@ -171,25 +171,78 @@ export class PlaywrightContext extends AbstractContext {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
/**
|
|
174
|
-
* Limits current set of matches to those matching
|
|
175
|
-
*
|
|
174
|
+
* Limits current set of matches to those also matching a given element
|
|
175
|
+
* type's selector, intersecting the current set rather than searching
|
|
176
|
+
* descendants.
|
|
176
177
|
*
|
|
177
|
-
* @param {string} typeOfElement names type of elements to
|
|
178
|
+
* @param {string} typeOfElement names type of elements to intersect with
|
|
178
179
|
* @param {SimpleSelector} defaultSelector selector to use as fallback
|
|
179
|
-
* @returns {Context} node describing
|
|
180
|
+
* @returns {Context} node describing the filtered set of elements
|
|
180
181
|
*/
|
|
181
|
-
|
|
182
|
+
filterByType( typeOfElement, defaultSelector = undefined ) {
|
|
182
183
|
const type = this.api.toNormalizedSingularName( typeOfElement );
|
|
183
184
|
const { query, dependencies } = this.getSelector( type, defaultSelector );
|
|
184
185
|
|
|
185
186
|
return new this.constructor( this, {
|
|
186
|
-
locator: this.locator.
|
|
187
|
+
locator: this.locator.and( this.createSubLocator( query, dependencies, true ) ),
|
|
187
188
|
type: this.type,
|
|
188
189
|
selectors: this.selectors,
|
|
189
190
|
cardinality: this.cardinality
|
|
190
191
|
} );
|
|
191
192
|
}
|
|
192
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Limits current set of matches to those also matching a given selector,
|
|
196
|
+
* intersecting the current set rather than searching descendants.
|
|
197
|
+
*
|
|
198
|
+
* @param {string} selector selector string accepted by Playwright locators
|
|
199
|
+
* @returns {Context} node describing the filtered set of elements
|
|
200
|
+
*/
|
|
201
|
+
filterBySelector( selector ) {
|
|
202
|
+
return new this.constructor( this, {
|
|
203
|
+
locator: this.locator.and( this.locator.page().locator( selector ) ),
|
|
204
|
+
type: this.type,
|
|
205
|
+
selectors: this.selectors,
|
|
206
|
+
cardinality: this.cardinality,
|
|
207
|
+
} );
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Limits current set of matches to those containing a descendant matching
|
|
212
|
+
* a given element type's selector.
|
|
213
|
+
*
|
|
214
|
+
* @param {string} typeOfElement names type of elements to look for in descendants
|
|
215
|
+
* @param {SimpleSelector} defaultSelector selector to use as fallback
|
|
216
|
+
* @returns {Context} node describing the filtered set of elements
|
|
217
|
+
*/
|
|
218
|
+
filterBySubType( typeOfElement, defaultSelector = undefined ) {
|
|
219
|
+
const type = this.api.toNormalizedSingularName( typeOfElement );
|
|
220
|
+
const { query, dependencies } = this.getSelector( type, defaultSelector );
|
|
221
|
+
|
|
222
|
+
return new this.constructor( this, {
|
|
223
|
+
locator: this.locator.filter( { has: this.createSubLocator( query, dependencies ) } ),
|
|
224
|
+
type: this.type,
|
|
225
|
+
selectors: this.selectors,
|
|
226
|
+
cardinality: this.cardinality,
|
|
227
|
+
} );
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Limits current set of matches to those containing a descendant matching
|
|
232
|
+
* a given selector.
|
|
233
|
+
*
|
|
234
|
+
* @param {string} selector selector string accepted by Playwright locators
|
|
235
|
+
* @returns {Context} node describing the filtered set of elements
|
|
236
|
+
*/
|
|
237
|
+
filterBySubSelector( selector ) {
|
|
238
|
+
return new this.constructor( this, {
|
|
239
|
+
locator: this.locator.filter( { has: this.locator.page().locator( selector ) } ),
|
|
240
|
+
type: this.type,
|
|
241
|
+
selectors: this.selectors,
|
|
242
|
+
cardinality: this.cardinality,
|
|
243
|
+
} );
|
|
244
|
+
}
|
|
245
|
+
|
|
193
246
|
/**
|
|
194
247
|
* Limits current set of matches to those picked by a given callback
|
|
195
248
|
* function invoked in browser.
|
|
@@ -10,6 +10,8 @@ export async function registerPlaywrightSelectors() {
|
|
|
10
10
|
registerAttributeSelector(),
|
|
11
11
|
registerShadowQuerySelector(),
|
|
12
12
|
registerMeSelector(),
|
|
13
|
+
registerWidthSelector(),
|
|
14
|
+
registerHeightSelector(),
|
|
13
15
|
] );
|
|
14
16
|
}
|
|
15
17
|
|
|
@@ -141,6 +143,116 @@ export function registerShadowQuerySelector() {
|
|
|
141
143
|
} ) );
|
|
142
144
|
}
|
|
143
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Registers custom selector engine `cg-width` for locating elements whose
|
|
148
|
+
* rendered width satisfies a numeric constraint.
|
|
149
|
+
*
|
|
150
|
+
* Query syntax: <value> exact match (px)
|
|
151
|
+
* >=<value> minimum width (px)
|
|
152
|
+
* <=<value> maximum width (px)
|
|
153
|
+
*
|
|
154
|
+
* When the engine root is an Element it is tested directly. When the root is
|
|
155
|
+
* a Document or ShadowRoot all descendant elements are searched instead.
|
|
156
|
+
*/
|
|
157
|
+
export function registerWidthSelector() {
|
|
158
|
+
return selectors.register( "cg-width", () => ( {
|
|
159
|
+
// client function, runs in browser, can't reliably use shared code other than what is injected as init script
|
|
160
|
+
query( root, query ) {
|
|
161
|
+
const [ , op, value ] = /^(>=|<=)?(\d+(?:\.\d+)?)$/.exec( query ) || [];
|
|
162
|
+
if ( value == null ) {
|
|
163
|
+
throw new TypeError( "malformed query for matching elements by width" );
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const target = parseFloat( value );
|
|
167
|
+
const test = node => {
|
|
168
|
+
const actual = node.getBoundingClientRect().width;
|
|
169
|
+
return op === ">=" ? actual >= target : op === "<=" ? actual <= target : actual === target;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
if ( typeof root.getBoundingClientRect === "function" ) {
|
|
173
|
+
return test( root ) ? root : undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return Array.from( root.querySelectorAll( "*" ) ).find( test );
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
// client function, runs in browser, can't reliably use shared code other than what is injected as init script
|
|
180
|
+
queryAll( root, query ) {
|
|
181
|
+
const [ , op, value ] = /^(>=|<=)?(\d+(?:\.\d+)?)$/.exec( query ) || [];
|
|
182
|
+
if ( value == null ) {
|
|
183
|
+
throw new TypeError( "malformed query for matching elements by width" );
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const target = parseFloat( value );
|
|
187
|
+
const test = node => {
|
|
188
|
+
const actual = node.getBoundingClientRect().width;
|
|
189
|
+
return op === ">=" ? actual >= target : op === "<=" ? actual <= target : actual === target;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
if ( typeof root.getBoundingClientRect === "function" ) {
|
|
193
|
+
return test( root ) ? [root] : [];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return Array.from( root.querySelectorAll( "*" ) ).filter( test );
|
|
197
|
+
},
|
|
198
|
+
} ) );
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Registers custom selector engine `cg-height` for locating elements whose
|
|
203
|
+
* rendered height satisfies a numeric constraint.
|
|
204
|
+
*
|
|
205
|
+
* Query syntax: <value> exact match (px)
|
|
206
|
+
* >=<value> minimum height (px)
|
|
207
|
+
* <=<value> maximum height (px)
|
|
208
|
+
*
|
|
209
|
+
* When the engine root is an Element it is tested directly. When the root is
|
|
210
|
+
* a Document or ShadowRoot all descendant elements are searched instead.
|
|
211
|
+
*/
|
|
212
|
+
export function registerHeightSelector() {
|
|
213
|
+
return selectors.register( "cg-height", () => ( {
|
|
214
|
+
// client function, runs in browser, can't reliably use shared code other than what is injected as init script
|
|
215
|
+
query( root, query ) {
|
|
216
|
+
const [ , op, value ] = /^(>=|<=)?(\d+(?:\.\d+)?)$/.exec( query ) || [];
|
|
217
|
+
if ( value == null ) {
|
|
218
|
+
throw new TypeError( "malformed query for matching elements by height" );
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const target = parseFloat( value );
|
|
222
|
+
const test = node => {
|
|
223
|
+
const actual = node.getBoundingClientRect().height;
|
|
224
|
+
return op === ">=" ? actual >= target : op === "<=" ? actual <= target : actual === target;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
if ( typeof root.getBoundingClientRect === "function" ) {
|
|
228
|
+
return test( root ) ? root : undefined;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return Array.from( root.querySelectorAll( "*" ) ).find( test );
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
// client function, runs in browser, can't reliably use shared code other than what is injected as init script
|
|
235
|
+
queryAll( root, query ) {
|
|
236
|
+
const [ , op, value ] = /^(>=|<=)?(\d+(?:\.\d+)?)$/.exec( query ) || [];
|
|
237
|
+
if ( value == null ) {
|
|
238
|
+
throw new TypeError( "malformed query for matching elements by height" );
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const target = parseFloat( value );
|
|
242
|
+
const test = node => {
|
|
243
|
+
const actual = node.getBoundingClientRect().height;
|
|
244
|
+
return op === ">=" ? actual >= target : op === "<=" ? actual <= target : actual === target;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
if ( typeof root.getBoundingClientRect === "function" ) {
|
|
248
|
+
return test( root ) ? [root] : [];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return Array.from( root.querySelectorAll( "*" ) ).filter( test );
|
|
252
|
+
},
|
|
253
|
+
} ) );
|
|
254
|
+
}
|
|
255
|
+
|
|
144
256
|
/**
|
|
145
257
|
* Registers custom selector for locating current root element itself primarily
|
|
146
258
|
* to feature its combination with other locators.
|
package/llms.txt
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# @cepharum/contextual-gherkin
|
|
2
|
+
|
|
3
|
+
> Flexible, context-aware Gherkin step definitions for Playwright-based E2E tests. Requires v3+.
|
|
4
|
+
> Full documentation: https://cepharum-foss.gitlab.io/contextual-gherkin/
|
|
5
|
+
|
|
6
|
+
## What it does
|
|
7
|
+
|
|
8
|
+
contextual-gherkin provides a large library of ready-made Cucumber step definitions and a development API for writing custom ones. Its key feature is *contextuality*: a step that finds elements can track them so a follow-up step can act on them without repeating the selector logic.
|
|
9
|
+
|
|
10
|
+
## Setup (one-time per project)
|
|
11
|
+
|
|
12
|
+
```javascript
|
|
13
|
+
// steps/setup.js
|
|
14
|
+
import { BeforeAll, Before } from "@cucumber/cucumber";
|
|
15
|
+
import { ContextualGherkin, use } from "@cepharum/contextual-gherkin";
|
|
16
|
+
|
|
17
|
+
let adapter;
|
|
18
|
+
|
|
19
|
+
BeforeAll( async () => {
|
|
20
|
+
adapter = await use.playwright(); // launches browser once
|
|
21
|
+
} );
|
|
22
|
+
|
|
23
|
+
Before( () => ContextualGherkin( {
|
|
24
|
+
selectors: {
|
|
25
|
+
button: "button",
|
|
26
|
+
panel: { "": ".panel", button: ".panel__btn" },
|
|
27
|
+
},
|
|
28
|
+
aliases: { btn: "button" },
|
|
29
|
+
}, adapter ) );
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Use `Before({ tags: "@tag" })` to provide different configurations per Cucumber tag.
|
|
33
|
+
|
|
34
|
+
## Selectors configuration
|
|
35
|
+
|
|
36
|
+
### Regular selectors
|
|
37
|
+
Map singular element names to CSS selectors (or Playwright extended selectors):
|
|
38
|
+
```javascript
|
|
39
|
+
button: "button"
|
|
40
|
+
panel: "#main > .panel"
|
|
41
|
+
item: "xpath=//li[@data-active]"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Selector hierarchies
|
|
45
|
+
Nest selectors so context-specific overrides apply automatically:
|
|
46
|
+
```javascript
|
|
47
|
+
menu: {
|
|
48
|
+
"": "#menu", // selector for "menu" itself
|
|
49
|
+
item: "a", // "item" inside a menu → <a>
|
|
50
|
+
},
|
|
51
|
+
overview: {
|
|
52
|
+
"": ".overview",
|
|
53
|
+
item: "li", // "item" inside overview → <li>
|
|
54
|
+
},
|
|
55
|
+
item: "li", // global fallback for "item"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Virtual selectors
|
|
59
|
+
Customise how built-in steps inspect parts/properties of matched elements:
|
|
60
|
+
- `$label` — element representing the label (default: `"label"`)
|
|
61
|
+
- `$text` — element representing textual content (default: `false` = self)
|
|
62
|
+
- `$click`, `$enter`, `$hover` — target for action steps (default: `false` = self)
|
|
63
|
+
- `$checked`, `$disabled`, `$value`, `$id`, `$name`, `$selected`, `$classList`
|
|
64
|
+
- `@attrName` — redirect attribute inspection to a different attribute
|
|
65
|
+
- `#propName` — redirect DOM property inspection to a different property
|
|
66
|
+
|
|
67
|
+
### Custom selectors (cg-*)
|
|
68
|
+
All registered automatically. Use anywhere a selector string is accepted:
|
|
69
|
+
|
|
70
|
+
| Selector | Meaning |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `cg-property=name:value` | DOM property exact/regex match |
|
|
73
|
+
| `cg-attribute=name:value` | HTML attribute exact/regex match |
|
|
74
|
+
| `cg-list-property=classList:foo` | classList contains "foo" |
|
|
75
|
+
| `cg-list-property=classList!foo` | classList does not contain "foo" |
|
|
76
|
+
| `cg-width=200` / `>=200` / `<=200` | rendered width in px |
|
|
77
|
+
| `cg-height=200` / `>=200` / `<=200` | rendered height in px |
|
|
78
|
+
| `cg-shadow-query=:shadow .inner` | query across shadow DOM boundary |
|
|
79
|
+
| `me=` | the context element itself |
|
|
80
|
+
|
|
81
|
+
`cg-shadow-query`: `:shadow` is a boundary token (not a CSS pseudo-class). A leading `:shadow` enters the context element's own shadow root. Multiple `:shadow` tokens cross multiple boundaries.
|
|
82
|
+
|
|
83
|
+
## Accessing the API in custom steps
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
import { Given, Then } from "@cucumber/cucumber";
|
|
87
|
+
import { ContextualGherkin } from "@cepharum/contextual-gherkin";
|
|
88
|
+
|
|
89
|
+
// Global search — find elements anywhere on page
|
|
90
|
+
Then( "there is/are {cardinal-word} with a width of {int}px", async ( query, width ) => {
|
|
91
|
+
const api = await ContextualGherkin();
|
|
92
|
+
const matches = await api.find( query.word ).filterBySelector( `cg-width=${width}` );
|
|
93
|
+
await matches.checkCardinalWord( query );
|
|
94
|
+
} );
|
|
95
|
+
|
|
96
|
+
// Contextual search — find elements relative to a previous match
|
|
97
|
+
Then( "{contextual-word} has/have a width of {int}px", async ( term, width ) => {
|
|
98
|
+
const api = await ContextualGherkin();
|
|
99
|
+
const context = api.getContextFor( term );
|
|
100
|
+
const box = await context.locator.boundingBox();
|
|
101
|
+
if ( box?.width !== width ) throw new Error( `expected ${width}px, got ${box?.width}` );
|
|
102
|
+
} );
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Important**: Cucumber step callbacks receive one argument per Cucumber expression token — never use array destructuring, as `fn.length` is used for arity validation.
|
|
106
|
+
|
|
107
|
+
## Key context methods
|
|
108
|
+
|
|
109
|
+
| Method | Description |
|
|
110
|
+
|---|---|
|
|
111
|
+
| `context.find( name, fallback? )` | search descendants by configured type name |
|
|
112
|
+
| `context.filterByType( name )` | intersect current set with named type (tests elements themselves) |
|
|
113
|
+
| `context.filterBySelector( sel )` | intersect current set with raw selector |
|
|
114
|
+
| `context.filterBySubType( name )` | keep elements that *contain* a descendant of named type |
|
|
115
|
+
| `context.filterBySubSelector( sel )` | keep elements that *contain* a descendant matching selector |
|
|
116
|
+
| `context.nth( index )` | pick single element by zero-based index |
|
|
117
|
+
| `context.withAttribute( name, value )` | filter by attribute value |
|
|
118
|
+
| `context.withProperty( name, value )` | filter by DOM property value |
|
|
119
|
+
| `context.checkCardinalWord( query )` | assert count and track in history |
|
|
120
|
+
| `context.track( type?, cardinality? )` | manually track in history |
|
|
121
|
+
| `context.locator` | underlying Playwright Locator |
|
|
122
|
+
|
|
123
|
+
## Cucumber expressions provided
|
|
124
|
+
|
|
125
|
+
- `{cardinal-word}` — e.g. "three buttons", "at least 2 icons" → `{ word, number, mode, cardinality }`
|
|
126
|
+
- `{contextual-word}` — e.g. "this button", "the first icon" → `{ word, index, subIndex, ... }`
|
|
127
|
+
- `{ordinal-word}` — e.g. "the third" → `{ word, index }`
|
|
128
|
+
- `{string-or-word}` — quoted string or single word
|
|
129
|
+
|
|
130
|
+
## Aliases
|
|
131
|
+
|
|
132
|
+
```javascript
|
|
133
|
+
aliases: {
|
|
134
|
+
btn: "button", // "btn" ↔ "button"
|
|
135
|
+
list: [ "menu", "overview", "sidebar" ] // all four alias each other
|
|
136
|
+
}
|
|
137
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cepharum/contextual-gherkin",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.1",
|
|
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/",
|