nativeproof 0.7.0 → 0.9.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,30 @@ 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.9.0
8
+
9
+ Regex frame matching — `expect(mock)` matches traffic by pattern.
10
+
11
+ **Added**
12
+
13
+ - **`toHaveSent` / `toHaveReceived` (and `FrameMatch`) accept a `RegExp`** for `path`, `type`, and any
14
+ payload field — `expect(mock).toHaveSent({ path: /\/users/, type: "request" })` matches a
15
+ query-suffixed or otherwise variable path, mirroring regex selectors on the locator side. A string
16
+ stays an exact match (deep equality for payload objects); a RegExp tests the actual string value.
17
+
18
+ ## 0.8.0
19
+
20
+ Relative locators — `Locator.near` scopes to the match nearest an anchor.
21
+
22
+ **Added**
23
+
24
+ - **`Locator.near(anchor, { maxDistance? })`** — orders this locator's matches by bounds-centre
25
+ distance to the `anchor` locator's match, nearest first, so a control is addressed by the element
26
+ beside it: `getByRole("checkbox").near(getByText("Wi-Fi"))` is the checkbox in the Wi-Fi row.
27
+ `maxDistance` (px) drops farther matches, so an absent control resolves to nothing. Composes with
28
+ `.nth()` / `.check()` / `expect(locator).toBeChecked()`. Together with role selectors this retires
29
+ source-bounds geometry for "the control next to this label".
30
+
7
31
  ## 0.7.0
8
32
 
9
33
  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
 
@@ -548,7 +555,8 @@ test.describe("chat room", "member", () => {
548
555
  - `mock.route(path).reject({ code })` — fail it (HTTP status, or WebSocket close code 3000–4999).
549
556
  - `mock.route(path).abort()` — drop the connect/request entirely.
550
557
  - `expect(mock).toHaveSent(match)` / `toHaveReceived(match)` — `match` is a partial frame:
551
- `path` / `type` plus any payload fields.
558
+ `path` / `type` plus any payload fields. Each field is a string (exact / deep-equal) **or a `RegExp`**
559
+ (`toHaveSent({ path: /\/users/, type: "request" })`) to match a query-suffixed or variable path.
552
560
 
553
561
  **Frame types** — real protocol messages keep their own `type` (a WS JSON message's `type`); the
554
562
  server also synthesises types for primitives so you can assert on them:
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/dist/mock.d.ts CHANGED
@@ -18,8 +18,8 @@ export interface MockFrame {
18
18
  }
19
19
  /** A partial frame to match against: `path` / `type` plus any payload fields. */
20
20
  export interface FrameMatch {
21
- path?: string;
22
- type?: string;
21
+ path?: string | RegExp;
22
+ type?: string | RegExp;
23
23
  [key: string]: unknown;
24
24
  }
25
25
  /** Controls how an intercepted path replies — the Playwright `Route` equivalent. */
package/dist/mock.js CHANGED
@@ -1,23 +1,33 @@
1
1
  import { isDeepStrictEqual } from "node:util";
2
+ /**
3
+ * Match one field of an observed frame against an expected value: a `RegExp` tests the
4
+ * actual string (so paths with query/suffix match — `toHaveSent({ path: /\/users/ })`),
5
+ * anything else is deep structural equality.
6
+ */
7
+ function fieldMatches(actual, expected) {
8
+ if (expected instanceof RegExp)
9
+ return typeof actual === "string" && expected.test(actual);
10
+ return isDeepStrictEqual(actual, expected);
11
+ }
2
12
  /**
3
13
  * True if `frame` satisfies every field of `match`: `path` / `type` at the top level,
4
14
  * every other key against the frame's payload.
5
15
  */
6
16
  export function matchesFrame(frame, match) {
7
- if (match.path !== undefined && frame.path !== match.path)
17
+ if (match.path !== undefined && !fieldMatches(frame.path, match.path))
8
18
  return false;
9
- if (match.type !== undefined && frame.type !== match.type)
19
+ if (match.type !== undefined && !fieldMatches(frame.type, match.type))
10
20
  return false;
11
21
  for (const [key, value] of Object.entries(match)) {
12
22
  if (key === "path" || key === "type")
13
23
  continue;
14
- if (!isDeepStrictEqual(frame.payload?.[key], value))
24
+ if (!fieldMatches(frame.payload?.[key], value))
15
25
  return false;
16
26
  }
17
27
  return true;
18
28
  }
19
29
  export function describeMatch(match) {
20
- return JSON.stringify(match);
30
+ return JSON.stringify(match, (_key, value) => (value instanceof RegExp ? String(value) : value));
21
31
  }
22
32
  /** Single-shot check: does any observed frame match `match` in `direction`? */
23
33
  export async function frameExists(source, direction, match) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nativeproof",
3
- "version": "0.7.0",
3
+ "version": "0.9.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.",