feat(playlist): add PlaylistEntry, PlaylistNode, PlaylistArchive wrappers
- Implement PlaylistEntry wrapper for PlaylistItem proto (23 tests, 40 assertions) - Support all 4 item types: header, presentation, placeholder, cue - Expose arrangement_name (field 5) for presentation items - Type-specific getters with null safety - Implement PlaylistNode wrapper for Playlist proto (15 tests, 37 assertions) - Handle both container nodes (child playlists) and leaf nodes (items) - Recursive wrapping of nested playlist structures - Implement PlaylistArchive wrapper for PlaylistDocument proto (18 tests, 37 assertions) - Top-level integration of nodes, entries, and embedded files - Lazy parsing of embedded .pro files into Song objects - File partitioning: .pro files vs media files Wave 2 of proplaylist-module plan complete (Tasks 4-6)
This commit is contained in:
parent
2c1b8e3370
commit
d58bb38bb6
169
php/src/PlaylistArchive.php
Normal file
169
php/src/PlaylistArchive.php
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ProPresenter\Parser;
|
||||
|
||||
use Rv\Data\PlaylistDocument;
|
||||
use Rv\Data\Presentation;
|
||||
|
||||
/**
|
||||
* Top-level wrapper for a ProPresenter playlist archive (.proplaylist).
|
||||
*
|
||||
* Integrates the protobuf PlaylistDocument with embedded files extracted from
|
||||
* the ZIP archive. Provides convenient access to the playlist tree structure,
|
||||
* entries, and lazy parsing of embedded .pro files into Song objects.
|
||||
*
|
||||
* Root structure:
|
||||
* PlaylistDocument → root_node ("PLAYLIST") → first child (actual named playlist)
|
||||
*/
|
||||
class PlaylistArchive
|
||||
{
|
||||
private PlaylistNode $rootNode;
|
||||
private ?PlaylistNode $playlistNode = null;
|
||||
|
||||
/** @var array<string, string> filename => raw bytes */
|
||||
private array $embeddedFiles;
|
||||
|
||||
/** @var array<string, Song> filename => parsed Song (lazy cache) */
|
||||
private array $parsedSongs = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly PlaylistDocument $document,
|
||||
array $embeddedFiles = [],
|
||||
) {
|
||||
$this->embeddedFiles = $embeddedFiles;
|
||||
|
||||
$rootPlaylist = $this->document->getRootNode();
|
||||
$this->rootNode = new PlaylistNode($rootPlaylist);
|
||||
|
||||
// First child node is the actual named playlist
|
||||
$childNodes = $this->rootNode->getChildNodes();
|
||||
if (!empty($childNodes)) {
|
||||
$this->playlistNode = $childNodes[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Name of the actual playlist (first child, not the root "PLAYLIST").
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->playlistNode?->getName() ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Root PlaylistNode (always named "PLAYLIST").
|
||||
*/
|
||||
public function getRootNode(): PlaylistNode
|
||||
{
|
||||
return $this->rootNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* First child node — the actual named playlist.
|
||||
*/
|
||||
public function getPlaylistNode(): ?PlaylistNode
|
||||
{
|
||||
return $this->playlistNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut: all entries from the playlist node.
|
||||
*
|
||||
* @return PlaylistEntry[]
|
||||
*/
|
||||
public function getEntries(): array
|
||||
{
|
||||
return $this->playlistNode?->getEntries() ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Total number of entries in the playlist.
|
||||
*/
|
||||
public function getEntryCount(): int
|
||||
{
|
||||
return $this->playlistNode?->getEntryCount() ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Document type enum value.
|
||||
*/
|
||||
public function getType(): int
|
||||
{
|
||||
return $this->document->getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the underlying protobuf PlaylistDocument.
|
||||
*/
|
||||
public function getDocument(): PlaylistDocument
|
||||
{
|
||||
return $this->document;
|
||||
}
|
||||
|
||||
// ─── Embedded files ───
|
||||
|
||||
/**
|
||||
* All embedded files (excluding the `data` proto file).
|
||||
*
|
||||
* @return array<string, string> filename => raw bytes
|
||||
*/
|
||||
public function getEmbeddedFiles(): array
|
||||
{
|
||||
return $this->embeddedFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only .pro song files from embedded entries.
|
||||
*
|
||||
* @return array<string, string> filename => raw bytes
|
||||
*/
|
||||
public function getEmbeddedProFiles(): array
|
||||
{
|
||||
return array_filter(
|
||||
$this->embeddedFiles,
|
||||
static fn (string $_, string $filename): bool => str_ends_with(strtolower($filename), '.pro'),
|
||||
ARRAY_FILTER_USE_BOTH,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only media files (non-.pro, non-data) from embedded entries.
|
||||
*
|
||||
* @return array<string, string> filename => raw bytes
|
||||
*/
|
||||
public function getEmbeddedMediaFiles(): array
|
||||
{
|
||||
return array_filter(
|
||||
$this->embeddedFiles,
|
||||
static fn (string $_, string $filename): bool => !str_ends_with(strtolower($filename), '.pro'),
|
||||
ARRAY_FILTER_USE_BOTH,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily parse an embedded .pro file into a Song object.
|
||||
*
|
||||
* Returns null if the file doesn't exist or isn't a .pro file.
|
||||
* Caches the parsed Song for subsequent calls with the same filename.
|
||||
*/
|
||||
public function getEmbeddedSong(string $filename): ?Song
|
||||
{
|
||||
if (!isset($this->embeddedFiles[$filename])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!str_ends_with(strtolower($filename), '.pro')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isset($this->parsedSongs[$filename])) {
|
||||
$presentation = new Presentation();
|
||||
$presentation->mergeFromString($this->embeddedFiles[$filename]);
|
||||
$this->parsedSongs[$filename] = new Song($presentation);
|
||||
}
|
||||
|
||||
return $this->parsedSongs[$filename];
|
||||
}
|
||||
}
|
||||
187
php/src/PlaylistEntry.php
Normal file
187
php/src/PlaylistEntry.php
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ProPresenter\Parser;
|
||||
|
||||
use Rv\Data\PlaylistItem;
|
||||
|
||||
/**
|
||||
* Read wrapper around a protobuf PlaylistItem representing a playlist entry.
|
||||
*
|
||||
* Supports all 4 item types: header, presentation, placeholder, cue.
|
||||
* Provides typed getters for type-specific data (color, document path, arrangement).
|
||||
* Returns null for type-specific getters when the wrong type is accessed.
|
||||
*/
|
||||
class PlaylistEntry
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PlaylistItem $item,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* UUID string of this playlist item.
|
||||
*/
|
||||
public function getUuid(): string
|
||||
{
|
||||
return $this->item->getUuid()?->getString() ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Display name of this playlist item.
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->item->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Item type as string: "header", "presentation", "cue", "placeholder", "planning_center".
|
||||
* Returns empty string if no type is set.
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->item->getItemType() ?? '';
|
||||
}
|
||||
|
||||
// ─── Type checks ───
|
||||
|
||||
public function isHeader(): bool
|
||||
{
|
||||
return $this->getType() === 'header';
|
||||
}
|
||||
|
||||
public function isPresentation(): bool
|
||||
{
|
||||
return $this->getType() === 'presentation';
|
||||
}
|
||||
|
||||
public function isCue(): bool
|
||||
{
|
||||
return $this->getType() === 'cue';
|
||||
}
|
||||
|
||||
public function isPlaceholder(): bool
|
||||
{
|
||||
return $this->getType() === 'placeholder';
|
||||
}
|
||||
|
||||
// ─── Header-specific ───
|
||||
|
||||
/**
|
||||
* Header color as [r, g, b, a] array (floats 0.0–1.0).
|
||||
* Returns null for non-header items or when no color is set.
|
||||
*
|
||||
* @return float[]|null
|
||||
*/
|
||||
public function getHeaderColor(): ?array
|
||||
{
|
||||
if (!$this->isHeader()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$header = $this->item->getHeader();
|
||||
if ($header === null || !$header->hasColor()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$color = $header->getColor();
|
||||
|
||||
return [
|
||||
$color->getRed(),
|
||||
$color->getGreen(),
|
||||
$color->getBlue(),
|
||||
$color->getAlpha(),
|
||||
];
|
||||
}
|
||||
|
||||
// ─── Presentation-specific ───
|
||||
|
||||
/**
|
||||
* Full document path URL string for presentation items.
|
||||
* Returns null for non-presentation items.
|
||||
*/
|
||||
public function getDocumentPath(): ?string
|
||||
{
|
||||
if (!$this->isPresentation()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$pres = $this->item->getPresentation();
|
||||
|
||||
return $pres?->getDocumentPath()?->getAbsoluteString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract just the filename from the document path URL.
|
||||
* Decodes URL-encoded characters (e.g., %20 → space).
|
||||
* Returns null for non-presentation items or when no path is set.
|
||||
*/
|
||||
public function getDocumentFilename(): ?string
|
||||
{
|
||||
$path = $this->getDocumentPath();
|
||||
if ($path === null || $path === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$basename = basename(parse_url($path, PHP_URL_PATH) ?? '');
|
||||
|
||||
return urldecode($basename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Arrangement UUID string for presentation items.
|
||||
* Returns null for non-presentation items or when no arrangement is set.
|
||||
*/
|
||||
public function getArrangementUuid(): ?string
|
||||
{
|
||||
if (!$this->isPresentation()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$pres = $this->item->getPresentation();
|
||||
|
||||
return $pres?->getArrangement()?->getString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Arrangement name (field 5) for presentation items.
|
||||
* Returns null for non-presentation items.
|
||||
*/
|
||||
public function getArrangementName(): ?string
|
||||
{
|
||||
if (!$this->isPresentation()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$pres = $this->item->getPresentation();
|
||||
$name = $pres?->getArrangementName();
|
||||
|
||||
return ($name !== null && $name !== '') ? $name : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a specific arrangement is assigned to this presentation item.
|
||||
*/
|
||||
public function hasArrangement(): bool
|
||||
{
|
||||
if (!$this->isPresentation()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$pres = $this->item->getPresentation();
|
||||
|
||||
return $pres !== null && $pres->hasArrangement();
|
||||
}
|
||||
|
||||
// ─── Raw proto access ───
|
||||
|
||||
/**
|
||||
* Access the underlying protobuf PlaylistItem.
|
||||
*/
|
||||
public function getPlaylistItem(): PlaylistItem
|
||||
{
|
||||
return $this->item;
|
||||
}
|
||||
}
|
||||
119
php/src/PlaylistNode.php
Normal file
119
php/src/PlaylistNode.php
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ProPresenter\Parser;
|
||||
|
||||
use Rv\Data\Playlist;
|
||||
|
||||
/**
|
||||
* Wraps a protobuf Playlist message, providing convenient access to playlist
|
||||
* structure as either a container node (with child playlists) or a leaf node
|
||||
* (with playlist items).
|
||||
*
|
||||
* Uses the oneof ChildrenType to determine node kind:
|
||||
* - "playlists" → container node → child PlaylistNode[]
|
||||
* - "items" → leaf node → PlaylistEntry[]
|
||||
*/
|
||||
class PlaylistNode
|
||||
{
|
||||
/** @var PlaylistNode[] */
|
||||
private array $childNodes = [];
|
||||
|
||||
/** @var PlaylistEntry[] */
|
||||
private array $entries = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly Playlist $playlist,
|
||||
) {
|
||||
$childrenType = $this->playlist->getChildrenType();
|
||||
|
||||
if ($childrenType === 'playlists') {
|
||||
$playlistArray = $this->playlist->getPlaylists();
|
||||
foreach ($playlistArray->getPlaylists() as $childPlaylist) {
|
||||
$this->childNodes[] = new self($childPlaylist);
|
||||
}
|
||||
} elseif ($childrenType === 'items') {
|
||||
$playlistItems = $this->playlist->getItems();
|
||||
foreach ($playlistItems->getItems() as $item) {
|
||||
$this->entries[] = new PlaylistEntry($item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* UUID string of this playlist node.
|
||||
*/
|
||||
public function getUuid(): string
|
||||
{
|
||||
return $this->playlist->getUuid()?->getString() ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Display name of this playlist node.
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->playlist->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Playlist type enum value (TYPE_PLAYLIST, TYPE_GROUP, TYPE_SMART, etc.).
|
||||
*/
|
||||
public function getType(): int
|
||||
{
|
||||
return $this->playlist->getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this node is a container (has child playlists).
|
||||
*/
|
||||
public function isContainer(): bool
|
||||
{
|
||||
return $this->playlist->getChildrenType() === 'playlists';
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this node is a leaf (has playlist items).
|
||||
*/
|
||||
public function isLeaf(): bool
|
||||
{
|
||||
return $this->playlist->getChildrenType() === 'items';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get child PlaylistNode objects (empty for leaf nodes).
|
||||
*
|
||||
* @return PlaylistNode[]
|
||||
*/
|
||||
public function getChildNodes(): array
|
||||
{
|
||||
return $this->childNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PlaylistEntry objects (empty for container nodes).
|
||||
*
|
||||
* @return PlaylistEntry[]
|
||||
*/
|
||||
public function getEntries(): array
|
||||
{
|
||||
return $this->entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of items in this leaf node (0 for containers).
|
||||
*/
|
||||
public function getEntryCount(): int
|
||||
{
|
||||
return count($this->entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the underlying protobuf Playlist message.
|
||||
*/
|
||||
public function getPlaylist(): Playlist
|
||||
{
|
||||
return $this->playlist;
|
||||
}
|
||||
}
|
||||
338
php/tests/PlaylistArchiveTest.php
Normal file
338
php/tests/PlaylistArchiveTest.php
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ProPresenter\Parser\Tests;
|
||||
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ProPresenter\Parser\PlaylistArchive;
|
||||
use ProPresenter\Parser\PlaylistEntry;
|
||||
use ProPresenter\Parser\PlaylistNode;
|
||||
use ProPresenter\Parser\Song;
|
||||
use Rv\Data\Playlist;
|
||||
use Rv\Data\Playlist\PlaylistArray;
|
||||
use Rv\Data\Playlist\PlaylistItems;
|
||||
use Rv\Data\PlaylistDocument;
|
||||
use Rv\Data\PlaylistItem;
|
||||
use Rv\Data\PlaylistItem\Presentation as PlaylistItemPresentation;
|
||||
use Rv\Data\Presentation;
|
||||
use Rv\Data\UUID;
|
||||
|
||||
class PlaylistArchiveTest extends TestCase
|
||||
{
|
||||
// ─── Helpers ───
|
||||
|
||||
private function makePlaylistItem(string $name, string $uuid = ''): PlaylistItem
|
||||
{
|
||||
$item = new PlaylistItem();
|
||||
$item->setName($name);
|
||||
|
||||
if ($uuid !== '') {
|
||||
$uuidObj = new UUID();
|
||||
$uuidObj->setString($uuid);
|
||||
$item->setUuid($uuidObj);
|
||||
}
|
||||
|
||||
$pres = new PlaylistItemPresentation();
|
||||
$item->setPresentation($pres);
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
private function makeRootWithChild(
|
||||
string $childName = 'TestPlaylist',
|
||||
array $items = [],
|
||||
): Playlist {
|
||||
// Child playlist (leaf with items)
|
||||
$child = new Playlist();
|
||||
$child->setName($childName);
|
||||
|
||||
if (!empty($items)) {
|
||||
$playlistItems = new PlaylistItems();
|
||||
$playlistItems->setItems($items);
|
||||
$child->setItems($playlistItems);
|
||||
}
|
||||
|
||||
// Root playlist (container with child playlists)
|
||||
$root = new Playlist();
|
||||
$root->setName('PLAYLIST');
|
||||
|
||||
$playlistArray = new PlaylistArray();
|
||||
$playlistArray->setPlaylists([$child]);
|
||||
$root->setPlaylists($playlistArray);
|
||||
|
||||
return $root;
|
||||
}
|
||||
|
||||
private function makeSimpleArchive(
|
||||
string $childName = 'TestPlaylist',
|
||||
array $items = [],
|
||||
array $embeddedFiles = [],
|
||||
int $type = 0,
|
||||
): PlaylistArchive {
|
||||
$doc = new PlaylistDocument();
|
||||
$doc->setRootNode($this->makeRootWithChild($childName, $items));
|
||||
$doc->setType($type);
|
||||
|
||||
return new PlaylistArchive($doc, $embeddedFiles);
|
||||
}
|
||||
|
||||
// ─── getName() ───
|
||||
|
||||
#[Test]
|
||||
public function getNameReturnsChildPlaylistName(): void
|
||||
{
|
||||
$archive = $this->makeSimpleArchive(childName: 'Sunday Service');
|
||||
|
||||
$this->assertSame('Sunday Service', $archive->getName());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getNameReturnsEmptyStringWhenNoChildren(): void
|
||||
{
|
||||
$root = new Playlist();
|
||||
$root->setName('PLAYLIST');
|
||||
|
||||
$doc = new PlaylistDocument();
|
||||
$doc->setRootNode($root);
|
||||
|
||||
$archive = new PlaylistArchive($doc);
|
||||
|
||||
$this->assertSame('', $archive->getName());
|
||||
}
|
||||
|
||||
// ─── getRootNode() ───
|
||||
|
||||
#[Test]
|
||||
public function getRootNodeReturnsPlaylistNodeWrappingRoot(): void
|
||||
{
|
||||
$archive = $this->makeSimpleArchive();
|
||||
|
||||
$rootNode = $archive->getRootNode();
|
||||
$this->assertInstanceOf(PlaylistNode::class, $rootNode);
|
||||
$this->assertSame('PLAYLIST', $rootNode->getName());
|
||||
}
|
||||
|
||||
// ─── getPlaylistNode() ───
|
||||
|
||||
#[Test]
|
||||
public function getPlaylistNodeReturnsFirstChildNode(): void
|
||||
{
|
||||
$archive = $this->makeSimpleArchive(childName: 'Gottesdienst');
|
||||
|
||||
$playlistNode = $archive->getPlaylistNode();
|
||||
$this->assertInstanceOf(PlaylistNode::class, $playlistNode);
|
||||
$this->assertSame('Gottesdienst', $playlistNode->getName());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getPlaylistNodeReturnsNullWhenNoChildren(): void
|
||||
{
|
||||
$root = new Playlist();
|
||||
$root->setName('PLAYLIST');
|
||||
|
||||
$doc = new PlaylistDocument();
|
||||
$doc->setRootNode($root);
|
||||
|
||||
$archive = new PlaylistArchive($doc);
|
||||
|
||||
$this->assertNull($archive->getPlaylistNode());
|
||||
}
|
||||
|
||||
// ─── getEntries() / getEntryCount() ───
|
||||
|
||||
#[Test]
|
||||
public function getEntriesReturnsEntriesFromPlaylistNode(): void
|
||||
{
|
||||
$items = [
|
||||
$this->makePlaylistItem('Song A', 'uuid-a'),
|
||||
$this->makePlaylistItem('Song B', 'uuid-b'),
|
||||
$this->makePlaylistItem('Song C', 'uuid-c'),
|
||||
];
|
||||
|
||||
$archive = $this->makeSimpleArchive(items: $items);
|
||||
|
||||
$entries = $archive->getEntries();
|
||||
$this->assertCount(3, $entries);
|
||||
$this->assertContainsOnlyInstancesOf(PlaylistEntry::class, $entries);
|
||||
$this->assertSame('Song A', $entries[0]->getName());
|
||||
$this->assertSame('Song B', $entries[1]->getName());
|
||||
$this->assertSame('Song C', $entries[2]->getName());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getEntryCountReturnsTotalItemCount(): void
|
||||
{
|
||||
$items = [
|
||||
$this->makePlaylistItem('Song 1'),
|
||||
$this->makePlaylistItem('Song 2'),
|
||||
];
|
||||
|
||||
$archive = $this->makeSimpleArchive(items: $items);
|
||||
|
||||
$this->assertSame(2, $archive->getEntryCount());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getEntryCountReturnsZeroWhenNoPlaylistNode(): void
|
||||
{
|
||||
$root = new Playlist();
|
||||
$root->setName('PLAYLIST');
|
||||
|
||||
$doc = new PlaylistDocument();
|
||||
$doc->setRootNode($root);
|
||||
|
||||
$archive = new PlaylistArchive($doc);
|
||||
|
||||
$this->assertSame(0, $archive->getEntryCount());
|
||||
}
|
||||
|
||||
// ─── getType() ───
|
||||
|
||||
#[Test]
|
||||
public function getTypeReturnsDocumentType(): void
|
||||
{
|
||||
$archive = $this->makeSimpleArchive(type: 1);
|
||||
|
||||
$this->assertSame(1, $archive->getType());
|
||||
}
|
||||
|
||||
// ─── getDocument() ───
|
||||
|
||||
#[Test]
|
||||
public function getDocumentReturnsUnderlyingProto(): void
|
||||
{
|
||||
$doc = new PlaylistDocument();
|
||||
$doc->setRootNode($this->makeRootWithChild());
|
||||
$doc->setType(2);
|
||||
|
||||
$archive = new PlaylistArchive($doc);
|
||||
|
||||
$this->assertSame($doc, $archive->getDocument());
|
||||
}
|
||||
|
||||
// ─── Embedded file partitioning ───
|
||||
|
||||
#[Test]
|
||||
public function getEmbeddedFilesReturnsAllEmbeddedEntries(): void
|
||||
{
|
||||
$files = [
|
||||
'Song.pro' => 'prodata',
|
||||
'Another.pro' => 'prodata2',
|
||||
'Users/path/image.jpg' => 'imgdata',
|
||||
];
|
||||
|
||||
$archive = $this->makeSimpleArchive(embeddedFiles: $files);
|
||||
|
||||
$embedded = $archive->getEmbeddedFiles();
|
||||
$this->assertCount(3, $embedded);
|
||||
$this->assertArrayHasKey('Song.pro', $embedded);
|
||||
$this->assertArrayHasKey('Another.pro', $embedded);
|
||||
$this->assertArrayHasKey('Users/path/image.jpg', $embedded);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getEmbeddedProFilesReturnsOnlyProFiles(): void
|
||||
{
|
||||
$files = [
|
||||
'Song.pro' => 'prodata',
|
||||
'Another Song.pro' => 'prodata2',
|
||||
'Users/path/image.jpg' => 'imgdata',
|
||||
'Users/path/video.mp4' => 'viddata',
|
||||
];
|
||||
|
||||
$archive = $this->makeSimpleArchive(embeddedFiles: $files);
|
||||
|
||||
$proFiles = $archive->getEmbeddedProFiles();
|
||||
$this->assertCount(2, $proFiles);
|
||||
$this->assertArrayHasKey('Song.pro', $proFiles);
|
||||
$this->assertArrayHasKey('Another Song.pro', $proFiles);
|
||||
$this->assertSame('prodata', $proFiles['Song.pro']);
|
||||
$this->assertSame('prodata2', $proFiles['Another Song.pro']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getEmbeddedMediaFilesReturnsNonProNonDataFiles(): void
|
||||
{
|
||||
$files = [
|
||||
'Song.pro' => 'prodata',
|
||||
'Users/path/image.jpg' => 'imgdata',
|
||||
'Users/path/video.mp4' => 'viddata',
|
||||
];
|
||||
|
||||
$archive = $this->makeSimpleArchive(embeddedFiles: $files);
|
||||
|
||||
$mediaFiles = $archive->getEmbeddedMediaFiles();
|
||||
$this->assertCount(2, $mediaFiles);
|
||||
$this->assertArrayHasKey('Users/path/image.jpg', $mediaFiles);
|
||||
$this->assertArrayHasKey('Users/path/video.mp4', $mediaFiles);
|
||||
$this->assertArrayNotHasKey('Song.pro', $mediaFiles);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function embeddedFilesEmptyByDefault(): void
|
||||
{
|
||||
$archive = $this->makeSimpleArchive();
|
||||
|
||||
$this->assertSame([], $archive->getEmbeddedFiles());
|
||||
$this->assertSame([], $archive->getEmbeddedProFiles());
|
||||
$this->assertSame([], $archive->getEmbeddedMediaFiles());
|
||||
}
|
||||
|
||||
// ─── Lazy .pro parsing ───
|
||||
|
||||
#[Test]
|
||||
public function getEmbeddedSongLazilyParsesProFile(): void
|
||||
{
|
||||
// Create minimal Presentation proto bytes
|
||||
$presentation = new Presentation();
|
||||
$presentation->setName('Amazing Grace');
|
||||
$proBytes = $presentation->serializeToString();
|
||||
|
||||
$archive = $this->makeSimpleArchive(embeddedFiles: [
|
||||
'Amazing Grace.pro' => $proBytes,
|
||||
]);
|
||||
|
||||
$song = $archive->getEmbeddedSong('Amazing Grace.pro');
|
||||
$this->assertInstanceOf(Song::class, $song);
|
||||
$this->assertSame('Amazing Grace', $song->getName());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getEmbeddedSongCachesParsedResult(): void
|
||||
{
|
||||
$presentation = new Presentation();
|
||||
$presentation->setName('Cached Song');
|
||||
$proBytes = $presentation->serializeToString();
|
||||
|
||||
$archive = $this->makeSimpleArchive(embeddedFiles: [
|
||||
'Cached.pro' => $proBytes,
|
||||
]);
|
||||
|
||||
$song1 = $archive->getEmbeddedSong('Cached.pro');
|
||||
$song2 = $archive->getEmbeddedSong('Cached.pro');
|
||||
|
||||
$this->assertSame($song1, $song2, 'Lazy parsing should cache and return same Song instance');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getEmbeddedSongReturnsNullForUnknownFile(): void
|
||||
{
|
||||
$archive = $this->makeSimpleArchive(embeddedFiles: [
|
||||
'Song.pro' => 'data',
|
||||
]);
|
||||
|
||||
$this->assertNull($archive->getEmbeddedSong('NonExistent.pro'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getEmbeddedSongReturnsNullForMediaFile(): void
|
||||
{
|
||||
$archive = $this->makeSimpleArchive(embeddedFiles: [
|
||||
'image.jpg' => 'imgdata',
|
||||
]);
|
||||
|
||||
$this->assertNull($archive->getEmbeddedSong('image.jpg'));
|
||||
}
|
||||
}
|
||||
361
php/tests/PlaylistEntryTest.php
Normal file
361
php/tests/PlaylistEntryTest.php
Normal file
|
|
@ -0,0 +1,361 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ProPresenter\Parser\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use ProPresenter\Parser\PlaylistEntry;
|
||||
use Rv\Data\PlaylistItem;
|
||||
use Rv\Data\PlaylistItem\Header;
|
||||
use Rv\Data\PlaylistItem\Presentation;
|
||||
use Rv\Data\PlaylistItem\Placeholder;
|
||||
use Rv\Data\Cue;
|
||||
use Rv\Data\UUID;
|
||||
use Rv\Data\URL;
|
||||
use Rv\Data\Color;
|
||||
|
||||
class PlaylistEntryTest extends TestCase
|
||||
{
|
||||
// ─── Helpers ───
|
||||
|
||||
private function makePresentationItem(
|
||||
string $uuid = 'test-uuid',
|
||||
string $name = 'Test Song',
|
||||
?string $documentPath = null,
|
||||
?string $arrangementUuid = null,
|
||||
string $arrangementName = '',
|
||||
): PlaylistItem {
|
||||
$item = new PlaylistItem();
|
||||
|
||||
$itemUuid = new UUID();
|
||||
$itemUuid->setString($uuid);
|
||||
$item->setUuid($itemUuid);
|
||||
$item->setName($name);
|
||||
|
||||
$pres = new Presentation();
|
||||
|
||||
if ($documentPath !== null) {
|
||||
$url = new URL();
|
||||
$url->setAbsoluteString($documentPath);
|
||||
$pres->setDocumentPath($url);
|
||||
}
|
||||
|
||||
if ($arrangementUuid !== null) {
|
||||
$arrUuid = new UUID();
|
||||
$arrUuid->setString($arrangementUuid);
|
||||
$pres->setArrangement($arrUuid);
|
||||
}
|
||||
|
||||
if ($arrangementName !== '') {
|
||||
$pres->setArrangementName($arrangementName);
|
||||
}
|
||||
|
||||
$item->setPresentation($pres);
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
private function makeHeaderItem(
|
||||
string $uuid = 'header-uuid',
|
||||
string $name = 'Section Header',
|
||||
?array $color = null,
|
||||
): PlaylistItem {
|
||||
$item = new PlaylistItem();
|
||||
|
||||
$itemUuid = new UUID();
|
||||
$itemUuid->setString($uuid);
|
||||
$item->setUuid($itemUuid);
|
||||
$item->setName($name);
|
||||
|
||||
$header = new Header();
|
||||
|
||||
if ($color !== null) {
|
||||
$c = new Color();
|
||||
$c->setRed($color[0]);
|
||||
$c->setGreen($color[1]);
|
||||
$c->setBlue($color[2]);
|
||||
$c->setAlpha($color[3]);
|
||||
$header->setColor($c);
|
||||
}
|
||||
|
||||
$item->setHeader($header);
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
private function makeCueItem(
|
||||
string $uuid = 'cue-uuid',
|
||||
string $name = 'Cue Item',
|
||||
): PlaylistItem {
|
||||
$item = new PlaylistItem();
|
||||
|
||||
$itemUuid = new UUID();
|
||||
$itemUuid->setString($uuid);
|
||||
$item->setUuid($itemUuid);
|
||||
$item->setName($name);
|
||||
|
||||
$cue = new Cue();
|
||||
$item->setCue($cue);
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
private function makePlaceholderItem(
|
||||
string $uuid = 'placeholder-uuid',
|
||||
string $name = 'Placeholder Item',
|
||||
): PlaylistItem {
|
||||
$item = new PlaylistItem();
|
||||
|
||||
$itemUuid = new UUID();
|
||||
$itemUuid->setString($uuid);
|
||||
$item->setUuid($itemUuid);
|
||||
$item->setName($name);
|
||||
|
||||
$placeholder = new Placeholder();
|
||||
$item->setPlaceholder($placeholder);
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
// ─── getUuid() ───
|
||||
|
||||
#[Test]
|
||||
public function getUuidReturnsUuidString(): void
|
||||
{
|
||||
$item = $this->makePresentationItem(uuid: 'ABC-123-DEF');
|
||||
$entry = new PlaylistEntry($item);
|
||||
|
||||
$this->assertSame('ABC-123-DEF', $entry->getUuid());
|
||||
}
|
||||
|
||||
// ─── getName() ───
|
||||
|
||||
#[Test]
|
||||
public function getNameReturnsItemName(): void
|
||||
{
|
||||
$item = $this->makePresentationItem(name: 'Amazing Grace');
|
||||
$entry = new PlaylistEntry($item);
|
||||
|
||||
$this->assertSame('Amazing Grace', $entry->getName());
|
||||
}
|
||||
|
||||
// ─── getType() ───
|
||||
|
||||
#[Test]
|
||||
public function getTypeReturnsPresentationForPresentationItem(): void
|
||||
{
|
||||
$item = $this->makePresentationItem();
|
||||
$entry = new PlaylistEntry($item);
|
||||
|
||||
$this->assertSame('presentation', $entry->getType());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getTypeReturnsHeaderForHeaderItem(): void
|
||||
{
|
||||
$item = $this->makeHeaderItem();
|
||||
$entry = new PlaylistEntry($item);
|
||||
|
||||
$this->assertSame('header', $entry->getType());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getTypeReturnsCueForCueItem(): void
|
||||
{
|
||||
$item = $this->makeCueItem();
|
||||
$entry = new PlaylistEntry($item);
|
||||
|
||||
$this->assertSame('cue', $entry->getType());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getTypeReturnsPlaceholderForPlaceholderItem(): void
|
||||
{
|
||||
$item = $this->makePlaceholderItem();
|
||||
$entry = new PlaylistEntry($item);
|
||||
|
||||
$this->assertSame('placeholder', $entry->getType());
|
||||
}
|
||||
|
||||
// ─── Type checks ───
|
||||
|
||||
#[Test]
|
||||
public function isPresentationReturnsTrueForPresentationItem(): void
|
||||
{
|
||||
$entry = new PlaylistEntry($this->makePresentationItem());
|
||||
|
||||
$this->assertTrue($entry->isPresentation());
|
||||
$this->assertFalse($entry->isHeader());
|
||||
$this->assertFalse($entry->isCue());
|
||||
$this->assertFalse($entry->isPlaceholder());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function isHeaderReturnsTrueForHeaderItem(): void
|
||||
{
|
||||
$entry = new PlaylistEntry($this->makeHeaderItem());
|
||||
|
||||
$this->assertTrue($entry->isHeader());
|
||||
$this->assertFalse($entry->isPresentation());
|
||||
$this->assertFalse($entry->isCue());
|
||||
$this->assertFalse($entry->isPlaceholder());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function isCueReturnsTrueForCueItem(): void
|
||||
{
|
||||
$entry = new PlaylistEntry($this->makeCueItem());
|
||||
|
||||
$this->assertTrue($entry->isCue());
|
||||
$this->assertFalse($entry->isPresentation());
|
||||
$this->assertFalse($entry->isHeader());
|
||||
$this->assertFalse($entry->isPlaceholder());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function isPlaceholderReturnsTrueForPlaceholderItem(): void
|
||||
{
|
||||
$entry = new PlaylistEntry($this->makePlaceholderItem());
|
||||
|
||||
$this->assertTrue($entry->isPlaceholder());
|
||||
$this->assertFalse($entry->isPresentation());
|
||||
$this->assertFalse($entry->isHeader());
|
||||
$this->assertFalse($entry->isCue());
|
||||
}
|
||||
|
||||
// ─── Header: getHeaderColor() ───
|
||||
|
||||
#[Test]
|
||||
public function getHeaderColorReturnsRgbaArrayForHeaderItem(): void
|
||||
{
|
||||
$item = $this->makeHeaderItem(color: [0.13, 0.59, 0.95, 1.0]);
|
||||
$entry = new PlaylistEntry($item);
|
||||
|
||||
$color = $entry->getHeaderColor();
|
||||
$this->assertIsArray($color);
|
||||
$this->assertCount(4, $color);
|
||||
$this->assertEqualsWithDelta(0.13, $color[0], 0.01);
|
||||
$this->assertEqualsWithDelta(0.59, $color[1], 0.01);
|
||||
$this->assertEqualsWithDelta(0.95, $color[2], 0.01);
|
||||
$this->assertEqualsWithDelta(1.0, $color[3], 0.01);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getHeaderColorReturnsNullForNonHeaderItem(): void
|
||||
{
|
||||
$entry = new PlaylistEntry($this->makePresentationItem());
|
||||
|
||||
$this->assertNull($entry->getHeaderColor());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getHeaderColorReturnsNullWhenHeaderHasNoColor(): void
|
||||
{
|
||||
$item = $this->makeHeaderItem(color: null);
|
||||
$entry = new PlaylistEntry($item);
|
||||
|
||||
$this->assertNull($entry->getHeaderColor());
|
||||
}
|
||||
|
||||
// ─── Presentation: document path ───
|
||||
|
||||
#[Test]
|
||||
public function getDocumentPathReturnsFullUrl(): void
|
||||
{
|
||||
$item = $this->makePresentationItem(
|
||||
documentPath: 'file:///Users/me/Documents/ProPresenter/Libraries/Default/Song.pro',
|
||||
);
|
||||
$entry = new PlaylistEntry($item);
|
||||
|
||||
$this->assertSame(
|
||||
'file:///Users/me/Documents/ProPresenter/Libraries/Default/Song.pro',
|
||||
$entry->getDocumentPath(),
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getDocumentPathReturnsNullForNonPresentationItem(): void
|
||||
{
|
||||
$entry = new PlaylistEntry($this->makeHeaderItem());
|
||||
|
||||
$this->assertNull($entry->getDocumentPath());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getDocumentFilenameExtractsFilenameFromUrl(): void
|
||||
{
|
||||
$item = $this->makePresentationItem(
|
||||
documentPath: 'file:///Users/me/Documents/ProPresenter/Libraries/Default/Amazing%20Grace.pro',
|
||||
);
|
||||
$entry = new PlaylistEntry($item);
|
||||
|
||||
$this->assertSame('Amazing Grace.pro', $entry->getDocumentFilename());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getDocumentFilenameReturnsNullForNonPresentationItem(): void
|
||||
{
|
||||
$entry = new PlaylistEntry($this->makeCueItem());
|
||||
|
||||
$this->assertNull($entry->getDocumentFilename());
|
||||
}
|
||||
|
||||
// ─── Presentation: arrangement ───
|
||||
|
||||
#[Test]
|
||||
public function getArrangementUuidReturnsUuidString(): void
|
||||
{
|
||||
$item = $this->makePresentationItem(arrangementUuid: 'ARR-UUID-123');
|
||||
$entry = new PlaylistEntry($item);
|
||||
|
||||
$this->assertSame('ARR-UUID-123', $entry->getArrangementUuid());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getArrangementNameReturnsFieldFiveValue(): void
|
||||
{
|
||||
$item = $this->makePresentationItem(arrangementName: 'normal');
|
||||
$entry = new PlaylistEntry($item);
|
||||
|
||||
$this->assertSame('normal', $entry->getArrangementName());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function hasArrangementReturnsTrueWhenArrangementSet(): void
|
||||
{
|
||||
$item = $this->makePresentationItem(arrangementUuid: 'ARR-UUID-123');
|
||||
$entry = new PlaylistEntry($item);
|
||||
|
||||
$this->assertTrue($entry->hasArrangement());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function hasArrangementReturnsFalseWhenNoArrangement(): void
|
||||
{
|
||||
$item = $this->makePresentationItem(arrangementUuid: null);
|
||||
$entry = new PlaylistEntry($item);
|
||||
|
||||
$this->assertFalse($entry->hasArrangement());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getArrangementNameReturnsNullForNonPresentationItem(): void
|
||||
{
|
||||
$entry = new PlaylistEntry($this->makeHeaderItem());
|
||||
|
||||
$this->assertNull($entry->getArrangementName());
|
||||
}
|
||||
|
||||
// ─── getPlaylistItem() ───
|
||||
|
||||
#[Test]
|
||||
public function getPlaylistItemReturnsOriginalProto(): void
|
||||
{
|
||||
$item = $this->makePresentationItem();
|
||||
$entry = new PlaylistEntry($item);
|
||||
|
||||
$this->assertSame($item, $entry->getPlaylistItem());
|
||||
}
|
||||
}
|
||||
243
php/tests/PlaylistNodeTest.php
Normal file
243
php/tests/PlaylistNodeTest.php
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ProPresenter\Parser\Tests;
|
||||
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ProPresenter\Parser\PlaylistEntry;
|
||||
use ProPresenter\Parser\PlaylistNode;
|
||||
use Rv\Data\Playlist;
|
||||
use Rv\Data\Playlist\PlaylistArray;
|
||||
use Rv\Data\Playlist\PlaylistItems;
|
||||
use Rv\Data\Playlist\Type;
|
||||
use Rv\Data\PlaylistItem;
|
||||
use Rv\Data\UUID;
|
||||
|
||||
class PlaylistNodeTest extends TestCase
|
||||
{
|
||||
private function makeUuid(string $value): UUID
|
||||
{
|
||||
$uuid = new UUID();
|
||||
$uuid->setString($value);
|
||||
|
||||
return $uuid;
|
||||
}
|
||||
|
||||
private function makeLeafPlaylist(string $name, string $uuid, array $itemNames = []): Playlist
|
||||
{
|
||||
$playlist = new Playlist();
|
||||
$playlist->setName($name);
|
||||
$playlist->setUuid($this->makeUuid($uuid));
|
||||
$playlist->setType(Type::TYPE_PLAYLIST);
|
||||
|
||||
if ($itemNames !== []) {
|
||||
$items = [];
|
||||
foreach ($itemNames as $i => $itemName) {
|
||||
$item = new PlaylistItem();
|
||||
$item->setName($itemName);
|
||||
$item->setUuid($this->makeUuid("item-uuid-{$i}"));
|
||||
$items[] = $item;
|
||||
}
|
||||
$playlistItems = new PlaylistItems();
|
||||
$playlistItems->setItems($items);
|
||||
$playlist->setItems($playlistItems);
|
||||
}
|
||||
|
||||
return $playlist;
|
||||
}
|
||||
|
||||
private function makeContainerPlaylist(string $name, string $uuid, array $children): Playlist
|
||||
{
|
||||
$playlist = new Playlist();
|
||||
$playlist->setName($name);
|
||||
$playlist->setUuid($this->makeUuid($uuid));
|
||||
$playlist->setType(Type::TYPE_GROUP);
|
||||
|
||||
$playlistArray = new PlaylistArray();
|
||||
$playlistArray->setPlaylists($children);
|
||||
$playlist->setPlaylists($playlistArray);
|
||||
|
||||
return $playlist;
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getUuidReturnsPlaylistUuid(): void
|
||||
{
|
||||
$proto = $this->makeLeafPlaylist('Test', 'abc-123');
|
||||
$node = new PlaylistNode($proto);
|
||||
|
||||
$this->assertSame('abc-123', $node->getUuid());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getNameReturnsPlaylistName(): void
|
||||
{
|
||||
$proto = $this->makeLeafPlaylist('My Playlist', 'uuid-1');
|
||||
$node = new PlaylistNode($proto);
|
||||
|
||||
$this->assertSame('My Playlist', $node->getName());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getTypeReturnsPlaylistType(): void
|
||||
{
|
||||
$proto = $this->makeLeafPlaylist('Test', 'uuid-1');
|
||||
$node = new PlaylistNode($proto);
|
||||
|
||||
$this->assertSame(Type::TYPE_PLAYLIST, $node->getType());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function containerNodeIsContainerAndNotLeaf(): void
|
||||
{
|
||||
$child = $this->makeLeafPlaylist('Child', 'child-uuid');
|
||||
$proto = $this->makeContainerPlaylist('Container', 'container-uuid', [$child]);
|
||||
$node = new PlaylistNode($proto);
|
||||
|
||||
$this->assertTrue($node->isContainer());
|
||||
$this->assertFalse($node->isLeaf());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function leafNodeIsLeafAndNotContainer(): void
|
||||
{
|
||||
$proto = $this->makeLeafPlaylist('Leaf', 'leaf-uuid', ['Song A', 'Song B']);
|
||||
$node = new PlaylistNode($proto);
|
||||
|
||||
$this->assertTrue($node->isLeaf());
|
||||
$this->assertFalse($node->isContainer());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function containerNodeReturnsChildPlaylistNodes(): void
|
||||
{
|
||||
$child1 = $this->makeLeafPlaylist('Worship', 'child-1');
|
||||
$child2 = $this->makeLeafPlaylist('Hymns', 'child-2');
|
||||
$proto = $this->makeContainerPlaylist('Root', 'root-uuid', [$child1, $child2]);
|
||||
$node = new PlaylistNode($proto);
|
||||
|
||||
$children = $node->getChildNodes();
|
||||
|
||||
$this->assertCount(2, $children);
|
||||
$this->assertInstanceOf(PlaylistNode::class, $children[0]);
|
||||
$this->assertInstanceOf(PlaylistNode::class, $children[1]);
|
||||
$this->assertSame('Worship', $children[0]->getName());
|
||||
$this->assertSame('Hymns', $children[1]->getName());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function leafNodeReturnsPlaylistEntries(): void
|
||||
{
|
||||
$proto = $this->makeLeafPlaylist('Service', 'leaf-uuid', ['Song 1', 'Song 2', 'Song 3']);
|
||||
$node = new PlaylistNode($proto);
|
||||
|
||||
$entries = $node->getEntries();
|
||||
|
||||
$this->assertCount(3, $entries);
|
||||
$this->assertInstanceOf(PlaylistEntry::class, $entries[0]);
|
||||
$this->assertInstanceOf(PlaylistEntry::class, $entries[1]);
|
||||
$this->assertInstanceOf(PlaylistEntry::class, $entries[2]);
|
||||
$this->assertSame('Song 1', $entries[0]->getName());
|
||||
$this->assertSame('Song 2', $entries[1]->getName());
|
||||
$this->assertSame('Song 3', $entries[2]->getName());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getEntryCountReturnsItemCountForLeaf(): void
|
||||
{
|
||||
$proto = $this->makeLeafPlaylist('Service', 'leaf-uuid', ['A', 'B']);
|
||||
$node = new PlaylistNode($proto);
|
||||
|
||||
$this->assertSame(2, $node->getEntryCount());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getEntryCountReturnsZeroForContainer(): void
|
||||
{
|
||||
$child = $this->makeLeafPlaylist('Child', 'child-uuid');
|
||||
$proto = $this->makeContainerPlaylist('Container', 'c-uuid', [$child]);
|
||||
$node = new PlaylistNode($proto);
|
||||
|
||||
$this->assertSame(0, $node->getEntryCount());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function containerNodeReturnsEmptyEntries(): void
|
||||
{
|
||||
$child = $this->makeLeafPlaylist('Child', 'child-uuid');
|
||||
$proto = $this->makeContainerPlaylist('Container', 'c-uuid', [$child]);
|
||||
$node = new PlaylistNode($proto);
|
||||
|
||||
$this->assertSame([], $node->getEntries());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function leafNodeReturnsEmptyChildNodes(): void
|
||||
{
|
||||
$proto = $this->makeLeafPlaylist('Leaf', 'leaf-uuid', ['Song']);
|
||||
$node = new PlaylistNode($proto);
|
||||
|
||||
$this->assertSame([], $node->getChildNodes());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getPlaylistReturnsUnderlyingProto(): void
|
||||
{
|
||||
$proto = $this->makeLeafPlaylist('Test', 'uuid-1');
|
||||
$node = new PlaylistNode($proto);
|
||||
|
||||
$this->assertSame($proto, $node->getPlaylist());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function recursiveWrappingOfNestedContainers(): void
|
||||
{
|
||||
$grandchild = $this->makeLeafPlaylist('Songs', 'gc-uuid', ['Amazing Grace']);
|
||||
$child = $this->makeContainerPlaylist('Folder', 'c-uuid', [$grandchild]);
|
||||
$root = $this->makeContainerPlaylist('Root', 'r-uuid', [$child]);
|
||||
$node = new PlaylistNode($root);
|
||||
|
||||
$children = $node->getChildNodes();
|
||||
$this->assertCount(1, $children);
|
||||
$this->assertTrue($children[0]->isContainer());
|
||||
|
||||
$grandchildren = $children[0]->getChildNodes();
|
||||
$this->assertCount(1, $grandchildren);
|
||||
$this->assertTrue($grandchildren[0]->isLeaf());
|
||||
$this->assertSame('Songs', $grandchildren[0]->getName());
|
||||
|
||||
$entries = $grandchildren[0]->getEntries();
|
||||
$this->assertCount(1, $entries);
|
||||
$this->assertSame('Amazing Grace', $entries[0]->getName());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function emptyPlaylistWithNoChildrenType(): void
|
||||
{
|
||||
$playlist = new Playlist();
|
||||
$playlist->setName('Empty');
|
||||
$playlist->setUuid($this->makeUuid('empty-uuid'));
|
||||
$playlist->setType(Type::TYPE_PLAYLIST);
|
||||
// No items or playlists set — ChildrenType is null
|
||||
|
||||
$node = new PlaylistNode($playlist);
|
||||
|
||||
$this->assertFalse($node->isContainer());
|
||||
$this->assertFalse($node->isLeaf());
|
||||
$this->assertSame([], $node->getChildNodes());
|
||||
$this->assertSame([], $node->getEntries());
|
||||
$this->assertSame(0, $node->getEntryCount());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getTypeReturnsGroupTypeForContainer(): void
|
||||
{
|
||||
$child = $this->makeLeafPlaylist('Child', 'child-uuid');
|
||||
$proto = $this->makeContainerPlaylist('Group', 'g-uuid', [$child]);
|
||||
$node = new PlaylistNode($proto);
|
||||
|
||||
$this->assertSame(Type::TYPE_GROUP, $node->getType());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue