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:''}) : 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:`
${skin}`})); 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();}