@@ -25,6 +25,7 @@ import type {
2525 PreviewAutomationNetworkEntry ,
2626 PreviewAutomationScrollInput ,
2727 PreviewAutomationSnapshot ,
28+ PreviewAutomationSnapshotInclude ,
2829 PreviewAutomationStatus ,
2930 PreviewAutomationTypeInput ,
3031 PreviewAutomationWaitForInput ,
@@ -2863,11 +2864,17 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
28632864 } ) ;
28642865
28652866 const captureAutomationSnapshot = Effect . fn ( "PreviewManager.captureAutomationSnapshot" ) (
2866- function * ( tabId : string , wc : Electron . WebContents , send : SendCommand ) {
2867- yield * Effect . all ( [ send ( "Runtime.enable" ) , send ( "Accessibility.enable" ) ] , {
2868- concurrency : 2 ,
2869- discard : true ,
2870- } ) ;
2867+ function * (
2868+ tabId : string ,
2869+ wc : Electron . WebContents ,
2870+ send : SendCommand ,
2871+ include : ReadonlyArray < PreviewAutomationSnapshotInclude > = [ ] ,
2872+ ) {
2873+ const includeAx = include . includes ( "ax" ) ;
2874+ const includeConsole = include . includes ( "console" ) ;
2875+ const includeNetwork = include . includes ( "network" ) ;
2876+ yield * send ( "Runtime.enable" ) ;
2877+ if ( includeAx ) yield * send ( "Accessibility.enable" ) ;
28712878 const page = yield * evaluateWithDebugger < {
28722879 url : string ;
28732880 title : string ;
@@ -2905,44 +2912,73 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
29052912 const rect = element.getBoundingClientRect();
29062913 return style.visibility !== "hidden" && style.display !== "none" && rect.width > 0 && rect.height > 0;
29072914 };
2915+ const clickable = (element) => {
2916+ if (element.matches("a[href],button,input,textarea,select,[role],[tabindex]")) return true;
2917+ const role = element.getAttribute("role");
2918+ if (role === "row" || role === "gridcell" || role === "option") return true;
2919+ const style = getComputedStyle(element);
2920+ return style.cursor === "pointer" && (element.tagName === "TR" || element.tagName === "TD" || element.tagName === "DIV");
2921+ };
2922+ const seen = new Set();
29082923 const elements = Array.from(document.querySelectorAll(
2909- "a[href],button,input,textarea,select,[role],[tabindex]"
2910- )).filter(visible).slice(0, ${ MAX_INTERACTIVE_ELEMENTS } ).map((element) => {
2924+ "a[href],button,input,textarea,select,[role],[tabindex],[role=row],tr,[role=gridcell]"
2925+ )).filter((element) => {
2926+ if (!visible(element) || !clickable(element) || seen.has(element)) return false;
2927+ seen.add(element);
2928+ return true;
2929+ }).slice(0, ${ MAX_INTERACTIVE_ELEMENTS } ).map((element, index) => {
29112930 const rect = element.getBoundingClientRect();
29122931 return {
2932+ id: "e" + (index + 1),
29132933 tag: element.tagName.toLowerCase(),
29142934 role: element.getAttribute("role"),
2915- name: element.getAttribute("aria-label") || element.innerText || element.getAttribute("name") || "",
2935+ name: ( element.getAttribute("aria-label") || element.innerText || element.getAttribute("name") || "").slice(0, 160) ,
29162936 selector: selectorFor(element),
29172937 x: rect.x,
29182938 y: rect.y,
29192939 width: rect.width,
29202940 height: rect.height
29212941 };
29222942 });
2943+ const main = document.querySelector("main");
29232944 return {
29242945 url: location.href,
29252946 title: document.title,
29262947 loading: document.readyState !== "complete",
2927- visibleText: (document.body?.innerText || "").slice(0, ${ MAX_VISIBLE_TEXT_LENGTH } ),
2948+ visibleText: ((main && main.innerText) || document.body?.innerText || "").slice(0, ${ MAX_VISIBLE_TEXT_LENGTH } ),
29282949 interactiveElements: elements
29292950 };
29302951 })()` ,
29312952 true ,
29322953 ) ;
2933- const [ accessibility , sourceImage , diagnostics , timelines ] = yield * Effect . all ( [
2934- send ( "Accessibility.getFullAXTree" ) ,
2935- attemptPromise (
2936- {
2937- operation : "automationSnapshot.capturePage" ,
2938- tabId,
2939- webContentsId : wc . id ,
2940- } ,
2941- ( ) => wc . capturePage ( ) ,
2954+ const accessibility = includeAx ? yield * send ( "Accessibility.getFullAXTree" ) : undefined ;
2955+ const [ diagnostics , timelines ] = yield * Effect . all (
2956+ [ Ref . get ( diagnosticsRef ) , Ref . get ( actionTimelineRef ) ] ,
2957+ { concurrency : 2 } ,
2958+ ) ;
2959+ const sourceImage = yield * attemptPromise (
2960+ {
2961+ operation : "automationSnapshot.capturePage" ,
2962+ tabId,
2963+ webContentsId : wc . id ,
2964+ } ,
2965+ ( ) => wc . capturePage ( ) ,
2966+ ) . pipe (
2967+ Effect . catch ( ( ) =>
2968+ send ( "Page.captureScreenshot" , { format : "png" } ) . pipe (
2969+ Effect . map ( ( result ) => {
2970+ const data =
2971+ result !== null &&
2972+ typeof result === "object" &&
2973+ "data" in result &&
2974+ typeof result . data === "string"
2975+ ? result . data
2976+ : "" ;
2977+ return nativeImage . createFromBuffer ( Buffer . from ( data , "base64" ) ) ;
2978+ } ) ,
2979+ ) ,
29422980 ) ,
2943- Ref . get ( diagnosticsRef ) ,
2944- Ref . get ( actionTimelineRef ) ,
2945- ] ) ;
2981+ ) ;
29462982 const sourceSize = sourceImage . getSize ( ) ;
29472983 const image =
29482984 sourceSize . width > MAX_SCREENSHOT_WIDTH
@@ -2952,9 +2988,9 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
29522988 const browserDiagnostics = diagnostics . get ( wc . id ) ;
29532989 return {
29542990 ...page ,
2955- accessibilityTree : accessibility ,
2956- consoleEntries : [ ...( browserDiagnostics ?. consoleEntries ?? [ ] ) ] ,
2957- networkEntries : [ ...( browserDiagnostics ?. networkEntries ?? [ ] ) ] ,
2991+ ... ( includeAx ? { accessibilityTree : accessibility } : { } ) ,
2992+ consoleEntries : includeConsole ? [ ...( browserDiagnostics ?. consoleEntries ?? [ ] ) ] : [ ] ,
2993+ networkEntries : includeNetwork ? [ ...( browserDiagnostics ?. networkEntries ?? [ ] ) ] : [ ] ,
29582994 actionTimeline : [ ...( timelines . get ( tabId ) ?? [ ] ) ] ,
29592995 screenshot : {
29602996 mimeType : "image/png" as const ,
@@ -2968,10 +3004,11 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
29683004
29693005 const automationSnapshot = Effect . fn ( "PreviewManager.automationSnapshot" ) ( function * (
29703006 tabId : string ,
3007+ include : ReadonlyArray < PreviewAutomationSnapshotInclude > = [ ] ,
29713008 ) {
29723009 const wc = yield * requireWebContents ( tabId ) ;
29733010 return yield * withControlSession ( tabId , wc , "snapshot" , ( send ) =>
2974- captureAutomationSnapshot ( tabId , wc , send ) ,
3011+ captureAutomationSnapshot ( tabId , wc , send , include ) ,
29753012 ) ;
29763013 } ) ;
29773014
@@ -3398,7 +3435,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
33983435 yield * send ( "Runtime.enable" ) ;
33993436 const locator = automationLocator ( input ) ;
34003437 if ( locator ) yield * ensurePlaywrightInjected ( tabId , send ) ;
3401- const [ locatorJson , textJson , urlIncludesJson ] = yield * Effect . all ( [
3438+ const [ locatorJson , textJson , urlIncludesJson , scopeJson ] = yield * Effect . all ( [
34023439 locator
34033440 ? encodeJson ( { operation : "automationWaitFor.encodeLocator" , tabId } , locator )
34043441 : Effect . succeed ( null ) ,
@@ -3408,6 +3445,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
34083445 input . urlIncludes
34093446 ? encodeJson ( { operation : "automationWaitFor.encodeUrl" , tabId } , input . urlIncludes )
34103447 : Effect . succeed ( null ) ,
3448+ encodeJson ( { operation : "automationWaitFor.encodeScope" , tabId } , input . scope ?? "main" ) ,
34113449 ] ) ;
34123450 const deadline = ( yield * currentMillis ) + timeoutMs ;
34133451 while ( ( yield * currentMillis ) <= deadline ) {
@@ -3418,9 +3456,29 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
34183456 send ,
34193457 `(() => {
34203458 try {
3421- const selectorMatched = ${ locatorJson ? `(() => { const injected = globalThis.__t3PlaywrightInjected; return injected.querySelector(injected.parseSelector(${ locatorJson } ), document, false) !== null; })()` : "true" } ;
3459+ const root = ${ scopeJson } === "document"
3460+ ? document.documentElement
3461+ : (document.querySelector("main") || document.documentElement);
3462+ const selectorMatched = ${
3463+ locatorJson
3464+ ? `(() => {
3465+ const injected = globalThis.__t3PlaywrightInjected;
3466+ const parsed = injected.parseSelector(${ locatorJson } );
3467+ const element = injected.querySelector(parsed, root, false);
3468+ if (!element) return false;
3469+ const visible = injected.elementState(element, "visible");
3470+ if (!visible.matches) return false;
3471+ if (element.getAttribute("role") === "dialog") {
3472+ const slot = element.getAttribute("data-slot") || "";
3473+ if (slot.includes("trigger")) return false;
3474+ }
3475+ return true;
3476+ })()`
3477+ : "true"
3478+ } ;
3479+ const textRoot = root instanceof Element ? root : (root.body || document.body);
34223480 const textMatched = ${
3423- textJson ? `(document.body ?.innerText || "").includes(${ textJson } )` : "true"
3481+ textJson ? `(textRoot ?.innerText || "").includes(${ textJson } )` : "true"
34243482 } ;
34253483 const urlMatched = ${
34263484 urlIncludesJson ? `location.href.includes(${ urlIncludesJson } )` : "true"
@@ -3876,6 +3934,7 @@ export class PreviewManager extends Context.Service<
38763934 ) => Effect . Effect < PreviewAutomationStatus , PreviewManagerError > ;
38773935 readonly automationSnapshot : (
38783936 tabId : string ,
3937+ include ?: ReadonlyArray < PreviewAutomationSnapshotInclude > ,
38793938 ) => Effect . Effect < PreviewAutomationSnapshot , PreviewManagerError > ;
38803939 readonly automationClick : (
38813940 tabId : string ,
0 commit comments