nativeproof 0.10.0 → 0.10.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/CHANGELOG.md +12 -0
- package/README.md +20 -1
- package/dist/locator.js +3 -3
- package/dist/source.d.ts +8 -0
- package/dist/source.js +45 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ 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.10.1
|
|
8
|
+
|
|
9
|
+
Cross-platform locator resolution fixes.
|
|
10
|
+
|
|
11
|
+
**Fixed**
|
|
12
|
+
|
|
13
|
+
- `by.text` / `getByText` with a `RegExp` now matches a label exposed via `content-desc`
|
|
14
|
+
(e.g. Compose). The RegExp path previously tested only the first attribute in the
|
|
15
|
+
`text`/`content-desc` alternation, so a label was missed when an empty `text=""` preceded it.
|
|
16
|
+
- Locators now resolve iOS element geometry (XCUITest `x`/`y`/`width`/`height`), so `tap()`,
|
|
17
|
+
`bounds()` and `near()` work on iOS — not just Android `bounds="[x1,y1][x2,y2]"`.
|
|
18
|
+
|
|
7
19
|
## 0.10.0
|
|
8
20
|
|
|
9
21
|
Element enabled-state assertions.
|
package/README.md
CHANGED
|
@@ -418,6 +418,13 @@ Every `getBy*` (and `by.*`) takes a **string** (exact match) or a **`RegExp`** (
|
|
|
418
418
|
element's decoded value), so a human pattern matches even entity-escaped source — `getByText(/Save( draft)?/)`,
|
|
419
419
|
`getByLabel(/^Remove /)`, `getByRole("checkbox", { name: /terms/i })`. The Playwright muscle memory carries over.
|
|
420
420
|
|
|
421
|
+
> **A string selector is exact — case-, space- and punctuation-sensitive.** `getByText("Sign In")`
|
|
422
|
+
> does **not** match a `Sign in` label; it just times out as "not found", with no hint that you were
|
|
423
|
+
> one capital letter away. When you don't yet know a label verbatim — porting an existing suite, or
|
|
424
|
+
> writing the spec before you've seen the screen — reach for a `RegExp` (`getByText(/sign in/i)`) or
|
|
425
|
+
> read the real label off the page source first (see [Troubleshooting](#troubleshooting)). A wrong
|
|
426
|
+
> guess fails the same way a genuinely-missing element does, so confirm the string against the device.
|
|
427
|
+
|
|
421
428
|
How each maps to the page source:
|
|
422
429
|
|
|
423
430
|
| Locator | Android attribute | iOS attribute |
|
|
@@ -763,9 +770,21 @@ The framework's own unit suite (`npm test`) needs **no device** and runs anywher
|
|
|
763
770
|
| `no nativeproof.config.ts or wdio.conf.ts found` | Run from the project root, or pass `--config <path>`. |
|
|
764
771
|
| "No specs found" | Specs must match `testDir`/`testMatch` (default `tests/**/*.spec.ts`), or pass `--spec`. |
|
|
765
772
|
| App can't reach the mock | Emulator → use `10.0.2.2`; real device → your machine's LAN IP. Bind the mock with `host: "0.0.0.0"`. |
|
|
766
|
-
| `expect(...)` times out | The selector never matched — confirm the attribute mapping (see the [Locators](#locators) table) and the value; raise `{ timeout }` for slow screens. |
|
|
773
|
+
| `expect(...)` times out | The selector never matched — confirm the attribute mapping (see the [Locators](#locators) table) and the **exact** value (see below); raise `{ timeout }` for slow screens. |
|
|
767
774
|
| iOS first run hangs | WebDriverAgent is building/signing. Set `appium:wdaLaunchTimeout` and, on a real device, the signing capabilities. |
|
|
768
775
|
|
|
776
|
+
**A selector won't match? Read the source — don't re-guess.** A string that's off by a capital letter,
|
|
777
|
+
a trailing space, or `(1)` vs ` (1)` fails as a silent timeout, identical to a genuinely-absent element.
|
|
778
|
+
The fix is always the same: look at what the device actually exposes.
|
|
779
|
+
|
|
780
|
+
- On any failure, the `onFailure` hook / `captureState(prefix)` writes the page source to your artifacts
|
|
781
|
+
dir. Grep it for the real label: `grep -oE 'text="[^"]+"' <file>` and `content-desc="…"` on Android,
|
|
782
|
+
`label="…"` / `value="…"` on iOS — then match it exactly, or with a `RegExp` if it varies.
|
|
783
|
+
- Or read it live mid-spec: `console.log(await wdioDriver().source())`.
|
|
784
|
+
|
|
785
|
+
This one-step loop — *fail → read the real attribute → match it* — is behind most "element not found"
|
|
786
|
+
mysteries, and it beats guessing label strings every time.
|
|
787
|
+
|
|
769
788
|
## API reference
|
|
770
789
|
|
|
771
790
|
- `defineApp(definition)` → `app` — the seam; `app.session(role?)` is a scenario fixture. `definition` also
|
package/dist/locator.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { decodeXmlEntities, encodeXmlEntities, escapeRegExp, nodesForAttribute, nodesForRole,
|
|
1
|
+
import { decodeXmlEntities, encodeXmlEntities, escapeRegExp, nodesForAttribute, nodesForRole, parseNodeBounds, smallestClickableAncestorBounds, } from "./source.js";
|
|
2
2
|
/**
|
|
3
3
|
* Build selectors Playwright-style. The cross-platform attribute each maps to is resolved
|
|
4
4
|
* per platform (see {@link attributeFor}), so you never have to know whether it's a
|
|
@@ -100,7 +100,7 @@ export class Locator {
|
|
|
100
100
|
return [];
|
|
101
101
|
const { maxDistance } = this.proximity;
|
|
102
102
|
return nodes
|
|
103
|
-
.map((node) => ({ node, bounds:
|
|
103
|
+
.map((node) => ({ node, bounds: parseNodeBounds(node) }))
|
|
104
104
|
.filter((entry) => entry.bounds !== null)
|
|
105
105
|
.map((entry) => ({
|
|
106
106
|
node: entry.node,
|
|
@@ -149,7 +149,7 @@ export class Locator {
|
|
|
149
149
|
/** Bounds of the matched node in the current source, or null if absent. */
|
|
150
150
|
async bounds() {
|
|
151
151
|
const node = this.pick(await this.matchedNodes());
|
|
152
|
-
return node ?
|
|
152
|
+
return node ? parseNodeBounds(node) : null;
|
|
153
153
|
}
|
|
154
154
|
/** The matched node's own visible text, or null if the node is absent. */
|
|
155
155
|
async textContent() {
|
package/dist/source.d.ts
CHANGED
|
@@ -18,6 +18,14 @@ export type Bounds = {
|
|
|
18
18
|
centerY: number;
|
|
19
19
|
};
|
|
20
20
|
export declare function parseBounds(bounds: string | undefined | null): Bounds | null;
|
|
21
|
+
/**
|
|
22
|
+
* Bounds of a single element node, cross-platform. Android UiAutomator exposes geometry as
|
|
23
|
+
* `bounds="[x1,y1][x2,y2]"`; iOS XCUITest exposes separate `x`/`y`/`width`/`height` attributes
|
|
24
|
+
* (no `bounds`). Tries the Android form first, then falls back to the iOS attributes — so locators
|
|
25
|
+
* resolve a tap point on both platforms. Without this, iOS elements have null bounds and `tap()`
|
|
26
|
+
* never finds a target.
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseNodeBounds(node: string): Bounds | null;
|
|
21
29
|
export declare function escapeRegExp(value: string): string;
|
|
22
30
|
/**
|
|
23
31
|
* Decode the XML entities UiAutomator/XCUITest escape attribute values with
|
package/dist/source.js
CHANGED
|
@@ -26,6 +26,38 @@ export function parseBounds(bounds) {
|
|
|
26
26
|
centerY: Math.round((y1 + y2) / 2),
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Bounds of a single element node, cross-platform. Android UiAutomator exposes geometry as
|
|
31
|
+
* `bounds="[x1,y1][x2,y2]"`; iOS XCUITest exposes separate `x`/`y`/`width`/`height` attributes
|
|
32
|
+
* (no `bounds`). Tries the Android form first, then falls back to the iOS attributes — so locators
|
|
33
|
+
* resolve a tap point on both platforms. Without this, iOS elements have null bounds and `tap()`
|
|
34
|
+
* never finds a target.
|
|
35
|
+
*/
|
|
36
|
+
export function parseNodeBounds(node) {
|
|
37
|
+
const android = parseBounds(/bounds="([^"]+)"/.exec(node)?.[1]);
|
|
38
|
+
if (android)
|
|
39
|
+
return android;
|
|
40
|
+
const attr = (name) => {
|
|
41
|
+
const m = new RegExp(`\\b${name}="(-?\\d+(?:\\.\\d+)?)"`).exec(node);
|
|
42
|
+
return m ? Number(m[1]) : null;
|
|
43
|
+
};
|
|
44
|
+
const x = attr("x");
|
|
45
|
+
const y = attr("y");
|
|
46
|
+
const w = attr("width");
|
|
47
|
+
const h = attr("height");
|
|
48
|
+
if (x === null || y === null || w === null || h === null)
|
|
49
|
+
return null;
|
|
50
|
+
return {
|
|
51
|
+
x1: Math.round(x),
|
|
52
|
+
y1: Math.round(y),
|
|
53
|
+
x2: Math.round(x + w),
|
|
54
|
+
y2: Math.round(y + h),
|
|
55
|
+
width: Math.round(w),
|
|
56
|
+
height: Math.round(h),
|
|
57
|
+
centerX: Math.round(x + w / 2),
|
|
58
|
+
centerY: Math.round(y + h / 2),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
29
61
|
export function escapeRegExp(value) {
|
|
30
62
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
31
63
|
}
|
|
@@ -68,12 +100,19 @@ export function nodesForAttribute(source, attribute, value) {
|
|
|
68
100
|
// A `g`-flagged RegExp is stateful across `.test()` calls; use a non-global copy so the
|
|
69
101
|
// per-candidate test is order-independent.
|
|
70
102
|
const test = value.global ? new RegExp(value.source, value.flags.replace("g", "")) : value;
|
|
71
|
-
|
|
103
|
+
// `attribute` may be an alternation (e.g. `(?:text|content-desc)`), and a node can carry
|
|
104
|
+
// more than one of them — Android often exposes both `text=""` and `content-desc="…"`. Test
|
|
105
|
+
// the pattern against EVERY matching attribute's value, not just the first: otherwise a label
|
|
106
|
+
// that lives in `content-desc` is missed whenever an empty `text=""` precedes it in the tag.
|
|
107
|
+
const candidates = new RegExp(`${attribute}="([^"]*)"`, "g");
|
|
72
108
|
const nodes = [];
|
|
73
109
|
for (const tag of source.matchAll(/<[^>]*>/g)) {
|
|
74
|
-
const attr
|
|
75
|
-
|
|
76
|
-
|
|
110
|
+
for (const attr of tag[0].matchAll(candidates)) {
|
|
111
|
+
if (test.test(decodeXmlEntities(attr[1] ?? ""))) {
|
|
112
|
+
nodes.push(tag[0]);
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
77
116
|
}
|
|
78
117
|
return nodes;
|
|
79
118
|
}
|
|
@@ -115,7 +154,7 @@ export function attributeMatches(source, attribute, value) {
|
|
|
115
154
|
*/
|
|
116
155
|
export function boundsForAttribute(source, attribute, value) {
|
|
117
156
|
const node = nodeForAttribute(source, attribute, value);
|
|
118
|
-
return node ?
|
|
157
|
+
return node ? parseNodeBounds(node) : null;
|
|
119
158
|
}
|
|
120
159
|
/** Bounds of an element addressed by Android `content-desc`. */
|
|
121
160
|
export function boundsForContentDesc(source, contentDesc) {
|
|
@@ -132,7 +171,7 @@ export function boundsForText(source, text) {
|
|
|
132
171
|
*/
|
|
133
172
|
export function smallestClickableAncestorBounds(source, nodeBounds) {
|
|
134
173
|
const clickable = [...source.matchAll(/<[^>]*clickable="true"[^>]*>/g)]
|
|
135
|
-
.map((m) =>
|
|
174
|
+
.map((m) => parseNodeBounds(m[0]))
|
|
136
175
|
.filter((b) => b !== null)
|
|
137
176
|
.filter((b) => b.x1 <= nodeBounds.x1 && b.x2 >= nodeBounds.x2 && b.y1 <= nodeBounds.y1 && b.y2 >= nodeBounds.y2)
|
|
138
177
|
.sort((a, b) => a.width * a.height - b.width * b.height);
|
package/package.json
CHANGED