mockApplication(); $this->testFilePath = Yii::getAlias('@yiiunit/runtime') . DIRECTORY_SEPARATOR . str_replace('\\', '_', get_class($this)) . uniqid(); $this->createDir($this->testFilePath); $this->testAssetsBasePath = $this->testFilePath . DIRECTORY_SEPARATOR . 'assets'; $this->createDir($this->testAssetsBasePath); } public function tearDown() { $this->removeDir($this->testFilePath); } /** * Creates directory. * @param string $dirName directory full name. */ protected function createDir($dirName) { FileHelper::createDirectory($dirName); } /** * Removes directory. * @param string $dirName directory full name */ protected function removeDir($dirName) { if (!empty($dirName)) { FileHelper::removeDirectory($dirName); } } /** * Creates test asset controller instance. * @return AssetControllerMock */ protected function createAssetController() { $module = $this->getMock('yii\\base\\Module', ['fake'], ['console']); $assetController = new AssetControllerMock('asset', $module); $assetController->interactive = false; $assetController->jsCompressor = 'cp {from} {to}'; $assetController->cssCompressor = 'cp {from} {to}'; return $assetController; } /** * Emulates running of the asset controller action. * @param string $actionID id of action to be run. * @param array $args action arguments. * @return string command output. */ protected function runAssetControllerAction($actionID, array $args = []) { $controller = $this->createAssetController(); $controller->run($actionID, $args); return $controller->flushStdOutBuffer(); } /** * Creates test compress config. * @param array[] $bundles asset bundles config. * @return array config array. */ protected function createCompressConfig(array $bundles) { static $classNumber = 0; $classNumber++; $className = $this->declareAssetBundleClass(['class' => 'AssetBundleAll' . $classNumber]); $baseUrl = '/test'; $config = [ 'bundles' => $bundles, 'targets' => [ $className => [ 'basePath' => $this->testAssetsBasePath, 'baseUrl' => $baseUrl, 'js' => 'all.js', 'css' => 'all.css', ], ], 'assetManager' => [ 'basePath' => $this->testAssetsBasePath, 'baseUrl' => '', ], ]; return $config; } /** * Creates test compress config file. * @param string $fileName output file name. * @param array[] $bundles asset bundles config. * @throws \Exception on failure. */ protected function createCompressConfigFile($fileName, array $bundles) { $content = 'createCompressConfig($bundles), true) . ';'; if (file_put_contents($fileName, $content) <= 0) { throw new \Exception("Unable to create file '{$fileName}'!"); } } /** * Creates test asset file. * @param string $fileRelativeName file name relative to [[testFilePath]] * @param string $content file content * @throws \Exception on failure. */ protected function createAssetSourceFile($fileRelativeName, $content) { $fileFullName = $this->testFilePath . DIRECTORY_SEPARATOR . $fileRelativeName; $this->createDir(dirname($fileFullName)); if (file_put_contents($fileFullName, $content) <= 0) { throw new \Exception("Unable to create file '{$fileFullName}'!"); } } /** * Creates a list of asset source files. * @param array $files assert source files in format: file/relative/name => fileContent */ protected function createAssetSourceFiles(array $files) { foreach ($files as $name => $content) { $this->createAssetSourceFile($name, $content); } } /** * Invokes the asset controller method even if it is protected. * @param string $methodName name of the method to be invoked. * @param array $args method arguments. * @return mixed method invoke result. */ protected function invokeAssetControllerMethod($methodName, array $args = []) { $controller = $this->createAssetController(); $controllerClassReflection = new \ReflectionClass(get_class($controller)); $methodReflection = $controllerClassReflection->getMethod($methodName); $methodReflection->setAccessible(true); $result = $methodReflection->invokeArgs($controller, $args); $methodReflection->setAccessible(false); return $result; } /** * Composes asset bundle class source code. * @param array $config asset bundle config. * @return string class source code. */ protected function composeAssetBundleClassSource(array &$config) { $config = array_merge( [ 'namespace' => StringHelper::dirname(get_class($this)), 'class' => 'AppAsset', 'basePath' => $this->testFilePath, 'baseUrl' => '', 'css' => [], 'js' => [], 'depends' => [], ], $config ); foreach ($config as $name => $value) { if (is_array($value)) { $config[$name] = var_export($value, true); } } $source = <<composeAssetBundleClassSource($config); eval($sourceCode); return $config['namespace'] . '\\' . $config['class']; } // Tests : public function testActionTemplate() { $configFileName = $this->testFilePath . DIRECTORY_SEPARATOR . 'config.php'; $this->runAssetControllerAction('template', [$configFileName]); $this->assertTrue(file_exists($configFileName), 'Unable to create config file template!'); $config = require($configFileName); $this->assertTrue(is_array($config), 'Invalid config created!'); } public function testActionCompress() { // Given : $cssFiles = [ 'css/test_body.css' => 'body { padding-top: 20px; padding-bottom: 60px; }', 'css/test_footer.css' => '.footer { margin: 20px; display: block; }', ]; $this->createAssetSourceFiles($cssFiles); $jsFiles = [ 'js/test_alert.js' => "function test() { alert('Test message'); }", 'js/test_sum_ab.js' => "function sumAB(a, b) { return a + b; }", ]; $this->createAssetSourceFiles($jsFiles); $assetBundleClassName = $this->declareAssetBundleClass([ 'css' => array_keys($cssFiles), 'js' => array_keys($jsFiles), ]); $bundles = [ $assetBundleClassName ]; $bundleFile = $this->testFilePath . DIRECTORY_SEPARATOR . 'bundle.php'; $configFile = $this->testFilePath . DIRECTORY_SEPARATOR . 'config2.php'; $this->createCompressConfigFile($configFile, $bundles); // When : $this->runAssetControllerAction('compress', [$configFile, $bundleFile]); // Then : $this->assertTrue(file_exists($bundleFile), 'Unable to create output bundle file!'); $compressedBundleConfig = require($bundleFile); $this->assertTrue(is_array($compressedBundleConfig), 'Output bundle file has incorrect format!'); $this->assertCount(2, $compressedBundleConfig, 'Output bundle config contains wrong bundle count!'); $this->assertArrayHasKey($assetBundleClassName, $compressedBundleConfig, 'Source bundle is lost!'); $compressedAssetBundleConfig = $compressedBundleConfig[$assetBundleClassName]; $this->assertEmpty($compressedAssetBundleConfig['css'], 'Compressed bundle css is not empty!'); $this->assertEmpty($compressedAssetBundleConfig['js'], 'Compressed bundle js is not empty!'); $this->assertNotEmpty($compressedAssetBundleConfig['depends'], 'Compressed bundle dependency is invalid!'); $compressedCssFileName = $this->testAssetsBasePath . DIRECTORY_SEPARATOR . 'all.css'; $this->assertTrue(file_exists($compressedCssFileName), 'Unable to compress CSS files!'); $compressedJsFileName = $this->testAssetsBasePath . DIRECTORY_SEPARATOR . 'all.js'; $this->assertTrue(file_exists($compressedJsFileName), 'Unable to compress JS files!'); $compressedCssFileContent = file_get_contents($compressedCssFileName); foreach ($cssFiles as $name => $content) { $this->assertContains($content, $compressedCssFileContent, "Source of '{$name}' is missing in combined file!"); } $compressedJsFileContent = file_get_contents($compressedJsFileName); foreach ($jsFiles as $name => $content) { $this->assertContains($content, $compressedJsFileContent, "Source of '{$name}' is missing in combined file!"); } } /** * @depends testActionCompress * * @see https://github.com/yiisoft/yii2/issues/5194 */ public function testCompressExternalAsset() { // Given : $externalAssetConfig = [ 'class' => 'ExternalAsset', 'sourcePath' => null, 'basePath' => null, 'js' => [ '//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js', ], 'css' => [ '//ajax.googleapis.com/css/libs/jquery/2.1.1/jquery.ui.min.css' ], ]; $externalAssetBundleClassName = $this->declareAssetBundleClass($externalAssetConfig); $cssFiles = [ 'css/test.css' => 'body { padding-top: 20px; padding-bottom: 60px; }', ]; $this->createAssetSourceFiles($cssFiles); $jsFiles = [ 'js/test.js' => "function test() { alert('Test message'); }", ]; $this->createAssetSourceFiles($jsFiles); $regularAssetBundleClassName = $this->declareAssetBundleClass([ 'class' => 'RegularAsset', 'css' => array_keys($cssFiles), 'js' => array_keys($jsFiles), 'depends' => [ $externalAssetBundleClassName ], ]); $bundles = [ $regularAssetBundleClassName ]; $bundleFile = $this->testFilePath . DIRECTORY_SEPARATOR . 'bundle.php'; $configFile = $this->testFilePath . DIRECTORY_SEPARATOR . 'config.php'; $this->createCompressConfigFile($configFile, $bundles); // When : $this->runAssetControllerAction('compress', [$configFile, $bundleFile]); // Then : $this->assertTrue(file_exists($bundleFile), 'Unable to create output bundle file!'); $compressedBundleConfig = require($bundleFile); $this->assertTrue(is_array($compressedBundleConfig), 'Output bundle file has incorrect format!'); $this->assertArrayHasKey($externalAssetBundleClassName, $compressedBundleConfig, 'External bundle is lost!'); $compressedExternalAssetConfig = $compressedBundleConfig[$externalAssetBundleClassName]; $this->assertEquals($externalAssetConfig['js'], $compressedExternalAssetConfig['js'], 'External bundle js is lost!'); $this->assertEquals($externalAssetConfig['css'], $compressedExternalAssetConfig['css'], 'External bundle css is lost!'); $compressedRegularAssetConfig = $compressedBundleConfig[$regularAssetBundleClassName]; $this->assertContains($externalAssetBundleClassName, $compressedRegularAssetConfig['depends'], 'Dependency on external bundle is lost!'); } /** * @depends testActionCompress * * @see https://github.com/yiisoft/yii2/issues/7539 */ public function testDetectCircularDependency() { // Given : $namespace = __NAMESPACE__; $this->declareAssetBundleClass([ 'namespace' => $namespace, 'class' => 'AssetStart', 'depends' => [ $namespace . '\AssetA' ], ]); $this->declareAssetBundleClass([ 'namespace' => $namespace, 'class' => 'AssetA', 'depends' => [ $namespace . '\AssetB' ], ]); $this->declareAssetBundleClass([ 'namespace' => $namespace, 'class' => 'AssetB', 'depends' => [ $namespace . '\AssetC' ], ]); $this->declareAssetBundleClass([ 'namespace' => $namespace, 'class' => 'AssetC', 'depends' => [ $namespace . '\AssetA' ], ]); $bundles = [ $namespace . '\AssetStart' ]; $bundleFile = $this->testFilePath . DIRECTORY_SEPARATOR . 'bundle.php'; $configFile = $this->testFilePath . DIRECTORY_SEPARATOR . 'config.php'; $this->createCompressConfigFile($configFile, $bundles); // Assert : $expectedExceptionMessage = ": {$namespace}\AssetA -> {$namespace}\AssetB -> {$namespace}\AssetC -> {$namespace}\AssetA"; $this->setExpectedException('yii\console\Exception', $expectedExceptionMessage); // When : $this->runAssetControllerAction('compress', [$configFile, $bundleFile]); } /** * Data provider for [[testAdjustCssUrl()]]. * @return array test data. */ public function adjustCssUrlDataProvider() { return [ [ '.published-same-dir-class {background-image: url(published_same_dir.png);}', '/test/base/path/assets/input', '/test/base/path/assets/output', '.published-same-dir-class {background-image: url(../input/published_same_dir.png);}', ], [ '.published-relative-dir-class {background-image: url(../img/published_relative_dir.png);}', '/test/base/path/assets/input', '/test/base/path/assets/output', '.published-relative-dir-class {background-image: url(../img/published_relative_dir.png);}', ], [ '.static-same-dir-class {background-image: url(\'static_same_dir.png\');}', '/test/base/path/css', '/test/base/path/assets/output', '.static-same-dir-class {background-image: url(\'../../css/static_same_dir.png\');}', ], [ '.static-relative-dir-class {background-image: url("../img/static_relative_dir.png");}', '/test/base/path/css', '/test/base/path/assets/output', '.static-relative-dir-class {background-image: url("../../img/static_relative_dir.png");}', ], [ '.absolute-url-class {background-image: url(http://domain.com/img/image.gif);}', '/test/base/path/assets/input', '/test/base/path/assets/output', '.absolute-url-class {background-image: url(http://domain.com/img/image.gif);}', ], [ '.absolute-url-secure-class {background-image: url(https://secure.domain.com/img/image.gif);}', '/test/base/path/assets/input', '/test/base/path/assets/output', '.absolute-url-secure-class {background-image: url(https://secure.domain.com/img/image.gif);}', ], [ "@font-face { src: url('../fonts/glyphicons-halflings-regular.eot'); src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'); }", '/test/base/path/assets/input/css', '/test/base/path/assets/output', "@font-face { src: url('../input/fonts/glyphicons-halflings-regular.eot'); src: url('../input/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'); }", ], [ "@font-face { src: url('../fonts/glyphicons-halflings-regular.eot'); src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'); }", '/test/base/path/assets/input/css', '/test/base/path/assets', "@font-face { src: url('input/fonts/glyphicons-halflings-regular.eot'); src: url('input/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'); }", ], [ "@font-face { src: url(data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT==) format('truetype'); }", '/test/base/path/assets/input/css', '/test/base/path/assets/output', "@font-face { src: url(data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT==) format('truetype'); }", ], [ '.published-same-dir-class {background-image: url(published_same_dir.png);}', 'C:\test\base\path\assets\input', 'C:\test\base\path\assets\output', '.published-same-dir-class {background-image: url(../input/published_same_dir.png);}', ], [ '.static-root-relative-class {background-image: url(\'/images/static_root_relative.png\');}', '/test/base/path/css', '/test/base/path/assets/output', '.static-root-relative-class {background-image: url(\'/images/static_root_relative.png\');}', ], [ '.published-relative-dir-class {background-image: url(../img/same_relative_dir.png);}', '/test/base/path/assets/css', '/test/base/path/assets/css', '.published-relative-dir-class {background-image: url(../img/same_relative_dir.png);}', ], ]; } /** * @dataProvider adjustCssUrlDataProvider * * @param $cssContent * @param $inputFilePath * @param $outputFilePath * @param $expectedCssContent */ public function testAdjustCssUrl($cssContent, $inputFilePath, $outputFilePath, $expectedCssContent) { $adjustedCssContent = $this->invokeAssetControllerMethod('adjustCssUrl', [$cssContent, $inputFilePath, $outputFilePath]); $this->assertEquals($expectedCssContent, $adjustedCssContent, 'Unable to adjust CSS correctly!'); } /** * Data provider for [[testFindRealPath()]] * @return array test data */ public function findRealPathDataProvider() { return [ [ '/linux/absolute/path', '/linux/absolute/path', ], [ '/linux/up/../path', '/linux/path', ], [ '/linux/twice/up/../../path', '/linux/path', ], [ '/linux/../mix/up/../path', '/mix/path', ], [ 'C:\\windows\\absolute\\path', 'C:\\windows\\absolute\\path', ], [ 'C:\\windows\\up\\..\\path', 'C:\\windows\\path', ], ]; } /** * @dataProvider findRealPathDataProvider * * @param string $sourcePath * @param string $expectedRealPath */ public function testFindRealPath($sourcePath, $expectedRealPath) { $expectedRealPath = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $expectedRealPath); $realPath = $this->invokeAssetControllerMethod('findRealPath', [$sourcePath]); $this->assertEquals($expectedRealPath, $realPath); } } /** * Mock class for [[\yii\console\controllers\AssetController]] */ class AssetControllerMock extends AssetController { use StdOutBufferControllerTrait; }