feat: PRD-001 ProductType (flags + multimedia) #38
@@ -1,50 +1,64 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { AlertCircle, Plus } from 'lucide-react'
|
import { AlertCircle, Plus } from 'lucide-react'
|
||||||
import { isAxiosError } from 'axios'
|
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
import { CanPerform } from '@/components/auth/CanPerform'
|
import { CanPerform } from '@/components/auth/CanPerform'
|
||||||
import { useProductTypes } from '../hooks/useProductTypes'
|
import { useProductTypes } from '../hooks/useProductTypes'
|
||||||
import { useCreateProductType } from '../hooks/useCreateProductType'
|
|
||||||
import { useUpdateProductType } from '../hooks/useUpdateProductType'
|
|
||||||
import { useDeactivateProductType } from '../hooks/useDeactivateProductType'
|
import { useDeactivateProductType } from '../hooks/useDeactivateProductType'
|
||||||
import type { ProductTypeListItem } from '../types'
|
import { ProductTypeFormDialog } from '../components/ProductTypeFormDialog'
|
||||||
import type { CreateProductTypeRequest } from '../types'
|
import { DeactivateProductTypeDialog } from '../components/DeactivateProductTypeDialog'
|
||||||
|
import type { ProductTypeListItem, ProductTypeDetail } from '../types'
|
||||||
|
|
||||||
export function ProductTypesPage() {
|
export function ProductTypesPage() {
|
||||||
const [formError, setFormError] = useState<unknown>(null)
|
// ── Create dialog state ──────────────────────────────────────────────────
|
||||||
|
const [createOpen, setCreateOpen] = useState(false)
|
||||||
|
|
||||||
|
// ── Edit dialog state ────────────────────────────────────────────────────
|
||||||
|
const [editOpen, setEditOpen] = useState(false)
|
||||||
|
const [editingProductType, setEditingProductType] = useState<ProductTypeDetail | null>(null)
|
||||||
|
|
||||||
|
// ── Deactivate dialog state ──────────────────────────────────────────────
|
||||||
|
const [deactivateOpen, setDeactivateOpen] = useState(false)
|
||||||
|
const [deactivatingProductType, setDeactivatingProductType] = useState<ProductTypeListItem | null>(null)
|
||||||
|
|
||||||
const { data: paged, isLoading, isError } = useProductTypes({ activo: true })
|
const { data: paged, isLoading, isError } = useProductTypes({ activo: true })
|
||||||
const { mutateAsync: createProductType, isPending: creating } = useCreateProductType()
|
|
||||||
const { mutateAsync: updateProductType, isPending: updating } = useUpdateProductType()
|
|
||||||
const { mutateAsync: deactivateProductType } = useDeactivateProductType()
|
const { mutateAsync: deactivateProductType } = useDeactivateProductType()
|
||||||
|
|
||||||
async function handleCreate(payload: CreateProductTypeRequest) {
|
// ── Handlers ─────────────────────────────────────────────────────────────
|
||||||
try {
|
|
||||||
setFormError(null)
|
function openCreate() {
|
||||||
await createProductType(payload)
|
setCreateOpen(true)
|
||||||
toast.success('Tipo de producto creado')
|
}
|
||||||
} catch (err) {
|
|
||||||
setFormError(err)
|
function openEdit(pt: ProductTypeListItem) {
|
||||||
if (isAxiosError(err) && err.response?.status === 409) {
|
// Map list item to detail shape for pre-filling (multimedia nulls are fine for list item)
|
||||||
toast.error('Ya existe un tipo de producto con ese nombre')
|
const detail: ProductTypeDetail = {
|
||||||
} else {
|
...pt,
|
||||||
toast.error('Error al crear tipo de producto')
|
maxImages: null,
|
||||||
}
|
maxImageSizeMB: null,
|
||||||
|
maxImageWidth: null,
|
||||||
|
maxImageHeight: null,
|
||||||
|
fechaCreacion: '',
|
||||||
|
fechaModificacion: null,
|
||||||
}
|
}
|
||||||
|
setEditingProductType(detail)
|
||||||
|
setEditOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDeactivate(pt: ProductTypeListItem) {
|
||||||
|
setDeactivatingProductType(pt)
|
||||||
|
setDeactivateOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeactivate(id: number) {
|
async function handleDeactivate(id: number) {
|
||||||
try {
|
await deactivateProductType(id)
|
||||||
await deactivateProductType(id)
|
toast.success('Tipo de producto desactivado')
|
||||||
toast.success('Tipo de producto desactivado')
|
|
||||||
} catch {
|
|
||||||
toast.error('Error al desactivar tipo de producto')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Loading / Error ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 p-4">
|
<div className="space-y-4 p-4">
|
||||||
@@ -63,79 +77,110 @@ export function ProductTypesPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isEmpty = !paged?.items.length
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 p-4">
|
<div className="space-y-4 p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h1 className="text-2xl font-bold">Tipos de Producto</h1>
|
<h1 className="text-2xl font-bold">Tipos de Producto</h1>
|
||||||
<CanPerform permission="catalogo:tipos:gestionar">
|
<CanPerform permission="catalogo:tipos:gestionar">
|
||||||
<Button size="sm">
|
<Button size="sm" onClick={openCreate}>
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Nuevo Tipo
|
Nuevo Tipo
|
||||||
</Button>
|
</Button>
|
||||||
</CanPerform>
|
</CanPerform>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{formError && (
|
{isEmpty ? (
|
||||||
<Alert variant="destructive">
|
<div className="flex flex-col items-center gap-4 py-16 text-center text-muted-foreground">
|
||||||
<AlertCircle className="h-4 w-4" />
|
<p>No hay tipos de producto.</p>
|
||||||
<AlertDescription>
|
<CanPerform permission="catalogo:tipos:gestionar">
|
||||||
{isAxiosError(formError) ? formError.message : 'Error inesperado'}
|
<Button variant="outline" onClick={openCreate}>
|
||||||
</AlertDescription>
|
Crear primer tipo
|
||||||
</Alert>
|
</Button>
|
||||||
|
</CanPerform>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b bg-muted/50">
|
||||||
|
<th className="px-4 py-2 text-left font-medium">Nombre</th>
|
||||||
|
<th className="px-4 py-2 text-left font-medium">Duración</th>
|
||||||
|
<th className="px-4 py-2 text-left font-medium">Texto</th>
|
||||||
|
<th className="px-4 py-2 text-left font-medium">Categoría</th>
|
||||||
|
<th className="px-4 py-2 text-left font-medium">Bundle</th>
|
||||||
|
<th className="px-4 py-2 text-left font-medium">Imágenes</th>
|
||||||
|
<th className="px-4 py-2 text-left font-medium">Activo</th>
|
||||||
|
<th className="px-4 py-2" />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{paged.items.map((pt: ProductTypeListItem) => (
|
||||||
|
<tr key={pt.id} className="border-b last:border-0 hover:bg-muted/25">
|
||||||
|
<td className="px-4 py-2 font-medium">{pt.nombre}</td>
|
||||||
|
<td className="px-4 py-2">{pt.hasDuration ? 'Sí' : 'No'}</td>
|
||||||
|
<td className="px-4 py-2">{pt.requiresText ? 'Sí' : 'No'}</td>
|
||||||
|
<td className="px-4 py-2">{pt.requiresCategory ? 'Sí' : 'No'}</td>
|
||||||
|
<td className="px-4 py-2">{pt.isBundle ? 'Sí' : 'No'}</td>
|
||||||
|
<td className="px-4 py-2">{pt.allowImages ? 'Sí' : 'No'}</td>
|
||||||
|
<td className="px-4 py-2">
|
||||||
|
<span className={pt.isActive ? 'text-green-600' : 'text-red-500'}>
|
||||||
|
{pt.isActive ? 'Activo' : 'Inactivo'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2">
|
||||||
|
<CanPerform permission="catalogo:tipos:gestionar">
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => openEdit(pt)}
|
||||||
|
>
|
||||||
|
Editar
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => openDeactivate(pt)}
|
||||||
|
disabled={!pt.isActive}
|
||||||
|
>
|
||||||
|
Desactivar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CanPerform>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="rounded-md border">
|
{/* Create dialog */}
|
||||||
<table className="w-full text-sm">
|
<ProductTypeFormDialog
|
||||||
<thead>
|
open={createOpen}
|
||||||
<tr className="border-b bg-muted/50">
|
onOpenChange={setCreateOpen}
|
||||||
<th className="px-4 py-2 text-left font-medium">Nombre</th>
|
/>
|
||||||
<th className="px-4 py-2 text-left font-medium">Duración</th>
|
|
||||||
<th className="px-4 py-2 text-left font-medium">Texto</th>
|
{/* Edit dialog */}
|
||||||
<th className="px-4 py-2 text-left font-medium">Categoría</th>
|
{editingProductType && (
|
||||||
<th className="px-4 py-2 text-left font-medium">Bundle</th>
|
<ProductTypeFormDialog
|
||||||
<th className="px-4 py-2 text-left font-medium">Imágenes</th>
|
open={editOpen}
|
||||||
<th className="px-4 py-2 text-left font-medium">Activo</th>
|
onOpenChange={setEditOpen}
|
||||||
<th className="px-4 py-2" />
|
productType={editingProductType}
|
||||||
</tr>
|
/>
|
||||||
</thead>
|
)}
|
||||||
<tbody>
|
|
||||||
{paged?.items.map((pt: ProductTypeListItem) => (
|
{/* Deactivate confirmation dialog */}
|
||||||
<tr key={pt.id} className="border-b last:border-0 hover:bg-muted/25">
|
{deactivatingProductType && (
|
||||||
<td className="px-4 py-2 font-medium">{pt.nombre}</td>
|
<DeactivateProductTypeDialog
|
||||||
<td className="px-4 py-2">{pt.hasDuration ? 'Sí' : 'No'}</td>
|
open={deactivateOpen}
|
||||||
<td className="px-4 py-2">{pt.requiresText ? 'Sí' : 'No'}</td>
|
onOpenChange={setDeactivateOpen}
|
||||||
<td className="px-4 py-2">{pt.requiresCategory ? 'Sí' : 'No'}</td>
|
productType={deactivatingProductType}
|
||||||
<td className="px-4 py-2">{pt.isBundle ? 'Sí' : 'No'}</td>
|
onConfirm={handleDeactivate}
|
||||||
<td className="px-4 py-2">{pt.allowImages ? 'Sí' : 'No'}</td>
|
/>
|
||||||
<td className="px-4 py-2">
|
)}
|
||||||
<span className={pt.isActive ? 'text-green-600' : 'text-red-500'}>
|
|
||||||
{pt.isActive ? 'Activo' : 'Inactivo'}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-2">
|
|
||||||
<CanPerform permission="catalogo:tipos:gestionar">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleDeactivate(pt.id)}
|
|
||||||
disabled={!pt.isActive}
|
|
||||||
>
|
|
||||||
Desactivar
|
|
||||||
</Button>
|
|
||||||
</CanPerform>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
{paged?.items.length === 0 && (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={8} className="px-4 py-8 text-center text-muted-foreground">
|
|
||||||
No hay tipos de producto.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
import { describe, it, expect, beforeAll, afterAll, afterEach, vi } from 'vitest'
|
||||||
|
import { render, screen, waitFor } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import { http, HttpResponse } from 'msw'
|
||||||
|
import { setupServer } from 'msw/node'
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import { MemoryRouter, Routes, Route } from 'react-router-dom'
|
||||||
|
import React from 'react'
|
||||||
|
import { ProductTypesPage } from '../../../features/product-types/pages/ProductTypesPage'
|
||||||
|
import { useAuthStore } from '../../../stores/authStore'
|
||||||
|
import type { ProductTypeListItem, PagedResult } from '../../../features/product-types/types'
|
||||||
|
|
||||||
|
const API_URL = 'http://localhost:5000'
|
||||||
|
|
||||||
|
vi.mock('sonner', () => ({
|
||||||
|
toast: { success: vi.fn(), error: vi.fn() },
|
||||||
|
}))
|
||||||
|
|
||||||
|
const adminUser = {
|
||||||
|
id: 1,
|
||||||
|
username: 'admin',
|
||||||
|
nombre: 'Admin',
|
||||||
|
rol: 'admin',
|
||||||
|
permisos: ['catalogo:tipos:gestionar'],
|
||||||
|
mustChangePassword: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
const regularUser = {
|
||||||
|
id: 2,
|
||||||
|
username: 'viewer',
|
||||||
|
nombre: 'Viewer',
|
||||||
|
rol: 'viewer',
|
||||||
|
permisos: [],
|
||||||
|
mustChangePassword: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockItem: ProductTypeListItem = {
|
||||||
|
id: 1,
|
||||||
|
nombre: 'Clasificados',
|
||||||
|
hasDuration: true,
|
||||||
|
requiresText: true,
|
||||||
|
requiresCategory: false,
|
||||||
|
isBundle: false,
|
||||||
|
allowImages: false,
|
||||||
|
isActive: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockPaged: PagedResult<ProductTypeListItem> = {
|
||||||
|
items: [mockItem],
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyPaged: PagedResult<ProductTypeListItem> = {
|
||||||
|
items: [],
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = setupServer()
|
||||||
|
|
||||||
|
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
|
||||||
|
afterEach(() => {
|
||||||
|
server.resetHandlers()
|
||||||
|
useAuthStore.getState().clearAuth()
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
afterAll(() => server.close())
|
||||||
|
|
||||||
|
function renderPage(user = adminUser) {
|
||||||
|
useAuthStore.setState({ user })
|
||||||
|
const qc = new QueryClient({
|
||||||
|
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
||||||
|
})
|
||||||
|
return render(
|
||||||
|
<QueryClientProvider client={qc}>
|
||||||
|
<MemoryRouter initialEntries={['/admin/product-types']}>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/admin/product-types" element={<ProductTypesPage />} />
|
||||||
|
</Routes>
|
||||||
|
</MemoryRouter>
|
||||||
|
</QueryClientProvider>,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Loading / Error / Data states ──────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('ProductTypesPage — loading and error states', () => {
|
||||||
|
it('renders loading skeleton while fetching', () => {
|
||||||
|
server.use(
|
||||||
|
http.get(`${API_URL}/api/v1/product-types`, async () => {
|
||||||
|
await new Promise(() => {})
|
||||||
|
return HttpResponse.json(emptyPaged)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
renderPage()
|
||||||
|
// During loading, skeletons are shown — verify they are rendered
|
||||||
|
const skeletons = document.querySelectorAll('[class*="skeleton"], .animate-pulse')
|
||||||
|
expect(skeletons.length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders data when loaded', async () => {
|
||||||
|
server.use(
|
||||||
|
http.get(`${API_URL}/api/v1/product-types`, () => HttpResponse.json(mockPaged)),
|
||||||
|
)
|
||||||
|
renderPage()
|
||||||
|
await waitFor(() => expect(screen.getByText('Clasificados')).toBeInTheDocument())
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows error state on fetch failure', async () => {
|
||||||
|
server.use(
|
||||||
|
http.get(`${API_URL}/api/v1/product-types`, () =>
|
||||||
|
HttpResponse.json({ error: 'server_error' }, { status: 500 }),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
renderPage()
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByText(/error al cargar tipos de producto/i)).toBeInTheDocument(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows empty state when no product types', async () => {
|
||||||
|
server.use(
|
||||||
|
http.get(`${API_URL}/api/v1/product-types`, () => HttpResponse.json(emptyPaged)),
|
||||||
|
)
|
||||||
|
renderPage()
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByText(/no hay tipos de producto/i)).toBeInTheDocument(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ─── Create dialog ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('ProductTypesPage — create dialog', () => {
|
||||||
|
it('opens create dialog when "Nuevo Tipo" button is clicked', async () => {
|
||||||
|
server.use(
|
||||||
|
http.get(`${API_URL}/api/v1/product-types`, () => HttpResponse.json(mockPaged)),
|
||||||
|
)
|
||||||
|
renderPage(adminUser)
|
||||||
|
await waitFor(() => expect(screen.getByRole('button', { name: /nuevo tipo/i })).toBeInTheDocument())
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: /nuevo tipo/i }))
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByRole('heading', { name: /nuevo tipo de producto/i })).toBeInTheDocument(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('opens create dialog from empty state CTA', async () => {
|
||||||
|
server.use(
|
||||||
|
http.get(`${API_URL}/api/v1/product-types`, () => HttpResponse.json(emptyPaged)),
|
||||||
|
)
|
||||||
|
renderPage(adminUser)
|
||||||
|
await waitFor(() => expect(screen.getByRole('button', { name: /crear primer tipo/i })).toBeInTheDocument())
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: /crear primer tipo/i }))
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByRole('heading', { name: /nuevo tipo de producto/i })).toBeInTheDocument(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('hides "Nuevo Tipo" button when user lacks permission', async () => {
|
||||||
|
server.use(
|
||||||
|
http.get(`${API_URL}/api/v1/product-types`, () => HttpResponse.json(mockPaged)),
|
||||||
|
)
|
||||||
|
renderPage(regularUser)
|
||||||
|
await waitFor(() => expect(screen.getByText('Clasificados')).toBeInTheDocument())
|
||||||
|
expect(screen.queryByRole('button', { name: /nuevo tipo/i })).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ─── Edit dialog ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('ProductTypesPage — edit dialog', () => {
|
||||||
|
it('opens edit dialog pre-filled when Edit button is clicked', async () => {
|
||||||
|
server.use(
|
||||||
|
http.get(`${API_URL}/api/v1/product-types`, () => HttpResponse.json(mockPaged)),
|
||||||
|
)
|
||||||
|
renderPage(adminUser)
|
||||||
|
await waitFor(() => expect(screen.getByText('Clasificados')).toBeInTheDocument())
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: /editar/i }))
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByRole('heading', { name: /editar tipo/i })).toBeInTheDocument(),
|
||||||
|
)
|
||||||
|
const input = screen.getByLabelText(/nombre/i) as HTMLInputElement
|
||||||
|
expect(input.value).toBe('Clasificados')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ─── Deactivate dialog ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('ProductTypesPage — deactivate dialog', () => {
|
||||||
|
it('opens deactivate confirmation dialog when Desactivar is clicked', async () => {
|
||||||
|
server.use(
|
||||||
|
http.get(`${API_URL}/api/v1/product-types`, () => HttpResponse.json(mockPaged)),
|
||||||
|
)
|
||||||
|
renderPage(adminUser)
|
||||||
|
await waitFor(() => expect(screen.getByText('Clasificados')).toBeInTheDocument())
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: /desactivar/i }))
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByRole('heading', { name: /desactivar tipo de producto/i })).toBeInTheDocument(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user