@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.
Files changed (3) hide show
  1. package/dist/cli.js +92 -27
  2. package/dist/cli.mjs +92 -27
  3. 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
- return k;
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.76",
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
- var norm = function (e) { return (e.textContent || e.value || '').trim().replace(/\\s+/g, ' '); };
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
- quarantinedAt.set(deviceId, now);
354954
+ const token = nextQuarantineToken++;
354955
+ quarantinedAt.set(deviceId, { at: now, token });
354956
+ return token;
354905
354957
  }
354906
- function unquarantineDevice(deviceId) {
354907
- quarantinedAt.delete(deviceId);
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 elementId = await findElement(sessionId, step.target, isPermissionTap ? Math.min(readStepTimeout(step) ?? DEFAULT_STEP_TIMEOUT_MS, PERMISSION_DIALOG_TIMEOUT_MS) : readStepTimeout(step), {
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
- quarantineDevice(deviceId);
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
- return k;
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.76",
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
- var norm = function (e) { return (e.textContent || e.value || '').trim().replace(/\\s+/g, ' '); };
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
- quarantinedAt.set(deviceId, now);
354959
+ const token = nextQuarantineToken++;
354960
+ quarantinedAt.set(deviceId, { at: now, token });
354961
+ return token;
354910
354962
  }
354911
- function unquarantineDevice(deviceId) {
354912
- quarantinedAt.delete(deviceId);
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 elementId = await findElement(sessionId, step.target, isPermissionTap ? Math.min(readStepTimeout(step) ?? DEFAULT_STEP_TIMEOUT_MS, PERMISSION_DIALOG_TIMEOUT_MS) : readStepTimeout(step), {
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
- quarantineDevice(deviceId);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@validate.qa/runner",
3
- "version": "1.0.76",
3
+ "version": "1.0.78",
4
4
  "description": "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
5
5
  "bin": {
6
6
  "validate-runner": "dist/cli.js"