pp-planer/tests/Feature/ExportBundleRelativeMediaTest.php
Thorsten Bus 94170857b6 feat(export): render uploaded slide images as slide content, keep background/keyvisual as background media action
- New SlideMediaElementBuilder converts FOREGROUND (uploaded) image media
  actions into slide-content fill.media elements placed at the top of the
  slide content, so uploaded information/moderation/sermon images render as
  real content rather than a media-layer action.
- Service background and key-visual images remain BACKGROUND media actions on
  the bottom layer; song text and name-tag stay text content elements.
- Wire the builder into ProExportService and ProBundleExportService (after
  generate) and split PlaylistExportService::writeProFile into
  generate -> inject -> write via ProFileWriter so the playlist path applies
  the same rule.
- Parser library unchanged.
- Migrate export tests to the combined rule via a shared InspectsSlideFillMedia
  support trait and add SlideMediaElementTest.
2026-07-07 22:15:12 +02:00

160 lines
5.4 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\Support\InspectsSlideFillMedia;
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 InspectsSlideFillMedia;
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)
{
parent::__construct(app(\App\Services\MacroResolutionService::class));
$this->captured = &$captured;
}
protected function writeProFile(string $path, string $name, array $groups, array $arrangements, array $options = []): 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();
// The foreground image is now a content fill.media element, not a media ACTION.
$mediaSlides = array_filter($slides, fn ($slide) => $this->fillMediaUrls($slide) !== []);
$this->assertNotEmpty($mediaSlides, 'Expected a foreground-image content slide in the bundle.');
foreach ($mediaSlides as $slide) {
$this->assertFalse($this->hasImageMediaAction($slide), 'No image media ACTION may remain.');
foreach ($this->fillMediaUrls($slide) as $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);
}
}