agent-device 0.17.2 → 0.17.4
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/README.md +2 -2
- package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.17.2.apk → agent-device-android-multitouch-helper-0.17.4.apk} +0 -0
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.17.4.apk.sha256 +1 -0
- package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.17.2.manifest.json → agent-device-android-multitouch-helper-0.17.4.manifest.json} +4 -4
- package/android-snapshot-helper/README.md +3 -0
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.17.2.apk → agent-device-android-snapshot-helper-0.17.4.apk} +0 -0
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.17.4.apk.sha256 +1 -0
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.17.2.manifest.json → agent-device-android-snapshot-helper-0.17.4.manifest.json} +6 -6
- package/dist/src/123.js +1 -1
- package/dist/src/1231.js +1 -1
- package/dist/src/1352.js +1 -1
- package/dist/src/221.js +2 -2
- package/dist/src/2403.js +3 -3
- package/dist/src/2415.js +37 -29
- package/dist/src/4012.js +4 -0
- package/dist/src/5611.js +1 -0
- package/dist/src/5827.js +1 -0
- package/dist/src/6088.js +1 -0
- package/dist/src/6133.js +1 -0
- package/dist/src/7847.js +1 -1
- package/dist/src/9471.js +1 -1
- package/dist/src/9542.js +6 -3
- package/dist/src/9616.js +2 -1
- package/dist/src/9818.js +1 -1
- package/dist/src/android-snapshot-helper.d.ts +0 -1
- package/dist/src/android.js +5 -5
- package/dist/src/apple.js +1 -1
- package/dist/src/apps.js +10 -10
- package/dist/src/args.js +53 -14
- package/dist/src/batch.js +1 -1
- package/dist/src/cli-output.js +30 -0
- package/dist/src/cli.js +15 -16
- package/dist/src/command-surface.js +1 -1
- package/dist/src/contracts.d.ts +52 -0
- package/dist/src/contracts.js +1 -1
- package/dist/src/find.js +1 -1
- package/dist/src/generic.js +5 -5
- package/dist/src/index.d.ts +127 -8
- package/dist/src/index.js +1 -1
- package/dist/src/input-actions.js +1 -1
- package/dist/src/interaction.js +1 -1
- package/dist/src/react-native.js +1 -1
- package/dist/src/record-trace-recording.js +3 -3
- package/dist/src/selector-runtime.js +1 -1
- package/dist/src/selectors.d.ts +14 -0
- package/dist/src/selectors.js +1 -1
- package/dist/src/server.js +2 -2
- package/dist/src/session.js +8 -10
- package/dist/src/snapshot.js +2 -2
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +1 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.h +14 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.m +297 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +327 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +122 -173
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +153 -5
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +171 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +0 -10
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +64 -18
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +200 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SequenceExecution.swift +450 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +82 -178
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +425 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +77 -6
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift +10 -1
- package/ios-runner/README.md +5 -5
- package/ios-runner/RUNNER_PROTOCOL.md +0 -1
- package/package.json +3 -3
- package/server.json +2 -2
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.17.2.apk.sha256 +0 -1
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.17.2.apk.sha256 +0 -1
- package/dist/src/1534.js +0 -1
- package/dist/src/1620.js +0 -1
- package/dist/src/1644.js +0 -2
- package/dist/src/2284.js +0 -1
- package/dist/src/2474.js +0 -1
- package/dist/src/2672.js +0 -1
- package/dist/src/3393.js +0 -1
- package/dist/src/5898.js +0 -1
- package/dist/src/695.js +0 -1
- package/dist/src/7455.js +0 -1
- package/dist/src/8173.js +0 -27
- package/dist/src/8407.js +0 -1
- package/dist/src/8699.js +0 -1
- package/dist/src/9238.js +0 -4
- package/dist/src/9673.js +0 -1
- package/dist/src/9974.js +0 -1
package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
|
|
3
|
+
extension RunnerTests {
|
|
4
|
+
private static let privateAXSnapshotMaxNodes = 5_000
|
|
5
|
+
/// Deep React Native trees make the AX server reject bulk snapshot requests outright with
|
|
6
|
+
/// kAXErrorIllegalArgument once the requested depth crosses a tree-size-dependent limit
|
|
7
|
+
/// (observed between depth 56 and 64 on the Bluesky Home feed; the limit moves with live
|
|
8
|
+
/// content). Retrying the same request at a shallower depth succeeds, so on failure we walk
|
|
9
|
+
/// this ladder instead of giving up. Capped at 4 attempts to bound worst-case latency on
|
|
10
|
+
/// apps where the AX surface is genuinely unavailable.
|
|
11
|
+
static let privateAXSnapshotDepthLadder = [56, 40, 24, 12]
|
|
12
|
+
|
|
13
|
+
func privateAXSnapshotCapture(
|
|
14
|
+
app: XCUIApplication,
|
|
15
|
+
options: SnapshotOptions
|
|
16
|
+
) -> SnapshotBackendCapture? {
|
|
17
|
+
#if os(iOS) && targetEnvironment(simulator)
|
|
18
|
+
let requestedDepth = options.depth ?? 64
|
|
19
|
+
var attemptDepths = [requestedDepth]
|
|
20
|
+
attemptDepths.append(
|
|
21
|
+
contentsOf: Self.privateAXSnapshotDepthLadder.filter { $0 < requestedDepth }
|
|
22
|
+
)
|
|
23
|
+
var response: [String: Any] = [:]
|
|
24
|
+
var effectiveDepth = requestedDepth
|
|
25
|
+
var lastError = "unknown private AX snapshot failure"
|
|
26
|
+
for depth in attemptDepths {
|
|
27
|
+
response = RunnerAXSnapshotBridge.snapshotTree(
|
|
28
|
+
for: app,
|
|
29
|
+
maxDepth: depth,
|
|
30
|
+
maxNodes: Self.privateAXSnapshotMaxNodes
|
|
31
|
+
)
|
|
32
|
+
if response["ok"] as? Bool == true {
|
|
33
|
+
effectiveDepth = depth
|
|
34
|
+
break
|
|
35
|
+
}
|
|
36
|
+
lastError = response["error"] as? String ?? lastError
|
|
37
|
+
NSLog(
|
|
38
|
+
"AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_DEPTH_RETRY depth=%ld error=%@",
|
|
39
|
+
depth,
|
|
40
|
+
lastError
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
guard response["ok"] as? Bool == true else {
|
|
44
|
+
NSLog("AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_FAILED=%@", lastError)
|
|
45
|
+
return nil
|
|
46
|
+
}
|
|
47
|
+
guard let root = response["root"] as? [String: Any] else {
|
|
48
|
+
NSLog("AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_FAILED=missing root")
|
|
49
|
+
return nil
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// If the app frame is unavailable, the private root's own frame is the reliable screen
|
|
53
|
+
// rect here. Avoid public window queries: stale transient windows can record XCTest
|
|
54
|
+
// failures after the runner already returned a successful command response.
|
|
55
|
+
var viewport = safeSnapshotViewport(app: app)
|
|
56
|
+
let rootFrame = privateAXRect(root["frame"])
|
|
57
|
+
if viewport.isInfinite || viewport.isNull || viewport.isEmpty, !rootFrame.isEmpty {
|
|
58
|
+
viewport = rootFrame
|
|
59
|
+
}
|
|
60
|
+
var nodes: [SnapshotNode] = []
|
|
61
|
+
appendPrivateAXNode(
|
|
62
|
+
root,
|
|
63
|
+
to: &nodes,
|
|
64
|
+
options: options,
|
|
65
|
+
viewport: viewport,
|
|
66
|
+
depth: 0,
|
|
67
|
+
parentIndex: nil,
|
|
68
|
+
insideMatchedScope: false
|
|
69
|
+
)
|
|
70
|
+
if nodes.count <= 1 {
|
|
71
|
+
NSLog("AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_SPARSE=%ld", nodes.count)
|
|
72
|
+
return nil
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let depthLimited = effectiveDepth < requestedDepth
|
|
76
|
+
NSLog(
|
|
77
|
+
"AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_USED nodes=%ld depth=%ld",
|
|
78
|
+
nodes.count,
|
|
79
|
+
effectiveDepth
|
|
80
|
+
)
|
|
81
|
+
return SnapshotBackendCapture(
|
|
82
|
+
payload: DataPayload(nodes: nodes, truncated: (response["truncated"] as? Bool) == true),
|
|
83
|
+
effectiveDepth: depthLimited ? effectiveDepth : nil
|
|
84
|
+
)
|
|
85
|
+
#else
|
|
86
|
+
return nil
|
|
87
|
+
#endif
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private func appendPrivateAXNode(
|
|
91
|
+
_ rawNode: [String: Any],
|
|
92
|
+
to nodes: inout [SnapshotNode],
|
|
93
|
+
options: SnapshotOptions,
|
|
94
|
+
viewport: CGRect,
|
|
95
|
+
depth: Int,
|
|
96
|
+
parentIndex: Int?,
|
|
97
|
+
insideMatchedScope: Bool
|
|
98
|
+
) {
|
|
99
|
+
if let limit = options.depth, depth > limit { return }
|
|
100
|
+
|
|
101
|
+
let rect = privateAXRect(rawNode["frame"])
|
|
102
|
+
let label = privateAXString(rawNode["label"])
|
|
103
|
+
let identifier = privateAXString(rawNode["identifier"])
|
|
104
|
+
let value = privateAXString(rawNode["value"])
|
|
105
|
+
let rawType = privateAXInt(rawNode["type"]) ?? 0
|
|
106
|
+
let typeName = elementTypeName(rawElementType: rawType)
|
|
107
|
+
let enabled = privateAXBool(rawNode["enabled"]) ?? true
|
|
108
|
+
let visible = isVisibleInViewport(rect, viewport)
|
|
109
|
+
let interactiveCandidate = privateAXInteractiveCandidate(rawElementType: rawType)
|
|
110
|
+
let filterDecision = flatSnapshotFilterDecision(
|
|
111
|
+
FlatSnapshotFilterNode(
|
|
112
|
+
isRoot: parentIndex == nil,
|
|
113
|
+
label: label,
|
|
114
|
+
identifier: identifier,
|
|
115
|
+
valueText: value.isEmpty ? nil : value,
|
|
116
|
+
visible: visible
|
|
117
|
+
),
|
|
118
|
+
options: options,
|
|
119
|
+
insideMatchedScope: insideMatchedScope
|
|
120
|
+
)
|
|
121
|
+
let include = filterDecision.include
|
|
122
|
+
let nowInsideScope = filterDecision.insideMatchedScope
|
|
123
|
+
|
|
124
|
+
let currentIndex: Int?
|
|
125
|
+
if include {
|
|
126
|
+
currentIndex = nodes.count
|
|
127
|
+
nodes.append(
|
|
128
|
+
SnapshotNode(
|
|
129
|
+
index: nodes.count,
|
|
130
|
+
type: typeName,
|
|
131
|
+
label: label.isEmpty ? nil : label,
|
|
132
|
+
identifier: identifier.isEmpty ? nil : identifier,
|
|
133
|
+
value: value.isEmpty ? nil : value,
|
|
134
|
+
rect: snapshotRect(from: rect),
|
|
135
|
+
enabled: enabled,
|
|
136
|
+
focused: privateAXBool(rawNode["focused"]) == true ? true : nil,
|
|
137
|
+
selected: privateAXBool(rawNode["selected"]) == true ? true : nil,
|
|
138
|
+
hittable: visible && enabled && interactiveCandidate,
|
|
139
|
+
depth: depth,
|
|
140
|
+
parentIndex: parentIndex,
|
|
141
|
+
hiddenContentAbove: nil,
|
|
142
|
+
hiddenContentBelow: nil
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
} else {
|
|
146
|
+
currentIndex = parentIndex
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
guard let children = rawNode["children"] as? [[String: Any]] else {
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
for child in children {
|
|
153
|
+
appendPrivateAXNode(
|
|
154
|
+
child,
|
|
155
|
+
to: &nodes,
|
|
156
|
+
options: options,
|
|
157
|
+
viewport: viewport,
|
|
158
|
+
depth: depth + 1,
|
|
159
|
+
parentIndex: currentIndex,
|
|
160
|
+
insideMatchedScope: nowInsideScope
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private func elementTypeName(rawElementType: Int) -> String {
|
|
166
|
+
if let type = flatSnapshotElementType(rawElementType: rawElementType) {
|
|
167
|
+
return elementTypeName(type)
|
|
168
|
+
}
|
|
169
|
+
return "Element(\(rawElementType))"
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private func privateAXString(_ value: Any?) -> String {
|
|
173
|
+
guard let value else { return "" }
|
|
174
|
+
if let string = value as? String {
|
|
175
|
+
return string.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
176
|
+
}
|
|
177
|
+
return String(describing: value).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private func privateAXInt(_ value: Any?) -> Int? {
|
|
181
|
+
if let value = value as? Int { return value }
|
|
182
|
+
if let value = value as? NSNumber { return value.intValue }
|
|
183
|
+
return nil
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private func privateAXBool(_ value: Any?) -> Bool? {
|
|
187
|
+
if let value = value as? Bool { return value }
|
|
188
|
+
if let value = value as? NSNumber { return value.boolValue }
|
|
189
|
+
return nil
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private func privateAXRect(_ value: Any?) -> CGRect {
|
|
193
|
+
guard let frame = value as? [String: Any] else {
|
|
194
|
+
return .zero
|
|
195
|
+
}
|
|
196
|
+
return CGRect(
|
|
197
|
+
x: privateAXDouble(frame["x"]) ?? 0,
|
|
198
|
+
y: privateAXDouble(frame["y"]) ?? 0,
|
|
199
|
+
width: privateAXDouble(frame["width"]) ?? 0,
|
|
200
|
+
height: privateAXDouble(frame["height"]) ?? 0
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private func privateAXDouble(_ value: Any?) -> Double? {
|
|
205
|
+
if let value = value as? Double { return value }
|
|
206
|
+
if let value = value as? NSNumber { return value.doubleValue }
|
|
207
|
+
return nil
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// MARK: - In-bundle unit tests
|
|
212
|
+
|
|
213
|
+
extension RunnerTests {
|
|
214
|
+
func testPrivateAXScopeSelectsSubtreeNotMatchingLabels() {
|
|
215
|
+
let tree: [String: Any] = [
|
|
216
|
+
"type": 1, "label": "App",
|
|
217
|
+
"children": [
|
|
218
|
+
[
|
|
219
|
+
"type": 9, "identifier": "homeScreen",
|
|
220
|
+
"children": [
|
|
221
|
+
["type": 48, "label": "Post body without the scope text", "children": []]
|
|
222
|
+
],
|
|
223
|
+
],
|
|
224
|
+
["type": 9, "label": "unrelated sibling", "children": []],
|
|
225
|
+
],
|
|
226
|
+
]
|
|
227
|
+
var nodes: [SnapshotNode] = []
|
|
228
|
+
appendPrivateAXNode(
|
|
229
|
+
tree,
|
|
230
|
+
to: &nodes,
|
|
231
|
+
options: SnapshotOptions(interactiveOnly: false, depth: nil, scope: "homeScreen", raw: false),
|
|
232
|
+
viewport: .infinite,
|
|
233
|
+
depth: 0,
|
|
234
|
+
parentIndex: nil,
|
|
235
|
+
insideMatchedScope: false
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
let labels = nodes.compactMap { $0.label ?? $0.identifier }
|
|
239
|
+
XCTAssertTrue(labels.contains("homeScreen"))
|
|
240
|
+
// Descendants of the matched scope are included even when they do not contain the text.
|
|
241
|
+
XCTAssertTrue(labels.contains("Post body without the scope text"))
|
|
242
|
+
XCTAssertFalse(labels.contains("unrelated sibling"))
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
func testPrivateAXInteractiveFiltersLoginLikeHiddenDrawer() {
|
|
246
|
+
let tree: [String: Any] = [
|
|
247
|
+
"type": Int(XCUIElement.ElementType.application.rawValue),
|
|
248
|
+
"label": "Blue Sky",
|
|
249
|
+
"frame": ["x": 0, "y": 0, "width": 390, "height": 844],
|
|
250
|
+
"children": [
|
|
251
|
+
[
|
|
252
|
+
"type": Int(XCUIElement.ElementType.scrollView.rawValue),
|
|
253
|
+
"frame": ["x": 0, "y": 0, "width": 390, "height": 844],
|
|
254
|
+
"children": [
|
|
255
|
+
[
|
|
256
|
+
"type": Int(XCUIElement.ElementType.image.rawValue),
|
|
257
|
+
"label": "Callstack",
|
|
258
|
+
"frame": ["x": 145, "y": 104, "width": 100, "height": 100],
|
|
259
|
+
"children": [],
|
|
260
|
+
],
|
|
261
|
+
[
|
|
262
|
+
"type": Int(XCUIElement.ElementType.staticText.rawValue),
|
|
263
|
+
"label": "Welcome back",
|
|
264
|
+
"frame": ["x": 32, "y": 260, "width": 326, "height": 32],
|
|
265
|
+
"children": [],
|
|
266
|
+
],
|
|
267
|
+
[
|
|
268
|
+
"type": Int(XCUIElement.ElementType.textField.rawValue),
|
|
269
|
+
"label": "Email",
|
|
270
|
+
"identifier": "login.email",
|
|
271
|
+
"frame": ["x": 32, "y": 348, "width": 326, "height": 48],
|
|
272
|
+
"children": [],
|
|
273
|
+
],
|
|
274
|
+
[
|
|
275
|
+
"type": Int(XCUIElement.ElementType.secureTextField.rawValue),
|
|
276
|
+
"label": "Password",
|
|
277
|
+
"identifier": "login.password",
|
|
278
|
+
"frame": ["x": 32, "y": 412, "width": 326, "height": 48],
|
|
279
|
+
"children": [],
|
|
280
|
+
],
|
|
281
|
+
[
|
|
282
|
+
"type": Int(XCUIElement.ElementType.button.rawValue),
|
|
283
|
+
"label": "Sign in",
|
|
284
|
+
"identifier": "login.submit",
|
|
285
|
+
"frame": ["x": 32, "y": 492, "width": 326, "height": 52],
|
|
286
|
+
"children": [],
|
|
287
|
+
],
|
|
288
|
+
[
|
|
289
|
+
"type": Int(XCUIElement.ElementType.link.rawValue),
|
|
290
|
+
"label": "Forgot password?",
|
|
291
|
+
"frame": ["x": 128, "y": 568, "width": 134, "height": 32],
|
|
292
|
+
"children": [],
|
|
293
|
+
],
|
|
294
|
+
[
|
|
295
|
+
"type": Int(XCUIElement.ElementType.button.rawValue),
|
|
296
|
+
"label": "Admin settings",
|
|
297
|
+
"frame": ["x": -260, "y": 184, "width": 220, "height": 44],
|
|
298
|
+
"children": [],
|
|
299
|
+
],
|
|
300
|
+
[
|
|
301
|
+
"type": Int(XCUIElement.ElementType.other.rawValue),
|
|
302
|
+
"frame": ["x": 16, "y": 184, "width": 220, "height": 44],
|
|
303
|
+
"children": [],
|
|
304
|
+
],
|
|
305
|
+
],
|
|
306
|
+
]
|
|
307
|
+
],
|
|
308
|
+
]
|
|
309
|
+
var nodes: [SnapshotNode] = []
|
|
310
|
+
appendPrivateAXNode(
|
|
311
|
+
tree,
|
|
312
|
+
to: &nodes,
|
|
313
|
+
options: SnapshotOptions(interactiveOnly: true, depth: nil, scope: nil, raw: false),
|
|
314
|
+
viewport: CGRect(x: 0, y: 0, width: 390, height: 844),
|
|
315
|
+
depth: 0,
|
|
316
|
+
parentIndex: nil,
|
|
317
|
+
insideMatchedScope: false
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
let labels = nodes.compactMap { $0.label }
|
|
321
|
+
XCTAssertEqual(
|
|
322
|
+
labels,
|
|
323
|
+
["Blue Sky", "Callstack", "Welcome back", "Email", "Password", "Sign in", "Forgot password?"]
|
|
324
|
+
)
|
|
325
|
+
XCTAssertFalse(labels.contains("Admin settings"))
|
|
326
|
+
}
|
|
327
|
+
}
|