@validate.qa/runner 1.0.75 → 1.0.77

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 +63 -24
  2. package/dist/cli.mjs +63 -24
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -353034,7 +353034,7 @@ var require_package4 = __commonJS({
353034
353034
  "package.json"(exports2, module2) {
353035
353035
  module2.exports = {
353036
353036
  name: "@validate.qa/runner",
353037
- version: "1.0.75",
353037
+ version: "1.0.77",
353038
353038
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
353039
353039
  bin: {
353040
353040
  "validate-runner": "dist/cli.js"
@@ -354700,17 +354700,13 @@ async function clickCssInCurrentContext(sessionPath, css) {
354700
354700
  }
354701
354701
  async function clickByVisibleTextInCurrentContext(sessionPath, text) {
354702
354702
  const script = `
354703
+ if (!document.body) { return 0; }
354703
354704
  var t = String(arguments[0]).trim().replace(/\\s+/g, ' ');
354704
- var norm = function (e) { return (e.textContent || e.value || '').trim().replace(/\\s+/g, ' '); };
354705
+ // String() coercion is load-bearing: e.value is a NUMBER for <progress>/
354706
+ // <meter>/<li value>, and (70).trim() would throw, aborting the whole
354707
+ // script (and thus the fallback) on any page containing one.
354708
+ var norm = function (e) { return String(e.textContent || e.value || '').trim().replace(/\\s+/g, ' '); };
354705
354709
  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
354710
  var interactive = function (e) {
354715
354711
  var tag = e.tagName;
354716
354712
  if (tag === 'BUTTON' || tag === 'A' || tag === 'INPUT' || tag === 'SELECT' || tag === 'SUMMARY') { return true; }
@@ -354719,6 +354715,33 @@ async function clickByVisibleTextInCurrentContext(sessionPath, text) {
354719
354715
  if (e.hasAttribute && (e.hasAttribute('onclick') || (e.hasAttribute('tabindex') && e.getAttribute('tabindex') !== '-1'))) { return true; }
354720
354716
  try { return window.getComputedStyle(e).cursor === 'pointer'; } catch (err) { return false; }
354721
354717
  };
354718
+ // Is this match itself interactive, or wrapped by an interactive ancestor
354719
+ // that contains ONLY this text?
354720
+ var hasInteractiveHost = function (e) {
354721
+ var hop = e;
354722
+ for (var i = 0; i < 5 && hop && hop !== document.body; i++) {
354723
+ if (norm(hop) !== t) { return false; }
354724
+ if (interactive(hop)) { return true; }
354725
+ hop = hop.parentElement;
354726
+ }
354727
+ return false;
354728
+ };
354729
+ var all = Array.prototype.slice.call(document.body.getElementsByTagName('*'));
354730
+ var match = all.filter(function (e) { return norm(e) === t && vis(e); });
354731
+ // Deepest-only: drop any match that contains another match (same text).
354732
+ var deepest = match.filter(function (e) {
354733
+ return !match.some(function (o) { return o !== e && e.contains(o); });
354734
+ });
354735
+ var candidates = deepest;
354736
+ if (candidates.length > 1) {
354737
+ // Disambiguate: a NON-interactive duplicate (page heading, badge, label
354738
+ // echoing a control's caption) must not veto the one real control. Only
354739
+ // when several INTERACTIVE hosts share the text is it truly ambiguous.
354740
+ var interactiveOnly = candidates.filter(hasInteractiveHost);
354741
+ if (interactiveOnly.length >= 1) { candidates = interactiveOnly; }
354742
+ }
354743
+ if (candidates.length !== 1) { return candidates.length; }
354744
+ var el = candidates[0];
354722
354745
  // Prefer the outermost interactive ancestor that wraps ONLY this text.
354723
354746
  var target = el;
354724
354747
  var hop = el;
@@ -354892,6 +354915,7 @@ var QUARANTINE_MAX_MS = (() => {
354892
354915
  return Number.isFinite(raw) && raw > 0 ? raw : 10 * 60 * 1e3;
354893
354916
  })();
354894
354917
  var quarantinedAt = /* @__PURE__ */ new Map();
354918
+ var nextQuarantineToken = 1;
354895
354919
  var DeviceQuarantinedError = class extends Error {
354896
354920
  constructor(deviceId) {
354897
354921
  super(
@@ -354901,14 +354925,21 @@ var DeviceQuarantinedError = class extends Error {
354901
354925
  }
354902
354926
  };
354903
354927
  function quarantineDevice(deviceId, now = Date.now()) {
354904
- quarantinedAt.set(deviceId, now);
354928
+ const token = nextQuarantineToken++;
354929
+ quarantinedAt.set(deviceId, { at: now, token });
354930
+ return token;
354905
354931
  }
354906
- function unquarantineDevice(deviceId) {
354907
- quarantinedAt.delete(deviceId);
354932
+ function unquarantineDevice(deviceId, token) {
354933
+ if (token === void 0) {
354934
+ quarantinedAt.delete(deviceId);
354935
+ return;
354936
+ }
354937
+ const entry = quarantinedAt.get(deviceId);
354938
+ if (entry && entry.token === token) quarantinedAt.delete(deviceId);
354908
354939
  }
354909
354940
  function isDeviceQuarantined(deviceId, now = Date.now()) {
354910
354941
  if (typeof deviceId !== "string") return false;
354911
- const since = quarantinedAt.get(deviceId);
354942
+ const since = quarantinedAt.get(deviceId)?.at;
354912
354943
  if (since === void 0) return false;
354913
354944
  if (now - since >= QUARANTINE_MAX_MS) {
354914
354945
  quarantinedAt.delete(deviceId);
@@ -357266,7 +357297,8 @@ async function executeTap(sessionId, step, platform3, isCrossPlatform = false) {
357266
357297
  const bounds = boundsForRun(step, platform3, isCrossPlatform);
357267
357298
  if (step.target) {
357268
357299
  try {
357269
- const elementId = await findElement(sessionId, step.target, isPermissionTap ? Math.min(readStepTimeout(step) ?? DEFAULT_STEP_TIMEOUT_MS, PERMISSION_DIALOG_TIMEOUT_MS) : readStepTimeout(step), {
357300
+ const explicitTimeout = readStepTimeout(step);
357301
+ const elementId = await findElement(sessionId, step.target, isPermissionTap ? explicitTimeout ?? PERMISSION_DIALOG_TIMEOUT_MS : explicitTimeout, {
357270
357302
  platform: platform3,
357271
357303
  recordedBounds: bounds
357272
357304
  });
@@ -358360,9 +358392,15 @@ let timer = null;
358360
358392
  async function post() {
358361
358393
  if (!url || !headers || !body) return; // inert until the first real heartbeat
358362
358394
  try {
358395
+ // The keepalive discards the response body, so it must identify itself:
358396
+ // the server's heartbeat handler destructively drains queued runner
358397
+ // control commands (kill-pid/kill-orphans) into the response, and a
358398
+ // drained command this worker throws away would be lost for a full
358399
+ // re-issue window. The header tells the server to keep the queue intact
358400
+ // and let the main loop (which executes commands) drain it.
358363
358401
  await fetch(url, {
358364
358402
  method: 'POST',
358365
- headers,
358403
+ headers: { ...headers, 'x-validateqa-keepalive': '1' },
358366
358404
  body,
358367
358405
  signal: AbortSignal.timeout(10000),
358368
358406
  });
@@ -360700,6 +360738,7 @@ var MobileRunWatchdog = class {
360700
360738
  phase = "idle";
360701
360739
  released = false;
360702
360740
  quarantined = false;
360741
+ quarantineToken;
360703
360742
  cancelAbortTimer = null;
360704
360743
  cancelGraceTimer = null;
360705
360744
  /** Current phase (exposed for tests / observability). */
@@ -360728,7 +360767,8 @@ var MobileRunWatchdog = class {
360728
360767
  this.clearAbortTimer();
360729
360768
  if (this.options.deviceId) {
360730
360769
  this.quarantined = true;
360731
- this.options.callbacks.quarantine(this.options.deviceId);
360770
+ const token = this.options.callbacks.quarantine(this.options.deviceId);
360771
+ this.quarantineToken = typeof token === "number" ? token : void 0;
360732
360772
  this.options.callbacks.abort(this.options.deviceId, this.options.runId, reason);
360733
360773
  } else {
360734
360774
  this.options.callbacks.log("warn", `Run ${this.options.runId} ${reason} \u2014 no leased device to abort; awaiting the run's own settle.`);
@@ -360753,7 +360793,7 @@ var MobileRunWatchdog = class {
360753
360793
  }
360754
360794
  if (this.quarantined && this.options.deviceId) {
360755
360795
  this.quarantined = false;
360756
- this.options.callbacks.unquarantine(this.options.deviceId);
360796
+ this.options.callbacks.unquarantine(this.options.deviceId, this.quarantineToken);
360757
360797
  if (wasForceReleased) {
360758
360798
  this.options.callbacks.log("info", `Run ${this.options.runId} finally settled \u2014 lifted the quarantine on device ${this.options.deviceId}.`);
360759
360799
  }
@@ -360771,7 +360811,8 @@ var MobileRunWatchdog = class {
360771
360811
  if (this.options.deviceId) {
360772
360812
  if (!this.quarantined) {
360773
360813
  this.quarantined = true;
360774
- this.options.callbacks.quarantine(this.options.deviceId);
360814
+ const token = this.options.callbacks.quarantine(this.options.deviceId);
360815
+ this.quarantineToken = typeof token === "number" ? token : void 0;
360775
360816
  }
360776
360817
  this.options.callbacks.log(
360777
360818
  "warn",
@@ -364417,11 +364458,9 @@ async function startRunner(opts) {
364417
364458
  if (sessionId) void deleteAppiumSession(sessionId);
364418
364459
  },
364419
364460
  release: applyRelease,
364420
- quarantine: (deviceId) => {
364421
- quarantineDevice(deviceId);
364422
- },
364423
- unquarantine: (deviceId) => {
364424
- unquarantineDevice(deviceId);
364461
+ quarantine: (deviceId) => quarantineDevice(deviceId),
364462
+ unquarantine: (deviceId, token) => {
364463
+ unquarantineDevice(deviceId, token);
364425
364464
  },
364426
364465
  log: (level, message) => {
364427
364466
  if (level === "warn") log.warn(message);
package/dist/cli.mjs CHANGED
@@ -353039,7 +353039,7 @@ var require_package4 = __commonJS({
353039
353039
  "package.json"(exports, module) {
353040
353040
  module.exports = {
353041
353041
  name: "@validate.qa/runner",
353042
- version: "1.0.75",
353042
+ version: "1.0.77",
353043
353043
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
353044
353044
  bin: {
353045
353045
  "validate-runner": "dist/cli.js"
@@ -354705,17 +354705,13 @@ async function clickCssInCurrentContext(sessionPath, css) {
354705
354705
  }
354706
354706
  async function clickByVisibleTextInCurrentContext(sessionPath, text) {
354707
354707
  const script = `
354708
+ if (!document.body) { return 0; }
354708
354709
  var t = String(arguments[0]).trim().replace(/\\s+/g, ' ');
354709
- var norm = function (e) { return (e.textContent || e.value || '').trim().replace(/\\s+/g, ' '); };
354710
+ // String() coercion is load-bearing: e.value is a NUMBER for <progress>/
354711
+ // <meter>/<li value>, and (70).trim() would throw, aborting the whole
354712
+ // script (and thus the fallback) on any page containing one.
354713
+ var norm = function (e) { return String(e.textContent || e.value || '').trim().replace(/\\s+/g, ' '); };
354710
354714
  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
354715
  var interactive = function (e) {
354720
354716
  var tag = e.tagName;
354721
354717
  if (tag === 'BUTTON' || tag === 'A' || tag === 'INPUT' || tag === 'SELECT' || tag === 'SUMMARY') { return true; }
@@ -354724,6 +354720,33 @@ async function clickByVisibleTextInCurrentContext(sessionPath, text) {
354724
354720
  if (e.hasAttribute && (e.hasAttribute('onclick') || (e.hasAttribute('tabindex') && e.getAttribute('tabindex') !== '-1'))) { return true; }
354725
354721
  try { return window.getComputedStyle(e).cursor === 'pointer'; } catch (err) { return false; }
354726
354722
  };
354723
+ // Is this match itself interactive, or wrapped by an interactive ancestor
354724
+ // that contains ONLY this text?
354725
+ var hasInteractiveHost = function (e) {
354726
+ var hop = e;
354727
+ for (var i = 0; i < 5 && hop && hop !== document.body; i++) {
354728
+ if (norm(hop) !== t) { return false; }
354729
+ if (interactive(hop)) { return true; }
354730
+ hop = hop.parentElement;
354731
+ }
354732
+ return false;
354733
+ };
354734
+ var all = Array.prototype.slice.call(document.body.getElementsByTagName('*'));
354735
+ var match = all.filter(function (e) { return norm(e) === t && vis(e); });
354736
+ // Deepest-only: drop any match that contains another match (same text).
354737
+ var deepest = match.filter(function (e) {
354738
+ return !match.some(function (o) { return o !== e && e.contains(o); });
354739
+ });
354740
+ var candidates = deepest;
354741
+ if (candidates.length > 1) {
354742
+ // Disambiguate: a NON-interactive duplicate (page heading, badge, label
354743
+ // echoing a control's caption) must not veto the one real control. Only
354744
+ // when several INTERACTIVE hosts share the text is it truly ambiguous.
354745
+ var interactiveOnly = candidates.filter(hasInteractiveHost);
354746
+ if (interactiveOnly.length >= 1) { candidates = interactiveOnly; }
354747
+ }
354748
+ if (candidates.length !== 1) { return candidates.length; }
354749
+ var el = candidates[0];
354727
354750
  // Prefer the outermost interactive ancestor that wraps ONLY this text.
354728
354751
  var target = el;
354729
354752
  var hop = el;
@@ -354897,6 +354920,7 @@ var QUARANTINE_MAX_MS = (() => {
354897
354920
  return Number.isFinite(raw) && raw > 0 ? raw : 10 * 60 * 1e3;
354898
354921
  })();
354899
354922
  var quarantinedAt = /* @__PURE__ */ new Map();
354923
+ var nextQuarantineToken = 1;
354900
354924
  var DeviceQuarantinedError = class extends Error {
354901
354925
  constructor(deviceId) {
354902
354926
  super(
@@ -354906,14 +354930,21 @@ var DeviceQuarantinedError = class extends Error {
354906
354930
  }
354907
354931
  };
354908
354932
  function quarantineDevice(deviceId, now = Date.now()) {
354909
- quarantinedAt.set(deviceId, now);
354933
+ const token = nextQuarantineToken++;
354934
+ quarantinedAt.set(deviceId, { at: now, token });
354935
+ return token;
354910
354936
  }
354911
- function unquarantineDevice(deviceId) {
354912
- quarantinedAt.delete(deviceId);
354937
+ function unquarantineDevice(deviceId, token) {
354938
+ if (token === void 0) {
354939
+ quarantinedAt.delete(deviceId);
354940
+ return;
354941
+ }
354942
+ const entry = quarantinedAt.get(deviceId);
354943
+ if (entry && entry.token === token) quarantinedAt.delete(deviceId);
354913
354944
  }
354914
354945
  function isDeviceQuarantined(deviceId, now = Date.now()) {
354915
354946
  if (typeof deviceId !== "string") return false;
354916
- const since = quarantinedAt.get(deviceId);
354947
+ const since = quarantinedAt.get(deviceId)?.at;
354917
354948
  if (since === void 0) return false;
354918
354949
  if (now - since >= QUARANTINE_MAX_MS) {
354919
354950
  quarantinedAt.delete(deviceId);
@@ -357274,7 +357305,8 @@ async function executeTap(sessionId, step, platform3, isCrossPlatform = false) {
357274
357305
  const bounds = boundsForRun(step, platform3, isCrossPlatform);
357275
357306
  if (step.target) {
357276
357307
  try {
357277
- const elementId = await findElement(sessionId, step.target, isPermissionTap ? Math.min(readStepTimeout(step) ?? DEFAULT_STEP_TIMEOUT_MS, PERMISSION_DIALOG_TIMEOUT_MS) : readStepTimeout(step), {
357308
+ const explicitTimeout = readStepTimeout(step);
357309
+ const elementId = await findElement(sessionId, step.target, isPermissionTap ? explicitTimeout ?? PERMISSION_DIALOG_TIMEOUT_MS : explicitTimeout, {
357278
357310
  platform: platform3,
357279
357311
  recordedBounds: bounds
357280
357312
  });
@@ -358368,9 +358400,15 @@ let timer = null;
358368
358400
  async function post() {
358369
358401
  if (!url || !headers || !body) return; // inert until the first real heartbeat
358370
358402
  try {
358403
+ // The keepalive discards the response body, so it must identify itself:
358404
+ // the server's heartbeat handler destructively drains queued runner
358405
+ // control commands (kill-pid/kill-orphans) into the response, and a
358406
+ // drained command this worker throws away would be lost for a full
358407
+ // re-issue window. The header tells the server to keep the queue intact
358408
+ // and let the main loop (which executes commands) drain it.
358371
358409
  await fetch(url, {
358372
358410
  method: 'POST',
358373
- headers,
358411
+ headers: { ...headers, 'x-validateqa-keepalive': '1' },
358374
358412
  body,
358375
358413
  signal: AbortSignal.timeout(10000),
358376
358414
  });
@@ -360708,6 +360746,7 @@ var MobileRunWatchdog = class {
360708
360746
  phase = "idle";
360709
360747
  released = false;
360710
360748
  quarantined = false;
360749
+ quarantineToken;
360711
360750
  cancelAbortTimer = null;
360712
360751
  cancelGraceTimer = null;
360713
360752
  /** Current phase (exposed for tests / observability). */
@@ -360736,7 +360775,8 @@ var MobileRunWatchdog = class {
360736
360775
  this.clearAbortTimer();
360737
360776
  if (this.options.deviceId) {
360738
360777
  this.quarantined = true;
360739
- this.options.callbacks.quarantine(this.options.deviceId);
360778
+ const token = this.options.callbacks.quarantine(this.options.deviceId);
360779
+ this.quarantineToken = typeof token === "number" ? token : void 0;
360740
360780
  this.options.callbacks.abort(this.options.deviceId, this.options.runId, reason);
360741
360781
  } else {
360742
360782
  this.options.callbacks.log("warn", `Run ${this.options.runId} ${reason} \u2014 no leased device to abort; awaiting the run's own settle.`);
@@ -360761,7 +360801,7 @@ var MobileRunWatchdog = class {
360761
360801
  }
360762
360802
  if (this.quarantined && this.options.deviceId) {
360763
360803
  this.quarantined = false;
360764
- this.options.callbacks.unquarantine(this.options.deviceId);
360804
+ this.options.callbacks.unquarantine(this.options.deviceId, this.quarantineToken);
360765
360805
  if (wasForceReleased) {
360766
360806
  this.options.callbacks.log("info", `Run ${this.options.runId} finally settled \u2014 lifted the quarantine on device ${this.options.deviceId}.`);
360767
360807
  }
@@ -360779,7 +360819,8 @@ var MobileRunWatchdog = class {
360779
360819
  if (this.options.deviceId) {
360780
360820
  if (!this.quarantined) {
360781
360821
  this.quarantined = true;
360782
- this.options.callbacks.quarantine(this.options.deviceId);
360822
+ const token = this.options.callbacks.quarantine(this.options.deviceId);
360823
+ this.quarantineToken = typeof token === "number" ? token : void 0;
360783
360824
  }
360784
360825
  this.options.callbacks.log(
360785
360826
  "warn",
@@ -364425,11 +364466,9 @@ async function startRunner(opts) {
364425
364466
  if (sessionId) void deleteAppiumSession(sessionId);
364426
364467
  },
364427
364468
  release: applyRelease,
364428
- quarantine: (deviceId) => {
364429
- quarantineDevice(deviceId);
364430
- },
364431
- unquarantine: (deviceId) => {
364432
- unquarantineDevice(deviceId);
364469
+ quarantine: (deviceId) => quarantineDevice(deviceId),
364470
+ unquarantine: (deviceId, token) => {
364471
+ unquarantineDevice(deviceId, token);
364433
364472
  },
364434
364473
  log: (level, message) => {
364435
364474
  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.75",
3
+ "version": "1.0.77",
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"