@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.
- package/README.md +639 -0
- package/build/index.d.ts +2 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2507 -52
- package/build/launcher.d.ts +1 -1
- package/build/launcher.d.ts.map +1 -1
- package/build/mobileSelectorPerformanceOptimizer/aggregator.d.ts +16 -0
- package/build/mobileSelectorPerformanceOptimizer/aggregator.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/markdown-formatter.d.ts +11 -0
- package/build/mobileSelectorPerformanceOptimizer/markdown-formatter.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/mspo-reporter.d.ts +45 -0
- package/build/mobileSelectorPerformanceOptimizer/mspo-reporter.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/mspo-service.d.ts +27 -0
- package/build/mobileSelectorPerformanceOptimizer/mspo-service.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/mspo-store.d.ts +50 -0
- package/build/mobileSelectorPerformanceOptimizer/mspo-store.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/optimizer.d.ts +10 -0
- package/build/mobileSelectorPerformanceOptimizer/optimizer.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/overwrite.d.ts +6 -0
- package/build/mobileSelectorPerformanceOptimizer/overwrite.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/reporting-types.d.ts +48 -0
- package/build/mobileSelectorPerformanceOptimizer/reporting-types.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/types.d.ts +53 -0
- package/build/mobileSelectorPerformanceOptimizer/types.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/browser-utils.d.ts +6 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/browser-utils.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/command-timing.d.ts +10 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/command-timing.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/constants.d.ts +14 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/constants.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/formatting.d.ts +15 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/formatting.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/index.d.ts +22 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/index.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/optimization.d.ts +8 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/optimization.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/performance-data.d.ts +10 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/performance-data.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/reporter.d.ts +16 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/reporter.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/selector-location.d.ts +20 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/selector-location.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/selector-testing.d.ts +10 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/selector-testing.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/selector-utils.d.ts +37 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/selector-utils.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/test-context.d.ts +24 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/test-context.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/timing.d.ts +7 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/timing.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/xpath-class-chain.d.ts +10 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/xpath-class-chain.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/xpath-conditions.d.ts +26 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/xpath-conditions.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/xpath-constants.d.ts +64 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/xpath-constants.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/xpath-converter.d.ts +14 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/xpath-converter.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/xpath-detection.d.ts +30 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/xpath-detection.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/xpath-page-source-executor.d.ts +30 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/xpath-page-source-executor.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/xpath-page-source.d.ts +20 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/xpath-page-source.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/xpath-parser.d.ts +9 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/xpath-parser.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/xpath-predicate.d.ts +18 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/xpath-predicate.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/xpath-selector-builder.d.ts +11 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/xpath-selector-builder.d.ts.map +1 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/xpath-types.d.ts +68 -0
- package/build/mobileSelectorPerformanceOptimizer/utils/xpath-types.d.ts.map +1 -0
- package/build/types.d.ts +46 -0
- package/build/types.d.ts.map +1 -1
- 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
|
|
3
|
+
import fs3 from "node:fs";
|
|
4
4
|
import fsp from "node:fs/promises";
|
|
5
5
|
import url from "node:url";
|
|
6
|
-
import
|
|
6
|
+
import path5 from "node:path";
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
8
|
import { promisify } from "node:util";
|
|
9
|
-
import
|
|
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
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
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 (
|
|
35
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
1811
|
+
return Array.from(selectorMap.values());
|
|
44
1812
|
}
|
|
45
|
-
function
|
|
46
|
-
const
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2155
|
+
log9.debug(data.toString());
|
|
163
2156
|
};
|
|
164
2157
|
#logStderr = (data) => {
|
|
165
|
-
|
|
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
|
-
|
|
2188
|
+
log9.info("Killing entire Appium tree");
|
|
174
2189
|
try {
|
|
175
2190
|
await this.promisifiedTreeKill(this._process.pid, "SIGTERM").catch(async (err) => {
|
|
176
|
-
|
|
2191
|
+
log9.warn("SIGTERM failed, attempting SIGKILL:", err);
|
|
177
2192
|
await this.promisifiedTreeKill(this._process.pid, "SIGKILL");
|
|
178
2193
|
});
|
|
179
|
-
|
|
2194
|
+
log9.info("Process and its children successfully terminated");
|
|
180
2195
|
} catch (err) {
|
|
181
|
-
|
|
2196
|
+
log9.error("Failed to kill Appium process tree:", err);
|
|
182
2197
|
try {
|
|
183
2198
|
this._process.kill("SIGKILL");
|
|
184
|
-
|
|
2199
|
+
log9.info("Killed main process directly");
|
|
185
2200
|
} catch (e) {
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2235
|
+
log9.warn(error);
|
|
221
2236
|
} else {
|
|
222
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
265
|
-
|
|
266
|
-
const logStream =
|
|
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
|
-
|
|
277
|
-
throw new
|
|
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 {
|