@opensteer/engine-playwright 0.7.0 → 0.8.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.
package/dist/index.cjs CHANGED
@@ -2373,8 +2373,421 @@ var CLASSIFY_POINTER_HIT_DECLARATION = String.raw`function(hitNode, point) {
2373
2373
  ambiguous,
2374
2374
  };
2375
2375
  }`;
2376
+ var LIVE_REPLAY_PATH_MATCH_ATTRIBUTE_PRIORITY = [
2377
+ "class",
2378
+ "data-testid",
2379
+ "data-test",
2380
+ "data-qa",
2381
+ "data-cy",
2382
+ "name",
2383
+ "role",
2384
+ "type",
2385
+ "aria-label",
2386
+ "title",
2387
+ "placeholder",
2388
+ "for",
2389
+ "aria-controls",
2390
+ "aria-labelledby",
2391
+ "aria-describedby",
2392
+ "id",
2393
+ "href",
2394
+ "value",
2395
+ "src",
2396
+ "srcset",
2397
+ "imagesrcset",
2398
+ "ping",
2399
+ "alt"
2400
+ ];
2401
+ var LIVE_REPLAY_PATH_STABLE_PRIMARY_ATTR_KEYS = [
2402
+ "data-testid",
2403
+ "data-test",
2404
+ "data-qa",
2405
+ "data-cy",
2406
+ "name",
2407
+ "role",
2408
+ "type",
2409
+ "aria-label",
2410
+ "title",
2411
+ "placeholder"
2412
+ ];
2413
+ var LIVE_REPLAY_PATH_DEFERRED_MATCH_ATTR_KEYS = [
2414
+ "href",
2415
+ "src",
2416
+ "srcset",
2417
+ "imagesrcset",
2418
+ "ping",
2419
+ "value",
2420
+ "for",
2421
+ "aria-controls",
2422
+ "aria-labelledby",
2423
+ "aria-describedby"
2424
+ ];
2425
+ var LIVE_REPLAY_PATH_POLICY = {
2426
+ matchAttributePriority: LIVE_REPLAY_PATH_MATCH_ATTRIBUTE_PRIORITY,
2427
+ stablePrimaryAttrKeys: LIVE_REPLAY_PATH_STABLE_PRIMARY_ATTR_KEYS,
2428
+ deferredMatchAttrKeys: LIVE_REPLAY_PATH_DEFERRED_MATCH_ATTR_KEYS
2429
+ };
2430
+ var BUILD_LIVE_REPLAY_PATH_DECLARATION = String.raw`function(policy, source) {
2431
+ const buildReplayPath = (0, eval)(source);
2432
+ return buildReplayPath(this, policy);
2433
+ }`;
2434
+ var BUILD_LIVE_REPLAY_PATH_SOURCE = String.raw`(target, policy) => {
2435
+ const MAX_ATTRIBUTE_VALUE_LENGTH = 300;
2436
+
2437
+ function isValidAttrKey(key) {
2438
+ const trimmed = String(key || "").trim();
2439
+ if (!trimmed) return false;
2440
+ if (/[\s"'<>/]/.test(trimmed)) return false;
2441
+ return /^[A-Za-z_][A-Za-z0-9_:\-.]*$/.test(trimmed);
2442
+ }
2443
+
2444
+ function isMediaTag(tag) {
2445
+ return new Set(["img", "video", "source", "iframe"]).has(String(tag || "").toLowerCase());
2446
+ }
2447
+
2448
+ function shouldKeepAttr(tag, key, value) {
2449
+ const normalized = String(key || "").trim().toLowerCase();
2450
+ if (!normalized || !String(value || "").trim()) return false;
2451
+ if (!isValidAttrKey(key)) return false;
2452
+ if (normalized === "c") return false;
2453
+ if (/^on[a-z]/i.test(normalized)) return false;
2454
+ if (new Set(["style", "nonce", "integrity", "crossorigin", "referrerpolicy", "autocomplete"]).has(normalized)) {
2455
+ return false;
2456
+ }
2457
+ if (normalized.startsWith("data-os-") || normalized.startsWith("data-opensteer-")) {
2458
+ return false;
2459
+ }
2460
+ if (
2461
+ isMediaTag(tag) &&
2462
+ new Set([
2463
+ "data-src",
2464
+ "data-lazy-src",
2465
+ "data-original",
2466
+ "data-lazy",
2467
+ "data-image",
2468
+ "data-url",
2469
+ "data-srcset",
2470
+ "data-lazy-srcset",
2471
+ "data-was-processed",
2472
+ ]).has(normalized)
2473
+ ) {
2474
+ return false;
2475
+ }
2476
+ return true;
2477
+ }
2478
+
2479
+ function collectAttrs(node) {
2480
+ const tag = node.tagName.toLowerCase();
2481
+ const attrs = {};
2482
+ for (const attr of Array.from(node.attributes)) {
2483
+ if (!shouldKeepAttr(tag, attr.name, attr.value)) {
2484
+ continue;
2485
+ }
2486
+ const value = String(attr.value || "");
2487
+ if (!value.trim()) continue;
2488
+ if (value.length > MAX_ATTRIBUTE_VALUE_LENGTH) continue;
2489
+ attrs[attr.name] = value;
2490
+ }
2491
+ return attrs;
2492
+ }
2493
+
2494
+ function getSiblings(node, root) {
2495
+ if (node.parentElement) return Array.from(node.parentElement.children);
2496
+ return Array.from(root.children || []);
2497
+ }
2498
+
2499
+ function toPosition(node, root) {
2500
+ const siblings = getSiblings(node, root);
2501
+ const tag = node.tagName.toLowerCase();
2502
+ const sameTag = siblings.filter((candidate) => candidate.tagName.toLowerCase() === tag);
2503
+ return {
2504
+ nthChild: siblings.indexOf(node) + 1,
2505
+ nthOfType: sameTag.indexOf(node) + 1,
2506
+ };
2507
+ }
2508
+
2509
+ function buildChain(node) {
2510
+ const chain = [];
2511
+ let current = node;
2512
+ while (current) {
2513
+ chain.push(current);
2514
+ if (current.parentElement) {
2515
+ current = current.parentElement;
2516
+ continue;
2517
+ }
2518
+ break;
2519
+ }
2520
+ chain.reverse();
2521
+ return chain;
2522
+ }
2523
+
2524
+ function sortAttributeKeys(keys) {
2525
+ const priority = Array.isArray(policy?.matchAttributePriority)
2526
+ ? policy.matchAttributePriority.map((value) => String(value))
2527
+ : [];
2528
+ return [...keys].sort((left, right) => {
2529
+ const leftIndex = priority.indexOf(left);
2530
+ const rightIndex = priority.indexOf(right);
2531
+ const leftRank = leftIndex === -1 ? Number.MAX_SAFE_INTEGER : leftIndex;
2532
+ const rightRank = rightIndex === -1 ? Number.MAX_SAFE_INTEGER : rightIndex;
2533
+ if (leftRank !== rightRank) {
2534
+ return leftRank - rightRank;
2535
+ }
2536
+ return left.localeCompare(right);
2537
+ });
2538
+ }
2539
+
2540
+ function tokenizeClassValue(value) {
2541
+ const seen = new Set();
2542
+ const out = [];
2543
+ for (const token of String(value || "").split(/\s+/)) {
2544
+ const normalized = token.trim();
2545
+ if (!normalized || seen.has(normalized)) continue;
2546
+ seen.add(normalized);
2547
+ out.push(normalized);
2548
+ }
2549
+ return out;
2550
+ }
2551
+
2552
+ function clauseKey(clause) {
2553
+ return JSON.stringify(clause);
2554
+ }
2555
+
2556
+ function shouldDeferMatchAttribute(rawKey) {
2557
+ const key = String(rawKey || "").trim().toLowerCase();
2558
+ if (!key || key === "class") return false;
2559
+ if (key === "id" || /(?:^|[-_:])id$/.test(key)) return true;
2560
+ const deferred = new Set(
2561
+ Array.isArray(policy?.deferredMatchAttrKeys)
2562
+ ? policy.deferredMatchAttrKeys.map((value) => String(value))
2563
+ : [],
2564
+ );
2565
+ if (deferred.has(key)) return true;
2566
+ const stablePrimary = new Set(
2567
+ Array.isArray(policy?.stablePrimaryAttrKeys)
2568
+ ? policy.stablePrimaryAttrKeys.map((value) => String(value))
2569
+ : [],
2570
+ );
2571
+ if (key.startsWith("data-") && !stablePrimary.has(key)) return true;
2572
+ return !stablePrimary.has(key);
2573
+ }
2574
+
2575
+ function buildSegmentSelector(data) {
2576
+ let selector = String(data.tag || "*").toLowerCase();
2577
+ for (const clause of data.match || []) {
2578
+ if (clause.kind === "position") {
2579
+ if (clause.axis === "nthOfType") {
2580
+ selector += ":nth-of-type(" + Math.max(1, Number(data.position?.nthOfType || 1)) + ")";
2581
+ } else {
2582
+ selector += ":nth-child(" + Math.max(1, Number(data.position?.nthChild || 1)) + ")";
2583
+ }
2584
+ continue;
2585
+ }
2586
+
2587
+ const key = String(clause.key || "");
2588
+ const value = typeof clause.value === "string" ? clause.value : data.attrs?.[key];
2589
+ if (!key || !value) continue;
2590
+ if (key === "class" && (clause.op || "exact") === "exact") {
2591
+ for (const token of tokenizeClassValue(value)) {
2592
+ const escapedToken = String(token).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
2593
+ selector += '[class~="' + escapedToken + '"]';
2594
+ }
2595
+ continue;
2596
+ }
2597
+ const escaped = String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
2598
+ const op = clause.op || "exact";
2599
+ if (op === "startsWith") selector += "[" + key + '^="' + escaped + '"]';
2600
+ else if (op === "contains") selector += "[" + key + '*="' + escaped + '"]';
2601
+ else selector += "[" + key + '="' + escaped + '"]';
2602
+ }
2603
+ return selector;
2604
+ }
2605
+
2606
+ function buildCandidates(nodes) {
2607
+ const parts = nodes.map((node) => buildSegmentSelector(node));
2608
+ const out = [];
2609
+ const seen = new Set();
2610
+ for (let start = 0; start < parts.length; start += 1) {
2611
+ const selector = parts.slice(start).join(" ");
2612
+ if (!selector || seen.has(selector)) continue;
2613
+ seen.add(selector);
2614
+ out.push(selector);
2615
+ }
2616
+ return out;
2617
+ }
2618
+
2619
+ function selectReplayCandidate(nodes, root) {
2620
+ const selectors = buildCandidates(nodes);
2621
+ let fallback = null;
2622
+ let fallbackSelector = null;
2623
+ let fallbackCount = 0;
2624
+ for (const selector of selectors) {
2625
+ let matches = [];
2626
+ try {
2627
+ matches = Array.from(root.querySelectorAll(selector));
2628
+ } catch {
2629
+ matches = [];
2630
+ }
2631
+ if (!matches.length) continue;
2632
+ if (matches.length === 1) {
2633
+ return {
2634
+ element: matches[0],
2635
+ selector,
2636
+ count: 1,
2637
+ mode: "unique",
2638
+ };
2639
+ }
2640
+ if (!fallback) {
2641
+ fallback = matches[0];
2642
+ fallbackSelector = selector;
2643
+ fallbackCount = matches.length;
2644
+ }
2645
+ }
2646
+ if (fallback && fallbackSelector) {
2647
+ return {
2648
+ element: fallback,
2649
+ selector: fallbackSelector,
2650
+ count: fallbackCount,
2651
+ mode: "fallback",
2652
+ };
2653
+ }
2654
+ return null;
2655
+ }
2656
+
2657
+ function buildClausePool(data) {
2658
+ const attrs = data.attrs || {};
2659
+ const pool = [];
2660
+ const deferred = [];
2661
+ const used = new Set();
2662
+
2663
+ const classValue = String(attrs.class || "").trim();
2664
+ if (classValue) {
2665
+ const clause = { kind: "attr", key: "class", op: "exact", value: classValue };
2666
+ used.add(clauseKey(clause));
2667
+ pool.push(clause);
2668
+ }
2669
+
2670
+ for (const key of sortAttributeKeys(Object.keys(attrs))) {
2671
+ if (key === "class") continue;
2672
+ const value = attrs[key];
2673
+ if (!value || !String(value).trim()) continue;
2674
+ const clause = { kind: "attr", key, op: "exact" };
2675
+ const keyId = clauseKey(clause);
2676
+ if (used.has(keyId)) continue;
2677
+ used.add(keyId);
2678
+ if (shouldDeferMatchAttribute(key)) deferred.push(clause);
2679
+ else pool.push(clause);
2680
+ }
2681
+
2682
+ for (const clause of [
2683
+ { kind: "position", axis: "nthOfType" },
2684
+ { kind: "position", axis: "nthChild" },
2685
+ ]) {
2686
+ const keyId = clauseKey(clause);
2687
+ if (used.has(keyId)) continue;
2688
+ used.add(keyId);
2689
+ pool.push(clause);
2690
+ }
2691
+
2692
+ if (!pool.some((clause) => clause.kind === "attr")) {
2693
+ pool.push(...deferred);
2694
+ }
2695
+
2696
+ return pool;
2697
+ }
2698
+
2699
+ function finalizePath(elements, root) {
2700
+ if (!elements.length) return null;
2701
+ const nodes = elements.map((element) => ({
2702
+ tag: element.tagName.toLowerCase(),
2703
+ attrs: collectAttrs(element),
2704
+ position: toPosition(element, root),
2705
+ match: [],
2706
+ }));
2707
+
2708
+ const pools = nodes.map((node) => {
2709
+ node.match = [];
2710
+ return [...buildClausePool(node)];
2711
+ });
2712
+
2713
+ for (let index = 0; index < pools.length; index += 1) {
2714
+ const classIndex = pools[index].findIndex(
2715
+ (clause) => clause.kind === "attr" && clause.key === "class",
2716
+ );
2717
+ if (classIndex < 0) continue;
2718
+ const classClause = pools[index][classIndex];
2719
+ if (!classClause) continue;
2720
+ nodes[index].match.push(classClause);
2721
+ pools[index].splice(classIndex, 1);
2722
+ }
2723
+
2724
+ const expected = elements[elements.length - 1];
2725
+ const totalRemaining = pools.reduce((count, pool) => count + pool.length, 0);
2726
+ for (let iteration = 0; iteration <= totalRemaining; iteration += 1) {
2727
+ const chosen = selectReplayCandidate(nodes, root);
2728
+ if (chosen && chosen.mode === "unique" && chosen.element === expected) {
2729
+ return {
2730
+ nodes,
2731
+ selector: chosen.selector,
2732
+ };
2733
+ }
2734
+
2735
+ let added = false;
2736
+ for (let index = pools.length - 1; index >= 0; index -= 1) {
2737
+ const next = pools[index][0];
2738
+ if (!next) continue;
2739
+ nodes[index].match.push(next);
2740
+ pools[index].shift();
2741
+ added = true;
2742
+ break;
2743
+ }
2744
+ if (!added) break;
2745
+ }
2746
+
2747
+ return null;
2748
+ }
2749
+
2750
+ if (!(target instanceof Element)) return null;
2751
+
2752
+ const context = [];
2753
+ let currentRoot = target.getRootNode() instanceof ShadowRoot ? target.getRootNode() : document;
2754
+ const targetChain = buildChain(target);
2755
+ const finalizedTarget = finalizePath(targetChain, currentRoot);
2756
+ if (!finalizedTarget) return null;
2757
+
2758
+ while (currentRoot instanceof ShadowRoot) {
2759
+ const host = currentRoot.host;
2760
+ const hostRoot =
2761
+ host.getRootNode() instanceof ShadowRoot ? host.getRootNode() : document;
2762
+ const hostChain = buildChain(host);
2763
+ const finalizedHost = finalizePath(hostChain, hostRoot);
2764
+ if (!finalizedHost) return null;
2765
+ context.unshift({
2766
+ kind: "shadow",
2767
+ host: finalizedHost.nodes,
2768
+ });
2769
+ currentRoot = hostRoot;
2770
+ }
2771
+
2772
+ return {
2773
+ resolution: "deterministic",
2774
+ context,
2775
+ nodes: finalizedTarget.nodes,
2776
+ };
2777
+ }`;
2376
2778
  function createPlaywrightDomActionBridge(context) {
2377
2779
  return {
2780
+ buildReplayPath(locator) {
2781
+ return withLiveNode(context, locator, async ({ controller, document, backendNodeId }) => {
2782
+ const localPath = await buildLiveReplayPathForLocator(
2783
+ controller,
2784
+ document,
2785
+ locator,
2786
+ backendNodeId
2787
+ );
2788
+ return prefixIframeReplayPath(context, document.frameRef, localPath);
2789
+ });
2790
+ },
2378
2791
  inspectActionTarget(locator) {
2379
2792
  return withLiveNode(context, locator, async ({ controller, document, backendNodeId }) => {
2380
2793
  const nodeId = await resolveFrontendNodeId(controller, document, locator, backendNodeId);
@@ -2668,6 +3081,58 @@ function normalizePointerHitAssessment(value, canonicalTarget) {
2668
3081
  canonicalTarget
2669
3082
  };
2670
3083
  }
3084
+ async function buildLiveReplayPathForLocator(controller, document, locator, backendNodeId) {
3085
+ const raw = await callNodeFunction(controller, document, locator, backendNodeId, {
3086
+ functionDeclaration: BUILD_LIVE_REPLAY_PATH_DECLARATION,
3087
+ arguments: [{ value: LIVE_REPLAY_PATH_POLICY }, { value: BUILD_LIVE_REPLAY_PATH_SOURCE }],
3088
+ returnByValue: true
3089
+ });
3090
+ return requireReplayPath(raw, locator);
3091
+ }
3092
+ async function prefixIframeReplayPath(context, frameRef, localPath) {
3093
+ let currentPath = localPath;
3094
+ let currentFrame = context.requireFrame(frameRef);
3095
+ while (currentFrame.parentFrame() !== null) {
3096
+ const frameElement = await currentFrame.frameElement();
3097
+ try {
3098
+ const hostPath = await buildLiveReplayPathForHandle(frameElement);
3099
+ currentPath = {
3100
+ resolution: "deterministic",
3101
+ context: [
3102
+ ...hostPath.context,
3103
+ { kind: "iframe", host: hostPath.nodes },
3104
+ ...currentPath.context
3105
+ ],
3106
+ nodes: currentPath.nodes
3107
+ };
3108
+ } finally {
3109
+ await frameElement.dispose().catch(() => void 0);
3110
+ }
3111
+ currentFrame = currentFrame.parentFrame();
3112
+ }
3113
+ return currentPath;
3114
+ }
3115
+ async function buildLiveReplayPathForHandle(handle) {
3116
+ const raw = await handle.evaluate(
3117
+ (element, input) => {
3118
+ const buildReplayPath = (0, eval)(input.source);
3119
+ return buildReplayPath(element, input.policy);
3120
+ },
3121
+ {
3122
+ policy: LIVE_REPLAY_PATH_POLICY,
3123
+ source: BUILD_LIVE_REPLAY_PATH_SOURCE
3124
+ }
3125
+ );
3126
+ return requireReplayPath(raw);
3127
+ }
3128
+ function requireReplayPath(value, locator) {
3129
+ if (!value || typeof value !== "object" || Array.isArray(value) || value.resolution !== "deterministic") {
3130
+ throw new Error(
3131
+ locator === void 0 ? "live DOM replay path builder returned an invalid result" : `live DOM replay path builder returned an invalid result for ${locator.nodeRef}`
3132
+ );
3133
+ }
3134
+ return value;
3135
+ }
2671
3136
  function unionQuadBounds(quads) {
2672
3137
  const bounds = quads.map((quad) => quadBounds(quad));
2673
3138
  const minX = Math.min(...bounds.map((rect) => rect.x));
@@ -2960,6 +3425,7 @@ var PlaywrightBrowserCoreEngine = class _PlaywrightBrowserCoreEngine {
2960
3425
  document.documentEpoch,
2961
3426
  this.nodeRefForBackendNode(document, backendNodeId)
2962
3427
  ),
3428
+ requireFrame: (frameRef) => this.requireLiveFrame(frameRef),
2963
3429
  requireLiveNode: (locator) => this.requireLiveNode(locator)
2964
3430
  });
2965
3431
  return this.domActionBridge;
@@ -5106,6 +5572,18 @@ var PlaywrightBrowserCoreEngine = class _PlaywrightBrowserCoreEngine {
5106
5572
  }
5107
5573
  return frame;
5108
5574
  }
5575
+ requireLiveFrame(frameRef) {
5576
+ const state = this.requireFrame(frameRef);
5577
+ const controller = this.requirePage(state.pageRef);
5578
+ for (const frame of controller.page.frames()) {
5579
+ if (controller.frameBindings.get(frame) === frameRef) {
5580
+ return frame;
5581
+ }
5582
+ }
5583
+ throw createBrowserCoreError("not-found", `frame ${frameRef} is not attached to a live page`, {
5584
+ details: { frameRef }
5585
+ });
5586
+ }
5109
5587
  requireDocument(documentRef) {
5110
5588
  const document = this.documents.get(documentRef);
5111
5589
  if (!document) {
@@ -5307,6 +5785,9 @@ async function connectPlaywrightChromiumBrowser(input) {
5307
5785
  ...input.headers === void 0 ? {} : { headers: input.headers }
5308
5786
  });
5309
5787
  }
5788
+ async function disconnectPlaywrightChromiumBrowser(browser) {
5789
+ void browser.close().catch(() => void 0);
5790
+ }
5310
5791
  function objectHeadersToEntries(headers) {
5311
5792
  if (!headers) {
5312
5793
  return [];
@@ -5464,5 +5945,6 @@ exports.PlaywrightBrowserCoreEngine = PlaywrightBrowserCoreEngine;
5464
5945
  exports.capturePlaywrightStorageOrigins = capturePlaywrightStorageOrigins;
5465
5946
  exports.connectPlaywrightChromiumBrowser = connectPlaywrightChromiumBrowser;
5466
5947
  exports.createPlaywrightBrowserCoreEngine = createPlaywrightBrowserCoreEngine;
5948
+ exports.disconnectPlaywrightChromiumBrowser = disconnectPlaywrightChromiumBrowser;
5467
5949
  //# sourceMappingURL=index.cjs.map
5468
5950
  //# sourceMappingURL=index.cjs.map