mirror of
https://github.com/torrentpier/torrentpier
synced 2025-08-20 05:13:54 -07:00
CodeRabbit Generated Unit Tests: Add comprehensive PHPUnit tests for Config, PageHeader, AdminIndexTemplate, and DefaultPageHeaderTemplate classes
This commit is contained in:
parent
c27d4373d6
commit
5aeadbb3a7
4 changed files with 1927 additions and 0 deletions
404
tests/library/ConfigTest.php
Normal file
404
tests/library/ConfigTest.php
Normal 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);
|
||||
}
|
||||
}
|
548
tests/library/includes/PageHeaderTest.php
Normal file
548
tests/library/includes/PageHeaderTest.php
Normal 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('<script>', $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);
|
||||
}
|
||||
}
|
510
tests/templates/AdminIndexTemplateTest.php
Normal file
510
tests/templates/AdminIndexTemplateTest.php
Normal 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('<script>', $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);
|
||||
}
|
||||
}
|
||||
}
|
465
tests/templates/DefaultPageHeaderTemplateTest.php
Normal file
465
tests/templates/DefaultPageHeaderTemplateTest.php
Normal 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('<script>', $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 & Page "with" 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 '';
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue