108 行
9.9 KiB
Python
108 行
9.9 KiB
Python
"""Native caret/QWERTY fixture using an owned emulator without Inspector."""
|
|
import argparse,hashlib,importlib.util,json,math,pathlib,shutil,subprocess,time
|
|
APP=pathlib.Path(__file__).resolve().parents[1]
|
|
spec=importlib.util.spec_from_file_location('native',APP/'tests/native.py')
|
|
native=importlib.util.module_from_spec(spec);spec.loader.exec_module(native)
|
|
emu=native.emu;emu.APPS=['inputmethod']
|
|
|
|
PROBE='''<template><div><p>输入组件原生验收</p></div></template>
|
|
<script>
|
|
import router from '@system.router'
|
|
import tc from '@system.test'
|
|
import {activeInputs} from '../common/ime-lifecycle.js'
|
|
import {themeStyles} from '../common/ime-theme.js'
|
|
export default {started:false,onShow(){if(this.started)return;this.started=true;setTimeout(()=>{
|
|
tc.testcase('preview-qwerty',async()=>{
|
|
router.push({uri:'/keyboard',params:{initial:'不'}});await tc.wait(.5);
|
|
var page=router.getPages().slice(-1)[0],p=activeInputs().filter(input=>input.$page===page)[0];
|
|
function check(ok,label){if(!ok)throw new Error(label);console.log('PROBE_CHECK '+label)}
|
|
async function rect(id){return await p.$element(id).getBoundingClientRect()}
|
|
var cases=[['chinese','不',1,false],['phrase','你好',2,false],['ascii','watch',5,false],['mixed','ab你😀好',4,false],['start','你好',0,false],['middle','你好',1,false],['long','长😀'.repeat(30),30,false],['secret','私密😀',2,true]];
|
|
for(var theme of ['bluewhite','blackgreen']){
|
|
p.invalidate();p.inputTheme=theme;p.themeStyles=themeStyles(theme);
|
|
for(var item of cases){p.invalidate();p.mode='en';p.upper=false;p.secret=item[3];p.revealSecret=false;p.text=item[1];p.cursor=item[2];p.composition='';p.renderKeys();p.updateDisplay();await tc.wait(.15);
|
|
console.log('PROBE_CAPTURE '+JSON.stringify({name:theme+'-'+item[0],theme:theme,kind:'caret',before:await rect('ime-preview-before'),caret:await rect('ime-caret'),after:await rect('ime-preview-after'),preview:await rect('ime-preview')}));await tc.wait(1.1);}
|
|
for(var mode of ['py','en'])for(var upper of [false,true]){
|
|
p.invalidate();p.secret=false;p.text='';p.cursor=0;p.composition='';p.mode=mode;p.upper=upper;p.renderKeys();p.updateDisplay();await tc.wait(.15);
|
|
var alphabet='qwertyuiopasdfghjklzxcvbnm',keys=[];
|
|
for(var letter of alphabet)keys.push({key:letter,rect:await rect('ime-key-'+letter)});
|
|
console.log('PROBE_CAPTURE '+JSON.stringify({name:theme+'-'+mode+'-'+(upper?'upper':'lower'),theme:theme,kind:'qwerty',keys:keys}));await tc.wait(1.1);}
|
|
}
|
|
p.invalidate();p.secret=false;p.text='';p.cursor=0;p.composition='';p.mode='en';p.upper=false;p.renderKeys();p.updateDisplay();await tc.wait(.2);
|
|
await tc.getById('ime-key-q').click();await tc.getById('ime-english').click();await tc.wait(.15);check(p.upper,'uppercase enabled');await tc.getById('ime-key-q').click();await tc.getById('ime-english').click();await tc.wait(.15);check(!p.upper,'lowercase restored');await tc.getById('ime-key-q').click();check(p.text==='qQq','actual native keys commit qQq');
|
|
console.log('PROBE_DONE');});tc.run('preview-qwerty');},300)}}
|
|
</script>'''
|
|
|
|
def main():
|
|
parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--work',type=pathlib.Path,required=True);parser.add_argument('--output',type=pathlib.Path,required=True);parser.add_argument('--profiles',nargs='+',default=emu.PROFILES)
|
|
args=parser.parse_args();args.output.mkdir(parents=True,exist_ok=True);fixture=args.work/'fixture';fixture.mkdir(parents=True,exist_ok=True)
|
|
shutil.copytree(APP/'src',fixture/'src',dirs_exist_ok=True);shutil.copy2(APP/'glyphix.config.js',fixture/'glyphix.config.js')
|
|
metadata=json.loads((fixture/'src/manifest.json').read_text(encoding='utf-8'));metadata['router']['entry']='native-probe';metadata['router']['pages']['native-probe']={'component':'index'}
|
|
(fixture/'src/manifest.json').write_text(json.dumps(metadata,ensure_ascii=False),encoding='utf-8')
|
|
(fixture/'src/native-probe').mkdir(exist_ok=True);(fixture/'src/native-probe/index.ux').write_text(PROBE,encoding='utf-8')
|
|
source_hash=hashlib.sha256((APP/'src/components/CrabInput/index.ux').read_bytes()).hexdigest()
|
|
assert (fixture/'src/components/CrabInput/index.ux').read_bytes()==(APP/'src/components/CrabInput/index.ux').read_bytes()
|
|
shell=shutil.which('pwsh')or'powershell';stable=pathlib.Path(r'C:\Users\herry\AppData\Local\Programs\PowerShell\7\pwsh.exe')
|
|
if stable.exists():shell=str(stable)
|
|
with (args.output/'fixture-build.log').open('w',encoding='utf-8')as log:
|
|
result=subprocess.run([shell,'-NoProfile','-File',str(APP.parent/'build-rtos-game.ps1'),'-ProjectRoot',str(fixture)],stdout=log,stderr=subprocess.STDOUT)
|
|
assert result.returncode==0,'See fixture-build.log'
|
|
report={'method':'owned native console; production keyboard copied byte-for-byte into an auto-running fixture','production_keyboard_sha256':source_hash,'production_keyboard_path':'src/components/CrabInput/index.ux','production_route_sha256':hashlib.sha256((APP/'src/keyboard/index.ux').read_bytes()).hexdigest(),'inspector_used':False,'cases':[],'profiles':[],'passed':False}
|
|
def rectangle(value):
|
|
if isinstance(value,dict)and'rect'in value:value=value['rect']
|
|
return [value.get('left',value.get('x',0)),value.get('top',value.get('y',0)),value.get('width',value.get('w',0)),value.get('height',value.get('h',0))]
|
|
for profile in args.profiles:
|
|
image,artifacts=emu.prepare(profile,args.work)
|
|
device='generic-watch-'+('466x466'if profile.startswith('round')else'390x450');package=metadata['package']
|
|
binary=fixture/'.glyphix-work'/('dist/'+device+'-quickapp/'+package+'/bundle.pkg'if profile.endswith('-quickapp')else'image/'+device+'/apps/'+package+'.pkg')
|
|
shutil.copy2(binary,image/'apps'/(package+'.pkg'))
|
|
command=[str(emu.ROOT/'temp/glyphix-v0.7.2-windows-x64/bin/glyphix-emu.exe'),'--config','pkgs.db','--target',package]
|
|
if profile.startswith('round'):command+=['--width','624','--height','624','--dpi','326','--font-size','32','--round','--surface','surface/surface-466x466.json']
|
|
else:command+=['--width','390','--height','450','--dpi','321','--font-size','29']
|
|
command+=['--memory-profile','8192','--font-map','font-faces','--font-family','sans-serif']
|
|
log_path=args.output/(profile+'-console.log');seen=set();finished=False;captures=0
|
|
with log_path.open('w',encoding='utf-8')as log:
|
|
process=subprocess.Popen(command,cwd=image,stdin=subprocess.PIPE,stdout=log,stderr=subprocess.STDOUT,creationflags=subprocess.CREATE_NO_WINDOW)
|
|
try:
|
|
deadline=time.monotonic()+100
|
|
while time.monotonic()<deadline and process.poll()is None:
|
|
lines=log_path.read_text(encoding='utf-8',errors='replace').splitlines()
|
|
for line in lines:
|
|
if 'PROBE_CAPTURE 'in line:
|
|
try:state=json.JSONDecoder().raw_decode(line.split('PROBE_CAPTURE ',1)[1].lstrip())[0]
|
|
except json.JSONDecodeError:continue
|
|
if state['name']in seen:continue
|
|
seen.add(state['name']);state['profile']=profile
|
|
# The vendor console accepts relative screenshot names only.
|
|
filename=profile+'-'+state['name']+'.png';process.stdin.write(('screenshot '+filename+'\n').encode());process.stdin.flush()
|
|
if state['kind']=='caret':
|
|
before,caret,after,preview=[rectangle(state[key])for key in ['before','caret','after','preview']]
|
|
state.update(left_gap_px=caret[0]-before[0]-before[2],right_gap_px=after[0]-caret[0]-caret[2])
|
|
state['passed']=abs(state['left_gap_px'])<=1 and abs(state['right_gap_px'])<=1 and 0<caret[2]<=2 and preview[0]<=caret[0]and caret[0]+caret[2]<=preview[0]+preview[2]
|
|
else:
|
|
width=466 if profile.startswith('round')else 390;height=466 if profile.startswith('round')else 450;failures=[]
|
|
for key in state['keys']:
|
|
x,y,w,h=rectangle(key['rect'])
|
|
if w<=0 or h<=0 or x<0 or y<0 or x+w>width or y+h>height:failures.append(key['key'])
|
|
if profile.startswith('round')and max(math.hypot(tx-233,ty-233)for tx in [x,x+w]for ty in [y,y+h])>231:failures.append(key['key'])
|
|
state.update(failures=failures,passed=len(state['keys'])==26 and not failures)
|
|
captures+=1;report['cases'].append(state);native.save(args.output/'caret-results.json',report);print(json.dumps(state,ensure_ascii=False),flush=True)
|
|
if 'PROBE_DONE'in line:finished=True
|
|
if finished:break
|
|
time.sleep(.05)
|
|
assert finished and captures==24,'Native fixture did not finish; see '+str(log_path)
|
|
assert 'terminated:'not in log_path.read_text(encoding='utf-8',errors='replace'),'Native assertion failed'
|
|
for state in report['cases']:
|
|
if state['profile']!=profile:continue
|
|
filename=profile+'-'+state['name']+'.png';snapshot=image/filename
|
|
assert snapshot.is_file() and snapshot.read_bytes().startswith(b'\x89PNG\r\n\x1a\n'),'Screenshot missing: '+filename
|
|
shutil.copy2(snapshot,args.output/filename);state['screenshot']=filename
|
|
report['profiles'].append({'profile':profile,'native_case_switch_passed':True,'captures':captures,'fixture_binary_sha256':hashlib.sha256(binary.read_bytes()).hexdigest()})
|
|
finally:
|
|
if process.poll()is None:process.terminate()
|
|
process.wait(timeout=10);process.stdin.close()
|
|
report['passed']=all(row['passed']for row in report['cases']);native.save(args.output/'caret-results.json',report)
|
|
assert report['passed'],'See caret-results.json'
|
|
|
|
if __name__=='__main__':main()
|