Browse Source

Merge branch 'master' into 3.next

ADmad 9 years ago
parent
commit
9aa6096a1d

+ 89 - 0
.mailmap

@@ -13,3 +13,92 @@ Walther Lalk <emailme@waltherlalk.com> <walther@octoplus.co.za>
 Walther Lalk <emailme@waltherlalk.com> <waltherlalk@gmail.com>
 Walther Lalk <emailme@waltherlalk.com> <dakota@users.noreply.github.com>
 Walther Lalk <emailme@waltherlalk.com> <walther@uafrica.com>
+Mark Scherer <euromark@web.de>
+Mark Scherer <euromark@web.de> <dereuromark@users.noreply.github.com>
+Mark Scherer <euromark@web.de> <mark.scherer@spryker.com>
+phpnut <phpnut@cakephp.org>
+phpnut <phpnut@cakephp.org> <phpnut@gmail.com>
+AD7six <andydawson76@gmail.com>
+AD7six <andydawson76@gmail.com> <andydawson76@yahoo.co.uk>
+predominant <graham@grahamweldon.com>
+mariano.iglesias <mariano.iglesias@3807eeeb-6ff5-0310-8944-8be069107fe0>
+mariano.iglesias <mariano.iglesias@3807eeeb-6ff5-0310-8944-8be069107fe0> <mariano@cricava.com>
+antograssiot <antograssiot@free.fr>
+Florian Krämer <florian.kraemer@cakedc.com>
+Florian Krämer <florian.kraemer@cakedc.com> <florian.kraemer@kreative-design.net>
+Florian Krämer <florian.kraemer@cakedc.com> <burzum@users.noreply.github.com>
+Rachman Chavik <rchavik@xintesa.com>
+Rachman Chavik <rchavik@xintesa.com> <rchavik@gmail.com>
+jperras <joel.perras@gmail.com>
+renan.saddam <renan.saddam@gmail.com>
+Ber Clausen <crashcookie@gmail.com>
+Marc Würth <ravage@bluewin.ch>
+Jad Bitar <jadbitar@mac.com>
+Jad Bitar <jadbitar@mac.com> <jadb@users.noreply.github.com>
+Jad Bitar <jadbitar@mac.com> <bitarjad@gmail.com>
+Yves P <havokinspiration@gmail.com>
+dogmatic69 <dogmatic69@gmail.com>
+Majna <majnaggz@gmail.com>
+Robert Pustułka <robert.pustulka@gmail.com>
+Robert Pustułka <robert.pustulka@gmail.com> <r.pustulka@zano.pl>
+Thomas Ploch <t.ploch@reizwerk.com>
+Tigran Gabrielyan <tigrangab@gmail.com>
+Bryan Crowe <bryan@zapsolutions.com>
+Sam <sgpinkus@gmail.com>
+Jorge González <steinkel@gmail.com>
+Saleh Souzanchi <saleh.souzanchi@gmail.com>
+Yevgeny Tomenko <skie@mail.ru>
+Ricardo Arturo Cabral <ricardo.arturo.cabral@gmail.com>
+Cauan Cabral <cauan@radig.com.br>
+pirouet <pirouet@me.com>
+davidsteinsland <david@davidsteinsland.net>
+jamiemill <jamiermill@gmail.com>
+Stefan Dickmann <stefan@php-engineer.de>
+Benjamin Tamási <h@lfto.me>
+Gordon Pettey (petteyg) <petteyg359@gmail.com>
+Mathieu de Ruiter <mathieu@fellicht.nl>
+Cees-Jan Kiewiet <ceesjank@gmail.com>
+Fiblan <fib@ingegnosamente.com>
+Haithem BEN GHORBAL <haithem.benghorbal@gmail.com>
+Pedro Perejón <pperejon@proavan.com>
+Algirdas Gurevicius <a.gurevicius@gmail.com>
+Calin <calinseciu@gmail.com>
+Mikaël Capelle <capelle.mikael@gmail.com>
+OKINAKA Kenshin <okinakak@yahoo.co.jp>
+Walter Nasich <wnasich@gmail.com>
+mstra001 <ms@creemedia.com>
+Aymeric Derbois <aymeric@derbois.com>
+Daniel <wrightdaniel86@gmail.com>
+James Michael DuPont <jamesmikedupont@gmail.com>
+James Michael DuPont <jamesmikedupont@gmail.com> <JamesMikeDuPont@gmail.com>
+Jan Dorsman <oldskool@fluxbb.org>
+Pierre Martin <contact@pierre-martin.fr>
+Jeremy Harris <jeremy@42pixels.com>
+Jeremy Harris <jeremy@42pixels.com> <jeremy@someguyjeremy.com>
+Christian Winther <jippignu@gmail.com>
+Christian Winther <jippignu@gmail.com> <cw@bownty.com>
+Christian Winther <jippignu@gmail.com> <cw@nodes.dk>
+Jose Diaz-Gonzalez <email@josediazgonzalez.com>
+Jose Diaz-Gonzalez <email@josediazgonzalez.com> <josegonzalez@users.noreply.github.com>
+Jose Diaz-Gonzalez <email@josediazgonzalez.com> <myaccounts@savant.be>
+Frank de Graaf <info@frankdegraaf.net>
+Frank de Graaf <info@frankdegraaf.net> Frank de Graaf <frank@fdeg.nl>
+Frank de Graaf <info@frankdegraaf.net> <Phally@users.noreply.github.com>
+Marlin Cremers <m.cremers@cvo-technologies.com>
+Marlin Cremers <m.cremers@cvo-technologies.com> <m.cremers@mms-projects.net>
+Marlin Cremers <m.cremers@cvo-technologies.com> <marlin.cremers@gmail.com>
+David Yell <neon1024@gmail.com> <dyell@ukwebmedia.com>
+James Watts <james.watts@cakephp.org> <jameswatts@solfenix.com>
+Jonas Hartmann <jh@ht-studios.de>
+Jonas Hartmann <jh@ht-studios.de> <github@ht-studios.de>
+Jonas Hartmann <jh@ht-studios.de> <fwd_github@jonas-hartmann.com>
+Thom Seddon <thom@seddonmedia.co.uk>
+Thom Seddon <thom@seddonmedia.co.uk> <thom@nightworld.com>
+Robbert Noordzij <robbert@xseeding.com>
+Robbert Noordzij <robbert@xseeding.com> <robbert@xseeding.nl>
+Simon East <me@simoneast.net>
+Simon East <me@simoneast.net> <simon@surfacemedia.com.au>
+Matt Alexander <alexandma@advisory.com>
+Matt Alexander <alexandma@advisory.com> <mattalexx@gmail.com>
+Mark van Driel <mvdriel@mvdriel-EP35-DS3R>
+Mark van Driel <mvdriel@mvdriel-EP35-DS3R> <mvdriel@mvdriel-UL30>

+ 2 - 1
src/ORM/Table.php

@@ -1728,7 +1728,8 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
         }
 
         if (!$entity->has($primaryColumns)) {
-            $message = 'All primary key value(s) are needed for updating';
+            $message = 'All primary key value(s) are needed for updating, ';
+            $message .= get_class($entity) . ' is missing ' . implode(', ', $primaryColumns);
             throw new InvalidArgumentException($message);
         }
 

+ 319 - 0
src/View/Helper/BreadcrumbsHelper.php

@@ -0,0 +1,319 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.3.6
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\View\Helper;
+
+use Cake\View\Helper;
+use Cake\View\StringTemplateTrait;
+use LogicException;
+
+/**
+ * BreadcrumbsHelper to register and display a breadcrumb trail for your views
+ *
+ * @property \Cake\View\Helper\UrlHelper $Url
+ */
+class BreadcrumbsHelper extends Helper
+{
+
+    use StringTemplateTrait;
+
+    /**
+     * Other helpers used by BreadcrumbsHelper.
+     *
+     * @var array
+     */
+    public $helpers = ['Url'];
+
+    /**
+     * Default config for the helper.
+     *
+     * @var array
+     */
+    protected $_defaultConfig = [
+        'templates' => [
+            'wrapper' => '<ul{{attrs}}>{{content}}</ul>',
+            'item' => '<li{{attrs}}><a href="{{url}}"{{innerAttrs}}>{{title}}</a></li>{{separator}}',
+            'itemWithoutLink' => '<li{{attrs}}><span{{innerAttrs}}>{{title}}</span></li>{{separator}}',
+            'separator' => '<li{{attrs}}><span{{innerAttrs}}>{{separator}}</span></li>'
+        ]
+    ];
+
+    /**
+     * The crumb list.
+     *
+     * @var array
+     */
+    protected $crumbs = [];
+
+    /**
+     * Add a crumb to the end of the trail.
+     *
+     * @param string|array $title If provided as a string, it represents the title of the crumb.
+     * Alternatively, if you want to add multiple crumbs at once, you can provide an array, with each values being a
+     * single crumb. Arrays are expected to be of this form:
+     * - *title* The title of the crumb
+     * - *link* The link of the crumb. If not provided, no link will be made
+     * - *options* Options of the crumb. See description of params option of this method.
+     * @param string|array|null $url URL of the crumb. Either a string, an array of route params to pass to
+     * Url::build() or null / empty if the crumb does not have a link.
+     * @param array $options Array of options. These options will be used as attributes HTML attribute the crumb will
+     * be rendered in (a <li> tag by default). It accepts two special keys:
+     * - *innerAttrs*: An array that allows you to define attributes for the inner element of the crumb (by default, to
+     * the link)
+     * - *templateVars*: Specific template vars in case you override the templates provided.
+     * @return $this
+     */
+    public function add($title, $url = null, array $options = [])
+    {
+        if (is_array($title)) {
+            foreach ($title as $crumb) {
+                $this->crumbs[] = $crumb + ['title' => '', 'url' => null, 'options' => []];
+            }
+
+            return $this;
+        }
+
+        $this->crumbs[] = compact('title', 'url', 'options');
+
+        return $this;
+    }
+
+    /**
+     * Prepend a crumb to the start of the queue.
+     *
+     * @param string $title If provided as a string, it represents the title of the crumb.
+     * Alternatively, if you want to add multiple crumbs at once, you can provide an array, with each values being a
+     * single crumb. Arrays are expected to be of this form:
+     * - *title* The title of the crumb
+     * - *link* The link of the crumb. If not provided, no link will be made
+     * - *options* Options of the crumb. See description of params option of this method.
+     * @param string|array|null $url URL of the crumb. Either a string, an array of route params to pass to
+     * Url::build() or null / empty if the crumb does not have a link.
+     * @param array $options Array of options. These options will be used as attributes HTML attribute the crumb will
+     * be rendered in (a <li> tag by default). It accepts two special keys:
+     * - *innerAttrs*: An array that allows you to define attributes for the inner element of the crumb (by default, to
+     * the link)
+     * - *templateVars*: Specific template vars in case you override the templates provided.
+     * @return $this
+     */
+    public function prepend($title, $url = null, array $options = [])
+    {
+        if (is_array($title)) {
+            $crumbs = [];
+            foreach ($title as $crumb) {
+                $crumbs[] = $crumb + ['title' => '', 'url' => null, 'options' => []];
+            }
+
+            array_splice($this->crumbs, 0, 0, $crumbs);
+
+            return $this;
+        }
+
+        array_unshift($this->crumbs, compact('title', 'url', 'options'));
+
+        return $this;
+    }
+
+    /**
+     * Insert a crumb at a specific index.
+     *
+     * If the index already exists, the new crumb will be inserted,
+     * and the existing element will be shifted one index greater.
+     * If the index is out of bounds, it will throw an exception.
+     *
+     * @param int $index The index to insert at.
+     * @param string $title Title of the crumb.
+     * @param string|array|null $url URL of the crumb. Either a string, an array of route params to pass to
+     * Url::build() or null / empty if the crumb does not have a link.
+     * @param array $options Array of options. These options will be used as attributes HTML attribute the crumb will
+     * be rendered in (a <li> tag by default). It accepts two special keys:
+     * - *innerAttrs*: An array that allows you to define attributes for the inner element of the crumb (by default, to
+     * the link)
+     * - *templateVars*: Specific template vars in case you override the templates provided.
+     * @return $this
+     * @throws LogicException In case the index is out of bound
+     */
+    public function insertAt($index, $title, $url = null, array $options = [])
+    {
+        if (!isset($this->crumbs[$index])) {
+            throw new LogicException(sprintf("No crumb could be found at index '%s'", $index));
+        }
+
+        array_splice($this->crumbs, $index, 0, [compact('title', 'url', 'options')]);
+
+        return $this;
+    }
+
+    /**
+     * Insert a crumb before the first matching crumb with the specified title.
+     *
+     * Finds the index of the first crumb that matches the provided class,
+     * and inserts the supplied callable before it.
+     *
+     * @param string $matchingTitle The title of the crumb you want to insert this one before.
+     * @param string $title Title of the crumb.
+     * @param string|array|null $url URL of the crumb. Either a string, an array of route params to pass to
+     * Url::build() or null / empty if the crumb does not have a link.
+     * @param array $options Array of options. These options will be used as attributes HTML attribute the crumb will
+     * be rendered in (a <li> tag by default). It accepts two special keys:
+     * - *innerAttrs*: An array that allows you to define attributes for the inner element of the crumb (by default, to
+     * the link)
+     * - *templateVars*: Specific template vars in case you override the templates provided.
+     * @return $this
+     * @throws LogicException In case the matching crumb can not be found
+     */
+    public function insertBefore($matchingTitle, $title, $url = null, array $options = [])
+    {
+        $key = $this->findCrumb($matchingTitle);
+
+        if ($key === null) {
+            throw new LogicException(sprintf("No crumb matching '%s' could be found.", $matchingTitle));
+        }
+
+        return $this->insertAt($key, $title, $url, $options);
+    }
+
+    /**
+     * Insert a crumb after the first matching crumb with the specified title.
+     *
+     * Finds the index of the first crumb that matches the provided class,
+     * and inserts the supplied callable before it.
+     *
+     * @param string $matchingTitle The title of the crumb you want to insert this one after.
+     * @param string $title Title of the crumb.
+     * @param string|array|null $url URL of the crumb. Either a string, an array of route params to pass to
+     * Url::build() or null / empty if the crumb does not have a link.
+     * @param array $options Array of options. These options will be used as attributes HTML attribute the crumb will
+     * be rendered in (a <li> tag by default). It accepts two special keys:
+     * - *innerAttrs*: An array that allows you to define attributes for the inner element of the crumb (by default, to
+     * the link)
+     * - *templateVars*: Specific template vars in case you override the templates provided.
+     * @return $this
+     * @throws LogicException In case the matching crumb can not be found.
+     */
+    public function insertAfter($matchingTitle, $title, $url = null, array $options = [])
+    {
+        $key = $this->findCrumb($matchingTitle);
+
+        if ($key === null) {
+            throw new LogicException(sprintf("No crumb matching '%s' could be found.", $matchingTitle));
+        }
+
+        return $this->insertAt($key + 1, $title, $url, $options);
+    }
+
+    /**
+     * Returns the crumb list.
+     *
+     * @return array
+     */
+    public function getCrumbs()
+    {
+        return $this->crumbs;
+    }
+
+    /**
+     * Renders the breadcrumbs trail.
+     *
+     * @param array $attributes Array of attributes applied to the `wrapper` template. Accepts the `templateVars` key to
+     * allow the insertion of custom template variable in the template.
+     * @param array $separator Array of attributes for the `separator` template.
+     * Possible properties are :
+     * - *separator* The string to be displayed as a separator
+     * - *templateVars* Allows the insertion of custom template variable in the template
+     * - *innerAttrs* To provide attributes in case your separator is divided in two elements.
+     * All other properties will be converted as HTML attributes and will replace the *attrs* key in the template.
+     * If you use the default for this option (empty), it will not render a separator.
+     * @return string The breadcrumbs trail
+     */
+    public function render(array $attributes = [], array $separator = [])
+    {
+        $crumbs = $this->crumbs;
+        $crumbsCount = count($crumbs);
+        $templater = $this->templater();
+        $separatorString = '';
+
+        if ($separator) {
+            if (isset($separator['innerAttrs'])) {
+                $separator['innerAttrs'] = $templater->formatAttributes($separator['innerAttrs']);
+            }
+
+            $separator['attrs'] = $templater->formatAttributes(
+                $separator,
+                ['innerAttrs', 'separator']
+            );
+
+            $separatorString = $this->formatTemplate('separator', $separator);
+        }
+
+        $crumbTrail = '';
+        foreach ($crumbs as $key => $crumb) {
+            $url = $crumb['url'] ? $this->Url->build($crumb['url']) : null;
+            $title = $crumb['title'];
+            $options = $crumb['options'];
+
+            $optionsLink = [];
+            if (isset($options['innerAttrs'])) {
+                $optionsLink = $options['innerAttrs'];
+                unset($options['innerAttrs']);
+            }
+
+            $template = 'item';
+            $templateParams = [
+                'attrs' => $templater->formatAttributes($options, ['templateVars']),
+                'innerAttrs' => $templater->formatAttributes($optionsLink),
+                'title' => $title,
+                'url' => $url,
+                'separator' => '',
+                'templateVars' => isset($options['templateVars']) ? $options['templateVars'] : []
+            ];
+
+            if (!$url) {
+                $template = 'itemWithoutLink';
+            }
+
+            if ($separatorString && $key !== ($crumbsCount - 1)) {
+                $templateParams['separator'] = $separatorString;
+            }
+
+            $crumbTrail .= $this->formatTemplate($template, $templateParams);
+        }
+
+        $crumbTrail = $this->formatTemplate('wrapper', [
+            'content' => $crumbTrail,
+            'attrs' => $templater->formatAttributes($attributes, ['templateVars']),
+            'templateVars' => isset($attributes['templateVars']) ? $attributes['templateVars'] : []
+        ]);
+
+        return $crumbTrail;
+    }
+
+    /**
+     * Search a crumb in the current stack which title matches the one provided as argument.
+     * If found, the index of the matching crumb will be returned.
+     *
+     * @param string $title Title to find.
+     * @return int|null Index of the crumb found, or null if it can not be found.
+     */
+    protected function findCrumb($title)
+    {
+        foreach ($this->crumbs as $key => $crumb) {
+            if ($crumb['title'] === $title) {
+                return $key;
+            }
+        }
+
+        return null;
+    }
+}

+ 5 - 0
src/View/Helper/HtmlHelper.php

@@ -89,6 +89,7 @@ class HtmlHelper extends Helper
      * Breadcrumbs.
      *
      * @var array
+     * @deprecated 3.3.6 Use the BreadcrumbsHelper instead
      */
     protected $_crumbs = [];
 
@@ -153,6 +154,7 @@ class HtmlHelper extends Helper
      * @return $this
      * @see \Cake\View\Helper\HtmlHelper::link() for details on $options that can be used.
      * @link http://book.cakephp.org/3.0/en/views/helpers/html.html#creating-breadcrumb-trails-with-htmlhelper
+     * @deprecated 3.3.6 Use the BreadcrumbsHelper instead
      */
     public function addCrumb($name, $link = null, array $options = [])
     {
@@ -682,6 +684,7 @@ class HtmlHelper extends Helper
      *   also be an array, see above for details.
      * @return string|null Composed bread crumbs
      * @link http://book.cakephp.org/3.0/en/views/helpers/html.html#creating-breadcrumb-trails-with-htmlhelper
+     * @deprecated 3.3.6 Use the BreadcrumbsHelper instead
      */
     public function getCrumbs($separator = '&raquo;', $startText = false)
     {
@@ -720,6 +723,7 @@ class HtmlHelper extends Helper
      *   also be an array, see `HtmlHelper::getCrumbs` for details.
      * @return string|null Breadcrumbs HTML list.
      * @link http://book.cakephp.org/3.0/en/views/helpers/html.html#creating-breadcrumb-trails-with-htmlhelper
+     * @deprecated 3.3.6 Use the BreadcrumbsHelper instead
      */
     public function getCrumbList(array $options = [], $startText = false)
     {
@@ -772,6 +776,7 @@ class HtmlHelper extends Helper
      * @param string|array|bool $startText Text to prepend
      * @param bool $escape If the output should be escaped or not
      * @return array Crumb list including startText (if provided)
+     * @deprecated 3.3.6 Use the BreadcrumbsHelper instead
      */
     protected function _prepareCrumbs($startText, $escape = true)
     {

+ 1 - 1
src/View/JsonView.php

@@ -136,7 +136,7 @@ class JsonView extends SerializedView
         $data = $this->_dataToSerialize($serialize);
 
         $jsonOptions = JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT |
-            JSON_ERROR_INF_OR_NAN | JSON_PARTIAL_OUTPUT_ON_ERROR;
+            JSON_PARTIAL_OUTPUT_ON_ERROR;
 
         if (isset($this->viewVars['_jsonOptions'])) {
             if ($this->viewVars['_jsonOptions'] === false) {

+ 487 - 0
tests/TestCase/View/Helper/BreadcrumbsHelperTest.php

@@ -0,0 +1,487 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.3.6
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Test\TestCase\View\Helper;
+
+use Cake\TestSuite\TestCase;
+use Cake\View\Helper\BreadcrumbsHelper;
+use Cake\View\View;
+
+class BreadcrumbsHelperTest extends TestCase
+{
+
+    /**
+     * Instance of the BreadcrumbsHelper
+     *
+     * @var BreadcrumbsHelper
+     */
+    public $breadcrumbs;
+
+    /**
+     * setUp method
+     *
+     * @return void
+     */
+    public function setUp()
+    {
+        parent::setUp();
+        $view = new View();
+        $this->breadcrumbs = new BreadcrumbsHelper($view);
+    }
+
+    /**
+     * Test adding crumbs to the trail using add()
+     *
+     * @return void
+     */
+    public function testAdd()
+    {
+        $this->breadcrumbs
+            ->add('Home', '/', ['class' => 'first'])
+            ->add('Some text', ['controller' => 'Some', 'action' => 'text']);
+
+        $result = $this->breadcrumbs->getCrumbs();
+        $expected = [
+            [
+                'title' => 'Home',
+                'url' => '/',
+                'options' => [
+                    'class' => 'first'
+                ]
+            ],
+            [
+                'title' => 'Some text',
+                'url' => [
+                    'controller' => 'Some',
+                    'action' => 'text'
+                ],
+                'options' => []
+            ]
+        ];
+        $this->assertEquals($expected, $result);
+    }
+
+
+    /**
+     * Test adding multiple crumbs at once to the trail using add()
+     *
+     * @return void
+     */
+    public function testAddMultiple()
+    {
+        $this->breadcrumbs
+            ->add([
+                [
+                    'title' => 'Home',
+                    'url' => '/',
+                    'options' => ['class' => 'first']
+                ],
+                [
+                    'title' => 'Some text',
+                    'url' => ['controller' => 'Some', 'action' => 'text']
+                ],
+                [
+                    'title' => 'Final',
+                ],
+            ]);
+
+        $result = $this->breadcrumbs->getCrumbs();
+        $expected = [
+            [
+                'title' => 'Home',
+                'url' => '/',
+                'options' => [
+                    'class' => 'first'
+                ]
+            ],
+            [
+                'title' => 'Some text',
+                'url' => [
+                    'controller' => 'Some',
+                    'action' => 'text'
+                ],
+                'options' => []
+            ],
+            [
+                'title' => 'Final',
+                'url' => null,
+                'options' => []
+            ]
+        ];
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
+     * Test adding crumbs to the trail using prepend()
+     *
+     * @return void
+     */
+    public function testPrepend()
+    {
+        $this->breadcrumbs
+            ->add('Home', '/', ['class' => 'first'])
+            ->prepend('Some text', ['controller' => 'Some', 'action' => 'text'])
+            ->prepend('The root', '/root', ['data-name' => 'some-name']);
+
+        $result = $this->breadcrumbs->getCrumbs();
+        $expected = [
+            [
+                'title' => 'The root',
+                'url' => '/root',
+                'options' => ['data-name' => 'some-name']
+            ],
+            [
+                'title' => 'Some text',
+                'url' => [
+                    'controller' => 'Some',
+                    'action' => 'text'
+                ],
+                'options' => []
+            ],
+            [
+                'title' => 'Home',
+                'url' => '/',
+                'options' => [
+                    'class' => 'first'
+                ]
+            ]
+        ];
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
+     * Test adding crumbs to the trail using prepend()
+     *
+     * @return void
+     */
+    public function testPrependMultiple()
+    {
+        $this->breadcrumbs
+            ->add('Home', '/', ['class' => 'first'])
+            ->prepend([
+                ['title' => 'Some text', 'url' => ['controller' => 'Some', 'action' => 'text']],
+                ['title' => 'The root', 'url' => '/root', 'options' => ['data-name' => 'some-name']]
+            ]);
+
+        $result = $this->breadcrumbs->getCrumbs();
+        $expected = [
+            [
+                'title' => 'Some text',
+                'url' => [
+                    'controller' => 'Some',
+                    'action' => 'text'
+                ],
+                'options' => []
+            ],
+            [
+                'title' => 'The root',
+                'url' => '/root',
+                'options' => ['data-name' => 'some-name']
+            ],
+            [
+                'title' => 'Home',
+                'url' => '/',
+                'options' => [
+                    'class' => 'first'
+                ]
+            ]
+        ];
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
+     * Test adding crumbs to a specific index
+     *
+     * @return void
+     */
+    public function testInsertAt()
+    {
+        $this->breadcrumbs
+            ->add('Home', '/', ['class' => 'first'])
+            ->prepend('Some text', ['controller' => 'Some', 'action' => 'text'])
+            ->insertAt(1, 'Insert At', ['controller' => 'Insert', 'action' => 'at'])
+            ->insertAt(1, 'Insert At Again', ['controller' => 'Insert', 'action' => 'at_again']);
+
+        $result = $this->breadcrumbs->getCrumbs();
+        $expected = [
+            [
+                'title' => 'Some text',
+                'url' => [
+                    'controller' => 'Some',
+                    'action' => 'text'
+                ],
+                'options' => []
+            ],
+            [
+                'title' => 'Insert At Again',
+                'url' => [
+                    'controller' => 'Insert',
+                    'action' => 'at_again'
+                ],
+                'options' => []
+            ],
+            [
+                'title' => 'Insert At',
+                'url' => [
+                    'controller' => 'Insert',
+                    'action' => 'at'
+                ],
+                'options' => []
+            ],
+            [
+                'title' => 'Home',
+                'url' => '/',
+                'options' => [
+                    'class' => 'first'
+                ]
+            ]
+        ];
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
+     * Test adding crumbs to a specific index
+     *
+     * @expectedException \LogicException
+     */
+    public function testInsertAtIndexOutOfBounds()
+    {
+        $this->breadcrumbs
+            ->add('Home', '/', ['class' => 'first'])
+            ->insertAt(2, 'Insert At Again', ['controller' => 'Insert', 'action' => 'at_again']);
+    }
+
+    /**
+     * Test adding crumbs before a specific one
+     *
+     * @return void
+     */
+    public function testInsertBefore()
+    {
+        $this->breadcrumbs
+            ->add('Home', '/', ['class' => 'first'])
+            ->prepend('Some text', ['controller' => 'Some', 'action' => 'text'])
+            ->prepend('The root', '/root', ['data-name' => 'some-name'])
+            ->insertBefore('The root', 'The super root');
+
+        $result = $this->breadcrumbs->getCrumbs();
+        $expected = [
+            [
+                'title' => 'The super root',
+                'url' => null,
+                'options' => []
+            ],
+            [
+                'title' => 'The root',
+                'url' => '/root',
+                'options' => ['data-name' => 'some-name']
+            ],
+            [
+                'title' => 'Some text',
+                'url' => [
+                    'controller' => 'Some',
+                    'action' => 'text'
+                ],
+                'options' => []
+            ],
+            [
+                'title' => 'Home',
+                'url' => '/',
+                'options' => [
+                    'class' => 'first'
+                ]
+            ]
+        ];
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
+     * Test adding crumbs after a specific one
+     *
+     * @return void
+     */
+    public function testInsertAfter()
+    {
+        $this->breadcrumbs
+            ->add('Home', '/', ['class' => 'first'])
+            ->prepend('Some text', ['controller' => 'Some', 'action' => 'text'])
+            ->prepend('The root', '/root', ['data-name' => 'some-name'])
+            ->insertAfter('The root', 'The less super root');
+
+        $result = $this->breadcrumbs->getCrumbs();
+        $expected = [
+            [
+                'title' => 'The root',
+                'url' => '/root',
+                'options' => ['data-name' => 'some-name']
+            ],
+            [
+                'title' => 'The less super root',
+                'url' => null,
+                'options' => []
+            ],
+            [
+                'title' => 'Some text',
+                'url' => [
+                    'controller' => 'Some',
+                    'action' => 'text'
+                ],
+                'options' => []
+            ],
+            [
+                'title' => 'Home',
+                'url' => '/',
+                'options' => [
+                    'class' => 'first'
+                ]
+            ]
+        ];
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
+     * Tests the render method
+     *
+     * @return void
+     */
+    public function testRender()
+    {
+        $this->breadcrumbs
+            ->add('Home', '/', ['class' => 'first', 'innerAttrs' => ['data-foo' => 'bar']])
+            ->add('Some text', ['controller' => 'tests_apps', 'action' => 'some_method'])
+            ->add('Final crumb', null, ['class' => 'final', 'innerAttrs' => ['class' => 'final-link']]);
+
+        $result = $this->breadcrumbs->render(
+            ['data-stuff' => 'foo and bar'],
+            ['separator' => '<i class="fa fa-angle-right"></i>', 'class' => 'separator']
+        );
+        $expected = [
+            ['ul' => ['data-stuff' => 'foo and bar']],
+            ['li' => ['class' => 'first']],
+            ['a' => ['href' => '/', 'data-foo' => 'bar']],
+            'Home',
+            '/a',
+            '/li',
+            ['li' => ['class' => 'separator']],
+            ['span' => []],
+            ['i' => ['class' => 'fa fa-angle-right']],
+            '/i',
+            '/span',
+            '/li',
+            ['li' => []],
+            ['a' => ['href' => '/some_alias']],
+            'Some text',
+            '/a',
+            '/li',
+            ['li' => ['class' => 'separator']],
+            ['span' => []],
+            ['i' => ['class' => 'fa fa-angle-right']],
+            '/i',
+            '/span',
+            '/li',
+            ['li' => ['class' => 'final']],
+            ['span' => ['class' => 'final-link']],
+            'Final crumb',
+            '/span',
+            '/li',
+            '/ul'
+        ];
+        $this->assertHtml($expected, $result);
+    }
+
+    /**
+     * Tests the render method with custom templates
+     *
+     * @return void
+     */
+    public function testRenderCustomTemplate()
+    {
+        $this->breadcrumbs = new BreadcrumbsHelper(new View(), [
+            'templates' => [
+                'wrapper' => '<ol itemtype="http://schema.org/BreadcrumbList"{{attrs}}>{{content}}</ol>',
+                'item' => '<li itemprop="itemListElement" itemtype="http://schema.org/ListItem"{{attrs}}><a itemtype="http://schema.org/Thing" itemprop="item" href="{{url}}"{{innerAttrs}}><span itemprop="name">{{title}}</span></a></li>',
+                'itemWithoutLink' => '<li itemprop="itemListElement" itemtype="http://schema.org/ListItem"{{attrs}}><span itemprop="name"{{innerAttrs}}>{{title}}</span></li>',
+            ]
+        ]);
+        $this->breadcrumbs
+            ->add('Home', '/', ['class' => 'first', 'innerAttrs' => ['data-foo' => 'bar']])
+            ->add('Final crumb', null, ['class' => 'final', 'innerAttrs' => ['class' => 'final-link']]);
+
+        $result = $this->breadcrumbs->render(
+            ['data-stuff' => 'foo and bar'],
+            ['separator' => ' > ', 'class' => 'separator']
+        );
+        $expected = [
+            ['ol' => ['itemtype' => 'http://schema.org/BreadcrumbList', 'data-stuff' => 'foo and bar']],
+            ['li' => ['itemprop' => 'itemListElement', 'itemtype' => 'http://schema.org/ListItem', 'class' => 'first']],
+            ['a' => ['itemtype' => 'http://schema.org/Thing', 'itemprop' => 'item', 'href' => '/', 'data-foo' => 'bar']],
+            ['span' => ['itemprop' => 'name']],
+            'Home',
+            '/span',
+            '/a',
+            '/li',
+            ['li' => ['itemprop' => 'itemListElement', 'itemtype' => 'http://schema.org/ListItem', 'class' => 'final']],
+            ['span' => ['itemprop' => 'name', 'class' => 'final-link']],
+            'Final crumb',
+            '/span',
+            '/li',
+            '/ol'
+        ];
+        $this->assertHtml($expected, $result, true);
+    }
+
+    /**
+     * Tests the render method with template vars
+     *
+     * @return void
+     */
+    public function testRenderCustomTemplateTemplateVars()
+    {
+        $this->breadcrumbs = new BreadcrumbsHelper(new View(), [
+            'templates' => [
+                'wrapper' => '{{thing}}<ol itemtype="http://schema.org/BreadcrumbList"{{attrs}}>{{content}}</ol>',
+                'item' => '<li itemprop="itemListElement" itemtype="http://schema.org/ListItem"{{attrs}}><a itemtype="http://schema.org/Thing" itemprop="item" href="{{url}}"{{innerAttrs}}><span itemprop="name">{{title}}</span></a>{{foo}}</li>',
+                'itemWithoutLink' => '<li itemprop="itemListElement" itemtype="http://schema.org/ListItem"{{attrs}}><span itemprop="name"{{innerAttrs}}>{{title}}</span>{{barbaz}}</li>',
+            ]
+        ]);
+        $this->breadcrumbs
+            ->add('Home', '/', ['class' => 'first', 'innerAttrs' => ['data-foo' => 'bar'], 'templateVars' => ['foo' => 'barbaz']])
+            ->add('Final crumb', null, ['class' => 'final', 'innerAttrs' => ['class' => 'final-link'], 'templateVars' => ['barbaz' => 'foo']]);
+
+        $result = $this->breadcrumbs->render(
+            ['data-stuff' => 'foo and bar', 'templateVars' => ['thing' => 'somestuff']],
+            ['separator' => ' > ', 'class' => 'separator']
+        );
+        $expected = [
+            'somestuff',
+            ['ol' => ['itemtype' => 'http://schema.org/BreadcrumbList', 'data-stuff' => 'foo and bar']],
+            ['li' => ['itemprop' => 'itemListElement', 'itemtype' => 'http://schema.org/ListItem', 'class' => 'first']],
+            ['a' => ['itemtype' => 'http://schema.org/Thing', 'itemprop' => 'item', 'href' => '/', 'data-foo' => 'bar']],
+            ['span' => ['itemprop' => 'name']],
+            'Home',
+            '/span',
+            '/a',
+            'barbaz',
+            '/li',
+            ['li' => ['itemprop' => 'itemListElement', 'itemtype' => 'http://schema.org/ListItem', 'class' => 'final']],
+            ['span' => ['itemprop' => 'name', 'class' => 'final-link']],
+            'Final crumb',
+            '/span',
+            'foo',
+            '/li',
+            '/ol'
+        ];
+        $this->assertHtml($expected, $result, true);
+    }
+}