EntriesIndex page test

This commit is contained in:
Matt Young 2024-07-01 22:04:27 -05:00
parent 526dd453cc
commit 38a72657be
11 changed files with 233 additions and 31 deletions

View File

@ -56,7 +56,7 @@ class EntryController extends Controller
}
$entry->delete();
return redirect('/entries')->with('success', 'The '.$entry->audition->name.'entry for '.$entry->student->full_name().'has been deleted.');
return redirect()->route('entries.index')->with('success', 'The '.$entry->audition->name.'entry for '.$entry->student->full_name().'has been deleted.');
}
}

View File

@ -13,8 +13,7 @@ class EntryObserver
*/
public function created(Entry $entry): void
{
AuditionChange::dispatch();
EntryChange::dispatch($entry->audition_id);
}
/**
@ -22,8 +21,7 @@ class EntryObserver
*/
public function updated(Entry $entry): void
{
AuditionChange::dispatch();
EntryChange::dispatch($entry->audition_id);
}
/**
@ -31,8 +29,7 @@ class EntryObserver
*/
public function deleted(Entry $entry): void
{
AuditionChange::dispatch();
EntryChange::dispatch($entry->audition_id);
}
/**
@ -40,8 +37,7 @@ class EntryObserver
*/
public function restored(Entry $entry): void
{
AuditionChange::dispatch();
EntryChange::dispatch($entry->audition_id);
}
/**
@ -49,6 +45,6 @@ class EntryObserver
*/
public function forceDeleted(Entry $entry): void
{
EntryChange::dispatch($entry->audition_id);
}
}

View File

@ -4,7 +4,7 @@ namespace App\Policies;
use App\Models\Entry;
use App\Models\User;
use Illuminate\Auth\Access\Response;
use function is_null;
class EntryPolicy
@ -14,7 +14,7 @@ class EntryPolicy
*/
public function viewAny(User $user): bool
{
//
return true;
}
/**
@ -22,7 +22,10 @@ class EntryPolicy
*/
public function view(User $user, Entry $entry): bool
{
if($user->is_admin) return true;
if ($user->is_admin) {
return true;
}
return $user->school_id == $entry->student()->school_id;
}
@ -31,7 +34,10 @@ class EntryPolicy
*/
public function create(User $user): bool
{
if($user->is_admin) return true;
if ($user->is_admin) {
return true;
}
return ! is_null($user->school_id);
}
@ -40,7 +46,10 @@ class EntryPolicy
*/
public function update(User $user, Entry $entry): bool
{
if($user->is_admin) return true;
if ($user->is_admin) {
return true;
}
return $user->school_id == $entry->student()->school_id;
}
@ -49,13 +58,15 @@ class EntryPolicy
*/
public function delete(User $user, Entry $entry): bool
{
if($user->is_admin) return true;
if ($user->is_admin) {
return true;
}
// Return false if $entry->audition->entry_deadline is in the past, continue if not
if ($entry->audition->entry_deadline < now()) {
return false;
}
return $user->school_id == $entry->student()->school_id;
return $user->school_id == $entry->student->school_id;
}
/**
@ -63,7 +74,7 @@ class EntryPolicy
*/
public function restore(User $user, Entry $entry): bool
{
//
return true;
}
/**
@ -71,6 +82,6 @@ class EntryPolicy
*/
public function forceDelete(User $user, Entry $entry): bool
{
//
return true;
}
}

View File

@ -45,8 +45,8 @@ class AuditionFactory extends Factory
'score_order' => $this->faker->numberBetween(2, 50),
'entry_deadline' => Carbon::tomorrow(),
'entry_fee' => 1000,
'minimum_grade' => 7,
'maximum_grade' => 12,
'minimum_grade' => $this->faker->numberBetween(7, 9),
'maximum_grade' => $this->faker->numberBetween(8, 12),
'for_seating' => 1,
'for_advancement' => 1,
];

View File

@ -1,4 +1,5 @@
@props(['color' => 'currentColor'])
@props(['color' => 'currentColor', 'title'=>false])
<svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
@if($title)<title>{{ $title }}</title>@endif
<path stroke="{{$color}}" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 11.917 9.724 16.5 19 7.5"/>
</svg>

View File

@ -1,4 +1,5 @@
@props(['color' => 'currentColor'])
@props(['color' => 'currentColor', 'title' => false])
<svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
@if($title)<title>{{ $title }}</title>@endif
<path stroke="{{ $color }}" stroke-linecap="round" stroke-width="2" d="m6 6 12 12m3-6a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
</svg>

View File

@ -1,3 +1,5 @@
@props(['title' => false])
<svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
@if($title)<title>{{ $title }}</title>@endif
<path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M5 7h14M5 12h14M5 17h14"/>
</svg>

Before

Width:  |  Height:  |  Size: 270 B

After

Width:  |  Height:  |  Size: 346 B

View File

@ -1,4 +1,5 @@
@props(['color' => 'currentColor'])
@props(['color' => 'currentColor','title'=>false])
<svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="{{ $color }}" viewBox="0 0 24 24">
@if($title)<title>{{ $title }}</title>@endif
<path fill-rule="evenodd" d="M8.97 14.316H5.004c-.322 0-.64-.08-.925-.232a2.022 2.022 0 0 1-.717-.645 2.108 2.108 0 0 1-.242-1.883l2.36-7.201C5.769 3.54 5.96 3 7.365 3c2.072 0 4.276.678 6.156 1.256.473.145.925.284 1.35.404h.114v9.862a25.485 25.485 0 0 0-4.238 5.514c-.197.376-.516.67-.901.83a1.74 1.74 0 0 1-1.21.048 1.79 1.79 0 0 1-.96-.757 1.867 1.867 0 0 1-.269-1.211l1.562-4.63ZM19.822 14H17V6a2 2 0 1 1 4 0v6.823c0 .65-.527 1.177-1.177 1.177Z" clip-rule="evenodd"/>
</svg>

View File

@ -1,4 +1,5 @@
@props(['color' => 'currentColor'])
@props(['color' => 'currentColor','title'=>false])
<svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="{{ $color }}" viewBox="0 0 24 24">
@if($title)<title>{{ $title }}</title>@endif
<path fill-rule="evenodd" d="M15.03 9.684h3.965c.322 0 .64.08.925.232.286.153.532.374.717.645a2.109 2.109 0 0 1 .242 1.883l-2.36 7.201c-.288.814-.48 1.355-1.884 1.355-2.072 0-4.276-.677-6.157-1.256-.472-.145-.924-.284-1.348-.404h-.115V9.478a25.485 25.485 0 0 0 4.238-5.514 1.8 1.8 0 0 1 .901-.83 1.74 1.74 0 0 1 1.21-.048c.396.13.736.397.96.757.225.36.32.788.269 1.211l-1.562 4.63ZM4.177 10H7v8a2 2 0 1 1-4 0v-6.823C3 10.527 3.527 10 4.176 10Z" clip-rule="evenodd"/>
</svg>

View File

@ -9,7 +9,7 @@
<x-layout.page-section-container>
<x-layout.page-section>
<x-slot:section_name>Add Entry</x-slot:section_name>
<x-form.form method="POST" action="/entries" class="pt-6 pb-8">
<x-form.form method="POST" action="{{ route('entries.store') }}" class="pt-6 pb-8">
<x-form.body-grid columns="6" class="max-w-full" x-data="studentAuditionFilter()">
@ -81,23 +81,33 @@
@if(auditionSetting('advanceTo'))
<x-table.td>
@if($entry->for_seating)
<div aria-label="{{ $entry->student->full_name() }} on {{ $entry->audition->name }} is entered for seating. Entry ID {{ $entry->id }}">
<x-icons.checkmark color="green"/>
</div>
@else
<div aria-label="{{ $entry->student->full_name() }} on {{ $entry->audition->name }} is not entered for seating. Entry ID {{ $entry->id }}">
<x-icons.circle-slash-no color="red"/>
</div>
@endif
</x-table.td>
<x-table.td>
@if($entry->for_advancement)
<div aria-label="{{ $entry->student->full_name() }} on {{ $entry->audition->name }} is entered for advancement. Entry ID {{ $entry->id }}">
<x-icons.checkmark color="green"/>
</div>
@else
<div aria-label="{{ $entry->student->full_name() }} on {{ $entry->audition->name }} is not entered for advancement. Entry ID {{ $entry->id }}">
<x-icons.circle-slash-no color="red"/>
</div>
@endif
</x-table.td>
@endif
<x-table.td for_button>
@if( $entry->audition->entry_deadline >= now())
<form method="POST" action="/entries/{{ $entry->id }}" class="inline">
<form method="POST" action="{{ route('entries.destroy',$entry) }}" class="inline">
@csrf
@method('DELETE')
<x-table.button

View File

@ -0,0 +1,179 @@
<?php
use App\Models\Audition;
use App\Models\Entry;
use App\Models\School;
use App\Models\Student;
use App\Models\User;
use App\Settings;
use Illuminate\Foundation\Testing\RefreshDatabase;
use function Pest\Laravel\actingAs;
use function Pest\Laravel\delete;
use function Pest\Laravel\get;
use function Pest\Laravel\post;
uses(RefreshDatabase::class);
beforeEach(function () {
Settings::set('auditionAbbreviation', 'SBDA');
Settings::set('advanceTo', 'OMEA');
$this->school = School::factory()->create();
$this->user = User::factory()->create(['school_id' => $this->school->id]);
});
it('works for a user with a school', function () {
actingAs($this->user)
->get(route('entries.index'))
->assertOk();
});
it('denies a user without a school', function () {
get(route('entries.index'))
->assertRedirect(route('home'));
actingAs(User::factory()->create())
->get(route('entries.index'))
->assertForbidden();
});
it('has appropriate students in JS array for select', function () {
// Arrange
$student = Student::factory()->create(['school_id' => $this->school->id]);
// Act & Assert
actingAs($this->user);
get(route('entries.index'))
->assertSeeInOrder([
'id: ',
$student->id,
'name: ',
$student->full_name(true),
], false); // The false parameter makes the assertion case-sensitive and allows for HTML tags
});
it('has auditions in JS array for select', function () {
// Arrange
$auditions = Audition::factory()->count(5)->create();
// Act & Assert
actingAs($this->user);
$response = get(route('entries.index'));
foreach ($auditions as $audition) {
$response->assertSeeInOrder([
'id: ',
$audition->id,
'name: ',
$audition->name,
'minGrade: ',
$audition->minimum_grade,
'maxGrade: ',
$audition->maximum_grade,
], false);
}
});
it('shows existing entries in a table', function () {
// Arrange
$students = Student::factory()->count(5)->create(['school_id' => $this->school->id]);
$entries = [];
foreach ($students as $student) {
$entries[] = Entry::factory()->create(['student_id' => $student->id]);
}
foreach ($students as $student) {
Entry::class::factory()->create(['student_id' => $student->id]);
}
// Act & Assert
actingAs($this->user);
$response = get(route('entries.index'));
foreach ($entries as $entry) {
$response->
assertSeeInOrder([
'<td',
$entry->student->full_name(true),
$entry->student->grade,
$entry->audition->name,
'</td>',
], false);
}
});
it('shows a delete link for entries whose deadline is not past', function () {
// Arrange
$openAudition = Audition::factory()->create();
$closedAudition = Audition::factory()->closed()->create();
$student = Student::factory()->create(['school_id' => $this->school->id]);
$pendingEntry = Entry::factory()->create(['audition_id' => $openAudition->id, 'student_id' => $student->id]);
$protectedEntry = Entry::factory()->create(['audition_id' => $closedAudition->id, 'student_id' => $student->id]);
// Act & Assert
actingAs($this->user);
get(route('entries.index'))
->assertSee(route('entries.destroy', $pendingEntry))
->assertDontSee(route('entries.destroy', $protectedEntry));
});
it('shows appropriate flags for entry types when advancement is enabled', function () {
// Arrange
$student = Student::factory()->create(['school_id' => $this->school->id]);
$auditionOnlyEntry = Entry::factory()->seatingOnly()->create(['student_id' => $student->id]);
$advanceOnlyEntry = Entry::factory()->advanceOnly()->create(['student_id' => $student->id]);
$bothEntry = Entry::factory()->create(['student_id' => $student->id]);
// Act & Assert
actingAs($this->user);
get(route('entries.index'))
->assertOk()
->assertSee('is entered for seating. Entry ID '.$bothEntry->id)
->assertSee('is entered for advancement. Entry ID '.$bothEntry->id)
->assertSee('is entered for seating. Entry ID '.$auditionOnlyEntry->id)
->assertSee('is not entered for advancement. Entry ID '.$auditionOnlyEntry->id)
->assertSee('is entered for advancement. Entry ID '.$advanceOnlyEntry->id)
->assertSee('is not entered for seating. Entry ID '.$advanceOnlyEntry->id);
});
it('accepts a valid entry', function () {
// Arrange
$student = Student::factory()->create(['school_id' => $this->school->id]);
$audition = Audition::factory()->create();
// Act & Assert
actingAs($this->user);
$response = post(route('entries.store'), [
'student_id' => $student->id,
'audition_id' => $audition->id,
]);
$response->assertSessionHasNoErrors();
$response->assertRedirect(route('entries.index'));
$this->assertDatabaseHas('entries', [
'student_id' => $student->id,
'audition_id' => $audition->id,
]);
});
it('deletes an entry', function () {
// Arrange
$student = Student::factory()->create(['school_id' => $this->school->id]);
$audition = Audition::factory()->create(['name' => 'Flute']);
$entry = Entry::factory()->create(['student_id' => $student->id, 'audition_id' => $audition->id]);
// Act
actingAs($this->user);
$response = delete(route('entries.destroy', $entry));
$response
->assertSessionHasNoErrors()
->assertRedirect(route('entries.index'));
// Assert
$this->assertDatabaseMissing('entries', [
'id' => $entry->id,
]);
});
it('shows entry type checkboxes only when advancement is enabled', function () {
// Arrange
Settings::set('advanceTo', 'OMEA');
// Act & Assert
actingAs($this->user);
get(route('entries.index'))
->assertSee('Enter for');
Settings::set('advanceTo', null);
get(route('entries.index'))
->assertDontSee('Enter for');
});