feat: Implementar tests
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);
|
||||
}
|
||||
}
|
||||
@ -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…
Reference in New Issue