*/ protected array $fixtures = ['core.Posts', 'core.Users']; /** * @var \Cake\View\View */ protected $View; /** * @var \TestApp\Controller\ViewPostsController */ protected $PostsController; /** * @var \TestApp\Controller\ThemePostsController */ protected $ThemePostsController; /** * @var \Cake\Controller\Controller */ protected $ThemeController; /** * setUp method */ public function setUp(): void { parent::setUp(); $request = new ServerRequest(); $this->PostsController = new ViewPostsController($request); $this->PostsController->index(); $this->View = $this->PostsController->createView(); $this->View->setTemplatePath('Posts'); $themeRequest = new ServerRequest(['url' => 'posts/index']); $this->ThemeController = new Controller($themeRequest); $this->ThemePostsController = new ThemePostsController($themeRequest); $this->ThemePostsController->index(); $this->loadPlugins(['TestPlugin', 'PluginJs', 'TestTheme', 'Company/TestPluginThree']); Configure::write('debug', true); } /** * tearDown method */ public function tearDown(): void { parent::tearDown(); $this->clearPlugins(); unset($this->View); unset($this->PostsController); unset($this->ThemePostsController); unset($this->ThemeController); } /** * Test getTemplateFileName method */ public function testGetTemplate(): void { $viewOptions = [ 'plugin' => null, 'name' => 'Pages', 'templatePath' => 'Pages', ]; $ThemeView = new TestView(null, null, null, $viewOptions); $ThemeView->setTheme('TestTheme'); $expected = TEST_APP . 'templates' . DS . 'Pages' . DS . 'home.php'; $result = $ThemeView->getTemplateFileName('home'); $this->assertPathEquals($expected, $result); $expected = Plugin::path('TestTheme') . 'templates' . DS . 'Posts' . DS . 'index.php'; $result = $ThemeView->getTemplateFileName('/Posts/index'); $this->assertPathEquals($expected, $result); $expected = Plugin::path('TestTheme') . 'templates' . DS . 'layout' . DS . 'default.php'; $result = $ThemeView->getLayoutFileName(); $this->assertPathEquals($expected, $result); $ThemeView->setLayoutPath('rss'); $expected = TEST_APP . 'templates' . DS . 'layout' . DS . 'rss' . DS . 'default.php'; $result = $ThemeView->getLayoutFileName(); $this->assertPathEquals($expected, $result); $ThemeView->setLayoutPath('email' . DS . 'html'); $expected = TEST_APP . 'templates' . DS . 'layout' . DS . 'email' . DS . 'html' . DS . 'default.php'; $result = $ThemeView->getLayoutFileName(); $this->assertPathEquals($expected, $result); $ThemeView = new TestView(null, null, null, $viewOptions); $ThemeView->setTheme('Company/TestPluginThree'); $expected = Plugin::path('Company/TestPluginThree') . 'templates' . DS . 'layout' . DS . 'default.php'; $result = $ThemeView->getLayoutFileName(); $this->assertPathEquals($expected, $result); } /** * Test getLayoutFileName method on plugin */ public function testPluginGetTemplate(): void { $viewOptions = [ 'plugin' => 'TestPlugin', 'name' => 'TestPlugin', 'templatePath' => 'Tests', 'template' => 'index', ]; $View = new TestView(null, null, null, $viewOptions); $expected = Plugin::path('TestPlugin') . 'templates' . DS . 'Tests' . DS . 'index.php'; $result = $View->getTemplateFileName('index'); $this->assertSame($expected, $result); $expected = Plugin::path('TestPlugin') . 'templates' . DS . 'layout' . DS . 'default.php'; $result = $View->getLayoutFileName(); $this->assertSame($expected, $result); } /** * Test that plugin files with absolute file paths are scoped * to the plugin and do now allow any file path. */ public function testPluginGetTemplateAbsoluteFail(): void { $this->expectException(MissingTemplateException::class); $viewOptions = [ 'plugin' => null, 'name' => 'Pages', 'viewPath' => 'Pages', ]; $view = new TestView(null, null, null, $viewOptions); $expected = TEST_APP . 'Plugin' . DS . 'Company' . DS . 'TestPluginThree' . DS . 'templates' . DS . 'Pages' . DS . 'index.php'; $result = $view->getTemplateFileName('Company/TestPluginThree./Pages/index'); $this->assertPathEquals($expected, $result); $view->getTemplateFileName('Company/TestPluginThree./etc/passwd'); } /** * Test getTemplateFileName method on plugin */ public function testPluginThemedGetTemplate(): void { $viewOptions = [ 'plugin' => 'TestPlugin', 'name' => 'TestPlugin', 'templatePath' => 'Tests', 'template' => 'index', 'theme' => 'TestTheme', ]; $ThemeView = new TestView(null, null, null, $viewOptions); $themePath = Plugin::path('TestTheme') . 'templates' . DS; $expected = $themePath . 'plugin' . DS . 'TestPlugin' . DS . 'Tests' . DS . 'index.php'; $result = $ThemeView->getTemplateFileName('index'); $this->assertPathEquals($expected, $result); $expected = $themePath . 'plugin' . DS . 'TestPlugin' . DS . 'layout' . DS . 'plugin_default.php'; $result = $ThemeView->getLayoutFileName('plugin_default'); $this->assertPathEquals($expected, $result); $expected = $themePath . 'layout' . DS . 'default.php'; $result = $ThemeView->getLayoutFileName('default'); $this->assertPathEquals($expected, $result); } /** * Test that plugin/$plugin_name is only appended to the paths it should be. */ public function testPathPluginGeneration(): void { $viewOptions = [ 'plugin' => 'TestPlugin', 'name' => 'TestPlugin', 'viewPath' => 'Tests', 'view' => 'index', ]; $View = new TestView(null, null, null, $viewOptions); $paths = $View->paths(); $expected = array_merge(App::path('templates'), App::core('templates')); $this->assertEquals($expected, $paths); $paths = $View->paths('TestPlugin'); $pluginPath = Plugin::path('TestPlugin'); $expected = [ TEST_APP . 'templates' . DS . 'plugin' . DS . 'TestPlugin' . DS, $pluginPath . 'templates' . DS, TEST_APP . 'templates' . DS, CORE_PATH . 'templates' . DS, ]; $this->assertPathEquals($expected, $paths); } /** * Test that themed plugin paths are generated correctly. */ public function testPathThemedPluginGeneration(): void { $viewOptions = [ 'plugin' => 'TestPlugin', 'name' => 'TestPlugin', 'viewPath' => 'Tests', 'view' => 'index', 'theme' => 'TestTheme', ]; $View = new TestView(null, null, null, $viewOptions); $paths = $View->paths('TestPlugin'); $pluginPath = Plugin::path('TestPlugin'); $themePath = Plugin::path('TestTheme'); $expected = [ $themePath . 'templates' . DS . 'plugin' . DS . 'TestPlugin' . DS, $themePath . 'templates' . DS, TEST_APP . 'templates' . DS . 'plugin' . DS . 'TestPlugin' . DS, $pluginPath . 'templates' . DS, TEST_APP . 'templates' . DS, CORE_PATH . 'templates' . DS, ]; $this->assertPathEquals($expected, $paths); } /** * Test that multiple paths can be used in App.paths.templates. */ public function testMultipleAppPaths(): void { $viewOptions = [ 'plugin' => 'TestPlugin', 'name' => 'TestPlugin', 'viewPath' => 'Tests', 'view' => 'index', 'theme' => 'TestTheme', ]; $paths = Configure::read('App.paths.templates'); $paths[] = Plugin::path('TestPlugin') . 'templates' . DS; Configure::write('App.paths.templates', $paths); $View = new TestView(null, null, null, $viewOptions); $paths = $View->paths('TestPlugin'); $pluginPath = Plugin::path('TestPlugin'); $themePath = Plugin::path('TestTheme'); $expected = [ $themePath . 'templates' . DS . 'plugin' . DS . 'TestPlugin' . DS, $themePath . 'templates' . DS, TEST_APP . 'templates' . DS . 'plugin' . DS . 'TestPlugin' . DS, $pluginPath . 'templates' . DS . 'plugin' . DS . 'TestPlugin' . DS, $pluginPath . 'templates' . DS, TEST_APP . 'templates' . DS, TEST_APP . 'Plugin' . DS . 'TestPlugin' . DS . 'templates' . DS, CORE_PATH . 'templates' . DS, ]; $this->assertPathEquals($expected, $paths); } /** * Test that CamelCase'd plugins still find their view files. */ public function testCamelCasePluginGetTemplate(): void { $viewOptions = [ 'plugin' => 'TestPlugin', 'name' => 'TestPlugin', 'templatePath' => 'Tests', 'template' => 'index', ]; $View = new TestView(null, null, null, $viewOptions); $pluginPath = Plugin::path('TestPlugin'); $expected = TEST_APP . 'Plugin' . DS . 'TestPlugin' . DS . 'templates' . DS . 'Tests' . DS . 'index.php'; $result = $View->getTemplateFileName('index'); $this->assertPathEquals($expected, $result); $expected = $pluginPath . 'templates' . DS . 'layout' . DS . 'default.php'; $result = $View->getLayoutFileName(); $this->assertPathEquals($expected, $result); } /** * Test getTemplateFileName method */ public function testGetViewFileNames(): void { $viewOptions = [ 'plugin' => null, 'name' => 'Pages', 'templatePath' => 'Pages', ]; $View = new TestView(null, null, null, $viewOptions); $expected = TEST_APP . 'templates' . DS . 'Pages' . DS . 'home.php'; $result = $View->getTemplateFileName('home'); $this->assertPathEquals($expected, $result); $expected = TEST_APP . 'templates' . DS . 'Posts' . DS . 'index.php'; $result = $View->getTemplateFileName('/Posts/index'); $this->assertPathEquals($expected, $result); $expected = TEST_APP . 'templates' . DS . 'Posts' . DS . 'index.php'; $result = $View->getTemplateFileName('../Posts/index'); $this->assertPathEquals($expected, $result); $expected = TEST_APP . 'templates' . DS . 'Pages' . DS . 'page.home.php'; $result = $View->getTemplateFileName('page.home'); $this->assertPathEquals($expected, $result, 'Should not ruin files with dots.'); $expected = TEST_APP . 'templates' . DS . 'Pages' . DS . 'home.php'; $result = $View->getTemplateFileName('TestPlugin.home'); $this->assertPathEquals($expected, $result, 'Plugin is missing the view, cascade to app.'); $View->setTemplatePath('Tests'); $expected = TEST_APP . 'Plugin' . DS . 'TestPlugin' . DS . 'templates' . DS . 'Tests' . DS . 'index.php'; $result = $View->getTemplateFileName('TestPlugin.index'); $this->assertPathEquals($expected, $result); } /** * Test that getTemplateFileName() protects against malicious directory traversal. */ public function testGetViewFileNameDirectoryTraversal(): void { $this->expectException(InvalidArgumentException::class); $viewOptions = [ 'plugin' => null, 'name' => 'Pages', 'templatePath' => 'Pages', ]; $view = new TestView(null, null, null, $viewOptions); $view->ext('.php'); $view->getTemplateFileName('../../../bootstrap'); } /** * Test getTemplateFileName doesn't re-apply existing subdirectories */ public function testGetViewFileNameSubDir(): void { $viewOptions = [ 'plugin' => null, 'name' => 'Posts', 'templatePath' => 'Posts/json', 'layoutPath' => 'json', ]; $view = new TestView(null, null, null, $viewOptions); $expected = TEST_APP . 'templates' . DS . 'Posts' . DS . 'json' . DS . 'index.php'; $result = $view->getTemplateFileName('index'); $this->assertPathEquals($expected, $result); $view->setSubDir('json'); $result = $view->getTemplateFileName('index'); $expected = TEST_APP . 'templates' . DS . 'Posts' . DS . 'json' . DS . 'index.php'; $this->assertPathEquals($expected, $result); } /** * Test getTemplateFileName applies subdirectories on equal length names */ public function testGetViewFileNameSubDirLength(): void { $viewOptions = [ 'plugin' => null, 'name' => 'Jobs', 'templatePath' => 'Jobs', 'layoutPath' => 'json', ]; $view = new TestView(null, null, null, $viewOptions); $view->setSubDir('json'); $result = $view->getTemplateFileName('index'); $expected = TEST_APP . 'templates' . DS . 'Jobs' . DS . 'json' . DS . 'index.php'; $this->assertPathEquals($expected, $result); } /** * Test getting layout filenames */ public function testGetLayoutFileName(): void { $viewOptions = [ 'plugin' => null, 'name' => 'Pages', 'viewPath' => 'Pages', 'action' => 'display', ]; $View = new TestView(null, null, null, $viewOptions); $expected = TEST_APP . 'templates' . DS . 'layout' . DS . 'default.php'; $result = $View->getLayoutFileName(); $this->assertPathEquals($expected, $result); $View->setLayoutPath('rss'); $expected = TEST_APP . 'templates' . DS . 'layout' . DS . 'rss' . DS . 'default.php'; $result = $View->getLayoutFileName(); $this->assertPathEquals($expected, $result); $View->setLayoutPath('email' . DS . 'html'); $expected = TEST_APP . 'templates' . DS . 'layout' . DS . 'email' . DS . 'html' . DS . 'default.php'; $result = $View->getLayoutFileName(); $this->assertPathEquals($expected, $result); } /** * Test getting layout filenames for plugins. */ public function testGetLayoutFileNamePlugin(): void { $viewOptions = [ 'plugin' => null, 'name' => 'Pages', 'viewPath' => 'Pages', 'action' => 'display', ]; $View = new TestView(null, null, null, $viewOptions); $expected = TEST_APP . 'Plugin' . DS . 'TestPlugin' . DS . 'templates' . DS . 'layout' . DS . 'default.php'; $result = $View->getLayoutFileName('TestPlugin.default'); $this->assertPathEquals($expected, $result); $View->setRequest($View->getRequest()->withParam('plugin', 'TestPlugin')); $expected = TEST_APP . 'Plugin' . DS . 'TestPlugin' . DS . 'templates' . DS . 'layout' . DS . 'default.php'; $result = $View->getLayoutFileName('default'); $this->assertPathEquals($expected, $result); } /** * Test getting layout filenames for prefix */ public function testGetLayoutFileNamePrefix(): void { $View = new TestView(); // Prefix specific layout $View->setRequest($View->getRequest()->withParam('prefix', 'foo_prefix')); $expected = TEST_APP . 'templates' . DS . 'FooPrefix' . DS . 'layout' . DS . 'default.php'; $result = $View->getLayoutFileName(); $this->assertPathEquals($expected, $result); $View->setRequest($View->getRequest()->withParam('prefix', 'FooPrefix')); $result = $View->getLayoutFileName(); $this->assertPathEquals($expected, $result); // Nested prefix layout $View->setRequest($View->getRequest()->withParam('prefix', 'foo_prefix/bar_prefix')); $expected = TEST_APP . 'templates' . DS . 'FooPrefix' . DS . 'BarPrefix' . DS . 'layout' . DS . 'default.php'; $result = $View->getLayoutFileName(); $this->assertPathEquals($expected, $result); $expected = TEST_APP . 'templates' . DS . 'FooPrefix' . DS . 'layout' . DS . 'nested_prefix_cascade.php'; $result = $View->getLayoutFileName('nested_prefix_cascade'); $this->assertPathEquals($expected, $result); // Fallback to app's layout $View->setRequest($View->getRequest()->withParam('prefix', 'Admin')); $expected = TEST_APP . 'templates' . DS . 'layout' . DS . 'default.php'; $result = $View->getLayoutFileName(); $this->assertPathEquals($expected, $result); } /** * Test that getLayoutFileName() protects against malicious directory traversal. */ public function testGetLayoutFileNameDirectoryTraversal(): void { $this->expectException(InvalidArgumentException::class); $viewOptions = [ 'plugin' => null, 'name' => 'Pages', 'viewPath' => 'Pages', ]; $view = new TestView(null, null, null, $viewOptions); $view->ext('.php'); $view->getLayoutFileName('../../../bootstrap'); } /** * Test for missing views */ public function testMissingTemplate(): void { $this->expectException(MissingTemplateException::class); $this->expectExceptionMessage('Template file `does_not_exist.php` could not be found'); $this->expectExceptionMessage('The following paths were searched'); $this->expectExceptionMessage('- `' . ROOT . DS . 'templates' . DS . 'does_not_exist.php`'); $viewOptions = [ 'plugin' => null, 'name' => 'Pages', 'viewPath' => 'Pages', ]; $request = new ServerRequest(); $response = new Response(); $View = new TestView($request, $response, null, $viewOptions); $View->getTemplateFileName('does_not_exist'); } /** * Test for missing layouts */ public function testMissingLayout(): void { $this->expectException(MissingLayoutException::class); $this->expectExceptionMessage('Layout file `whatever.php` could not be found'); $this->expectExceptionMessage('The following paths were searched'); $this->expectExceptionMessage('- `' . ROOT . DS . 'templates' . DS . 'layout' . DS . 'whatever.php`'); $viewOptions = [ 'plugin' => null, 'name' => 'Pages', 'viewPath' => 'Pages', 'layout' => 'whatever', ]; $View = new TestView(null, null, null, $viewOptions); $View->getLayoutFileName(); } /** * Test viewVars method */ public function testViewVars(): void { $this->assertEquals(['testData', 'test2', 'test3'], $this->View->getVars()); } /** * Test elementExists method */ public function testElementExists(): void { $result = $this->View->elementExists('test_element'); $this->assertTrue($result); $result = $this->View->elementExists('TestPlugin.plugin_element'); $this->assertTrue($result); $result = $this->View->elementExists('nonexistent_element'); $this->assertFalse($result); $result = $this->View->elementExists('TestPlugin.element'); $this->assertFalse($result); $this->View->setRequest($this->View->getRequest()->withParam('plugin', 'TestPlugin')); $result = $this->View->elementExists('plugin_element'); $this->assertTrue($result); } /** * Test element method */ public function testElement(): void { $result = $this->View->element('test_element'); $this->assertSame('this is the test element', $result); $result = $this->View->element('TestPlugin.plugin_element'); $this->assertSame("Element in the TestPlugin\n", $result); $this->View->setRequest($this->View->getRequest()->withParam('plugin', 'TestPlugin')); $result = $this->View->element('plugin_element'); $this->assertSame("Element in the TestPlugin\n", $result); $result = $this->View->element('plugin_element', [], ['plugin' => false]); $this->assertSame("Plugin element overridden in app\n", $result); } /** * Test element method with a prefix */ public function testPrefixElement(): void { $this->View->setRequest($this->View->getRequest()->withParam('prefix', 'Admin')); $result = $this->View->element('prefix_element'); $this->assertSame('this is a prefixed test element', $result); $result = $this->View->element('TestPlugin.plugin_element'); $this->assertSame('this is the plugin prefixed element using params[plugin]', $result); $this->View->setRequest($this->View->getRequest()->withParam('plugin', 'TestPlugin')); $result = $this->View->element('test_plugin_element'); $this->assertSame('this is the test set using View::$plugin plugin prefixed element', $result); $this->View->setRequest($this->View->getRequest()->withParam('prefix', 'FooPrefix/BarPrefix')); $result = $this->View->element('prefix_element'); $this->assertSame('this is a nested prefixed test element', $result); $result = $this->View->element('prefix_element_in_parent'); $this->assertSame('this is a nested prefixed test element in first level element', $result); } /** * Test loading missing view element */ public function testElementMissing(): void { $this->expectException(MissingElementException::class); $this->expectExceptionMessage('Element file `nonexistent_element.php` could not be found'); $this->View->element('nonexistent_element'); } /** * Test loading nonexistent plugin view element */ public function testElementMissingPluginElement(): void { $this->expectException(MissingElementException::class); $this->expectExceptionMessage('Element file `TestPlugin.nope.php` could not be found'); $this->expectExceptionMessage(implode(DS, ['test_app', 'templates', 'plugin', 'TestPlugin', 'element', 'nope.php'])); $this->expectExceptionMessage(implode(DS, ['test_app', 'Plugin', 'TestPlugin', 'templates', 'element', 'nope.php'])); $this->View->element('TestPlugin.nope'); } /** * Test that elements can have callbacks */ public function testElementCallbacks(): void { $count = 0; $callback = function (EventInterface $event, $file) use (&$count): void { $count++; }; $events = $this->View->getEventManager(); $events->on('View.beforeRender', $callback); $events->on('View.afterRender', $callback); $this->View->element('test_element', [], ['callbacks' => true]); $this->assertSame(2, $count); } /** * Test that additional element viewVars don't get overwritten with helpers. */ public function testElementParamsDontOverwriteHelpers(): void { $Controller = new ViewPostsController(new ServerRequest()); $View = $Controller->createView(); $result = $View->element('type_check', ['form' => 'string'], ['callbacks' => true]); $this->assertSame('string', $result); $View->set('form', 'string'); $result = $View->element('type_check', [], ['callbacks' => true]); $this->assertSame('string', $result); } /** * Test elementCacheHelperNoCache method */ public function testElementCacheHelperNoCache(): void { $Controller = new ViewPostsController(new ServerRequest()); $View = $Controller->createView(); $result = $View->element('test_element', ['ram' => 'val', 'test' => ['foo', 'bar']]); $this->assertSame('this is the test element', $result); } /** * Test elementCache method */ public function testElementCache(): void { Cache::drop('test_view'); Cache::setConfig('test_view', [ 'engine' => 'File', 'duration' => '+1 day', 'path' => CACHE . 'views/', 'prefix' => '', ]); Cache::clear('test_view'); $View = $this->PostsController->createView(); $View->setElementCache('test_view'); $result = $View->element('test_element', [], ['cache' => true]); $expected = 'this is the test element'; $this->assertSame($expected, $result); $result = Cache::read('element__test_element', 'test_view'); $this->assertSame($expected, $result); $result = $View->element('test_element', ['param' => 'one', 'foo' => 'two'], ['cache' => true]); $this->assertSame($expected, $result); $result = Cache::read('element__test_element_param_foo', 'test_view'); $this->assertSame($expected, $result); $View->element('test_element', [ 'param' => 'one', 'foo' => 'two', ], [ 'cache' => ['key' => 'custom_key'], ]); $result = Cache::read('element_custom_key', 'test_view'); $this->assertSame($expected, $result); $View->setElementCache('default'); $View->element('test_element', [ 'param' => 'one', 'foo' => 'two', ], [ 'cache' => ['config' => 'test_view'], ]); $result = Cache::read('element__test_element_param_foo', 'test_view'); $this->assertSame($expected, $result); Cache::clear('test_view'); Cache::drop('test_view'); } /** * Test elementCache method with namespaces and subfolder */ public function testElementCacheSubfolder(): void { Cache::drop('test_view'); Cache::setConfig('test_view', [ 'engine' => 'File', 'duration' => '+1 day', 'path' => CACHE . 'views/', 'prefix' => '', ]); Cache::clear('test_view'); $View = $this->PostsController->createView(); $View->setElementCache('test_view'); $result = $View->element('subfolder/test_element', [], ['cache' => true]); $expected = 'this is the test element in subfolder'; $this->assertSame($expected, trim($result)); $result = Cache::read('element__subfolder_test_element', 'test_view'); $this->assertSame($expected, trim($result)); } /** * Test element events */ public function testViewEvent(): void { $View = $this->PostsController->createView(); $View->setTemplatePath($this->PostsController->getName()); $View->enableAutoLayout(false); $listener = new TestViewEventListenerInterface(); $View->getEventManager()->on($listener); $View->render('index'); $this->assertSame(View::TYPE_TEMPLATE, $listener->beforeRenderViewType); $this->assertSame(View::TYPE_TEMPLATE, $listener->afterRenderViewType); $this->assertSame($View->getCurrentType(), View::TYPE_TEMPLATE); $View->element('test_element', [], ['callbacks' => true]); $this->assertSame($View->getCurrentType(), View::TYPE_TEMPLATE); $this->assertSame(View::TYPE_ELEMENT, $listener->beforeRenderViewType); $this->assertSame(View::TYPE_ELEMENT, $listener->afterRenderViewType); } /** * Test adding helpers in initialize(). */ public function testAddHelper(): void { $View = new TestView(); $this->assertInstanceOf(HtmlHelper::class, $View->Html); $config = $View->Html->getConfig(); $this->assertSame('myval', $config['mykey']); } /** * Test loading helper using loadHelper(). */ public function testLoadHelper(): void { $View = new View(); $View->loadHelper('Html', ['foo' => 'bar']); $this->assertInstanceOf(HtmlHelper::class, $View->Html); $config = $View->Html->getConfig(); $this->assertSame('bar', $config['foo']); } /** * Test loading helper when duplicate. */ public function testLoadHelperDuplicate(): void { $View = new View(); $this->assertNotEmpty($View->loadHelper('Html', ['foo' => 'bar'])); try { $View->loadHelper('Html', ['test' => 'value']); $this->fail('No exception'); } catch (RuntimeException $e) { $this->assertStringContainsString('The `Html` alias has already been loaded', $e->getMessage()); } } /** * Test loadHelpers method */ public function testLoadHelpers(): void { $View = new View(null, null, null, [ 'helpers' => ['Html' => ['foo' => 'bar'], 'Form' => ['foo' => 'baz']], ]); $result = $View->loadHelpers(); $this->assertSame($View, $result); $this->assertInstanceOf(HtmlHelper::class, $View->Html, 'Object type is wrong.'); $this->assertInstanceOf(FormHelper::class, $View->Form, 'Object type is wrong.'); $config = $View->Html->getConfig(); $this->assertSame('bar', $config['foo']); $config = $View->Form->getConfig(); $this->assertSame('baz', $config['foo']); } /** * Test lazy loading helpers */ public function testLazyLoadHelpers(): void { $View = new View(); $this->assertInstanceOf(HtmlHelper::class, $View->Html, 'Object type is wrong.'); $this->assertInstanceOf(FormHelper::class, $View->Form, 'Object type is wrong.'); } /** * Test lazy loading helpers in plugins */ public function testLazyLoadHelpersPlugin(): void { $view = new View(null, null, null, ['plugin' => 'TestPlugin']); $this->assertEquals('hello other', $view->OtherHelper->hello()); $this->assertEquals('hello plugged, hello other', $view->PluggedHelper->hello()); } /** * Test manipulating class properties in initialize() */ public function testInitialize(): void { $View = new TestView(); $config = $View->Html->getConfig(); $this->assertSame('myval', $config['mykey']); } /** * Test the correct triggering of helper callbacks */ public function testHelperCallbackTriggering(): void { $View = $this->PostsController->createView(); $View->setTemplatePath($this->PostsController->getName()); $manager = $this->getMockBuilder(EventManager::class)->getMock(); $View->setEventManager($manager); $manager->expects($this->exactly(8)) ->method('dispatch') ->with( ...self::withConsecutive( [$this->callback(function (EventInterface $event) { return $event->getName() === 'View.beforeRender'; })], [$this->callback(function (EventInterface $event) { return $event->getName() === 'View.beforeRenderFile'; })], [$this->callback(function (EventInterface $event) { return $event->getName() === 'View.afterRenderFile'; })], [$this->callback(function (EventInterface $event) { return $event->getName() === 'View.afterRender'; })], [$this->callback(function (EventInterface $event) { return $event->getName() === 'View.beforeLayout'; })], [$this->callback(function (EventInterface $event) { return $event->getName() === 'View.beforeRenderFile'; })], [$this->callback(function (EventInterface $event) { return $event->getName() === 'View.afterRenderFile'; })], [$this->callback(function (EventInterface $event) { return $event->getName() === 'View.afterLayout'; })] ) ); $View->render('index'); } /** * Test beforeLayout method */ public function testBeforeLayout(): void { $this->PostsController->viewBuilder()->setHelpers([ 'TestBeforeAfter' => ['className' => TestBeforeAfterHelper::class], 'Html', ]); $View = $this->PostsController->createView(); $View->setTemplatePath($this->PostsController->getName()); $View->render('index'); $this->assertSame('Valuation', $View->helpers()->TestBeforeAfter->property); } /** * Test afterLayout method */ public function testAfterLayout(): void { $this->PostsController->viewBuilder()->setHelpers([ 'TestBeforeAfter' => ['className' => TestBeforeAfterHelper::class], 'Html', ]); $this->PostsController->set('variable', 'values'); $View = $this->PostsController->createView(); $View->setTemplatePath($this->PostsController->getName()); $content = 'This is my view output'; $result = $View->renderLayout($content, 'default'); $this->assertMatchesRegularExpression('/modified in the afterlife/', $result); $this->assertMatchesRegularExpression('/This is my view output/', $result); } /** * Test renderLoadHelper method */ public function testRenderLoadHelper(): void { $this->PostsController->viewBuilder()->addHelpers(['Form', 'Number']); $View = $this->PostsController->createView(TestView::class); $View->setTemplatePath($this->PostsController->getName()); $result = $View->render('index', false); $this->assertSame('posts index', $result); $attached = $View->helpers()->loaded(); // HtmlHelper is loaded in TestView::initialize() $this->assertEquals(['Form', 'Number', 'Html'], $attached); $this->PostsController->viewBuilder()->addHelpers( ['Html', 'Form', 'Number', 'TestPlugin.PluggedHelper'] ); $View = $this->PostsController->createView(TestView::class); $View->setTemplatePath($this->PostsController->getName()); $result = $View->render('index', false); $this->assertSame('posts index', $result); $attached = $View->helpers()->loaded(); $expected = ['Form', 'Number', 'Html', 'PluggedHelper']; $this->assertEquals($expected, $attached, 'Attached helpers are wrong.'); } /** * Test render method */ public function testRender(): void { $View = $this->PostsController->createView(TestView::class); $View->setTemplatePath($this->PostsController->getName()); $result = $View->render('index'); $this->assertMatchesRegularExpression("/\s*/", $result); $this->assertMatchesRegularExpression("/<div id=\"content\">\s*posts index\s*<\/div>/", $result); $this->assertMatchesRegularExpression("/<div id=\"content\">\s*posts index\s*<\/div>/", $result); $this->PostsController->viewBuilder()->addHelpers(['Html']); $this->PostsController->setRequest( $this->PostsController->getRequest()->withParam('action', 'index') ); Configure::write('Cache.check', true); $View = $this->PostsController->createView(TestView::class); $View->setTemplatePath($this->PostsController->getName()); $result = $View->render('index'); $this->assertMatchesRegularExpression("/<meta charset=\"utf-8\">\s*<title>/", $result); $this->assertMatchesRegularExpression("/<div id=\"content\">\s*posts index\s*<\/div>/", $result); } /** * Test that View::$view works */ public function testRenderUsingViewProperty(): void { $View = $this->PostsController->createView(TestView::class); $View->setTemplatePath($this->PostsController->getName()); $View->setTemplate('cache_form'); $this->assertSame('cache_form', $View->getTemplate()); $result = $View->render(); $this->assertMatchesRegularExpression('/Add User/', $result); } /** * Test that layout set from view file takes precedence over layout set * as argument to render(). */ public function testRenderUsingLayoutArgument(): void { $pdoException = $this->getMockBuilder(PDOException::class)->getMock(); $error = new QueryException('this is sql string', $pdoException); $exceptions = [$error]; $message = 'it works'; $trace = $error->getTrace(); $View = $this->PostsController->createView(TestView::class); $View->set(compact('error', 'exceptions', 'message', 'trace')); $View->setTemplatePath('Error'); $result = $View->render('pdo_error', 'error'); $this->assertMatchesRegularExpression('/this is sql string/', $result); $this->assertMatchesRegularExpression('/it works/', $result); } /** * Test renderLayout() */ public function testRenderLayout(): void { $View = $this->PostsController->createView(TestView::class); $result = $View->renderLayout('', 'ajax2'); $this->assertMatchesRegularExpression('/Ajax\!/', $result); } /** * Test render()ing a file in a subdir from a custom viewPath * in a plugin. */ public function testGetTemplateFileNameSubdirWithPluginAndViewPath(): void { $this->PostsController->setPlugin('TestPlugin'); $this->PostsController->setName('Posts'); /** @var \TestApp\View\TestView $View */ $View = $this->PostsController->createView(TestView::class); $View->setTemplatePath('element'); $pluginPath = TEST_APP . 'Plugin' . DS . 'TestPlugin' . DS; $result = $View->getTemplateFileName('sub_dir/sub_element'); $expected = $pluginPath . 'templates' . DS . 'element' . DS . 'sub_dir' . DS . 'sub_element.php'; $this->assertPathEquals($expected, $result); } public function testGetTemplateException(): void { $this->expectException(CakeException::class); $this->expectExceptionMessage('Template name not provided'); $view = new View(); $view->render(); } /** * Test that view vars can replace the local helper variables * and not overwrite the $this->Helper references */ public function testViewVarOverwritingLocalHelperVar(): void { $Controller = new ViewPostsController(new ServerRequest()); $Controller->set('html', 'I am some test html'); $View = $Controller->createView(); $View->setTemplatePath($Controller->getName()); $result = $View->render('helper_overwrite', false); $this->assertMatchesRegularExpression('/I am some test html/', $result); $this->assertMatchesRegularExpression('/Test link/', $result); } /** * Test getTemplateFileName method */ public function testViewFileName(): void { /** @var \TestApp\View\TestView $View */ $View = $this->PostsController->createView(TestView::class); $View->setTemplatePath('Posts'); $result = $View->getTemplateFileName('index'); $this->assertMatchesRegularExpression('/Posts(\/|\\\)index.php/', $result); $result = $View->getTemplateFileName('TestPlugin.index'); $this->assertMatchesRegularExpression('/Posts(\/|\\\)index.php/', $result); $result = $View->getTemplateFileName('/Pages/home'); $this->assertMatchesRegularExpression('/Pages(\/|\\\)home.php/', $result); $result = $View->getTemplateFileName('../element/test_element'); $this->assertMatchesRegularExpression('/element(\/|\\\)test_element.php/', $result); $expected = TEST_APP . 'templates' . DS . 'Posts' . DS . 'index.php'; $result = $View->getTemplateFileName('../Posts/index'); $this->assertPathEquals($expected, $result); } /** * Test creating a block with capturing output. */ public function testBlockCaptureOverwrite(): void { $result = $this->View->start('test'); $this->assertSame($this->View, $result); echo 'Block content'; $result = $this->View->end(); $this->assertSame($this->View, $result); $this->View->start('test'); echo 'New content'; $this->View->end(); $result = $this->View->fetch('test'); $this->assertSame('New content', $result); } /** * Test that blocks can be fetched inside a block with the same name */ public function testBlockExtend(): void { $this->View->start('test'); echo 'Block content'; $this->View->end(); $this->View->start('test'); echo $this->View->fetch('test'); echo 'New content'; $this->View->end(); $result = $this->View->fetch('test'); $this->assertSame('Block contentNew content', $result); } /** * Test creating a block with capturing output. */ public function testBlockCapture(): void { $this->View->start('test'); echo 'Block content'; $this->View->end(); $result = $this->View->fetch('test'); $this->assertSame('Block content', $result); } /** * Test appending to a block with capturing output. */ public function testBlockAppendCapture(): void { $this->View->start('test'); echo 'Content '; $this->View->end(); $result = $this->View->append('test'); $this->assertSame($this->View, $result); echo 'appended'; $this->View->end(); $result = $this->View->fetch('test'); $this->assertSame('Content appended', $result); } /** * Test setting a block's content. */ public function testBlockSet(): void { $result = $this->View->assign('test', 'Block content'); $this->assertSame($this->View, $result); $result = $this->View->fetch('test'); $this->assertSame('Block content', $result); } /** * Test resetting a block's content. */ public function testBlockReset(): void { $this->View->assign('test', ''); $result = $this->View->fetch('test', 'This should not be returned'); $this->assertSame('', $result); } /** * Test resetting a block's content with reset. */ public function testBlockResetFunc(): void { $this->View->assign('test', 'Block content'); $result = $this->View->fetch('test', 'This should not be returned'); $this->assertSame('Block content', $result); $result = $this->View->reset('test'); $this->assertSame($this->View, $result); $result = $this->View->fetch('test', 'This should not be returned'); $this->assertSame('', $result); } /** * Test checking a block's existence. */ public function testBlockExist(): void { $this->assertFalse($this->View->exists('test')); $this->View->assign('test', 'Block content'); $this->assertTrue($this->View->exists('test')); } /** * Test setting a block's content to null */ public function testBlockSetNull(): void { $this->View->assign('testWithNull', null); $result = $this->View->fetch('testWithNull'); $this->assertSame('', $result); } /** * Test setting a block's content to an object with __toString magic method */ public function testBlockSetObjectWithToString(): void { $objectWithToString = new TestObjectWithToString(); $this->View->assign('testWithObjectWithToString', $objectWithToString); $result = $this->View->fetch('testWithObjectWithToString'); $this->assertSame("I'm ObjectWithToString", $result); } /** * Test setting a block's content to an object without __toString magic method */ public function testBlockSetObjectWithoutToString(): void { $this->checkException( 'Object of class ' . TestObjectWithoutToString::class . ' could not be converted to string' ); $objectWithToString = new TestObjectWithoutToString(); $this->View->assign('testWithObjectWithoutToString', $objectWithToString); } /** * Test setting a block's content to a decimal */ public function testBlockSetDecimal(): void { $this->View->assign('testWithDecimal', 1.23456789); $result = $this->View->fetch('testWithDecimal'); $this->assertSame('1.23456789', $result); } /** * Data provider for block related tests. * * @return array */ public static function blockValueProvider(): array { return [ 'string' => ['A string value'], 'decimal' => [1.23456], 'object with __toString' => [new TestObjectWithToString()], ]; } /** * Test appending to a block with append. * * @param mixed $value Value */ #[DataProvider('blockValueProvider')] public function testBlockAppend($value): void { $this->View->assign('testBlock', 'Block'); $this->View->append('testBlock', $value); $result = $this->View->fetch('testBlock'); $this->assertSame('Block' . $value, $result); } /** * Test appending an object without __toString magic method to a block with append. */ public function testBlockAppendObjectWithoutToString(): void { $this->checkException( 'Object of class ' . TestObjectWithoutToString::class . ' could not be converted to string' ); $object = new TestObjectWithoutToString(); $this->View->assign('testBlock', 'Block '); $this->View->append('testBlock', $object); } /** * Test prepending to a block with prepend. * * @param mixed $value Value */ #[DataProvider('blockValueProvider')] public function testBlockPrepend($value): void { $this->View->assign('test', 'Block'); $result = $this->View->prepend('test', $value); $this->assertSame($this->View, $result); $result = $this->View->fetch('test'); $this->assertSame($value . 'Block', $result); } /** * Test prepending an object without __toString magic method to a block with prepend. */ public function testBlockPrependObjectWithoutToString(): void { $this->checkException( 'Object of class ' . TestObjectWithoutToString::class . ' could not be converted to string' ); $object = new TestObjectWithoutToString(); $this->View->assign('test', 'Block '); $this->View->prepend('test', $object); } /** * You should be able to append to undefined blocks. */ public function testBlockAppendUndefined(): void { $result = $this->View->append('test', 'Unknown'); $this->assertSame($this->View, $result); $result = $this->View->fetch('test'); $this->assertSame('Unknown', $result); } /** * You should be able to prepend to undefined blocks. */ public function testBlockPrependUndefined(): void { $this->View->prepend('test', 'Unknown'); $result = $this->View->fetch('test'); $this->assertSame('Unknown', $result); } /** * Test getting block names */ public function testBlocks(): void { $this->View->append('test', 'one'); $this->View->assign('test1', 'one'); $this->assertEquals(['test', 'test1'], $this->View->blocks()); } /** * Test that blocks can be nested. */ public function testNestedBlocks(): void { $this->View->start('first'); echo 'In first '; $this->View->start('second'); echo 'In second'; $this->View->end(); echo 'In first'; $this->View->end(); $this->assertSame('In first In first', $this->View->fetch('first')); $this->assertSame('In second', $this->View->fetch('second')); } /** * Test that starting the same block twice throws an exception */ public function testStartBlocksTwice(): void { try { $this->View->start('first'); $this->View->start('first'); $this->fail('No exception'); } catch (CakeException) { ob_end_clean(); $this->assertTrue(true); } } /** * Test that an exception gets thrown when you leave a block open at the end * of a view. */ public function testExceptionOnOpenBlock(): void { try { $this->View->render('open_block'); $this->fail('No exception'); } catch (LogicException $e) { ob_end_clean(); $this->assertStringContainsString('The `no_close` block was left open', $e->getMessage()); } } /** * Test nested extended views. */ public function testExtendNested(): void { $content = $this->View->render('nested_extends', false); $expected = <<<TEXT This is the second parent. This is the first parent. This is the first template. Sidebar Content. TEXT; $this->assertSame($expected, $content); } /** * Make sure that extending the current view with itself causes an exception */ public function testExtendSelf(): void { try { $this->View->render('extend_self', false); $this->fail('No exception'); } catch (LogicException $e) { $this->assertStringContainsString('cannot have templates extend themselves', $e->getMessage()); } } /** * Make sure that extending in a loop causes an exception */ public function testExtendLoop(): void { try { $this->View->render('extend_loop', false); $this->fail('No exception'); } catch (LogicException $e) { $this->assertStringContainsString('cannot have templates extend in a loop', $e->getMessage()); } } /** * Test extend() in an element and a view. */ public function testExtendElement(): void { $content = $this->View->render('extend_element', false); $expected = <<<TEXT Parent View. View content. Parent Element. Element content. TEXT; $this->assertSame($expected, $content); } /** * Test extend() in an element and a view. */ public function testExtendPrefixElement(): void { $this->View->setRequest($this->View->getRequest()->withParam('prefix', 'Admin')); $content = $this->View->render('extend_element', false); $expected = <<<TEXT Parent View. View content. Parent Element. Prefix Element content. TEXT; $this->assertSame($expected, $content); } /** * Extending an element which doesn't exist should throw a missing view exception */ public function testExtendMissingElement(): void { try { $this->View->render('extend_missing_element', false); $this->fail('No exception'); } catch (LogicException $e) { $this->assertStringContainsString('element', $e->getMessage()); } } /** * Test extend() preceded by an element() */ public function testExtendWithElementBeforeExtend(): void { $result = $this->View->render('extend_with_element', false); $expected = <<<TEXT Parent View. this is the test elementThe view TEXT; $this->assertSame($expected, $result); } /** * Test extend() preceded by an element() */ public function testExtendWithPrefixElementBeforeExtend(): void { $this->View->setRequest($this->View->getRequest()->withParam('prefix', 'Admin')); $this->View->disableAutoLayout(); $result = $this->View->render('extend_with_element'); $expected = <<<TEXT Parent View. this is the test prefix elementThe view TEXT; $this->assertSame($expected, $result); } /** * Tests that the buffers that are opened when evaluating a template * are being closed in case an exception happens. */ public function testBuffersOpenedDuringTemplateEvaluationAreBeingClosed(): void { $bufferLevel = ob_get_level(); $e = null; try { $this->View->element('exception_with_open_buffers'); } catch (Exception $e) { } $this->assertNotNull($e); $this->assertSame('Exception with open buffers', $e->getMessage()); $this->assertSame($bufferLevel, ob_get_level()); } /** * Tests that the buffers that are opened during block caching are * being closed in case an exception happens. */ public function testBuffersOpenedDuringBlockCachingAreBeingClosed(): void { Cache::drop('test_view'); Cache::setConfig('test_view', [ 'engine' => 'File', 'duration' => '+1 day', 'path' => CACHE . 'views/', 'prefix' => '', ]); Cache::clear('test_view'); $bufferLevel = ob_get_level(); $e = null; try { $this->View->cache(function (): void { ob_start(); throw new Exception('Exception with open buffers'); }, [ 'key' => __FUNCTION__, 'config' => 'test_view', ]); } catch (Exception $e) { } Cache::clear('test_view'); Cache::drop('test_view'); $this->assertNotNull($e); $this->assertSame('Exception with open buffers', $e->getMessage()); $this->assertSame($bufferLevel, ob_get_level()); } /** * Test memory leaks that existed in _paths at one point. */ public function testMemoryLeakInPaths(): void { $this->skipIf((bool)env('CODECOVERAGE'), 'Running coverage this causes this tests to fail sometimes.'); $this->ThemeController->setName('Posts'); $View = $this->ThemeController->createView(); $View->setTemplatePath('Posts'); $View->setLayout('whatever'); $View->setTheme('TestTheme'); $View->element('test_element'); $start = memory_get_usage(); for ($i = 0; $i < 10; $i++) { $View->element('test_element'); } $end = memory_get_usage(); $this->assertLessThanOrEqual($start + 15000, $end); } /** * Tests that a view block uses default value when not assigned and uses assigned value when it is */ public function testBlockDefaultValue(): void { $default = 'Default'; $result = $this->View->fetch('title', $default); $this->assertSame($default, $result); $expected = 'My Title'; $this->View->assign('title', $expected); $result = $this->View->fetch('title', $default); $this->assertSame($expected, $result); } /** * Tests that a view variable uses default value when not assigned and uses assigned value when it is */ public function testViewVarDefaultValue(): void { $default = 'Default'; $result = $this->View->get('title', $default); $this->assertSame($default, $result); $expected = 'Back to the Future'; $this->View->set('title', $expected); $result = $this->View->get('title', $default); $this->assertSame($expected, $result); } /** * Test the helpers() method. */ public function testHelpers(): void { $this->assertInstanceOf(HelperRegistry::class, $this->View->helpers()); $result = $this->View->helpers(); $this->assertSame($result, $this->View->helpers()); } /** * Test getTemplatePath() and setTemplatePath(). */ public function testGetSetTemplatePath(): void { $result = $this->View->setTemplatePath('foo'); $this->assertSame($this->View, $result); $templatePath = $this->View->getTemplatePath(); $this->assertSame($templatePath, 'foo'); } /** * Test getLayoutPath() and setLayoutPath(). */ public function testGetSetLayoutPath(): void { $result = $this->View->setLayoutPath('foo'); $this->assertSame($this->View, $result); $layoutPath = $this->View->getLayoutPath(); $this->assertSame($layoutPath, 'foo'); } /** * Test isAutoLayoutEnabled() and enableAutoLayout(). */ public function testAutoLayout(): void { $result = $this->View->enableAutoLayout(false); $this->assertSame($this->View, $result); $autoLayout = $this->View->isAutoLayoutEnabled(); $this->assertSame($autoLayout, false); $this->View->enableAutoLayout(); $autoLayout = $this->View->isAutoLayoutEnabled(); $this->assertSame($autoLayout, true); } /** * testDisableAutoLayout */ public function testDisableAutoLayout(): void { $this->assertTrue($this->View->isAutoLayoutEnabled()); $result = $this->View->disableAutoLayout(); $this->assertSame($this->View, $result); $this->assertFalse($this->View->isAutoLayoutEnabled()); } /** * Test getTheme() and setTheme(). */ public function testGetSetTheme(): void { $result = $this->View->setTheme('foo'); $this->assertSame($this->View, $result); $theme = $this->View->getTheme(); $this->assertSame($theme, 'foo'); } /** * Test getTemplate() and setTemplate(). */ public function testGetSetTemplate(): void { $result = $this->View->setTemplate('foo'); $this->assertSame($this->View, $result); $template = $this->View->getTemplate(); $this->assertSame($template, 'foo'); } /** * Test setLayout() and getLayout(). */ public function testGetSetLayout(): void { $result = $this->View->setLayout('foo'); $this->assertSame($this->View, $result); $layout = $this->View->getLayout(); $this->assertSame($layout, 'foo'); } /** * Test getName() and getPlugin(). */ public function testGetNamePlugin(): void { $this->assertSame('Posts', $this->View->getName()); $this->assertNull($this->View->getPlugin()); $this->assertSame($this->View, $this->View->setPlugin('TestPlugin')); $this->assertSame('TestPlugin', $this->View->getPlugin()); } /** * Somewhat pointless, but helps ensure BC for defaults. */ public function testContentType(): void { $this->assertSame('', $this->View->contentType()); } protected function checkException(string $message): void { $this->expectException(Error::class); $this->expectExceptionMessage($message); } }