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'])
|