LazyBoy2/tests/desktop_controls_ui.mjs

110 lines
8.2 KiB
JavaScript
Raw Permalink Normal View History

2026-09-16 08:17:38 +00:00
import { chromium } from '../tools/playwright/node_modules/playwright/index.mjs';
import { readFileSync } from 'node:fs';
import assert from 'node:assert/strict';
const source = readFileSync(new URL('../crates/lazyboy-core/src/novnc_skin.rs', import.meta.url), 'utf8');
const skin = source.split('r###"')[1].split('"###;')[0];
const browser = await chromium.launch({headless:true});
try {
for (const mobile of [false,true]) {
const context=await browser.newContext({viewport:mobile?{width:390,height:740}:{width:1100,height:800},hasTouch:mobile,isMobile:mobile});
const page=await context.newPage();page.setDefaultTimeout(10000);
const errors=[];page.on('pageerror',e=>{errors.push(e.message);console.error(e.message)});
await page.route('http://desktop.test/**', route=>route.request().url().endsWith('/host.html')
? route.fulfill({contentType:'text/html',body:'<iframe src="/vnc.html" style="width:380px;height:600px"></iframe>'})
: route.request().url().endsWith('/core/rfb.js')
? route.fulfill({contentType:'application/javascript',body:'export default {messages:{extendedClipboardProvide(sock,formats,text){window.pastes.push(text[0])},extendedClipboardRequest(){}}};'})
: route.request().url().endsWith('/app/ui.js')
? route.fulfill({contentType:'application/javascript',body:`
const session = new EventTarget();
Object.assign(session, {_canvas:document.querySelector('canvas'),_screen:document.querySelector('#screen'),_display:{_fbWidth:1200,_fbHeight:800},_rfbConnectionState:'connected',_sendMouse(x,y,mask){window.pointerEvents.push({x,y,mask})},clipboardPasteFrom(text){window.pastes.push(text)},sendKey(...key){window.keys.push(key)}});
window.session=session; window.pastes=[];window.keys=[];window.pointerEvents=[];
export default {rfb:session,toggleVirtualKeyboard(){window.keyboardOpened=true}};`})
: route.fulfill({contentType:'text/html',body:`<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><style>#noVNC_container{border-bottom-right-radius:800px 600px}</style><body><div id="noVNC_container"><div id="screen" style="display:flex;width:100%;height:100%"><canvas width="1200" height="800"></canvas></div></div><textarea id="noVNC_clipboard_text" hidden></textarea>${skin}</body>`}));
await page.goto('http://desktop.test/vnc.html');
await page.waitForFunction(()=>window.session?.__lazyboyBound);
const bounds=()=>page.locator('canvas').boundingBox();
assert.equal(await page.locator("#noVNC_container").evaluate(el=>getComputedStyle(el).borderBottomRightRadius),"0px");
const initial=await bounds();assert.ok(Math.abs(initial.width/initial.height-1.5)<.01);
assert.equal(await page.locator('#lazyboy-tools').isVisible(),mobile);
if(mobile) await page.getByRole('button',{name:'放大',exact:true}).click();
else await page.keyboard.press('Control+Alt+Equal');
const zoomed=await bounds(); assert.ok(zoomed.width>initial.width*1.2);
const coords=await page.evaluate(()=>[session._display.absX(session._canvas.clientWidth/2),session._display.absY(session._canvas.clientHeight/2)]);
assert.deepEqual(coords,[600,400]);
if(mobile){
await page.getByRole('button',{name:'移動畫面'}).click();
assert.equal(await page.locator('#lazyboy-pan').isVisible(),true);
await page.getByRole('button',{name:'移動畫面'}).click();
}
if(mobile) await page.getByRole('button',{name:'剪貼簿 / 輸入'}).click();
else await page.keyboard.press('Control+Alt+KeyB');
const input=page.locator('#lazyboy-clipboard-text');
await input.fill('手機中文貼上 test');
await page.getByRole('button',{name:'貼到電腦',exact:true}).click();
await page.waitForTimeout(300);
await page.getByRole('button',{name:'貼到電腦',exact:true}).click();
await page.waitForTimeout(100);
assert.deepEqual(await page.evaluate(()=>pastes),['手機中文貼上 test','手機中文貼上 test']);
assert.equal(await page.evaluate(()=>keys.filter(k=>k[1]==='KeyV'&&k[2]).length),2);
await page.evaluate(()=>session.dispatchEvent(new CustomEvent('clipboard',{detail:{text:'電腦回傳 ✓'}})));
assert.equal(await input.inputValue(),'電腦回傳 ✓');
await page.getByRole('button',{name:'複製到本機'}).click();
assert.equal(await input.evaluate(el=>el.selectionEnd-el.selectionStart),'電腦回傳 ✓'.length);
await page.getByRole('button',{name:'關閉',exact:true}).click();
if(mobile) {
await page.getByRole('button',{name:'鍵盤',exact:true}).click();
assert.equal(await page.evaluate(()=>keyboardOpened),true);
await page.getByRole('button',{name:'Enter',exact:true}).click();
assert.ok(await page.evaluate(()=>keys.some(k=>k[1]==='Enter'&&k[2])));
const pad=page.locator('#lazyboy-trackpad');
const box=await pad.boundingBox(), x=box.x+box.width/2,y=box.y+box.height/2;
await page.mouse.move(x,y);await page.mouse.down();await page.mouse.move(x+30,y+10,{steps:4});await page.mouse.up();
assert.ok(await page.evaluate(()=>pointerEvents.some(e=>e.mask===0 && e.x>session._canvas.clientWidth/2)));
await page.getByRole('button',{name:'右鍵',exact:true}).click();
assert.deepEqual(await page.evaluate(()=>pointerEvents.slice(-2).map(e=>e.mask)),[4,0]);
await page.getByRole('button',{name:'拖曳',exact:true}).click();
await page.mouse.move(x,y);await page.mouse.down();await page.mouse.move(x-30,y,{steps:3});await page.mouse.up();
assert.equal(await page.evaluate(()=>pointerEvents.at(-1).mask),1);
await page.getByRole('button',{name:'放開拖曳',exact:true}).click();
assert.equal(await page.evaluate(()=>pointerEvents.at(-1).mask),0);
const cdp=await context.newCDPSession(page);
await cdp.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[{x:x-20,y},{x:x+20,y}]});
await cdp.send('Input.dispatchTouchEvent',{type:'touchMove',touchPoints:[{x:x-20,y:y-30},{x:x+20,y:y-30}]});
await cdp.send('Input.dispatchTouchEvent',{type:'touchEnd',touchPoints:[]});
assert.ok(await page.evaluate(()=>pointerEvents.some(e=>e.mask===16)),'two fingers scroll');
await page.evaluate(()=>pointerEvents=[]);
await cdp.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[{x:x-20,y},{x:x+20,y}]});
await cdp.send('Input.dispatchTouchEvent',{type:'touchEnd',touchPoints:[]});
assert.deepEqual(await page.evaluate(()=>pointerEvents.map(e=>e.mask)),[4,0],'two finger tap is only right click');
await page.getByRole('button',{name:'拖曳',exact:true}).click();
await page.evaluate(()=>window.dispatchEvent(new Event('blur')));
assert.equal(await page.evaluate(()=>pointerEvents.at(-1).mask),0,'blur releases held button');
await page.evaluate(()=>{pointerEvents=[];session._viewOnly=true});
await page.getByRole('button',{name:'左鍵',exact:true}).click();
assert.equal(await page.evaluate(()=>pointerEvents.length),0);
await page.evaluate(()=>session._viewOnly=false);
await page.screenshot({path:'.run/mobile-trackpad-portrait.png'});
await page.setViewportSize({width:844,height:390});
await page.waitForFunction(()=>document.documentElement.dataset.landscape==='true');
const panel=await page.locator('#lazyboy-tools').boundingBox(),screen=await page.locator('#noVNC_container').boundingBox();
assert.ok(panel.x+panel.width<=screen.x+1,'landscape controls belong on the left');
await page.screenshot({path:'.run/mobile-trackpad-landscape.png'});
}
await page.evaluate(()=>{session._clipboardServerCapabilitiesFormats={1:true};session._clipboardServerCapabilitiesActions={[1<<28]:true};});
if(mobile) await page.getByRole('button',{name:'剪貼簿 / 輸入'}).click();
else await page.keyboard.press('Control+Alt+KeyB');
await input.fill('UTF-8 中文 ✓');
await page.getByRole('button',{name:'貼到電腦',exact:true}).click();
assert.equal(await page.evaluate(()=>pastes.at(-1)),'UTF-8 中文 ✓');
assert.deepEqual(errors,[]);
console.log(`PASS ${mobile?'mobile':'desktop'} scaling, coordinates, repeated paste, remote clipboard, fallback and keyboard`);
if(!mobile){
await page.goto('http://desktop.test/host.html');
const inner=page.frameLocator('iframe');
await inner.locator('#lazyboy-tools').waitFor({state:'attached'});
assert.equal(await inner.locator('#lazyboy-tools').isVisible(),false,'narrow desktop iframe is not a phone');
}
await context.close();
}
} finally {await browser.close();}