@wdio/appium-service 9.23.3 → 9.25.0

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.
Files changed (75) hide show
  1. package/README.md +639 -0
  2. package/build/index.d.ts +2 -1
  3. package/build/index.d.ts.map +1 -1
  4. package/build/index.js +2507 -52
  5. package/build/launcher.d.ts +1 -1
  6. package/build/launcher.d.ts.map +1 -1
  7. package/build/mobileSelectorPerformanceOptimizer/aggregator.d.ts +16 -0
  8. package/build/mobileSelectorPerformanceOptimizer/aggregator.d.ts.map +1 -0
  9. package/build/mobileSelectorPerformanceOptimizer/markdown-formatter.d.ts +11 -0
  10. package/build/mobileSelectorPerformanceOptimizer/markdown-formatter.d.ts.map +1 -0
  11. package/build/mobileSelectorPerformanceOptimizer/mspo-reporter.d.ts +45 -0
  12. package/build/mobileSelectorPerformanceOptimizer/mspo-reporter.d.ts.map +1 -0
  13. package/build/mobileSelectorPerformanceOptimizer/mspo-service.d.ts +27 -0
  14. package/build/mobileSelectorPerformanceOptimizer/mspo-service.d.ts.map +1 -0
  15. package/build/mobileSelectorPerformanceOptimizer/mspo-store.d.ts +50 -0
  16. package/build/mobileSelectorPerformanceOptimizer/mspo-store.d.ts.map +1 -0
  17. package/build/mobileSelectorPerformanceOptimizer/optimizer.d.ts +10 -0
  18. package/build/mobileSelectorPerformanceOptimizer/optimizer.d.ts.map +1 -0
  19. package/build/mobileSelectorPerformanceOptimizer/overwrite.d.ts +6 -0
  20. package/build/mobileSelectorPerformanceOptimizer/overwrite.d.ts.map +1 -0
  21. package/build/mobileSelectorPerformanceOptimizer/reporting-types.d.ts +48 -0
  22. package/build/mobileSelectorPerformanceOptimizer/reporting-types.d.ts.map +1 -0
  23. package/build/mobileSelectorPerformanceOptimizer/types.d.ts +53 -0
  24. package/build/mobileSelectorPerformanceOptimizer/types.d.ts.map +1 -0
  25. package/build/mobileSelectorPerformanceOptimizer/utils/browser-utils.d.ts +6 -0
  26. package/build/mobileSelectorPerformanceOptimizer/utils/browser-utils.d.ts.map +1 -0
  27. package/build/mobileSelectorPerformanceOptimizer/utils/command-timing.d.ts +10 -0
  28. package/build/mobileSelectorPerformanceOptimizer/utils/command-timing.d.ts.map +1 -0
  29. package/build/mobileSelectorPerformanceOptimizer/utils/constants.d.ts +14 -0
  30. package/build/mobileSelectorPerformanceOptimizer/utils/constants.d.ts.map +1 -0
  31. package/build/mobileSelectorPerformanceOptimizer/utils/formatting.d.ts +15 -0
  32. package/build/mobileSelectorPerformanceOptimizer/utils/formatting.d.ts.map +1 -0
  33. package/build/mobileSelectorPerformanceOptimizer/utils/index.d.ts +22 -0
  34. package/build/mobileSelectorPerformanceOptimizer/utils/index.d.ts.map +1 -0
  35. package/build/mobileSelectorPerformanceOptimizer/utils/optimization.d.ts +8 -0
  36. package/build/mobileSelectorPerformanceOptimizer/utils/optimization.d.ts.map +1 -0
  37. package/build/mobileSelectorPerformanceOptimizer/utils/performance-data.d.ts +10 -0
  38. package/build/mobileSelectorPerformanceOptimizer/utils/performance-data.d.ts.map +1 -0
  39. package/build/mobileSelectorPerformanceOptimizer/utils/reporter.d.ts +16 -0
  40. package/build/mobileSelectorPerformanceOptimizer/utils/reporter.d.ts.map +1 -0
  41. package/build/mobileSelectorPerformanceOptimizer/utils/selector-location.d.ts +20 -0
  42. package/build/mobileSelectorPerformanceOptimizer/utils/selector-location.d.ts.map +1 -0
  43. package/build/mobileSelectorPerformanceOptimizer/utils/selector-testing.d.ts +10 -0
  44. package/build/mobileSelectorPerformanceOptimizer/utils/selector-testing.d.ts.map +1 -0
  45. package/build/mobileSelectorPerformanceOptimizer/utils/selector-utils.d.ts +37 -0
  46. package/build/mobileSelectorPerformanceOptimizer/utils/selector-utils.d.ts.map +1 -0
  47. package/build/mobileSelectorPerformanceOptimizer/utils/test-context.d.ts +24 -0
  48. package/build/mobileSelectorPerformanceOptimizer/utils/test-context.d.ts.map +1 -0
  49. package/build/mobileSelectorPerformanceOptimizer/utils/timing.d.ts +7 -0
  50. package/build/mobileSelectorPerformanceOptimizer/utils/timing.d.ts.map +1 -0
  51. package/build/mobileSelectorPerformanceOptimizer/utils/xpath-class-chain.d.ts +10 -0
  52. package/build/mobileSelectorPerformanceOptimizer/utils/xpath-class-chain.d.ts.map +1 -0
  53. package/build/mobileSelectorPerformanceOptimizer/utils/xpath-conditions.d.ts +26 -0
  54. package/build/mobileSelectorPerformanceOptimizer/utils/xpath-conditions.d.ts.map +1 -0
  55. package/build/mobileSelectorPerformanceOptimizer/utils/xpath-constants.d.ts +64 -0
  56. package/build/mobileSelectorPerformanceOptimizer/utils/xpath-constants.d.ts.map +1 -0
  57. package/build/mobileSelectorPerformanceOptimizer/utils/xpath-converter.d.ts +14 -0
  58. package/build/mobileSelectorPerformanceOptimizer/utils/xpath-converter.d.ts.map +1 -0
  59. package/build/mobileSelectorPerformanceOptimizer/utils/xpath-detection.d.ts +30 -0
  60. package/build/mobileSelectorPerformanceOptimizer/utils/xpath-detection.d.ts.map +1 -0
  61. package/build/mobileSelectorPerformanceOptimizer/utils/xpath-page-source-executor.d.ts +30 -0
  62. package/build/mobileSelectorPerformanceOptimizer/utils/xpath-page-source-executor.d.ts.map +1 -0
  63. package/build/mobileSelectorPerformanceOptimizer/utils/xpath-page-source.d.ts +20 -0
  64. package/build/mobileSelectorPerformanceOptimizer/utils/xpath-page-source.d.ts.map +1 -0
  65. package/build/mobileSelectorPerformanceOptimizer/utils/xpath-parser.d.ts +9 -0
  66. package/build/mobileSelectorPerformanceOptimizer/utils/xpath-parser.d.ts.map +1 -0
  67. package/build/mobileSelectorPerformanceOptimizer/utils/xpath-predicate.d.ts +18 -0
  68. package/build/mobileSelectorPerformanceOptimizer/utils/xpath-predicate.d.ts.map +1 -0
  69. package/build/mobileSelectorPerformanceOptimizer/utils/xpath-selector-builder.d.ts +11 -0
  70. package/build/mobileSelectorPerformanceOptimizer/utils/xpath-selector-builder.d.ts.map +1 -0
  71. package/build/mobileSelectorPerformanceOptimizer/utils/xpath-types.d.ts +68 -0
  72. package/build/mobileSelectorPerformanceOptimizer/utils/xpath-types.d.ts.map +1 -0
  73. package/build/types.d.ts +46 -0
  74. package/build/types.d.ts.map +1 -1
  75. package/package.json +9 -6
package/build/index.js CHANGED
@@ -1,55 +1,2048 @@
1
1
  // src/launcher.ts
2
2
  import os from "node:os";
3
- import fs from "node:fs";
3
+ import fs3 from "node:fs";
4
4
  import fsp from "node:fs/promises";
5
5
  import url from "node:url";
6
- import path from "node:path";
6
+ import path5 from "node:path";
7
7
  import { spawn } from "node:child_process";
8
8
  import { promisify } from "node:util";
9
- import logger from "@wdio/logger";
9
+ import logger9 from "@wdio/logger";
10
10
  import getPort from "get-port";
11
11
  import { resolve as resolve2 } from "import-meta-resolve";
12
12
  import { isCloudCapability } from "@wdio/config";
13
- import { SevereServiceError } from "webdriverio";
13
+ import { SevereServiceError as SevereServiceError3 } from "webdriverio";
14
14
  import { isAppiumCapability } from "@wdio/utils";
15
15
 
16
- // src/utils.ts
17
- import { basename, join, resolve } from "node:path";
18
- import { kebabCase } from "change-case";
19
- var FILE_EXTENSION_REGEX = /\.[0-9a-z]+$/i;
20
- function getFilePath(filePath, defaultFilename) {
21
- let absolutePath = resolve(filePath);
22
- if (!FILE_EXTENSION_REGEX.test(basename(absolutePath))) {
23
- absolutePath = join(absolutePath, defaultFilename);
16
+ // src/utils.ts
17
+ import { basename, join, resolve } from "node:path";
18
+ import { kebabCase } from "change-case";
19
+ var FILE_EXTENSION_REGEX = /\.[0-9a-z]+$/i;
20
+ function getFilePath(filePath, defaultFilename) {
21
+ let absolutePath = resolve(filePath);
22
+ if (!FILE_EXTENSION_REGEX.test(basename(absolutePath))) {
23
+ absolutePath = join(absolutePath, defaultFilename);
24
+ }
25
+ return absolutePath;
26
+ }
27
+ function formatCliArgs(args) {
28
+ const cliArgs = [];
29
+ for (const key in args) {
30
+ const value = args[key];
31
+ if (typeof value === "boolean" && !value || value === null) {
32
+ continue;
33
+ }
34
+ if (key === "chromedriver_autodownload") {
35
+ cliArgs.push(key);
36
+ continue;
37
+ }
38
+ cliArgs.push(`--${kebabCase(key)}`);
39
+ if (typeof value !== "boolean") {
40
+ cliArgs.push(sanitizeCliOptionValue(value));
41
+ }
42
+ }
43
+ return cliArgs;
44
+ }
45
+ function sanitizeCliOptionValue(value) {
46
+ const valueString = typeof value === "object" ? JSON.stringify(value) : String(value);
47
+ return /\s/.test(valueString) ? `'${valueString}'` : valueString;
48
+ }
49
+
50
+ // src/launcher.ts
51
+ import treeKill from "tree-kill";
52
+
53
+ // src/mobileSelectorPerformanceOptimizer/aggregator.ts
54
+ import fs2 from "node:fs";
55
+ import path4 from "node:path";
56
+ import { SevereServiceError as SevereServiceError2 } from "webdriverio";
57
+
58
+ // src/mobileSelectorPerformanceOptimizer/mspo-store.ts
59
+ var currentSuiteName;
60
+ var currentTestFile;
61
+ var currentTestName;
62
+ var currentDeviceName;
63
+ var performanceData = [];
64
+ function setCurrentSuiteName(suiteName) {
65
+ currentSuiteName = suiteName;
66
+ }
67
+ function getCurrentSuiteName() {
68
+ return currentSuiteName;
69
+ }
70
+ function setCurrentTestFile(testFile) {
71
+ currentTestFile = testFile;
72
+ }
73
+ function getCurrentTestFile() {
74
+ return currentTestFile;
75
+ }
76
+ function setCurrentTestName(testName) {
77
+ currentTestName = testName;
78
+ }
79
+ function getCurrentTestName() {
80
+ return currentTestName;
81
+ }
82
+ function setCurrentDeviceName(deviceName) {
83
+ currentDeviceName = deviceName;
84
+ }
85
+ function getCurrentDeviceName() {
86
+ return currentDeviceName;
87
+ }
88
+ function addPerformanceData(data) {
89
+ performanceData.push(data);
90
+ }
91
+ function getPerformanceData() {
92
+ return performanceData;
93
+ }
94
+
95
+ // src/mobileSelectorPerformanceOptimizer/markdown-formatter.ts
96
+ import path3 from "node:path";
97
+
98
+ // src/mobileSelectorPerformanceOptimizer/utils/constants.ts
99
+ var LOG_PREFIX = "Mobile Selector Performance";
100
+ var SINGLE_ELEMENT_COMMANDS = ["$", "custom$"];
101
+ var MULTIPLE_ELEMENT_COMMANDS = ["$$", "custom$$"];
102
+ var USER_COMMANDS = [...SINGLE_ELEMENT_COMMANDS, ...MULTIPLE_ELEMENT_COMMANDS];
103
+ var REPORT_INDENT_SUMMARY = " ";
104
+ var REPORT_INDENT_FILE = " ";
105
+ var REPORT_INDENT_SUITE = " ";
106
+ var REPORT_INDENT_SELECTOR = " ";
107
+
108
+ // src/mobileSelectorPerformanceOptimizer/utils/selector-location.ts
109
+ import path from "node:path";
110
+ import fs from "node:fs";
111
+ import logger from "@wdio/logger";
112
+
113
+ // src/mobileSelectorPerformanceOptimizer/utils/selector-utils.ts
114
+ function extractSelectorFromArgs(args) {
115
+ if (!args || args.length === 0) {
116
+ return null;
117
+ }
118
+ const firstArg = args[0];
119
+ if (typeof firstArg === "string") {
120
+ return firstArg;
121
+ }
122
+ if (typeof firstArg === "object" && firstArg !== null) {
123
+ try {
124
+ return JSON.stringify(firstArg);
125
+ } catch {
126
+ return String(firstArg);
127
+ }
128
+ }
129
+ return String(firstArg);
130
+ }
131
+ function isXPathSelector(selector) {
132
+ if (typeof selector !== "string") {
133
+ return false;
134
+ }
135
+ if (selector.startsWith("/") || selector.startsWith("../") || selector.startsWith("./") || selector.startsWith("*/")) {
136
+ return true;
137
+ }
138
+ if (selector.startsWith("(")) {
139
+ if (selector.startsWith("(:")) {
140
+ return false;
141
+ }
142
+ return selector.includes("/") || selector.includes("@");
143
+ }
144
+ return false;
145
+ }
146
+ function parseOptimizedSelector(optimizedSelector) {
147
+ if (optimizedSelector.startsWith("~")) {
148
+ return {
149
+ using: "accessibility id",
150
+ value: optimizedSelector.substring(1)
151
+ };
152
+ }
153
+ if (optimizedSelector.startsWith("-ios predicate string:")) {
154
+ return {
155
+ using: "-ios predicate string",
156
+ value: optimizedSelector.substring("-ios predicate string:".length)
157
+ };
158
+ }
159
+ if (optimizedSelector.startsWith("-ios class chain:")) {
160
+ return {
161
+ using: "-ios class chain",
162
+ value: optimizedSelector.substring("-ios class chain:".length)
163
+ };
164
+ }
165
+ return null;
166
+ }
167
+
168
+ // src/mobileSelectorPerformanceOptimizer/utils/selector-location.ts
169
+ var log = logger("@wdio/appium-service:selector-optimizer");
170
+ function findSelectorInFile(filePath, selector) {
171
+ try {
172
+ if (!fs.existsSync(filePath)) {
173
+ return void 0;
174
+ }
175
+ const content = fs.readFileSync(filePath, "utf-8");
176
+ const lines = content.split("\n");
177
+ for (let i = 0; i < lines.length; i++) {
178
+ const line = lines[i];
179
+ if (line.includes(selector) || line.includes(`'${selector}'`) || line.includes(`"${selector}"`) || line.includes(`\`${selector}\``)) {
180
+ return i + 1;
181
+ }
182
+ }
183
+ return void 0;
184
+ } catch {
185
+ return void 0;
186
+ }
187
+ }
188
+ function findPotentialPageObjects(testFile) {
189
+ const testDir = path.dirname(testFile);
190
+ const testBasename = path.basename(testFile);
191
+ const ext = path.extname(testFile);
192
+ const baseName = testBasename.replace(/\.(spec|test|e2e)/, "").replace(ext, "");
193
+ const potentialFiles = [];
194
+ const pageObjectDirs = ["pageobjects", "pageObjects", "page-objects", "pages", "page_objects"];
195
+ let currentDir = testDir;
196
+ for (let i = 0; i < 5; i++) {
197
+ for (const poDir of pageObjectDirs) {
198
+ const pageObjectDir = path.join(currentDir, poDir);
199
+ if (fs.existsSync(pageObjectDir)) {
200
+ const patterns = [
201
+ `${baseName}.page${ext}`,
202
+ `${baseName}.po${ext}`,
203
+ `${baseName}Page${ext}`,
204
+ `${baseName}${ext}`
205
+ ];
206
+ for (const pattern of patterns) {
207
+ const fullPath = path.join(pageObjectDir, pattern);
208
+ if (fs.existsSync(fullPath)) {
209
+ potentialFiles.push(fullPath);
210
+ }
211
+ }
212
+ }
213
+ }
214
+ const parentDir = path.dirname(currentDir);
215
+ if (parentDir === currentDir) {
216
+ break;
217
+ }
218
+ currentDir = parentDir;
219
+ }
220
+ return potentialFiles;
221
+ }
222
+ function findFilesInDirectory(dirPath, maxDepth = 5, currentDepth = 0) {
223
+ const files = [];
224
+ if (currentDepth >= maxDepth) {
225
+ return files;
226
+ }
227
+ try {
228
+ if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) {
229
+ return files;
230
+ }
231
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
232
+ for (const entry of entries) {
233
+ const fullPath = path.join(dirPath, entry.name);
234
+ if (entry.isDirectory()) {
235
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) {
236
+ continue;
237
+ }
238
+ files.push(...findFilesInDirectory(fullPath, maxDepth, currentDepth + 1));
239
+ } else if (entry.isFile() && /\.(js|ts|jsx|tsx)$/.test(entry.name)) {
240
+ files.push(fullPath);
241
+ }
242
+ }
243
+ } catch {
244
+ }
245
+ return files;
246
+ }
247
+ function findPageObjectFilesFromConfig(pageObjectPaths) {
248
+ const files = [];
249
+ for (const configPath of pageObjectPaths) {
250
+ try {
251
+ const resolvedPath = path.isAbsolute(configPath) ? configPath : path.resolve(process.cwd(), configPath);
252
+ const stat = fs.statSync(resolvedPath);
253
+ if (stat.isDirectory()) {
254
+ files.push(...findFilesInDirectory(resolvedPath));
255
+ } else if (stat.isFile() && /\.(js|ts|jsx|tsx)$/.test(resolvedPath)) {
256
+ files.push(resolvedPath);
257
+ }
258
+ } catch {
259
+ }
260
+ }
261
+ return files;
262
+ }
263
+ function findSelectorLocation(testFile, selector, pageObjectPaths) {
264
+ if (!testFile || !selector) {
265
+ log.debug("[Selector Location] No test file or selector provided");
266
+ return [];
267
+ }
268
+ if (!isXPathSelector(selector)) {
269
+ log.debug(`[Selector Location] Skipping non-XPath selector: ${selector}`);
270
+ return [];
271
+ }
272
+ try {
273
+ const locations = [];
274
+ log.debug(`[Selector Location] Searching for XPath selector: ${selector}`);
275
+ log.debug(`[Selector Location] Starting with test file: ${testFile}`);
276
+ const testFileLine = findSelectorInFile(testFile, selector);
277
+ if (testFileLine) {
278
+ log.debug(`[Selector Location] Found in test file at line ${testFileLine}`);
279
+ locations.push({
280
+ file: testFile,
281
+ line: testFileLine,
282
+ isPageObject: false
283
+ });
284
+ }
285
+ log.debug("[Selector Location] Searching page objects...");
286
+ const pageObjectFiles = pageObjectPaths && pageObjectPaths.length > 0 ? findPageObjectFilesFromConfig(pageObjectPaths) : findPotentialPageObjects(testFile);
287
+ if (pageObjectPaths && pageObjectPaths.length > 0) {
288
+ log.debug("[Selector Location] Using configured page object paths:");
289
+ pageObjectPaths.forEach((p) => {
290
+ log.debug(`[Selector Location] - ${p}`);
291
+ });
292
+ }
293
+ if (pageObjectFiles.length > 0) {
294
+ log.debug(`[Selector Location] Found ${pageObjectFiles.length} page object file(s) to search`);
295
+ } else {
296
+ log.debug("[Selector Location] No page object files found");
297
+ }
298
+ for (const pageObjectFile of pageObjectFiles) {
299
+ const pageObjectLine = findSelectorInFile(pageObjectFile, selector);
300
+ if (pageObjectLine) {
301
+ log.debug(`[Selector Location] Found in page object at ${pageObjectFile}:${pageObjectLine}`);
302
+ locations.push({
303
+ file: pageObjectFile,
304
+ line: pageObjectLine,
305
+ isPageObject: true
306
+ });
307
+ }
308
+ }
309
+ if (locations.length === 0) {
310
+ log.debug("[Selector Location] Selector not found in test file or page objects");
311
+ } else {
312
+ log.debug(`[Selector Location] Found ${locations.length} location(s)`);
313
+ }
314
+ return locations;
315
+ } catch (error) {
316
+ log.debug(`[Selector Location] Error: ${error instanceof Error ? error.message : String(error)}`);
317
+ return [];
318
+ }
319
+ }
320
+
321
+ // src/mobileSelectorPerformanceOptimizer/utils/browser-utils.ts
322
+ import logger2 from "@wdio/logger";
323
+ var log2 = logger2("@wdio/appium-service:selector-optimizer");
324
+ function isNativeContext(browser) {
325
+ if (!browser) {
326
+ return false;
327
+ }
328
+ try {
329
+ const browserWithNativeContext = browser;
330
+ if ("instances" in browser && Array.isArray(browser.instances)) {
331
+ log2.warn("Mobile Selector Performance Optimizer does not support MultiRemote sessions yet. Feature disabled for this session.");
332
+ return false;
333
+ }
334
+ return browserWithNativeContext.isNativeContext === true;
335
+ } catch {
336
+ return false;
337
+ }
338
+ }
339
+
340
+ // src/mobileSelectorPerformanceOptimizer/utils/timing.ts
341
+ function getHighResTime() {
342
+ return performance.now();
343
+ }
344
+
345
+ // src/mobileSelectorPerformanceOptimizer/utils/selector-testing.ts
346
+ import logger3 from "@wdio/logger";
347
+ var log3 = logger3("@wdio/appium-service:selector-optimizer");
348
+ async function extractMatchingElementsFromPageSource(browser, using, value) {
349
+ try {
350
+ const browserWithPageSource = browser;
351
+ const pageSource = await browserWithPageSource.getPageSource();
352
+ if (!pageSource || typeof pageSource !== "string") {
353
+ return [];
354
+ }
355
+ const matchingElements = [];
356
+ if (using === "-ios predicate string") {
357
+ const typeMatch = value.match(/type\s*==\s*'([^']+)'/);
358
+ const elementType = typeMatch ? typeMatch[1] : null;
359
+ const conditions = [];
360
+ const attrPattern = /(\w+)\s*==\s*'([^']+)'/g;
361
+ let attrMatch;
362
+ while ((attrMatch = attrPattern.exec(value)) !== null) {
363
+ if (attrMatch[1] !== "type") {
364
+ conditions.push({ attr: attrMatch[1], value: attrMatch[2] });
365
+ }
366
+ }
367
+ if (!elementType) {
368
+ return [];
369
+ }
370
+ const elementPattern = new RegExp(`<${elementType.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([^>]*)>`, "gi");
371
+ let match;
372
+ while ((match = elementPattern.exec(pageSource)) !== null) {
373
+ const attrs = match[1] || "";
374
+ let matches = true;
375
+ for (const condition of conditions) {
376
+ const attrPattern2 = new RegExp(`${condition.attr}="([^"]*)"`, "i");
377
+ const attrMatch2 = attrs.match(attrPattern2);
378
+ if (!attrMatch2 || attrMatch2[1] !== condition.value) {
379
+ matches = false;
380
+ break;
381
+ }
382
+ }
383
+ if (matches) {
384
+ matchingElements.push(match[0]);
385
+ }
386
+ }
387
+ } else if (using === "-ios class chain") {
388
+ const typeMatch = value.match(/^\*\*\/(\w+)/);
389
+ const elementType = typeMatch ? typeMatch[1] : null;
390
+ if (elementType) {
391
+ const predicateMatch = value.match(/\[`([^`]+)`\]/);
392
+ const conditions = [];
393
+ if (predicateMatch) {
394
+ const predicateContent = predicateMatch[1];
395
+ const attrPattern = /(\w+)\s*==\s*"([^"]+)"/g;
396
+ let attrMatch;
397
+ while ((attrMatch = attrPattern.exec(predicateContent)) !== null) {
398
+ conditions.push({ attr: attrMatch[1], value: attrMatch[2] });
399
+ }
400
+ }
401
+ const elementPattern = new RegExp(`<${elementType.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([^>]*)>`, "gi");
402
+ let match;
403
+ while ((match = elementPattern.exec(pageSource)) !== null) {
404
+ const attrs = match[1] || "";
405
+ let matches = true;
406
+ for (const condition of conditions) {
407
+ const attrPattern = new RegExp(`${condition.attr}="([^"]*)"`, "i");
408
+ const attrMatch = attrs.match(attrPattern);
409
+ if (!attrMatch || attrMatch[1] !== condition.value) {
410
+ matches = false;
411
+ break;
412
+ }
413
+ }
414
+ if (matches) {
415
+ matchingElements.push(match[0]);
416
+ }
417
+ }
418
+ }
419
+ }
420
+ return matchingElements;
421
+ } catch {
422
+ return [];
423
+ }
424
+ }
425
+ async function testOptimizedSelector(browser, using, value, isMultiple, debug = false) {
426
+ try {
427
+ if (debug) {
428
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 1: Preparing to call findElement${isMultiple ? "s" : ""}`);
429
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 1.1: Using strategy: "${using}"`);
430
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 1.2: Selector value: "${value}"`);
431
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 1.3: Multiple elements: ${isMultiple}`);
432
+ }
433
+ const startTime = getHighResTime();
434
+ const browserWithProtocol = browser;
435
+ if (debug) {
436
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 2: Executing findElement${isMultiple ? "s" : ""}(${JSON.stringify(using)}, ${JSON.stringify(value)})`);
437
+ }
438
+ let elementRefs = [];
439
+ let duration;
440
+ if (isMultiple) {
441
+ const result = await browserWithProtocol.findElements(using, value);
442
+ duration = getHighResTime() - startTime;
443
+ elementRefs = Array.isArray(result) ? result : [];
444
+ if (debug) {
445
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 3: findElements() completed`);
446
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 3.1: Found ${elementRefs.length} element(s)`);
447
+ if (elementRefs.length > 0) {
448
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 3.2: Element reference(s): ${JSON.stringify(elementRefs)}`);
449
+ } else {
450
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 3.2: No elements found - selector may not match any elements`);
451
+ }
452
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 3.3: Execution time: ${duration.toFixed(2)}ms`);
453
+ }
454
+ } else {
455
+ const result = await browserWithProtocol.findElement(using, value);
456
+ duration = getHighResTime() - startTime;
457
+ const isError = result && typeof result === "object" && "error" in result;
458
+ const isValidElement = result && !isError && ("ELEMENT" in result || "element-6066-11e4-a52e-4f735466cecf" in result);
459
+ elementRefs = isValidElement ? [result] : [];
460
+ if (debug) {
461
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 3: findElement() completed`);
462
+ if (isError) {
463
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 3.1: Element NOT found - error returned`);
464
+ const errorMsg = result.message || result.error || "Unknown error";
465
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 3.2: Error details: ${errorMsg}`);
466
+ } else if (isValidElement) {
467
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 3.1: Element found successfully`);
468
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 3.2: Element reference: ${JSON.stringify(result)}`);
469
+ } else {
470
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 3.1: No element found - selector may not match any element`);
471
+ }
472
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 3.3: Execution time: ${duration.toFixed(2)}ms`);
473
+ }
474
+ }
475
+ if (debug) {
476
+ if (elementRefs.length > 0) {
477
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 4: Verification successful - selector is valid and found element(s)`);
478
+ }
479
+ if (elementRefs.length === 0) {
480
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 4: Verification failed - selector did not find any element(s)`);
481
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 5: Collecting fresh page source to investigate...`);
482
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 5.0: Searching for elements matching: ${using}="${value}"`);
483
+ const matchingElements = await extractMatchingElementsFromPageSource(browser, using, value);
484
+ if (matchingElements.length > 0) {
485
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 5.1: Found ${matchingElements.length} matching element(s) in fresh page source:`);
486
+ matchingElements.forEach((element, index) => {
487
+ const truncated = element.length > 200 ? element.substring(0, 200) + "..." : element;
488
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 5.1.${index + 1}: ${truncated}`);
489
+ });
490
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 5.2: Retrying selector with fresh page source state...`);
491
+ const retryStartTime = getHighResTime();
492
+ try {
493
+ if (isMultiple) {
494
+ const retryResult = await browserWithProtocol.findElements(using, value);
495
+ const retryDuration = getHighResTime() - retryStartTime;
496
+ const retryElementRefs = Array.isArray(retryResult) ? retryResult : [];
497
+ if (retryElementRefs.length > 0) {
498
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 5.3: Retry successful! Found ${retryElementRefs.length} element(s) in ${retryDuration.toFixed(2)}ms`);
499
+ return { elementRefs: retryElementRefs, duration: retryDuration };
500
+ }
501
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 5.3: Retry failed - still no elements found (${retryDuration.toFixed(2)}ms)`);
502
+ } else {
503
+ const retryResult = await browserWithProtocol.findElement(using, value);
504
+ const retryDuration = getHighResTime() - retryStartTime;
505
+ const isError = retryResult && typeof retryResult === "object" && "error" in retryResult;
506
+ const isValidElement = retryResult && !isError && ("ELEMENT" in retryResult || "element-6066-11e4-a52e-4f735466cecf" in retryResult);
507
+ const retryElementRefs = isValidElement ? [retryResult] : [];
508
+ if (retryElementRefs.length > 0) {
509
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 5.3: Retry successful! Found element in ${retryDuration.toFixed(2)}ms`);
510
+ return { elementRefs: retryElementRefs, duration: retryDuration };
511
+ }
512
+ const errorMsg = isError ? retryResult.message || retryResult.error || "Unknown error" : "No element found";
513
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 5.3: Retry failed - ${errorMsg} (${retryDuration.toFixed(2)}ms)`);
514
+ }
515
+ } catch (retryError) {
516
+ const retryDuration = getHighResTime() - retryStartTime;
517
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 5.3: Retry threw error: ${retryError instanceof Error ? retryError.message : String(retryError)} (${retryDuration.toFixed(2)}ms)`);
518
+ }
519
+ } else {
520
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 5.1: No matching elements found in fresh page source - element may have disappeared`);
521
+ }
522
+ }
523
+ }
524
+ return { elementRefs, duration };
525
+ } catch (error) {
526
+ if (debug) {
527
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 3: findElement${isMultiple ? "s" : ""}() threw an error`);
528
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 3.1: Error: ${error instanceof Error ? error.message : String(error)}`);
529
+ log3.debug(`[${LOG_PREFIX}: Debug] Step 4: Verification failed - selector execution error`);
530
+ }
531
+ return null;
532
+ }
533
+ }
534
+
535
+ // src/mobileSelectorPerformanceOptimizer/utils/optimization.ts
536
+ import logger7 from "@wdio/logger";
537
+
538
+ // src/mobileSelectorPerformanceOptimizer/utils/xpath-converter.ts
539
+ import logger6 from "@wdio/logger";
540
+
541
+ // src/mobileSelectorPerformanceOptimizer/utils/xpath-constants.ts
542
+ var UNMAPPABLE_XPATH_AXES = [
543
+ { pattern: /ancestor::/i, name: "ancestor axis" },
544
+ { pattern: /ancestor-or-self::/i, name: "ancestor-or-self axis" },
545
+ { pattern: /following-sibling::/i, name: "following-sibling axis" },
546
+ { pattern: /preceding-sibling::/i, name: "preceding-sibling axis" },
547
+ { pattern: /following::/i, name: "following axis" },
548
+ { pattern: /preceding::/i, name: "preceding axis" },
549
+ { pattern: /parent::/i, name: "parent axis" },
550
+ { pattern: /\/\.\.(?:\/|$|\[|\))/, name: "parent axis" }
551
+ ];
552
+ var UNMAPPABLE_XPATH_FUNCTIONS = [
553
+ { pattern: /normalize-space\(/i, name: "normalize-space() function" },
554
+ { pattern: /position\(\)/i, name: "position() function" },
555
+ { pattern: /count\(/i, name: "count() function" }
556
+ ];
557
+ var ATTRIBUTE_PRIORITY = ["name", "label", "value", "enabled", "visible", "accessible", "hittable"];
558
+ var MEANINGFUL_ATTRIBUTES = ["name", "label", "value"];
559
+ var BOOLEAN_ATTRIBUTES = ["enabled", "visible", "accessible", "hittable"];
560
+
561
+ // src/mobileSelectorPerformanceOptimizer/utils/xpath-detection.ts
562
+ function detectUnmappableXPathFeatures(xpath2) {
563
+ const unmappableFeatures = [];
564
+ for (const axis of UNMAPPABLE_XPATH_AXES) {
565
+ if (axis.pattern.test(xpath2)) {
566
+ unmappableFeatures.push(axis.name);
567
+ }
568
+ }
569
+ for (const func of UNMAPPABLE_XPATH_FUNCTIONS) {
570
+ if (func.pattern.test(xpath2)) {
571
+ unmappableFeatures.push(func.name);
572
+ }
573
+ }
574
+ if (/substring\([^)]+\)/.test(xpath2)) {
575
+ const textSubstringMatch = xpath2.match(/substring\(text\(\),\s*1\s*,\s*\d+\)/i);
576
+ const attrSubstringMatch = xpath2.match(/substring\(@\w+,\s*1\s*,\s*\d+\)/i);
577
+ if (!textSubstringMatch && !attrSubstringMatch) {
578
+ const substringMatch = xpath2.match(/substring\([^,]+,\s*(\d+)/i);
579
+ if (substringMatch && substringMatch[1] !== "1") {
580
+ unmappableFeatures.push("complex substring() function (not starting at position 1)");
581
+ }
582
+ }
583
+ }
584
+ if (containsUnionOperator(xpath2)) {
585
+ unmappableFeatures.push("union operator (|)");
586
+ }
587
+ return unmappableFeatures;
588
+ }
589
+ function containsUnionOperator(xpath2) {
590
+ let depth = 0;
591
+ let inSingleQuote = false;
592
+ let inDoubleQuote = false;
593
+ for (let i = 0; i < xpath2.length; i++) {
594
+ const char = xpath2[i];
595
+ if (char === "'" && !inDoubleQuote) {
596
+ inSingleQuote = !inSingleQuote;
597
+ } else if (char === '"' && !inSingleQuote) {
598
+ inDoubleQuote = !inDoubleQuote;
599
+ } else if (!inSingleQuote && !inDoubleQuote) {
600
+ if (char === "[" || char === "(") {
601
+ depth++;
602
+ } else if (char === "]" || char === ")") {
603
+ depth--;
604
+ } else if (char === "|" && depth === 0) {
605
+ return true;
606
+ }
607
+ }
608
+ }
609
+ return false;
610
+ }
611
+
612
+ // src/mobileSelectorPerformanceOptimizer/utils/xpath-page-source.ts
613
+ import logger4 from "@wdio/logger";
614
+ var log4 = logger4("@wdio/appium-service:selector-optimizer");
615
+ function isSelectorUniqueInPageSource(selector, pageSource) {
616
+ try {
617
+ if (selector.startsWith("~")) {
618
+ return isAccessibilityIdUnique(selector.substring(1), pageSource);
619
+ } else if (selector.startsWith("-ios predicate string:")) {
620
+ const predicateString = selector.substring("-ios predicate string:".length);
621
+ return countMatchingElementsByPredicate(predicateString, pageSource) === 1;
622
+ } else if (selector.startsWith("-ios class chain:")) {
623
+ const chainString = selector.substring("-ios class chain:".length);
624
+ return countMatchingElementsByClassChain(chainString, pageSource) === 1;
625
+ }
626
+ return false;
627
+ } catch (error) {
628
+ log4.debug(`Selector uniqueness check failed: ${error instanceof Error ? error.message : String(error)}`);
629
+ return false;
630
+ }
631
+ }
632
+ function isAccessibilityIdUnique(value, pageSource) {
633
+ const escapedValue = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
634
+ const namePattern = new RegExp(`<\\w+[^>]*\\s+name="${escapedValue}"[^>]*>`, "gi");
635
+ const labelPattern = new RegExp(`<\\w+[^>]*\\s+label="${escapedValue}"[^>]*>`, "gi");
636
+ const nameMatches = pageSource.match(namePattern) || [];
637
+ const labelMatches = pageSource.match(labelPattern) || [];
638
+ const allMatches = /* @__PURE__ */ new Set([...nameMatches, ...labelMatches]);
639
+ return allMatches.size === 1;
640
+ }
641
+ function countMatchingElementsByPredicate(predicateString, pageSource) {
642
+ const conditions = parsePredicateConditions(predicateString);
643
+ const typeMatch = predicateString.match(/type\s*==\s*'([^']+)'/);
644
+ const elementType = typeMatch ? typeMatch[1] : null;
645
+ const elementPattern = elementType ? new RegExp(`<${elementType.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([^>]*)>`, "gi") : /<(\w+)([^>]*)>/gi;
646
+ let match;
647
+ let count = 0;
648
+ while ((match = elementPattern.exec(pageSource)) !== null) {
649
+ const attrs = match[1] || match[2] || "";
650
+ if (elementType && !match[0].includes(`<${elementType}`)) {
651
+ continue;
652
+ }
653
+ if (matchesPredicateConditions(attrs, conditions)) {
654
+ count++;
655
+ }
656
+ }
657
+ return count;
658
+ }
659
+ function parsePredicateConditions(predicateString) {
660
+ const conditions = [];
661
+ const attrPattern = /(\w+)\s*==\s*'([^']+)'/g;
662
+ let attrMatch;
663
+ while ((attrMatch = attrPattern.exec(predicateString)) !== null) {
664
+ if (attrMatch[1] !== "type") {
665
+ conditions.push({ attr: attrMatch[1], op: "==", value: attrMatch[2] });
666
+ }
667
+ }
668
+ return conditions;
669
+ }
670
+ function matchesPredicateConditions(attrs, conditions) {
671
+ for (const condition of conditions) {
672
+ const attrPattern = new RegExp(`${condition.attr}="([^"]*)"`, "i");
673
+ const attrMatch = attrs.match(attrPattern);
674
+ if (!attrMatch || attrMatch[1] !== condition.value) {
675
+ return false;
676
+ }
677
+ }
678
+ return true;
679
+ }
680
+ function countMatchingElementsByClassChain(chainString, pageSource) {
681
+ const typeMatch = chainString.match(/^\*\*\/(\w+)/);
682
+ const elementType = typeMatch ? typeMatch[1] : null;
683
+ if (!elementType) {
684
+ return 0;
685
+ }
686
+ const predicateMatch = chainString.match(/\[`([^`]+)`\]/);
687
+ const conditions = [];
688
+ if (predicateMatch) {
689
+ const predicateContent = predicateMatch[1];
690
+ const attrPattern = /(\w+)\s*==\s*"([^"]+)"/g;
691
+ let attrMatch;
692
+ while ((attrMatch = attrPattern.exec(predicateContent)) !== null) {
693
+ conditions.push({ attr: attrMatch[1], op: "==", value: attrMatch[2] });
694
+ }
695
+ }
696
+ const elementPattern = new RegExp(`<${elementType.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([^>]*)>`, "gi");
697
+ let match;
698
+ let count = 0;
699
+ while ((match = elementPattern.exec(pageSource)) !== null) {
700
+ const attrs = match[1] || "";
701
+ let matches = true;
702
+ for (const condition of conditions) {
703
+ const attrPattern = new RegExp(`${condition.attr}="([^"]*)"`, "i");
704
+ const attrMatch = attrs.match(attrPattern);
705
+ if (!attrMatch || attrMatch[1] !== condition.value) {
706
+ matches = false;
707
+ break;
708
+ }
709
+ }
710
+ if (matches) {
711
+ count++;
712
+ }
713
+ }
714
+ return count;
715
+ }
716
+
717
+ // src/mobileSelectorPerformanceOptimizer/utils/xpath-selector-builder.ts
718
+ function buildSelectorFromElementData(elementData, pageSource) {
719
+ const { type, attributes } = elementData;
720
+ const name = attributes.name || attributes.label;
721
+ if (name) {
722
+ const accessibilitySelector = `~${name}`;
723
+ if (isSelectorUniqueInPageSource(accessibilitySelector, pageSource)) {
724
+ return { selector: accessibilitySelector };
725
+ }
726
+ }
727
+ if (type) {
728
+ const predicateResult = buildUniquePredicateString(type, attributes, pageSource);
729
+ if (predicateResult) {
730
+ return predicateResult;
731
+ }
732
+ }
733
+ if (type) {
734
+ const classChainResult = buildUniqueClassChain(type, attributes, pageSource);
735
+ if (classChainResult) {
736
+ return classChainResult;
737
+ }
738
+ }
739
+ return {
740
+ selector: null,
741
+ warning: "Could not generate a unique selector from element data. Multiple elements may match the suggested selector."
742
+ };
743
+ }
744
+ function buildUniquePredicateString(type, attributes, pageSource) {
745
+ const predicateParts = [`type == '${type}'`];
746
+ let selector = `-ios predicate string:${predicateParts.join(" AND ")}`;
747
+ if (isSelectorUniqueInPageSource(selector, pageSource)) {
748
+ return { selector };
749
+ }
750
+ const name = attributes.name;
751
+ const label = attributes.label;
752
+ const nameEqualsLabel = name && label && name === label;
753
+ for (const attr of MEANINGFUL_ATTRIBUTES) {
754
+ if (attr === "label" && nameEqualsLabel) {
755
+ continue;
756
+ }
757
+ if (attributes[attr] !== void 0) {
758
+ const value = attributes[attr];
759
+ if (typeof value === "string" && value.length > 0) {
760
+ predicateParts.push(`${attr} == '${value}'`);
761
+ selector = `-ios predicate string:${predicateParts.join(" AND ")}`;
762
+ if (isSelectorUniqueInPageSource(selector, pageSource)) {
763
+ return { selector };
764
+ }
765
+ }
766
+ }
767
+ }
768
+ for (const attr of BOOLEAN_ATTRIBUTES) {
769
+ if (attributes[attr] === "true") {
770
+ predicateParts.push(`${attr} == 'true'`);
771
+ selector = `-ios predicate string:${predicateParts.join(" AND ")}`;
772
+ if (isSelectorUniqueInPageSource(selector, pageSource)) {
773
+ return { selector };
774
+ }
775
+ }
776
+ }
777
+ const meaningfulOnlyParts = predicateParts.filter((part) => {
778
+ const attr = part.split(" == ")[0];
779
+ return !BOOLEAN_ATTRIBUTES.includes(attr);
780
+ });
781
+ if (meaningfulOnlyParts.length > 1) {
782
+ return {
783
+ selector: `-ios predicate string:${meaningfulOnlyParts.join(" AND ")}`,
784
+ warning: "Selector may match multiple elements. Consider adding more specific attributes."
785
+ };
786
+ }
787
+ if (predicateParts.length > 1) {
788
+ return {
789
+ selector: `-ios predicate string:${predicateParts.join(" AND ")}`,
790
+ warning: "Selector may match multiple elements. Consider adding more specific attributes."
791
+ };
792
+ }
793
+ return null;
794
+ }
795
+ function buildUniqueClassChain(type, attributes, pageSource) {
796
+ const chain = `**/${type}`;
797
+ const predicateParts = [];
798
+ for (const attr of ATTRIBUTE_PRIORITY) {
799
+ if (attributes[attr] !== void 0) {
800
+ const value = attributes[attr];
801
+ if (typeof value === "string" && value.length > 0) {
802
+ predicateParts.push(`${attr} == "${value}"`);
803
+ const selector = `-ios class chain:${chain}[\`${predicateParts.join(" AND ")}\`]`;
804
+ if (isSelectorUniqueInPageSource(selector, pageSource)) {
805
+ return { selector };
806
+ }
807
+ }
808
+ }
809
+ }
810
+ if (predicateParts.length > 0) {
811
+ return {
812
+ selector: `-ios class chain:${chain}[\`${predicateParts.join(" AND ")}\`]`,
813
+ warning: "Selector may match multiple elements. Consider adding more specific attributes."
814
+ };
815
+ }
816
+ const basicSelector = `-ios class chain:${chain}`;
817
+ if (isSelectorUniqueInPageSource(basicSelector, pageSource)) {
818
+ return { selector: basicSelector };
819
+ }
820
+ return {
821
+ selector: basicSelector,
822
+ warning: "Selector may match multiple elements. Consider adding more specific attributes."
823
+ };
824
+ }
825
+
826
+ // src/mobileSelectorPerformanceOptimizer/utils/xpath-page-source-executor.ts
827
+ import { DOMParser } from "@xmldom/xmldom";
828
+ import xpath from "xpath";
829
+ import logger5 from "@wdio/logger";
830
+ var log5 = logger5("@wdio/appium-service:selector-optimizer");
831
+ function executeXPathOnPageSource(xpathExpr, pageSource) {
832
+ if (!pageSource || !xpathExpr) {
833
+ return null;
834
+ }
835
+ try {
836
+ const doc = new DOMParser().parseFromString(pageSource, "text/xml");
837
+ const parseErrors = doc.getElementsByTagName("parsererror");
838
+ if (parseErrors.length > 0) {
839
+ log5.debug("XML parsing error in page source");
840
+ return null;
841
+ }
842
+ const nodes = xpath.select(xpathExpr, doc);
843
+ if (!nodes || !Array.isArray(nodes) || nodes.length === 0) {
844
+ return [];
845
+ }
846
+ const elements = [];
847
+ for (const node of nodes) {
848
+ if (node && typeof node === "object" && "nodeName" in node && "attributes" in node) {
849
+ const elementNode = node;
850
+ const attributes = {};
851
+ if (elementNode.attributes) {
852
+ for (let i = 0; i < elementNode.attributes.length; i++) {
853
+ const attr = elementNode.attributes[i];
854
+ if (attr && attr.name && attr.value !== void 0) {
855
+ attributes[attr.name] = attr.value;
856
+ }
857
+ }
858
+ }
859
+ elements.push({
860
+ type: elementNode.nodeName,
861
+ attributes
862
+ });
863
+ }
864
+ }
865
+ return elements;
866
+ } catch (error) {
867
+ log5.debug(`XPath execution failed: ${error instanceof Error ? error.message : String(error)}`);
868
+ return null;
869
+ }
870
+ }
871
+ function findElementByXPathWithFallback(xpathExpr, pageSource) {
872
+ const elements = executeXPathOnPageSource(xpathExpr, pageSource);
873
+ if (!elements || elements.length === 0) {
874
+ return null;
875
+ }
876
+ return {
877
+ element: elements[0],
878
+ matchCount: elements.length
879
+ };
880
+ }
881
+
882
+ // src/mobileSelectorPerformanceOptimizer/utils/xpath-converter.ts
883
+ var log6 = logger6("@wdio/appium-service:selector-optimizer");
884
+ async function convertXPathToOptimizedSelector(xpath2, options) {
885
+ if (!xpath2 || typeof xpath2 !== "string") {
886
+ return null;
887
+ }
888
+ const unmappableFeatures = detectUnmappableXPathFeatures(xpath2);
889
+ const hasUnmappableFeatures = unmappableFeatures.length > 0;
890
+ const unmappableWarning = hasUnmappableFeatures ? `XPath contains unmappable features: ${unmappableFeatures.join(", ")}.` : void 0;
891
+ try {
892
+ const browserWithPageSource = options.browser;
893
+ const pageSource = await browserWithPageSource.getPageSource();
894
+ if (!pageSource || typeof pageSource !== "string") {
895
+ return {
896
+ selector: null,
897
+ warning: hasUnmappableFeatures ? `${unmappableWarning} Page source unavailable.` : "Page source unavailable."
898
+ };
899
+ }
900
+ const result = findElementByXPathWithFallback(xpath2, pageSource);
901
+ if (!result) {
902
+ return {
903
+ selector: null,
904
+ warning: hasUnmappableFeatures ? `${unmappableWarning} Element not found in page source.` : "Element not found in page source."
905
+ };
906
+ }
907
+ const { element, matchCount } = result;
908
+ const selectorResult = buildSelectorFromElementData(element, pageSource);
909
+ if (!selectorResult || !selectorResult.selector) {
910
+ return {
911
+ selector: null,
912
+ warning: hasUnmappableFeatures ? `${unmappableWarning} Could not build selector from element attributes.` : "Could not build selector from element attributes."
913
+ };
914
+ }
915
+ if (matchCount > 1) {
916
+ return {
917
+ selector: null,
918
+ warning: `XPath matched ${matchCount} elements. The suggested selector may not be unique. You can use this selector but be aware it may match multiple elements.`,
919
+ suggestion: selectorResult.selector
920
+ };
921
+ }
922
+ return selectorResult;
923
+ } catch (error) {
924
+ log6.debug(`Page source analysis failed: ${error instanceof Error ? error.message : String(error)}`);
925
+ return {
926
+ selector: null,
927
+ warning: hasUnmappableFeatures ? `${unmappableWarning} Page source analysis failed.` : "Page source analysis failed."
928
+ };
929
+ }
930
+ }
931
+
932
+ // src/mobileSelectorPerformanceOptimizer/utils/optimization.ts
933
+ var log7 = logger7("@wdio/appium-service:selector-optimizer");
934
+ async function findOptimizedSelector(xpath2, options) {
935
+ log7.info(`[${LOG_PREFIX}: Step 2] Collecting page source for dynamic analysis...`);
936
+ const pageSourceStartTime = getHighResTime();
937
+ const result = await convertXPathToOptimizedSelector(xpath2, {
938
+ browser: options.browser
939
+ });
940
+ const pageSourceDuration = getHighResTime() - pageSourceStartTime;
941
+ log7.info(`[${LOG_PREFIX}: Step 2] Page source collected in ${pageSourceDuration.toFixed(2)}ms`);
942
+ return result;
943
+ }
944
+
945
+ // src/mobileSelectorPerformanceOptimizer/utils/formatting.ts
946
+ import logger8 from "@wdio/logger";
947
+ var log8 = logger8("@wdio/appium-service:selector-optimizer");
948
+ function formatSelectorForDisplay(selector, maxLength = 100) {
949
+ if (typeof selector === "string") {
950
+ if (selector.length > maxLength) {
951
+ return selector.substring(0, maxLength) + "...";
952
+ }
953
+ return selector;
954
+ }
955
+ return String(selector);
956
+ }
957
+ function formatSelectorLocations(locations) {
958
+ if (locations.length === 0) {
959
+ return "";
960
+ }
961
+ if (locations.length === 1) {
962
+ const loc = locations[0];
963
+ const fileDisplay = loc.isPageObject ? `${loc.file} (page object)` : loc.file;
964
+ return ` at ${fileDisplay}:${loc.line}`;
965
+ }
966
+ const locationStrings = locations.map((loc) => {
967
+ const fileDisplay = loc.isPageObject ? `${loc.file} (page object)` : loc.file;
968
+ return `${fileDisplay}:${loc.line}`;
969
+ });
970
+ return ` at multiple locations: ${locationStrings.join(", ")}. Note: The selector was found in ${locations.length} files. Please verify which one is correct.`;
971
+ }
972
+ function logOptimizationConclusion(timeDifference, improvementPercent, originalSelector, optimizedSelector, locationInfo = "") {
973
+ const formattedOriginal = formatSelectorForDisplay(originalSelector);
974
+ const formattedOptimized = formatSelectorForDisplay(optimizedSelector);
975
+ const quoteStyle = optimizedSelector.startsWith("-ios class chain:") ? "'" : '"';
976
+ if (timeDifference > 0) {
977
+ log8.info(`[${LOG_PREFIX}: Conclusion] Optimized selector is ${timeDifference.toFixed(2)}ms faster than XPath (${improvementPercent.toFixed(1)}% improvement)`);
978
+ log8.info(`[${LOG_PREFIX}: Advice] Consider using the optimized selector ${quoteStyle}${formattedOptimized}${quoteStyle} for better performance${locationInfo ? locationInfo : ""}.`);
979
+ } else if (timeDifference < 0) {
980
+ log8.info(`[${LOG_PREFIX}: Conclusion] Optimized selector is ${Math.abs(timeDifference).toFixed(2)}ms slower than XPath`);
981
+ log8.info(`[${LOG_PREFIX}: Advice] There is no improvement in performance, consider using the original selector '${formattedOriginal}' if performance is critical. If performance is not critical, you can use the optimized selector ${quoteStyle}${formattedOptimized}${quoteStyle} for better stability${locationInfo ? locationInfo : ""}.`);
982
+ } else {
983
+ log8.info(`[${LOG_PREFIX}: Conclusion] Optimized selector has the same performance as XPath`);
984
+ log8.info(`[${LOG_PREFIX}: Advice] There is no improvement in performance, consider using the original selector '${formattedOriginal}' if performance is critical. If performance is not critical, you can use the optimized selector ${quoteStyle}${formattedOptimized}${quoteStyle} for better stability${locationInfo ? locationInfo : ""}.`);
985
+ }
986
+ }
987
+
988
+ // src/mobileSelectorPerformanceOptimizer/utils/performance-data.ts
989
+ function createOptimizedSelectorData(testContext, originalSelector, originalDuration, optimizedSelector, optimizedDuration) {
990
+ const timeDifference = originalDuration - optimizedDuration;
991
+ const improvementPercent = originalDuration > 0 ? timeDifference / originalDuration * 100 : 0;
992
+ return {
993
+ testFile: testContext.testFile || "unknown",
994
+ suiteName: testContext.suiteName,
995
+ testName: testContext.testName,
996
+ lineNumber: testContext.lineNumber,
997
+ selectorFile: testContext.selectorFile,
998
+ selector: originalSelector,
999
+ selectorType: "xpath",
1000
+ duration: originalDuration,
1001
+ timestamp: Date.now(),
1002
+ deviceName: getCurrentDeviceName(),
1003
+ optimizedSelector,
1004
+ optimizedDuration,
1005
+ improvementMs: timeDifference,
1006
+ improvementPercent
1007
+ };
1008
+ }
1009
+ function storePerformanceData(timing, duration, testContext) {
1010
+ const data = {
1011
+ testFile: testContext.testFile || "unknown",
1012
+ suiteName: testContext.suiteName,
1013
+ testName: testContext.testName,
1014
+ lineNumber: testContext.lineNumber,
1015
+ selector: timing.selector,
1016
+ selectorType: timing.selectorType,
1017
+ duration,
1018
+ timestamp: Date.now(),
1019
+ deviceName: getCurrentDeviceName()
1020
+ };
1021
+ addPerformanceData(data);
1022
+ }
1023
+
1024
+ // src/mobileSelectorPerformanceOptimizer/utils/command-timing.ts
1025
+ function findMostRecentUnmatchedUserCommand(commandTimings) {
1026
+ return Array.from(commandTimings.entries()).filter(([_id, timing]) => timing.isUserCommand && !timing.selectorType).sort(([_idA, a], [_idB, b]) => b.startTime - a.startTime)[0];
1027
+ }
1028
+ function findMatchingInternalCommandTiming(commandTimings, formattedSelector, selectorType) {
1029
+ return Array.from(commandTimings.entries()).filter(
1030
+ ([_id, timing]) => !timing.isUserCommand && timing.formattedSelector === formattedSelector && timing.selectorType === selectorType
1031
+ ).sort(([_idA, a], [_idB, b]) => b.startTime - a.startTime)[0];
1032
+ }
1033
+
1034
+ // src/mobileSelectorPerformanceOptimizer/utils/reporter.ts
1035
+ import path2 from "node:path";
1036
+ import { SevereServiceError } from "webdriverio";
1037
+ function isReporterRegistered(reporters, reporterName) {
1038
+ return reporters.some((reporter) => {
1039
+ if (Array.isArray(reporter)) {
1040
+ const reporterClass = reporter[0];
1041
+ if (typeof reporterClass === "function") {
1042
+ return reporterClass.name === reporterName;
1043
+ }
1044
+ return false;
1045
+ }
1046
+ if (typeof reporter === "function") {
1047
+ return reporter.name === reporterName;
1048
+ }
1049
+ return false;
1050
+ });
1051
+ }
1052
+ function determineReportDirectory(reportPath, config, appiumServiceOptions) {
1053
+ let reportDir;
1054
+ if (reportPath) {
1055
+ reportDir = path2.isAbsolute(reportPath) ? reportPath : path2.join(process.cwd(), reportPath);
1056
+ } else if (config?.outputDir) {
1057
+ reportDir = path2.isAbsolute(config.outputDir) ? config.outputDir : path2.join(process.cwd(), config.outputDir);
1058
+ } else if (appiumServiceOptions?.logPath) {
1059
+ reportDir = path2.isAbsolute(appiumServiceOptions.logPath) ? appiumServiceOptions.logPath : path2.join(process.cwd(), appiumServiceOptions.logPath);
1060
+ } else if (appiumServiceOptions?.args?.log) {
1061
+ const logPath = appiumServiceOptions.args.log;
1062
+ reportDir = path2.isAbsolute(logPath) ? path2.dirname(logPath) : path2.join(process.cwd(), path2.dirname(logPath));
1063
+ }
1064
+ if (!reportDir) {
1065
+ throw new SevereServiceError(
1066
+ "Mobile Selector Performance Optimizer: JSON report cannot be created. Please provide one of the following:\n 1. reportPath in trackSelectorPerformance service options\n 2. outputDir in WebdriverIO config\n 3. logPath in Appium service options\n 4. log in Appium service args"
1067
+ );
1068
+ }
1069
+ return reportDir;
1070
+ }
1071
+
1072
+ // src/mobileSelectorPerformanceOptimizer/markdown-formatter.ts
1073
+ function countSelectorUsage(data) {
1074
+ const counts = /* @__PURE__ */ new Map();
1075
+ for (const entry of data) {
1076
+ const count = counts.get(entry.selector) || 0;
1077
+ counts.set(entry.selector, count + 1);
1078
+ }
1079
+ return counts;
1080
+ }
1081
+ function deduplicateSelectors(data, usageCounts) {
1082
+ const selectorMap = /* @__PURE__ */ new Map();
1083
+ for (const entry of data) {
1084
+ if (!entry.optimizedSelector || entry.improvementMs === void 0) {
1085
+ continue;
1086
+ }
1087
+ const existing = selectorMap.get(entry.selector);
1088
+ const current = {
1089
+ selector: entry.selector,
1090
+ optimizedSelector: entry.optimizedSelector,
1091
+ improvementMs: entry.improvementMs,
1092
+ improvementPercent: entry.improvementPercent || 0,
1093
+ lineNumber: entry.lineNumber,
1094
+ selectorFile: entry.selectorFile,
1095
+ testFile: entry.testFile,
1096
+ usageCount: usageCounts.get(entry.selector) || 1
1097
+ };
1098
+ if (!existing || Math.abs(current.improvementMs) > Math.abs(existing.improvementMs)) {
1099
+ if (existing && !current.selectorFile && existing.selectorFile) {
1100
+ current.selectorFile = existing.selectorFile;
1101
+ current.lineNumber = existing.lineNumber;
1102
+ }
1103
+ selectorMap.set(entry.selector, current);
1104
+ } else if (!existing.selectorFile && current.selectorFile) {
1105
+ existing.selectorFile = current.selectorFile;
1106
+ existing.lineNumber = current.lineNumber;
1107
+ }
1108
+ }
1109
+ return Array.from(selectorMap.values());
1110
+ }
1111
+ function groupByFile(optimizations) {
1112
+ const fileMap = /* @__PURE__ */ new Map();
1113
+ const workspaceWide = [];
1114
+ for (const opt of optimizations) {
1115
+ const filePath = opt.selectorFile;
1116
+ if (filePath && opt.lineNumber) {
1117
+ if (!fileMap.has(filePath)) {
1118
+ fileMap.set(filePath, []);
1119
+ }
1120
+ fileMap.get(filePath).push(opt);
1121
+ } else {
1122
+ workspaceWide.push(opt);
1123
+ }
1124
+ }
1125
+ const fileGroups = [];
1126
+ for (const [filePath, opts] of fileMap.entries()) {
1127
+ opts.sort((a, b) => (a.lineNumber || 0) - (b.lineNumber || 0));
1128
+ const totalSavingsMs = opts.reduce((sum, o) => sum + o.improvementMs, 0);
1129
+ const totalSavingsWithUsage = opts.reduce((sum, o) => sum + o.improvementMs * o.usageCount, 0);
1130
+ fileGroups.push({ filePath, optimizations: opts, totalSavingsMs, totalSavingsWithUsage });
1131
+ }
1132
+ fileGroups.sort((a, b) => b.totalSavingsWithUsage - a.totalSavingsWithUsage);
1133
+ workspaceWide.sort((a, b) => b.improvementMs * b.usageCount - a.improvementMs * a.usageCount);
1134
+ return { fileGroups, workspaceWide };
1135
+ }
1136
+ function escapeForTable(str) {
1137
+ return str.replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
1138
+ }
1139
+ function getQuoteStyle(selector) {
1140
+ if (selector.startsWith("//") || selector.startsWith("/")) {
1141
+ return "'";
1142
+ }
1143
+ if (selector.startsWith("-ios class chain:")) {
1144
+ return "'";
1145
+ }
1146
+ return '"';
1147
+ }
1148
+ function formatSelector(selector) {
1149
+ const truncated = formatSelectorForDisplay(selector, 60);
1150
+ const quote = getQuoteStyle(selector);
1151
+ return `\`$(${quote}${escapeForTable(truncated)}${quote})\``;
1152
+ }
1153
+ function toRelativePath(filePath, projectRoot) {
1154
+ if (!projectRoot) {
1155
+ return filePath;
1156
+ }
1157
+ if (!path3.isAbsolute(filePath)) {
1158
+ return filePath;
1159
+ }
1160
+ return path3.relative(projectRoot, filePath);
1161
+ }
1162
+ function getFileName(filePath) {
1163
+ return path3.basename(filePath);
1164
+ }
1165
+ function formatFileLink(filePath, lineNumber, projectRoot) {
1166
+ const relativePath = toRelativePath(filePath, projectRoot);
1167
+ const fileName = getFileName(filePath);
1168
+ if (lineNumber) {
1169
+ return `[\`${fileName}:${lineNumber}\`](${relativePath}#L${lineNumber})`;
1170
+ }
1171
+ return `[\`${fileName}\`](${relativePath})`;
1172
+ }
1173
+ function formatLineLink(filePath, lineNumber, projectRoot) {
1174
+ if (!lineNumber) {
1175
+ return "L?:";
1176
+ }
1177
+ const relativePath = toRelativePath(filePath, projectRoot);
1178
+ return `[L${lineNumber}:](${relativePath}#L${lineNumber})`;
1179
+ }
1180
+ function formatTime(timestamp) {
1181
+ const date = new Date(timestamp);
1182
+ return date.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
1183
+ }
1184
+ function formatDuration(ms) {
1185
+ if (ms < 1e3) {
1186
+ return `${ms.toFixed(0)}ms`;
1187
+ }
1188
+ const seconds = ms / 1e3;
1189
+ if (seconds < 60) {
1190
+ return `${seconds.toFixed(2)}s`;
1191
+ }
1192
+ const minutes = Math.floor(seconds / 60);
1193
+ const remainingSeconds = seconds % 60;
1194
+ return `${minutes}m ${remainingSeconds.toFixed(0)}s`;
1195
+ }
1196
+ function generateMarkdownReport(optimizedSelectors, deviceName, timingInfo, projectRoot) {
1197
+ const lines = [];
1198
+ lines.push("# \u{1F4CA} Mobile Selector Performance Optimizer Report");
1199
+ lines.push("");
1200
+ if (optimizedSelectors.length === 0) {
1201
+ lines.push(`**Device:** ${deviceName}`);
1202
+ lines.push(`**Generated:** ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`);
1203
+ lines.push("");
1204
+ lines.push("## \u2705 Summary");
1205
+ lines.push("");
1206
+ lines.push("No optimization opportunities found. All selectors are already optimized!");
1207
+ lines.push("");
1208
+ return lines.join("\n");
1209
+ }
1210
+ const usageCounts = countSelectorUsage(optimizedSelectors);
1211
+ const deduplicated = deduplicateSelectors(optimizedSelectors, usageCounts);
1212
+ const positiveOptimizations = deduplicated.filter((o) => o.improvementMs > 0);
1213
+ const negativeOptimizations = deduplicated.filter((o) => o.improvementMs < 0);
1214
+ const totalSavingsMs = positiveOptimizations.reduce((sum, o) => sum + o.improvementMs * o.usageCount, 0);
1215
+ const avgImprovement = positiveOptimizations.length > 0 ? positiveOptimizations.reduce((sum, o) => sum + o.improvementPercent, 0) / positiveOptimizations.length : 0;
1216
+ const highImpact = positiveOptimizations.filter((o) => o.improvementPercent >= 50);
1217
+ const mediumImpact = positiveOptimizations.filter((o) => o.improvementPercent >= 20 && o.improvementPercent < 50);
1218
+ const lowImpact = positiveOptimizations.filter((o) => o.improvementPercent >= 10 && o.improvementPercent < 20);
1219
+ const minorImpact = positiveOptimizations.filter((o) => o.improvementPercent > 0 && o.improvementPercent < 10);
1220
+ lines.push(`**Device:** ${deviceName}`);
1221
+ if (timingInfo) {
1222
+ lines.push(`**Run Time:** ${formatTime(timingInfo.startTime)} \u2192 ${formatTime(timingInfo.endTime)} (${formatDuration(timingInfo.totalRunDurationMs)})`);
1223
+ }
1224
+ lines.push(`**Analyzed:** ${deduplicated.length} unique selectors (${positiveOptimizations.length} optimizable${negativeOptimizations.length > 0 ? `, ${negativeOptimizations.length} not recommended` : ""})`);
1225
+ lines.push("");
1226
+ const savingsLine = `**Total Potential Savings:** **${formatDuration(totalSavingsMs)}** per test run`;
1227
+ if (timingInfo && timingInfo.totalRunDurationMs > 0) {
1228
+ const improvementPercent = totalSavingsMs / timingInfo.totalRunDurationMs * 100;
1229
+ lines.push(`${savingsLine} (**${improvementPercent.toFixed(1)}%** of total run time)`);
1230
+ } else {
1231
+ lines.push(savingsLine);
1232
+ }
1233
+ lines.push(`**Average Improvement per Selector:** **${avgImprovement.toFixed(1)}%** faster`);
1234
+ lines.push("");
1235
+ lines.push("---");
1236
+ lines.push("");
1237
+ lines.push("## \u{1F4C8} Summary");
1238
+ lines.push("");
1239
+ lines.push("| Impact Level | Count | Action |");
1240
+ lines.push("|:-------------|------:|:-------|");
1241
+ if (highImpact.length > 0) {
1242
+ lines.push(`| \u{1F534} **High** (>50% gain) | ${highImpact.length} | Fix immediately |`);
1243
+ }
1244
+ if (mediumImpact.length > 0) {
1245
+ lines.push(`| \u{1F7E0} **Medium** (20-50% gain) | ${mediumImpact.length} | Recommended |`);
1246
+ }
1247
+ if (lowImpact.length > 0) {
1248
+ lines.push(`| \u{1F7E1} **Low** (10-20% gain) | ${lowImpact.length} | Minor optimization |`);
1249
+ }
1250
+ if (minorImpact.length > 0) {
1251
+ lines.push(`| \u26AA **Minor** (<10% gain) | ${minorImpact.length} | Optional |`);
1252
+ }
1253
+ if (negativeOptimizations.length > 0) {
1254
+ lines.push(`| \u26A0\uFE0F **Slower in Testing** | ${negativeOptimizations.length} | See warnings below |`);
1255
+ }
1256
+ lines.push("");
1257
+ lines.push("---");
1258
+ lines.push("");
1259
+ const { fileGroups, workspaceWide } = groupByFile(positiveOptimizations);
1260
+ if (fileGroups.length > 0) {
1261
+ lines.push("## \u{1F3AF} File-Based Fixes");
1262
+ lines.push("");
1263
+ lines.push("*Update these specific lines in your Page Objects or Test files for immediate impact.*");
1264
+ lines.push("");
1265
+ for (const group of fileGroups) {
1266
+ const fileLink = formatFileLink(group.filePath, void 0, projectRoot);
1267
+ lines.push(`### \u{1F4C1} ${fileLink}`);
1268
+ lines.push("");
1269
+ lines.push("| Location | Original | Optimized | Per Use | Uses | Total Saved |");
1270
+ lines.push("|:---------|:---------|:----------|--------:|-----:|-----------:|");
1271
+ for (const opt of group.optimizations) {
1272
+ const original = formatSelector(opt.selector);
1273
+ const optimized = formatSelector(opt.optimizedSelector);
1274
+ const perUse = `${opt.improvementMs.toFixed(1)}ms`;
1275
+ const uses = `${opt.usageCount}\xD7`;
1276
+ const totalSaved = `${(opt.improvementMs * opt.usageCount).toFixed(1)}ms`;
1277
+ const location = formatLineLink(group.filePath, opt.lineNumber, projectRoot);
1278
+ lines.push(`| ${location} | ${original} | ${optimized} | ${perUse} | ${uses} | ${totalSaved} |`);
1279
+ }
1280
+ lines.push("");
1281
+ lines.push(`> **File total:** ${formatDuration(group.totalSavingsWithUsage)} saved (${group.optimizations.length} selector${group.optimizations.length > 1 ? "s" : ""})`);
1282
+ lines.push("");
1283
+ }
1284
+ lines.push("---");
1285
+ lines.push("");
1286
+ }
1287
+ if (workspaceWide.length > 0) {
1288
+ lines.push("## \u{1F50D} Workspace-Wide Optimizations");
1289
+ lines.push("");
1290
+ lines.push("*Source file location unknown. Search your IDE (Cmd+Shift+F / Ctrl+Shift+F) for these selectors.*");
1291
+ lines.push("");
1292
+ lines.push("| Original | Optimized | Per Use | Uses | Total Saved |");
1293
+ lines.push("|:---------|:----------|--------:|-----:|-----------:|");
1294
+ for (const opt of workspaceWide) {
1295
+ const original = formatSelector(opt.selector);
1296
+ const optimized = formatSelector(opt.optimizedSelector);
1297
+ const perUse = `${opt.improvementMs.toFixed(1)}ms`;
1298
+ const uses = `${opt.usageCount}\xD7`;
1299
+ const totalSaved = `${(opt.improvementMs * opt.usageCount).toFixed(1)}ms`;
1300
+ lines.push(`| ${original} | ${optimized} | ${perUse} | ${uses} | ${totalSaved} |`);
1301
+ }
1302
+ lines.push("");
1303
+ lines.push("---");
1304
+ lines.push("");
1305
+ }
1306
+ if (negativeOptimizations.length > 0) {
1307
+ lines.push("## \u26A0\uFE0F Performance Warnings");
1308
+ lines.push("");
1309
+ lines.push("*In these cases, the suggested native selector was **slower** than XPath in your test environment. This can happen due to:*");
1310
+ lines.push("");
1311
+ lines.push("- **App-specific optimizations** - Some apps have optimized XPath handling");
1312
+ lines.push("- **Element hierarchy** - Deep nesting can sometimes favor XPath's tree traversal");
1313
+ lines.push("- **Caching effects** - First lookups may differ from subsequent ones");
1314
+ lines.push("- **Appium/driver version** - Native selector support varies by version");
1315
+ lines.push("");
1316
+ lines.push("*Recommendation: Keep using XPath for these selectors, or test both approaches in your CI pipeline.*");
1317
+ lines.push("");
1318
+ lines.push("| Location | XPath (Current) | Suggested Native | XPath Time | Native Time | Result |");
1319
+ lines.push("|:---------|:----------------|:-----------------|:-----------|:------------|:-------|");
1320
+ for (const opt of negativeOptimizations) {
1321
+ const xpathSelector = formatSelector(opt.selector);
1322
+ const nativeSelector = formatSelector(opt.optimizedSelector);
1323
+ const originalEntry = optimizedSelectors.find((e) => e.selector === opt.selector);
1324
+ if (originalEntry) {
1325
+ const xpathTime = `${originalEntry.duration.toFixed(0)}ms`;
1326
+ const nativeTime = originalEntry.optimizedDuration ? `${originalEntry.optimizedDuration.toFixed(0)}ms` : `${(originalEntry.duration - opt.improvementMs).toFixed(0)}ms`;
1327
+ const slowdownMs = Math.abs(opt.improvementMs);
1328
+ const slowdownPercent = Math.abs(opt.improvementPercent);
1329
+ const result = `Native ${slowdownMs.toFixed(0)}ms slower (${slowdownPercent.toFixed(0)}%)`;
1330
+ const location = opt.selectorFile ? formatFileLink(opt.selectorFile, opt.lineNumber, projectRoot) : "*unknown*";
1331
+ lines.push(`| ${location} | ${xpathSelector} | ${nativeSelector} | ${xpathTime} | ${nativeTime} | ${result} |`);
1332
+ }
1333
+ }
1334
+ lines.push("");
1335
+ lines.push("---");
1336
+ lines.push("");
1337
+ }
1338
+ lines.push("## \u{1F4A1} Implementation Guide");
1339
+ lines.push("");
1340
+ lines.push("### Why make these changes?");
1341
+ lines.push("");
1342
+ lines.push("- **Speed:** Native selectors (`~` Accessibility IDs, `-ios predicate string`, `-ios class chain`) bypass expensive XML tree traversal required by XPath");
1343
+ lines.push("- **Stability:** Native selectors are less affected by UI hierarchy changes that often break XPath queries");
1344
+ lines.push("- **Maintainability:** Shorter, more readable selectors are easier to debug and update");
1345
+ lines.push("");
1346
+ lines.push("### Selector Priority (fastest to slowest)");
1347
+ lines.push("");
1348
+ lines.push("1. **Accessibility ID** (`~elementId`) - Direct lookup, fastest");
1349
+ lines.push("2. **iOS Predicate String** (`-ios predicate string:...`) - Native predicate evaluation");
1350
+ lines.push("3. **iOS Class Chain** (`-ios class chain:...`) - Native hierarchy traversal");
1351
+ lines.push("4. **XPath** (`//...`) - Full XML serialization + parsing, slowest");
1352
+ lines.push("");
1353
+ lines.push("### Resources");
1354
+ lines.push("");
1355
+ lines.push("- [WebdriverIO Mobile Selectors](https://webdriver.io/docs/selectors#mobile-selectors)");
1356
+ lines.push("- [iOS Predicate String](https://webdriver.io/docs/selectors#ios-predicate-string)");
1357
+ lines.push("- [iOS Class Chain](https://webdriver.io/docs/selectors#ios-class-chain)");
1358
+ lines.push("");
1359
+ lines.push("---");
1360
+ lines.push("");
1361
+ lines.push(`*Generated by WebdriverIO Mobile Selector Performance Optimizer \u2022 ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}*`);
1362
+ lines.push("");
1363
+ return lines.join("\n");
1364
+ }
1365
+
1366
+ // src/mobileSelectorPerformanceOptimizer/aggregator.ts
1367
+ function buildInferenceMaps(data) {
1368
+ const testFileInference = /* @__PURE__ */ new Map();
1369
+ const suiteNameInference = /* @__PURE__ */ new Map();
1370
+ for (const entry of data) {
1371
+ if (entry.testFile && entry.testFile !== "unknown" && entry.suiteName && entry.suiteName !== "unknown" && entry.testName) {
1372
+ const key1 = `${entry.suiteName}::${entry.testName}`;
1373
+ if (!testFileInference.has(key1)) {
1374
+ testFileInference.set(key1, entry.testFile);
1375
+ }
1376
+ const key2 = `${entry.testFile}::${entry.testName}`;
1377
+ if (!suiteNameInference.has(key2)) {
1378
+ suiteNameInference.set(key2, entry.suiteName);
1379
+ }
1380
+ }
1381
+ }
1382
+ return { testFileInference, suiteNameInference };
1383
+ }
1384
+ function applyInference(testFile, suiteName, testName, inferenceMaps) {
1385
+ let resolvedTestFile = testFile;
1386
+ let resolvedSuiteName = suiteName;
1387
+ if (resolvedTestFile === "unknown" && resolvedSuiteName !== "unknown" && testName !== "unknown") {
1388
+ const key = `${resolvedSuiteName}::${testName}`;
1389
+ const inferred = inferenceMaps.testFileInference.get(key);
1390
+ if (inferred) {
1391
+ resolvedTestFile = inferred;
1392
+ }
1393
+ }
1394
+ if (resolvedSuiteName === "unknown" && resolvedTestFile !== "unknown" && testName !== "unknown") {
1395
+ const key = `${resolvedTestFile}::${testName}`;
1396
+ const inferred = inferenceMaps.suiteNameInference.get(key);
1397
+ if (inferred) {
1398
+ resolvedSuiteName = inferred;
1399
+ }
1400
+ }
1401
+ return [resolvedTestFile, resolvedSuiteName];
1402
+ }
1403
+ function groupDataByDevice(data, capabilities) {
1404
+ const deviceMap = /* @__PURE__ */ new Map();
1405
+ for (const entry of data) {
1406
+ const deviceName = entry.deviceName || "unknown";
1407
+ if (!deviceMap.has(deviceName)) {
1408
+ deviceMap.set(deviceName, []);
1409
+ }
1410
+ deviceMap.get(deviceName).push(entry);
1411
+ }
1412
+ if (deviceMap.size === 1 && deviceMap.has("unknown")) {
1413
+ const fallbackDeviceName = getDeviceName(capabilities);
1414
+ if (fallbackDeviceName !== "unknown") {
1415
+ const unknownData = deviceMap.get("unknown");
1416
+ deviceMap.delete("unknown");
1417
+ deviceMap.set(fallbackDeviceName, unknownData);
1418
+ }
1419
+ }
1420
+ return deviceMap;
1421
+ }
1422
+ async function aggregateSelectorPerformanceData(capabilities, maxLineLength, writeFn, reportDirectory, options) {
1423
+ const enableCliReport = options?.enableCliReport === true;
1424
+ const enableMarkdownReport = options?.enableMarkdownReport === true;
1425
+ const markdownLines = [];
1426
+ const cliWrite = writeFn || ((message) => process.stdout.write(message));
1427
+ const write = (message) => {
1428
+ if (enableCliReport) {
1429
+ cliWrite(message);
1430
+ }
1431
+ if (enableMarkdownReport) {
1432
+ markdownLines.push(message);
1433
+ }
1434
+ };
1435
+ const writeError = writeFn || console.error;
1436
+ if (!reportDirectory) {
1437
+ throw new SevereServiceError2(
1438
+ "Mobile Selector Performance Optimizer: Report directory was not determined. This should have been validated during service initialization."
1439
+ );
1440
+ }
1441
+ if (!fs2.existsSync(reportDirectory)) {
1442
+ fs2.mkdirSync(reportDirectory, { recursive: true });
1443
+ }
1444
+ const workersDataDir = path4.join(reportDirectory, "selector-performance-worker-data");
1445
+ const timestamp = Date.now();
1446
+ try {
1447
+ let allData = [];
1448
+ if (fs2.existsSync(workersDataDir)) {
1449
+ const files = fs2.readdirSync(workersDataDir);
1450
+ const workerDataFiles = files.filter((file) => file.startsWith("worker-data-") && file.endsWith(".json"));
1451
+ if (workerDataFiles.length > 0) {
1452
+ workerDataFiles.forEach((file) => {
1453
+ const filePath = path4.join(workersDataDir, file);
1454
+ try {
1455
+ const content = fs2.readFileSync(filePath, "utf8");
1456
+ const workerData = JSON.parse(content);
1457
+ if (Array.isArray(workerData)) {
1458
+ allData.push(...workerData);
1459
+ }
1460
+ } catch (err) {
1461
+ writeError(`Failed to read worker data file ${file}:`, err);
1462
+ }
1463
+ });
1464
+ try {
1465
+ fs2.rmSync(workersDataDir, { recursive: true, force: true });
1466
+ } catch {
1467
+ }
1468
+ }
1469
+ }
1470
+ if (allData.length === 0) {
1471
+ allData = getPerformanceData();
1472
+ }
1473
+ if (allData.length === 0) {
1474
+ return;
1475
+ }
1476
+ const dataByDevice = groupDataByDevice(allData, capabilities);
1477
+ for (const [deviceName, deviceData] of dataByDevice.entries()) {
1478
+ const sanitizedDeviceName = deviceName.replace(/[^a-zA-Z0-9-_]/g, "_").toLowerCase() || "unknown";
1479
+ const finalJsonPath = path4.join(reportDirectory, `mobile-selector-performance-optimizer-report-${sanitizedDeviceName}-${timestamp}.json`);
1480
+ const groupedData = groupDataBySpecFile(deviceData);
1481
+ fs2.writeFileSync(finalJsonPath, JSON.stringify(groupedData, null, 2));
1482
+ const totalSelectors = deviceData.length;
1483
+ const avgDuration = totalSelectors > 0 ? deviceData.reduce((sum, d) => sum + d.duration, 0) / totalSelectors : 0;
1484
+ const optimizedSelectors = deviceData.filter((d) => d.optimizedSelector && d.improvementMs !== void 0);
1485
+ let timingInfo;
1486
+ if (deviceData.length > 0) {
1487
+ const timestamps = deviceData.map((d) => d.timestamp).filter((t) => t > 0);
1488
+ if (timestamps.length > 0) {
1489
+ const startTime = Math.min(...timestamps);
1490
+ const endTime = Math.max(...timestamps);
1491
+ const totalRunDurationMs = endTime - startTime;
1492
+ timingInfo = { startTime, endTime, totalRunDurationMs };
1493
+ }
1494
+ }
1495
+ if (totalSelectors === 0) {
1496
+ write("\n\u{1F4CA} Selector Performance Summary:\n");
1497
+ write(`${REPORT_INDENT_SUMMARY}No element-finding commands were tracked.
1498
+ `);
1499
+ write(`${REPORT_INDENT_SUMMARY}\u{1F4A1} JSON file written to: ${finalJsonPath}
1500
+ `);
1501
+ } else {
1502
+ if (optimizedSelectors.length > 0) {
1503
+ generateGroupedSummaryReport(optimizedSelectors, deviceName, write, maxLineLength, timingInfo);
1504
+ } else {
1505
+ write("\n\u{1F4CA} Selector Performance Summary:\n");
1506
+ write(`${REPORT_INDENT_SUMMARY}Total element finds: ${totalSelectors}
1507
+ `);
1508
+ write(`${REPORT_INDENT_SUMMARY}Average duration: ${avgDuration.toFixed(2)}ms
1509
+ `);
1510
+ }
1511
+ if (optimizedSelectors.length === 0) {
1512
+ write(`
1513
+ ${REPORT_INDENT_SUMMARY}\u2705 All selectors performed well
1514
+ `);
1515
+ write(`${REPORT_INDENT_SUMMARY}\u{1F4A1} JSON file written to: ${finalJsonPath}
1516
+ `);
1517
+ }
1518
+ }
1519
+ if (enableMarkdownReport) {
1520
+ const markdownPath = path4.join(reportDirectory, `mobile-selector-performance-optimizer-report-${sanitizedDeviceName}-${timestamp}.md`);
1521
+ const projectRoot = process.cwd();
1522
+ const markdownContent = generateMarkdownReport(optimizedSelectors, deviceName, timingInfo, projectRoot);
1523
+ fs2.writeFileSync(markdownPath, markdownContent);
1524
+ const message = `
1525
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1526
+ \u{1F4DD} Mobile Selector Performance Optimizer - Markdown Report
1527
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1528
+ \u{1F4C1} Markdown report written to: ${markdownPath}
1529
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1530
+ `;
1531
+ console.log(message);
1532
+ }
1533
+ if (!enableCliReport && !enableMarkdownReport) {
1534
+ const message = `
1535
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1536
+ \u{1F4CA} Mobile Selector Performance Optimizer
1537
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1538
+ \u{1F4C1} JSON report written to: ${finalJsonPath}
1539
+
1540
+ \u{1F4A1} To get actionable optimization advice in your terminal or as a file,
1541
+ enable one of these options in your config:
1542
+
1543
+ trackSelectorPerformance: {
1544
+ pageObjectPaths: ['./tests/pageobjects'],
1545
+ enableCliReport: true, // Show report in terminal
1546
+ enableMarkdownReport: true // Save report as markdown file
1547
+ }
1548
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1549
+ `;
1550
+ console.log(message);
1551
+ }
1552
+ }
1553
+ } catch (err) {
1554
+ writeError("Failed to aggregate selector performance data:", err);
1555
+ if (err instanceof Error) {
1556
+ writeError("Error details:", err.message);
1557
+ }
24
1558
  }
25
- return absolutePath;
26
1559
  }
27
- function formatCliArgs(args) {
28
- const cliArgs = [];
29
- for (const key in args) {
30
- const value = args[key];
31
- if (typeof value === "boolean" && !value || value === null) {
32
- continue;
1560
+ function groupDataBySpecFile(allData) {
1561
+ const inferenceMaps = buildInferenceMaps(allData);
1562
+ const processedData = allData.map((data) => {
1563
+ const testName = data.testName || "unknown";
1564
+ const [testFile, suiteName] = applyInference(
1565
+ data.testFile || "unknown",
1566
+ data.suiteName || "unknown",
1567
+ testName,
1568
+ inferenceMaps
1569
+ );
1570
+ return { ...data, testFile, suiteName };
1571
+ });
1572
+ const grouped = {};
1573
+ for (const data of processedData) {
1574
+ const specFile = data.testFile || "unknown";
1575
+ let suiteName = data.suiteName || "unknown";
1576
+ const testName = data.testName || "unknown";
1577
+ if (suiteName === "unknown" && specFile !== "unknown" && testName !== "unknown") {
1578
+ const key = `${specFile}::${testName}`;
1579
+ const inferred = inferenceMaps.suiteNameInference.get(key);
1580
+ if (inferred) {
1581
+ suiteName = inferred;
1582
+ }
33
1583
  }
34
- if (key === "chromedriver_autodownload") {
35
- cliArgs.push(key);
1584
+ if (!grouped[specFile]) {
1585
+ grouped[specFile] = {};
1586
+ }
1587
+ if (!grouped[specFile][suiteName]) {
1588
+ grouped[specFile][suiteName] = {};
1589
+ }
1590
+ if (!grouped[specFile][suiteName][testName]) {
1591
+ grouped[specFile][suiteName][testName] = [];
1592
+ }
1593
+ const existing = grouped[specFile][suiteName][testName].find((d) => d.selector === data.selector);
1594
+ if (!existing) {
1595
+ grouped[specFile][suiteName][testName].push(data);
1596
+ }
1597
+ }
1598
+ for (const specFile of Object.keys(grouped)) {
1599
+ const suites = grouped[specFile];
1600
+ const suiteNames = Object.keys(suites);
1601
+ for (const suiteName of suiteNames) {
1602
+ if (suiteName === "unknown") {
1603
+ const unknownSuite = suites[suiteName];
1604
+ const testNames = Object.keys(unknownSuite);
1605
+ for (const testName of testNames) {
1606
+ const key = `${specFile}::${testName}`;
1607
+ const knownSuiteName = inferenceMaps.suiteNameInference.get(key);
1608
+ if (knownSuiteName && suites[knownSuiteName]) {
1609
+ if (!suites[knownSuiteName][testName]) {
1610
+ suites[knownSuiteName][testName] = [];
1611
+ }
1612
+ for (const data of unknownSuite[testName]) {
1613
+ const existing = suites[knownSuiteName][testName].find((d) => d.selector === data.selector);
1614
+ if (!existing) {
1615
+ data.suiteName = knownSuiteName;
1616
+ suites[knownSuiteName][testName].push(data);
1617
+ }
1618
+ }
1619
+ delete unknownSuite[testName];
1620
+ }
1621
+ }
1622
+ if (Object.keys(unknownSuite).length === 0) {
1623
+ delete suites[suiteName];
1624
+ }
1625
+ }
1626
+ }
1627
+ }
1628
+ for (const specFile of Object.keys(grouped)) {
1629
+ const suites = grouped[specFile];
1630
+ const sortedSuiteNames = Object.keys(suites).sort((suiteA, suiteB) => {
1631
+ const allTimestampsA = [];
1632
+ const suiteATests = suites[suiteA];
1633
+ for (const testName of Object.keys(suiteATests)) {
1634
+ const testData = suiteATests[testName];
1635
+ allTimestampsA.push(...testData.map((d) => d.timestamp));
1636
+ }
1637
+ const allTimestampsB = [];
1638
+ const suiteBTests = suites[suiteB];
1639
+ for (const testName of Object.keys(suiteBTests)) {
1640
+ const testData = suiteBTests[testName];
1641
+ allTimestampsB.push(...testData.map((d) => d.timestamp));
1642
+ }
1643
+ const firstA = allTimestampsA.length > 0 ? Math.min(...allTimestampsA) : 0;
1644
+ const firstB = allTimestampsB.length > 0 ? Math.min(...allTimestampsB) : 0;
1645
+ return firstA - firstB;
1646
+ });
1647
+ const sortedSuites = {};
1648
+ for (const suiteName of sortedSuiteNames) {
1649
+ const tests = suites[suiteName];
1650
+ const sortedTestNames = Object.keys(tests).sort((testA, testB) => {
1651
+ const firstA = tests[testA].length > 0 ? Math.min(...tests[testA].map((d) => d.timestamp)) : 0;
1652
+ const firstB = tests[testB].length > 0 ? Math.min(...tests[testB].map((d) => d.timestamp)) : 0;
1653
+ return firstA - firstB;
1654
+ });
1655
+ const sortedTests = {};
1656
+ for (const testName of sortedTestNames) {
1657
+ sortedTests[testName] = tests[testName];
1658
+ }
1659
+ sortedSuites[suiteName] = sortedTests;
1660
+ }
1661
+ grouped[specFile] = sortedSuites;
1662
+ }
1663
+ return grouped;
1664
+ }
1665
+ function getDeviceName(capabilities) {
1666
+ if (!capabilities) {
1667
+ return "unknown";
1668
+ }
1669
+ const extractDeviceName = (caps) => {
1670
+ if (!caps || typeof caps !== "object") {
1671
+ return void 0;
1672
+ }
1673
+ const capsRecord = caps;
1674
+ const appiumDeviceName = capsRecord["appium:deviceName"];
1675
+ if (appiumDeviceName && typeof appiumDeviceName === "string") {
1676
+ return appiumDeviceName;
1677
+ }
1678
+ const w3cCap = caps.alwaysMatch || caps;
1679
+ if (w3cCap && typeof w3cCap === "object") {
1680
+ const w3cRecord = w3cCap;
1681
+ const w3cAppiumDeviceName = w3cRecord["appium:deviceName"];
1682
+ if (w3cAppiumDeviceName && typeof w3cAppiumDeviceName === "string") {
1683
+ return w3cAppiumDeviceName;
1684
+ }
1685
+ }
1686
+ return void 0;
1687
+ };
1688
+ if (!Array.isArray(capabilities) && typeof capabilities === "object") {
1689
+ const entries = Object.entries(capabilities);
1690
+ if (entries.length > 0) {
1691
+ const [, firstCap] = entries[0];
1692
+ if (firstCap && typeof firstCap === "object") {
1693
+ if ("capabilities" in firstCap) {
1694
+ const nestedCaps = firstCap.capabilities;
1695
+ const deviceName3 = extractDeviceName(nestedCaps);
1696
+ if (deviceName3) {
1697
+ return deviceName3;
1698
+ }
1699
+ }
1700
+ const deviceName2 = extractDeviceName(firstCap);
1701
+ if (deviceName2) {
1702
+ return deviceName2;
1703
+ }
1704
+ }
1705
+ }
1706
+ }
1707
+ if (Array.isArray(capabilities) && capabilities.length > 0) {
1708
+ const deviceName2 = extractDeviceName(capabilities[0]);
1709
+ if (deviceName2) {
1710
+ return deviceName2;
1711
+ }
1712
+ }
1713
+ const deviceName = extractDeviceName(capabilities);
1714
+ if (deviceName) {
1715
+ return deviceName;
1716
+ }
1717
+ return "unknown";
1718
+ }
1719
+ function wrapLine(line, maxLineLength, indent = "") {
1720
+ if (line.length <= maxLineLength) {
1721
+ return [line];
1722
+ }
1723
+ const prefixMatch = line.match(/^(\s*)(\d+\.|•|→)\s+/);
1724
+ let continuationIndent = indent;
1725
+ if (prefixMatch) {
1726
+ const leadingSpaces = prefixMatch[1];
1727
+ const prefixMarker = prefixMatch[2];
1728
+ continuationIndent = leadingSpaces + " ".repeat(prefixMarker.length + 1);
1729
+ } else if (!indent) {
1730
+ const leadingWhitespaceMatch = line.match(/^(\s+)/);
1731
+ if (leadingWhitespaceMatch) {
1732
+ continuationIndent = leadingWhitespaceMatch[1];
1733
+ }
1734
+ }
1735
+ const lines = [];
1736
+ let remaining = line;
1737
+ let isFirstLine = true;
1738
+ while (true) {
1739
+ const effectiveMaxLength = isFirstLine ? maxLineLength : maxLineLength - continuationIndent.length;
1740
+ if (remaining.length <= effectiveMaxLength) {
1741
+ if (remaining.length > 0) {
1742
+ if (isFirstLine) {
1743
+ lines.push(remaining);
1744
+ } else {
1745
+ lines.push(continuationIndent + remaining);
1746
+ }
1747
+ }
1748
+ break;
1749
+ }
1750
+ let breakPoint = effectiveMaxLength;
1751
+ const searchStart = Math.max(0, effectiveMaxLength - 20);
1752
+ const spaceIndex = remaining.lastIndexOf(" ", effectiveMaxLength);
1753
+ const commaIndex = remaining.lastIndexOf(",", effectiveMaxLength);
1754
+ const arrowIndex = remaining.lastIndexOf("\u2192", effectiveMaxLength);
1755
+ if (arrowIndex > searchStart) {
1756
+ breakPoint = arrowIndex + 1;
1757
+ } else if (commaIndex > searchStart) {
1758
+ breakPoint = commaIndex + 1;
1759
+ } else if (spaceIndex > searchStart) {
1760
+ breakPoint = spaceIndex + 1;
1761
+ }
1762
+ if (isFirstLine) {
1763
+ const firstLinePart = remaining.substring(0, breakPoint);
1764
+ lines.push(firstLinePart);
1765
+ } else {
1766
+ lines.push(continuationIndent + remaining.substring(0, breakPoint).trim());
1767
+ }
1768
+ remaining = remaining.substring(breakPoint).trim();
1769
+ isFirstLine = false;
1770
+ }
1771
+ return lines;
1772
+ }
1773
+ function countSelectorUsage2(data) {
1774
+ const counts = /* @__PURE__ */ new Map();
1775
+ for (const entry of data) {
1776
+ const count = counts.get(entry.selector) || 0;
1777
+ counts.set(entry.selector, count + 1);
1778
+ }
1779
+ return counts;
1780
+ }
1781
+ function deduplicateSelectorsForCli(data, usageCounts) {
1782
+ const selectorMap = /* @__PURE__ */ new Map();
1783
+ for (const entry of data) {
1784
+ if (!entry.optimizedSelector || entry.improvementMs === void 0) {
36
1785
  continue;
37
1786
  }
38
- cliArgs.push(`--${kebabCase(key)}`);
39
- if (typeof value !== "boolean") {
40
- cliArgs.push(sanitizeCliOptionValue(value));
1787
+ const existing = selectorMap.get(entry.selector);
1788
+ const current = {
1789
+ selector: entry.selector,
1790
+ optimizedSelector: entry.optimizedSelector,
1791
+ improvementMs: entry.improvementMs,
1792
+ improvementPercent: entry.improvementPercent || 0,
1793
+ lineNumber: entry.lineNumber,
1794
+ selectorFile: entry.selectorFile,
1795
+ testFile: entry.testFile,
1796
+ usageCount: usageCounts.get(entry.selector) || 1,
1797
+ duration: entry.duration,
1798
+ optimizedDuration: entry.optimizedDuration
1799
+ };
1800
+ if (!existing || Math.abs(current.improvementMs) > Math.abs(existing.improvementMs)) {
1801
+ if (existing && !current.selectorFile && existing.selectorFile) {
1802
+ current.selectorFile = existing.selectorFile;
1803
+ current.lineNumber = existing.lineNumber;
1804
+ }
1805
+ selectorMap.set(entry.selector, current);
1806
+ } else if (!existing.selectorFile && current.selectorFile) {
1807
+ existing.selectorFile = current.selectorFile;
1808
+ existing.lineNumber = current.lineNumber;
41
1809
  }
42
1810
  }
43
- return cliArgs;
1811
+ return Array.from(selectorMap.values());
44
1812
  }
45
- function sanitizeCliOptionValue(value) {
46
- const valueString = typeof value === "object" ? JSON.stringify(value) : String(value);
47
- return /\s/.test(valueString) ? `'${valueString}'` : valueString;
1813
+ function groupByFileForCli(optimizations) {
1814
+ const fileMap = /* @__PURE__ */ new Map();
1815
+ const workspaceWide = [];
1816
+ for (const opt of optimizations) {
1817
+ const filePath = opt.selectorFile;
1818
+ if (filePath && opt.lineNumber) {
1819
+ if (!fileMap.has(filePath)) {
1820
+ fileMap.set(filePath, []);
1821
+ }
1822
+ fileMap.get(filePath).push(opt);
1823
+ } else {
1824
+ workspaceWide.push(opt);
1825
+ }
1826
+ }
1827
+ const fileGroups = [];
1828
+ for (const [filePath, opts] of fileMap.entries()) {
1829
+ opts.sort((a, b) => (a.lineNumber || 0) - (b.lineNumber || 0));
1830
+ const totalSavingsMs = opts.reduce((sum, o) => sum + o.improvementMs, 0);
1831
+ const totalSavingsWithUsage = opts.reduce((sum, o) => sum + o.improvementMs * o.usageCount, 0);
1832
+ fileGroups.push({ filePath, optimizations: opts, totalSavingsMs, totalSavingsWithUsage });
1833
+ }
1834
+ fileGroups.sort((a, b) => b.totalSavingsWithUsage - a.totalSavingsWithUsage);
1835
+ workspaceWide.sort((a, b) => b.improvementMs * b.usageCount - a.improvementMs * a.usageCount);
1836
+ return { fileGroups, workspaceWide };
1837
+ }
1838
+ function generateGroupedSummaryReport(optimizedSelectors, deviceName, write, maxLineLength, timingInfo) {
1839
+ const usageCounts = countSelectorUsage2(optimizedSelectors);
1840
+ const deduplicated = deduplicateSelectorsForCli(optimizedSelectors, usageCounts);
1841
+ const positiveOptimizations = deduplicated.filter((o) => o.improvementMs > 0);
1842
+ const negativeOptimizations = deduplicated.filter((o) => o.improvementMs < 0);
1843
+ if (positiveOptimizations.length === 0 && negativeOptimizations.length === 0) {
1844
+ write("\n");
1845
+ write("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n");
1846
+ write("\u{1F4CA} Mobile Selector Performance Optimizer\n");
1847
+ write("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n");
1848
+ write(`${REPORT_INDENT_SUMMARY}No optimizations found.
1849
+ `);
1850
+ write("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n");
1851
+ return;
1852
+ }
1853
+ const totalSavingsMs = positiveOptimizations.reduce((sum, o) => sum + o.improvementMs * o.usageCount, 0);
1854
+ const avgImprovement = positiveOptimizations.length > 0 ? positiveOptimizations.reduce((sum, o) => sum + o.improvementPercent, 0) / positiveOptimizations.length : 0;
1855
+ const highImpact = positiveOptimizations.filter((o) => o.improvementPercent >= 50);
1856
+ const mediumImpact = positiveOptimizations.filter((o) => o.improvementPercent >= 20 && o.improvementPercent < 50);
1857
+ const lowImpact = positiveOptimizations.filter((o) => o.improvementPercent >= 10 && o.improvementPercent < 20);
1858
+ const minorImpact = positiveOptimizations.filter((o) => o.improvementPercent > 0 && o.improvementPercent < 10);
1859
+ const { fileGroups, workspaceWide } = groupByFileForCli(positiveOptimizations);
1860
+ write("\n");
1861
+ write("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n");
1862
+ write("\u{1F4CA} Mobile Selector Performance Optimizer Report\n");
1863
+ write("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n");
1864
+ write("\n");
1865
+ if (deviceName && deviceName !== "unknown") {
1866
+ write(`${REPORT_INDENT_SUMMARY}Device: ${deviceName}
1867
+ `);
1868
+ }
1869
+ if (timingInfo) {
1870
+ const formatTime2 = (ts) => new Date(ts).toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
1871
+ const formatDuration2 = (ms) => {
1872
+ if (ms < 1e3) {
1873
+ return `${ms.toFixed(0)}ms`;
1874
+ }
1875
+ const seconds = ms / 1e3;
1876
+ if (seconds < 60) {
1877
+ return `${seconds.toFixed(2)}s`;
1878
+ }
1879
+ const minutes = Math.floor(seconds / 60);
1880
+ const remainingSeconds = seconds % 60;
1881
+ return `${minutes}m ${remainingSeconds.toFixed(0)}s`;
1882
+ };
1883
+ write(`${REPORT_INDENT_SUMMARY}Run Time: ${formatTime2(timingInfo.startTime)} \u2192 ${formatTime2(timingInfo.endTime)} (${formatDuration2(timingInfo.totalRunDurationMs)})
1884
+ `);
1885
+ }
1886
+ const analyzedText = negativeOptimizations.length > 0 ? `${deduplicated.length} unique selectors (${positiveOptimizations.length} optimizable, ${negativeOptimizations.length} not recommended)` : `${deduplicated.length} unique selectors (${positiveOptimizations.length} optimizable)`;
1887
+ write(`${REPORT_INDENT_SUMMARY}Analyzed: ${analyzedText}
1888
+ `);
1889
+ const formatSavings = (ms) => {
1890
+ if (ms < 1e3) {
1891
+ return `${ms.toFixed(0)}ms`;
1892
+ }
1893
+ return `${(ms / 1e3).toFixed(2)}s`;
1894
+ };
1895
+ let savingsLine = `${REPORT_INDENT_SUMMARY}Total Potential Savings: ${formatSavings(totalSavingsMs)} per test run`;
1896
+ if (timingInfo && timingInfo.totalRunDurationMs > 0) {
1897
+ const improvementPercent = totalSavingsMs / timingInfo.totalRunDurationMs * 100;
1898
+ savingsLine += ` (${improvementPercent.toFixed(1)}% of total run time)`;
1899
+ }
1900
+ write(`${savingsLine}
1901
+ `);
1902
+ write(`${REPORT_INDENT_SUMMARY}Average Improvement per Selector: ${avgImprovement.toFixed(1)}% faster
1903
+ `);
1904
+ write("\n");
1905
+ write("\u{1F4C8} Summary\n");
1906
+ write("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
1907
+ if (highImpact.length > 0) {
1908
+ write(`${REPORT_INDENT_SUMMARY}\u{1F534} High (>50% gain): ${String(highImpact.length).padStart(3)} \u2192 Fix immediately
1909
+ `);
1910
+ }
1911
+ if (mediumImpact.length > 0) {
1912
+ write(`${REPORT_INDENT_SUMMARY}\u{1F7E0} Medium (20-50% gain): ${String(mediumImpact.length).padStart(3)} \u2192 Recommended
1913
+ `);
1914
+ }
1915
+ if (lowImpact.length > 0) {
1916
+ write(`${REPORT_INDENT_SUMMARY}\u{1F7E1} Low (10-20% gain): ${String(lowImpact.length).padStart(3)} \u2192 Minor optimization
1917
+ `);
1918
+ }
1919
+ if (minorImpact.length > 0) {
1920
+ write(`${REPORT_INDENT_SUMMARY}\u26AA Minor (<10% gain): ${String(minorImpact.length).padStart(3)} \u2192 Optional
1921
+ `);
1922
+ }
1923
+ if (negativeOptimizations.length > 0) {
1924
+ write(`${REPORT_INDENT_SUMMARY}\u26A0\uFE0F Slower in Testing: ${String(negativeOptimizations.length).padStart(3)} \u2192 See warnings below
1925
+ `);
1926
+ }
1927
+ write("\n");
1928
+ if (fileGroups.length > 0) {
1929
+ write("\u{1F3AF} File-Based Fixes\n");
1930
+ write("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
1931
+ write(`${REPORT_INDENT_SUMMARY}Update these specific lines for immediate impact:
1932
+ `);
1933
+ write("\n");
1934
+ for (const group of fileGroups) {
1935
+ write(`${REPORT_INDENT_FILE}\u{1F4C1} ${group.filePath}
1936
+ `);
1937
+ for (const opt of group.optimizations) {
1938
+ const formattedOriginal = formatSelectorForDisplay(opt.selector, 45);
1939
+ const formattedOptimized = formatSelectorForDisplay(opt.optimizedSelector, 45);
1940
+ const quoteStyle = opt.optimizedSelector.startsWith("-ios class chain:") ? "'" : '"';
1941
+ const lineNum = opt.lineNumber ? `L${opt.lineNumber}` : "?";
1942
+ const perUse = `${opt.improvementMs.toFixed(1)}ms/use`;
1943
+ const totalSaved = opt.improvementMs * opt.usageCount;
1944
+ const totalDisplay = totalSaved >= 1e3 ? `${(totalSaved / 1e3).toFixed(2)}s` : `${totalSaved.toFixed(1)}ms`;
1945
+ if (opt.usageCount > 1) {
1946
+ const line = `${REPORT_INDENT_SELECTOR}${lineNum}: $('${formattedOriginal}') \u2192 $(${quoteStyle}${formattedOptimized}${quoteStyle})`;
1947
+ const wrapped = wrapLine(line, maxLineLength, REPORT_INDENT_SELECTOR + " ");
1948
+ for (const wrappedLine of wrapped) {
1949
+ write(`${wrappedLine}
1950
+ `);
1951
+ }
1952
+ write(`${REPORT_INDENT_SELECTOR} \u26A1 ${perUse} \xD7 ${opt.usageCount} uses = ${totalDisplay} total
1953
+ `);
1954
+ } else {
1955
+ const line = `${REPORT_INDENT_SELECTOR}${lineNum}: $('${formattedOriginal}') \u2192 $(${quoteStyle}${formattedOptimized}${quoteStyle}) [${opt.improvementMs.toFixed(1)}ms]`;
1956
+ const wrapped = wrapLine(line, maxLineLength, REPORT_INDENT_SELECTOR + " ");
1957
+ for (const wrappedLine of wrapped) {
1958
+ write(`${wrappedLine}
1959
+ `);
1960
+ }
1961
+ }
1962
+ }
1963
+ const fileTotalDisplay = group.totalSavingsWithUsage >= 1e3 ? `${(group.totalSavingsWithUsage / 1e3).toFixed(2)}s` : `${group.totalSavingsWithUsage.toFixed(1)}ms`;
1964
+ write(`${REPORT_INDENT_SUITE}\u2514\u2500 File total: ${fileTotalDisplay} saved (${group.optimizations.length} selector${group.optimizations.length > 1 ? "s" : ""})
1965
+ `);
1966
+ write("\n");
1967
+ }
1968
+ }
1969
+ if (workspaceWide.length > 0) {
1970
+ write("\u{1F50D} Workspace-Wide Optimizations\n");
1971
+ write("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
1972
+ write(`${REPORT_INDENT_SUMMARY}Source file unknown. Search your IDE (Cmd+Shift+F) for these selectors:
1973
+ `);
1974
+ write("\n");
1975
+ for (const opt of workspaceWide) {
1976
+ const formattedOriginal = formatSelectorForDisplay(opt.selector, 45);
1977
+ const formattedOptimized = formatSelectorForDisplay(opt.optimizedSelector, 45);
1978
+ const quoteStyle = opt.optimizedSelector.startsWith("-ios class chain:") ? "'" : '"';
1979
+ const perUse = `${opt.improvementMs.toFixed(1)}ms/use`;
1980
+ const totalSaved = opt.improvementMs * opt.usageCount;
1981
+ const totalDisplay = totalSaved >= 1e3 ? `${(totalSaved / 1e3).toFixed(2)}s` : `${totalSaved.toFixed(1)}ms`;
1982
+ const line = `${REPORT_INDENT_SELECTOR}$('${formattedOriginal}') \u2192 $(${quoteStyle}${formattedOptimized}${quoteStyle})`;
1983
+ const wrapped = wrapLine(line, maxLineLength, REPORT_INDENT_SELECTOR + " ");
1984
+ for (const wrappedLine of wrapped) {
1985
+ write(`${wrappedLine}
1986
+ `);
1987
+ }
1988
+ if (opt.usageCount > 1) {
1989
+ write(`${REPORT_INDENT_SELECTOR} \u26A1 ${perUse} \xD7 ${opt.usageCount} uses = ${totalDisplay} total
1990
+ `);
1991
+ } else {
1992
+ write(`${REPORT_INDENT_SELECTOR} \u26A1 ${totalDisplay}
1993
+ `);
1994
+ }
1995
+ }
1996
+ write("\n");
1997
+ }
1998
+ if (negativeOptimizations.length > 0) {
1999
+ write("\u26A0\uFE0F Performance Warnings\n");
2000
+ write("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
2001
+ write(`${REPORT_INDENT_SUMMARY}Native selectors were SLOWER than XPath for these cases.
2002
+ `);
2003
+ write(`${REPORT_INDENT_SUMMARY}This can happen due to app-specific optimizations, element hierarchy,
2004
+ `);
2005
+ write(`${REPORT_INDENT_SUMMARY}caching effects, or Appium/driver version differences.
2006
+ `);
2007
+ write(`${REPORT_INDENT_SUMMARY}Recommendation: Keep using XPath for these selectors.
2008
+ `);
2009
+ write("\n");
2010
+ for (const opt of negativeOptimizations) {
2011
+ const xpathSelector = formatSelectorForDisplay(opt.selector, 40);
2012
+ const nativeSelector = formatSelectorForDisplay(opt.optimizedSelector, 40);
2013
+ const xpathTime = opt.duration ? `${opt.duration.toFixed(0)}ms` : "?";
2014
+ const nativeTime = opt.optimizedDuration ? `${opt.optimizedDuration.toFixed(0)}ms` : opt.duration ? `${(opt.duration - opt.improvementMs).toFixed(0)}ms` : "?";
2015
+ const slowdownMs = Math.abs(opt.improvementMs).toFixed(0);
2016
+ const slowdownPercent = Math.abs(opt.improvementPercent).toFixed(0);
2017
+ const location = opt.selectorFile && opt.lineNumber ? `${opt.selectorFile}:${opt.lineNumber}` : opt.selectorFile ? opt.selectorFile : null;
2018
+ if (location) {
2019
+ write(`${REPORT_INDENT_FILE}\u{1F4CD} ${location}
2020
+ `);
2021
+ }
2022
+ write(`${REPORT_INDENT_SELECTOR}XPath: $('${xpathSelector}') \u2192 ${xpathTime}
2023
+ `);
2024
+ write(`${REPORT_INDENT_SELECTOR}Native: $('${nativeSelector}') \u2192 ${nativeTime}
2025
+ `);
2026
+ write(`${REPORT_INDENT_SELECTOR} \u274C Native was ${slowdownMs}ms slower (${slowdownPercent}%)
2027
+ `);
2028
+ write("\n");
2029
+ }
2030
+ }
2031
+ write("\u{1F4A1} Why Change?\n");
2032
+ write("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
2033
+ write(`${REPORT_INDENT_SUMMARY}\u2022 Speed: Native selectors bypass expensive XML tree traversal
2034
+ `);
2035
+ write(`${REPORT_INDENT_SUMMARY}\u2022 Stability: Less affected by UI hierarchy changes
2036
+ `);
2037
+ write(`${REPORT_INDENT_SUMMARY}\u2022 Priority: ~accessibilityId > -ios predicate string > -ios class chain > //xpath
2038
+ `);
2039
+ write(`${REPORT_INDENT_SUMMARY}\u2022 Docs: https://webdriver.io/docs/selectors#mobile-selectors
2040
+ `);
2041
+ write("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n");
48
2042
  }
49
2043
 
50
2044
  // src/launcher.ts
51
- import treeKill from "tree-kill";
52
- var log = logger("@wdio/appium-service");
2045
+ var log9 = logger9("@wdio/appium-service");
53
2046
  var DEFAULT_APPIUM_PORT = 4723;
54
2047
  var DEFAULT_LOG_FILENAME = "wdio-appium.log";
55
2048
  var DEFAULT_CONNECTION = {
@@ -143,7 +2136,7 @@ var AppiumLauncher = class _AppiumLauncher {
143
2136
  this._args.port = typeof this._args.port === "number" ? this._args.port : await getPort({ port: DEFAULT_APPIUM_PORT });
144
2137
  const capabilityWasUpdated = this._setCapabilities(this._args.port);
145
2138
  if (!capabilityWasUpdated) {
146
- log.warn("Could not identify any capability that indicates a local Appium session, skipping Appium launch");
2139
+ log9.warn("Could not identify any capability that indicates a local Appium session, skipping Appium launch");
147
2140
  return;
148
2141
  }
149
2142
  this._appiumCliArgs.push(...formatCliArgs({ ...this._args }));
@@ -153,43 +2146,65 @@ var AppiumLauncher = class _AppiumLauncher {
153
2146
  if (this._logPath) {
154
2147
  this._redirectLogStream(this._logPath);
155
2148
  } else {
156
- log.info("Appium logs written to stdout");
2149
+ log9.info("Appium logs written to stdout");
157
2150
  this._process.stdout.on("data", this.#logStdout);
158
2151
  this._process.stderr.on("data", this.#logStderr);
159
2152
  }
160
2153
  }
161
2154
  #logStdout = (data) => {
162
- log.debug(data.toString());
2155
+ log9.debug(data.toString());
163
2156
  };
164
2157
  #logStderr = (data) => {
165
- log.warn(data.toString());
2158
+ log9.warn(data.toString());
166
2159
  };
167
2160
  promisifiedTreeKill = promisify(treeKill);
168
- async onComplete() {
2161
+ async onComplete(exitCode, config, capabilities) {
169
2162
  this._isShuttingDown = true;
2163
+ const trackConfig = this._options.trackSelectorPerformance;
2164
+ if (trackConfig && typeof trackConfig === "object" && !Array.isArray(trackConfig)) {
2165
+ try {
2166
+ const reportDirectory = determineReportDirectory(
2167
+ trackConfig.reportPath,
2168
+ this._config,
2169
+ this._options
2170
+ );
2171
+ const maxLineLength = trackConfig.maxLineLength || 100;
2172
+ const enableCliReport = trackConfig.enableCliReport === true;
2173
+ const enableMarkdownReport = trackConfig.enableMarkdownReport === true;
2174
+ await aggregateSelectorPerformanceData(
2175
+ capabilities,
2176
+ maxLineLength,
2177
+ void 0,
2178
+ reportDirectory,
2179
+ { enableCliReport, enableMarkdownReport }
2180
+ );
2181
+ } catch (err) {
2182
+ log9.error("Failed to aggregate selector performance data:", err);
2183
+ }
2184
+ }
170
2185
  if (this._process && this._process.pid) {
171
2186
  this._process.stdout.off("data", this.#logStdout);
172
2187
  this._process.stderr.off("data", this.#logStderr);
173
- log.info("Killing entire Appium tree");
2188
+ log9.info("Killing entire Appium tree");
174
2189
  try {
175
2190
  await this.promisifiedTreeKill(this._process.pid, "SIGTERM").catch(async (err) => {
176
- log.warn("SIGTERM failed, attempting SIGKILL:", err);
2191
+ log9.warn("SIGTERM failed, attempting SIGKILL:", err);
177
2192
  await this.promisifiedTreeKill(this._process.pid, "SIGKILL");
178
2193
  });
179
- log.info("Process and its children successfully terminated");
2194
+ log9.info("Process and its children successfully terminated");
180
2195
  } catch (err) {
181
- log.error("Failed to kill Appium process tree:", err);
2196
+ log9.error("Failed to kill Appium process tree:", err);
182
2197
  try {
183
2198
  this._process.kill("SIGKILL");
184
- log.info("Killed main process directly");
2199
+ log9.info("Killed main process directly");
185
2200
  } catch (e) {
186
- log.error("Failed to kill process directly:", e);
2201
+ log9.error("Failed to kill process directly:", e);
187
2202
  }
188
2203
  }
189
2204
  }
190
2205
  }
191
2206
  _startAppium(command, args, timeout = APPIUM_START_TIMEOUT) {
192
- log.info(`Will spawn Appium process: ${command} ${args.join(" ")}`);
2207
+ log9.info(`Will spawn Appium process: ${command} ${args.join(" ")}`);
193
2208
  const appiumEnv = { ...process.env, NODE_OPTIONS: "" };
194
2209
  const appiumProcess = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"], env: appiumEnv });
195
2210
  let errorCaptured = false;
@@ -217,9 +2232,9 @@ var AppiumLauncher = class _AppiumLauncher {
217
2232
  error = message || "Appium exited without unknown error message";
218
2233
  const isWarning = message.trim().startsWith("WARN");
219
2234
  if (isWarning) {
220
- log.warn(error);
2235
+ log9.warn(error);
221
2236
  } else {
222
- log.error(error);
2237
+ log9.error(error);
223
2238
  }
224
2239
  if (!isWarning) {
225
2240
  rejectOnce(new Error(error));
@@ -229,7 +2244,7 @@ var AppiumLauncher = class _AppiumLauncher {
229
2244
  outputBuffer += data.toString();
230
2245
  if (outputBuffer.includes("Appium REST http interface listener started")) {
231
2246
  outputBuffer = "";
232
- log.info(`Appium started with ID: ${appiumProcess.pid}`);
2247
+ log9.info(`Appium started with ID: ${appiumProcess.pid}`);
233
2248
  clearTimeout(timeoutId);
234
2249
  appiumProcess.stdout.off("data", onStdout);
235
2250
  appiumProcess.stderr.off("data", onErrorMessage);
@@ -250,7 +2265,7 @@ var AppiumLauncher = class _AppiumLauncher {
250
2265
  ${error?.toString()}`;
251
2266
  }
252
2267
  if (exitCode !== 0) {
253
- log.error(errorMessage);
2268
+ log9.error(errorMessage);
254
2269
  }
255
2270
  rejectOnce(new Error(errorMessage));
256
2271
  });
@@ -261,9 +2276,9 @@ ${error?.toString()}`;
261
2276
  throw Error("No Appium process to redirect log stream");
262
2277
  }
263
2278
  const logFile = getFilePath(logPath, DEFAULT_LOG_FILENAME);
264
- await fsp.mkdir(path.dirname(logFile), { recursive: true });
265
- log.debug(`Appium logs written to: ${logFile}`);
266
- const logStream = fs.createWriteStream(logFile, { flags: "w" });
2279
+ await fsp.mkdir(path5.dirname(logFile), { recursive: true });
2280
+ log9.debug(`Appium logs written to: ${logFile}`);
2281
+ const logStream = fs3.createWriteStream(logFile, { flags: "w" });
267
2282
  this._process.stdout.pipe(logStream);
268
2283
  this._process.stderr.pipe(logStream);
269
2284
  }
@@ -273,14 +2288,454 @@ ${error?.toString()}`;
273
2288
  return url.fileURLToPath(entryPath);
274
2289
  } catch (err) {
275
2290
  const errorMessage = "Appium is not installed locally. Please install via e.g. `npm i --save-dev appium`.\nIf you use globally installed appium please add: `appium: { command: 'appium' }`\nto your wdio.conf.js!\n\n" + err.stack;
276
- log.error(errorMessage);
277
- throw new SevereServiceError(errorMessage);
2291
+ log9.error(errorMessage);
2292
+ throw new SevereServiceError3(errorMessage);
2293
+ }
2294
+ }
2295
+ };
2296
+
2297
+ // src/mobileSelectorPerformanceOptimizer/mspo-service.ts
2298
+ import crypto from "node:crypto";
2299
+ import fs4 from "node:fs";
2300
+ import path6 from "node:path";
2301
+ import { SevereServiceError as SevereServiceError4 } from "webdriverio";
2302
+ import logger11 from "@wdio/logger";
2303
+
2304
+ // src/mobileSelectorPerformanceOptimizer/mspo-reporter.ts
2305
+ import WDIOReporter from "@wdio/reporter";
2306
+ var MobileSelectorPerformanceReporter = class extends WDIOReporter {
2307
+ _reportDirectory;
2308
+ constructor(options) {
2309
+ super({ ...options, stdout: true });
2310
+ this._reportDirectory = options.reportDirectory;
2311
+ }
2312
+ /**
2313
+ * Get the current suite from the reporter's suite tracking
2314
+ * Returns the most nested suite (skips root suite)
2315
+ * Matches spec-reporter approach: uses currentSuites array
2316
+ * WDIOReporter base class manages currentSuites - last item is most nested
2317
+ */
2318
+ getCurrentSuite() {
2319
+ if (!this.currentSuites || this.currentSuites.length === 0) {
2320
+ return void 0;
2321
+ }
2322
+ for (let i = this.currentSuites.length - 1; i >= 0; i--) {
2323
+ const suite = this.currentSuites[i];
2324
+ if (suite.title && suite.title !== "(root)" && suite.title !== "{root}") {
2325
+ return suite;
2326
+ }
2327
+ }
2328
+ return void 0;
2329
+ }
2330
+ /**
2331
+ * Update context from current suite (helper to avoid duplication)
2332
+ */
2333
+ updateContextFromCurrentSuite() {
2334
+ const currentSuite = this.getCurrentSuite();
2335
+ if (currentSuite) {
2336
+ const suiteName = this.extractSuiteName(currentSuite);
2337
+ if (suiteName) {
2338
+ setCurrentSuiteName(suiteName);
2339
+ }
2340
+ const testFile = this.extractTestFile(currentSuite);
2341
+ if (testFile) {
2342
+ setCurrentTestFile(testFile);
2343
+ }
2344
+ }
2345
+ }
2346
+ /**
2347
+ * Extract test file path from suite, matching spec-reporter logic
2348
+ * Spec reporter: suite.file?.replace(process.cwd(), '')
2349
+ * Then slices leading '/' when displaying: .slice(1)
2350
+ */
2351
+ extractTestFile(suite) {
2352
+ if (!suite.file) {
2353
+ return void 0;
2354
+ }
2355
+ const testFile = suite.file.replace(process.cwd(), "");
2356
+ return testFile.startsWith("/") ? testFile.slice(1) : testFile;
2357
+ }
2358
+ /**
2359
+ * Extract suite name from suite title, matching spec-reporter logic
2360
+ * Only skip root suite which has "(root)" title
2361
+ */
2362
+ extractSuiteName(suite) {
2363
+ if (suite.title === "(root)" || suite.title === "{root}") {
2364
+ return void 0;
2365
+ }
2366
+ return suite.title;
2367
+ }
2368
+ /**
2369
+ * Extract test name from test title, matching spec-reporter logic
2370
+ * Spec reporter uses test.title directly for all frameworks
2371
+ */
2372
+ extractTestName(test) {
2373
+ return test.title || void 0;
2374
+ }
2375
+ onSuiteStart(suite) {
2376
+ const testFile = this.extractTestFile(suite);
2377
+ if (testFile) {
2378
+ setCurrentTestFile(testFile);
2379
+ }
2380
+ if (suite.title && suite.title !== "(root)" && suite.title !== "{root}") {
2381
+ setCurrentSuiteName(suite.title);
2382
+ }
2383
+ }
2384
+ onTestStart(test) {
2385
+ const testName = this.extractTestName(test);
2386
+ if (testName) {
2387
+ setCurrentTestName(testName);
2388
+ }
2389
+ this.updateContextFromCurrentSuite();
2390
+ }
2391
+ onTestPass(_test) {
2392
+ }
2393
+ onTestFail(_test) {
2394
+ }
2395
+ onTestSkip(test) {
2396
+ const testName = this.extractTestName(test);
2397
+ if (testName) {
2398
+ setCurrentTestName(testName);
2399
+ }
2400
+ }
2401
+ onTestPending(test) {
2402
+ const testName = this.extractTestName(test);
2403
+ if (testName) {
2404
+ setCurrentTestName(testName);
2405
+ }
2406
+ }
2407
+ onHookStart() {
2408
+ this.updateContextFromCurrentSuite();
2409
+ }
2410
+ onHookEnd() {
2411
+ this.updateContextFromCurrentSuite();
2412
+ }
2413
+ };
2414
+
2415
+ // src/mobileSelectorPerformanceOptimizer/optimizer.ts
2416
+ import logger10 from "@wdio/logger";
2417
+ var log10 = logger10("@wdio/appium-service:selector-optimizer");
2418
+ async function optimizeSelector(commandName, selector, originalFunc, browser, options, isMultiple) {
2419
+ const elementWord = isMultiple ? "element(s)" : "element";
2420
+ const testFile = getCurrentTestFile();
2421
+ const locations = findSelectorLocation(testFile, selector, options.pageObjectPaths);
2422
+ const locationInfo = formatSelectorLocations(locations);
2423
+ log10.info(`[${LOG_PREFIX}: Research Selector] ${commandName}('${formatSelectorForDisplay(selector)}')${locationInfo}`);
2424
+ log10.info(`[${LOG_PREFIX}: Step 1] Testing current selector: ${commandName}('${formatSelectorForDisplay(selector)}')`);
2425
+ const originalStartTime = getHighResTime();
2426
+ const originalResult = await originalFunc.call(browser, selector);
2427
+ const originalDuration = getHighResTime() - originalStartTime;
2428
+ log10.info(`[${LOG_PREFIX}: Step 1] ${commandName}('${formatSelectorForDisplay(selector)}') took ${originalDuration.toFixed(2)}ms`);
2429
+ const conversionResult = await findOptimizedSelector(selector, {
2430
+ browser: options.browser
2431
+ });
2432
+ if (!conversionResult || !conversionResult.selector) {
2433
+ if (conversionResult?.warning) {
2434
+ log10.warn(`[${LOG_PREFIX}: Warning] ${conversionResult.warning}`);
2435
+ }
2436
+ if (conversionResult?.suggestion) {
2437
+ log10.info(`[${LOG_PREFIX}: Suggestion] Consider using: ${conversionResult.suggestion}`);
2438
+ }
2439
+ return originalResult;
2440
+ }
2441
+ const optimizedSelector = conversionResult.selector;
2442
+ const quoteStyle = optimizedSelector.startsWith("-ios class chain:") ? "'" : '"';
2443
+ log10.info(`[${LOG_PREFIX}: Step 3] Search for a better selector`);
2444
+ log10.info(`[${LOG_PREFIX}: Outcome] Potential Optimized Selector: ${commandName}(${quoteStyle}${optimizedSelector}${quoteStyle})`);
2445
+ const parsed = parseOptimizedSelector(optimizedSelector);
2446
+ if (!parsed) {
2447
+ log10.warn(`[${LOG_PREFIX}: Warning] Unknown optimized selector type: ${optimizedSelector}. Using original XPath`);
2448
+ return originalResult;
2449
+ }
2450
+ const isAccessibilityId = parsed.using === "accessibility id";
2451
+ if (!isAccessibilityId) {
2452
+ log10.debug(`[${LOG_PREFIX}: Debug] Selector type: ${parsed.using}`);
2453
+ log10.debug(`[${LOG_PREFIX}: Debug] Selector value: "${parsed.value}"`);
2454
+ log10.debug(`[${LOG_PREFIX}: Debug] Starting verification process...`);
2455
+ }
2456
+ log10.info(`[${LOG_PREFIX}: Step 4] Testing optimized selector: ${commandName}(${quoteStyle}${optimizedSelector}${quoteStyle})`);
2457
+ const testResult = await testOptimizedSelector(browser, parsed.using, parsed.value, isMultiple, !isAccessibilityId);
2458
+ if (!testResult || testResult.elementRefs.length === 0) {
2459
+ log10.warn(`[${LOG_PREFIX}: Warning] Optimized selector '${optimizedSelector}' did not find ${elementWord}, using original XPath`);
2460
+ return originalResult;
2461
+ }
2462
+ const foundMessage = isMultiple ? `Optimized selector found ${testResult.elementRefs.length} element(s) in ${testResult.duration.toFixed(2)}ms` : `Optimized selector found element in ${testResult.duration.toFixed(2)}ms`;
2463
+ log10.info(`[${LOG_PREFIX}: Step 4] ${foundMessage}`);
2464
+ const timeDifference = originalDuration - testResult.duration;
2465
+ const improvementPercent = originalDuration > 0 ? timeDifference / originalDuration * 100 : 0;
2466
+ const testContext = {
2467
+ testFile: getCurrentTestFile() || "unknown",
2468
+ suiteName: getCurrentSuiteName() || "unknown",
2469
+ testName: getCurrentTestName() || "unknown",
2470
+ lineNumber: locations.length > 0 ? locations[0].line : void 0,
2471
+ selectorFile: locations.length > 0 ? locations[0].file : void 0
2472
+ };
2473
+ const optimizedData = createOptimizedSelectorData(
2474
+ testContext,
2475
+ selector,
2476
+ originalDuration,
2477
+ optimizedSelector,
2478
+ testResult.duration
2479
+ );
2480
+ addPerformanceData(optimizedData);
2481
+ logOptimizationConclusion(timeDifference, improvementPercent, selector, optimizedSelector, locationInfo);
2482
+ options.isReplacingSelector.value = true;
2483
+ try {
2484
+ return await originalFunc.call(browser, optimizedSelector);
2485
+ } finally {
2486
+ options.isReplacingSelector.value = false;
2487
+ }
2488
+ }
2489
+ async function optimizeSingleSelector(commandName, selector, originalFunc, browser, options) {
2490
+ return optimizeSelector(commandName, selector, originalFunc, browser, options, false);
2491
+ }
2492
+ async function optimizeMultipleSelectors(commandName, selector, originalFunc, browser, options) {
2493
+ return optimizeSelector(commandName, selector, originalFunc, browser, options, true);
2494
+ }
2495
+
2496
+ // src/mobileSelectorPerformanceOptimizer/overwrite.ts
2497
+ function overwriteUserCommands(browser, options) {
2498
+ if (!("overwriteCommand" in browser && typeof browser.overwriteCommand === "function")) {
2499
+ return;
2500
+ }
2501
+ const browserWithOverwrite = browser;
2502
+ for (const commandName of SINGLE_ELEMENT_COMMANDS) {
2503
+ browserWithOverwrite.overwriteCommand(commandName, async (originalFunc, selector) => {
2504
+ if (options.isReplacingSelector.value || !isNativeContext(browser) || !isXPathSelector(selector)) {
2505
+ return originalFunc.call(browser, selector);
2506
+ }
2507
+ return optimizeSingleSelector(commandName, selector, originalFunc, browser, options);
2508
+ });
2509
+ }
2510
+ for (const commandName of MULTIPLE_ELEMENT_COMMANDS) {
2511
+ browserWithOverwrite.overwriteCommand(commandName, async (originalFunc, selector) => {
2512
+ if (options.isReplacingSelector.value || !isNativeContext(browser) || !isXPathSelector(selector)) {
2513
+ return originalFunc.call(browser, selector);
2514
+ }
2515
+ return optimizeMultipleSelectors(commandName, selector, originalFunc, browser, options);
2516
+ });
2517
+ }
2518
+ }
2519
+
2520
+ // src/mobileSelectorPerformanceOptimizer/mspo-service.ts
2521
+ var log11 = logger11("@wdio/appium-service:selector-optimizer");
2522
+ var SelectorPerformanceService = class {
2523
+ constructor(_options, _config) {
2524
+ this._options = _options;
2525
+ this._config = _config;
2526
+ const trackConfig = _options.trackSelectorPerformance;
2527
+ if (trackConfig !== void 0 && trackConfig !== null) {
2528
+ if (typeof trackConfig !== "object" || Array.isArray(trackConfig)) {
2529
+ throw new SevereServiceError4(
2530
+ "trackSelectorPerformance must be an object. Expected format: { pageObjectPaths: string[], enableCliReport?: boolean, enableMarkdownReport?: boolean, reportPath?: string, maxLineLength?: number }"
2531
+ );
2532
+ }
2533
+ if (!trackConfig.pageObjectPaths || trackConfig.pageObjectPaths.length === 0) {
2534
+ throw new SevereServiceError4(
2535
+ "trackSelectorPerformance.pageObjectPaths is required. Please provide an array of paths to directories containing page objects or test files where selectors are defined. Example: pageObjectPaths: ['./tests/pageobjects']"
2536
+ );
2537
+ }
2538
+ this._enabled = true;
2539
+ this._pageObjectPaths = trackConfig.pageObjectPaths;
2540
+ this._enableCliReport = trackConfig.enableCliReport === true;
2541
+ this._enableMarkdownReport = trackConfig.enableMarkdownReport === true;
2542
+ this._reportDirectory = determineReportDirectory(
2543
+ trackConfig.reportPath,
2544
+ this._config,
2545
+ this._options
2546
+ );
2547
+ }
2548
+ }
2549
+ // Service configuration
2550
+ _enabled = false;
2551
+ _enableCliReport = false;
2552
+ _enableMarkdownReport = false;
2553
+ _reportDirectory;
2554
+ _pageObjectPaths = [];
2555
+ // Some internal consts
2556
+ _browser;
2557
+ _isReplacingSelectorRef = { value: false };
2558
+ _commandTimings = /* @__PURE__ */ new Map();
2559
+ // User commands to track
2560
+ _userCommands = new Set(USER_COMMANDS);
2561
+ // Internal commands to not track
2562
+ _internalCommands = /* @__PURE__ */ new Set([
2563
+ "findElement",
2564
+ "findElements"
2565
+ ]);
2566
+ async beforeSession(config, _capabilities, _specs) {
2567
+ if (this._enabled && config) {
2568
+ if (!config.reporters) {
2569
+ config.reporters = [];
2570
+ }
2571
+ const isAlreadyRegistered = isReporterRegistered(config.reporters, "MobileSelectorPerformanceReporter");
2572
+ if (!isAlreadyRegistered) {
2573
+ const reporterOptions = {
2574
+ reportDirectory: this._reportDirectory
2575
+ };
2576
+ const reporterEntry = [MobileSelectorPerformanceReporter, reporterOptions];
2577
+ config.reporters.push(reporterEntry);
2578
+ }
2579
+ }
2580
+ }
2581
+ async before(_capabilities, _specs, browser) {
2582
+ this._browser = browser;
2583
+ if (this._enabled) {
2584
+ log11.info("Mobile Selector Performance Optimizer (BETA)");
2585
+ log11.info(" \u2192 All feedback is welcome!");
2586
+ log11.info(" \u2192 Currently optimized for iOS (shows the most significant performance and stability gains)");
2587
+ if (this._browser.isAndroid) {
2588
+ log11.info("Mobile Selector Performance Optimizer is disabled for Android");
2589
+ log11.info(" \u2192 Android support coming in a future release");
2590
+ this._enabled = false;
2591
+ return;
2592
+ }
2593
+ const deviceName = this._extractDeviceName(browser);
2594
+ if (deviceName) {
2595
+ setCurrentDeviceName(deviceName);
2596
+ log11.debug(`Device name stored: ${deviceName}`);
2597
+ }
2598
+ log11.info("Mobile Selector Performance Optimizer enabled for iOS");
2599
+ }
2600
+ if (this._enabled) {
2601
+ overwriteUserCommands(browser, {
2602
+ browser,
2603
+ isReplacingSelector: this._isReplacingSelectorRef,
2604
+ pageObjectPaths: this._pageObjectPaths
2605
+ });
2606
+ }
2607
+ }
2608
+ async beforeCommand(commandName, args) {
2609
+ if (!this._enabled) {
2610
+ return;
2611
+ }
2612
+ if (!isNativeContext(this._browser)) {
2613
+ log11.info("Mobile Selector Performance Optimizer is disabled for non-native context");
2614
+ return;
2615
+ }
2616
+ if (this._userCommands.has(commandName)) {
2617
+ const selector = extractSelectorFromArgs(args);
2618
+ if (!selector || typeof selector !== "string") {
2619
+ return;
2620
+ }
2621
+ const formattedSelector = formatSelectorForDisplay(selector);
2622
+ const timingId = crypto.randomUUID();
2623
+ const testFile = getCurrentTestFile();
2624
+ const locations = findSelectorLocation(testFile, selector, this._pageObjectPaths);
2625
+ const lineNumber = locations.length > 0 ? locations[0].line : void 0;
2626
+ this._commandTimings.set(timingId, {
2627
+ startTime: getHighResTime(),
2628
+ commandName,
2629
+ selector,
2630
+ formattedSelector,
2631
+ timingId,
2632
+ isUserCommand: true,
2633
+ lineNumber
2634
+ });
2635
+ return;
2636
+ }
2637
+ if (this._internalCommands.has(commandName)) {
2638
+ if (!args || args.length < 2) {
2639
+ return;
2640
+ }
2641
+ const using = args[0];
2642
+ const value = args[1];
2643
+ if (using !== "xpath" || !value || typeof value !== "string") {
2644
+ return;
2645
+ }
2646
+ const formattedSelector = formatSelectorForDisplay(value);
2647
+ const matchingUserCommand = findMostRecentUnmatchedUserCommand(this._commandTimings);
2648
+ if (matchingUserCommand) {
2649
+ const [, userTiming] = matchingUserCommand;
2650
+ userTiming.selectorType = using;
2651
+ const timingId = crypto.randomUUID();
2652
+ this._commandTimings.set(timingId, {
2653
+ startTime: getHighResTime(),
2654
+ commandName: userTiming.commandName,
2655
+ selector: value,
2656
+ formattedSelector,
2657
+ selectorType: using,
2658
+ timingId,
2659
+ isUserCommand: false,
2660
+ lineNumber: userTiming.lineNumber
2661
+ });
2662
+ }
2663
+ }
2664
+ }
2665
+ async afterCommand(commandName, args, _result, _error) {
2666
+ if (!this._enabled) {
2667
+ return;
2668
+ }
2669
+ if (!isNativeContext(this._browser)) {
2670
+ log11.info("Mobile Selector Performance Optimizer is disabled for non-native context");
2671
+ return;
2672
+ }
2673
+ if (this._internalCommands.has(commandName)) {
2674
+ if (!args || args.length < 2) {
2675
+ return;
2676
+ }
2677
+ const using = args[0];
2678
+ const value = args[1];
2679
+ if (using !== "xpath" || !value || typeof value !== "string") {
2680
+ return;
2681
+ }
2682
+ const formattedSelector = formatSelectorForDisplay(value);
2683
+ const matchingTiming = findMatchingInternalCommandTiming(this._commandTimings, formattedSelector, using);
2684
+ if (!matchingTiming) {
2685
+ return;
2686
+ }
2687
+ const [timingId, timing] = matchingTiming;
2688
+ const duration = getHighResTime() - timing.startTime;
2689
+ if (duration < 0 || !timing.selector || !timing.selectorType) {
2690
+ this._commandTimings.delete(timingId);
2691
+ return;
2692
+ }
2693
+ const testContext = {
2694
+ testFile: getCurrentTestFile() || "unknown",
2695
+ suiteName: getCurrentSuiteName() || "unknown",
2696
+ testName: getCurrentTestName() || "unknown",
2697
+ lineNumber: timing.lineNumber
2698
+ };
2699
+ storePerformanceData(timing, duration, testContext);
2700
+ this._commandTimings.delete(timingId);
2701
+ }
2702
+ }
2703
+ async afterSession() {
2704
+ if (!this._enabled) {
2705
+ return;
2706
+ }
2707
+ if (!this._reportDirectory) {
2708
+ log11.warn("Report directory not set, cannot write worker selector performance data");
2709
+ return;
2710
+ }
2711
+ const workersDataDir = path6.join(this._reportDirectory, "selector-performance-worker-data");
2712
+ const workerDataPath = path6.join(workersDataDir, `worker-data-${process.pid}.json`);
2713
+ try {
2714
+ const performanceData2 = getPerformanceData();
2715
+ fs4.mkdirSync(workersDataDir, { recursive: true });
2716
+ fs4.writeFileSync(workerDataPath, JSON.stringify(performanceData2, null, 2));
2717
+ log11.debug(`Worker selector performance data written to: ${workerDataPath} (${performanceData2.length} entries)`);
2718
+ } catch (err) {
2719
+ log11.error("Failed to write worker selector performance data:", err);
2720
+ }
2721
+ }
2722
+ /**
2723
+ * Extract device name from browser capabilities
2724
+ */
2725
+ _extractDeviceName(browser) {
2726
+ if (!browser || !("capabilities" in browser)) {
2727
+ return void 0;
2728
+ }
2729
+ const caps = browser.capabilities;
2730
+ if (caps["appium:deviceName"] && typeof caps["appium:deviceName"] === "string") {
2731
+ return caps["appium:deviceName"];
278
2732
  }
2733
+ return void 0;
279
2734
  }
280
2735
  };
281
2736
 
282
2737
  // src/index.ts
283
- var AppiumService = class {
2738
+ var AppiumService = class extends SelectorPerformanceService {
284
2739
  };
285
2740
  var launcher = AppiumLauncher;
286
2741
  export {