Files

112 lines
25 KiB
TypeScript

import React from 'react';import{createRoot}from'react-dom/client';
import{FrameFilePicker}from'../ui/src/file-picker';
import{ApkManager}from'../ui/src/apk-manager';import{configureLanguage,t}from'../ui/src/i18n';import{Brand,Icon}from'../ui/src/icons';import'../ui/src/style.css';
const wait=(ms=80)=>new Promise(r=>setTimeout(r,ms));const until=async(f:()=>unknown)=>{for(let i=0;i<150;i++){if(f())return;await wait(30);}throw Error('Timed out '+document.body.innerText.slice(-200)+' calls='+JSON.stringify(calls.slice(-4))+' lifecycle='+autoStopContainer+'/'+stopContainerOnClose+' disabled='+document.querySelector('[aria-label="Stop container after app exits"]')?.outerHTML);};
const meta=(pkg:string,name:string,launch=true)=>({package:pkg,name,version:'1.2.3',versionCode:123,minSdk:24,activities:launch?[`${pkg}.Main`]:[],declaredActivities:[`${pkg}.Main`],abis:['arm64-v8a'],vr:false});
const apps:any[]=[{id:'framely-wolvic/com.igalia.wolvic',context:'framely-wolvic',metadata:meta('com.igalia.wolvic','Wolvic'),steamLaunch:true,steamRegistered:true,installed:true,running:false,managed:true,size:128*1024*1024},{id:'dev/com.example.service',context:'dev',metadata:meta('com.example.service','Background service',false),installed:true,running:true,managed:false,size:20*1024*1024},{id:'old/com.example.notes',context:'old',metadata:meta('com.example.notes','Notes'),installed:false,running:false,managed:true,size:4*1024*1024}];
const containers=[{acceptsAdditionalApps:false,id:'framely-wolvic',name:'framely-wolvic',managed:true,running:false,baked:'/home/steamos/.local/share/lepton/contexts/framely-wolvic/baked'},{acceptsAdditionalApps:true,id:'dev',name:'dev',managed:false,running:true,baked:'/home/steamos/.local/share/lepton/contexts/dev/baked'},{id:'old',name:'old',managed:true,running:false,baked:'/home/steamos/.local/share/lepton/contexts/old/baked'}];
const calls:{method:string;params:any}[]=[];const jobs=new Map<string,any>();let uninstallError:string|null=null;let counter=0,fail=false,gamepadEnabled=false,gamepadTriggerThreshold=80,autoStopContainer=false,stopContainerOnClose=true,cleanupItems:any[]=[{id:'data:old/com.example.notes',kind:'data',name:'Notes',bytes:4*1024*1024,sensitive:true,app:'old/com.example.notes'},{id:'backups:backup1',kind:'backups',name:'20261005-backup',bytes:128*1024*1024,sensitive:true,context:'framely-wolvic',restorable:true},{id:'unknown:dev/com.example.orphan',kind:'unknown',name:'com.example.orphan',bytes:20,sensitive:true,unknown:true}];
window.fetch=async(url,init)=>{if(String(url).startsWith('/host/files/')){const p=JSON.parse(String(init?.body));calls.push({method:String(url),params:p});let result:any=true;if(String(url).endsWith('/list'))result={path:'/home/steamos/Downloads',parent:'/home/steamos',roots:[],entries:[{name:'device.apk',path:'/home/steamos/Downloads/device.apk',directory:false,size:1024}],total:1,offset:0,skipped:0};else if(String(url).endsWith('/apk-inspect')){const id='job-'+counter++;jobs.set(id,{id,kind:'apk',phase:'done',result:{ticket:'local-review-ticket',metadata:meta('com.example.local','Local APK'),bytes:1024},received:0});result={job:id};}return new Response(JSON.stringify({result}));}if(String(url).startsWith('/api/upload/')){const offset=+String(url).split('/').at(-1)!;return new Response(JSON.stringify({result:{received:offset+(init?.body as Blob).size}}));}const{method,params}=JSON.parse(String(init?.body));calls.push({method,params});let result:any=true;
if(method==='apk.upload.start')result={upload:'fixture-upload',chunkSize:512*1024};
else if(method==='apk.cleanup.list')result=cleanupItems;
else if(method==='diagnostics.export')result={name:'logs.zip',path:'/home/steamos/Downloads/logs.zip'};
else if(method==='apk.logs')result={name:'apk.log',text:'Installation complete\nAndroid package manager: Success'};
else if(method==='apk.list.start'||method==='apk.inspect.start'||method==='apk.operation.start'){
const id='job-'+counter++;let value:any=true;
if(method==='apk.list.start')value={apps:structuredClone(apps),containers,roots:[],warnings:[],available:true,gamepadEnabled,gamepadTriggerThreshold,autoStopContainer,stopContainerOnClose};
else if(method==='apk.inspect.start')value={ticket:'review-ticket',metadata:meta('com.example.new','New APK'),bytes:4};
else{if(params.operation==='lifecycle.settings'){autoStopContainer=params.autoStopContainer;stopContainerOnClose=params.stopContainerOnClose;}if(params.operation==='gamepad.settings'){gamepadEnabled=params.enabled;if(params.triggerThreshold!==undefined)gamepadTriggerThreshold=params.triggerThreshold;}const app=apps.find(a=>a.id===params.app);if(params.operation==='steam.settings')app.steamLaunch=params.enabled;if(params.operation==='settings'){if('steamName' in params)app.steamName=params.steamName.trim()||null;else Object.assign(app,{activity:params.activity,showWindow:params.showWindow});}if(params.operation==='uninstall'&&!uninstallError)app.installed=false;if(params.operation==='cleanup'){cleanupItems=cleanupItems.filter(i=>!params.items.includes(i.id));value={freedBytes:4*1024*1024,failed:[]};}}
jobs.set(id,{id,kind:'apk',phase:method==='apk.operation.start'&&(fail||(params.operation==='uninstall'&&uninstallError))?'failed':'done',result:value,error:uninstallError??'Fixture signature mismatch',received:0});result={job:id};
}else if(method==='job.status')result=jobs.get(params.job);
return new Response(JSON.stringify({result}));};
const root=createRoot(document.getElementById('root')!);function render(){root.render(<div className="app manager"><header className="shell-header"><Brand/><span>Framely</span></header><div className="workspace"><nav>{[['installed','已安装'],['download','插件库'],['download','APK 管理'],['settings','设置']].map(([icon,key])=><button className={key==='APK 管理'?'active':''} key={key}><Icon name={icon as any}/>{t(key)}</button>)}</nav><main className="content"><ApkManager/><FrameFilePicker/></main></div></div>);}
const button=(text:string,scope:ParentNode=document)=>Array.from(scope.querySelectorAll<HTMLButtonElement>('button')).find(b=>b.textContent?.trim()===text)??(()=>{throw Error('Missing button '+text)})();
async function snapshot(name:string){await wait(220);console.log('FRAMELY_PREVIEW_INSTALL_'+name);await wait(180);}
(async()=>{
configureLanguage('en-US',[]);render();await until(()=>document.querySelectorAll('.apk-row').length===2);const content=document.querySelector('.content')!.getBoundingClientRect(),manager=document.querySelector('.apk-manager')!.getBoundingClientRect();const padding=getComputedStyle(document.querySelector('.content')!);if(Math.abs(manager.width-(document.querySelector('.content')!.clientWidth-parseFloat(padding.paddingLeft)-parseFloat(padding.paddingRight)))>2)throw Error(`Manager width does not fill content: ${manager.width}/${content.width}, ${padding.paddingLeft}/${padding.paddingRight}`);const search=document.querySelector<HTMLInputElement>('.apk-filters input')!.getBoundingClientRect(),filter=document.querySelector<HTMLButtonElement>('.apk-filters .framely-select>button')!.getBoundingClientRect();if(window.innerWidth>700?(Math.abs(search.y-filter.y)>1||Math.abs(search.height-filter.height)>1||search.right>=filter.left):(filter.top<search.bottom||Math.abs(filter.width-search.width)>1))throw Error('APK search/filter controls are not aligned');await snapshot('APK_EN');
if(document.querySelector('[role=switch]'))throw Error('Global settings leaked onto app list');button('Settings',document.querySelector('.apk-tabs')!).click();await until(()=>!!document.querySelector('[role=switch][aria-label="Stop container after app exits"]'));
const automatic=()=>document.querySelector<HTMLButtonElement>('[role=switch][aria-label="Stop container after app exits"]')!;
const closePolicy=()=>document.querySelector<HTMLButtonElement>('[role=switch][aria-label="Stop container when closing its window"]')!;
if(automatic().getAttribute('aria-checked')!=='false'||closePolicy().getAttribute('aria-checked')!=='true')throw Error('Incorrect container lifecycle defaults');
automatic().click();await until(()=>automatic().getAttribute('aria-checked')==='true'&&!automatic().disabled);closePolicy().click();await until(()=>closePolicy().getAttribute('aria-checked')==='false'&&!closePolicy().disabled);
automatic().click();await until(()=>automatic().getAttribute('aria-checked')==='false'&&!automatic().disabled);closePolicy().click();await until(()=>closePolicy().getAttribute('aria-checked')==='true'&&!closePolicy().disabled);
const gamepad=()=>document.querySelector<HTMLButtonElement>('[role=switch][aria-label="Gamepad input"]')!;
if(gamepad().getAttribute('aria-checked')!=='false')throw Error('Gamepad input does not default off');
gamepad().click();await until(()=>gamepad().getAttribute('aria-checked')==='true'&&!gamepad().disabled);if(!gamepadEnabled)throw Error('Gamepad switch was not persisted');
gamepad().click();await until(()=>gamepad().getAttribute('aria-checked')==='false'&&!gamepad().disabled);if(gamepadEnabled)throw Error('Gamepad input did not turn off');
const trigger=()=>document.querySelector<HTMLInputElement>('input[type=range][aria-valuetext]')!;
if(trigger().value!=='80')throw Error('Trigger threshold default is not 80%');
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value')!.set!.call(trigger(),'65');trigger().dispatchEvent(new Event('input',{bubbles:true}));await until(()=>trigger().getAttribute('aria-valuetext')==='65%');
if(gamepadTriggerThreshold!==80)throw Error('Unsaved trigger threshold applied');
button('Save',document.querySelector('.apk-trigger-setting')!).click();await until(()=>gamepadTriggerThreshold===65&&!trigger().disabled&&button('Save',document.querySelector('.apk-trigger-setting')!).disabled);
if(trigger().value!=='65'||gamepadEnabled||!calls.some(c=>c.params.operation==='gamepad.settings'&&c.params.triggerThreshold===65))throw Error('Trigger setting was not persisted without enabling input');
// Container startup and shutdown are not data deletion operations.
button('Containers').click();await until(()=>document.querySelectorAll('.apk-container').length===3);
for(const title of ['Start container','Stop container']){button(title).click();await until(()=>!!document.querySelector('.apk-dialog'));if(document.querySelector('.apk-dialog')!.textContent!.includes(t('永久删除的数据无法撤销。')))throw Error('Container operation displayed data deletion warning');if(!button('Confirm',document.querySelector('.apk-dialog')!).classList.contains('primary'))throw Error('Non-destructive container confirmation uses destructive styling');button('Cancel',document.querySelector('.apk-dialog')!).click();await until(()=>!document.querySelector('.apk-dialog'));}
const external=document.querySelectorAll('.apk-container')[1];button('Delete container',external).click();await until(()=>document.querySelector('.apk-dialog'));if(!document.querySelector('.apk-dialog')!.textContent!.includes(t('容器数据、关联 APK 缓存和 Steam 库条目将永久删除,历史备份保留。')))throw Error('External deletion lacks data warning');button('Cancel',document.querySelector('.apk-dialog')!).click();await until(()=>!document.querySelector('.apk-dialog'));if(calls.some(c=>c.params.operation==='container.delete'))throw Error('Cancelled deletion modified external container');
button(t('应用')).click();await until(()=>document.querySelectorAll('.apk-row').length===2);
button('Manage',document.querySelector('.apk-row')!).click();await until(()=>document.querySelector('.apk-dialog'));const steamName=()=>document.querySelector<HTMLInputElement>('.apk-steam-name input')!;
if(!steamName()||steamName().placeholder!=='Wolvic')throw Error('Per-app Steam name setting missing');
for(const name of ['My Wolvic','']){
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value')!.set!.call(steamName(),name);steamName().dispatchEvent(new Event('input',{bubbles:true}));await wait();
button('Save',document.querySelector('.apk-steam-name')!).click();
await until(()=>calls.some(c=>c.params.operation==='settings'&&c.params.steamName===name));
await until(()=>!!steamName()&&!steamName().disabled);
if(steamName().value!==name||apps[0].metadata.name!=='Wolvic')throw Error('Custom name changed original APK name');
}
const advanced=document.querySelector<HTMLDetailsElement>('.apk-advanced')!;advanced.open=true;document.querySelector<HTMLButtonElement>('[role=switch][aria-label="Handle window display automatically"]')!.click();await until(()=>calls.some(c=>c.method==='apk.operation.start'&&c.params.operation==='settings'));await until(()=>!document.querySelector('progress'));await until(()=>document.querySelector('.apk-dialog')&&!button('Uninstall app',document.querySelector('.apk-dialog')!).disabled);
button('Uninstall app',document.querySelector('.apk-dialog')!).click();await until(()=>document.querySelector('[role=switch][aria-label="Delete app data"]'));
const purgeSwitch=()=>document.querySelector<HTMLButtonElement>('[aria-label="Delete app data"]')!;
const deleteSwitch=()=>document.querySelector<HTMLButtonElement>('[aria-label="Delete container"]')!;
if(purgeSwitch().getAttribute('aria-checked')!=='false'||deleteSwitch().getAttribute('aria-checked')!=='false'||!deleteSwitch().disabled)throw Error('Uninstall defaults to data deletion');
if(document.querySelector('.apk-dialog')!.textContent!.includes(t('永久删除的数据无法撤销。')))throw Error('Data-preserving uninstall warns of permanent data deletion');
await snapshot('APK_UNINSTALL_KEEP');purgeSwitch().click();await until(()=>deleteSwitch().getAttribute('aria-checked')==='true');
if(deleteSwitch().disabled)throw Error('Exclusive container deletion is disabled');await snapshot('APK_UNINSTALL_DELETE');
deleteSwitch().click();await until(()=>deleteSwitch().getAttribute('aria-checked')==='false');if(purgeSwitch().getAttribute('aria-checked')!=='true')throw Error('Container opt-out cleared purge');
purgeSwitch().click();await until(()=>!document.querySelector('.apk-dialog')!.textContent!.includes(t('永久删除的数据无法撤销。')));
button('Uninstall',document.querySelector('.apk-dialog')!).click();await until(()=>calls.some(c=>c.params.operation==='uninstall'));await until(()=>!document.querySelector('progress'));
const uninstall=calls.find(c=>c.params.operation==='uninstall')!.params;if(uninstall.purge||uninstall.deleteContainer)throw Error('Uninstall purged by default');await until(()=>Array.from(document.querySelectorAll('.apk-dialog button')).some(b=>b.textContent==='Done'));button('Done',document.querySelector('.apk-dialog')!).click();
button('Data cleanup').click();await until(()=>document.querySelectorAll('.apk-cleanup-row').length===3);if(document.querySelector('[role=switch][aria-checked=true]'))throw Error('Sensitive data selected by default');if(!document.querySelector<HTMLButtonElement>('[role=switch][aria-label*="com.example.orphan"]')!.disabled)throw Error('Unknown data can be deleted');await snapshot('APK_CLEANUP_EN');
document.querySelector<HTMLButtonElement>('[role=switch][aria-label="Select Notes"]')!.click();await until(()=>!button('Clean selected').disabled);button('Clean selected').click();await until(()=>document.querySelector('.apk-dialog'));button('Cancel',document.querySelector('.apk-dialog')!).click();if(calls.some(c=>c.params.operation==='cleanup'))throw Error('Cancel performed cleanup');
apps[0].installed=true;await wait();configureLanguage('zh-CN',[]);console.log('APK_LOCALE '+t('APK 管理'));root.render(<div/>);await wait();configureLanguage('zh-CN',[]);render();await until(()=>document.querySelector('.apk-manager h1')?.textContent==='APK 管理');await until(()=>!document.querySelector('.empty'));await snapshot('APK_ZH');button('数据清理').click();await until(()=>document.querySelectorAll('.apk-cleanup-row').length===3);await snapshot('APK_CLEANUP_ZH');button('应用').click();
const file=new File(['test'],'sample.apk',{type:'application/vnd.android.package-archive'}),transfer=new DataTransfer();transfer.items.add(file);const input=document.querySelector<HTMLInputElement>('input[type=file]')!;input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true}));await until(()=>document.querySelector('[aria-label="安装 APK"] .apk-meta'));if(calls.find(c=>c.method==='apk.upload.start')?.params.size!==4)throw Error('APK upload transport mismatch');await snapshot('APK_REVIEW_ZH');document.querySelector<HTMLButtonElement>('.apk-dialog button[aria-label="显示方式"]')!.click();await until(()=>document.querySelector('[role=listbox]'));button('VR 应用',document.querySelector('[role=listbox]')!).click();await until(()=>document.querySelector('.apk-dialog button[aria-label="显示方式"]')!.textContent?.includes('VR 应用'));if(document.querySelector('.apk-dialog button[aria-label="目标容器"]')||!document.querySelector('.apk-install-location')!.textContent!.includes(t('创建独立容器')))throw Error('New APK can select a shared container');button('取消',document.querySelector('.apk-dialog')!).click();await until(()=>calls.some(c=>c.method==='apk.review.drop'));if(calls.some(c=>c.params.operation==='install'))throw Error('Cancelled review installed APK');input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true}));await until(()=>document.querySelector('[aria-label="安装 APK"] .apk-meta'));document.querySelector<HTMLButtonElement>('.apk-dialog button[aria-label="显示方式"]')!.click();await until(()=>document.querySelector('[role=listbox]'));button('VR 应用',document.querySelector('[role=listbox]')!).click();await until(()=>document.querySelector('.apk-dialog button[aria-label="显示方式"]')!.textContent?.includes('VR 应用'));button('确认安装',document.querySelector('.apk-dialog')!).click();await until(()=>calls.some(c=>c.params.operation==='install'));if(calls.find(c=>c.params.operation==='install')!.params.showWindow!==false)throw Error('VR display mode was not submitted');await until(()=>!document.querySelector('.apk-dialog'));
configureLanguage('en-US',[]);apps.push({id:'unknown/com.example.unknown',context:'unknown',metadata:meta('com.example.unknown','Unknown app'),installed:false,stateKnown:false,managed:false,size:10,pending:'Container unavailable; data has not been deleted'},{id:'steam/com.example.steam',context:'steam',metadata:meta('com.example.steam','Steam app'),installed:true,steam:true,size:10});root.render(<div/>);await wait();render();await until(()=>[...document.querySelectorAll('.apk-row')].some(r=>r.textContent?.includes('Unknown app')));if([...document.querySelectorAll('.apk-row')].some(r=>r.textContent?.includes('Steam app')))throw Error('Steam app exposed in side-loading app list');const unknown=[...document.querySelectorAll('.apk-row')].find(r=>r.textContent?.includes('Unknown app'))!;if(!unknown.textContent?.includes(t('安装状态未知'))||[...unknown.querySelectorAll('button')].some(b=>b.textContent==='Open'))throw Error('Unknown state presented as launchable or uninstalled');button('Manage',unknown).click();await until(()=>document.querySelector('.apk-dialog'));if([...document.querySelectorAll('.apk-dialog button')].some(b=>b.textContent==='Reinstall APK'))throw Error('Unknown app offers reinstallation');if(!button(t('清除保留数据'),document.querySelector('.apk-dialog')!).disabled)throw Error('Unknown app can delete retained data');button('Done',document.querySelector('.apk-dialog')!).click();
// Frame-local selection cancels CEF's File result and starts inspection by path.
button('Install APK').click();window.dispatchEvent(new CustomEvent('framely.fileDialog',{detail:{id:'native-cancel',accept:['.apk'],multiple:false,directory:false}}));await until(()=>!!document.querySelector('.frame-files'));button('Cancel',document.querySelector('.frame-files')!).click();await until(()=>!document.querySelector('.frame-files'));if(calls.some(c=>c.method==='/host/files/apk-inspect'))throw Error('Cancelling local picker inspected APK');
const uploadsBefore=calls.filter(c=>c.method==='apk.upload.start').length;
button('Install APK').click();window.dispatchEvent(new CustomEvent('framely.fileDialog',{detail:{id:'native-local',accept:['.apk'],multiple:false,directory:false}}));
await until(()=>!!document.querySelector('.frame-file-row'));document.querySelector<HTMLButtonElement>('.frame-file-row')!.click();await until(()=>!document.querySelector<HTMLButtonElement>('.frame-files-confirm')!.disabled);button('Select',document.querySelector('.frame-files')!).click();
await until(()=>document.querySelector('.apk-dialog')?.textContent?.includes('Local APK'));
if(calls.filter(c=>c.method==='apk.upload.start').length!==uploadsBefore)throw Error('Frame local APK was uploaded');
const local=calls.find(c=>c.method==='/host/files/apk-inspect');if(local?.params.paths[0]!=='/home/steamos/Downloads/device.apk')throw Error('Local APK path not inspected');
if(calls.find(c=>c.method==='/host/files/complete'&&c.params.id==='native-local')?.params.paths.length!==0)throw Error('Local APK returned a browser File');
button('Cancel',document.querySelector('.apk-dialog')!).click();await until(()=>!document.querySelector('.apk-dialog'));
button('Manage',[...document.querySelectorAll('.apk-row')].find(r=>r.textContent?.includes('Wolvic'))!).click();await until(()=>document.querySelector('.apk-dialog'));
document.querySelector<HTMLDetailsElement>('.apk-advanced')!.open=true;
const steamSwitch=()=>document.querySelector<HTMLButtonElement>('[role=switch][aria-label="Launch through Steam"]')!;
if(steamSwitch().getAttribute('aria-checked')!=='true')throw Error('Steam launch must default on');
steamSwitch().click();await until(()=>steamSwitch().getAttribute('aria-checked')==='false'&&!steamSwitch().disabled);
steamSwitch().click();await until(()=>steamSwitch().getAttribute('aria-checked')==='true'&&!steamSwitch().disabled);
if(calls.filter(c=>c.method==='apk.operation.start'&&c.params.operation==='steam.settings').length!==2)throw Error('Steam launch switch did not use the APK operation API');
button('Done',document.querySelector('.apk-dialog')!).click();
// Shared containers include retained apps, so purge must never select container deletion.
apps.push({...apps[0],id:'framely-wolvic/com.example.other',metadata:meta('com.example.other','Other retained app'),installed:false});
root.render(<div/>);await wait();render();await until(()=>document.querySelectorAll('.apk-row').length>=3);
button('Manage',[...document.querySelectorAll('.apk-row')].find(r=>r.textContent?.includes('Wolvic'))!).click();await until(()=>document.querySelector('.apk-dialog'));button('Uninstall app',document.querySelector('.apk-dialog')!).click();await until(()=>!!document.querySelector('.apk-uninstall'));
purgeSwitch().click();await until(()=>purgeSwitch().getAttribute('aria-checked')==='true');if(!deleteSwitch().disabled||deleteSwitch().getAttribute('aria-checked')!=='false')throw Error('Shared container selected for deletion');button('Cancel',document.querySelector('.apk-dialog')!).click();await wait();button('Done',document.querySelector('.apk-dialog')!).click();apps.pop();
root.render(<div/>);await wait();render();await until(()=>document.querySelectorAll('.apk-row').length>=3);
button('Manage',[...document.querySelectorAll('.apk-row')].find(r=>r.textContent?.includes('Wolvic'))!).click();await until(()=>document.querySelector('.apk-dialog'));button('Uninstall app',document.querySelector('.apk-dialog')!).click();await until(()=>!!document.querySelector('.apk-uninstall'));purgeSwitch().click();await until(()=>deleteSwitch().getAttribute('aria-checked')==='true');
deleteSwitch().click();purgeSwitch().click();await until(()=>purgeSwitch().getAttribute('aria-checked')==='false');
uninstallError='Android PackageManager did not become ready; export logs and retry.';
button('Uninstall',document.querySelector('.apk-dialog')!).click();await until(()=>!!document.querySelector('.apk-uninstall .error'));
if(!apps[0].installed)throw Error('Failed uninstall removed app');
if(!document.querySelector('.apk-uninstall .error')?.textContent?.includes('PackageManager'))throw Error('Missing specific readiness error');
button('Export logs',document.querySelector('.apk-dialog')!).click();await until(()=>[...document.querySelectorAll('.apk-uninstall [role=status]')].some(e=>e.textContent?.includes('/home/steamos/Downloads/logs.zip')));
if(!calls.some(c=>c.method==='diagnostics.export'))throw Error('Uninstall diagnostics not exported');
button('Retry',document.querySelector('.apk-dialog')!).click();await wait();await until(()=>!document.querySelector('progress')&&!!document.querySelector('.apk-uninstall .error')&&!button('Retry',document.querySelector('.apk-dialog')!).disabled);
uninstallError=null;purgeSwitch().click();await until(()=>deleteSwitch().getAttribute('aria-checked')==='true');
button('Retry',document.querySelector('.apk-dialog')!).click();await until(()=>calls.some(c=>c.params.operation==='uninstall'&&c.params.deleteContainer));if(!calls.find(c=>c.params.operation==='uninstall'&&c.params.deleteContainer)!.params.purge)throw Error('Container deletion omitted purge');
console.log('FRAMELY_APK_MANAGER_PASS');console.log('FRAMELY_BRIDGE_PASS');
})().catch(e=>console.error('FRAMELY_BRIDGE_FAIL '+(e.stack??e)));