@validate.qa/runner 1.0.76 → 1.0.78
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/dist/cli.js +92 -27
- package/dist/cli.mjs +92 -27
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -86290,11 +86290,25 @@ var require_api_call_redaction = __commonJS({
|
|
|
86290
86290
|
const lower = name.toLowerCase();
|
|
86291
86291
|
return headers.find((h) => h.name?.toLowerCase() === lower)?.value;
|
|
86292
86292
|
}
|
|
86293
|
+
var ENUM_LIKE_FIELD_NAME = /status|state|phase|role|tier|kind|mode|level|category|priority|visibility|^type$|_type$/i;
|
|
86294
|
+
var NEVER_SAMPLE_FIELD_NAME = /password|token|secret|api.?key|auth|bearer|credential|session|email|phone|address|ssn/i;
|
|
86295
|
+
var ENUM_LIKE_VALUE = /^[A-Za-z][A-Za-z0-9_-]{0,29}$/;
|
|
86296
|
+
function enumSampleHint(key, val) {
|
|
86297
|
+
if (typeof val !== "string")
|
|
86298
|
+
return null;
|
|
86299
|
+
if (NEVER_SAMPLE_FIELD_NAME.test(key))
|
|
86300
|
+
return null;
|
|
86301
|
+
if (!ENUM_LIKE_FIELD_NAME.test(key))
|
|
86302
|
+
return null;
|
|
86303
|
+
if (!ENUM_LIKE_VALUE.test(val))
|
|
86304
|
+
return null;
|
|
86305
|
+
return val;
|
|
86306
|
+
}
|
|
86293
86307
|
function extractBodyShape(body) {
|
|
86294
86308
|
if (!body)
|
|
86295
86309
|
return void 0;
|
|
86296
86310
|
try {
|
|
86297
|
-
let describeValue = function(val, depth) {
|
|
86311
|
+
let describeValue = function(val, depth, key) {
|
|
86298
86312
|
if (val === null)
|
|
86299
86313
|
return "null";
|
|
86300
86314
|
if (Array.isArray(val)) {
|
|
@@ -86311,10 +86325,16 @@ var require_api_call_redaction = __commonJS({
|
|
|
86311
86325
|
const subFields = Object.keys(v).slice(0, 15).join(",");
|
|
86312
86326
|
return subFields ? `${k}:{${subFields}}` : k;
|
|
86313
86327
|
}
|
|
86314
|
-
|
|
86328
|
+
const hint = enumSampleHint(k, v);
|
|
86329
|
+
return hint ? `${k}(e.g. "${hint}")` : k;
|
|
86315
86330
|
});
|
|
86316
86331
|
return `object{${fieldDescs.join(",")}}`;
|
|
86317
86332
|
}
|
|
86333
|
+
if (key) {
|
|
86334
|
+
const hint = enumSampleHint(key, val);
|
|
86335
|
+
if (hint)
|
|
86336
|
+
return `string (e.g. "${hint}")`;
|
|
86337
|
+
}
|
|
86318
86338
|
return typeof val;
|
|
86319
86339
|
};
|
|
86320
86340
|
const parsed = JSON.parse(body);
|
|
@@ -86322,7 +86342,7 @@ var require_api_call_redaction = __commonJS({
|
|
|
86322
86342
|
return void 0;
|
|
86323
86343
|
const shape = {};
|
|
86324
86344
|
for (const [key, val] of Object.entries(parsed)) {
|
|
86325
|
-
shape[key] = describeValue(val, 1);
|
|
86345
|
+
shape[key] = describeValue(val, 1, key);
|
|
86326
86346
|
}
|
|
86327
86347
|
return Object.keys(shape).length > 0 ? shape : void 0;
|
|
86328
86348
|
} catch {
|
|
@@ -97641,6 +97661,12 @@ ${testOpen}`;
|
|
|
97641
97661
|
}
|
|
97642
97662
|
}
|
|
97643
97663
|
}
|
|
97664
|
+
const droppedByCalls = codeForMatcherScan.match(/\.get(?:Role|Text|Label|Placeholder|TestId|AltText|Title)\s*\(/g) ?? [];
|
|
97665
|
+
for (const call of droppedByCalls) {
|
|
97666
|
+
const methodName = call.replace(/[\s(]+$/, "").slice(1);
|
|
97667
|
+
logs2?.push(` \u2717 Rejected "${t.name}": "${methodName}" is not a real Playwright method \u2014 likely "${methodName.replace("get", "getBy")}" with "By" dropped`);
|
|
97668
|
+
return false;
|
|
97669
|
+
}
|
|
97644
97670
|
if (code.includes("// TODO")) {
|
|
97645
97671
|
logs2?.push(` \u26A0 "${t.name}" contains TODO comment \u2014 marking as unverified`);
|
|
97646
97672
|
t.tags = t.tags.filter((tag) => tag !== "@verified");
|
|
@@ -353034,7 +353060,7 @@ var require_package4 = __commonJS({
|
|
|
353034
353060
|
"package.json"(exports2, module2) {
|
|
353035
353061
|
module2.exports = {
|
|
353036
353062
|
name: "@validate.qa/runner",
|
|
353037
|
-
version: "1.0.
|
|
353063
|
+
version: "1.0.78",
|
|
353038
353064
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
353039
353065
|
bin: {
|
|
353040
353066
|
"validate-runner": "dist/cli.js"
|
|
@@ -354700,17 +354726,13 @@ async function clickCssInCurrentContext(sessionPath, css) {
|
|
|
354700
354726
|
}
|
|
354701
354727
|
async function clickByVisibleTextInCurrentContext(sessionPath, text) {
|
|
354702
354728
|
const script = `
|
|
354729
|
+
if (!document.body) { return 0; }
|
|
354703
354730
|
var t = String(arguments[0]).trim().replace(/\\s+/g, ' ');
|
|
354704
|
-
|
|
354731
|
+
// String() coercion is load-bearing: e.value is a NUMBER for <progress>/
|
|
354732
|
+
// <meter>/<li value>, and (70).trim() would throw, aborting the whole
|
|
354733
|
+
// script (and thus the fallback) on any page containing one.
|
|
354734
|
+
var norm = function (e) { return String(e.textContent || e.value || '').trim().replace(/\\s+/g, ' '); };
|
|
354705
354735
|
var vis = function (e) { var r = e.getBoundingClientRect(); return r.width > 0 && r.height > 0; };
|
|
354706
|
-
var all = Array.prototype.slice.call(document.body.getElementsByTagName('*'));
|
|
354707
|
-
var match = all.filter(function (e) { return norm(e) === t && vis(e); });
|
|
354708
|
-
// Deepest-only: drop any match that contains another match (same text).
|
|
354709
|
-
var deepest = match.filter(function (e) {
|
|
354710
|
-
return !match.some(function (o) { return o !== e && e.contains(o); });
|
|
354711
|
-
});
|
|
354712
|
-
if (deepest.length !== 1) { return deepest.length; }
|
|
354713
|
-
var el = deepest[0];
|
|
354714
354736
|
var interactive = function (e) {
|
|
354715
354737
|
var tag = e.tagName;
|
|
354716
354738
|
if (tag === 'BUTTON' || tag === 'A' || tag === 'INPUT' || tag === 'SELECT' || tag === 'SUMMARY') { return true; }
|
|
@@ -354719,6 +354741,33 @@ async function clickByVisibleTextInCurrentContext(sessionPath, text) {
|
|
|
354719
354741
|
if (e.hasAttribute && (e.hasAttribute('onclick') || (e.hasAttribute('tabindex') && e.getAttribute('tabindex') !== '-1'))) { return true; }
|
|
354720
354742
|
try { return window.getComputedStyle(e).cursor === 'pointer'; } catch (err) { return false; }
|
|
354721
354743
|
};
|
|
354744
|
+
// Is this match itself interactive, or wrapped by an interactive ancestor
|
|
354745
|
+
// that contains ONLY this text?
|
|
354746
|
+
var hasInteractiveHost = function (e) {
|
|
354747
|
+
var hop = e;
|
|
354748
|
+
for (var i = 0; i < 5 && hop && hop !== document.body; i++) {
|
|
354749
|
+
if (norm(hop) !== t) { return false; }
|
|
354750
|
+
if (interactive(hop)) { return true; }
|
|
354751
|
+
hop = hop.parentElement;
|
|
354752
|
+
}
|
|
354753
|
+
return false;
|
|
354754
|
+
};
|
|
354755
|
+
var all = Array.prototype.slice.call(document.body.getElementsByTagName('*'));
|
|
354756
|
+
var match = all.filter(function (e) { return norm(e) === t && vis(e); });
|
|
354757
|
+
// Deepest-only: drop any match that contains another match (same text).
|
|
354758
|
+
var deepest = match.filter(function (e) {
|
|
354759
|
+
return !match.some(function (o) { return o !== e && e.contains(o); });
|
|
354760
|
+
});
|
|
354761
|
+
var candidates = deepest;
|
|
354762
|
+
if (candidates.length > 1) {
|
|
354763
|
+
// Disambiguate: a NON-interactive duplicate (page heading, badge, label
|
|
354764
|
+
// echoing a control's caption) must not veto the one real control. Only
|
|
354765
|
+
// when several INTERACTIVE hosts share the text is it truly ambiguous.
|
|
354766
|
+
var interactiveOnly = candidates.filter(hasInteractiveHost);
|
|
354767
|
+
if (interactiveOnly.length >= 1) { candidates = interactiveOnly; }
|
|
354768
|
+
}
|
|
354769
|
+
if (candidates.length !== 1) { return candidates.length; }
|
|
354770
|
+
var el = candidates[0];
|
|
354722
354771
|
// Prefer the outermost interactive ancestor that wraps ONLY this text.
|
|
354723
354772
|
var target = el;
|
|
354724
354773
|
var hop = el;
|
|
@@ -354892,6 +354941,7 @@ var QUARANTINE_MAX_MS = (() => {
|
|
|
354892
354941
|
return Number.isFinite(raw) && raw > 0 ? raw : 10 * 60 * 1e3;
|
|
354893
354942
|
})();
|
|
354894
354943
|
var quarantinedAt = /* @__PURE__ */ new Map();
|
|
354944
|
+
var nextQuarantineToken = 1;
|
|
354895
354945
|
var DeviceQuarantinedError = class extends Error {
|
|
354896
354946
|
constructor(deviceId) {
|
|
354897
354947
|
super(
|
|
@@ -354901,14 +354951,21 @@ var DeviceQuarantinedError = class extends Error {
|
|
|
354901
354951
|
}
|
|
354902
354952
|
};
|
|
354903
354953
|
function quarantineDevice(deviceId, now = Date.now()) {
|
|
354904
|
-
|
|
354954
|
+
const token = nextQuarantineToken++;
|
|
354955
|
+
quarantinedAt.set(deviceId, { at: now, token });
|
|
354956
|
+
return token;
|
|
354905
354957
|
}
|
|
354906
|
-
function unquarantineDevice(deviceId) {
|
|
354907
|
-
|
|
354958
|
+
function unquarantineDevice(deviceId, token) {
|
|
354959
|
+
if (token === void 0) {
|
|
354960
|
+
quarantinedAt.delete(deviceId);
|
|
354961
|
+
return;
|
|
354962
|
+
}
|
|
354963
|
+
const entry = quarantinedAt.get(deviceId);
|
|
354964
|
+
if (entry && entry.token === token) quarantinedAt.delete(deviceId);
|
|
354908
354965
|
}
|
|
354909
354966
|
function isDeviceQuarantined(deviceId, now = Date.now()) {
|
|
354910
354967
|
if (typeof deviceId !== "string") return false;
|
|
354911
|
-
const since = quarantinedAt.get(deviceId);
|
|
354968
|
+
const since = quarantinedAt.get(deviceId)?.at;
|
|
354912
354969
|
if (since === void 0) return false;
|
|
354913
354970
|
if (now - since >= QUARANTINE_MAX_MS) {
|
|
354914
354971
|
quarantinedAt.delete(deviceId);
|
|
@@ -357266,7 +357323,8 @@ async function executeTap(sessionId, step, platform3, isCrossPlatform = false) {
|
|
|
357266
357323
|
const bounds = boundsForRun(step, platform3, isCrossPlatform);
|
|
357267
357324
|
if (step.target) {
|
|
357268
357325
|
try {
|
|
357269
|
-
const
|
|
357326
|
+
const explicitTimeout = readStepTimeout(step);
|
|
357327
|
+
const elementId = await findElement(sessionId, step.target, isPermissionTap ? explicitTimeout ?? PERMISSION_DIALOG_TIMEOUT_MS : explicitTimeout, {
|
|
357270
357328
|
platform: platform3,
|
|
357271
357329
|
recordedBounds: bounds
|
|
357272
357330
|
});
|
|
@@ -358360,9 +358418,15 @@ let timer = null;
|
|
|
358360
358418
|
async function post() {
|
|
358361
358419
|
if (!url || !headers || !body) return; // inert until the first real heartbeat
|
|
358362
358420
|
try {
|
|
358421
|
+
// The keepalive discards the response body, so it must identify itself:
|
|
358422
|
+
// the server's heartbeat handler destructively drains queued runner
|
|
358423
|
+
// control commands (kill-pid/kill-orphans) into the response, and a
|
|
358424
|
+
// drained command this worker throws away would be lost for a full
|
|
358425
|
+
// re-issue window. The header tells the server to keep the queue intact
|
|
358426
|
+
// and let the main loop (which executes commands) drain it.
|
|
358363
358427
|
await fetch(url, {
|
|
358364
358428
|
method: 'POST',
|
|
358365
|
-
headers,
|
|
358429
|
+
headers: { ...headers, 'x-validateqa-keepalive': '1' },
|
|
358366
358430
|
body,
|
|
358367
358431
|
signal: AbortSignal.timeout(10000),
|
|
358368
358432
|
});
|
|
@@ -360700,6 +360764,7 @@ var MobileRunWatchdog = class {
|
|
|
360700
360764
|
phase = "idle";
|
|
360701
360765
|
released = false;
|
|
360702
360766
|
quarantined = false;
|
|
360767
|
+
quarantineToken;
|
|
360703
360768
|
cancelAbortTimer = null;
|
|
360704
360769
|
cancelGraceTimer = null;
|
|
360705
360770
|
/** Current phase (exposed for tests / observability). */
|
|
@@ -360728,7 +360793,8 @@ var MobileRunWatchdog = class {
|
|
|
360728
360793
|
this.clearAbortTimer();
|
|
360729
360794
|
if (this.options.deviceId) {
|
|
360730
360795
|
this.quarantined = true;
|
|
360731
|
-
this.options.callbacks.quarantine(this.options.deviceId);
|
|
360796
|
+
const token = this.options.callbacks.quarantine(this.options.deviceId);
|
|
360797
|
+
this.quarantineToken = typeof token === "number" ? token : void 0;
|
|
360732
360798
|
this.options.callbacks.abort(this.options.deviceId, this.options.runId, reason);
|
|
360733
360799
|
} else {
|
|
360734
360800
|
this.options.callbacks.log("warn", `Run ${this.options.runId} ${reason} \u2014 no leased device to abort; awaiting the run's own settle.`);
|
|
@@ -360753,7 +360819,7 @@ var MobileRunWatchdog = class {
|
|
|
360753
360819
|
}
|
|
360754
360820
|
if (this.quarantined && this.options.deviceId) {
|
|
360755
360821
|
this.quarantined = false;
|
|
360756
|
-
this.options.callbacks.unquarantine(this.options.deviceId);
|
|
360822
|
+
this.options.callbacks.unquarantine(this.options.deviceId, this.quarantineToken);
|
|
360757
360823
|
if (wasForceReleased) {
|
|
360758
360824
|
this.options.callbacks.log("info", `Run ${this.options.runId} finally settled \u2014 lifted the quarantine on device ${this.options.deviceId}.`);
|
|
360759
360825
|
}
|
|
@@ -360771,7 +360837,8 @@ var MobileRunWatchdog = class {
|
|
|
360771
360837
|
if (this.options.deviceId) {
|
|
360772
360838
|
if (!this.quarantined) {
|
|
360773
360839
|
this.quarantined = true;
|
|
360774
|
-
this.options.callbacks.quarantine(this.options.deviceId);
|
|
360840
|
+
const token = this.options.callbacks.quarantine(this.options.deviceId);
|
|
360841
|
+
this.quarantineToken = typeof token === "number" ? token : void 0;
|
|
360775
360842
|
}
|
|
360776
360843
|
this.options.callbacks.log(
|
|
360777
360844
|
"warn",
|
|
@@ -364417,11 +364484,9 @@ async function startRunner(opts) {
|
|
|
364417
364484
|
if (sessionId) void deleteAppiumSession(sessionId);
|
|
364418
364485
|
},
|
|
364419
364486
|
release: applyRelease,
|
|
364420
|
-
quarantine: (deviceId) =>
|
|
364421
|
-
|
|
364422
|
-
|
|
364423
|
-
unquarantine: (deviceId) => {
|
|
364424
|
-
unquarantineDevice(deviceId);
|
|
364487
|
+
quarantine: (deviceId) => quarantineDevice(deviceId),
|
|
364488
|
+
unquarantine: (deviceId, token) => {
|
|
364489
|
+
unquarantineDevice(deviceId, token);
|
|
364425
364490
|
},
|
|
364426
364491
|
log: (level, message) => {
|
|
364427
364492
|
if (level === "warn") log.warn(message);
|
package/dist/cli.mjs
CHANGED
|
@@ -86295,11 +86295,25 @@ var require_api_call_redaction = __commonJS({
|
|
|
86295
86295
|
const lower = name.toLowerCase();
|
|
86296
86296
|
return headers.find((h) => h.name?.toLowerCase() === lower)?.value;
|
|
86297
86297
|
}
|
|
86298
|
+
var ENUM_LIKE_FIELD_NAME = /status|state|phase|role|tier|kind|mode|level|category|priority|visibility|^type$|_type$/i;
|
|
86299
|
+
var NEVER_SAMPLE_FIELD_NAME = /password|token|secret|api.?key|auth|bearer|credential|session|email|phone|address|ssn/i;
|
|
86300
|
+
var ENUM_LIKE_VALUE = /^[A-Za-z][A-Za-z0-9_-]{0,29}$/;
|
|
86301
|
+
function enumSampleHint(key, val) {
|
|
86302
|
+
if (typeof val !== "string")
|
|
86303
|
+
return null;
|
|
86304
|
+
if (NEVER_SAMPLE_FIELD_NAME.test(key))
|
|
86305
|
+
return null;
|
|
86306
|
+
if (!ENUM_LIKE_FIELD_NAME.test(key))
|
|
86307
|
+
return null;
|
|
86308
|
+
if (!ENUM_LIKE_VALUE.test(val))
|
|
86309
|
+
return null;
|
|
86310
|
+
return val;
|
|
86311
|
+
}
|
|
86298
86312
|
function extractBodyShape(body) {
|
|
86299
86313
|
if (!body)
|
|
86300
86314
|
return void 0;
|
|
86301
86315
|
try {
|
|
86302
|
-
let describeValue = function(val, depth) {
|
|
86316
|
+
let describeValue = function(val, depth, key) {
|
|
86303
86317
|
if (val === null)
|
|
86304
86318
|
return "null";
|
|
86305
86319
|
if (Array.isArray(val)) {
|
|
@@ -86316,10 +86330,16 @@ var require_api_call_redaction = __commonJS({
|
|
|
86316
86330
|
const subFields = Object.keys(v).slice(0, 15).join(",");
|
|
86317
86331
|
return subFields ? `${k}:{${subFields}}` : k;
|
|
86318
86332
|
}
|
|
86319
|
-
|
|
86333
|
+
const hint = enumSampleHint(k, v);
|
|
86334
|
+
return hint ? `${k}(e.g. "${hint}")` : k;
|
|
86320
86335
|
});
|
|
86321
86336
|
return `object{${fieldDescs.join(",")}}`;
|
|
86322
86337
|
}
|
|
86338
|
+
if (key) {
|
|
86339
|
+
const hint = enumSampleHint(key, val);
|
|
86340
|
+
if (hint)
|
|
86341
|
+
return `string (e.g. "${hint}")`;
|
|
86342
|
+
}
|
|
86323
86343
|
return typeof val;
|
|
86324
86344
|
};
|
|
86325
86345
|
const parsed = JSON.parse(body);
|
|
@@ -86327,7 +86347,7 @@ var require_api_call_redaction = __commonJS({
|
|
|
86327
86347
|
return void 0;
|
|
86328
86348
|
const shape = {};
|
|
86329
86349
|
for (const [key, val] of Object.entries(parsed)) {
|
|
86330
|
-
shape[key] = describeValue(val, 1);
|
|
86350
|
+
shape[key] = describeValue(val, 1, key);
|
|
86331
86351
|
}
|
|
86332
86352
|
return Object.keys(shape).length > 0 ? shape : void 0;
|
|
86333
86353
|
} catch {
|
|
@@ -97646,6 +97666,12 @@ ${testOpen}`;
|
|
|
97646
97666
|
}
|
|
97647
97667
|
}
|
|
97648
97668
|
}
|
|
97669
|
+
const droppedByCalls = codeForMatcherScan.match(/\.get(?:Role|Text|Label|Placeholder|TestId|AltText|Title)\s*\(/g) ?? [];
|
|
97670
|
+
for (const call of droppedByCalls) {
|
|
97671
|
+
const methodName = call.replace(/[\s(]+$/, "").slice(1);
|
|
97672
|
+
logs2?.push(` \u2717 Rejected "${t.name}": "${methodName}" is not a real Playwright method \u2014 likely "${methodName.replace("get", "getBy")}" with "By" dropped`);
|
|
97673
|
+
return false;
|
|
97674
|
+
}
|
|
97649
97675
|
if (code.includes("// TODO")) {
|
|
97650
97676
|
logs2?.push(` \u26A0 "${t.name}" contains TODO comment \u2014 marking as unverified`);
|
|
97651
97677
|
t.tags = t.tags.filter((tag) => tag !== "@verified");
|
|
@@ -353039,7 +353065,7 @@ var require_package4 = __commonJS({
|
|
|
353039
353065
|
"package.json"(exports, module) {
|
|
353040
353066
|
module.exports = {
|
|
353041
353067
|
name: "@validate.qa/runner",
|
|
353042
|
-
version: "1.0.
|
|
353068
|
+
version: "1.0.78",
|
|
353043
353069
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
353044
353070
|
bin: {
|
|
353045
353071
|
"validate-runner": "dist/cli.js"
|
|
@@ -354705,17 +354731,13 @@ async function clickCssInCurrentContext(sessionPath, css) {
|
|
|
354705
354731
|
}
|
|
354706
354732
|
async function clickByVisibleTextInCurrentContext(sessionPath, text) {
|
|
354707
354733
|
const script = `
|
|
354734
|
+
if (!document.body) { return 0; }
|
|
354708
354735
|
var t = String(arguments[0]).trim().replace(/\\s+/g, ' ');
|
|
354709
|
-
|
|
354736
|
+
// String() coercion is load-bearing: e.value is a NUMBER for <progress>/
|
|
354737
|
+
// <meter>/<li value>, and (70).trim() would throw, aborting the whole
|
|
354738
|
+
// script (and thus the fallback) on any page containing one.
|
|
354739
|
+
var norm = function (e) { return String(e.textContent || e.value || '').trim().replace(/\\s+/g, ' '); };
|
|
354710
354740
|
var vis = function (e) { var r = e.getBoundingClientRect(); return r.width > 0 && r.height > 0; };
|
|
354711
|
-
var all = Array.prototype.slice.call(document.body.getElementsByTagName('*'));
|
|
354712
|
-
var match = all.filter(function (e) { return norm(e) === t && vis(e); });
|
|
354713
|
-
// Deepest-only: drop any match that contains another match (same text).
|
|
354714
|
-
var deepest = match.filter(function (e) {
|
|
354715
|
-
return !match.some(function (o) { return o !== e && e.contains(o); });
|
|
354716
|
-
});
|
|
354717
|
-
if (deepest.length !== 1) { return deepest.length; }
|
|
354718
|
-
var el = deepest[0];
|
|
354719
354741
|
var interactive = function (e) {
|
|
354720
354742
|
var tag = e.tagName;
|
|
354721
354743
|
if (tag === 'BUTTON' || tag === 'A' || tag === 'INPUT' || tag === 'SELECT' || tag === 'SUMMARY') { return true; }
|
|
@@ -354724,6 +354746,33 @@ async function clickByVisibleTextInCurrentContext(sessionPath, text) {
|
|
|
354724
354746
|
if (e.hasAttribute && (e.hasAttribute('onclick') || (e.hasAttribute('tabindex') && e.getAttribute('tabindex') !== '-1'))) { return true; }
|
|
354725
354747
|
try { return window.getComputedStyle(e).cursor === 'pointer'; } catch (err) { return false; }
|
|
354726
354748
|
};
|
|
354749
|
+
// Is this match itself interactive, or wrapped by an interactive ancestor
|
|
354750
|
+
// that contains ONLY this text?
|
|
354751
|
+
var hasInteractiveHost = function (e) {
|
|
354752
|
+
var hop = e;
|
|
354753
|
+
for (var i = 0; i < 5 && hop && hop !== document.body; i++) {
|
|
354754
|
+
if (norm(hop) !== t) { return false; }
|
|
354755
|
+
if (interactive(hop)) { return true; }
|
|
354756
|
+
hop = hop.parentElement;
|
|
354757
|
+
}
|
|
354758
|
+
return false;
|
|
354759
|
+
};
|
|
354760
|
+
var all = Array.prototype.slice.call(document.body.getElementsByTagName('*'));
|
|
354761
|
+
var match = all.filter(function (e) { return norm(e) === t && vis(e); });
|
|
354762
|
+
// Deepest-only: drop any match that contains another match (same text).
|
|
354763
|
+
var deepest = match.filter(function (e) {
|
|
354764
|
+
return !match.some(function (o) { return o !== e && e.contains(o); });
|
|
354765
|
+
});
|
|
354766
|
+
var candidates = deepest;
|
|
354767
|
+
if (candidates.length > 1) {
|
|
354768
|
+
// Disambiguate: a NON-interactive duplicate (page heading, badge, label
|
|
354769
|
+
// echoing a control's caption) must not veto the one real control. Only
|
|
354770
|
+
// when several INTERACTIVE hosts share the text is it truly ambiguous.
|
|
354771
|
+
var interactiveOnly = candidates.filter(hasInteractiveHost);
|
|
354772
|
+
if (interactiveOnly.length >= 1) { candidates = interactiveOnly; }
|
|
354773
|
+
}
|
|
354774
|
+
if (candidates.length !== 1) { return candidates.length; }
|
|
354775
|
+
var el = candidates[0];
|
|
354727
354776
|
// Prefer the outermost interactive ancestor that wraps ONLY this text.
|
|
354728
354777
|
var target = el;
|
|
354729
354778
|
var hop = el;
|
|
@@ -354897,6 +354946,7 @@ var QUARANTINE_MAX_MS = (() => {
|
|
|
354897
354946
|
return Number.isFinite(raw) && raw > 0 ? raw : 10 * 60 * 1e3;
|
|
354898
354947
|
})();
|
|
354899
354948
|
var quarantinedAt = /* @__PURE__ */ new Map();
|
|
354949
|
+
var nextQuarantineToken = 1;
|
|
354900
354950
|
var DeviceQuarantinedError = class extends Error {
|
|
354901
354951
|
constructor(deviceId) {
|
|
354902
354952
|
super(
|
|
@@ -354906,14 +354956,21 @@ var DeviceQuarantinedError = class extends Error {
|
|
|
354906
354956
|
}
|
|
354907
354957
|
};
|
|
354908
354958
|
function quarantineDevice(deviceId, now = Date.now()) {
|
|
354909
|
-
|
|
354959
|
+
const token = nextQuarantineToken++;
|
|
354960
|
+
quarantinedAt.set(deviceId, { at: now, token });
|
|
354961
|
+
return token;
|
|
354910
354962
|
}
|
|
354911
|
-
function unquarantineDevice(deviceId) {
|
|
354912
|
-
|
|
354963
|
+
function unquarantineDevice(deviceId, token) {
|
|
354964
|
+
if (token === void 0) {
|
|
354965
|
+
quarantinedAt.delete(deviceId);
|
|
354966
|
+
return;
|
|
354967
|
+
}
|
|
354968
|
+
const entry = quarantinedAt.get(deviceId);
|
|
354969
|
+
if (entry && entry.token === token) quarantinedAt.delete(deviceId);
|
|
354913
354970
|
}
|
|
354914
354971
|
function isDeviceQuarantined(deviceId, now = Date.now()) {
|
|
354915
354972
|
if (typeof deviceId !== "string") return false;
|
|
354916
|
-
const since = quarantinedAt.get(deviceId);
|
|
354973
|
+
const since = quarantinedAt.get(deviceId)?.at;
|
|
354917
354974
|
if (since === void 0) return false;
|
|
354918
354975
|
if (now - since >= QUARANTINE_MAX_MS) {
|
|
354919
354976
|
quarantinedAt.delete(deviceId);
|
|
@@ -357274,7 +357331,8 @@ async function executeTap(sessionId, step, platform3, isCrossPlatform = false) {
|
|
|
357274
357331
|
const bounds = boundsForRun(step, platform3, isCrossPlatform);
|
|
357275
357332
|
if (step.target) {
|
|
357276
357333
|
try {
|
|
357277
|
-
const
|
|
357334
|
+
const explicitTimeout = readStepTimeout(step);
|
|
357335
|
+
const elementId = await findElement(sessionId, step.target, isPermissionTap ? explicitTimeout ?? PERMISSION_DIALOG_TIMEOUT_MS : explicitTimeout, {
|
|
357278
357336
|
platform: platform3,
|
|
357279
357337
|
recordedBounds: bounds
|
|
357280
357338
|
});
|
|
@@ -358368,9 +358426,15 @@ let timer = null;
|
|
|
358368
358426
|
async function post() {
|
|
358369
358427
|
if (!url || !headers || !body) return; // inert until the first real heartbeat
|
|
358370
358428
|
try {
|
|
358429
|
+
// The keepalive discards the response body, so it must identify itself:
|
|
358430
|
+
// the server's heartbeat handler destructively drains queued runner
|
|
358431
|
+
// control commands (kill-pid/kill-orphans) into the response, and a
|
|
358432
|
+
// drained command this worker throws away would be lost for a full
|
|
358433
|
+
// re-issue window. The header tells the server to keep the queue intact
|
|
358434
|
+
// and let the main loop (which executes commands) drain it.
|
|
358371
358435
|
await fetch(url, {
|
|
358372
358436
|
method: 'POST',
|
|
358373
|
-
headers,
|
|
358437
|
+
headers: { ...headers, 'x-validateqa-keepalive': '1' },
|
|
358374
358438
|
body,
|
|
358375
358439
|
signal: AbortSignal.timeout(10000),
|
|
358376
358440
|
});
|
|
@@ -360708,6 +360772,7 @@ var MobileRunWatchdog = class {
|
|
|
360708
360772
|
phase = "idle";
|
|
360709
360773
|
released = false;
|
|
360710
360774
|
quarantined = false;
|
|
360775
|
+
quarantineToken;
|
|
360711
360776
|
cancelAbortTimer = null;
|
|
360712
360777
|
cancelGraceTimer = null;
|
|
360713
360778
|
/** Current phase (exposed for tests / observability). */
|
|
@@ -360736,7 +360801,8 @@ var MobileRunWatchdog = class {
|
|
|
360736
360801
|
this.clearAbortTimer();
|
|
360737
360802
|
if (this.options.deviceId) {
|
|
360738
360803
|
this.quarantined = true;
|
|
360739
|
-
this.options.callbacks.quarantine(this.options.deviceId);
|
|
360804
|
+
const token = this.options.callbacks.quarantine(this.options.deviceId);
|
|
360805
|
+
this.quarantineToken = typeof token === "number" ? token : void 0;
|
|
360740
360806
|
this.options.callbacks.abort(this.options.deviceId, this.options.runId, reason);
|
|
360741
360807
|
} else {
|
|
360742
360808
|
this.options.callbacks.log("warn", `Run ${this.options.runId} ${reason} \u2014 no leased device to abort; awaiting the run's own settle.`);
|
|
@@ -360761,7 +360827,7 @@ var MobileRunWatchdog = class {
|
|
|
360761
360827
|
}
|
|
360762
360828
|
if (this.quarantined && this.options.deviceId) {
|
|
360763
360829
|
this.quarantined = false;
|
|
360764
|
-
this.options.callbacks.unquarantine(this.options.deviceId);
|
|
360830
|
+
this.options.callbacks.unquarantine(this.options.deviceId, this.quarantineToken);
|
|
360765
360831
|
if (wasForceReleased) {
|
|
360766
360832
|
this.options.callbacks.log("info", `Run ${this.options.runId} finally settled \u2014 lifted the quarantine on device ${this.options.deviceId}.`);
|
|
360767
360833
|
}
|
|
@@ -360779,7 +360845,8 @@ var MobileRunWatchdog = class {
|
|
|
360779
360845
|
if (this.options.deviceId) {
|
|
360780
360846
|
if (!this.quarantined) {
|
|
360781
360847
|
this.quarantined = true;
|
|
360782
|
-
this.options.callbacks.quarantine(this.options.deviceId);
|
|
360848
|
+
const token = this.options.callbacks.quarantine(this.options.deviceId);
|
|
360849
|
+
this.quarantineToken = typeof token === "number" ? token : void 0;
|
|
360783
360850
|
}
|
|
360784
360851
|
this.options.callbacks.log(
|
|
360785
360852
|
"warn",
|
|
@@ -364425,11 +364492,9 @@ async function startRunner(opts) {
|
|
|
364425
364492
|
if (sessionId) void deleteAppiumSession(sessionId);
|
|
364426
364493
|
},
|
|
364427
364494
|
release: applyRelease,
|
|
364428
|
-
quarantine: (deviceId) =>
|
|
364429
|
-
|
|
364430
|
-
|
|
364431
|
-
unquarantine: (deviceId) => {
|
|
364432
|
-
unquarantineDevice(deviceId);
|
|
364495
|
+
quarantine: (deviceId) => quarantineDevice(deviceId),
|
|
364496
|
+
unquarantine: (deviceId, token) => {
|
|
364497
|
+
unquarantineDevice(deviceId, token);
|
|
364433
364498
|
},
|
|
364434
364499
|
log: (level, message) => {
|
|
364435
364500
|
if (level === "warn") log.warn(message);
|
package/package.json
CHANGED