* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE */ namespace Mokoconsulting\MokoOG\Tests\Unit\Helper; use Joomla\Plugin\System\MokoOG\Helper\JsonLdBuilder; use PHPUnit\Framework\TestCase; class JsonLdBuilderTest extends TestCase { // ── FAQPage ────────────────────────────────────────────────────── public function testBuildFaqReturnsNullForEmptyArray(): void { $this->assertNull(JsonLdBuilder::buildFaq([])); } public function testBuildFaqSkipsEmptyQuestions(): void { $faqs = [ ['question' => '', 'answer' => 'An answer'], ['question' => 'Valid?', 'answer' => ''], ['question' => ' ', 'answer' => 'Still empty'], ]; $this->assertNull(JsonLdBuilder::buildFaq($faqs)); } public function testBuildFaqReturnsValidSchema(): void { $faqs = [ ['question' => 'What is OG?', 'answer' => 'Open Graph protocol.'], ['question' => 'Why use it?', 'answer' => 'Better social previews.'], ]; $result = JsonLdBuilder::buildFaq($faqs); $this->assertNotNull($result); $this->assertSame('https://schema.org', $result['@context']); $this->assertSame('FAQPage', $result['@type']); $this->assertCount(2, $result['mainEntity']); $this->assertSame('Question', $result['mainEntity'][0]['@type']); $this->assertSame('What is OG?', $result['mainEntity'][0]['name']); $this->assertSame('Open Graph protocol.', $result['mainEntity'][0]['acceptedAnswer']['text']); } // ── HowTo ──────────────────────────────────────────────────────── public function testBuildHowToReturnsNullForEmptySteps(): void { $this->assertNull(JsonLdBuilder::buildHowTo('Test', [])); $this->assertNull(JsonLdBuilder::buildHowTo('Test', ['', ' '])); } public function testBuildHowToReturnsValidSchema(): void { $result = JsonLdBuilder::buildHowTo('Install Joomla', ['Download ZIP', 'Upload files', 'Run installer']); $this->assertNotNull($result); $this->assertSame('HowTo', $result['@type']); $this->assertSame('Install Joomla', $result['name']); $this->assertCount(3, $result['step']); $this->assertSame(1, $result['step'][0]['position']); $this->assertSame('HowToStep', $result['step'][0]['@type']); $this->assertSame('Download ZIP', $result['step'][0]['text']); $this->assertArrayNotHasKey('image', $result); } public function testBuildHowToIncludesImageWhenProvided(): void { $result = JsonLdBuilder::buildHowTo('Fix a bike', ['Remove wheel'], 'https://example.com/bike.jpg'); $this->assertNotNull($result); $this->assertSame('https://example.com/bike.jpg', $result['image']); } // ── Recipe ─────────────────────────────────────────────────────── public function testBuildRecipeReturnsNullWhenNoData(): void { $data = (object) ['name' => '', 'description' => '']; $this->assertNull(JsonLdBuilder::buildRecipe($data)); } public function testBuildRecipeCalculatesTotalTime(): void { $data = (object) [ 'name' => 'Pasta', 'prepTime' => 'PT15M', 'cookTime' => 'PT30M', ]; $result = JsonLdBuilder::buildRecipe($data); $this->assertNotNull($result); $this->assertSame('Recipe', $result['@type']); $this->assertSame('PT45M', $result['totalTime']); } public function testBuildRecipeSplitsIngredientsByNewline(): void { $data = (object) [ 'name' => 'Salad', 'ingredients' => "Lettuce\nTomato\nOnion", ]; $result = JsonLdBuilder::buildRecipe($data); $this->assertNotNull($result); $this->assertSame(['Lettuce', 'Tomato', 'Onion'], $result['recipeIngredient']); } // ── Event ──────────────────────────────────────────────────────── public function testBuildEventReturnsNullWithoutStartDate(): void { $data = (object) ['name' => 'Conference', 'startDate' => '']; $this->assertNull(JsonLdBuilder::buildEvent($data)); } public function testBuildEventIncludesLocationAndOffers(): void { $data = (object) [ 'name' => 'Tech Summit', 'startDate' => '2026-09-01T09:00:00', 'endDate' => '2026-09-01T17:00:00', 'location' => (object) [ 'name' => 'Convention Center', 'address' => '123 Main St', ], 'offers' => (object) [ 'price' => '99.00', 'currency' => 'EUR', 'url' => 'https://example.com/tickets', ], ]; $result = JsonLdBuilder::buildEvent($data); $this->assertNotNull($result); $this->assertSame('Event', $result['@type']); $this->assertSame('2026-09-01T09:00:00', $result['startDate']); $this->assertSame('2026-09-01T17:00:00', $result['endDate']); $this->assertSame('Place', $result['location']['@type']); $this->assertSame('Convention Center', $result['location']['name']); $this->assertSame('Offer', $result['offers']['@type']); $this->assertSame('99.00', $result['offers']['price']); $this->assertSame('EUR', $result['offers']['priceCurrency']); } // ── LocalBusiness ──────────────────────────────────────────────── public function testBuildLocalBusinessReturnsNullWithoutName(): void { $params = $this->createParamsMock([]); $this->assertNull(JsonLdBuilder::buildLocalBusiness($params)); } public function testBuildLocalBusinessIncludesAddress(): void { $params = $this->createParamsMock([ 'business_name' => 'Moko Consulting', 'street_address' => '456 Oak Ave', 'city' => 'Austin', 'region' => 'TX', 'postal_code' => '78701', 'country' => 'US', 'telephone' => '+1-555-0100', ]); $result = JsonLdBuilder::buildLocalBusiness($params); $this->assertNotNull($result); $this->assertSame('LocalBusiness', $result['@type']); $this->assertSame('Moko Consulting', $result['name']); $this->assertSame('PostalAddress', $result['address']['@type']); $this->assertSame('456 Oak Ave', $result['address']['streetAddress']); $this->assertSame('Austin', $result['address']['addressLocality']); $this->assertSame('TX', $result['address']['addressRegion']); $this->assertSame('78701', $result['address']['postalCode']); $this->assertSame('US', $result['address']['addressCountry']); $this->assertSame('+1-555-0100', $result['telephone']); } // ── VideoObject ────────────────────────────────────────────────── public function testBuildVideoReturnsNullForEmptyUrl(): void { $this->assertNull(JsonLdBuilder::buildVideo('')); } public function testBuildVideoAddsEmbedUrlForYoutube(): void { $result = JsonLdBuilder::buildVideo( 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', 'Test Video', 'A description' ); $this->assertNotNull($result); $this->assertSame('VideoObject', $result['@type']); $this->assertSame('https://www.youtube.com/watch?v=dQw4w9WgXcQ', $result['contentUrl']); $this->assertSame('https://www.youtube.com/embed/dQw4w9WgXcQ', $result['embedUrl']); $this->assertSame('Test Video', $result['name']); } // ── toScriptTag ────────────────────────────────────────────────── public function testToScriptTagEscapesClosingScriptTags(): void { $schema = [ '@context' => 'https://schema.org', '@type' => 'Article', 'headline' => 'Test ', ]; $output = JsonLdBuilder::toScriptTag($schema); $this->assertStringStartsWith('', $output); // The closing inside the JSON must be escaped $this->assertStringNotContainsString('