pp-planer/tests/Feature/ExportBundleRelativeMediaTest.php
Thorsten Bus 42b8b5f428 fix(export): copyright/blank slides, bundle-relative media, probundle injection, song links
- populate COPYRIGHT (title/author/copyright/CCLI) + blank slides on every song; songHasContent ignores locked sections
- foreground info/moderation images now bundle-relative (fixes blank images)
- pre-added .probundle injection: Zip64-fix + verbatim .pro extraction (fixes empty bundle)
- nametag subtitle split (text + subtitle); smaller non-bold render
- skip songs with no content slides at export with German warning
- link service agenda songs to SongDB edit modal via #song-<id>
- allow CCLI import of metadata-only songs (no lyric sections)
- expose has_content_slides on service songs; show "Keine Inhaltsfolien"
2026-06-21 09:58:55 +02:00

155 lines
5 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\Service;
use App\Models\Slide;
use App\Services\PlaylistExportService;
use App\Services\ProBundleExportService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;
use ProPresenter\Parser\ProBundleReader;
use Tests\TestCase;
/**
* FIX 2: Foreground image slide data (information/moderation/sermon blocks) must
* carry 'bundleRelative' => true so the .pro references the embedded image by a
* bundle-relative resource URL instead of an absolute path. Otherwise the slide
* renders blank on the presenter PC.
*/
final class ExportBundleRelativeMediaTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Storage::fake('public');
}
private function createSlideFile(string $storedFilename): void
{
$image = imagecreatetruecolor(1920, 1080);
ob_start();
imagejpeg($image);
$contents = ob_get_clean();
imagedestroy($image);
Storage::disk('public')->put($storedFilename, $contents);
}
private function createSlide(array $attributes): Slide
{
return Slide::create(array_merge([
'thumbnail_filename' => 'thumb.jpg',
'uploaded_at' => now()->subDay(),
], $attributes));
}
public function test_playlist_export_setzt_bundle_relative_auf_info_folien_slide_daten(): void
{
$service = Service::factory()->create(['finalized_at' => now(), 'date' => now()]);
$this->createSlideFile('slides/info1.jpg');
$this->createSlide([
'type' => 'information',
'service_id' => null,
'original_filename' => 'info1.jpg',
'stored_filename' => 'slides/info1.jpg',
'sort_order' => 1,
]);
$capturedGroups = [];
$exportService = new class($capturedGroups) extends PlaylistExportService
{
/** @var array<int, array> */
private array $captured;
public function __construct(array &$captured)
{
$this->captured = &$captured;
}
protected function writeProFile(string $path, string $name, array $groups, array $arrangements): void
{
$this->captured[] = $groups;
file_put_contents($path, 'mock-pro:'.$name);
}
protected function writePlaylistFile(string $path, string $name, array $items, array $embeddedFiles): void
{
file_put_contents($path, 'mock-playlist:'.$name);
}
};
$result = $exportService->generatePlaylist($service);
// Find the slide data array carrying our image media.
$found = false;
foreach ($capturedGroups as $groups) {
foreach ($groups as $group) {
foreach ($group['slides'] ?? [] as $slideData) {
if (isset($slideData['media'])) {
$found = true;
$this->assertArrayHasKey('bundleRelative', $slideData);
$this->assertTrue($slideData['bundleRelative']);
}
}
}
}
$this->assertTrue($found, 'Expected at least one media slide in captured pro groups.');
$this->cleanupTempDir($result['temp_dir']);
}
public function test_probundle_export_referenziert_vordergrund_medien_bundle_relativ(): void
{
$service = Service::factory()->create(['finalized_at' => now(), 'date' => now()]);
$this->createSlideFile('slides/mod1.jpg');
$this->createSlide([
'type' => 'moderation',
'service_id' => $service->id,
'original_filename' => 'mod1.jpg',
'stored_filename' => 'slides/mod1.jpg',
'sort_order' => 1,
]);
$bundlePath = app(ProBundleExportService::class)->generateBundle($service, 'moderation');
$this->assertFileExists($bundlePath);
$bundle = ProBundleReader::read($bundlePath);
$slides = $bundle->getSong()->getSlides();
$mediaSlides = array_filter($slides, fn ($slide) => $slide->hasMedia());
$this->assertNotEmpty($mediaSlides, 'Expected a foreground-media slide in the bundle.');
foreach ($mediaSlides as $slide) {
$url = $slide->getMediaUrl();
$this->assertNotNull($url);
// Bundle-relative resources use a bare basename (no absolute path / no slash).
$this->assertStringNotContainsString('/', $url, 'Foreground media URL must be bundle-relative (basename only).');
}
@unlink($bundlePath);
}
private function cleanupTempDir(string $dir): void
{
if (! is_dir($dir)) {
return;
}
foreach (scandir($dir) as $item) {
if ($item === '.' || $item === '..') {
continue;
}
$path = $dir.'/'.$item;
is_dir($path) ? $this->cleanupTempDir($path) : unlink($path);
}
rmdir($dir);
}
}