bunite-core 0.14.0 → 0.16.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.
@@ -1,6 +1,10 @@
1
1
  #include "webview2_internal.h"
2
2
 
3
+ #include <algorithm>
4
+ #include <array>
3
5
  #include <cstring>
6
+ #include <memory>
7
+ #include <vector>
4
8
  #include <wincrypt.h> // CryptBinaryToStringA — base64 encoding for screenshot payload.
5
9
 
6
10
  using namespace bunite_webview2;
@@ -12,7 +16,7 @@ void setViewInputPassthrough(ViewHost* v, bool passthrough);
12
16
 
13
17
  extern "C" {
14
18
 
15
- BUNITE_EXPORT int32_t bunite_abi_version(void) { return 9; }
19
+ BUNITE_EXPORT int32_t bunite_abi_version(void) { return 11; }
16
20
 
17
21
  BUNITE_EXPORT void bunite_set_log_level(int32_t level) {
18
22
  if (level < 0) level = 0;
@@ -379,7 +383,8 @@ BUNITE_EXPORT void bunite_view_reload(uint32_t view_id) {
379
383
  BUNITE_EXPORT void bunite_view_remove(uint32_t view_id) { destroyView(view_id); }
380
384
 
381
385
  // Input dispatch — CDP via CallDevToolsProtocolMethod (Playwright pattern).
382
- // MouseEvent.isTrusted is false (CDP-synthesized); capability honest.
386
+ // Edge runtime injects below DevTools surface — DOM `isTrusted=true`
387
+ // (see `bunite_view_capabilities` note).
383
388
  namespace {
384
389
 
385
390
  const char* cdpMouseButton(int32_t b) {
@@ -392,6 +397,23 @@ void cdpCall(ViewHost* v, const wchar_t* method, const std::string& json) {
392
397
  method, utf8ToWide(json).c_str(), nullptr);
393
398
  }
394
399
 
400
+ // Result-aware CDP call. `cb(ok, json)` runs on the UI thread once the result
401
+ // arrives; on failure `ok=false` and json is an error description.
402
+ void cdpCallWithResult(ViewHost* v, const wchar_t* method, const std::string& json,
403
+ std::function<void(bool, std::string)> cb) {
404
+ if (!v || !v->webview) { if (cb) cb(false, "view not ready"); return; }
405
+ auto lifetime = g_runtime.lifetime;
406
+ v->webview->CallDevToolsProtocolMethod(
407
+ method, utf8ToWide(json).c_str(),
408
+ Microsoft::WRL::Callback<ICoreWebView2CallDevToolsProtocolMethodCompletedHandler>(
409
+ [lifetime, cb](HRESULT hr, LPCWSTR result) -> HRESULT {
410
+ if (!lifetime || !lifetime->alive.load()) return S_OK;
411
+ if (FAILED(hr) || !result) { cb(false, "CDP call failed"); return S_OK; }
412
+ cb(true, wideToUtf8(result));
413
+ return S_OK;
414
+ }).Get());
415
+ }
416
+
395
417
  } // namespace
396
418
 
397
419
  BUNITE_EXPORT void bunite_view_click(uint32_t view_id, double x, double y,
@@ -428,6 +450,10 @@ BUNITE_EXPORT void bunite_view_press(uint32_t view_id, int32_t windows_vk_code,
428
450
  std::string key_str = key ? key : "";
429
451
  std::string code_str = code ? code : "";
430
452
 
453
+ // CDP modifier mask: 1=alt, 2=ctrl, 4=meta, 8=shift. Suppress text when any
454
+ // non-shift modifier is set so shortcuts like Ctrl+A don't insert "a".
455
+ const bool has_non_shift_modifier = (modifiers & ~static_cast<uint32_t>(8)) != 0;
456
+
431
457
  auto buildPart = [&](const char* type, bool include_text) {
432
458
  std::string out = "{\"type\":\"";
433
459
  out += type;
@@ -437,7 +463,7 @@ BUNITE_EXPORT void bunite_view_press(uint32_t view_id, int32_t windows_vk_code,
437
463
  if (!code_str.empty()) out += ",\"code\":\"" + escapeJsonString(code_str) + "\"";
438
464
  // CDP `location`: 0 standard, 1 left mod, 2 right mod, 3 numpad.
439
465
  if (location > 0) out += ",\"location\":" + std::to_string(location);
440
- if (include_text && !char_str.empty())
466
+ if (include_text && !char_str.empty() && !has_non_shift_modifier)
441
467
  out += ",\"text\":\"" + escapeJsonString(char_str) + "\"";
442
468
  out += "}";
443
469
  return out;
@@ -485,7 +511,765 @@ BUNITE_EXPORT uint32_t bunite_view_capabilities(uint32_t view_id) {
485
511
  BUNITE_CAP_NATIVE_INPUT_TRUSTED |
486
512
  BUNITE_CAP_CLICK | BUNITE_CAP_TYPE | BUNITE_CAP_PRESS | BUNITE_CAP_SCROLL |
487
513
  BUNITE_CAP_MOUSE | BUNITE_CAP_DIALOGS | BUNITE_CAP_CONSOLE |
488
- BUNITE_CAP_SCREENSHOT | BUNITE_CAP_FORMAT_PNG | BUNITE_CAP_FORMAT_JPEG;
514
+ BUNITE_CAP_SCREENSHOT | BUNITE_CAP_FORMAT_PNG | BUNITE_CAP_FORMAT_JPEG |
515
+ BUNITE_CAP_AX | BUNITE_CAP_BOUNDING_RECT | BUNITE_CAP_FRAMES |
516
+ BUNITE_CAP_DOWNLOADS | BUNITE_CAP_POPUPS |
517
+ BUNITE_CAP_RESOLVE_AND_CLICK;
518
+ }
519
+
520
+ BUNITE_EXPORT void bunite_view_set_download_policy(uint32_t view_id, int32_t policy, const char* download_dir) {
521
+ ViewHost* v = getView(view_id);
522
+ if (!v) return;
523
+ if (policy < 0 || policy > 2) policy = 2;
524
+ v->download_policy.store(policy);
525
+ v->download_dir = download_dir ? download_dir : "";
526
+ }
527
+
528
+ BUNITE_EXPORT void bunite_view_popup_accept(uint32_t new_view_id, uint32_t host_window_id,
529
+ double x, double y, double w, double h) {
530
+ ViewHost* v = getView(new_view_id);
531
+ if (!v || !v->controller || !v->container_hwnd) return;
532
+ WindowHost* host_window = nullptr;
533
+ {
534
+ std::lock_guard<std::mutex> g(g_runtime.object_mutex);
535
+ auto wit = g_runtime.windows_by_id.find(host_window_id);
536
+ if (wit == g_runtime.windows_by_id.end()) return;
537
+ host_window = wit->second;
538
+ }
539
+ if (!host_window || !host_window->hwnd) return;
540
+ v->window = host_window;
541
+ host_window->views.push_back(v);
542
+ SetParent(v->container_hwnd, host_window->hwnd);
543
+ MoveWindow(v->container_hwnd, static_cast<int>(x), static_cast<int>(y),
544
+ static_cast<int>(w), static_cast<int>(h), TRUE);
545
+ ShowWindow(v->container_hwnd, SW_SHOW);
546
+ RECT bounds{0, 0, static_cast<LONG>(w), static_cast<LONG>(h)};
547
+ v->controller->put_Bounds(bounds);
548
+ v->controller->put_IsVisible(TRUE);
549
+ emitWebviewEvent(new_view_id, "view-ready", "");
550
+ }
551
+
552
+ BUNITE_EXPORT void bunite_view_popup_dismiss(uint32_t new_view_id) {
553
+ ViewHost* v = nullptr;
554
+ {
555
+ std::lock_guard<std::mutex> g(g_runtime.object_mutex);
556
+ auto it = g_runtime.views_by_id.find(new_view_id);
557
+ if (it == g_runtime.views_by_id.end()) return;
558
+ v = it->second;
559
+ g_runtime.views_by_id.erase(it);
560
+ }
561
+ if (!v) return;
562
+ if (v->controller) v->controller->Close();
563
+ if (v->container_hwnd) DestroyWindow(v->container_hwnd);
564
+ delete v;
565
+ }
566
+
567
+ namespace {
568
+ void emitAxError(uint32_t view_id, uint32_t request_id, const char* code, const std::string& message) {
569
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
570
+ ",\"ok\":false,\"code\":\"" + code +
571
+ "\",\"message\":\"" + escapeJsonString(message) + "\"}";
572
+ emitWebviewEvent(view_id, "accessibility-result", payload);
573
+ }
574
+ } // namespace
575
+
576
+ namespace {
577
+ // Extract integer field from a CDP JSON response — used to pluck
578
+ // `executionContextId` without pulling in a JSON parser.
579
+ bool extractJsonInt(const std::string& json, const std::string& key, int& out) {
580
+ std::string needle = "\"" + key + "\":";
581
+ auto p = json.find(needle);
582
+ if (p == std::string::npos) return false;
583
+ p += needle.size();
584
+ while (p < json.size() && (json[p] == ' ' || json[p] == '\t')) ++p;
585
+ auto end = p;
586
+ while (end < json.size() && (json[end] == '-' || (json[end] >= '0' && json[end] <= '9'))) ++end;
587
+ if (end == p) return false;
588
+ out = std::stoi(json.substr(p, end - p));
589
+ return true;
590
+ }
591
+ void emitListFramesErrorWv2(uint32_t view_id, uint32_t request_id, const char* code, const std::string& message) {
592
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
593
+ ",\"ok\":false,\"code\":\"" + code +
594
+ "\",\"message\":\"" + escapeJsonString(message) + "\"}";
595
+ emitWebviewEvent(view_id, "list-frames-result", payload);
596
+ }
597
+ } // namespace
598
+
599
+ BUNITE_EXPORT void bunite_view_list_frames(uint32_t view_id, uint32_t request_id) {
600
+ ViewHost* v = getView(view_id);
601
+ if (!v || !v->webview) { emitListFramesErrorWv2(view_id, request_id, "not_supported", "view not ready"); return; }
602
+ cdpCallWithResult(v, L"Page.getFrameTree", "{}",
603
+ [view_id, request_id](bool ok, std::string result) {
604
+ if (!ok) { emitListFramesErrorWv2(view_id, request_id, "runtime_error", "getFrameTree failed: " + result); return; }
605
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
606
+ ",\"ok\":true,\"raw\":" + result + "}";
607
+ emitWebviewEvent(view_id, "list-frames-result", payload);
608
+ });
609
+ }
610
+
611
+ BUNITE_EXPORT void bunite_view_evaluate_in_frame(uint32_t view_id, uint32_t request_id,
612
+ const char* script_c, const char* frame_id_c) {
613
+ std::string script = script_c ? script_c : "";
614
+ std::string frameId = frame_id_c ? frame_id_c : "";
615
+ if (frameId.empty()) {
616
+ bunite_view_evaluate(view_id, request_id, script_c);
617
+ return;
618
+ }
619
+ ViewHost* v = getView(view_id);
620
+ if (!v || !v->webview) {
621
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
622
+ ",\"ok\":false,\"code\":\"not_supported\",\"message\":\"view not ready\"}";
623
+ emitWebviewEvent(view_id, "evaluate-result", payload);
624
+ return;
625
+ }
626
+ std::string isoParams = "{\"frameId\":\"" + escapeJsonString(frameId) + "\",\"worldName\":\"bunite-eval\"}";
627
+ cdpCallWithResult(v, L"Page.createIsolatedWorld", isoParams,
628
+ [view_id, request_id, script](bool ok, std::string isoResult) {
629
+ if (!ok) {
630
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
631
+ ",\"ok\":false,\"code\":\"runtime_error\","
632
+ "\"message\":\"createIsolatedWorld failed\"}";
633
+ emitWebviewEvent(view_id, "evaluate-result", payload);
634
+ return;
635
+ }
636
+ int contextId = 0;
637
+ if (!extractJsonInt(isoResult, "executionContextId", contextId)) {
638
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
639
+ ",\"ok\":false,\"code\":\"runtime_error\","
640
+ "\"message\":\"missing executionContextId\"}";
641
+ emitWebviewEvent(view_id, "evaluate-result", payload);
642
+ return;
643
+ }
644
+ // Re-lookup — the async gap could have torn down the view.
645
+ ViewHost* v2 = getView(view_id);
646
+ if (!v2 || !v2->webview) {
647
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
648
+ ",\"ok\":false,\"code\":\"not_supported\","
649
+ "\"message\":\"view destroyed\"}";
650
+ emitWebviewEvent(view_id, "evaluate-result", payload);
651
+ return;
652
+ }
653
+ std::string evalParams = "{\"expression\":\"" + escapeJsonString(script) +
654
+ "\",\"contextId\":" + std::to_string(contextId) +
655
+ ",\"returnByValue\":true,\"awaitPromise\":true}";
656
+ cdpCallWithResult(v2, L"Runtime.evaluate", evalParams,
657
+ [view_id, request_id](bool ok2, std::string evalResult) {
658
+ if (!ok2) {
659
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
660
+ ",\"ok\":false,\"code\":\"runtime_error\","
661
+ "\"message\":\"Runtime.evaluate failed\"}";
662
+ emitWebviewEvent(view_id, "evaluate-result", payload);
663
+ return;
664
+ }
665
+ // Normalize CDP shape to the flat `{requestId, ok, value/code/message}`
666
+ // that the host's evaluate-result handler expects. exceptionDetails
667
+ // at the top level is preceded by `},` — distinguishes from a value
668
+ // that happens to contain the literal token inside a string.
669
+ auto excPos = evalResult.find("},\"exceptionDetails\"");
670
+ if (excPos == std::string::npos) excPos = evalResult.find("}, \"exceptionDetails\"");
671
+ if (excPos != std::string::npos) {
672
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
673
+ ",\"ok\":false,\"code\":\"runtime_error\","
674
+ "\"message\":\"evaluate threw\"}";
675
+ emitWebviewEvent(view_id, "evaluate-result", payload);
676
+ return;
677
+ }
678
+ // Extract result.value as a JSON substring. CDP returns
679
+ // `{"result":{"type":"...","value":<json>}}`. We find the `"value":`
680
+ // token inside the inner object and slice until the matching close.
681
+ auto resPos = evalResult.find("\"result\":");
682
+ if (resPos == std::string::npos) {
683
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
684
+ ",\"ok\":true,\"value\":\"null\"}";
685
+ emitWebviewEvent(view_id, "evaluate-result", payload);
686
+ return;
687
+ }
688
+ auto valKey = evalResult.find("\"value\":", resPos);
689
+ if (valKey == std::string::npos) {
690
+ // type === "undefined" — no value field.
691
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
692
+ ",\"ok\":true,\"value\":\"null\"}";
693
+ emitWebviewEvent(view_id, "evaluate-result", payload);
694
+ return;
695
+ }
696
+ size_t start = valKey + 8; // past `"value":`
697
+ // Skip whitespace.
698
+ while (start < evalResult.size() && (evalResult[start] == ' ' || evalResult[start] == '\t')) ++start;
699
+ // Find balanced end of the JSON value.
700
+ size_t end = start;
701
+ if (start < evalResult.size()) {
702
+ char c = evalResult[start];
703
+ if (c == '"') {
704
+ ++end;
705
+ while (end < evalResult.size() && evalResult[end] != '"') {
706
+ if (evalResult[end] == '\\' && end + 1 < evalResult.size()) ++end;
707
+ ++end;
708
+ }
709
+ if (end < evalResult.size()) ++end;
710
+ } else if (c == '{' || c == '[') {
711
+ int depth = 0;
712
+ bool inStr = false;
713
+ while (end < evalResult.size()) {
714
+ char ch = evalResult[end];
715
+ if (inStr) {
716
+ if (ch == '\\' && end + 1 < evalResult.size()) ++end;
717
+ else if (ch == '"') inStr = false;
718
+ } else if (ch == '"') inStr = true;
719
+ else if (ch == '{' || ch == '[') ++depth;
720
+ else if (ch == '}' || ch == ']') { --depth; if (depth == 0) { ++end; break; } }
721
+ ++end;
722
+ }
723
+ } else {
724
+ // Number / true / false / null — read until non-token char.
725
+ while (end < evalResult.size()) {
726
+ char ch = evalResult[end];
727
+ if (ch == ',' || ch == '}' || ch == ' ' || ch == '\t' || ch == '\n') break;
728
+ ++end;
729
+ }
730
+ }
731
+ }
732
+ std::string valueJson = evalResult.substr(start, end - start);
733
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
734
+ ",\"ok\":true,\"value\":\"" + escapeJsonString(valueJson) + "\"}";
735
+ emitWebviewEvent(view_id, "evaluate-result", payload);
736
+ });
737
+ });
738
+ }
739
+
740
+ } // extern "C" — exit so the helpers below can return std::string.
741
+
742
+ namespace {
743
+
744
+ void emitResolveAndClickErrorWv2(uint32_t view_id, uint32_t request_id, const char* code, const std::string& message) {
745
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
746
+ ",\"ok\":false,\"code\":\"" + code +
747
+ "\",\"message\":\"" + escapeJsonString(message) + "\"}";
748
+ emitWebviewEvent(view_id, "resolve-and-click-result", payload);
749
+ }
750
+
751
+ const char* cdpButtonNameWv2(int32_t b) {
752
+ switch (b) { case 1: return "middle"; case 2: return "right"; default: return "left"; }
753
+ }
754
+
755
+ std::string extractJsonValueRaw(const std::string& s, const std::string& key) {
756
+ std::string needle = "\"" + key + "\":";
757
+ size_t p = s.find(needle);
758
+ if (p == std::string::npos) return {};
759
+ p += needle.size();
760
+ while (p < s.size() && (s[p] == ' ' || s[p] == '\t')) ++p;
761
+ if (p >= s.size()) return {};
762
+ size_t end = p;
763
+ char c = s[p];
764
+ if (c == '"') {
765
+ ++end;
766
+ while (end < s.size() && s[end] != '"') { if (s[end] == '\\' && end + 1 < s.size()) ++end; ++end; }
767
+ if (end < s.size()) ++end;
768
+ } else if (c == '{' || c == '[') {
769
+ int depth = 0; bool inStr = false;
770
+ while (end < s.size()) {
771
+ char ch = s[end];
772
+ if (inStr) { if (ch == '\\' && end + 1 < s.size()) ++end; else if (ch == '"') inStr = false; }
773
+ else if (ch == '"') inStr = true;
774
+ else if (ch == '{' || ch == '[') ++depth;
775
+ else if (ch == '}' || ch == ']') { --depth; if (depth == 0) { ++end; break; } }
776
+ ++end;
777
+ }
778
+ } else {
779
+ while (end < s.size()) {
780
+ char ch = s[end];
781
+ if (ch == ',' || ch == '}' || ch == ' ' || ch == '\t' || ch == '\n') break;
782
+ ++end;
783
+ }
784
+ }
785
+ return s.substr(p, end - p);
786
+ }
787
+
788
+ double extractJsonDouble(const std::string& s, const std::string& key, double dflt = 0.0) {
789
+ std::string raw = extractJsonValueRaw(s, key);
790
+ if (raw.empty()) return dflt;
791
+ try { return std::stod(raw); } catch (...) { return dflt; }
792
+ }
793
+
794
+ std::string extractJsonStringDecoded(const std::string& s, const std::string& key) {
795
+ std::string raw = extractJsonValueRaw(s, key);
796
+ if (raw.size() < 2 || raw.front() != '"' || raw.back() != '"') return {};
797
+ std::string out; out.reserve(raw.size());
798
+ for (size_t i = 1; i + 1 < raw.size(); ++i) {
799
+ if (raw[i] == '\\' && i + 1 < raw.size() - 1) {
800
+ char nxt = raw[++i];
801
+ switch (nxt) {
802
+ case '"': out += '"'; break;
803
+ case '\\': out += '\\'; break;
804
+ case 'n': out += '\n'; break;
805
+ case 'r': out += '\r'; break;
806
+ case 't': out += '\t'; break;
807
+ case '/': out += '/'; break;
808
+ default: out += nxt; break;
809
+ }
810
+ } else out += raw[i];
811
+ }
812
+ return out;
813
+ }
814
+
815
+ std::string escapeForJsStringWv2(const std::string& s) {
816
+ std::string out; out.reserve(s.size() + 2);
817
+ for (char c : s) {
818
+ if (c == '"' || c == '\\') { out.push_back('\\'); out.push_back(c); }
819
+ else if (c == '\n') out += "\\n";
820
+ else if (c == '\r') out += "\\r";
821
+ else if (c == '\t') out += "\\t";
822
+ else out.push_back(c);
823
+ }
824
+ return out;
825
+ }
826
+
827
+ std::string buildResolveScriptWv2(const std::string& selector) {
828
+ // Frame-local rect + innerWidth/innerHeight for bilinear mapping when the
829
+ // frame is transformed (rotate/scale). Main frame uses iw/ih harmlessly.
830
+ std::string sel_lit = "\"" + escapeForJsStringWv2(selector) + "\"";
831
+ return
832
+ "(function(){"
833
+ "var el=document.querySelector(" + sel_lit + ");"
834
+ "if(!el)return{ok:false,code:\"not_found\"};"
835
+ "el.scrollIntoView({block:\"nearest\",inline:\"nearest\",behavior:\"instant\"});"
836
+ "var r=el.getBoundingClientRect();"
837
+ "var vis=r.width>0&&r.height>0&&r.bottom>0&&r.right>0"
838
+ "&&r.top<innerHeight&&r.left<innerWidth;"
839
+ "if(!vis)return{ok:false,code:\"not_visible\"};"
840
+ "return{ok:true,x:r.x,y:r.y,w:r.width,h:r.height,"
841
+ "cx:r.x+r.width/2,cy:r.y+r.height/2,"
842
+ "iw:innerWidth,ih:innerHeight};"
843
+ "})()";
844
+ }
845
+
846
+ void dispatchCdpClickWv2(ViewHost* v, double cx, double cy,
847
+ int32_t button, int32_t click_count, uint32_t modifiers) {
848
+ if (click_count < 1) click_count = 1;
849
+ const char* btn = cdpButtonNameWv2(button);
850
+ for (int i = 1; i <= click_count; ++i) {
851
+ std::string base = "\"x\":" + std::to_string(cx) + ",\"y\":" + std::to_string(cy) +
852
+ ",\"button\":\"" + btn + "\",\"clickCount\":" + std::to_string(i) +
853
+ ",\"modifiers\":" + std::to_string(modifiers);
854
+ cdpCall(v, L"Input.dispatchMouseEvent", "{\"type\":\"mousePressed\"," + base + "}");
855
+ cdpCall(v, L"Input.dispatchMouseEvent", "{\"type\":\"mouseReleased\"," + base + "}");
856
+ }
857
+ }
858
+
859
+ // CDP call routed to a child target session (flatten:true). Requires
860
+ // ICoreWebView2_11.
861
+ void cdpCallForSession(ViewHost* v, const std::string& session_id, const wchar_t* method,
862
+ const std::string& params_json,
863
+ std::function<void(bool, std::string)> cb) {
864
+ if (!v || !v->webview) { if (cb) cb(false, "view not ready"); return; }
865
+ ComPtr<ICoreWebView2_11> wv11;
866
+ if (FAILED(v->webview.As(&wv11)) || !wv11) { if (cb) cb(false, "ICoreWebView2_11 unavailable"); return; }
867
+ auto lifetime = g_runtime.lifetime;
868
+ wv11->CallDevToolsProtocolMethodForSession(
869
+ utf8ToWide(session_id).c_str(), method, utf8ToWide(params_json).c_str(),
870
+ Microsoft::WRL::Callback<ICoreWebView2CallDevToolsProtocolMethodCompletedHandler>(
871
+ [lifetime, cb](HRESULT hr, LPCWSTR result) -> HRESULT {
872
+ if (!lifetime || !lifetime->alive.load()) return S_OK;
873
+ if (FAILED(hr) || !result) { cb(false, "CDP-for-session call failed"); return S_OK; }
874
+ cb(true, wideToUtf8(result));
875
+ return S_OK;
876
+ }).Get());
877
+ }
878
+
879
+ // Register one-time listeners for Target.attachedToTarget / detachedFromTarget.
880
+ // Populates view->oopif_sessions as OOPIF child sessions attach.
881
+ void armOopifEvents(ViewHost* v) {
882
+ if (v->oopif_event_tokens_registered) return;
883
+ using namespace Microsoft::WRL;
884
+ ComPtr<ICoreWebView2DevToolsProtocolEventReceiver> attached_r, detached_r;
885
+ if (FAILED(v->webview->GetDevToolsProtocolEventReceiver(L"Target.attachedToTarget", &attached_r))
886
+ || FAILED(v->webview->GetDevToolsProtocolEventReceiver(L"Target.detachedFromTarget", &detached_r))) {
887
+ return;
888
+ }
889
+ uint32_t view_id = v->id;
890
+ attached_r->add_DevToolsProtocolEventReceived(
891
+ Callback<ICoreWebView2DevToolsProtocolEventReceivedEventHandler>(
892
+ [view_id](ICoreWebView2*, ICoreWebView2DevToolsProtocolEventReceivedEventArgs* args) -> HRESULT {
893
+ LPWSTR raw = nullptr;
894
+ if (FAILED(args->get_ParameterObjectAsJson(&raw)) || !raw) return S_OK;
895
+ std::string p = wideToUtf8(raw); CoTaskMemFree(raw);
896
+ std::string session_id = extractJsonStringDecoded(p, "sessionId");
897
+ std::string info = extractJsonValueRaw(p, "targetInfo");
898
+ std::string type = extractJsonStringDecoded(info, "type");
899
+ std::string target_id = extractJsonStringDecoded(info, "targetId");
900
+ if (session_id.empty() || target_id.empty() || type != "iframe") return S_OK;
901
+ auto* v = getView(view_id);
902
+ if (!v) return S_OK;
903
+ std::lock_guard<std::mutex> lk(v->oopif_sessions_mutex);
904
+ v->oopif_sessions[target_id] = session_id;
905
+ return S_OK;
906
+ }).Get(),
907
+ &v->target_attached_token);
908
+ detached_r->add_DevToolsProtocolEventReceived(
909
+ Callback<ICoreWebView2DevToolsProtocolEventReceivedEventHandler>(
910
+ [view_id](ICoreWebView2*, ICoreWebView2DevToolsProtocolEventReceivedEventArgs* args) -> HRESULT {
911
+ LPWSTR raw = nullptr;
912
+ if (FAILED(args->get_ParameterObjectAsJson(&raw)) || !raw) return S_OK;
913
+ std::string p = wideToUtf8(raw); CoTaskMemFree(raw);
914
+ std::string session_id = extractJsonStringDecoded(p, "sessionId");
915
+ if (session_id.empty()) return S_OK;
916
+ auto* v = getView(view_id);
917
+ if (!v) return S_OK;
918
+ std::lock_guard<std::mutex> lk(v->oopif_sessions_mutex);
919
+ for (auto it = v->oopif_sessions.begin(); it != v->oopif_sessions.end(); ) {
920
+ if (it->second == session_id) it = v->oopif_sessions.erase(it);
921
+ else ++it;
922
+ }
923
+ return S_OK;
924
+ }).Get(),
925
+ &v->target_detached_token);
926
+ v->oopif_event_tokens_registered = true;
927
+ }
928
+
929
+ void finishResolveAndClickWv2(uint32_t view_id, uint32_t request_id, double x, double y,
930
+ double w, double h, double cx, double cy,
931
+ int32_t button, int32_t click_count, uint32_t modifiers) {
932
+ ViewHost* v = getView(view_id);
933
+ if (!v || !v->webview) { emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "view destroyed"); return; }
934
+ dispatchCdpClickWv2(v, cx, cy, button, click_count, modifiers);
935
+ // Edge runtime CDP → DOM trust=true (empirical; matches existing click cap).
936
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
937
+ ",\"ok\":true,\"rect\":{\"x\":" + std::to_string(x) +
938
+ ",\"y\":" + std::to_string(y) +
939
+ ",\"width\":" + std::to_string(w) +
940
+ ",\"height\":" + std::to_string(h) + "},"
941
+ "\"isTrustedEvent\":true}";
942
+ emitWebviewEvent(view_id, "resolve-and-click-result", payload);
943
+ }
944
+
945
+ struct FrameResolveOkWv2 { double x, y, w, h, cx, cy, iw, ih; };
946
+
947
+ // Parse a Runtime.evaluate response (regardless of session); on success forwards
948
+ // frame-local fields to `onOk`. Script's failure branch routes through error emit.
949
+ void parseEvalAndContinueWv2(uint32_t view_id, uint32_t request_id, bool ok, const std::string& evalResult,
950
+ std::function<void(const FrameResolveOkWv2&)> onOk) {
951
+ if (!ok) {
952
+ BUNITE_INFO("webview2/eval: Runtime.evaluate failed view=%u request=%u body=%.300s%s",
953
+ view_id, request_id, evalResult.c_str(),
954
+ evalResult.size() > 300 ? "..." : "");
955
+ emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "Runtime.evaluate failed"); return;
956
+ }
957
+ if (evalResult.find("\"exceptionDetails\"") != std::string::npos) {
958
+ emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "evaluate threw"); return;
959
+ }
960
+ std::string value = extractJsonValueRaw(evalResult, "value");
961
+ if (value.empty()) { emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "evaluate returned no value"); return; }
962
+ std::string okRaw = extractJsonValueRaw(value, "ok");
963
+ if (okRaw != "true") {
964
+ std::string code = extractJsonStringDecoded(value, "code");
965
+ if (code.empty()) code = "runtime_error";
966
+ emitResolveAndClickErrorWv2(view_id, request_id, code.c_str(), "");
967
+ return;
968
+ }
969
+ onOk(FrameResolveOkWv2{
970
+ extractJsonDouble(value, "x"), extractJsonDouble(value, "y"),
971
+ extractJsonDouble(value, "w"), extractJsonDouble(value, "h"),
972
+ extractJsonDouble(value, "cx"), extractJsonDouble(value, "cy"),
973
+ extractJsonDouble(value, "iw"), extractJsonDouble(value, "ih"),
974
+ });
975
+ }
976
+
977
+ void evalInFrameWv2(uint32_t view_id, uint32_t request_id, const std::string& frameId,
978
+ const std::string& script,
979
+ std::function<void(const FrameResolveOkWv2&)> onOk) {
980
+ ViewHost* v = getView(view_id);
981
+ if (!v || !v->webview) { emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "view destroyed"); return; }
982
+ std::string session_id;
983
+ {
984
+ std::lock_guard<std::mutex> lk(v->oopif_sessions_mutex);
985
+ auto it = v->oopif_sessions.find(frameId);
986
+ if (it != v->oopif_sessions.end()) session_id = it->second;
987
+ }
988
+ if (!session_id.empty()) {
989
+ std::string evalParams = "{\"expression\":\"" + escapeJsonString(script) +
990
+ "\",\"returnByValue\":true,\"awaitPromise\":true}";
991
+ cdpCallForSession(v, session_id, L"Runtime.evaluate", evalParams,
992
+ [view_id, request_id, onOk](bool ok, std::string r) {
993
+ parseEvalAndContinueWv2(view_id, request_id, ok, r, onOk);
994
+ });
995
+ return;
996
+ }
997
+ // In-process: createIsolatedWorld + Runtime.evaluate via main session.
998
+ std::string isoParams = "{\"frameId\":\"" + escapeJsonString(frameId) + "\",\"worldName\":\"bunite-rac\"}";
999
+ cdpCallWithResult(v, L"Page.createIsolatedWorld", isoParams,
1000
+ [view_id, request_id, script, onOk](bool ok, std::string isoResult) {
1001
+ if (!ok) { emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "createIsolatedWorld failed"); return; }
1002
+ int contextId = 0;
1003
+ if (!extractJsonInt(isoResult, "executionContextId", contextId)) {
1004
+ emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "missing executionContextId"); return;
1005
+ }
1006
+ ViewHost* v2 = getView(view_id);
1007
+ if (!v2 || !v2->webview) { emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "view destroyed"); return; }
1008
+ std::string evalParams = "{\"expression\":\"" + escapeJsonString(script) +
1009
+ "\",\"contextId\":" + std::to_string(contextId) +
1010
+ ",\"returnByValue\":true,\"awaitPromise\":true}";
1011
+ cdpCallWithResult(v2, L"Runtime.evaluate", evalParams,
1012
+ [view_id, request_id, onOk](bool ok2, std::string r) {
1013
+ parseEvalAndContinueWv2(view_id, request_id, ok2, r, onOk);
1014
+ });
1015
+ });
1016
+ }
1017
+
1018
+ inline void bilinearMapWv2(const std::array<double, 8>& q, double iw, double ih,
1019
+ double fx, double fy, double& px, double& py) {
1020
+ const double u = (iw > 0) ? (fx / iw) : 0.0;
1021
+ const double v = (ih > 0) ? (fy / ih) : 0.0;
1022
+ px = (1-u)*(1-v)*q[0] + u*(1-v)*q[2] + u*v*q[4] + (1-u)*v*q[6];
1023
+ py = (1-u)*(1-v)*q[1] + u*(1-v)*q[3] + u*v*q[5] + (1-u)*v*q[7];
1024
+ }
1025
+
1026
+ // Recursive frame path lookup in Page.getFrameTree response (text-based parser).
1027
+ // Returns [main_frame_id, ..., target_frame_id]; empty if target not found.
1028
+ std::vector<std::string> findFramePathWv2(const std::string& node, const std::string& target) {
1029
+ std::string frame = extractJsonValueRaw(node, "frame");
1030
+ std::string this_id = extractJsonStringDecoded(frame, "id");
1031
+ if (this_id.empty()) return {};
1032
+ if (this_id == target) return {this_id};
1033
+ std::string children = extractJsonValueRaw(node, "childFrames");
1034
+ if (children.size() < 2 || children.front() != '[') return {};
1035
+ // Walk child array — each element is a JSON object {frame, childFrames?}.
1036
+ size_t pos = 1;
1037
+ while (pos < children.size() && children[pos] != ']') {
1038
+ while (pos < children.size() && (children[pos] == ' ' || children[pos] == ',')) ++pos;
1039
+ if (pos >= children.size() || children[pos] != '{') break;
1040
+ int depth = 0;
1041
+ size_t end = pos;
1042
+ bool inStr = false;
1043
+ while (end < children.size()) {
1044
+ char ch = children[end];
1045
+ if (inStr) { if (ch == '\\' && end + 1 < children.size()) ++end; else if (ch == '"') inStr = false; }
1046
+ else if (ch == '"') inStr = true;
1047
+ else if (ch == '{') ++depth;
1048
+ else if (ch == '}') { --depth; if (depth == 0) { ++end; break; } }
1049
+ ++end;
1050
+ }
1051
+ auto sub = findFramePathWv2(children.substr(pos, end - pos), target);
1052
+ if (!sub.empty()) { sub.insert(sub.begin(), this_id); return sub; }
1053
+ pos = end;
1054
+ }
1055
+ return {};
1056
+ }
1057
+
1058
+ bool parseQuad8(const std::string& content, std::array<double, 8>& out) {
1059
+ if (content.size() < 2 || content.front() != '[' || content.back() != ']') return false;
1060
+ size_t pos = 1;
1061
+ for (int i = 0; i < 8; ++i) {
1062
+ while (pos < content.size() && (content[pos] == ' ' || content[pos] == ',')) ++pos;
1063
+ size_t end = pos;
1064
+ while (end < content.size() && content[end] != ',' && content[end] != ']') ++end;
1065
+ if (end == pos) return false;
1066
+ try { out[i] = std::stod(content.substr(pos, end - pos)); } catch (...) { return false; }
1067
+ pos = end;
1068
+ }
1069
+ return true;
1070
+ }
1071
+
1072
+ // Issue CDP on a specific OOPIF session, or main session if `session_id` empty.
1073
+ void cdpForChain(ViewHost* v, const std::string& session_id, const wchar_t* method,
1074
+ const std::string& params_json,
1075
+ std::function<void(bool, std::string)> cb) {
1076
+ if (session_id.empty()) cdpCallWithResult(v, method, params_json, std::move(cb));
1077
+ else cdpCallForSession(v, session_id, method, params_json, std::move(cb));
1078
+ }
1079
+
1080
+ struct ChainStateWv2 {
1081
+ uint32_t view_id;
1082
+ uint32_t request_id;
1083
+ std::string targetFrameId;
1084
+ std::string script;
1085
+ int32_t button, click_count;
1086
+ uint32_t modifiers;
1087
+ std::vector<std::string> chain; // [main, ..., target]
1088
+ std::vector<std::array<double, 8>> link_quads;
1089
+ std::vector<std::pair<double, double>> ancestor_inner; // chain[1..N-2]'s iw/ih
1090
+ };
1091
+
1092
+ void composeAndDispatchWv2(std::shared_ptr<ChainStateWv2> s, const FrameResolveOkWv2& fr);
1093
+ void fetchTargetEvalWv2(std::shared_ptr<ChainStateWv2> s);
1094
+ void fetchAncestorInnerWv2(std::shared_ptr<ChainStateWv2> s, size_t i);
1095
+ void fetchLinkWv2(std::shared_ptr<ChainStateWv2> s, size_t link_idx);
1096
+
1097
+ std::string sessionForChainIdxWv2(uint32_t view_id, const std::vector<std::string>& chain, size_t idx) {
1098
+ if (idx == 0) return {};
1099
+ ViewHost* v = getView(view_id);
1100
+ if (!v) return {};
1101
+ std::lock_guard<std::mutex> lk(v->oopif_sessions_mutex);
1102
+ auto it = v->oopif_sessions.find(chain[idx]);
1103
+ return (it != v->oopif_sessions.end()) ? it->second : std::string{};
1104
+ }
1105
+
1106
+ void fetchLinkWv2(std::shared_ptr<ChainStateWv2> s, size_t link_idx) {
1107
+ if (link_idx + 1 >= s->chain.size()) { fetchAncestorInnerWv2(s, 1); return; }
1108
+ const std::string parent_session = sessionForChainIdxWv2(s->view_id, s->chain, link_idx);
1109
+ const std::string& child_frameId = s->chain[link_idx + 1];
1110
+ ViewHost* v = getView(s->view_id);
1111
+ if (!v) { emitResolveAndClickErrorWv2(s->view_id, s->request_id, "runtime_error", "view destroyed"); return; }
1112
+ std::string ownerParams = "{\"frameId\":\"" + escapeJsonString(child_frameId) + "\"}";
1113
+ cdpForChain(v, parent_session, L"DOM.getFrameOwner", ownerParams,
1114
+ [s, link_idx, parent_session](bool ok, std::string r) {
1115
+ if (!ok) { emitResolveAndClickErrorWv2(s->view_id, s->request_id, "not_found", "getFrameOwner failed"); return; }
1116
+ int backendNodeId = 0;
1117
+ if (!extractJsonInt(r, "backendNodeId", backendNodeId) || !backendNodeId) {
1118
+ emitResolveAndClickErrorWv2(s->view_id, s->request_id, "not_found", "no backendNodeId"); return;
1119
+ }
1120
+ ViewHost* v2 = getView(s->view_id);
1121
+ if (!v2) { emitResolveAndClickErrorWv2(s->view_id, s->request_id, "runtime_error", "view destroyed"); return; }
1122
+ std::string boxParams = "{\"backendNodeId\":" + std::to_string(backendNodeId) + "}";
1123
+ cdpForChain(v2, parent_session, L"DOM.getBoxModel", boxParams,
1124
+ [s, link_idx](bool ok2, std::string rb) {
1125
+ if (!ok2) { emitResolveAndClickErrorWv2(s->view_id, s->request_id, "not_visible", "iframe has no box"); return; }
1126
+ std::string model = extractJsonValueRaw(rb, "model");
1127
+ std::string content = extractJsonValueRaw(model, "content");
1128
+ std::array<double, 8> quad{};
1129
+ if (!parseQuad8(content, quad)) { emitResolveAndClickErrorWv2(s->view_id, s->request_id, "runtime_error", "bad quad"); return; }
1130
+ s->link_quads.push_back(quad);
1131
+ fetchLinkWv2(s, link_idx + 1);
1132
+ });
1133
+ });
1134
+ }
1135
+
1136
+ void fetchAncestorInnerWv2(std::shared_ptr<ChainStateWv2> s, size_t i) {
1137
+ if (i + 1 >= s->chain.size()) { fetchTargetEvalWv2(s); return; }
1138
+ const std::string sid = sessionForChainIdxWv2(s->view_id, s->chain, i);
1139
+ ViewHost* v = getView(s->view_id);
1140
+ if (!v) { emitResolveAndClickErrorWv2(s->view_id, s->request_id, "runtime_error", "view destroyed"); return; }
1141
+ std::string params = "{\"expression\":\"JSON.stringify({iw:innerWidth,ih:innerHeight})\",\"returnByValue\":true,\"awaitPromise\":true}";
1142
+ cdpForChain(v, sid, L"Runtime.evaluate", params,
1143
+ [s, i](bool ok, std::string r) {
1144
+ if (!ok) { emitResolveAndClickErrorWv2(s->view_id, s->request_id, "runtime_error", "ancestor eval failed"); return; }
1145
+ // Result: {"result":{"type":"string","value":"<json>"}}
1146
+ std::string value = extractJsonValueRaw(r, "value");
1147
+ if (value.size() < 2) { emitResolveAndClickErrorWv2(s->view_id, s->request_id, "runtime_error", "ancestor eval no value"); return; }
1148
+ // value is a JSON string literal — extractJsonValueRaw returns it with quotes; decode.
1149
+ std::string inner;
1150
+ for (size_t p = 1; p + 1 < value.size(); ++p) {
1151
+ if (value[p] == '\\' && p + 2 < value.size()) {
1152
+ char nxt = value[++p];
1153
+ switch (nxt) { case '"': inner += '"'; break; case '\\': inner += '\\'; break;
1154
+ case 'n': inner += '\n'; break; default: inner += nxt; }
1155
+ } else inner += value[p];
1156
+ }
1157
+ double iw = extractJsonDouble(inner, "iw"), ih = extractJsonDouble(inner, "ih");
1158
+ s->ancestor_inner.push_back({iw, ih});
1159
+ fetchAncestorInnerWv2(s, i + 1);
1160
+ });
1161
+ }
1162
+
1163
+ void fetchTargetEvalWv2(std::shared_ptr<ChainStateWv2> s) {
1164
+ evalInFrameWv2(s->view_id, s->request_id, s->targetFrameId, s->script,
1165
+ [s](const FrameResolveOkWv2& fr) { composeAndDispatchWv2(s, fr); });
1166
+ }
1167
+
1168
+ void composeAndDispatchWv2(std::shared_ptr<ChainStateWv2> s, const FrameResolveOkWv2& fr) {
1169
+ auto mapCorner = [&](double fx, double fy, double& px, double& py) {
1170
+ double cur_x = fx, cur_y = fy;
1171
+ double cur_iw = fr.iw, cur_ih = fr.ih;
1172
+ for (size_t i = s->link_quads.size(); i-- > 0; ) {
1173
+ double mx, my;
1174
+ bilinearMapWv2(s->link_quads[i], cur_iw, cur_ih, cur_x, cur_y, mx, my);
1175
+ cur_x = mx; cur_y = my;
1176
+ if (i == 0) break;
1177
+ cur_iw = s->ancestor_inner[i - 1].first;
1178
+ cur_ih = s->ancestor_inner[i - 1].second;
1179
+ }
1180
+ px = cur_x; py = cur_y;
1181
+ };
1182
+ double pcx, pcy; mapCorner(fr.cx, fr.cy, pcx, pcy);
1183
+ double cx0, cy0, cx1, cy1, cx2, cy2, cx3, cy3;
1184
+ mapCorner(fr.x, fr.y, cx0, cy0);
1185
+ mapCorner(fr.x + fr.w, fr.y, cx1, cy1);
1186
+ mapCorner(fr.x + fr.w, fr.y + fr.h, cx2, cy2);
1187
+ mapCorner(fr.x, fr.y + fr.h, cx3, cy3);
1188
+ const double min_x = std::min(std::min(cx0, cx1), std::min(cx2, cx3));
1189
+ const double max_x = std::max(std::max(cx0, cx1), std::max(cx2, cx3));
1190
+ const double min_y = std::min(std::min(cy0, cy1), std::min(cy2, cy3));
1191
+ const double max_y = std::max(std::max(cy0, cy1), std::max(cy2, cy3));
1192
+ finishResolveAndClickWv2(s->view_id, s->request_id,
1193
+ min_x, min_y, max_x - min_x, max_y - min_y, pcx, pcy,
1194
+ s->button, s->click_count, s->modifiers);
1195
+ }
1196
+
1197
+ // Walk ancestor chain via Page.getFrameTree, compose bilinear transforms across
1198
+ // nested OOPIF/same-origin frames, dispatch click in main-page coords.
1199
+ void runFrameTargetedWv2(uint32_t view_id, uint32_t request_id, const std::string& frameId,
1200
+ const std::string& script,
1201
+ int32_t button, int32_t click_count, uint32_t modifiers) {
1202
+ ViewHost* v = getView(view_id);
1203
+ if (!v || !v->webview) { emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "view destroyed"); return; }
1204
+ cdpCallWithResult(v, L"Page.getFrameTree", "{}",
1205
+ [view_id, request_id, frameId, script, button, click_count, modifiers](bool ok, std::string r) {
1206
+ if (!ok) { emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "getFrameTree failed"); return; }
1207
+ std::string root = extractJsonValueRaw(r, "frameTree");
1208
+ std::vector<std::string> chain = findFramePathWv2(root, frameId);
1209
+ if (chain.size() < 2) { emitResolveAndClickErrorWv2(view_id, request_id, "not_found", "frame not in tree"); return; }
1210
+ auto s = std::make_shared<ChainStateWv2>();
1211
+ s->view_id = view_id; s->request_id = request_id;
1212
+ s->targetFrameId = frameId; s->script = script;
1213
+ s->button = button; s->click_count = click_count; s->modifiers = modifiers;
1214
+ s->chain = std::move(chain);
1215
+ fetchLinkWv2(s, 0);
1216
+ });
1217
+ }
1218
+ } // namespace
1219
+
1220
+ extern "C" {
1221
+
1222
+ BUNITE_EXPORT void bunite_view_resolve_and_click(
1223
+ uint32_t view_id, uint32_t request_id,
1224
+ const char* selector_c, const char* frame_id_c,
1225
+ int32_t button, int32_t click_count, uint32_t modifiers) {
1226
+ ViewHost* v = getView(view_id);
1227
+ if (!v || !v->webview) { emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "view not ready"); return; }
1228
+ std::string selector = selector_c ? selector_c : "";
1229
+ std::string frameId = frame_id_c ? frame_id_c : "";
1230
+ std::string script = buildResolveScriptWv2(selector);
1231
+
1232
+ if (frameId.empty()) {
1233
+ std::string evalParams = "{\"expression\":\"" + escapeJsonString(script) + "\",\"returnByValue\":true,\"awaitPromise\":true}";
1234
+ cdpCallWithResult(v, L"Runtime.evaluate", evalParams,
1235
+ [view_id, request_id, button, click_count, modifiers](bool ok, std::string r) {
1236
+ parseEvalAndContinueWv2(view_id, request_id, ok, r,
1237
+ [view_id, request_id, button, click_count, modifiers](const FrameResolveOkWv2& fr) {
1238
+ finishResolveAndClickWv2(view_id, request_id,
1239
+ fr.x, fr.y, fr.w, fr.h, fr.cx, fr.cy,
1240
+ button, click_count, modifiers);
1241
+ });
1242
+ });
1243
+ return;
1244
+ }
1245
+
1246
+ // Frame-targeted: lazy event subscription + setAutoAttach so OOPIF child
1247
+ // sessions populate v->oopif_sessions before frame eval routes.
1248
+ armOopifEvents(v);
1249
+ if (!v->oopif_autoattach_armed.exchange(true)) {
1250
+ cdpCallWithResult(v, L"Target.setAutoAttach",
1251
+ "{\"autoAttach\":true,\"flatten\":true,\"waitForDebuggerOnStart\":false}",
1252
+ [view_id, request_id, frameId, script, button, click_count, modifiers](bool ok, std::string) {
1253
+ if (!ok) { emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "setAutoAttach failed"); return; }
1254
+ runFrameTargetedWv2(view_id, request_id, frameId, script, button, click_count, modifiers);
1255
+ });
1256
+ return;
1257
+ }
1258
+ runFrameTargetedWv2(view_id, request_id, frameId, script, button, click_count, modifiers);
1259
+ }
1260
+
1261
+ BUNITE_EXPORT void bunite_view_accessibility_snapshot(uint32_t view_id, uint32_t request_id,
1262
+ int32_t /*interesting_only*/) {
1263
+ // CDP getFullAXTree accepts only depth/frameId — interesting-only filter is TS-side.
1264
+ ViewHost* v = getView(view_id);
1265
+ if (!v || !v->webview) { emitAxError(view_id, request_id, "not_supported", "view not ready"); return; }
1266
+ cdpCallWithResult(v, L"Accessibility.getFullAXTree", "{}",
1267
+ [view_id, request_id](bool ok, std::string result) {
1268
+ if (!ok) { emitAxError(view_id, request_id, "runtime_error", "getFullAXTree failed: " + result); return; }
1269
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
1270
+ ",\"ok\":true,\"tree\":" + result + "}";
1271
+ emitWebviewEvent(view_id, "accessibility-result", payload);
1272
+ });
489
1273
  }
490
1274
 
491
1275
  BUNITE_EXPORT void bunite_view_screenshot(uint32_t view_id, uint32_t request_id,