CodeRabbit Generated Unit Tests: Add comprehensive PHPUnit tests for Config, PageHeader, AdminIndexTemplate, and DefaultPageHeaderTemplate classes

This commit is contained in:
coderabbitai[bot] 2025-07-03 19:15:03 +00:00 committed by GitHub
commit 5aeadbb3a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 1927 additions and 0 deletions

View file

@ -0,0 +1,404 @@
<?php
use PHPUnit\Framework\TestCase;
class ConfigTest extends TestCase
{
private $config;
private $testConfigFile;
private $testConfigData;
protected function setUp(): void
{
$this->config = new Config();
$this->testConfigFile = sys_get_temp_dir() . '/test_config.ini';
$this->testConfigData = [
'database' => [
'host' => 'localhost',
'port' => 3306,
'username' => 'test_user',
'password' => 'test_pass',
'database' => 'test_db'
],
'cache' => [
'enabled' => true,
'ttl' => 3600,
'driver' => 'redis'
],
'app' => [
'name' => 'Test App',
'version' => '1.0.0',
'debug' => false
]
];
}
protected function tearDown(): void
{
if (file_exists($this->testConfigFile)) {
unlink($this->testConfigFile);
}
}
// Happy path tests
public function testConfigCanBeInstantiated()
{
$this->assertInstanceOf(Config::class, $this->config);
}
public function testSetAndGetConfigValue()
{
$this->config->set('test.key', 'test_value');
$this->assertEquals('test_value', $this->config->get('test.key'));
}
public function testSetAndGetNestedConfigValue()
{
$this->config->set('database.host', 'localhost');
$this->config->set('database.port', 3306);
$this->assertEquals('localhost', $this->config->get('database.host'));
$this->assertEquals(3306, $this->config->get('database.port'));
}
public function testSetMultipleValuesAtOnce()
{
$this->config->setMultiple($this->testConfigData);
$this->assertEquals('localhost', $this->config->get('database.host'));
$this->assertEquals(3306, $this->config->get('database.port'));
$this->assertEquals('test_user', $this->config->get('database.username'));
$this->assertTrue($this->config->get('cache.enabled'));
$this->assertEquals('Test App', $this->config->get('app.name'));
}
public function testHasConfigValue()
{
$this->config->set('test.key', 'value');
$this->assertTrue($this->config->has('test.key'));
$this->assertFalse($this->config->has('nonexistent.key'));
}
public function testRemoveConfigValue()
{
$this->config->set('test.key', 'value');
$this->assertTrue($this->config->has('test.key'));
$this->config->remove('test.key');
$this->assertFalse($this->config->has('test.key'));
}
public function testGetAllConfig()
{
$this->config->setMultiple($this->testConfigData);
$allConfig = $this->config->getAll();
$this->assertIsArray($allConfig);
$this->assertArrayHasKey('database', $allConfig);
$this->assertArrayHasKey('cache', $allConfig);
$this->assertArrayHasKey('app', $allConfig);
}
public function testGetConfigSection()
{
$this->config->setMultiple($this->testConfigData);
$databaseConfig = $this->config->getSection('database');
$this->assertIsArray($databaseConfig);
$this->assertEquals('localhost', $databaseConfig['host']);
$this->assertEquals(3306, $databaseConfig['port']);
}
// Edge cases and error conditions
public function testGetNonExistentConfigReturnsNull()
{
$this->assertNull($this->config->get('nonexistent.key'));
}
public function testGetNonExistentConfigReturnsDefaultValue()
{
$defaultValue = 'default_value';
$this->assertEquals($defaultValue, $this->config->get('nonexistent.key', $defaultValue));
}
public function testGetWithEmptyKey()
{
$this->assertNull($this->config->get(''));
$this->assertNull($this->config->get(null));
}
public function testSetWithEmptyKey()
{
$this->expectException(InvalidArgumentException::class);
$this->config->set('', 'value');
}
public function testSetWithNullKey()
{
$this->expectException(InvalidArgumentException::class);
$this->config->set(null, 'value');
}
public function testOverwriteExistingConfigValue()
{
$this->config->set('test.key', 'original_value');
$this->assertEquals('original_value', $this->config->get('test.key'));
$this->config->set('test.key', 'new_value');
$this->assertEquals('new_value', $this->config->get('test.key'));
}
public function testSetComplexDataTypes()
{
$arrayValue = ['item1', 'item2', 'item3'];
$objectValue = new stdClass();
$objectValue->property = 'value';
$this->config->set('test.array', $arrayValue);
$this->config->set('test.object', $objectValue);
$this->assertEquals($arrayValue, $this->config->get('test.array'));
$this->assertEquals($objectValue, $this->config->get('test.object'));
}
public function testRemoveNonExistentKey()
{
// Should not throw an exception
$this->config->remove('nonexistent.key');
$this->assertFalse($this->config->has('nonexistent.key'));
}
public function testGetSectionNonExistent()
{
$result = $this->config->getSection('nonexistent');
$this->assertNull($result);
}
public function testClearAllConfig()
{
$this->config->setMultiple($this->testConfigData);
$this->assertTrue($this->config->has('database.host'));
$this->config->clear();
$this->assertFalse($this->config->has('database.host'));
$this->assertEmpty($this->config->getAll());
}
// File operations tests
public function testLoadConfigFromFile()
{
$configContent = "[database]\nhost = localhost\nport = 3306\n\n[cache]\nenabled = true\nttl = 3600";
file_put_contents($this->testConfigFile, $configContent);
$this->config->loadFromFile($this->testConfigFile);
$this->assertEquals('localhost', $this->config->get('database.host'));
$this->assertEquals('3306', $this->config->get('database.port'));
$this->assertEquals('true', $this->config->get('cache.enabled'));
$this->assertEquals('3600', $this->config->get('cache.ttl'));
}
public function testLoadConfigFromNonExistentFile()
{
$this->expectException(InvalidArgumentException::class);
$this->config->loadFromFile('/nonexistent/path/config.ini');
}
public function testSaveConfigToFile()
{
$this->config->setMultiple($this->testConfigData);
$this->config->saveToFile($this->testConfigFile);
$this->assertFileExists($this->testConfigFile);
$content = file_get_contents($this->testConfigFile);
$this->assertStringContains('localhost', $content);
$this->assertStringContains('3306', $content);
}
public function testSaveConfigToInvalidPath()
{
$this->expectException(RuntimeException::class);
$this->config->saveToFile('/invalid/path/config.ini');
}
// Type casting and validation tests
public function testGetBooleanValue()
{
$this->config->set('test.bool_true', true);
$this->config->set('test.bool_false', false);
$this->config->set('test.bool_string_true', 'true');
$this->config->set('test.bool_string_false', 'false');
$this->assertTrue($this->config->getBool('test.bool_true'));
$this->assertFalse($this->config->getBool('test.bool_false'));
$this->assertTrue($this->config->getBool('test.bool_string_true'));
$this->assertFalse($this->config->getBool('test.bool_string_false'));
}
public function testGetIntegerValue()
{
$this->config->set('test.int', 42);
$this->config->set('test.int_string', '123');
$this->assertEquals(42, $this->config->getInt('test.int'));
$this->assertEquals(123, $this->config->getInt('test.int_string'));
}
public function testGetFloatValue()
{
$this->config->set('test.float', 3.14);
$this->config->set('test.float_string', '2.718');
$this->assertEquals(3.14, $this->config->getFloat('test.float'));
$this->assertEquals(2.718, $this->config->getFloat('test.float_string'));
}
public function testGetArrayValue()
{
$arrayValue = ['a', 'b', 'c'];
$this->config->set('test.array', $arrayValue);
$this->assertEquals($arrayValue, $this->config->getArray('test.array'));
}
// Environment variable integration tests
public function testGetFromEnvironmentVariable()
{
$_ENV['TEST_CONFIG_VALUE'] = 'env_value';
$this->config->set('test.env', '${TEST_CONFIG_VALUE}');
$result = $this->config->get('test.env', null, true); // true for env expansion
$this->assertEquals('env_value', $result);
unset($_ENV['TEST_CONFIG_VALUE']);
}
public function testGetFromEnvironmentVariableWithDefault()
{
$this->config->set('test.env', '${NONEXISTENT_VAR:default_value}');
$result = $this->config->get('test.env', null, true);
$this->assertEquals('default_value', $result);
}
// Merge and extend functionality tests
public function testMergeConfigs()
{
$this->config->setMultiple([
'database' => ['host' => 'localhost', 'port' => 3306],
'cache' => ['enabled' => true]
]);
$additionalConfig = [
'database' => ['port' => 5432, 'ssl' => true],
'logging' => ['level' => 'debug']
];
$this->config->merge($additionalConfig);
$this->assertEquals('localhost', $this->config->get('database.host'));
$this->assertEquals(5432, $this->config->get('database.port')); // Should be overwritten
$this->assertTrue($this->config->get('database.ssl')); // Should be added
$this->assertTrue($this->config->get('cache.enabled')); // Should remain
$this->assertEquals('debug', $this->config->get('logging.level')); // Should be added
}
// Performance and memory tests
public function testHandleLargeConfigData()
{
$largeData = [];
for ($i = 0; $i < 1000; $i++) {
$largeData["key_$i"] = "value_$i";
}
$this->config->setMultiple(['large_section' => $largeData]);
$this->assertEquals('value_500', $this->config->get('large_section.key_500'));
$this->assertEquals(1000, count($this->config->getSection('large_section')));
}
public function testConfigImmutability()
{
$originalData = ['immutable' => ['key' => 'value']];
$this->config->setMultiple($originalData);
$retrievedData = $this->config->getSection('immutable');
$retrievedData['key'] = 'modified_value';
// Original config should remain unchanged
$this->assertEquals('value', $this->config->get('immutable.key'));
}
// Validation and sanitization tests
public function testConfigKeyValidation()
{
$invalidKeys = ['', null, 123, [], new stdClass()];
foreach ($invalidKeys as $key) {
try {
$this->config->set($key, 'value');
$this->fail('Expected InvalidArgumentException for invalid key: ' . var_export($key, true));
} catch (InvalidArgumentException $e) {
$this->assertTrue(true); // Expected exception
}
}
}
public function testNestedKeyDepthLimit()
{
// Test very deep nesting
$deepKey = implode('.', array_fill(0, 20, 'level'));
$this->config->set($deepKey, 'deep_value');
$this->assertEquals('deep_value', $this->config->get($deepKey));
}
// Thread safety and concurrent access tests
public function testConcurrentAccess()
{
$this->config->set('concurrent.test', 'initial_value');
// Simulate concurrent read/write operations
$processes = [];
for ($i = 0; $i < 5; $i++) {
$this->config->set("concurrent.key_$i", "value_$i");
$this->assertEquals("value_$i", $this->config->get("concurrent.key_$i"));
}
$this->assertEquals('initial_value', $this->config->get('concurrent.test'));
}
// Serialization tests
public function testConfigSerialization()
{
$this->config->setMultiple($this->testConfigData);
$serialized = serialize($this->config);
$unserialized = unserialize($serialized);
$this->assertEquals($this->config->get('database.host'), $unserialized->get('database.host'));
$this->assertEquals($this->config->getAll(), $unserialized->getAll());
}
// Configuration validation tests
public function testRequiredConfigValidation()
{
$requiredKeys = ['database.host', 'database.port', 'app.name'];
$this->config->setMultiple($this->testConfigData);
foreach ($requiredKeys as $key) {
$this->assertTrue($this->config->has($key), "Required key '$key' is missing");
}
}
public function testConfigValueConstraints()
{
$this->config->set('database.port', 3306);
$port = $this->config->getInt('database.port');
$this->assertGreaterThan(0, $port);
$this->assertLessThanOrEqual(65535, $port);
}
}

View file

@ -0,0 +1,548 @@
<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\MockObject\MockObject;
/**
* Comprehensive unit tests for PageHeader class
* Testing Framework: PHPUnit
*/
class PageHeaderTest extends TestCase
{
private $pageHeader;
private $mockRequest;
private $mockResponse;
private $mockConfig;
protected function setUp(): void
{
parent::setUp();
// Create mocks for dependencies
$this->mockRequest = $this->createMock(Request::class);
$this->mockResponse = $this->createMock(Response::class);
$this->mockConfig = $this->createMock(Config::class);
// Initialize PageHeader with mocked dependencies
$this->pageHeader = new PageHeader($this->mockRequest, $this->mockResponse, $this->mockConfig);
}
protected function tearDown(): void
{
$this->pageHeader = null;
$this->mockRequest = null;
$this->mockResponse = null;
$this->mockConfig = null;
parent::tearDown();
}
/**
* Test basic header generation with default settings
*/
public function testGenerateHeaderWithDefaults()
{
$this->mockConfig->method('get')
->willReturnMap([
['site_title', 'Test Site'],
['site_description', 'Test Description'],
['theme', 'default']
]);
$header = $this->pageHeader->generateHeader();
$this->assertNotEmpty($header);
$this->assertStringContainsString('Test Site', $header);
$this->assertStringContainsString('Test Description', $header);
$this->assertStringContainsString('<!DOCTYPE html>', $header);
}
/**
* Test header generation with custom title
*/
public function testGenerateHeaderWithCustomTitle()
{
$customTitle = 'Custom Page Title';
$this->mockConfig->method('get')
->willReturnMap([
['site_title', 'Test Site'],
['site_description', 'Test Description'],
['theme', 'default']
]);
$header = $this->pageHeader->generateHeader($customTitle);
$this->assertStringContainsString($customTitle, $header);
$this->assertStringContainsString('Test Site', $header);
}
/**
* Test header generation with empty title
*/
public function testGenerateHeaderWithEmptyTitle()
{
$this->mockConfig->method('get')
->willReturnMap([
['site_title', 'Test Site'],
['site_description', 'Test Description'],
['theme', 'default']
]);
$header = $this->pageHeader->generateHeader('');
$this->assertStringContainsString('Test Site', $header);
$this->assertStringNotContainsString(' | ', $header);
}
/**
* Test header generation with null title
*/
public function testGenerateHeaderWithNullTitle()
{
$this->mockConfig->method('get')
->willReturnMap([
['site_title', 'Test Site'],
['site_description', 'Test Description'],
['theme', 'default']
]);
$header = $this->pageHeader->generateHeader(null);
$this->assertStringContainsString('Test Site', $header);
$this->assertNotEmpty($header);
}
/**
* Test meta tag generation
*/
public function testGenerateMetaTags()
{
$this->mockConfig->method('get')
->willReturnMap([
['site_description', 'Test Description'],
['site_keywords', 'test,keywords,site'],
['site_author', 'Test Author']
]);
$metaTags = $this->pageHeader->generateMetaTags();
$this->assertStringContainsString('name="description"', $metaTags);
$this->assertStringContainsString('Test Description', $metaTags);
$this->assertStringContainsString('name="keywords"', $metaTags);
$this->assertStringContainsString('test,keywords,site', $metaTags);
$this->assertStringContainsString('name="author"', $metaTags);
$this->assertStringContainsString('Test Author', $metaTags);
}
/**
* Test meta tag generation with missing configuration
*/
public function testGenerateMetaTagsWithMissingConfig()
{
$this->mockConfig->method('get')
->willReturn(null);
$metaTags = $this->pageHeader->generateMetaTags();
$this->assertStringContainsString('name="viewport"', $metaTags);
$this->assertStringContainsString('charset=', $metaTags);
}
/**
* Test CSS link generation
*/
public function testGenerateCssLinks()
{
$this->mockConfig->method('get')
->willReturnMap([
['theme', 'custom'],
['css_files', ['main.css', 'theme.css']],
['base_url', 'https://example.com']
]);
$cssLinks = $this->pageHeader->generateCssLinks();
$this->assertStringContainsString('rel="stylesheet"', $cssLinks);
$this->assertStringContainsString('main.css', $cssLinks);
$this->assertStringContainsString('theme.css', $cssLinks);
$this->assertStringContainsString('https://example.com', $cssLinks);
}
/**
* Test CSS link generation with empty CSS files
*/
public function testGenerateCssLinksWithEmptyFiles()
{
$this->mockConfig->method('get')
->willReturnMap([
['theme', 'default'],
['css_files', []],
['base_url', 'https://example.com']
]);
$cssLinks = $this->pageHeader->generateCssLinks();
$this->assertStringContainsString('rel="stylesheet"', $cssLinks);
$this->assertStringContainsString('default', $cssLinks);
}
/**
* Test JavaScript link generation
*/
public function testGenerateJsLinks()
{
$this->mockConfig->method('get')
->willReturnMap([
['js_files', ['main.js', 'utils.js']],
['base_url', 'https://example.com']
]);
$jsLinks = $this->pageHeader->generateJsLinks();
$this->assertStringContainsString('src=', $jsLinks);
$this->assertStringContainsString('main.js', $jsLinks);
$this->assertStringContainsString('utils.js', $jsLinks);
$this->assertStringContainsString('https://example.com', $jsLinks);
}
/**
* Test JavaScript link generation with no JS files
*/
public function testGenerateJsLinksWithNoFiles()
{
$this->mockConfig->method('get')
->willReturnMap([
['js_files', null],
['base_url', 'https://example.com']
]);
$jsLinks = $this->pageHeader->generateJsLinks();
$this->assertEmpty($jsLinks);
}
/**
* Test canonical URL generation
*/
public function testGenerateCanonicalUrl()
{
$this->mockRequest->method('getUri')
->willReturn('https://example.com/page');
$this->mockConfig->method('get')
->willReturnMap([
['canonical_url', true],
['base_url', 'https://example.com']
]);
$canonical = $this->pageHeader->generateCanonicalUrl();
$this->assertStringContainsString('rel="canonical"', $canonical);
$this->assertStringContainsString('https://example.com/page', $canonical);
}
/**
* Test canonical URL generation when disabled
*/
public function testGenerateCanonicalUrlWhenDisabled()
{
$this->mockConfig->method('get')
->willReturn(false);
$canonical = $this->pageHeader->generateCanonicalUrl();
$this->assertEmpty($canonical);
}
/**
* Test Open Graph meta tag generation
*/
public function testGenerateOpenGraphTags()
{
$this->mockConfig->method('get')
->willReturnMap([
['og_title', 'Test OG Title'],
['og_description', 'Test OG Description'],
['og_image', 'https://example.com/image.jpg'],
['og_url', 'https://example.com'],
['og_type', 'website']
]);
$ogTags = $this->pageHeader->generateOpenGraphTags();
$this->assertStringContainsString('property="og:title"', $ogTags);
$this->assertStringContainsString('Test OG Title', $ogTags);
$this->assertStringContainsString('property="og:description"', $ogTags);
$this->assertStringContainsString('Test OG Description', $ogTags);
$this->assertStringContainsString('property="og:image"', $ogTags);
$this->assertStringContainsString('https://example.com/image.jpg', $ogTags);
}
/**
* Test Twitter Card meta tag generation
*/
public function testGenerateTwitterCardTags()
{
$this->mockConfig->method('get')
->willReturnMap([
['twitter_card', 'summary'],
['twitter_site', '@testsite'],
['twitter_creator', '@testcreator'],
['twitter_title', 'Test Twitter Title'],
['twitter_description', 'Test Twitter Description'],
['twitter_image', 'https://example.com/twitter-image.jpg']
]);
$twitterTags = $this->pageHeader->generateTwitterCardTags();
$this->assertStringContainsString('name="twitter:card"', $twitterTags);
$this->assertStringContainsString('summary', $twitterTags);
$this->assertStringContainsString('name="twitter:site"', $twitterTags);
$this->assertStringContainsString('@testsite', $twitterTags);
$this->assertStringContainsString('name="twitter:creator"', $twitterTags);
$this->assertStringContainsString('@testcreator', $twitterTags);
}
/**
* Test favicon generation
*/
public function testGenerateFavicon()
{
$this->mockConfig->method('get')
->willReturnMap([
['favicon', 'favicon.ico'],
['base_url', 'https://example.com']
]);
$favicon = $this->pageHeader->generateFavicon();
$this->assertStringContainsString('rel="icon"', $favicon);
$this->assertStringContainsString('favicon.ico', $favicon);
$this->assertStringContainsString('https://example.com', $favicon);
}
/**
* Test favicon generation with no favicon configured
*/
public function testGenerateFaviconWithNone()
{
$this->mockConfig->method('get')
->willReturn(null);
$favicon = $this->pageHeader->generateFavicon();
$this->assertEmpty($favicon);
}
/**
* Test complete header generation with all components
*/
public function testGenerateCompleteHeader()
{
$this->mockConfig->method('get')
->willReturnMap([
['site_title', 'Complete Test Site'],
['site_description', 'Complete Test Description'],
['theme', 'complete'],
['css_files', ['main.css']],
['js_files', ['main.js']],
['canonical_url', true],
['og_title', 'Test OG Title'],
['twitter_card', 'summary'],
['favicon', 'favicon.ico'],
['base_url', 'https://example.com']
]);
$this->mockRequest->method('getUri')
->willReturn('https://example.com/complete');
$header = $this->pageHeader->generateCompleteHeader('Complete Page');
$this->assertStringContainsString('<!DOCTYPE html>', $header);
$this->assertStringContainsString('Complete Page', $header);
$this->assertStringContainsString('Complete Test Site', $header);
$this->assertStringContainsString('rel="stylesheet"', $header);
$this->assertStringContainsString('main.css', $header);
$this->assertStringContainsString('main.js', $header);
$this->assertStringContainsString('rel="canonical"', $header);
$this->assertStringContainsString('property="og:title"', $header);
$this->assertStringContainsString('name="twitter:card"', $header);
$this->assertStringContainsString('rel="icon"', $header);
}
/**
* Test header generation with XSS protection
*/
public function testGenerateHeaderWithXssProtection()
{
$maliciousTitle = '<script>alert("xss")</script>';
$this->mockConfig->method('get')
->willReturnMap([
['site_title', 'Test Site'],
['site_description', 'Test Description'],
['theme', 'default']
]);
$header = $this->pageHeader->generateHeader($maliciousTitle);
$this->assertStringNotContainsString('<script>', $header);
$this->assertStringNotContainsString('alert("xss")', $header);
$this->assertStringContainsString('&lt;script&gt;', $header);
}
/**
* Test header generation with special characters
*/
public function testGenerateHeaderWithSpecialCharacters()
{
$specialTitle = 'Café & Résumé - "Quotes" & \'Apostrophes\'';
$this->mockConfig->method('get')
->willReturnMap([
['site_title', 'Test Site'],
['site_description', 'Test Description'],
['theme', 'default']
]);
$header = $this->pageHeader->generateHeader($specialTitle);
$this->assertStringContainsString($specialTitle, $header);
$this->assertStringContainsString('Test Site', $header);
}
/**
* Test header generation with very long title
*/
public function testGenerateHeaderWithLongTitle()
{
$longTitle = str_repeat('Very Long Title ', 20);
$this->mockConfig->method('get')
->willReturnMap([
['site_title', 'Test Site'],
['site_description', 'Test Description'],
['theme', 'default']
]);
$header = $this->pageHeader->generateHeader($longTitle);
$this->assertStringContainsString($longTitle, $header);
$this->assertNotEmpty($header);
}
/**
* Test error handling when config is unavailable
*/
public function testGenerateHeaderWithUnavailableConfig()
{
$this->mockConfig->method('get')
->willThrowException(new Exception('Config unavailable'));
$header = $this->pageHeader->generateHeader('Test Title');
$this->assertStringContainsString('Test Title', $header);
$this->assertStringContainsString('<!DOCTYPE html>', $header);
}
/**
* Test header generation with RTL language support
*/
public function testGenerateHeaderWithRtlLanguage()
{
$this->mockConfig->method('get')
->willReturnMap([
['site_title', 'موقع الاختبار'],
['site_description', 'وصف الاختبار'],
['language', 'ar'],
['text_direction', 'rtl'],
['theme', 'default']
]);
$header = $this->pageHeader->generateHeader('صفحة الاختبار');
$this->assertStringContainsString('dir="rtl"', $header);
$this->assertStringContainsString('lang="ar"', $header);
$this->assertStringContainsString('موقع الاختبار', $header);
$this->assertStringContainsString('صفحة الاختبار', $header);
}
/**
* Test header caching mechanism
*/
public function testHeaderCaching()
{
$this->mockConfig->method('get')
->willReturnMap([
['site_title', 'Cached Site'],
['site_description', 'Cached Description'],
['theme', 'default'],
['cache_headers', true]
]);
$header1 = $this->pageHeader->generateHeader('Cached Page');
$header2 = $this->pageHeader->generateHeader('Cached Page');
$this->assertEquals($header1, $header2);
$this->assertStringContainsString('Cached Page', $header1);
$this->assertStringContainsString('Cached Site', $header1);
}
/**
* Test responsive meta viewport tag
*/
public function testResponsiveViewportTag()
{
$this->mockConfig->method('get')
->willReturnMap([
['responsive', true],
['viewport_meta', 'width=device-width, initial-scale=1.0']
]);
$metaTags = $this->pageHeader->generateMetaTags();
$this->assertStringContainsString('name="viewport"', $metaTags);
$this->assertStringContainsString('width=device-width', $metaTags);
$this->assertStringContainsString('initial-scale=1.0', $metaTags);
}
/**
* Test CSP (Content Security Policy) header generation
*/
public function testContentSecurityPolicyHeader()
{
$this->mockConfig->method('get')
->willReturnMap([
['csp_enabled', true],
['csp_policy', "default-src 'self'; script-src 'self' 'unsafe-inline'"]
]);
$cspHeader = $this->pageHeader->generateCspHeader();
$this->assertStringContainsString('Content-Security-Policy', $cspHeader);
$this->assertStringContainsString("default-src 'self'", $cspHeader);
$this->assertStringContainsString("script-src 'self' 'unsafe-inline'", $cspHeader);
}
/**
* Test structured data (JSON-LD) generation
*/
public function testStructuredDataGeneration()
{
$this->mockConfig->method('get')
->willReturnMap([
['structured_data', true],
['site_name', 'Test Site'],
['site_url', 'https://example.com'],
['site_logo', 'https://example.com/logo.png']
]);
$structuredData = $this->pageHeader->generateStructuredData();
$this->assertStringContainsString('application/ld+json', $structuredData);
$this->assertStringContainsString('@context', $structuredData);
$this->assertStringContainsString('@type', $structuredData);
$this->assertStringContainsString('Test Site', $structuredData);
$this->assertStringContainsString('https://example.com', $structuredData);
}
}

View file

@ -0,0 +1,510 @@
<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\MockObject\MockObject;
/**
* Comprehensive unit tests for AdminIndexTemplate
*
* Tests cover happy paths, edge cases, and failure conditions
* Following PHPUnit best practices for clean, readable, and maintainable tests
*/
class AdminIndexTemplateTest extends TestCase
{
private $adminIndexTemplate;
private $mockRequest;
private $mockResponse;
private $mockDatabase;
private $mockAuth;
/**
* Set up test fixtures before each test
*/
protected function setUp(): void
{
parent::setUp();
// Create mock dependencies
$this->mockRequest = $this->createMock(Request::class);
$this->mockResponse = $this->createMock(Response::class);
$this->mockDatabase = $this->createMock(Database::class);
$this->mockAuth = $this->createMock(Auth::class);
// Initialize the template with mocked dependencies
$this->adminIndexTemplate = new AdminIndexTemplate(
$this->mockRequest,
$this->mockResponse,
$this->mockDatabase,
$this->mockAuth
);
}
/**
* Clean up after each test
*/
protected function tearDown(): void
{
$this->adminIndexTemplate = null;
parent::tearDown();
}
/**
* Test successful template rendering with valid admin user
*/
public function testRenderWithValidAdminUser(): void
{
// Arrange
$mockUser = $this->createMockUser('admin', ['admin' => true]);
$this->mockAuth->expects($this->once())
->method('getCurrentUser')
->willReturn($mockUser);
$this->mockDatabase->expects($this->once())
->method('getAdminStats')
->willReturn([
'total_users' => 100,
'active_sessions' => 15,
'pending_reviews' => 5
]);
// Act
$result = $this->adminIndexTemplate->render();
// Assert
$this->assertIsString($result);
$this->assertStringContainsString('Admin Dashboard', $result);
$this->assertStringContainsString('100', $result); // total users
$this->assertStringContainsString('15', $result); // active sessions
$this->assertStringContainsString('5', $result); // pending reviews
}
/**
* Test template rendering fails with unauthorized user
*/
public function testRenderWithUnauthorizedUser(): void
{
// Arrange
$mockUser = $this->createMockUser('user', ['admin' => false]);
$this->mockAuth->expects($this->once())
->method('getCurrentUser')
->willReturn($mockUser);
// Act & Assert
$this->expectException(UnauthorizedException::class);
$this->expectExceptionMessage('Access denied: Admin privileges required');
$this->adminIndexTemplate->render();
}
/**
* Test template rendering with null/guest user
*/
public function testRenderWithNullUser(): void
{
// Arrange
$this->mockAuth->expects($this->once())
->method('getCurrentUser')
->willReturn(null);
// Act & Assert
$this->expectException(UnauthorizedException::class);
$this->expectExceptionMessage('Access denied: Authentication required');
$this->adminIndexTemplate->render();
}
/**
* Test template rendering with database connection failure
*/
public function testRenderWithDatabaseFailure(): void
{
// Arrange
$mockUser = $this->createMockUser('admin', ['admin' => true]);
$this->mockAuth->expects($this->once())
->method('getCurrentUser')
->willReturn($mockUser);
$this->mockDatabase->expects($this->once())
->method('getAdminStats')
->willThrowException(new DatabaseException('Connection failed'));
// Act & Assert
$this->expectException(DatabaseException::class);
$this->expectExceptionMessage('Connection failed');
$this->adminIndexTemplate->render();
}
/**
* Test template rendering with empty/invalid database response
*/
public function testRenderWithEmptyDatabaseResponse(): void
{
// Arrange
$mockUser = $this->createMockUser('admin', ['admin' => true]);
$this->mockAuth->expects($this->once())
->method('getCurrentUser')
->willReturn($mockUser);
$this->mockDatabase->expects($this->once())
->method('getAdminStats')
->willReturn([]);
// Act
$result = $this->adminIndexTemplate->render();
// Assert
$this->assertIsString($result);
$this->assertStringContainsString('Admin Dashboard', $result);
$this->assertStringContainsString('0', $result); // Should show zero for missing stats
}
/**
* Test template rendering with malformed database response
*/
public function testRenderWithMalformedDatabaseResponse(): void
{
// Arrange
$mockUser = $this->createMockUser('admin', ['admin' => true]);
$this->mockAuth->expects($this->once())
->method('getCurrentUser')
->willReturn($mockUser);
$this->mockDatabase->expects($this->once())
->method('getAdminStats')
->willReturn(['invalid' => 'data']);
// Act
$result = $this->adminIndexTemplate->render();
// Assert
$this->assertIsString($result);
$this->assertStringContainsString('Admin Dashboard', $result);
// Should gracefully handle missing expected keys
}
/**
* Test template CSS and JavaScript inclusion
*/
public function testRenderIncludesAssets(): void
{
// Arrange
$mockUser = $this->createMockUser('admin', ['admin' => true]);
$this->mockAuth->expects($this->once())
->method('getCurrentUser')
->willReturn($mockUser);
$this->mockDatabase->expects($this->once())
->method('getAdminStats')
->willReturn(['total_users' => 50]);
// Act
$result = $this->adminIndexTemplate->render();
// Assert
$this->assertStringContainsString('<link', $result);
$this->assertStringContainsString('admin.css', $result);
$this->assertStringContainsString('<script', $result);
$this->assertStringContainsString('admin.js', $result);
}
/**
* Test template meta tags and title
*/
public function testRenderIncludesMetaTags(): void
{
// Arrange
$mockUser = $this->createMockUser('admin', ['admin' => true]);
$this->mockAuth->expects($this->once())
->method('getCurrentUser')
->willReturn($mockUser);
$this->mockDatabase->expects($this->once())
->method('getAdminStats')
->willReturn(['total_users' => 25]);
// Act
$result = $this->adminIndexTemplate->render();
// Assert
$this->assertStringContainsString('<title>Admin Dashboard</title>', $result);
$this->assertStringContainsString('<meta charset="utf-8">', $result);
$this->assertStringContainsString('<meta name="viewport"', $result);
$this->assertStringContainsString('content="width=device-width"', $result);
}
/**
* Test template navigation elements
*/
public function testRenderIncludesNavigation(): void
{
// Arrange
$mockUser = $this->createMockUser('admin', ['admin' => true]);
$this->mockAuth->expects($this->once())
->method('getCurrentUser')
->willReturn($mockUser);
$this->mockDatabase->expects($this->once())
->method('getAdminStats')
->willReturn(['total_users' => 75]);
// Act
$result = $this->adminIndexTemplate->render();
// Assert
$this->assertStringContainsString('<nav', $result);
$this->assertStringContainsString('Users', $result);
$this->assertStringContainsString('Settings', $result);
$this->assertStringContainsString('Reports', $result);
$this->assertStringContainsString('Logout', $result);
}
/**
* Test template with extremely large database values
*/
public function testRenderWithLargeStatValues(): void
{
// Arrange
$mockUser = $this->createMockUser('admin', ['admin' => true]);
$this->mockAuth->expects($this->once())
->method('getCurrentUser')
->willReturn($mockUser);
$this->mockDatabase->expects($this->once())
->method('getAdminStats')
->willReturn([
'total_users' => 999999999,
'active_sessions' => 50000,
'pending_reviews' => 10000
]);
// Act
$result = $this->adminIndexTemplate->render();
// Assert
$this->assertIsString($result);
$this->assertStringContainsString('999,999,999', $result); // Should format large numbers
}
/**
* Test template with negative database values
*/
public function testRenderWithNegativeStatValues(): void
{
// Arrange
$mockUser = $this->createMockUser('admin', ['admin' => true]);
$this->mockAuth->expects($this->once())
->method('getCurrentUser')
->willReturn($mockUser);
$this->mockDatabase->expects($this->once())
->method('getAdminStats')
->willReturn([
'total_users' => -5,
'active_sessions' => -1,
'pending_reviews' => -10
]);
// Act
$result = $this->adminIndexTemplate->render();
// Assert
$this->assertIsString($result);
// Should handle negative values gracefully, possibly converting to 0
$this->assertStringNotContainsString('-5', $result);
}
/**
* Test template XSS prevention
*/
public function testRenderPreventsXSS(): void
{
// Arrange
$mockUser = $this->createMockUser('admin<script>alert("xss")</script>', ['admin' => true]);
$this->mockAuth->expects($this->once())
->method('getCurrentUser')
->willReturn($mockUser);
$this->mockDatabase->expects($this->once())
->method('getAdminStats')
->willReturn(['total_users' => 100]);
// Act
$result = $this->adminIndexTemplate->render();
// Assert
$this->assertIsString($result);
$this->assertStringNotContainsString('<script>alert("xss")</script>', $result);
$this->assertStringContainsString('&lt;script&gt;', $result); // Should be escaped
}
/**
* Test template caching functionality
*/
public function testRenderWithCaching(): void
{
// Arrange
$mockUser = $this->createMockUser('admin', ['admin' => true]);
$this->mockAuth->expects($this->exactly(2))
->method('getCurrentUser')
->willReturn($mockUser);
$this->mockDatabase->expects($this->once()) // Should only call once due to caching
->method('getAdminStats')
->willReturn(['total_users' => 100]);
// Act
$result1 = $this->adminIndexTemplate->render();
$result2 = $this->adminIndexTemplate->render();
// Assert
$this->assertEquals($result1, $result2);
}
/**
* Test template rendering with custom theme
*/
public function testRenderWithCustomTheme(): void
{
// Arrange
$mockUser = $this->createMockUser('admin', ['admin' => true, 'theme' => 'dark']);
$this->mockAuth->expects($this->once())
->method('getCurrentUser')
->willReturn($mockUser);
$this->mockDatabase->expects($this->once())
->method('getAdminStats')
->willReturn(['total_users' => 100]);
// Act
$result = $this->adminIndexTemplate->render();
// Assert
$this->assertStringContainsString('theme-dark', $result);
}
/**
* Test template accessibility features
*/
public function testRenderIncludesAccessibilityFeatures(): void
{
// Arrange
$mockUser = $this->createMockUser('admin', ['admin' => true]);
$this->mockAuth->expects($this->once())
->method('getCurrentUser')
->willReturn($mockUser);
$this->mockDatabase->expects($this->once())
->method('getAdminStats')
->willReturn(['total_users' => 100]);
// Act
$result = $this->adminIndexTemplate->render();
// Assert
$this->assertStringContainsString('aria-label', $result);
$this->assertStringContainsString('role=', $result);
$this->assertStringContainsString('alt=', $result);
}
/**
* Helper method to create mock user objects
*/
private function createMockUser(string $username, array $attributes = []): MockObject
{
$mockUser = $this->createMock(User::class);
$mockUser->expects($this->any())
->method('getUsername')
->willReturn($username);
$mockUser->expects($this->any())
->method('hasRole')
->with('admin')
->willReturn($attributes['admin'] ?? false);
$mockUser->expects($this->any())
->method('getTheme')
->willReturn($attributes['theme'] ?? 'default');
return $mockUser;
}
/**
* Test template rendering performance with large datasets
*/
public function testRenderPerformanceWithLargeDataset(): void
{
// Arrange
$mockUser = $this->createMockUser('admin', ['admin' => true]);
$this->mockAuth->expects($this->once())
->method('getCurrentUser')
->willReturn($mockUser);
$this->mockDatabase->expects($this->once())
->method('getAdminStats')
->willReturn(['total_users' => 1000000]);
// Act
$startTime = microtime(true);
$result = $this->adminIndexTemplate->render();
$endTime = microtime(true);
// Assert
$this->assertIsString($result);
$this->assertLessThan(1.0, $endTime - $startTime); // Should render in under 1 second
}
/**
* Test template error handling with invalid template file
*/
public function testRenderWithMissingTemplateFile(): void
{
// Arrange
$adminIndexTemplate = new AdminIndexTemplate(
$this->mockRequest,
$this->mockResponse,
$this->mockDatabase,
$this->mockAuth,
'/nonexistent/template.php' // Invalid template path
);
$mockUser = $this->createMockUser('admin', ['admin' => true]);
$this->mockAuth->expects($this->once())
->method('getCurrentUser')
->willReturn($mockUser);
// Act & Assert
$this->expectException(TemplateNotFoundException::class);
$adminIndexTemplate->render();
}
/**
* Test template rendering with concurrent access
*/
public function testRenderWithConcurrentAccess(): void
{
// Arrange
$mockUser = $this->createMockUser('admin', ['admin' => true]);
$this->mockAuth->expects($this->exactly(3))
->method('getCurrentUser')
->willReturn($mockUser);
$this->mockDatabase->expects($this->exactly(3))
->method('getAdminStats')
->willReturn(['total_users' => 100]);
// Act - Simulate concurrent rendering
$results = [];
for ($i = 0; $i < 3; $i++) {
$results[] = $this->adminIndexTemplate->render();
}
// Assert
$this->assertCount(3, $results);
foreach ($results as $result) {
$this->assertIsString($result);
$this->assertStringContainsString('Admin Dashboard', $result);
}
}
}

View file

@ -0,0 +1,465 @@
<?php
namespace Tests\Templates;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\MockObject\MockObject;
use App\Templates\DefaultPageHeaderTemplate;
use App\Models\Page;
use App\Models\User;
use App\Services\NavigationService;
use App\Services\ThemeService;
use App\Exceptions\TemplateException;
class DefaultPageHeaderTemplateTest extends TestCase
{
private DefaultPageHeaderTemplate $template;
private MockObject $navigationService;
private MockObject $themeService;
private MockObject $page;
private MockObject $user;
protected function setUp(): void
{
parent::setUp();
$this->navigationService = $this->createMock(NavigationService::class);
$this->themeService = $this->createMock(ThemeService::class);
$this->page = $this->createMock(Page::class);
$this->user = $this->createMock(User::class);
$this->template = new DefaultPageHeaderTemplate(
$this->navigationService,
$this->themeService
);
}
protected function tearDown(): void
{
parent::tearDown();
unset($this->template, $this->navigationService, $this->themeService, $this->page, $this->user);
}
/**
* @test
*/
public function it_renders_basic_header_with_page_title(): void
{
$this->page->method('getTitle')->willReturn('Test Page');
$this->page->method('getMetaDescription')->willReturn('Test description');
$this->themeService->method('getCurrentTheme')->willReturn('default');
$result = $this->template->render($this->page);
$this->assertStringContainsString('<title>Test Page</title>', $result);
$this->assertStringContainsString('<meta name="description" content="Test description">', $result);
}
/**
* @test
*/
public function it_renders_header_with_user_authentication(): void
{
$this->page->method('getTitle')->willReturn('Dashboard');
$this->user->method('getName')->willReturn('John Doe');
$this->user->method('getEmail')->willReturn('john@example.com');
$this->user->method('isAuthenticated')->willReturn(true);
$result = $this->template->render($this->page, $this->user);
$this->assertStringContainsString('John Doe', $result);
$this->assertStringContainsString('john@example.com', $result);
}
/**
* @test
*/
public function it_renders_header_for_anonymous_user(): void
{
$this->page->method('getTitle')->willReturn('Public Page');
$this->user->method('isAuthenticated')->willReturn(false);
$result = $this->template->render($this->page, $this->user);
$this->assertStringContainsString('Login', $result);
$this->assertStringContainsString('Register', $result);
$this->assertStringNotContainsString('Logout', $result);
}
/**
* @test
*/
public function it_includes_navigation_menu(): void
{
$this->page->method('getTitle')->willReturn('Test Page');
$this->navigationService->method('getMainMenu')->willReturn([
['label' => 'Home', 'url' => '/'],
['label' => 'About', 'url' => '/about'],
['label' => 'Contact', 'url' => '/contact']
]);
$result = $this->template->render($this->page);
$this->assertStringContainsString('Home', $result);
$this->assertStringContainsString('About', $result);
$this->assertStringContainsString('Contact', $result);
}
/**
* @test
*/
public function it_applies_theme_specific_styling(): void
{
$this->page->method('getTitle')->willReturn('Themed Page');
$this->themeService->method('getCurrentTheme')->willReturn('dark');
$this->themeService->method('getThemeStyles')->willReturn([
'header-bg' => '#333333',
'header-text' => '#ffffff'
]);
$result = $this->template->render($this->page);
$this->assertStringContainsString('#333333', $result);
$this->assertStringContainsString('#ffffff', $result);
$this->assertStringContainsString('theme-dark', $result);
}
/**
* @test
*/
public function it_handles_empty_page_title(): void
{
$this->page->method('getTitle')->willReturn('');
$this->page->method('getSlug')->willReturn('test-page');
$result = $this->template->render($this->page);
$this->assertStringContainsString('<title>Untitled Page</title>', $result);
}
/**
* @test
*/
public function it_handles_null_page_title(): void
{
$this->page->method('getTitle')->willReturn(null);
$this->page->method('getSlug')->willReturn('test-page');
$result = $this->template->render($this->page);
$this->assertStringContainsString('<title>Untitled Page</title>', $result);
}
/**
* @test
*/
public function it_sanitizes_page_title_for_xss(): void
{
$this->page->method('getTitle')->willReturn('<script>alert("xss")</script>');
$result = $this->template->render($this->page);
$this->assertStringNotContainsString('<script>', $result);
$this->assertStringContainsString('&lt;script&gt;', $result);
}
/**
* @test
*/
public function it_includes_canonical_url(): void
{
$this->page->method('getTitle')->willReturn('Test Page');
$this->page->method('getCanonicalUrl')->willReturn('https://example.com/test-page');
$result = $this->template->render($this->page);
$this->assertStringContainsString('<link rel="canonical" href="https://example.com/test-page">', $result);
}
/**
* @test
*/
public function it_includes_open_graph_meta_tags(): void
{
$this->page->method('getTitle')->willReturn('Test Page');
$this->page->method('getMetaDescription')->willReturn('Test description');
$this->page->method('getOpenGraphImage')->willReturn('https://example.com/image.jpg');
$result = $this->template->render($this->page);
$this->assertStringContainsString('<meta property="og:title" content="Test Page">', $result);
$this->assertStringContainsString('<meta property="og:description" content="Test description">', $result);
$this->assertStringContainsString('<meta property="og:image" content="https://example.com/image.jpg">', $result);
}
/**
* @test
*/
public function it_includes_twitter_meta_tags(): void
{
$this->page->method('getTitle')->willReturn('Test Page');
$this->page->method('getMetaDescription')->willReturn('Test description');
$this->page->method('getTwitterImage')->willReturn('https://example.com/twitter-image.jpg');
$result = $this->template->render($this->page);
$this->assertStringContainsString('<meta name="twitter:title" content="Test Page">', $result);
$this->assertStringContainsString('<meta name="twitter:description" content="Test description">', $result);
$this->assertStringContainsString('<meta name="twitter:image" content="https://example.com/twitter-image.jpg">', $result);
}
/**
* @test
*/
public function it_handles_long_page_titles(): void
{
$longTitle = str_repeat('Very Long Title ', 20);
$this->page->method('getTitle')->willReturn($longTitle);
$result = $this->template->render($this->page);
$this->assertStringContainsString('<title>', $result);
$this->assertLessThan(70, strlen($this->extractTitleFromHtml($result)));
}
/**
* @test
*/
public function it_includes_responsive_meta_viewport(): void
{
$this->page->method('getTitle')->willReturn('Test Page');
$result = $this->template->render($this->page);
$this->assertStringContainsString('<meta name="viewport" content="width=device-width, initial-scale=1">', $result);
}
/**
* @test
*/
public function it_includes_charset_meta_tag(): void
{
$this->page->method('getTitle')->willReturn('Test Page');
$result = $this->template->render($this->page);
$this->assertStringContainsString('<meta charset="UTF-8">', $result);
}
/**
* @test
*/
public function it_handles_special_characters_in_title(): void
{
$this->page->method('getTitle')->willReturn('Test & Page "with" quotes');
$result = $this->template->render($this->page);
$this->assertStringContainsString('Test &amp; Page &quot;with&quot; quotes', $result);
}
/**
* @test
*/
public function it_includes_breadcrumb_navigation(): void
{
$this->page->method('getTitle')->willReturn('Test Page');
$this->navigationService->method('getBreadcrumbs')->willReturn([
['label' => 'Home', 'url' => '/'],
['label' => 'Category', 'url' => '/category'],
['label' => 'Test Page', 'url' => '/category/test-page']
]);
$result = $this->template->render($this->page);
$this->assertStringContainsString('breadcrumb', $result);
$this->assertStringContainsString('Home', $result);
$this->assertStringContainsString('Category', $result);
}
/**
* @test
*/
public function it_throws_exception_when_page_is_null(): void
{
$this->expectException(TemplateException::class);
$this->expectExceptionMessage('Page cannot be null');
$this->template->render(null);
}
/**
* @test
*/
public function it_handles_navigation_service_failure(): void
{
$this->page->method('getTitle')->willReturn('Test Page');
$this->navigationService->method('getMainMenu')->willThrowException(new \Exception('Navigation service unavailable'));
$result = $this->template->render($this->page);
$this->assertStringContainsString('<title>Test Page</title>', $result);
$this->assertStringNotContainsString('Navigation service unavailable', $result);
}
/**
* @test
*/
public function it_handles_theme_service_failure(): void
{
$this->page->method('getTitle')->willReturn('Test Page');
$this->themeService->method('getCurrentTheme')->willThrowException(new \Exception('Theme service unavailable'));
$result = $this->template->render($this->page);
$this->assertStringContainsString('<title>Test Page</title>', $result);
$this->assertStringContainsString('theme-default', $result);
}
/**
* @test
*/
public function it_includes_csrf_token_in_forms(): void
{
$this->page->method('getTitle')->willReturn('Test Page');
$this->user->method('isAuthenticated')->willReturn(true);
$result = $this->template->render($this->page, $this->user);
$this->assertStringContainsString('csrf_token', $result);
$this->assertStringContainsString('name="_token"', $result);
}
/**
* @test
*/
public function it_includes_language_attributes(): void
{
$this->page->method('getTitle')->willReturn('Test Page');
$this->page->method('getLanguage')->willReturn('en');
$result = $this->template->render($this->page);
$this->assertStringContainsString('lang="en"', $result);
$this->assertStringContainsString('hreflang="en"', $result);
}
/**
* @test
*/
public function it_includes_search_functionality(): void
{
$this->page->method('getTitle')->willReturn('Test Page');
$result = $this->template->render($this->page);
$this->assertStringContainsString('search', $result);
$this->assertStringContainsString('type="search"', $result);
}
/**
* @test
*/
public function it_handles_mobile_specific_rendering(): void
{
$_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15';
$this->page->method('getTitle')->willReturn('Test Page');
$result = $this->template->render($this->page);
$this->assertStringContainsString('mobile-header', $result);
$this->assertStringContainsString('hamburger-menu', $result);
unset($_SERVER['HTTP_USER_AGENT']);
}
/**
* @test
*/
public function it_renders_with_custom_css_classes(): void
{
$this->page->method('getTitle')->willReturn('Test Page');
$this->page->method('getCssClasses')->willReturn(['custom-class', 'another-class']);
$result = $this->template->render($this->page);
$this->assertStringContainsString('custom-class', $result);
$this->assertStringContainsString('another-class', $result);
}
/**
* @test
*/
public function it_includes_accessibility_attributes(): void
{
$this->page->method('getTitle')->willReturn('Test Page');
$result = $this->template->render($this->page);
$this->assertStringContainsString('role="banner"', $result);
$this->assertStringContainsString('aria-label', $result);
$this->assertStringContainsString('skip-to-content', $result);
}
/**
* @test
*/
public function it_performance_with_large_navigation_menu(): void
{
$this->page->method('getTitle')->willReturn('Test Page');
$largeMenu = [];
for ($i = 1; $i <= 100; $i++) {
$largeMenu[] = ['label' => "Item $i", 'url' => "/item-$i"];
}
$this->navigationService->method('getMainMenu')->willReturn($largeMenu);
$startTime = microtime(true);
$result = $this->template->render($this->page);
$endTime = microtime(true);
$this->assertLessThan(0.1, $endTime - $startTime, 'Template rendering should be fast even with large menus');
$this->assertStringContainsString('Item 1', $result);
$this->assertStringContainsString('Item 100', $result);
}
/**
* @test
*/
public function it_handles_unicode_characters_in_title(): void
{
$this->page->method('getTitle')->willReturn('Test Page with émojis 🚀 and unicode ñ');
$result = $this->template->render($this->page);
$this->assertStringContainsString('émojis 🚀 and unicode ñ', $result);
}
/**
* @test
*/
public function it_generates_valid_html5_markup(): void
{
$this->page->method('getTitle')->willReturn('Test Page');
$result = $this->template->render($this->page);
$this->assertStringContainsString('<!DOCTYPE html>', $result);
$this->assertStringContainsString('<html', $result);
$this->assertStringContainsString('<head>', $result);
$this->assertStringContainsString('</head>', $result);
$this->assertStringContainsString('<body', $result);
}
/**
* Helper method to extract title from HTML string
*/
private function extractTitleFromHtml(string $html): string
{
if (preg_match('/<title>(.*?)<\/title>/', $html, $matches)) {
return $matches[1];
}
return '';
}
}