seed(RoleSeeder::class); $this->org = Organisation::factory()->create(); // Festival parent + 2 children — mirrors DevSeeder::seedEchtFeesten // so the controller's festival-aware merge is exercised. $this->festival = Event::factory()->create([ 'organisation_id' => $this->org->id, 'name' => 'Testfestival 2026', 'event_type' => 'festival', 'parent_event_id' => null, 'status' => 'registration_open', ]); $this->dag1 = Event::factory()->create([ 'organisation_id' => $this->org->id, 'parent_event_id' => $this->festival->id, 'name' => 'Dag 1', 'event_type' => 'event', 'status' => 'registration_open', ]); $this->dag2 = Event::factory()->create([ 'organisation_id' => $this->org->id, 'parent_event_id' => $this->festival->id, 'name' => 'Dag 2', 'event_type' => 'event', 'status' => 'registration_open', ]); // Time slots — parent has 1 VOLUNTEER + 1 CREW (CREW must be // filtered out); children have 3 VOLUNTEER slots across 2 dates. TimeSlot::factory()->create([ 'event_id' => $this->festival->id, 'name' => 'Vrijwilligers opbouw', 'person_type' => 'VOLUNTEER', 'date' => '2026-07-10', 'start_time' => '09:00:00', 'end_time' => '12:00:00', ]); TimeSlot::factory()->create([ 'event_id' => $this->festival->id, 'name' => 'Crew-only nacht', 'person_type' => 'CREW', 'date' => '2026-07-10', 'start_time' => '23:00:00', 'end_time' => '07:00:00', ]); TimeSlot::factory()->create([ 'event_id' => $this->dag1->id, 'name' => 'Dag 1 middag', 'person_type' => 'VOLUNTEER', 'date' => '2026-07-11', 'start_time' => '12:00:00', 'end_time' => '18:00:00', ]); TimeSlot::factory()->create([ 'event_id' => $this->dag1->id, 'name' => 'Dag 1 avond', 'person_type' => 'VOLUNTEER', 'date' => '2026-07-11', 'start_time' => '18:00:00', 'end_time' => '23:00:00', ]); TimeSlot::factory()->create([ 'event_id' => $this->dag2->id, 'name' => 'Dag 2 middag', 'person_type' => 'VOLUNTEER', 'date' => '2026-07-12', 'start_time' => '12:00:00', 'end_time' => '18:00:00', ]); // Sections — positive (standard+registration) + duplicate name // across parent/child + negative cases (cross_event, hidden). FestivalSection::factory()->create([ 'event_id' => $this->festival->id, 'name' => 'Bar', 'type' => 'standard', 'sort_order' => 1, 'show_in_registration' => true, 'registration_description' => 'Overkoepelende bar', ]); FestivalSection::factory()->create([ 'event_id' => $this->dag1->id, 'name' => 'Bar', // Duplicate name with parent 'type' => 'standard', 'sort_order' => 2, 'show_in_registration' => true, ]); FestivalSection::factory()->create([ 'event_id' => $this->dag1->id, 'name' => 'Hospitality', 'type' => 'standard', 'sort_order' => 3, 'show_in_registration' => true, ]); FestivalSection::factory()->create([ 'event_id' => $this->dag2->id, 'name' => 'Techniek', 'type' => 'standard', 'sort_order' => 4, 'show_in_registration' => true, ]); // Negative: hidden from registration FestivalSection::factory()->create([ 'event_id' => $this->dag1->id, 'name' => 'Intern overleg', 'type' => 'standard', 'sort_order' => 10, 'show_in_registration' => false, ]); // Negative: cross_event FestivalSection::factory()->create([ 'event_id' => $this->festival->id, 'name' => 'EHBO', 'type' => 'cross_event', 'sort_order' => 11, 'show_in_registration' => true, ]); $this->schema = FormBuilderDevSeeder::seedEventRegistrationShowcaseSchema( $this->org, $this->festival, ); } public function test_showcase_schema_resolves_via_its_public_token(): void { $response = $this->getJson("/api/v1/public/forms/{$this->schema->public_token}"); $response->assertOk() ->assertJsonPath('data.id', $this->schema->id); } public function test_showcase_schema_contains_availability_picker_and_section_priority(): void { $response = $this->getJson("/api/v1/public/forms/{$this->schema->public_token}"); $response->assertOk(); $fields = collect($response->json('data.fields'))->keyBy('slug'); $this->assertArrayHasKey('beschikbaarheid', $fields); $this->assertSame(FormFieldType::AVAILABILITY_PICKER->value, $fields['beschikbaarheid']['field_type']); $this->assertSame('full', $fields['beschikbaarheid']['display_width']); $this->assertArrayHasKey('sectie_voorkeur', $fields); $this->assertSame(FormFieldType::SECTION_PRIORITY->value, $fields['sectie_voorkeur']['field_type']); $this->assertSame('full', $fields['sectie_voorkeur']['display_width']); // WS-5b canonicalised `max_priorities` → `max_selected` (shared // semantic of "cap on entries in a list-valued field"). Resource // emits the new canonical key via // FormFieldValidationRuleService::toJsonShape(). $this->assertSame(['max_selected' => 3], $fields['sectie_voorkeur']['validation_rules']); } public function test_time_slots_endpoint_returns_at_least_four_volunteer_rows(): void { $response = $this->getJson("/api/v1/public/forms/{$this->schema->public_token}/time-slots"); $response->assertOk(); $rows = $response->json('data'); $this->assertIsArray($rows); $this->assertGreaterThanOrEqual(4, count($rows)); } public function test_time_slots_endpoint_surfaces_both_parent_and_child_events(): void { $response = $this->getJson("/api/v1/public/forms/{$this->schema->public_token}/time-slots"); $response->assertOk(); $eventNames = collect($response->json('data'))->pluck('event_name')->unique()->values()->all(); $this->assertContains('Testfestival 2026', $eventNames); $this->assertTrue(in_array('Dag 1', $eventNames, true) || in_array('Dag 2', $eventNames, true)); } public function test_time_slots_endpoint_filters_out_non_volunteer_rows(): void { $response = $this->getJson("/api/v1/public/forms/{$this->schema->public_token}/time-slots"); $response->assertOk(); $names = collect($response->json('data'))->pluck('name')->all(); $this->assertNotContains('Crew-only nacht', $names); } public function test_sections_endpoint_returns_only_standard_registration_visible_rows(): void { $response = $this->getJson("/api/v1/public/forms/{$this->schema->public_token}/sections"); $response->assertOk(); $names = collect($response->json('data'))->pluck('name')->all(); $this->assertContains('Bar', $names); $this->assertContains('Hospitality', $names); $this->assertContains('Techniek', $names); } public function test_sections_endpoint_excludes_hidden_and_cross_event_rows(): void { $response = $this->getJson("/api/v1/public/forms/{$this->schema->public_token}/sections"); $response->assertOk(); $names = collect($response->json('data'))->pluck('name')->all(); $this->assertNotContains('Intern overleg', $names); $this->assertNotContains('EHBO', $names); } public function test_sections_endpoint_dedupes_duplicate_names_across_parent_and_child(): void { $response = $this->getJson("/api/v1/public/forms/{$this->schema->public_token}/sections"); $response->assertOk(); $names = collect($response->json('data'))->pluck('name')->all(); $barCount = collect($names)->filter(fn ($n) => $n === 'Bar')->count(); $this->assertSame(1, $barCount, 'Duplicate "Bar" across parent and child should be deduped.'); } }