-
Notifications
You must be signed in to change notification settings - Fork 174
Expand file tree
/
Copy pathtext-surface.ts
More file actions
110 lines (101 loc) · 3.26 KB
/
Copy pathtext-surface.ts
File metadata and controls
110 lines (101 loc) · 3.26 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
type TextSurfaceNode = {
type?: string;
label?: string;
value?: string;
identifier?: string;
role?: string;
subrole?: string;
};
export function extractReadableText(node: TextSurfaceNode): string {
const label = trimText(node.label);
const value = trimText(node.value);
const identifier = trimText(node.identifier);
const fallbackIdentifier = isMeaningfulReadableIdentifier(identifier) ? identifier : '';
if (prefersValueForReadableText(node.type ?? '')) {
return value || label || fallbackIdentifier;
}
return label || value || fallbackIdentifier;
}
function isLargeTextSurface(node: TextSurfaceNode, displayType?: string): boolean {
if (displayType === 'text-view' || displayType === 'text-field' || displayType === 'search') {
return true;
}
const normalized = normalizeType(node.type ?? '');
const rawRole = `${node.role ?? ''} ${node.subrole ?? ''}`.toLowerCase();
return (
normalized.includes('textview') ||
normalized.includes('textarea') ||
normalized.includes('textfield') ||
normalized.includes('securetextfield') ||
normalized.includes('searchfield') ||
normalized.includes('edittext') ||
rawRole.includes('text area') ||
rawRole.includes('text field')
);
}
export function buildTextPreview(text: string): string {
const normalized = text.replace(/\s+/g, ' ').trim();
if (normalized.length <= 48) {
return normalized;
}
return `${normalized.slice(0, 45)}...`;
}
export function describeTextSurface(
node: TextSurfaceNode,
displayType?: string,
): {
text: string;
isLargeSurface: boolean;
shouldSummarize: boolean;
} {
const text = extractReadableText(node);
const isLargeSurface = isLargeTextSurface(node, displayType);
return {
text,
isLargeSurface,
shouldSummarize: isLargeSurface && shouldSummarizeTextSurface(text),
};
}
function shouldSummarizeTextSurface(text: string): boolean {
if (!text) {
return false;
}
return text.length > 80 || /[\r\n]/.test(text);
}
export function trimText(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
}
export function normalizeType(type: string): string {
let normalized = type
.trim()
.replace(/XCUIElementType/gi, '')
.replace(/^AX/, '')
.toLowerCase();
const lastSeparator = Math.max(normalized.lastIndexOf('.'), normalized.lastIndexOf('/'));
if (lastSeparator !== -1) {
normalized = normalized.slice(lastSeparator + 1);
}
return normalized;
}
/**
* Editable / expandable text-bearing element types whose live on-screen value can exceed
* the captured snapshot text. For these the readable text prefers `value`, and a backend
* (e.g. iOS XCUITest) re-read at the element can recover fuller text than the snapshot node.
*/
export function prefersValueForReadableText(type: string): boolean {
const normalized = normalizeType(type);
return (
normalized.includes('textfield') ||
normalized.includes('securetextfield') ||
normalized.includes('searchfield') ||
normalized.includes('edittext') ||
normalized.includes('textview') ||
normalized.includes('textarea')
);
}
function isMeaningfulReadableIdentifier(value: string): boolean {
if (!value) {
return false;
}
return !/^[\w.]+:id\/[\w.-]+$/i.test(value) && !/^_?NS:\d+$/i.test(value);
}