242 lines
29 KiB
Python
242 lines
29 KiB
Python
|
|
"""Ficha SMB: HTTP real, volumen sintético, operaciones y evidencias mediante MCP."""
|
||
|
|
import json
|
||
|
|
import re
|
||
|
|
import subprocess
|
||
|
|
|
||
|
|
|
||
|
|
def verify(context, fixture, admin_password):
|
||
|
|
api, tool, browser, capture = context['api'], context['tool'], context['browser'], context['capture']
|
||
|
|
origin, company, directory = context['origin'], fixture['company'], context['directory']
|
||
|
|
api.request('/auth/login', {'email': fixture['email'], 'password': fixture['password']})
|
||
|
|
catalog = api.request('/resource-catalog/' + company)['items']
|
||
|
|
template = next(row for row in catalog if row.get('builtinKey') == 'template:shared_folder')
|
||
|
|
fields = {row['builtinKey']: row['id'] for row in catalog if row.get('builtinKey')}
|
||
|
|
address = '\\\\alias-corporativo\\Documentación administrativa y operaciones\\Área compartida con nombres largos'
|
||
|
|
resource = api.request('/resources/' + company, {'values': {
|
||
|
|
'name': 'General — Documentación administrativa y operaciones de la compañía',
|
||
|
|
'templateId': template['id'], 'active': True, 'notes': '',
|
||
|
|
'characteristics': {fields['field:unc']: address, fields['field:server']: 'alias-corporativo', fields['field:description']: 'Se mapea como unidad G en clientes; configuración documental por perfil.'}}}, expected=201)['resource']
|
||
|
|
resource_id = resource['id']
|
||
|
|
base = origin + '/companies/' + company + '/resources/' + resource_id
|
||
|
|
assets = api.request('/assets/' + company)['assets']
|
||
|
|
selected = [next(row for row in assets if row['id'] == identity) for identity in fixture['softwareAssets'][:2]]
|
||
|
|
screenshots, checks, measurements = [], [], []
|
||
|
|
|
||
|
|
def code(client, source):
|
||
|
|
result = context['result_json'](tool(client, 'browser_run_code_unsafe', {'code': 'async (page) => { page.setDefaultTimeout(15000); try { ' + source + ' return true; } catch(error) { return {failure: String(error.message)}; } }'}))
|
||
|
|
if isinstance(result, dict) and 'failure' in result:
|
||
|
|
raise RuntimeError(result['failure'])
|
||
|
|
return result
|
||
|
|
|
||
|
|
def contrast(client, theme):
|
||
|
|
result = code(client, """return await page.locator('[data-slot=network-share-detail]').evaluate(root=>{
|
||
|
|
const ctx=document.createElement('canvas').getContext('2d');
|
||
|
|
const lum=channels=>channels.slice(0,3).map(v=>{v/=255;return v<=.04045?v/12.92:((v+.055)/1.055)**2.4}).reduce((v,c,i)=>v+c*[.2126,.7152,.0722][i],0);
|
||
|
|
const values=[...root.querySelectorAll('p,h2,h3,dt,dd,time,code,summary,button,a')].filter(e=>e.textContent.trim()&&e.getClientRects().length&&!e.closest('button:disabled')).map(e=>{
|
||
|
|
ctx.clearRect(0,0,1,1);ctx.fillStyle='white';ctx.fillRect(0,0,1,1);
|
||
|
|
const chain=[];let current=e;while(current){chain.push(current);current=current.parentElement}
|
||
|
|
for(const parent of chain.reverse()){ctx.fillStyle=getComputedStyle(parent).backgroundColor;ctx.fillRect(0,0,1,1)}
|
||
|
|
const bg=lum([...ctx.getImageData(0,0,1,1).data]);const style=getComputedStyle(e);ctx.fillStyle=style.color;ctx.fillRect(0,0,1,1);const fg=lum([...ctx.getImageData(0,0,1,1).data]);
|
||
|
|
const ratio=(Math.max(bg,fg)+.05)/(Math.min(bg,fg)+.05);const large=parseFloat(style.fontSize)>=24||(parseFloat(style.fontSize)>=18.66&&parseInt(style.fontWeight)>=700);
|
||
|
|
return {tag:e.tagName,text:e.textContent.trim().slice(0,80),ratio,required:large?3:4.5};
|
||
|
|
});return {count:values.length,min:Math.min(...values.map(v=>v.ratio)),failures:values.filter(v=>v.ratio+.02<v.required)};
|
||
|
|
});""")
|
||
|
|
if result['failures']:
|
||
|
|
raise RuntimeError('Contraste de texto insuficiente: ' + json.dumps(result))
|
||
|
|
measurements.append({'contrast': theme, **result})
|
||
|
|
|
||
|
|
def controls_contrast(client, theme):
|
||
|
|
code(client, "await page.keyboard.press('Tab'); await page.getByRole('button',{name:'Cambiar activo',exact:true}).focus();")
|
||
|
|
result = code(client, """return await page.locator('[data-slot=network-share-detail]').evaluate(root=>{
|
||
|
|
const ctx=document.createElement('canvas').getContext('2d');
|
||
|
|
const lum=c=>c.slice(0,3).map(v=>{v/=255;return v<=.04045?v/12.92:((v+.055)/1.055)**2.4}).reduce((v,c,i)=>v+c*[.2126,.7152,.0722][i],0);
|
||
|
|
const contrast=(element,color)=>{ctx.clearRect(0,0,1,1);ctx.fillStyle='white';ctx.fillRect(0,0,1,1);let p=element;const chain=[];while(p){chain.push(p);p=p.parentElement}for(const n of chain.reverse()){ctx.fillStyle=getComputedStyle(n).backgroundColor;ctx.fillRect(0,0,1,1)}const bg=lum([...ctx.getImageData(0,0,1,1).data]);ctx.fillStyle=color;ctx.fillRect(0,0,1,1);const fg=lum([...ctx.getImageData(0,0,1,1).data]);return (Math.max(bg,fg)+.05)/(Math.min(bg,fg)+.05)};
|
||
|
|
const focus=document.activeElement;const style=getComputedStyle(focus);
|
||
|
|
const outline={kind:'focus-outline',ratio:contrast(focus.parentElement,style.outlineColor),width:style.outlineWidth,style:style.outlineStyle};
|
||
|
|
const icons=[...root.querySelectorAll('button:not(:disabled) svg')].filter(e=>e.getClientRects().length).map(e=>({kind:'interactive-icon',ratio:contrast(e,getComputedStyle(e).color)}));
|
||
|
|
return {outline,icons,failures:[outline,...icons].filter(x=>x.ratio+.02<3)};
|
||
|
|
});""")
|
||
|
|
if result['failures'] or result['outline']['style'] == 'none' or result['outline']['width'] == '0px':
|
||
|
|
raise RuntimeError('Contraste de controles/foco insuficiente: ' + json.dumps(result))
|
||
|
|
measurements.append({'nonTextContrast': theme, **result})
|
||
|
|
|
||
|
|
def open_share(client):
|
||
|
|
tool(client, 'browser_navigate', {'url': base})
|
||
|
|
code(client, "await page.locator('[data-slot=network-share-detail]').waitFor(); const panel=page.getByRole('complementary'); if(await panel.count())await panel.getByRole('button',{name:'Ficha e historial',exact:true}).click(); await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:" + "/" + re.escape(resource['values']['name']) + "/" + "})}).waitFor();")
|
||
|
|
|
||
|
|
def pick(client, name):
|
||
|
|
code(client, "await page.getByRole('button',{name:'Seleccionar activo publicador',exact:true}).click(); await page.getByRole('textbox',{name:'Buscar',exact:true}).fill(" + json.dumps(name) + "); await page.getByRole('radio',{name:" + json.dumps('Seleccionar: ' + name) + ",exact:true}).check(); await page.getByRole('button',{name:'Seleccionar',exact:true}).click();")
|
||
|
|
|
||
|
|
with browser(context['config']) as client:
|
||
|
|
try:
|
||
|
|
tool(client, 'browser_resize', {'width': 1440, 'height': 1000})
|
||
|
|
tool(client, 'browser_navigate', {'url': origin + '/login'})
|
||
|
|
code(client, "await page.getByRole('button',{name:'Entrar',exact:true}).waitFor();")
|
||
|
|
tool(client, 'browser_fill_form', {'fields': [
|
||
|
|
{'name': 'Correo electrónico', 'type': 'textbox', 'target': 'input[type=email]', 'value': fixture['email']},
|
||
|
|
{'name': 'Contraseña', 'type': 'textbox', 'target': 'input[type=password]', 'value': fixture['password']}]})
|
||
|
|
tool(client, 'browser_click', {'target': 'button[type=submit]'})
|
||
|
|
code(client, "await page.locator('[data-slot=workspace]').waitFor({timeout:30000});")
|
||
|
|
open_share(client)
|
||
|
|
code(client, "await page.getByText('Esta carpeta no tiene un activo publicador asociado',{exact:true}).waitFor(); await page.getByText('No hay publicaciones registradas.',{exact:true}).waitFor(); if(await page.getByText(/Correspondencia no verificada/).count()!==1)throw Error('Correspondencia no neutral');")
|
||
|
|
screenshots.append(capture(client, directory, 'share-detail-empty'))
|
||
|
|
checks.extend(['ordinary-user', 'empty-publication', 'empty-history', 'neutral-server-correspondence', 'no-fictional-consumption'])
|
||
|
|
code(client, "await page.getByRole('button',{name:'Asignar activo',exact:true}).focus(); await page.keyboard.press('Enter');")
|
||
|
|
pick(client, selected[0]['values']['name'])
|
||
|
|
code(client, "await page.getByRole('button',{name:'Confirmar',exact:true}).click(); await page.getByRole('button',{name:'Cambiar activo',exact:true}).waitFor();")
|
||
|
|
code(client, "await page.getByRole('button',{name:'Acciones',exact:true}).focus(); await page.keyboard.press('Enter'); await page.getByRole('menuitem',{name:'Desactivar recurso',exact:true}).waitFor(); await page.keyboard.press('Escape'); if(!await page.getByRole('button',{name:'Acciones',exact:true}).evaluate(e=>e===document.activeElement))throw Error('Foco del menú');")
|
||
|
|
checks.extend(['assign-existing-command', 'keyboard-assign', 'keyboard-menu-focus-return'])
|
||
|
|
for width in [320, 390, 768, 1024, 1440]:
|
||
|
|
tool(client, 'browser_resize', {'width': width, 'height': 1000})
|
||
|
|
code(client, "await page.evaluate(()=>new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)))); await page.locator('[data-slot=dialog-content]').evaluate(async e=>await Promise.allSettled(e.getAnimations({subtree:true}).filter(a=>Number.isFinite(a.effect?.getComputedTiming().endTime)).map(a=>a.finished)));")
|
||
|
|
value = code(client, "await page.getByRole('button',{name:'Cambiar activo',exact:true}).waitFor(); return await page.locator('[data-slot=network-share-detail]').evaluate(e=>({width:innerWidth,overflow:e.scrollWidth>e.clientWidth+1||document.documentElement.scrollWidth>innerWidth+1||[...e.querySelectorAll('button,a,code,h2,p')].some(n=>n.getBoundingClientRect().right>e.getBoundingClientRect().right+1),columns:getComputedStyle(e.children[1]).gridTemplateColumns,publisherHeight:e.querySelector('[data-slot=published-share-asset]').getBoundingClientRect().height,publicationHeight:e.children[0].getBoundingClientRect().height,headerHeight:e.closest('[role=dialog]').querySelector('[data-slot=dialog-header]').getBoundingClientRect().height}));")
|
||
|
|
if value['overflow']:
|
||
|
|
raise RuntimeError('Desbordamiento: ' + json.dumps(value))
|
||
|
|
measurements.append(value)
|
||
|
|
screenshots.append(capture(client, directory, 'share-detail-light-' + str(width)))
|
||
|
|
if width in [320, 390]:
|
||
|
|
code(client, "const toggle=page.getByRole('button',{name:'Mostrar nombre completo',exact:true}); if(await toggle.count()){await toggle.focus(); await page.keyboard.press('Space'); const id=await page.getByRole('button',{name:'Contraer nombre',exact:true}).getAttribute('aria-controls'); if(!id||!await page.locator('[id='+JSON.stringify(id)+']').isVisible())throw Error('Nombre completo inaccesible'); await page.getByRole('button',{name:'Contraer nombre',exact:true}).focus(); await page.keyboard.press('Enter');} if(!await page.getByRole('button',{name:'Cerrar',exact:true}).last().isVisible())throw Error('Cierre oculto');")
|
||
|
|
checks.append('title-clamp-keyboard-accessible-name-and-close')
|
||
|
|
code(client, "await page.getByRole('button',{name:'Consultar historial',exact:true}).click(); if(!await page.locator('[data-slot=publication-history]').evaluate(e=>e.parentElement===document.activeElement))throw Error('Foco de historial');")
|
||
|
|
screenshots.append(capture(client, directory, 'share-detail-data-history'))
|
||
|
|
code(client, "await page.getByRole('heading',{name:'Uso y navegación',exact:true}).scrollIntoViewIfNeeded();")
|
||
|
|
screenshots.append(capture(client, directory, 'share-detail-usage'))
|
||
|
|
code(client, "await page.context().grantPermissions(['clipboard-read','clipboard-write'],{origin:location.origin});".replace('location.origin', json.dumps(origin)))
|
||
|
|
code(client, "const copy=page.getByRole('button',{name:'Copiar dirección UNC',exact:true}).first(); await copy.click(); await page.getByRole('status').filter({hasText:'Dirección copiada'}).waitFor(); const expected=" + json.dumps(address) + "; if(await page.evaluate(()=>navigator.clipboard.readText())!==expected)throw Error('UNC copiada alterada'); if(await page.locator('[data-slot=network-share-address][data-variant=block] code').textContent()!==expected)throw Error('UNC mostrada alterada');")
|
||
|
|
code(client, "await page.evaluate(()=>{window.shareClipboardWrite=navigator.clipboard.writeText.bind(navigator.clipboard); Object.defineProperty(navigator.clipboard,'writeText',{configurable:true,value:async()=>{throw Error('Synthetic clipboard failure')}})}); await page.getByRole('button',{name:'Copiar dirección UNC',exact:true}).first().click(); await page.locator('[data-slot=network-share-address] [role=alert]').waitFor(); await page.evaluate(()=>Object.defineProperty(navigator.clipboard,'writeText',{configurable:true,value:window.shareClipboardWrite})); await page.getByRole('button',{name:'Copiar dirección UNC',exact:true}).first().click();")
|
||
|
|
checks.extend(['clipboard-real-exact-unc-and-displayed-characters', 'clipboard-failure-and-recovery'])
|
||
|
|
current_period = api.request('/asset-network-share-publications/' + company + '?resourceId=' + resource_id)['publications'][0]
|
||
|
|
code(client, "const history=page.locator('[data-slot=publication-history]'); const technical=history.getByRole('button',{name:'Detalles técnicos',exact:true}).first(); const period=history.getByText(" + json.dumps(current_period['id']) + ",{exact:true}); if(await period.isVisible())throw Error('UUID principal'); await technical.focus(); await page.keyboard.press('Space'); const panel=await technical.getAttribute('aria-controls'); if(!panel||!await page.locator('[id='+JSON.stringify(panel)+']').isVisible()||!await period.isVisible())throw Error('Detalle técnico inaccesible'); await page.keyboard.press('Shift+Tab'); await page.keyboard.press('Tab'); if(!await technical.evaluate(e=>e===document.activeElement))throw Error('Orden de tabulación del historial');")
|
||
|
|
screenshots.append(capture(client, directory, 'share-detail-history-expanded'))
|
||
|
|
tool(client, 'browser_resize', {'width': 390, 'height': 1000})
|
||
|
|
code(client, "await page.locator('[data-slot=publication-history]').getByRole('button',{name:'Detalles técnicos',exact:true}).first().scrollIntoViewIfNeeded();")
|
||
|
|
screenshots.append(capture(client, directory, 'share-detail-history-expanded-mobile'))
|
||
|
|
tool(client, 'browser_resize', {'width': 1440, 'height': 1000})
|
||
|
|
checks.append('technical-details-keyboard-and-related-panel')
|
||
|
|
code(client, "for(const action of await page.locator('[data-slot=resource-publication]').getByRole('button').all()){const label=await action.getAttribute('aria-label');const text=(await action.innerText()).trim();if(label&&text&&!label.toLocaleLowerCase().includes(text.toLocaleLowerCase()))throw Error('Etiqueta visible fuera del nombre accesible');}")
|
||
|
|
code(client, "await page.locator('[data-slot=publication-history]').getByText('Por Auditoría visual',{exact:true}).waitFor(); const status=page.locator('[data-slot=resource-publication] [role=status]').filter({hasText:'Publicación asignada a'}); if(!await status.textContent().then(v=>v.includes('Publicación asignada a')))throw Error('Anuncio de asignación ausente');")
|
||
|
|
checks.append('authorized-actor-name-and-publication-status')
|
||
|
|
checks.extend(['five-widths-no-overflow', 'history-focus', 'copy-unc-feedback'])
|
||
|
|
contrast(client, 'light')
|
||
|
|
code(client, "await page.getByRole('button',{name:'Cambiar activo',exact:true}).hover();")
|
||
|
|
contrast(client, 'light-primary-hover')
|
||
|
|
controls_contrast(client, 'light')
|
||
|
|
code(client, "await page.getByRole('button',{name:'Consultar historial',exact:true}).hover();")
|
||
|
|
contrast(client, 'light-secondary-hover')
|
||
|
|
code(client, "await page.getByRole('button',{name:'Finalizar publicación',exact:true}).hover();")
|
||
|
|
contrast(client, 'light-sensitive-hover')
|
||
|
|
code(client, "await page.evaluate(()=>document.documentElement.classList.add('dark')); await page.emulateMedia({reducedMotion:'reduce'});")
|
||
|
|
code(client, "await page.locator('[data-slot=publication-history]').getByRole('button',{name:'Detalles técnicos',exact:true}).first().scrollIntoViewIfNeeded();")
|
||
|
|
screenshots.append(capture(client, directory, 'share-detail-history-expanded-dark'))
|
||
|
|
code(client, "await page.locator('[data-slot=form-body]').evaluate(e=>e.scrollTop=0);")
|
||
|
|
screenshots.append(capture(client, directory, 'share-detail-dark'))
|
||
|
|
tool(client, 'browser_resize', {'width': 390, 'height': 1000})
|
||
|
|
code(client, "await page.locator('[data-slot=form-body]').evaluate(e=>e.scrollTop=0);")
|
||
|
|
screenshots.append(capture(client, directory, 'share-detail-dark-mobile'))
|
||
|
|
tool(client, 'browser_resize', {'width': 1440, 'height': 1000})
|
||
|
|
contrast(client, 'dark')
|
||
|
|
code(client, "await page.getByRole('button',{name:'Cambiar activo',exact:true}).hover();")
|
||
|
|
contrast(client, 'dark-primary-hover')
|
||
|
|
controls_contrast(client, 'dark')
|
||
|
|
code(client, "await page.getByRole('button',{name:'Consultar historial',exact:true}).hover();")
|
||
|
|
contrast(client, 'dark-secondary-hover')
|
||
|
|
code(client, "await page.getByRole('button',{name:'Finalizar publicación',exact:true}).hover();")
|
||
|
|
contrast(client, 'dark-sensitive-hover')
|
||
|
|
code(client, "await page.evaluate(()=>document.documentElement.classList.remove('dark')); await page.emulateMedia({reducedMotion:'no-preference'});")
|
||
|
|
for scale in [2, 4]:
|
||
|
|
code(client, "await page.evaluate(scale=>document.documentElement.style.fontSize=(scale*100)+'%'," + str(scale) + "); if(await page.locator('[data-slot=network-share-detail]').evaluate(e=>e.scrollWidth>e.clientWidth+1))throw Error('Desbordamiento al ampliar texto'); if(await page.locator('[data-slot=form-body]').evaluate(e=>e.clientHeight<parseFloat(getComputedStyle(e).lineHeight)*2))throw Error('Cuerpo sin espacio al ampliar texto');")
|
||
|
|
screenshots.append(capture(client, directory, 'share-detail-text-' + str(scale * 100)))
|
||
|
|
code(client, "await page.evaluate(()=>document.documentElement.style.fontSize='');")
|
||
|
|
code(client, "const technical=page.locator('[data-slot=publication-history]').getByRole('button',{name:'Detalles técnicos',exact:true}).first(); await technical.focus(); await page.keyboard.press('Enter'); if(await technical.getAttribute('aria-expanded')!=='false')throw Error('No contrae detalles con Enter');")
|
||
|
|
checks.extend(['dark-theme', 'reduced-motion', 'text-200-400-not-browser-zoom'])
|
||
|
|
# Cancelar el cambio conserva la publicación; volver a abrir y confirmar utiliza transfer.
|
||
|
|
code(client, "await page.getByRole('button',{name:'Cambiar activo',exact:true}).click(); await page.getByText('Activo publicador actual',{exact:true}).waitFor();")
|
||
|
|
pick(client, selected[1]['values']['name'])
|
||
|
|
screenshots.append(capture(client, directory, 'share-detail-transfer'))
|
||
|
|
tool(client, 'browser_resize', {'width': 390, 'height': 1000})
|
||
|
|
code(client, "const dialog=page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Cambiar publicador',exact:true})}); if(await dialog.evaluate(e=>e.scrollWidth>e.clientWidth+1))throw Error('Traslado desbordado'); if(!await dialog.getByRole('button',{name:'Confirmar',exact:true}).isVisible())throw Error('Pie del traslado inaccesible');")
|
||
|
|
screenshots.append(capture(client, directory, 'share-detail-transfer-mobile'))
|
||
|
|
code(client, "await page.keyboard.press('Escape'); await page.getByRole('button',{name:'Cambiar activo',exact:true}).waitFor(); if(!await page.getByRole('button',{name:'Cambiar activo',exact:true}).evaluate(e=>e===document.activeElement))throw Error('Foco no restaurado al cancelar');")
|
||
|
|
publications = api.request('/asset-network-share-publications/' + company + '?resourceId=' + resource_id)['publications']
|
||
|
|
if len(publications) != 1 or publications[0]['assetId'] != selected[0]['id']:
|
||
|
|
raise RuntimeError('Cancelación modificó publicación')
|
||
|
|
checks.append('cancel-transfer-no-write')
|
||
|
|
tool(client, 'browser_resize', {'width': 1440, 'height': 1000})
|
||
|
|
code(client, "await page.getByRole('button',{name:'Cambiar activo',exact:true}).click();")
|
||
|
|
pick(client, selected[1]['values']['name'])
|
||
|
|
code(client, "await page.getByRole('button',{name:'Confirmar',exact:true}).click(); await page.getByRole('heading',{name:/Publicación trasladada/}).waitFor();")
|
||
|
|
code(client, "await page.locator('[data-slot=resource-publication] [role=status]').filter({hasText:'Publicación trasladada a'}).waitFor();")
|
||
|
|
checks.append('transfer-and-real-history')
|
||
|
|
# Cambiar el periodo en el servidor mientras se prepara un traslado genera conflicto real.
|
||
|
|
code(client, "await page.getByRole('button',{name:'Cambiar activo',exact:true}).click();")
|
||
|
|
pick(client, selected[0]['values']['name'])
|
||
|
|
current = api.request('/asset-network-share-publications/' + company + '?resourceId=' + resource_id)['publications'][0]
|
||
|
|
api.request('/asset-network-share-publications/' + company, {'action': 'end', 'publicationId': current['id']})
|
||
|
|
code(client, "await page.getByRole('button',{name:'Confirmar',exact:true}).click(); await page.getByRole('alert').filter({hasText:'La publicación ha cambiado'}).waitFor();")
|
||
|
|
screenshots.append(capture(client, directory, 'share-detail-conflict'))
|
||
|
|
code(client, "await page.keyboard.press('Escape');")
|
||
|
|
open_share(client)
|
||
|
|
code(client, "await page.getByText('La publicación registrada ha finalizado',{exact:true}).waitFor();")
|
||
|
|
checks.extend(['real-stale-period-conflict', 'ended-distinct-from-active'])
|
||
|
|
code(client, "await page.getByRole('button',{name:'Asignar activo',exact:true}).click();")
|
||
|
|
pick(client, selected[0]['values']['name'])
|
||
|
|
code(client, "await page.getByRole('button',{name:'Confirmar',exact:true}).click(); await page.getByRole('button',{name:'Finalizar publicación',exact:true}).click(); await page.getByText(/esta acción no deshabilita la carpeta/).waitFor();")
|
||
|
|
screenshots.append(capture(client, directory, 'share-detail-end-confirmation'))
|
||
|
|
code(client, "await page.getByRole('button',{name:'Confirmar',exact:true}).click(); await page.getByText('La publicación registrada ha finalizado',{exact:true}).waitFor();")
|
||
|
|
code(client, "await page.locator('[data-slot=resource-publication] [role=status]').filter({hasText:'Finalizada'}).waitFor();")
|
||
|
|
after = api.request('/resources/' + company)['resources']
|
||
|
|
saved = next(item for item in after if item['id'] == resource_id)
|
||
|
|
if saved['values'] != resource['values']:
|
||
|
|
raise RuntimeError('La publicación modificó datos del recurso')
|
||
|
|
checks.extend(['end-confirmation-documentary', 'end-keeps-history', 'resource-fields-unchanged'])
|
||
|
|
# Más de una página real de periodos; todos los cambios se limitan al recurso sintético.
|
||
|
|
for _ in range(18):
|
||
|
|
period = api.request('/asset-network-share-publications/' + company, {'action': 'publish', 'resourceId': resource_id, 'assetId': selected[0]['id']})['publication']
|
||
|
|
api.request('/asset-network-share-publications/' + company, {'action': 'end', 'publicationId': period['id']})
|
||
|
|
open_share(client)
|
||
|
|
code(client, "const history=page.locator('[data-slot=publication-history]'); await history.getByRole('button',{name:'Siguiente',exact:true}).click(); await history.getByRole('button',{name:'Anterior',exact:true}).click();")
|
||
|
|
checks.append('real-history-pagination-21-periods')
|
||
|
|
# Recuperación de lectura y denegación reales de transporte sin datos ficticios de presentación.
|
||
|
|
pattern = '**/api/asset-network-share-publications/' + company + '/relationships?*'
|
||
|
|
code(client, "page.pendingShareReads=[]; await page.route(" + json.dumps(pattern) + ",r=>{page.pendingShareReads.push(r);});")
|
||
|
|
tool(client, 'browser_resize', {'width': 390, 'height': 1000})
|
||
|
|
open_share(client)
|
||
|
|
code(client, "await page.locator('[data-slot=resource-publication] [aria-busy=true]').waitFor(); await page.emulateMedia({reducedMotion:'reduce'}); if(await page.locator('[data-slot=network-share-detail] [data-slot=skeleton]').evaluateAll(items=>items.some(e=>getComputedStyle(e).animationName!=='none')))throw Error('Skeleton animado con movimiento reducido'); await page.emulateMedia({reducedMotion:'no-preference'});")
|
||
|
|
screenshots.append(capture(client, directory, 'share-detail-loading'))
|
||
|
|
code(client, "await Promise.all(page.pendingShareReads.map(r=>r.continue())); await page.unroute(" + json.dumps(pattern) + "); await page.getByText('La publicación registrada ha finalizado',{exact:true}).waitFor();")
|
||
|
|
tool(client, 'browser_resize', {'width': 1440, 'height': 1000})
|
||
|
|
checks.append('loading-skeleton-real-pending-requests')
|
||
|
|
code(client, "await page.route(" + json.dumps(pattern) + ",r=>r.fulfill({status:500,contentType:'application/json',json:{code:'INTERNAL_ERROR'}}));")
|
||
|
|
open_share(client)
|
||
|
|
code(client, "await page.getByText('No se han podido cargar las publicaciones. Actualiza los datos.',{exact:true}).first().waitFor(); await page.unroute(" + json.dumps(pattern) + "); await page.locator('[data-slot=resource-publication]').getByRole('button',{name:'Reintentar',exact:true}).click(); await page.locator('[data-slot=publication-history]').getByRole('button',{name:'Reintentar',exact:true}).click(); await page.getByText('La publicación registrada ha finalizado',{exact:true}).waitFor();")
|
||
|
|
checks.append('backend-error-retry')
|
||
|
|
code(client, "await page.route(" + json.dumps(pattern) + ",r=>r.fulfill({status:403,contentType:'application/json',json:{code:'FORBIDDEN'}}));")
|
||
|
|
open_share(client)
|
||
|
|
code(client, "await page.getByText('No tienes permiso para consultar esta información de la compañía.',{exact:true}).first().waitFor(); if(await page.getByRole('button',{name:'Asignar activo',exact:true}).count())throw Error('Acción con lectura denegada'); await page.unroute(" + json.dumps(pattern) + ");")
|
||
|
|
checks.append('forbidden-hides-actions-and-data')
|
||
|
|
# Navegación real al activo desde publicación vigente.
|
||
|
|
api.request('/asset-network-share-publications/' + company, {'action': 'publish', 'resourceId': resource_id, 'assetId': selected[0]['id']})
|
||
|
|
api.request('/resources/' + company + '/' + resource_id + '/status', {'active': False})
|
||
|
|
open_share(client)
|
||
|
|
code(client, "await page.getByText('Inactivo',{exact:true}).waitFor(); if(await page.getByRole('button',{name:'Cambiar activo',exact:true}).count())throw Error('Traslado de recurso inactivo'); await page.getByRole('button',{name:'Finalizar publicación',exact:true}).waitFor();")
|
||
|
|
checks.append('inactive-resource-keeps-publication-and-end-without-transfer')
|
||
|
|
api.request('/resources/' + company + '/' + resource_id + '/status', {'active': True})
|
||
|
|
open_share(client)
|
||
|
|
code(client, "await page.getByLabel('Ver ficha del activo',{exact:true}).click(); await page.waitForURL('**/assets/" + selected[0]['id'] + "');")
|
||
|
|
checks.append('navigate-publisher-asset')
|
||
|
|
except Exception:
|
||
|
|
capture(client, directory, 'share-detail-failure')
|
||
|
|
(directory / 'share-detail-failure.json').write_text(json.dumps(tool(client, 'browser_snapshot', {}), ensure_ascii=False))
|
||
|
|
raise
|
||
|
|
# Integración existente contra la misma BD desechable, sin usar fixtures para preparar la auditoría.
|
||
|
|
environment = {**context['active_environment'], 'TSX_TSCONFIG_PATH': '../../tsconfig.json'}
|
||
|
|
with (directory / 'share-detail-integration.log').open('w') as log:
|
||
|
|
result = subprocess.run(['node', '--import', 'tsx', '--test', '--test-isolation=none', '--test-concurrency=1', 'src/network-share-publications.integration.test.ts', 'src/network-shares.integration.test.ts', 'src/smb-access.integration.test.ts'], cwd=context['ROOT'] / 'apps/server', env=environment, stdout=log, stderr=subprocess.STDOUT, timeout=120, check=False)
|
||
|
|
if result.returncode:
|
||
|
|
raise RuntimeError('Integración SMB fallida: revisar share-detail-integration.log')
|
||
|
|
checks.append('mariadb-publications-smb-access-regressions')
|
||
|
|
report = {'iteration': 'v2', 'baseline': '.artifacts/playwright/authenticated-qe_17tcw/', 'checks': checks, 'measurements': measurements, 'screenshots': screenshots, 'database': 'metis_desk_test', 'companies': 15,
|
||
|
|
'principal': {'people': 50, 'assets': 51, 'profiles': 50, 'workstations': 50, 'resources': 67}}
|
||
|
|
(directory / 'share-detail-report.json').write_text(json.dumps(report, ensure_ascii=False, indent=2) + '\n')
|
||
|
|
print('Ficha SMB verificada: ' + str(len(checks)) + ' comprobaciones. Evidencias: ' + str(directory), flush=True)
|