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) { fs.writeFileSync(SHARED_FILE, JSON.stringify(data)) } function loadShared(): Record { 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(300) } 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(500) 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(500) 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(500) 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(500) // 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(800) await page.getByRole('option').first().click() await page.waitForTimeout(500) // Patente — bits-ui Select, 2do trigger await triggers.nth(1).click() await page.waitForTimeout(800) await page.getByRole('option').first().click() await page.waitForTimeout(500) // 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(800) await page.getByRole('option').first().click() await page.waitForTimeout(500) // Tipo de Operación — bits-ui Select, 4to trigger await triggers.nth(3).click() await page.waitForTimeout(800) await page.getByRole('option').first().click() await page.waitForTimeout(500) // Régimen — bits-ui Select, 5to trigger await triggers.nth(4).click() await page.waitForTimeout(800) await page.getByRole('option').first().click() await page.waitForTimeout(800) 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(1500) await page.locator('#invoice_number').click() await page.keyboard.press('Control+A') await page.locator('#invoice_number').pressSequentially(INVOICE_NUMBER, { delay: 50 }) await page.waitForTimeout(1500) await page.locator('#invoice_date').fill(TODAY) await page.waitForTimeout(500) await page.getByRole('tab', { name: /General/ }).click() await page.waitForTimeout(1500) await page.locator('#provider_id').click() await page.waitForTimeout(500) await page.getByRole('option').first().click() await page.locator('#sold_to_id').click() await page.waitForTimeout(500) await page.getByRole('option').first().click() await page.locator('#shipped_to_id').click() await page.waitForTimeout(500) await page.getByRole('option').first().click() await page.locator('#customs_broker_id').click() await page.waitForTimeout(500) await page.getByRole('option').first().click() await page.locator('#aduana').click() await page.waitForTimeout(500) await page.getByRole('option').first().click() await page.locator('#document_type').click() await page.waitForTimeout(500) 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(2000) 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(800) // Abrir sheet de nueva partida await page.getByRole('button', { name: /Agregar Partidas/ }).click() await page.waitForTimeout(1000) // Clase — abre un dialog de búsqueda con tabla, scopear al dialog activo await page.locator('#clase').click() await page.waitForTimeout(800) // 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(500) // Unidad de medida — mismo patron await page.locator('#um').click() await page.waitForTimeout(800) const umDialog = page.locator('[data-dialog-content]').last() await umDialog.locator('tbody tr').first().click() await page.waitForTimeout(500) // País de origen — abre dialog con tabla igual que clase y UM await page.locator('#pais_origen').click() await page.waitForTimeout(800) const paisDialog = page.locator('[data-dialog-content]').last() await paisDialog.locator('tbody tr').first().click() await page.waitForTimeout(500) // 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(800) // 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(1500) await page.locator('#customs_broker_id').click() await page.waitForTimeout(500) await page.getByRole('option').first().click() await page.locator('#aduana').click() await page.waitForTimeout(500) await page.getByRole('option').first().click() await page.locator('#document_type').click() await page.waitForTimeout(500) 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(800) // 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(1000) // 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(800) // 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(800) // 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(1500) // Verificar resultado await expect(page.getByText(/actualiz|procesad|exito/i).first()).toBeVisible({ timeout: 20000 }) }) })