- Move src/, tests/, bin/, generated/, proto/, composer.json, composer.lock, phpunit.xml from php/ to repo root - Move ref/ to doc/reference_samples/ for better organization - Remove vendor/ from git tracking (now properly gitignored) - Update all test file paths (dirname adjustments and ref/ -> doc/reference_samples/) - Update all documentation paths (AGENTS.md, doc/*.md) - Remove php.bak/ directory - All 252 tests pass
1157 lines
41 KiB
PHP
1157 lines
41 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace ProPresenter\Parser\Tests;
|
|
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\TestCase;
|
|
use ProPresenter\Parser\PresentationBundle;
|
|
use ProPresenter\Parser\ProBundleReader;
|
|
use ProPresenter\Parser\ProBundleWriter;
|
|
use ProPresenter\Parser\ProFileGenerator;
|
|
use ProPresenter\Parser\ProFileReader;
|
|
use ProPresenter\Parser\ProFileWriter;
|
|
|
|
class ProFileGeneratorTest extends TestCase
|
|
{
|
|
private string $tmpDir;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->tmpDir = sys_get_temp_dir() . '/propresenter-generator-test-' . uniqid();
|
|
mkdir($this->tmpDir, 0777, true);
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
if (!is_dir($this->tmpDir)) {
|
|
return;
|
|
}
|
|
|
|
foreach (scandir($this->tmpDir) ?: [] as $entry) {
|
|
if ($entry === '.' || $entry === '..') {
|
|
continue;
|
|
}
|
|
|
|
@unlink($this->tmpDir . '/' . $entry);
|
|
}
|
|
|
|
@rmdir($this->tmpDir);
|
|
}
|
|
|
|
#[Test]
|
|
public function testGenerateCreatesValidSong(): void
|
|
{
|
|
$song = ProFileGenerator::generate(
|
|
'Simple Song',
|
|
[
|
|
[
|
|
'name' => 'Verse 1',
|
|
'color' => [0.1, 0.2, 0.3, 1.0],
|
|
'slides' => [
|
|
['text' => 'Hello World'],
|
|
],
|
|
],
|
|
],
|
|
[
|
|
['name' => 'normal', 'groupNames' => ['Verse 1']],
|
|
],
|
|
);
|
|
|
|
$this->assertSame('Simple Song', $song->getName());
|
|
$this->assertCount(1, $song->getGroups());
|
|
$this->assertCount(1, $song->getSlides());
|
|
$this->assertSame('Hello World', $song->getSlides()[0]->getPlainText());
|
|
|
|
$arrangement = $song->getArrangementByName('normal');
|
|
$this->assertNotNull($arrangement);
|
|
$groups = $song->getGroupsForArrangement($arrangement);
|
|
$this->assertCount(1, $groups);
|
|
$this->assertSame('Verse 1', $groups[0]->getName());
|
|
|
|
// Verify HotKey is present on Group
|
|
$this->assertNotNull($groups[0]->getHotKey());
|
|
|
|
// Verify HotKey is present on Cue
|
|
$cues = $song->getPresentation()->getCues();
|
|
$this->assertCount(1, $cues);
|
|
$this->assertNotNull($cues[0]->getHotKey());
|
|
|
|
// Verify UUID is uppercase
|
|
$this->assertMatchesRegularExpression('/^[A-F0-9]{8}-[A-F0-9]{4}-4[A-F0-9]{3}-[89AB][A-F0-9]{3}-[A-F0-9]{12}$/', $song->getUuid());
|
|
}
|
|
|
|
#[Test]
|
|
public function testGenerateCreatesSeparatePlatformAndApplicationVersions(): void
|
|
{
|
|
$song = ProFileGenerator::generate(
|
|
'Version Test',
|
|
[
|
|
[
|
|
'name' => 'V1',
|
|
'color' => [0, 0, 0, 1],
|
|
'slides' => [
|
|
['text' => 'Test'],
|
|
],
|
|
],
|
|
],
|
|
[
|
|
['name' => 'normal', 'groupNames' => ['V1']],
|
|
],
|
|
);
|
|
|
|
$appInfo = $song->getPresentation()->getApplicationInfo();
|
|
$this->assertNotNull($appInfo);
|
|
|
|
// Verify platform version
|
|
$platformVersion = $appInfo->getPlatformVersion();
|
|
$this->assertNotNull($platformVersion);
|
|
$this->assertSame(14, $platformVersion->getMajorVersion());
|
|
$this->assertSame(8, $platformVersion->getMinorVersion());
|
|
$this->assertSame(3, $platformVersion->getPatchVersion());
|
|
$this->assertSame('', $platformVersion->getBuild());
|
|
|
|
// Verify application version
|
|
$applicationVersion = $appInfo->getApplicationVersion();
|
|
$this->assertNotNull($applicationVersion);
|
|
$this->assertSame(20, $applicationVersion->getMajorVersion());
|
|
$this->assertSame('335544354', $applicationVersion->getBuild());
|
|
|
|
// Verify they are different objects
|
|
$this->assertNotSame($platformVersion, $applicationVersion);
|
|
}
|
|
|
|
#[Test]
|
|
public function testGenerateWithMultipleGroupsAndArrangements(): void
|
|
{
|
|
$song = ProFileGenerator::generate(
|
|
'Multi Song',
|
|
[
|
|
[
|
|
'name' => 'Verse 1',
|
|
'color' => [0.1, 0.2, 0.3, 1.0],
|
|
'slides' => [
|
|
['text' => 'V1.1'],
|
|
['text' => 'V1.2'],
|
|
],
|
|
],
|
|
[
|
|
'name' => 'Chorus',
|
|
'color' => [0.4, 0.5, 0.6, 1.0],
|
|
'slides' => [
|
|
['text' => 'C1'],
|
|
],
|
|
],
|
|
],
|
|
[
|
|
['name' => 'normal', 'groupNames' => ['Verse 1', 'Chorus']],
|
|
['name' => 'short', 'groupNames' => ['Chorus']],
|
|
],
|
|
);
|
|
|
|
$this->assertSame(['Verse 1', 'Chorus'], array_map(fn ($group) => $group->getName(), $song->getGroups()));
|
|
|
|
$verse1 = $song->getGroupByName('Verse 1');
|
|
$this->assertNotNull($verse1);
|
|
$verseSlides = $song->getSlidesForGroup($verse1);
|
|
$this->assertSame(['V1.1', 'V1.2'], array_map(fn ($slide) => $slide->getPlainText(), $verseSlides));
|
|
|
|
$chorus = $song->getGroupByName('Chorus');
|
|
$this->assertNotNull($chorus);
|
|
$chorusSlides = $song->getSlidesForGroup($chorus);
|
|
$this->assertSame(['C1'], array_map(fn ($slide) => $slide->getPlainText(), $chorusSlides));
|
|
|
|
$normal = $song->getArrangementByName('normal');
|
|
$this->assertNotNull($normal);
|
|
$this->assertSame(
|
|
['Verse 1', 'Chorus'],
|
|
array_map(fn ($group) => $group->getName(), $song->getGroupsForArrangement($normal)),
|
|
);
|
|
|
|
$short = $song->getArrangementByName('short');
|
|
$this->assertNotNull($short);
|
|
$this->assertSame(
|
|
['Chorus'],
|
|
array_map(fn ($group) => $group->getName(), $song->getGroupsForArrangement($short)),
|
|
);
|
|
}
|
|
|
|
#[Test]
|
|
public function testGenerateWithTranslation(): void
|
|
{
|
|
$song = ProFileGenerator::generate(
|
|
'Translation Song',
|
|
[
|
|
[
|
|
'name' => 'Verse 1',
|
|
'color' => [0.1, 0.2, 0.3, 1.0],
|
|
'slides' => [
|
|
['text' => 'Original', 'translation' => 'Translated'],
|
|
],
|
|
],
|
|
],
|
|
[
|
|
['name' => 'normal', 'groupNames' => ['Verse 1']],
|
|
],
|
|
);
|
|
|
|
$slide = $song->getSlides()[0];
|
|
$this->assertTrue($slide->hasTranslation());
|
|
$this->assertSame('Translated', $slide->getTranslation()?->getPlainText());
|
|
}
|
|
|
|
#[Test]
|
|
public function testGenerateWithCcliMetadata(): void
|
|
{
|
|
$song = ProFileGenerator::generate(
|
|
'CCLI Song',
|
|
[
|
|
[
|
|
'name' => 'Verse 1',
|
|
'color' => [0.1, 0.2, 0.3, 1.0],
|
|
'slides' => [
|
|
['text' => 'Line'],
|
|
],
|
|
],
|
|
],
|
|
[
|
|
['name' => 'normal', 'groupNames' => ['Verse 1']],
|
|
],
|
|
[
|
|
'author' => 'Author Name',
|
|
'song_title' => 'Song Title',
|
|
'publisher' => 'Publisher Name',
|
|
'copyright_year' => 2024,
|
|
'song_number' => 12345,
|
|
'display' => true,
|
|
'artist_credits' => 'Artist Credits',
|
|
'album' => 'Album Name',
|
|
],
|
|
);
|
|
|
|
$this->assertSame('Author Name', $song->getCcliAuthor());
|
|
$this->assertSame('Song Title', $song->getCcliSongTitle());
|
|
$this->assertSame('Publisher Name', $song->getCcliPublisher());
|
|
$this->assertSame(2024, $song->getCcliCopyrightYear());
|
|
$this->assertSame(12345, $song->getCcliSongNumber());
|
|
$this->assertTrue($song->getCcliDisplay());
|
|
$this->assertSame('Artist Credits', $song->getCcliArtistCredits());
|
|
$this->assertSame('Album Name', $song->getCcliAlbum());
|
|
}
|
|
|
|
#[Test]
|
|
public function testRoundTripFromTestPro(): void
|
|
{
|
|
$original = ProFileReader::read(__DIR__ . '/../doc/reference_samples/Test.pro');
|
|
|
|
$groups = [];
|
|
foreach ($original->getGroups() as $group) {
|
|
$color = $group->getColor();
|
|
$slides = [];
|
|
foreach ($original->getSlidesForGroup($group) as $slide) {
|
|
$slides[] = [
|
|
'text' => $slide->getPlainText(),
|
|
'translation' => $slide->hasTranslation() ? $slide->getTranslation()?->getPlainText() : null,
|
|
];
|
|
}
|
|
|
|
$groups[] = [
|
|
'name' => $group->getName(),
|
|
'color' => [
|
|
$color['r'] ?? 0.0,
|
|
$color['g'] ?? 0.0,
|
|
$color['b'] ?? 0.0,
|
|
$color['a'] ?? 1.0,
|
|
],
|
|
'slides' => $slides,
|
|
];
|
|
}
|
|
|
|
$arrangements = [];
|
|
foreach ($original->getArrangements() as $arrangement) {
|
|
$arrangements[] = [
|
|
'name' => $arrangement->getName(),
|
|
'groupNames' => array_map(
|
|
fn ($group) => $group->getName(),
|
|
$original->getGroupsForArrangement($arrangement),
|
|
),
|
|
];
|
|
}
|
|
|
|
$ccli = [
|
|
'author' => $original->getCcliAuthor(),
|
|
'song_title' => $original->getCcliSongTitle(),
|
|
'publisher' => $original->getCcliPublisher(),
|
|
'copyright_year' => $original->getCcliCopyrightYear(),
|
|
'song_number' => $original->getCcliSongNumber(),
|
|
'display' => $original->getCcliDisplay(),
|
|
'artist_credits' => $original->getCcliArtistCredits(),
|
|
'album' => $original->getCcliAlbum(),
|
|
];
|
|
|
|
$generated = ProFileGenerator::generate($original->getName(), $groups, $arrangements, $ccli);
|
|
$filePath = $this->tmpDir . '/test-roundtrip.pro';
|
|
ProFileWriter::write($generated, $filePath);
|
|
$roundTrip = ProFileReader::read($filePath);
|
|
|
|
$this->assertSame($original->getName(), $roundTrip->getName());
|
|
|
|
$this->assertSame(
|
|
array_map(fn ($group) => $group->getName(), $original->getGroups()),
|
|
array_map(fn ($group) => $group->getName(), $roundTrip->getGroups()),
|
|
);
|
|
|
|
foreach ($original->getGroups() as $group) {
|
|
$actualGroup = $roundTrip->getGroupByName($group->getName());
|
|
$this->assertNotNull($actualGroup);
|
|
|
|
$expectedSlides = $original->getSlidesForGroup($group);
|
|
$actualSlides = $roundTrip->getSlidesForGroup($actualGroup);
|
|
$this->assertCount(count($expectedSlides), $actualSlides);
|
|
|
|
foreach ($expectedSlides as $index => $expectedSlide) {
|
|
$actualSlide = $actualSlides[$index];
|
|
$this->assertSame($expectedSlide->getPlainText(), $actualSlide->getPlainText());
|
|
$this->assertSame($expectedSlide->hasTranslation(), $actualSlide->hasTranslation());
|
|
if ($expectedSlide->hasTranslation()) {
|
|
$this->assertSame(
|
|
$expectedSlide->getTranslation()?->getPlainText(),
|
|
$actualSlide->getTranslation()?->getPlainText(),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->assertSame(
|
|
array_map(fn ($arrangement) => $arrangement->getName(), $original->getArrangements()),
|
|
array_map(fn ($arrangement) => $arrangement->getName(), $roundTrip->getArrangements()),
|
|
);
|
|
|
|
foreach ($original->getArrangements() as $arrangement) {
|
|
$roundTripArrangement = $roundTrip->getArrangementByName($arrangement->getName());
|
|
$this->assertNotNull($roundTripArrangement);
|
|
|
|
$expectedNames = array_map(
|
|
fn ($group) => $group->getName(),
|
|
$original->getGroupsForArrangement($arrangement),
|
|
);
|
|
$actualNames = array_map(
|
|
fn ($group) => $group->getName(),
|
|
$roundTrip->getGroupsForArrangement($roundTripArrangement),
|
|
);
|
|
|
|
$this->assertSame($expectedNames, $actualNames);
|
|
}
|
|
}
|
|
|
|
#[Test]
|
|
public function testGenerateAndWriteCreatesFile(): void
|
|
{
|
|
$filePath = $this->tmpDir . '/generated.pro';
|
|
|
|
ProFileGenerator::generateAndWrite(
|
|
$filePath,
|
|
'Write Song',
|
|
[
|
|
[
|
|
'name' => 'Verse 1',
|
|
'color' => [0.1, 0.2, 0.3, 1.0],
|
|
'slides' => [
|
|
['text' => 'Line 1'],
|
|
],
|
|
],
|
|
],
|
|
[
|
|
['name' => 'normal', 'groupNames' => ['Verse 1']],
|
|
],
|
|
);
|
|
|
|
$this->assertFileExists($filePath);
|
|
|
|
$song = ProFileReader::read($filePath);
|
|
$this->assertSame('Write Song', $song->getName());
|
|
}
|
|
|
|
#[Test]
|
|
public function testGenerateWithMacro(): void
|
|
{
|
|
$song = ProFileGenerator::generate(
|
|
'Macro Song',
|
|
[
|
|
[
|
|
'name' => 'Verse 1',
|
|
'color' => [0.1, 0.2, 0.3, 1.0],
|
|
'slides' => [
|
|
[
|
|
'text' => 'Line 1',
|
|
'macro' => [
|
|
'name' => 'Macro Name',
|
|
'uuid' => '11111111-2222-3333-4444-555555555555',
|
|
'collectionName' => '--MAIN--',
|
|
'collectionUuid' => '8D02FC57-83F8-4042-9B90-81C229728426',
|
|
],
|
|
],
|
|
],
|
|
],
|
|
],
|
|
[
|
|
['name' => 'normal', 'groupNames' => ['Verse 1']],
|
|
],
|
|
);
|
|
|
|
$slide = $song->getSlides()[0];
|
|
$this->assertTrue($slide->hasMacro());
|
|
$this->assertSame('Macro Name', $slide->getMacroName());
|
|
$this->assertSame('11111111-2222-3333-4444-555555555555', $slide->getMacroUuid());
|
|
}
|
|
|
|
#[Test]
|
|
public function testGenerateMediaSlide(): void
|
|
{
|
|
$song = ProFileGenerator::generate(
|
|
'Media Song',
|
|
[
|
|
[
|
|
'name' => 'Verse 1',
|
|
'color' => [0.1, 0.2, 0.3, 1.0],
|
|
'slides' => [
|
|
[
|
|
'media' => 'file:///tmp/test-image.jpg',
|
|
'format' => 'JPG',
|
|
],
|
|
],
|
|
],
|
|
],
|
|
[
|
|
['name' => 'normal', 'groupNames' => ['Verse 1']],
|
|
],
|
|
);
|
|
|
|
$slide = $song->getSlides()[0];
|
|
$this->assertTrue($slide->hasMedia());
|
|
$this->assertSame('file:///tmp/test-image.jpg', $slide->getMediaUrl());
|
|
$this->assertSame('JPG', $slide->getMediaFormat());
|
|
$this->assertSame('', $slide->getPlainText());
|
|
}
|
|
|
|
#[Test]
|
|
public function testGenerateMediaSlideWithLabelAndMacro(): void
|
|
{
|
|
$song = ProFileGenerator::generate(
|
|
'Media Macro Song',
|
|
[
|
|
[
|
|
'name' => 'Verse 1',
|
|
'color' => [0.1, 0.2, 0.3, 1.0],
|
|
'slides' => [
|
|
[
|
|
'media' => 'file:///tmp/test-image-2.jpg',
|
|
'format' => 'JPG',
|
|
'label' => 'Image Slide Label',
|
|
'macro' => [
|
|
'name' => 'Image Macro',
|
|
'uuid' => 'AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE',
|
|
],
|
|
],
|
|
],
|
|
],
|
|
],
|
|
[
|
|
['name' => 'normal', 'groupNames' => ['Verse 1']],
|
|
],
|
|
);
|
|
|
|
$slide = $song->getSlides()[0];
|
|
$this->assertTrue($slide->hasMedia());
|
|
$this->assertSame('Image Slide Label', $slide->getLabel());
|
|
$this->assertTrue($slide->hasMacro());
|
|
$this->assertSame('Image Macro', $slide->getMacroName());
|
|
$this->assertSame('AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE', $slide->getMacroUuid());
|
|
}
|
|
|
|
#[Test]
|
|
public function testGenerateAttributesAreDisabled(): void
|
|
{
|
|
$song = ProFileGenerator::generate(
|
|
'Attributes Test',
|
|
[
|
|
[
|
|
'name' => 'Verse 1',
|
|
'color' => [0.1, 0.2, 0.3, 1.0],
|
|
'slides' => [
|
|
['text' => 'Test Text'],
|
|
],
|
|
],
|
|
],
|
|
[
|
|
['name' => 'normal', 'groupNames' => ['Verse 1']],
|
|
],
|
|
);
|
|
|
|
$slide = $song->getSlides()[0];
|
|
$elements = $slide->getAllElements();
|
|
$this->assertCount(1, $elements);
|
|
|
|
$element = $elements[0];
|
|
$graphicsElement = $element->getGraphicsElement();
|
|
|
|
// Verify fill is present but disabled
|
|
$this->assertNotNull($graphicsElement->getFill());
|
|
$this->assertFalse($graphicsElement->getFill()->getEnable());
|
|
|
|
// Verify stroke is present but disabled
|
|
$this->assertNotNull($graphicsElement->getStroke());
|
|
$this->assertFalse($graphicsElement->getStroke()->getEnable());
|
|
|
|
// Verify shadow is present but disabled
|
|
$this->assertNotNull($graphicsElement->getShadow());
|
|
$this->assertFalse($graphicsElement->getShadow()->getEnable());
|
|
|
|
// Verify feather is present but disabled
|
|
$this->assertNotNull($graphicsElement->getFeather());
|
|
$this->assertFalse($graphicsElement->getFeather()->getEnable());
|
|
|
|
// Verify text scroller is present but disabled (should_scroll = false)
|
|
// Access the raw SlideElement protobuf to get TextScroller
|
|
$slideElements = $slide->getCue()->getActions()[0]->getSlide()->getPresentation()->getBaseSlide()->getElements();
|
|
$this->assertCount(1, $slideElements);
|
|
$textScroller = $slideElements[0]->getTextScroller();
|
|
$this->assertNotNull($textScroller);
|
|
$this->assertFalse($textScroller->getShouldScroll());
|
|
}
|
|
|
|
#[Test]
|
|
public function testGenerateSelectsNormalArrangementWhenPresent(): void
|
|
{
|
|
$song = ProFileGenerator::generate(
|
|
'SelectTest',
|
|
[
|
|
[
|
|
'name' => 'V1',
|
|
'color' => [0, 0, 0, 1],
|
|
'slides' => [
|
|
['text' => 'Hello'],
|
|
],
|
|
],
|
|
],
|
|
[
|
|
['name' => 'other', 'groupNames' => ['V1']],
|
|
['name' => 'normal', 'groupNames' => ['V1']],
|
|
],
|
|
);
|
|
|
|
$filePath = $this->tmpDir . '/select-test.pro';
|
|
ProFileWriter::write($song, $filePath);
|
|
$readSong = ProFileReader::read($filePath);
|
|
$selectedUuid = $readSong->getSelectedArrangementUuid();
|
|
|
|
// Find which arrangement has this UUID
|
|
$selectedArrangement = null;
|
|
foreach ($readSong->getArrangements() as $arrangement) {
|
|
if ($arrangement->getUuid() === $selectedUuid) {
|
|
$selectedArrangement = $arrangement;
|
|
break;
|
|
}
|
|
}
|
|
|
|
$this->assertNotNull($selectedArrangement);
|
|
$this->assertSame('normal', $selectedArrangement->getName());
|
|
}
|
|
|
|
#[Test]
|
|
public function testGenerateFallsBackToFirstArrangementWhenNoNormal(): void
|
|
{
|
|
$song = ProFileGenerator::generate(
|
|
'FallbackTest',
|
|
[
|
|
[
|
|
'name' => 'V1',
|
|
'color' => [0, 0, 0, 1],
|
|
'slides' => [
|
|
['text' => 'Hello'],
|
|
],
|
|
],
|
|
],
|
|
[
|
|
['name' => 'custom', 'groupNames' => ['V1']],
|
|
],
|
|
);
|
|
|
|
$filePath = $this->tmpDir . '/fallback-test.pro';
|
|
ProFileWriter::write($song, $filePath);
|
|
$readSong = ProFileReader::read($filePath);
|
|
$selectedUuid = $readSong->getSelectedArrangementUuid();
|
|
|
|
// Find which arrangement has this UUID
|
|
$selectedArrangement = null;
|
|
foreach ($readSong->getArrangements() as $arrangement) {
|
|
if ($arrangement->getUuid() === $selectedUuid) {
|
|
$selectedArrangement = $arrangement;
|
|
break;
|
|
}
|
|
}
|
|
|
|
$this->assertNotNull($selectedArrangement);
|
|
$this->assertSame('custom', $selectedArrangement->getName());
|
|
}
|
|
|
|
#[Test]
|
|
public function testTranslatedSlideHasCorrectDualBounds(): void
|
|
{
|
|
$song = ProFileGenerator::generate(
|
|
'TranslateTest',
|
|
[
|
|
[
|
|
'name' => 'V1',
|
|
'color' => [0, 0, 0, 1],
|
|
'slides' => [
|
|
['text' => 'Amazing Grace', 'translation' => 'Erstaunliche Gnade'],
|
|
],
|
|
],
|
|
],
|
|
[
|
|
['name' => 'normal', 'groupNames' => ['V1']],
|
|
],
|
|
);
|
|
|
|
$filePath = $this->tmpDir . '/translate-test.pro';
|
|
ProFileWriter::write($song, $filePath);
|
|
$readSong = ProFileReader::read($filePath);
|
|
$slides = $readSong->getSlides();
|
|
$elements = $slides[0]->getAllElements();
|
|
|
|
$this->assertCount(2, $elements);
|
|
$this->assertSame('Orginal', $elements[0]->getName());
|
|
$this->assertSame('Deutsch', $elements[1]->getName());
|
|
|
|
$bounds0 = $elements[0]->getGraphicsElement()->getBounds();
|
|
$bounds1 = $elements[1]->getGraphicsElement()->getBounds();
|
|
|
|
// Check heights differ and match expected values
|
|
$this->assertEqualsWithDelta(182.946, $bounds0->getSize()->getHeight(), 0.01);
|
|
$this->assertEqualsWithDelta(113.889, $bounds1->getSize()->getHeight(), 0.01);
|
|
|
|
// Check Y positions
|
|
$this->assertEqualsWithDelta(99.543, $bounds0->getOrigin()->getY(), 0.01);
|
|
$this->assertEqualsWithDelta(303.166, $bounds1->getOrigin()->getY(), 0.01);
|
|
}
|
|
|
|
#[Test]
|
|
public function testNonTranslatedSlideHasSingleFullBounds(): void
|
|
{
|
|
$song = ProFileGenerator::generate(
|
|
'NoTranslateTest',
|
|
[
|
|
[
|
|
'name' => 'V1',
|
|
'color' => [0, 0, 0, 1],
|
|
'slides' => [
|
|
['text' => 'Amazing Grace'],
|
|
],
|
|
],
|
|
],
|
|
[
|
|
['name' => 'normal', 'groupNames' => ['V1']],
|
|
],
|
|
);
|
|
|
|
$filePath = $this->tmpDir . '/no-translate-test.pro';
|
|
ProFileWriter::write($song, $filePath);
|
|
$readSong = ProFileReader::read($filePath);
|
|
$slides = $readSong->getSlides();
|
|
$elements = $slides[0]->getAllElements();
|
|
|
|
$this->assertCount(1, $elements);
|
|
$this->assertSame('Orginal', $elements[0]->getName());
|
|
|
|
$bounds = $elements[0]->getGraphicsElement()->getBounds();
|
|
|
|
// Check full-size bounds
|
|
$this->assertEqualsWithDelta(880, $bounds->getSize()->getHeight(), 0.01);
|
|
$this->assertEqualsWithDelta(1620, $bounds->getSize()->getWidth(), 0.01);
|
|
$this->assertEqualsWithDelta(100, $bounds->getOrigin()->getY(), 0.01);
|
|
$this->assertEqualsWithDelta(150, $bounds->getOrigin()->getX(), 0.01);
|
|
}
|
|
|
|
#[Test]
|
|
public function testGeneratePresentationFields(): void
|
|
{
|
|
$song = ProFileGenerator::generate(
|
|
'Fields Test',
|
|
[
|
|
[
|
|
'name' => 'V1',
|
|
'color' => [0, 0, 0, 1],
|
|
'slides' => [
|
|
['text' => 'Test'],
|
|
],
|
|
],
|
|
],
|
|
[
|
|
['name' => 'normal', 'groupNames' => ['V1']],
|
|
],
|
|
);
|
|
|
|
$p = $song->getPresentation();
|
|
$this->assertNotNull($p->getBackground());
|
|
$this->assertNotNull($p->getChordChart());
|
|
$this->assertNotNull($p->getCcli());
|
|
$this->assertNotNull($p->getTimeline());
|
|
$this->assertSame(300.0, $p->getTimeline()->getDuration());
|
|
}
|
|
|
|
#[Test]
|
|
public function testGeneratePresentationFieldsWithEmptyCcli(): void
|
|
{
|
|
$song = ProFileGenerator::generate(
|
|
'Empty CCLI Test',
|
|
[
|
|
[
|
|
'name' => 'V1',
|
|
'color' => [0, 0, 0, 1],
|
|
'slides' => [
|
|
['text' => 'Test'],
|
|
],
|
|
],
|
|
],
|
|
[
|
|
['name' => 'normal', 'groupNames' => ['V1']],
|
|
],
|
|
[],
|
|
);
|
|
|
|
$p = $song->getPresentation();
|
|
$this->assertNotNull($p->getCcli(), 'CCLI should be set even when empty array is passed');
|
|
}
|
|
|
|
#[Test]
|
|
public function testGenerateSlideSizeAndChordChart(): void
|
|
{
|
|
$song = ProFileGenerator::generate(
|
|
'Slide Size Test',
|
|
[
|
|
[
|
|
'name' => 'V1',
|
|
'color' => [0, 0, 0, 1],
|
|
'slides' => [
|
|
['text' => 'Test Slide'],
|
|
],
|
|
],
|
|
],
|
|
[
|
|
['name' => 'normal', 'groupNames' => ['V1']],
|
|
],
|
|
);
|
|
|
|
// Navigate to the first cue's slide action
|
|
$cue = $song->getPresentation()->getCues()[0];
|
|
$action = $cue->getActions()[0]; // slide action
|
|
$baseSlide = $action->getSlide()->getPresentation()->getBaseSlide();
|
|
|
|
// Verify slide size
|
|
$this->assertNotNull($baseSlide->getSize());
|
|
$this->assertSame(1920.0, $baseSlide->getSize()->getWidth());
|
|
$this->assertSame(1080.0, $baseSlide->getSize()->getHeight());
|
|
|
|
// Verify PresentationSlide has chordChart
|
|
$presentationSlide = $action->getSlide()->getPresentation();
|
|
$this->assertNotNull($presentationSlide->getChordChart());
|
|
}
|
|
|
|
#[Test]
|
|
public function testBuildLocalRelativePathMappedDirectory(): void
|
|
{
|
|
// Test: Known directory mapping (Downloads) → ROOT_USER_DOWNLOADS with correct relative path
|
|
$song = ProFileGenerator::generate(
|
|
'Mapped Path Test',
|
|
[
|
|
[
|
|
'name' => 'V1',
|
|
'color' => [0, 0, 0, 1],
|
|
'slides' => [
|
|
[
|
|
'media' => 'file:///Users/testuser/Downloads/test-image.jpg',
|
|
'format' => 'JPG',
|
|
],
|
|
],
|
|
],
|
|
],
|
|
[
|
|
['name' => 'normal', 'groupNames' => ['V1']],
|
|
],
|
|
);
|
|
|
|
// Navigate to the first slide's media action
|
|
$cue = $song->getPresentation()->getCues()[0];
|
|
$mediaAction = null;
|
|
foreach ($cue->getActions() as $action) {
|
|
if ($action->getType() === 2) { // ACTION_TYPE_MEDIA = 2
|
|
$mediaAction = $action;
|
|
break;
|
|
}
|
|
}
|
|
$this->assertNotNull($mediaAction);
|
|
|
|
$url = $mediaAction->getMedia()->getElement()->getUrl();
|
|
|
|
$this->assertNotNull($url);
|
|
$this->assertNotNull($url->getLocal());
|
|
$this->assertSame(4, $url->getLocal()->getRoot()); // ROOT_USER_DOWNLOADS = 4
|
|
$this->assertSame('test-image.jpg', $url->getLocal()->getPath());
|
|
}
|
|
|
|
#[Test]
|
|
public function testBuildLocalRelativePathUnmappedUserDirectory(): void
|
|
{
|
|
// Test: Unmapped user directory (AI) → ROOT_USER_HOME with full relative path
|
|
$song = ProFileGenerator::generate(
|
|
'Unmapped Path Test',
|
|
[
|
|
[
|
|
'name' => 'V1',
|
|
'color' => [0, 0, 0, 1],
|
|
'slides' => [
|
|
[
|
|
'media' => 'file:///Users/thorsten/AI/propresenter/doc/reference_samples/Media/test.png',
|
|
'format' => 'PNG',
|
|
],
|
|
],
|
|
],
|
|
],
|
|
[
|
|
['name' => 'normal', 'groupNames' => ['V1']],
|
|
],
|
|
);
|
|
|
|
// Navigate to the first slide's media action
|
|
$cue = $song->getPresentation()->getCues()[0];
|
|
$mediaAction = null;
|
|
foreach ($cue->getActions() as $action) {
|
|
if ($action->getType() === 2) { // ACTION_TYPE_MEDIA = 2
|
|
$mediaAction = $action;
|
|
break;
|
|
}
|
|
}
|
|
$this->assertNotNull($mediaAction);
|
|
|
|
$url = $mediaAction->getMedia()->getElement()->getUrl();
|
|
|
|
$this->assertNotNull($url);
|
|
$this->assertNotNull($url->getLocal());
|
|
$this->assertSame(2, $url->getLocal()->getRoot()); // ROOT_USER_HOME = 2
|
|
$this->assertSame('AI/propresenter/doc/reference_samples/Media/test.png', $url->getLocal()->getPath());
|
|
}
|
|
|
|
#[Test]
|
|
public function testBuildLocalRelativePathNonUserPath(): void
|
|
{
|
|
// Test: Non-user path (tmp) → ROOT_BOOT_VOLUME with full path
|
|
$song = ProFileGenerator::generate(
|
|
'Non-User Path Test',
|
|
[
|
|
[
|
|
'name' => 'V1',
|
|
'color' => [0, 0, 0, 1],
|
|
'slides' => [
|
|
[
|
|
'media' => 'file:///tmp/test-image.jpg',
|
|
'format' => 'JPG',
|
|
],
|
|
],
|
|
],
|
|
],
|
|
[
|
|
['name' => 'normal', 'groupNames' => ['V1']],
|
|
],
|
|
);
|
|
|
|
// Navigate to the first slide's media action
|
|
$cue = $song->getPresentation()->getCues()[0];
|
|
$mediaAction = null;
|
|
foreach ($cue->getActions() as $action) {
|
|
if ($action->getType() === 2) { // ACTION_TYPE_MEDIA = 2
|
|
$mediaAction = $action;
|
|
break;
|
|
}
|
|
}
|
|
$this->assertNotNull($mediaAction);
|
|
|
|
$url = $mediaAction->getMedia()->getElement()->getUrl();
|
|
|
|
$this->assertNotNull($url);
|
|
$this->assertNotNull($url->getLocal());
|
|
$this->assertSame(1, $url->getLocal()->getRoot()); // ROOT_BOOT_VOLUME = 1
|
|
$this->assertSame('tmp/test-image.jpg', $url->getLocal()->getPath());
|
|
}
|
|
|
|
#[Test]
|
|
public function testMediaActionHasNameFromFilename(): void
|
|
{
|
|
$song = ProFileGenerator::generate(
|
|
'Media Name Test',
|
|
[
|
|
[
|
|
'name' => 'V1',
|
|
'color' => [0, 0, 0, 1],
|
|
'slides' => [
|
|
[
|
|
'text' => 'x',
|
|
'media' => 'file:///Users/test/AI/Media/test-image.png',
|
|
'format' => 'png',
|
|
'mediaWidth' => 200,
|
|
'mediaHeight' => 150,
|
|
],
|
|
],
|
|
],
|
|
],
|
|
[
|
|
['name' => 'n', 'groupNames' => ['V1']],
|
|
],
|
|
);
|
|
|
|
$mediaAction = $song->getPresentation()->getCues()[0]->getActions()[1];
|
|
$this->assertSame('test-image', $mediaAction->getName());
|
|
}
|
|
|
|
#[Test]
|
|
public function testMediaActionHasNameFromLabel(): void
|
|
{
|
|
$song = ProFileGenerator::generate(
|
|
'Media Label Name Test',
|
|
[
|
|
[
|
|
'name' => 'V1',
|
|
'color' => [0, 0, 0, 1],
|
|
'slides' => [
|
|
[
|
|
'text' => 'x',
|
|
'media' => 'file:///Users/test/AI/Media/test-image.png',
|
|
'format' => 'png',
|
|
'label' => 'My Custom Label',
|
|
],
|
|
],
|
|
],
|
|
],
|
|
[
|
|
['name' => 'n', 'groupNames' => ['V1']],
|
|
],
|
|
);
|
|
|
|
$mediaAction = $song->getPresentation()->getCues()[0]->getActions()[1];
|
|
$this->assertSame('My Custom Label', $mediaAction->getName());
|
|
}
|
|
|
|
#[Test]
|
|
public function testMediaActionHasAudioOnMediaType(): void
|
|
{
|
|
$song = ProFileGenerator::generate(
|
|
'Media Audio Test',
|
|
[
|
|
[
|
|
'name' => 'V1',
|
|
'color' => [0, 0, 0, 1],
|
|
'slides' => [
|
|
[
|
|
'text' => 'x',
|
|
'media' => 'file:///Users/test/AI/Media/test-image.png',
|
|
'format' => 'png',
|
|
],
|
|
],
|
|
],
|
|
],
|
|
[
|
|
['name' => 'n', 'groupNames' => ['V1']],
|
|
],
|
|
);
|
|
|
|
$mediaAction = $song->getPresentation()->getCues()[0]->getActions()[1];
|
|
$media = $mediaAction->getMedia();
|
|
$this->assertNotNull($media->getAudio());
|
|
|
|
// Element should still be set (audio oneof doesn't clear element)
|
|
$this->assertNotNull($media->getElement());
|
|
}
|
|
|
|
#[Test]
|
|
public function testMediaActionHasImageDrawingAndFileProperties(): void
|
|
{
|
|
$song = ProFileGenerator::generate(
|
|
'Media Image Props Test',
|
|
[
|
|
[
|
|
'name' => 'V1',
|
|
'color' => [0, 0, 0, 1],
|
|
'slides' => [
|
|
[
|
|
'text' => 'x',
|
|
'media' => 'file:///Users/test/AI/Media/test-image.png',
|
|
'format' => 'png',
|
|
'mediaWidth' => 200,
|
|
'mediaHeight' => 150,
|
|
],
|
|
],
|
|
],
|
|
],
|
|
[
|
|
['name' => 'n', 'groupNames' => ['V1']],
|
|
],
|
|
);
|
|
|
|
$mediaAction = $song->getPresentation()->getCues()[0]->getActions()[1];
|
|
$image = $mediaAction->getMedia()->getElement()->getImage();
|
|
$this->assertNotNull($image);
|
|
|
|
// Verify drawing properties
|
|
$this->assertNotNull($image->getDrawing());
|
|
$this->assertSame(200.0, $image->getDrawing()->getNaturalSize()->getWidth());
|
|
$this->assertSame(150.0, $image->getDrawing()->getNaturalSize()->getHeight());
|
|
$this->assertNotNull($image->getDrawing()->getCustomImageBounds());
|
|
$this->assertNotNull($image->getDrawing()->getCustomImageBounds()->getOrigin());
|
|
$this->assertNotNull($image->getDrawing()->getCustomImageBounds()->getSize());
|
|
$this->assertNotNull($image->getDrawing()->getCropInsets());
|
|
$this->assertSame(1, $image->getDrawing()->getAlphaType()); // ALPHA_TYPE_STRAIGHT = 1
|
|
|
|
// Verify file properties
|
|
$this->assertNotNull($image->getFile());
|
|
$this->assertNotNull($image->getFile()->getLocalUrl());
|
|
$this->assertSame('file:///Users/test/AI/Media/test-image.png', $image->getFile()->getLocalUrl()->getAbsoluteString());
|
|
}
|
|
|
|
#[Test]
|
|
public function testGeneratedProFileMatchesProPresenterNativeStructure(): void
|
|
{
|
|
$song = ProFileGenerator::generate(
|
|
'TestBild',
|
|
[[
|
|
'name' => 'Verse 1',
|
|
'color' => [0.0, 0.0, 0.0, 1.0],
|
|
'slides' => [[
|
|
'label' => 'test-background',
|
|
'media' => 'file:///Users/thorsten/AI/propresenter/doc/reference_samples/Media/test-background.png',
|
|
'format' => 'png',
|
|
'mediaWidth' => 200,
|
|
'mediaHeight' => 150,
|
|
]],
|
|
]],
|
|
[['name' => 'normal', 'groupNames' => ['Verse 1']]],
|
|
);
|
|
|
|
$p = $song->getPresentation();
|
|
|
|
// Fix 1: UUIDs are uppercase
|
|
$this->assertMatchesRegularExpression(
|
|
'/^[A-F0-9]{8}-[A-F0-9]{4}-4[A-F0-9]{3}-[89AB][A-F0-9]{3}-[A-F0-9]{12}$/',
|
|
$p->getUuid()->getString(),
|
|
'Presentation UUID must be uppercase',
|
|
);
|
|
|
|
// Fix 2: Separate platform and application versions
|
|
$appInfo = $p->getApplicationInfo();
|
|
$this->assertSame(14, $appInfo->getPlatformVersion()->getMajorVersion());
|
|
$this->assertSame(8, $appInfo->getPlatformVersion()->getMinorVersion());
|
|
$this->assertSame(3, $appInfo->getPlatformVersion()->getPatchVersion());
|
|
$this->assertSame(20, $appInfo->getApplicationVersion()->getMajorVersion());
|
|
$this->assertSame('335544354', $appInfo->getApplicationVersion()->getBuild());
|
|
|
|
// Fix 3a: Background present
|
|
$this->assertNotNull($p->getBackground(), 'Background must be set');
|
|
$this->assertSame(1.0, $p->getBackground()->getColor()->getAlpha());
|
|
|
|
// Fix 3b: ChordChart on Presentation
|
|
$this->assertNotNull($p->getChordChart(), 'Presentation chordChart must be set');
|
|
|
|
// Fix 3c: CCLI always present (even with no data)
|
|
$this->assertNotNull($p->getCcli(), 'CCLI must always be set');
|
|
|
|
// Fix 3d: Timeline
|
|
$this->assertNotNull($p->getTimeline(), 'Timeline must be set');
|
|
$this->assertSame(300.0, $p->getTimeline()->getDuration());
|
|
|
|
// Fix 4: HotKey on Group and Cue
|
|
$this->assertNotNull($p->getCueGroups()[0]->getGroup()->getHotKey(), 'Group must have hotKey');
|
|
$this->assertNotNull($p->getCues()[0]->getHotKey(), 'Cue must have hotKey');
|
|
|
|
// Fix 5a: Slide size 1920x1080
|
|
$slidePresentation = $p->getCues()[0]->getActions()[0]->getSlide()->getPresentation();
|
|
$size = $slidePresentation->getBaseSlide()->getSize();
|
|
$this->assertNotNull($size, 'Slide must have size');
|
|
$this->assertSame(1920.0, $size->getWidth());
|
|
$this->assertSame(1080.0, $size->getHeight());
|
|
|
|
// Fix 5b: PresentationSlide chordChart
|
|
$this->assertNotNull($slidePresentation->getChordChart(), 'PresentationSlide must have chordChart');
|
|
|
|
// Fix 6: LOCAL PATH ROOT_USER_HOME for /Users/ paths
|
|
$mediaUrl = $p->getCues()[0]->getActions()[1]->getMedia()->getElement()->getUrl();
|
|
$this->assertSame(2, $mediaUrl->getLocal()->getRoot(), 'Must use ROOT_USER_HOME (2) for /Users/ paths');
|
|
$this->assertSame('AI/propresenter/doc/reference_samples/Media/test-background.png', $mediaUrl->getLocal()->getPath());
|
|
|
|
// Fix 7a: Media action name
|
|
$mediaAction = $p->getCues()[0]->getActions()[1];
|
|
$this->assertSame('test-background', $mediaAction->getName());
|
|
|
|
// Fix 7b: MediaType.audio set (oneof discriminator)
|
|
$this->assertNotNull($mediaAction->getMedia()->getAudio(), 'MediaType must have audio set');
|
|
|
|
// Fix 7c: ImageTypeProperties with drawing and file
|
|
$img = $mediaAction->getMedia()->getElement()->getImage();
|
|
$this->assertNotNull($img->getDrawing(), 'ImageTypeProperties must have drawing');
|
|
$this->assertSame(200.0, $img->getDrawing()->getNaturalSize()->getWidth());
|
|
$this->assertSame(150.0, $img->getDrawing()->getNaturalSize()->getHeight());
|
|
$this->assertNotNull($img->getDrawing()->getCustomImageBounds());
|
|
$this->assertNotNull($img->getDrawing()->getCropInsets());
|
|
$this->assertNotNull($img->getFile(), 'ImageTypeProperties must have file');
|
|
$this->assertNotNull($img->getFile()->getLocalUrl());
|
|
$this->assertSame(
|
|
'file:///Users/thorsten/AI/propresenter/doc/reference_samples/Media/test-background.png',
|
|
$img->getFile()->getLocalUrl()->getAbsoluteString(),
|
|
);
|
|
}
|
|
|
|
#[Test]
|
|
public function testBundleRoundTripWithGeneratedSong(): void
|
|
{
|
|
$song = ProFileGenerator::generate(
|
|
'TestBild',
|
|
[[
|
|
'name' => 'Verse 1',
|
|
'color' => [0.0, 0.0, 0.0, 1.0],
|
|
'slides' => [[
|
|
'label' => 'test-background',
|
|
'media' => 'file:///Users/thorsten/AI/propresenter/doc/reference_samples/Media/test-background.png',
|
|
'format' => 'png',
|
|
'mediaWidth' => 200,
|
|
'mediaHeight' => 150,
|
|
]],
|
|
]],
|
|
[['name' => 'normal', 'groupNames' => ['Verse 1']]],
|
|
);
|
|
|
|
$fakeImageBytes = "\x89PNG\r\n\x1a\n" . str_repeat("\x00", 100);
|
|
|
|
$bundle = new PresentationBundle(
|
|
$song,
|
|
'TestBild.pro',
|
|
['test-background.png' => $fakeImageBytes],
|
|
);
|
|
|
|
$path = $this->tmpDir . '/TestBild.probundle';
|
|
ProBundleWriter::write($bundle, $path);
|
|
|
|
$this->assertFileExists($path);
|
|
|
|
$read = ProBundleReader::read($path);
|
|
$this->assertSame('TestBild', $read->getName());
|
|
$this->assertSame('TestBild.pro', $read->getProFilename());
|
|
$this->assertSame(1, $read->getMediaFileCount());
|
|
$this->assertTrue($read->hasMediaFile('test-background.png'));
|
|
|
|
// UUID preserved through serialization
|
|
$this->assertSame(
|
|
$song->getPresentation()->getUuid()->getString(),
|
|
$read->getSong()->getPresentation()->getUuid()->getString(),
|
|
);
|
|
}
|
|
}
|