forked from appium/appium-ios-remotexpc
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathax-element.ts
More file actions
183 lines (171 loc) · 6.8 KB
/
Copy pathax-element.ts
File metadata and controls
183 lines (171 loc) · 6.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
import {util} from '@appium/support';
import {AX_OBJECT_TYPE} from './ax-deserialize.js';
/**
* A handle to one element in the device's accessibility tree.
*
* `platformElement` is the daemon's opaque 20-byte identifier and is what makes
* the handle usable in later calls — it has to be sent back verbatim.
*/
export interface AxElement {
/** The daemon's opaque element identifier. */
platformElement: Buffer;
/** The element's `accessibilityIdentifier`, when it has one. */
accessibilityIdentifier?: string;
}
/**
* One attribute the daemon exposes for an element, e.g. `Label` or `Traits`.
*
* These are descriptors only — they carry no value. Reading a value takes a
* second call (`deviceElement:valueForAttribute:`) passing the element and the
* descriptor back, which is exactly what Xcode's Inspector does to fill each row
* of its panel.
*/
export interface AxElementAttribute {
/** Wire name, e.g. `TraitsHumanReadable`. Pass this back to read a value. */
name: string;
/** Display name, e.g. `Traits`. */
humanReadableName: string;
/** Whether the value can be written back. */
settable: boolean;
/** Whether reading it performs an action rather than returning data. */
performsAction: boolean;
/** Whether the daemon considers this internal/debug-only. */
isInternal: boolean;
/** The daemon's value-type discriminator. */
valueType?: number;
/** The raw descriptor, needed verbatim when asking for the value. */
raw: Record<string, unknown>;
}
/** A titled group of attributes — `Basic`, `Actions`, `Element`, `Hierarchy`. */
export interface AxInspectorSection {
/** Stable identifier, e.g. `Basic_v1`. */
identifier: string;
/** Display title, e.g. `Basic`. */
title: string;
/** The attributes in this section. */
attributes: AxElementAttribute[];
}
/** The inspector panel the device pushes when the focused element changes. */
export interface AxInspectedElement {
/** What VoiceOver would announce, when the daemon provides it. */
spokenDescription?: string;
/** The caption shown above the panel, when present. */
caption?: string;
/** The panel's sections, in the order the device sent them. */
sections: AxInspectorSection[];
}
/** Recovers a `Buffer` from a decoded `NS.data` blob. */
function toBuffer(value: unknown): Buffer | undefined {
if (Buffer.isBuffer(value)) {
return value;
}
if (typeof value === 'object' && value !== null) {
// The archiver decodes NSData into an index-keyed object.
const bytes = Object.values(value as Record<string, unknown>).filter((b): b is number => typeof b === 'number');
if (bytes.length > 0) {
return Buffer.from(bytes);
}
}
return undefined;
}
/**
* Parses a deserialized `AXAuditElement_v1`.
*
* The `_v1` suffixes are the daemon's own wire keys, not our assumption. A
* future shape would carry different keys, so this returns `undefined` rather
* than misreading one.
*/
export function toAxElement(value: unknown): AxElement | undefined {
if (!util.isPlainObject(value)) {
return undefined;
}
const fields = value as Record<string, unknown>;
const platformValue = fields.PlatformElementValue_v1;
const container = util.isPlainObject(platformValue)
? ((platformValue as Record<string, unknown>)['NS.data'] ?? platformValue)
: undefined;
const platformElement = toBuffer(container);
if (!platformElement) {
return undefined;
}
return {
platformElement,
accessibilityIdentifier:
typeof fields.AccessibilityIdentifier_v1 === 'string' ? fields.AccessibilityIdentifier_v1 : undefined,
};
}
/**
* Rebuilds the serialized form the daemon expects when an element is passed
* back, matching what Xcode's Inspector sends.
*/
export function serializeAxElement(element: AxElement): Record<string, unknown> {
const value: Record<string, unknown> = {
PlatformElementValue_v1: {ObjectType: 'passthrough', Value: element.platformElement},
};
if (element.accessibilityIdentifier !== undefined) {
value.AccessibilityIdentifier_v1 = {ObjectType: 'passthrough', Value: element.accessibilityIdentifier};
}
return {
ObjectType: 'AXAuditElement_v1',
Value: {ObjectType: 'passthrough', Value: value},
};
}
function toAttribute(value: unknown): AxElementAttribute | undefined {
if (!util.isPlainObject(value)) {
return undefined;
}
const fields = value as Record<string, unknown>;
const name = fields.AttributeNameValue_v1;
if (typeof name !== 'string') {
return undefined;
}
return {
name,
humanReadableName: typeof fields.HumanReadableNameValue_v1 === 'string' ? fields.HumanReadableNameValue_v1 : name,
settable: fields.SettableValue_v1 === true,
performsAction: fields.PerformsActionValue_v1 === true,
isInternal: fields.IsInternal_v1 === true,
valueType: typeof fields.ValueTypeValue_v1 === 'number' ? fields.ValueTypeValue_v1 : undefined,
raw: stripTag(fields),
};
}
/** Drops the decoder's type tag so the object round-trips as the daemon sent it. */
function stripTag(fields: Record<string, unknown>): Record<string, unknown> {
return Object.fromEntries(Object.entries(fields).filter(([key]) => key !== AX_OBJECT_TYPE));
}
/** Rebuilds an attribute descriptor for the wire. */
export function serializeAxAttribute(attribute: AxElementAttribute): Record<string, unknown> {
const value = Object.fromEntries(
Object.entries(attribute.raw).map(([key, inner]) => [key, {ObjectType: 'passthrough', Value: inner}]),
);
return {
ObjectType: 'AXAuditElementAttribute_v1',
Value: {ObjectType: 'passthrough', Value: value},
};
}
/** Parses the payload of an inbound `hostInspectorCurrentElementChanged:`. */
export function toInspectedElement(value: unknown): AxInspectedElement {
const fields = (util.isPlainObject(value) ? value : {}) as Record<string, unknown>;
const rawSections = Array.isArray(fields.InspectorSectionsValue_v1) ? fields.InspectorSectionsValue_v1 : [];
const sections: AxInspectorSection[] = [];
for (const rawSection of rawSections) {
if (!util.isPlainObject(rawSection)) {
continue;
}
const section = rawSection as Record<string, unknown>;
const rawAttributes = Array.isArray(section.ElementAttributesValue_v1) ? section.ElementAttributesValue_v1 : [];
sections.push({
identifier: typeof section.IdentifierValue_v1 === 'string' ? section.IdentifierValue_v1 : '',
title: typeof section.TitleValue_v1 === 'string' ? section.TitleValue_v1 : '',
attributes: rawAttributes
.map(toAttribute)
.filter((attribute): attribute is AxElementAttribute => attribute !== undefined),
});
}
return {
spokenDescription:
typeof fields.SpokenDescriptionValue_v1 === 'string' ? fields.SpokenDescriptionValue_v1 : undefined,
caption: typeof fields.CaptionTextValue_v1 === 'string' ? fields.CaptionTextValue_v1 : undefined,
sections,
};
}