- 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.
227 lines
19 KiB
Python
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())
|