metis-desk/scripts/workplace-add-asset.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

159 lines
16 KiB
Python

#!/usr/bin/env python3
"""WD04.4: asociación real por HTTP y UI en metis_desk_test desechable mediante MCP."""
import json
from pathlib import Path
import runpy
import unittest
ROOT = Path(__file__).resolve().parent.parent
BASE = runpy.run_path(str(ROOT / 'scripts/workplace-visual-map.test.py'))
class WorkplaceAddAssetTests(BASE['WorkplaceVisualMapTests']):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.output = cls.output.parents[1] / 'wd044-add-asset'
cls.output.mkdir(parents=True, exist_ok=True)
evidence = cls.output / 'measurements.json'
cls.evidence = json.loads(evidence.read_text()) if evidence.exists() else []
cls.code("page.__wd044={};return true;")
@classmethod
def code(cls, body):
result = super().code("try {" + body + "} catch(error) { return {wd044Error: String(error.message).split(String.fromCharCode(10))[0]}; }")
if isinstance(result, dict) and 'wd044Error' in result:
raise RuntimeError(result['wd044Error'])
return result
def prepare(self, key='main'):
self.tool('browser_resize', {'width':1440,'height':900})
self.map_ready(key, software=key=='main')
# API real, autenticación ordinaria de la sesión privada del preparador.
self.code("""page.__wd044.get=async path=>{const r=await page.request.get(new URL('/api'+path,page.url()).href);if(!r.ok())throw Error('GET fixture '+r.status());return r.json()};
page.__wd044.post=async(path,data,revision)=>{const session=await page.__wd044.get('/auth/session');const r=await page.request.post(new URL('/api'+path,page.url()).href,{data,headers:{'x-csrf-token':session.csrfToken,'origin':new URL(page.url()).origin,...(revision?{'if-match':'"'+revision+'"'}:{})}});return {status:r.status(),body:await r.json()}};return true;""")
def open_picker(self, keyboard=False):
key = 'Space' if keyboard == 'space' else 'Enter'
opening = "await b.focus();await b.press(" + json.dumps(key) + ");" if keyboard else "await b.click();"
self.code("const b=page.getByRole('button',{name:'Añadir activo al puesto',exact:true});" + opening + "await page.getByRole('dialog',{name:'Añadir activo al puesto',exact:true}).waitFor();return true;")
def choose(self, name):
self.code("const d=page.getByRole('dialog',{name:'Añadir activo al puesto',exact:true});await d.locator('[data-quick-search]').fill("+json.dumps(name)+");await d.getByRole('radio',{name:"+json.dumps('Seleccionar '+name)+",exact:true}).press('Space');return true;")
def test_01_existing_cancel_conflict(self):
self.prepare()
data=self.code("""const cid="""+json.dumps(self.fixture['companyId'])+""";const inv=await page.__wd044.get('/assets/'+cid);const w=inv.workstations.find(x=>x.id==="""+json.dumps(self.fixture['main'])+""");const occupied=new Set(inv.workstations.filter(x=>x.values.active).flatMap(x=>x.values.assetIds));const candidate=inv.assets.find(x=>x.values.status==='operational'&&!occupied.has(x.id));page.__wd044.before=w;page.__wd044.candidate=candidate;return {name:candidate.values.name,previous:w.values.assetIds,principal:w.values.computerAssetId,history:w.history.length};""")
self.open_picker(keyboard=True)
self.assertTrue(self.code("return await page.getByRole('dialog',{name:'Añadir activo al puesto',exact:true}).locator('[data-quick-search]').evaluate(e=>e===document.activeElement);"))
self.assertEqual(self.code("return await page.getByRole('radio',{name:'Seleccionar PORT-ADM-01 · Documentación y gestión',exact:true}).count();"),0)
self.choose(data['name'])
self.code("await page.keyboard.press('Escape');return true;")
self.assertTrue(self.code("return await page.getByRole('button',{name:'Añadir activo al puesto',exact:true}).evaluate(e=>e===document.activeElement);"))
self.open_picker()
self.choose(data['name'])
# Otro usuario cambia el nombre mientras la selección permanece abierta.
conflict=self.code("const w=page.__wd044.before;return await page.__wd044.post('/assets/'+w.companyId+'/workstations/'+w.id+'/update',{values:{...w.values,name:'Administración — cambio concurrente WD04.4'}},w.history.at(-1).id);")
self.assertEqual(conflict['status'],200)
self.code("await page.getByRole('button',{name:'Añadir al puesto',exact:true}).click();await page.getByRole('alert').filter({hasText:'Otro usuario ha modificado'}).waitFor();return true;")
self.capture('S06-conflict')
self.code("await page.getByRole('button',{name:'Actualizar puesto y revisar selección',exact:true}).click();await page.getByRole('button',{name:'Añadir al puesto',exact:true}).waitFor({state:'visible'});return true;")
self.choose(data['name'])
self.code("await page.getByRole('button',{name:'Añadir al puesto',exact:true}).click();await page.getByRole('dialog',{name:'Añadir activo al puesto',exact:true}).waitFor({state:'hidden'});return true;")
result=self.code("const b=page.__wd044.before;const inv=await page.__wd044.get('/assets/'+b.companyId);const w=inv.workstations.find(x=>x.id===b.id);return {ids:w.values.assetIds,principal:w.values.computerAssetId,history:w.history.length,name:w.values.name,actionPersisted:w.values.assetIds.some(x=>x.includes('presentation:'))};")
self.assertEqual(len(result['ids']),len(data['previous'])+1)
self.assertTrue(set(data['previous']).issubset(result['ids']))
self.assertEqual(result['principal'],data['principal'])
self.assertEqual(result['history'],data['history']+2)
self.assertFalse(result['actionPersisted'])
self.record('S01-S03-S05-S06',result)
self.capture('S01-associated')
def test_02_responsive_empty_mobile(self):
self.prepare('empty')
for width,height in [(1600,1000),(1440,900),(1280,900),(1024,768),(768,1024),(390,844)]:
self.tool('browser_resize',{'width':width,'height':height})
self.settle()
self.open_picker(keyboard='space' if width == 1600 else True)
self.capture('picker-'+str(width))
geometry=self.code("return await page.evaluate(()=>({width:innerWidth,scrollWidth:document.documentElement.scrollWidth,searchFocused:document.activeElement.matches('[data-quick-search]'),dialogWidth:document.querySelector('[role=dialog]').getBoundingClientRect().width}));")
self.assertLessEqual(geometry['scrollWidth'],width)
self.assertTrue(geometry['searchFocused'])
self.record('responsive-'+str(width),geometry)
self.code("await page.keyboard.press('Escape');return true;")
self.assertTrue(self.code("return await page.getByRole('button',{name:'Añadir activo al puesto',exact:true}).evaluate(e=>e===document.activeElement);"))
# Asociación móvil en puesto sin principal, preservando null.
candidate=self.code("const inv=await page.__wd044.get('/assets/"+self.fixture['companyId']+"');const occupied=new Set(inv.workstations.filter(w=>w.values.active).flatMap(w=>w.values.assetIds));const a=inv.assets.find(a=>a.values.status==='operational'&&!occupied.has(a.id));return a.values.name;")
before_count=self.code("const inv=await page.__wd044.get('/assets/"+self.fixture['companyId']+"');return inv.workstations.find(w=>w.id==='"+self.fixture['empty']+"').values.assetIds.length;")
self.open_picker()
self.choose(candidate)
self.code("await page.getByRole('button',{name:'Añadir al puesto',exact:true}).click();await page.getByRole('dialog',{name:'Añadir activo al puesto',exact:true}).waitFor({state:'hidden'});return true;")
result=self.code("const inv=await page.__wd044.get('/assets/"+self.fixture['companyId']+"');const w=inv.workstations.find(w=>w.id==='"+self.fixture['empty']+"');return {principal:w.values.computerAssetId??null,count:w.values.assetIds.length};")
self.assertIsNone(result['principal'])
self.assertEqual(result['count'],before_count+1)
self.capture('S02-S10-mobile-associated')
def test_03_create_cancel_save_error_refresh(self):
self.prepare('withoutResponsible')
created_name=self.code("const inv=await page.__wd044.get('/assets/"+self.fixture['companyId']+"');page.__wd044.newName='MON-WD044 '+(inv.assets.filter(a=>a.values.name.startsWith('MON-WD044')).length+1);return page.__wd044.newName;")
self.open_picker()
self.code("const d=page.getByRole('dialog',{name:'Añadir activo al puesto',exact:true});await d.locator('[data-quick-search]').fill('WD044 selección conservada');await d.getByRole('button',{name:'Crear activo nuevo',exact:true}).click();await page.getByRole('dialog',{name:'Nuevo activo',exact:true}).waitFor();return true;")
self.capture('S04-create-form')
self.code("await page.keyboard.press('Escape');return true;")
self.assertEqual(self.code("return await page.getByRole('dialog',{name:'Añadir activo al puesto',exact:true}).locator('[data-quick-search]').inputValue();"),'WD044 selección conservada')
self.code("await page.getByRole('button',{name:'Crear activo nuevo',exact:true}).click();const d=page.getByRole('dialog',{name:'Nuevo activo',exact:true});await d.getByRole('combobox',{name:'Plantilla',exact:true}).click();await page.getByRole('option',{name:'Monitor',exact:true}).click();await d.getByRole('button',{name:'Siguiente',exact:true}).click();await d.getByRole('button',{name:'Siguiente',exact:true}).click();await d.getByRole('textbox',{name:'Nombre',exact:true}).fill(page.__wd044.newName);await d.getByRole('button',{name:'Guardar',exact:true}).click();await d.waitFor({state:'hidden'});return true;")
self.assertTrue(self.code("return await page.getByRole('radio',{name:'Seleccionar '+page.__wd044.newName,exact:true}).isChecked();"))
result=self.code("const inv=await page.__wd044.get('/assets/"+self.fixture['companyId']+"');const a=inv.assets.find(a=>a.values.name===page.__wd044.newName);page.__wd044.created=a;return {created:!!a,assigned:inv.workstations.some(w=>w.values.assetIds.includes(a.id))};")
self.assertTrue(result['created'])
self.assertFalse(result['assigned'])
self.capture('S04-created-selected')
self.code("await page.keyboard.press('Escape');return true;")
self.open_picker()
self.choose(created_name)
# Fallo de escritura: el activo ya creado permanece y se reintenta la misma asociación.
self.code("await page.route('**/workstations/*/update',route=>route.fulfill({status:500,json:{code:'INTERNAL_ERROR'}}));await page.getByRole('button',{name:'Añadir al puesto',exact:true}).click();await page.getByRole('dialog',{name:'Añadir activo al puesto',exact:true}).getByRole('alert').waitFor();return true;")
self.capture('S07-save-error')
self.code("await page.unroute('**/workstations/*/update');page.__wd044.writes=0;page.__wd044.failRead=false;await page.route('**/workstations/*/update',async route=>{page.__wd044.writes++;const response=await route.fetch();if(response.ok())page.__wd044.failRead=true;await route.fulfill({response})});await page.route('**/api/assets/"+self.fixture['companyId']+"',async route=>{if(page.__wd044.failRead)await route.fulfill({status:500,json:{code:'INTERNAL_ERROR'}});else await route.fallback()});await page.getByRole('button',{name:'Añadir al puesto',exact:true}).click();await page.getByRole('alert').filter({hasText:'El activo se ha asociado'}).waitFor();return true;")
self.capture('S07-saved-refresh-error')
self.code("page.__wd044.failRead=false;await page.getByRole('dialog',{name:'Añadir activo al puesto',exact:true}).getByRole('button',{name:'Reintentar',exact:true}).click();await page.getByRole('dialog',{name:'Añadir activo al puesto',exact:true}).waitFor({state:'hidden'});return true;")
self.assertEqual(self.code("return page.__wd044.writes;"),1)
self.code("await page.unroute('**/workstations/*/update');await page.unroute('**/api/assets/"+self.fixture['companyId']+"');return true;")
result=self.code("const inv=await page.__wd044.get('/assets/"+self.fixture['companyId']+"');return {assetStillExists:inv.assets.some(a=>a.id===page.__wd044.created.id),associated:inv.workstations.find(w=>w.id==='"+self.fixture['withoutResponsible']+"').values.assetIds.includes(page.__wd044.created.id)};")
self.assertTrue(result['assetStillExists'])
self.assertTrue(result['associated'])
self.record('S04-S05-S07',result)
def test_04_visual_limit_and_http_guards(self):
self.prepare('dense')
before=self.code("const inv=await page.__wd044.get('/assets/"+self.fixture['companyId']+"');const w=inv.workstations.find(w=>w.id==='"+self.fixture['dense']+"');const occupied=new Set(inv.workstations.filter(w=>w.values.active).flatMap(w=>w.values.assetIds));const a=inv.assets.find(a=>a.values.status==='operational'&&!occupied.has(a.id));page.__wd044.denseBefore=w;return {name:a.values.name,count:w.values.assetIds.length,principal:w.values.computerAssetId,history:w.history.length};")
self.open_picker()
self.choose(before['name'])
self.code("await page.getByRole('button',{name:'Añadir al puesto',exact:true}).click();await page.getByRole('dialog',{name:'Añadir activo al puesto',exact:true}).waitFor({state:'hidden'});return true;")
result=self.code("const b=page.__wd044.denseBefore;const inv=await page.__wd044.get('/assets/'+b.companyId);const w=inv.workstations.find(w=>w.id===b.id);page.__wd044.denseCurrent=w;return {count:w.values.assetIds.length,principal:w.values.computerAssetId,history:w.history.length,coverage:await page.locator('[data-workplace-coverage=equipment]').innerText(),realNodes:await page.locator('[data-workplace-map] article[data-workplace-equipment]').count(),actions:await page.locator('[data-workplace-add]').count()};")
self.assertEqual(result['count'],before['count']+1)
self.assertEqual(result['principal'],before['principal'])
self.assertEqual(result['history'],before['history']+1)
self.assertEqual(result['realNodes'],3)
self.assertEqual(result['actions'],1)
self.assertIn(str(before['count']+1),result['coverage'])
self.record('S08-limit-counters',result)
self.capture('S08-limit')
guards=self.code("""const w=page.__wd044.denseCurrent;const path='/assets/'+w.companyId+'/workstations/'+w.id+'/update';const duplicate=await page.__wd044.post(path,{values:{...w.values,assetIds:[...w.values.assetIds,w.values.assetIds[0]]}},w.history.at(-1).id);
const occupied=await page.__wd044.post(path,{values:{...w.values,assetIds:[...w.values.assetIds,"""+json.dumps(self.fixture['host'])+"""]}},w.history.at(-1).id);
const other=await page.__wd044.post('/assets/"""+self.fixture['otherCompany']+"""/workstations',{values:{name:'Aislamiento WD04.4',active:false,siteId:null,personId:null,computerAssetId:null,assetIds:[]}});
const foreign=await page.__wd044.post('/assets/"""+self.fixture['otherCompany']+"""/workstations/'+other.body.workstation.id+'/update',{values:{...other.body.workstation.values,assetIds:[w.values.assetIds[0]]}},other.body.workstation.history.at(-1).id);
const anonymousContext=await page.context().browser().newContext();let anonymous;try{const response=await anonymousContext.request.get(new URL('/api/assets/"""+self.fixture['companyId']+"""',page.url()).href);anonymous=response.status()}finally{await anonymousContext.close()};
return {duplicate:{status:duplicate.status,code:duplicate.body.code},occupied:{status:occupied.status,code:occupied.body.code},foreign:{status:foreign.status,code:foreign.body.code},anonymous};""")
self.assertEqual(guards['duplicate']['code'],'INVALID_MEMBERS')
self.assertEqual(guards['occupied']['code'],'ASSET_IN_WORKSTATION')
self.assertEqual(guards['foreign']['code'],'ASSET_NOT_FOUND')
self.assertEqual(guards['anonymous'],401)
self.record('HTTP-MariaDB-guards',guards)
if __name__ == '__main__':
unittest.main(defaultTest=['WorkplaceAddAssetTests.test_01_existing_cancel_conflict','WorkplaceAddAssetTests.test_02_responsive_empty_mobile','WorkplaceAddAssetTests.test_03_create_cancel_save_error_refresh','WorkplaceAddAssetTests.test_04_visual_limit_and_http_guards'])