diff --git a/core/core.services.yml b/core/core.services.yml index 735bd23..d5539a1 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -700,6 +700,14 @@ services: asset.css.collection_optimizer: class: Drupal\Core\Asset\CssCollectionOptimizer arguments: [ '@asset.css.collection_grouper', '@asset.css.optimizer', '@asset.css.dumper', '@state' ] + asset.css.collection_optimizer_nv: + class: Drupal\Core\Asset\Optimize\CssCollectionOptimizer + arguments: [ '@asset.css.collection_aggregator', '@asset.css.optimizer', '@asset.css.dumper', '@state' ] + asset.css.collection_aggregator: + class: Drupal\Core\Asset\Optimize\CssCollectionAggregator + arguments: [ '@asset.css.graph_sorter' ] + asset.css.graph_sorter: + class: Drupal\Core\Asset\GroupSort\CssGraphSorter asset.css.optimizer: class: Drupal\Core\Asset\CssOptimizer asset.css.collection_grouper: @@ -718,5 +726,14 @@ services: class: Drupal\Core\Asset\JsCollectionGrouper asset.js.dumper: class: Drupal\Core\Asset\AssetDumper + asset.library_factory: + class: Drupal\Core\Asset\Factory\AssetLibraryFactory + arguments: ['@module_handler'] + asset.library_repository: + class: Drupal\Core\Asset\AssetLibraryRepository + arguments: ['@asset.library_factory'] + asset.css.collection_renderer_nv: + class: Drupal\Core\Asset\Render\CssCollectionRenderer + arguments: [ '@state', '@asset.css.graph_sorter' ] info_parser: class: Drupal\Core\Extension\InfoParser diff --git a/core/includes/common.inc b/core/includes/common.inc index e523b1b..ce282ef 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -8,6 +8,7 @@ use Drupal\Component\Utility\Tags; use Drupal\Component\Utility\Url; use Drupal\Component\Utility\Xss; +use Drupal\Core\Asset\Optimize\CssCollectionOptimizer; use Drupal\Core\Cache\Cache; use Drupal\Core\Language\Language; use Symfony\Component\HttpFoundation\Response; @@ -1526,7 +1527,7 @@ function drupal_add_html_head_link($attributes, $header = FALSE) { * * @see drupal_get_css() */ -function _drupal_add_css($data = NULL, $options = NULL) { +function _drupal_add_css($data = NULL, $options = NULL, $collect = TRUE) { $css = &drupal_static(__FUNCTION__, array()); // Construct the options, taking the defaults into consideration. @@ -1542,6 +1543,11 @@ function _drupal_add_css($data = NULL, $options = NULL) { // Create an array of CSS files for each media type first, since each type needs to be served // to the browser differently. if (isset($data)) { + $options['type'] = isset($options['type']) ? $options['type'] : 'file'; + if ($collect) { + drupal_collect_assets($data, $options, 'css'); + } + $options += array( 'type' => 'file', 'group' => CSS_AGGREGATE_DEFAULT, @@ -1593,6 +1599,35 @@ function _drupal_add_css($data = NULL, $options = NULL) { return $css; } +function drupal_collect_assets($data, $options, $type = '') { + $collection = &drupal_static('global_asset_collection', FALSE); + $collector = &drupal_static('global_asset_collector', FALSE); + + $collection = ($collection instanceof \Drupal\Core\Asset\Collection\AssetCollection) ? $collection : new \Drupal\Core\Asset\Collection\AssetCollection(); + if (!$collector instanceof \Drupal\Core\Asset\Factory\AssetCollector) { + $collector = new \Drupal\Core\Asset\Factory\AssetCollector($collection); + } + + if ($data instanceof \Drupal\Core\Asset\AssetInterface) { + $collector->add($data); + return; + } + + if ($type == 'js-setting') { + // TODO handle js settings + return; + } + + if ($type == 'library') { + $collection->addUnresolvedLibrary($data); + return; + } + + // TODO: simplify + // $type is 'css' or 'js', $options['type'] is 'file', 'external' or 'inline'. + $collector->create($type, $options['type'], $data, $options); +} + /** * Returns a themed representation of all stylesheets to attach to the page. * @@ -1773,11 +1808,23 @@ function drupal_sort_css_js($a, $b) { function drupal_pre_render_styles($elements) { $css_assets = $elements['#items']; + $collection = &drupal_static('global_asset_collection', FALSE); + + /** @var $collection \Drupal\Core\Asset\Collection\AssetCollectionInterface */ + $collection->resolveLibraries(\Drupal::service('asset.library_repository')); + $collection->ksort(); + + $sorter = \Drupal::service('asset.css.graph_sorter'); + $sorted_css = $sorter->groupAndSort($collection); + $rendered = \Drupal::service('asset.css.collection_renderer_nv')->render($sorted_css); + // Aggregate the CSS if necessary, but only during normal site operation. if (!defined('MAINTENANCE_MODE') && \Drupal::config('system.performance')->get('css.preprocess')) { $css_assets = \Drupal::service('asset.css.collection_optimizer')->optimize($css_assets); + return \Drupal::service('asset.css.collection_renderer')->render($css_assets); } - return \Drupal::service('asset.css.collection_renderer')->render($css_assets); + + return $rendered; } /** @@ -2113,7 +2160,7 @@ function drupal_html_id($id) { * * @see drupal_get_js() */ -function _drupal_add_js($data = NULL, $options = NULL) { +function _drupal_add_js($data = NULL, $options = NULL, $collect = TRUE) { $javascript = &drupal_static(__FUNCTION__, array()); // Construct the options, taking the defaults into consideration. @@ -2127,6 +2174,13 @@ function _drupal_add_js($data = NULL, $options = NULL) { } $options += drupal_js_defaults($data); + if (isset($data) && is_array($options)) { + $options['type'] = isset($options['type']) ? $options['type'] : 'file'; + if ($collect) { + drupal_collect_assets($data, $options, $options['type'] == 'setting' ? 'js-setting' : 'js'); + } + } + // Preprocess can only be set if caching is enabled and no attributes are set. $options['preprocess'] = $options['cache'] && empty($options['attributes']) ? $options['preprocess'] : FALSE; @@ -2490,7 +2544,17 @@ function drupal_pre_render_scripts($elements) { * @see _drupal_add_css() * @see drupal_render() */ -function drupal_process_attached($elements, $dependency_check = FALSE) { +function drupal_process_attached($elements, $dependency_check = FALSE, $collect = TRUE) { + // TODO duplicates the start of drupal_collect_assets()...just for now. + $collection = &drupal_static('global_asset_collection', FALSE); + $collector = &drupal_static('global_asset_collector', FALSE); + + $collection = ($collection instanceof \Drupal\Core\Asset\Collection\AssetCollection) ? $collection : new \Drupal\Core\Asset\Collection\AssetCollection(); + if (!$collector instanceof \Drupal\Core\Asset\Factory\AssetCollector) { + $collector = new \Drupal\Core\Asset\Factory\AssetCollector(); + $collector->setCollection($collection); + } + // Add defaults to the special attached structures that should be processed differently. $elements['#attached'] += array( 'library' => array(), @@ -2511,6 +2575,9 @@ function drupal_process_attached($elements, $dependency_check = FALSE) { } unset($elements['#attached']['library']); + // Clear the last CSS before we start to avoid unintentional relative positioning + $collector->clearLastCss(); + // Add both the JavaScript and the CSS. // The parameters for _drupal_add_js() and _drupal_add_css() require special // handling. @@ -2528,10 +2595,12 @@ function drupal_process_attached($elements, $dependency_check = FALSE) { $data = $options['data']; unset($options['data']); } - call_user_func('_drupal_add_' . $type, $data, $options); + call_user_func('_drupal_add_' . $type, $data, $options, $collect); } unset($elements['#attached'][$type]); } + // Clear the last CSS asset, ending the auto-predecessor-creation chain. + $collector->clearLastCss(); // Add additional types of attachments specified in the render() structure. // Libraries, JavaScript and CSS have been added already, as they require @@ -2705,6 +2774,7 @@ function drupal_process_states(&$elements) { */ function drupal_add_library($module, $name, $every_page = NULL) { $added = &drupal_static(__FUNCTION__, array()); + drupal_collect_assets("$module/$name", NULL, 'library'); // Only process the library if it exists and it was not added already. if (!isset($added[$module][$name])) { @@ -2728,7 +2798,7 @@ function drupal_add_library($module, $name, $every_page = NULL) { } } - $added[$module][$name] = drupal_process_attached($elements, TRUE); + $added[$module][$name] = drupal_process_attached($elements, TRUE, FALSE); } else { // Requested library does not exist. diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 0a57f16..b0a5148 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -455,10 +455,16 @@ function install_begin_request(&$install_state) { CoreServiceProvider::registerUuid($container); // Register the CSS and JavaScript asset collection renderers. - $container->register('asset.css.collection_renderer', 'Drupal\Core\Asset\CssCollectionRenderer') - ->addArgument(new Reference('state')); $container->register('asset.js.collection_renderer', 'Drupal\Core\Asset\JsCollectionRenderer') ->addArgument(new Reference('state')); + $container->register('asset.library_factory', 'Drupal\Core\Asset\Factory\AssetLibraryFactory') + ->addArgument(new Reference('module_handler')); + $container->register('asset.library_repository', 'Drupal\Core\Asset\AssetLibraryRepository') + ->addArgument(new Reference('asset.library_factory')); + $container->register('asset.css.graph_sorter', 'Drupal\Core\Asset\GroupSort\CssGraphSorter'); + $container->register('asset.css.collection_renderer_nv', 'Drupal\Core\Asset\Optimize\CssCollectionRenderer') + ->addArgument(new Reference('state')) + ->addArgument(new Reference('asset.css.graph_sorter')); // Register the info parser. $container->register('info_parser', 'Drupal\Core\Extension\InfoParser'); diff --git a/core/lib/Drupal/Component/ObjectState/FreezableInterface.php b/core/lib/Drupal/Component/ObjectState/FreezableInterface.php new file mode 100644 index 0000000..60d11f9 --- /dev/null +++ b/core/lib/Drupal/Component/ObjectState/FreezableInterface.php @@ -0,0 +1,31 @@ +_tfrozen = TRUE; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function isFrozen() { + return $this->_tfrozen; + } + + /** + * Checks if the asset collection is frozen, throws an exception if it is. + * + * @param string $method + * The name of the method that was originally called. + * + * @throws FrozenObjectException + */ + protected function attemptWrite($method) { + if ($this->isFrozen()) { + throw new FrozenObjectException(sprintf('State-changing method %s::%s called on a frozen object instance.', __CLASS__, $method)); + } + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Component/ObjectState/FrozenObjectException.php b/core/lib/Drupal/Component/ObjectState/FrozenObjectException.php new file mode 100644 index 0000000..85f0154 --- /dev/null +++ b/core/lib/Drupal/Component/ObjectState/FrozenObjectException.php @@ -0,0 +1,14 @@ +metadata = $metadata; + $this->_bcinit(); + + parent::__construct($assets); + } + + /** + * {@inheritdoc} + */ + public function id() { + if (empty($this->id)) { + $this->calculateId(); + } + + return $this->id; + } + + /** + * {@inheritdoc} + */ + public function getAssetType() { + return $this->metadata->getType(); + } + + /** + * Calculates and stores an id for this aggregate from the contained assets. + * + * @return void + */ + protected function calculateId() { + $id = ''; + foreach ($this->eachLeaf() as $asset) { + $id .= $asset->id(); + } + // TODO come up with something stabler/more serialization friendly than object hash + $this->id = hash('sha256', $id ?: spl_object_hash($this)); + } + + /** + * {@inheritdoc} + */ + public function getMetadata() { + // TODO should this immutable? doable if we further granulate the interfaces + return $this->metadata; + } + + /** + * {@inheritdoc} + */ + public function removeLeaf(AsseticAssetInterface $needle, $graceful = FALSE) { + if (!$needle instanceof AssetInterface) { + throw new UnsupportedAsseticBehaviorException('Vanilla Assetic asset provided; Drupal aggregates require Drupal-flavored assets.'); + } + + return $this->doRemove($needle, $graceful); + } + + /** + * {@inheritdoc} + */ + public function replaceLeaf(AsseticAssetInterface $needle, AsseticAssetInterface $replacement, $graceful = FALSE) { + if (!($needle instanceof AssetInterface && $replacement instanceof AssetInterface)) { + throw new UnsupportedAsseticBehaviorException('Vanilla Assetic asset(s) provided; Drupal aggregates require Drupal-flavored assets.'); + } + + $this->ensureCorrectType($replacement); + if ($this->contains($replacement)) { + throw new \LogicException('Asset to be swapped in is already present in the collection.'); + } + + return $this->doReplace($needle, $replacement, $graceful); + } + + /** + * {@inheritdoc} + */ + protected function ensureCorrectType(AssetInterface $asset) { + if ($asset->getAssetType() != $this->getAssetType()) { + throw new AssetTypeMismatchException(sprintf('Aggregate/asset incompatibility, aggregate of type "%s", asset of type "%s". Aggregates and their contained assets must be of the same type.', $this->getAssetType(), $asset->getAssetType())); + } + } + + /** + * {@inheritdoc} + * + * Aggregate assets are inherently eligible for preprocessing, so this is + * always true. + */ + public function isPreprocessable() { + return TRUE; + } +} diff --git a/core/lib/Drupal/Core/Asset/Aggregate/AggregateAssetInterface.php b/core/lib/Drupal/Core/Asset/Aggregate/AggregateAssetInterface.php new file mode 100644 index 0000000..fb98e38 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Aggregate/AggregateAssetInterface.php @@ -0,0 +1,38 @@ +factory = $factory; + } + + /** + * Gets a library by its composite key. + * + * @param string $key + * The key of the library, as a string of the form "$module/$name". + * + * @return \Drupal\Core\Asset\Collection\AssetLibrary + * The requested library. + * + * @throws \OutOfBoundsException + * Thrown if no library can be found with the given key. + */ + public function get($key) { + if ($this->has($key)) { + return $this->libraries[$key]; + } + + if ($library = $this->factory->getLibrary($key)) { + $this->set($key, $library); + } + else { + throw new \OutOfBoundsException(sprintf('No library could be found with the key "%s".', $key)); + } + + return $this->libraries[$key]; + } + + public function set($key, AssetLibrary $library) { + if (!preg_match('/^[0-9A-Za-z_]*\/[0-9A-Za-z._-]*$/', $key)) { + throw new \InvalidArgumentException(sprintf('The name "%s" is invalid.', $key)); + } + + $this->libraries[$key] = $library; + } + + /** + * Checks if the current library repository contains a certain library. + * + * Note that this does not verify whether or not such a library could be + * created from declarations elsewhere in the system - only if it HAS been + * created already. + * + * @param string $key + * The key of the library, as a string of the form "$module/$name". + * + * @return bool + * TRUE if the library has been built, FALSE otherwise. + */ + public function has($key) { + return isset($this->libraries[$key]); + } + + /** + * Resolves declared dependencies into an array of library objects. + * + * @param \Drupal\Core\Asset\DependencyInterface $asset + * The asset whose dependencies should be resolved. + * + * @param bool $attach + * Whether to automatically attach resolved dependencies to the given asset. + * + * @return \Drupal\Core\Asset\Collection\AssetLibrary[] + * An array of AssetLibraryInterface objects if any dependencies were found; + * otherwise, an empty array. + */ + public function resolveDependencies(DependencyInterface $asset, $attach = TRUE) { + $dependencies = array(); + + if ($asset->hasDependencies()) { + foreach ($asset->getDependencyInfo() as $key) { + $dependencies[] = $library = $this->get($key); + + // Only auto-attach if the argument is capable of it. + if ($attach && $asset instanceof RelativePositionInterface) { + foreach ($library as $libasset) { + // If operating on a proper AssetInterface object, only attach if + // the dependency and the given asset are of the same type. + if ($asset instanceof AssetInterface && + $asset->getAssetType() !== $libasset->getAssetType()) { + continue; + } + + $asset->after($libasset); + } + } + } + } + + return $dependencies; + } + + /** + * Returns an array of library names. + * + * @return array + * An array of library names. + */ + public function getNames() { + return array_keys($this->libraries); + } + + /** + * Clears all libraries. + */ + public function clear() { + $this->libraries = array(); + } + +} diff --git a/core/lib/Drupal/Core/Asset/AsseticAdapterTrait.php b/core/lib/Drupal/Core/Asset/AsseticAdapterTrait.php new file mode 100644 index 0000000..5ef6386 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/AsseticAdapterTrait.php @@ -0,0 +1,45 @@ +metadata = $metadata; + parent::__construct($filters, $sourceRoot, $sourcePath); + } + + public function __clone() { + parent::__clone(); + $this->metadata = clone $this->metadata; + } + + /** + * {@inheritdoc} + */ + public function getMetadata() { + return $this->metadata; + } + + /** + * {@inheritdoc} + */ + public function getAssetType() { + return $this->metadata->getType(); + } + + /** + * {@inheritdoc} + */ + public function isPreprocessable() { + return (bool) $this->metadata->get('preprocess'); + } + +} + diff --git a/core/lib/Drupal/Core/Asset/Collection/AssetCollection.php b/core/lib/Drupal/Core/Asset/Collection/AssetCollection.php new file mode 100644 index 0000000..51a8209 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Collection/AssetCollection.php @@ -0,0 +1,216 @@ +attemptWrite(__METHOD__); + return $this->_add($asset); + } + + /** + * {@inheritdoc} + */ + public function mergeCollection(AssetCollectionInterface $collection, $freeze = TRUE) { + $this->attemptWrite(__METHOD__); + + foreach ($collection as $asset) { + if (!$this->contains($asset)) { + $this->add($asset); + } + } + + foreach ($collection->getUnresolvedLibraries() as $library) { + // TODO just cheat and merge these in? + $this->addUnresolvedLibrary($library); + } + + if ($freeze) { + $collection->freeze(); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function remove($needle, $graceful = FALSE) { + $this->attemptWrite(__METHOD__); + return $this->_remove($needle, $graceful); + } + + /** + * {@inheritdoc} + */ + public function replace($needle, AssetInterface $replacement, $graceful = FALSE) { + $this->attemptWrite(__METHOD__); + return $this->_replace($needle, $replacement, $graceful); + } + + /** + * {@inheritdoc} + */ + public function getCss() { + $collection = new self(); + foreach (new AssetSubtypeFilterIterator(new \ArrayIterator($this->all()), 'css') as $asset) { + $collection->add($asset); + } + + return $collection; + } + + /** + * {@inheritdoc} + */ + public function getJs() { + $collection = new self(); + foreach (new AssetSubtypeFilterIterator(new \ArrayIterator($this->all()), 'js') as $asset) { + $collection->add($asset); + } + + return $collection; + } + + /** + * {@inheritdoc} + */ + public function uksort($callback) { + $this->attemptWrite(__METHOD__); + uksort($this->assetIdMap, $callback); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function ksort() { + $this->attemptWrite(__METHOD__); + ksort($this->assetIdMap); + + return $this; + } + + /** + * {@inheritdoc} + * + * TODO deal with nested assets - should they also be reversed? + */ + public function reverse() { + $this->attemptWrite(__METHOD__); + $this->assetIdMap = array_reverse($this->assetIdMap); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function addUnresolvedLibrary($key) { + $this->attemptWrite(__METHOD__); + // The library key is stored as the key for cheap deduping. + $this->libraries[$key] = TRUE; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function hasUnresolvedLibraries() { + return !empty($this->libraries); + } + + /** + * {@inheritdoc} + */ + public function getUnresolvedLibraries() { + return array_keys($this->libraries); + } + + /** + * {@inheritdoc} + */ + public function clearUnresolvedLibraries() { + $this->attemptWrite(__METHOD__); + $this->libraries = array(); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function resolveLibraries(AssetLibraryRepository $repository) { + $this->attemptWrite(__METHOD__); + + // Resolving directly added libraries first ensures their contained assets + // are processed in the next loop. + foreach ($this->getUnresolvedLibraries() as $key) { + $library = $repository->get($key); + foreach ($library as $asset) { + $this->add($asset); + } + } + + $this->clearUnresolvedLibraries(); + + // By iterating the assetStorage SPLOS, we guarantee that this loop won't + // finish until every added asset has been processed - including ones + // attached to the SPLOS during the loop. The alternative is a recursive + // closure - far more complex, and slower. + foreach ($this->assetStorage as $asset) { + if ($asset instanceof DependencyInterface) { + foreach ($repository->resolveDependencies($asset) as $library) { + foreach ($library as $libasset) { + // The repository already attached positioning info for us; just add. + $this->add($libasset); + } + } + } + } + + return $this; + } +} diff --git a/core/lib/Drupal/Core/Asset/Collection/AssetCollectionInterface.php b/core/lib/Drupal/Core/Asset/Collection/AssetCollectionInterface.php new file mode 100644 index 0000000..c7ad1ae --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Collection/AssetCollectionInterface.php @@ -0,0 +1,137 @@ +attemptWrite(__METHOD__); + $this->title = $title; + return $this; + } + + /** + * Get the asset library's title. + * + * @return string + * The title of the asset library. + */ + public function getTitle() { + return $this->title; + } + + /** + * Set the asset library's website. + * + * @param string $website + * The website of the asset library. + * + * @return \Drupal\Core\Asset\AssetLibrary + * The asset library, to allow for chaining. + */ + public function setWebsite($website) { + $this->attemptWrite(__METHOD__); + $this->website = $website; + return $this; + } + + /** + * Get the asset library's website. + * + * @return string + * The website of the asset library. + */ + public function getWebsite() { + return $this->website; + } + + /** + * Set the asset library's version. + * + * @param string $version + * The version of the asset library. + * + * @return \Drupal\Core\Asset\AssetLibrary + * The asset library, to allow for chaining. + */ + public function setVersion($version) { + $this->attemptWrite(__METHOD__); + $this->version = $version; + return $this; + } + + /** + * Get the asset library's version. + * + * @return string + * The version of the asset library. + */ + public function getVersion() { + return $this->version; + } + + /** + * {@inheritdoc} + */ + public function addDependency($key) { + $this->attemptWrite(__METHOD__); + return $this->_addDependency($key); + } + + /** + * {@inheritdoc} + */ + public function clearDependencies() { + $this->attemptWrite(__METHOD__); + $this->dependencies = array(); + return $this; + } +} diff --git a/core/lib/Drupal/Core/Asset/Collection/BasicCollectionInterface.php b/core/lib/Drupal/Core/Asset/Collection/BasicCollectionInterface.php new file mode 100644 index 0000000..7bc6cab --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Collection/BasicCollectionInterface.php @@ -0,0 +1,172 @@ +_bcinit === FALSE) { + $this->assetStorage = new \SplObjectStorage(); + $this->nestedStorage = new \SplObjectStorage(); + foreach ($assets as $asset) { + $this->add($asset); + } + $this->_bcinit = TRUE; + } + } + + /** + * {@inheritdoc} + */ + public function add(AsseticAssetInterface $asset) { + if (!$asset instanceof AssetInterface) { + throw new UnsupportedAsseticBehaviorException('Vanilla Assetic asset provided; Drupal collections require Drupal-flavored assets.'); + } + $this->ensureCorrectType($asset); + + if (!($this->contains($asset) || $this->find($asset->id()))) { + $this->assetStorage->attach($asset); + $this->assetIdMap[$asset->id()] = $asset; + + if ($asset instanceof BasicCollectionInterface) { + $this->nestedStorage->attach($asset); + } + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function contains(AssetInterface $asset) { + if ($this->assetStorage->contains($asset)) { + return TRUE; + } + + foreach ($this->nestedStorage as $aggregate) { + if ($aggregate->contains($asset)) { + return TRUE; + } + } + + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function find($id, $graceful = TRUE) { + if (isset($this->assetIdMap[$id])) { + return $this->assetIdMap[$id]; + } + else { + // Recursively search for the id + foreach ($this->nestedStorage as $aggregate) { + if ($found = $aggregate->find($id)) { + return $found; + } + } + } + + if ($graceful) { + return FALSE; + } + + throw new \OutOfBoundsException(sprintf('This collection does not contain an asset with id %s.', $id)); + } + + /** + * {@inheritdoc} + */ + public function remove($needle, $graceful = FALSE) { + if (is_string($needle)) { + if (!$needle = $this->find($needle, $graceful)) { + return FALSE; + } + } + else if (!$needle instanceof AssetInterface) { + throw new \InvalidArgumentException('Invalid type provided to BasicCollectionInterface::replace(); must provide either a string asset id or AssetInterface instance.'); + } + + return $this->doRemove($needle, $graceful); + } + + /** + * Performs the actual work of removing an asset from the collection. + * + * @param AssetInterface|string $needle + * Either an AssetInterface instance, or the string id of an asset. + * @param bool $graceful + * Whether failure should return FALSE or throw an exception. + * + * @return bool + * TRUE on success, FALSE on failure to locate the given asset (or an + * exception, depending on the value of $graceful). + * + * @throws \OutOfBoundsException + * Thrown if $needle could not be located and $graceful = FALSE. + */ + protected function doRemove(AssetInterface $needle, $graceful) { + foreach ($this->assetIdMap as $id => $asset) { + if ($asset === $needle) { + unset($this->assetStorage[$asset], $this->assetIdMap[$id], $this->nestedStorage[$asset]); + + return TRUE; + } + + if ($asset instanceof BasicCollectionInterface && $asset->remove($needle, TRUE)) { + return TRUE; + } + } + + if ($graceful) { + return FALSE; + } + + throw new \OutOfBoundsException('Provided asset was not found in the collection.'); + } + + /** + * {@inheritdoc} + */ + public function replace($needle, AssetInterface $replacement, $graceful = FALSE) { + if (is_string($needle)) { + if (!$needle = $this->find($needle, $graceful)) { + return FALSE; + } + } + else if (!$needle instanceof AssetInterface) { + throw new \InvalidArgumentException('Invalid type provided to BasicCollectionInterface::replace(); must provide either a string asset id or AssetInterface instance.'); + } + + $this->ensureCorrectType($replacement); + if ($this->contains($replacement)) { + throw new \LogicException('Asset to be swapped in is already present in the collection.'); + } + + return $this->doReplace($needle, $replacement, $graceful); + } + + /** + * Performs the actual work of replacing one asset with another. + * + * @param AssetInterface $needle + * The AssetInterface instance to swap out. + * @param AssetInterface $replacement + * The new asset to swap in. + * @param bool $graceful + * Whether failure should return FALSE or throw an exception. + * + * @return bool + * TRUE on success, FALSE on failure to locate the given asset (or an + * exception, depending on the value of $graceful). + * + * @throws \OutOfBoundsException + */ + protected function doReplace(AssetInterface $needle, AssetInterface $replacement, $graceful) { + $i = 0; + foreach ($this->assetIdMap as $id => $asset) { + if ($asset === $needle) { + unset($this->assetStorage[$asset], $this->nestedStorage[$asset]); + + array_splice($this->assetIdMap, $i, 1, array($replacement->id() => $replacement)); + $this->assetStorage->attach($replacement); + if ($replacement instanceof BasicCollectionInterface) { + $this->nestedStorage->attach($replacement); + } + + return TRUE; + } + + if ($asset instanceof BasicCollectionInterface && $asset->replace($needle, $replacement, TRUE)) { + return TRUE; + } + $i++; + } + + if ($graceful) { + return FALSE; + } + + throw new \OutOfBoundsException('Provided asset was not found in the collection.'); + } + + /** + * {@inheritdoc} + */ + public function all() { + return $this->assetIdMap; + } + + /** + * {@inheritdoc} + * TODO Assetic uses their iterator to clone, then populate values and return here; is that a good model for us? + */ + public function getIterator() { + return new \RecursiveIteratorIterator(new RecursiveBasicCollectionIterator($this), \RecursiveIteratorIterator::SELF_FIRST); + } + + /** + * {@inheritdoc} + */ + public function each() { + return $this->getIterator(); + } + + /** + * {@inheritdoc} + */ + public function eachLeaf() { + return new \RecursiveIteratorIterator(new RecursiveBasicCollectionIterator($this)); + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + $maincount = $this->assetStorage->count(); + if ($maincount === 0) { + return TRUE; + } + + $i = 0; + foreach ($this->nestedStorage as $aggregate) { + if (!$aggregate->isEmpty()) { + return FALSE; + } + $i++; + } + + return $i === $maincount; + } + + /** + * {@inheritdoc} + */ + public function count() { + if ($this->nestedStorage->count() === 0) { + return $this->assetStorage->count(); + } + + $c = $i = 0; + foreach ($this->nestedStorage as $collection) { + $c += $collection->count(); + $i++; + } + + return $this->assetStorage->count() - $i + $c; + } + + /** + * Ensures that the asset is the correct type for this collection. + * + * "Type" here refers to 'css' vs. 'js'. + * + * BasicCollectionTrait's implementation has no body because it has no type + * restrictions; only aggregates do. + * + * @param AssetInterface $asset + * + * @throws \Drupal\Core\Asset\Exception\AssetTypeMismatchException + */ + protected function ensureCorrectType(AssetInterface $asset) {} +} + diff --git a/core/lib/Drupal/Core/Asset/Collection/Iterator/AssetSubtypeFilterIterator.php b/core/lib/Drupal/Core/Asset/Collection/Iterator/AssetSubtypeFilterIterator.php new file mode 100644 index 0000000..b7a0395 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Collection/Iterator/AssetSubtypeFilterIterator.php @@ -0,0 +1,36 @@ +match = $match; + } + + /** + * {@inheritdoc} + */ + public function accept() { + return $this->current()->getAssetType() === $this->match; + } + +} diff --git a/core/lib/Drupal/Core/Asset/Collection/Iterator/RecursiveBasicCollectionIterator.php b/core/lib/Drupal/Core/Asset/Collection/Iterator/RecursiveBasicCollectionIterator.php new file mode 100644 index 0000000..2205c84 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Collection/Iterator/RecursiveBasicCollectionIterator.php @@ -0,0 +1,25 @@ +all()); + } + + public function hasChildren() { + return $this->current() instanceof BasicCollectionInterface; + } + +} diff --git a/core/lib/Drupal/Core/Asset/DependencyInterface.php b/core/lib/Drupal/Core/Asset/DependencyInterface.php new file mode 100644 index 0000000..d1d78c7 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/DependencyInterface.php @@ -0,0 +1,53 @@ +dependencies[$key] = TRUE; + return $this; + } + + /** + * {@inheritdoc} + */ + public function hasDependencies() { + return !empty($this->dependencies); + } + + /** + * {@inheritdoc} + */ + public function getDependencyInfo() { + return array_keys($this->dependencies); + } + + /** + * {@inheritdoc} + */ + public function clearDependencies() { + $this->dependencies = array(); + return $this; + } + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/Exception/AssetTypeMismatchException.php b/core/lib/Drupal/Core/Asset/Exception/AssetTypeMismatchException.php new file mode 100644 index 0000000..16edd41 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Exception/AssetTypeMismatchException.php @@ -0,0 +1,16 @@ +sourceUrl = $sourceUrl; + $this->setTargetPath($sourceUrl); // TODO do this immediately...for now. + + list($scheme, $url) = explode('://', $sourceUrl, 2); + list($host, $path) = explode('/', $url, 2); + + parent::__construct($metadata, $filters, $scheme . '://' . $host, $path); + } + + /** + * {@inheritdoc} + */ + public function id() { + return $this->sourceUrl; + } + + /** + * {@inheritdoc} + */ + public function load(FilterInterface $additionalFilter = NULL) { + // TODO very wrong. decide how to do this right. + throw new UnsupportedAsseticBehaviorException('Drupal does not support the retrieval or manipulation of remote assets.'); + } + + /** + * {@inheritdoc} + */ + public function dump(FilterInterface $additionalFilter = NULL) { + // TODO very wrong. decide how to do this right. + throw new UnsupportedAsseticBehaviorException('Drupal does not support the retrieval or manipulation of remote assets.'); + } +} + diff --git a/core/lib/Drupal/Core/Asset/Factory/AssetCollector.php b/core/lib/Drupal/Core/Asset/Factory/AssetCollector.php new file mode 100644 index 0000000..d35a5e1 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Factory/AssetCollector.php @@ -0,0 +1,251 @@ + 'Drupal\\Core\\Asset\\FileAsset', + 'external' => 'Drupal\\Core\\Asset\\ExternalAsset', + 'string' => 'Drupal\\Core\\Asset\\StringAsset', + ); + + public function __construct(AssetCollectionInterface $collection = NULL, MetadataFactoryInterface $factory = NULL) { + if (!is_null($factory)) { + $this->metadataFactory = $factory; + } + else { + $this->restoreDefaults(); + } + + if (!is_null($collection)) { + $this->setCollection($collection); + } + } + + /** + * {@inheritdoc} + */ + public function add(AssetInterface $asset) { + if (empty($this->collection)) { + throw new \RuntimeException('No collection is currently attached to this collector.'); + } + $this->collection->add($asset); + return $this; + } + + /** + * {@inheritdoc} + */ + public function create($asset_type, $source_type, $data, $options = array(), $filters = array(), $keep_last = TRUE) { + // TODO this normalization points to a deeper modeling problem. + $source_type = $source_type == 'inline' ? 'string' : $source_type; + + if (!in_array($asset_type, array('css', 'js'))) { + throw new \InvalidArgumentException(sprintf('Only assets of type "js" or "css" are allowed, "%s" requested.', $asset_type)); + } + if (!isset($this->classMap[$source_type])) { + throw new \InvalidArgumentException(sprintf('Only sources of type "file", "string", or "external" are allowed, "%s" requested.', $source_type)); + } + + $metadata = $this->getMetadataDefaults($asset_type, $source_type, $data); + if (!empty($options)) { + $metadata->add($options); + } + + $class = $this->classMap[$source_type]; + $asset = new $class($metadata, $data, $filters); + + if (!empty($this->collection)) { + $this->add($asset); + } + + if ($asset_type == 'css') { + if (!empty($this->lastCss)) { + $asset->after($this->lastCss); + } + if ($keep_last) { + $this->lastCss = $asset; + } + } + + return $asset; + } + + /** + * {@inheritdoc} + */ + public function clearLastCss() { + unset($this->lastCss); + return $this; + } + + /** + * {@inheritdoc} + */ + public function setCollection(AssetCollectionInterface $collection) { + if ($this->isLocked()) { + throw new LockedObjectException('The collector instance is locked. A new collection cannot be attached to a locked collector.'); + } + $this->collection = $collection; + } + + /** + * {@inheritdoc} + */ + public function clearCollection() { + if ($this->isLocked()) { + throw new LockedObjectException('The collector instance is locked. Collections cannot be cleared on a locked collector.'); + } + $this->collection = NULL; + } + + /** + * {@inheritdoc} + */ + public function hasCollection() { + return $this->collection instanceof AssetCollectionInterface; + } + + /** + * {@inheritdoc} + */ + public function lock($key) { + if ($this->isLocked()) { + throw new LockedObjectException('Collector is already locked.', E_WARNING); + } + + $this->locked = TRUE; + $this->lockKey = $key; + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function unlock($key) { + if (!$this->isLocked()) { + throw new LockedObjectException('Collector is not locked', E_WARNING); + } + + if ($this->lockKey !== $key) { + throw new LockedObjectException('Attempted to unlock Collector with incorrect key.', E_WARNING); + } + + $this->locked = FALSE; + $this->lockKey = NULL; + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function isLocked() { + return $this->locked; + } + + /** + * {@inheritdoc} + */ + public function setMetadataFactory(MetadataFactoryInterface $factory) { + if ($this->isLocked()) { + throw new LockedObjectException('The collector instance is locked. Asset defaults cannot be modified on a locked collector.'); + } + + $this->metadataFactory = $factory; + } + + /** + * {@inheritdoc} + */ + public function getMetadataDefaults($asset_type, $source_type, $data) { + if ($asset_type === 'css') { + return $this->metadataFactory->createCssMetadata($source_type, $data); + } + elseif ($asset_type === 'js') { + return $this->metadataFactory->createJsMetadata($source_type, $data); + } + else { + throw new \InvalidArgumentException(sprintf('Only assets of type "js" or "css" are supported, "%s" requested.', $asset_type)); + } + } + + /** + * {@inheritdoc} + */ + public function restoreDefaults() { + if ($this->isLocked()) { + throw new LockedObjectException('The collector instance is locked. Asset defaults cannot be modified on a locked collector.'); + } + + $this->metadataFactory = new DefaultAssetMetadataFactory(); + } + +} diff --git a/core/lib/Drupal/Core/Asset/Factory/AssetCollectorInterface.php b/core/lib/Drupal/Core/Asset/Factory/AssetCollectorInterface.php new file mode 100644 index 0000000..5664f51 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Factory/AssetCollectorInterface.php @@ -0,0 +1,229 @@ +moduleHandler = $moduleHandler; + $this->metadataFactory = $metadataFactory ?: new DefaultAssetMetadataFactory(); + $this->collector = $collector ?: new AssetCollector(NULL, $this->metadataFactory); + + if ($this->collector->isLocked()) { + throw new \RuntimeException('The collector provided to an AssetLibraryFactory was locked; it must be unlocked so the factory can fully control it.'); + } + } + + /** + * Returns an AssetLibrary based on data declared in hook_library_info(). + * + * @param $key + * + * @return AssetLibrary|bool + * An AssetLibrary instance, or FALSE if the key did not resolve to library + * data. + */ + public function getLibrary($key) { + list($module, $name) = preg_split('/\//', $key); + + if (!$this->moduleHandler->implementsHook($module, 'library_info')) { + // Module doesn't implement hook_library_info(), a library can't exist. + return FALSE; + } + + $declarations = call_user_func($module . '_library_info') ?: array(); + + $this->moduleHandler->alter('library_info', $declarations, $module); + + if (!isset($declarations[$name])) { + // No library by the given name. + return FALSE; + } + + // Normalize the data - hook_library_info() allows sloppiness + $info = $declarations[$name] + array('dependencies' => array(), 'js' => array(), 'css' => array()); + $library = new AssetLibrary(); + + if (isset($info['title'])) { + $library->setTitle($info['title']); + } + if (isset($info['version'])) { + $library->setVersion($info['version']); + } + if (isset($info['website'])) { + $library->setWebsite($info['website']); + } + + // Record dependencies on the library, if any. + foreach ($info['dependencies'] as $dep) { + // TODO remove this, this is the wrong level at which to declare. + $library->addDependency($dep[0] . '/' . $dep[1]); + } + + // Populate the library with asset objects. + $this->collector->setCollection($library); + foreach (array('js', 'css') as $type) { + foreach ($info[$type] as $data => $options) { + if (is_scalar($options)) { + $data = $options; + $options = array(); + } + + $source_type = isset($options['type']) ? $options['type'] : 'file'; + unset($options['type']); + + if ($type == 'js' && $source_type == 'setting') { + // TODO temporarily continue/skip if it's a js setting, can't handle those yet + continue; + } + + $asset = $this->collector->create($type, $source_type, $data, $options); + foreach ($info['dependencies'] as $dep) { + $asset->addDependency($dep[0] . '/' . $dep[1]); + } + } + } + + // Ensure that auto-aftering of CSS doesn't bleed across libraries. + $this->collector->clearLastCss(); + + $library->freeze(); + return $library; + } +} + diff --git a/core/lib/Drupal/Core/Asset/FileAsset.php b/core/lib/Drupal/Core/Asset/FileAsset.php new file mode 100644 index 0000000..9c6834e --- /dev/null +++ b/core/lib/Drupal/Core/Asset/FileAsset.php @@ -0,0 +1,71 @@ +source = $source; + $this->setTargetPath($source); // TODO do this immediately...for now. + + parent::__construct($metadata, $filters, $sourceRoot, $sourcePath); + } + + /** + * {@inheritdoc} + */ + public function id() { + return $this->source; + } + + /** + * {@inheritdoc} + */ + public function load(FilterInterface $additionalFilter = NULL) { + if (!is_file($this->source)) { + throw new \RuntimeException(sprintf('The source file "%s" does not exist.', $this->source)); + } + + $this->doLoad(file_get_contents($this->source), $additionalFilter); + } + +} diff --git a/core/lib/Drupal/Core/Asset/GroupSort/AssetGraph.php b/core/lib/Drupal/Core/Asset/GroupSort/AssetGraph.php new file mode 100644 index 0000000..20a6954 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/GroupSort/AssetGraph.php @@ -0,0 +1,182 @@ +process = $process; + } + + /** + * {@inheritdoc} + */ + public function addVertex($vertex) { + if (!$vertex instanceof AssetInterface) { + throw new InvalidVertexTypeException('AssetGraph requires vertices to implement AssetInterface.'); + } + + if (!$this->hasVertex($vertex)) { + $this->vertices[$vertex] = new \SplObjectStorage(); + $this->verticesById[$vertex->id()] = $vertex; + + if ($this->process) { + $this->processNewVertex($vertex); + } + } + } + + /** + * Processes all positioning information for a given vertex. + * + * @param AssetInterface $vertex + */ + protected function processNewVertex(AssetInterface $vertex) { + $id = $vertex->id(); + // First, check if anything has a watch out for this vertex. + if (isset($this->before[$id])) { + foreach ($this->before[$id] as $predecessor) { + $this->addDirectedEdge($predecessor, $vertex); + } + unset($this->before[$id]); + } + + if (isset($this->after[$id])) { + foreach ($this->after[$id] as $successor) { + $this->addDirectedEdge($vertex, $successor); + } + unset($this->after[$id]); + } + + // Add watches for this vertex, if it implements the interface. + if ($vertex instanceof RelativePositionInterface) { + // TODO this logic assumes collections enforce uniqueness - ensure that's the case. + foreach ($vertex->getPredecessors() as $predecessor) { + // Normalize to id string. + $predecessor = is_string($predecessor) ? $predecessor : $predecessor->id(); + + // Add a directed edge indicating that this asset vertex succeeds + // another asset vertex. Or, if that other asset does not yet have a + // vertex in the AssetGraph, set up a watch for it. + if (isset($this->verticesById[$predecessor])) { + $this->addDirectedEdge($vertex, $this->verticesById[$predecessor]); + } + else { + if (!isset($this->before[$predecessor])) { + $this->before[$predecessor] = array(); + } + $this->before[$predecessor][] = $vertex; + } + } + + foreach ($vertex->getSuccessors() as $successor) { + // Normalize to id string. + $successor = is_string($successor) ? $successor : $successor->id(); + + // Add a directed edge indicating that this asset vertex preceeds + // another asset vertex. Or, if that other asset does not yet have a + // vertex in the AssetGraph, set up a watch for it. + if (isset($this->verticesById[$successor])) { + $this->addDirectedEdge($this->verticesById[$successor], $vertex); + } + else { + if (!isset($this->before[$successor])) { + $this->after[$successor] = array(); + } + $this->after[$successor][] = $vertex; + } + } + } + } + + /** + * Remove a vertex from the graph. Unsupported in AssetGraph. + * + * Vertex removals are unsupported because it would necessitate permanent + * bookkeeping on positioning data. With forty or fifty assets, each having + * only a few dependencies, there would be a fair bit of pointless iterating. + * + * @throws \LogicException + * This exception will always be thrown. + */ + public function removeVertex($vertex) { + throw new \LogicException('AssetGraph does not support vertex removals.'); + } + + /** + * {@inheritdoc} + */ + public function transpose() { + $graph = new self(FALSE); + $this->eachVertex(function($v, $adjacent) use (&$graph) { + $graph->addVertex($v); + + foreach ($adjacent as $adj) { + $graph->addDirectedEdge($adj, $v); + } + }); + + return $graph; + } + +} diff --git a/core/lib/Drupal/Core/Asset/GroupSort/AssetGraphSorter.php b/core/lib/Drupal/Core/Asset/GroupSort/AssetGraphSorter.php new file mode 100644 index 0000000..1968730 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/GroupSort/AssetGraphSorter.php @@ -0,0 +1,74 @@ +insert($vertex, count($reach_visitor->getReachable($vertex))); + } + + // Dump the priority queue into a normal queue + // TODO maybe gliph should support pq/heaps as a queue type on which to operate? + $queue = new \SplQueue(); + foreach ($pq as $vertex) { + $queue->push($vertex); + } + + return $queue; + } + +} diff --git a/core/lib/Drupal/Core/Asset/GroupSort/AssetGroupSorterInterface.php b/core/lib/Drupal/Core/Asset/GroupSort/AssetGroupSorterInterface.php new file mode 100644 index 0000000..cd7d74d --- /dev/null +++ b/core/lib/Drupal/Core/Asset/GroupSort/AssetGroupSorterInterface.php @@ -0,0 +1,47 @@ +getMetadata(); + // The browsers for which the CSS item needs to be loaded is part of the + // information that determines when a new group is needed, but the order + // of keys in the array doesn't matter, and we don't want a new group if + // all that's different is that order. + $browsers = $meta->get('browsers'); + ksort($browsers); + + if ($asset instanceof FileAsset) { + // Compose a string key out of the set of relevant properties. + // TODO - this ignores group, which is used in core's current implementation. wishful thinking? maybe, maybe not. + // TODO media has been pulled out - needs to be handled by the aggregator, wrapping css in media queries + $k = $asset->isPreprocessable() + ? implode(':', array('file', $meta->get('every_page'), implode('', $browsers))) + : FALSE; + } + else if ($asset instanceof StringAsset) { + // String items are always grouped. + // TODO use the term 'inline' here? do "string" and "inline" necessarily mean the same? + $k = implode(':', 'string', implode('', $browsers)); + } + else if ($asset instanceof ExternalAsset) { + // Never group external assets. + $k = FALSE; + } + else { + throw new \UnexpectedValueException(sprintf('Unknown CSS asset type "%s" somehow made it into the CSS collection during grouping.', get_class($asset))); + } + + return $k; + } + + /** + * {@inheritdoc} + */ + public function groupAndSort(AssetCollectionInterface $collection) { + // We need to define the optimum minimal group set, given metadata + // boundaries across which aggregates cannot be safely made. + $optimal = array(); + + // Also create an SplObjectStorage to act as a lookup table on an asset to + // its group, if any. + // TODO try and find an elegant way to pass this out so we don't have to calculate keys twice + $optimal_lookup = new \SplObjectStorage(); + + // Finally, create a specialized directed adjacency list that will capture + // all ordering information. + $graph = new AssetGraph(); + + // TODO Would probably be better to inject the right collection rather than asking for it here + foreach ($collection->getCss() as $asset) { + $graph->addVertex($asset); + + $k = self::getGroupingKey($asset); + + if ($k === FALSE) { + // Record no optimality information for ungroupable assets; they will + // be visited normally and rearranged as needed. + continue; + } + + if (!isset($optimal[$k])) { + // Create an SplObjectStorage to represent each set of assets that would + // optimally be grouped together. + $optimal[$k] = new \SplObjectStorage(); + } + $optimal[$k]->attach($asset, $k); + $optimal_lookup->attach($asset, $optimal[$k]); + } + + // First, transpose the graph in order to get an appropriate answer. + // (In the AssetGraph, if asset A comes before asset B, a directed edge + // exists from B to A. By transposing the graph, all directed edges are + // reversed, so that a directed edge exists from A to B. + // A topological sort on a graph will provide a linear ordering of all + // vertices, in our example: "A, B". Without performing the transpose + // operation, we'd get "B, A", which is the inverse of what we need.) + $transpose = $graph->transpose(); + + // Create a queue of start vertices to prime the traversal. + $queue = $this->createSourceQueue($transpose); + + // Now, create the visitor and walk the graph to get an optimal TSL. + $visitor = new OptimallyGroupedTSLVisitor($optimal, $optimal_lookup); + DepthFirst::traverse($transpose, $visitor, $queue); + + return $visitor->getTSL()->reverse(); + } + +} diff --git a/core/lib/Drupal/Core/Asset/GroupSort/OptimallyGroupedTSLVisitor.php b/core/lib/Drupal/Core/Asset/GroupSort/OptimallyGroupedTSLVisitor.php new file mode 100644 index 0000000..9b46e25 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/GroupSort/OptimallyGroupedTSLVisitor.php @@ -0,0 +1,118 @@ +tsl = new AssetCollection(); + $this->groups = $groups; + $this->vertexMap = $vertex_map; + } + + /** + * {@inheritdoc} + */ + public function beginTraversal() {} + + /** + * {@inheritdoc} + */ + public function endTraversal() {} + + /** + * {@inheritdoc} + */ + public function onInitializeVertex($vertex, $source, \SplQueue $queue) {} + + /** + * {@inheritdoc} + */ + public function onBackEdge($vertex, \Closure $visit) {} + + /** + * {@inheritdoc} + */ + public function onStartVertex($vertex, \Closure $visit) { + // If there's a record in the vertex map, it means this vertex has an + // optimal group. Remove it from that group, as being provided to this + // visitor method indicates the vertex is being visited. + if ($this->vertexMap->contains($vertex)) { + $this->vertexMap[$vertex]->detach($vertex); + } + } + + /** + * {@inheritdoc} + */ + public function onExamineEdge($from, $to, \Closure $visit) {} + + /** + * Here be the unicorns. + * + * Once the depth-first traversal is done for a vertex, rather than + * simply pushing it onto the TSL and moving on (as in a basic depth-first + * traversal), if the finished vertex is a member of an optimality group, then + * visit all other (unvisited) members of that optimality group. + * + * This ensures the final TSL has the tightest possible adherence to the + * defined optimal groupings while still respecting the DAG. + * + */ + public function onFinishVertex($vertex, \Closure $visit) { + if ($this->vertexMap->contains($vertex)) { + foreach ($this->vertexMap[$vertex] as $adjacent) { + $visit($adjacent); + } + } + $this->tsl->add($vertex); + } + + /** + * Returns the TSL produced by a depth-first traversal. + * + * @return \Drupal\Core\Asset\Collection\AssetCollection + * A topologically sorted list of vertices. + */ + public function getTSL() { + return $this->tsl; + } + +} diff --git a/core/lib/Drupal/Core/Asset/Metadata/AssetMetadataBag.php b/core/lib/Drupal/Core/Asset/Metadata/AssetMetadataBag.php new file mode 100644 index 0000000..7de5fd1 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Metadata/AssetMetadataBag.php @@ -0,0 +1,38 @@ +type = $type; + parent::__construct($values); + } + + /** + * {@inheritdoc} + */ + public function getType() { + return $this->type; + } + +} diff --git a/core/lib/Drupal/Core/Asset/Metadata/AssetMetadataInterface.php b/core/lib/Drupal/Core/Asset/Metadata/AssetMetadataInterface.php new file mode 100644 index 0000000..1f77f65 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Metadata/AssetMetadataInterface.php @@ -0,0 +1,83 @@ + FALSE, + 'media' => 'all', + 'preprocess' => TRUE, + 'browsers' => array( + 'IE' => TRUE, + '!IE' => TRUE, + ), + )); + } + + /** + * {@inheritdoc} + */ + public function createJsMetadata($source_type, $data) { + return new AssetMetadataBag('js', array( + 'every_page' => FALSE, + 'scope' => 'footer', + 'cache' => TRUE, + 'preprocess' => TRUE, + 'attributes' => array(), + 'version' => NULL, + 'browsers' => array(), + )); + } + +} diff --git a/core/lib/Drupal/Core/Asset/Metadata/MetadataFactoryInterface.php b/core/lib/Drupal/Core/Asset/Metadata/MetadataFactoryInterface.php new file mode 100644 index 0000000..0e3b866 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Metadata/MetadataFactoryInterface.php @@ -0,0 +1,45 @@ +sorter = $sorter; + } + + /** + * {@inheritdoc} + */ + public function aggregate(AssetCollectionInterface $collection) { + $tsl = $this->sorter->groupAndSort($collection); + + $processed = new AssetCollection(); + $last_key = FALSE; + foreach ($tsl as $asset) { + $key = $this->sorter->getGroupingKey($asset); + + if ($key && $key !== $last_key) { + $aggregate = new AggregateAsset($asset->getMetadata()); + $processed->add($aggregate); + } + + $key ? $aggregate->add($asset) : $processed->add($asset); + $last_key = $key; + } + + return $processed; + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/Optimize/CssCollectionOptimizer.php b/core/lib/Drupal/Core/Asset/Optimize/CssCollectionOptimizer.php new file mode 100644 index 0000000..74a6c34 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Optimize/CssCollectionOptimizer.php @@ -0,0 +1,95 @@ +aggregator = $aggregator; + $this->optimizer = $optimizer; + $this->dumper = $dumper; + $this->state = $state; + } + + /** + * {@inheritdoc} + */ + public function optimize(AssetCollectionInterface $collection) { + $collection = $this->aggregator->aggregate($collection); + + // Get the map of all aggregates that have been generated so far. + $map = $this->state->get('drupal_css_cache_files') ?: array(); + foreach ($collection as $asset) { + if ($asset->isPreprocessable()) { + $id = $asset->id(); + $uri = isset($map[$id]) ? $map[$id] : ''; + if (empty($uri) || !file_exists($uri)) { + // TODO optimizer needs to be refactored to basically just set filters. + $this->optimizer->optimize($asset); + // TODO refactor dumper to not need second param + $this->dumper->dump($asset, 'css'); + + $map[$id] = $asset->getTargetPath(); + } + } + } + + return $collection; + } + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/RelativePositionInterface.php b/core/lib/Drupal/Core/Asset/RelativePositionInterface.php new file mode 100644 index 0000000..fa49ab7 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/RelativePositionInterface.php @@ -0,0 +1,95 @@ +predecessors[] = $asset; + return $this; + } + + /** + * {@inheritdoc} + */ + public function hasPredecessors() { + return !empty($this->predecessors); + } + + /** + * {@inheritdoc} + */ + public function getPredecessors() { + return $this->predecessors; + } + + /** + * {@inheritdoc} + */ + public function clearPredecessors() { + $this->predecessors = array(); + return $this; + } + + /** + * {@inheritdoc} + */ + public function before($asset) { + if (!($asset instanceof AssetInterface || is_string($asset))) { + throw new \InvalidArgumentException('Ordering information must be declared using either an asset string id or the full AssetInterface object.'); + } + + $this->successors[] = $asset; + return $this; + } + + /** + * {@inheritdoc} + */ + public function hasSuccessors() { + return !empty($this->successors); + } + + /** + * {@inheritdoc} + */ + public function getSuccessors() { + return $this->successors; + } + + /** + * {@inheritdoc} + */ + public function clearSuccessors() { + $this->successors = array(); + return $this; + } + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/Render/AssetCollectionRendererInterface.php b/core/lib/Drupal/Core/Asset/Render/AssetCollectionRendererInterface.php new file mode 100644 index 0000000..5b586e1 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Render/AssetCollectionRendererInterface.php @@ -0,0 +1,27 @@ + 'html_tag', + '#tag' => 'link', + '#attributes' => array( + 'rel' => 'stylesheet', + ), + ); + + /** + * Default render array properties for style tag elements. + * + * @var array + */ + protected $styleElementDefaults = array( + '#type' => 'html_tag', + '#tag' => 'style', + ); + + /** + * Constructs a CssCollectionRenderer. + * + * @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface + * The state key/value store. + * + * @param \Drupal\Core\Asset\GroupSort\AssetGroupSorterInterface $sorter + * The CSS sorter service. Used only to reduce stylesheet count below + * 31 for state = $state; + $this->sorter = $sorter; + } + + public function render(AssetCollectionInterface $collection) { + // Deal with all(); + if (count($all) > 31) { + $link_count = 0; + foreach ($all as $asset) { + if ($asset instanceof FileAsset || $asset instanceof ExternalAsset || + $asset instanceof AggregateAssetInterface) { + $link_count++; + } + } + + if ($link_count > 31) { + $asset = reset($all); + + do { + $key = $this->sorter->getGroupingKey($asset); + + if ($key) { + $add = array(); + + $group_count = 0; + do { + $group_count++; + $add[] = $asset; + $asset = next($all); + $nkey = $this->sorter->getGroupingKey($asset); + } while ($key == $nkey && $group_count < 31); // IE has max of 31 @imports per style tag + + if (count($add) > 1) { + // only make aggregate if there's more than 1 + $aggregate = new AggregateAsset(reset($add)->getMetadata(), $add); + $meta = $aggregate->getMetadata(); + $meta->set('light_grouping', TRUE); + + $first = array_shift($add); + foreach ($add as $added) { + $collection->remove($added); + } + // Have to replace after removing, otherwise they'll be removed + // from the aggregate. + $collection->replace($first, $aggregate); + } + + $link_count -= $group_count - 1; // add one to account for aggregate + prev($all); // rewind for next loop + + if ($link_count <= 31) { + break; + } + } + // It's possible to still more than 31 assets here. If so...oh well. + } while ($asset = next($all)); + } + } + + $elements = array(); + + // A dummy query-string is added to filenames, to gain control over + // browser-caching. The string changes on every update or full cache + // flush, forcing browsers to load a new copy of the files, as the + // URL changed. + $query_string = $this->state->get('system.css_js_query_string') ?: '0'; + + foreach ($collection->all() as $asset) { + $meta = $asset->getMetadata(); + + if ($asset instanceof StringAsset) { + $element = $this->styleElementDefaults; + $element['#value'] = $asset->getContent(); + // For inline CSS to validate as XHTML, all CSS containing XHTML needs + // to be wrapped in CDATA. To make that backwards compatible with HTML + // 4, we need to comment out the CDATA-tag. + $element['#value_prefix'] = "\n/* */\n"; + } + elseif ($asset instanceof AggregateAssetInterface && $meta->get('light_grouping')) { + $import = array(); + foreach ($asset as $subasset) { + $import[] = '@import url("' . String::checkPlain(file_create_url($subasset->getTargetPath()) . '?' . $query_string) . '");'; + } + + $element = $this->styleElementDefaults; + $element['#value'] = "\n" . implode("\n", $import) . "\n"; + } + elseif ($asset instanceof ExternalAsset) { + $element = $this->linkElementDefaults; + $element['#attributes']['href'] = $asset->getTargetPath(); + } + else { + // individual files and aggregates + $query_string_separator = (strpos($asset->getTargetPath(), '?') !== FALSE) ? '&' : '?'; + $element = $this->linkElementDefaults; + $element['#attributes']['href'] = file_create_url($asset->getTargetPath()) . $query_string_separator . $query_string;; + } + + $element['#attributes']['media'] = $meta->get('media'); + $element['#browsers'] = $meta->get('browsers'); + $elements[] = $element; + } + + return $elements; + } +} + diff --git a/core/lib/Drupal/Core/Asset/StringAsset.php b/core/lib/Drupal/Core/Asset/StringAsset.php new file mode 100644 index 0000000..9862a87 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/StringAsset.php @@ -0,0 +1,65 @@ +id = hash('sha256', $content); + $this->setContent($content); + + parent::__construct($metadata, $filters); + } + + /** + * {@inheritdoc} + */ + public function id() { + return $this->id; + } + + /** + * {@inheritdoc} + */ + public function load(FilterInterface $additionalFilter = NULL) { + $this->doLoad($this->getContent(), $additionalFilter); + } + +} diff --git a/core/modules/block/lib/Drupal/block/BlockBase.php b/core/modules/block/lib/Drupal/block/BlockBase.php index 48e469b..22fdf74 100644 --- a/core/modules/block/lib/Drupal/block/BlockBase.php +++ b/core/modules/block/lib/Drupal/block/BlockBase.php @@ -12,6 +12,7 @@ use Drupal\Component\Utility\Unicode; use Drupal\Core\Language\Language; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Asset\Factory\AssetCollector; /** * Defines a base block implementation that most blocks plugins will extend. @@ -184,5 +185,10 @@ public function getMachineNameSuggestion() { return $transliterated; } - + /** + * {@inheritdoc} + */ + public function declareAssets(AssetCollector $collector) {} } + + diff --git a/core/modules/block/lib/Drupal/block/BlockPluginInterface.php b/core/modules/block/lib/Drupal/block/BlockPluginInterface.php index 047efd9..82943d8 100644 --- a/core/modules/block/lib/Drupal/block/BlockPluginInterface.php +++ b/core/modules/block/lib/Drupal/block/BlockPluginInterface.php @@ -11,6 +11,7 @@ use Drupal\Component\Plugin\ConfigurablePluginInterface; use Drupal\Core\Plugin\PluginFormInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Asset\Factory\AssetCollector; /** * Defines the required interface for all block plugins. @@ -126,4 +127,12 @@ public function blockSubmit($form, &$form_state); */ public function getMachineNameSuggestion(); + /** + * Declares the assets required by this block to a collector. + * + * @param \Drupal\Core\Asset\Factory\AssetCollector $collector + * + * @return void + */ + public function declareAssets(AssetCollector $collector); } diff --git a/core/tests/Drupal/Tests/Core/Asset/Aggregate/AggregateAssetTest.php b/core/tests/Drupal/Tests/Core/Asset/Aggregate/AggregateAssetTest.php new file mode 100644 index 0000000..7cb0de0 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/Aggregate/AggregateAssetTest.php @@ -0,0 +1,304 @@ + 'Asset aggregate tests', + 'description' => 'Unit tests on AggregateAsset', + 'group' => 'Asset', + ); + } + + /** + * Generates a AggregateAsset mock with three leaf assets. + */ + public function getThreeLeafAggregate() { + $aggregate = $this->getAggregate(); + $nested_aggregate = $this->getAggregate(); + + foreach (array('foo', 'bar', 'baz') as $var) { + $$var = $this->createStubFileAsset('css', $var); + } + + $nested_aggregate->add($foo); + $nested_aggregate->add($bar); + $aggregate->add($nested_aggregate); + $aggregate->add($baz); + + return array($aggregate, $foo, $bar, $baz, $nested_aggregate); + } + + /** + * Returns an AggregateAsset, the base collection type for this unit test. + * + * @return BasicCollectionInterface + */ + public function getCollection() { + return $this->getAggregate(); + } + + public function testGetAssetType() { + $mockmeta = $this->getMock('Drupal\Core\Asset\Metadata\AssetMetadataBag', array(), array(), '', FALSE); + $mockmeta->expects($this->once()) + ->method('getType') + ->will($this->returnValue('unicorns')); + $aggregate = $this->getMockForAbstractClass('Drupal\Core\Asset\Aggregate\AggregateAsset', array($mockmeta)); + + $this->assertEquals('unicorns', $aggregate->getAssetType()); + } + + public function testGetMetadata() { + $mockmeta = $this->createStubAssetMetadata(); + $aggregate = $this->getMockForAbstractClass('Drupal\Core\Asset\Aggregate\AggregateAsset', array($mockmeta)); + + $this->assertSame($mockmeta, $aggregate->getMetadata()); + } + + /** + * This uses PHPUnit's reflection-based assertions rather than assertContains + * so that this test can honestly sit at the root of the test method + * dependency tree. + * + * @covers ::add + */ + public function testAdd() { + $aggregate = $this->getAggregate(); + $asset = $this->createStubFileAsset(); + $this->assertSame($aggregate, $aggregate->add($asset)); + + $this->assertAttributeContains($asset, 'assetStorage', $aggregate); + $this->assertAttributeContains($asset, 'assetIdMap', $aggregate); + + // Nesting: add an aggregate to the first aggregate. + $nested_aggregate = $this->getAggregate(); + $aggregate->add($nested_aggregate); + + $this->assertAttributeContains($nested_aggregate, 'assetStorage', $aggregate); + $this->assertAttributeContains($nested_aggregate, 'assetIdMap', $aggregate); + $this->assertAttributeContains($nested_aggregate, 'nestedStorage', $aggregate); + } + + /** + * @depends testAdd + * @covers ::ensureCorrectType + * @expectedException \Drupal\Core\Asset\Exception\AssetTypeMismatchException + */ + public function testAddEnsureCorrectType() { + $aggregate = $this->getAggregate(); + $aggregate->add($this->createStubFileAsset('js')); + } + + /** + * @depends testAdd + * @covers ::each + * @covers ::getIterator + * @covers \Drupal\Core\Asset\Collection\Iterator\RecursiveBasicCollectionIterator + */ + public function testEach() { + list($aggregate, $foo, $bar, $baz, $nested_aggregate) = $this->getThreeLeafAggregate(); + + $contained = array(); + foreach ($aggregate->each() as $leaf) { + $contained[] = $leaf; + } + $this->assertEquals(array($nested_aggregate, $foo, $bar, $baz), $contained); + } + + /** + * @depends testAdd + * @depends testEach + * @covers ::__construct + */ + public function testCreateWithAssets() { + $asset1 = $this->createStubFileAsset(); + $asset2 = $this->createStubFileAsset(); + $meta = $this->createStubAssetMetadata(); + $collection = $this->getMockForAbstractClass('Drupal\Core\Asset\Aggregate\AggregateAsset', array($meta, array($asset1, $asset2))); + + $this->assertContains($asset1, $collection); + $this->assertContains($asset2, $collection); + } + + /** + * @depends testAdd + * @covers ::id + * @covers ::calculateId + */ + public function testId() { + // Simple case - test with one contained asset first. + $aggregate = $this->getAggregate(); + $asset1 = $this->createStubFileAsset(); + $aggregate->add($asset1); + + $this->assertEquals(hash('sha256', $asset1->id()), $aggregate->id()); + + // Now use two contained assets, one nested in another aggregate. + $aggregate = $this->getAggregate(); + $aggregate->add($asset1); + + $aggregate2 = $this->getAggregate(); + $asset2 = $this->createStubFileAsset(); + $aggregate2->add($asset2); + + $aggregate->add($aggregate2); + + // The aggregate only uses leaf, non-aggregate assets to determine its id. + $this->assertEquals(hash('sha256', $asset1->id() . $asset2->id()), $aggregate->id()); + } + + public function testIsPreprocessable() { + $this->assertTrue($this->getAggregate()->isPreprocessable()); + } + + /** + * @depends testEach + * @covers ::removeLeaf + * @expectedException \OutOfBoundsException + */ + public function testRemoveNonexistentNeedle() { + list($aggregate) = $this->getThreeLeafAggregate(); + // Nonexistent leaf removal returns FALSE in graceful mode + $this->assertFalse($aggregate->removeLeaf($this->createStubFileAsset(), TRUE)); + + // In non-graceful mode, an exception is thrown. + $aggregate->removeLeaf($this->createStubFileAsset()); + } + + /** + * @covers ::removeLeaf + * @expectedException \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException + */ + public function testRemoveLeafVanillaAsseticAsset() { + $aggregate = $this->getAggregate(); + $vanilla = $this->getMock('\Assetic\Asset\BaseAsset', array(), array(), '', FALSE); + $aggregate->removeLeaf($vanilla); + } + + /** + * @depends testAdd + * @covers ::ensureCorrectType + * @expectedException \Drupal\Core\Asset\Exception\AssetTypeMismatchException + */ + public function testReplaceLeafEnsureCorrectType() { + $aggregate = $this->getAggregate(); + $asset1 = $this->createStubFileAsset(); + $aggregate->add($asset1); + + $asset2 = $this->createStubFileAsset('js'); + $aggregate->replaceLeaf($asset1, $asset2); + } + + /** + * @depends testAdd + * @covers ::ensureCorrectType + * @expectedException \Drupal\Core\Asset\Exception\AssetTypeMismatchException + */ + public function testReplaceEnsureCorrectType() { + $aggregate = $this->getAggregate(); + $asset1 = $this->createStubFileAsset(); + $aggregate->add($asset1); + + $asset2 = $this->createStubFileAsset('js'); + $aggregate->replace($asset1, $asset2); + } + + /** + * @depends testEach + * @covers ::replaceLeaf + * @expectedException \OutOfBoundsException + */ + public function testReplaceLeafNonexistentNeedle() { + list($aggregate) = $this->getThreeLeafAggregate(); + // Nonexistent leaf replacement returns FALSE in graceful mode + $qux = $this->createStubFileAsset(); + $this->assertFalse($aggregate->replaceLeaf($this->createStubFileAsset(), $qux, TRUE)); + $this->assertNotContains($qux, $aggregate); + + // In non-graceful mode, an exception is thrown. + $aggregate->replaceLeaf($this->createStubFileAsset(), $qux); + } + + /** + * @depends testEach + * @covers ::replaceLeaf + * @expectedException \LogicException + */ + public function testReplaceLeafWithAlreadyPresentAsset() { + list($aggregate, $foo) = $this->getThreeLeafAggregate(); + $aggregate->replaceLeaf($this->createStubFileAsset(), $foo); + } + + /** + * @depends testAdd + * @depends testReplaceLeafWithAlreadyPresentAsset + * @covers ::replace + * @expectedException \LogicException + * + * This fails on the same check that testReplaceLeafWithAlreadyPresentAsset, + * but it is demonstrated as its own test for clarity. + */ + public function testReplaceLeafWithSelf() { + list($aggregate, $foo) = $this->getThreeLeafAggregate(); + $aggregate->replaceLeaf($foo, $foo); + } + + /** + * @depends testAdd + * @covers ::replaceLeaf + */ + public function testReplaceLeafVanillaAsseticAsset() { + $aggregate = $this->getAggregate(); + $vanilla = $this->getMock('\Assetic\Asset\BaseAsset', array(), array(), '', FALSE); + $drupally = $this->createStubFileAsset(); + + try { + $aggregate->replaceLeaf($vanilla, $drupally); + $this->fail('AggregateAsset::removeLeaf() did not throw an UnsupportedAsseticBehaviorException when provided a vanilla asset leaf.'); + } catch (UnsupportedAsseticBehaviorException $e) {} + + try { + $aggregate->replaceLeaf($vanilla, $vanilla); + $this->fail('AggregateAsset::removeLeaf() did not throw an UnsupportedAsseticBehaviorException when provided a vanilla asset leaf.'); + } catch (UnsupportedAsseticBehaviorException $e) {} + + try { + $aggregate->replaceLeaf($drupally, $vanilla); + $this->fail('AggregateAsset::removeLeaf() did not throw an UnsupportedAsseticBehaviorException when provided a vanilla asset leaf.'); + } catch (UnsupportedAsseticBehaviorException $e) {} + } + + /** + * @depends testAdd + * @covers ::load + */ + public function testLoad() { + $this->fail(); + } + + /** + * @depends testAdd + * @covers ::dump + */ + public function testDump() { + $this->fail(); + } +} + diff --git a/core/tests/Drupal/Tests/Core/Asset/AssetLibraryRepositoryTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetLibraryRepositoryTest.php new file mode 100644 index 0000000..657e89f --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/AssetLibraryRepositoryTest.php @@ -0,0 +1,253 @@ + 'Asset library repository test', + 'description' => 'Exercises methods on AssetLibraryRepository.', + 'group' => 'Asset', + ); + } + + public function createAssetLibraryRepository() { + $module_handler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'); + $module_handler->expects($this->any()) + ->method('getImplementations') + ->with('library_info') + ->will($this->returnValue(array('stub1', 'stub2'))); + + $factory = $this->getMock('Drupal\Core\Asset\Factory\AssetLibraryFactory', array(), array($module_handler)); + return new AssetLibraryRepository($factory); + } + + /** + * @covers ::__construct + * @covers ::set + */ + public function testSet() { + $repository = $this->createAssetLibraryRepository(); + $library = $this->getMock('Drupal\Core\Asset\Collection\AssetLibrary'); + $repository->set('foo0_qux/bar0.baz', $library); + + $this->assertAttributeContains($library, 'libraries', $repository); + } + + /** + * @covers ::set + * @expectedException \InvalidArgumentException + */ + public function testSetNoSlash() { + $repository = $this->createAssetLibraryRepository(); + $library = $this->getMock('Drupal\Core\Asset\Collection\AssetLibrary'); + + $repository->set('foo0_quxbar0.baz', $library); + } + + /** + * @covers ::set + * @expectedException \InvalidArgumentException + */ + public function testSetTooManySlashes() { + $repository = $this->createAssetLibraryRepository(); + $library = $this->getMock('Drupal\Core\Asset\Collection\AssetLibrary'); + + $repository->set('foo0_qux//bar0.baz', $library); + } + + /** + * @covers ::set + * @expectedException \InvalidArgumentException + */ + public function testSetInvalidKeyChars() { + $repository = $this->createAssetLibraryRepository(); + $library = $this->getMock('Drupal\Core\Asset\Collection\AssetLibrary'); + + $repository->set("\$∫≤:ˆ\"'\n\t\r", $library); + } + + /** + * @depends testSet + * @covers ::has + */ + public function testHas() { + $repository = $this->createAssetLibraryRepository(); + $library = $this->getMock('Drupal\Core\Asset\Collection\AssetLibrary'); + + $this->assertFalse($repository->has('foo/bar')); + + $repository->set('foo/bar', $library); + $this->assertTrue($repository->has('foo/bar')); + } + + /** + * @depends testSet + * @covers ::getNames + */ + public function testGetNames() { + $repository = $this->createAssetLibraryRepository(); + $library = $this->getMock('Drupal\Core\Asset\Collection\AssetLibrary'); + + $repository->set('foo/bar', $library); + $repository->set('baz/bing', $library); + + $this->assertEquals(array('foo/bar', 'baz/bing'), $repository->getNames()); + } + + /** + * @depends testSet + * @covers ::get + */ + public function testGet() { + $library = $this->getMock('Drupal\Core\Asset\Collection\AssetLibrary'); + $factory = $this->getMock('Drupal\Core\Asset\Factory\AssetLibraryFactory', array(), array(), '', FALSE); + $factory->expects($this->once()) + ->method('getLibrary') + ->with($this->equalTo('foo/bar')) + ->will($this->returnValue($library)); + + $repository = new AssetLibraryRepository($factory); + $this->assertSame($library, $repository->get('foo/bar')); + // Do it twice, for cache hit coverage. + $this->assertSame($library, $repository->get('foo/bar')); + } + + /** + * @depends testSet + * @covers ::get + * @expectedException \OutOfBoundsException + */ + public function testGetMissing() { + $repository = $this->createAssetLibraryRepository(); + $repository->get('foo/bar'); + } + + /** + * @depends testSet + * @covers ::clear + */ + public function testClear() { + $repository = $this->createAssetLibraryRepository(); + $library = $this->getMock('Drupal\Core\Asset\Collection\AssetLibrary'); + + $repository->set('foo/bar', $library); + $this->assertAttributeContains($library, 'libraries', $repository); + + $repository->clear(); + + $this->setExpectedException('\OutOfBoundsException'); + $repository->get('foo/bar'); + } + + /** + * @depends testSet + * @covers ::resolveDependencies + */ + public function testResolveDependencies() { + $repository = $this->createAssetLibraryRepository(); + + $compatible_dep = $this->createStubFileAsset(); + $incompatible_dep = $this->createStubFileAsset('js'); + $lib_dep = $this->createStubFileAsset(); + + $main_asset = $this->getMock('Drupal\Core\Asset\FileAsset', array(), array(), '', FALSE); + $main_asset->expects($this->exactly(2)) + ->method('getAssetType') + ->will($this->returnValue('css')); + $main_asset->expects($this->exactly(2)) + ->method('hasDependencies') + ->will($this->returnValue(TRUE)); + $main_asset->expects($this->exactly(2)) + ->method('getDependencyInfo') + ->will($this->returnValue(array('foo/bar', 'foo/baz'))); + $main_asset->expects($this->once()) + ->method('after')->with($compatible_dep); + + $library1 = $this->getMock('Drupal\Core\Asset\Collection\AssetLibrary'); + $library1->expects($this->once()) + ->method('hasDependencies') + ->will($this->returnValue(TRUE)); + $library1->expects($this->once()) + ->method('getDependencyInfo') + ->will($this->returnValue(array('foo/baz', 'qux/bing'))); + + $it = new \ArrayIterator(array($compatible_dep, $incompatible_dep)); + + $library1->expects($this->any()) + ->method('getIterator') + ->will($this->returnValue($it)); + + $library2 = $this->getMock('Drupal\Core\Asset\Collection\AssetLibrary'); + $library2->expects($this->once()) + ->method('getIterator') + ->will($this->returnValue(new \ArrayIterator(array()))); + // Never to ensure resolution is non-recursive + $library2->expects($this->never()) + ->method('hasDependencies'); + + $library3 = $this->getMock('Drupal\Core\Asset\Collection\AssetLibrary'); + // Never because !$library1 instanceof RelativePositionInterface + $library3->expects($this->never()) + ->method('getIterator') + ->will($this->returnValue(new \ArrayIterator(array($lib_dep)))); + // Never to ensure resolution is non-recursive + $library3->expects($this->never()) + ->method('hasDependencies') + ->will($this->returnValue(array('qux/quark'))); + + + $repository->set('foo/bar', $library1); + $repository->set('foo/baz', $library2); + $repository->set('qux/bing', $library3); + + // Ensure no auto-attach when the second param turns it off. + $this->assertEquals(array($library1, $library2), $repository->resolveDependencies($main_asset, FALSE)); + + // Now, let it auto-attach. + $this->assertEquals(array($library1, $library2), $repository->resolveDependencies($main_asset)); + // The correctness of $main_asset's predecessor data is guaranteed by the + // method counts on the mock; no direct validation is necessary. + + // This ensures that dependency resolution is non-recursive. + $this->assertEquals(array($library2, $library3), $repository->resolveDependencies($library1)); + } +} + diff --git a/core/tests/Drupal/Tests/Core/Asset/AssetUnitTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetUnitTest.php new file mode 100644 index 0000000..0814724 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/AssetUnitTest.php @@ -0,0 +1,96 @@ +getMock('Drupal\Core\Asset\FileAsset', array(), array(), '', FALSE); + $asset->expects($this->any()) + ->method('getAssetType') + ->will($this->returnValue($type)); + + $asset->expects($this->any()) + ->method('id') + ->will($this->returnValue($id ?: $this->randomName())); + + return $asset; + } + + /** + * Creates an asset metadata stub with basic values. + * + * @param string $type + * @param array $values + * + * @return AssetMetadataBag + */ + public function createStubAssetMetadata($type = 'css', $values = array()) { + $stub = $this->getMockBuilder('Drupal\Core\Asset\Metadata\AssetMetadataBag') + ->setConstructorArgs(array($type, $values)) + ->getMock(); + + $stub->expects($this->any()) + ->method('getType') + ->will($this->returnValue($type)); + + return $stub; + } + + + /** + * Generates a simple AggregateAsset mock. + * + * @param array $defaults + * Defaults to inject into the aggregate's metadata bag. + * + * @return AggregateAsset + */ + public function getAggregate($defaults = array()) { + $mockmeta = $this->createStubAssetMetadata(); + return $this->getMockForAbstractClass('Drupal\Core\Asset\Aggregate\AggregateAsset', array($mockmeta)); + } + + /** + * Creates a BaseAsset for testing purposes. + * + * @param array $defaults + * + * @return BaseAsset + */ + public function createBaseAsset($defaults = array()) { + $mockmeta = $this->createStubAssetMetadata(NULL, $defaults); + + return $this->getMockForAbstractClass('Drupal\Core\Asset\BaseAsset', array($mockmeta)); + } + +} \ No newline at end of file diff --git a/core/tests/Drupal/Tests/Core/Asset/AsseticAdapterTraitTest.php b/core/tests/Drupal/Tests/Core/Asset/AsseticAdapterTraitTest.php new file mode 100644 index 0000000..1f43760 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/AsseticAdapterTraitTest.php @@ -0,0 +1,64 @@ + 'Assetic adapter trait test', + 'description' => 'Tests that certain Assetic methods throw known exceptions in a Drupal context', + 'group' => 'Asset', + ); + } + + public function setUp() { + $this->mock = $this->getObjectForTrait('Drupal\Core\Asset\AsseticAdapterTrait'); + } + + /** + * @expectedException \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException + */ + public function testGetVars() { + $this->mock->getVars(); + } + + /** + * @expectedException \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException + */ + public function testSetValues() { + $this->mock->setValues(array()); + } + + /** + * @expectedException \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException + */ + public function testGetValues() { + $this->mock->getValues(); + } + + /** + * @expectedException \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException + */ + public function testGetLastModified() { + $this->mock->getLastModified(); + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/BaseAssetTest.php b/core/tests/Drupal/Tests/Core/Asset/BaseAssetTest.php new file mode 100644 index 0000000..8daee14 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/BaseAssetTest.php @@ -0,0 +1,60 @@ + 'Base Asset tests', + 'description' => 'Unit tests for Drupal\'s BaseAsset.', + 'group' => 'Asset', + ); + } + + public function testGetMetadata() { + $mockmeta = $this->createStubAssetMetadata(); + $asset = $this->getMockForAbstractClass('Drupal\Core\Asset\BaseAsset', array($mockmeta)); + + $this->assertSame($mockmeta, $asset->getMetadata()); + } + + public function testGetAssetType() { + $mockmeta = $this->getMock('Drupal\Core\Asset\Metadata\AssetMetadataBag', array(), array(), '', FALSE); + $mockmeta->expects($this->once()) + ->method('getType') + ->will($this->returnValue('css')); + $asset = $this->getMockForAbstractClass('Drupal\Core\Asset\BaseAsset', array($mockmeta)); + + $this->assertEquals('css', $asset->getAssetType()); + } + + public function testIsPreprocessable() { + $mockmeta = $this->getMock('Drupal\Core\Asset\Metadata\AssetMetadataBag', array(), array(), '', FALSE); + $mockmeta->expects($this->once()) + ->method('get') + ->with('preprocess') + ->will($this->returnValue(TRUE)); + $asset = $this->getMockForAbstractClass('Drupal\Core\Asset\BaseAsset', array($mockmeta)); + + $this->assertTrue($asset->isPreprocessable()); + } + + public function testClone() { + $mockmeta = $this->createStubAssetMetadata(); + $asset = $this->getMockForAbstractClass('Drupal\Core\Asset\BaseAsset', array($mockmeta)); + + $clone = clone $asset; + $this->assertNotSame($mockmeta, $clone->getMetadata()); + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/Collection/AssetCollectionTest.php b/core/tests/Drupal/Tests/Core/Asset/Collection/AssetCollectionTest.php new file mode 100644 index 0000000..5a33bf4 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/Collection/AssetCollectionTest.php @@ -0,0 +1,514 @@ + 'Asset collection tests', + 'description' => 'Unit tests on AssetCollection', + 'group' => 'Asset', + ); + } + + public function setUp() { + $this->collection = new AssetCollection(); + } + + /** + * Returns an AssetCollection, the base collection type for this unit test. + * + * @return BasicCollectionInterface + */ + public function getCollection() { + return new AssetCollection(); + } + + /** + * @covers ::add + */ + public function testAdd() { + $asset1 = $this->createStubFileAsset(); + $asset2 = $this->createStubFileAsset(); + + // test fluency + $this->assertSame($this->collection, $this->collection->add($asset1)); + $this->assertSame($this->collection, $this->collection->add($asset2)); + + $this->assertContains($asset1, $this->collection); + $this->assertContains($asset2, $this->collection); + } + + /** + * @depends testAdd + * @covers ::contains + */ + public function testContains() { + $asset = $this->createStubFileAsset(); + $this->collection->add($asset); + $this->assertTrue($this->collection->contains($asset)); + } + + /** + * @depends testAdd + * @depends testContains + * @covers ::__construct + */ + public function testCreateWithAssets() { + $asset1 = $this->createStubFileAsset(); + $asset2 = $this->createStubFileAsset(); + $collection = new AssetCollection(array($asset1, $asset2)); + + $this->assertContains($asset1, $collection); + $this->assertContains($asset2, $collection); + } + + + /** + * @depends testAdd + * @covers ::getCss + */ + public function testGetCss() { + $css = $this->createStubFileAsset('css'); + $js = $this->createStubFileAsset('js'); + + $this->collection->add($css); + $this->collection->add($js); + + $css_result = array(); + foreach ($this->collection->getCss() as $asset) { + $css_result[] = $asset; + } + + $this->assertEquals(array($css), $css_result); + } + + /** + * @depends testAdd + * @covers ::getJs + */ + public function testGetJs() { + $css = $this->createStubFileAsset('css'); + $js = $this->createStubFileAsset('js'); + + $this->collection->add($css); + $this->collection->add($js); + + $js_result = array(); + foreach ($this->collection->getJs() as $asset) { + $js_result[] = $asset; + } + + $this->assertEquals(array($js), $js_result); + } + + /** + * @depends testAdd + * @covers ::all + */ + public function testAll() { + $css = $this->createStubFileAsset('css'); + $js = $this->createStubFileAsset('js'); + + $this->collection->add($css); + $this->collection->add($js); + + $this->assertEquals(array($css->id() => $css, $js->id() => $js), $this->collection->all()); + } + + /** + * @depends testAdd + * @covers ::remove + */ + public function testRemoveByAsset() { + $stub = $this->createStubFileAsset(); + + $this->collection->add($stub); + $this->collection->remove($stub); + + $this->assertNotContains($stub, $this->collection); + } + + /** + * @depends testAdd + * @covers ::remove + */ + public function testRemoveById() { + $stub = $this->createStubFileAsset(); + + $this->collection->add($stub); + $this->collection->remove($stub->id()); + + $this->assertNotContains($stub, $this->collection); + } + + /** + * @expectedException \OutOfBoundsException + * @covers ::remove + */ + public function testRemoveNonexistentId() { + $this->assertFalse($this->collection->remove('foo', TRUE)); + $this->collection->remove('foo'); + } + + /** + * @expectedException \OutOfBoundsException + * @covers ::remove + */ + public function testRemoveNonexistentAsset() { + $stub = $this->createStubFileAsset(); + $this->assertFalse($this->collection->remove($stub, TRUE)); + $this->collection->remove($stub); + } + + /** + * Tests that all methods that should be disabled by freezing the collection + * correctly trigger an exception. + * + * @covers ::freeze + * @covers ::isFrozen + * @covers ::attemptWrite + */ + public function testExceptionOnWriteWhenFrozen() { + $stub = $this->createStubFileAsset(); + $write_protected = array( + 'add' => array($stub), + 'remove' => array($stub), + 'replace' => array($stub, $this->createStubFileAsset()), + 'mergeCollection' => array($this->getMock('Drupal\Core\Asset\Collection\AssetCollection')), + 'uksort' => array(function() {}), + 'ksort' => array(), + 'reverse' => array(), + 'addUnresolvedLibrary' => array('foo/bar'), + 'clearUnresolvedLibraries' => array(), + 'resolveLibraries' => array($this->getMock('Drupal\Core\Asset\AssetLibraryRepository', array(), array(), '', FALSE)), + ); + + // No exception before freeze + list($method, $args) = each($write_protected); + call_user_func_array(array($this->collection, $method), $args); + + $this->collection->freeze(); + foreach ($write_protected as $method => $args) { + try { + call_user_func_array(array($this->collection, $method), $args); + $this->fail(sprintf('Was able to run write method "%s" on frozen AssetCollection', $method)); + } catch (FrozenObjectException $e) {} + } + } + + /** + * @depends testAdd + * @covers ::find + * @expectedException OutOfBoundsException + */ + public function testFind() { + $metamock = $this->createStubAssetMetadata(); + + $asset = $this->getMock('Drupal\Core\Asset\FileAsset', array(), array($metamock, 'foo')); + $asset->expects($this->exactly(2)) // once on add, once on searching + ->method('id') + ->will($this->returnValue('foo')); + + $this->collection->add($asset); + $this->assertSame($asset, $this->collection->find('foo')); + + // Nonexistent asset + $this->assertFalse($this->collection->find('bar')); + + // Nonexistent asset, non-graceful + $this->collection->find('bar', FALSE); + } + + /** + * @depends testAdd + * @covers ::uksort + */ + public function testUkSort() { + $stub1 = $this->createStubFileAsset(); + $stub2 = $this->createStubFileAsset(); + $stub3 = $this->createStubFileAsset(); + + $this->collection->add($stub1); + $this->collection->add($stub2); + $this->collection->add($stub3); + + $assets = array( + $stub1->id() => $stub1, + $stub2->id() => $stub2, + $stub3->id() => $stub3, + ); + + $dummysort = function ($a, $b) { + return strnatcasecmp($a, $b); + }; + + $this->assertSame($this->collection, $this->collection->uksort($dummysort)); + uksort($assets, $dummysort); + $this->assertEquals($assets, $this->collection->all()); + } + + /** + * @depends testAdd + * @covers ::ksort + */ + public function testKsort() { + $stub1 = $this->createStubFileAsset(); + $stub2 = $this->createStubFileAsset(); + $stub3 = $this->createStubFileAsset(); + + $this->collection->add($stub1); + $this->collection->add($stub2); + $this->collection->add($stub3); + + $assets = array( + $stub1->id() => $stub1, + $stub2->id() => $stub2, + $stub3->id() => $stub3, + ); + + $this->assertSame($this->collection, $this->collection->ksort()); + ksort($assets); + $this->assertEquals($assets, $this->collection->all()); + } + + /** + * @depends testAdd + * @covers ::reverse + */ + public function testReverse() { + $stub1 = $this->createStubFileAsset(); + $stub2 = $this->createStubFileAsset(); + $stub3 = $this->createStubFileAsset(); + + $this->collection->add($stub1); + $this->collection->add($stub2); + $this->collection->add($stub3); + + $assets = array( + $stub3->id() => $stub3, + $stub2->id() => $stub2, + $stub1->id() => $stub1, + ); + + $this->assertSame($this->collection, $this->collection->reverse()); + $this->assertEquals($assets, $this->collection->all()); + } + + /** + * @covers ::addUnresolvedLibrary + */ + public function testAddUnresolvedLibrary() { + $this->assertSame($this->collection, $this->collection->addUnresolvedLibrary('foo/bar')); + + $this->assertAttributeContains('foo/bar', 'libraries', $this->collection); + } + + /** + * @depends testAddUnresolvedLibrary + * @covers ::hasUnresolvedLibraries + */ + public function testHasUnresolvedLibraries() { + $this->assertFalse($this->collection->hasUnresolvedLibraries()); + + $this->collection->addUnresolvedLibrary('foo/bar'); + + $this->assertTrue($this->collection->hasUnresolvedLibraries()); + } + + /** + * @depends testAddUnresolvedLibrary + * @depends testHasUnresolvedLibraries + * @covers ::clearUnresolvedLibraries + */ + public function testClearUnresolvedLibraries() { + $this->collection->addUnresolvedLibrary('foo/bar'); + $this->assertSame($this->collection, $this->collection->clearUnresolvedLibraries()); + + $this->assertFalse($this->collection->hasUnresolvedLibraries()); + } + + /** + * @depends testAddUnresolvedLibrary + * @covers ::getUnresolvedLibraries + */ + public function testGetUnresolvedLibraries() { + $this->collection->addUnresolvedLibrary('foo/bar'); + + $this->assertEquals(array('foo/bar'), $this->collection->getUnresolvedLibraries()); + } + + /** + * @depends testAdd + * @depends testContains + * @depends testAddUnresolvedLibrary + * @depends testClearUnresolvedLibraries + * @depends testGetUnresolvedLibraries + * @covers ::resolveLibraries + */ + public function testResolveLibrariesDirectLibraries() { + $lib_asset1 = $this->getMockBuilder('Drupal\Core\Asset\AssetInterface') + ->disableOriginalConstructor() + ->setMethods(array('id')) + ->setMockClassName('lib_asset_mock1') + ->getMockForAbstractClass(); + $lib_asset1->expects($this->any()) + ->method('id') + ->will($this->returnValue($this->randomName())); + + $lib_asset2 = $this->getMockBuilder('Drupal\Core\Asset\AssetInterface') + ->disableOriginalConstructor() + ->setMethods(array('id')) + ->setMockClassName('lib_asset_mock2') + ->getMockForAbstractClass(); + $lib_asset2->expects($this->any()) + ->method('id') + ->will($this->returnValue($this->randomName())); + + $it1 = new \ArrayIterator(array($lib_asset1, $lib_asset2)); + $lib1 = $this->getMock('Drupal\Core\Asset\Collection\AssetLibrary'); + $lib1->expects($this->once()) + ->method('getIterator') + ->will($this->returnValue($it1)); + + $repository = $this->getMock('Drupal\Core\Asset\AssetLibraryRepository', array(), array(), '', FALSE); + $repository->expects($this->once()) + ->method('get')->with('foo/bar') + ->will($this->returnValue($lib1)); + + $this->collection->addUnresolvedLibrary('foo/bar'); + $this->collection->resolveLibraries($repository); + + $expected = array( + $lib_asset1->id() => $lib_asset1, + $lib_asset2->id() => $lib_asset2, + ); + $this->assertEquals($expected, $this->collection->all()); + $this->assertFalse($this->collection->hasUnresolvedLibraries()); + } + + /** + * @depends testAdd + * @depends testAll + * @depends testAddUnresolvedLibrary + * @depends testClearUnresolvedLibraries + * @depends testGetUnresolvedLibraries + * @covers ::resolveLibraries + */ + public function testResolveLibrariesAgain() { + $coll_asset = $this->getMockBuilder('Drupal\Core\Asset\BaseAsset') + ->disableOriginalConstructor() + ->setMethods(array('id')) + ->setMockClassName('coll_asset') + ->getMockForAbstractClass(); + $coll_asset->expects($this->any()) + ->method('id') + ->will($this->returnValue($this->randomName())); + + $direct_lib_asset = $this->getMockBuilder('Drupal\Core\Asset\BaseAsset') + ->disableOriginalConstructor() + ->setMethods(array('id')) + ->setMockClassName('direct_lib_asset') + ->getMockForAbstractClass(); + $direct_lib_asset->expects($this->any()) + ->method('id') + ->will($this->returnValue($this->randomName())); + + $indirect_lib_asset = $this->getMockBuilder('Drupal\Core\Asset\BaseAsset') + ->disableOriginalConstructor() + ->setMethods(array('id')) + ->setMockClassName('indirect_lib_asset') + ->getMockForAbstractClass(); + $indirect_lib_asset->expects($this->any()) + ->method('id') + ->will($this->returnValue($this->randomName())); + + $direct_lib = $this->getMock('Drupal\Core\Asset\Collection\AssetLibrary'); + $direct_lib->expects($this->once()) + ->method('getIterator') + ->will($this->returnValue(new \ArrayIterator(array($direct_lib_asset)))); + + $indirect_lib = $this->getMock('Drupal\Core\Asset\Collection\AssetLibrary'); + $indirect_lib->expects($this->once()) + ->method('getIterator') + ->will($this->returnValue(new \ArrayIterator(array($indirect_lib_asset)))); + + $repository = $this->getMock('Drupal\Core\Asset\AssetLibraryRepository', array(), array(), '', FALSE); + $repository->expects($this->at(0)) + ->method('resolveDependencies')->with($coll_asset) + ->will($this->returnValue(array($direct_lib))); + $repository->expects($this->at(1)) + ->method('resolveDependencies')->with($direct_lib_asset) + ->will($this->returnValue(array($indirect_lib))); + $repository->expects($this->at(2)) + ->method('resolveDependencies')->with($indirect_lib_asset) + ->will($this->returnValue(array())); + + $this->collection->add($coll_asset); + $this->assertSame($this->collection, $this->collection->resolveLibraries($repository)); + + $expected = array( + $coll_asset->id() => $coll_asset, + $direct_lib_asset->id() => $direct_lib_asset, + $indirect_lib_asset->id() => $indirect_lib_asset, + ); + + $this->assertEquals($expected, $this->collection->all()); + } + + /** + * @depends testAdd + * @depends testAddUnresolvedLibrary + * @depends testGetUnresolvedLibraries + * @covers ::mergeCollection + */ + public function testMergeCollection() { + $coll2 = new AssetCollection(); + $stub1 = $this->createStubFileAsset(); + $stub2 = $this->createStubFileAsset(); + + $coll2->add($stub1); + $coll2->addUnresolvedLibrary('foo/bar'); + // Assert same to check fluency + $this->assertSame($this->collection, $this->collection->mergeCollection($coll2)); + + $this->assertEquals(array('foo/bar'), $this->collection->getUnresolvedLibraries()); + $this->assertContains($stub1, $this->collection); + $this->assertTrue($coll2->isFrozen()); + + $coll3 = new AssetCollection(); + $coll3->add($stub1); + $coll3->add($stub2); + $coll3->addUnresolvedLibrary('foo/bar'); + // Ensure no duplicates, and don't freeze merged bag + $this->collection->mergeCollection($coll3, FALSE); + + $this->assertEquals(array('foo/bar'), $this->collection->getUnresolvedLibraries()); + $contained = array( + $stub1->id() => $stub1, + $stub2->id() => $stub2, + ); + $this->assertEquals($contained, $this->collection->all()); + $this->assertFalse($coll3->isFrozen()); + } +} + diff --git a/core/tests/Drupal/Tests/Core/Asset/Collection/AssetLibraryTest.php b/core/tests/Drupal/Tests/Core/Asset/Collection/AssetLibraryTest.php new file mode 100644 index 0000000..a39010a --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/Collection/AssetLibraryTest.php @@ -0,0 +1,114 @@ + 'Asset Library tests', + 'description' => 'Tests that the AssetLibrary behaves correctly.', + 'group' => 'Asset', + ); + } + + public function getLibraryFixture() { + $library = new AssetLibrary(); + $library->setTitle('foo') + ->setVersion('1.2.3') + ->setWebsite('http://foo.bar'); + return $library; + } + + /** + * These simply don't merit individual tests. + * + * @covers ::setWebsite + * @covers ::getWebsite + * @covers ::setVersion + * @covers ::getVersion + * @covers ::setTitle + * @covers ::getTitle + */ + public function testMetadataProps() { + $library = $this->getLibraryFixture(); + + $this->assertEquals('foo', $library->getTitle()); + $this->assertEquals('1.2.3', $library->getVersion()); + $this->assertEquals('http://foo.bar', $library->getWebsite()); + } + + /** + * @covers ::addDependency + */ + public function testAddDependency() { + $library = $this->getLibraryFixture(); + + $this->assertSame($library, $library->addDependency('foo/bar')); + $this->assertAttributeContains('foo/bar', 'dependencies', $library); + + $invalid = array('foo', 'foo//bar', 0, 1.1, fopen(__FILE__, 'r'), TRUE, array(), new \stdClass); + + try { + foreach ($invalid as $val) { + $library->addDependency($val, $val); + $this->fail('Was able to create an ordering relationship with an inappropriate value.'); + } + } catch (\InvalidArgumentException $e) {} + } + + /** + * @depends testAddDependency + * @covers ::clearDependencies + */ + public function testClearDependencies() { + $library = $this->getLibraryFixture(); + $library->addDependency('foo/bar'); + + $this->assertSame($library, $library->clearDependencies()); + $this->assertFalse($library->hasDependencies()); + } + + /** + * Tests that all methods that should be disabled by freezing the collection + * correctly trigger an exception. + * + * @covers ::freeze + * @covers ::isFrozen + * @covers ::attemptWrite + */ + public function testExceptionOnWriteWhenFrozen() { + $library = new AssetLibrary(); + $write_protected = array( + 'setTitle' => array('foo'), + 'setVersion' => array('foo'), + 'setWebsite' => array('foo'), + 'addDependency' => array('foo/bar'), + 'clearDependencies' => array(function() {}), + ); + + // No exception before freeze + list($method, $args) = each($write_protected); + call_user_func_array(array($library, $method), $args); + + $library->freeze(); + foreach ($write_protected as $method => $args) { + try { + call_user_func_array(array($library, $method), $args); + $this->fail(sprintf('Was able to run write method "%s" on frozen AssetLibrary', $method)); + } catch (FrozenObjectException $e) {} + } + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/Collection/BasicCollectionTraitTest.php b/core/tests/Drupal/Tests/Core/Asset/Collection/BasicCollectionTraitTest.php new file mode 100644 index 0000000..62c5cda --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/Collection/BasicCollectionTraitTest.php @@ -0,0 +1,457 @@ + 'BasicCollectionTrait unit tests', + 'description' => 'Unit tests for BasicCollectionTrait', + 'group' => 'Asset', + ); + } + + /** + * Generates a simple BasicCollectionTrait mock. + * + * @return BasicCollectionTrait + */ + public function getBasicCollection() { + return new BasicCollectionTraitStub(); + } + + /** + * Method to return the appropriate collection type for the current test. + * + * If demonstrating adherence to Liskov is desired, this test class can be + * extended and this method swapped out to provide the correct + * BasicCollectionInterface object for testing. + * + * @return BasicCollectionInterface + */ + public function getCollection() { + return $this->getBasicCollection(); + } + + /** + * Generates a AggregateAsset mock with three leaf assets. + */ + public function getThreeLeafBasicCollection() { + $collection = $this->getCollection(); + $nested_aggregate = $this->getAggregate(); + + foreach (array('foo', 'bar', 'baz') as $var) { + $$var = $this->createStubFileAsset('css', $var); + } + + $nested_aggregate->add($foo); + $nested_aggregate->add($bar); + $collection->add($nested_aggregate); + $collection->add($baz); + + return array($collection, $foo, $bar, $baz, $nested_aggregate); + } + + /** + * This uses PHPUnit's reflection-based assertions rather than assertContains + * so that this test can honestly sit at the root of the test method + * dependency tree. + * + * @covers ::add + */ + public function testAdd() { + $collection = $this->getCollection(); + $asset = $this->createStubFileAsset(); + $this->assertSame($collection, $collection->add($asset)); + + $this->assertAttributeContains($asset, 'assetStorage', $collection); + $this->assertAttributeContains($asset, 'assetIdMap', $collection); + + // Nesting: add an aggregate to the first aggregate. + $nested_aggregate = $this->getAggregate(); + $collection->add($nested_aggregate); + + $this->assertAttributeContains($nested_aggregate, 'assetStorage', $collection); + $this->assertAttributeContains($nested_aggregate, 'assetIdMap', $collection); + $this->assertAttributeContains($nested_aggregate, 'nestedStorage', $collection); + } + + /** + * @expectedException \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException + * @covers ::add + */ + public function testVanillaAsseticAdd() { + $vanilla = $this->getMock('\Assetic\Asset\BaseAsset', array(), array(), '', FALSE); + $this->getCollection()->add($vanilla); + } + + /** + * @depends testAdd + * @covers ::each + * @covers ::getIterator + * @covers \Drupal\Core\Asset\Collection\Iterator\RecursiveBasicCollectionIterator + */ + public function testEach() { + list($collection, $foo, $bar, $baz, $nested_aggregate) = $this->getThreeLeafBasicCollection(); + + $contained = array(); + foreach ($collection->each() as $leaf) { + $contained[] = $leaf; + } + $this->assertEquals(array($nested_aggregate, $foo, $bar, $baz), $contained); + } + + /** + * @depends testAdd + * @covers ::eachLeaf + * @covers \Drupal\Core\Asset\Collection\Iterator\RecursiveBasicCollectionIterator + */ + public function testEachLeaf() { + list($collection, $foo, $bar, $baz) = $this->getThreeLeafBasicCollection(); + + $contained = array(); + foreach ($collection->eachLeaf() as $leaf) { + $contained[] = $leaf; + } + $this->assertEquals(array($foo, $bar, $baz), $contained); + } + + /** + * @depends testAdd + * @covers ::contains + */ + public function testContains() { + $collection = $this->getCollection(); + $asset = $this->createStubFileAsset(); + $collection->add($asset); + + $this->assertTrue($collection->contains($asset)); + + // Nesting: add an aggregate to the first aggregate. + $nested_aggregate = $this->getAggregate(); + $nested_asset = $this->createStubFileAsset(); + + $nested_aggregate->add($nested_asset); + $collection->add($nested_aggregate); + + $this->assertTrue($collection->contains($nested_asset)); + } + + /** + * @covers ::find + * @expectedException \OutOfBoundsException + */ + public function testFind() { + $collection = $this->getCollection(); + + $asset = $this->createStubFileAsset(); + $collection->add($asset); + $this->assertSame($asset, $collection->find($asset->id())); + + $nested_aggregate = $this->getAggregate(); + $nested_asset = $this->createStubFileAsset(); + + $nested_aggregate->add($nested_asset); + $collection->add($nested_aggregate); + + $this->assertSame($nested_asset, $collection->find($nested_asset->id())); + + // Nonexistent asset + $this->assertFalse($collection->find('bar')); + + // Nonexistent asset, non-graceful + $collection->find('bar', FALSE); + } + + /** + * @depends testAdd + * @covers ::all + */ + public function testAll() { + $collection = $this->getCollection(); + + $asset1 = $this->createStubFileAsset(); + $asset2 = $this->createStubFileAsset(); + $collection->add($asset1); + $collection->add($asset2); + + $output = array( + $asset1->id() => $asset1, + $asset2->id() => $asset2, + ); + + $this->assertEquals($output, $collection->all()); + + // Ensure that only top-level assets are returned. + $nested_aggregate = $this->getAggregate(); + $nested_aggregate->add($this->createStubFileAsset()); + $collection->add($nested_aggregate); + + $output[$nested_aggregate->id()] = $nested_aggregate; + $this->assertEquals($output, $collection->all()); + } + + /** + * @depends testEach + * @covers ::remove + * @covers ::doRemove + */ + public function testRemove() { + list($collection, $foo, $bar, $baz, $nested_aggregate) = $this->getThreeLeafBasicCollection(); + $this->assertFalse($collection->remove('arglebargle', TRUE)); + $this->assertTrue($collection->remove('foo')); + + $this->assertNotContains($foo, $collection); + $this->assertContains($bar, $collection); + $this->assertContains($baz, $collection); + + $this->assertTrue($collection->remove($bar)); + + $this->assertNotContains($bar, $collection); + $this->assertContains($baz, $collection); + + $this->assertTrue($collection->remove($nested_aggregate)); + $this->assertNotContains($nested_aggregate, $collection); + } + + /** + * @depends testAdd + * @depends testRemove + * @covers ::count + */ + public function testCount() { + $collection = $this->getCollection(); + $this->assertCount(0, $collection); + + $collection->add($this->getAggregate()); + $this->assertCount(0, $collection); + + $aggregate = $this->getAggregate(); + $asset = $this->createStubFileAsset(); + $aggregate->add($asset); + $collection->add($aggregate); + $this->assertCount(1, $collection); + + $collection->remove($aggregate); + $this->assertCount(0, $collection); + + $collection->add($asset); + $this->assertCount(1, $collection); + + $collection->remove($asset); + $this->assertCount(0, $collection); + } + + /** + * Tests that adding the same asset twice results in just one asset. + * + * @depends testAdd + * @depends testCount + * @covers ::add + */ + public function testDoubleAdd() { + $collection = $this->getCollection(); + $asset = $this->createStubFileAsset(); + + $collection->add($asset); + + // Test by object identity + $collection->add($asset); + $this->assertCount(1, $collection); + + // Test by id + $asset2 = $this->createStubFileAsset('css', $asset->id()); + + $collection->add($asset2); + $this->assertCount(1, $collection); + } + + /** + * @depends testEach + * @covers ::remove + * @covers ::doRemove + * @expectedException \OutOfBoundsException + */ + public function testRemoveNonexistentNeedle() { + list($collection) = $this->getThreeLeafBasicCollection(); + // Nonexistent leaf removal returns FALSE in graceful mode + $this->assertFalse($collection->remove($this->createStubFileAsset(), TRUE)); + + // In non-graceful mode, an exception is thrown. + $collection->remove($this->createStubFileAsset()); + } + + /** + * @depends testEach + * @depends testEachLeaf + * @covers ::replace + * @covers ::doReplace + */ + public function testReplace() { + list($collection, $foo, $bar, $baz, $nested_aggregate) = $this->getThreeLeafBasicCollection(); + $qux = $this->createStubFileAsset('css', 'qux'); + + $this->assertFalse($collection->replace('arglebargle', $qux, TRUE)); + $this->assertTrue($collection->replace('foo', $qux)); + + $this->assertContains($qux, $collection); + $this->assertNotContains($foo, $collection); + + $contained = array(); + foreach ($collection->eachLeaf() as $leaf) { + $contained[] = $leaf; + } + $this->assertEquals(array($qux, $bar, $baz), $contained); + + $this->assertTrue($collection->replace($bar, $foo)); + + $this->assertContains($foo, $collection); + $this->assertNotContains($bar, $collection); + + $contained = array(); + foreach ($collection->eachLeaf() as $leaf) { + $contained[] = $leaf; + } + $this->assertEquals(array($qux, $foo, $baz), $contained); + + $aggregate2 = $this->getAggregate(); + $this->assertTrue($collection->replace($baz, $aggregate2)); + + $this->assertContains($aggregate2, $collection); + $this->assertNotContains($baz, $collection); + + $contained = array(); + foreach ($collection->eachLeaf() as $leaf) { + $contained[] = $leaf; + } + $this->assertEquals(array($qux, $foo), $contained); + + $contained = array(); + foreach ($collection->each() as $leaf) { + $contained[] = $leaf; + } + $this->assertEquals(array($nested_aggregate, $qux, $foo, $aggregate2), $contained); + } + + /** + * @depends testEach + * @covers ::replace + * @covers ::doReplace + * @expectedException \OutOfBoundsException + */ + public function testReplaceNonexistentNeedle() { + list($collection) = $this->getThreeLeafBasicCollection(); + // Nonexistent leaf replacement returns FALSE in graceful mode + $qux = $this->createStubFileAsset(); + $this->assertFalse($collection->replace($this->createStubFileAsset(), $qux, TRUE)); + $this->assertNotContains($qux, $collection); + + // In non-graceful mode, an exception is thrown. + $collection->replace($this->createStubFileAsset(), $qux); + } + + /** + * @depends testEach + * @covers ::replace + * @expectedException \LogicException + */ + public function testReplaceWithAlreadyPresentAsset() { + list($aggregate, $foo) = $this->getThreeLeafBasicCollection(); + $aggregate->replace($this->createStubFileAsset(), $foo); + } + + /** + * @depends testAdd + * @depends testReplaceWithAlreadyPresentAsset + * @covers ::replace + * @expectedException \LogicException + * + * This fails on the same check that testReplaceWithAlreadyPresentAsset, + * but it is demonstrated as its own test for clarity. + */ + public function testReplaceWithSelf() { + list($collection, $foo) = $this->getThreeLeafBasicCollection(); + $collection->replace($foo, $foo); + } + + /** + * @depends testAdd + * @depends testRemove + * @covers ::isEmpty + */ + public function testIsEmpty() { + $collection = $this->getCollection(); + $this->assertTrue($collection->isEmpty()); + + // Collections containing only empty collections are considered empty. + $collection->add($this->getAggregate()); + $this->assertTrue($collection->isEmpty()); + + $aggregate = $this->getAggregate(); + $asset = $this->createStubFileAsset(); + $aggregate->add($asset); + $collection->add($aggregate); + $this->assertFalse($collection->isEmpty()); + + $collection->remove($aggregate); + $this->assertTrue($collection->isEmpty()); + + $collection->add($asset); + $this->assertFalse($collection->isEmpty()); + + $collection->remove($asset); + $this->assertTrue($collection->isEmpty()); + } + + /** + * @covers ::remove + */ + public function testRemoveInvalidNeedle() { + $collection = $this->getCollection(); + $invalid = array(0, 1.1, fopen(__FILE__, 'r'), TRUE, array(), new \stdClass); + + try { + foreach ($invalid as $val) { + $collection->remove($val); + $this->fail('BasicCollectionTrait::remove() did not throw exception on invalid argument type for $needle.'); + } + } catch (\InvalidArgumentException $e) {} + } + + /** + * @covers ::replace + */ + public function testReplaceInvalidNeedle() { + $collection = $this->getCollection(); + $invalid = array(0, 1.1, fopen(__FILE__, 'r'), TRUE, array(), new \stdClass); + + try { + foreach ($invalid as $val) { + $collection->replace($val, $this->createStubFileAsset()); + $this->fail('BasicCollectionTrait::replace() did not throw exception on invalid argument type for $needle.'); + } + } catch (\InvalidArgumentException $e) {} + } + +} + +class BasicCollectionTraitStub implements \IteratorAggregate, BasicCollectionInterface { + use BasicCollectionTrait { + _bcinit as public __construct; + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/DependencyTraitTest.php b/core/tests/Drupal/Tests/Core/Asset/DependencyTraitTest.php new file mode 100644 index 0000000..9f7c81a --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/DependencyTraitTest.php @@ -0,0 +1,87 @@ + 'Dependency trait test', + 'description' => 'Tests that the boilerplate implementation of DependencyInterface by DependencyTrait works correctly.', + 'group' => 'Asset', + ); + } + + public function setUp() { + $this->mock = $this->getObjectForTrait('Drupal\Core\Asset\DependencyTrait'); + } + + /** + * @covers ::addDependency + */ + public function testAddDependency() { + $this->assertSame($this->mock, $this->mock->addDependency('foo/bar')); + $this->assertAttributeContains('foo/bar', 'dependencies', $this->mock); + + $invalid = array('foo', 'foo//bar', 0, 1.1, fopen(__FILE__, 'r'), TRUE, array(), new \stdClass); + + try { + foreach ($invalid as $val) { + $this->mock->addDependency($val, $val); + $this->fail('Was able to create an ordering relationship with an inappropriate value.'); + } + } catch (\InvalidArgumentException $e) {} + } + + /** + * @depends testAddDependency + * @covers ::hasDependencies + */ + public function testHasDependencies() { + $this->assertFalse($this->mock->hasDependencies()); + + $this->mock->addDependency('foo/bar'); + $this->assertTrue($this->mock->hasDependencies()); + } + + /** + * @depends testAddDependency + * @covers ::getDependencyInfo + */ + public function testGetDependencyInfo() { + $this->assertEmpty($this->mock->getDependencyInfo()); + + $this->mock->addDependency('foo/bar'); + $this->assertEquals(array('foo/bar'), $this->mock->getDependencyInfo()); + } + + /** + * @depends testAddDependency + * @depends testHasDependencies + * @covers ::clearDependencies + */ + public function testClearDependencies() { + $this->mock->addDependency('foo/bar'); + + $this->assertSame($this->mock, $this->mock->clearDependencies()); + $this->assertFalse($this->mock->hasDependencies()); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Asset/ExternalAssetTest.php b/core/tests/Drupal/Tests/Core/Asset/ExternalAssetTest.php new file mode 100644 index 0000000..7ba786a --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/ExternalAssetTest.php @@ -0,0 +1,57 @@ + 'File asset tests', + 'description' => 'Unit tests for FileAsset', + 'group' => 'Asset', + ); + } + + public function testInitialCreation() { + $meta = $this->createStubAssetMetadata(); + $asset = new ExternalAsset($meta, self::JQUERY); + + $this->assertEquals(self::JQUERY, $asset->id()); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testCreateMalformedUrl() { + $meta = $this->createStubAssetMetadata(); + new ExternalAsset($meta, __FILE__); + } + + public function testLoad() { + $meta = $this->createStubAssetMetadata(); + $asset = new ExternalAsset($meta, self::JQUERY); + + // TODO this throws an exception, but it should not. test fails till we fix. + $asset->load(); + } + + public function testDump() { + $meta = $this->createStubAssetMetadata(); + $asset = new ExternalAsset($meta, self::JQUERY); + + // TODO this throws an exception, but it should not. test fails till we fix. + $asset->dump(); + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/Factory/AssetCollectorTest.php b/core/tests/Drupal/Tests/Core/Asset/Factory/AssetCollectorTest.php new file mode 100644 index 0000000..8c01166 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/Factory/AssetCollectorTest.php @@ -0,0 +1,291 @@ + 'Asset Collector tests', + 'description' => 'Tests that the AssetCollector system works correctly.', + 'group' => 'Asset', + ); + } + + public function setUp() { + parent::setUp(); + $this->collector = new AssetCollector(); + } + + /** + * Tests that constructor-injected params end up in the right place. + */ + public function testConstructorInjection() { + $factory = $this->getMock('Drupal\Core\Asset\Metadata\DefaultAssetMetadataFactory'); + $collection = $this->getMock('Drupal\Core\Asset\Collection\AssetCollection'); + + $collector = new AssetCollector($collection, $factory); + + $this->assertAttributeSame($collection, 'collection', $collector); + $this->assertAttributeSame($factory, 'metadataFactory', $collector); + } + + /** + * Tests that the collector injects provided metadata to created assets. + */ + public function testMetadataInjection() { + $asset = $this->collector->create('css', 'file', 'foo', array('group' => CSS_AGGREGATE_THEME)); + $meta = $asset->getMetadata(); + $this->assertEquals(CSS_AGGREGATE_THEME, $meta->get('group'), 'Collector injected user-passed parameters into the created asset.'); + } + + public function testDefaultPropagation() { + // Test that defaults are correctly applied by the factory. + $meta = new AssetMetadataBag('css', array('every_page' => TRUE, 'group' => CSS_AGGREGATE_THEME)); + $factory = $this->getMock('Drupal\Core\Asset\Metadata\DefaultAssetMetadataFactory'); + $factory->expects($this->once()) + ->method('createCssMetadata') + ->will($this->returnValue($meta)); + + $this->collector->setMetadataFactory($factory); + $css1 = $this->collector->create('css', 'file', 'foo'); + + $asset_meta = $css1->getMetadata(); + $this->assertTrue($asset_meta->get('every_page')); + $this->assertEquals(CSS_AGGREGATE_THEME, $asset_meta->get('group')); + } + + /** + * @expectedException \RuntimeException + */ + public function testExceptionOnAddingAssetWithoutCollectionPresent() { + $asset = $this->collector->create('css', 'string', 'foo'); + $this->collector->add($asset); + } + + /** + * TODO separate test for an explicit add() call. + */ + public function testAssetsImplicitlyArriveInInjectedCollection() { + $collection = new AssetCollection(); + $this->collector->setCollection($collection); + + $asset = $this->collector->create('css', 'file', 'bar'); + $this->assertContains($asset, $collection->getCss(), 'Created asset was implicitly added to collection.'); + } + + public function testAddAssetExplicitly() { + $collection = new AssetCollection(); + $this->collector->setCollection($collection); + + $mock = $this->createStubFileAsset('css'); + $this->collector->add($mock); + + $this->assertContains($mock, $collection); + } + + public function testSetCollection() { + $collection = new AssetCollection(); + $this->collector->setCollection($collection); + $this->assertTrue($this->collector->hasCollection()); + } + + public function testClearCollection() { + $collection = new AssetCollection(); + $this->collector->setCollection($collection); + $this->collector->clearCollection(); + $this->assertFalse($this->collector->hasCollection()); + } + + public function testLock() { + $this->assertTrue($this->collector->lock($this), 'Collector locked successfully.'); + $this->assertTrue($this->collector->isLocked(), 'Collector accurately reports that it is locked via isLocked() method.'); + } + + public function testUnlock() { + $this->collector->lock($this); + $this->assertTrue($this->collector->unlock($this), 'Collector unlocked successfully when appropriate key was provided.'); + $this->assertFalse($this->collector->isLocked(), 'Collector correctly reported unlocked state via isLocked() method after unlocking.'); + } + + /** + * @expectedException \Drupal\Core\Asset\Exception\LockedObjectException + */ + public function testUnlockFailsWithoutCorrectSecret() { + $this->collector->lock('foo'); + $this->collector->unlock('bar'); + } + + /** + * @expectedException \Drupal\Core\Asset\Exception\LockedObjectException + */ + public function testUnlockFailsIfNotLocked() { + $this->collector->unlock('foo'); + } + + /** + * @expectedException \Drupal\Core\Asset\Exception\LockedObjectException + */ + public function testLockFailsIfLocked() { + $this->collector->lock('foo'); + $this->collector->lock('error'); + } + + /** + * @expectedException \Drupal\Core\Asset\Exception\LockedObjectException + */ + public function testLockingPreventsSettingDefaults() { + $this->collector->lock($this); + $this->collector->setMetadataFactory($this->getMock('Drupal\Core\Asset\Metadata\DefaultAssetMetadataFactory')); + } + + /** + * @expectedException \Drupal\Core\Asset\Exception\LockedObjectException + */ + public function testLockingPreventsRestoringDefaults() { + $this->collector->lock($this); + $this->collector->restoreDefaults(); + } + + /** + * @expectedException \Drupal\Core\Asset\Exception\LockedObjectException + */ + public function testLockingPreventsClearingCollection() { + $this->collector->lock($this); + $this->collector->clearCollection(); + } + + /** + * @expectedException \Drupal\Core\Asset\Exception\LockedObjectException + */ + public function testLockingPreventsSettingCollection() { + $this->collector->lock($this); + $this->collector->setCollection(new AssetCollection()); + } + + public function testChangeAndRestoreDefaults() { + // TODO this test is now in fuzzy territory - kinda more the factory's responsibility + $default_factory = new DefaultAssetMetadataFactory(); + // Ensure we're in a good state first + $this->assertEquals($default_factory->createCssMetadata('file', 'foo/bar.css'), $this->collector->getMetadataDefaults('css', 'file', 'foo/bar.css')); + + $changed_css = new AssetMetadataBag('css', array('foo' => 'bar', 'every_page' => TRUE)); + $factory = $this->getMock('Drupal\Core\Asset\Metadata\DefaultAssetMetadataFactory'); + $factory->expects($this->exactly(2)) + ->method('createCssMetadata') + ->will($this->returnValue(clone $changed_css)); + + $this->collector->setMetadataFactory($factory); + + $this->assertEquals($changed_css, $this->collector->getMetadataDefaults('css', 'file', 'foo/bar.css')); + // TODO this is totally cheating, only passes because we clone earlier. but it should be a guarantee of the interface...how to test this? + $this->assertNotSame($changed_css, $this->collector->getMetadataDefaults('css', 'file', 'foo/bar.css'), 'New metadata instance is created on retrieval from collector.'); + + $this->collector->restoreDefaults(); + $this->assertEquals($default_factory->createCssMetadata('file', 'foo/bar.css'), $this->collector->getMetadataDefaults('css', 'file', 'foo/bar.css')); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testGetNonexistentDefault() { + $this->collector->getMetadataDefaults('foo', 'file', 'foo/bar.css'); + } + + + public function testCreateCssFileAsset() { + $css_file = $this->collector->create('css', 'file', 'foo'); + $this->assertInstanceOf('\Drupal\Core\Asset\FileAsset', $css_file); + $this->assertEquals('css', $css_file->getAssetType()); + } + + public function testCreateStylesheetExternalAsset() { + $css_external = $this->collector->create('css', 'external', 'http://foo.bar/path/to/asset.css'); + $this->assertInstanceOf('\Drupal\Core\Asset\ExternalAsset', $css_external); + $this->assertEquals('css', $css_external->getAssetType()); + } + + public function testCreateStylesheetStringAsset() { + $css_string = $this->collector->create('css', 'string', 'foo'); + $this->assertInstanceOf('\Drupal\Core\Asset\StringAsset', $css_string); + $this->assertEquals('css', $css_string->getAssetType()); + } + + public function testCreateJavascriptFileAsset() { + $js_file = $this->collector->create('js', 'file', 'foo'); + $this->assertInstanceOf('\Drupal\Core\Asset\FileAsset', $js_file); + $this->assertEquals('js', $js_file->getAssetType()); + } + + public function testCreateJavascriptExternalAsset() { + $js_external = $this->collector->create('js', 'external', 'http://foo.bar/path/to/asset.js'); + $this->assertInstanceOf('\Drupal\Core\Asset\ExternalAsset', $js_external); + $this->assertEquals('js', $js_external->getAssetType()); + } + + public function testCreateJavascriptStringAsset() { + $js_string = $this->collector->create('js', 'string', 'foo'); + $this->assertInstanceOf('\Drupal\Core\Asset\StringAsset', $js_string); + $this->assertEquals('js', $js_string->getAssetType()); + } + + public function testLastCssAutoAfter() { + $js = $this->collector->create('js', 'file', 'foo.js'); + $css1 = $this->collector->create('css', 'file', 'foo.css'); + $css2 = $this->collector->create('css', 'file', 'foo2.css', array(), array(), FALSE); + $this->assertEquals(array($css1), $css2->getPredecessors()); + + $css3 = $this->collector->create('css', 'file', 'foo3.css'); + $this->assertEquals(array($css1), $css3->getPredecessors()); + + $this->collector->clearLastCss(); + $css4 = $this->collector->create('css', 'file', 'foo4.css'); + $this->assertEmpty($css4->getPredecessors()); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testExceptionOnInvalidSourceType() { + $this->collector->create('foo', 'bar', 'baz'); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testExceptionOnInvalidAssetType() { + $this->collector->create('css', 'bar', 'qux'); + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/Factory/AssetLibraryFactoryTest.php b/core/tests/Drupal/Tests/Core/Asset/Factory/AssetLibraryFactoryTest.php new file mode 100644 index 0000000..3a0a07e --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/Factory/AssetLibraryFactoryTest.php @@ -0,0 +1,305 @@ + 'AssetLibraryFactory unit tests', + 'description' => 'Unit tests on AssetLibraryFactory', + 'group' => 'Asset', + ); + } + + /** + * @covers ::__construct + */ + public function testCreateFactory() { + $module_handler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'); + $collector = $this->getMock('Drupal\Core\Asset\Factory\AssetCollector'); + + $factory = new AssetLibraryFactory($module_handler, $collector); + $this->assertAttributeSame($collector, 'collector', $factory); + + $metadata_factory = $this->getMock('Drupal\Core\Asset\Metadata\MetadataFactoryInterface'); + + $factory = new AssetLibraryFactory($module_handler, NULL, $metadata_factory); + $prop = new \ReflectionProperty($factory, 'collector'); + $prop->setAccessible(TRUE); + $collector = $prop->getValue($factory); + + $this->assertAttributeSame($metadata_factory, 'metadataFactory', $collector); + } + + /** + * @covers ::__construct + * @expectedException \RuntimeException + */ + public function testCreateFactoryWithLockedCollector() { + $module_handler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'); + $collector = $this->getMock('Drupal\Core\Asset\Factory\AssetCollector'); + $collector->expects($this->once()) + ->method('isLocked') + ->will($this->returnValue(TRUE)); + + new AssetLibraryFactory($module_handler, $collector); + } + + /** + * @covers ::getLibrary + */ + public function testGetLibrary() { + $module_handler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'); + $module_handler->expects($this->exactly(3)) + ->method('implementsHook') + ->with('stub1', 'library_info') + ->will($this->returnValue(TRUE)); + $module_handler->expects($this->exactly(3)) + ->method('alter') + ->with('library_info') // matching more args is unnecessary and annoying + ->will($this->returnArgument(1)); + + $collector = $this->getMock('Drupal\Core\Asset\Factory\AssetCollector'); + $collector->expects($this->exactly(2)) + ->method('create') + ->will($this->returnCallback(array($this, 'createStubFileAsset'))); + $collector->expects($this->exactly(2)) + ->method('clearLastCss'); + $factory = new AssetLibraryFactory($module_handler, $collector); + + $this->assertFalse($factory->getLibrary('stub1/foo')); + + $lib1 = $factory->getLibrary('stub1/solo-nodeps-js'); + + $this->assertInstanceOf('Drupal\Core\Asset\Collection\AssetLibrary', $lib1); + $this->assertEquals('solo-nodeps-js', $lib1->getTitle()); + $this->assertEquals('1.2.3', $lib1->getVersion()); + $this->assertEquals('http://foo.bar', $lib1->getWebsite()); + $this->assertTrue($lib1->isFrozen()); + + $lib2 = $factory->getLibrary('stub1/solo-onedep-same'); + + $this->assertInstanceOf('Drupal\Core\Asset\Collection\AssetLibrary', $lib2); + $this->assertEquals(array('stub1/solo-nodeps-js'), $lib2->getDependencyInfo()); + + foreach ($lib2 as $asset) { + $this->assertEquals(array('stub1/solo-nodeps-js'), $asset->getDependencyInfo()); + } + } + + /** + * @covers ::getLibrary + */ + public function testGetLibraryModuleDoesNotImplementHook() { + $module_handler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'); + $module_handler->expects($this->once()) + ->method('implementsHook') + ->with('foo', 'library_info') + ->will($this->returnValue(FALSE)); + + $collector = $this->getMock('Drupal\Core\Asset\Factory\AssetCollector'); + $factory = new AssetLibraryFactory($module_handler, $collector); + + $this->assertFalse($factory->getLibrary('foo/bar')); + } +} + +} +namespace { + +/* + * Several permutations need to be covered: + * - single-asset library | homogeneous multi-asset library | heterogeneous multi-asset library + * - no dependencies | single dep | multi dep + * - dep with same type | dep with cross type | heterogeneous mix + */ +function stub1_library_info() { + $libraries['solo-nodeps-js'] = array( + 'title' => 'solo-nodeps-js', + 'version' => '1.2.3', + 'website' => 'http://foo.bar', + 'js' => array( + 'js/solo/nodeps.js', + ) + ); + + $libraries['solo-nodeps-css'] = array( + 'title' => 'solo-nodeps-css', + 'css' => array( + 'css/solo/nodeps.css' => array(), + ) + ); + + $libraries['solo-onedep-same'] = array( + 'title' => 'solo-onedep-same', + 'js' => array( + 'js/solo/onedep/same.js' => array(), + ), + 'dependencies' => array( + array('stub1', 'solo-nodeps-js'), + ) + ); + + $libraries['solo-onedep-diff'] = array( + 'title' => 'solo-onedep-same', + 'js' => array( + 'js/solo/onedep/diff.js' => array(), + ), + 'dependencies' => array( + array('stub1', 'solo-nodeps-css'), + ) + ); + + $libraries['solo-multidep-same'] = array( + 'title' => 'solo-multidep-same', + 'js' => array( + 'js/solo/multidep/same.js' => array(), + ), + 'dependencies' => array( + array('stub1', 'solo-nodeps-js'), + array('stub1', 'solo-onedep-same'), + ) + ); + + $libraries['solo-multidep-hetero'] = array( + 'title' => 'solo-multidep-hetero', + 'js' => array( + 'js/solo/multidep/hetero.js' => array(), + ), + 'dependencies' => array( + array('stub1', 'solo-nodeps-js'), + array('stub1', 'solo-nodeps-css'), + ) + ); + + return $libraries; +} + +function stub2_library_info() { + $libraries['homo-nodeps-js'] = array( + 'title' => 'homo-nodeps-js', + 'js' => array( + 'js/homo/nodeps1.js' => array(), + 'js/homo/nodeps2.js' => array(), + ), + ); + + $libraries['homo-nodeps-css'] = array( + 'title' => 'homo-nodeps-css', + 'css' => array( + 'css/homo/nodeps1.css' => array(), + 'css/homo/nodeps2.css' => array(), + ), + ); + + $libraries['hetero-nodeps'] = array( + 'title' => 'hetero-nodeps', + 'css' => array( + 'css/hetero/nodeps.css' => array(), + ), + 'js' => array( + 'js/hetero/nodeps.js' => array(), + ), + ); + + $libraries['homo-onedep-same'] = array( + 'title' => 'homo-onedep-same', + 'css' => array( + 'css/homo/onedep/same1.css' => array(), + 'css/homo/onedep/same2.css' => array(), + ), + 'dependencies' => array( + array('stub1', 'solo-nodeps-css'), + ), + ); + + $libraries['homo-onedep-diff'] = array( + 'title' => 'homo-onedep-diff', + 'css' => array( + 'css/homo/onedep/diff1.css' => array(), + 'css/homo/onedep/diff2.css' => array(), + ), + 'dependencies' => array( + array('stub1', 'solo-nodeps-js'), + ), + ); + + $libraries['hetero-onedep'] = array( + 'title' => 'hetero-onedep', + 'css' => array( + 'css/hetero/onedep.css' => array(), + ), + 'js' => array( + 'js/hetero/onedep.js' => array(), + ), + 'dependencies' => array( + array('stub2', 'hetero-nodeps'), + ), + ); + + $libraries['homo-multidep-same'] = array( + 'title' => 'homo-multidep-same', + 'css' => array( + 'css/homo/multidep/same1.css' => array(), + 'css/homo/multidep/same2.css' => array(), + ), + 'dependencies' => array( + array('stub1', 'solo-nodeps-css'), + array('stub2', 'homo-nodeps-css'), + ), + ); + + $libraries['homo-multidep-diff'] = array( + 'title' => 'homo-multidep-diff', + 'js' => array( + 'js/homo/multidep/diff1.js' => array(), + 'js/homo/multidep/diff1.js' => array(), + ), + 'dependencies' => array( + array('stub1', 'solo-nodeps-css'), + array('stub2', 'homo-nodeps-css'), + ), + ); + + $libraries['homo-multidep-hetero'] = array( + 'title' => 'homo-multidep-hetero', + 'css' => array( + 'css/homo/multidep/hetero1.css' => array(), + 'css/homo/multidep/hetero1.css' => array(), + ), + 'dependencies' => array( + array('stub1', 'solo-nodeps-css'), + array('stub2', 'homo-nodeps-js'), + ), + ); + + $libraries['hetero-multidep'] = array( + 'title' => 'hetero-multidep', + 'css' => array( + 'css/homo/multidep1.css' => array(), + ), + 'js' => array( + 'js/homo/multidep1.js' => array(), + ), + 'dependencies' => array( + array('stub1', 'solo-nodeps-css'), + array('stub2', 'homo-nodeps-js'), + ), + ); + + return $libraries; +} +} \ No newline at end of file diff --git a/core/tests/Drupal/Tests/Core/Asset/FileAssetTest.php b/core/tests/Drupal/Tests/Core/Asset/FileAssetTest.php new file mode 100644 index 0000000..376f664 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/FileAssetTest.php @@ -0,0 +1,57 @@ + 'File asset tests', + 'description' => 'Unit tests for FileAsset', + 'group' => 'Asset', + ); + } + + public function testInitialCreation() { + $meta = $this->createStubAssetMetadata(); + $asset = new FileAsset($meta, __FILE__); + + $this->assertEquals(__FILE__, $asset->id()); + $this->assertEquals(dirname(__FILE__), $asset->getSourceRoot()); + $this->assertEquals(basename(__FILE__), $asset->getSourcePath()); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testCreateNonString() { + $meta = $this->createStubAssetMetadata(); + new FileAsset($meta, new \stdClass()); + } + + /** + * @expectedException \RuntimeException + */ + public function testLoad() { + $meta = $this->createStubAssetMetadata(); + $asset = new FileAsset($meta, __FILE__); + + $this->assertEmpty($asset->getContent()); // ensure content is lazy loaded + + $asset->load(); + $this->assertEquals(file_get_contents(__FILE__), $asset->getContent()); + + $asset = new FileAsset($meta, __FILE__ . '.foo'); + $asset->load(); + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/GroupSort/AssetGraphTest.php b/core/tests/Drupal/Tests/Core/Asset/GroupSort/AssetGraphTest.php new file mode 100644 index 0000000..b54378b --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/GroupSort/AssetGraphTest.php @@ -0,0 +1,259 @@ + 'Asset graph test', + 'description' => 'Tests that custom additions in the asset graph work correctly.', + 'group' => 'Asset', + ); + } + + public function setUp() { + parent::setUp(); + $this->graph = new AssetGraph(); + } + + /** + * Generates a simple mock asset object. + * + * @param string $id + * An id to give the asset; it will returned from the mocked + * AssetInterface::id() method. + * + * @return \PHPUnit_Framework_MockObject_MockObject + * A mock of a BaseAsset object. + */ + public function createBasicAssetMock($id = 'foo') { + $mockmeta = $this->createStubAssetMetadata(); + $mock = $this->getMockBuilder('Drupal\Core\Asset\BaseAsset') + ->setConstructorArgs(array($mockmeta)) + ->getMock(); + + $mock->expects($this->any()) + ->method('id') + ->will($this->returnValue($id)); + + $mock->expects($this->once()) + ->method('getPredecessors') + ->will($this->returnValue(array())); + + $mock->expects($this->once()) + ->method('getSuccessors') + ->will($this->returnValue(array())); + + return $mock; + } + + public function doCheckVertexCount($count, AssetGraph $graph = NULL) { + $found = array(); + $graph = is_null($graph) ? $this->graph : $graph; + + $graph->eachVertex(function ($vertex) use (&$found) { + $found[] = $vertex; + }); + + $this->assertCount($count, $found); + } + + public function doCheckVerticesEqual($vertices, AssetGraph $graph = NULL) { + $found = array(); + $graph = is_null($graph) ? $this->graph : $graph; + + $graph->eachVertex(function ($vertex) use (&$found) { + $found[] = $vertex; + }); + + $this->assertEquals($vertices, $found); + } + + public function testAddSingleVertex() { + $mock = $this->createBasicAssetMock(); + + $mock->expects($this->exactly(2)) + ->method('id') + ->will($this->returnValue('foo')); + + $this->graph->addVertex($mock); + + $this->doCheckVerticesEqual(array($mock)); + } + + /** + * @expectedException \Gliph\Exception\InvalidVertexTypeException + */ + public function testAddInvalidVertexType() { + $this->graph->addVertex(new \stdClass()); + } + + /** + * @expectedException \LogicException + */ + public function testExceptionOnRemoval() { + $mock = $this->createBasicAssetMock(); + $this->graph->addVertex($mock); + $this->graph->removeVertex($mock); + } + + public function testAddUnconnectedVertices() { + $foo = $this->createBasicAssetMock('foo'); + $bar = $this->createBasicAssetMock('bar'); + + $this->graph->addVertex($foo); + $this->graph->addVertex($bar); + + $this->doCheckVerticesEqual(array($foo, $bar)); + } + + /** + * Tests that edges are automatically created correctly when assets have + * sequencing information. + */ + public function testAddConnectedVertices() { + $mockmeta = $this->createStubAssetMetadata(); + $foo = $this->getMockBuilder('Drupal\Core\Asset\BaseAsset') + ->setConstructorArgs(array($mockmeta)) + ->getMock(); + + $foo->expects($this->exactly(3)) + ->method('id') + ->will($this->returnValue('foo')); + + $foo->expects($this->once()) + ->method('getPredecessors') + ->will($this->returnValue(array('bar'))); + + $foo->expects($this->once()) + ->method('getSuccessors') + ->will($this->returnValue(array('baz'))); + + $bar = $this->createBasicAssetMock('bar'); + $baz = $this->createBasicAssetMock('baz'); + + $this->graph->addVertex($foo); + $this->graph->addVertex($bar); + $this->graph->addVertex($baz); + + $this->doCheckVerticesEqual(array($foo, $bar, $baz)); + + $lister = function($vertex) use (&$out) { + $out[] = $vertex; + }; + + $out = array(); + $this->graph->eachAdjacent($foo, $lister); + $this->assertEquals(array($bar), $out); + + $out = array(); + $this->graph->eachAdjacent($baz, $lister); + $this->assertEquals(array($foo), $out); + + $out = array(); + $this->graph->eachAdjacent($bar, $lister); + $this->assertEmpty($out); + + // Now add another vertex with sequencing info that targets already-inserted + // vertices. + + $qux = $this->getMockBuilder('Drupal\Core\Asset\BaseAsset') + ->setConstructorArgs(array($mockmeta)) + ->getMock(); + + $qux->expects($this->exactly(2)) + ->method('id') + ->will($this->returnValue('qux')); + + // Do this one with the foo vertex itself, not its string id. + $qux->expects($this->once()) + ->method('getPredecessors') + ->will($this->returnValue(array($foo))); + + $qux->expects($this->once()) + ->method('getSuccessors') + ->will($this->returnValue(array('bar', 'baz'))); + + $this->graph->addVertex($qux); + + $this->doCheckVerticesEqual(array($foo, $bar, $baz, $qux)); + + $out = array(); + $this->graph->eachAdjacent($qux, $lister); + $this->assertEquals(array($foo), $out); + + $out = array(); + $this->graph->eachAdjacent($bar, $lister); + $this->assertEquals(array($qux), $out); + + $out = array(); + $this->graph->eachAdjacent($baz, $lister); + $this->assertEquals(array($foo, $qux), $out); + } + + public function testTranspose() { + $mockmeta = $this->createStubAssetMetadata(); + $foo = $this->getMockBuilder('Drupal\Core\Asset\BaseAsset') + ->setConstructorArgs(array($mockmeta)) + ->getMock(); + + $foo->expects($this->exactly(3)) + ->method('id') + ->will($this->returnValue('foo')); + + $foo->expects($this->once()) + ->method('getPredecessors') + ->will($this->returnValue(array('bar'))); + + $foo->expects($this->once()) + ->method('getSuccessors') + ->will($this->returnValue(array('baz'))); + + $bar = $this->createBasicAssetMock('bar'); + $baz = $this->createBasicAssetMock('baz'); + + $this->graph->addVertex($foo); + $this->graph->addVertex($bar); + $this->graph->addVertex($baz); + + $transpose = $this->graph->transpose(); + $this->doCheckVerticesEqual(array($foo, $bar, $baz), $transpose); + + // Verify that the transpose has a fully inverted edge set. + $lister = function($vertex) use (&$out) { + $out[] = $vertex; + }; + + $out = array(); + $transpose->eachAdjacent($bar, $lister); + $this->assertEquals(array($foo), $out); + + $out = array(); + $transpose->eachAdjacent($foo, $lister); + $this->assertEquals(array($baz), $out); + + $out = array(); + $transpose->eachAdjacent($baz, $lister); + $this->assertEmpty($out); + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/GroupSort/OptimallyGroupedTSLVisitorTest.php b/core/tests/Drupal/Tests/Core/Asset/GroupSort/OptimallyGroupedTSLVisitorTest.php new file mode 100644 index 0000000..ab377c4 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/GroupSort/OptimallyGroupedTSLVisitorTest.php @@ -0,0 +1,214 @@ + 'Tests depth first visitor.', + 'description' => 'Integration tests on OptimallyGroupedTSLVisitor.', + 'group' => 'Asset', + ); + } + + public function createStubPositioningAsset($id, $predecessors = array(), $successors = array()) { + $asset = $this->getMockBuilder('Drupal\Core\Asset\BaseAsset') + ->disableOriginalConstructor() + ->setMockClassName("mock_asset_$id") + ->getMock(); + + $asset->expects($this->any()) + ->method('id') + ->will($this->returnValue($id)); + + if ($predecessors !== FALSE) { + $asset->expects($this->any()) + ->method('getPredecessors') + ->will($this->returnValue($predecessors)); + } + + if ($successors !== FALSE) { + $asset->expects($this->any()) + ->method('getSuccessors') + ->will($this->returnValue($successors)); + } + + return $asset; + } + + /** + * Vertices: a, b, c, d, e, f, g, h + * Edges: + * f -> c + * d -> c + * e -> d + * b -> e + * h -> a + */ + public function getVertexSet() { + $vertices = array(); + $vertices['a'] = $this->createStubPositioningAsset('a'); + $vertices['b'] = $this->createStubPositioningAsset('b', array('e')); + $vertices['c'] = $this->createStubPositioningAsset('c'); + $vertices['d'] = $this->createStubPositioningAsset('d', array($vertices['c'])); + $vertices['e'] = $this->createStubPositioningAsset('e', array($vertices['d'])); + $vertices['f'] = $this->createStubPositioningAsset('f', array($vertices['c'])); + $vertices['g'] = $this->createStubPositioningAsset('g'); + $vertices['h'] = $this->createStubPositioningAsset('h', array($vertices['a'])); + return $vertices; + } + + /** + * Optimality groups: + * g1: a, b, c + * g2: d, e + * g3: f, g + * + * Ungrouped: + * h + */ + public function createSimpleGraph() { + $vertices = $this->getVertexSet(); + extract($vertices); + + + // Populate the graph + $graph = new AssetGraph(); + foreach ($vertices as $v) { + $graph->addVertex($v); + } + + return array($graph, $vertices); + } + + /** + * @covers Drupal\Core\Asset\AssetGraph::addVertex + * @covers Drupal\Core\Asset\AssetGraph::processNewVertex + */ + public function testAssetGraphBuildsEdgesCorrectly() { + list($graph, $vertices) = $this->createSimpleGraph(); + extract($vertices); + + $that = $this; + // First, take care of vertices that should have no edges + foreach (array('a', 'c', 'g') as $vertex_id) { + $graph->eachAdjacent($$vertex_id, function($adjacent) use ($that) { + $that->fail(); + }); + } + + // Now handle the individual cases. + $graph->eachAdjacent($b, function($adjacent) use ($that, $vertices) { + $that->assertSame($vertices['e'], $adjacent); + }); + $graph->eachAdjacent($d, function($adjacent) use ($that, $vertices) { + $that->assertSame($vertices['c'], $adjacent); + }); + $graph->eachAdjacent($e, function($adjacent) use ($that, $vertices) { + $that->assertSame($vertices['d'], $adjacent); + }); + $graph->eachAdjacent($f, function($adjacent) use ($that, $vertices) { + $that->assertSame($vertices['c'], $adjacent); + }); + $graph->eachAdjacent($h, function($adjacent) use ($that, $vertices) { + $that->assertSame($vertices['a'], $adjacent); + }); + } + + /** + * @depends testAssetGraphBuildsEdgesCorrectly + * @covers Drupal\Core\Asset\GroupSort\OptimallyGroupedTSLVisitor + */ + public function testRealSort() { + list($graph, $vertices) = $this->createSimpleGraph(); + extract($vertices); + + $transpose = $graph->transpose(); + + $reach_visitor = new DepthFirstBasicVisitor(); + + // Find source vertices (outdegree 0) in the original graph + $sources = DepthFirst::find_sources($transpose, $reach_visitor); + $this->assertCount(3, $sources); + $this->assertContains($c, $sources); + $this->assertContains($a, $sources); + $this->assertContains($g, $sources); + + // Traverse the transposed graph for reachability data on each vertex + DepthFirst::traverse($transpose, $reach_visitor, clone $sources); + + $this->assertCount(4, $reach_visitor->getReachable($c)); + $this->assertCount(1, $reach_visitor->getReachable($a)); + $this->assertCount(0, $reach_visitor->getReachable($b)); + $this->assertCount(2, $reach_visitor->getReachable($d)); + $this->assertCount(1, $reach_visitor->getReachable($e)); + $this->assertCount(0, $reach_visitor->getReachable($f)); + $this->assertCount(0, $reach_visitor->getReachable($g)); + + // Sort vertices via a PriorityQueue based on total reach + $pq = new \SplPriorityQueue(); + foreach ($sources as $vertex) { + $pq->insert($vertex, count($reach_visitor->getReachable($vertex))); + } + + // Dump the priority queue into a normal queue + $queue = new \SplQueue(); + foreach ($pq as $vertex) { + $queue->push($vertex); + } + $optimal = array( + 'g1' => new \SplObjectStorage(), + 'g2' => new \SplObjectStorage(), + 'g3' => new \SplObjectStorage(), + ); + $optimal_lookup = new \SplObjectStorage(); + + $optimal['g1']->attach($a, 'g1'); + $optimal_lookup->attach($a, $optimal['g1']); + $optimal['g1']->attach($b, 'g1'); + $optimal_lookup->attach($b, $optimal['g1']); + $optimal['g1']->attach($c, 'g1'); + $optimal_lookup->attach($c, $optimal['g1']); + + $optimal['g2']->attach($d, 'g2'); + $optimal_lookup->attach($d, $optimal['g2']); + $optimal['g2']->attach($e, 'g2'); + $optimal_lookup->attach($e, $optimal['g2']); + + $optimal['g3']->attach($f, 'g3'); + $optimal_lookup->attach($f, $optimal['g3']); + $optimal['g3']->attach($g, 'g3'); + $optimal_lookup->attach($g, $optimal['g3']); + + $vis = new OptimallyGroupedTSLVisitor($optimal, $optimal_lookup); + DepthFirst::traverse($transpose, $vis, $queue); + + // Ta-da! + $expected = array( + 'h' => $h, + 'a' => $a, + 'b' => $b, + 'e' => $e, + 'd' => $d, + 'g' => $g, + 'f' => $f, + 'c' => $c, + ); + $this->assertEquals($expected, $vis->getTSL()->all()); + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/Metadata/AssetMetadataBagTest.php b/core/tests/Drupal/Tests/Core/Asset/Metadata/AssetMetadataBagTest.php new file mode 100644 index 0000000..747e966 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/Metadata/AssetMetadataBagTest.php @@ -0,0 +1,32 @@ + 'Asset Metadata bag test', + 'description' => 'Tests various methods of AssetMetadatabag', + 'group' => 'Asset', + ); + } + + + public function testGetType() { + $bag = new AssetMetadataBag('arglebargle', array()); + $this->assertEquals('arglebargle', $bag->getType()); + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/Metadata/DefaultAssetMetadataFactoryTest.php b/core/tests/Drupal/Tests/Core/Asset/Metadata/DefaultAssetMetadataFactoryTest.php new file mode 100644 index 0000000..8c485b5 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/Metadata/DefaultAssetMetadataFactoryTest.php @@ -0,0 +1,57 @@ + 'DefaultAssetMetadataFactory test', + 'description' => 'Unit tests for DefaultAssetMetadataFactory', + 'group' => 'Asset', + ); + } + + public function testCreateCssMetadata() { + $factory = new DefaultAssetMetadataFactory(); + $bag = new AssetMetadataBag('css', array( + 'every_page' => FALSE, + 'media' => 'all', + 'preprocess' => TRUE, + 'browsers' => array( + 'IE' => TRUE, + '!IE' => TRUE, + ), + )); + + $this->assertEquals($bag, $factory->createCssMetadata('file', 'foo/bar.css')); + } + + public function testCreateJsMetadata() { + $factory = new DefaultAssetMetadataFactory(); + $bag = new AssetMetadataBag('js', array( + 'every_page' => FALSE, + 'scope' => 'footer', + 'cache' => TRUE, + 'preprocess' => TRUE, + 'attributes' => array(), + 'version' => NULL, + 'browsers' => array(), + )); + + $this->assertEquals($bag, $factory->createJsMetadata('file', 'foo/bar.js')); + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/RelativePositionTraitTest.php b/core/tests/Drupal/Tests/Core/Asset/RelativePositionTraitTest.php new file mode 100644 index 0000000..c50f0c8 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/RelativePositionTraitTest.php @@ -0,0 +1,144 @@ + 'Relative position trait test', + 'description' => 'Tests that the boilerplate implementation of RelativePositionInterface by RelativePositionTrait works correctly.', + 'group' => 'Asset', + ); + } + + public function setUp() { + $this->mock = $this->getObjectForTrait('Drupal\Core\Asset\RelativePositionTrait'); + } + + /** + * @covers ::after + */ + public function testAfter() { + $dep = $this->createBaseAsset(); + + $this->assertSame($this->mock, $this->mock->after('foo')); + $this->assertSame($this->mock, $this->mock->after($dep)); + + $this->assertAttributeContains($dep, 'predecessors', $this->mock); + + $invalid = array(0, 1.1, fopen(__FILE__, 'r'), TRUE, array(), new \stdClass); + + try { + foreach ($invalid as $val) { + $this->mock->after($val); + $this->fail('Was able to create an ordering relationship with an inappropriate value.'); + } + } catch (\InvalidArgumentException $e) {} + } + + /** + * @depends testAfter + * @covers ::hasPredecessors + */ + public function testHasPredecessors() { + $this->assertFalse($this->mock->hasPredecessors()); + + $this->mock->after('foo'); + $this->assertTrue($this->mock->hasPredecessors()); + } + + /** + * @depends testAfter + * @covers ::getPredecessors + */ + public function testGetPredecessors() { + $this->assertEmpty($this->mock->getPredecessors()); + + $this->mock->after('foo'); + $this->assertEquals(array('foo'), $this->mock->getPredecessors()); + } + + /** + * @depends testAfter + * @depends testHasPredecessors + * @covers ::clearPredecessors + */ + public function testClearPredecessors() { + $this->mock->after('foo'); + + $this->assertSame($this->mock, $this->mock->clearPredecessors()); + $this->assertFalse($this->mock->hasPredecessors()); + } + + /** + * @covers ::before + */ + public function testBefore() { + $dep = $this->createBaseAsset(); + + $this->assertSame($this->mock, $this->mock->before('foo')); + $this->assertSame($this->mock, $this->mock->before($dep)); + + $this->assertAttributeContains($dep, 'successors', $this->mock); + + $invalid = array(0, 1.1, fopen(__FILE__, 'r'), TRUE, array(), new \stdClass); + + try { + foreach ($invalid as $val) { + $this->mock->after($val); + $this->fail('Was able to create an ordering relationship with an inappropriate value.'); + } + } catch (\InvalidArgumentException $e) {} + } + + /** + * @depends testBefore + * @covers ::hasSuccessors + */ + public function testHasSuccessors() { + $this->assertFalse($this->mock->hasSuccessors()); + + $this->mock->before('foo'); + $this->assertTrue($this->mock->hasSuccessors()); + } + + /** + * @depends testBefore + * @covers ::getSuccessors + */ + public function testGetSuccessors() { + $this->mock = $this->createBaseAsset(); + $this->assertEmpty($this->mock->getSuccessors()); + + $this->mock->before('foo'); + $this->assertEquals(array('foo'), $this->mock->getSuccessors()); + } + + /** + * @depends testBefore + * @covers ::clearSuccessors + */ + public function testClearSuccessors() { + $this->mock->before('foo'); + + $this->assertSame($this->mock, $this->mock->clearSuccessors()); + $this->assertFalse($this->mock->hasSuccessors()); + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/StringAssetTest.php b/core/tests/Drupal/Tests/Core/Asset/StringAssetTest.php new file mode 100644 index 0000000..b981519 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/StringAssetTest.php @@ -0,0 +1,77 @@ + 'String asset tests', + 'description' => 'Unit tests for StringAsset', + 'group' => 'Asset', + ); + } + + /** + * @covers ::__construct + */ + public function testInitialCreation() { + $meta = $this->createStubAssetMetadata(); + $content = 'foo bar baz'; + $asset = new StringAsset($meta, $content); + + $this->assertEquals($content, $asset->getContent()); + } + + /** + * @covers ::__construct + */ + public function testCreateInvalidContent() { + $meta = $this->createStubAssetMetadata(); + $invalid = array('', 0, 1.1, fopen(__FILE__, 'r'), TRUE, array(), new \stdClass); + + try { + foreach ($invalid as $val) { + new StringAsset($meta, $val); + $varinfo = (gettype($val) == 'string') ? 'an empty string' : 'of type ' . gettype($val); + $this->fail(sprintf('Was able to create a string asset with invalid content; content was %s.', $varinfo)); + } + } catch (\InvalidArgumentException $e) {} + } + + /** + * @covers ::id + */ + public function testId() { + $meta = $this->createStubAssetMetadata(); + $content = 'foo bar baz'; + $asset = new StringAsset($meta, $content); + + $this->assertEquals(hash('sha256', $content), $asset->id()); + } + + /** + * @covers ::load + */ + public function testLoad() { + $meta = $this->createStubAssetMetadata(); + $content = 'foo bar baz'; + $asset = new StringAsset($meta, $content); + + // With no filters, loading result in the same content we started with. + $asset->load(); + $this->assertEquals($content, $asset->getContent()); + } +} +