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->getMockBuilder('yii\\base\\Module') ->setMethods(['fake']) ->setConstructorArgs(['console']) ->getMock(); $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. * @param array $config additional config. * @return array config array. */ protected function createCompressConfig(array $bundles, array $config = []) { static $classNumber = 0; $classNumber++; $className = $this->declareAssetBundleClass(['class' => 'AssetBundleAll' . $classNumber]); $baseUrl = '/test'; $config = ArrayHelper::merge($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. * @param array $config additional config parameters. * @throws \Exception on failure. */ protected function createCompressConfigFile($fileName, array $bundles, array $config = []) { $content = 'createCompressConfig($bundles, $config), 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 * @param string $fileBasePath base path for the created files, if not set [[testFilePath]] is used. * @throws \Exception on failure. */ protected function createAssetSourceFile($fileRelativeName, $content, $fileBasePath = null) { if ($fileBasePath === null) { $fileBasePath = $this->testFilePath; } $fileFullName = $fileBasePath . 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 * @param string $fileBasePath base path for the created files, if not set [[testFilePath]] */ protected function createAssetSourceFiles(array $files, $fileBasePath = null) { foreach ($files as $name => $content) { $this->createAssetSourceFile($name, $content, $fileBasePath); } } /** * 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', 'sourcePath' => null, 'basePath' => $this->testFilePath, 'baseUrl' => '', 'css' => [], 'js' => [], 'depends' => [], ], $config ); foreach ($config as $name => $value) { if (!in_array($name, ['namespace', 'class'])) { $config[$name] = VarDumper::export($value); } } $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->assertFileExists($configFileName, 'Unable to create config file template!'); $config = require $configFileName; $this->assertInternalType('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->assertFileExists($bundleFile, 'Unable to create output bundle file!'); $compressedBundleConfig = require $bundleFile; $this->assertInternalType('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->assertFileExists($compressedCssFileName, 'Unable to compress CSS files!'); $compressedJsFileName = $this->testAssetsBasePath . DIRECTORY_SEPARATOR . 'all.js'; $this->assertFileExists($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->assertFileExists($bundleFile, 'Unable to create output bundle file!'); $compressedBundleConfig = require $bundleFile; $this->assertInternalType('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->expectException('yii\console\Exception'); $this->expectExceptionMessage($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);}', ], [ 'img {clip-path: url(#xxx)}', '/test/base/path/css', '/test/base/path/assets/output', 'img {clip-path: url(#xxx)}', ], ]; } /** * @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); } /** * @depends testActionCompress * * @see https://github.com/yiisoft/yii2/issues/9708 */ public function testActionCompressDeleteSource() { // Given : $cssFiles = [ 'css/test_body.css' => 'body { padding-top: 20px; padding-bottom: 60px; }', ]; $this->createAssetSourceFiles($cssFiles); $jsFiles = [ 'js/test_alert.js' => "function test() { alert('Test message'); }", ]; $sourcePath = $this->testFilePath . DIRECTORY_SEPARATOR . 'source'; $this->createAssetSourceFiles($cssFiles, $sourcePath); $this->createAssetSourceFiles($jsFiles, $sourcePath); $assetBundleClassName = $this->declareAssetBundleClass([ 'class' => 'AssetDelete', 'css' => array_keys($cssFiles), 'js' => array_keys($jsFiles), 'basePath' => null, 'sourcePath' => $sourcePath, ]); $bundles = [ $assetBundleClassName, ]; $bundleFile = $this->testFilePath . DIRECTORY_SEPARATOR . 'bundle.php'; // Keep source : $configFile = $this->testFilePath . DIRECTORY_SEPARATOR . 'config_no_source_delete.php'; $this->createCompressConfigFile($configFile, $bundles, [ 'deleteSource' => false, ]); $this->runAssetControllerAction('compress', [$configFile, $bundleFile]); $files = FileHelper::findFiles($this->testAssetsBasePath, [ 'only' => [ 'test_body.css', 'test_alert.js', ], ]); $this->assertNotEmpty($files); // Delete source : $configFile = $this->testFilePath . DIRECTORY_SEPARATOR . 'config_source_delete.php'; $this->createCompressConfigFile($configFile, $bundles, [ 'deleteSource' => true, ]); $this->runAssetControllerAction('compress', [$configFile, $bundleFile]); $files = FileHelper::findFiles($this->testAssetsBasePath, [ 'only' => [ 'test_body.css', 'test_alert.js', ], ]); $this->assertEmpty($files); } /** * @depends testActionCompress * * @see https://github.com/yiisoft/yii2/issues/10567 */ public function testActionCompressOverrideAsExternal() { // Given : $cssFiles = [ 'css/override_external.css' => 'body { padding-top: 20px; padding-bottom: 60px; }', ]; $this->createAssetSourceFiles($cssFiles); $jsFiles = [ 'js/override_external.js' => "function test() { alert('Test message'); }", ]; //$this->createAssetSourceFiles($cssFiles, $sourcePath); //$this->createAssetSourceFiles($jsFiles, $sourcePath); $assetBundleClassName = $this->declareAssetBundleClass([ 'class' => 'AssetOverrideExternal', 'css' => array_keys($cssFiles), 'js' => array_keys($jsFiles), ]); $bundles = [ $assetBundleClassName, ]; $bundleFile = $this->testFilePath . DIRECTORY_SEPARATOR . 'bundle_override_as_external.php'; // Keep source : $configFile = $this->testFilePath . DIRECTORY_SEPARATOR . 'config_override_as_external.php'; $assetBundleOverrideConfig = [ 'sourcePath' => null, 'basePath' => null, 'baseUrl' => null, 'css' => [ '//some.cdn.com/js/override_external.css', ], 'js' => [ '//some.cdn.com/js/override_external.js', ], ]; $this->createCompressConfigFile($configFile, $bundles, [ 'assetManager' => [ 'bundles' => [ $assetBundleClassName => $assetBundleOverrideConfig, ], ], ]); $this->runAssetControllerAction('compress', [$configFile, $bundleFile]); $bundlesConfig = require $bundleFile; $this->assertEquals($assetBundleOverrideConfig['css'], $bundlesConfig[$assetBundleClassName]['css']); $this->assertEquals($assetBundleOverrideConfig['js'], $bundlesConfig[$assetBundleClassName]['js']); } } /** * Mock class for [[\yii\console\controllers\AssetController]]. */ class AssetControllerMock extends AssetController { use StdOutBufferControllerTrait; }