- 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.
159 lines
16 KiB
Python
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'])
|