diff --git a/composer.json b/composer.json index 436dae9..b45baf8 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,8 @@ "symfony-cmf/routing": "1.1.*@alpha", "easyrdf/easyrdf": "0.8.*@beta", "phpunit/phpunit": "3.7.*", - "zendframework/zend-feed": "2.2.*" + "zendframework/zend-feed": "2.2.*", + "sdboyer/gliph": "0.1.*" }, "autoload": { "psr-0": { diff --git a/core/core.services.yml b/core/core.services.yml index 608c439..9dd036c 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -631,3 +631,4 @@ services: class: Drupal\Core\Asset\JsCollectionGrouper asset.js.dumper: class: Drupal\Core\Asset\AssetDumper + diff --git a/core/includes/common.inc b/core/includes/common.inc index 77ea718..687eeb9 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -1631,6 +1631,9 @@ 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'; + drupal_collect_assets($data, $options, 'css'); + $options += array( 'type' => 'file', 'group' => CSS_AGGREGATE_DEFAULT, @@ -1682,6 +1685,29 @@ function drupal_add_css($data = NULL, $options = NULL) { return $css; } +function drupal_collect_assets($data, $options, $type = '') { + $collection = &drupal_static('global_asset_bag', 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); + } + + if ($data instanceof \Drupal\Core\Asset\AssetInterface) { + $collector->add($data); + return; + } + + if ($type == 'js-setting') { + // TODO handle js settings + return; + } + + $collector->create($type, $options['type'], $data, $options); +} + /** * Returns a themed representation of all stylesheets to attach to the page. * @@ -2212,6 +2238,11 @@ 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'; + 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; diff --git a/core/lib/Drupal/Core/Asset/Aggregate/AssetAggregateInterface.php b/core/lib/Drupal/Core/Asset/Aggregate/AssetAggregateInterface.php new file mode 100644 index 0000000..8d37137 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Aggregate/AssetAggregateInterface.php @@ -0,0 +1,53 @@ +metadata = $metadata; + $this->sourceRoot = $sourceRoot; + $this->assetStorage = new \SplObjectStorage(); + $this->nestedStorage = new \SplObjectStorage(); + + $this->filters = new FilterCollection($filters); + + foreach ($assets as $asset) { + $this->add($asset); + } + } + + /** + * {@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->assetStorage as $asset) { + // Preserve a little id stability by not composing id from aggregates + if (!$asset instanceof AssetAggregateInterface) { + $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 add(AsseticAssetInterface $asset) { + if (!$asset instanceof AssetInterface) { + throw new UnsupportedAsseticBehaviorException('Vanilla Assetic asset provided; Drupal aggregates require Drupal-flavored assets.'); + } + $this->ensureCorrectType($asset); + + $this->assetStorage->attach($asset); + $this->assetIdMap[$asset->id()] = $asset; + + if ($asset instanceof AssetAggregateInterface) { + $this->nestedStorage->attach($asset); + } + } + + /** + * {@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 getById($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->getById($id)) { + return $found; + } + } + } + + if ($graceful) { + return FALSE; + } + + throw new \OutOfBoundsException(sprintf('This aggregate does not contain an asset with id %s.', $id)); + } + + /** + * {@inheritdoc} + */ + public function remove($needle, $graceful = TRUE) { + if (is_string($needle)) { + if (!$needle = $this->getById($needle, $graceful)) { + return FALSE; + } + } + + return $this->removeLeaf($needle, $graceful); + } + + /** + * {@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.'); + } + $this->ensureCorrectType($needle); + + foreach ($this->assetIdMap as $id => $asset) { + if ($asset === $needle) { + unset($this->assetStorage[$asset], $this->assetIdMap[$id], $this->nestedStorage[$asset]); + + return TRUE; + } + + if ($asset instanceof AssetAggregateInterface && $asset->removeLeaf($needle, $graceful)) { + return TRUE; + } + } + + if ($graceful) { + return FALSE; + } + + throw new \OutOfBoundsException('Asset not found.'); + } + + /** + * {@inheritdoc} + */ + public function replace($needle, AssetInterface $replacement, $graceful = TRUE) { + if (is_string($needle)) { + if (!$needle = $this->getById($needle, $graceful)) { + return FALSE; + } + } + + return $this->replaceLeaf($needle, $replacement, $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($needle); + $this->ensureCorrectType($replacement); + + 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 AssetAggregateInterface) { + $this->nestedStorage->attach($replacement); + } + + return TRUE; + } + + if ($asset instanceof AssetAggregateInterface && $asset->replaceLeaf($needle, $replacement, $graceful)) { + return TRUE; + } + } + + if ($graceful) { + return FALSE; + } + + throw new \OutOfBoundsException('Asset not found.'); + } + + /** + * {@inheritdoc} + * + * Aggregate assets are inherently eligible for preprocessing, so this is + * always true. + */ + public function isPreprocessable() { + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function all() { + return $this->assetIdMap; + } + + /** + * {@inheritdoc} + */ + public function ensureFilter(FilterInterface $filter) { + $this->filters->ensure($filter); + } + + /** + * {@inheritdoc} + */ + public function getFilters() { + return $this->filters->all(); + } + + /** + * {@inheritdoc} + */ + public function clearFilters() { + $this->filters->clear(); + } + + /** + * {@inheritdoc} + */ + public function load(FilterInterface $additionalFilter = NULL) { + // loop through leaves and load each asset + $parts = array(); + foreach ($this as $asset) { + $asset->load($additionalFilter); + $parts[] = $asset->getContent(); + } + + $this->content = implode("\n", $parts); + } + + /** + * {@inheritdoc} + */ + public function dump(FilterInterface $additionalFilter = NULL) { + // loop through leaves and dump each asset + $parts = array(); + foreach ($this as $asset) { + $parts[] = $asset->dump($additionalFilter); + } + + return implode("\n", $parts); + } + + /** + * {@inheritdoc} + */ + public function getContent() { + return $this->content; + } + + /** + * {@inheritdoc} + */ + public function setContent($content) { + $this->content = $content; + } + + /** + * {@inheritdoc} + */ + public function getSourceRoot() { + return $this->sourceRoot; + } + + /** + * {@inheritdoc} + */ + public function getSourcePath() { + } + + /** + * {@inheritdoc} + */ + public function getTargetPath() { + return $this->targetPath; + } + + /** + * {@inheritdoc} + */ + public function setTargetPath($targetPath) { + $this->targetPath = $targetPath; + } + + /** + * Returns the highest last-modified value of all contained assets. + * + * @return integer|null + * A UNIX timestamp + */ + public function getLastModified() { + if (!count($this->assetStorage)) { + return; + } + + $mtime = 0; + foreach ($this->assetStorage as $asset) { + $assetMtime = $asset->getLastModified(); + if ($assetMtime > $mtime) { + $mtime = $assetMtime; + } + } + + return $mtime; + } + + /** + * TODO Assetic uses their iterator to clone, then populate values and return here; is that a good model for us? + */ + public function getIterator() { + // TODO this is totally junk + return new \ArrayIterator($this->assetIdMap); + } + + /** + * Indicates whether this collection contains any assets. + * + * @return bool + * TRUE if contained assets are present, FALSE otherwise. + */ + public function isEmpty() { + return $this->assetStorage->count() === 0; + } + + + /** + * Ensures that the asset is of the correct subtype (e.g., css vs. js). + * + * @param AssetInterface $asset + * + * @throws \Drupal\Core\Asset\Exception\AssetTypeMismatchException + */ + abstract protected function ensureCorrectType(AssetInterface $asset); +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/Aggregate/CssAggregateAsset.php b/core/lib/Drupal/Core/Asset/Aggregate/CssAggregateAsset.php new file mode 100644 index 0000000..934317a --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Aggregate/CssAggregateAsset.php @@ -0,0 +1,40 @@ +getAssetType() !== 'css') { + throw new AssetTypeMismatchException('CSS aggregates can only work with CSS assets.'); + } + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/Aggregate/JsAggregateAsset.php b/core/lib/Drupal/Core/Asset/Aggregate/JsAggregateAsset.php new file mode 100644 index 0000000..37f8a2e --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Aggregate/JsAggregateAsset.php @@ -0,0 +1,40 @@ +getAssetType() !== 'js') { + throw new AssetTypeMismatchException('JS aggregates can only work with JS assets.'); + } + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/AssetGraph.php b/core/lib/Drupal/Core/Asset/AssetGraph.php new file mode 100644 index 0000000..ee94b08 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/AssetGraph.php @@ -0,0 +1,162 @@ +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 sequencing 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 AssetOrderingInterface) { + // 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(); + + 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(); + + 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 sequencing 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->eachEdge(function($edge) use (&$graph) { + $graph->addDirectedEdge($edge[1], $edge[0]); + }); + + return $graph; + } +} diff --git a/core/lib/Drupal/Core/Asset/AssetInterface.php b/core/lib/Drupal/Core/Asset/AssetInterface.php new file mode 100644 index 0000000..1d21dac --- /dev/null +++ b/core/lib/Drupal/Core/Asset/AssetInterface.php @@ -0,0 +1,59 @@ +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'); + } + + /** + * {@inheritdoc} + */ + public function hasDependencies() { + return !empty($this->dependencies); + } + + /** + * {@inheritdoc} + */ + public function addDependency($module, $name) { + if (!(is_string($module) && is_string($name))) { + throw new \InvalidArgumentException('Dependencies must be expressed as 2-tuple with the first element being owner/module, and the second being name.'); + } + + $this->dependencies[] = array($module, $name); + } + + /** + * {@inheritdoc} + */ + public function clearDependencies() { + $this->dependencies = array(); + } + + /** + * {@inheritdoc} + */ + public function getDependencyInfo() { + return $this->dependencies; + } + + /** + * {@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; + } + + /** + * {@inheritdoc} + */ + public function after($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->predecessors[] = $asset; + } + + /** + * {@inheritdoc} + */ + public function getPredecessors() { + return $this->predecessors; + } + + /** + * {@inheritdoc} + */ + public function getSuccessors() { + return $this->successors; + } + + /** + * {@inheritdoc} + */ + public function clearSuccessors() { + $this->successors = array(); + } + + /** + * {@inheritdoc} + */ + public function clearPredecessors() { + $this->predecessors = array(); + } +} 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..0fe0a90 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Collection/AssetCollection.php @@ -0,0 +1,184 @@ +assetStorage = new \SplObjectStorage(); + } + + /** + * {@inheritdoc} + */ + public function add(AssetInterface $asset) { + $this->attemptWrite(); + + if (!$this->contains($asset)) { + $this->assetStorage->attach($asset); + $this->assetIdMap[$asset->id()] = $asset; + } + } + + /** + * {@inheritdoc} + */ + public function contains(AssetInterface $asset) { + // TODO decide whether to do this by id or object instance + return $this->assetStorage->contains($asset); + } + + /** + * {@inheritdoc} + */ + public function getById($id, $graceful = TRUE) { + if (isset($this->assetIdMap[$id])) { + return $this->assetIdMap[$id]; + } + else 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 = TRUE) { + // TODO fix horrible complexity of conditionals, exceptions, and returns. + $this->attemptWrite(); + + // Validate and normalize type to AssetInterface + if (is_string($needle)) { + if (!$needle = $this->getById($needle, $graceful)) { + // Asset couldn't be found but we're in graceful mode - return FALSE. + return FALSE; + } + } + else if (!$needle instanceof AssetInterface) { + throw new \InvalidArgumentException('Invalid type provided to AssetCollection::remove(); must provide either a string asset id or AssetInterface instance.'); + } + + // Check for membership + if ($this->contains($needle)) { + unset($this->assetIdMap[$needle->id()], $this->assetStorage[$needle]); + return TRUE; + } + else if (!$graceful) { + throw new \OutOfBoundsException(sprintf('This collection does not contain an asset with id %s.', $needle->id())); + } + + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function all() { + return $this->assetIdMap; + } + + /** + * {@inheritdoc} + */ + public function mergeCollection(AssetCollectionInterface $collection, $freeze = TRUE) { + $this->attemptWrite(); + + foreach ($collection as $asset) { + if (!$this->contains($asset)) { + $this->add($asset); + } + } + + if ($freeze) { + $collection->freeze(); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function freeze() { + $this->frozen = TRUE; + } + + /** + * {@inheritdoc} + */ + public function isFrozen() { + return $this->frozen; + } + + /** + * {@inheritdoc} + */ + public function getIterator() { + return new \ArrayIterator($this->assetIdMap); + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + return empty($this->assetIdMap); + } + + /** + * {@inheritdoc} + */ + public function getCss() { + // TODO evaluate potential performance impact if this is done a lot... + $collection = new self(); + foreach (new AssetSubtypeFilterIterator($this->getIterator(), 'css') as $asset) { + $collection->add($asset); + } + + return $collection; + } + + /** + * {@inheritdoc} + */ + public function getJs() { + $collection = new self(); + foreach (new AssetSubtypeFilterIterator($this->getIterator(), 'js') as $asset) { + $collection->add($asset); + } + + return $collection; + } + + /** + * Checks if the asset library is frozen, throws an exception if it is. + */ + protected function attemptWrite() { + if ($this->isFrozen()) { + throw new \LogicException('Cannot write to a frozen AssetCollection.'); + } + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/Collection/AssetCollectionBasicInterface.php b/core/lib/Drupal/Core/Asset/Collection/AssetCollectionBasicInterface.php new file mode 100644 index 0000000..e4cebc2 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Collection/AssetCollectionBasicInterface.php @@ -0,0 +1,79 @@ + $val) { + $this->$key = $val; + } + } + + /** + * Set the asset library's title. + * + * @param string $title + * The title of the asset library. + * + * @return \Drupal\Core\Asset\AssetLibrary + * The asset library, to allow for chaining. + */ + public function setTitle($title) { + $this->attemptWrite(); + $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(); + $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(); + $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 hasDependencies() { + return !empty($this->dependencies); + } + + /** + * {@inheritdoc} + */ + public function addDependency($module, $name) { + $this->attemptWrite(); + $this->dependencies[] = array($module, $name); + } + + /** + * {@inheritdoc} + */ + public function clearDependencies() { + $this->attemptWrite(); + $this->dependencies = array(); + } + + /** + * {@inheritdoc} + */ + public function getDependencyInfo() { + return $this->dependencies; + } + + /** + * {@inheritdoc} + */ + public function before($asset) { + $this->successors[] = $asset; + } + + /** + * {@inheritdoc} + */ + public function after($asset) { + $this->predecessors[] = $asset; + } + + /** + * {@inheritdoc} + */ + public function getPredecessors() { + return $this->predecessors; + } + + /** + * {@inheritdoc} + */ + public function getSuccessors() { + return $this->successors; + } + + /** + * {@inheritdoc} + */ + public function clearSuccessors() { + $this->successors = array(); + } + + /** + * {@inheritdoc} + */ + public function clearPredecessors() { + $this->predecessors = array(); + } + + /** + * Checks if the asset library is frozen, throws an exception if it is. + */ + protected function attemptWrite() { + if ($this->isFrozen()) { + throw new \LogicException('Metadata cannot be modified on a frozen AssetLibrary.'); + } + } +} 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..5b2c1e5 --- /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; + } + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/CssCollectionGrouperNouveaux.php b/core/lib/Drupal/Core/Asset/CssCollectionGrouperNouveaux.php new file mode 100644 index 0000000..562163a --- /dev/null +++ b/core/lib/Drupal/Core/Asset/CssCollectionGrouperNouveaux.php @@ -0,0 +1,209 @@ +repository = $repository; + } + + /** + * Groups a collection of assets into logical groups of asset collections. + * + * @param array $assets + * An asset collection. + * + * @return array + * A sorted array of asset groups. + */ + public function group(AssetCollection $assets) { + $tsl = $this->getOptimalTSL($assets); + + // TODO ordering suddenly matters here...problem? + $processed = new AssetCollection(); + $last_key = FALSE; + foreach ($tsl as $asset) { + // TODO fix the visitor - this will fail right now because the optimality data got depleted during traversal + $key = $this->optimal_lookup->contains($asset) ? $this->optimal_lookup[$asset] : FALSE; + + if ($key !== $last_key) { + $aggregate = new CssAggregateAsset($asset->getMetadata()); + $processed->add($aggregate); + } + + $aggregate->add($asset); + } + + return $processed; + } + + /** + * Gets a topologically sorted list that is optimal for grouping. + * + * @param array $assets + * + * @return array + * A linear list of assets that will enable optimal groupings. + * + * @throws \LogicException + */ + protected function getOptimalTSL(AssetCollection $assets) { + // We need to define the optimum minimal group set, given metadata + // boundaries across which aggregates cannot be safely made. + $this->optimal = array(); + + // Also create an SplObjectStorage to act as a lookup table on an asset to + // its group, if any. + $this->optimal_lookup = new \SplObjectStorage(); + + // Finally, create a specialized directed adjacency list that will capture + // sequencing information. + $graph = new AssetGraph(); + + foreach ($assets->getCss() as $asset) { + $graph->addVertex($asset); + + $k = $this->getGroupKey($asset); + + if ($k === FALSE) { + // Record no optimality information for ungroupable assets; they will + // be visited normally and rearranged as needed. + continue; + } + + if (!isset($this->optimal[$k])) { + // Create an SplObjectStorage to represent each set of assets that would + // optimally be grouped together. + $this->optimal[$k] = new \SplObjectStorage(); + } + $this->optimal[$k]->attach($asset, $k); + $this->optimal_lookup->attach($asset, $this->optimal[$k]); + } + + // First, transpose the graph in order to get an appropriate answer + $transpose = $graph->transpose(); + + // Create a queue of start vertices to prime the traversal. + $queue = $this->createSourceQueue($graph, $transpose); + + // Now, create the visitor and walk the graph to get an optimal TSL. + $visitor = new OptimallyGroupedTSLVisitor($this->optimal, $this->optimal_lookup); + DepthFirst::traverse($transpose, $visitor, $queue); + + return $visitor->getTSL(); + } + + /** + * Gets the grouping key for the provided asset. + * + * @param $asset + * + * @return bool|string + * @throws \UnexpectedValueException + */ + protected function getGroupKey(AssetInterface $asset) { + $meta = $asset->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 - currently ignoring group, which is used in the 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; + + return $k; + } + 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)); + + return $k; + } + else if ($asset instanceof ExternalAsset) { + // Never group external assets. + $k = FALSE; + + return $k; + } + else { + throw new \UnexpectedValueException(sprintf('Unknown CSS asset type "%s" somehow made it into the CSS collection during grouping.', get_class($asset))); + } + } + + /** + * Creates a queue of starting vertices that will facilitate an ideal TSL. + * + * @param AssetGraph $graph + * @param AssetGraph $transpose + * + * @return \SplQueue $queue + * A queue of vertices + */ + protected function createSourceQueue(AssetGraph $graph, AssetGraph $transpose) { + $reach_visitor = new DepthFirstBasicVisitor(); + + // Find source vertices (outdegree 0) in the original graph + $sources = DepthFirst::find_sources($graph, $reach_visitor); + + // Traverse the transposed graph to get reachability data on each vertex + DepthFirst::traverse($transpose, $reach_visitor, clone $sources); + + // 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 + // 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; + } + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/CssCollectionOptimizerNouveaux.php b/core/lib/Drupal/Core/Asset/CssCollectionOptimizerNouveaux.php new file mode 100644 index 0000000..d35d3f9 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/CssCollectionOptimizerNouveaux.php @@ -0,0 +1,72 @@ +grouper = $grouper; + $this->optimizer = $optimizer; + $this->dumper = $dumper; + $this->state = $state; + } + + /** + * {@inheritdoc} + */ + public function optimize(array $assets) { + $tsl = $this->grouper->group($assets); + } + + +} \ 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..ed651bf --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Exception/AssetTypeMismatchException.php @@ -0,0 +1,16 @@ +sourceUrl = $sourceUrl; + $this->ignoreErrors = FALSE; // TODO expose somehow + + 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; + } + + /** + * Returns the time the current asset was last modified. + * + * @todo copied right from Assetic. needs to be made more Drupalish. + * + * @return integer|null A UNIX timestamp + */ + public function getLastModified() { + if (false !== @file_get_contents($this->sourceUrl, false, stream_context_create(array('http' => array('method' => 'HEAD'))))) { + foreach ($http_response_header as $header) { + if (0 === stripos($header, 'Last-Modified: ')) { + list(, $mtime) = explode(':', $header, 2); + + return strtotime(trim($mtime)); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function load(FilterInterface $additionalFilter = NULL) { + // TODO dumb and kinda 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..3125b26 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Factory/AssetCollector.php @@ -0,0 +1,267 @@ + 'Drupal\\Core\\Asset\\FileAsset', + 'external' => 'Drupal\\Core\\Asset\\ExternalAsset', + 'string' => 'Drupal\\Core\\Asset\\StringAsset', + ); + + public function __construct(AssetCollectionInterface $collection = NULL) { + $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); + if (!empty($options)) { + $metadata->replace($options); + } + + $class = $this->classMap[$source_type]; + $asset = new $class($metadata, $data, $filters); + + if (!empty($this->collection)) { + $this->add($asset); + } + + if ($asset_type == 'css' && !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} + * + * @throws \InvalidArgumentException + * Thrown if an invalid metadata type is provided (i.e., not 'css' or 'js'). + */ + public function setDefaultMetadata(AssetMetadataBag $metadata) { + if ($this->isLocked()) { + throw new LockedObjectException('The collector instance is locked. Asset defaults cannot be modified on a locked collector.'); + } + + $type = $metadata->getType(); + + if ($type === 'css') { + $this->defaultCssMetadata = $metadata; + } + elseif ($type === 'js') { + $this->defaultJsMetadata = $metadata; + } + else { + throw new \InvalidArgumentException(sprintf('Only assets of type "js" or "css" are supported, "%s" requested.', $type)); + } + } + + /** + * {@inheritdoc} + */ + public function getMetadataDefaults($type) { + if ($type === 'css') { + return clone $this->defaultCssMetadata; + } + elseif ($type === 'js') { + return clone $this->defaultJsMetadata; + } + else { + throw new \InvalidArgumentException(sprintf('Only assets of type "js" or "css" are supported, "%s" requested.', $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->defaultCssMetadata = new CssMetadataBag(); + $this->defaultJsMetadata = new JsMetadataBag(); + } +} + 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..2c5a817 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Factory/AssetCollectorInterface.php @@ -0,0 +1,210 @@ +source = $source; + + parent::__construct($metadata, $filters, $sourceRoot, $sourcePath); + } + + /** + * {@inheritdoc} + */ + public function id() { + return $this->source; + } + + /** + * Returns the time the current asset was last modified. + * + * @return integer|null A UNIX timestamp + */ + public function getLastModified() { + if (!is_file($this->source)) { + throw new \RuntimeException(sprintf('The source file "%s" does not exist.', $this->source)); + } + + return filemtime($this->source); + } + + /** + * Loads the asset into memory and applies load filters. + * + * You may provide an additional filter to apply during load. + * + * @todo copied right from Assetic. needs to be made more Drupalish. + * + * @param FilterInterface $additionalFilter An additional filter + */ + 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/Metadata/AssetMetadataBag.php b/core/lib/Drupal/Core/Asset/Metadata/AssetMetadataBag.php new file mode 100644 index 0000000..4681ea9 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Metadata/AssetMetadataBag.php @@ -0,0 +1,110 @@ +default = array_replace_recursive($this->default, $default); + } + + /** + * Indicates the type of asset for which this metadata is intended. + * + * @return string + * A string indicating type - 'js' or 'css' are the expected values. + */ + abstract public function getType(); + + public function all() { + return array_replace_recursive($this->default, $this->explicit); + } + + public function keys() { + return array_keys($this->all()); + } + + public function has($key) { + return array_key_exists($key, $this->explicit) || + array_key_exists($key, $this->default); + } + + public function set($key, $value) { + $this->explicit[$key] = $value; + } + + /** + * Reverts the associated with the passed key back to its default. + * + * If no default is set, the value for that key simply disappears. + * + * @param $key + * The key identifying the value to revert. + * + * @return void + */ + public function revert($key) { + unset($this->explicit[$key]); + } + + public function isDefault($key) { + return !array_key_exists($key, $this->explicit) && + array_key_exists($key, $this->default); + } + + public function add(array $values = array()) { + $this->explicit = array_replace_recursive($this->explicit, $values); + } + + public function replace(array $values = array()) { + $this->explicit = $values; + } + + public function get($key) { + if (array_key_exists($key, $this->explicit)) { + return $this->explicit[$key]; + } + + if (array_key_exists($key, $this->default)) { + return $this->default[$key]; + } + } + + public function getIterator() { + return new \ArrayIterator($this->all()); + } + + public function count() { + return count($this->all()); + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/Metadata/CssMetadataBag.php b/core/lib/Drupal/Core/Asset/Metadata/CssMetadataBag.php new file mode 100644 index 0000000..d4c6322 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Metadata/CssMetadataBag.php @@ -0,0 +1,37 @@ + CSS_AGGREGATE_DEFAULT, // TODO Just removing this would be *awesome*. + 'every_page' => FALSE, + 'media' => 'all', + 'preprocess' => TRUE, + 'browsers' => array( + 'IE' => TRUE, + '!IE' => TRUE, + ), + ); + + public function __construct(array $default = array()) { + $this->default = array_replace_recursive($this->default, $default); + } + + /** + * {@inheritdoc} + */ + public function getType() { + return 'css'; + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/Metadata/JsMetadataBag.php b/core/lib/Drupal/Core/Asset/Metadata/JsMetadataBag.php new file mode 100644 index 0000000..39927c0 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Metadata/JsMetadataBag.php @@ -0,0 +1,37 @@ + JS_DEFAULT, + 'every_page' => FALSE, + 'scope' => 'header', + 'cache' => TRUE, + 'preprocess' => TRUE, + 'attributes' => array(), + 'version' => NULL, + 'browsers' => array(), + ); + + public function __construct(array $default = array()) { + $this->default = array_replace_recursive($this->default, $default); + } + + /** + * {@inheritdoc} + */ + public function getType() { + return 'js'; + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/OptimallyGroupedTSLVisitor.php b/core/lib/Drupal/Core/Asset/OptimallyGroupedTSLVisitor.php new file mode 100644 index 0000000..cc77d0e --- /dev/null +++ b/core/lib/Drupal/Core/Asset/OptimallyGroupedTSLVisitor.php @@ -0,0 +1,111 @@ +tsl = array(); + $this->groups = $groups; + $this->vertexMap = $vertex_map; + } + + /** + * {@inheritdoc} + */ + public function onInitializeVertex($vertex, $source, \SplQueue $queue) {} + + /** + * {@inheritdoc} + */ + public function onBackEdge($vertex, \Closure $visit) { + // TODO: Implement onBackEdge() method. + } + + /** + * {@inheritdoc} + */ + public function onStartVertex($vertex, \Closure $visit) { + $this->active->attach($vertex); + + // If there's a record in the vertex map, it means this vertex has an + // optimal group. Remove it from that group, as it being here means it's + // been 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) { + // TODO this still isn't quite optimal; it can split groups unnecessarily. tweak a little more. + // TODO explore risk of hitting the 100 call stack limit + if ($this->vertexMap->contains($vertex)) { + foreach ($this->vertexMap[$vertex] as $vertex) { + $visit($vertex); + } + } + $this->tsl[] = $vertex; + } + + /** + * Returns the TSL produced by a depth-first traversal. + * + * @return array + * A topologically sorted list of vertices. + */ + public function getTSL() { + return $this->tsl; + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/StringAsset.php b/core/lib/Drupal/Core/Asset/StringAsset.php new file mode 100644 index 0000000..e98000e --- /dev/null +++ b/core/lib/Drupal/Core/Asset/StringAsset.php @@ -0,0 +1,59 @@ +id= empty($content) ? Crypt::randomStringHashed(32) : hash('sha256', $content); + $this->setContent($content); + $this->lastModified = REQUEST_TIME; // TODO this is terrible + + parent::__construct($metadata, $filters); + } + + /** + * {@inheritdoc} + */ + public function id() { + return $this->id; + } + + public function setLastModified($last_modified) { + $this->lastModified = $last_modified; + } + + public function getLastModified() { + return $this->lastModified; + } + + 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 579e841..1768991 100644 --- a/core/modules/block/lib/Drupal/block/BlockBase.php +++ b/core/modules/block/lib/Drupal/block/BlockBase.php @@ -11,6 +11,7 @@ use Drupal\block\BlockInterface; use Drupal\Component\Utility\Unicode; use Drupal\Core\Language\Language; +use Drupal\Core\Asset\Factory\AssetCollector; /** * Defines a base block implementation that most blocks plugins will extend. @@ -181,5 +182,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 b5433e6..d625616 100644 --- a/core/modules/block/lib/Drupal/block/BlockPluginInterface.php +++ b/core/modules/block/lib/Drupal/block/BlockPluginInterface.php @@ -10,6 +10,7 @@ use Drupal\Component\Plugin\PluginInspectionInterface; use Drupal\Component\Plugin\ConfigurablePluginInterface; use Drupal\Core\Plugin\PluginFormInterface; +use Drupal\Core\Asset\Factory\AssetCollector; /** * Defines the required interface for all block plugins. @@ -122,4 +123,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/BaseAggregateAssetTest.php b/core/tests/Drupal/Tests/Core/Asset/Aggregate/BaseAggregateAssetTest.php new file mode 100644 index 0000000..2871580 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/Aggregate/BaseAggregateAssetTest.php @@ -0,0 +1,108 @@ + 'Asset aggregate tests', + 'description' => 'Unit tests on BaseAggregateAsset', + 'group' => 'Asset', + ); + } + + public function getAggregate($defaults = array()) { + $mockmeta = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag', $defaults); + $this->aggregate = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Aggregate\\BaseAggregateAsset'); + } + + public function testId() { + + } + + public function testGetAssetType() { + + } + + public function testGetMetadata() { + + } + + public function testContains() { + + } + + public function testGetById() { + + } + + public function testIsPreprocessable() { + + } + + public function testAll() { + + } + + public function testEnsureFilter() { + + } + + public function testGetFilters() { + + } + + public function testClearFilters() { + + } + + public function testGetContent() { + + } + + public function testSetContent() { + + } + + public function testGetSourceRoot() { + + } + + public function testGetSourcePath() { + + } + + public function testGetTargetPath() { + + } + + public function testSetTargetPath() { + + } + + public function testGetLastModified() { + + } + + public function testGetIterator() { // ?? + + } + + public function testIsEmpty() { + + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/AssetGraphTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetGraphTest.php new file mode 100644 index 0000000..b83698d --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/AssetGraphTest.php @@ -0,0 +1,258 @@ + '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->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag'); + $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->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag'); + $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->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag'); + $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/AssetUnitTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetUnitTest.php new file mode 100644 index 0000000..dab09b3 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/AssetUnitTest.php @@ -0,0 +1,28 @@ +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($this->randomName())); + + return $asset; + } +} \ No newline at end of file diff --git a/core/tests/Drupal/Tests/Core/Asset/AsseticAdapterAssetTest.php b/core/tests/Drupal/Tests/Core/Asset/AsseticAdapterAssetTest.php new file mode 100644 index 0000000..69a2bdb --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/AsseticAdapterAssetTest.php @@ -0,0 +1,58 @@ + 'Assetic adapter asset test', + 'description' => 'Tests that certain Assetic methods throw known exceptions in a Drupal context', + 'group' => 'Asset', + ); + } + + public function setUp() { + $this->mock = $this->getMockForAbstractClass('Drupal\\Core\\Asset\\AsseticAdapterAsset'); + } + + /** + * @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(); + } +} 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..f40fc23 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/BaseAssetTest.php @@ -0,0 +1,138 @@ + 'Base Asset tests', + 'description' => 'Unit tests for Drupal\'s BaseAsset.', + 'group' => 'Asset', + ); + } + + /** + * Creates a BaseAsset for testing purposes. + * + * @param $type + * + * @return BaseAsset; + */ + public function createBaseAsset($defaults = array()) { + $mockmeta = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag', $defaults); + return $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\BaseAsset', array($mockmeta)); + } + + public function testGetMetadata() { + $mockmeta = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag'); + $asset = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\BaseAsset', array($mockmeta)); + + $this->assertSame($mockmeta, $asset->getMetadata()); + } + + public function testGetAssetType() { + $mockmeta = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag'); + $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'); + $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()); + } + + /** + * Tests all dependency-related methods. + */ + public function testDependencies() { + $asset = $this->createBaseAsset(); + + $asset->addDependency('foo', 'bar'); + $this->assertEquals(array(array('foo', 'bar')), $asset->getDependencyInfo()); + $this->assertTrue($asset->hasDependencies()); + + $asset->clearDependencies(); + $this->assertEmpty($asset->getDependencyInfo()); + + $invalid = array(0, 1.1, fopen(__FILE__, 'r'), TRUE, array(), new \stdClass); + + try { + foreach ($invalid as $val) { + $asset->addDependency($val, $val); + $this->fail('Was able to create an ordering relationship with an inappropriate value.'); + } + } catch (\InvalidArgumentException $e) {} + } + + public function testSuccessors() { + $asset = $this->createBaseAsset(); + $dep = $this->createBaseAsset(); + + $asset->before('foo'); + $asset->before($dep); + + $this->assertEquals(array('foo', $dep), $asset->getSuccessors()); + + $asset->clearSuccessors(); + $this->assertEmpty($asset->getSuccessors()); + + $invalid = array(0, 1.1, fopen(__FILE__, 'r'), TRUE, array(), new \stdClass); + + try { + foreach ($invalid as $val) { + $asset->before($val); + $this->fail('Was able to create an ordering relationship with an inappropriate value.'); + } + } catch (\InvalidArgumentException $e) {} + } + + public function testPredecessors() { + $asset = $this->createBaseAsset(); + $dep = $this->createBaseAsset(); + + $asset->after('foo'); + $asset->after($dep); + $this->assertEquals(array('foo', $dep), $asset->getPredecessors()); + + $asset->clearPredecessors(); + $this->assertEmpty($asset->getPredecessors()); + + $invalid = array(0, 1.1, fopen(__FILE__, 'r'), TRUE, array(), new \stdClass); + + try { + foreach ($invalid as $val) { + $asset->after($val); + $this->fail('Was able to create an ordering relationship with an inappropriate value.'); + } + } catch (\InvalidArgumentException $e) {} + } + + public function testClone() { + $mockmeta = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag'); + $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..22edcc2 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/Collection/AssetCollectionTest.php @@ -0,0 +1,203 @@ + 'Asset collection tests', + 'description' => 'Unit tests on AssetCollection', + 'group' => 'Asset', + ); + } + + public function setUp() { + $this->collection = new AssetCollection(); + } + + public function testAdd() { + $css = $this->createMockFileAsset('css'); + $js = $this->createMockFileAsset('js'); + + $this->collection->add($css); + $this->collection->add($js); + + $this->assertContains($css, $this->collection); + $this->assertContains($js, $this->collection); + } + + public function testGetCss() { + $css = $this->createMockFileAsset('css'); + $js = $this->createMockFileAsset('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); + } + + public function testGetJs() { + $css = $this->createMockFileAsset('css'); + $js = $this->createMockFileAsset('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); + } + + public function testAll() { + $css = $this->createMockFileAsset('css'); + $js = $this->createMockFileAsset('js'); + + $this->collection->add($css); + $this->collection->add($js); + + $this->assertEquals(array($css->id() => $css, $js->id() => $js), $this->collection->all()); + } + + public function testRemoveByAsset() { + $stub = $this->createMockFileAsset('css'); + + $this->collection->add($stub); + $this->collection->remove($stub); + + $this->assertNotContains($stub, $this->collection); + } + + public function testRemoveById() { + $stub = $this->createMockFileAsset('css'); + + $this->collection->add($stub); + $this->collection->remove($stub->id()); + + $this->assertNotContains($stub, $this->collection); + } + + /** + * @expectedException OutOfBoundsException + */ + public function testRemoveNonexistentId() { + $this->assertFalse($this->collection->remove('foo')); + $this->collection->remove('foo', FALSE); + } + + /** + * @expectedException OutOfBoundsException + */ + public function testRemoveNonexistentAsset() { + $stub = $this->createMockFileAsset('css'); + $this->assertFalse($this->collection->remove($stub)); + $this->collection->remove($stub, FALSE); + } + + public function testRemoveInvalidType() { + $invalid = array(0, 1.1, fopen(__FILE__, 'r'), TRUE, array(), new \stdClass); + try { + foreach ($invalid as $val) { + $this->collection->remove($val); + $this->fail('AssetCollection::remove() did not throw exception on invalid argument type.'); + } + } catch (\InvalidArgumentException $e) {} + } + + public function testMergeCollection() { + $coll2 = new AssetCollection(); + $stub1 = $this->createMockFileAsset('css'); + $stub2 = $this->createMockFileAsset('js'); + + $coll2->add($stub1); + $this->collection->mergeCollection($coll2); + + $this->assertContains($stub1, $this->collection); + $this->assertTrue($coll2->isFrozen()); + + $coll3 = new AssetCollection(); + $coll3->add($stub1); + $coll3->add($stub2); + // Ensure no duplicates, and don't freeze merged bag + $this->collection->mergeCollection($coll3, FALSE); + + $contained = array( + $stub1->id() => $stub1, + $stub2->id() => $stub2, + ); + $this->assertEquals($contained, $this->collection->all()); + $this->assertFalse($coll3->isFrozen()); + } + + /** + * Tests that all methods should be disabled by freezing the collection + * correctly trigger an exception. + */ + public function testExceptionOnWriteWhenFrozen() { + $stub = $this->createMockFileAsset('css'); + $write_protected = array( + 'add' => $stub, + 'remove' => $stub, + 'mergeCollection' => $this->getMock('\\Drupal\\Core\\Asset\\Collection\\AssetCollection'), + ); + + $this->collection->freeze(); + foreach ($write_protected as $method => $arg) { + try { + $this->collection->$method($arg); + $this->fail('Was able to run writable method on frozen AssetCollection'); + } + catch (\LogicException $e) {} + } + } + + /** + * @expectedException OutOfBoundsException + */ + public function testGetById() { + $metamock = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag'); + + $asset = $this->getMock('\\Drupal\\Core\\Asset\\FileAsset', array(), array($metamock, 'foo')); + $asset->expects($this->once()) + ->method('id') + ->will($this->returnValue('foo')); + + $this->collection->add($asset); + $this->assertSame($asset, $this->collection->getById('foo')); + + // Nonexistent asset + $this->assertFalse($this->collection->getById('bar')); + + // Nonexistent asset, non-graceful + $this->collection->getById('bar', FALSE); + } + + public function testIsEmpty() { + $this->assertTrue($this->collection->isEmpty()); + } + +} 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..5608951 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/Collection/AssetLibraryTest.php @@ -0,0 +1,96 @@ + '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') + ->addDependency('foo', 'bar'); + return $library; + } + + public function testConstructorValueInjection() { + $values = array( + 'title' => 'foo', + 'version' => '1.2.3', + 'website' => 'http://foo.bar', + 'dependencies' => array(array('foo', 'bar')), + ); + $library = new AssetLibrary($values); + + $fixture = $this->getLibraryFixture(); + $this->assertEquals($fixture->getTitle(), $library->getTitle(), 'Title passed correctly through the constructor.'); + $this->assertEquals($fixture->getVersion(), $library->getVersion(), 'Version passed correctly through the constructor.'); + $this->assertEquals($fixture->getWebsite(), $library->getWebsite(), 'Website passed correctly through the constructor.'); + $this->assertEquals($fixture->getDependencyInfo(), $library->getDependencyInfo(), 'Dependencies information passed correctly through the constructor.'); + } + + public function testAddDependency() { + $library = $this->getLibraryFixture(); + $library->addDependency('baz', 'bing'); + $this->assertEquals($library->getDependencyInfo(), array(array('foo', 'bar'), array('baz', 'bing')), 'Dependencies added to library successfully.'); + } + + public function testClearDependencies() { + $library = $this->getLibraryFixture(); + $library->clearDependencies(); + $this->assertEmpty($library->getDependencyInfo(), 'Dependencies recorded in the library were cleared correctly.'); + } + + public function testFrozenNonwriteability() { + $library = $this->getLibraryFixture(); + $library->freeze(); + try { + $library->setTitle('bar'); + $this->fail('No exception thrown when attempting to set a new title on a frozen library.'); + } + catch (\LogicException $e) {} + + try { + $library->setVersion('2.3.4'); + $this->fail('No exception thrown when attempting to set a new version on a frozen library.'); + } + catch (\LogicException $e) {} + + try { + $library->setWebsite('http://bar.baz'); + $this->fail('No exception thrown when attempting to set a new website on a frozen library.'); + } + catch (\LogicException $e) {} + + try { + $library->addDependency('bing', 'bang'); + $this->fail('No exception thrown when attempting to add a new dependency on a frozen library.'); + } + catch (\LogicException $e) {} + + try { + $library->clearDependencies(); + $this->fail('No exception thrown when attempting to clear dependencies from a frozen library.'); + } + catch (\LogicException $e) {} + } +} 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..a853afe --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/Factory/AssetCollectorTest.php @@ -0,0 +1,289 @@ + 'Asset Collector tests', + 'description' => 'Tests that the AssetCollector system works correctly.', + 'group' => 'Asset', + ); + } + + public function setUp() { + parent::setUp(); + $this->collector = new AssetCollector(); + } + + /** + * 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.'); + $this->assertFalse($meta->isDefault('group')); + } + + public function testDefaultPropagation() { + // Test that defaults are correctly applied by the factory. + $meta = new CssMetadataBag(array('every_page' => TRUE, 'group' => CSS_AGGREGATE_THEME)); + $this->collector->setDefaultMetadata($meta); + $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->createMockFileAsset('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->setDefaultMetadata(new CssMetadataBag()); + } + + /** + * @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 testBuiltinDefaultAreTheSame() { + $this->assertEquals(new CssMetadataBag(), $this->collector->getMetadataDefaults('css')); + $this->assertEquals(new JsMetadataBag(), $this->collector->getMetadataDefaults('js')); + } + + public function testChangeAndRestoreDefaults() { + $changed_css = new CssMetadataBag(array('foo' => 'bar', 'every_page' => TRUE)); + $this->collector->setDefaultMetadata($changed_css); + + $this->assertEquals($changed_css, $this->collector->getMetadataDefaults('css')); + $this->assertNotSame($changed_css, $this->collector->getMetadataDefaults('css'), 'Metadata is cloned on retrieval from collector.'); + + $this->collector->restoreDefaults(); + $this->assertEquals(new CssMetadataBag(), $this->collector->getMetadataDefaults('css')); + + // Do another check to ensure that both metadata bags are correctly reset + $changed_js = new JsMetadataBag(array('scope' => 'footer', 'fizzbuzz' => 'llama')); + $this->collector->setDefaultMetadata($changed_css); + $this->collector->setDefaultMetadata($changed_js); + + $this->assertEquals($changed_css, $this->collector->getMetadataDefaults('css')); + $this->assertEquals($changed_js, $this->collector->getMetadataDefaults('js')); + + $this->collector->restoreDefaults(); + $this->assertEquals(new CssMetadataBag(), $this->collector->getMetadataDefaults('css')); + $this->assertEquals(new JsMetadataBag(), $this->collector->getMetadataDefaults('js')); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testMetadataTypeMustBeCorrect() { + $mock = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag'); + $mock->expects($this->once()) + ->method('getType') + ->will($this->returnValue('foo')); + + $this->collector->setDefaultMetadata($mock); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testGetNonexistentDefault() { + $this->collector->getMetadataDefaults('foo'); + } + + + 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() { + $css1 = $this->collector->create('css', 'file', 'foo.css'); + $css2 = $this->collector->create('css', 'file', 'foo2.css'); + $this->assertEquals(array($css1), $css2->getPredecessors()); + + $this->collector->clearLastCss(); + $css3 = $this->collector->create('css', 'file', 'foo3.css'); + $this->assertEmpty($css3->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/Metadata/AssetMetadataBagTest.php b/core/tests/Drupal/Tests/Core/Asset/Metadata/AssetMetadataBagTest.php new file mode 100644 index 0000000..00876f8 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/Metadata/AssetMetadataBagTest.php @@ -0,0 +1,95 @@ + 'Asset Metadata bag test', + 'description' => 'Tests various methods of AssetMetadatabag', + 'group' => 'Asset', + ); + } + + public function createBag($args = array(array('foo' => 'bar', 'baz' => 'qux'))) { + return $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag', $args); + } + + /** + * A unified test for all operations that rely on calling get() in order to + * verify their correctness. + */ + public function testGetValueOperations() { + // First, ensure that constructor-injected defaults are correctly tracked. + $mock = $this->createBag(); + $this->assertEquals('bar', $mock->get('foo')); + // Ensure that constructor-injected defaults are correctly reported as such. + $this->assertTrue($mock->isDefault('foo')); + + // Set an explicit value, and ensure that it comes back out correctly. + $mock->set('bing', 'bang'); + $this->assertEquals('bang', $mock->get('bing')); + $this->assertFalse($mock->isDefault('bing')); + + // Set an explicit value that overrides a default, this time. + $mock->set('foo', 'kablow'); + $this->assertEquals('kablow', $mock->get('foo')); + $this->assertFalse($mock->isDefault('foo')); + + // Revert the set value, and ensure the old default comes through. + $mock->revert('foo'); + $this->assertEquals('bar', $mock->get('foo')); + $this->assertTrue($mock->isDefault('foo')); + + // Add value via add(), now + $mock->add(array('llama' => 'a pink one')); + $this->assertEquals('a pink one', $mock->get('llama')); + $this->assertFalse($mock->isDefault('llama')); + + // Finally, check that getting an unknown key returns nothing + $this->assertNull($mock->get('nonexistent')); + } + + public function testAll() { + $this->assertEquals(array('foo' => 'bar', 'baz' => 'qux'), $this->createBag()->all()); + } + + public function testKeys() { + $this->assertEquals(array('foo', 'baz'), $this->createBag()->keys()); + } + + public function testHas() { + $this->assertTrue($this->createBag()->has('foo')); + } + + public function testIteration() { + $found = array(); + foreach ($this->createBag() as $val) { + $found[] = $val; + } + + $this->assertEquals(array('bar', 'qux'), $found); + } + + public function testCount() { + $this->assertCount(2, $this->createBag()); + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/Metadata/CssMetadataBagTest.php b/core/tests/Drupal/Tests/Core/Asset/Metadata/CssMetadataBagTest.php new file mode 100644 index 0000000..08fbb83 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/Metadata/CssMetadataBagTest.php @@ -0,0 +1,32 @@ + 'CSS Metadata bag test', + 'description' => 'Tests various methods of CssMetadatabag', + 'group' => 'Asset', + ); + } + + public function testGetType() { + $bag = new CssMetadataBag(); + $this->assertEquals('css', $bag->getType()); + } +} + diff --git a/core/tests/Drupal/Tests/Core/Asset/Metadata/JsMetadataBagTest.php b/core/tests/Drupal/Tests/Core/Asset/Metadata/JsMetadataBagTest.php new file mode 100644 index 0000000..7cc5600 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/Metadata/JsMetadataBagTest.php @@ -0,0 +1,32 @@ + 'JS Metadata bag test', + 'description' => 'Tests various methods of JsMetadatabag', + 'group' => 'Asset', + ); + } + + public function testGetType() { + $bag = new JsMetadataBag(); + $this->assertEquals('js', $bag->getType()); + } +} +