Skip to content

Commit bfd8ce7

Browse files
committed
fix(android): carry an empty text on a node labelled by its content description
1 parent ddbfc85 commit bfd8ce7

10 files changed

Lines changed: 44 additions & 7 deletions

File tree

‎packages/command-runtime/src/commands/capture/runtime/snapshot-unchanged.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ test.each<Partial<SnapshotNode>>([
7575
{ value: 'Draft' },
7676
{ identifier: 'create' },
7777
{ contentDescription: 'Create a draft' },
78+
{ text: '' },
7879
{ enabled: false },
7980
{ selected: true },
8081
{ checked: true },

‎packages/command-runtime/src/commands/capture/runtime/snapshot-unchanged.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ const PRESENTATION_SCALAR_FIELDS = {
119119
label: true,
120120
value: true,
121121
contentDescription: true,
122+
text: true,
122123
identifier: true,
123124
enabled: true,
124125
editable: true,

‎packages/daemon/src/server/__tests__/response-views.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ test('get attrs digest compacts the node under a ref target', () => {
336336
test('attrs digest keeps explicit false/zero/empty field facts; unavailable ones stay absent (#2288)', () => {
337337
const fieldFacts = {
338338
value: '',
339+
text: '',
339340
editable: false,
340341
password: false,
341342
hintShowing: false,

‎packages/daemon/src/server/response-views.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ const SELECTOR_DIGEST_NODE_FIELDS = [
116116
'label',
117117
'value',
118118
'contentDescription',
119+
'text',
119120
'identifier',
120121
'enabled',
121122
'selected',

‎packages/kernel/src/snapshot.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,12 @@ export type RawSnapshotNode = {
300300
* filled or hinted field) is carried here for consumers that want the accessible name.
301301
*/
302302
contentDescription?: string;
303+
/**
304+
* Android view text when it is not already the `label`: the empty string on a view that has
305+
* a content description and no text, where the description is the label. Absent otherwise,
306+
* so `text ?? label` is the text the view shows.
307+
*/
308+
text?: string;
303309
identifier?: string;
304310
rect?: Rect;
305311
enabled?: boolean;

‎packages/platform-android/src/ui-hierarchy-node.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { RawSnapshotNode, Rect } from '@agent-device/kernel/snapshot';
44
type AndroidPassiveNodeFacts = Pick<
55
RawSnapshotNode,
66
| 'contentDescription'
7+
| 'text'
78
| 'enabled'
89
| 'selected'
910
| 'heading'

‎packages/platform-android/src/ui-hierarchy.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ function normalizeAndroidUiHierarchyNode(
331331
value: androidFieldValue(attrs),
332332
passiveFacts: {
333333
contentDescription: attrs.desc && attrs.desc !== label ? attrs.desc : undefined,
334+
text: label && !attrs.text ? '' : undefined,
334335
enabled: attrs.enabled,
335336
selected: attrs.selected,
336337
heading: attrs.heading,

‎packages/provider-webdriver/src/webdriver-source.test.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,11 @@ test('Android WebDriver facts treat invalid state attributes as false', () => {
3535
// by, so a filled field used to read as a node labelled with its own value and no value at all:
3636
// `toHaveValue` saw "" on a field holding "Ada Lovelace".
3737
test("Android WebDriver facts name a node by its text, carry a content description beside it, and read a field's text as its value", () => {
38-
const [field, label] = parseWebDriverSourceFacts(
38+
const [field, label, described] = parseWebDriverSourceFacts(
3939
'<hierarchy>' +
4040
'<android.widget.EditText text="Ada Lovelace" content-desc="Name field" hint="Your name" bounds="[0,0][100,40]" />' +
4141
'<android.widget.TextView text="Ada Lovelace" bounds="[0,120][100,160]" />' +
42+
'<android.widget.TextView text="" content-desc="Name field" bounds="[0,160][100,200]" />' +
4243
'</hierarchy>',
4344
'android',
4445
).nodes;
@@ -55,9 +56,10 @@ test("Android WebDriver facts name a node by its text, carry a content descripti
5556
['Ada Lovelace', 'Ada Lovelace', 'Name field', 'Your name', undefined, undefined],
5657
);
5758
assert.deepEqual(
58-
[label!.label, label!.value, 'editable' in label!],
59-
['Ada Lovelace', undefined, false],
59+
[label!.label, label!.value, 'editable' in label!, 'text' in label!, 'text' in field!],
60+
['Ada Lovelace', undefined, false, false, false],
6061
);
62+
assert.deepEqual([described!.label, described!.text], ['Name field', '']);
6163
});
6264

6365
test('Android WebDriver facts read a hinted field as empty, a masked field as a password, and editability as unavailable when unreported', () => {

‎packages/provider-webdriver/src/webdriver-source.ts‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,22 +194,25 @@ function sourceNodeFromAttributes(
194194
* How a node is named. An Android node is labelled by its text and falls back to the content
195195
* description only when it has none, as `normalizeAndroidUiHierarchyNode` reads the same
196196
* attributes for the native helper; a content description beside visible text travels as
197-
* `contentDescription`. A hinted empty field is labelled by its hint, which is its text. XCUITest
198-
* names a node by `label`, else `name`.
197+
* `contentDescription`, and a node labelled by its description carries an empty `text`. A hinted
198+
* empty field is labelled by its hint, which is its text. XCUITest names a node by `label`, else
199+
* `name`.
199200
*/
200201
function labelFacts(
201202
attrs: Record<string, string>,
202203
platform: WebDriverSourcePlatform,
203-
): Pick<RawSnapshotNode, 'label' | 'contentDescription'> {
204+
): Pick<RawSnapshotNode, 'label' | 'contentDescription' | 'text'> {
204205
if (platform === 'ios')
205206
return { label: firstWebDriverAttribute(attrs, ['label', 'text', 'name']) };
206207
const description = nonEmptyWebDriverAttribute(attrs['content-desc']);
207-
const label = nonEmptyWebDriverAttribute(attrs.text) ?? description;
208+
const text = nonEmptyWebDriverAttribute(attrs.text);
209+
const label = text ?? description;
208210
return {
209211
label,
210212
...(description !== undefined && description !== label
211213
? { contentDescription: description }
212214
: {}),
215+
...(label !== undefined && text === undefined ? { text: '' } : {}),
213216
};
214217
}
215218

‎src/__tests__/android-ui-hierarchy.test.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,26 @@ test('an Android content description beside text is carried, and one that is the
136136
assert.equal(result.nodes[3]?.contentDescription, undefined);
137137
});
138138

139+
test('an Android view labelled by its content description carries an empty text, and one labelled by its text carries none', () => {
140+
const xml = `<hierarchy>
141+
<node class="android.widget.TextView" package="com.example.app" text="" content-desc="saved_task" bounds="[0,0][390,44]" visible-to-user="true"/>
142+
<node class="android.widget.TextView" package="com.example.app" text="saved_task" bounds="[0,60][390,104]" visible-to-user="true"/>
143+
<node class="android.widget.TextView" package="com.example.app" text="Release checklist" content-desc="saved_task" bounds="[0,120][390,164]" visible-to-user="true"/>
144+
<node class="android.widget.TextView" package="com.example.app" text="" resource-id="empty" bounds="[0,180][390,224]" visible-to-user="true"/>
145+
</hierarchy>`;
146+
147+
const result = parseUiHierarchy(xml, 800, { raw: true });
148+
149+
assert.equal(result.nodes[0]?.label, 'saved_task');
150+
assert.equal(result.nodes[0]?.text, '');
151+
assert.equal(result.nodes[1]?.label, 'saved_task');
152+
assert.equal(result.nodes[1]?.text, undefined);
153+
assert.equal(result.nodes[2]?.label, 'Release checklist');
154+
assert.equal(result.nodes[2]?.text, undefined);
155+
assert.equal(result.nodes[3]?.label, undefined);
156+
assert.equal(result.nodes[3]?.text, undefined);
157+
});
158+
139159
test('interactive Android snapshots keep a fixed sibling outside filtered scroll content (#1377)', () => {
140160
const xml = `<hierarchy>
141161
<node class="android.widget.FrameLayout" bounds="[0,0][400,800]" visible-to-user="true">

0 commit comments

Comments
 (0)