metis-desk/scripts/current-operating-systems-ui.test.py
David 91f1d07e97 Add tests for current operating systems and workplace asset management
- Implement `current-operating-systems-ui.test.py` to validate the functionality of current operating systems in the UI, including tests for source and states, computer changes, responsive design, error handling, and version saving.
- Create `workplace-add-asset.test.py` to test the addition of assets to workstations, covering scenarios such as conflict resolution, responsive design on mobile, error handling during asset creation, and HTTP guards for asset management.
2026-10-25 13:46:06 +01:00

227 lines
19 KiB
Python

#!/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())