@testspectra/react 1.2.0 → 1.2.2

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/index.js +119 -35
  2. package/dist/setup.js +33 -33
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -196,6 +196,13 @@ function mockResponseBodyText(body) {
196
196
  }
197
197
 
198
198
  // ../../core/worker-runtime/src/dsl/selector_resolver.ts
199
+ function parseElementSentinel(selector) {
200
+ const closest = /^(.*)::closest\((.+)\)$/.exec(selector);
201
+ if (closest) return { kind: "closest", base: closest[1], arg: closest[2] };
202
+ const parent = /^(.*)::parent$/.exec(selector);
203
+ if (parent) return { kind: "parent", base: parent[1] };
204
+ return null;
205
+ }
199
206
  function parseA11ySelector(selector) {
200
207
  const trimmed = selector.trim();
201
208
  if (!trimmed.startsWith("~")) return null;
@@ -391,6 +398,35 @@ function resolveEl(selector, index) {
391
398
  if (parsed) return resolveLabelledBy(parsed.name, document, index);
392
399
  return null;
393
400
  }
401
+ function selectorText(target) {
402
+ return typeof target === "string" ? target : target.selector;
403
+ }
404
+ function resolveSentinel(raw, parent) {
405
+ const sentinel = parseElementSentinel(raw);
406
+ if (!sentinel) return void 0;
407
+ const baseTarget = parent ? { selector: sentinel.base, index: null, parent } : sentinel.base;
408
+ const base = resolveScopedEl(baseTarget, null);
409
+ if (!base) return null;
410
+ if (sentinel.kind === "parent") return base.parentElement ?? null;
411
+ return base.closest(sentinel.arg) ?? null;
412
+ }
413
+ function resolveScopedEl(target, fallbackIndex) {
414
+ if (typeof target === "string") {
415
+ const sentinel = resolveSentinel(target, void 0);
416
+ if (sentinel !== void 0) return sentinel;
417
+ return resolveEl(target, fallbackIndex);
418
+ }
419
+ const childSentinel = resolveSentinel(target.selector, target.parent);
420
+ if (childSentinel !== void 0) return childSentinel;
421
+ const parentEl = target.parent ? resolveScopedEl(target.parent, target.parent.index) : document.documentElement;
422
+ if (!parentEl) return null;
423
+ const css = resolveSelector(target.selector);
424
+ const all = Array.from(parentEl.querySelectorAll(css));
425
+ const idx = target.index ?? fallbackIndex;
426
+ if (idx === null || idx === void 0) return all[0] ?? null;
427
+ if (idx >= 0) return all[idx] ?? null;
428
+ return all[all.length + idx] ?? null;
429
+ }
394
430
  function isFormEl(el) {
395
431
  const tag = el.tagName;
396
432
  return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
@@ -399,17 +435,18 @@ var DomDriverBridge = class {
399
435
  mockRules = [];
400
436
  consoleErrors = [];
401
437
  recordedNetwork = [];
402
- /** Polls until `selector` resolves to an element, then returns it — shared by every
403
- * interaction method below (user-event needs a real element handle, not a selector string). */
404
- async waitForEl(selector, index) {
405
- const immediate = resolveEl(selector, index);
438
+ /** Polls until `target` resolves to an element, then returns it — shared by every interaction
439
+ * method below (user-event needs a real element handle, not a selector string). Accepts a
440
+ * `ScopedSelector` so chained `.get()`/`.getAll()` lookups resolve within their parent. */
441
+ async waitForScopedEl(target, fallbackIndex) {
442
+ const immediate = resolveScopedEl(target, fallbackIndex);
406
443
  if (immediate) return immediate;
407
444
  let found = null;
408
445
  const ok = await globalThis.__TS_RUNTIME__.pollCondition(async () => {
409
- found = resolveEl(selector, index);
446
+ found = resolveScopedEl(target, fallbackIndex);
410
447
  return found !== null;
411
448
  }, globalThis.__TS_RUNTIME__.getImplicitWaitMs());
412
- if (!ok || !found) throw new Error("Element not found: " + JSON.stringify(selector));
449
+ if (!ok || !found) throw new Error("Element not found: " + selectorText(target));
413
450
  return found;
414
451
  }
415
452
  async connect() {
@@ -449,20 +486,20 @@ var DomDriverBridge = class {
449
486
  }
450
487
  // ─── Element Actions ─────────────────────────────────────────────────────
451
488
  async click(selector, index = null) {
452
- const el = await this.waitForEl(selector, index);
489
+ const el = await this.waitForScopedEl(selector, index);
453
490
  await getUser().click(el);
454
491
  }
455
492
  async doubleClick(selector, index = null) {
456
- const el = await this.waitForEl(selector, index);
493
+ const el = await this.waitForScopedEl(selector, index);
457
494
  await getUser().dblClick(el);
458
495
  }
459
496
  async rightClick(selector, index = null) {
460
- const el = await this.waitForEl(selector, index);
497
+ const el = await this.waitForScopedEl(selector, index);
461
498
  await getUser().pointer({ keys: "[MouseRight]", target: el });
462
499
  }
463
500
  async setValue(selector, value, index = null) {
464
- const el = await this.waitForEl(selector, index);
465
- if (!isFormEl(el)) throw new Error("Element is not a form field: " + JSON.stringify(selector));
501
+ const el = await this.waitForScopedEl(selector, index);
502
+ if (!isFormEl(el)) throw new Error("Element is not a form field: " + selectorText(selector));
466
503
  if (el.tagName === "INPUT" && el.type === "file") {
467
504
  return this.upload(selector, value, index);
468
505
  }
@@ -473,7 +510,7 @@ var DomDriverBridge = class {
473
510
  }
474
511
  }
475
512
  async upload(selector, files, index = null) {
476
- const el = await this.waitForEl(selector, index);
513
+ const el = await this.waitForScopedEl(selector, index);
477
514
  const fileList = Array.isArray(files) ? files : [files];
478
515
  const filesToUpload = [];
479
516
  const MIME_MAP = {
@@ -556,12 +593,12 @@ var DomDriverBridge = class {
556
593
  }
557
594
  }
558
595
  async clearValue(selector, index = null) {
559
- const el = await this.waitForEl(selector, index);
596
+ const el = await this.waitForScopedEl(selector, index);
560
597
  await getUser().clear(el);
561
598
  }
562
599
  async select(selector, option, index = null) {
563
600
  const ok = await globalThis.__TS_RUNTIME__.pollCondition(async () => {
564
- const el = resolveEl(selector, index);
601
+ const el = resolveScopedEl(selector, index);
565
602
  if (!el || el.tagName !== "SELECT") return false;
566
603
  for (let i = 0; i < el.options.length; i++) {
567
604
  const opt = el.options[i];
@@ -576,29 +613,71 @@ var DomDriverBridge = class {
576
613
  if (!ok) throw new Error("Select option not found: " + JSON.stringify(option));
577
614
  }
578
615
  async hover(selector, index = null) {
579
- const el = await this.waitForEl(selector, index);
616
+ const el = await this.waitForScopedEl(selector, index);
580
617
  await getUser().hover(el);
581
618
  }
582
619
  async focus(selector, index = null) {
583
620
  const ok = await globalThis.__TS_RUNTIME__.pollCondition(async () => {
584
- const el = resolveEl(selector, index);
621
+ const el = resolveScopedEl(selector, index);
585
622
  if (!el) return false;
586
623
  el.focus?.();
587
624
  return true;
588
625
  });
589
- if (!ok) throw new Error("Element not found: " + JSON.stringify(selector));
626
+ if (!ok) throw new Error("Element not found: " + selectorText(selector));
590
627
  }
591
628
  async blur(selector, index = null) {
592
629
  const ok = await globalThis.__TS_RUNTIME__.pollCondition(async () => {
593
- const el = resolveEl(selector, index);
630
+ const el = resolveScopedEl(selector, index);
594
631
  if (!el) return false;
595
632
  el.blur?.();
596
633
  return true;
597
634
  });
598
- if (!ok) throw new Error("Element not found: " + JSON.stringify(selector));
635
+ if (!ok) throw new Error("Element not found: " + selectorText(selector));
636
+ }
637
+ async waitForElementRemoved(selector, timeoutMs = 5e3, index = null) {
638
+ const ok = await globalThis.__TS_RUNTIME__.pollCondition(async () => {
639
+ return !await this.isExisting(selector, index);
640
+ }, timeoutMs);
641
+ if (!ok) throw new Error("Element still exists after timeout: " + selectorText(selector));
642
+ }
643
+ async waitForUndisplayed(selector, opts, index = null) {
644
+ const ok = await globalThis.__TS_RUNTIME__.pollCondition(async () => {
645
+ return !await this.isDisplayed(selector, index);
646
+ }, opts?.timeout ?? 5e3);
647
+ if (!ok) throw new Error("Element still displayed after timeout: " + selectorText(selector));
648
+ }
649
+ async waitForClickable(selector, opts, index = null) {
650
+ return await globalThis.__TS_RUNTIME__.pollCondition(async () => {
651
+ return this.isClickable(selector, index);
652
+ }, opts?.timeout ?? 5e3);
653
+ }
654
+ async getBounds(selector, index = null) {
655
+ const el = resolveScopedEl(selector, index);
656
+ if (!el) return { x: 0, y: 0, top: 0, right: 0, bottom: 0, left: 0, width: 0, height: 0 };
657
+ const r = el.getBoundingClientRect();
658
+ return {
659
+ x: r.x,
660
+ y: r.y,
661
+ top: r.top,
662
+ right: r.right,
663
+ bottom: r.bottom,
664
+ left: r.left,
665
+ width: r.width,
666
+ height: r.height
667
+ };
668
+ }
669
+ async closest(selector, targetSelector, index = null) {
670
+ const el = resolveScopedEl(selector, index);
671
+ if (!el) return null;
672
+ const ancestor = el.closest(targetSelector);
673
+ return ancestor ? ancestor.tagName.toLowerCase() : null;
674
+ }
675
+ async clickAt(x, y) {
676
+ const el = document.elementFromPoint(x, y);
677
+ el?.click();
599
678
  }
600
679
  async dragDrop(sourceSelector, targetSelector, sourceIndex = null) {
601
- const src = resolveEl(sourceSelector, sourceIndex);
680
+ const src = resolveScopedEl(sourceSelector, sourceIndex);
602
681
  const dest = resolveEl(targetSelector, null);
603
682
  if (!src || !dest) throw new Error("Drag source or destination not found");
604
683
  const dataTransfer = new DataTransfer();
@@ -608,30 +687,30 @@ var DomDriverBridge = class {
608
687
  src.dispatchEvent(new DragEvent("dragend", { bubbles: true, dataTransfer }));
609
688
  }
610
689
  async longPress(selector, duration, index = null) {
611
- const el = resolveEl(selector, index);
612
- if (!el) throw new Error("Element not found: " + JSON.stringify(selector));
690
+ const el = resolveScopedEl(selector, index);
691
+ if (!el) throw new Error("Element not found: " + selectorText(selector));
613
692
  el.dispatchEvent(new TouchEvent("touchstart", { bubbles: true }));
614
693
  await this.pause(duration);
615
694
  el.dispatchEvent(new TouchEvent("touchend", { bubbles: true }));
616
695
  }
617
696
  async scrollIntoView(selector, index = null) {
618
- const el = resolveEl(selector, index);
697
+ const el = resolveScopedEl(selector, index);
619
698
  el?.scrollIntoView({ block: "center", inline: "center" });
620
699
  }
621
700
  // ─── Element State Inspection ────────────────────────────────────────────
622
701
  async getText(selector, index = null) {
623
- const el = resolveEl(selector, index);
702
+ const el = resolveScopedEl(selector, index);
624
703
  if (!el) return "";
625
704
  return el.innerText !== void 0 ? el.innerText : el.textContent || "";
626
705
  }
627
706
  async getValue(selector, index = null) {
628
- const el = resolveEl(selector, index);
707
+ const el = resolveScopedEl(selector, index);
629
708
  if (!el) return "";
630
709
  if (isFormEl(el)) return el.value ?? "";
631
710
  return el.getAttribute("value") || "";
632
711
  }
633
712
  async isDisplayed(selector, index = null) {
634
- const el = resolveEl(selector, index);
713
+ const el = resolveScopedEl(selector, index);
635
714
  if (!el) return false;
636
715
  const style = window.getComputedStyle(el);
637
716
  if (style.visibility === "hidden" || style.display === "none" || style.opacity === "0") return false;
@@ -639,25 +718,25 @@ var DomDriverBridge = class {
639
718
  return rect.width > 0 && rect.height > 0 || el.getClientRects().length > 0 || el.offsetWidth > 0 && el.offsetHeight > 0;
640
719
  }
641
720
  async isExisting(selector, index = null) {
642
- return resolveEl(selector, index) !== null;
721
+ return resolveScopedEl(selector, index) !== null;
643
722
  }
644
723
  async isEnabled(selector, index = null) {
645
- const el = resolveEl(selector, index);
724
+ const el = resolveScopedEl(selector, index);
646
725
  if (!el) return false;
647
726
  return !el.disabled && !el.hasAttribute("disabled") && !el.classList.contains("disabled") && el.getAttribute("aria-disabled") !== "true";
648
727
  }
649
728
  async isSelected(selector, index = null) {
650
- const el = resolveEl(selector, index);
729
+ const el = resolveScopedEl(selector, index);
651
730
  if (!el) return false;
652
731
  return !!el.checked || el.getAttribute("aria-checked") === "true" || el.getAttribute("data-state") === "checked";
653
732
  }
654
733
  async isFocused(selector, index = null) {
655
- const el = resolveEl(selector, index);
734
+ const el = resolveScopedEl(selector, index);
656
735
  if (!el) return false;
657
736
  return document.activeElement === el || el.matches(":focus");
658
737
  }
659
738
  async isClickable(selector, index = null) {
660
- const el = resolveEl(selector, index);
739
+ const el = resolveScopedEl(selector, index);
661
740
  if (!el) return false;
662
741
  const style = window.getComputedStyle(el);
663
742
  if (style.visibility === "hidden" || style.display === "none" || style.opacity === "0") return false;
@@ -666,19 +745,24 @@ var DomDriverBridge = class {
666
745
  return !el.disabled && !el.hasAttribute("disabled") && !el.classList.contains("disabled") && el.getAttribute("aria-disabled") !== "true";
667
746
  }
668
747
  async hasClass(selector, className, index = null) {
669
- const el = resolveEl(selector, index);
748
+ const el = resolveScopedEl(selector, index);
670
749
  return el ? el.classList.contains(className) : false;
671
750
  }
672
751
  async getAttribute(selector, name, index = null) {
673
- const el = resolveEl(selector, index);
752
+ const el = resolveScopedEl(selector, index);
674
753
  return el ? el.getAttribute(name) : null;
675
754
  }
676
755
  async getCSSProperty(selector, name, index = null) {
677
- const el = resolveEl(selector, index);
756
+ const el = resolveScopedEl(selector, index);
678
757
  return { value: el ? window.getComputedStyle(el).getPropertyValue(name) : "" };
679
758
  }
680
759
  async count(selector) {
681
- return document.querySelectorAll(resolveSelector(selector)).length;
760
+ if (typeof selector === "string") {
761
+ return document.querySelectorAll(resolveSelector(selector)).length;
762
+ }
763
+ const parentEl = selector.parent ? resolveScopedEl(selector.parent, selector.parent.index) : document.documentElement;
764
+ if (!parentEl) return 0;
765
+ return parentEl.querySelectorAll(resolveSelector(selector.selector)).length;
682
766
  }
683
767
  // ─── Gestures & Keys ─────────────────────────────────────────────────────
684
768
  async pressKey(key) {