#!/usr/bin/env python3 """Pruebas MCP del SO actual sobre el entorno privado desechable de visual-audit.py.""" import base64 import hashlib import json import sys from pathlib import Path import runpy import unittest ROOT = Path(__file__).resolve().parent.parent BASE = runpy.run_path(str(ROOT / 'scripts/workstation-dashboard-ui.test.py')) class CurrentOperatingSystemsTests(BASE['WorkstationDashboardTests']): @classmethod def setUpClass(cls): super().setUpClass() cls.output = cls.output.parent.parent / 'current-operating-systems' cls.output.mkdir(exist_ok=True) evidence = cls.output / "measurements.json" cls.evidence = json.loads(evidence.read_text()) if evidence.exists() else [] existing = cls.output / 'fixtures.json' if existing.exists(): cls.os = json.loads(existing.read_text()) cls.code('''const origin=__ORIGIN__;async function get(path){const r=await page.request.get(origin+'/api'+path);if(!r.ok())throw Error('GET '+path);return r.json()}const {csrfToken}=await get('/auth/session');async function post(path,data){const r=await page.request.post(origin+'/api'+path,{data,headers:{origin,'x-csrf-token':csrfToken,'X-Metis-Resources-Contract':'2'}});if(!r.ok())throw Error('POST '+path+': '+r.status());return r.json()}page.__osApi={get,post};return true;'''.replace('__ORIGIN__',json.dumps(cls.origin))) return cls.os = cls.code(''' const origin=__ORIGIN__,company=__COMPANY__; async function get(path){const r=await page.request.get(origin+'/api'+path);if(!r.ok())throw Error('GET '+path+': '+r.status());return r.json()} const {csrfToken}=await get('/auth/session'); async function post(path,data){const r=await page.request.post(origin+'/api'+path,{data,headers:{origin,'x-csrf-token':csrfToken,'X-Metis-Resources-Contract':'2'}});if(!r.ok())throw Error('POST '+path+': '+r.status());return r.json()} const catalog=(await get('/asset-catalog/'+company)).items; const rc=(await get('/resource-catalog/'+company)).items; const laptop=catalog.find(x=>x.builtinKey==='template:laptop'); const desktop=catalog.find(x=>x.builtinKey==='template:desktop'); const server=catalog.find(x=>x.builtinKey==='template:server'); const factory=catalog.find(x=>x.builtinKey==='field:factory_os').id; const custom=(await post('/asset-catalog/'+company,{values:{kind:'template',name:'OSUI — plantilla propia',description:'Solo datos sintéticos',fields:laptop.values.fields.map(x=>({fieldId:x.fieldId,required:false}))}})).items.find(x=>x.values.name==='OSUI — plantilla propia'); async function asset(name,template,original){return (await post('/assets/'+company,{values:{name,templateId:template.id,siteId:null,status:'operational',notes:'Prueba sintética de SO actual',characteristics:original?{[factory]:original}:{}}})).asset} async function resource(name,kind){return (await post('/resources/'+company,{values:{name,templateId:rc.find(x=>x.builtinKey==='template:'+kind).id,active:true,notes:'Prueba sintética',characteristics:{}}})).resource} const win=await resource('Windows 11 Pro','os'),ubuntu=await resource('Ubuntu 24.04 LTS — edición de ingeniería y documentación con un nombre extenso de prueba','os'); const application=await resource('OSUI Editor de documentos','application'); const main=await asset('OSUI — portátil con FreeDOS de fábrica',laptop,'FreeDOS'); const same=await asset('OSUI — sobremesa original Windows',desktop,'Windows 11 Pro'); const physicalServer=await asset('OSUI — servidor sin puesto',server); const own=await asset('OSUI — activo de plantilla propia',custom); const empty=await asset('OSUI — Windows de fábrica sin instalación',laptop,'Windows 11 Pro'); const pending=await asset('OSUI — pendiente',laptop); const removed=await asset('OSUI — retirado',laptop); const other=await asset('OSUI — segundo ordenador',laptop); async function install(a,r,state='configured',version='24H2'){return (await post('/software-installations/'+company,{values:{assetId:a.id,resourceId:r.id,licenseResourceId:null,version,state,notes:null}})).configuration} const current=await install(main,win); await install(main,application); for(const a of [same,physicalServer,own])await install(a,win); await install(pending,win,'pending'); let closed=await install(removed,win); for(const state of ['removal_pending','removed'])closed=(await post('/software-installations/'+company+'/'+closed.id+'/update',{values:{...closed.values,state}})).configuration; await install(other,ubuntu,'configured','24.04 LTS'); const station=(await post('/assets/'+company+'/workstations',{values:{name:'OSUI — puesto de comprobación',active:true,siteId:null,personId:null,computerAssetId:main.id,assetIds:[main.id]}})).workstation; page.__osApi={get,post}; return {main:main.id,same:same.id,server:physicalServer.id,custom:own.id,empty:empty.id,pending:pending.id,removed:removed.id,other:other.id,station,current,win,ubuntu,application}; '''.replace('__ORIGIN__',json.dumps(cls.origin)).replace('__COMPANY__',json.dumps(cls.fixture['companyId']))) (cls.output/'fixtures.json').write_text(json.dumps(cls.os,ensure_ascii=False,indent=2)+'\n') def capture(self,name): self.settle() encoded=self.code("return (await page.locator('[data-current-operating-systems]').locator('xpath=ancestor::section[1]').screenshot()).toString('base64');") data=base64.b64decode(encoded,validate=True) path=self.output/(name+'.png') path.write_bytes(data) self.record(name,{'path':str(path.relative_to(ROOT)),'sha256':hashlib.sha256(data).hexdigest(),'bytes':len(data),'scope':'current operating system card'}) def open_asset(self,key): self.tool('browser_navigate',{'url':self.origin+self.base+'/assets/'+self.os[key]}) self.code("await page.locator('[data-current-operating-systems]').waitFor();return true;") def ready(self,name='Windows 11 Pro'): self.code('await page.locator("[data-current-operating-systems]").getByRole("link",{name:'+json.dumps(name)+',exact:true}).waitFor({timeout:20000});return true;') def summary(self): return self.code("return await page.locator('[data-current-operating-systems]').innerText();") def test_os_01_source_and_states(self): for key in ['main','same','server','custom']: self.open_asset(key) self.ready() text=self.summary() self.assertIn('Windows 11 Pro',text) self.assertNotIn('FreeDOS',text) self.assertNotIn(self.os['application']['values']['name'],text) for key in ['empty','pending','removed']: self.open_asset(key) self.code("await page.locator('[data-current-operating-systems]').getByText('No registrado',{exact:true}).waitFor();return true;") self.assertNotIn('Windows 11 Pro',self.summary()) self.record('OSUI-01-05-07-12-14-17-18',{'source':'SoftwareInstallation','factoryIndependent':True,'serverWithoutWorkstation':True,'customTemplate':True}) def test_os_02_change_computer(self): self.code("await page.__osApi.post('/assets/'+"+json.dumps(self.fixture['companyId'])+"+'/workstations/'+"+json.dumps(self.os['station']['id'])+"+'/update',{values:"+json.dumps(self.os['station']['values'])+"});return true;") self.tool('browser_navigate',{'url':self.origin+self.base+'/workstations/'+self.os['station']['id']}) self.ready() result=self.code(''' const {get,post}=page.__osApi; const before=await get('/software-installations/'+__COMPANY__); const station=__STATION__,other=__OTHER__; await post('/assets/'+__COMPANY__+'/workstations/'+station.id+'/update',{values:{...station.values,computerAssetId:other,assetIds:[other]}}); return {before,after:await get('/software-installations/'+__COMPANY__)}; '''.replace('__COMPANY__',json.dumps(self.fixture['companyId'])).replace('__STATION__',json.dumps(self.os['station'])).replace('__OTHER__',json.dumps(self.os['other']))) self.assertEqual(result['before'],result['after']) self.code("await page.reload();return true;") self.ready(self.os['ubuntu']['values']['name']) self.assertNotIn('Windows 11 Pro',self.summary()) self.record('OSUI-08',{'newPrincipal':True,'installationsAndHistoryUnchanged':True}) def test_os_03_multiple_responsive_and_themes(self): self.open_asset('main') self.ready() self.code(''' const existing=await page.__osApi.get('/software-installations/'+__COMPANY__);if(!existing.configurations.some(x=>x.values.assetId===__ASSET__&&x.values.resourceId===__RESOURCE__))await page.__osApi.post('/software-installations/'+__COMPANY__,{values:{assetId:__ASSET__,resourceId:__RESOURCE__,licenseResourceId:null,version:'24.04 LTS',state:'configured',notes:null}}); await page.reload();return true; '''.replace('__COMPANY__',json.dumps(self.fixture['companyId'])).replace('__ASSET__',json.dumps(self.os['main'])).replace('__RESOURCE__',json.dumps(self.os['ubuntu']['id']))) for theme in ['light','dark']: for width,height in [(320,800),(390,844),(768,1024),(1024,768),(1280,900),(1600,1000)]: self.tool('browser_resize',{'width':width,'height':height}) self.open_asset('main') self.code("await page.evaluate(theme=>document.documentElement.classList.toggle('dark',theme==='dark'),"+json.dumps(theme)+");return true;") self.ready(self.os['ubuntu']['values']['name']) self.code("await page.locator('[data-current-operating-systems]').scrollIntoViewIfNeeded();return true;") self.ready(self.os['ubuntu']['values']['name']) measurements=self.code("return await page.locator('[data-current-operating-systems]').evaluate(e=>({viewport:innerWidth,overflow:e.scrollWidth>e.clientWidth,links:e.querySelectorAll('a').length,text:e.innerText,colors:[...e.querySelectorAll('a,p')].map(x=>({text:x.textContent,color:getComputedStyle(x).color,background:getComputedStyle(x.closest('[data-slot=card]')).backgroundColor}))}));") self.assertFalse(measurements['overflow']) self.assertEqual(measurements['links'],2) self.assertNotIn('Versión 24.04 LTS',measurements['text']) self.record(f'OSUI-06-{theme}-{width}',measurements) self.capture(f'asset-{theme}-{width}') keyboard=self.code("const link=page.locator('[data-current-operating-systems] a').first();await link.focus();await page.keyboard.press('Tab');return await page.locator('[data-current-operating-systems] a').last().evaluate(e=>({focused:e===document.activeElement,outline:getComputedStyle(e).outlineStyle}));") self.assertTrue(keyboard['focused']) self.assertEqual(keyboard['outline'],'solid') self.record('keyboard',keyboard) self.code("await page.evaluate(()=>{document.documentElement.classList.remove('dark');document.documentElement.style.zoom='2'});return true;") self.capture('asset-zoom-200') self.assertFalse(self.code("return await page.locator('[data-current-operating-systems]').evaluate(e=>e.scrollWidth>e.clientWidth);")) self.code("await page.evaluate(()=>document.documentElement.style.removeProperty('zoom'));return true;") def test_os_04_read_failures(self): self.open_asset('main') self.ready() path='**/api/software-installations/'+self.fixture['companyId'] self.addCleanup(self.code, 'await page.unroute('+json.dumps(path)+');return true;') self.code("await page.route("+json.dumps(path)+",route=>route.fulfill({status:500,json:{code:'INTERNAL_ERROR'}}));await page.reload();return true;") self.code("await page.locator('[data-current-operating-systems]').getByRole('button',{name:'Reintentar',exact:true}).waitFor({timeout:20000});return true;") self.assertNotIn('No registrado',self.summary()) self.capture('asset-error') self.code("await page.unroute("+json.dumps(path)+");await page.locator('[data-current-operating-systems]').getByRole('button',{name:'Reintentar',exact:true}).click();return true;") self.ready() refs='**/api/resource-application-references/**/resolve' self.addCleanup(self.code, 'await page.unroute('+json.dumps(refs)+');return true;') self.code("await page.route("+json.dumps(refs)+",async route=>{const r=await route.fetch();const data=await r.json();await route.fulfill({response:r,json:{...data,items:data.items.map(x=>({id:x.id,status:'unavailable'}))}})});await page.reload();return true;") self.code("await page.locator('[data-current-operating-systems]').getByText(/Referencia de software no disponible/).waitFor();return true;") self.assertNotIn('No registrado',self.summary()) self.capture('asset-unavailable-reference') self.code("await page.unroute("+json.dumps(refs)+");return true;") self.record('OSUI-10-11',{'errorIsNotEmpty':True,'retryRecovers':True,'unavailableReferenceIsNotEmpty':True}) def test_os_05_save_version_and_invalidation(self): self.open_asset('same') self.ready() self.code("await page.getByRole('region',{name:'Software del ordenador',exact:true}).getByRole('button',{name:/Editar/}).first().click();return true;") self.code("const dialog=page.getByRole('dialog');await dialog.getByLabel('Versión', {exact:true}).fill('25H2');await dialog.getByRole('button',{name:'Guardar',exact:true}).click();await dialog.waitFor({state:'hidden'});return true;") self.code("await page.locator('[data-current-operating-systems]').getByText('Versión 25H2',{exact:true}).waitFor();return true;") self.capture('saved-version') self.record('OSUI-09-15',{'savedThroughExistingForm':True,'summaryUpdatedWithoutReload':True}) def test_os_06_workstation_loading_language_and_contrast(self): self.code("await page.__osApi.post('/assets/'+"+json.dumps(self.fixture['companyId'])+"+'/workstations/'+"+json.dumps(self.os['station']['id'])+"+'/update',{values:"+json.dumps(self.os['station']['values'])+"});return true;") for theme in ['light','dark']: for width,height in [(320,800),(390,844),(768,1024),(1024,768),(1280,900),(1600,1000)]: self.tool('browser_resize',{'width':width,'height':height}) self.tool('browser_navigate',{'url':self.origin+self.base+'/workstations/'+self.os['station']['id']}) self.ready(self.os['ubuntu']['values']['name']) self.code("await page.evaluate(theme=>document.documentElement.classList.toggle('dark',theme==='dark'),"+json.dumps(theme)+");return true;") self.code("await page.locator('[data-current-operating-systems]').scrollIntoViewIfNeeded();return true;") self.ready(self.os['ubuntu']['values']['name']) measurements=self.code("return await page.locator('[data-current-operating-systems]').evaluate(e=>({viewport:innerWidth,overflow:e.scrollWidth>e.clientWidth,links:e.querySelectorAll('a').length,text:e.innerText}));") self.record(f'workstation-{theme}-{width}',measurements) self.assertFalse(measurements['overflow']) self.assertEqual(measurements['links'],2,measurements['text']) self.capture(f'workstation-{theme}-{width}') ratios=self.code("""return await page.locator('[data-current-operating-systems]').locator('xpath=ancestor::section[1]').evaluate(root=>{ const canvas=document.createElement('canvas');canvas.width=canvas.height=1;const ctx=canvas.getContext('2d',{willReadFrequently:true}); const rgba=color=>{ctx.clearRect(0,0,1,1);ctx.fillStyle=color;ctx.fillRect(0,0,1,1);return [...ctx.getImageData(0,0,1,1).data].map((x,i)=>i===3?x/255:x)}; const over=(a,b)=>{const alpha=a[3]+b[3]*(1-a[3]);return [...a.slice(0,3).map((v,i)=>(v*a[3]+b[i]*b[3]*(1-a[3]))/alpha),alpha]}; const bg=e=>e?over(rgba(getComputedStyle(e).backgroundColor),bg(e.parentElement)):[255,255,255,1]; const luminance=c=>c.slice(0,3).map(x=>x/255).map(x=>x<=.04045?x/12.92:((x+.055)/1.055)**2.4).reduce((a,x,i)=>a+x*[.2126,.7152,.0722][i],0); const ratio=(a,b)=>{const l=[luminance(a),luminance(b)].sort((a,b)=>b-a);return (l[0]+.05)/(l[1]+.05)}; return [...root.querySelectorAll('h2,a,p')].map(e=>({text:e.textContent,ratio:ratio(over(rgba(getComputedStyle(e).color),bg(e)),bg(e))})); });""") self.record('contrast-'+theme,ratios) for item in ratios: self.assertGreaterEqual(item['ratio'],4.5,item['text']) def test_os_07_loading_and_language(self): self.tool('browser_resize',{'width':1600,'height':1000}) self.tool('browser_navigate',{'url':self.origin+self.base+'/workstations/'+self.os['station']['id']}) self.ready(self.os['ubuntu']['values']['name']) self.code("await page.evaluate(()=>document.documentElement.classList.remove('dark'));return true;") path='**/api/software-installations/'+self.fixture['companyId'] result=self.code("await page.route("+json.dumps(path)+",route=>route.fulfill({status:503,json:{code:'INTERNAL_ERROR'}}));try{await page.reload();await page.locator('[data-current-operating-systems]').getByRole('status',{name:'Cargando sistema operativo actual',exact:true}).waitFor();return (await page.locator('[data-current-operating-systems]').locator('xpath=ancestor::section[1]').screenshot()).toString('base64')}finally{await page.unroute("+json.dumps(path)+")}") self.assertIsInstance(result,str,result) data=base64.b64decode(result,validate=True) screenshot=self.output/'workstation-loading.png' screenshot.write_bytes(data) self.record('workstation-loading',{'path':str(screenshot.relative_to(ROOT)),'sha256':hashlib.sha256(data).hexdigest(),'bytes':len(data)}) self.ready() self.code("await page.getByRole('button',{name:'Configuración',exact:true}).click();await page.getByRole('dialog').getByRole('combobox',{name:'Idioma',exact:true}).click();await page.getByRole('option',{name:'English',exact:true}).click();await page.getByRole('heading',{name:'Settings',exact:true}).waitFor();await page.keyboard.press('Escape');return true;") self.code("await page.getByRole('heading',{name:'Current operating systems',exact:true}).waitFor();return true;") self.capture('workstation-english') self.record('OSUI-16-browser',{'language':'en','loadingDistinctFromEmpty':True}) if __name__=='__main__': names=sys.argv[1:] or [name for name in dir(CurrentOperatingSystemsTests) if name.startswith('test_os_')] suite=unittest.TestSuite(CurrentOperatingSystemsTests(name) for name in names) result=unittest.TextTestRunner(verbosity=2).run(suite) raise SystemExit(not result.wasSuccessful())