feat: implement SMB folder creation and publication linking flow
This commit is contained in:
parent
0a5fb28263
commit
a72d6a2ec8
@ -506,6 +506,20 @@ se añade una primitiva genérica de wizard sin otros consumidores. Véase
|
|||||||
[el comportamiento del alta](recursos-campos-y-plantillas.md#alta-de-recursos-en-tres-pasos).
|
[el comportamiento del alta](recursos-campos-y-plantillas.md#alta-de-recursos-en-tres-pasos).
|
||||||
|
|
||||||
|
|
||||||
|
### Vinculación del publicador tras el alta SMB
|
||||||
|
|
||||||
|
`ResourceForm` entrega al callback `onSaved` el UUID y el recurso confirmado por el
|
||||||
|
servidor. `ResourcesPage.onCreated` permite completar una operación posterior al alta
|
||||||
|
antes de abrir la ficha; el consumidor llama a `onComplete` al finalizarla o cancelarla.
|
||||||
|
`CompanyResources` utiliza este contrato para abrir el `PublicationEditor` de Assets
|
||||||
|
cuando la carpeta recién creada está activa y tiene capacidad `network:smb-share`.
|
||||||
|
|
||||||
|
El editor conserva la selección explícita del activo, búsqueda, paginación, errores e
|
||||||
|
invalidación de la relación existente. Informa de que la carpeta ya está guardada y ofrece
|
||||||
|
«Vincular más tarde». Reintentar la publicación no repite el alta del recurso. Las
|
||||||
|
ediciones, los recursos ordinarios y las altas inactivas siguen abriendo directamente
|
||||||
|
su ficha. Resources no importa Assets; Companies compone ambos módulos.
|
||||||
|
|
||||||
### Presentación compacta del alta por pasos
|
### Presentación compacta del alta por pasos
|
||||||
|
|
||||||
El alta de recursos y el alta de activos desde el inventario de compañía usan
|
El alta de recursos y el alta de activos desde el inventario de compañía usan
|
||||||
|
|||||||
@ -129,3 +129,65 @@ Incidencias previas indicadas por el encargo: dos expectativas de texto de Compa
|
|||||||
| `git diff --check` | Correcto. |
|
| `git diff --check` | Correcto. |
|
||||||
|
|
||||||
Los comandos PNPM se ejecutaron con `--config.verify-deps-before-run=false` para utilizar las dependencias instaladas. No se instalaron librerías ni se cambiaron lockfiles. La última adaptación de los editores de Access fija su altura y coloca `EntityHelp` mediante `FormHelp` junto al pie; conserva los mismos textos y la variante original de ayuda para los demás consumidores.
|
Los comandos PNPM se ejecutaron con `--config.verify-deps-before-run=false` para utilizar las dependencias instaladas. No se instalaron librerías ni se cambiaron lockfiles. La última adaptación de los editores de Access fija su altura y coloca `EntityHelp` mediante `FormHelp` junto al pie; conserva los mismos textos y la variante original de ayuda para los demás consumidores.
|
||||||
|
|
||||||
|
## Continuación del alta hacia el activo publicador
|
||||||
|
|
||||||
|
Al guardar una carpeta SMB nueva y activa desde Recursos de la compañía, se cierra
|
||||||
|
el alta y se abre el editor de publicación existente, con el UUID del recurso ya
|
||||||
|
confirmado. El operador selecciona expresamente un activo de esa compañía; no se
|
||||||
|
infiere a partir del host UNC ni del campo documental Servidor.
|
||||||
|
|
||||||
|
La carpeta y su publicación son dos operaciones explícitas. El editor explica que la
|
||||||
|
carpeta ya está guardada, conserva la selección ante errores y permite reintentar solo
|
||||||
|
la publicación. «Vincular más tarde», Escape o el cierre conservan la carpeta sin
|
||||||
|
publicador; la ficha mantiene la acción de vincularlo después. Las carpetas inactivas,
|
||||||
|
los demás recursos y las ediciones no abren esta continuación.
|
||||||
|
|
||||||
|
La composición pertenece a `CompanyResources`. Resources entrega el resultado del alta
|
||||||
|
mediante `onCreated` y abre la ficha cuando el consumidor completa el flujo; Assets
|
||||||
|
conserva el editor, la selección remota, los comandos y la invalidación de publicaciones.
|
||||||
|
No se añaden tablas, endpoints, migraciones ni conversiones de carpetas históricas.
|
||||||
|
|
||||||
|
Regresión reproducible con datos sintéticos y el MCP Docker fijado:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/visual-audit.py --network-share-publisher --check
|
||||||
|
```
|
||||||
|
|
||||||
|
El preparador utiliza exclusivamente una instalación desechable de `metis_desk_test`,
|
||||||
|
con 15 compañías y una principal con 50 personas, 51 activos, 50 perfiles, 50 puestos y
|
||||||
|
66 recursos antes del recorrido. La prueba verifica alta, selección, reintento sin
|
||||||
|
duplicados, aplazamiento, edición, recursos inactivos y ordinarios y aislamiento de
|
||||||
|
publicaciones; genera capturas a 1440, 390 y 320 px. Los resultados de cada ejecución
|
||||||
|
se guardan en `publisher-report.json` dentro del directorio local de evidencias.
|
||||||
|
|
||||||
|
### Validación de la continuación del alta (26 de octubre de 2026)
|
||||||
|
|
||||||
|
- `python3 scripts/visual-audit.py --network-share-publisher --check`: 18 comprobaciones
|
||||||
|
correctas; 15 compañías, 50 personas, 51 activos, 50 perfiles, 50 puestos y 70 recursos
|
||||||
|
finales en la principal. Teclado, retorno de foco, búsqueda, paginación, búsqueda vacía,
|
||||||
|
error simulado de publicación, reintento con persistencia real, aplazamiento, edición,
|
||||||
|
altas inactivas/ordinarias y aislamiento. Capturas inspeccionadas a 1440 y 320 px;
|
||||||
|
también se generó y comprobó ausencia de desbordamiento a 390 px. Entorno retirado.
|
||||||
|
- `pnpm --filter @metis/resources --filter @metis/assets --filter @metis/companies
|
||||||
|
--filter @metis/web typecheck`: correcto. `pnpm build`: correcto.
|
||||||
|
- Suites completas con `--test-isolation=none`: Resources 124/124; Assets 150/150;
|
||||||
|
Companies 310/312. Persisten las expectativas de «Personas (1)» y del texto de acceso
|
||||||
|
de personas en `companies-page.test.tsx`, ajenas a este cambio.
|
||||||
|
- `pnpm test`, repetido con acceso a loopback: termina con esos dos fallos de Companies.
|
||||||
|
La primera ejecución de Resources dentro del sandbox falló al abrir un puerto local;
|
||||||
|
su suite completa pasó al repetir con acceso a loopback.
|
||||||
|
- `pnpm typecheck`: falla en `packages/shadcn-ui/src/components/calendar.tsx:165`
|
||||||
|
(TS2375), fuera del cambio y sin modificar vendor. `pnpm lint` y `pnpm check` globales
|
||||||
|
fallan en archivos ajenos, incluidos ejemplos de skills y código de fotos de personas.
|
||||||
|
- Biome y ESLint dirigidos a todos los archivos TS/TSX/JSON del cambio: correctos;
|
||||||
|
sintaxis de los dos scripts Python y `git diff --check`: correctos.
|
||||||
|
`python3 scripts/visual-audit.test.py`: 10/10.
|
||||||
|
|
||||||
|
Skills consultadas: `rdx-solid` para reutilizar ownership y contratos; `ux-inputs-and-forms`
|
||||||
|
para explicar el guardado y el aplazamiento; `shadcn` para componer el editor existente;
|
||||||
|
`vercel-react-best-practices` para coordinar el avance desde el callback sin efectos ni
|
||||||
|
copias de estado remoto. Los auditores especializados referidos por la skill de formularios
|
||||||
|
no están instalados; las comprobaciones citadas son directas mediante Playwright MCP,
|
||||||
|
no una certificación de accesibilidad. No se han ejecutado suites MariaDB adicionales:
|
||||||
|
los comandos existentes se han ejercitado a través de HTTP en el entorno desechable.
|
||||||
|
|||||||
@ -628,6 +628,8 @@
|
|||||||
"publisher": "Publishing asset"
|
"publisher": "Publishing asset"
|
||||||
},
|
},
|
||||||
"shareUi": {
|
"shareUi": {
|
||||||
|
"linkLater": "Link later",
|
||||||
|
"resourceCreated": "The share “{{name}}” has been saved. Select its publishing asset. If you leave this for later, you can link it from the share details.",
|
||||||
"published": "Published network shares",
|
"published": "Published network shares",
|
||||||
"publisher": "Published by",
|
"publisher": "Published by",
|
||||||
"publish": "Assign publication",
|
"publish": "Assign publication",
|
||||||
|
|||||||
@ -628,6 +628,8 @@
|
|||||||
"publisher": "Activo publicador"
|
"publisher": "Activo publicador"
|
||||||
},
|
},
|
||||||
"shareUi": {
|
"shareUi": {
|
||||||
|
"linkLater": "Vincular más tarde",
|
||||||
|
"resourceCreated": "La carpeta «{{name}}» está guardada. Selecciona el activo que la publica. Si lo dejas para más tarde, podrás vincularlo desde la ficha de la carpeta.",
|
||||||
"published": "Recursos compartidos publicados",
|
"published": "Recursos compartidos publicados",
|
||||||
"publisher": "Publicado por",
|
"publisher": "Publicado por",
|
||||||
"publish": "Asignar publicación",
|
"publish": "Asignar publicación",
|
||||||
|
|||||||
@ -16,6 +16,7 @@ export {
|
|||||||
} from "./infrastructure/shared/api/infrastructure-api";
|
} from "./infrastructure/shared/api/infrastructure-api";
|
||||||
export { getInfrastructureItems } from "./infrastructure/shared/helpers/infrastructure-items";
|
export { getInfrastructureItems } from "./infrastructure/shared/helpers/infrastructure-items";
|
||||||
export { NetworkSharePublications } from "./network-share-publications/detail/ui/network-share-publications";
|
export { NetworkSharePublications } from "./network-share-publications/detail/ui/network-share-publications";
|
||||||
|
export { PublicationEditor } from "./network-share-publications/manage/ui/publication-editor";
|
||||||
export { PublicationPicker } from "./network-share-publications/select/ui/publication-picker";
|
export { PublicationPicker } from "./network-share-publications/select/ui/publication-picker";
|
||||||
export { assetsEnglish, assetsSpanish } from "./shared";
|
export { assetsEnglish, assetsSpanish } from "./shared";
|
||||||
export { inventoryApi } from "./shared/api";
|
export { inventoryApi } from "./shared/api";
|
||||||
|
|||||||
@ -22,8 +22,10 @@ export function PublicationEditor({
|
|||||||
companyId,
|
companyId,
|
||||||
operation,
|
operation,
|
||||||
onClose,
|
onClose,
|
||||||
|
createdResourceName,
|
||||||
}: {
|
}: {
|
||||||
companyId: string;
|
companyId: string;
|
||||||
|
createdResourceName?: string;
|
||||||
operation: PublicationOperation;
|
operation: PublicationOperation;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) {
|
}) {
|
||||||
@ -95,7 +97,7 @@ export function PublicationEditor({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormDialog
|
<FormDialog
|
||||||
cancelLabel={t("cancel")}
|
cancelLabel={t(createdResourceName ? "shareUi.linkLater" : "cancel")}
|
||||||
formActions
|
formActions
|
||||||
layout="stable"
|
layout="stable"
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
@ -107,6 +109,11 @@ export function PublicationEditor({
|
|||||||
title={t(`shareUi.${operation.action}`)}
|
title={t(`shareUi.${operation.action}`)}
|
||||||
>
|
>
|
||||||
<FormBody>
|
<FormBody>
|
||||||
|
{createdResourceName && (
|
||||||
|
<p className="text-sm" role="status">
|
||||||
|
{t("shareUi.resourceCreated", { name: createdResourceName })}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
{operation.action !== "end" && (
|
{operation.action !== "end" && (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<FormLabel htmlFor={id} required>
|
<FormLabel htmlFor={id} required>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { AssetCatalogPage, InfrastructurePage } from "@metis/assets/client";
|
import { AssetCatalogPage, InfrastructurePage } from "@metis/assets/client";
|
||||||
import { useBrowserSearchParams, useCreateNavigation } from "@metis/core/client";
|
import { useBrowserSearchParams, useCreateNavigation } from "@metis/core/client";
|
||||||
import { ResourceCatalogPage, ResourcesPage } from "@metis/resources/client";
|
import { ResourceCatalogPage } from "@metis/resources/client";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
|
||||||
import type { CompanyDTO } from "../../../../common";
|
import type { CompanyDTO } from "../../../../common";
|
||||||
@ -14,14 +14,13 @@ import { PeopleSection } from "../../../people";
|
|||||||
import { PrinterConfigurationsPage } from "../../../printer-configurations";
|
import { PrinterConfigurationsPage } from "../../../printer-configurations";
|
||||||
import { ProcessesPage } from "../../../processes";
|
import { ProcessesPage } from "../../../processes";
|
||||||
import { RemoteConnectionsPage } from "../../../remote-connections";
|
import { RemoteConnectionsPage } from "../../../remote-connections";
|
||||||
import { CompanyResourceRelatedContent } from "../../../resources/detail/ui/company-resource-related-content";
|
import { CompanyResources } from "../../../resources/list/ui/company-resources";
|
||||||
import { SiteSection } from "../../../sites";
|
import { SiteSection } from "../../../sites";
|
||||||
import { TeleworkAuthorizationsPage } from "../../../telework-authorizations";
|
import { TeleworkAuthorizationsPage } from "../../../telework-authorizations";
|
||||||
import { WorkProfilesPage } from "../../../work-profiles";
|
import { WorkProfilesPage } from "../../../work-profiles";
|
||||||
import { CatalogOverview } from "../../../workspace/catalogs/ui/catalog-overview";
|
import { CatalogOverview } from "../../../workspace/catalogs/ui/catalog-overview";
|
||||||
import type { CompanyTab } from "../../shared/types";
|
import type { CompanyTab } from "../../shared/types";
|
||||||
|
|
||||||
import { CompanyEntityContext } from "./company-entity-context";
|
|
||||||
import { CompanyInventory } from "./company-inventory";
|
import { CompanyInventory } from "./company-inventory";
|
||||||
import { CompanyOverview } from "./company-overview";
|
import { CompanyOverview } from "./company-overview";
|
||||||
|
|
||||||
@ -156,20 +155,12 @@ export function CompanySection({
|
|||||||
);
|
);
|
||||||
case "resources":
|
case "resources":
|
||||||
return (
|
return (
|
||||||
<ResourcesPage
|
<CompanyResources
|
||||||
companyId={company.id}
|
company={company}
|
||||||
initialCreate={create}
|
initialCreate={create}
|
||||||
initialId={initialEntityId}
|
initialId={initialEntityId}
|
||||||
relatedContent={(resource) => (
|
key={company.id}
|
||||||
<CompanyEntityContext
|
|
||||||
companyId={company.id}
|
|
||||||
initialFocus={false}
|
|
||||||
key={resource.id}
|
|
||||||
layoutReady={layoutReady}
|
layoutReady={layoutReady}
|
||||||
>
|
|
||||||
<CompanyResourceRelatedContent company={company} resource={resource} />
|
|
||||||
</CompanyEntityContext>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "resource-catalog":
|
case "resource-catalog":
|
||||||
|
|||||||
@ -0,0 +1,70 @@
|
|||||||
|
import { PublicationEditor } from "@metis/assets/client";
|
||||||
|
import { ResourcesPage } from "@metis/resources/client";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import type { CompanyDTO } from "../../../../common";
|
||||||
|
import { CompanyEntityContext } from "../../../companies/detail/ui/company-entity-context";
|
||||||
|
import { CompanyResourceRelatedContent } from "../../detail/ui/company-resource-related-content";
|
||||||
|
|
||||||
|
export function CompanyResources({
|
||||||
|
company,
|
||||||
|
initialCreate,
|
||||||
|
initialId,
|
||||||
|
layoutReady,
|
||||||
|
}: {
|
||||||
|
company: CompanyDTO;
|
||||||
|
initialCreate?: boolean | undefined;
|
||||||
|
initialId?: string | undefined;
|
||||||
|
layoutReady: boolean;
|
||||||
|
}) {
|
||||||
|
const [publication, setPublication] = useState<{
|
||||||
|
resourceId: string;
|
||||||
|
resourceName: string;
|
||||||
|
onComplete: () => void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ResourcesPage
|
||||||
|
companyId={company.id}
|
||||||
|
initialCreate={initialCreate}
|
||||||
|
initialId={initialId}
|
||||||
|
onCreated={(resource, onComplete) => {
|
||||||
|
if (resource.networkShare?.capability === "network:smb-share" && resource.values.active) {
|
||||||
|
setPublication({
|
||||||
|
resourceId: resource.id,
|
||||||
|
resourceName: resource.values.name,
|
||||||
|
onComplete,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
onComplete();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
relatedContent={(resource) => (
|
||||||
|
<CompanyEntityContext
|
||||||
|
companyId={company.id}
|
||||||
|
initialFocus={false}
|
||||||
|
key={resource.id}
|
||||||
|
layoutReady={layoutReady}
|
||||||
|
>
|
||||||
|
<CompanyResourceRelatedContent company={company} resource={resource} />
|
||||||
|
</CompanyEntityContext>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{publication && (
|
||||||
|
<PublicationEditor
|
||||||
|
companyId={company.id}
|
||||||
|
createdResourceName={publication.resourceName}
|
||||||
|
onClose={() => {
|
||||||
|
setPublication(undefined);
|
||||||
|
publication.onComplete();
|
||||||
|
}}
|
||||||
|
operation={{
|
||||||
|
action: "publish",
|
||||||
|
anchor: { type: "resources:resource", id: publication.resourceId },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -4,6 +4,8 @@ import type { ResourceDTO } from "../../../../common";
|
|||||||
|
|
||||||
export type ResourcesPageProps = {
|
export type ResourcesPageProps = {
|
||||||
companyId: string;
|
companyId: string;
|
||||||
|
/** Completa el flujo posterior al alta antes de abrir la ficha del recurso guardado. */
|
||||||
|
onCreated?: (resource: ResourceDTO, onComplete: () => void) => void;
|
||||||
relatedContent?: (resource: ResourceDTO) => ReactNode;
|
relatedContent?: (resource: ResourceDTO) => ReactNode;
|
||||||
initialId?: string | undefined;
|
initialId?: string | undefined;
|
||||||
initialCreate?: boolean | undefined;
|
initialCreate?: boolean | undefined;
|
||||||
|
|||||||
@ -27,7 +27,7 @@ import { ResourceInventoryTable } from "./resource-inventory-table";
|
|||||||
|
|
||||||
export function ResourcesPage(props: ResourcesPageProps) {
|
export function ResourcesPage(props: ResourcesPageProps) {
|
||||||
const controller = useResourcesPageController(props);
|
const controller = useResourcesPageController(props);
|
||||||
const { companyId, relatedContent } = props;
|
const { companyId, relatedContent, onCreated } = props;
|
||||||
const {
|
const {
|
||||||
editor,
|
editor,
|
||||||
setEditor,
|
setEditor,
|
||||||
@ -213,9 +213,13 @@ export function ResourcesPage(props: ResourcesPageProps) {
|
|||||||
initialKind={creationKind}
|
initialKind={creationKind}
|
||||||
items={catalog.data.items}
|
items={catalog.data.items}
|
||||||
onClose={() => setEditor(undefined)}
|
onClose={() => setEditor(undefined)}
|
||||||
onSaved={(id) => {
|
onSaved={(id, resource) => {
|
||||||
setEditor(undefined);
|
setEditor(undefined);
|
||||||
|
if (editor === "new" && onCreated) {
|
||||||
|
onCreated(resource, () => selectResource(id));
|
||||||
|
} else {
|
||||||
selectResource(id);
|
selectResource(id);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
resource={edited}
|
resource={edited}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -89,7 +89,7 @@ export function useResourceFormController({
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
if (mounted.current) {
|
if (mounted.current) {
|
||||||
onSaved(response.resource.id);
|
onSaved(response.resource.id, response.resource);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,7 +5,7 @@ export type ResourceFormProps = {
|
|||||||
companyId: string;
|
companyId: string;
|
||||||
resource?: ResourceDTO | undefined;
|
resource?: ResourceDTO | undefined;
|
||||||
items: readonly CatalogItemDTO[];
|
items: readonly CatalogItemDTO[];
|
||||||
onSaved: (id: string) => void;
|
onSaved: (id: string, resource: ResourceDTO) => void;
|
||||||
allowQuickCreate?: boolean;
|
allowQuickCreate?: boolean;
|
||||||
creationLayout?: "single-page" | "steps";
|
creationLayout?: "single-page" | "steps";
|
||||||
initialKind?: "mailbox" | "access" | "application" | "certificate" | "support" | "os" | undefined;
|
initialKind?: "mailbox" | "access" | "application" | "certificate" | "support" | "os" | undefined;
|
||||||
|
|||||||
@ -19,7 +19,7 @@ export function ResourceEditorDialog({
|
|||||||
items: ComponentProps<typeof ResourceForm>["items"];
|
items: ComponentProps<typeof ResourceForm>["items"];
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
finalFocus?: ComponentProps<typeof FormDialog>["finalFocus"];
|
finalFocus?: ComponentProps<typeof FormDialog>["finalFocus"];
|
||||||
onSaved: (id: string) => void;
|
onSaved: ComponentProps<typeof ResourceForm>["onSaved"];
|
||||||
initialKind?: ComponentProps<typeof ResourceForm>["initialKind"];
|
initialKind?: ComponentProps<typeof ResourceForm>["initialKind"];
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation("resources");
|
const { t } = useTranslation("resources");
|
||||||
|
|||||||
78
scripts/network-share-publisher-ui-audit.py
Normal file
78
scripts/network-share-publisher-ui-audit.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
"""Alta SMB y publicación: regresión con HTTP real en el entorno visual desechable."""
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def verify(context, fixture, admin_password):
|
||||||
|
tool, browser, capture = context['tool'], context['browser'], context['capture']
|
||||||
|
origin, directory = context['origin'], context['directory']
|
||||||
|
company = fixture['company']
|
||||||
|
base = origin + '/companies/' + company + '/resources'
|
||||||
|
screenshots, checks = [], []
|
||||||
|
|
||||||
|
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 create(client, name, active=True, smb=True):
|
||||||
|
print('Publicador SMB: ' + name, flush=True)
|
||||||
|
tool(client, 'browser_navigate', {'url': base})
|
||||||
|
code(client, "await page.getByRole('button',{name:'Nuevo recurso',exact:true}).click(); await page.getByRole('combobox',{name:'Plantilla de recurso',exact:true}).click(); await page.getByRole('option',{name:" + json.dumps('Carpeta compartida' if smb else 'Licencia de software') + ",exact:true}).click(); await page.getByRole('button',{name:'Siguiente',exact:true}).click();")
|
||||||
|
if smb:
|
||||||
|
unc = '\\\\alias-documental\\' + name.replace(' ', '-')
|
||||||
|
code(client, "await page.getByRole('textbox',{name:'Ruta UNC',exact:true}).fill(" + json.dumps(unc) + ");")
|
||||||
|
code(client, "await page.getByRole('button',{name:'Siguiente',exact:true}).click(); await page.getByRole('textbox',{name:'Nombre',exact:true}).fill(" + json.dumps(name) + ");")
|
||||||
|
if not active:
|
||||||
|
code(client, "await page.getByRole('combobox',{name:'Estado',exact:true}).click(); await page.getByRole('option',{name:'Inactivo',exact:true}).click();")
|
||||||
|
code(client, "await page.getByRole('button',{name:'Guardar',exact:true}).click(); await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Nuevo recurso',exact:true})}).waitFor({state:'hidden'});")
|
||||||
|
|
||||||
|
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});")
|
||||||
|
# Las comprobaciones y escrituras siguientes apuntan solo al contenedor sintético.
|
||||||
|
create(client, 'AUDIT-SMB Álvarez vinculado')
|
||||||
|
code(client, "await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Asignar publicación',exact:true})}).waitFor(); await page.getByRole('status').filter({hasText:'está guardada'}).waitFor(); if(!await page.getByRole('button',{name:'Confirmar',exact:true}).isDisabled())throw Error('Confirmación sin activo'); await page.getByRole('button',{name:'Seleccionar activo publicador',exact:true}).focus(); await page.keyboard.press('Enter'); await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Seleccionar activo publicador',exact:true})}).waitFor();")
|
||||||
|
checks.extend(['ordinary-user', 'create-smb-opens-publication', 'saved-resource-explained', 'no-default-publisher', 'keyboard-open-picker'])
|
||||||
|
code(client, "await page.getByRole('button',{name:'Siguiente',exact:true}).click(); await page.getByRole('button',{name:'Anterior',exact:true}).click(); await page.getByRole('textbox',{name:'Buscar',exact:true}).fill('NO-EXISTE-SMB'); await page.getByText('No hay elementos disponibles para esta búsqueda.',{exact:true}).waitFor(); await page.getByRole('textbox',{name:'Buscar',exact:true}).fill('AUDIT-R23-PORT-50'); await page.getByRole('radio',{name:'Seleccionar: AUDIT-R23-PORT-50',exact:true}).check(); await page.getByRole('button',{name:'Seleccionar',exact:true}).click(); await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Seleccionar activo publicador',exact:true})}).waitFor({state:'hidden'}); if(!await page.getByRole('button',{name:'Seleccionar activo publicador',exact:true}).evaluate(e=>e===document.activeElement))throw Error('Foco no restaurado');")
|
||||||
|
checks.extend(['asset-pagination', 'empty-search-recovery', 'asset-search', 'picker-focus-return'])
|
||||||
|
for width in [1440, 390, 320]:
|
||||||
|
tool(client, 'browser_resize', {'width': width, 'height': 900})
|
||||||
|
code(client, "if(await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Asignar publicación',exact:true})}).evaluate(e=>e.scrollWidth>e.clientWidth+1))throw Error('Desbordamiento horizontal');")
|
||||||
|
screenshots.append(capture(client, directory, 'publisher-create-' + str(width)))
|
||||||
|
tool(client, 'browser_resize', {'width': 1440, 'height': 1000})
|
||||||
|
pattern = '**/api/asset-network-share-publications/' + company
|
||||||
|
code(client, "await page.route(" + json.dumps(pattern) + ",route=>route.request().method()==='POST'?route.fulfill({status:409,contentType:'application/json',json:{code:'ASSET_INACTIVE'}}):route.continue()); await page.getByRole('button',{name:'Confirmar',exact:true}).click(); await page.getByRole('alert').waitFor(); if(await page.getByRole('button',{name:'Seleccionar activo publicador',exact:true}).textContent()!=='AUDIT-R23-PORT-50')throw Error('Selección perdida'); await page.unroute(" + json.dumps(pattern) + "); await page.getByRole('button',{name:'Confirmar',exact:true}).click(); await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Asignar publicación',exact:true})}).waitFor({state:'hidden'});")
|
||||||
|
outcome = code(client, "const resources=await (await page.request.get('" + origin + "/api/resources/" + company + "')).json(); const found=resources.resources.filter(r=>r.values.name==='AUDIT-SMB Álvarez vinculado'); if(found.length!==1)throw Error('Recurso duplicado'); const publications=await (await page.request.get('" + origin + "/api/asset-network-share-publications/" + company + "?resourceId='+found[0].id)).json(); if(publications.publications.length!==1||publications.publications[0].assetId!==" + json.dumps(fixture['softwareAssets'][-1]) + ")throw Error('Publicador incorrecto'); return {id:found[0].id};")
|
||||||
|
checks.extend(['publication-error-retains-selection', 'retry-no-duplicate-resource', 'real-publication-persisted', 'alias-does-not-infer-asset'])
|
||||||
|
tool(client, 'browser_navigate', {'url': base + '/' + outcome['id']})
|
||||||
|
code(client, "await page.getByRole('complementary').getByRole('button',{name:'Editar',exact:true}).click(); await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Editar recurso',exact:true})}).waitFor(); await page.getByRole('button',{name:'Guardar',exact:true}).click(); await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Editar recurso',exact:true})}).waitFor({state:'hidden'}); if(await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Asignar publicación',exact:true})}).count())throw Error('Edición repite alta');")
|
||||||
|
checks.append('editing-does-not-reopen-publication')
|
||||||
|
create(client, 'AUDIT-SMB pendiente')
|
||||||
|
code(client, "await page.getByRole('button',{name:'Vincular más tarde',exact:true}).click(); await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Asignar publicación',exact:true})}).waitFor({state:'hidden'}); const resources=await (await page.request.get('" + origin + "/api/resources/" + company + "')).json(); const r=resources.resources.find(r=>r.values.name==='AUDIT-SMB pendiente'); if(!r)throw Error('Recurso perdido'); const p=await (await page.request.get('" + origin + "/api/asset-network-share-publications/" + company + "?resourceId='+r.id)).json(); if(p.publications.length)throw Error('Publicación no confirmada');")
|
||||||
|
checks.append('link-later-keeps-unpublished-resource')
|
||||||
|
create(client, 'AUDIT-SMB inactivo', active=False)
|
||||||
|
code(client, "if(await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Asignar publicación',exact:true})}).count())throw Error('Alta inactiva ofrece publicación');")
|
||||||
|
create(client, 'AUDIT-LIC licencia', smb=False)
|
||||||
|
code(client, "if(await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Asignar publicación',exact:true})}).count())throw Error('Recurso ordinario ofrece publicación');")
|
||||||
|
checks.extend(['inactive-share-skips-publication', 'ordinary-resource-skips-publication'])
|
||||||
|
code(client, "const response=await page.request.get('" + origin + "/api/asset-network-share-publications/" + fixture['otherCompanies'][0] + "'); if(!response.ok()||(await response.json()).publications.length)throw Error('Aislamiento de compañía');")
|
||||||
|
checks.append('other-company-has-no-publications')
|
||||||
|
except Exception:
|
||||||
|
capture(client, directory, 'publisher-failure')
|
||||||
|
snapshot = tool(client, 'browser_snapshot', {})
|
||||||
|
(directory / 'publisher-failure.json').write_text(json.dumps(snapshot, ensure_ascii=False))
|
||||||
|
raise
|
||||||
|
report = {'checks': checks, 'screenshots': screenshots, 'companies': 15,
|
||||||
|
'principal': {'people': 50, 'assets': 51, 'resources': 70, 'profiles': 50, 'workstations': 50},
|
||||||
|
'database': 'metis_desk_test', 'mcp': 'Docker stdio fijado'}
|
||||||
|
(directory / 'publisher-report.json').write_text(json.dumps(report, ensure_ascii=False, indent=2) + '\n')
|
||||||
|
print('Publicador SMB verificado: ' + str(len(checks)) + ' comprobaciones. Evidencias: ' + str(directory), flush=True)
|
||||||
@ -301,6 +301,7 @@ def verify_browser(config, origin, fixture, directory, state, logout):
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--network-share-publisher", action="store_true", help="Alta de carpeta SMB y vinculación del activo publicador con datos sintéticos")
|
||||||
parser.add_argument("--dialog-help", action="store_true", help="Ayuda contextual en diálogos reales con datos sintéticos y MCP")
|
parser.add_argument("--dialog-help", action="store_true", help="Ayuda contextual en diálogos reales con datos sintéticos y MCP")
|
||||||
parser.add_argument("--check", action="store_true", help="Verifica y retira el entorno; sin esta opción espera hasta Ctrl+C")
|
parser.add_argument("--check", action="store_true", help="Verifica y retira el entorno; sin esta opción espera hasta Ctrl+C")
|
||||||
parser.add_argument("--global-applications", action="store_true", help="Recorrido R2.3 con catálogo habilitado solo tras migración desechable")
|
parser.add_argument("--global-applications", action="store_true", help="Recorrido R2.3 con catálogo habilitado solo tras migración desechable")
|
||||||
@ -349,6 +350,11 @@ def main():
|
|||||||
"DB_NAME": "metis_desk_test", "DB_USER": "visual_audit", "DB_PASSWORD": database_password,
|
"DB_NAME": "metis_desk_test", "DB_USER": "visual_audit", "DB_PASSWORD": database_password,
|
||||||
"SESSION_SECRET": secrets.token_urlsafe(48), "NODE_ENV": "test", "APP_ORIGIN": origin,
|
"SESSION_SECRET": secrets.token_urlsafe(48), "NODE_ENV": "test", "APP_ORIGIN": origin,
|
||||||
"PORT": str(api_port), "API_PROXY_TARGET": api.base}
|
"PORT": str(api_port), "API_PROXY_TARGET": api.base}
|
||||||
|
if options.network_share_publisher:
|
||||||
|
runner = runpy.run_path(str(ROOT / "scripts/global-applications-ui-audit.py"))["run"]
|
||||||
|
verifier = runpy.run_path(str(ROOT / "scripts/network-share-publisher-ui-audit.py"))["verify"]
|
||||||
|
runner({**globals(), **locals()}, verifier=verifier)
|
||||||
|
return
|
||||||
if options.dialog_help:
|
if options.dialog_help:
|
||||||
runner = runpy.run_path(str(ROOT / "scripts/global-applications-ui-audit.py"))["run"]
|
runner = runpy.run_path(str(ROOT / "scripts/global-applications-ui-audit.py"))["run"]
|
||||||
verifier = runpy.run_path(str(ROOT / "scripts/dialog-help-ui-audit.py"))["verify"]
|
verifier = runpy.run_path(str(ROOT / "scripts/dialog-help-ui-audit.py"))["verify"]
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user