$title, 'ccli_id' => (string) fake()->unique()->numerify('#####'), 'author' => 'Test Author', 'copyright_text' => 'Test Publisher', ]); $verseLabel = Label::firstOrCreate(['name' => 'Strophe 1 - '.$title], ['color' => '#2196F3']); $chorusLabel = Label::firstOrCreate(['name' => 'Refrain - '.$title], ['color' => '#F44336']); $bridgeLabel = Label::firstOrCreate(['name' => 'Bridge - '.$title], ['color' => '#4CAF50']); $verse = $song->sections()->create(['label_id' => $verseLabel->id, 'order' => 0]); $verse->slides()->create(['order' => 0, 'text_content' => 'Strophe Zeile']); $chorus = $song->sections()->create(['label_id' => $chorusLabel->id, 'order' => 1]); $chorus->slides()->create(['order' => 0, 'text_content' => 'Refrain Zeile']); $bridge = $song->sections()->create(['label_id' => $bridgeLabel->id, 'order' => 2]); $bridge->slides()->create(['order' => 0, 'text_content' => 'Bridge Zeile']); $normal = $song->arrangements()->create(['name' => 'Normal', 'is_default' => true]); $normal->arrangementSections()->create(['song_section_id' => $verse->id, 'order' => 0]); $normal->arrangementSections()->create(['song_section_id' => $chorus->id, 'order' => 1]); $bene = $song->arrangements()->create(['name' => 'Bene', 'is_default' => false]); $bene->arrangementSections()->create(['song_section_id' => $chorus->id, 'order' => 0]); $bene->arrangementSections()->create(['song_section_id' => $bridge->id, 'order' => 1]); return [$song->fresh(), $normal, $bene]; } public function test_generate_parser_song_mit_gewaehltem_arrangement_setzt_selected_und_behaelt_alle(): void { [$song, $normal, $bene] = $this->createSongWithTwoArrangements(); $parserSong = app(ProExportService::class)->generateParserSong($song, null, $bene); $beneParser = $parserSong->getArrangementByName('Bene'); $this->assertNotNull($beneParser, 'Gewähltes Arrangement "Bene" muss im .pro vorhanden sein'); $this->assertSame( $beneParser->getUuid(), $parserSong->getSelectedArrangementUuid(), 'selected_arrangement muss auf das gewählte Arrangement "Bene" zeigen', ); $names = array_map(fn ($arr) => $arr->getName(), $parserSong->getArrangements()); $this->assertContains('Normal', $names, 'Default-Arrangement muss erhalten bleiben'); $this->assertContains('Bene', $names, 'Gewähltes Arrangement muss erhalten bleiben'); } public function test_generate_parser_song_ohne_auswahl_nutzt_default_arrangement(): void { [$song, $normal] = $this->createSongWithTwoArrangements(); $parserSong = app(ProExportService::class)->generateParserSong($song, null, null); $normalParser = $parserSong->getArrangementByName('Normal'); $this->assertNotNull($normalParser); $this->assertSame( $normalParser->getUuid(), $parserSong->getSelectedArrangementUuid(), 'Ohne explizite Auswahl muss das Default-Arrangement vorausgewählt sein', ); } public function test_union_der_sektionen_laesst_jedes_arrangement_gruppen_rendern(): void { [$song, $normal, $bene] = $this->createSongWithTwoArrangements(); // Auswahl "Bene" (Refrain, Bridge) — ohne Union würde "Normal" (Strophe) leer/unvollständig rendern. $parserSong = app(ProExportService::class)->generateParserSong($song, null, $bene); // Alle drei Sektionen (Strophe, Refrain, Bridge) als Gruppen emittiert. $this->assertCount(3, $parserSong->getGroups(), 'Union muss alle referenzierten Sektionen als Gruppen emittieren'); foreach ($parserSong->getArrangements() as $arrangement) { $resolved = $parserSong->getGroupsForArrangement($arrangement); $this->assertCount( 2, $resolved, "Arrangement '{$arrangement->getName()}' muss alle 2 Gruppen-Referenzen auflösen können", ); } } public function test_playlist_export_setzt_arrangement_name_auf_gewaehltes_arrangement(): void { Storage::fake('public'); $service = Service::factory()->create(['title' => 'Arrangement Service', 'date' => now()]); [$song, $normal, $bene] = $this->createSongWithTwoArrangements('Playlist Song'); $serviceSong = ServiceSong::create([ 'service_id' => $service->id, 'song_id' => $song->id, 'song_arrangement_id' => $bene->id, 'cts_song_name' => 'Playlist Song', 'order' => 1, ]); ServiceAgendaItem::factory()->create([ 'service_id' => $service->id, 'title' => 'Playlist Song', 'service_song_id' => $serviceSong->id, 'sort_order' => 1, 'is_before_event' => false, ]); $result = app(PlaylistExportService::class)->generatePlaylist($service); $playlist = ProPlaylistReader::read($result['path']); $songEntry = null; foreach ($playlist->getEntries() as $entry) { if ($entry->getName() === 'Playlist Song') { $songEntry = $entry; break; } } $this->assertNotNull($songEntry, 'Song-Eintrag muss in der Playlist vorhanden sein'); $this->assertSame('Bene', $songEntry->getArrangementName(), 'arrangement_name muss das gewählte Arrangement sein'); // Und der eingebettete .pro trägt selected_arrangement = "Bene". $embedded = $playlist->getEmbeddedSong('Playlist Song.pro'); $this->assertNotNull($embedded); $this->assertSame( $embedded->getArrangementByName('Bene')->getUuid(), $embedded->getSelectedArrangementUuid(), ); $this->cleanupTempDir($result['temp_dir']); } public function test_agenda_bundle_export_selektiert_gewaehltes_arrangement(): void { Storage::fake('public'); $service = Service::factory()->create(['title' => 'Bundle Service', 'date' => now()]); [$song, $normal, $bene] = $this->createSongWithTwoArrangements('Bundle Song'); $serviceSong = ServiceSong::create([ 'service_id' => $service->id, 'song_id' => $song->id, 'song_arrangement_id' => $bene->id, 'cts_song_name' => 'Bundle Song', 'order' => 1, ]); $agendaItem = ServiceAgendaItem::factory()->create([ 'service_id' => $service->id, 'title' => 'Bundle Song', 'service_song_id' => $serviceSong->id, 'sort_order' => 1, 'is_before_event' => false, ]); $bundlePath = app(ProBundleExportService::class)->generateAgendaItemBundle($agendaItem); $bundleSong = ProBundleReader::read($bundlePath)->getSong(); $beneParser = $bundleSong->getArrangementByName('Bene'); $this->assertNotNull($beneParser, 'Bundle .pro muss das Arrangement "Bene" enthalten'); $this->assertSame( $beneParser->getUuid(), $bundleSong->getSelectedArrangementUuid(), 'Bundle .pro selected_arrangement muss "Bene" sein', ); $this->assertNotNull($bundleSong->getArrangementByName('Normal'), 'Alle Arrangements bleiben erhalten'); @unlink($bundlePath); } private function cleanupTempDir(string $dir): void { if (! is_dir($dir)) { return; } $items = scandir($dir); if ($items === false) { return; } foreach ($items as $item) { if ($item === '.' || $item === '..') { continue; } $path = $dir.'/'.$item; is_dir($path) ? $this->cleanupTempDir($path) : unlink($path); } rmdir($dir); } }