nativeproof 0.7.0 → 0.8.0

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/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@ All notable changes to NativeProof are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/) and the project adheres to
5
5
  [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## 0.8.0
8
+
9
+ Relative locators — `Locator.near` scopes to the match nearest an anchor.
10
+
11
+ **Added**
12
+
13
+ - **`Locator.near(anchor, { maxDistance? })`** — orders this locator's matches by bounds-centre
14
+ distance to the `anchor` locator's match, nearest first, so a control is addressed by the element
15
+ beside it: `getByRole("checkbox").near(getByText("Wi-Fi"))` is the checkbox in the Wi-Fi row.
16
+ `maxDistance` (px) drops farther matches, so an absent control resolves to nothing. Composes with
17
+ `.nth()` / `.check()` / `expect(locator).toBeChecked()`. Together with role selectors this retires
18
+ source-bounds geometry for "the control next to this label".
19
+
7
20
  ## 0.7.0
8
21
 
9
22
  Role selectors — `getByRole` matches by element role, not just name.
package/README.md CHANGED
@@ -449,8 +449,15 @@ await p.getByText("Item").count(); // how many elements match
449
449
  await p.getByText("Item").nth(1).tap(); // the 2nd match (.first() / .last(); negative counts from the end)
450
450
  await member.terms.check(); // checkbox/switch → tap to checked (no-op if already there); also uncheck()
451
451
  await member.terms.isChecked(); // boolean
452
+ await p.getByRole("checkbox").near(p.getByText("Wi-Fi")).check(); // the checkbox in the Wi-Fi row
452
453
  ```
453
454
 
455
+ **Relative locators.** `getByRole(role)` matches by element class/type (`checkbox`, `switch`, `button`,
456
+ `textfield`, `image`), and `Locator.near(anchor, { maxDistance? })` scopes to the match nearest an
457
+ anchor's element — so a control beside a label is addressed by the label, not by coordinates:
458
+ `page(driver).getByRole("checkbox").near(page(driver).getByText("Wi-Fi"))`. Compose with `.check()` /
459
+ `expect(locator).toBeChecked()`.
460
+
454
461
  `tap()` resolves the element's bounds from the page source and taps the centre — a coordinate
455
462
  tap that works even on Compose / SwiftUI nodes Appium reports as non-clickable.
456
463
 
package/dist/locator.d.ts CHANGED
@@ -74,18 +74,36 @@ export declare class Locator {
74
74
  private readonly options;
75
75
  /** When set, the locator resolves to the nth match (negative counts from the end). */
76
76
  private readonly index?;
77
+ /** When set, matches are ordered by proximity to the anchor (and filtered by maxDistance). */
78
+ private readonly proximity?;
77
79
  constructor(driver: Driver, selector: Selector, options?: WaitOptions,
78
80
  /** When set, the locator resolves to the nth match (negative counts from the end). */
79
- index?: number | undefined);
81
+ index?: number | undefined,
82
+ /** When set, matches are ordered by proximity to the anchor (and filtered by maxDistance). */
83
+ proximity?: {
84
+ anchor: Locator;
85
+ maxDistance?: number;
86
+ } | undefined);
80
87
  private attribute;
81
88
  /** Node tags this selector matches in `source`, in document order (role- or attribute-based). */
82
89
  private nodesIn;
83
- /** All node tags this selector matches in the current source, in document order. */
90
+ /**
91
+ * All node tags this selector matches in the current source — document order, or, when a
92
+ * `near` anchor is set, ordered nearest-first by bounds-centre distance (filtered by maxDistance).
93
+ */
84
94
  private matchedNodes;
85
95
  /** The single node this locator resolves to (the nth match, or the first when unindexed). */
86
96
  private pick;
87
97
  /** A locator scoped to the nth match (0-based; negative counts from the end). */
88
98
  nth(index: number): Locator;
99
+ /**
100
+ * Scope to the match nearest `anchor` (by bounds-centre distance) — the relative locator for
101
+ * native: `getByRole("checkbox").near(getByText("Wi-Fi"))` is the checkbox in the Wi-Fi row.
102
+ * `maxDistance` (px) drops matches farther than that, so an absent control resolves to nothing.
103
+ */
104
+ near(anchor: Locator, options?: {
105
+ maxDistance?: number;
106
+ }): Locator;
89
107
  /** A locator scoped to the first match. */
90
108
  first(): Locator;
91
109
  /** A locator scoped to the last match. */
package/dist/locator.js CHANGED
@@ -66,13 +66,17 @@ export class Locator {
66
66
  selector;
67
67
  options;
68
68
  index;
69
+ proximity;
69
70
  constructor(driver, selector, options = {},
70
71
  /** When set, the locator resolves to the nth match (negative counts from the end). */
71
- index) {
72
+ index,
73
+ /** When set, matches are ordered by proximity to the anchor (and filtered by maxDistance). */
74
+ proximity) {
72
75
  this.driver = driver;
73
76
  this.selector = selector;
74
77
  this.options = options;
75
78
  this.index = index;
79
+ this.proximity = proximity;
76
80
  }
77
81
  attribute() {
78
82
  return attributeFor(this.selector, this.driver.platform);
@@ -83,9 +87,28 @@ export class Locator {
83
87
  ? nodesForRole(source, this.selector.value, this.driver.platform)
84
88
  : nodesForAttribute(source, this.attribute(), this.selector.value);
85
89
  }
86
- /** All node tags this selector matches in the current source, in document order. */
90
+ /**
91
+ * All node tags this selector matches in the current source — document order, or, when a
92
+ * `near` anchor is set, ordered nearest-first by bounds-centre distance (filtered by maxDistance).
93
+ */
87
94
  async matchedNodes() {
88
- return this.nodesIn(await this.driver.source());
95
+ const nodes = this.nodesIn(await this.driver.source());
96
+ if (!this.proximity)
97
+ return nodes;
98
+ const anchor = await this.proximity.anchor.bounds();
99
+ if (!anchor)
100
+ return [];
101
+ const { maxDistance } = this.proximity;
102
+ return nodes
103
+ .map((node) => ({ node, bounds: parseBounds(/bounds="([^"]+)"/.exec(node)?.[1]) }))
104
+ .filter((entry) => entry.bounds !== null)
105
+ .map((entry) => ({
106
+ node: entry.node,
107
+ distance: Math.hypot(entry.bounds.centerX - anchor.centerX, entry.bounds.centerY - anchor.centerY),
108
+ }))
109
+ .filter((entry) => maxDistance === undefined || entry.distance <= maxDistance)
110
+ .sort((a, b) => a.distance - b.distance)
111
+ .map((entry) => entry.node);
89
112
  }
90
113
  /** The single node this locator resolves to (the nth match, or the first when unindexed). */
91
114
  pick(nodes) {
@@ -96,7 +119,16 @@ export class Locator {
96
119
  }
97
120
  /** A locator scoped to the nth match (0-based; negative counts from the end). */
98
121
  nth(index) {
99
- return new Locator(this.driver, this.selector, this.options, index);
122
+ return new Locator(this.driver, this.selector, this.options, index, this.proximity);
123
+ }
124
+ /**
125
+ * Scope to the match nearest `anchor` (by bounds-centre distance) — the relative locator for
126
+ * native: `getByRole("checkbox").near(getByText("Wi-Fi"))` is the checkbox in the Wi-Fi row.
127
+ * `maxDistance` (px) drops matches farther than that, so an absent control resolves to nothing.
128
+ */
129
+ near(anchor, options = {}) {
130
+ const proximity = options.maxDistance === undefined ? { anchor } : { anchor, maxDistance: options.maxDistance };
131
+ return new Locator(this.driver, this.selector, this.options, this.index, proximity);
100
132
  }
101
133
  /** A locator scoped to the first match. */
102
134
  first() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nativeproof",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Native Mobile E2E test framework inspired by Playwright (fixtures, locators, expect, route-style mocking) on Appium/WebdriverIO.",