Files
SteamFramelyHomebrew--framely/tests/browser_probe.cpp
T
toorux d4af27d63b Add in-headset review for app-requested APK updates
Intercept content, file and PackageInstaller requests in managed Lepton containers and route retained APKs through the normal review, backup and installation flow. Persist presentation receipts, retry delivery, and reopen repeated requests without duplicating packages.

Use authenticated localized update windows with visible laser cursors, and switch native Steam runtime aliases back to stable context names for installation. Verified all three request paths and a live Steam-runtime reinstall on Frame with backup creation and unchanged save data.
2026-10-24 22:05:47 +08:00

56 lines
22 KiB
C++

#include "../native/page_scroll_injection.h"
#include "include/capi/cef_load_handler_capi.h"
#include "../native/paint_buffer.h"
#include "../native/launcher_pointer.h"
// Integration test only: render the real manager and exercise its iframe bridge.
#include "include/capi/cef_app_capi.h"
#include "include/capi/cef_client_capi.h"
#include "include/cef_api_hash.h"
#include "../native/render_resolution.h"
#include "../native/keyboard_input.h"
#include <atomic>
#include <chrono>
#include <cstring>
#include <iostream>
#include <fstream>
#include <vector>
#include <thread>
template<class T> struct Handler {
T api{};std::atomic<int> refs{1};
Handler(){api.base.size=sizeof(T);api.base.add_ref=[](cef_base_ref_counted_t* p){++reinterpret_cast<Handler*>(p)->refs;};api.base.release=[](cef_base_ref_counted_t* p){--reinterpret_cast<Handler*>(p)->refs;return 0;};api.base.has_one_ref=[](cef_base_ref_counted_t* p){return int(reinterpret_cast<Handler*>(p)->refs==1);};api.base.has_at_least_one_ref=[](cef_base_ref_counted_t* p){return int(reinterpret_cast<Handler*>(p)->refs>0);};}
T* acquire(){api.base.add_ref(&api.base);return &api;}
};
static Handler<cef_client_t> client;
static Handler<cef_render_handler_t> render;
static Handler<cef_display_handler_t> display;
static Handler<cef_life_span_handler_t> life;
static Handler<cef_load_handler_t> load;
static std::vector<unsigned char> pixels;static int pixel_width=0,pixel_height=0;
static void snapshot(const std::string& name){const char* dir=std::getenv("FRAMELY_PREVIEW_DIR");if(!dir||pixels.empty())return;std::ofstream out(std::string(dir)+"/"+name+".ppm",std::ios::binary);out<<"P6\n"<<pixel_width<<" "<<pixel_height<<"\n255\n";for(size_t i=0;i<pixels.size();i+=4){unsigned char rgb[]={pixels[i+2],pixels[i+1],pixels[i]};out.write(reinterpret_cast<char*>(rgb),3);}}
static bool install_review_mode=std::getenv("FRAMELY_INSTALL_REVIEW_TEST")!=nullptr;
static bool launcher_mode=std::getenv("FRAMELY_LAUNCHER_TEST")!=nullptr;
static bool notification_mode=std::getenv("FRAMELY_NOTIFICATION_TEST")!=nullptr;
static LauncherPointer test_pointer;static bool press_pending=false;static int press_dx=0,press_dy=0,press_duration=0;
static bool ready=false,passed=false,failed=false,closed=false;static int paints=0;static bool scaled_paint=false,dpi_click=false,rounded_paint=false;
static void str(cef_string_t& s,const std::string& v){cef_string_utf8_to_utf16(v.data(),v.size(),&s);}
static std::string text(const cef_string_t* s){cef_string_utf8_t o{};cef_string_utf16_to_utf8(s->str,s->length,&o);std::string r(o.str,o.length);cef_string_utf8_clear(&o);return r;}
int main(int argc,char** argv){
load.api.on_load_end=[](cef_load_handler_t*,cef_browser_t*,cef_frame_t* frame,int){inject_page_scroll(frame);};
client.api.get_load_handler=[](cef_client_t*){return load.acquire();};
std::cout.setf(std::ios::unitbuf);std::string url=argc>1?argv[1]:"",runtime=argc>2?argv[2]:"",cef=std::getenv("FRAMELY_CEF_ROOT")?std::getenv("FRAMELY_CEF_ROOT"):"";
cef_main_args_t args{argc,argv};if(std::strcmp(cef_api_hash(CEF_API_VERSION,0),CEF_API_HASH_PLATFORM))return 2;int child=cef_execute_process(&args,nullptr,nullptr);if(child>=0)return child;if(url.empty()||runtime.empty()||cef.empty())return 2;
cef_settings_t settings{};settings.size=sizeof(settings);settings.windowless_rendering_enabled=1;str(settings.root_cache_path,runtime+"/cache");str(settings.log_file,runtime+"/cef.log");str(settings.resources_dir_path,cef+"/Resources");str(settings.locales_dir_path,cef+"/Resources/locales");
if(!cef_initialize(&args,&settings,nullptr,nullptr))return 3;
render.api.get_view_rect=[](cef_render_handler_t*,cef_browser_t*,cef_rect_t* r){*r=launcher_mode?cef_rect_t{0,0,1440,800}:std::getenv("FRAMELY_STORE_TEST")?cef_rect_t{0,0,1440,810}:cef_rect_t{0,0,std::getenv("FRAMELY_TEST_VIEW_WIDTH")?std::atoi(std::getenv("FRAMELY_TEST_VIEW_WIDTH")):600,notification_mode?360:840};};render.api.get_screen_info=[](cef_render_handler_t*,cef_browser_t*,cef_screen_info_t* info){if(launcher_mode)return render_screen_info(info,1440,800,1.5f);return render_screen_info(info,std::getenv("FRAMELY_STORE_TEST")?1440:(std::getenv("FRAMELY_TEST_VIEW_WIDTH")?std::atoi(std::getenv("FRAMELY_TEST_VIEW_WIDTH")):600),std::getenv("FRAMELY_STORE_TEST")?810:(notification_mode?360:840),std::getenv("FRAMELY_STORE_TEST")?4:2);};render.api.on_paint=[](cef_render_handler_t*,cef_browser_t*,cef_paint_element_type_t type,size_t dirty_count,const cef_rect_t* dirty,const void* data,int w,int h){if(type!=PET_VIEW)return;++paints;if(w==(launcher_mode?2160:std::getenv("FRAMELY_STORE_TEST")?5760:(std::getenv("FRAMELY_TEST_VIEW_WIDTH")?std::atoi(std::getenv("FRAMELY_TEST_VIEW_WIDTH"))*2:1200))&&h==(launcher_mode?1200:std::getenv("FRAMELY_STORE_TEST")?3240:(notification_mode?720:1680)))scaled_paint=true;pixel_width=w;pixel_height=h;merge_paint(pixels,data,w,h,dirty_count,dirty);// Semi-transparent charcoal panels still need a readable surface and clear rounded corners.
if(!notification_mode&&w>0&&h>0){auto alpha=[&](int x,int y){return pixels[(y*w+x)*4+3];};if(alpha(0,0)==0&&alpha(w-1,0)==0&&alpha(0,h-1)==0&&alpha(w-1,h-1)==0&&alpha(w/2,40)>200)rounded_paint=true;}};
display.api.on_console_message=[](cef_display_handler_t*,cef_browser_t* b,cef_log_severity_t,const cef_string_t* m,const cef_string_t*,int){auto s=text(m);std::cout<<s<<"\n";if(s.rfind("FRAMELY_PAGE_WHEEL_",0)==0){int x=0,y=0,dx=0,dy=0;if(std::sscanf(s.c_str(),"FRAMELY_PAGE_WHEEL_%d_%d_%d_%d",&x,&y,&dx,&dy)==4){auto* host=b->get_host(b);cef_mouse_event_t mouse{};mouse.x=x;mouse.y=y;host->send_mouse_move_event(host,&mouse,0);host->send_mouse_wheel_event(host,&mouse,dx,dy);host->base.release(&host->base);}}if(s.rfind("FRAMELY_NATIVE_KEYBOARD_",0)==0){auto* frame=b->get_focused_frame(b);auto command=s.substr(24);if(command=="TEXT")keyboard_edit(frame,"汉😃");else if(command=="BACKSPACE")keyboard_edit(frame,"\b");else if(command=="ENTER")keyboard_edit(frame,"\n");else if(command=="X")keyboard_edit(frame,"x");else if(command=="Y")keyboard_edit(frame,"y");else if(command=="LEFT")keyboard_edit(frame,"\x1b[D");else if(command=="RIGHT")keyboard_edit(frame,"\x1b[C");frame->base.release(&frame->base);}if(s=="FRAMELY_LAUNCHER_CANCEL_PRESS"){auto* pointer_host=b->get_host(b);cef_mouse_event_t outside{};outside.modifiers=EVENTFLAG_LEFT_MOUSE_BUTTON;pointer_host->send_mouse_move_event(pointer_host,&outside,0);outside.modifiers=0;pointer_host->send_mouse_click_event(pointer_host,&outside,MBT_LEFT,1,1);test_pointer.clear();press_pending=false;auto* frame=b->get_main_frame(b);cef_string_t code{},source{};str(code,"window.dispatchEvent(new Event('framely.launcher.cancelInput'))");frame->execute_java_script(frame,&code,&source,1);cef_string_utf16_clear(&code);frame->base.release(&frame->base);pointer_host->base.release(&pointer_host->base);}if(s.rfind("FRAMELY_LAUNCHER_DRAG_",0)==0){int x=0,y=0;if(press_pending&&std::sscanf(s.c_str(),"FRAMELY_LAUNCHER_DRAG_%d_%d",&x,&y)==2){press_dx=x-test_pointer.x;press_dy=y-test_pointer.y;}}if(s.rfind("FRAMELY_LAUNCHER_PRESS_",0)==0){int x=0,y=0;if(std::sscanf(s.c_str(),"FRAMELY_LAUNCHER_PRESS_%d_%d_%d_%d_%d",&x,&y,&press_dx,&press_dy,&press_duration)==5){auto* pointer_host=b->get_host(b);test_pointer.begin(x,y,std::chrono::steady_clock::now());press_pending=true;cef_mouse_event_t mouse{};mouse.x=x;mouse.y=y;pointer_host->set_focus(pointer_host,1);pointer_host->send_mouse_move_event(pointer_host,&mouse,0);mouse.modifiers=EVENTFLAG_LEFT_MOUSE_BUTTON;pointer_host->send_mouse_click_event(pointer_host,&mouse,MBT_LEFT,0,1);pointer_host->base.release(&pointer_host->base);}}if(s.rfind("FRAMELY_LAUNCHER_HOVER_",0)==0){int x=0,y=0;if(std::sscanf(s.c_str(),"FRAMELY_LAUNCHER_HOVER_%d_%d",&x,&y)==2){auto* hover_host=b->get_host(b);cef_mouse_event_t mouse{};mouse.x=x;mouse.y=y;hover_host->send_mouse_move_event(hover_host,&mouse,0);hover_host->base.release(&hover_host->base);}}if(s.rfind("FRAMELY_LAUNCHER_REGION_",0)==0){int x=0,y=0,w=0,h=0;if(std::sscanf(s.c_str(),"FRAMELY_LAUNCHER_REGION_%d_%d_%d_%d",&x,&y,&w,&h)==4){const float scale=launcher_mode?1.5f:4.f;int ink=0;for(int py=std::max(0,int(y*scale));py<std::min(pixel_height,int((y+h)*scale));py++)for(int px=std::max(0,int(x*scale));px<std::min(pixel_width,int((x+w)*scale));px++)if(pixels[(size_t(py)*pixel_width+px)*4+3]>128)ink++;if(ink<30){failed=true;std::cerr<<"FRAMELY_BRIDGE_FAIL missing painted control "<<x<<","<<y<<"\n";}}}if(s.rfind("FRAMELY_LAUNCHER_INK_",0)==0){int x=0,y=0,w=0,h=0;if(std::sscanf(s.c_str(),"FRAMELY_LAUNCHER_INK_%d_%d_%d_%d",&x,&y,&w,&h)==4){const float scale=launcher_mode?1.5f:4.f;int ink=0;for(int py=std::max(0,int(y*scale));py<std::min(pixel_height,int((y+h)*scale));py++)for(int px=std::max(0,int(x*scale));px<std::min(pixel_width,int((x+w)*scale));px++){size_t i=(size_t(py)*pixel_width+px)*4;if(pixels[i+3]>128&&pixels[i]>180&&pixels[i+1]>180&&pixels[i+2]>180)ink++;}if(ink<30){failed=true;std::cerr<<"FRAMELY_BRIDGE_FAIL missing painted label "<<x<<","<<y<<" ink="<<ink<<"\n";}}}
if(s=="FRAMELY_DPI_CLICK_REQUEST"){auto* host=b->get_host(b);cef_mouse_event_t mouse{};mouse.x=50;mouse.y=45;host->set_focus(host,1);host->send_mouse_move_event(host,&mouse,0);host->send_mouse_click_event(host,&mouse,MBT_LEFT,0,1);host->send_mouse_click_event(host,&mouse,MBT_LEFT,1,1);host->base.release(&host->base);}if(s=="FRAMELY_DPI_CLICK_PASS")dpi_click=true;if(s.rfind("FRAMELY_PREVIEW_INSTALL_",0)==0)snapshot(s.substr(16));if(s=="FRAMELY_PREVIEW_STORE")snapshot("store");if(s=="FRAMELY_PREVIEW_FAVORITES")snapshot("favorites");if(s=="FRAMELY_PREVIEW_INSTALLED")snapshot("installed");if(s=="FRAMELY_PREVIEW_SETTINGS")snapshot("settings");if(s=="FRAMELY_VIEW_READY")ready=true;if(s=="FRAMELY_BRIDGE_PASS")passed=true;if(s.rfind("FRAMELY_BRIDGE_FAIL",0)==0)failed=true;return 1;};life.api.on_before_close=[](cef_life_span_handler_t*,cef_browser_t*){closed=true;};
client.api.get_render_handler=[](cef_client_t*){return render.acquire();};client.api.get_display_handler=[](cef_client_t*){return display.acquire();};client.api.get_life_span_handler=[](cef_client_t*){return life.acquire();};
cef_window_info_t win{};win.size=sizeof(win);win.windowless_rendering_enabled=1;cef_browser_settings_t bs{};bs.size=sizeof(bs);bs.windowless_frame_rate=60;cef_string_t uri{};str(uri,url);auto* browser=cef_browser_host_create_browser_sync(&win,&client.api,&uri,&bs,nullptr,nullptr);cef_string_utf16_clear(&uri);if(!browser)return 4;auto* host=browser->get_host(browser);
auto end=std::chrono::steady_clock::now()+std::chrono::seconds(45);bool injected=false;
while(std::chrono::steady_clock::now()<end&&!passed&&!failed){cef_do_message_loop_work();if(press_pending){auto now=std::chrono::steady_clock::now();cef_mouse_event_t mouse{};mouse.x=test_pointer.x+press_dx;mouse.y=test_pointer.y+press_dy;test_pointer.constrain(mouse.x,mouse.y,now);mouse.modifiers=EVENTFLAG_LEFT_MOUSE_BUTTON;host->send_mouse_move_event(host,&mouse,0);if(now-test_pointer.pressed>=std::chrono::milliseconds(press_duration)){mouse.modifiers=0;host->send_mouse_click_event(host,&mouse,MBT_LEFT,1,1);test_pointer.clear();press_pending=false;}}if(ready&&!injected){auto* f=browser->get_main_frame(browser);cef_string_t code{},source{};str(code,install_review_mode?"window.runInstallReviewChecks()":notification_mode?R"JS((async()=>{const wait=ms=>new Promise(r=>setTimeout(r,ms));for(let i=0;i<100&&!document.querySelector('.toast');i++)await wait(30);if(innerWidth!==600||innerHeight!==360||devicePixelRatio!==2)throw Error('notification dimensions');const toast=document.querySelector('.toast');if(!toast?.textContent.includes('Toast integration'))throw Error('notification missing');if(toast.getBoundingClientRect().height>240)throw Error('notification too large');if(toast.querySelectorAll('.toast-actions button').length!==3)throw Error('actions missing');toast.querySelector('[aria-label="确认"]').click();await wait(150);document.querySelector('[aria-label="关闭通知"]').click();await wait(150);const status=await fetch('/api',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({method:'status',params:{}})}).then(r=>r.json());if(status.result.notifications.length)throw Error('notification not withdrawn');console.log('FRAMELY_BRIDGE_PASS');})().catch(e=>console.error('FRAMELY_BRIDGE_FAIL '+e)))JS":std::getenv("FRAMELY_REMOTE_TEST")?R"JS((async()=>{const wait=ms=>new Promise(r=>setTimeout(r,ms));await wait(300);const button=document.createElement('button');button.style.cssText='position:fixed;left:20px;top:20px;width:100px;height:50px;z-index:9999';button.onclick=()=>console.log('FRAMELY_DPI_CLICK_PASS');document.body.append(button);console.log('FRAMELY_DPI_CLICK_REQUEST');await wait(200);button.remove();for(let i=0;i<100;i++){const plugin=document.querySelector('.plugin-main');if(plugin){plugin.click();return;}await wait(50);}throw Error('remote plugin list timeout');})().catch(e=>console.error('FRAMELY_BRIDGE_FAIL '+e)))JS":std::getenv("FRAMELY_STORE_TEST")?R"JS((async()=>{const wait=ms=>new Promise(r=>setTimeout(r,ms));const until=async find=>{for(let i=0;i<160;i++){const value=find();if(value)return value;await wait(50);}throw Error("store timeout "+document.body.innerText);};await wait(300);if(innerWidth!==1440||innerHeight!==810||devicePixelRatio!==4)throw Error("manager layout or DPI mismatch");[...document.querySelectorAll("nav button")].find(b=>b.textContent==="插件源").click();await wait(200);if(!document.body.textContent.includes("Subscription fixture"))throw Error("subscription settings missing");const autoRefresh=[...document.querySelectorAll("button")].find(b=>b.textContent==="关闭自动刷新");if(!autoRefresh)throw Error("subscription automatic refresh missing");autoRefresh.click();await until(()=>[...document.querySelectorAll("button")].find(b=>b.textContent==="开启自动刷新"));[...document.querySelectorAll("nav button")].find(b=>b.textContent==="关于").click();await wait(150);const updates=document.querySelector(".system-updates");if(!updates||updates.querySelector("input:not([type=checkbox]),textarea"))throw Error("release configuration leaked into user settings");if(!updates.querySelector(".automatic-update-settings input[type=checkbox]")?.disabled)throw Error("automatic check controls enabled without source");if(!updates.textContent.includes("尚未配置更新服务"))throw Error("missing unconfigured update service message");const check=[...updates.querySelectorAll("button")].find(b=>b.textContent==="检查更新");if(!check?.disabled)throw Error("unconfigured check enabled");[...document.querySelectorAll("button")].find(b=>b.textContent==="查看协议与隐私声明").click();await until(()=>[...document.querySelectorAll("button")].find(b=>b.textContent==="拒绝协议"));document.querySelector('.language-picker button').click();const english=await until(()=>[...document.querySelectorAll('[role=option]')].find(b=>b.textContent==='English'));english.click();await until(()=>document.querySelector('h1')?.textContent==='Before using Framely');document.querySelector('.language-picker button').click();const chinese=await until(()=>[...document.querySelectorAll('[role=option]')].find(b=>b.textContent==='简体中文'));chinese.click();await until(()=>[...document.querySelectorAll('button')].find(b=>b.textContent==='拒绝协议'));[...document.querySelectorAll("button")].find(b=>b.textContent==="拒绝协议").click();await until(()=>document.body.textContent.includes("拒绝协议将停用全部插件"));[...document.querySelectorAll("button")].find(b=>b.textContent==="取消").click();await until(()=>[...document.querySelectorAll("button")].find(b=>b.textContent==="返回关于"));[...document.querySelectorAll("button")].find(b=>b.textContent==="返回关于").click();await until(()=>document.querySelector(".system-updates"));[...document.querySelectorAll("nav button")].find(b=>b.textContent.includes("插件库")).click();const card=await until(()=>document.querySelector(".store-card"));if(!card.textContent.includes("可更新"))throw Error("update not detected");if(card.querySelector(".plugin-avatar").getBoundingClientRect().width<64)throw Error("store icon not enlarged");if(document.querySelectorAll(".store-card").length!==1)throw Error("versions duplicated plugin cards");document.querySelector(".framely-select button").click();const tag=await until(()=>[...document.querySelectorAll("[role=option]")].find(b=>b.textContent==="工具"));if(!tag)throw Error("tag missing");tag.click();await wait(100);if(document.querySelector(".store-card .run-user-badge")?.textContent!=="root")throw Error("store run user missing");document.querySelector(".store-card").click();await wait(100);if(!document.body.textContent.includes("Store integration details"))throw Error("detail missing");if(!document.querySelector(".store-detail .run-user-notice.is-root"))throw Error("store root warning missing");console.log("FRAMELY_PREVIEW_STORE");document.querySelector(".detail-heading button").click();const approve=await until(()=>[...document.querySelectorAll("button")].find(b=>b.textContent==="确认安装"));if(document.querySelector(".modal h2")?.textContent!=="更新 Bridge")throw Error("manual install update title missing");if(document.querySelector(".install-review .install-account")?.textContent!=="steamos")throw Error("install must use package identity");if(document.body.textContent.includes("权限变化"))throw Error("unexpected privilege change");approve.click();await until(()=>!document.querySelector(".modal"));const status=await fetch("/api",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({method:"status",params:{}})}).then(r=>r.json());if(status.result.database.plugins["test.bridge"].manifest.version!=="2.0.0")throw Error("install not persisted");const chooseVersion=async v=>{document.querySelector('.store-detail .framely-select button').click();const option=await until(()=>[...document.querySelectorAll('[role=option]')].find(b=>b.textContent.split(' · ')[0]===v));option.click();await wait(100);if(document.querySelector('.store-detail .install-account')?.textContent!==(v==='2.0.0'?'root':'steamos'))throw Error('selected version user incorrect');document.querySelector('.detail-heading button').click();const approveVersion=await until(()=>[...document.querySelectorAll('button')].find(b=>b.textContent==='确认安装'));approveVersion.click();await until(()=>!document.querySelector('.modal'));const result=await fetch('/api',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({method:'status',params:{}})}).then(r=>r.json());if(result.result.database.plugins['test.bridge'].manifest.version!==v)throw Error('selected version not installed '+v);};await chooseVersion('1');await chooseVersion('2.0.0');console.log('FRAMELY_VERSION_SELECTION_PASS');[...document.querySelectorAll("nav button")].find(b=>b.textContent==="插件源").click();const removeSub=await until(()=>[...document.querySelectorAll("button")].find(b=>b.textContent==="删除订阅"));removeSub.click();const confirm=await until(()=>[...document.querySelectorAll(".modal button")].find(b=>b.textContent==="确认"));confirm.click();await until(()=>!document.body.textContent.includes("Subscription fixture"));console.log("FRAMELY_BRIDGE_PASS");})().catch(e=>console.error("FRAMELY_BRIDGE_FAIL "+e)))JS":R"JS((async()=>{const wait=ms=>new Promise(r=>setTimeout(r,ms));await wait(300);if(innerWidth!==600||innerHeight!==840||devicePixelRatio!==2)throw Error('quick layout or DPI changed');const dpiButton=document.createElement('button');dpiButton.style.cssText='position:fixed;left:20px;top:20px;width:100px;height:50px;z-index:9999';dpiButton.textContent='DPI test';let clicked=false;dpiButton.onclick=()=>{clicked=true;console.log('FRAMELY_DPI_CLICK_PASS');};document.body.append(dpiButton);console.log('FRAMELY_DPI_CLICK_REQUEST');await wait(200);dpiButton.remove();if(!clicked)throw Error('logical pointer coordinates failed');const originalFetch=window.fetch;let pulses=0;window.fetch=function(url,init){if(init?.body&&JSON.parse(init.body).method==='host.haptic')pulses++;return originalFetch.call(this,url,init);};const fixture=document.createElement('div');fixture.innerHTML='<button><span>A</span><span>B</span></button><button disabled><span>D</span></button><button>C</button>';document.body.append(fixture);const children=fixture.querySelectorAll('span'),buttons=fixture.querySelectorAll('button');const over=e=>e.dispatchEvent(new PointerEvent('pointerover',{bubbles:true}));const leave=(e,target)=>e.dispatchEvent(new PointerEvent('pointerout',{bubbles:true,relatedTarget:target}));over(children[0]);leave(children[0],children[1]);over(children[1]);leave(children[1],children[2]);over(children[2]);leave(children[2],buttons[2]);over(buttons[2]);await wait(100);fixture.remove();window.fetch=originalFetch;if(pulses!==2)throw Error('hover count='+pulses);console.log('FRAMELY_HOVER_PASS');console.log('FRAMELY_PREVIEW_FAVORITES');[...document.querySelectorAll('nav button')].find(b=>b.textContent==='设置').click();await wait(200);console.log('FRAMELY_PREVIEW_SETTINGS');if(!document.querySelector('.quick-settings').firstElementChild.classList.contains('setting-entry'))throw Error('manager entry is below language settings');document.querySelector('.setting-entry').click();await wait(100);[...document.querySelectorAll('nav button')].find(b=>b.textContent==='已安装').click();await wait(200);console.log('FRAMELY_PREVIEW_INSTALLED');for(let i=0;i<100;i++){await wait(50);const b=document.querySelector('.plugin-main');if(b){b.click();return;}}console.error('FRAMELY_BRIDGE_FAIL manager plugin list timeout');})().catch(e=>console.error('FRAMELY_BRIDGE_FAIL '+e)))JS");f->execute_java_script(f,&code,&source,1);cef_string_utf16_clear(&code);f->base.release(&f->base);injected=true;}std::this_thread::sleep_for(std::chrono::milliseconds(5));}
if(!passed&&!failed){auto* f=browser->get_main_frame(browser);cef_string_t diagnostic{},source{};str(diagnostic,"console.error('FRAMELY_TIMEOUT '+location.pathname.replace(/\\/boot\\/[^/]+/,'/boot/token')+' '+document.body.innerText.slice(0,400))");f->execute_java_script(f,&diagnostic,&source,1);cef_string_utf16_clear(&diagnostic);f->base.release(&f->base);for(int i=0;i<40;i++){cef_do_message_loop_work();std::this_thread::sleep_for(std::chrono::milliseconds(5));}snapshot("FAILURE");}
host->close_browser(host,1);for(int i=0;i<400&&!closed;i++){cef_do_message_loop_work();std::this_thread::sleep_for(std::chrono::milliseconds(5));}bool ok=passed&&!failed&&paints>0&&closed&&scaled_paint&&(notification_mode||std::getenv("FRAMELY_APK_UPDATE_TEST")||rounded_paint)&&(notification_mode||install_review_mode||std::getenv("FRAMELY_STORE_TEST")||dpi_click);std::cout<<"BROWSER_RESULT pass="<<ok<<" paints="<<paints<<" closed="<<closed<<"\n";if(!closed)_Exit(5);host->base.release(&host->base);browser->base.release(&browser->base);cef_shutdown();return ok?0:5;
}