metis-desk/scripts/network-share-detail-ui-audit.py
David c863cc6d96 feat: add unpublished share component and publication operation summary
- Introduced `UnpublishedShare` component for displaying empty state with actions for assigning and viewing history.
- Added `PublicationOperationSummary` component to summarize publication context and actions based on the current state.
- Implemented `CompanyNetworkShareDetail` to manage and display network share details for companies.
- Created `NetworkShareResourceTitle` for collapsible display of resource names with accessibility features.
- Developed `ResourceDetailActions` for managing resource actions like edit, activate/deactivate, and delete.
- Added `ResourceRevisionHistory` to display the history of resource revisions with detailed information.
- Implemented a comprehensive audit script for network share detail UI, including accessibility checks and error handling.
2026-10-26 18:45:30 +01:00

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)