- populate COPYRIGHT (title/author/copyright/CCLI) + blank slides on every song; songHasContent ignores locked sections - foreground info/moderation images now bundle-relative (fixes blank images) - pre-added .probundle injection: Zip64-fix + verbatim .pro extraction (fixes empty bundle) - nametag subtitle split (text + subtitle); smaller non-bold render - skip songs with no content slides at export with German warning - link service agenda songs to SongDB edit modal via #song-<id> - allow CCLI import of metadata-only songs (no lyric sections) - expose has_content_slides on service songs; show "Keine Inhaltsfolien"
180 lines
7.2 KiB
PHP
180 lines
7.2 KiB
PHP
<?php
|
|
|
|
use App\Services\CcliPasteParser;
|
|
use App\Services\DTO\ParsedCcliSection;
|
|
use App\Services\DTO\ParsedCcliSong;
|
|
|
|
function ccliFixturePath(string $filename): string
|
|
{
|
|
return base_path("tests/fixtures/ccli/{$filename}");
|
|
}
|
|
|
|
function ccliFixtureContent(string $filename): string
|
|
{
|
|
return file_get_contents(ccliFixturePath($filename));
|
|
}
|
|
|
|
test('each fixture parses into a valid ParsedCcliSong DTO', function (): void {
|
|
$parser = new CcliPasteParser;
|
|
|
|
foreach (glob(base_path('tests/fixtures/ccli/*.txt')) as $path) {
|
|
$filename = basename($path);
|
|
$result = $parser->parse(ccliFixtureContent($filename));
|
|
|
|
expect($result)->toBeInstanceOf(ParsedCcliSong::class);
|
|
expect($result->title)->not->toBeEmpty("Fixture {$filename}: title should not be empty");
|
|
expect($result->sections)->not->toBeEmpty("Fixture {$filename}: should have at least one section");
|
|
|
|
foreach ($result->sections as $section) {
|
|
expect($section)->toBeInstanceOf(ParsedCcliSection::class);
|
|
expect($section->kind)->not->toBeEmpty("Fixture {$filename}: section kind should not be empty");
|
|
expect($section->lines)->not->toBeEmpty("Fixture {$filename}: section should have lines");
|
|
}
|
|
}
|
|
});
|
|
|
|
test('english-only-multi-verse.txt parses 4+ sections without translation', function (): void {
|
|
$parser = new CcliPasteParser;
|
|
$result = $parser->parse(ccliFixtureContent('english-only-multi-verse.txt'));
|
|
|
|
expect(count($result->sections))->toBeGreaterThanOrEqual(4);
|
|
expect($result->ccliId)->not->toBeNull();
|
|
|
|
$hasTranslated = false;
|
|
foreach ($result->sections as $section) {
|
|
if ($section->linesTranslated !== null) {
|
|
$hasTranslated = true;
|
|
}
|
|
}
|
|
|
|
expect($hasTranslated)->toBeFalse('English-only should have no linesTranslated');
|
|
});
|
|
|
|
test('english-german-side-by-side.txt extracts both languages per section', function (): void {
|
|
$parser = new CcliPasteParser;
|
|
$result = $parser->parse(ccliFixtureContent('english-german-side-by-side.txt'));
|
|
|
|
$translatedSections = array_filter($result->sections, fn (ParsedCcliSection $section): bool => $section->linesTranslated !== null);
|
|
expect(count($translatedSections))->toBeGreaterThanOrEqual(1, 'Should have at least 1 section with translation');
|
|
|
|
$first = array_values($translatedSections)[0];
|
|
expect($first->lines)->not->toBeEmpty();
|
|
expect($first->linesTranslated)->not->toBeEmpty();
|
|
});
|
|
|
|
test('german-only.txt detects German labels and normalizes section kind', function (): void {
|
|
$parser = new CcliPasteParser;
|
|
$result = $parser->parse(ccliFixtureContent('german-only.txt'));
|
|
|
|
$labels = array_map(fn (ParsedCcliSection $section): string => $section->label, $result->sections);
|
|
$kinds = array_map(fn (ParsedCcliSection $section): string => $section->kind, $result->sections);
|
|
|
|
expect($labels)->toContain('Strophe 1');
|
|
expect($kinds)->toContain('Verse');
|
|
expect($kinds)->toContain('Chorus');
|
|
});
|
|
|
|
test('copy-icon-vers-author-trailing.txt parses SongSelect copy icon format', function (): void {
|
|
$parser = new CcliPasteParser;
|
|
$result = $parser->parse(ccliFixtureContent('copy-icon-vers-author-trailing.txt'));
|
|
|
|
expect($result->title)->toBe('Heilig ist der Herr')
|
|
->and($result->author)->toBe('Albert Frey')
|
|
->and($result->ccliId)->toBe('4327499')
|
|
->and($result->year)->toBe('1998')
|
|
->and($result->sections)->toHaveCount(2);
|
|
|
|
$verse = $result->sections[0];
|
|
$chorus = $result->sections[1];
|
|
|
|
expect($verse->label)->toBe('Vers')
|
|
->and($verse->kind)->toBe('Verse')
|
|
->and($verse->lines)->toHaveCount(9)
|
|
->and($chorus->label)->toBe('Chorus')
|
|
->and($chorus->kind)->toBe('Chorus')
|
|
->and($chorus->lines)->toHaveCount(3)
|
|
->and($chorus->lines)->not->toContain('Albert Frey');
|
|
});
|
|
|
|
test('common CCLI metadata formats extract the song ID but not license numbers', function (): void {
|
|
$parser = new CcliPasteParser;
|
|
|
|
$result = $parser->parse("Test Song\nTest Artist\n\nVerse 1\nLine\n\nCCLI Song # 1234567\nCCLI License # 111222");
|
|
expect($result->ccliId)->toBe('1234567');
|
|
|
|
$result = $parser->parse("Test Song\nTest Artist\n\nStrophe 1\nZeile\n\nCCLI-Nr. 7654321");
|
|
expect($result->ccliId)->toBe('7654321');
|
|
});
|
|
|
|
test('repeat-marker.txt preserves modifier in section DTO', function (): void {
|
|
$parser = new CcliPasteParser;
|
|
$result = $parser->parse(ccliFixtureContent('repeat-marker.txt'));
|
|
|
|
$repeatSections = array_filter($result->sections, fn (ParsedCcliSection $section): bool => $section->modifier !== null);
|
|
expect(count($repeatSections))->toBeGreaterThanOrEqual(1, 'Should have at least 1 section with Repeat modifier');
|
|
});
|
|
|
|
test('umlauts.txt preserves Unicode characters', function (): void {
|
|
$parser = new CcliPasteParser;
|
|
$result = $parser->parse(ccliFixtureContent('umlauts.txt'));
|
|
|
|
$allText = $result->title;
|
|
foreach ($result->sections as $section) {
|
|
$allText .= implode(' ', $section->lines);
|
|
}
|
|
|
|
expect((bool) preg_match('/[äöüßÄÖÜ]/u', $allText))->toBeTrue('Umlauts should be preserved');
|
|
});
|
|
|
|
test('missing-copyright.txt returns null copyrightText', function (): void {
|
|
$parser = new CcliPasteParser;
|
|
$result = $parser->parse(ccliFixtureContent('missing-copyright.txt'));
|
|
|
|
expect($result->ccliId)->not->toBeNull('CCLI ID should still be extracted');
|
|
expect($result->copyrightText)->toBeNull('No © line should mean null copyrightText');
|
|
expect($result->year)->toBeNull('No © means no year either');
|
|
});
|
|
|
|
test('5-verses.txt handles 5 verse sections correctly', function (): void {
|
|
$parser = new CcliPasteParser;
|
|
$result = $parser->parse(ccliFixtureContent('5-verses.txt'));
|
|
|
|
$verseSections = array_filter($result->sections, fn (ParsedCcliSection $section): bool => in_array(mb_strtolower($section->kind), ['verse', 'strophe'], true));
|
|
expect(count($verseSections))->toBeGreaterThanOrEqual(5, 'Should have 5 verse sections');
|
|
});
|
|
|
|
test('parse throws InvalidArgumentException on empty input', function (): void {
|
|
$parser = new CcliPasteParser;
|
|
|
|
expect(fn () => $parser->parse(''))->toThrow(InvalidArgumentException::class);
|
|
});
|
|
|
|
test('parse erlaubt Metadaten-only Seite (Titel + CCLI, keine Sektionen)', function (): void {
|
|
$parser = new CcliPasteParser;
|
|
|
|
// Echte CCLI-Seite ohne Liedtext-Sektionen: Import muss möglich sein.
|
|
$result = $parser->parse("Instrumental\n\n© 2024 Verlag\nCCLI # 9999777");
|
|
|
|
expect($result->title)->toBe('Instrumental')
|
|
->and($result->ccliId)->toBe('9999777')
|
|
->and($result->sections)->toBe([]);
|
|
});
|
|
|
|
test('parse throws InvalidArgumentException when neither title nor ccli present', function (): void {
|
|
$parser = new CcliPasteParser;
|
|
|
|
// Reiner Whitespace -> kein Song -> Exception.
|
|
expect(fn () => $parser->parse(" \n \n "))->toThrow(InvalidArgumentException::class);
|
|
});
|
|
|
|
test('parse error messages are in German', function (): void {
|
|
$parser = new CcliPasteParser;
|
|
|
|
try {
|
|
$parser->parse('');
|
|
} catch (InvalidArgumentException $exception) {
|
|
expect($exception->getMessage())->toMatch('/[A-Za-zÄÖÜäöü]/u');
|
|
expect($exception->getMessage())->not->toContain('Error:');
|
|
}
|
|
});
|