- Updated `auth.setup.ts` to handle multiple app launches in Workspace and ensure proper redirection to the dashboard. - Improved invoice creation tests in `export-flow.spec.ts` and `invoice-flow.spec.ts` to check for existing exchange rates before creating new ones, and adjusted button names for consistency. - Refactored selectors to use placeholders for invoice number inputs across multiple test files. - Enhanced error handling and visibility checks in various test scenarios to improve reliability. - Removed obsolete `setup-catalogs.spec.ts` file as its functionality is no longer needed. These changes aim to streamline the testing process and ensure more robust interactions with the application.
453 lines
19 KiB
TypeScript
453 lines
19 KiB
TypeScript
import { test, expect, type Page } from '@playwright/test'
|
|
import * as fs from 'fs'
|
|
import { fileURLToPath } from 'url'
|
|
import * as path from 'path'
|
|
|
|
const __filename = fileURLToPath(import.meta.url)
|
|
const __dirname = path.dirname(__filename)
|
|
const SHARED_FILE = path.join(__dirname, '.e2e-shared.json')
|
|
|
|
function saveShared(data: Record<string, string>) {
|
|
fs.writeFileSync(SHARED_FILE, JSON.stringify(data))
|
|
}
|
|
|
|
function loadShared(): Record<string, string> {
|
|
try {
|
|
return JSON.parse(fs.readFileSync(SHARED_FILE, 'utf-8'))
|
|
} catch {
|
|
return {}
|
|
}
|
|
}
|
|
|
|
const SUFFIX = Date.now().toString().slice(-6)
|
|
const TODAY = new Date().toISOString().split('T')[0]
|
|
|
|
const HOMOCLAVE = SUFFIX.slice(-3).toUpperCase()
|
|
|
|
const PROVEEDOR_RFC = `XAXX010101${HOMOCLAVE}`
|
|
const PROVEEDOR_NOMBRE = `Proveedor E2E ${SUFFIX}`
|
|
|
|
const CLIENTE_RFC = `XBXX010101${HOMOCLAVE}`
|
|
const CLIENTE_NOMBRE = `Cliente E2E ${SUFFIX}`
|
|
|
|
const BROKER_KEY = `T${SUFFIX.slice(-4)}`
|
|
const BROKER_LICENSE = SUFFIX.slice(-4).replace(/^0/, '1')
|
|
const INVOICE_NUMBER = `E2E-${SUFFIX}`
|
|
|
|
const PEDIMENTO_YEAR = '26'
|
|
const PEDIMENTO_OFFICE = '240'
|
|
const PEDIMENTO_LICENSE = '3101'
|
|
const PEDIMENTO_NUMBER = `${SUFFIX}`
|
|
|
|
async function fillInput(page: Page, selector: string, value: string) {
|
|
await page.locator(selector).click()
|
|
await page.evaluate(({ sel, val }: { sel: string; val: string }) => {
|
|
const el = document.querySelector(sel) as HTMLInputElement
|
|
if (!el) return
|
|
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set
|
|
setter?.call(el, val)
|
|
el.dispatchEvent(new Event('input', { bubbles: true }))
|
|
el.dispatchEvent(new Event('change', { bubbles: true }))
|
|
}, { sel: selector, val: value })
|
|
await page.waitForTimeout(2000)
|
|
}
|
|
|
|
async function fillTextarea(page: Page, selector: string, value: string) {
|
|
await page.locator(selector).click()
|
|
await page.evaluate(({ sel, val }: { sel: string; val: string }) => {
|
|
const el = document.querySelector(sel) as HTMLTextAreaElement
|
|
if (!el) return
|
|
const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set
|
|
setter?.call(el, val)
|
|
el.dispatchEvent(new Event('input', { bubbles: true }))
|
|
el.dispatchEvent(new Event('change', { bubbles: true }))
|
|
}, { sel: selector, val: value })
|
|
await page.waitForTimeout(500)
|
|
}
|
|
|
|
test.describe('Flujo completo — creacion y actualizacion de factura', () => {
|
|
|
|
test('1. crear proveedor', async ({ page }) => {
|
|
await page.goto('/dashboard/clients_and_providers')
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
await page.getByRole('link', { name: /Nuevo/ }).click()
|
|
await expect(page).toHaveURL(/edit/, { timeout: 10000 })
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
await fillInput(page, '#rfc', PROVEEDOR_RFC)
|
|
await fillInput(page, '#name', PROVEEDOR_NOMBRE)
|
|
|
|
await page.locator('#type').click()
|
|
await page.getByRole('option', { name: 'Proveedor' }).click()
|
|
|
|
await page.waitForTimeout(3000)
|
|
|
|
await page.getByRole('button', { name: /Guardar Socio|Actualizar Socio/ }).click()
|
|
|
|
await expect(page).toHaveURL(/clients_and_providers$/, { timeout: 15000 })
|
|
await expect(page.getByText(PROVEEDOR_NOMBRE)).toBeVisible()
|
|
})
|
|
|
|
test('2. crear cliente', async ({ page }) => {
|
|
await page.goto('/dashboard/clients_and_providers')
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
await page.getByRole('link', { name: /Nuevo/ }).click()
|
|
await expect(page).toHaveURL(/edit/, { timeout: 10000 })
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
await fillInput(page, '#rfc', CLIENTE_RFC)
|
|
await fillInput(page, '#name', CLIENTE_NOMBRE)
|
|
|
|
await page.locator('#type').click()
|
|
await page.getByRole('option', { name: 'Cliente' }).click()
|
|
|
|
await page.waitForTimeout(3000)
|
|
|
|
await page.getByRole('button', { name: /Guardar Socio|Actualizar Socio/ }).click()
|
|
|
|
await expect(page).toHaveURL(/clients_and_providers$/, { timeout: 15000 })
|
|
await expect(page.getByText(CLIENTE_NOMBRE)).toBeVisible()
|
|
})
|
|
|
|
test('3. crear agente aduanal', async ({ page }) => {
|
|
await page.goto('/dashboard/customs_brokers')
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
await page.getByRole('link', { name: /Nuevo/ }).click()
|
|
await expect(page).toHaveURL(/edit/, { timeout: 20000 })
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
await fillInput(page, 'input[placeholder="Ej. 550"]', BROKER_KEY)
|
|
await fillInput(page, 'input[placeholder="Ej. 3421"]', BROKER_LICENSE)
|
|
await fillInput(page, 'input[placeholder="Nombre oficial"]', `Agente E2E ${SUFFIX}`)
|
|
|
|
await page.getByRole('button', { name: /Guardar Agente|Actualizar Agente/ }).click()
|
|
|
|
await expect(page.getByText(/Agente creado|Agente actualizado/i)).toBeVisible({ timeout: 20000 })
|
|
await expect(page).toHaveURL(/customs_brokers$/, { timeout: 25000 })
|
|
})
|
|
|
|
test('4. crear tipo de cambio', async ({ page }) => {
|
|
await page.goto('/dashboard/general_catalogs/exchange-rate')
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
// Si ya existe TC del día (creado por auth.setup.ts), no intentar duplicar.
|
|
const todayCell = page.locator('tbody').getByText(new Date().toLocaleDateString('es-MX'))
|
|
if (await todayCell.first().isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
return
|
|
}
|
|
|
|
// El botón se llama "Nuevo Registro"; antes era "Nuevo Tipo de Cambio".
|
|
await page.getByRole('button', { name: /Nuevo Registro|Nuevo Tipo de Cambio/ }).click()
|
|
await expect(page.getByRole('heading', { name: 'Nuevo Tipo de Cambio' })).toBeVisible()
|
|
|
|
await page.locator('#date').fill(TODAY)
|
|
await page.waitForTimeout(3000)
|
|
|
|
await fillInput(page, '#value', '17.5')
|
|
|
|
await page.getByRole('button', { name: /^Ok$/ }).click()
|
|
await page.getByRole('button', { name: 'Confirmar' }).click()
|
|
|
|
await expect(page.getByText(/tipo de cambio creado|tipo de cambio actualizado/i)).toBeVisible({ timeout: 20000 })
|
|
})
|
|
|
|
test('5. crear pedimento', async ({ page }) => {
|
|
await page.goto('/dashboard/pedimentos/edit/new')
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
await expect(page.getByText('Nuevo Pedimento')).toBeVisible({ timeout: 20000 })
|
|
|
|
const prereqModal = page.getByRole('button', { name: /Continuar|Aceptar/ })
|
|
if (await prereqModal.isVisible()) await prereqModal.click()
|
|
|
|
await page.waitForTimeout(3000)
|
|
|
|
// Año — input con id estable
|
|
await fillInput(page, '#year', PEDIMENTO_YEAR)
|
|
|
|
// Aduana — bits-ui Select, 1er trigger de la fila superior
|
|
const triggers = page.locator('[data-select-trigger]')
|
|
await triggers.nth(0).click()
|
|
await page.waitForTimeout(2000)
|
|
await page.getByRole('option').first().click()
|
|
await page.waitForTimeout(1000)
|
|
|
|
// Patente — bits-ui Select, 2do trigger
|
|
await triggers.nth(1).click()
|
|
await page.waitForTimeout(2000)
|
|
await page.getByRole('option').first().click()
|
|
await page.waitForTimeout(1000)
|
|
|
|
// Número de pedimento — input con id estable
|
|
await fillInput(page, '#pedimento_number', PEDIMENTO_NUMBER)
|
|
|
|
// Clave — bits-ui Select, 3er trigger
|
|
await triggers.nth(2).click()
|
|
await page.waitForTimeout(2000)
|
|
await page.getByRole('option').first().click()
|
|
await page.waitForTimeout(1000)
|
|
|
|
// Tipo de Operación — bits-ui Select, 4to trigger
|
|
await triggers.nth(3).click()
|
|
await page.waitForTimeout(2000)
|
|
await page.getByRole('option').first().click()
|
|
await page.waitForTimeout(1000)
|
|
|
|
// Régimen — bits-ui Select, 5to trigger
|
|
await triggers.nth(4).click()
|
|
await page.waitForTimeout(2000)
|
|
await page.getByRole('option').first().click()
|
|
await page.waitForTimeout(2000)
|
|
|
|
await page.getByRole('button', { name: /Guardar Todos los Cambios/ }).click()
|
|
|
|
await expect(page).toHaveURL(/pedimentos$/, { timeout: 20000 })
|
|
await expect(page.getByText(/Pedimento creado/i)).toBeVisible({ timeout: 15000 })
|
|
})
|
|
|
|
test('6. crear factura de importacion TEM', async ({ page }) => {
|
|
await page.goto('/dashboard/invoices/edit/new?operation_type=imp&invoice_type=TEM')
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
await expect(page.getByText('Nueva Factura')).toBeVisible({ timeout: 20000 })
|
|
|
|
const prereqModal = page.getByRole('button', { name: /Continuar|Aceptar/ })
|
|
if (await prereqModal.isVisible()) await prereqModal.click()
|
|
|
|
await page.waitForTimeout(5000)
|
|
|
|
await page.locator('#invoice_number').click()
|
|
await page.keyboard.press('Control+A')
|
|
await page.locator('#invoice_number').pressSequentially(INVOICE_NUMBER, { delay: 1000 })
|
|
|
|
await page.waitForTimeout(5000)
|
|
|
|
await page.locator('#invoice_date').fill(TODAY)
|
|
await page.waitForTimeout(3000)
|
|
|
|
await page.getByRole('tab', { name: /General/ }).click()
|
|
await page.waitForTimeout(5000)
|
|
|
|
await page.locator('#provider_id').click()
|
|
await page.waitForTimeout(3000)
|
|
await page.getByRole('option').first().click()
|
|
|
|
await page.locator('#sold_to_id').click()
|
|
await page.waitForTimeout(3000)
|
|
await page.getByRole('option').first().click()
|
|
|
|
await page.locator('#shipped_to_id').click()
|
|
await page.waitForTimeout(3000)
|
|
await page.getByRole('option').first().click()
|
|
|
|
await page.locator('#customs_broker_id').click()
|
|
await page.waitForTimeout(3000)
|
|
await page.getByRole('option').first().click()
|
|
|
|
await page.locator('#aduana').click()
|
|
await page.waitForTimeout(3000)
|
|
await page.getByRole('option').first().click()
|
|
|
|
await page.locator('#document_type').click()
|
|
await page.waitForTimeout(3000)
|
|
await page.getByRole('option').first().click()
|
|
|
|
await page.getByRole('button', { name: /Guardar Todos los Cambios/ }).click()
|
|
await expect(page.getByText('Todos los cambios se guardaron correctamente')).toBeVisible({ timeout: 15000 })
|
|
// Guardar numero de factura para tests posteriores
|
|
saveShared({ INVOICE_NUMBER })
|
|
})
|
|
|
|
test('7. factura aparece en la lista de importacion', async ({ page }) => {
|
|
const shared = loadShared()
|
|
const invoiceNumber = shared.INVOICE_NUMBER || INVOICE_NUMBER
|
|
|
|
await page.goto('/dashboard/invoices?operation_type=imp&invoice_type=TEM')
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
await expect(page.locator('main')).toBeVisible()
|
|
|
|
await fillInput(page, '#filter-invoice-number, input[placeholder="No. Factura"]', invoiceNumber)
|
|
await page.waitForTimeout(8000)
|
|
|
|
await expect(page.locator('tbody').getByText(invoiceNumber).first()).toBeVisible({ timeout: 10000 })
|
|
})
|
|
|
|
test('8. agregar partida a la factura', async ({ page }) => {
|
|
const shared = loadShared()
|
|
const invoiceNumber = shared.INVOICE_NUMBER || INVOICE_NUMBER
|
|
|
|
await page.goto('/dashboard/invoices?operation_type=imp&invoice_type=TEM')
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
await fillInput(page, '#filter-invoice-number, input[placeholder="No. Factura"]', invoiceNumber)
|
|
await expect(page.locator('tbody').getByText(invoiceNumber).first()).toBeVisible({ timeout: 20000 })
|
|
|
|
await page.locator('tbody').getByText(invoiceNumber).first().click()
|
|
await page.getByRole('button', { name: /Editar/ }).click()
|
|
|
|
await expect(page).toHaveURL(/invoices\/edit\/\d+/, { timeout: 10000 })
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
// Ir a pestaña Partidas
|
|
await page.getByRole('tab', { name: /Partidas/ }).click()
|
|
await page.waitForTimeout(3000)
|
|
|
|
// Abrir sheet de nueva partida
|
|
await page.getByRole('button', { name: /Agregar Partidas/ }).click()
|
|
await page.waitForTimeout(3000)
|
|
|
|
// Clase — abre un dialog de búsqueda con tabla, scopear al dialog activo
|
|
await page.locator('#clase').click()
|
|
await page.waitForTimeout(3000)
|
|
// El dialog de clase tiene data-nested y está encima del sheet
|
|
// Scopear al último dialog abierto para evitar que el sheet intercepte
|
|
const claseDialog = page.locator('[data-dialog-content]').last()
|
|
await claseDialog.locator('tbody tr').first().click()
|
|
await page.waitForTimeout(2000)
|
|
|
|
// Unidad de medida — mismo patron
|
|
await page.locator('#um').click()
|
|
await page.waitForTimeout(3000)
|
|
const umDialog = page.locator('[data-dialog-content]').last()
|
|
await umDialog.locator('tbody tr').first().click()
|
|
await page.waitForTimeout(2000)
|
|
|
|
// País de origen — abre dialog con tabla igual que clase y UM
|
|
await page.locator('#pais_origen').click()
|
|
await page.waitForTimeout(3000)
|
|
const paisDialog = page.locator('[data-dialog-content]').last()
|
|
await paisDialog.locator('tbody tr').first().click()
|
|
await page.waitForTimeout(2000)
|
|
|
|
// Cantidad
|
|
await fillInput(page, '#cantidad', '10')
|
|
|
|
// Costo unitario
|
|
await fillInput(page, '#costo_unitario', '100')
|
|
|
|
// Peso neto y bruto — requeridos por el backend
|
|
await fillInput(page, '#peso_neto', '5')
|
|
await fillInput(page, '#peso_bruto', '6')
|
|
|
|
// Descripción en español (textarea)
|
|
await fillTextarea(page, '#desc_espanol', `Partida E2E ${SUFFIX}`)
|
|
|
|
// Guardar partida — botón "Crear" dentro del sheet
|
|
const sheet = page.locator('[data-slot="sheet-content"]')
|
|
await sheet.getByRole('button', { name: /^(Crear|Guardar)$/i }).click()
|
|
await page.waitForTimeout(3000)
|
|
|
|
// Verificar que la partida aparece en la tabla
|
|
await expect(page.locator('tbody').first()).toBeVisible({ timeout: 10000 })
|
|
|
|
// Guardar factura completa
|
|
await page.getByRole('button', { name: /Guardar Todos los Cambios/ }).click()
|
|
await expect(page.getByText('Todos los cambios se guardaron correctamente')).toBeVisible({ timeout: 15000 })
|
|
})
|
|
|
|
test('9. editar factura existente', async ({ page }) => {
|
|
await page.goto('/dashboard/invoices?operation_type=imp&invoice_type=TEM')
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
const shared9 = loadShared()
|
|
const invoiceNumber9 = shared9.INVOICE_NUMBER || INVOICE_NUMBER
|
|
|
|
await fillInput(page, '#filter-invoice-number, input[placeholder="No. Factura"]', invoiceNumber9)
|
|
await expect(page.locator('tbody').getByText(invoiceNumber9).first()).toBeVisible({ timeout: 20000 })
|
|
|
|
await page.locator('tbody').getByText(invoiceNumber9).first().click()
|
|
await page.getByRole('button', { name: /Editar/ }).click()
|
|
|
|
await expect(page).toHaveURL(/invoices\/edit\/\d+/, { timeout: 10000 })
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
await expect(page.getByText(/Factura #/)).toBeVisible()
|
|
|
|
await page.getByRole('tab', { name: /General/ }).click()
|
|
await page.waitForTimeout(5000)
|
|
|
|
await page.locator('#customs_broker_id').click()
|
|
await page.waitForTimeout(3000)
|
|
await page.getByRole('option').first().click()
|
|
|
|
await page.locator('#aduana').click()
|
|
await page.waitForTimeout(3000)
|
|
await page.getByRole('option').first().click()
|
|
|
|
await page.locator('#document_type').click()
|
|
await page.waitForTimeout(3000)
|
|
await page.getByRole('option').first().click()
|
|
|
|
await page.getByRole('button', { name: /Guardar Todos los Cambios/ }).click()
|
|
await expect(page.getByText('Todos los cambios se guardaron correctamente')).toBeVisible({ timeout: 15000 })
|
|
})
|
|
|
|
test('10. editar partida existente', async ({ page }) => {
|
|
const shared10 = loadShared()
|
|
const invoiceNumber10 = shared10.INVOICE_NUMBER || INVOICE_NUMBER
|
|
|
|
await page.goto('/dashboard/invoices?operation_type=imp&invoice_type=TEM')
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
await fillInput(page, '#filter-invoice-number, input[placeholder="No. Factura"]', invoiceNumber10)
|
|
await expect(page.locator('tbody').getByText(invoiceNumber10).first()).toBeVisible({ timeout: 20000 })
|
|
|
|
await page.locator('tbody').getByText(invoiceNumber10).first().click()
|
|
await page.getByRole('button', { name: /Editar/ }).click()
|
|
|
|
await expect(page).toHaveURL(/invoices\/edit\/\d+/, { timeout: 10000 })
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
// Ir a pestaña Partidas
|
|
await page.getByRole('tab', { name: /Partidas/ }).click()
|
|
await page.waitForTimeout(3000)
|
|
|
|
// Abrir sheet de edición — la fila de la partida contiene "E2E01"; el primer botón
|
|
// de la cell de acciones es editar.
|
|
const partidaRow = page.locator('tbody tr').filter({ hasText: 'E2E01' }).first()
|
|
await partidaRow.locator('button').first().click({ force: true, timeout: 10000 })
|
|
await page.waitForTimeout(3000)
|
|
|
|
// Modificar cantidad
|
|
await fillInput(page, '#cantidad', '20')
|
|
|
|
// Modificar costo unitario
|
|
await fillInput(page, '#costo_unitario', '200')
|
|
|
|
// Guardar partida editada — botón "Actualizar" dentro del sheet
|
|
const sheetEdit = page.locator('[data-slot="sheet-content"]')
|
|
await sheetEdit.getByRole('button', { name: /^(Actualizar|Guardar)$/i }).click()
|
|
await page.waitForTimeout(3000)
|
|
|
|
// Guardar factura completa
|
|
await page.getByRole('button', { name: /Guardar Todos los Cambios/ }).click()
|
|
await expect(page.getByText('Todos los cambios se guardaron correctamente')).toBeVisible({ timeout: 15000 })
|
|
})
|
|
|
|
test('11. actualizar factura — verificacion final', async ({ page }) => {
|
|
const shared11 = loadShared()
|
|
const invoiceNumber11 = shared11.INVOICE_NUMBER || INVOICE_NUMBER
|
|
|
|
await page.goto('/dashboard/invoices?operation_type=imp&invoice_type=TEM')
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
await fillInput(page, '#filter-invoice-number, input[placeholder="No. Factura"]', invoiceNumber11)
|
|
await expect(page.locator('tbody').getByText(invoiceNumber11).first()).toBeVisible({ timeout: 20000 })
|
|
|
|
// Seleccionar la fila
|
|
await page.locator('tbody tr').first().click()
|
|
await page.waitForTimeout(2000)
|
|
|
|
// Click en boton Actualizar del footer — tiene h-8, gap-1.5 y border
|
|
await page.locator('button.h-8:has([class*="lucide-refresh"])').click()
|
|
await page.waitForTimeout(5000)
|
|
|
|
// Verificar resultado
|
|
await expect(page.getByText(/actualiz|procesad|exito/i).first()).toBeVisible({ timeout: 20000 })
|
|
})
|
|
|
|
}) |