@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 CHANGED
@@ -690,18 +690,43 @@ declare module "@cepharum/contextual-gherkin" {
690
690
  nth( index: number|null ): AbstractContext;
691
691
 
692
692
  /**
693
- * Reduces current context's set of matching elements to those matching
694
- * another selector for given type of element.
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
- * @note In opposition to filter(), this function isn't descending into
697
- * the hierarchy of elements but inspects currently matching
698
- * elements, only.
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 reduce matches 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 match selector of given name, too
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
- filter( typeOfElement: string, defaultSelector?: SimpleSelector ): AbstractContext;
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 as a given type of
175
- * elements, too.
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 focus on
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 matches of filtering and their processing context
180
+ * @returns {Context} node describing the filtered set of elements
180
181
  */
181
- filter( typeOfElement, defaultSelector = undefined ) {
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.filter( { has: this.createSubLocator( query, dependencies ) } ),
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.1.0",
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/",