From e95abbc1e6cf3c06c233ee605579bd8c2c2e2dac Mon Sep 17 00:00:00 2001 From: Thorsten Bus Date: Sun, 31 May 2026 06:30:25 +0200 Subject: [PATCH] feat(export): sermon sequence + moderator injection --- tests/Feature/PlaylistSequenceTest.php | 307 ++++++++++++++++--------- 1 file changed, 203 insertions(+), 104 deletions(-) diff --git a/tests/Feature/PlaylistSequenceTest.php b/tests/Feature/PlaylistSequenceTest.php index 4fd111e..daa626b 100644 --- a/tests/Feature/PlaylistSequenceTest.php +++ b/tests/Feature/PlaylistSequenceTest.php @@ -2,13 +2,17 @@ namespace Tests\Feature; +use App\Models\Label; use App\Models\Service; use App\Models\ServiceAgendaItem; +use App\Models\ServiceSong; use App\Models\Setting; use App\Models\Slide; +use App\Models\Song; use App\Services\PlaylistExportService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Storage; +use ProPresenter\Parser\PlaylistArchive; use ProPresenter\Parser\ProPlaylistReader; use Tests\TestCase; @@ -23,144 +27,239 @@ protected function setUp(): void Storage::fake('public'); } - public function test_sermon_sequence_prepends_keyvisual_and_preacher_nametag_before_slides(): void + public function test_sermon_sequence_is_keyvisual_preacher_nametag_then_uploaded_sermon_slides(): void { - Setting::set('namenseinblender_macro_name', 'Namenseinblender'); + $this->configureNameTagMacro(); Storage::disk('public')->put('slides/keyvisual.jpg', 'keyvisual-image'); - Storage::disk('public')->put('slides/sermon.jpg', 'sermon-image'); + Storage::disk('public')->put('slides/sermon-1.jpg', 'sermon-one'); + Storage::disk('public')->put('slides/sermon-2.jpg', 'sermon-two'); $service = Service::factory()->create([ - 'title' => 'Predigt Service', + 'title' => 'Predigt Sequenz', 'date' => now(), 'key_visual_filename' => 'slides/keyvisual.jpg', + 'preacher_name_override' => 'Erika Predigt', + ]); + $sermonItem = ServiceAgendaItem::factory()->create([ + 'service_id' => $service->id, + 'title' => 'Predigt', + 'service_song_id' => null, + 'sort_order' => 1, + 'is_before_event' => false, + ]); + $this->createSermonSlide($service, $sermonItem, 'sermon-1.jpg', 0); + $this->createSermonSlide($service, $sermonItem, 'sermon-2.jpg', 1); + + $result = app(PlaylistExportService::class)->generatePlaylist($service); + $playlist = ProPlaylistReader::read($result['path']); + $entries = $playlist->getEntries(); + + $this->assertSame(['Keyvisual-Predigt', 'Predigername', 'Predigt'], $this->entryNames($playlist)); + + $keyVisualSlides = $this->slidesForEntry($playlist, $entries[0]); + $this->assertCount(1, $keyVisualSlides); + $this->assertTrue($keyVisualSlides[0]->hasBackgroundMedia()); + $this->assertSame(Storage::disk('public')->path('slides/keyvisual.jpg'), $keyVisualSlides[0]->getBackgroundMediaUrl()); + + $nameTagSlides = $this->slidesForEntry($playlist, $entries[1]); + $this->assertCount(1, $nameTagSlides); + $this->assertSame("Erika Predigt\nPredigt", $nameTagSlides[0]->getPlainText()); + $this->assertTrue($nameTagSlides[0]->hasMacro()); + + $sermonSlides = $this->slidesForEntry($playlist, $entries[2]); + $this->assertCount(2, $sermonSlides); + $this->assertSame('sermon-1.jpg', $sermonSlides[0]->getLabel()); + $this->assertSame('sermon-2.jpg', $sermonSlides[1]->getLabel()); + $this->assertSame(2, Slide::count()); + + $this->cleanupTempDir($result['temp_dir']); + } + + public function test_moderator_nametag_is_first_presentation_for_first_visible_agenda_item(): void + { + $this->configureNameTagMacro(); + + $service = Service::factory()->create([ + 'title' => 'Moderator Sequenz', + 'moderator_name' => 'Max Moderation', + ]); + ServiceAgendaItem::factory()->create([ + 'service_id' => $service->id, + 'title' => 'Vorprogramm', + 'service_song_id' => null, + 'sort_order' => 1, + 'is_before_event' => true, + 'responsible' => [['name' => 'Versteckte Person']], + ]); + $song = $this->createSongWithContent('Erstes sichtbares Lied'); + $serviceSong = ServiceSong::create([ + 'service_id' => $service->id, + 'song_id' => $song->id, + 'cts_song_name' => 'Erstes sichtbares Lied', + 'order' => 1, + ]); + ServiceAgendaItem::factory()->create([ + 'service_id' => $service->id, + 'title' => 'Erstes sichtbares Lied', + 'service_song_id' => $serviceSong->id, + 'sort_order' => 2, + 'is_before_event' => false, + ]); + + $result = app(PlaylistExportService::class)->generatePlaylist($service); + $playlist = ProPlaylistReader::read($result['path']); + $entries = $playlist->getEntries(); + + $this->assertSame(['Moderator', 'Erstes sichtbares Lied'], $this->entryNames($playlist)); + $moderatorSlides = $this->slidesForEntry($playlist, $entries[0]); + $this->assertCount(1, $moderatorSlides); + $this->assertSame("Max Moderation\nModeration", $moderatorSlides[0]->getPlainText()); + $this->assertTrue($moderatorSlides[0]->hasMacro()); + + $this->cleanupTempDir($result['temp_dir']); + } + + public function test_without_macro_configured_no_nametags_are_added_and_sermon_sequence_keeps_keyvisual_then_slides(): void + { + Storage::disk('public')->put('slides/keyvisual.jpg', 'keyvisual-image'); + Storage::disk('public')->put('slides/sermon-1.jpg', 'sermon-one'); + Storage::disk('public')->put('slides/sermon-2.jpg', 'sermon-two'); + + $service = Service::factory()->create([ + 'title' => 'Ohne Namenseinblender', + 'date' => now(), + 'key_visual_filename' => 'slides/keyvisual.jpg', + 'moderator_name' => 'Max Moderation', + 'preacher_name_override' => 'Erika Predigt', + ]); + $sermonItem = ServiceAgendaItem::factory()->create([ + 'service_id' => $service->id, + 'title' => 'Predigt', + 'service_song_id' => null, + 'sort_order' => 1, + 'is_before_event' => false, + ]); + $this->createSermonSlide($service, $sermonItem, 'sermon-1.jpg', 0); + $this->createSermonSlide($service, $sermonItem, 'sermon-2.jpg', 1); + + $result = app(PlaylistExportService::class)->generatePlaylist($service); + $playlist = ProPlaylistReader::read($result['path']); + $entries = $playlist->getEntries(); + + $this->assertSame(['Keyvisual-Predigt', 'Predigt'], $this->entryNames($playlist)); + $this->assertCount(1, $this->slidesForEntry($playlist, $entries[0])); + $sermonSlides = $this->slidesForEntry($playlist, $entries[1]); + $this->assertCount(2, $sermonSlides); + + foreach (array_keys($playlist->getEmbeddedProFiles()) as $filename) { + foreach ($this->allParserSlides($playlist->getEmbeddedSong($filename)) as $slide) { + $this->assertFalse($slide->hasMacro()); + } + } + + $this->cleanupTempDir($result['temp_dir']); + } + + public function test_without_moderator_name_no_moderator_nametag_is_added(): void + { + $this->configureNameTagMacro(); + + $service = Service::factory()->create([ + 'title' => 'Ohne Moderator', 'moderator_name' => null, - 'preacher_name' => 'Pastor Paul', - 'preacher_name_override' => null, ]); - - $agendaItem = ServiceAgendaItem::factory()->create([ + $song = $this->createSongWithContent('Startlied'); + $serviceSong = ServiceSong::create([ 'service_id' => $service->id, - 'title' => 'Predigt', - 'service_song_id' => null, + 'song_id' => $song->id, + 'cts_song_name' => 'Startlied', + 'order' => 1, + ]); + ServiceAgendaItem::factory()->create([ + 'service_id' => $service->id, + 'title' => 'Startlied', + 'service_song_id' => $serviceSong->id, 'sort_order' => 1, 'is_before_event' => false, - ]); - - Slide::factory()->create([ - 'service_id' => $service->id, - 'service_agenda_item_id' => $agendaItem->id, - 'type' => 'sermon', - 'original_filename' => 'sermon.jpg', - 'stored_filename' => 'slides/sermon.jpg', - 'sort_order' => 0, + 'responsible' => [], ]); $result = app(PlaylistExportService::class)->generatePlaylist($service); $playlist = ProPlaylistReader::read($result['path']); - $names = array_map(fn ($entry) => $entry->getName(), $playlist->getEntries()); - - $kvIndex = array_search('Keyvisual-Predigt', $names, true); - $nameTagIndex = array_search('Predigername', $names, true); - $sermonIndex = array_search('Predigt', $names, true); - - $this->assertNotFalse($kvIndex, 'Keyvisual-Predigt entry missing'); - $this->assertNotFalse($nameTagIndex, 'Predigername entry missing'); - $this->assertNotFalse($sermonIndex, 'Predigt entry missing'); - - $this->assertLessThan($nameTagIndex, $kvIndex, 'Keyvisual must come before preacher nametag'); - $this->assertLessThan($sermonIndex, $nameTagIndex, 'Preacher nametag must come before sermon slides'); + $this->assertSame(['Startlied'], $this->entryNames($playlist)); + $this->assertNull($playlist->getEmbeddedSong('Moderator.pro')); $this->cleanupTempDir($result['temp_dir']); } - public function test_moderator_nametag_is_first_for_first_visible_agenda_item(): void + private function configureNameTagMacro(): void { Setting::set('namenseinblender_macro_name', 'Namenseinblender'); - Storage::disk('public')->put('slides/info.jpg', 'info-image'); - - $service = Service::factory()->create([ - 'title' => 'Moderator Service', - 'date' => now(), - 'key_visual_filename' => null, - 'moderator_name' => 'Moderator Max', - ]); - - $agendaItem = ServiceAgendaItem::factory()->create([ - 'service_id' => $service->id, - 'title' => 'Begrüßung', - 'service_song_id' => null, - 'sort_order' => 1, - 'is_before_event' => false, - ]); - - Slide::factory()->create([ - 'service_id' => $service->id, - 'service_agenda_item_id' => $agendaItem->id, - 'type' => 'moderation', - 'original_filename' => 'info.jpg', - 'stored_filename' => 'slides/info.jpg', - 'sort_order' => 0, - ]); - - $result = app(PlaylistExportService::class)->generatePlaylist($service); - $playlist = ProPlaylistReader::read($result['path']); - - $names = array_map(fn ($entry) => $entry->getName(), $playlist->getEntries()); - - $this->assertNotEmpty($names); - $this->assertSame('Moderator', $names[0], 'Moderator nametag must be the first playlist entry'); - - $this->cleanupTempDir($result['temp_dir']); + Setting::set('namenseinblender_macro_uuid', '11111111-1111-4111-8111-111111111111'); + Setting::set('namenseinblender_macro_collection_name', 'Service Macros'); + Setting::set('namenseinblender_macro_collection_uuid', '22222222-2222-4222-8222-222222222222'); } - public function test_without_macro_no_nametags_are_injected(): void + private function createSermonSlide(Service $service, ServiceAgendaItem $agendaItem, string $filename, int $sortOrder): Slide { - Setting::set('namenseinblender_macro_name', ''); - Storage::disk('public')->put('slides/keyvisual.jpg', 'keyvisual-image'); - Storage::disk('public')->put('slides/sermon.jpg', 'sermon-image'); - - $service = Service::factory()->create([ - 'title' => 'Ohne Macro', - 'date' => now(), - 'key_visual_filename' => 'slides/keyvisual.jpg', - 'moderator_name' => 'Moderator Max', - 'preacher_name' => 'Pastor Paul', - 'preacher_name_override' => null, - ]); - - $agendaItem = ServiceAgendaItem::factory()->create([ - 'service_id' => $service->id, - 'title' => 'Predigt', - 'service_song_id' => null, - 'sort_order' => 1, - 'is_before_event' => false, - ]); - - Slide::factory()->create([ + return Slide::factory()->create([ 'service_id' => $service->id, 'service_agenda_item_id' => $agendaItem->id, 'type' => 'sermon', - 'original_filename' => 'sermon.jpg', - 'stored_filename' => 'slides/sermon.jpg', - 'sort_order' => 0, + 'original_filename' => $filename, + 'stored_filename' => 'slides/'.$filename, + 'sort_order' => $sortOrder, + ]); + } + + private function createSongWithContent(string $title): Song + { + $song = Song::create([ + 'title' => $title, + 'ccli_id' => fake()->unique()->numerify('#####'), + 'author' => 'Test Author', + 'copyright_text' => 'Test Publisher', ]); - $result = app(PlaylistExportService::class)->generatePlaylist($service); - $playlist = ProPlaylistReader::read($result['path']); + $label = Label::firstOrCreate( + ['name' => 'Verse 1 - '.$title], + ['color' => '#2196F3'], + ); + $label->songSlides()->create(['order' => 0, 'text_content' => 'Erste Zeile']); - $names = array_map(fn ($entry) => $entry->getName(), $playlist->getEntries()); + $arrangement = $song->arrangements()->create(['name' => 'normal', 'is_default' => true]); + $arrangement->arrangementLabels()->create(['label_id' => $label->id, 'order' => 0]); - $this->assertNotContains('Moderator', $names); - $this->assertNotContains('Predigername', $names); + return $song; + } - $kvIndex = array_search('Keyvisual-Predigt', $names, true); - $sermonIndex = array_search('Predigt', $names, true); - $this->assertNotFalse($kvIndex, 'Keyvisual-Predigt entry missing'); - $this->assertNotFalse($sermonIndex, 'Predigt entry missing'); - $this->assertLessThan($sermonIndex, $kvIndex, 'Keyvisual must come before sermon slides'); + /** @return array */ + private function entryNames(PlaylistArchive $playlist): array + { + return array_map(fn ($entry) => $entry->getName(), $playlist->getEntries()); + } - $this->cleanupTempDir($result['temp_dir']); + private function slidesForEntry(PlaylistArchive $playlist, $entry): array + { + $filename = $entry->getDocumentFilename(); + $this->assertNotNull($filename); + + return $this->allParserSlides($playlist->getEmbeddedSong($filename)); + } + + private function allParserSlides(?\ProPresenter\Parser\Song $parserSong): array + { + $this->assertNotNull($parserSong); + + $slides = []; + foreach ($parserSong->getGroups() as $group) { + foreach ($parserSong->getSlidesForGroup($group) as $slide) { + $slides[] = $slide; + } + } + + return $slides; } private function cleanupTempDir(string $dir): void