├── .github └── workflows │ ├── dynamic-readme.yml │ └── dynamic-security.yml ├── .gitignore ├── .ruby-version ├── .travis.yml ├── CONTRIBUTING.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── SECURITY.md ├── app └── views │ └── breadcrumbs │ └── _breadcrumbs.html.erb ├── croutons.gemspec ├── lib ├── croutons.rb └── croutons │ ├── breadcrumb.rb │ ├── breadcrumb_trail.rb │ ├── controller.rb │ ├── engine.rb │ └── version.rb └── spec ├── features └── breadcrumbs_spec.rb ├── lib └── croutons │ ├── breadcrumb_spec.rb │ └── breadcrumb_trail_spec.rb ├── spec_helper.rb └── support └── rails_app.rb /.github/workflows/dynamic-readme.yml: -------------------------------------------------------------------------------- 1 | name: update-templates 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | jobs: 10 | update-templates: 11 | permissions: 12 | contents: write 13 | pull-requests: write 14 | pages: write 15 | uses: thoughtbot/templates/.github/workflows/dynamic-readme.yaml@main 16 | secrets: 17 | token: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/dynamic-security.yml: -------------------------------------------------------------------------------- 1 | name: update-security 2 | 3 | on: 4 | push: 5 | paths: 6 | - SECURITY.md 7 | branches: 8 | - main 9 | workflow_dispatch: 10 | 11 | jobs: 12 | update-security: 13 | permissions: 14 | contents: write 15 | pull-requests: write 16 | pages: write 17 | uses: thoughtbot/templates/.github/workflows/dynamic-security.yaml@main 18 | secrets: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | pkg/ 3 | spec/dummy_app 4 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.5.3 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | cache: bundler 3 | rvm: 4 | - 2.3.8 5 | - 2.4.5 6 | - 2.5.3 7 | script: bundle exec rspec 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | We love pull requests from everyone. By participating in this project, you agree 2 | to abide by the thoughtbot [code of conduct]. 3 | 4 | [code of conduct]: https://thoughtbot.com/open-source-code-of-conduct 5 | 6 | ## Contributing 7 | 8 | 1. Fork the repo. 9 | 10 | 2. Run the tests. We only take pull requests with passing tests, and it's great 11 | to know that you have a clean slate: `bundle exec rake` 12 | 13 | 3. Add a test for your change. Only refactoring and documentation changes 14 | require no new tests. If you are adding functionality or fixing a bug, we 15 | need a test! 16 | 17 | 4. Make the test pass. 18 | 19 | 5. Push to your fork and submit a pull request. 20 | 21 | At this point you're waiting on us. We like to at least comment on, if not 22 | accept, pull requests within three business days (and, typically, one business 23 | day). We may suggest some changes or improvements or alternatives. 24 | 25 | Some things that will increase the chance that your pull request is accepted, 26 | taken straight from the Ruby on Rails guide: 27 | 28 | * Use Rails idioms and helpers 29 | * Include tests that fail without your code, and pass with it 30 | * Update the documentation, the surrounding one, examples elsewhere, guides, 31 | whatever is affected by your contribution 32 | 33 | Syntax: 34 | 35 | * Two spaces, no tabs. 36 | * No trailing whitespace. Blank lines should not have any space. 37 | * Prefer &&/|| over and/or. 38 | * MyClass.my_method(my_arg) not my_method( my_arg ) or my_method my_arg. 39 | * a = b and not a=b. 40 | * Follow the conventions you see used in the source already. 41 | 42 | And in case we didn't emphasize it enough: we love tests! 43 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Declare your gem's dependencies in croutons.gemspec. 4 | # Bundler will treat runtime dependencies like base dependencies, and 5 | # development dependencies will be added by default to the :development group. 6 | gemspec 7 | 8 | # Declare any dependencies that are still in development here instead of in 9 | # your gemspec. These might include edge Rails or gems from your path or 10 | # Git. Remember to move these dependencies to your gemspec before releasing 11 | # your gem to rubygems.org. 12 | 13 | # To use debugger 14 | # gem 'debugger' 15 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | croutons (0.0.2) 5 | rails (>= 4.2.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | actioncable (5.2.2) 11 | actionpack (= 5.2.2) 12 | nio4r (~> 2.0) 13 | websocket-driver (>= 0.6.1) 14 | actionmailer (5.2.2) 15 | actionpack (= 5.2.2) 16 | actionview (= 5.2.2) 17 | activejob (= 5.2.2) 18 | mail (~> 2.5, >= 2.5.4) 19 | rails-dom-testing (~> 2.0) 20 | actionpack (5.2.2) 21 | actionview (= 5.2.2) 22 | activesupport (= 5.2.2) 23 | rack (~> 2.0) 24 | rack-test (>= 0.6.3) 25 | rails-dom-testing (~> 2.0) 26 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 27 | actionview (5.2.2) 28 | activesupport (= 5.2.2) 29 | builder (~> 3.1) 30 | erubi (~> 1.4) 31 | rails-dom-testing (~> 2.0) 32 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 33 | activejob (5.2.2) 34 | activesupport (= 5.2.2) 35 | globalid (>= 0.3.6) 36 | activemodel (5.2.2) 37 | activesupport (= 5.2.2) 38 | activerecord (5.2.2) 39 | activemodel (= 5.2.2) 40 | activesupport (= 5.2.2) 41 | arel (>= 9.0) 42 | activestorage (5.2.2) 43 | actionpack (= 5.2.2) 44 | activerecord (= 5.2.2) 45 | marcel (~> 0.3.1) 46 | activesupport (5.2.2) 47 | concurrent-ruby (~> 1.0, >= 1.0.2) 48 | i18n (>= 0.7, < 2) 49 | minitest (~> 5.1) 50 | tzinfo (~> 1.1) 51 | addressable (2.5.2) 52 | public_suffix (>= 2.0.2, < 4.0) 53 | arel (9.0.0) 54 | builder (3.2.3) 55 | capybara (3.12.0) 56 | addressable 57 | mini_mime (>= 0.1.3) 58 | nokogiri (~> 1.8) 59 | rack (>= 1.6.0) 60 | rack-test (>= 0.6.3) 61 | regexp_parser (~> 1.2) 62 | xpath (~> 3.2) 63 | concurrent-ruby (1.1.4) 64 | crass (1.0.5) 65 | diff-lcs (1.3) 66 | erubi (1.8.0) 67 | globalid (0.4.1) 68 | activesupport (>= 4.2.0) 69 | i18n (1.4.0) 70 | concurrent-ruby (~> 1.0) 71 | loofah (2.4.0) 72 | crass (~> 1.0.2) 73 | nokogiri (>= 1.5.9) 74 | mail (2.7.1) 75 | mini_mime (>= 0.1.1) 76 | marcel (0.3.3) 77 | mimemagic (~> 0.3.2) 78 | method_source (0.9.2) 79 | mimemagic (0.3.3) 80 | mini_mime (1.0.1) 81 | mini_portile2 (2.4.0) 82 | minitest (5.11.3) 83 | nio4r (2.3.1) 84 | nokogiri (1.10.8) 85 | mini_portile2 (~> 2.4.0) 86 | public_suffix (3.0.3) 87 | rack (2.0.8) 88 | rack-test (1.1.0) 89 | rack (>= 1.0, < 3) 90 | rails (5.2.2) 91 | actioncable (= 5.2.2) 92 | actionmailer (= 5.2.2) 93 | actionpack (= 5.2.2) 94 | actionview (= 5.2.2) 95 | activejob (= 5.2.2) 96 | activemodel (= 5.2.2) 97 | activerecord (= 5.2.2) 98 | activestorage (= 5.2.2) 99 | activesupport (= 5.2.2) 100 | bundler (>= 1.3.0) 101 | railties (= 5.2.2) 102 | sprockets-rails (>= 2.0.0) 103 | rails-dom-testing (2.0.3) 104 | activesupport (>= 4.2.0) 105 | nokogiri (>= 1.6) 106 | rails-html-sanitizer (1.0.4) 107 | loofah (~> 2.2, >= 2.2.2) 108 | railties (5.2.2) 109 | actionpack (= 5.2.2) 110 | activesupport (= 5.2.2) 111 | method_source 112 | rake (>= 0.8.7) 113 | thor (>= 0.19.0, < 2.0) 114 | rake (12.3.2) 115 | regexp_parser (1.3.0) 116 | rspec-core (3.8.0) 117 | rspec-support (~> 3.8.0) 118 | rspec-expectations (3.8.2) 119 | diff-lcs (>= 1.2.0, < 2.0) 120 | rspec-support (~> 3.8.0) 121 | rspec-mocks (3.8.0) 122 | diff-lcs (>= 1.2.0, < 2.0) 123 | rspec-support (~> 3.8.0) 124 | rspec-rails (3.8.1) 125 | actionpack (>= 3.0) 126 | activesupport (>= 3.0) 127 | railties (>= 3.0) 128 | rspec-core (~> 3.8.0) 129 | rspec-expectations (~> 3.8.0) 130 | rspec-mocks (~> 3.8.0) 131 | rspec-support (~> 3.8.0) 132 | rspec-support (3.8.0) 133 | sprockets (3.7.2) 134 | concurrent-ruby (~> 1.0) 135 | rack (> 1, < 3) 136 | sprockets-rails (3.2.1) 137 | actionpack (>= 4.0) 138 | activesupport (>= 4.0) 139 | sprockets (>= 3.0.0) 140 | sqlite3 (1.3.13) 141 | thor (0.20.3) 142 | thread_safe (0.3.6) 143 | tzinfo (1.2.5) 144 | thread_safe (~> 0.1) 145 | websocket-driver (0.7.0) 146 | websocket-extensions (>= 0.1.0) 147 | websocket-extensions (0.1.3) 148 | xpath (3.2.0) 149 | nokogiri (~> 1.8) 150 | 151 | PLATFORMS 152 | ruby 153 | 154 | DEPENDENCIES 155 | capybara 156 | croutons! 157 | rspec-rails 158 | sqlite3 159 | 160 | BUNDLED WITH 161 | 1.17.2 162 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014-2017 Calle Erlandsson, George Brocklehurst, and thoughtbot inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Deprecated as of September 20, 2024 2 | 3 | This project is no longer maintained. If you wish to continue to develop this code yourself, we recommend you fork it. 4 | 5 | Croutons 6 | ======== 7 | 8 | Easy breadcrumbs for Rails apps. 9 | 10 | Usage 11 | ----- 12 | 13 | ### Required steps 14 | 15 | 1. Include `Croutons::Controller` in your `ApplicationController`. 16 | 17 | This will make a `#breadcrumbs` helper available in your layouts and views. 18 | 2. Call the `#breadcrumbs` helper in your layouts or views. 19 | 3. Define a `BreadcrumbTrail` class, which inherits from 20 | `Croutons::BreadcrumbTrail`. 21 | 4. Define missing methods on the `BreadcrumbTrail` class. 22 | 23 | For example, for the `admin/locations/index.html.erb` view you would define 24 | an `#admin_locations_index` method. 25 | 26 | In these methods, you build up a breadcrumb trail by calling `#breadcrumb` 27 | with a label and an optional URL. You can also call previously defined 28 | methods to build on existing trails. View assigns (i.e. the controller 29 | instance variables) are available via the `#objects` method which returns a 30 | `Hash`. Rails route helpers are also available inside this class. 31 | 32 | Please see [the example below](#example) for further reference. 33 | 34 | ### Optional steps 35 | 36 | * Instead of defining a `BreadcrumbTrail` class you can use an object of your 37 | own that responds to `#breadcrumbs`. 38 | 39 | To do this, override the private `#breadcrumb_trail` method in the controller 40 | where you included `Croutons::Controller`, to return the object you want to 41 | use. 42 | 43 | The `#breadcrumbs` method is passed two parameters: one `template_identifier` 44 | `String` and one `objects` `Hash`. The `#breadcrumbs` method should return an 45 | `Array` of `Croutons::Breadcrumb`s. 46 | 47 | * Override the view used to render breadcrumbs. 48 | 49 | To do this, create a view called `breadcrumbs/_breadcrumbs.html.erb`. 50 | 51 | In this view, an `Array` of `Croutons::Breadcrumb`s is assigned to the local 52 | variable `breadcrumbs`. These `Croutons::Breadcrumb`s have two public 53 | attributes: `#label` and `#url`. The `#url` attribute is optional. To check 54 | whether the `Croutons::Breadcrumb` has a `#url` or not (i.e. should be 55 | rendered as a link or not), check whether the `#link?` method returns `true` 56 | or `false`. 57 | 58 | ### Example 59 | 60 | #### `app/controllers/application_controller.rb` 61 | 62 | class ApplicationController < ActionController::Base 63 | include Croutons::Controller 64 | end 65 | 66 | #### `app/controllers/posts_controller.rb` 67 | 68 | class PostsController < ApplicationController 69 | def index 70 | @posts = Post.all 71 | end 72 | 73 | def show 74 | @post = Post.find(params[:id]) 75 | end 76 | end 77 | 78 | #### `app/views/layouts/application.html.erb` 79 | 80 | 81 | 82 | 83 | My blog 84 | 85 | 86 | <%= breadcrumbs %> 87 | <%= yield %> 88 | 89 | 90 | 91 | #### `app/models/breadcrumb_trail.rb` 92 | 93 | class BreadcrumbTrail < Croutons::BreadcrumbTrail 94 | def posts_index 95 | breadcrumb("Posts", posts_path) 96 | end 97 | 98 | def posts_show 99 | posts_index 100 | breadcrumb(objects[:post].title, post_path(objects[:post])) 101 | end 102 | end 103 | 104 | License 105 | ------- 106 | 107 | Croutons is Copyright © 2014 Calle Erlandsson, George Brocklehurst, and 108 | thoughtbot. It is free software, and may be redistributed under the terms 109 | specified in the LICENSE file. 110 | 111 | 112 | ## About thoughtbot 113 | 114 | ![thoughtbot](https://thoughtbot.com/thoughtbot-logo-for-readmes.svg) 115 | 116 | This repo is maintained and funded by thoughtbot, inc. 117 | The names and logos for thoughtbot are trademarks of thoughtbot, inc. 118 | 119 | We love open source software! 120 | See [our other projects][community]. 121 | We are [available for hire][hire]. 122 | 123 | [community]: https://thoughtbot.com/community?utm_source=github 124 | [hire]: https://thoughtbot.com/hire-us?utm_source=github 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'bundler/setup' 3 | rescue LoadError 4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 5 | end 6 | 7 | Bundler::GemHelper.install_tasks 8 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/views/breadcrumbs/_breadcrumbs.html.erb: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /croutons.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | 3 | require "croutons/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "croutons" 7 | s.version = Croutons::VERSION 8 | s.authors = ["Calle Erlandsson", "George Brocklehurst"] 9 | s.email = ["calle@thoughtbot.com", "george@thoughtbot.com", "hello@thoughtbot.com"] 10 | s.homepage = "https://github.com/thoughtbot/croutons" 11 | s.summary = "Easy breadcrumbs for Rails apps." 12 | s.license = "MIT" 13 | s.files = `git ls-files -z`.split("\x0") 14 | s.test_files = s.files.grep(%r{^(test|spec|features)/}) 15 | s.require_paths = ["lib"] 16 | s.add_dependency "rails", ">= 4.2.0" 17 | s.add_development_dependency "rspec-rails" 18 | s.add_development_dependency "capybara" 19 | s.add_development_dependency "sqlite3" 20 | end 21 | -------------------------------------------------------------------------------- /lib/croutons.rb: -------------------------------------------------------------------------------- 1 | require 'croutons/engine' 2 | require 'croutons/controller' 3 | require 'croutons/breadcrumb_trail' 4 | 5 | module Croutons 6 | end 7 | -------------------------------------------------------------------------------- /lib/croutons/breadcrumb.rb: -------------------------------------------------------------------------------- 1 | module Croutons 2 | class Breadcrumb 3 | attr_reader :label, :url 4 | 5 | def initialize(label, url = nil) 6 | self.label = label 7 | self.url = url 8 | end 9 | 10 | def labelize 11 | self.url = nil 12 | end 13 | 14 | def link? 15 | url.present? 16 | end 17 | 18 | def ==(other) 19 | label == other.label && url == other.url 20 | end 21 | 22 | private 23 | 24 | attr_writer :label, :url 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/croutons/breadcrumb_trail.rb: -------------------------------------------------------------------------------- 1 | require 'croutons/breadcrumb' 2 | 3 | module Croutons 4 | class BreadcrumbTrail 5 | def self.breadcrumbs(*args) 6 | new(*args).breadcrumbs 7 | end 8 | 9 | attr_reader :breadcrumbs 10 | 11 | def initialize(template_identifer, objects = {}) 12 | @template_identifer = template_identifer 13 | @objects = objects.with_indifferent_access 14 | @breadcrumbs = [] 15 | build_breadcrumbs 16 | end 17 | 18 | def method_missing(name, *args) 19 | if respond_to_missing?(name) 20 | Rails.application.routes.url_helpers.public_send(name, *args) 21 | else 22 | super 23 | end 24 | end 25 | 26 | def respond_to_missing?(name) 27 | Rails.application.routes.url_helpers.respond_to?(name) 28 | end 29 | 30 | private 31 | 32 | attr_reader :template_identifer, :objects 33 | 34 | def build_breadcrumbs 35 | send(template_identifer) 36 | labelize_last 37 | end 38 | 39 | def labelize_last 40 | breadcrumbs.last.try(:labelize) 41 | end 42 | 43 | def breadcrumb(*args) 44 | breadcrumbs << Breadcrumb.new(*args) 45 | end 46 | 47 | def t(*args) 48 | I18n.t(*args) 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/croutons/controller.rb: -------------------------------------------------------------------------------- 1 | module Croutons 2 | module Controller 3 | def self.included(controller) 4 | controller.helper_method(:breadcrumbs) 5 | end 6 | 7 | def render_to_body(options) 8 | @_template = options[:template] 9 | @_prefixes = options[:prefixes] 10 | super 11 | end 12 | 13 | private 14 | 15 | def breadcrumbs(objects = {}) 16 | template = lookup_context.find_template(@_template, @_prefixes) 17 | template_identifier = template.virtual_path.gsub('/', '_') 18 | objects.reverse_merge!(view_assigns) 19 | breadcrumbs = breadcrumb_trail.breadcrumbs(template_identifier, objects) 20 | render_to_string( 21 | partial: 'breadcrumbs/breadcrumbs', 22 | locals: { breadcrumbs: breadcrumbs }, 23 | ) 24 | end 25 | 26 | def breadcrumb_trail 27 | ::BreadcrumbTrail 28 | rescue NameError 29 | raise NotImplementedError, 30 | 'Define a `BreadcrumbTrail` class that inherits from '\ 31 | '`Breadcrumbs::BreadcrumbTrail`, or override the '\ 32 | '`breadcrumb_trail` method in your controller so that it '\ 33 | 'returns an object that responds to `#breadcrumbs`.' 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/croutons/engine.rb: -------------------------------------------------------------------------------- 1 | require 'rails' 2 | 3 | module Croutons 4 | class Engine < Rails::Engine 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/croutons/version.rb: -------------------------------------------------------------------------------- 1 | module Croutons 2 | VERSION = "0.0.2" 3 | end 4 | -------------------------------------------------------------------------------- /spec/features/breadcrumbs_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "Breadcrumbs" do 4 | before(:all) do |test| 5 | setup_rails_app do |rails_app| 6 | rails_app.scaffold_model "Post", "title:string" 7 | rails_app.add_croutons_mixin_to_application_controller 8 | rails_app.add_to_view('posts/index', '<%= breadcrumbs %>') 9 | rails_app.add_to_view('posts/show', '<%= breadcrumbs role: params[:role] %>') 10 | rails_app.add_to_view('posts/new', '<%= breadcrumbs %>') 11 | rails_app.add_breadcrumb_trail_class <<-RUBY 12 | require "croutons/breadcrumb_trail" 13 | 14 | class BreadcrumbTrail < Croutons::BreadcrumbTrail 15 | private 16 | 17 | def posts_index 18 | breadcrumb("Posts", posts_path) 19 | end 20 | 21 | def posts_show 22 | if objects[:role] == "admin" 23 | posts_index 24 | end 25 | breadcrumb(objects[:post].title) 26 | end 27 | end 28 | RUBY 29 | end 30 | 31 | test.class.send :include, Capybara::DSL 32 | test.class.send :include, Rails.application.routes.url_helpers 33 | end 34 | 35 | it "are rendered on page" do 36 | post = Post.create!(title: "My first post") 37 | 38 | visit posts_path 39 | 40 | with_breadcrumbs do |items| 41 | expect(items.length).to eq 1 42 | expect(items.first).to have_content("Posts") 43 | expect(items.first).not_to have_css("a") 44 | end 45 | 46 | visit post_path(post, role: "admin") 47 | 48 | with_breadcrumbs do |items| 49 | expect(items.length).to eq 2 50 | expect(items.first).to have_link("Posts", href: posts_path) 51 | expect(items.last).to have_content(post.title) 52 | expect(items.last).not_to have_css("a") 53 | end 54 | 55 | visit post_path(post, role: "guest") 56 | 57 | with_breadcrumbs do |items| 58 | expect(items.length).to eq 1 59 | expect(items.first).to have_content(post.title) 60 | expect(items.first).not_to have_css("a") 61 | end 62 | end 63 | 64 | context "when not defined" do 65 | it "raises a helpful exception" do 66 | expect { visit new_post_path }.to raise_exception(/posts_new/) 67 | end 68 | end 69 | 70 | def with_breadcrumbs 71 | within(".breadcrumbs") do 72 | yield all("li") 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /spec/lib/croutons/breadcrumb_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "croutons/breadcrumb" 3 | 4 | describe Croutons::Breadcrumb do 5 | describe "#labelize" do 6 | it "sets url to nil" do 7 | breadcrumb = described_class.new("Some text", "Some url") 8 | 9 | breadcrumb.labelize 10 | 11 | expect(breadcrumb.url).to be_nil 12 | end 13 | end 14 | 15 | describe "#link?" do 16 | it "returns true when the url is present" do 17 | expect(described_class.new("Some text", "Some url")).to be_link 18 | end 19 | 20 | it "returns false when the url is blank" do 21 | expect(described_class.new("Some text", nil)).not_to be_link 22 | end 23 | end 24 | 25 | describe "#==" do 26 | it "returns true when the label and URL match" do 27 | one = described_class.new("label", "url") 28 | two = described_class.new("label", "url") 29 | 30 | expect(one).to eq(two) 31 | end 32 | 33 | it "returns false when the labels differ" do 34 | one = described_class.new("label", "url") 35 | two = described_class.new("other", "url") 36 | 37 | expect(one).not_to eq(two) 38 | end 39 | 40 | it "returns false when the URLs differ" do 41 | one = described_class.new("label", "url") 42 | two = described_class.new("label", "other") 43 | 44 | expect(one).not_to eq(two) 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/lib/croutons/breadcrumb_trail_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "croutons/breadcrumb_trail" 3 | require "croutons/breadcrumb" 4 | require "rails" 5 | 6 | describe Croutons::BreadcrumbTrail do 7 | describe ".breadcrumbs" do 8 | let(:trail) do 9 | Class.new(described_class) do 10 | def one 11 | breadcrumb("One", "/one") 12 | end 13 | 14 | def two 15 | one 16 | breadcrumb("Two", "/two") 17 | end 18 | 19 | def thing 20 | breadcrumb(objects[:thing]) 21 | end 22 | 23 | def translated 24 | breadcrumb(t(objects[:key])) 25 | end 26 | 27 | def routes 28 | breadcrumb("Some path", some_path) 29 | breadcrumb("You are here") 30 | end 31 | end 32 | end 33 | 34 | context "for a known template identifier" do 35 | it "returns breadcrumbs" do 36 | expect(trail.breadcrumbs(:one)).to eq [ 37 | Croutons::Breadcrumb.new("One"), 38 | ] 39 | end 40 | 41 | it "removes the URL from the final crumb" do 42 | expect(trail.breadcrumbs(:two)).to eq [ 43 | Croutons::Breadcrumb.new("One", "/one"), 44 | Croutons::Breadcrumb.new("Two"), 45 | ] 46 | end 47 | 48 | it "makes the given objects available through #objects" do 49 | expect(trail.breadcrumbs(:thing, thing: "Foo")). 50 | to eq [Croutons::Breadcrumb.new("Foo")] 51 | expect(trail.breadcrumbs(:thing, "thing" => "Bar")). 52 | to eq [Croutons::Breadcrumb.new("Bar")] 53 | end 54 | 55 | it "makes I18n available" do 56 | allow(I18n).to receive(:t). 57 | with("some.translation"). 58 | and_return("translated string") 59 | expect(trail.breadcrumbs(:translated, key: "some.translation")). 60 | to eq [Croutons::Breadcrumb.new("translated string")] 61 | end 62 | 63 | it "makes Rails route helpers available" do 64 | route_helpers = double("route_helpers", some_path: "/some/path") 65 | allow(Rails). 66 | to receive_message_chain(:application, :routes, :url_helpers). 67 | and_return(route_helpers) 68 | 69 | expect(trail.breadcrumbs(:routes)).to eq [ 70 | Croutons::Breadcrumb.new("Some path", "/some/path"), 71 | Croutons::Breadcrumb.new("You are here"), 72 | ] 73 | end 74 | end 75 | 76 | context "for an unknown template identifier" do 77 | it "raises a helpful exception" do 78 | expect { trail.breadcrumbs(:not_real) }. 79 | to raise_exception(NoMethodError) 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "active_support/all" 2 | 3 | ENV["RAILS_ENV"] = "test" 4 | 5 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 6 | 7 | RSpec.configure do |config| 8 | config.expect_with :rspec do |c| 9 | c.syntax = :expect 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/support/rails_app.rb: -------------------------------------------------------------------------------- 1 | class RailsApp 2 | PROJECT_ROOT = File.expand_path("../../..", __FILE__).freeze 3 | 4 | module Helpers 5 | def setup_rails_app(&block) 6 | RailsApp.new.setup(&block) 7 | end 8 | end 9 | 10 | def setup 11 | create_rails_app 12 | disable_class_caching 13 | customize_gemfile 14 | bundle 15 | yield self 16 | setup_database 17 | load_environment 18 | end 19 | 20 | def scaffold_model(name, *columns) 21 | in_app_directory do 22 | run "rails generate scaffold #{name} #{columns.join(' ')} --force" 23 | end 24 | end 25 | 26 | def add_croutons_mixin_to_application_controller 27 | transform_file(path("app/controllers/application_controller.rb")) do |content| 28 | content.sub( 29 | /^(class.*)$/, 30 | "require 'croutons/controller'\n\n\\1\n include Croutons::Controller\n" 31 | ) 32 | end 33 | end 34 | 35 | def add_to_view(name, content_to_add) 36 | transform_file(path("app/views/#{name}.html.erb")) do |content| 37 | content << content_to_add 38 | end 39 | end 40 | 41 | def add_breadcrumb_trail_class(source) 42 | File.write(path("app/models/breadcrumb_trail.rb"), source) 43 | end 44 | 45 | private 46 | 47 | def create_rails_app 48 | run "bundle exec rails new #{path} --skip-gemfile --skip-bundle "\ 49 | "--skip-git --skip-keeps --skip-spring --skip-javascript "\ 50 | "--skip-test-unit --no-rc --skip-sprockets --skip-bootsnap --force" 51 | end 52 | 53 | def disable_class_caching 54 | transform_file(path("config/environments/test.rb")) do |content| 55 | content.gsub(/^\s*config\.cache_classes.*$/, "config.cache_classes = false") 56 | end 57 | end 58 | 59 | def customize_gemfile 60 | File.open(path("Gemfile"), "w") do |f| 61 | f << "source 'https://rubygems.org'\n" 62 | f << "gem 'croutons', path: '#{PROJECT_ROOT}'\n" 63 | f << "gem 'rspec-rails', group: :test\n" 64 | f << "gem 'capybara', group: :test\n" 65 | end 66 | end 67 | 68 | def bundle 69 | in_app_directory do 70 | run "bundle" 71 | end 72 | end 73 | 74 | def setup_database 75 | in_app_directory do 76 | run "rake db:drop:all db:create:all db:migrate" 77 | end 78 | end 79 | 80 | def load_environment 81 | require path("config/environment.rb") 82 | require "rspec/rails" 83 | end 84 | 85 | def in_app_directory(&block) 86 | Dir.chdir(path, &block) 87 | end 88 | 89 | def run(command) 90 | `#{command}` 91 | end 92 | 93 | def transform_file(filename) 94 | content = File.read(filename) 95 | File.open(filename, "w") do |f| 96 | content = yield(content) 97 | f.write(content) 98 | end 99 | end 100 | 101 | def path(filename = "") 102 | File.join(PROJECT_ROOT, "spec", "dummy_app", filename) 103 | end 104 | end 105 | 106 | RSpec.configure do |config| 107 | config.include RailsApp::Helpers 108 | end 109 | --------------------------------------------------------------------------------