@@ -264,6 +264,24 @@ fn route(request: *std.http.Server.Request, arena: std.mem.Allocator, bridge: *B
264264 handleBatch (request , arena , bridge );
265265 } else if (std .mem .eql (u8 , clean_path , "/element/state" )) {
266266 handleElementState (request , arena , bridge );
267+ } else if (std .mem .eql (u8 , clean_path , "/find-element" )) {
268+ handleFindElement (request , arena , bridge );
269+ } else if (std .mem .eql (u8 , clean_path , "/dialog/auto" )) {
270+ handleDialogAuto (request , arena , bridge );
271+ } else if (std .mem .eql (u8 , clean_path , "/dialog/accept" )) {
272+ handleDialogRespond (request , arena , bridge , true );
273+ } else if (std .mem .eql (u8 , clean_path , "/dialog/dismiss" )) {
274+ handleDialogRespond (request , arena , bridge , false );
275+ } else if (std .mem .eql (u8 , clean_path , "/mouse/move" )) {
276+ handleMouseEvent (request , arena , bridge , "mouseMoved" );
277+ } else if (std .mem .eql (u8 , clean_path , "/mouse/down" )) {
278+ handleMouseEvent (request , arena , bridge , "mousePressed" );
279+ } else if (std .mem .eql (u8 , clean_path , "/mouse/up" )) {
280+ handleMouseEvent (request , arena , bridge , "mouseReleased" );
281+ } else if (std .mem .eql (u8 , clean_path , "/mouse/wheel" )) {
282+ handleMouseWheel (request , arena , bridge );
283+ } else if (std .mem .eql (u8 , clean_path , "/page/state" )) {
284+ handlePageState (request , arena , bridge );
267285 } else {
268286 resp .sendError (request , 404 , "Not Found" );
269287 }
@@ -5705,6 +5723,325 @@ fn handleBatch(request: *std.http.Server.Request, arena: std.mem.Allocator, brid
57055723 resp .sendJson (request , results .items );
57065724}
57075725
5726+ fn handleFindElement (request : * std.http.Server.Request , arena : std.mem.Allocator , bridge : * Bridge ) void {
5727+ const target = request .head .target ;
5728+ const tab_id = requireEffectiveTabId (arena , request , bridge ) orelse return ;
5729+ const by_text = getDecodedQueryParamAlloc (arena , target , "text" );
5730+ const by_role = getQueryParam (target , "role" );
5731+ const by_label = getDecodedQueryParamAlloc (arena , target , "label" );
5732+ const by_placeholder = getDecodedQueryParamAlloc (arena , target , "placeholder" );
5733+ const by_testid = getDecodedQueryParamAlloc (arena , target , "testid" );
5734+
5735+ const client = bridge .getCdpClient (tab_id ) orelse {
5736+ resp .sendError (request , 404 , "Tab not found" );
5737+ return ;
5738+ };
5739+
5740+ const js : []const u8 = if (by_text ) | txt | blk : {
5741+ const escaped = jsonEscapeAlloc (arena , txt ) orelse {
5742+ resp .sendError (request , 500 , "Internal Server Error" );
5743+ return ;
5744+ };
5745+ break :blk std .fmt .allocPrint (arena ,
5746+ "(() => {{ const all = document.querySelectorAll('a,button,input,select,textarea,[role],[onclick]'); for (const el of all) {{ if ((el.textContent || '').trim().includes('{s}') || (el.value || '') === '{s}' || (el.ariaLabel || '') === '{s}') {{ el.scrollIntoViewIfNeeded(); const r = el.getBoundingClientRect(); return JSON.stringify({{found:true,tag:el.tagName.toLowerCase(),text:(el.textContent||'').trim().substring(0,80),x:Math.round(r.x+r.width/2),y:Math.round(r.y+r.height/2)}}); }} }} return JSON.stringify({{found:false}}); }})()" ,
5747+ .{ escaped , escaped , escaped }) catch {
5748+ resp .sendError (request , 500 , "Internal Server Error" );
5749+ return ;
5750+ };
5751+ } else if (by_role ) | role | blk : {
5752+ const escaped_role = jsonEscapeAlloc (arena , role ) orelse {
5753+ resp .sendError (request , 500 , "Internal Server Error" );
5754+ return ;
5755+ };
5756+ const name_filter = if (getDecodedQueryParamAlloc (arena , target , "name" )) | n |
5757+ jsonEscapeAlloc (arena , n ) orelse ""
5758+ else
5759+ null ;
5760+ if (name_filter ) | nf | {
5761+ break :blk std .fmt .allocPrint (arena ,
5762+ "(() => {{ const els = document.querySelectorAll('[role=\" {s}\" ]'); for (const el of els) {{ if ((el.textContent||'').trim().includes('{s}') || (el.ariaLabel||'')=== '{s}') {{ el.scrollIntoViewIfNeeded(); const r = el.getBoundingClientRect(); return JSON.stringify({{found:true,tag:el.tagName.toLowerCase(),role:'{s}',name:(el.textContent||'').trim().substring(0,80),x:Math.round(r.x+r.width/2),y:Math.round(r.y+r.height/2)}}); }} }} return JSON.stringify({{found:false}}); }})()" ,
5763+ .{ escaped_role , nf , nf , escaped_role }) catch {
5764+ resp .sendError (request , 500 , "Internal Server Error" );
5765+ return ;
5766+ };
5767+ } else {
5768+ break :blk std .fmt .allocPrint (arena ,
5769+ "(() => {{ const el = document.querySelector('[role=\" {s}\" ]'); if (!el) return JSON.stringify({{found:false}}); el.scrollIntoViewIfNeeded(); const r = el.getBoundingClientRect(); return JSON.stringify({{found:true,tag:el.tagName.toLowerCase(),role:'{s}',name:(el.textContent||'').trim().substring(0,80),x:Math.round(r.x+r.width/2),y:Math.round(r.y+r.height/2)}}); }})()" ,
5770+ .{ escaped_role , escaped_role }) catch {
5771+ resp .sendError (request , 500 , "Internal Server Error" );
5772+ return ;
5773+ };
5774+ }
5775+ } else if (by_label ) | lbl | blk : {
5776+ const escaped = jsonEscapeAlloc (arena , lbl ) orelse {
5777+ resp .sendError (request , 500 , "Internal Server Error" );
5778+ return ;
5779+ };
5780+ break :blk std .fmt .allocPrint (arena ,
5781+ "(() => {{ const labels = document.querySelectorAll('label'); for (const l of labels) {{ if ((l.textContent||'').trim().includes('{s}') && l.control) {{ l.control.scrollIntoViewIfNeeded(); const r = l.control.getBoundingClientRect(); return JSON.stringify({{found:true,tag:l.control.tagName.toLowerCase(),label:'{s}',x:Math.round(r.x+r.width/2),y:Math.round(r.y+r.height/2)}}); }} }} const aria = document.querySelector('[aria-label=\" {s}\" ]'); if (aria) {{ aria.scrollIntoViewIfNeeded(); const r = aria.getBoundingClientRect(); return JSON.stringify({{found:true,tag:aria.tagName.toLowerCase(),label:'{s}',x:Math.round(r.x+r.width/2),y:Math.round(r.y+r.height/2)}}); }} return JSON.stringify({{found:false}}); }})()" ,
5782+ .{ escaped , escaped , escaped , escaped }) catch {
5783+ resp .sendError (request , 500 , "Internal Server Error" );
5784+ return ;
5785+ };
5786+ } else if (by_placeholder ) | ph | blk : {
5787+ const escaped = jsonEscapeAlloc (arena , ph ) orelse {
5788+ resp .sendError (request , 500 , "Internal Server Error" );
5789+ return ;
5790+ };
5791+ break :blk std .fmt .allocPrint (arena ,
5792+ "(() => {{ const el = document.querySelector('[placeholder=\" {s}\" ]') || document.querySelector('input[placeholder*=\" {s}\" ],textarea[placeholder*=\" {s}\" ]'); if (!el) return JSON.stringify({{found:false}}); el.scrollIntoViewIfNeeded(); const r = el.getBoundingClientRect(); return JSON.stringify({{found:true,tag:el.tagName.toLowerCase(),placeholder:'{s}',x:Math.round(r.x+r.width/2),y:Math.round(r.y+r.height/2)}}); }})()" ,
5793+ .{ escaped , escaped , escaped , escaped }) catch {
5794+ resp .sendError (request , 500 , "Internal Server Error" );
5795+ return ;
5796+ };
5797+ } else if (by_testid ) | tid | blk : {
5798+ const escaped = jsonEscapeAlloc (arena , tid ) orelse {
5799+ resp .sendError (request , 500 , "Internal Server Error" );
5800+ return ;
5801+ };
5802+ break :blk std .fmt .allocPrint (arena ,
5803+ "(() => {{ const el = document.querySelector('[data-testid=\" {s}\" ]'); if (!el) return JSON.stringify({{found:false}}); el.scrollIntoViewIfNeeded(); const r = el.getBoundingClientRect(); return JSON.stringify({{found:true,tag:el.tagName.toLowerCase(),testid:'{s}',x:Math.round(r.x+r.width/2),y:Math.round(r.y+r.height/2)}}); }})()" ,
5804+ .{ escaped , escaped }) catch {
5805+ resp .sendError (request , 500 , "Internal Server Error" );
5806+ return ;
5807+ };
5808+ } else {
5809+ resp .sendError (request , 400 , "Provide one of: text, role, label, placeholder, testid" );
5810+ return ;
5811+ };
5812+
5813+ const escaped_js = jsonEscapeAlloc (arena , js ) orelse {
5814+ resp .sendError (request , 500 , "Internal Server Error" );
5815+ return ;
5816+ };
5817+ const params = std .fmt .allocPrint (arena , "{{\" expression\" :\" {s}\" ,\" returnByValue\" :true}}" , .{escaped_js }) catch {
5818+ resp .sendError (request , 500 , "Internal Server Error" );
5819+ return ;
5820+ };
5821+ const response = client .send (arena , protocol .Methods .runtime_evaluate , params ) catch {
5822+ resp .sendError (request , 502 , "CDP command failed" );
5823+ return ;
5824+ };
5825+ const val = extractSimpleJsonString (response , 0 , "\" value\" " ) orelse {
5826+ resp .sendJson (request , response );
5827+ return ;
5828+ };
5829+ resp .sendJson (request , val );
5830+ }
5831+
5832+ fn handleDialogAuto (request : * std.http.Server.Request , arena : std.mem.Allocator , bridge : * Bridge ) void {
5833+ const target = request .head .target ;
5834+ const tab_id = requireEffectiveTabId (arena , request , bridge ) orelse return ;
5835+ const mode = getQueryParam (target , "mode" ) orelse "accept" ;
5836+
5837+ const client = bridge .getCdpClient (tab_id ) orelse {
5838+ resp .sendError (request , 404 , "Tab not found" );
5839+ return ;
5840+ };
5841+
5842+ _ = client .send (arena , protocol .Methods .page_enable , null ) catch {};
5843+
5844+ const accept_str = if (std .mem .eql (u8 , mode , "dismiss" )) "false" else "true" ;
5845+ const js = std .fmt .allocPrint (arena ,
5846+ "(() => {{ window.__kuri_dialog_auto = {s}; window.__kuri_dialog_log = []; window.addEventListener('beforeunload', (e) => {{ e.preventDefault(); }}); return 'auto-dialog-{s}'; }})()" , .{ accept_str , mode }) catch {
5847+ resp .sendError (request , 500 , "Internal Server Error" );
5848+ return ;
5849+ };
5850+ const escaped = jsonEscapeAlloc (arena , js ) orelse {
5851+ resp .sendError (request , 500 , "Internal Server Error" );
5852+ return ;
5853+ };
5854+ const params = std .fmt .allocPrint (arena , "{{\" expression\" :\" {s}\" ,\" returnByValue\" :true}}" , .{escaped }) catch {
5855+ resp .sendError (request , 500 , "Internal Server Error" );
5856+ return ;
5857+ };
5858+ _ = client .send (arena , protocol .Methods .runtime_evaluate , params ) catch {};
5859+
5860+ const dialog_js =
5861+ \\(() => {
5862+ \\ const handler = (e) => {
5863+ \\ window.__kuri_dialog_log = window.__kuri_dialog_log || [];
5864+ \\ window.__kuri_dialog_log.push({type: e.type, message: e.message || '', defaultPrompt: e.defaultPrompt || ''});
5865+ \\ };
5866+ \\ window.addEventListener('alert', handler);
5867+ \\ return 'listeners-attached';
5868+ \\})()
5869+ ;
5870+ const escaped_dlg = jsonEscapeAlloc (arena , dialog_js ) orelse "" ;
5871+ const dlg_params = std .fmt .allocPrint (arena , "{{\" expression\" :\" {s}\" ,\" returnByValue\" :true}}" , .{escaped_dlg }) catch {
5872+ resp .sendError (request , 500 , "Internal Server Error" );
5873+ return ;
5874+ };
5875+ _ = client .send (arena , protocol .Methods .runtime_evaluate , dlg_params ) catch {};
5876+
5877+ const handle_params = std .fmt .allocPrint (arena , "{{\" accept\" :{s}}}" , .{accept_str }) catch {
5878+ resp .sendError (request , 500 , "Internal Server Error" );
5879+ return ;
5880+ };
5881+ _ = client .send (arena , protocol .Methods .page_handle_dialog , handle_params ) catch {};
5882+
5883+ const body = std .fmt .allocPrint (arena , "{{\" ok\" :true,\" mode\" :\" {s}\" }}" , .{mode }) catch {
5884+ resp .sendError (request , 500 , "Internal Server Error" );
5885+ return ;
5886+ };
5887+ resp .sendJson (request , body );
5888+ }
5889+
5890+ fn handleDialogRespond (request : * std.http.Server.Request , arena : std.mem.Allocator , bridge : * Bridge , accept : bool ) void {
5891+ const target = request .head .target ;
5892+ const tab_id = requireEffectiveTabId (arena , request , bridge ) orelse return ;
5893+ const prompt_text = getDecodedQueryParamAlloc (arena , target , "text" );
5894+
5895+ const client = bridge .getCdpClient (tab_id ) orelse {
5896+ resp .sendError (request , 404 , "Tab not found" );
5897+ return ;
5898+ };
5899+
5900+ const params = if (prompt_text ) | pt | blk : {
5901+ const escaped_pt = jsonEscapeAlloc (arena , pt ) orelse "" ;
5902+ break :blk std .fmt .allocPrint (arena , "{{\" accept\" :{s},\" promptText\" :\" {s}\" }}" , .{ if (accept ) "true" else "false" , escaped_pt }) catch {
5903+ resp .sendError (request , 500 , "Internal Server Error" );
5904+ return ;
5905+ };
5906+ } else std .fmt .allocPrint (arena , "{{\" accept\" :{s}}}" , .{if (accept ) "true" else "false" }) catch {
5907+ resp .sendError (request , 500 , "Internal Server Error" );
5908+ return ;
5909+ };
5910+
5911+ _ = client .send (arena , protocol .Methods .page_handle_dialog , params ) catch {
5912+ resp .sendError (request , 502 , "No dialog present or CDP failed" );
5913+ return ;
5914+ };
5915+
5916+ const action = if (accept ) "accepted" else "dismissed" ;
5917+ const body = std .fmt .allocPrint (arena , "{{\" ok\" :true,\" action\" :\" {s}\" }}" , .{action }) catch {
5918+ resp .sendError (request , 500 , "Internal Server Error" );
5919+ return ;
5920+ };
5921+ resp .sendJson (request , body );
5922+ }
5923+
5924+ fn handleMouseEvent (request : * std.http.Server.Request , arena : std.mem.Allocator , bridge : * Bridge , event_type : []const u8 ) void {
5925+ const target = request .head .target ;
5926+ const tab_id = requireEffectiveTabId (arena , request , bridge ) orelse return ;
5927+ const x_str = getQueryParam (target , "x" ) orelse "0" ;
5928+ const y_str = getQueryParam (target , "y" ) orelse "0" ;
5929+ const button = getQueryParam (target , "button" ) orelse "left" ;
5930+ const click_count_str = getQueryParam (target , "clickCount" ) orelse "1" ;
5931+
5932+ const x = std .fmt .parseInt (i64 , x_str , 10 ) catch 0 ;
5933+ const y = std .fmt .parseInt (i64 , y_str , 10 ) catch 0 ;
5934+ const click_count = std .fmt .parseInt (i32 , click_count_str , 10 ) catch 1 ;
5935+
5936+ const client = bridge .getCdpClient (tab_id ) orelse {
5937+ resp .sendError (request , 404 , "Tab not found" );
5938+ return ;
5939+ };
5940+
5941+ const escaped_type = jsonEscapeAlloc (arena , event_type ) orelse event_type ;
5942+ const escaped_button = jsonEscapeAlloc (arena , button ) orelse button ;
5943+ const params = std .fmt .allocPrint (arena ,
5944+ "{{\" type\" :\" {s}\" ,\" x\" :{d},\" y\" :{d},\" button\" :\" {s}\" ,\" clickCount\" :{d}}}" , .{ escaped_type , x , y , escaped_button , click_count }) catch {
5945+ resp .sendError (request , 500 , "Internal Server Error" );
5946+ return ;
5947+ };
5948+ _ = client .send (arena , protocol .Methods .input_dispatch_mouse_event , params ) catch {
5949+ resp .sendError (request , 502 , "Input.dispatchMouseEvent failed" );
5950+ return ;
5951+ };
5952+
5953+ const body = std .fmt .allocPrint (arena , "{{\" ok\" :true,\" type\" :\" {s}\" ,\" x\" :{d},\" y\" :{d}}}" , .{ escaped_type , x , y }) catch {
5954+ resp .sendError (request , 500 , "Internal Server Error" );
5955+ return ;
5956+ };
5957+ resp .sendJson (request , body );
5958+ }
5959+
5960+ fn handleMouseWheel (request : * std.http.Server.Request , arena : std.mem.Allocator , bridge : * Bridge ) void {
5961+ const target = request .head .target ;
5962+ const tab_id = requireEffectiveTabId (arena , request , bridge ) orelse return ;
5963+ const x_str = getQueryParam (target , "x" ) orelse "0" ;
5964+ const y_str = getQueryParam (target , "y" ) orelse "0" ;
5965+ const dx_str = getQueryParam (target , "deltaX" ) orelse "0" ;
5966+ const dy_str = getQueryParam (target , "deltaY" ) orelse "-120" ;
5967+
5968+ const x = std .fmt .parseInt (i64 , x_str , 10 ) catch 0 ;
5969+ const y = std .fmt .parseInt (i64 , y_str , 10 ) catch 0 ;
5970+ const dx = std .fmt .parseInt (i64 , dx_str , 10 ) catch 0 ;
5971+ const dy = std .fmt .parseInt (i64 , dy_str , 10 ) catch -120 ;
5972+
5973+ const client = bridge .getCdpClient (tab_id ) orelse {
5974+ resp .sendError (request , 404 , "Tab not found" );
5975+ return ;
5976+ };
5977+
5978+ const params = std .fmt .allocPrint (arena ,
5979+ "{{\" type\" :\" mouseWheel\" ,\" x\" :{d},\" y\" :{d},\" deltaX\" :{d},\" deltaY\" :{d}}}" , .{ x , y , dx , dy }) catch {
5980+ resp .sendError (request , 500 , "Internal Server Error" );
5981+ return ;
5982+ };
5983+ _ = client .send (arena , protocol .Methods .input_dispatch_mouse_event , params ) catch {
5984+ resp .sendError (request , 502 , "Input.dispatchMouseEvent failed" );
5985+ return ;
5986+ };
5987+
5988+ const body = std .fmt .allocPrint (arena , "{{\" ok\" :true,\" type\" :\" mouseWheel\" ,\" deltaX\" :{d},\" deltaY\" :{d}}}" , .{ dx , dy }) catch {
5989+ resp .sendError (request , 500 , "Internal Server Error" );
5990+ return ;
5991+ };
5992+ resp .sendJson (request , body );
5993+ }
5994+
5995+ fn handlePageState (request : * std.http.Server.Request , arena : std.mem.Allocator , bridge : * Bridge ) void {
5996+ const tab_id = requireEffectiveTabId (arena , request , bridge ) orelse return ;
5997+
5998+ const client = bridge .getCdpClient (tab_id ) orelse {
5999+ resp .sendError (request , 404 , "Tab not found" );
6000+ return ;
6001+ };
6002+
6003+ const js =
6004+ \\(() => {
6005+ \\ const s = {
6006+ \\ url: location.href,
6007+ \\ title: document.title,
6008+ \\ readyState: document.readyState,
6009+ \\ scrollX: Math.round(window.scrollX),
6010+ \\ scrollY: Math.round(window.scrollY),
6011+ \\ scrollHeight: document.documentElement.scrollHeight,
6012+ \\ viewportWidth: window.innerWidth,
6013+ \\ viewportHeight: window.innerHeight,
6014+ \\ documentHeight: document.documentElement.scrollHeight,
6015+ \\ documentWidth: document.documentElement.scrollWidth,
6016+ \\ scrollPercent: Math.round((window.scrollY / Math.max(1, document.documentElement.scrollHeight - window.innerHeight)) * 100),
6017+ \\ forms: document.forms.length,
6018+ \\ links: document.links.length,
6019+ \\ images: document.images.length,
6020+ \\ inputs: document.querySelectorAll('input,textarea,select').length
6021+ \\ };
6022+ \\ return JSON.stringify(s);
6023+ \\})()
6024+ ;
6025+ const escaped = jsonEscapeAlloc (arena , js ) orelse {
6026+ resp .sendError (request , 500 , "Internal Server Error" );
6027+ return ;
6028+ };
6029+ const params = std .fmt .allocPrint (arena , "{{\" expression\" :\" {s}\" ,\" returnByValue\" :true}}" , .{escaped }) catch {
6030+ resp .sendError (request , 500 , "Internal Server Error" );
6031+ return ;
6032+ };
6033+ const response = client .send (arena , protocol .Methods .runtime_evaluate , params ) catch {
6034+ resp .sendError (request , 502 , "CDP command failed" );
6035+ return ;
6036+ };
6037+ const val = extractSimpleJsonString (response , 0 , "\" value\" " ) orelse {
6038+ resp .sendJson (request , response );
6039+ return ;
6040+ };
6041+ resp .sendJson (request , val );
6042+ }
6043+
6044+
57086045test "screenshot routes match" {
57096046 for ([_ ][]const u8 { "/screenshot/annotated" , "/screenshot/diff" , "/screencast/start" , "/screencast/stop" }) | p | {
57106047 try std .testing .expect (p .len > 0 );
@@ -5966,8 +6303,12 @@ test "total endpoint count" {
59666303 "/ws/start" , "/ws/stop" ,
59676304 // Tier 4 new endpoints
59686305 "/batch" , "/element/state" ,
6306+ // Tier 5 new endpoints
6307+ "/find-element" , "/dialog/auto" , "/dialog/accept" , "/dialog/dismiss" ,
6308+ "/mouse/move" , "/mouse/down" , "/mouse/up" , "/mouse/wheel" ,
6309+ "/page/state" ,
59696310 };
5970- try std .testing .expectEqual (@as (usize , 89 ), routes .len );
6311+ try std .testing .expectEqual (@as (usize , 98 ), routes .len );
59716312}
59726313
59736314test "buildGetExpression title" {
0 commit comments