@wdio/nightwatch-devtools 1.8.1 → 2.0.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.
@@ -452,6 +452,29 @@ function formatActionTitle(action, args, params, command) {
452
452
  }
453
453
  var FILL_METHODS = /* @__PURE__ */ new Set(["fill", "selectOption"]);
454
454
 
455
+ // ../shared/src/browser-schemes.ts
456
+ var BROWSER_INTERNAL_SCHEMES = /* @__PURE__ */ new Set([
457
+ "about",
458
+ "chrome",
459
+ "chrome-error",
460
+ "chrome-extension",
461
+ "chrome-native",
462
+ "chrome-search",
463
+ "chrome-untrusted",
464
+ "devtools",
465
+ "edge",
466
+ "moz-extension",
467
+ "safari-web-extension"
468
+ ]);
469
+ var SCHEME = /^([a-z][a-z0-9+.-]*):/;
470
+ function schemeOf(url) {
471
+ return SCHEME.exec(url.toLowerCase())?.[1];
472
+ }
473
+ function isBrowserInternalUrl(url) {
474
+ const scheme = schemeOf(url);
475
+ return scheme !== void 0 && (BROWSER_INTERNAL_SCHEMES.has(scheme) || scheme.startsWith("chrome-"));
476
+ }
477
+
455
478
  // ../shared/src/console.ts
456
479
  var CONSOLE_METHODS = ["log", "info", "warn", "error"];
457
480
  var ANSI_REGEX = /\x1b\[[?]?[0-9;]*[A-Za-z]/g;
@@ -1051,6 +1074,46 @@ function imageMime(base64) {
1051
1074
  return base64.startsWith(PNG_BASE64_MAGIC) ? "image/png" : "image/jpeg";
1052
1075
  }
1053
1076
 
1077
+ // ../shared/src/request-type.ts
1078
+ function getRequestType(url, mimeType) {
1079
+ const contentType = mimeType?.toLowerCase() ?? "";
1080
+ const urlLower = url.toLowerCase();
1081
+ if (contentType.includes("text/html")) {
1082
+ return "document";
1083
+ }
1084
+ if (contentType.includes("text/css")) {
1085
+ return "stylesheet";
1086
+ }
1087
+ if (contentType.includes("javascript") || contentType.includes("ecmascript")) {
1088
+ return "script";
1089
+ }
1090
+ if (contentType.includes("image/")) {
1091
+ return "image";
1092
+ }
1093
+ if (contentType.includes("font/") || contentType.includes("woff")) {
1094
+ return "font";
1095
+ }
1096
+ if (contentType.includes("application/json")) {
1097
+ return "fetch";
1098
+ }
1099
+ if (urlLower.endsWith(".html") || urlLower.endsWith(".htm")) {
1100
+ return "document";
1101
+ }
1102
+ if (urlLower.endsWith(".css")) {
1103
+ return "stylesheet";
1104
+ }
1105
+ if (urlLower.endsWith(".js") || urlLower.endsWith(".mjs")) {
1106
+ return "script";
1107
+ }
1108
+ if (/\.(png|jpg|jpeg|gif|svg|webp|ico)$/.test(urlLower)) {
1109
+ return "image";
1110
+ }
1111
+ if (/\.(woff|woff2|ttf|eot|otf)$/.test(urlLower)) {
1112
+ return "font";
1113
+ }
1114
+ return "xhr";
1115
+ }
1116
+
1054
1117
  // ../shared/src/routes.ts
1055
1118
  var WS_PATHS = {
1056
1119
  /** Adapter session upgrade endpoint. One socket per running adapter. */
@@ -1147,6 +1210,9 @@ var TEST_STATE = {
1147
1210
  FAILED: "failed",
1148
1211
  SKIPPED: "skipped"
1149
1212
  };
1213
+ var NETWORK_CAPTURE_SOURCE = {
1214
+ RESOURCE_TIMING: "resource-timing"
1215
+ };
1150
1216
  var SCREENCAST_DEFAULTS = {
1151
1217
  enabled: false,
1152
1218
  captureFormat: "jpeg",
@@ -1805,6 +1871,21 @@ function buildActionEvents(commands, pageId, wallTime, testMetadata, snapshotInd
1805
1871
 
1806
1872
  // ../trace/src/trace-har.ts
1807
1873
  var INLINE_BODY_TEXT_MAX_BYTES = 8 * 1024;
1874
+ var HAR_PHASE_NOT_APPLICABLE = -1;
1875
+ function toHarTimings(timings, duration) {
1876
+ if (!timings) {
1877
+ return { send: 0, wait: Math.max(0, duration), receive: 0 };
1878
+ }
1879
+ return {
1880
+ blocked: timings.blocked ?? HAR_PHASE_NOT_APPLICABLE,
1881
+ dns: timings.dns ?? HAR_PHASE_NOT_APPLICABLE,
1882
+ connect: timings.connect ?? HAR_PHASE_NOT_APPLICABLE,
1883
+ ssl: timings.ssl ?? HAR_PHASE_NOT_APPLICABLE,
1884
+ send: timings.send ?? 0,
1885
+ wait: timings.wait ?? 0,
1886
+ receive: timings.receive ?? 0
1887
+ };
1888
+ }
1808
1889
  function toHeaderArray(h) {
1809
1890
  if (!h) {
1810
1891
  return [];
@@ -1868,11 +1949,9 @@ function networkRequestToHar(entry, opts = {}) {
1868
1949
  bodySize: entry.size ?? -1
1869
1950
  },
1870
1951
  cache: {},
1871
- timings: {
1872
- send: 0,
1873
- wait: Math.max(0, duration),
1874
- receive: 0
1875
- }
1952
+ timings: toHarTimings(entry.timings, duration),
1953
+ _resourceType: entry.type,
1954
+ ...entry.source ? { _source: entry.source } : {}
1876
1955
  }
1877
1956
  };
1878
1957
  }
@@ -2764,48 +2843,6 @@ import { createRequire } from "module";
2764
2843
 
2765
2844
  // ../core/src/net.ts
2766
2845
  import * as net from "net";
2767
-
2768
- // ../core/src/request-type.ts
2769
- function getRequestType(url, mimeType) {
2770
- const contentType = mimeType?.toLowerCase() ?? "";
2771
- const urlLower = url.toLowerCase();
2772
- if (contentType.includes("text/html")) {
2773
- return "document";
2774
- }
2775
- if (contentType.includes("text/css")) {
2776
- return "stylesheet";
2777
- }
2778
- if (contentType.includes("javascript") || contentType.includes("ecmascript")) {
2779
- return "script";
2780
- }
2781
- if (contentType.includes("image/")) {
2782
- return "image";
2783
- }
2784
- if (contentType.includes("font/") || contentType.includes("woff")) {
2785
- return "font";
2786
- }
2787
- if (contentType.includes("application/json")) {
2788
- return "fetch";
2789
- }
2790
- if (urlLower.endsWith(".html") || urlLower.endsWith(".htm")) {
2791
- return "document";
2792
- }
2793
- if (urlLower.endsWith(".css")) {
2794
- return "stylesheet";
2795
- }
2796
- if (urlLower.endsWith(".js") || urlLower.endsWith(".mjs")) {
2797
- return "script";
2798
- }
2799
- if (/\.(png|jpg|jpeg|gif|svg|webp|ico)$/.test(urlLower)) {
2800
- return "image";
2801
- }
2802
- if (/\.(woff|woff2|ttf|eot|otf)$/.test(urlLower)) {
2803
- return "font";
2804
- }
2805
- return "xhr";
2806
- }
2807
-
2808
- // ../core/src/net.ts
2809
2846
  function isPortInUse(port, hostname) {
2810
2847
  return new Promise((resolve) => {
2811
2848
  const server = net.createServer();
@@ -2893,34 +2930,56 @@ function bidiResponseTiming(timings, startTime, timestamp) {
2893
2930
  const endTime = Number(timestamp ?? Date.now());
2894
2931
  return { endTime, time: Math.max(0, endTime - startTime) };
2895
2932
  }
2896
- function handleBidiRequestSent(rawEvent, pending, sinks, log) {
2933
+ var MAX_TRACKED_REQUESTS = 5e3;
2934
+ function remember(rows, id, entry) {
2935
+ rows.set(id, entry);
2936
+ if (rows.size <= MAX_TRACKED_REQUESTS) {
2937
+ return;
2938
+ }
2939
+ for (const [key, row] of rows) {
2940
+ if (row.status !== void 0) {
2941
+ rows.delete(key);
2942
+ return;
2943
+ }
2944
+ }
2945
+ rows.delete(rows.keys().next().value);
2946
+ }
2947
+ function handleBidiRequestSent(rawEvent, rows, sinks, log) {
2897
2948
  const event = rawEvent;
2898
2949
  try {
2899
2950
  const requestId = String(event?.request?.request ?? event?.id ?? "");
2900
- if (!requestId) {
2951
+ const shape = rawEvent;
2952
+ if (!requestId || shape?.response || shape?.errorText) {
2901
2953
  return;
2902
2954
  }
2955
+ const previous = rows.get(requestId);
2903
2956
  const entry = {
2904
2957
  id: requestId,
2905
2958
  url: event?.request?.url ?? "",
2906
2959
  method: event?.request?.method ?? "GET",
2907
2960
  requestHeaders: arrayHeadersToObject(event?.request?.headers),
2908
- timestamp: Date.now(),
2909
- startTime: Number(event?.timestamp ?? Date.now()),
2961
+ // A redirect hop re-sends the request under the same id: one row, which
2962
+ // keeps the clock of the hop it follows and reopens for the next answer.
2963
+ timestamp: previous?.timestamp ?? Date.now(),
2964
+ startTime: previous?.startTime ?? Number(event?.timestamp ?? Date.now()),
2910
2965
  type: getRequestType(event?.request?.url ?? "")
2911
2966
  };
2912
- pending.set(requestId, entry);
2913
- sinks.pushNetworkRequest(entry);
2967
+ remember(rows, requestId, entry);
2968
+ if (previous) {
2969
+ sinks.replaceNetworkRequest(requestId, entry);
2970
+ } else {
2971
+ sinks.pushNetworkRequest(entry);
2972
+ }
2914
2973
  } catch (err) {
2915
2974
  log("warn", `beforeRequestSent threw: ${errorMessage(err)}`);
2916
2975
  }
2917
2976
  }
2918
- function handleBidiResponseCompleted(rawEvent, pending, sinks, log) {
2977
+ function handleBidiResponseCompleted(rawEvent, rows, sinks, log) {
2919
2978
  const event = rawEvent;
2920
2979
  try {
2921
2980
  const requestId = String(event?.request?.request ?? event?.id ?? "");
2922
- const previous = pending.get(requestId);
2923
- if (!previous) {
2981
+ const previous = rows.get(requestId);
2982
+ if (!previous || !event?.response) {
2924
2983
  return;
2925
2984
  }
2926
2985
  const { endTime, time } = bidiResponseTiming(
@@ -2938,22 +2997,75 @@ function handleBidiResponseCompleted(rawEvent, pending, sinks, log) {
2938
2997
  time,
2939
2998
  size: Number(event?.response?.bytesReceived) || void 0
2940
2999
  };
2941
- pending.delete(requestId);
3000
+ remember(rows, requestId, finalized);
2942
3001
  sinks.replaceNetworkRequest(requestId, finalized);
2943
3002
  } catch (err) {
2944
3003
  log("warn", `responseCompleted threw: ${errorMessage(err)}`);
2945
3004
  }
2946
3005
  }
3006
+ function handleBidiFetchError(rawEvent, rows, sinks, log) {
3007
+ const event = rawEvent;
3008
+ try {
3009
+ const requestId = String(event?.request?.request ?? event?.id ?? "");
3010
+ const previous = rows.get(requestId);
3011
+ if (!previous || typeof event?.errorText !== "string") {
3012
+ return;
3013
+ }
3014
+ const { endTime, time } = bidiResponseTiming(
3015
+ event?.request?.timings,
3016
+ previous.startTime,
3017
+ event?.timestamp
3018
+ );
3019
+ const errorText = event?.errorText ?? "Failed";
3020
+ const finalized = {
3021
+ ...previous,
3022
+ status: 0,
3023
+ statusText: errorText,
3024
+ error: errorText,
3025
+ endTime,
3026
+ time
3027
+ };
3028
+ remember(rows, requestId, finalized);
3029
+ sinks.replaceNetworkRequest(requestId, finalized);
3030
+ } catch (err) {
3031
+ log("warn", `fetchError threw: ${errorMessage(err)}`);
3032
+ }
3033
+ }
3034
+ async function subscribeFetchErrors(inspector, rows, sinks, log) {
3035
+ if (typeof inspector.fetchError === "function") {
3036
+ await inspector.fetchError((e) => handleBidiFetchError(e, rows, sinks, log));
3037
+ return;
3038
+ }
3039
+ const bidi = inspector.bidi;
3040
+ if (typeof bidi?.subscribe !== "function" || !bidi.socket?.on) {
3041
+ return;
3042
+ }
3043
+ await bidi.subscribe("network.fetchError");
3044
+ bidi.socket.on("message", (raw) => {
3045
+ try {
3046
+ const { method, params } = JSON.parse(String(raw));
3047
+ if (method === "network.fetchError") {
3048
+ handleBidiFetchError(params, rows, sinks, log);
3049
+ }
3050
+ } catch {
3051
+ }
3052
+ });
3053
+ }
2947
3054
  async function attachNetworkInspector(driver, factory, sinks, log) {
2948
3055
  try {
2949
3056
  const inspector = await factory(driver);
2950
- const pending = /* @__PURE__ */ new Map();
3057
+ const rows = /* @__PURE__ */ new Map();
2951
3058
  await inspector.beforeRequestSent(
2952
- (e) => handleBidiRequestSent(e, pending, sinks, log)
3059
+ (e) => handleBidiRequestSent(e, rows, sinks, log)
2953
3060
  );
2954
3061
  await inspector.responseCompleted(
2955
- (e) => handleBidiResponseCompleted(e, pending, sinks, log)
3062
+ (e) => handleBidiResponseCompleted(e, rows, sinks, log)
2956
3063
  );
3064
+ try {
3065
+ await subscribeFetchErrors(inspector, rows, sinks, log);
3066
+ } catch (err) {
3067
+ log("warn", `BiDi network.fetchError unavailable: ${errorMessage(err)}`);
3068
+ }
2957
3069
  log("info", "\u2713 BiDi NetworkInspector attached (request + response)");
2958
3070
  return true;
2959
3071
  } catch (err) {
@@ -3111,6 +3223,144 @@ function serializeWebSnapshot(nodes, context, options = {}) {
3111
3223
  // ../core/src/locators/xml-parsing.ts
3112
3224
  import { DOMParser } from "@xmldom/xmldom";
3113
3225
  import xpath from "xpath";
3226
+
3227
+ // ../core/src/locators/xpath-index.ts
3228
+ var NAME = /[A-Za-z_][\w.-]*/y;
3229
+ var active = null;
3230
+ function parseSimpleXPath(expr) {
3231
+ if (!expr.startsWith("//")) {
3232
+ return null;
3233
+ }
3234
+ let pos = 2;
3235
+ let tag;
3236
+ if (expr[pos] === "*") {
3237
+ tag = "*";
3238
+ pos++;
3239
+ } else {
3240
+ NAME.lastIndex = pos;
3241
+ const m = NAME.exec(expr);
3242
+ if (!m) {
3243
+ return null;
3244
+ }
3245
+ tag = m[0];
3246
+ pos = NAME.lastIndex;
3247
+ }
3248
+ if (pos === expr.length) {
3249
+ return { tag, conditions: [] };
3250
+ }
3251
+ if (expr[pos] !== "[" || !expr.endsWith("]")) {
3252
+ return null;
3253
+ }
3254
+ const conditions = parseConditions(expr.slice(pos + 1, -1));
3255
+ return conditions ? { tag, conditions } : null;
3256
+ }
3257
+ function parseConditions(body) {
3258
+ const conditions = [];
3259
+ let pos = 0;
3260
+ for (; ; ) {
3261
+ if (body[pos] !== "@") {
3262
+ return null;
3263
+ }
3264
+ NAME.lastIndex = pos + 1;
3265
+ const m = NAME.exec(body);
3266
+ if (!m || body[NAME.lastIndex] !== "=") {
3267
+ return null;
3268
+ }
3269
+ const quote = body[NAME.lastIndex + 1];
3270
+ if (quote !== '"' && quote !== "'") {
3271
+ return null;
3272
+ }
3273
+ const start = NAME.lastIndex + 2;
3274
+ const end = body.indexOf(quote, start);
3275
+ if (end === -1) {
3276
+ return null;
3277
+ }
3278
+ conditions.push({ name: m[0], value: body.slice(start, end) });
3279
+ pos = end + 1;
3280
+ if (pos === body.length) {
3281
+ return conditions;
3282
+ }
3283
+ if (!body.startsWith(" and ", pos)) {
3284
+ return null;
3285
+ }
3286
+ pos += 5;
3287
+ }
3288
+ }
3289
+ function append(map, key, value) {
3290
+ const list = map.get(key);
3291
+ if (list) {
3292
+ list.push(value);
3293
+ } else {
3294
+ map.set(key, [value]);
3295
+ }
3296
+ }
3297
+ function buildIndex(doc) {
3298
+ const index = {
3299
+ doc,
3300
+ all: [],
3301
+ byTag: /* @__PURE__ */ new Map(),
3302
+ byAttr: /* @__PURE__ */ new Map()
3303
+ };
3304
+ const stack = doc.documentElement ? [doc.documentElement] : [];
3305
+ const ordered = [];
3306
+ while (stack.length > 0) {
3307
+ const el = stack.pop();
3308
+ if (el.namespaceURI || el.nodeName.includes(":")) {
3309
+ return null;
3310
+ }
3311
+ ordered.push(el);
3312
+ for (let i = el.childNodes.length - 1; i >= 0; i--) {
3313
+ const child = el.childNodes.item(i);
3314
+ if (child?.nodeType === 1) {
3315
+ stack.push(child);
3316
+ }
3317
+ }
3318
+ }
3319
+ for (const el of ordered) {
3320
+ index.all.push(el);
3321
+ append(index.byTag, el.nodeName, el);
3322
+ for (let i = 0; i < el.attributes.length; i++) {
3323
+ const { name, value } = el.attributes.item(i);
3324
+ if (name === "xmlns" || name.includes(":")) {
3325
+ return null;
3326
+ }
3327
+ let byValue = index.byAttr.get(name);
3328
+ if (!byValue) {
3329
+ byValue = /* @__PURE__ */ new Map();
3330
+ index.byAttr.set(name, byValue);
3331
+ }
3332
+ append(byValue, value, el);
3333
+ }
3334
+ }
3335
+ return index;
3336
+ }
3337
+ function withXPathIndex(doc, walk) {
3338
+ const previous = active;
3339
+ active = doc ? buildIndex(doc) : null;
3340
+ try {
3341
+ return walk();
3342
+ } finally {
3343
+ active = previous;
3344
+ }
3345
+ }
3346
+ function selectIndexed(doc, expr) {
3347
+ if (active?.doc !== doc) {
3348
+ return null;
3349
+ }
3350
+ const query = parseSimpleXPath(expr);
3351
+ if (!query) {
3352
+ return null;
3353
+ }
3354
+ const [first, ...rest] = query.conditions;
3355
+ const candidates = first ? active.byAttr.get(first.name)?.get(first.value) ?? [] : query.tag === "*" ? active.all : active.byTag.get(query.tag) ?? [];
3356
+ return candidates.filter(
3357
+ (el) => (query.tag === "*" || el.nodeName === query.tag) && rest.every(
3358
+ (c) => el.hasAttribute(c.name) && el.getAttribute(c.name) === c.value
3359
+ )
3360
+ );
3361
+ }
3362
+
3363
+ // ../core/src/locators/xml-parsing.ts
3114
3364
  function childNodesOf(node) {
3115
3365
  const children = [];
3116
3366
  if (node.childNodes) {
@@ -3240,6 +3490,10 @@ function xmlToDOM(sourceXML) {
3240
3490
  }
3241
3491
  function evaluateXPath(doc, xpathExpr) {
3242
3492
  try {
3493
+ const indexed = selectIndexed(doc, xpathExpr);
3494
+ if (indexed) {
3495
+ return indexed;
3496
+ }
3243
3497
  const nodes = xpath.select(xpathExpr, doc);
3244
3498
  if (Array.isArray(nodes)) {
3245
3499
  return nodes;
@@ -4488,7 +4742,7 @@ function generateAllElementLocators(sourceXML, options) {
4488
4742
  results: [],
4489
4743
  parsedDOM
4490
4744
  };
4491
- traverseTree(sourceJSON, ctx);
4745
+ withXPathIndex(parsedDOM, () => traverseTree(sourceJSON, ctx));
4492
4746
  return ctx.results;
4493
4747
  }
4494
4748
 
@@ -4945,6 +5199,207 @@ function webdriverPost(address, sessionId, path10, body) {
4945
5199
  body
4946
5200
  );
4947
5201
  }
5202
+ function webdriverExecute(address, sessionId, body, args = []) {
5203
+ return webdriverPost(address, sessionId, "execute/sync", {
5204
+ script: body,
5205
+ args
5206
+ });
5207
+ }
5208
+
5209
+ // ../core/src/appium-interceptor.ts
5210
+ var INTERCEPT_CAPABILITY = "appium:intercept";
5211
+ var APPIUM_OPTIONS = "appium:options";
5212
+ var NATIVE_NETWORK_DRAIN_INTERVAL_MS = 2e3;
5213
+ function flattenInterceptedHeaders(headers) {
5214
+ const flat = {};
5215
+ for (const [name, value] of Object.entries(headers ?? {})) {
5216
+ if (/\s/.test(name)) {
5217
+ continue;
5218
+ }
5219
+ flat[name.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
5220
+ }
5221
+ return flat;
5222
+ }
5223
+ function interceptedBodyText(body) {
5224
+ if (body === void 0 || body === null || body === "") {
5225
+ return void 0;
5226
+ }
5227
+ return typeof body === "string" ? body : JSON.stringify(body);
5228
+ }
5229
+ function interceptedToNetworkRequest(entry, id, observedAt) {
5230
+ const responseHeaders = flattenInterceptedHeaders(entry.responseHeaders);
5231
+ const responseBody = interceptedBodyText(entry.responseBody);
5232
+ return {
5233
+ id,
5234
+ url: entry.url,
5235
+ method: entry.method,
5236
+ status: entry.statusCode,
5237
+ type: getRequestType(entry.url, responseHeaders["content-type"]),
5238
+ timestamp: observedAt.timestamp,
5239
+ startTime: observedAt.startTime,
5240
+ endTime: observedAt.startTime,
5241
+ requestHeaders: flattenInterceptedHeaders(entry.requestHeaders),
5242
+ responseHeaders,
5243
+ response: {
5244
+ fromCache: false,
5245
+ headers: responseHeaders,
5246
+ mimeType: responseHeaders["content-type"]?.split(";")[0]?.trim() ?? "",
5247
+ status: entry.statusCode
5248
+ },
5249
+ requestBody: interceptedBodyText(entry.requestBody),
5250
+ responseBody,
5251
+ size: responseBody === void 0 ? void 0 : Buffer.byteLength(responseBody)
5252
+ };
5253
+ }
5254
+ var InterceptorNetworkPoller = class {
5255
+ #execute;
5256
+ #listenerId;
5257
+ #seen = 0;
5258
+ // Drains are chained: two overlapping reads would both list the entries past the same `#seen`.
5259
+ #tail = Promise.resolve([]);
5260
+ constructor(execute) {
5261
+ this.#execute = execute;
5262
+ }
5263
+ async start() {
5264
+ const id = await this.#execute("interceptor: startListening", [{}]);
5265
+ this.#listenerId = typeof id === "string" ? id : void 0;
5266
+ return this.#listenerId !== void 0;
5267
+ }
5268
+ drain() {
5269
+ this.#tail = this.#tail.catch(() => []).then(() => this.#readFresh());
5270
+ return this.#tail;
5271
+ }
5272
+ async #readFresh() {
5273
+ const listenerId = this.#listenerId;
5274
+ if (!listenerId) {
5275
+ return [];
5276
+ }
5277
+ const data = await this.#execute("interceptor: getInterceptedData", [
5278
+ { id: listenerId }
5279
+ ]);
5280
+ if (!Array.isArray(data)) {
5281
+ throw new Error("the plugin returned no intercepted data");
5282
+ }
5283
+ if (data.length <= this.#seen) {
5284
+ return [];
5285
+ }
5286
+ const observedAt = { timestamp: Date.now(), startTime: performance.now() };
5287
+ const fresh = data.slice(this.#seen).map(
5288
+ (entry, offset) => interceptedToNetworkRequest(
5289
+ entry,
5290
+ `interceptor-${listenerId}-${this.#seen + offset}`,
5291
+ observedAt
5292
+ )
5293
+ );
5294
+ this.#seen = data.length;
5295
+ return fresh;
5296
+ }
5297
+ };
5298
+ function asksForIntercept(caps) {
5299
+ if (!caps || typeof caps !== "object") {
5300
+ return false;
5301
+ }
5302
+ const map = caps;
5303
+ const read = (key) => typeof map.get === "function" ? map.get(key) : map[key];
5304
+ if (read(INTERCEPT_CAPABILITY) === true) {
5305
+ return true;
5306
+ }
5307
+ const grouped = read(APPIUM_OPTIONS);
5308
+ return !!grouped && typeof grouped === "object" && grouped.intercept === true;
5309
+ }
5310
+ function requestsInterceptedNetwork(capabilities) {
5311
+ if (asksForIntercept(capabilities)) {
5312
+ return true;
5313
+ }
5314
+ if (!capabilities || typeof capabilities !== "object") {
5315
+ return false;
5316
+ }
5317
+ const { alwaysMatch, firstMatch } = capabilities;
5318
+ return [alwaysMatch, ...Array.isArray(firstMatch) ? firstMatch : []].some(
5319
+ asksForIntercept
5320
+ );
5321
+ }
5322
+ var NativeNetworkCapture = class {
5323
+ #sink;
5324
+ #onLog;
5325
+ #now;
5326
+ #lastDrainAt = Number.NEGATIVE_INFINITY;
5327
+ #poller;
5328
+ // A start superseded by a later one (session replaced mid-handshake) must not install its poller.
5329
+ #generation = 0;
5330
+ constructor(sink, onLog, now = Date.now) {
5331
+ this.#sink = sink;
5332
+ this.#onLog = onLog;
5333
+ this.#now = now;
5334
+ }
5335
+ /** Called per session: a listener dies with its session. */
5336
+ async start(target) {
5337
+ const generation = ++this.#generation;
5338
+ this.#poller = void 0;
5339
+ if (!target?.requested || !isNativeAppSession(target.capabilities)) {
5340
+ return;
5341
+ }
5342
+ if (!target.execute) {
5343
+ this.#onLog(
5344
+ "warn",
5345
+ `${INTERCEPT_CAPABILITY} is set but ${target.unavailableReason ?? "the driver cannot be reached outside the command path"}; native network is not captured`
5346
+ );
5347
+ return;
5348
+ }
5349
+ const poller = new InterceptorNetworkPoller(target.execute);
5350
+ const started = await poller.start().catch((err) => {
5351
+ this.#onLog(
5352
+ "warn",
5353
+ `appium-interceptor listener failed to start: ${errorMessage(err)}`
5354
+ );
5355
+ return false;
5356
+ });
5357
+ if (generation !== this.#generation) {
5358
+ return;
5359
+ }
5360
+ if (!started) {
5361
+ this.#onLog(
5362
+ "warn",
5363
+ `${INTERCEPT_CAPABILITY} is set but the interceptor listener did not start; is Appium running with --use-plugins=appium-interceptor?`
5364
+ );
5365
+ return;
5366
+ }
5367
+ this.#onLog("info", "Native network capture active via appium-interceptor");
5368
+ this.#poller = poller;
5369
+ this.#lastDrainAt = Number.NEGATIVE_INFINITY;
5370
+ }
5371
+ /** Whether a listener is running, so a caller can skip a drain that cannot find anything. */
5372
+ get active() {
5373
+ return this.#poller !== void 0;
5374
+ }
5375
+ /** At session end, so a drain after the driver quit does not reach a dead session. */
5376
+ stop() {
5377
+ this.#generation++;
5378
+ this.#poller = void 0;
5379
+ }
5380
+ /** Lists what the proxy recorded since the last call. A test or session boundary must not pass `throttle`. */
5381
+ async forward(options = {}) {
5382
+ const poller = this.#poller;
5383
+ if (!poller) {
5384
+ return;
5385
+ }
5386
+ const now = this.#now();
5387
+ if (options.throttle && now - this.#lastDrainAt < NATIVE_NETWORK_DRAIN_INTERVAL_MS) {
5388
+ return;
5389
+ }
5390
+ this.#lastDrainAt = now;
5391
+ const requests = await poller.drain().catch((err) => {
5392
+ this.#onLog(
5393
+ "warn",
5394
+ `Reading appium-interceptor data failed: ${errorMessage(err)}`
5395
+ );
5396
+ return [];
5397
+ });
5398
+ if (requests.length > 0) {
5399
+ this.#sink.recordNetworkRequests(requests);
5400
+ }
5401
+ }
5402
+ };
4948
5403
 
4949
5404
  // ../core/src/viewport.ts
4950
5405
  var VISUAL_VIEWPORT_SCRIPT = `
@@ -5284,20 +5739,8 @@ async function loadCollectorSource() {
5284
5739
  async function loadInjectableScript() {
5285
5740
  return `(async function() { ${await loadCollectorSource()} })()`;
5286
5741
  }
5287
- var NON_INSTRUMENTABLE_SCHEMES = [
5288
- "about:",
5289
- "data:",
5290
- "chrome:",
5291
- "chrome-error:",
5292
- "edge:",
5293
- "moz-extension:",
5294
- "chrome-extension:"
5295
- ];
5296
5742
  function isInstrumentableDocument(url) {
5297
- if (!url) {
5298
- return false;
5299
- }
5300
- return !NON_INSTRUMENTABLE_SCHEMES.some((scheme) => url.startsWith(scheme));
5743
+ return Boolean(url) && !isBrowserInternalUrl(url) && schemeOf(url) !== "data";
5301
5744
  }
5302
5745
  async function drainCollectorWithRecovery(opts) {
5303
5746
  const payload = await opts.drain();
@@ -5329,6 +5772,9 @@ async function drainCollectorWithRecovery(opts) {
5329
5772
  async function pollUntilReady(check, opts = {}) {
5330
5773
  const attempts = opts.attempts ?? 5;
5331
5774
  const intervalMs = opts.intervalMs ?? 200;
5775
+ if (opts.checkFirst && await check()) {
5776
+ return true;
5777
+ }
5332
5778
  for (let i = 0; i < attempts; i++) {
5333
5779
  await new Promise((resolve) => setTimeout(resolve, intervalMs));
5334
5780
  if (await check()) {
@@ -5605,18 +6051,28 @@ function resolveAssertTargetFromArgs(args, fromHandle) {
5605
6051
  }
5606
6052
 
5607
6053
  // ../core/src/retry-tracker.ts
5608
- var RetryTracker = class {
6054
+ var RetryTracker = class _RetryTracker {
5609
6055
  #lastSig = null;
6056
+ #lastBareSig = null;
5610
6057
  #lastId = null;
5611
6058
  /** Build the canonical signature used for retry-equality checks. */
5612
6059
  static signature(command, args, callSource) {
5613
6060
  return JSON.stringify({ command, args, src: callSource ?? null });
5614
6061
  }
6062
+ /** The signature without the call site: a framework's own repeat of a call
6063
+ * (a `wait()` poll from a timer) has no user frame on its stack. */
6064
+ static bareSignature(command, args) {
6065
+ return JSON.stringify({ command, args });
6066
+ }
5615
6067
  /** True when the incoming signature matches the last captured one AND we
5616
6068
  * have an id to replace (otherwise there's nothing to replace yet). */
5617
6069
  isRetry(sig) {
5618
6070
  return sig === this.#lastSig && this.#lastId !== null;
5619
6071
  }
6072
+ /** As `isRetry`, for a repeat that carries no call site of its own. */
6073
+ isRetryIgnoringSource(bareSig) {
6074
+ return bareSig === this.#lastBareSig && this.#lastId !== null;
6075
+ }
5620
6076
  /** The id of the last captured command, if any (for the replace-in-place
5621
6077
  * flow). */
5622
6078
  get lastId() {
@@ -5625,8 +6081,17 @@ var RetryTracker = class {
5625
6081
  /** Record a fresh capture — sets both sig and id together. */
5626
6082
  recordCapture(sig, id) {
5627
6083
  this.#lastSig = sig;
6084
+ this.#lastBareSig = _RetryTracker.bareOf(sig);
5628
6085
  this.#lastId = id;
5629
6086
  }
6087
+ static bareOf(sig) {
6088
+ try {
6089
+ const { command, args } = JSON.parse(sig);
6090
+ return _RetryTracker.bareSignature(command, args);
6091
+ } catch {
6092
+ return null;
6093
+ }
6094
+ }
5630
6095
  /** Record only the id (used by adapters that compute the sig but defer the
5631
6096
  * id assignment to after an async capture call). */
5632
6097
  setLastId(id) {
@@ -5637,10 +6102,12 @@ var RetryTracker = class {
5637
6102
  * commands). Pair with {@link setLastId} once the capture resolves. */
5638
6103
  setLastSig(sig) {
5639
6104
  this.#lastSig = sig;
6105
+ this.#lastBareSig = _RetryTracker.bareOf(sig);
5640
6106
  }
5641
6107
  /** Reset at test/scenario boundaries so the next capture is "fresh". */
5642
6108
  reset() {
5643
6109
  this.#lastSig = null;
6110
+ this.#lastBareSig = null;
5644
6111
  this.#lastId = null;
5645
6112
  }
5646
6113
  };
@@ -5919,6 +6386,31 @@ var ScreencastRecorderBase = class {
5919
6386
  }
5920
6387
  };
5921
6388
 
6389
+ // ../core/src/browser-focus.ts
6390
+ import { spawn } from "child_process";
6391
+ var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
6392
+ function restoreBrowserFocus(target, platform = process.platform, launch = spawn) {
6393
+ const local = !target.host || LOCAL_HOSTS.has(target.host);
6394
+ if (platform !== "darwin" || !local || !/safari/i.test(target.browserName ?? "") || /ios/i.test(target.platformName ?? "")) {
6395
+ return false;
6396
+ }
6397
+ try {
6398
+ const child = launch("open", ["-a", "Safari"], {
6399
+ detached: true,
6400
+ stdio: "ignore"
6401
+ });
6402
+ child.on?.(
6403
+ "error",
6404
+ (err) => target.onError?.(`Could not bring Safari to the front: ${err.message}`)
6405
+ );
6406
+ child.unref?.();
6407
+ return true;
6408
+ } catch (err) {
6409
+ target.onError?.(`Could not bring Safari to the front: ${String(err)}`);
6410
+ return false;
6411
+ }
6412
+ }
6413
+
5922
6414
  // ../core/src/session-capturer.ts
5923
6415
  import fs10 from "fs/promises";
5924
6416
  import { WebSocket } from "ws";
@@ -6267,26 +6759,24 @@ var SessionCapturerBase = class _SessionCapturerBase {
6267
6759
  *
6268
6760
  * `skipConsoleLogs` / `skipNetworkRequests` opt out when an out-of-band
6269
6761
  * channel (BiDi) is already delivering those streams — without the gate
6270
- * the dashboard would see each entry twice.
6762
+ * the dashboard would see each entry twice. `skipResourceTimingRequests`
6763
+ * drops only the page's Resource Timing entries: a driver-side source
6764
+ * reports the same loads with their headers, while the collector's patched
6765
+ * fetch/XHR rows still carry the bodies nothing else captures.
6271
6766
  */
6272
6767
  processTracePayload(payload, opts = {}) {
6273
6768
  const { mutations, traceLogs, consoleLogs, networkRequests, metadata } = payload;
6274
6769
  if (metadata && typeof metadata === "object") {
6275
6770
  this.mergeMetadata(metadata);
6276
6771
  }
6277
- if (!opts.skipConsoleLogs && Array.isArray(consoleLogs) && consoleLogs.length > 0) {
6278
- const tagged = consoleLogs.map((entry) => ({
6279
- ...entry,
6280
- source: LOG_SOURCES.BROWSER
6281
- }));
6772
+ const tagged = opts.skipConsoleLogs ? [] : this.#tagBrowserConsoleLogs(consoleLogs);
6773
+ if (tagged.length > 0) {
6282
6774
  this.consoleLogs.push(...tagged);
6283
6775
  this.sendUpstream("consoleLogs", tagged);
6284
6776
  }
6285
- if (!opts.skipNetworkRequests && Array.isArray(networkRequests) && networkRequests.length > 0) {
6286
- const reqs = networkRequests;
6287
- this.networkRequests.push(...reqs);
6288
- this.sendUpstream("networkRequests", reqs);
6289
- }
6777
+ this.recordNetworkRequests(
6778
+ this.#acceptedNetworkRequests(networkRequests, opts)
6779
+ );
6290
6780
  if (Array.isArray(mutations) && mutations.length > 0) {
6291
6781
  const batch = mutations;
6292
6782
  reattributeDomAnchors(
@@ -6303,6 +6793,50 @@ var SessionCapturerBase = class _SessionCapturerBase {
6303
6793
  this.sendUpstream("logs", logs);
6304
6794
  }
6305
6795
  }
6796
+ #tagBrowserConsoleLogs(consoleLogs) {
6797
+ if (!Array.isArray(consoleLogs)) {
6798
+ return [];
6799
+ }
6800
+ return consoleLogs.map((entry) => ({
6801
+ ...entry,
6802
+ source: LOG_SOURCES.BROWSER
6803
+ }));
6804
+ }
6805
+ /** Lists network rows and sends them in one message; a re-sent id replaces its row, as the dashboard does. */
6806
+ recordNetworkRequests(reqs) {
6807
+ if (reqs.length === 0) {
6808
+ return;
6809
+ }
6810
+ for (const req of reqs) {
6811
+ this.#upsertNetworkRequest(req);
6812
+ }
6813
+ this.sendUpstream("networkRequests", reqs);
6814
+ }
6815
+ /** Row index by request id; checked against the row before use, since adapters also write rows by index. */
6816
+ #networkRequestIndex = /* @__PURE__ */ new Map();
6817
+ #upsertNetworkRequest(req) {
6818
+ let at = req.id ? this.#networkRequestIndex.get(req.id) : void 0;
6819
+ if (at !== void 0 && this.networkRequests[at]?.id !== req.id) {
6820
+ at = this.networkRequests.findIndex((r) => r.id === req.id);
6821
+ }
6822
+ if (at !== void 0 && at !== -1) {
6823
+ this.networkRequests[at] = req;
6824
+ this.#networkRequestIndex.set(req.id, at);
6825
+ return;
6826
+ }
6827
+ if (req.id) {
6828
+ this.#networkRequestIndex.set(req.id, this.networkRequests.length);
6829
+ }
6830
+ this.networkRequests.push(req);
6831
+ }
6832
+ #acceptedNetworkRequests(networkRequests, opts) {
6833
+ if (opts.skipNetworkRequests || !Array.isArray(networkRequests)) {
6834
+ return [];
6835
+ }
6836
+ return networkRequests.filter(
6837
+ (req) => !opts.skipResourceTimingRequests || req.source !== NETWORK_CAPTURE_SOURCE.RESOURCE_TIMING
6838
+ );
6839
+ }
6306
6840
  /**
6307
6841
  * Resolve when the WS reaches OPEN state, or `false` on timeout / error.
6308
6842
  * Returns immediately if already open. Used by adapters that need a
@@ -6713,9 +7247,11 @@ export {
6713
7247
  mapCommandToAction,
6714
7248
  stripAnsi,
6715
7249
  mapChromeBrowserLogs,
7250
+ getRequestType,
6716
7251
  WS_SCOPE,
6717
7252
  REUSE_ENV,
6718
7253
  TEST_STATE,
7254
+ NETWORK_CAPTURE_SOURCE,
6719
7255
  SCREENCAST_DEFAULTS,
6720
7256
  captureActionSnapshot,
6721
7257
  toError,
@@ -6728,6 +7264,9 @@ export {
6728
7264
  TestAttemptTracker,
6729
7265
  webdriverGet,
6730
7266
  webdriverPost,
7267
+ webdriverExecute,
7268
+ requestsInterceptedNetwork,
7269
+ NativeNetworkCapture,
6731
7270
  resolveViewport,
6732
7271
  isUserCodeFrame,
6733
7272
  normalizeFilePath,
@@ -6744,7 +7283,6 @@ export {
6744
7283
  recordSpecBoundary,
6745
7284
  flushRangeLogged,
6746
7285
  finalizeTraceExport,
6747
- getRequestType,
6748
7286
  findFreePort,
6749
7287
  attachBidiHandlers,
6750
7288
  COLLECTOR_READY_EXPRESSION,
@@ -6762,6 +7300,7 @@ export {
6762
7300
  resolveAssertTargetFromArgs,
6763
7301
  RetryTracker,
6764
7302
  ScreencastRecorderBase,
7303
+ restoreBrowserFocus,
6765
7304
  SessionCapturerBase,
6766
7305
  createSuiteStats,
6767
7306
  createTestStats,
@@ -6773,4 +7312,4 @@ export {
6773
7312
  findTestDefinitions,
6774
7313
  TestReporterBase
6775
7314
  };
6776
- //# sourceMappingURL=chunk-DM4FCAIJ.js.map
7315
+ //# sourceMappingURL=chunk-TUK7ZLQI.js.map