/* Isolated mobile/tablet UI tests: all requests are mocked, no Hub data is changed. * BMC_PLAYWRIGHT_MODULE=/path/to/playwright node tests/browser/remote_control.cjs * BMC_REMOTE_BROWSER=webkit tests the Safari/WebKit engine. */ const assert = require('node:assert/strict'); const fs = require('node:fs'); const pw = require(process.env.BMC_PLAYWRIGHT_MODULE || 'playwright'); const engine = process.env.BMC_REMOTE_BROWSER || 'chromium'; (async () => { const browser = await pw[engine].launch(engine === 'chromium' ? {headless:true, executablePath:process.env.BMC_CHROME_EXECUTABLE || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'} : {headless:true}); try { for (const viewport of [{width:390,height:844}, {width:820,height:1180}]) { const context = await browser.newContext({viewport}); const page = await context.newPage(); const errors = [], writes = [], completed = new Map(); let disconnected = false, dropReply = false; const cases = [11,12].map(id => ({id,titel:'Testsag '+id,status:'Ny',deadline:null,deadline_state:'none'})); let state = {user_id:1,user_name:'Testbruger',version:0,timer_version:'a'.repeat(64),active_case:cases[0],desktop_connected:false, server_time:new Date().toISOString(), timer:{active:{active:false,elapsed:0},paused:[]},today_minutes:0,messages:{count:1,list:[{id:1,from:'Kollega',text:'Testbesked',is_unread:true}]}}; page.on('pageerror', e => errors.push(e.message)); await page.route('**/*', async route => { const req = route.request(), path = new URL(req.url()).pathname; if (path === '/remote') return route.fulfill({contentType:'text/html',body:fs.readFileSync('app/modules/remote/mobile.html','utf8')}); if (path.startsWith('/static/')) return route.fulfill({contentType:path.endsWith('.css')?'text/css':'application/javascript',body:fs.readFileSync('.'+path,'utf8')}); if (disconnected) return route.abort('internetdisconnected'); let result = {}; if (path.endsWith('/bootstrap')) result = {csrf:'test-csrf',status:'connect'}; if (path.endsWith('/connect')) {writes.push(req.postDataJSON());result={ok:true};} if (path.endsWith('/state')) result = state; if (path.endsWith('/telephony')) result = {allowed:true,incoming:[{call_id:'incoming-1',contact_name:'Indgående testkontakt',caller_number:'12345678',company_name:'Testfirma',ringing_phones:['Support']}]}; if (path.endsWith('/contacts')) result = {items:[{id:1,first_name:'Test',last_name:'Kontakt',mobile:'87654321',company_names:['Testfirma']}],has_more:false}; if (path.endsWith('/call')) {writes.push(req.postDataJSON());result={status:'ok',message:'Opkald startet på din VoIP-telefon'};} if (path.endsWith('/cases')) result = {items:cases,has_more:false}; if (path.includes('/command-result/')) result = {completed:completed.has(path.split('/').pop())}; if (path.endsWith('/command')) { const data = req.postDataJSON(); writes.push(data); if (!completed.has(data.request_id)) { state.version++; if (data.action === 'start') state.timer.active={active:true,sag_id:data.case_id,sag_navn:'Testsag '+data.case_id,time_entry_id:1,elapsed:10}; if (data.action === 'select') {state.active_case=cases.find(c=>c.id===data.case_id);if(data.switch_timer)state.timer.active.sag_id=data.case_id;} if (data.action === 'pause') {state.timer.paused=[{sag_navn:'Testsag',time_entry_id:1,elapsed_hhmmss:'00:00:10'}];state.timer.active.active=false;} if (data.action === 'resume') {state.timer.active.active=true;state.timer.paused=[];} if (data.action === 'stop') state.timer.active.active=false; if (data.action === 'manual') state.today_minutes+=data.minutes; completed.set(data.request_id,{ok:true}); } result=completed.get(data.request_id); if(dropReply){dropReply=false;return route.abort('internetdisconnected');} } return route.fulfill({contentType:'application/json',body:JSON.stringify(result)}); }); await page.goto('http://remote.test/remote'); await page.locator('#code').fill('000000000042'); await page.locator('#pin').fill('1234'); await page.getByRole('button',{name:'Forbind',exact:true}).click(); await page.locator('#controls').waitFor({state:'visible'}); assert.equal(writes[0].code,'000000000042'); assert.equal(await page.locator('#deadline').textContent(),'Ingen deadline'); assert.equal(await page.locator('#messages').textContent().then(s=>s.includes('Testbesked')),true); await page.getByRole('button',{name:'Start timer',exact:true}).evaluate(b=>{b.click();b.click();}); await page.waitForFunction(()=>document.querySelector('[data-action="pause"]').disabled===false); assert.equal(writes.filter(x=>x.action==='start').length,1); await page.getByText('Indgående testkontakt',{exact:true}).waitFor(); await page.locator('#contact-search').fill('Kontakt'); await page.getByRole('button',{name:'Søg kontakter',exact:true}).click(); await page.getByRole('button',{name:'Ring mobil · 87654321',exact:true}).evaluate(b=>{b.click();b.click();}); await page.getByText('Opkald startet på din VoIP-telefon',{exact:true}).waitFor(); assert.equal(writes.filter(x=>x.contact_id===1).length,1); await page.locator('.case').filter({hasText:'#12'}).click(); await page.getByRole('button',{name:'Kun skift sag',exact:true}).click(); await page.waitForFunction(()=>document.querySelector('#active-case').textContent.startsWith('#12')); assert.equal(state.timer.active.sag_id,11); await page.getByRole('button',{name:'Pause',exact:true}).click(); await page.getByRole('button',{name:'Genoptag',exact:true}).waitFor(); await page.getByRole('button',{name:'Genoptag',exact:true}).click(); await page.waitForFunction(()=>document.querySelector('[data-action="pause"]').disabled===false); await page.locator('#description').fill('Testarbejde'); await page.getByRole('button',{name:'Stop og gem tid',exact:true}).click(); await page.waitForFunction(()=>document.querySelector('[data-action="start"]').disabled===false); await page.locator('#description').fill('Manuel test');await page.locator('#minutes').fill('17'); dropReply=true; await page.getByRole('button',{name:'Gem tid med beskrivelsen ovenfor',exact:true}).click(); await page.locator('#uncertain').waitFor({state:'visible'}); await page.waitForFunction(()=>!document.querySelector('#check-command').disabled); await page.getByRole('button',{name:'Kontrollér resultat',exact:true}).click(); await page.locator('#uncertain').waitFor({state:'hidden'}); assert.equal(state.today_minutes,17); assert.equal(writes.filter(x=>x.action==='manual').length,1); disconnected=true; await page.waitForFunction(()=>document.querySelector('[data-action="start"]').disabled===true); const count=writes.length; await page.getByRole('button',{name:'Start timer',exact:true}).evaluate(b=>b.click()); assert.equal(writes.length,count); disconnected=false; await page.waitForFunction(()=>document.querySelector('[data-action="start"]').disabled===false); assert.equal(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),true); assert.deepEqual(errors,[]); await page.screenshot({path:`/tmp/bmc-remote-${engine}-${viewport.width}.png`,fullPage:true}); await context.close(); console.log(`${engine} ${viewport.width}px: login, timer, sagsvalg, manuel tid, tabt svar og forbindelse OK`); } // Normal Hub administration uses separate authenticated routes and clears codes. const context = await browser.newContext({viewport:{width:820,height:1180}}); const page = await context.newPage(); const errors = [], adminWrites = []; const admin = {pin_set:false,remotes:[],devices:[]}; let follower = null, seq = 0; await context.route('**/*', async route => { const req=route.request(), path=new URL(req.url()).pathname; if(path==='/remote/manage')return route.fulfill({contentType:'text/html',body:fs.readFileSync('app/modules/remote/manage.html','utf8')}); if(path==='/' || path.startsWith('/sag/'))return route.fulfill({contentType:'text/html',body:'