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->assetStorage = new \SplObjectStorage(); + $this->nestedStorage = new \SplObjectStorage(); + + 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); + + if (!$this->contains($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 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; + } + + /** + * 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..10ca555 --- /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..2c9f86d --- /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/AssetCollectionAggregatorInterface.php b/core/lib/Drupal/Core/Asset/AssetCollectionAggregatorInterface.php new file mode 100644 index 0000000..17b7191 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/AssetCollectionAggregatorInterface.php @@ -0,0 +1,28 @@ +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(); + + 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 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->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..ac1512b --- /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..713ba1e --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Collection/AssetCollection.php @@ -0,0 +1,198 @@ +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; + } + + /** + * {@inheritdoc} + */ + public function sort($callback) { + uksort($this->assetIdMap, $callback); + } + + /** + * {@inheritdoc} + */ + public function ksort() { + ksort($this->assetIdMap); + } + + /** + * 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..b837b6e --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Collection/AssetCollectionBasicInterface.php @@ -0,0 +1,78 @@ + $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/CssCollectionAggregator.php b/core/lib/Drupal/Core/Asset/CssCollectionAggregator.php new file mode 100644 index 0000000..626cc1c --- /dev/null +++ b/core/lib/Drupal/Core/Asset/CssCollectionAggregator.php @@ -0,0 +1,68 @@ +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 CssAggregateAsset($asset->getMetadata()); + $processed->add($aggregate); + } + + $aggregate->add($asset); + $last_key = $key; + } + + return $processed; + } +} \ 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..1e64377 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/CssCollectionOptimizerNouveaux.php @@ -0,0 +1,89 @@ +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'); + } + } + } + + return $collection; + } + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/DependencyInterface.php b/core/lib/Drupal/Core/Asset/DependencyInterface.php new file mode 100644 index 0000000..35132a8 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/DependencyInterface.php @@ -0,0 +1,47 @@ +sourceUrl = $sourceUrl; + + 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 getLastModified() { + // 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 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..26bbde4 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Factory/AssetCollector.php @@ -0,0 +1,249 @@ + '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' && !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..0a59ec2 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Factory/AssetCollectorInterface.php @@ -0,0 +1,219 @@ +source = $source; + + parent::__construct($metadata, $filters, $sourceRoot, $sourcePath); + } + + /** + * {@inheritdoc} + */ + public function id() { + return $this->source; + } + + /** + * {@inheritdoc} + */ + 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); + } + + /** + * {@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/AssetGraphSorter.php b/core/lib/Drupal/Core/Asset/GroupSort/AssetGraphSorter.php new file mode 100644 index 0000000..6311f9f --- /dev/null +++ b/core/lib/Drupal/Core/Asset/GroupSort/AssetGraphSorter.php @@ -0,0 +1,53 @@ +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/GroupSort/AssetGroupSorterInterface.php b/core/lib/Drupal/Core/Asset/GroupSort/AssetGroupSorterInterface.php new file mode 100644 index 0000000..eeb771c --- /dev/null +++ b/core/lib/Drupal/Core/Asset/GroupSort/AssetGroupSorterInterface.php @@ -0,0 +1,45 @@ +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(); + + 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 + $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($optimal, $optimal_lookup); + DepthFirst::traverse($transpose, $visitor, $queue); + + return $visitor->getTSL(); + } +} \ No newline at end of file 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..37f2e08 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Metadata/AssetMetadataBag.php @@ -0,0 +1,36 @@ +type = $type; + parent::__construct($values); + } + + /** + * {@inheritdoc} + */ + public function getType() { + return $this->type; + } +} \ No newline at end of file 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..e8eded6 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Metadata/AssetMetadataInterface.php @@ -0,0 +1,82 @@ + 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(), + )); + } +} \ No newline at end of file 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..9e3eb30 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Metadata/MetadataFactoryInterface.php @@ -0,0 +1,43 @@ +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/RelativePositionInterface.php b/core/lib/Drupal/Core/Asset/RelativePositionInterface.php new file mode 100644 index 0000000..66293d8 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/RelativePositionInterface.php @@ -0,0 +1,74 @@ +id= empty($content) ? Crypt::randomBytes(32) : hash('sha256', $content); + $this->setContent($content); + + 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..dca5da9 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/Aggregate/BaseAggregateAssetTest.php @@ -0,0 +1,156 @@ + 'Asset aggregate tests', + 'description' => 'Unit tests on BaseAggregateAsset', + 'group' => 'Asset', + ); + } + + /** + * Generates a simple BaseAggregateAsset mock. + * + * @param array $defaults + * Defaults to inject into the aggregate's metadata bag. + * + * @return BaseAggregateAsset + */ + public function getAggregate($defaults = array()) { + $mockmeta = $this->createStubAssetMetadata(); + return $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Aggregate\\BaseAggregateAsset', array($mockmeta)); + } + + public function testId() { + $aggregate = $this->getAggregate(); + + $asset1 = $this->createMockFileAsset('css'); + $asset2 = $this->createMockFileAsset('css'); + $aggregate->add($asset1); + $aggregate->add($asset2); + + $this->assertEquals(hash('sha256', $asset1->id() . $asset2->id()), $aggregate->id()); + } + + 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\\BaseAggregateAsset', array($mockmeta)); + + $this->assertEquals('unicorns', $aggregate->getAssetType()); + } + + public function testGetMetadata() { + $mockmeta = $this->createStubAssetMetadata(); + $aggregate = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Aggregate\\BaseAggregateAsset', array($mockmeta)); + + $this->assertSame($mockmeta, $aggregate->getMetadata()); + } + + public function testAdd() { + $aggregate = $this->getAggregate(); + + $metamock = $this->createStubAssetMetadata(); + $asset = $this->getMock('\\Drupal\\Core\\Asset\\FileAsset', array(), array($metamock, 'foo')); + $asset->expects($this->once()) + ->method('id') + ->will($this->returnValue('foo')); + + $aggregate->add($asset); + + $this->assertContains($asset, $aggregate); + } + + public function testContains() { + $aggregate = $this->getAggregate(); + $css = $this->createMockFileAsset('css'); + + $aggregate->add($css); + $this->assertTrue($aggregate->contains($css)); + } + + /** + * @expectedException OutOfBoundsException + */ + public function testGetById() { + $aggregate = $this->getAggregate(); + + $asset = $this->createMockFileAsset('css'); + $aggregate->add($asset); + $this->assertSame($asset, $aggregate->getById($asset->id())); + + // Nonexistent asset + $this->assertFalse($aggregate->getById('bar')); + + // Nonexistent asset, non-graceful + $aggregate->getById('bar', FALSE); + } + + public function testIsPreprocessable() { + $this->assertTrue($this->getAggregate()->isPreprocessable()); + } + + public function testAll() { + $aggregate = $this->getAggregate(); + + $asset1 = $this->createMockFileAsset('css'); + $asset2 = $this->createMockFileAsset('css'); + $aggregate->add($asset1); + $aggregate->add($asset2); + + $output = array( + $asset1->id() => $asset1, + $asset2->id() => $asset2, + ); + + $this->assertEquals($output, $aggregate->all()); + } + + public function testIsEmpty() { + $this->assertTrue($this->getAggregate()->isEmpty()); + } + + public function testRemove() { + $this->fail(); + } + + public function testRemoveLeaf() { + $this->fail(); + } + + public function testReplace() { + $this->fail(); + } + + public function testReplaceLeaf() { + $this->fail(); + } + + public function testLoad() { + $this->fail(); + } + + public function testDump() { + $this->fail(); + } +} 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..9b651ce --- /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->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/AssetUnitTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetUnitTest.php new file mode 100644 index 0000000..f2bc236 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/AssetUnitTest.php @@ -0,0 +1,57 @@ +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; + } + + /** + * Creates an asset metadata stub with basic values. + * + * @param string $type + * @param array $values + * + * @return AssetMetadataBag + */ + public function createStubAssetMetadata($type = 'css', $values = array()) { + return $this->getMockBuilder('Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag') + ->setConstructorArgs(array($type, $values)) + ->setMethods(array()) // mock nothing + ->getMock(); + } +} \ 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..79125da --- /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 array $defaults + * + * @return BaseAsset + */ + public function createBaseAsset($defaults = array()) { + $mockmeta = $this->createStubAssetMetadata(NULL, $defaults); + return $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\BaseAsset', array($mockmeta)); + } + + 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()); + } + + /** + * 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->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..56a092c --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/Collection/AssetCollectionTest.php @@ -0,0 +1,252 @@ + '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 testContains() { + $css = $this->createMockFileAsset('css'); + $this->collection->add($css); + $this->assertTrue($this->collection->contains($css)); + } + + 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->createStubAssetMetadata(); + + $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()); + } + + public function testSort() { + $stub1 = $this->createMockFileAsset('css'); + $stub2 = $this->createMockFileAsset('js'); + $stub3 = $this->createMockFileAsset('css'); + + $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->collection->sort($dummysort); + uksort($assets, $dummysort); + $this->assertEquals($assets, $this->collection->all()); + } + + public function testKsort() { + $stub1 = $this->createMockFileAsset('css'); + $stub2 = $this->createMockFileAsset('js'); + $stub3 = $this->createMockFileAsset('css'); + + $this->collection->add($stub1); + $this->collection->add($stub2); + $this->collection->add($stub3); + + $assets = array( + $stub1->id() => $stub1, + $stub2->id() => $stub2, + $stub3->id() => $stub3, + ); + + $this->collection->ksort(); + ksort($assets); + $this->assertEquals($assets, $this->collection->all()); + } +} 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/ExternalAssetTest.php b/core/tests/Drupal/Tests/Core/Asset/ExternalAssetTest.php new file mode 100644 index 0000000..b4d2618 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/ExternalAssetTest.php @@ -0,0 +1,65 @@ + '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 testGetLastModified() { + $meta = $this->createStubAssetMetadata(); + $asset = new ExternalAsset($meta, self::JQUERY); + + // TODO this throws an exception, but it should not. test fails till we fix. + $asset->getLastModified(); + } + + 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..723de5d --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/Factory/AssetCollectorTest.php @@ -0,0 +1,274 @@ + '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.'); + } + + 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->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->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() { + $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/FileAssetTest.php b/core/tests/Drupal/Tests/Core/Asset/FileAssetTest.php new file mode 100644 index 0000000..759d5d8 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/FileAssetTest.php @@ -0,0 +1,71 @@ + '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 testLastModified() { + // This seems less than ideal, but it's what Assetic does. So... + $meta = $this->createStubAssetMetadata(); + $asset = new FileAsset($meta, __FILE__); + + $this->assertInternalType('integer', $asset->getLastModified()); + + $asset = new FileAsset($meta, __FILE__ . '.foo'); + $asset->getLastModified(); + } + + /** + * @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/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/StringAssetTest.php b/core/tests/Drupal/Tests/Core/Asset/StringAssetTest.php new file mode 100644 index 0000000..27ee7f3 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/StringAssetTest.php @@ -0,0 +1,72 @@ + 'String asset tests', + 'description' => 'Unit tests for StringAsset', + 'group' => 'Asset', + ); + } + + public function testInitialCreation() { + $meta = $this->createStubAssetMetadata(); + $content = 'foo bar baz'; + $asset = new StringAsset($meta, $content); + + $this->assertEquals($content, $asset->getContent()); + $this->assertFalse($asset->getLastModified()); // TODO change this once we have a better plan + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testCreateNonString() { + $meta = $this->createStubAssetMetadata(); + $asset = new StringAsset($meta, new \stdClass()); + } + + public function testSetLastModified() { + $meta = $this->createStubAssetMetadata(); + $content = 'foo bar baz'; + $asset = new StringAsset($meta, $content); + + $asset->setLastModified(100); + $this->assertEquals(100, $asset->getLastModified()); + } + + public function testId() { + $meta = $this->createStubAssetMetadata(); + $content = 'foo bar baz'; + $asset = new StringAsset($meta, $content); + + $this->assertEquals(hash('sha256', $content), $asset->id()); + + $asset = new StringAsset($meta, ''); + // If no content is provided, the id should be a 32-byte random string (ick) + $this->assertEquals(32, strlen($asset->id())); + } + + 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()); + } +}