419 lines
16 KiB
PHP
419 lines
16 KiB
PHP
<?php
|
|
|
|
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;
|
|
|
|
final class PlaylistSequenceTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
Storage::fake('public');
|
|
}
|
|
|
|
public function test_sermon_sequence_is_keyvisual_preacher_nametag_then_uploaded_sermon_slides(): void
|
|
{
|
|
$this->configureNameTagMacro();
|
|
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' => '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();
|
|
|
|
$names = $this->entryNames($playlist);
|
|
// A moderator name tag may precede the sermon intro when the (factory-random)
|
|
// responsible person resolves to a moderator; skip it if present.
|
|
$offset = str_starts_with((string) ($names[0] ?? ''), 'Moderator') ? 1 : 0;
|
|
$this->assertSame(['Prediger - Erika Predigt', 'Predigt'], array_slice($names, $offset));
|
|
|
|
// The sermon intro is ONE presentation with two cues: the key-visual alone,
|
|
// then the same key-visual with the preacher name tag rendered on top.
|
|
$introSlides = $this->slidesForEntry($playlist, $entries[$offset]);
|
|
$this->assertCount(2, $introSlides);
|
|
|
|
$this->assertTrue($introSlides[0]->hasBackgroundMedia());
|
|
$this->assertSame('KEY_VISUAL.jpg', $introSlides[0]->getBackgroundMediaUrl());
|
|
$this->assertArrayHasKey('KEY_VISUAL.jpg', $playlist->getEmbeddedMediaFiles());
|
|
|
|
$this->assertTrue($introSlides[1]->hasBackgroundMedia());
|
|
$this->assertSame('KEY_VISUAL.jpg', $introSlides[1]->getBackgroundMediaUrl());
|
|
// Name and role are split: the name is the main (\fs84) run and the
|
|
// role is a separate smaller, non-bold (\b0\fs50) subtitle run.
|
|
[$name, $subtitle] = $this->nameTagNameAndSubtitle($introSlides[1]);
|
|
$this->assertSame('Erika Predigt', $name);
|
|
$this->assertSame('Predigt', $subtitle);
|
|
$this->assertTrue($introSlides[1]->hasMacro());
|
|
|
|
$sermonSlides = $this->slidesForEntry($playlist, $entries[$offset + 1]);
|
|
$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 - Max Moderation', 'Erstes sichtbares Lied'], $this->entryNames($playlist));
|
|
$moderatorSlides = $this->slidesForEntry($playlist, $entries[0]);
|
|
$this->assertCount(1, $moderatorSlides);
|
|
// Name and role are split: the name is the main run, the role the subtitle run.
|
|
[$name, $subtitle] = $this->nameTagNameAndSubtitle($moderatorSlides[0]);
|
|
$this->assertSame('Max Moderation', $name);
|
|
$this->assertSame('Moderation', $subtitle);
|
|
$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,
|
|
]);
|
|
$song = $this->createSongWithContent('Startlied');
|
|
$serviceSong = ServiceSong::create([
|
|
'service_id' => $service->id,
|
|
'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,
|
|
'responsible' => [],
|
|
]);
|
|
|
|
$result = app(PlaylistExportService::class)->generatePlaylist($service);
|
|
$playlist = ProPlaylistReader::read($result['path']);
|
|
|
|
$this->assertSame(['Startlied'], $this->entryNames($playlist));
|
|
$this->assertNull($playlist->getEmbeddedSong('Moderator.pro'));
|
|
|
|
$this->cleanupTempDir($result['temp_dir']);
|
|
}
|
|
|
|
public function test_nametag_presentations_are_named_by_role_and_person(): void
|
|
{
|
|
$this->configureNameTagMacro();
|
|
Storage::disk('public')->put('slides/keyvisual.jpg', 'keyvisual-image');
|
|
Storage::disk('public')->put('slides/sermon.jpg', 'sermon-image');
|
|
|
|
$service = Service::factory()->create([
|
|
'title' => 'Rollen Benennung',
|
|
'date' => now(),
|
|
'key_visual_filename' => 'slides/keyvisual.jpg',
|
|
'moderator_name' => 'Max Moderation',
|
|
'preacher_name_override' => 'Erika Predigt',
|
|
]);
|
|
|
|
$song = $this->createSongWithContent('Eröffnungslied');
|
|
$serviceSong = ServiceSong::create([
|
|
'service_id' => $service->id,
|
|
'song_id' => $song->id,
|
|
'cts_song_name' => 'Eröffnungslied',
|
|
'order' => 1,
|
|
]);
|
|
ServiceAgendaItem::factory()->create([
|
|
'service_id' => $service->id,
|
|
'title' => 'Eröffnungslied',
|
|
'service_song_id' => $serviceSong->id,
|
|
'sort_order' => 1,
|
|
'is_before_event' => false,
|
|
]);
|
|
$sermonItem = ServiceAgendaItem::factory()->create([
|
|
'service_id' => $service->id,
|
|
'title' => 'Predigt',
|
|
'service_song_id' => null,
|
|
'sort_order' => 2,
|
|
'is_before_event' => false,
|
|
]);
|
|
$this->createSermonSlide($service, $sermonItem, 'sermon.jpg', 0);
|
|
|
|
$result = app(PlaylistExportService::class)->generatePlaylist($service);
|
|
$playlist = ProPlaylistReader::read($result['path']);
|
|
$names = $this->entryNames($playlist);
|
|
|
|
$this->assertContains('Moderator - Max Moderation', $names);
|
|
$this->assertContains('Prediger - Erika Predigt', $names);
|
|
|
|
$this->cleanupTempDir($result['temp_dir']);
|
|
}
|
|
|
|
public function test_sermon_item_without_uploaded_slides_still_emits_intro_and_no_images_presentation(): void
|
|
{
|
|
$this->configureNameTagMacro();
|
|
Setting::set('agenda_sermon_matching', 'Predigt');
|
|
Storage::disk('public')->put('slides/keyvisual.jpg', 'keyvisual-image');
|
|
|
|
$service = Service::factory()->create([
|
|
'title' => 'Predigt ohne Folien',
|
|
'date' => now(),
|
|
'key_visual_filename' => 'slides/keyvisual.jpg',
|
|
'preacher_name_override' => 'Erika Predigt',
|
|
]);
|
|
ServiceAgendaItem::factory()->create([
|
|
'service_id' => $service->id,
|
|
'title' => 'Predigt',
|
|
'service_song_id' => null,
|
|
'sort_order' => 1,
|
|
'is_before_event' => false,
|
|
'responsible' => [],
|
|
]);
|
|
|
|
$result = app(PlaylistExportService::class)->generatePlaylist($service);
|
|
$playlist = ProPlaylistReader::read($result['path']);
|
|
$entries = $playlist->getEntries();
|
|
|
|
// A sermon item with no uploaded slides emits ONLY the intro presentation.
|
|
$this->assertSame(['Prediger - Erika Predigt'], $this->entryNames($playlist));
|
|
|
|
// The intro .pro carries both cues: key-visual alone + key-visual with name tag.
|
|
$introSlides = $this->slidesForEntry($playlist, $entries[0]);
|
|
$this->assertCount(2, $introSlides);
|
|
$this->assertTrue($introSlides[0]->hasBackgroundMedia());
|
|
$this->assertSame('KEY_VISUAL.jpg', $introSlides[0]->getBackgroundMediaUrl());
|
|
$this->assertTrue($introSlides[1]->hasBackgroundMedia());
|
|
$this->assertSame('KEY_VISUAL.jpg', $introSlides[1]->getBackgroundMediaUrl());
|
|
[$name, $subtitle] = $this->nameTagNameAndSubtitle($introSlides[1]);
|
|
$this->assertSame('Erika Predigt', $name);
|
|
$this->assertSame('Predigt', $subtitle);
|
|
$this->assertTrue($introSlides[1]->hasMacro());
|
|
|
|
// No separate sermon-images presentation is emitted.
|
|
$this->assertNull($playlist->getEmbeddedSong('Predigt.pro'));
|
|
|
|
$this->cleanupTempDir($result['temp_dir']);
|
|
}
|
|
|
|
private function configureNameTagMacro(): void
|
|
{
|
|
Setting::set('namenseinblender_macro_name', 'Namenseinblender');
|
|
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');
|
|
}
|
|
|
|
private function createSermonSlide(Service $service, ServiceAgendaItem $agendaItem, string $filename, int $sortOrder): Slide
|
|
{
|
|
return Slide::factory()->create([
|
|
'service_id' => $service->id,
|
|
'service_agenda_item_id' => $agendaItem->id,
|
|
'type' => 'sermon',
|
|
'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',
|
|
]);
|
|
|
|
$label = Label::firstOrCreate(
|
|
['name' => 'Verse 1 - '.$title],
|
|
['color' => '#2196F3'],
|
|
);
|
|
$section = $song->sections()->create(['label_id' => $label->id, 'order' => 0]);
|
|
$section->slides()->create(['order' => 0, 'text_content' => 'Erste Zeile']);
|
|
|
|
$arrangement = $song->arrangements()->create(['name' => 'normal', 'is_default' => true]);
|
|
$arrangement->arrangementSections()->create(['song_section_id' => $section->id, 'order' => 0]);
|
|
|
|
return $song;
|
|
}
|
|
|
|
/** @return array<int, string> */
|
|
private function entryNames(PlaylistArchive $playlist): array
|
|
{
|
|
return array_map(fn ($entry) => $entry->getName(), $playlist->getEntries());
|
|
}
|
|
|
|
private function slidesForEntry(PlaylistArchive $playlist, $entry): array
|
|
{
|
|
$filename = $entry->getDocumentFilename();
|
|
$this->assertNotNull($filename);
|
|
|
|
return $this->allParserSlides($playlist->getEmbeddedSong($filename));
|
|
}
|
|
|
|
/**
|
|
* Split a name-tag slide into its [name, subtitle] parts. The generator emits
|
|
* the name as the main (\fs84) run and the role as a separate smaller,
|
|
* non-bold (\b0\fs50) subtitle run on a new line inside the same RTF element.
|
|
*
|
|
* @return array{0: string, 1: string}
|
|
*/
|
|
private function nameTagNameAndSubtitle(\ProPresenter\Parser\Slide $slide): array
|
|
{
|
|
$elements = $slide->getTextElements();
|
|
$this->assertNotEmpty($elements);
|
|
$rtf = $elements[0]->getRtfData();
|
|
$this->assertStringContainsString('\b0\fs50', $rtf, 'Name-tag slide is missing the subtitle run');
|
|
|
|
// getPlainText() returns both runs joined by a newline ("Name\nRole").
|
|
$plain = $slide->getPlainText();
|
|
$parts = explode("\n", $plain, 2);
|
|
$this->assertCount(2, $parts, 'Name-tag slide does not contain a name and a subtitle line');
|
|
|
|
return [trim($parts[0]), trim($parts[1])];
|
|
}
|
|
|
|
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
|
|
{
|
|
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);
|
|
}
|
|
}
|