feat: Implementar tests

main
parent 7311693ed2
commit 575b70439c

@ -0,0 +1,156 @@
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\ProcesoAdmision;
use Illuminate\Foundation\Testing\RefreshDatabase;
class ProcesoAdmisionPublicadosTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
// Ensure the procesos_admision table exists
$this->artisan('migrate', ['--path' => 'database/migrations']);
// Create table if doesn't exist (for testing with SQLite)
\Illuminate\Support\Facades\Schema::dropIfExists('procesos_admision');
\Illuminate\Support\Facades\Schema::create('procesos_admision', function ($table) {
$table->id();
$table->string('titulo');
$table->string('subtitulo')->nullable();
$table->text('descripcion')->nullable();
$table->string('slug', 120)->unique();
$table->string('tipo_proceso', 60)->nullable();
$table->string('modalidad', 50)->nullable();
$table->boolean('publicado')->default(false);
$table->datetime('fecha_publicacion')->nullable();
$table->datetime('fecha_inicio_preinscripcion')->nullable();
$table->datetime('fecha_fin_preinscripcion')->nullable();
$table->datetime('fecha_inicio_inscripcion')->nullable();
$table->datetime('fecha_fin_inscripcion')->nullable();
$table->datetime('fecha_examen1')->nullable();
$table->datetime('fecha_examen2')->nullable();
$table->datetime('fecha_resultados')->nullable();
$table->datetime('fecha_inicio_biometrico')->nullable();
$table->datetime('fecha_fin_biometrico')->nullable();
$table->string('imagen_path', 500)->nullable();
$table->string('banner_path', 500)->nullable();
$table->string('brochure_path', 500)->nullable();
$table->string('link_preinscripcion', 500)->nullable();
$table->string('link_inscripcion', 500)->nullable();
$table->string('link_resultados', 500)->nullable();
$table->string('link_reglamento', 500)->nullable();
$table->enum('estado', ['nuevo', 'publicado', 'en_proceso', 'finalizado', 'cancelado'])->default('nuevo');
$table->timestamps();
});
}
/** @test */
public function publicados_endpoint_returns_only_published_processes(): void
{
// Create published processes
$published1 = ProcesoAdmision::create([
'titulo' => 'Proceso Publicado 1',
'slug' => 'proceso-publicado-1',
'publicado' => true,
'estado' => 'publicado',
]);
$published2 = ProcesoAdmision::create([
'titulo' => 'Proceso Publicado 2',
'slug' => 'proceso-publicado-2',
'publicado' => true,
'estado' => 'en_proceso',
]);
// Create unpublished processes
ProcesoAdmision::create([
'titulo' => 'Proceso No Publicado 1',
'slug' => 'proceso-no-publicado-1',
'publicado' => false,
'estado' => 'nuevo',
]);
ProcesoAdmision::create([
'titulo' => 'Proceso No Publicado 2',
'slug' => 'proceso-no-publicado-2',
'publicado' => false,
'estado' => 'finalizado',
]);
// Call the publicados endpoint
$response = $this->getJson('/api/procesos-admision/publicados');
// Assert response is successful
$response->assertStatus(200);
// Assert only published processes are returned
$response->assertJsonCount(2);
// Assert the returned processes are the published ones
$responseData = $response->json();
$returnedTitles = array_column($responseData, 'titulo');
$this->assertContains('Proceso Publicado 1', $returnedTitles);
$this->assertContains('Proceso Publicado 2', $returnedTitles);
$this->assertNotContains('Proceso No Publicado 1', $returnedTitles);
$this->assertNotContains('Proceso No Publicado 2', $returnedTitles);
}
/** @test */
public function publicados_endpoint_returns_empty_array_when_no_published_processes(): void
{
// Create only unpublished processes
ProcesoAdmision::create([
'titulo' => 'Proceso No Publicado',
'slug' => 'proceso-no-publicado',
'publicado' => false,
'estado' => 'nuevo',
]);
$response = $this->getJson('/api/procesos-admision/publicados');
$response->assertStatus(200);
$response->assertJsonCount(0);
$response->assertJson([]);
}
/** @test */
public function publicados_endpoint_returns_processes_ordered_by_id_desc(): void
{
// Create published processes
$first = ProcesoAdmision::create([
'titulo' => 'Primer Proceso',
'slug' => 'primer-proceso',
'publicado' => true,
]);
$second = ProcesoAdmision::create([
'titulo' => 'Segundo Proceso',
'slug' => 'segundo-proceso',
'publicado' => true,
]);
$third = ProcesoAdmision::create([
'titulo' => 'Tercer Proceso',
'slug' => 'tercer-proceso',
'publicado' => true,
]);
$response = $this->getJson('/api/procesos-admision/publicados');
$response->assertStatus(200);
$responseData = $response->json();
// Should be ordered by id desc (newest first)
$this->assertEquals('Tercer Proceso', $responseData[0]['titulo']);
$this->assertEquals('Segundo Proceso', $responseData[1]['titulo']);
$this->assertEquals('Primer Proceso', $responseData[2]['titulo']);
}
}

@ -0,0 +1,213 @@
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\User;
use App\Models\ProcesoAdmision;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
class ProcesoAdmisionUpdateNullableTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->artisan('migrate', ['--path' => 'database/migrations']);
// Create procesos_admision table
\Illuminate\Support\Facades\Schema::dropIfExists('procesos_admision');
\Illuminate\Support\Facades\Schema::create('procesos_admision', function ($table) {
$table->id();
$table->string('titulo');
$table->string('subtitulo')->nullable();
$table->text('descripcion')->nullable();
$table->string('slug', 120)->unique();
$table->string('tipo_proceso', 60)->nullable();
$table->string('modalidad', 50)->nullable();
$table->boolean('publicado')->default(false);
$table->datetime('fecha_publicacion')->nullable();
$table->datetime('fecha_inicio_preinscripcion')->nullable();
$table->datetime('fecha_fin_preinscripcion')->nullable();
$table->datetime('fecha_inicio_inscripcion')->nullable();
$table->datetime('fecha_fin_inscripcion')->nullable();
$table->datetime('fecha_examen1')->nullable();
$table->datetime('fecha_examen2')->nullable();
$table->datetime('fecha_resultados')->nullable();
$table->datetime('fecha_inicio_biometrico')->nullable();
$table->datetime('fecha_fin_biometrico')->nullable();
$table->string('imagen_path', 500)->nullable();
$table->string('banner_path', 500)->nullable();
$table->string('brochure_path', 500)->nullable();
$table->string('link_preinscripcion', 500)->nullable();
$table->string('link_inscripcion', 500)->nullable();
$table->string('link_resultados', 500)->nullable();
$table->string('link_reglamento', 500)->nullable();
$table->enum('estado', ['nuevo', 'publicado', 'en_proceso', 'finalizado', 'cancelado'])->default('nuevo');
$table->timestamps();
});
}
protected function authenticateUser(): void
{
$user = User::factory()->create();
Sanctum::actingAs($user);
}
/** @test */
public function update_sets_nullable_fields_to_null_when_empty_values_provided(): void
{
$this->authenticateUser();
// Create a process with filled nullable fields
$proceso = ProcesoAdmision::create([
'titulo' => 'Proceso Test',
'slug' => 'proceso-test',
'subtitulo' => 'Subtitulo Original',
'descripcion' => 'Descripcion Original',
'tipo_proceso' => 'ordinario',
'modalidad' => 'presencial',
'fecha_publicacion' => '2026-01-15 10:00:00',
'fecha_inicio_preinscripcion' => '2026-01-20 08:00:00',
'fecha_fin_preinscripcion' => '2026-01-25 18:00:00',
'link_preinscripcion' => 'https://example.com/preinscripcion',
'link_inscripcion' => 'https://example.com/inscripcion',
]);
// Verify initial values
$this->assertEquals('Subtitulo Original', $proceso->subtitulo);
$this->assertEquals('Descripcion Original', $proceso->descripcion);
$this->assertEquals('ordinario', $proceso->tipo_proceso);
$this->assertNotNull($proceso->fecha_publicacion);
$this->assertNotNull($proceso->link_preinscripcion);
// Update with empty values for nullable fields
$response = $this->patchJson("/api/admin/procesos-admision/{$proceso->id}", [
'subtitulo' => '',
'descripcion' => null,
'tipo_proceso' => '',
'fecha_publicacion' => null,
'link_preinscripcion' => '',
]);
$response->assertStatus(200);
// Refresh the model from database
$proceso->refresh();
// Assert nullable fields are now null
$this->assertNull($proceso->subtitulo);
$this->assertNull($proceso->descripcion);
$this->assertNull($proceso->tipo_proceso);
$this->assertNull($proceso->fecha_publicacion);
$this->assertNull($proceso->link_preinscripcion);
// Assert other fields remain unchanged
$this->assertEquals('Proceso Test', $proceso->titulo);
$this->assertEquals('presencial', $proceso->modalidad);
$this->assertEquals('https://example.com/inscripcion', $proceso->link_inscripcion);
}
/** @test */
public function update_sets_date_fields_to_null_when_empty(): void
{
$this->authenticateUser();
$proceso = ProcesoAdmision::create([
'titulo' => 'Proceso Con Fechas',
'slug' => 'proceso-con-fechas',
'fecha_inicio_inscripcion' => '2026-02-01 08:00:00',
'fecha_fin_inscripcion' => '2026-02-15 18:00:00',
'fecha_examen1' => '2026-02-20 09:00:00',
'fecha_examen2' => '2026-02-21 09:00:00',
'fecha_resultados' => '2026-03-01 12:00:00',
]);
// Update dates to empty/null
$response = $this->patchJson("/api/admin/procesos-admision/{$proceso->id}", [
'fecha_inicio_inscripcion' => null,
'fecha_fin_inscripcion' => '',
'fecha_examen1' => null,
'fecha_examen2' => '',
'fecha_resultados' => null,
]);
$response->assertStatus(200);
$proceso->refresh();
// All date fields should be null
$this->assertNull($proceso->fecha_inicio_inscripcion);
$this->assertNull($proceso->fecha_fin_inscripcion);
$this->assertNull($proceso->fecha_examen1);
$this->assertNull($proceso->fecha_examen2);
$this->assertNull($proceso->fecha_resultados);
}
/** @test */
public function update_sets_link_fields_to_null_when_empty(): void
{
$this->authenticateUser();
$proceso = ProcesoAdmision::create([
'titulo' => 'Proceso Con Links',
'slug' => 'proceso-con-links',
'link_preinscripcion' => 'https://example.com/preinscripcion',
'link_inscripcion' => 'https://example.com/inscripcion',
'link_resultados' => 'https://example.com/resultados',
'link_reglamento' => 'https://example.com/reglamento',
]);
// Update links to empty
$response = $this->patchJson("/api/admin/procesos-admision/{$proceso->id}", [
'link_preinscripcion' => '',
'link_inscripcion' => null,
'link_resultados' => '',
'link_reglamento' => null,
]);
$response->assertStatus(200);
$proceso->refresh();
// All link fields should be null
$this->assertNull($proceso->link_preinscripcion);
$this->assertNull($proceso->link_inscripcion);
$this->assertNull($proceso->link_resultados);
$this->assertNull($proceso->link_reglamento);
}
/** @test */
public function update_does_not_affect_fields_not_in_request(): void
{
$this->authenticateUser();
$proceso = ProcesoAdmision::create([
'titulo' => 'Proceso Original',
'slug' => 'proceso-original',
'subtitulo' => 'Subtitulo que no debe cambiar',
'descripcion' => 'Descripcion que no debe cambiar',
'link_preinscripcion' => 'https://example.com/link',
]);
// Update only titulo (subtitulo, descripcion, link not in request)
$response = $this->patchJson("/api/admin/procesos-admision/{$proceso->id}", [
'titulo' => 'Titulo Actualizado',
]);
$response->assertStatus(200);
$proceso->refresh();
// Updated field
$this->assertEquals('Titulo Actualizado', $proceso->titulo);
// Fields not in request should remain unchanged
$this->assertEquals('Subtitulo que no debe cambiar', $proceso->subtitulo);
$this->assertEquals('Descripcion que no debe cambiar', $proceso->descripcion);
$this->assertEquals('https://example.com/link', $proceso->link_preinscripcion);
}
}

@ -6,7 +6,9 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
"preview": "vite preview",
"test": "vitest",
"test:run": "vitest run"
},
"dependencies": {
"@vueup/vue-quill": "^1.2.0",
@ -29,6 +31,9 @@
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"vite": "^7.2.4"
"@vue/test-utils": "^2.4.6",
"happy-dom": "^15.11.7",
"vite": "^7.2.4",
"vitest": "^2.1.8"
}
}

@ -0,0 +1,313 @@
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { h } from 'vue'
import ConvocatoriasSection from '../../src/components/WebPageSections/ConvocatoriasSection.vue'
// Mock Ant Design Vue components
const mockAntComponents = {
'a-badge': {
template: '<span class="a-badge"><slot /></span>',
props: ['count']
},
'a-card': {
template: '<div class="a-card"><slot /></div>'
},
'a-tag': {
template: '<span class="a-tag" :class="color"><slot /></span>',
props: ['color']
},
'a-divider': {
template: '<hr class="a-divider" />'
},
'a-button': {
template: '<button class="a-button" @click="$emit(\'click\')"><slot /><slot name="icon" /></button>',
props: ['type', 'size', 'ghost'],
emits: ['click']
},
'a-image': {
template: '<img class="a-image" :src="src" :alt="alt" />',
props: ['src', 'alt', 'preview']
},
'a-empty': {
template: '<div class="a-empty"><slot /></div>',
props: ['description']
}
}
// Mock icons
const mockIcons = {
FileTextOutlined: { template: '<span class="icon file-text" />' },
DollarOutlined: { template: '<span class="icon dollar" />' },
TeamOutlined: { template: '<span class="icon team" />' },
CalendarOutlined: { template: '<span class="icon calendar" />' },
FormOutlined: { template: '<span class="icon form" />' }
}
const createWrapper = (props = {}) => {
return mount(ConvocatoriasSection, {
props,
global: {
stubs: {
...mockAntComponents,
...mockIcons
}
}
})
}
describe('ConvocatoriasSection', () => {
describe('when no procesos are provided', () => {
it('renders empty state message', () => {
const wrapper = createWrapper({ procesos: [] })
expect(wrapper.find('.empty-state').exists()).toBe(true)
expect(wrapper.text()).toContain('No hay convocatorias vigentes en este momento')
})
it('does not render main or secondary cards', () => {
const wrapper = createWrapper({ procesos: [] })
expect(wrapper.find('.main-convocatoria-card').exists()).toBe(false)
expect(wrapper.find('.secondary-list').exists()).toBe(false)
})
})
describe('when one proceso is provided', () => {
const singleProceso = [
{
id: 1,
titulo: 'Admisión Ordinaria 2026',
subtitulo: 'Proceso regular de admisión',
descripcion: 'Descripción del proceso de admisión',
estado: 'publicado',
fecha_inicio_inscripcion: '2026-02-01',
fecha_fin_inscripcion: '2026-02-28',
link_preinscripcion: 'https://example.com/preinscripcion'
}
]
it('renders main convocatoria card with correct title', () => {
const wrapper = createWrapper({ procesos: singleProceso })
expect(wrapper.find('.main-convocatoria-card').exists()).toBe(true)
expect(wrapper.find('h3').text()).toBe('Admisión Ordinaria 2026')
})
it('renders the "Principal" badge on main card', () => {
const wrapper = createWrapper({ procesos: singleProceso })
expect(wrapper.find('.card-badge').exists()).toBe(true)
expect(wrapper.find('.card-badge').text()).toBe('Principal')
})
it('renders action buttons for requisitos, pagos, vacantes, and cronograma', () => {
const wrapper = createWrapper({ procesos: singleProceso })
const actionButtons = wrapper.findAll('.action-btn')
expect(actionButtons.length).toBe(4)
const buttonTexts = actionButtons.map(btn => btn.text())
expect(buttonTexts).toContain('Requisitos')
expect(buttonTexts).toContain('Pagos')
expect(buttonTexts).toContain('Vacantes')
expect(buttonTexts).toContain('Cronograma')
})
it('does not render secondary cards list when only one proceso', () => {
const wrapper = createWrapper({ procesos: singleProceso })
expect(wrapper.find('.secondary-list').exists()).toBe(false)
})
it('displays the estado tag with correct label', () => {
const wrapper = createWrapper({ procesos: singleProceso })
const statusTag = wrapper.find('.status-tag')
expect(statusTag.exists()).toBe(true)
expect(statusTag.text()).toBe('Abierto') // 'publicado' maps to 'Abierto'
})
it('displays inscription dates when provided', () => {
const wrapper = createWrapper({ procesos: singleProceso })
expect(wrapper.text()).toContain('Inscripciones:')
})
})
describe('when multiple procesos are provided', () => {
const multipleProcesos = [
{
id: 1,
titulo: 'Proceso Principal',
descripcion: 'Descripción del proceso principal',
estado: 'publicado'
},
{
id: 2,
titulo: 'Proceso Secundario 1',
descripcion: 'Descripción secundario 1',
estado: 'en_proceso'
},
{
id: 3,
titulo: 'Proceso Secundario 2',
descripcion: 'Descripción secundario 2',
estado: 'nuevo'
}
]
it('renders main card for first proceso', () => {
const wrapper = createWrapper({ procesos: multipleProcesos })
expect(wrapper.find('.main-convocatoria-card').exists()).toBe(true)
expect(wrapper.find('h3').text()).toBe('Proceso Principal')
})
it('renders secondary cards for remaining procesos', () => {
const wrapper = createWrapper({ procesos: multipleProcesos })
expect(wrapper.find('.secondary-list').exists()).toBe(true)
const secondaryCards = wrapper.findAll('.secondary-convocatoria-card')
expect(secondaryCards.length).toBe(2)
})
it('renders correct titles in secondary cards', () => {
const wrapper = createWrapper({ procesos: multipleProcesos })
const secondaryTitles = wrapper.findAll('.secondary-title')
expect(secondaryTitles[0].text()).toBe('Proceso Secundario 1')
expect(secondaryTitles[1].text()).toBe('Proceso Secundario 2')
})
it('renders estado tags with correct colors for different estados', () => {
const wrapper = createWrapper({ procesos: multipleProcesos })
const statusTags = wrapper.findAll('.status-tag')
// First is main card (publicado), then secondary cards
expect(statusTags[0].text()).toBe('Abierto') // publicado
expect(statusTags[1].text()).toBe('En Proceso') // en_proceso
expect(statusTags[2].text()).toBe('PRÓXIMAMENTE') // nuevo
})
})
describe('emits events correctly', () => {
const procesos = [
{
id: 1,
titulo: 'Proceso Test',
estado: 'publicado'
},
{
id: 2,
titulo: 'Proceso Secundario',
estado: 'publicado'
}
]
it('emits show-modal event with correct payload when action button is clicked', async () => {
const wrapper = createWrapper({ procesos })
const actionButtons = wrapper.findAll('.action-btn')
await actionButtons[0].trigger('click') // Requisitos button
expect(wrapper.emitted('show-modal')).toBeTruthy()
expect(wrapper.emitted('show-modal')[0]).toEqual([
{ procesoId: 1, tipo: 'requisitos' }
])
})
it('emits show-modal for pagos button', async () => {
const wrapper = createWrapper({ procesos })
const actionButtons = wrapper.findAll('.action-btn')
await actionButtons[1].trigger('click') // Pagos button
expect(wrapper.emitted('show-modal')[0]).toEqual([
{ procesoId: 1, tipo: 'pagos' }
])
})
it('emits show-modal for vacantes button', async () => {
const wrapper = createWrapper({ procesos })
const actionButtons = wrapper.findAll('.action-btn')
await actionButtons[2].trigger('click') // Vacantes button
expect(wrapper.emitted('show-modal')[0]).toEqual([
{ procesoId: 1, tipo: 'vacantes' }
])
})
it('emits show-modal for cronograma button', async () => {
const wrapper = createWrapper({ procesos })
const actionButtons = wrapper.findAll('.action-btn')
await actionButtons[3].trigger('click') // Cronograma button
expect(wrapper.emitted('show-modal')[0]).toEqual([
{ procesoId: 1, tipo: 'cronograma' }
])
})
})
describe('estado mapping', () => {
const testEstadoCases = [
{ estado: 'publicado', expectedLabel: 'Abierto' },
{ estado: 'en_proceso', expectedLabel: 'En Proceso' },
{ estado: 'nuevo', expectedLabel: 'PRÓXIMAMENTE' },
{ estado: 'finalizado', expectedLabel: 'FINALIZADO' },
{ estado: 'cancelado', expectedLabel: 'CANCELADO' }
]
testEstadoCases.forEach(({ estado, expectedLabel }) => {
it(`maps estado "${estado}" to label "${expectedLabel}"`, () => {
const wrapper = createWrapper({
procesos: [{ id: 1, titulo: 'Test', estado }]
})
expect(wrapper.find('.status-tag').text()).toBe(expectedLabel)
})
})
})
describe('description display', () => {
it('displays descripcion when provided', () => {
const wrapper = createWrapper({
procesos: [{
id: 1,
titulo: 'Test',
descripcion: 'Esta es la descripción del proceso',
estado: 'publicado'
}]
})
expect(wrapper.find('.convocatoria-desc').text()).toBe('Esta es la descripción del proceso')
})
it('falls back to subtitulo when descripcion is not provided', () => {
const wrapper = createWrapper({
procesos: [{
id: 1,
titulo: 'Test',
subtitulo: 'Este es el subtítulo',
estado: 'publicado'
}]
})
expect(wrapper.find('.convocatoria-desc').text()).toBe('Este es el subtítulo')
})
it('shows default text when neither descripcion nor subtitulo is provided', () => {
const wrapper = createWrapper({
procesos: [{
id: 1,
titulo: 'Test',
estado: 'publicado'
}]
})
expect(wrapper.find('.convocatoria-desc').text()).toBe('Proceso de admisión')
})
})
})

@ -0,0 +1,396 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { mount, shallowMount } from '@vue/test-utils'
import { ref, computed, nextTick } from 'vue'
import { setActivePinia, createPinia } from 'pinia'
// Since WebPage.vue has many dependencies, we'll test the showModal logic in isolation
// by extracting the logic into a testable unit
describe('WebPage showModal function', () => {
// Simulate the showModal function logic from WebPage.vue
const tipoLabels = {
requisitos: 'Requisitos',
pagos: 'Pagos',
vacantes: 'Vacantes',
cronograma: 'Cronograma'
}
const createShowModal = (procesosPublicados) => {
const detalleModal = ref({
titulo: '',
descripcion: '',
imagen_url: null,
imagen_url_2: null,
listas: []
})
const detalleModalVisible = ref(false)
const showModal = ({ procesoId, tipo }) => {
const proceso = procesosPublicados.value.find(p => p.id === procesoId)
if (!proceso) return
const detalle = proceso.detalles?.find(d => d.tipo === tipo)
if (detalle) {
detalleModal.value = {
titulo: detalle.titulo_detalle || tipoLabels[tipo] || tipo,
descripcion: detalle.descripcion || '',
imagen_url: detalle.imagen_url || null,
imagen_url_2: detalle.imagen_url_2 || null,
listas: detalle.listas || []
}
} else {
detalleModal.value = {
titulo: `${tipoLabels[tipo] || tipo} - ${proceso.titulo}`,
descripcion: '',
imagen_url: null,
imagen_url_2: null,
listas: []
}
}
detalleModalVisible.value = true
}
return { showModal, detalleModal, detalleModalVisible }
}
describe('when proceso has matching detalle', () => {
it('displays detalle information when detalle exists for the tipo', () => {
const procesosPublicados = ref([
{
id: 1,
titulo: 'Admisión 2026',
detalles: [
{
tipo: 'requisitos',
titulo_detalle: 'Requisitos de Admisión',
descripcion: 'Lista de requisitos necesarios para postular',
imagen_url: 'https://example.com/requisitos.jpg',
imagen_url_2: 'https://example.com/requisitos2.jpg',
listas: ['DNI vigente', 'Certificado de estudios', 'Foto carnet']
}
]
}
])
const { showModal, detalleModal, detalleModalVisible } = createShowModal(procesosPublicados)
showModal({ procesoId: 1, tipo: 'requisitos' })
expect(detalleModalVisible.value).toBe(true)
expect(detalleModal.value.titulo).toBe('Requisitos de Admisión')
expect(detalleModal.value.descripcion).toBe('Lista de requisitos necesarios para postular')
expect(detalleModal.value.imagen_url).toBe('https://example.com/requisitos.jpg')
expect(detalleModal.value.imagen_url_2).toBe('https://example.com/requisitos2.jpg')
expect(detalleModal.value.listas).toEqual(['DNI vigente', 'Certificado de estudios', 'Foto carnet'])
})
it('uses tipo label as title when titulo_detalle is not provided', () => {
const procesosPublicados = ref([
{
id: 1,
titulo: 'Admisión 2026',
detalles: [
{
tipo: 'pagos',
descripcion: 'Información de pagos'
}
]
}
])
const { showModal, detalleModal } = createShowModal(procesosPublicados)
showModal({ procesoId: 1, tipo: 'pagos' })
expect(detalleModal.value.titulo).toBe('Pagos')
expect(detalleModal.value.descripcion).toBe('Información de pagos')
})
it('handles detalle with vacantes tipo correctly', () => {
const procesosPublicados = ref([
{
id: 1,
titulo: 'Proceso Ordinario',
detalles: [
{
tipo: 'vacantes',
titulo_detalle: 'Vacantes Disponibles',
descripcion: '100 vacantes para todas las carreras',
listas: ['Medicina: 20', 'Ingeniería: 30', 'Derecho: 50']
}
]
}
])
const { showModal, detalleModal } = createShowModal(procesosPublicados)
showModal({ procesoId: 1, tipo: 'vacantes' })
expect(detalleModal.value.titulo).toBe('Vacantes Disponibles')
expect(detalleModal.value.listas).toHaveLength(3)
})
it('handles detalle with cronograma tipo correctly', () => {
const procesosPublicados = ref([
{
id: 1,
titulo: 'Admisión Extraordinaria',
detalles: [
{
tipo: 'cronograma',
titulo_detalle: 'Cronograma de Actividades',
descripcion: 'Fechas importantes del proceso',
imagen_url: 'https://example.com/cronograma.png'
}
]
}
])
const { showModal, detalleModal } = createShowModal(procesosPublicados)
showModal({ procesoId: 1, tipo: 'cronograma' })
expect(detalleModal.value.titulo).toBe('Cronograma de Actividades')
expect(detalleModal.value.imagen_url).toBe('https://example.com/cronograma.png')
})
})
describe('when proceso does not have matching detalle', () => {
it('creates default modal data with proceso title when no detalle found', () => {
const procesosPublicados = ref([
{
id: 1,
titulo: 'Admisión 2026',
detalles: [] // No detalles
}
])
const { showModal, detalleModal, detalleModalVisible } = createShowModal(procesosPublicados)
showModal({ procesoId: 1, tipo: 'requisitos' })
expect(detalleModalVisible.value).toBe(true)
expect(detalleModal.value.titulo).toBe('Requisitos - Admisión 2026')
expect(detalleModal.value.descripcion).toBe('')
expect(detalleModal.value.imagen_url).toBeNull()
expect(detalleModal.value.imagen_url_2).toBeNull()
expect(detalleModal.value.listas).toEqual([])
})
it('creates default modal for pagos when no detalle', () => {
const procesosPublicados = ref([
{
id: 2,
titulo: 'Proceso Especial',
detalles: [
{ tipo: 'requisitos', titulo_detalle: 'Requisitos' } // Only requisitos, no pagos
]
}
])
const { showModal, detalleModal } = createShowModal(procesosPublicados)
showModal({ procesoId: 2, tipo: 'pagos' })
expect(detalleModal.value.titulo).toBe('Pagos - Proceso Especial')
})
it('handles proceso with undefined detalles array', () => {
const procesosPublicados = ref([
{
id: 1,
titulo: 'Proceso Sin Detalles'
// No detalles property at all
}
])
const { showModal, detalleModal, detalleModalVisible } = createShowModal(procesosPublicados)
showModal({ procesoId: 1, tipo: 'vacantes' })
expect(detalleModalVisible.value).toBe(true)
expect(detalleModal.value.titulo).toBe('Vacantes - Proceso Sin Detalles')
})
})
describe('when proceso is not found', () => {
it('does not open modal when procesoId does not match any proceso', () => {
const procesosPublicados = ref([
{
id: 1,
titulo: 'Admisión 2026',
detalles: []
}
])
const { showModal, detalleModal, detalleModalVisible } = createShowModal(procesosPublicados)
showModal({ procesoId: 999, tipo: 'requisitos' }) // Non-existent ID
expect(detalleModalVisible.value).toBe(false)
})
it('does not modify modal data when proceso not found', () => {
const procesosPublicados = ref([
{ id: 1, titulo: 'Admisión 2026', detalles: [] }
])
const { showModal, detalleModal } = createShowModal(procesosPublicados)
// Set initial state
const initialState = { ...detalleModal.value }
showModal({ procesoId: 999, tipo: 'requisitos' })
expect(detalleModal.value).toEqual(initialState)
})
})
describe('modal data handling edge cases', () => {
it('handles empty strings in detalle properties', () => {
const procesosPublicados = ref([
{
id: 1,
titulo: 'Test Proceso',
detalles: [
{
tipo: 'requisitos',
titulo_detalle: '',
descripcion: '',
imagen_url: '',
listas: []
}
]
}
])
const { showModal, detalleModal } = createShowModal(procesosPublicados)
showModal({ procesoId: 1, tipo: 'requisitos' })
// Empty titulo_detalle should fall back to tipo label
expect(detalleModal.value.titulo).toBe('Requisitos')
expect(detalleModal.value.descripcion).toBe('')
})
it('handles null values in detalle properties', () => {
const procesosPublicados = ref([
{
id: 1,
titulo: 'Test Proceso',
detalles: [
{
tipo: 'pagos',
titulo_detalle: 'Pagos',
descripcion: null,
imagen_url: null,
imagen_url_2: null,
listas: null
}
]
}
])
const { showModal, detalleModal } = createShowModal(procesosPublicados)
showModal({ procesoId: 1, tipo: 'pagos' })
expect(detalleModal.value.descripcion).toBe('')
expect(detalleModal.value.imagen_url).toBeNull()
expect(detalleModal.value.listas).toEqual([])
})
it('correctly maps all tipo labels', () => {
const testCases = [
{ tipo: 'requisitos', expectedLabel: 'Requisitos' },
{ tipo: 'pagos', expectedLabel: 'Pagos' },
{ tipo: 'vacantes', expectedLabel: 'Vacantes' },
{ tipo: 'cronograma', expectedLabel: 'Cronograma' }
]
testCases.forEach(({ tipo, expectedLabel }) => {
const procesosPublicados = ref([
{ id: 1, titulo: 'Test', detalles: [] }
])
const { showModal, detalleModal } = createShowModal(procesosPublicados)
showModal({ procesoId: 1, tipo })
expect(detalleModal.value.titulo).toBe(`${expectedLabel} - Test`)
})
})
it('handles unknown tipo by using tipo as label', () => {
const procesosPublicados = ref([
{ id: 1, titulo: 'Test', detalles: [] }
])
const { showModal, detalleModal } = createShowModal(procesosPublicados)
showModal({ procesoId: 1, tipo: 'unknown_tipo' })
expect(detalleModal.value.titulo).toBe('unknown_tipo - Test')
})
})
describe('multiple procesos scenarios', () => {
it('correctly finds the right proceso among multiple', () => {
const procesosPublicados = ref([
{
id: 1,
titulo: 'Proceso Uno',
detalles: [{ tipo: 'requisitos', titulo_detalle: 'Requisitos Uno' }]
},
{
id: 2,
titulo: 'Proceso Dos',
detalles: [{ tipo: 'requisitos', titulo_detalle: 'Requisitos Dos' }]
},
{
id: 3,
titulo: 'Proceso Tres',
detalles: [{ tipo: 'requisitos', titulo_detalle: 'Requisitos Tres' }]
}
])
const { showModal, detalleModal } = createShowModal(procesosPublicados)
showModal({ procesoId: 2, tipo: 'requisitos' })
expect(detalleModal.value.titulo).toBe('Requisitos Dos')
})
it('handles showing different tipos for different procesos', () => {
const procesosPublicados = ref([
{
id: 1,
titulo: 'Proceso A',
detalles: [
{ tipo: 'requisitos', titulo_detalle: 'Requisitos A' },
{ tipo: 'pagos', titulo_detalle: 'Pagos A' }
]
},
{
id: 2,
titulo: 'Proceso B',
detalles: [
{ tipo: 'vacantes', titulo_detalle: 'Vacantes B' }
]
}
])
const { showModal, detalleModal } = createShowModal(procesosPublicados)
// Show pagos for proceso 1
showModal({ procesoId: 1, tipo: 'pagos' })
expect(detalleModal.value.titulo).toBe('Pagos A')
// Then show vacantes for proceso 2
showModal({ procesoId: 2, tipo: 'vacantes' })
expect(detalleModal.value.titulo).toBe('Vacantes B')
})
})
})

@ -0,0 +1,166 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import axios from 'axios'
import { useProcesoAdmisionStore } from '../../src/store/procesosAdmisionStore'
// Mock axios
vi.mock('axios')
describe('procesosAdmisionStore', () => {
let store
beforeEach(() => {
setActivePinia(createPinia())
store = useProcesoAdmisionStore()
vi.stubEnv('VITE_API_URL', 'http://localhost:8000/api')
})
afterEach(() => {
vi.clearAllMocks()
vi.unstubAllEnvs()
})
describe('fetchProcesosPublicados', () => {
it('should fetch and update procesosPublicados state with returned data', async () => {
const mockProcesos = [
{
id: 1,
titulo: 'Proceso Ordinario 2026',
slug: 'proceso-ordinario-2026',
publicado: true,
estado: 'publicado',
detalles: [
{ tipo: 'requisitos', titulo_detalle: 'Requisitos', descripcion: 'Lista de requisitos' }
]
},
{
id: 2,
titulo: 'Proceso Extraordinario 2026',
slug: 'proceso-extraordinario-2026',
publicado: true,
estado: 'en_proceso',
detalles: []
}
]
axios.get.mockResolvedValueOnce({ data: mockProcesos })
// Initial state should be empty
expect(store.procesosPublicados).toEqual([])
expect(store.loading).toBe(false)
// Call the action
const result = await store.fetchProcesosPublicados()
// Verify the result
expect(result).toBe(true)
expect(store.procesosPublicados).toEqual(mockProcesos)
expect(store.procesosPublicados).toHaveLength(2)
expect(store.loading).toBe(false)
expect(store.error).toBeNull()
})
it('should set loading to true while fetching', async () => {
let loadingDuringFetch = false
axios.get.mockImplementation(() => {
loadingDuringFetch = store.loading
return Promise.resolve({ data: [] })
})
await store.fetchProcesosPublicados()
expect(loadingDuringFetch).toBe(true)
})
it('should handle empty response array', async () => {
axios.get.mockResolvedValueOnce({ data: [] })
const result = await store.fetchProcesosPublicados()
expect(result).toBe(true)
expect(store.procesosPublicados).toEqual([])
expect(store.error).toBeNull()
})
it('should handle non-array response by setting empty array', async () => {
axios.get.mockResolvedValueOnce({ data: null })
const result = await store.fetchProcesosPublicados()
expect(result).toBe(true)
expect(store.procesosPublicados).toEqual([])
})
it('should set error state when fetch fails', async () => {
const errorMessage = 'Network Error'
axios.get.mockRejectedValueOnce(new Error(errorMessage))
const result = await store.fetchProcesosPublicados()
expect(result).toBe(false)
expect(store.error).toBe(errorMessage)
expect(store.loading).toBe(false)
})
it('should handle API error response with message', async () => {
axios.get.mockRejectedValueOnce({
response: {
data: { message: 'Server error occurred' },
status: 500
}
})
const result = await store.fetchProcesosPublicados()
expect(result).toBe(false)
expect(store.error).toBe('Server error occurred')
})
it('should call the correct API endpoint', async () => {
axios.get.mockResolvedValueOnce({ data: [] })
await store.fetchProcesosPublicados()
expect(axios.get).toHaveBeenCalledTimes(1)
expect(axios.get).toHaveBeenCalledWith(
expect.stringContaining('/procesos-admision/publicados')
)
})
it('should clear previous error before fetching', async () => {
// Set an error first
store.error = 'Previous error'
axios.get.mockResolvedValueOnce({ data: [] })
await store.fetchProcesosPublicados()
expect(store.error).toBeNull()
})
it('should update state with processes containing detalles', async () => {
const mockProcesos = [
{
id: 1,
titulo: 'Proceso con Detalles',
slug: 'proceso-con-detalles',
publicado: true,
detalles: [
{ tipo: 'requisitos', titulo_detalle: 'Requisitos', descripcion: 'Requisito 1' },
{ tipo: 'pagos', titulo_detalle: 'Pagos', descripcion: 'Pago info' },
{ tipo: 'vacantes', titulo_detalle: 'Vacantes', descripcion: '100 vacantes' },
{ tipo: 'cronograma', titulo_detalle: 'Cronograma', descripcion: 'Fechas importantes' }
]
}
]
axios.get.mockResolvedValueOnce({ data: mockProcesos })
await store.fetchProcesosPublicados()
expect(store.procesosPublicados[0].detalles).toHaveLength(4)
expect(store.procesosPublicados[0].detalles[0].tipo).toBe('requisitos')
})
})
})

@ -0,0 +1,14 @@
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'happy-dom',
globals: true,
include: ['tests/**/*.{test,spec}.{js,ts}'],
coverage: {
reporter: ['text', 'json', 'html'],
},
},
})

@ -0,0 +1,37 @@
I've created all the unit tests for the 5 requested cases. Here's a summary:
Backend Tests (Laravel/PHPUnit)
1. back/tests/Feature/ProcesoAdmisionPublicadosTest.php
• Tests that /api/procesos-admision/publicados returns only processes with publicado=true
• Verifies empty array returned when no published processes exist
• Confirms ordering by id descending
2. back/tests/Feature/ProcesoAdmisionUpdateNullableTest.php
• Tests that update method sets nullable fields to null when empty values are provided
• Covers date fields, link fields, and text fields
• Verifies fields not in request remain unchanged
Frontend Tests (Vitest)
3. front/tests/store/procesosAdmisionStore.test.js
• Tests fetchProcesosPublicados action fetches and updates state
• Covers loading states, error handling, and API endpoint verification
4. front/tests/components/ConvocatoriasSection.test.js
• Tests rendering of main card for first process and secondary cards for remaining
• Verifies estado label mapping, action button events, and empty state
5. front/tests/components/WebPage.test.js
• Tests showModal function displays correct detalle information
• Covers all tipo cases (requisitos, pagos, vacantes, cronograma)
• Handles edge cases: missing proceso, empty detalles, null values
To Run Tests
Backend:
cd back && php artisan test
bash
Frontend (after installing dependencies):
cd front && npm install && npm test
bash
Loading…
Cancel
Save