- 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.
221 lines
9.1 KiB
PHP
221 lines
9.1 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Label;
|
|
use App\Models\Service;
|
|
use App\Models\Song;
|
|
use App\Services\ProExportService;
|
|
use App\Services\SlideMediaElementBuilder;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use ProPresenter\Parser\ProFileGenerator;
|
|
use ProPresenter\Parser\ProFileReader;
|
|
use ProPresenter\Parser\Slide as ParserSlide;
|
|
use Tests\Support\InspectsSlideFillMedia;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* Verifies the COMBINED image rule enforced by SlideMediaElementBuilder:
|
|
*
|
|
* 1. Song slide → text element + BACKGROUND media action, NO image element.
|
|
* 2. Uploaded content slide → full-frame fill.media content element, NO foreground
|
|
* action (a service background, if set, stays a
|
|
* BACKGROUND media action behind the element).
|
|
* 3. Key-visual slide → BACKGROUND media action only, NO content element.
|
|
* 4. Name-tag slide → text element + BACKGROUND media action + macro.
|
|
*/
|
|
final class SlideMediaElementTest extends TestCase
|
|
{
|
|
use InspectsSlideFillMedia;
|
|
use RefreshDatabase;
|
|
|
|
private function bgData(string $name = 'BACKGROUND.jpg'): array
|
|
{
|
|
return ['path' => $name, 'format' => 'JPG', 'width' => 1920, 'height' => 1080, 'bundleRelative' => true];
|
|
}
|
|
|
|
/** Generate a one-slide song and run the production builder over it. */
|
|
private function build(array $slideData, string $name = 'Test'): ParserSlide
|
|
{
|
|
$groups = [['name' => 'Gruppe', 'color' => [0, 0, 0, 1], 'slides' => [$slideData]]];
|
|
$arrangements = [['name' => 'normal', 'groupNames' => ['Gruppe']]];
|
|
|
|
$song = ProFileGenerator::generate($name, $groups, $arrangements);
|
|
app(SlideMediaElementBuilder::class)->replaceImageActionsWithElements($song);
|
|
|
|
return $song->getSlides()[0];
|
|
}
|
|
|
|
public function test_song_slide_keeps_background_action_and_text_without_image_element(): void
|
|
{
|
|
$groups = [['name' => 'Gruppe', 'color' => [0, 0, 0, 1], 'slides' => [[
|
|
'text' => 'Textzeile',
|
|
'background' => $this->bgData(),
|
|
]]]];
|
|
$arrangements = [['name' => 'normal', 'groupNames' => ['Gruppe']]];
|
|
|
|
$song = ProFileGenerator::generate('Song', $groups, $arrangements);
|
|
|
|
// Sanity: the generator emits a BACKGROUND media action before conversion.
|
|
$this->assertTrue($song->getSlides()[0]->hasBackgroundMedia());
|
|
|
|
app(SlideMediaElementBuilder::class)->replaceImageActionsWithElements($song);
|
|
$slide = $song->getSlides()[0];
|
|
|
|
// Background stays a BACKGROUND media action (NOT converted to an element).
|
|
$this->assertTrue($slide->hasBackgroundMedia());
|
|
$this->assertSame('BACKGROUND.jpg', $slide->getBackgroundMediaUrl());
|
|
|
|
// Text element preserved; no content fill.media element exists.
|
|
$this->assertTrue($this->elementHasTextAt($slide, 0));
|
|
$this->assertSame([], $this->fillMediaUrls($slide));
|
|
|
|
// No foreground image action.
|
|
$this->assertFalse($this->hasForegroundImageMediaAction($slide));
|
|
$this->assertFalse($slide->hasMedia());
|
|
}
|
|
|
|
public function test_uploaded_content_slide_becomes_full_frame_element_without_foreground_action(): void
|
|
{
|
|
// Production uploaded-slide data: media + format + label + bundleRelative (no text).
|
|
$slide = $this->build([
|
|
'media' => 'UPLOAD.jpg',
|
|
'format' => 'JPG',
|
|
'label' => 'upload.jpg',
|
|
'bundleRelative' => true,
|
|
]);
|
|
|
|
// A single full-frame content fill.media element, no text.
|
|
$this->assertCount(1, $this->baseSlideElements($slide));
|
|
$this->assertSame('UPLOAD.jpg', $this->fillMediaUrlAt($slide, 0));
|
|
$this->assertTrue($this->isFullFrameAt($slide, 0));
|
|
$this->assertFalse($this->elementHasTextAt($slide, 0));
|
|
|
|
// No foreground media action remains; no background configured at all.
|
|
$this->assertFalse($this->hasForegroundImageMediaAction($slide));
|
|
$this->assertFalse($slide->hasBackgroundMedia());
|
|
$this->assertFalse($this->hasImageMediaAction($slide));
|
|
}
|
|
|
|
public function test_uploaded_content_slide_with_service_background_keeps_background_action_behind_element(): void
|
|
{
|
|
$slide = $this->build([
|
|
'media' => 'UPLOAD.jpg',
|
|
'format' => 'JPG',
|
|
'label' => 'upload.jpg',
|
|
'bundleRelative' => true,
|
|
'background' => $this->bgData(),
|
|
]);
|
|
|
|
// Uploaded image is a full-frame content element on top.
|
|
$this->assertSame('UPLOAD.jpg', $this->fillMediaUrlAt($slide, 0));
|
|
$this->assertTrue($this->isFullFrameAt($slide, 0));
|
|
$this->assertFalse($this->hasForegroundImageMediaAction($slide));
|
|
|
|
// Service background stays a BACKGROUND media action behind the element.
|
|
$this->assertTrue($slide->hasBackgroundMedia());
|
|
$this->assertSame('BACKGROUND.jpg', $slide->getBackgroundMediaUrl());
|
|
}
|
|
|
|
public function test_key_visual_slide_is_background_action_only_without_content_element(): void
|
|
{
|
|
// Production key-visual slide data: imageOnly + background.
|
|
$slide = $this->build([
|
|
'imageOnly' => true,
|
|
'background' => $this->bgData('KEY_VISUAL.jpg'),
|
|
]);
|
|
|
|
// Key-visual is a BACKGROUND media action, not a content element.
|
|
$this->assertTrue($slide->hasBackgroundMedia());
|
|
$this->assertSame('KEY_VISUAL.jpg', $slide->getBackgroundMediaUrl());
|
|
|
|
// No content fill.media element and no foreground action.
|
|
$this->assertSame([], $this->fillMediaUrls($slide));
|
|
$this->assertFalse($this->hasForegroundImageMediaAction($slide));
|
|
}
|
|
|
|
public function test_name_tag_slide_keeps_text_background_action_and_macro(): void
|
|
{
|
|
$slide = $this->build([
|
|
'text' => 'Erika Predigt',
|
|
'subtitle' => 'Predigt',
|
|
'background' => $this->bgData('KEY_VISUAL.jpg'),
|
|
'macro' => [
|
|
'name' => 'Namenseinblender',
|
|
'uuid' => '11111111-1111-4111-8111-111111111111',
|
|
'collectionName' => '--MAIN--',
|
|
'collectionUuid' => null,
|
|
],
|
|
]);
|
|
|
|
// Text element preserved (name-tag text sits above the background).
|
|
$this->assertTrue($this->elementHasTextAt($slide, 0));
|
|
$this->assertSame([], $this->fillMediaUrls($slide));
|
|
|
|
// Key-visual behind the name tag stays a BACKGROUND media action.
|
|
$this->assertTrue($slide->hasBackgroundMedia());
|
|
$this->assertSame('KEY_VISUAL.jpg', $slide->getBackgroundMediaUrl());
|
|
|
|
// Macro action preserved.
|
|
$this->assertTrue($slide->hasMacro());
|
|
$this->assertSame('Namenseinblender', $slide->getMacroName());
|
|
|
|
// No image content element, no foreground action.
|
|
$this->assertFalse($this->hasForegroundImageMediaAction($slide));
|
|
}
|
|
|
|
public function test_exported_song_pro_roundtrip_keeps_background_action_and_text(): void
|
|
{
|
|
Storage::fake('public');
|
|
Storage::disk('public')->put('slides/background.jpg', 'background-image');
|
|
|
|
$service = Service::factory()->create(['background_filename' => 'slides/background.jpg']);
|
|
$song = $this->createSongWithContent();
|
|
|
|
$path = app(ProExportService::class)->generateProFile($song, $service);
|
|
$parserSong = ProFileReader::read($path);
|
|
@unlink($path);
|
|
|
|
$slides = [];
|
|
foreach ($parserSong->getGroups() as $group) {
|
|
foreach ($parserSong->getSlidesForGroup($group) as $slide) {
|
|
$slides[] = $slide;
|
|
}
|
|
}
|
|
|
|
$this->assertNotEmpty($slides);
|
|
|
|
foreach ($slides as $slide) {
|
|
// Background survives the .pro write+read as a BACKGROUND media action.
|
|
$this->assertTrue($slide->hasBackgroundMedia());
|
|
$this->assertSame('BACKGROUND.jpg', $slide->getBackgroundMediaUrl());
|
|
// Text preserved; no content image element; no foreground action.
|
|
$this->assertTrue($this->elementHasTextAt($slide, 0));
|
|
$this->assertNotSame('', $slide->getPlainText());
|
|
$this->assertSame([], $this->fillMediaUrls($slide));
|
|
$this->assertFalse($this->hasForegroundImageMediaAction($slide));
|
|
}
|
|
}
|
|
|
|
private function createSongWithContent(): Song
|
|
{
|
|
$song = Song::create([
|
|
'title' => 'Fill Media Song',
|
|
'ccli_id' => '99887',
|
|
'author' => 'Test Author',
|
|
'copyright_text' => 'Test Publisher',
|
|
]);
|
|
|
|
$verse = Label::firstOrCreate(['name' => 'Verse 1 - Fill Media Song'], ['color' => '#2196F3']);
|
|
$section = $song->sections()->create(['label_id' => $verse->id, 'order' => 0]);
|
|
$section->slides()->create(['order' => 0, 'text_content' => 'Erste Zeile']);
|
|
$section->slides()->create(['order' => 1, 'text_content' => 'Zweite Zeile']);
|
|
|
|
$arrangement = $song->arrangements()->create(['name' => 'normal', 'is_default' => true]);
|
|
$arrangement->arrangementSections()->create(['song_section_id' => $section->id, 'order' => 0]);
|
|
|
|
return $song;
|
|
}
|
|
}
|