From 40d18545874263fca3edbe7b3f6905b0eddeb1c0 Mon Sep 17 00:00:00 2001 From: Joey Smith Date: Thu, 1 Oct 2026 02:41:48 -0500 Subject: [PATCH] test: cover the resolver factories ThemeResolverFactory and AggregateResolverFactory had no tests, which left 16 lines of the resolver PR uncovered. Adds unit tests for both, including the attach priorities. Signed-off-by: Joey Smith --- .../AggregateResolverFactoryTest.php | 102 ++++++++++++++++++ .../Container/ThemeResolverFactoryTest.php | 101 +++++++++++++++++ 2 files changed, 203 insertions(+) create mode 100644 test/unit/Resolver/Container/AggregateResolverFactoryTest.php create mode 100644 test/unit/Resolver/Container/ThemeResolverFactoryTest.php diff --git a/test/unit/Resolver/Container/AggregateResolverFactoryTest.php b/test/unit/Resolver/Container/AggregateResolverFactoryTest.php new file mode 100644 index 0000000..4d7957c --- /dev/null +++ b/test/unit/Resolver/Container/AggregateResolverFactoryTest.php @@ -0,0 +1,102 @@ + + * and contributors. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace WebwareTest\Theme\Resolver\Container; + +use Laminas\Stdlib\PriorityQueue; +use Laminas\View\Resolver\AggregateResolver; +use Laminas\View\Resolver\TemplateMapResolver; +use Mezzio\LaminasView\NamespacedPathStackResolver; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; +use Webware\Theme\Resolver\Container\AggregateResolverFactory; +use Webware\Theme\Resolver\ThemeResolver; + +#[CoversClass(AggregateResolverFactory::class)] +#[CoversMethod(AggregateResolverFactory::class, '__invoke')] +final class AggregateResolverFactoryTest extends TestCase +{ + private const DEFAULT_APP = __DIR__ . '/../../../integration/TestAssets/themes/default/app'; + + #[Test] + public function theNamespacedPathStackAnswersWhatNeitherElseCarries(): void + { + $aggregate = $this->build([], []); + + static::assertStringEndsWith( + '/themes/default/app/home-page.phtml', + (string) $aggregate->resolve('app::home-page.phtml'), + ); + } + + #[Test] + public function theResolversAreAttachedAtTheDocumentedPriorities(): void + { + $queue = $this->build([], [])->getIterator(); + static::assertInstanceOf(PriorityQueue::class, $queue); + + $priorities = []; + foreach ($queue->toArray(PriorityQueue::EXTR_BOTH) as $entry) { + $priorities[$entry['data']::class] = $entry['priority']; + } + + static::assertSame( + [ + ThemeResolver::class => 100, + TemplateMapResolver::class => 50, + NamespacedPathStackResolver::class => 1, + ], + $priorities, + ); + } + + #[Test] + public function theTemplateMapIsConsultedBeforeThePathStack(): void + { + $aggregate = $this->build([], ['app::home-page.phtml' => '/map.phtml']); + + static::assertSame('/map.phtml', $aggregate->resolve('app::home-page.phtml')); + } + + #[Test] + public function theThemeResolverIsConsultedBeforeTheTemplateMap(): void + { + $aggregate = $this->build(['app::home-page.phtml' => '/theme.phtml'], ['app::home-page.phtml' => '/map.phtml']); + + static::assertSame('/theme.phtml', $aggregate->resolve('app::home-page.phtml')); + } + + /** @param array $theme */ + private function build(array $theme, array $map): AggregateResolver + { + $services = [ + ThemeResolver::class => new ThemeResolver(['default' => $theme]), + TemplateMapResolver::class => new TemplateMapResolver($map), + NamespacedPathStackResolver::class => new NamespacedPathStackResolver(['script_paths' => [ + 'app' => self::DEFAULT_APP, + ]]), + ]; + + $container = $this->createStub(ContainerInterface::class); + $container->method('get')->willReturnCallback(static fn(string $id): mixed => $services[$id]); + + $aggregate = new AggregateResolverFactory()->__invoke($container); + static::assertInstanceOf(AggregateResolver::class, $aggregate); + + return $aggregate; + } +} diff --git a/test/unit/Resolver/Container/ThemeResolverFactoryTest.php b/test/unit/Resolver/Container/ThemeResolverFactoryTest.php new file mode 100644 index 0000000..a051015 --- /dev/null +++ b/test/unit/Resolver/Container/ThemeResolverFactoryTest.php @@ -0,0 +1,101 @@ + + * and contributors. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace WebwareTest\Theme\Resolver\Container; + +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; +use Webware\Theme\Resolver\Container\ThemeResolverFactory; +use Webware\Theme\Resolver\ThemeResolver; + +#[CoversClass(ThemeResolverFactory::class)] +#[CoversMethod(ThemeResolverFactory::class, '__invoke')] +final class ThemeResolverFactoryTest extends TestCase +{ + #[Test] + public function aMissingActiveThemeMeansDefault(): void + { + static::assertSame('/default/layout.phtml', $this->build($this->config(null))->resolve('layout::default')); + } + + #[Test] + public function anAddressTheActiveThemeLacksComesFromDefault(): void + { + static::assertSame('/default/home.phtml', $this->build($this->config('acme'))->resolve('app::home')); + } + + #[Test] + public function anEmptyActiveThemeMeansDefault(): void + { + static::assertSame('/default/layout.phtml', $this->build($this->config(''))->resolve('layout::default')); + } + + #[Test] + public function aNonStringActiveThemeMeansDefault(): void + { + static::assertSame('/default/layout.phtml', $this->build($this->config(1))->resolve('layout::default')); + } + + #[Test] + public function anUnknownAddressIsNotResolved(): void + { + static::assertFalse($this->build($this->config('acme'))->resolve('missing::address')); + } + + #[Test] + public function malformedConfigurationNothingResolves(): void + { + static::assertFalse($this->build(['theme' => 'x'])->resolve('layout::default')); + static::assertFalse($this->build(['theme' => ['themes' => 'x']])->resolve('layout::default')); + } + + #[Test] + public function theActiveThemeWinsAnAddressItCarries(): void + { + static::assertSame('/acme/layout.phtml', $this->build($this->config('acme'))->resolve('layout::default')); + } + + #[Test] + public function withoutConfigurationNothingResolves(): void + { + static::assertFalse($this->build([], hasConfig: false)->resolve('layout::default')); + } + + /** @param array $config */ + private function build(array $config, bool $hasConfig = true): ThemeResolver + { + $container = $this->createStub(ContainerInterface::class); + $container->method('has')->willReturn($hasConfig); + $container->method('get')->willReturn($config); + + return new ThemeResolverFactory()->__invoke($container); + } + + /** @return array */ + private function config(mixed $active): array + { + return [ + 'theme' => [ + 'active' => $active, + 'themes' => [ + 'default' => ['layout::default' => '/default/layout.phtml', 'app::home' => '/default/home.phtml'], + 'acme' => ['layout::default' => '/acme/layout.phtml'], + ], + ], + ]; + } +}