├── .ruby-version ├── .ruby-gemset ├── .yardopts ├── .rspec ├── lib ├── yard-cucumber │ └── version.rb ├── templates │ └── default │ │ ├── feature │ │ └── html │ │ │ ├── no_steps_defined.erb │ │ │ ├── pystring.erb │ │ │ ├── table.erb │ │ │ ├── feature.erb │ │ │ ├── steps.erb │ │ │ ├── setup.rb │ │ │ ├── scenario.erb │ │ │ └── outline.erb │ │ ├── steptransformers │ │ └── html │ │ │ ├── header.erb │ │ │ ├── index.erb │ │ │ ├── undefinedsteps.erb │ │ │ ├── setup.rb │ │ │ └── transformers.erb │ │ ├── fulldoc │ │ └── html │ │ │ ├── full_list_featuredirectories.erb │ │ │ ├── full_list_tags.erb │ │ │ ├── full_list_steps.erb │ │ │ ├── full_list_stepdefinitions.erb │ │ │ ├── directories.erb │ │ │ ├── full_list_features.erb │ │ │ ├── setup.rb │ │ │ ├── js │ │ │ └── cucumber.js │ │ │ └── css │ │ │ └── cucumber.css │ │ ├── tag │ │ └── html │ │ │ ├── setup.rb │ │ │ ├── tag.erb │ │ │ └── alpha_table.erb │ │ ├── featuretags │ │ └── html │ │ │ ├── setup.rb │ │ │ └── namespace.erb │ │ ├── featuredirectory │ │ └── html │ │ │ ├── directory.erb │ │ │ ├── alpha_table.erb │ │ │ └── setup.rb │ │ ├── requirements │ │ └── html │ │ │ ├── alpha_table.erb │ │ │ ├── setup.rb │ │ │ └── requirements.erb │ │ └── layout │ │ └── html │ │ └── setup.rb ├── yard │ ├── code_objects │ │ ├── step_transform.rb │ │ ├── step_definition.rb │ │ ├── cucumber │ │ │ ├── feature.rb │ │ │ ├── base.rb │ │ │ ├── scenario.rb │ │ │ ├── tag.rb │ │ │ ├── step.rb │ │ │ ├── namespace_object.rb │ │ │ └── scenario_outline.rb │ │ └── step_transformer.rb │ ├── handlers │ │ ├── cucumber │ │ │ ├── base.rb │ │ │ └── feature_handler.rb │ │ ├── legacy │ │ │ ├── step_transform_handler.rb │ │ │ └── step_definition_handler.rb │ │ ├── step_transform_handler.rb │ │ ├── step_definition_handler.rb │ │ └── constant_transform_handler.rb │ ├── server │ │ ├── commands │ │ │ └── list_command.rb │ │ ├── router.rb │ │ └── adapter.rb │ ├── templates │ │ └── helpers │ │ │ └── base_helper.rb │ └── parser │ │ └── cucumber │ │ └── feature.rb ├── docserver │ ├── doc_server │ │ └── full_list │ │ │ └── html │ │ │ ├── setup.rb │ │ │ └── full_list.erb │ └── default │ │ ├── layout │ │ └── html │ │ │ └── headers.erb │ │ └── fulldoc │ │ └── html │ │ └── js │ │ └── cucumber.js ├── yard-cucumber.rb └── cucumber │ └── city_builder.rb ├── .gitignore ├── Gemfile ├── example ├── empty.feature ├── step_definitions │ ├── support │ │ ├── env.rb │ │ ├── support.rb │ │ └── env_support.rb │ ├── struct.rb │ ├── first.step.rb │ ├── french_steps.rb │ └── example.step.rb ├── child_feature │ ├── child.feature │ ├── grandchild_feature │ │ └── grandchild.feature │ └── README.md ├── tags.feature ├── README.md ├── scenario_outline_multi.feature ├── french.feature ├── transform.feature ├── scenario.feature └── scenario_outline.feature ├── Rakefile ├── Gemfile.lock ├── LICENSE.txt ├── yard-cucumber.gemspec ├── README.md └── History.txt /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.0.0-p353 2 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | yard-cucumber 2 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --plugin yard-cucumber 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | # /.rspec 2 | --format d 3 | --color 4 | -------------------------------------------------------------------------------- /lib/yard-cucumber/version.rb: -------------------------------------------------------------------------------- 1 | module CucumberInTheYARD 2 | VERSION = '4.0.0' 3 | end 4 | -------------------------------------------------------------------------------- /lib/templates/default/feature/html/no_steps_defined.erb: -------------------------------------------------------------------------------- 1 |
No Steps Defined
2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore any built gems that are laying around 2 | *.gem 3 | 4 | # Documents folder 5 | doc 6 | .yardoc -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'redcarpet' 4 | gem 'gherkin', '>= 4.0', '< 6.0' 5 | 6 | gemspec 7 | -------------------------------------------------------------------------------- /lib/templates/default/feature/html/pystring.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= htmlify_with_newlines @step.text %> 3 |
4 | -------------------------------------------------------------------------------- /example/empty.feature: -------------------------------------------------------------------------------- 1 | # This is a feature that has not been written 2 | # However, I don't want parser to fail when reaching this file -------------------------------------------------------------------------------- /example/step_definitions/support/env.rb: -------------------------------------------------------------------------------- 1 | 2 | module Environment 3 | module Database 4 | class Connection 5 | end 6 | end 7 | end -------------------------------------------------------------------------------- /lib/yard/code_objects/step_transform.rb: -------------------------------------------------------------------------------- 1 | module YARD::CodeObjects 2 | class StepTransformObject < StepTransformerObject; 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/yard/code_objects/step_definition.rb: -------------------------------------------------------------------------------- 1 | module YARD::CodeObjects 2 | class StepDefinitionObject < StepTransformerObject; 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /example/step_definitions/support/support.rb: -------------------------------------------------------------------------------- 1 | 2 | ORDER = /(?:first|second|third)/ 3 | 4 | TEDDY_BEAR = /teddy bear/ 5 | 6 | CUSTOMER = /(?:(?:an?|the|#{ORDER}) customer|#{TEDDY_BEAR})/ -------------------------------------------------------------------------------- /example/step_definitions/support/env_support.rb: -------------------------------------------------------------------------------- 1 | 2 | class SupportClass 3 | 4 | end 5 | 6 | module Web 7 | module Interface 8 | class CachedReference 9 | 10 | end 11 | end 12 | end -------------------------------------------------------------------------------- /example/child_feature/child.feature: -------------------------------------------------------------------------------- 1 | @scenarios 2 | Feature: Child Feature 3 | As a reader of the documentation I expect that scenario are documented correctly 4 | 5 | Background: 6 | Given this background step 7 | 8 | Scenario: Child Scenario 9 | Given this first step 10 | When this second step 11 | Then this third step -------------------------------------------------------------------------------- /example/step_definitions/struct.rb: -------------------------------------------------------------------------------- 1 | # 2 | # @see https://github.com/burtlo/yard-cucumber/issues/18 3 | # 4 | CustomerUsageBehavior = Struct.new(:weight, :days, :time, :location, :other_party, :usage_type, :direction, :quantity) 5 | 6 | 7 | class CustomerProfile 8 | def generate_winner(max=@total_weight) 9 | # blah 10 | end 11 | end -------------------------------------------------------------------------------- /example/child_feature/grandchild_feature/grandchild.feature: -------------------------------------------------------------------------------- 1 | @scenarios 2 | Feature: Grandchild Feature 3 | As a reader of the documentation I expect that scenario are documented correctly 4 | 5 | Background: 6 | Given this background step 7 | 8 | @first 9 | Scenario: Grandchild Scenario 10 | Given this first step 11 | When this second step 12 | Then this third step -------------------------------------------------------------------------------- /example/tags.feature: -------------------------------------------------------------------------------- 1 | @tags 2 | Feature: Tags 3 | As a developer of the test suite I expect that various tags will be supported 4 | 5 | @tag 6 | Scenario: Basic Tag 7 | 8 | @tag123456 9 | Scenario: Tag With Numbers 10 | 11 | @tag_with_underscore 12 | Scenario: Tag With Underscore 13 | 14 | @tag-with-dash 15 | Scenario: Tag With Dash 16 | 17 | @tag+with+plus 18 | Scenario: Tag With Plus -------------------------------------------------------------------------------- /lib/templates/default/steptransformers/html/header.erb: -------------------------------------------------------------------------------- 1 |
2 | <% if @item_type %> 3 | [Collapse All] 4 |
5 | <% end %> 6 | 7 | <% if @item_anchor_name %> 8 | 9 | <% end %> 10 |
<%= @item_title %>
11 |
12 | 13 | -------------------------------------------------------------------------------- /lib/yard/code_objects/cucumber/feature.rb: -------------------------------------------------------------------------------- 1 | module YARD::CodeObjects::Cucumber 2 | class Feature < NamespaceObject 3 | attr_accessor :background, :comments, :description, :keyword, :scenarios, :tags, :value 4 | 5 | def total_scenarios 6 | scenarios.count 7 | end 8 | 9 | def initialize(namespace, name) 10 | @comments = "" 11 | @scenarios = [] 12 | @tags = [] 13 | super(namespace, name.to_s.strip) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /example/step_definitions/first.step.rb: -------------------------------------------------------------------------------- 1 | Transform /^#{ORDER}$/ do |order| 2 | order 3 | end 4 | 5 | Transform /^background$/ do |background| 6 | "background" 7 | end 8 | 9 | # 10 | # This step transform converts "scenario" to "scenario" 11 | # 12 | Transform /^scenario$/ do |scenario| 13 | "scenario" 14 | end 15 | 16 | # 17 | # This step definition is all about steps 18 | # 19 | Given /^this (scenario|background|#{ORDER}) step$/ do |step| 20 | puts "step #{order}" 21 | end -------------------------------------------------------------------------------- /lib/templates/default/fulldoc/html/full_list_featuredirectories.erb: -------------------------------------------------------------------------------- 1 | <% n = 'odd' %> 2 | <% @items.each do |directory| %> 3 |
  • 4 |
    5 | <%= "" unless directory.features.empty? && directory.subdirectories.empty? %> 6 | <%= linkify directory, directory.value %> 7 | <%= directory.location %> 8 |
    9 | <%= directory_node directory, 45, n %> 10 |
  • 11 | <% end %> 12 | -------------------------------------------------------------------------------- /lib/templates/default/steptransformers/html/index.erb: -------------------------------------------------------------------------------- 1 |
    2 |
    Step Definitions & Transforms
    3 | 4 |
    5 |
    <%= step_definitions.length %>Step Definitions
    6 |
    <%= step_transforms.length %>Step Transforms
    7 |
    <%= undefined_steps.length %>Undefined Steps
    8 |
    9 | 10 |
    -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | This collection of features are really just to test some of the varied cases 2 | that CITY may come in contact with while parsing a series of features. All 3 | of the features, tags, and directories displayed here are contained in this 4 | directory and all subdirectories. 5 | 6 | * First, the features which are broken down alphabetically and displayed 7 | * Second, the tags, used by all the features and scenarios 8 | * Third, the subdirectories, contained in this directory and in the subdirectories. -------------------------------------------------------------------------------- /lib/yard/code_objects/cucumber/base.rb: -------------------------------------------------------------------------------- 1 | module YARD::CodeObjects::Cucumber 2 | module LocationHelper 3 | 4 | def line_number 5 | files.first.last 6 | end 7 | 8 | def file 9 | files.first.first if files && !files.empty? 10 | end 11 | 12 | def location 13 | "#{file}:#{line_number}" 14 | end 15 | end 16 | 17 | class Base < YARD::CodeObjects::Base 18 | include LocationHelper 19 | 20 | def path 21 | @value || super 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /example/scenario_outline_multi.feature: -------------------------------------------------------------------------------- 1 | Feature: My Feature 2 | 3 | Scenario Outline: Multiple Example Table 4 | Given that is a valid customer 5 | And that the product, named '', is a valid product 6 | When the customer has purchased the product 7 | Then I expect the customer to be a member of the '' group 8 | 9 | Examples: Example group A 10 | | Customer | Product | 11 | | Customer A | Product A | 12 | 13 | Examples: Example group B 14 | | Customer | Product | 15 | | Customer B | Product A | -------------------------------------------------------------------------------- /lib/templates/default/fulldoc/html/full_list_tags.erb: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | <%= linkify YARD::CodeObjects::Cucumber::CUCUMBER_TAG_NAMESPACE, "All Tags" %> 4 |
    6 | 7 | <% n = 'odd' %> 8 | <% @items.each do |tag| %> 9 |
  • 10 |
    11 | <%= linkify tag, tag.value %> 12 | <%= tag.total_scenarios %> 13 |
    14 |
  • 15 | <% n = n == 'odd' ? 'even' : 'odd' %> 16 | <% end %> 17 | -------------------------------------------------------------------------------- /lib/yard/code_objects/cucumber/scenario.rb: -------------------------------------------------------------------------------- 1 | module YARD::CodeObjects::Cucumber 2 | 3 | class Scenario < NamespaceObject 4 | 5 | attr_accessor :value, :comments, :keyword, :description, :steps, :tags, :feature 6 | 7 | def initialize(namespace, name) 8 | super(namespace, name.to_s.strip) 9 | @comments = @description = @keyword = @value = @feature = nil 10 | @steps = [] 11 | @tags = [] 12 | end 13 | 14 | def background? 15 | @keyword == "Background" 16 | end 17 | 18 | def outline? 19 | false 20 | end 21 | end 22 | end -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | 3 | task :default => :gendoc 4 | 5 | desc "Clean out any existing documentation" 6 | task :clean do 7 | `rm -rf doc` 8 | `rm -rf .yardoc` 9 | end 10 | 11 | desc "Generate documentation from the example data" 12 | task :gendoc => :clean do 13 | puts `yardoc -e ./lib/yard-cucumber.rb 'example/**/*' --debug` 14 | end 15 | 16 | desc "Run the YARD Server" 17 | task :server => :gendoc do 18 | puts `yard server -e ./lib/yard-cucumber.rb` 19 | end 20 | 21 | desc "Create the yard-cucumber gem" 22 | task :gem do 23 | puts `gem build yard-cucumber.gemspec` 24 | end 25 | -------------------------------------------------------------------------------- /lib/docserver/doc_server/full_list/html/setup.rb: -------------------------------------------------------------------------------- 1 | include T('default/fulldoc/html') 2 | 3 | def init 4 | # This is the css class type; here we just default to class 5 | @list_class = "class" 6 | case @list_type.to_sym 7 | when :features; @list_title = "Features" 8 | when :tags; @list_title = "Tags" 9 | when :class; @list_title = "Class List" 10 | when :methods; @list_title = "Method List" 11 | when :files; @list_title = "File List" 12 | end 13 | sections :full_list 14 | end 15 | 16 | def all_features_link 17 | linkify YARD::CodeObjects::Cucumber::CUCUMBER_NAMESPACE, "All Features" 18 | end -------------------------------------------------------------------------------- /example/french.feature: -------------------------------------------------------------------------------- 1 | # language: fr 2 | Fonctionnalité: Addition 3 | Afin de gagner du temps lors du calcul de la facture 4 | En tant que commerçant 5 | Je souhaite pouvoir faire une addition 6 | 7 | Plan du Scénario: Addition de deux nombres 8 | Soit une calculatrice 9 | Et que j'entre pour le premier nombre 10 | Et que je tape sur la touche "+" 11 | Et que j'entre pour le second nombre 12 | Lorsque je tape sur la touche "=" 13 | Alors le résultat affiché doit être 14 | 15 | Exemples: 16 | | a | b | somme | 17 | | 2 | 2 | 4 | 18 | | 2 | 3 | 5 | -------------------------------------------------------------------------------- /lib/yard/handlers/cucumber/base.rb: -------------------------------------------------------------------------------- 1 | module YARD 2 | module Handlers 3 | module Cucumber 4 | 5 | class Base < Handlers::Base 6 | class << self 7 | include Parser::Cucumber 8 | def handles?(node) 9 | handlers.any? do |a_handler| 10 | #log.debug "YARD::Handlers::Cucumber::Base#handles?(#{node.class})" 11 | node.class == a_handler 12 | end 13 | end 14 | include Parser::Cucumber 15 | end 16 | end 17 | 18 | Processor.register_handler_namespace :feature, Cucumber 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/templates/default/tag/html/setup.rb: -------------------------------------------------------------------------------- 1 | def init 2 | super 3 | @tag = object 4 | 5 | sections.push :tag 6 | end 7 | 8 | def features 9 | @tag.features 10 | end 11 | 12 | def scenarios 13 | @tag.scenarios 14 | end 15 | 16 | 17 | def alpha_table(objects) 18 | @elements = Hash.new 19 | 20 | objects = run_verifier(objects) 21 | objects.each {|o| (@elements[o.value.to_s[0,1].upcase] ||= []) << o } 22 | @elements.values.each {|v| v.sort! {|a,b| b.value.to_s <=> a.value.to_s } } 23 | @elements = @elements.sort_by {|l,o| l.to_s } 24 | 25 | @elements.each {|letter,objects| objects.sort! {|a,b| b.value.to_s <=> a.value.to_s }} 26 | erb(:alpha_table) 27 | end -------------------------------------------------------------------------------- /lib/templates/default/feature/html/table.erb: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 6 | <% @step.table.first.each_with_index do |column,column_index| %> 7 | 8 | <% end %> 9 | 10 | 11 | 12 | <% @step.table[1..-1].each_with_index do |row,row_index| %> 13 | 14 | <% row.each_with_index do |column,column_index| %> 15 | 16 | <% end %> 17 | 18 | <% end %> 19 |
    <%= h(column.strip) %>
    <%= h(column.strip) %>
    20 |
    21 | -------------------------------------------------------------------------------- /example/child_feature/README.md: -------------------------------------------------------------------------------- 1 | Child Features 2 | ============== 3 | 4 | Synopsis 5 | -------- 6 | 7 | This collection of features are contained in this folder. The description README 8 | allows a user to place a description for entire directory of the features and appears 9 | in the output to assist with understanding about the collection of features. 10 | 11 | Resources 12 | --------- 13 | 14 | Links to particular resources like the links to the stories, tasks, or other areas 15 | can also be represented. 16 | 17 | The implemented example has been deployed at [http://recursivegames.com/cukes/](http://recursivegames.com/cukes/). 18 | 19 | **1. An Item** [example](http://recursivegames.com/cukes/requirements/) 20 | 21 | 22 | -------------------------------------------------------------------------------- /example/transform.feature: -------------------------------------------------------------------------------- 1 | @scenarios @bvt 2 | Feature: Step Transforms 3 | As a developer of the test suite I expect that step transforms are documented correctly 4 | 5 | @first 6 | Scenario: Step with step transformation 7 | Given this scenario step 8 | Then I expect that the step, on the step transformer page, will link to the step transform 9 | 10 | @second 11 | Scenario: Step Transform uses a constant 12 | Given this first step 13 | Then I expect that the step, on the step transformer page, will link to the step transform 14 | 15 | @third 16 | Scenario: Step Transform uses an interpolated transform 17 | Given this first step 18 | Then the file './somelocation/somefile.input' will be replaced with the file contents 19 | -------------------------------------------------------------------------------- /lib/templates/default/fulldoc/html/full_list_steps.erb: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | <%= linkify YARD::CodeObjects::Cucumber::CUCUMBER_STEPTRANSFORM_NAMESPACE, "All Defined Steps" %> 4 |
    5 |
  • 6 | <% n = 'even' %> 7 | <% @items.each do |step| %> 8 |
  • 9 | 18 |
  • 19 | <% n = n == 'even' ? 'odd' : 'even' %> 20 | <% end %> 21 | -------------------------------------------------------------------------------- /example/step_definitions/french_steps.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | Soit /^une calculatrice$/ do 3 | @calc = Calculatrice.new 4 | end 5 | 6 | Etantdonné /^qu'on tape (.*)$/ do |n| 7 | @calc.push n.to_i 8 | end 9 | 10 | Etantdonné /^que j'entre (\d+) pour le (.*) nombre/ do |n, x| 11 | @calc.push n.to_i 12 | end 13 | 14 | Lorsque /^je tape sur la touche "="$/ do 15 | @expected_result = @calc.additionner 16 | end 17 | 18 | Lorsqu /on tape additionner/ do 19 | @expected_result = @calc.additionner 20 | end 21 | 22 | Alors /le résultat affiché doit être (\d*)/ do |result| 23 | result.to_i.should == @expected_result 24 | end 25 | 26 | Alors /le résultat doit être (\d*)/ do |result| 27 | result.to_i.should == @expected_result 28 | end 29 | 30 | Soit /^que je tape sur la touche "\+"$/ do 31 | # noop 32 | end -------------------------------------------------------------------------------- /lib/templates/default/fulldoc/html/full_list_stepdefinitions.erb: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | <%= linkify YARD::CodeObjects::Cucumber::CUCUMBER_STEPTRANSFORM_NAMESPACE, "All Step Definitions" %> 4 |
    5 |
  • 6 | <% n = 'odd' %> 7 | <% @items.each do |stepdef| %> 8 |
  • 9 |
    10 | 11 | <%= stepdef.keyword %> 12 | 13 | <%= h stepdef.literal_value %> 14 |    15 | 16 | <%= h(stepdef.location) %> 17 |
    18 |
  • 19 | <% n = n == 'even' ? 'odd' : 'even' %> 20 | <% end %> 21 | -------------------------------------------------------------------------------- /lib/yard/server/commands/list_command.rb: -------------------------------------------------------------------------------- 1 | module YARD 2 | module Server 3 | module Commands 4 | 5 | # 6 | # List Features powers the features menu option in `yard server` 7 | # 8 | class ListFeaturesCommand < ListCommand 9 | def type; :features end 10 | 11 | def items 12 | Registry.load_all 13 | run_verifier(Registry.all(:feature)) 14 | end 15 | end 16 | 17 | # 18 | # List Tags powers the tags menu option in `yard server` 19 | # 20 | class ListTagsCommand < ListCommand 21 | def type; :tags end 22 | 23 | def items 24 | Registry.load_all 25 | run_verifier(Registry.all(:tag).sort_by {|t| t.value.to_s }) 26 | end 27 | end 28 | 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/templates/default/featuretags/html/setup.rb: -------------------------------------------------------------------------------- 1 | def init 2 | super 3 | sections.push :namespace 4 | @namespace = object 5 | end 6 | 7 | def namespace 8 | erb(:namespace) 9 | end 10 | 11 | def all_tags_by_letter 12 | hash = {} 13 | objects = tags 14 | objects = run_verifier(objects) 15 | objects.each {|o| (hash[o.value.to_s[1,1].upcase] ||= []) << o } 16 | hash 17 | end 18 | 19 | def tags 20 | @tags ||= Registry.all(:tag).sort_by {|l| l.value.to_s } 21 | end 22 | 23 | def features 24 | @features ||= Registry.all(:feature).sort {|x,y| x.value.to_s <=> y.value.to_s } 25 | end 26 | 27 | def feature_tags_with_all_scenario_tags(feature) 28 | feature.tags.collect {|t| t.value} + feature.scenarios.collect {|s| s.tags.collect {|t| t.value} }.flatten.uniq 29 | end 30 | 31 | 32 | def tagify(tag) 33 | %{#{tag.value}} 34 | end 35 | -------------------------------------------------------------------------------- /lib/yard/code_objects/cucumber/tag.rb: -------------------------------------------------------------------------------- 1 | module YARD::CodeObjects::Cucumber 2 | class Tag < NamespaceObject 3 | 4 | attr_accessor :value, :owners, :total_scenarios 5 | 6 | def features 7 | @owners.find_all { |owner| owner.is_a?(Feature) } 8 | end 9 | 10 | def scenarios 11 | all = @owners.find_all do |owner| 12 | owner.is_a?(Scenario) || owner.is_a?(ScenarioOutline) || () 13 | end 14 | 15 | @owners.each do |owner| 16 | if owner.is_a?(ScenarioOutline::Examples) && !all.include?(owner.scenario) 17 | all << owner.scenario 18 | end 19 | end 20 | all 21 | end 22 | 23 | def indirect_scenarios 24 | @owners.find_all { |owner| owner.is_a?(Feature) }.collect { |feature| feature.scenarios }.flatten 25 | end 26 | 27 | def all_scenarios 28 | scenarios + indirect_scenarios 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/yard/templates/helpers/base_helper.rb: -------------------------------------------------------------------------------- 1 | module YARD::Templates::Helpers 2 | 3 | module BaseHelper 4 | 5 | def format_object_title(object) 6 | if object.is_a?(YARD::CodeObjects::Cucumber::FeatureTags) 7 | "Tags" 8 | elsif object.is_a?(YARD::CodeObjects::Cucumber::StepTransformersObject) 9 | "Step Definitions and Transforms" 10 | elsif object.is_a?(YARD::CodeObjects::Cucumber::NamespaceObject) 11 | "#{format_object_type(object)}#{object.value ? ": #{object.value}" : ''}" 12 | elsif object.is_a?(YARD::CodeObjects::Cucumber::FeatureDirectory) 13 | "Feature Directory: #{object.name}" 14 | else 15 | case object 16 | when YARD::CodeObjects::RootObject 17 | "Top Level Namespace" 18 | else 19 | format_object_type(object) + ": " + object.path 20 | end 21 | end 22 | end 23 | 24 | end 25 | 26 | end 27 | -------------------------------------------------------------------------------- /lib/yard/handlers/legacy/step_transform_handler.rb: -------------------------------------------------------------------------------- 1 | class YARD::Handlers::Ruby::Legacy::StepTransformHandler < YARD::Handlers::Ruby::Legacy::Base 2 | STEP_TRANSFORM_MATCH = /^(Transform\s*(\/.+\/)\s+do(?:\s*\|.+\|)?\s*)$/ unless defined?(STEP_TRANSFORM_MATCH) 3 | handles STEP_TRANSFORM_MATCH 4 | 5 | @@unique_name = 0 6 | 7 | def process 8 | transform = statement.tokens.to_s[STEP_TRANSFORM_MATCH,2] 9 | @@unique_name = @@unique_name + 1 10 | 11 | instance = StepTransformObject.new(YARD::CodeObjects::Cucumber::CUCUMBER_STEPTRANSFORM_NAMESPACE, "transform_#{@@unique_name}") do |o| 12 | o.source = "Transform #{transform} do #{statement.block.to_s}\nend" 13 | o.value = transform 14 | o.keyword = "Transform" 15 | end 16 | 17 | obj = register instance 18 | parse_block :owner => obj 19 | 20 | rescue YARD::Handlers::NamespaceMissingError 21 | end 22 | 23 | end 24 | 25 | -------------------------------------------------------------------------------- /lib/templates/default/featuredirectory/html/directory.erb: -------------------------------------------------------------------------------- 1 | <% if @directory %> 2 | 3 |
    4 |
    5 | Directory: 6 | <%= h @directory.name.to_s.capitalize %> 7 |
    8 | 9 |
    10 | <%= markdown @directory.description %> 11 |
    12 | 13 | <% if features && !features.empty? %> 14 | <%= alpha_table(features) %> 15 | <% end %> 16 | 17 |
    18 |
    Tags
    19 |
    20 |
    21 | <%= tags.collect {|tag| linkify(tag,tag.value) }.join(",\n") %> 22 |
    23 | 24 | <% if directories && !directories.empty? %> 25 |
    26 |
    Subdirectories
    27 |
    28 | <%= alpha_table(directories) %> 29 | <% end %> 30 |
    31 | 32 | <% end %> -------------------------------------------------------------------------------- /lib/templates/default/requirements/html/alpha_table.erb: -------------------------------------------------------------------------------- 1 | <% if @elements && !@elements.empty? %> 2 | <% i = (@elements.length % 2 == 0 ? 1 : 0) %> 3 | 4 | 5 | 24 | 25 |
    6 | <% @elements.each do |letter, objects| %> 7 | <% if (i += 1) > (@elements.length / 2 + 1) %> 8 | 9 | <% i = 0 %> 10 | <% end %> 11 |
      12 |
    • <%= letter %>
    • 13 |
        14 | <% objects.each do |obj| %> 15 |
      • 16 | <%= linkify obj, obj.value %> 17 | (<%= obj.file %>) 18 |
      • 19 | <% end %> 20 |
      21 |
    22 | <% end %> 23 |
    26 | <% end %> 27 | -------------------------------------------------------------------------------- /lib/yard/server/router.rb: -------------------------------------------------------------------------------- 1 | module YARD 2 | module Server 3 | 4 | # 5 | # The YARD::Server::Router needs the following modification, 6 | # so that it will provide routing for the features and tags commands 7 | # to their appropriate definitions 8 | # 9 | Router.class_eval do 10 | 11 | alias_method :core_route_list, :route_list 12 | 13 | # 14 | # Provide the full list of features and tags 15 | # 16 | def route_list(library, paths) 17 | 18 | if paths && !paths.empty? && paths.first =~ /^(?:features|tags)$/ 19 | case paths.shift 20 | when "features"; cmd = Commands::ListFeaturesCommand 21 | when "tags"; cmd = Commands::ListTagsCommand 22 | end 23 | cmd.new(final_options(library, paths)).call(request) 24 | else 25 | core_route_list(library,paths) 26 | end 27 | 28 | end 29 | 30 | end 31 | end 32 | end -------------------------------------------------------------------------------- /lib/docserver/default/layout/html/headers.erb: -------------------------------------------------------------------------------- 1 | 2 | <%= @page_title %> 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /lib/templates/default/steptransformers/html/undefinedsteps.erb: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | <% if @undefined_steps && !@undefined_steps.empty? %> 4 | <% @undefined_steps.each_with_index do |uniq_step,step_index| %> 5 |
    6 |
    7 | <%= uniq_step.last.first.keyword %> 8 | 9 | <%= h uniq_step.last.first.value %> 10 | 11 |
    12 | <% uniq_step.last.each do |step| %> 13 | 14 | <%= h step.location %> 15 | 16 | <% end%> 17 |
    18 |
    19 | <% end %> 20 | <% else %> 21 |
    22 |
    No undefined steps
    23 |
    24 | <% end%> 25 |
    26 |
    27 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | yard-cucumber (3.1.0) 5 | cucumber (>= 2.0, < 4.0) 6 | gherkin (>= 4.0, < 6.0) 7 | yard (~> 0.8, >= 0.8.1) 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | builder (3.2.3) 13 | cucumber (2.2.0) 14 | builder (>= 2.1.2) 15 | cucumber-core (~> 1.3.0) 16 | cucumber-wire (~> 0.0.1) 17 | diff-lcs (>= 1.1.3) 18 | event-bus (~> 0.1.0) 19 | gherkin3 (~> 3.1.0) 20 | multi_json (>= 1.7.5, < 2.0) 21 | multi_test (>= 0.1.2) 22 | cucumber-core (1.3.1) 23 | gherkin3 (~> 3.1.0) 24 | cucumber-wire (0.0.1) 25 | diff-lcs (1.3) 26 | event-bus (0.1.0) 27 | gherkin (5.0.0) 28 | gherkin3 (3.1.2) 29 | multi_json (1.13.1) 30 | multi_test (0.1.2) 31 | rake (10.5.0) 32 | redcarpet (3.4.0) 33 | yard (0.9.12) 34 | 35 | PLATFORMS 36 | ruby 37 | 38 | DEPENDENCIES 39 | gherkin (>= 4.0, < 6.0) 40 | rake (~> 10) 41 | redcarpet 42 | yard-cucumber! 43 | 44 | BUNDLED WITH 45 | 1.16.1 46 | -------------------------------------------------------------------------------- /lib/yard/code_objects/cucumber/step.rb: -------------------------------------------------------------------------------- 1 | module YARD::CodeObjects::Cucumber 2 | class Step < Base 3 | 4 | attr_accessor :comments, 5 | :definition, 6 | :examples, 7 | :keyword, 8 | :scenario, 9 | :table, 10 | :text, 11 | :transforms, 12 | :value 13 | 14 | def initialize(namespace, name) 15 | super(namespace, name.to_s.strip) 16 | @comments = @definition = @description = @keyword = @table = @text = @value = nil 17 | @examples = {} 18 | @transforms = [] 19 | end 20 | 21 | def has_table? 22 | !@table.nil? 23 | end 24 | 25 | def has_text? 26 | !@text.nil? 27 | end 28 | 29 | def definition=(stepdef) 30 | @definition = stepdef 31 | 32 | unless stepdef.steps.map(&:files).include?(files) 33 | stepdef.steps << self 34 | end 35 | end 36 | 37 | def transformed? 38 | !@transforms.empty? 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Franklin Webber 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /lib/templates/default/featuredirectory/html/alpha_table.erb: -------------------------------------------------------------------------------- 1 | <% if @elements && !@elements.empty? %> 2 | <% i = (@elements.length % 2 == 0 ? 1 : 0) %> 3 | 4 | 5 | 28 | 29 |
    6 | <% @elements.each do |letter, objects| %> 7 | <% if (i += 1) > (@elements.length / 2 + 1) %> 8 | 9 | <% i = 0 %> 10 | <% end %> 11 |
      12 |
    • <%= letter %>
    • 13 |
        14 | <% objects.each do |obj| %> 15 |
      • 16 | <%= linkify obj, obj.value %> 17 | <% if obj.is_a? YARD::CodeObjects::Cucumber::FeatureDirectory %> 18 | (<%= obj.expanded_path %>) 19 | <% else %> 20 | (<%= obj.file %>) 21 | <% end %> 22 |
      • 23 | <% end %> 24 |
      25 |
    26 | <% end %> 27 |
    30 | <% end %> 31 | -------------------------------------------------------------------------------- /lib/templates/default/fulldoc/html/directories.erb: -------------------------------------------------------------------------------- 1 | <% @row = @row == 'even' ? 'odd' : 'even' %> 2 | <% if @directory.children %> 3 |
      4 | <% @directory.children.sort {|x,y| x.value.to_s <=> y.value.to_s }.each_with_index do |child,index| %> 5 | <% if child.is_a?(YARD::CodeObjects::Cucumber::Feature) %> 6 |
    • 7 |
      8 | <%= linkify child, child.value %> 9 | <%= child.location %> 10 |
      11 |
    • 12 | <% @row = @row == 'even' ? 'odd' : 'even' %> 13 | <% end %> 14 | 15 | <% if child.is_a?(YARD::CodeObjects::Cucumber::FeatureDirectory) %> 16 |
    • 17 |
      18 | <%= "" unless child.features.empty? && child.subdirectories.empty? %> 19 | <%= linkify child, child.value.to_s.capitalize %> 20 | <%= child.location %> 21 |
      22 | <%= directory_node child, @padding + 15, @row %> 23 |
    • 24 | <% end %> 25 | <% end %> 26 |
    27 | <% end %> 28 | -------------------------------------------------------------------------------- /lib/templates/default/feature/html/feature.erb: -------------------------------------------------------------------------------- 1 | 7 | 8 |
    9 |
    10 | [More Detail] 11 | [Collapse All] 12 |
    13 | <%= @feature.keyword %>: 14 | <%= @feature.value %> 15 |
    16 | 17 | <% if @feature.comments.length > 0 %> 18 |
    19 | <%= htmlify_with_newlines @feature.comments %> 20 |
    21 | <% end %> 22 | 23 |
    24 | <%= htmlify_with_newlines @feature.description %> 25 |
    26 | 27 |
    28 |
    <%= h(@feature.file) %>
    29 |
    30 |
    31 | <% @feature.tags.each do |tag| %> 32 | <%= tag.value %> 33 | <% end %> 34 |
    35 |
    36 | 37 | <%= yieldall %> 38 |
    39 | 40 | -------------------------------------------------------------------------------- /lib/templates/default/tag/html/tag.erb: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | Tag: 4 | <%= @tag.value %> 5 | 6 |
    7 | 8 |
    9 |
    10 | <%= @tag.features.size > 0 ? "#{@tag.features.size} feature#{@tag.features.size > 1 ? 's' : ''} (with #{@tag.indirect_scenarios.size} scenario#{@tag.indirect_scenarios.size > 1 ? 's' : ''})" : "" %> 11 | <%= " and " if @tag.features.size > 0 && @tag.scenarios.size > 0 %> 12 | <%= @tag.scenarios.size > 0 ? "#{@tag.scenarios.size} scenario#{@tag.scenarios.size > 1 ? 's' : ''}" : "" %> 13 |
    14 |
    15 | 16 |
     
    17 | 18 | <% if features && !features.empty? %> 19 |
    20 |
    Features
    21 |
    22 | <%= alpha_table(features) %> 23 | 24 | <% end %> 25 | 26 | <% if scenarios && !scenarios.empty? %> 27 |
    28 |
    Scenarios
    29 |
    30 | <%= alpha_table(scenarios) %> 31 | 32 | <% end %> 33 | 34 | 35 |
    36 | -------------------------------------------------------------------------------- /lib/templates/default/featuredirectory/html/setup.rb: -------------------------------------------------------------------------------- 1 | def init 2 | super 3 | sections.push :directory 4 | @directory = object 5 | end 6 | 7 | def markdown(text) 8 | htmlify(text,:markdown) rescue h(text) 9 | end 10 | 11 | def htmlify_with_newlines(text) 12 | text.split("\n").collect {|c| h(c).gsub(/\s/,' ') }.join("
    ") 13 | end 14 | 15 | def directories 16 | @directories ||= @directory.subdirectories 17 | end 18 | 19 | def features 20 | @features ||= @directory.features + directories.collect {|d| d.features }.flatten 21 | end 22 | 23 | def scenarios 24 | @scenarios ||= features.collect {|feature| feature.scenarios }.flatten 25 | end 26 | 27 | def tags 28 | @tags ||= (features.collect{|feature| feature.tags } + scenarios.collect {|scenario| scenario.tags }).flatten.uniq.sort_by {|l| l.value.to_s } 29 | end 30 | 31 | def alpha_table(objects) 32 | @elements = Hash.new 33 | 34 | objects = run_verifier(objects) 35 | objects.each {|o| (@elements[o.value.to_s[0,1].upcase] ||= []) << o } 36 | @elements.values.each {|v| v.sort! {|a,b| b.value.to_s <=> a.value.to_s } } 37 | @elements = @elements.sort_by {|l,o| l.to_s } 38 | 39 | @elements.each {|letter,objects| objects.sort! {|a,b| b.value.to_s <=> a.value.to_s }} 40 | erb(:alpha_table) 41 | end 42 | -------------------------------------------------------------------------------- /lib/templates/default/fulldoc/html/full_list_features.erb: -------------------------------------------------------------------------------- 1 | <% n = 'odd' %> 2 |
  • 3 |
    4 | <%= all_features_link %> 5 |
    6 |
  • 7 | <% n = n == 'odd' ? 'even' : 'odd' %> 8 | <% @items.each do |feature| %> 9 |
  • 10 |
    11 | <%= "" unless feature.scenarios.empty? %> 12 | <%= linkify feature, feature.value %> 13 | <%= feature.total_scenarios %> 14 |
    15 | 16 | <% n = n == 'odd' ? 'even' : 'odd' %> 17 | <% if feature.scenarios %> 18 | 34 | <% end %> 35 | 36 |
  • 37 | <% end %> 38 | -------------------------------------------------------------------------------- /lib/templates/default/feature/html/steps.erb: -------------------------------------------------------------------------------- 1 | <% @steps.each_with_index do |step,index| %> 2 | <% @step = step %> 3 | 4 | <% if step.comments && step.comments.length > 0 %> 5 |
    <%= htmlify_with_newlines step.comments %>
    6 | <% end %> 7 |
    8 | <%= step.keyword %> 9 | 10 | <% if @scenario.outline? %> 11 | <%= h step.value %> 12 | <% else %> 13 | 14 | <% if step.definition %> 15 | 16 | <%= highlight_matches(step) %> 17 |
    18 |
     
    19 |
    20 |
    21 | <% else %> 22 | 23 | <%= h step.value %> 24 |
    25 | 26 |
     
    27 |
    28 |
    29 |
    30 | <% end %> 31 | 32 | <% end %> 33 |
    34 | 35 | <%= erb(:table) if step.has_table? %> 36 | <%= erb(:pystring) if step.has_text? %> 37 | 38 | 39 | <% end %> 40 | -------------------------------------------------------------------------------- /lib/templates/default/feature/html/setup.rb: -------------------------------------------------------------------------------- 1 | def init 2 | super 3 | @feature = object 4 | 5 | sections.push :feature 6 | 7 | sections.push :scenarios if object.scenarios 8 | 9 | end 10 | 11 | def background 12 | @scenario = @feature.background 13 | @id = "background" 14 | erb(:scenario) 15 | end 16 | 17 | def scenarios 18 | scenarios = "" 19 | 20 | if @feature.background 21 | @scenario = @feature.background 22 | @id = "background" 23 | scenarios += erb(:scenario) 24 | end 25 | 26 | @feature.scenarios.each_with_index do |scenario,index| 27 | @scenario = scenario 28 | @id = "scenario_#{index}" 29 | scenarios += erb(:scenario) 30 | end 31 | 32 | scenarios 33 | end 34 | 35 | 36 | def highlight_matches(step) 37 | value = step.value.dup 38 | 39 | if step.definition 40 | matches = step.value.match(step.definition.regex) 41 | 42 | if matches 43 | matches[1..-1].reverse.each_with_index do |match,index| 44 | next if match == nil 45 | value[matches.begin((matches.size - 1) - index)..(matches.end((matches.size - 1) - index) - 1)] = "#{h(match)}" 46 | end 47 | end 48 | end 49 | 50 | value 51 | end 52 | 53 | def htmlify_with_newlines(text) 54 | text.split("\n").collect {|c| h(c).gsub(/\s/,' ') }.join("
    ") 55 | end -------------------------------------------------------------------------------- /lib/templates/default/tag/html/alpha_table.erb: -------------------------------------------------------------------------------- 1 | <% if @elements && !@elements.empty? %> 2 | <% i = (@elements.length % 2 == 0 ? 1 : 0) %> 3 | 4 | 5 | 31 | 32 |
    6 | <% @elements.each do |letter, objects| %> 7 | <% if (i += 1) > (@elements.length / 2 + 1) %> 8 | 9 | <% i = 0 %> 10 | <% end %> 11 |
      12 |
    • <%= letter %>
    • 13 |
        14 | <% objects.each do |obj| %> 15 |
      • 16 | <% if obj.is_a?(YARD::CodeObjects::Cucumber::Scenario) || obj.is_a?(YARD::CodeObjects::Cucumber::ScenarioOutline) %> 17 | <% index = obj.path[(/.+_(\d+)$/),1].to_i - 1 %> 18 | "> 19 | <%= h obj.value %> 20 | 21 | <% else %> 22 | <%= linkify obj, obj.value %> 23 | <% end %> 24 | (<%= obj.file %>) 25 |
      • 26 | <% end %> 27 |
      28 |
    29 | <% end %> 30 |
    33 | <% end %> 34 | -------------------------------------------------------------------------------- /lib/yard/server/adapter.rb: -------------------------------------------------------------------------------- 1 | module YARD 2 | module Server 3 | 4 | class Adapter 5 | 6 | class << self 7 | 8 | alias_method :yard_setup, :setup 9 | 10 | # 11 | # To provide the templates necessary for `yard-cucumber` to integrate 12 | # with YARD the adapter has to around-alias the setup method to place 13 | # the `yard-cucumber` server templates as the last template in the list. 14 | # 15 | # When they are normally loaded with the plugin they cause an error with 16 | # the `yardoc` command. They are also not used because the YARD server 17 | # templates are placed after all plugin templates. 18 | # 19 | def setup 20 | yard_setup 21 | YARD::Templates::Engine.template_paths += 22 | [File.dirname(__FILE__) + '/../../templates',File.dirname(__FILE__) + '/../../docserver'] 23 | end 24 | 25 | alias_method :yard_shutdown, :shutdown 26 | 27 | # 28 | # Similar to the addition, it is good business to tear down the templates 29 | # that were added by again around-aliasing the shutdown method. 30 | # 31 | def shutdown 32 | yard_shutdown 33 | YARD::Templates::Engine.template_paths -= 34 | [File.dirname(__FILE__) + '/../../templates',File.dirname(__FILE__) + '/../../docserver'] 35 | end 36 | 37 | end 38 | 39 | 40 | end 41 | 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/templates/default/requirements/html/setup.rb: -------------------------------------------------------------------------------- 1 | def init 2 | super 3 | sections.push :requirements 4 | @namespace = object 5 | 6 | end 7 | 8 | def features 9 | @features ||= Registry.all(:feature) 10 | end 11 | 12 | def tags 13 | @tags ||= Registry.all(:tag).sort_by {|l| l.value.to_s } 14 | end 15 | 16 | def feature_directories 17 | @feature_directories ||= YARD::CodeObjects::Cucumber::CUCUMBER_NAMESPACE.children.find_all {|child| child.is_a?(YARD::CodeObjects::Cucumber::FeatureDirectory)} 18 | end 19 | 20 | def feature_subdirectories 21 | @feature_subdirectories ||= Registry.all(:featuredirectory) - feature_directories 22 | end 23 | 24 | def step_transformers 25 | YARD::CodeObjects::Cucumber::CUCUMBER_STEPTRANSFORM_NAMESPACE 26 | end 27 | 28 | def step_definitions 29 | @step_definitions ||= YARD::Registry.all(:stepdefinition) 30 | end 31 | 32 | def transformers 33 | @transformers ||= YARD::Registry.all(:steptransform) 34 | end 35 | 36 | def undefined_steps 37 | @undefined_steps ||= Registry.all(:step).reject {|s| s.definition || s.scenario.outline? } 38 | end 39 | 40 | 41 | def alpha_table(objects) 42 | @elements = Hash.new 43 | 44 | objects = run_verifier(objects) 45 | objects.each {|o| (@elements[o.value.to_s[0,1].upcase] ||= []) << o } 46 | @elements.values.each {|v| v.sort! {|a,b| b.value.to_s <=> a.value.to_s } } 47 | @elements = @elements.sort_by {|l,o| l.to_s } 48 | 49 | @elements.each {|letter,objects| objects.sort! {|a,b| b.value.to_s <=> a.value.to_s }} 50 | erb(:alpha_table) 51 | end 52 | -------------------------------------------------------------------------------- /lib/yard/code_objects/cucumber/namespace_object.rb: -------------------------------------------------------------------------------- 1 | module YARD::CodeObjects::Cucumber 2 | class NamespaceObject < YARD::CodeObjects::NamespaceObject 3 | include LocationHelper 4 | 5 | def value; 6 | nil; 7 | end 8 | end 9 | 10 | class Requirements < NamespaceObject; 11 | end 12 | class FeatureTags < NamespaceObject; 13 | end 14 | class StepTransformersObject < NamespaceObject; 15 | end 16 | 17 | class FeatureDirectory < YARD::CodeObjects::NamespaceObject 18 | 19 | attr_accessor :description 20 | 21 | def initialize(namespace, name) 22 | super(namespace, name) 23 | @description = "" 24 | end 25 | 26 | def location 27 | files.first.first if files && !files.empty? 28 | end 29 | 30 | def expanded_path 31 | to_s.split('::')[1..-1].join('/') 32 | end 33 | 34 | def value; 35 | name; 36 | end 37 | 38 | def features 39 | children.find_all { |d| d.is_a?(Feature) } 40 | end 41 | 42 | def subdirectories 43 | subdirectories = children.find_all { |d| d.is_a?(FeatureDirectory) } 44 | subdirectories + subdirectories.collect { |s| s.subdirectories }.flatten 45 | end 46 | 47 | end 48 | 49 | CUCUMBER_NAMESPACE = Requirements.new(:root, "requirements") unless defined?(CUCUMBER_NAMESPACE) 50 | 51 | CUCUMBER_TAG_NAMESPACE = FeatureTags.new(CUCUMBER_NAMESPACE, "tags") unless defined?(CUCUMBER_TAG_NAMESPACE) 52 | 53 | CUCUMBER_STEPTRANSFORM_NAMESPACE = StepTransformersObject.new(CUCUMBER_NAMESPACE, "step_transformers") unless defined?(CUCUMBER_STEPTRANSFORM_NAMESPACE) 54 | 55 | end 56 | -------------------------------------------------------------------------------- /lib/templates/default/feature/html/scenario.erb: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 |
    5 | - 6 | <%= @scenario.keyword %>: 7 | <%= h @scenario.value %> 8 |
    9 | link 10 |
    11 | 12 | <% if @scenario.description.length > 0 %> 13 |
    14 | <%= htmlify_with_newlines @scenario.description %> 15 |
    16 | <% end %> 17 | 18 | <% if @scenario.comments.length > 0 %> 19 |
    20 | <%= htmlify_with_newlines @scenario.comments %> 21 |
    22 | <% end %> 23 | 24 |
    25 |
    26 |
    <%= @scenario.location %>
    27 | <% unless @scenario.tags.empty? %> 28 |
    29 |
    30 | <% @scenario.tags.each do |tag| %> 31 | <%= tag.value %> 32 | <% end %> 33 |
    34 | <% end%> 35 |
    36 |
    37 | 38 |
    39 | <% if @scenario.steps.empty? %> 40 | <%= erb(:no_steps_defined) %> 41 | <% else %> 42 | <%= @steps = @scenario.steps ; erb(:steps) %> 43 | <% end %> 44 |
    45 | 46 | 47 | <%= erb(:outline) if @scenario.outline? %> 48 | 49 |
    50 | 51 | 54 | 55 |
    56 | -------------------------------------------------------------------------------- /lib/yard/handlers/step_transform_handler.rb: -------------------------------------------------------------------------------- 1 | 2 | class YARD::Handlers::Ruby::StepTransformHandler < YARD::Handlers::Ruby::Base 3 | handles method_call(:Transform) 4 | handles method_call(:ParameterType) 5 | 6 | process do 7 | 8 | nextStatement = nil 9 | instance = YARD::CodeObjects::StepTransformObject.new(step_transform_namespace,step_transformer_name) do |o| 10 | o.source = statement.source 11 | o.comments = statement.comments 12 | o.keyword = statement[0].source 13 | if (o.keyword == 'Transform') 14 | o.value = statement[1].source.gsub(/(^\(?\/|\/\)?$)/, '').gsub(/(^\^|\$$)/, '') 15 | nextStatement = statement[2] 16 | elsif (o.keyword == 'ParameterType') 17 | o.value = find(statement, :label, 'regexp:').parent.children[1].source.gsub(/(^\(?\/|\/\)?$)/, '').gsub(/(^\^|\$$)/, '') 18 | nextStatement = find(statement, :label, 'transformer:').parent.children[1] 19 | end 20 | end 21 | 22 | obj = register instance 23 | parse_block(nextStatement,:owner => obj) 24 | 25 | end 26 | 27 | def step_transform_namespace 28 | YARD::CodeObjects::Cucumber::CUCUMBER_STEPTRANSFORM_NAMESPACE 29 | end 30 | 31 | def step_transformer_name 32 | # If the owner is a constant then we get the name of the constant so that the reference from the constant will work 33 | if (owner.is_a?(YARD::CodeObjects::ConstantObject)) 34 | owner.name 35 | else 36 | "step_transform#{self.class.generate_unique_id}" 37 | end 38 | end 39 | 40 | def self.generate_unique_id 41 | @step_transformer_count = @step_transformer_count.to_i + 1 42 | end 43 | 44 | private 45 | 46 | def find(node, node_type, value) 47 | node.traverse { |child| return(child) if node_type == child.type && child.source == value } 48 | self 49 | end 50 | end -------------------------------------------------------------------------------- /lib/yard/code_objects/cucumber/scenario_outline.rb: -------------------------------------------------------------------------------- 1 | module YARD::CodeObjects::Cucumber 2 | class ScenarioOutline < NamespaceObject 3 | 4 | attr_accessor :value, :comments, :keyword, :description, :steps, :tags, :feature 5 | attr_accessor :scenarios, :examples 6 | 7 | def initialize(namespace, name) 8 | super(namespace, name.to_s.strip) 9 | @comments = @description = @value = @feature = nil 10 | @steps = [] 11 | @tags = [] 12 | @scenarios = [] 13 | @examples = [] 14 | end 15 | 16 | def background? 17 | false 18 | end 19 | 20 | def outline? 21 | true 22 | end 23 | 24 | def examples? 25 | @examples.find { |example| example.rows } 26 | end 27 | 28 | class Examples 29 | 30 | attr_accessor :name, :line, :keyword, :comments, :rows, :tags, :scenario 31 | 32 | # The first row of the rows contains the headers for the table 33 | def headers 34 | rows.first 35 | end 36 | 37 | # The data of the table starts at the second row. When there is no data then 38 | # return a empty string. 39 | def data 40 | rows ? rows[1..-1] : "" 41 | end 42 | 43 | def values_for_row(row) 44 | hash = {} 45 | 46 | headers.each_with_index do |header, index| 47 | hash[header] = data[row][index] 48 | end 49 | 50 | hash 51 | end 52 | 53 | def to_hash 54 | hash = {} 55 | 56 | rows.each_with_index do |header, index| 57 | hash[header] = rows.collect { |row| row[index] } 58 | end 59 | 60 | hash 61 | end 62 | 63 | def initialize(parameters = {}) 64 | parameters.each { |key, value| send("#{key.to_sym}=", value) if respond_to? "#{key.to_sym}=" } 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/templates/default/requirements/html/requirements.erb: -------------------------------------------------------------------------------- 1 | <% if @namespace %> 2 | 3 | 9 | 10 |
    11 | 12 | <% if features && !features.empty? %> 13 | <%= alpha_table(features) %> 14 | <% else %> 15 | 16 |
    No Features Defined
    17 | <% end %> 18 | 19 |
    20 |
    Tags
    21 |
    22 |
    23 | <%= tags.collect {|tag| linkify(tag,tag.value) }.join(",\n") %> 24 |
    25 | 26 | <% if feature_subdirectories && !feature_subdirectories.empty? %> 27 |
    28 |
    Subdirectories
    29 |
    30 | <%= alpha_table(feature_subdirectories) %> 31 | <% else %> 32 | 33 | <% end %> 34 | 35 |
    36 |
    Step Definitions & Transforms
    37 |
    38 |
    39 | <%= step_definitions.length %> 40 | ">Step Definitions 41 | <%= transformers.length %> 42 | ">Transforms 43 | and <%= undefined_steps.length %> 44 | ">Undefined Steps 45 |
    46 | 47 |
    48 | <% end %> 49 | 50 | 51 | -------------------------------------------------------------------------------- /lib/yard/handlers/legacy/step_definition_handler.rb: -------------------------------------------------------------------------------- 1 | class YARD::Handlers::Ruby::Legacy::StepDefinitionHandler < YARD::Handlers::Ruby::Legacy::Base 2 | STEP_DEFINITION_MATCH = /^((When|Given|And|Then)\s*(\/.+\/)\s+do(?:\s*\|.+\|)?\s*)$/ unless defined?(STEP_DEFINITION_MATCH) 3 | handles STEP_DEFINITION_MATCH 4 | 5 | @@unique_name = 0 6 | 7 | def process 8 | keyword = statement.tokens.to_s[STEP_DEFINITION_MATCH,2] 9 | step_definition = statement.tokens.to_s[STEP_DEFINITION_MATCH,3] 10 | 11 | @@unique_name = @@unique_name + 1 12 | 13 | stepdef_instance = StepDefinitionObject.new(YARD::CodeObjects::Cucumber::CUCUMBER_STEPTRANSFORM_NAMESPACE, "definition_#{@@unique_name}") do |o| 14 | o.source = "#{keyword} #{step_definition} do #{statement.block.to_s =~ /^\s*\|.+/ ? '' : "\n "}#{statement.block.to_s}\nend" 15 | o.value = step_definition 16 | o.keyword = keyword 17 | end 18 | 19 | obj = register stepdef_instance 20 | parse_block :owner => obj 21 | 22 | rescue YARD::Handlers::NamespaceMissingError 23 | end 24 | 25 | # 26 | # Step Definitions can contain defined steps within them. While it is likely that they could not 27 | # very easily be parsed because of variables that are only calculated at runtime, it would be nice 28 | # to at least list those in use within a step definition and if you can find a match, go ahead and 29 | # make it 30 | # 31 | def find_steps_defined_in_block(block) 32 | #log.debug "#{block} #{block.class}" 33 | block.each_with_index do |token,index| 34 | #log.debug "Token #{token.class} #{token.text}" 35 | if token.is_a?(YARD::Parser::Ruby::Legacy::RubyToken::TkCONSTANT) && 36 | token.text =~ /^(given|when|then|and)$/i && 37 | block[index + 2].is_a?(YARD::Parser::Ruby::Legacy::RubyToken::TkSTRING) 38 | log.debug "Step found in Step Definition: #{block[index + 2].text} " 39 | end 40 | 41 | end 42 | 43 | end 44 | 45 | end 46 | -------------------------------------------------------------------------------- /lib/docserver/doc_server/full_list/html/full_list.erb: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 |
    22 |

    <%= @list_title %>

    23 | 30 | 31 | 32 | 33 |
      34 | <%= erb "full_list_#{@list_type}" %> 35 |
    36 |
    37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /example/scenario.feature: -------------------------------------------------------------------------------- 1 | # Comments that appear before the feature are associated with the feature 2 | @scenarios 3 | Feature: Displaying Scenarios 4 | As a reader of the documentation I expect that scenario are documented correctly 5 | 6 | # Comments after the feature description belong to the background or first scenario 7 | Background: 8 | Given this background step 9 | 10 | @first @bvt 11 | Scenario: No Step Scenario 12 | 13 | @second @bvt 14 | Scenario: Scenario With Steps 15 | Given this first step 16 | When this second step 17 | Then this third step 18 | 19 | @third @optional_parameters 20 | Scenario: Optional Parameter Step Definition 21 | # This step definition has some optional parameters 22 | Given a project 23 | And an inactive project 24 | And a project with the name 'optional', start date 10/26/2010, nicknamed 'norman' 25 | 26 | @fourth @highlight 27 | Scenario: Matched Term Highlighting 28 | Given a duck that has a bill 29 | Then I expect the duck to quack 30 | 31 | @fifth @table 32 | Scenario: Scenario With Table 33 | Given the following table: 34 | | column 1 | column 2 | column 3 | 35 | | value 1 | value 2 | value 3 | 36 | 37 | @sixth @text 38 | Scenario: Scenario With Text 39 | Given the following text: 40 | """ 41 | Oh what a bother! 42 | That this text has to take up two lines 43 | This line should be indented 2 spaces 44 | This line should be indented 4 spaces 45 | """ 46 | 47 | # Comments before the scenario 48 | @seventh @comments 49 | Scenario: Scenario with comments and a description 50 | There once was a need for information to be displayed alongside all the 51 | entities that I hoped to test 52 | # First Comment 53 | Given this first step 54 | # Second Comment that 55 | # spans a few lines 56 | And this second step 57 | # Third Comment 58 | And this third step 59 | # Comments after the last step, where do they go? 60 | 61 | Scenario: Step ending with a match with double-quotes 62 | When searching the log for the exact match of the message "Entering application." 63 | When the step definition has HTML escaped characters like: "<>&" -------------------------------------------------------------------------------- /lib/templates/default/feature/html/outline.erb: -------------------------------------------------------------------------------- 1 | <% sc = 0 %> 2 | <% @scenario.examples.each_with_index do |example, example_index| %> 3 | <% example.data.each_with_index do |row, row_index| %> 4 | 11 | <% end %> 12 | <% end %> 13 | 14 | 15 |
    16 | 17 | <% if @scenario.examples? %> 18 | 19 | <% @scenario.examples.each_with_index do |example,example_index| %> 20 |
    21 | <%= h example.keyword %><%= example.name != "" ? ':' : '' %> 22 |
    23 |
    <%= h example.name %>
    24 |
    25 | 26 | 27 | 28 | <% example.headers.each_with_index do |header,header_index| %> 29 | 30 | <% end %> 31 | 32 | 33 | <% unless example.data.empty? %> 34 | <% example.data.each_with_index do |row,row_index| %> 35 | "> 36 | <% row.each_with_index do |column,column_index| %> 37 | 38 | <% end %> 39 | 40 | <% end %> 41 | <% else %> 42 | 43 | 44 | 47 | 48 | <% end %> 49 |
    <%= h(header) %>
    <%= h(column.to_s.strip) %>
    45 | No Examples Defined 46 |
    50 | <% end %> 51 | 52 | <% else %> 53 |
    No Example Table Defined
    54 |
    [!] Did you mean to create a Scenario?
    55 | <% end %> 56 |
    57 | -------------------------------------------------------------------------------- /lib/docserver/default/fulldoc/html/js/cucumber.js: -------------------------------------------------------------------------------- 1 | function cucumberSearchFrameLinks() { 2 | $('#feature_list_link').click(function() { 3 | toggleSearchFrame(this, '/' + library + '/features'); 4 | }); 5 | $('#tag_list_link').click(function() { 6 | toggleSearchFrame(this, '/' + library + '/tags'); 7 | }); 8 | } 9 | 10 | $(cucumberSearchFrameLinks); 11 | 12 | 13 | 14 | function toggleScenarioExample(id,example) { 15 | 16 | var element = $("#" + id + "Example" + example + "Steps")[0]; 17 | 18 | $('#' + id + ' tr').each(function(index) { 19 | this.style.backgroundColor = (index % 2 == 0 ? '#FFFFFF' : '#F0F6F9' ); 20 | }); 21 | 22 | if (element.style.display != 'none') { 23 | element = $("#" + id + "Steps")[0]; 24 | } else { 25 | $('#' + id + ' .outline * tr')[example].style.backgroundColor = '#FFCC80'; 26 | } 27 | 28 | $('#' + id + ' .steps').each(function(index) { 29 | this.style.display = 'none'; 30 | }); 31 | 32 | element.style.display = 'block'; 33 | 34 | } 35 | 36 | function determine_tags_used_in_formula(tag_string) { 37 | //$("#tag_debug")[0].innerHTML = ""; 38 | 39 | tag_string = tag_string.replace(/^(\s+)|(\s+)$/,'').replace(/\s{2,}/,' '); 40 | 41 | var tags = tag_string.match(/@\w+/g); 42 | 43 | var return_tags = []; 44 | 45 | if (tags != null) { 46 | tags.forEach(function(tag, index, array) { 47 | //$("#tag_debug")[0].innerHTML += tag + " "; 48 | if (tag_list.indexOf(tag) != -1) { return_tags.push(tag); } 49 | }); 50 | } 51 | 52 | return return_tags; 53 | } 54 | 55 | 56 | function display_example_command_line(tags) { 57 | $("#command_example")[0].innerHTML = "cucumber "; 58 | 59 | if (tags.length > 0) { 60 | $("#command_example")[0].innerHTML += "--tags " + tags.join(" --tags "); 61 | } 62 | } 63 | 64 | function display_qualifying_features_and_scenarios(tags) { 65 | //$("#tag_debug")[0].innerHTML = ""; 66 | 67 | if (tags.length > 0) { 68 | 69 | $(".feature,.scenario").each(function(feature){ 70 | this.style.display = "none"; 71 | }); 72 | 73 | $(".feature.\\" + tags.join(".\\") + ",.scenario.\\" + tags.join(".\\")).each(function(feature) { 74 | //$("#tag_debug")[0].innerHTML += feature + " " + this; 75 | this.style.display = "block"; 76 | }); 77 | 78 | 79 | } else { 80 | $(".feature,.scenario").each(function(feature){ 81 | this.style.display = "block"; 82 | }); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /lib/yard/parser/cucumber/feature.rb: -------------------------------------------------------------------------------- 1 | module YARD::Parser::Cucumber 2 | 3 | class FeatureParser < YARD::Parser::Base 4 | 5 | # 6 | # Each found feature found is creates a new FeatureParser 7 | # 8 | # This logic was copied from the logic found in Cucumber to create the builder 9 | # and then set up the formatter and parser. The difference is really the 10 | # custom Cucumber::Parser::CityBuilder that is being used to parse the elements 11 | # of the feature into YARD::CodeObjects. 12 | # 13 | # @param [] source containing the string conents of the feauture file 14 | # @param [] file the filename that contains the source 15 | # 16 | def initialize(source, file = '(stdin)') 17 | 18 | @builder = Cucumber::Parser::CityBuilder.new(file) 19 | @tag_counts = {} 20 | @parser = Gherkin::Parser.new(@builder) 21 | 22 | @source = source 23 | @file = file 24 | 25 | @feature = nil 26 | end 27 | 28 | # 29 | # When parse is called, the gherkin parser is executed and all the feature 30 | # elements that are found are sent to the various methods in the 31 | # Cucumber::Parser::CityBuilder. The result of which is the feature element 32 | # that contains all the scenarios, steps, etc. associated with that feature. 33 | # 34 | # @see Cucumber::Parser::CityBuilder 35 | def parse 36 | begin 37 | @parser.parse(@source) 38 | @feature = @builder.ast 39 | return nil if @feature.nil? # Nothing matched 40 | 41 | # The parser used the following keywords when parsing the feature 42 | # @feature.language = @parser.i18n_language.get_code_keywords.map {|word| word } 43 | 44 | rescue Gherkin::ParserError => e 45 | e.message.insert(0, "#{@file}: ") 46 | warn e 47 | end 48 | 49 | self 50 | end 51 | 52 | # 53 | # This is not used as all the work is done in the parse method 54 | # 55 | def tokenize 56 | 57 | end 58 | 59 | # 60 | # The only enumeration that can be done here is returning the feature itself 61 | # 62 | def enumerator 63 | [@feature] 64 | end 65 | 66 | end 67 | 68 | # 69 | # Register all feature files (.feature) to be processed with the above FeatureParser 70 | YARD::Parser::SourceParser.register_parser_type :feature, FeatureParser, 'feature' 71 | 72 | end -------------------------------------------------------------------------------- /lib/yard-cucumber.rb: -------------------------------------------------------------------------------- 1 | require 'yard' 2 | require 'cucumber/platform' 3 | require 'gherkin/parser' 4 | 5 | require File.dirname(__FILE__) + "/yard-cucumber/version.rb" 6 | 7 | require File.dirname(__FILE__) + "/yard/code_objects/cucumber/base.rb" 8 | require File.dirname(__FILE__) + "/yard/code_objects/cucumber/namespace_object.rb" 9 | require File.dirname(__FILE__) + "/yard/code_objects/cucumber/feature.rb" 10 | require File.dirname(__FILE__) + "/yard/code_objects/cucumber/scenario_outline.rb" 11 | require File.dirname(__FILE__) + "/yard/code_objects/cucumber/scenario.rb" 12 | require File.dirname(__FILE__) + "/yard/code_objects/cucumber/step.rb" 13 | require File.dirname(__FILE__) + "/yard/code_objects/cucumber/tag.rb" 14 | 15 | require File.dirname(__FILE__) + "/cucumber/city_builder.rb" 16 | 17 | require File.dirname(__FILE__) + "/yard/code_objects/step_transformer.rb" 18 | require File.dirname(__FILE__) + "/yard/code_objects/step_definition.rb" 19 | require File.dirname(__FILE__) + "/yard/code_objects/step_transform.rb" 20 | 21 | require File.dirname(__FILE__) + "/yard/parser/cucumber/feature.rb" 22 | 23 | require File.dirname(__FILE__) + "/yard/handlers/cucumber/base.rb" 24 | require File.dirname(__FILE__) + "/yard/handlers/cucumber/feature_handler.rb" 25 | 26 | if RUBY19 27 | require File.dirname(__FILE__) + "/yard/handlers/step_definition_handler.rb" 28 | require File.dirname(__FILE__) + "/yard/handlers/step_transform_handler.rb" 29 | require File.dirname(__FILE__) + "/yard/handlers/constant_transform_handler.rb" 30 | end 31 | 32 | require File.dirname(__FILE__) + "/yard/handlers/legacy/step_definition_handler.rb" 33 | require File.dirname(__FILE__) + "/yard/handlers/legacy/step_transform_handler.rb" 34 | 35 | #require File.dirname(__FILE__) + "/yard/parser/source_parser.rb" 36 | require File.dirname(__FILE__) + "/yard/templates/helpers/base_helper.rb" 37 | 38 | require File.dirname(__FILE__) + "/yard/server/adapter.rb" 39 | require File.dirname(__FILE__) + "/yard/server/commands/list_command.rb" 40 | require File.dirname(__FILE__) + "/yard/server/router.rb" 41 | 42 | 43 | # This registered template works for yardoc 44 | YARD::Templates::Engine.register_template_path File.dirname(__FILE__) + '/templates' 45 | 46 | # The following static paths and templates are for yard server 47 | YARD::Server.register_static_path File.dirname(__FILE__) + "/templates/default/fulldoc/html" 48 | YARD::Server.register_static_path File.dirname(__FILE__) + "/docserver/default/fulldoc/html" 49 | -------------------------------------------------------------------------------- /yard-cucumber.gemspec: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + "/lib/yard-cucumber/version" 2 | 3 | module CucumberInTheYARD 4 | def self.show_version_changes(version) 5 | date = "" 6 | changes = [] 7 | grab_changes = false 8 | 9 | File.open("#{File.dirname(__FILE__)}/History.txt",'r') do |file| 10 | while (line = file.gets) do 11 | 12 | if line =~ /^===\s*#{version.gsub('.','\.')}\s*\/\s*(.+)\s*$/ 13 | grab_changes = true 14 | date = $1.strip 15 | elsif line =~ /^===\s*.+$/ 16 | grab_changes = false 17 | elsif grab_changes 18 | changes = changes << line 19 | end 20 | 21 | end 22 | end 23 | 24 | { :date => date, :changes => changes } 25 | end 26 | end 27 | 28 | Gem::Specification.new do |s| 29 | s.name = 'yard-cucumber' 30 | s.version = ::CucumberInTheYARD::VERSION 31 | s.authors = ["Franklin Webber"] 32 | s.description = %{ 33 | YARD-Cucumber is a YARD extension that processes Cucumber Features, Scenarios, Steps, 34 | Step Definitions, Transforms, and Tags and provides a documentation interface that allows you 35 | easily view and investigate the test suite. This tools hopes to bridge the gap of being able 36 | to provide your feature descriptions to your Product Owners and Stakeholders. } 37 | s.summary = "Cucumber Features in YARD" 38 | s.email = 'franklin.webber@gmail.com' 39 | s.homepage = "http://github.com/burtlo/yard-cucumber" 40 | s.license = 'MIT' 41 | 42 | s.platform = Gem::Platform::RUBY 43 | 44 | changes = CucumberInTheYARD.show_version_changes(::CucumberInTheYARD::VERSION) 45 | 46 | s.post_install_message = %{ 47 | (::) (::) (::) (::) (::) (::) (::) (::) (::) (::) (::) (::) (::) (::) (::) 48 | 49 | Thank you for installing yard-cucumber #{::CucumberInTheYARD::VERSION} / #{changes[:date]}. 50 | 51 | Changes: 52 | #{changes[:changes].collect{|change| " #{change}"}.join("")} 53 | (::) (::) (::) (::) (::) (::) (::) (::) (::) (::) (::) (::) (::) (::) (::) 54 | 55 | } 56 | 57 | s.add_development_dependency 'rake', '~> 10' 58 | 59 | s.add_dependency 'gherkin', '>= 4.0', '< 6.0' 60 | s.add_dependency 'cucumber', '>= 2.0', '< 4.0' 61 | s.add_dependency 'yard', '~> 0.8', '>= 0.8.1' 62 | 63 | s.rubygems_version = "1.3.7" 64 | s.files = `git ls-files`.split("\n") 65 | s.extra_rdoc_files = ["README.md", "History.txt"] 66 | s.rdoc_options = ["--charset=UTF-8"] 67 | s.require_path = "lib" 68 | end 69 | -------------------------------------------------------------------------------- /lib/yard/handlers/step_definition_handler.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Finds and processes all the step definitions defined in the ruby source 3 | # code. By default the english gherkin language will be parsed. 4 | # 5 | # To override the language you can define the step definitions in the YARD 6 | # configuration file `~./yard/config`: 7 | # 8 | # @example `~/.yard/config` with LOLCatz step definitions 9 | # 10 | # :"yard-cucumber": 11 | # language: 12 | # step_definitions: [ 'WEN', 'I CAN HAZ', 'AN', 'DEN' ] 13 | # 14 | # @example `~/.yard/config` with French step definitions 15 | # 16 | # :"yard-cucumber": 17 | # language: 18 | # step_definitions: [ 'Soit', 'Etantdonné', 'Lorsque', 'Lorsqu', 'Alors', 'Et' ] 19 | # 20 | class YARD::Handlers::Ruby::StepDefinitionHandler < YARD::Handlers::Ruby::Base 21 | 22 | def self.default_step_definitions 23 | [ "When", "Given", "And", "Then" ] 24 | end 25 | 26 | def self.custom_step_definitions 27 | YARD::Config.options["yard-cucumber"]["language"]["step_definitions"] 28 | end 29 | 30 | def self.custom_step_definitions_defined? 31 | YARD::Config.options["yard-cucumber"] and 32 | YARD::Config.options["yard-cucumber"]["language"] and 33 | YARD::Config.options["yard-cucumber"]["language"]["step_definitions"] 34 | end 35 | 36 | def self.step_definitions 37 | if custom_step_definitions_defined? 38 | custom_step_definitions 39 | else 40 | default_step_definitions 41 | end 42 | end 43 | 44 | step_definitions.each { |step_def| handles method_call(step_def) } 45 | 46 | process do 47 | 48 | instance = YARD::CodeObjects::StepDefinitionObject.new(step_transform_namespace,step_definition_name) do |o| 49 | o.source = statement.source 50 | o.comments = statement.comments 51 | o.keyword = statement.method_name.source 52 | o.value = statement.parameters.source 53 | o.pending = pending_keyword_used?(statement.block) 54 | end 55 | 56 | obj = register instance 57 | parse_block(statement[2],:owner => obj) 58 | 59 | end 60 | 61 | def pending_keyword 62 | "pending" 63 | end 64 | 65 | def pending_command_statement?(line) 66 | (line.type == :command || line.type == :vcall) && line.first.source == pending_keyword 67 | end 68 | 69 | def pending_keyword_used?(block) 70 | code_in_block = block.last 71 | code_in_block.find { |line| pending_command_statement?(line) } 72 | end 73 | 74 | def step_transform_namespace 75 | YARD::CodeObjects::Cucumber::CUCUMBER_STEPTRANSFORM_NAMESPACE 76 | end 77 | 78 | def step_definition_name 79 | "step_definition#{self.class.generate_unique_id}" 80 | end 81 | 82 | def self.generate_unique_id 83 | @step_definition_count = @step_definition_count.to_i + 1 84 | end 85 | 86 | end 87 | -------------------------------------------------------------------------------- /lib/templates/default/steptransformers/html/setup.rb: -------------------------------------------------------------------------------- 1 | def init 2 | super 3 | sections.push :index, :stepdefinitions, :steptransforms, :undefinedsteps 4 | end 5 | 6 | def step_definitions 7 | @step_definitions ||= begin 8 | YARD::Registry.all(:stepdefinition).sort_by {|definition| definition.steps.length * -1 } 9 | end 10 | end 11 | 12 | def step_transforms 13 | @step_transforms ||= begin 14 | YARD::Registry.all(:steptransform).sort_by {|definition| definition.steps.length * -1 } 15 | end 16 | end 17 | 18 | def undefined_steps 19 | @undefined_steps ||= begin 20 | unique_steps(Registry.all(:step).reject {|s| s.definition || s.scenario.outline? }).sort_by{|steps| steps.last.length * -1 } 21 | end 22 | end 23 | 24 | def stepdefinitions 25 | @item_title = "Step Definitions" 26 | @item_anchor_name = "step_definitions" 27 | @item_type = "step definition" 28 | @items = step_definitions 29 | erb(:header) + erb(:transformers) 30 | end 31 | 32 | def steptransforms 33 | @item_title = "Step Transforms" 34 | @item_anchor_name = "step_transforms" 35 | @item_type = "step transform" 36 | @items = step_transforms 37 | erb(:header) + erb(:transformers) 38 | end 39 | 40 | def undefinedsteps 41 | @item_title = "Undefined Steps" 42 | @item_anchor_name = "undefined_steps" 43 | @item_type = nil 44 | @items = undefined_steps 45 | erb(:header) + erb(:undefinedsteps) 46 | end 47 | 48 | 49 | def unique_steps(steps) 50 | uniq_steps = {} 51 | steps.each {|s| (uniq_steps[s.value.to_s] ||= []) << s } 52 | uniq_steps 53 | end 54 | 55 | def display_comments_for(item) 56 | begin 57 | T('docstring').run(options.dup.merge({:object => item})) 58 | rescue 59 | log.warn %{An error occurred while attempting to render the comments for: #{item.location} } 60 | return "" 61 | end 62 | 63 | end 64 | 65 | def link_constants(definition) 66 | value = definition.literal_value.dup 67 | 68 | definition.constants_from_value(value).each do |name| 69 | constant = YARD::Registry.all(:constant).find{|c| c.name == name.to_sym } 70 | value.gsub!(/\b#{name}\b/,"#{name}") if constant 71 | end 72 | 73 | value 74 | end 75 | 76 | 77 | def link_transformed_step(step) 78 | value = step.value.dup 79 | 80 | if step.definition 81 | matches = step.value.match(step.definition.regex) 82 | 83 | if matches 84 | matches[1..-1].reverse.each_with_index do |match,index| 85 | next if match == nil 86 | transform = step.transforms.find {|transform| transform.regex.match(match) } 87 | 88 | value[matches.begin((matches.size - 1) - index)..(matches.end((matches.size - 1) - index) - 1)] = transform ? "#{h(match)}" : "#{match}" 89 | end 90 | end 91 | end 92 | 93 | value 94 | end -------------------------------------------------------------------------------- /lib/templates/default/steptransformers/html/transformers.erb: -------------------------------------------------------------------------------- 1 | <% @items.each do |item| %> 2 | 3 |
    5 | <%= "pending" if item.pending %>"> 6 | 7 |
    8 | <%= h item.keyword %> 9 | <%= item.value %> 10 |
    11 | Rubular 12 | <%= "| PENDING" if item.pending %> 13 | <%= "| UNUSED" if item.steps.nil? || item.steps.empty? %> 14 | [Collapse] 15 |
    16 |
    17 |
    18 | 19 |
    20 | 21 |
    22 |
    <%= h item.location %>
    23 |
    24 | 25 | 26 |
    27 | <%= display_comments_for item %> 28 |
    29 | 30 | 31 | 32 |
    33 | 34 | 35 | 38 | 41 | 42 |
    36 |
    <%= "" %><%= h format_lines(item) %>
    37 |
    39 |
    <%= "" %><%= html_syntax_highlight item.source %>
    40 |
    43 |
    44 | 45 | 46 | <% if item.steps.length > 0 %> 47 | 50 | <% end %> 51 |
    52 | <% if item.steps && !item.steps.empty? %> 53 | <% unique_steps(item.steps).each_with_index do |uniq_step,step_index| %> 54 |
    55 |
    56 | 57 | <%= h uniq_step.last.first.keyword %> 58 | 59 | 60 | <%= link_transformed_step(uniq_step.last.first) %> 61 | 62 |
    63 | <% uniq_step.last.each do |step| %> 64 | <%= h step.location %> 65 | <% end %> 66 |
    67 |
    68 | <% end %> 69 | <% else %> 70 |
    No steps were found to match this <%= @item_type %>.
    71 | <% end%> 72 |
    73 | 74 |
    75 | 76 |
    77 | 78 |
    79 | 80 | <% end %> -------------------------------------------------------------------------------- /lib/yard/code_objects/step_transformer.rb: -------------------------------------------------------------------------------- 1 | module YARD::CodeObjects 2 | class StepTransformerObject < Base 3 | 4 | include Cucumber::LocationHelper 5 | 6 | attr_reader :constants, :keyword, :source, :value, :literal_value 7 | attr_accessor :steps, :pending, :substeps 8 | 9 | # This defines an escape pattern within a string or regex: 10 | # /^the first #{CONSTANT} step$/ 11 | # 12 | # This is used below in the value to process it if there happen to be 13 | # constants defined here. 14 | # 15 | # @note this does not handle the result of method calls 16 | # @note this does not handle multiple constants within the same escaped area 17 | # 18 | def escape_pattern 19 | /#\{\s*(\w+)\s*\}/ 20 | end 21 | 22 | # 23 | # When requesting a step tranformer object value, process it, if it hasn't 24 | # alredy been processed, replacing any constants that may be lurking within 25 | # the value. 26 | # 27 | # Processing it means looking for any escaped characters that happen to be 28 | # CONSTANTS that could be matched and then replaced. This is done recursively 29 | # as CONSTANTS can be defined with more CONSTANTS. 30 | # 31 | def value 32 | unless @processed 33 | @processed = true 34 | until (nested = constants_from_value).empty? 35 | nested.each {|n| @value.gsub!(value_regex(n),find_value_for_constant(n)) } 36 | end 37 | end 38 | 39 | @value 40 | end 41 | 42 | # 43 | # Set the literal value and the value of the step definition. 44 | # 45 | # The literal value is as it appears in the step definition file with any 46 | # constants. The value, when retrieved will attempt to replace those 47 | # constants with their regex or string equivalents to hopefully match more 48 | # steps and step definitions. 49 | # 50 | # 51 | def value=(value) 52 | @literal_value = format_source(value) 53 | @value = format_source(value) 54 | 55 | @steps = [] 56 | value 57 | end 58 | 59 | # Generate a regex with the step transformers value 60 | def regex 61 | @regex ||= /#{strip_regex_from(value)}/ 62 | end 63 | 64 | # Look through the specified data for the escape pattern and return an array 65 | # of those constants found. This defaults to the @value within step transformer 66 | # as it is used internally, however, it can be called externally if it's 67 | # needed somewhere. 68 | def constants_from_value(data=@value) 69 | data.scan(escape_pattern).flatten.collect { |value| value.strip } 70 | end 71 | 72 | protected 73 | 74 | # 75 | # Looking through all the constants in the registry and returning the value 76 | # with the regex items replaced from the constnat if present 77 | # 78 | def find_value_for_constant(name) 79 | constant = YARD::Registry.all(:constant).find{|c| c.name == name.to_sym } 80 | log.warn "StepTransformer#find_value_for_constant : Could not find the CONSTANT [#{name}] using the string value." unless constant 81 | constant ? strip_regex_from(constant.value) : name 82 | end 83 | 84 | # Return a regex of the value 85 | def value_regex(value) 86 | /#\{\s*#{value}\s*\}/ 87 | end 88 | 89 | # Step the regex starting / and ending / from the value 90 | def strip_regex_from(value) 91 | value.gsub(/^\/|\/$/,'') 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /example/scenario_outline.feature: -------------------------------------------------------------------------------- 1 | @scenario_outlines @bvt @duplicate 2 | Feature: Displaying Scenario Outlines 3 | As a reader of the documentation I expect that scenario outlines are documented correctly 4 | 5 | @first 6 | Scenario Outline: Three Examples 7 | Given that is a valid customer 8 | And that the product, named '', is a valid product 9 | When the customer has purchased the product 10 | Then I expect the customer to be a member of the '' group 11 | 12 | @tagged_examples 13 | Examples: 14 | | Customer | Product | 15 | | Customer A | Product A | 16 | | Customer A | Product B | 17 | | Customer A | Product C | 18 | 19 | @second 20 | Scenario Outline: Step contains a text block 21 | Given the following text: 22 | """ 23 | The jumped over the 24 | """ 25 | When I click on an example row 26 | Then I expect to be replaced by the example noun 27 | And I expect to be replaced by the example place 28 | 29 | Examples: 30 | | noun | place | 31 | | cow | moon | 32 | | horse | spoon | 33 | 34 | @third 35 | Scenario Outline: Step contains a table 36 | Given the following table: 37 | | name | price | quantity | 38 | | | | 100000 | 39 | When I click on an example row 40 | Then I expect to be replaced by the example name 41 | And I expect to be replaced by the example price 42 | 43 | Examples: 44 | | name | price | 45 | | toy | $99 | 46 | | game | $49 | 47 | 48 | @fourth 49 | Scenario Outline: Step contains a table; table header uses an example 50 | Given the following table: 51 | | name | | quantity | 52 | | | | 100000 | 53 | When I click on an example row 54 | Then I expect to be replaced by the example name 55 | And I expect to be replaced by the example price 56 | And I expect to be replaced by the example denomination 57 | 58 | Examples: 59 | | name | price | denomination | 60 | | toy | 99 | cost in euros | 61 | | game | 49 | cost in dollars | 62 | 63 | # This is an example of a scenario outline in development. 64 | # The example table has not been defined yet 65 | @fifth 66 | Scenario Outline: Example Table Missing 67 | When I click on an example row 68 | Then I expect to be replaced by the example name 69 | And I expect to be replaced by the example price 70 | And I expect to be replaced by the example denomination 71 | 72 | 73 | # This is an example of a scenario outline in development. 74 | # The examples table has been defined, but is missing data. 75 | @sixth 76 | Scenario Outline: Empty Example Table 77 | When I click on an example row 78 | Then I expect to be replaced by the example name 79 | And I expect to be replaced by the example price 80 | And I expect to be replaced by the example denomination 81 | 82 | Examples: 83 | | name | price | denomination | 84 | 85 | @seventh @duplicate 86 | Scenario Outline: Multiple Example Table 87 | Given that is a valid customer 88 | And that the product, named '', is a valid product 89 | When the customer has purchased the product 90 | Then I expect the customer to be a member of the '' group 91 | 92 | @groupA @duplicate 93 | Examples: Example group A 94 | | Customer | Product | 95 | | Customer A | Product A | 96 | 97 | @groupB 98 | Examples: Example group B 99 | | Customer | Product | 100 | | Customer B | Product A | 101 | -------------------------------------------------------------------------------- /example/step_definitions/example.step.rb: -------------------------------------------------------------------------------- 1 | 2 | Transform /^#{CUSTOMER}$/ do |customer| 3 | "a transformed customer" 4 | end 5 | 6 | Transform /^an? customer$/ do |customer| 7 | "a transformed customer" 8 | end 9 | 10 | Transform /^the customer$/ do |customer| 11 | "the transformed customer" 12 | end 13 | 14 | Transform /^(#{ORDER}) customer$/ do |order| 15 | "#{order} customer" 16 | end 17 | 18 | Transform /^#{TEDDY_BEAR}$/ do |teddy| 19 | "the tranformed teddy bear" 20 | end 21 | 22 | # 23 | # This is a complicated Transform with a comment 24 | # 25 | Transform /^((?:\d{1,2}[\/-]){2}(?:\d\d){1,2})?\s*(\w{3})?\s*(\d{1,2}:\d{2}\s*(?:AM|PM)?)$/ do |date,day,time| 26 | "#{date} #{day} #{time}" 27 | end 28 | 29 | # 30 | # Assign a trnasform to a variable to be interpolated later in a step definition 31 | # 32 | SUBSTITUTED_FILE = Transform(/^file '([^']*)'$/) do |filepath| 33 | "Contents loaded from file" 34 | end 35 | 36 | Given /^that (#{CUSTOMER}) is a valid customer$/ do |customer| 37 | pending "Customer #{customer} validation" 38 | end 39 | 40 | # 41 | # This comment will likely blow {things up}! 42 | # 43 | When /^a customer logs in as username '([^']+)' with password '([^']+)'$/ do |username,password| 44 | Given "that the customer is a valid customer" 45 | pending "Customer logs in with #{username} and #{password}" 46 | end 47 | 48 | Then /^I expect them to have logged in (successfully|miserably)$/ do |success| 49 | pending "Validation that the customer has logged in #{success}" 50 | end 51 | 52 | When /^the customer logs out$/ do 53 | pending 54 | end 55 | 56 | Then /^I expect the customer to be shown the logout page$/ do 57 | pending 58 | end 59 | 60 | And /^this (third) defined step definition$/ do |number| 61 | pending 62 | end 63 | 64 | And /^the customer has the following details:$/ do |table| 65 | pending "Table of data #{table.hashes}" 66 | end 67 | 68 | And /^edits their the (biography) to state:$/ do |section,text| 69 | pending "text_field not present for #{section} #{bio} for this release" 70 | end 71 | 72 | Then /I expect (#{CUSTOMER}) to be a member of the '([^']+)' group/ do |customer,product| 73 | pending "Customer #{customer} with product #{product}" 74 | end 75 | 76 | # 77 | # Complicated step definition with optional parameters 78 | # 79 | Given /^(?:I create )?an? (?:(active|deactive|inactive|simulated)d?)? ?project(?:[\s,]*(?:with)? ?(?:an?|the)? (?:(?:the size? (?:at|of)? ?(\d+)|named? (?:of )?'([^']+)'|start date (?:of )?((?:(?:\d{1,2}[\/-]){2}(?:\d\d){1,2}))|end date (?:of )?((?:(?:\d{1,2}[\/-]){2}(?:\d\d){1,2}))|user range (?:of )?(\d+-\d+)|description (?:of )?'([^']+)'|nicknamed? (?:of )?'([^']+)')[,\s]*)* ?)?$/ do |state,size,name,start_date,end_date,user_range,description,nickname| 80 | pending "#{state} #{size} #{name} #{start_date} #{end_date} #{user_range} #{description} #{nickname}" 81 | end 82 | 83 | 84 | # 85 | # The highlighting replacement uses a span which had trouble when blindly using 86 | # a gsub replacement. 87 | # 88 | Given /(a|\d+) ducks? that ha(?:s|ve) (a|\d+) bills?/ do |duck_count,bills_count| 89 | pending 90 | end 91 | 92 | Then /I expect the (duck|bird) to (\w+)/ do |animal,verb| 93 | pending 94 | end 95 | 96 | # 97 | # This double-quoted step definition caused some problems when being rendered 98 | # 99 | When /^searching the log for the exact match of the message "([^"]+)"$/ do |message| 100 | pending message 101 | end 102 | 103 | # 104 | # 105 | # 106 | When /^the step definition has HTML escaped characters like: "([^"]+)"$/ do |characters| 107 | pending characters 108 | end 109 | 110 | # 111 | # Step using interpolated transform to replace file reference with contents of file 112 | # 113 | Then /^the (#{SUBSTITUTED_FILE}) will be replaced with the file contents$/ do |content| 114 | pending "File contained #{content}" 115 | end 116 | 117 | # 118 | # Some details about the helper method that might be picked up in the documentation. 119 | # 120 | def a_helper_method 121 | puts "performs some operation" 122 | end -------------------------------------------------------------------------------- /lib/yard/handlers/constant_transform_handler.rb: -------------------------------------------------------------------------------- 1 | # There might be a nicer way to decorate this class but with my limited knowledge could only get this handler 2 | # to be applied after the default constant handler by inheriting from the default constant handler. 3 | # This is required so that the value assigned from the transform is not overridden in the registry by the 4 | # default handler 5 | class YARD::Handlers::Ruby::ConstantTransformHandler < YARD::Handlers::Ruby::ConstantHandler 6 | include YARD::Handlers::Ruby::StructHandlerMethods 7 | handles :assign 8 | 9 | namespace_only 10 | 11 | process do 12 | begin 13 | if statement[1][0][0] == "Transform" 14 | name = statement[0][0][0] 15 | # Move the docstring to the transform statement 16 | statement[1].docstring = statement.docstring 17 | # Set the docstring on the constant to reference the transform that will be processed 18 | statement.docstring = "Reference to {#{YARD::CodeObjects::Cucumber::CUCUMBER_STEPTRANSFORM_NAMESPACE}::#{name} transform}" 19 | value = statement[1][1].source 20 | value = substitute(value) 21 | value = convert_captures(strip_anchors(value)) 22 | instance = register ConstantObject.new(namespace, name) {|o| o.source = statement; o.value = value } 23 | # specify the owner so that the transform can use the registered constant's name 24 | parse_block(statement[1], {:owner => instance}) 25 | elsif statement[1][0][0] == "ParameterType" 26 | name = statement[0][0][0] 27 | # Move the docstring to the transform statement 28 | statement[1].docstring = statement.docstring 29 | # Set the docstring on the constant to reference the transform that will be processed 30 | statement.docstring = "Reference to {#{YARD::CodeObjects::Cucumber::CUCUMBER_STEPTRANSFORM_NAMESPACE}::#{name} transform}" 31 | 32 | value = find(statement, :label, 'regexp:').parent.children[1].source 33 | 34 | value = substitute(value) 35 | value = convert_captures(strip_anchors(value)) 36 | instance = register ConstantObject.new(namespace, name) {|o| o.source = statement; o.value = value } 37 | # specify the owner so that the transform can use the registered constant's name 38 | parse_block(statement[1], {:owner => instance}) 39 | end 40 | rescue 41 | # This supresses any errors where any of the statement elements are out of bounds. 42 | # In this case the or in cases where the object being assigned is not a Transform 43 | # the default constant handler will already have performed the relevant action 44 | end 45 | end 46 | 47 | private 48 | 49 | def find(node, node_type, value) 50 | node.traverse { |child| return(child) if node_type == child.type && child.source == value } 51 | self 52 | end 53 | 54 | # Cucumber's Transform object overrides the to_s function and strips 55 | # the anchor tags ^$ and any captures so that id it is interpolated in a step definition 56 | # the it can appear anywhere in the step without being effected by position or captures 57 | def convert_captures(regexp_source) 58 | regexp_source 59 | .gsub(/(\()(?!\?[<:=!])/,'(?:') 60 | .gsub(/(\(\?<)(?![=!])/,'(?:<') 61 | end 62 | 63 | def strip_anchors(regexp_source) 64 | regexp_source. 65 | gsub(/(^\(\/|\/\)$)/, ''). 66 | gsub(/(^\^|\$$)/, '') 67 | end 68 | 69 | # Support for interpolation in the Transform's Regex 70 | def substitute(data) 71 | until (nested = constants_from_value(data)).empty? 72 | nested.each {|n| data.gsub!(value_regex(n),find_value_for_constant(n)) } 73 | end 74 | data 75 | end 76 | 77 | def constants_from_value(data) 78 | escape_pattern = /#\{\s*(\w+)\s*\}/ 79 | data.scan(escape_pattern).flatten.collect { |value| value.strip } 80 | end 81 | 82 | # Return a regex of the value 83 | def value_regex(value) 84 | /#\{\s*#{value}\s*\}/ 85 | end 86 | 87 | def find_value_for_constant(name) 88 | constant = YARD::Registry.all(:constant).find{|c| c.name == name.to_sym } 89 | log.warn "ConstantTransformHandler#find_value_for_constant : Could not find the CONSTANT [#{name}] using the string value." unless constant 90 | constant ? strip_regex_from(constant.value) : name 91 | end 92 | 93 | # Step the regex starting / and ending / from the value 94 | def strip_regex_from(value) 95 | value.gsub(/^\/|\/$/,'') 96 | end 97 | 98 | end 99 | -------------------------------------------------------------------------------- /lib/templates/default/layout/html/setup.rb: -------------------------------------------------------------------------------- 1 | def init 2 | super 3 | end 4 | 5 | # 6 | # Append yard-cucumber stylesheet to yard core stylesheets 7 | # 8 | def stylesheets 9 | super + %w(css/cucumber.css) 10 | end 11 | 12 | # 13 | # Append yard-cucumber javascript to yard core javascripts 14 | # 15 | def javascripts 16 | super + %w(js/cucumber.js) 17 | end 18 | 19 | # 20 | # Append yard-cucumber specific menus 'features' and 'tags' 21 | # 22 | # 'features' and 'tags' are enabled by default. 23 | # 24 | # 'step definitions' and 'steps' may be enabled by setting up a value in 25 | # yard configuration file '~/.yard/config' 26 | # 27 | # @example `~/.yard.config` 28 | # 29 | # yard-cucumber: 30 | # menus: [ 'features', 'directories', 'tags', 'step definitions', 'steps' ] 31 | # 32 | def menu_lists 33 | current_menu_lists.map {|menu_name| yard_cucumber_menus[menu_name] }.compact + super 34 | end 35 | 36 | # 37 | # By default we want to display the 'features' and 'tags' menu but we will allow 38 | # the YARD configuration to override that functionality. 39 | # 40 | def current_menu_lists 41 | @current_menu_lists ||= begin 42 | menus = [ "features", "tags" ] 43 | 44 | if YARD::Config.options["yard-cucumber"] and YARD::Config.options["yard-cucumber"]["menus"] 45 | menus = YARD::Config.options["yard-cucumber"]["menus"] 46 | end 47 | 48 | menus 49 | end 50 | end 51 | 52 | # 53 | # When a menu is specified in the yard configuration file, this hash contains 54 | # the details about the menu necessary for it to be displayed. 55 | # 56 | # @see #menu_lists 57 | # 58 | def yard_cucumber_menus 59 | { "features" => { :type => 'feature', :title => 'Features', :search_title => 'Features' }, 60 | "directories" => { :type => 'featuredirectories', :title => 'Features by Directory', :search_title => 'Features by Directory' }, 61 | "tags" => { :type => 'tag', :title => 'Tags', :search_title => 'Tags' }, 62 | "step definitions" => { :type => 'stepdefinition', :title => 'Step Definitions', :search_title => 'Step Defs' }, 63 | "steps" => { :type => 'step', :title => 'Steps', :search_title => 'Steps' } } 64 | end 65 | 66 | # 67 | # @note This method overrides YARD's default layout template's layout method. 68 | # 69 | # The existing YARD layout method generates the url for the nav menu on the left 70 | # side. For YARD-Cucumber objects this will default to the class_list.html. 71 | # which is not what we want for features, tags, etc. 72 | # 73 | # So we override this method and put in some additional logic to figure out the 74 | # correct list to appear in the search. This can be particularly tricky because 75 | # 76 | # This method removes the namespace from the root node, generates the class list, 77 | # and then adds it back into the root node. 78 | # 79 | def layout 80 | @nav_url = url_for_list(!@file || options.index ? 'class' : 'file') 81 | 82 | 83 | if is_yard_cucumber_object?(object) 84 | @nav_url = rewrite_nav_url(@nav_url) 85 | end 86 | 87 | if !object || object.is_a?(String) 88 | @path = nil 89 | elsif @file 90 | @path = @file.path 91 | elsif !object.is_a?(YARD::CodeObjects::NamespaceObject) 92 | @path = object.parent.path 93 | else 94 | @path = object.path 95 | end 96 | 97 | erb(:layout) 98 | end 99 | 100 | # 101 | # Determine if the object happens to be a CodeObject defined in this gem. 102 | # 103 | # @note quite a few of the classes/modules defined here are not object that we 104 | # would never want to display but it's alright if we match on them. 105 | # 106 | # @return [Boolean] true if the object's class name is one of the CodeObjects 107 | # 108 | def is_yard_cucumber_object?(object) 109 | YARD::CodeObjects::Cucumber.constants.any? {|constant| object.class.name == "YARD::CodeObjects::Cucumber::#{constant}" } 110 | end 111 | 112 | # 113 | # The re-write rules will only change the nav link to a new menu if it is a 114 | # a Cucumber CodeObject that we care about and that we have also generated a 115 | # menu for that item. 116 | # 117 | def rewrite_nav_url(nav_url) 118 | if object.is_a?(YARD::CodeObjects::Cucumber::Feature) && current_menu_lists.include?('features') 119 | nav_url.gsub('class_list.html','feature_list.html') 120 | elsif object.is_a?(YARD::CodeObjects::Cucumber::FeatureDirectory) && current_menu_lists.include?('directories') 121 | nav_url.gsub('class_list.html','featuredirectories_list.html') 122 | elsif object.is_a?(YARD::CodeObjects::Cucumber::Tag) && current_menu_lists.include?('tags') 123 | nav_url.gsub('class_list.html','tag_list.html') 124 | elsif object.is_a?(YARD::CodeObjects::Cucumber::Step) && current_menu_lists.include?('steps') 125 | nav_url.gsub('class_list.html','step_list.html') 126 | elsif object.is_a?(YARD::CodeObjects::Cucumber::StepTransformersObject) && current_menu_lists.include?('step definitions') 127 | nav_url.gsub('class_list.html','stepdefinition_list.html') 128 | else 129 | nav_url 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /lib/yard/handlers/cucumber/feature_handler.rb: -------------------------------------------------------------------------------- 1 | module YARD 2 | module Handlers 3 | module Cucumber 4 | class FeatureHandler < Base 5 | 6 | handles CodeObjects::Cucumber::Feature 7 | 8 | def process 9 | # 10 | # Features have already been created when they were parsed. So there 11 | # is no need to process the feature further. Previously this is where 12 | # feature steps were matched to step definitions and step definitions 13 | # were matched to step transforms. This only worked if the feature 14 | # files were were assured to be processed last which was accomplished 15 | # by overriding YARD::SourceParser to make it load file in a similar 16 | # order as Cucumber. 17 | # 18 | # As of YARD 0.7.0 this is no longer necessary as there are callbacks 19 | # that can be registered after all the files have been loaded. That 20 | # callback _after_parse_list_ is defined below and performs the 21 | # functionality described above. 22 | # 23 | end 24 | 25 | # 26 | # Register, once, when that when all files are finished to perform 27 | # the final matching of feature steps to step definitions and step 28 | # definitions to step transforms. 29 | # 30 | YARD::Parser::SourceParser.after_parse_list do |files,globals| 31 | # For every feature found in the Registry, find their steps and step 32 | # definitions... 33 | YARD::Registry.all(:feature).each do |feature| 34 | log.debug "Finding #{feature.file} - steps, step definitions, and step transforms" 35 | FeatureHandler.match_steps_to_step_definitions(feature) 36 | end 37 | 38 | end 39 | 40 | class << self 41 | 42 | @@step_definitions = nil 43 | @@step_transforms = nil 44 | 45 | def match_steps_to_step_definitions(statement) 46 | # Create a cache of all of the step definitions and the step transforms 47 | @@step_definitions = cache(:stepdefinition) unless @@step_definitions 48 | @@step_transforms = cache(:steptransform) unless @@step_transforms 49 | 50 | if statement 51 | # For the background and the scenario, find the steps that have definitions 52 | process_scenario(statement.background) if statement.background 53 | 54 | statement.scenarios.each do |scenario| 55 | if scenario.outline? 56 | #log.info "Scenario Outline: #{scenario.value}" 57 | scenario.scenarios.each_with_index do |example,index| 58 | #log.info " * Processing Example #{index + 1}" 59 | process_scenario(example) 60 | end 61 | else 62 | process_scenario(scenario) 63 | end 64 | end 65 | 66 | 67 | else 68 | log.warn "Empty feature file. A feature failed to process correctly or contains no feature" 69 | end 70 | 71 | rescue YARD::Handlers::NamespaceMissingError 72 | rescue Exception => exception 73 | log.error "Skipping feature because an error has occurred." 74 | log.debug "\n#{exception}\n#{exception.backtrace.join("\n")}\n" 75 | end 76 | 77 | # 78 | # Store all comparable items with their compare_value as the key and the item as the value 79 | # - Reject any compare values that contain escapes #{} as that means they have unpacked constants 80 | # 81 | def cache(type) 82 | YARD::Registry.all(type).inject({}) do |hash,item| 83 | hash[item.regex] = item if item.regex 84 | hash 85 | end 86 | end 87 | 88 | # process a scenario 89 | def process_scenario(scenario) 90 | scenario.steps.each {|step| process_step(step) } 91 | end 92 | 93 | # process a step 94 | def process_step(step) 95 | match_step_to_step_definition_and_transforms(step) 96 | end 97 | 98 | # 99 | # Given a step object, attempt to match that step to a step 100 | # transformation 101 | # 102 | def match_step_to_step_definition_and_transforms(step) 103 | @@step_definitions.each_pair do |stepdef,stepdef_object| 104 | stepdef_matches = step.value.match(stepdef) 105 | 106 | if stepdef_matches 107 | step.definition = stepdef_object 108 | stepdef_matches[1..-1].each do |match| 109 | @@step_transforms.each do |steptrans,steptransform_object| 110 | if steptrans.match(match) 111 | step.transforms << steptransform_object 112 | steptransform_object.steps << step 113 | end 114 | end 115 | end 116 | 117 | # Step has been matched to step definition and step transforms 118 | # TODO: If the step were to match again then we would be able to display ambigous step definitions 119 | break 120 | 121 | end 122 | 123 | end 124 | 125 | end 126 | 127 | end 128 | end 129 | end 130 | end 131 | end -------------------------------------------------------------------------------- /lib/templates/default/featuretags/html/namespace.erb: -------------------------------------------------------------------------------- 1 | <% if @namespace %> 2 | <% count = 0 %> 3 | <% features.each {|f| count += f.total_scenarios } %> 4 | 5 |
    6 | 64 | 65 |
    66 | Tags 67 |
    68 | 69 |
    70 | Tag Filtering ? 71 | 72 |
    73 | 74 |
    75 | Type in tags with spaces between to 'AND' and commas between to 'OR'
    76 | LEFT CLICK to AND tags; CTRL+LEFT CLICK to OR tags; hold ALT for inverse (~) tags 77 |
    78 | 79 |
    80 | 81 |
    82 | clear 83 |
    84 | 85 |
    86 | 87 | 88 | Example command line execution: 89 |
    cucumber
    90 |
    91 | 92 |
    Tags: 93 | <%= tags.collect {|tag| tagify(tag) }.join(",\n") %> 94 |
    95 | 96 |
    97 |
    98 | Features (<%= count %>) 99 |
    100 | <% n = 1 %> 101 |
      102 | <% features.each do |feature| %> 103 |
    • "> 104 | <%= linkify feature, feature.value %> 105 | <% ftags = feature.tags.collect{|t| tagify(t) }.join(", ") %> 106 | <% if ftags && ftags != "" %> 107 | - <%= ftags %> 108 | <% end %> 109 |
    • 110 | <% n = n == 2 ? 1 : 2 %> 111 | <% if feature.scenarios %> 112 |
        113 | <% feature.scenarios.each do |scenario| %> 114 |
      • <%= scenario.tags.collect{|t| t.value }.join(" ") %>" count="<%= scenario.outline? ? 0 : 1 %>"> 115 | 116 | "> 117 | <%= h scenario.value %> 118 | 119 | 120 | <% stags = scenario.tags.collect{|t| tagify(t) }.join(", ") %> 121 | <% if stags && stags != "" %> 122 | - <%= stags %> 123 | <% end %> 124 |
      • 125 | 126 | <% n = n == 2 ? 1 : 2 %> 127 | 128 | <% if scenario.outline? %> 129 |
          130 | <% scenario.examples.each do |example| %> 131 |
        • <%= scenario.tags.collect{|t| t.value }.join(" ") %> <%= example.tags.collect{|t| t.value }.join(" ") %>" count="<%= example.data.size %>"> 132 | 133 | 134 | "> 135 | <%= example.name.nil? || example.name.empty? ? "Examples" : example.name %> 136 | 137 | 138 | 139 | <% etags = example.tags.collect{|t| tagify(t) }.join(", ") %> 140 | <% if etags && etags != "" %> 141 | - <%= etags %> 142 | <% end %> 143 |
        • 144 | <% n = n == 2 ? 1 : 2 %> 145 | <% end %> 146 |
        147 | <% end %> 148 | <% end %> 149 |
      150 | <% end %> 151 | <% end %> 152 |
    153 | 154 |
    155 | 156 |
    157 | <% end %> 158 | 159 | 160 | -------------------------------------------------------------------------------- /lib/templates/default/fulldoc/html/setup.rb: -------------------------------------------------------------------------------- 1 | include YARD::Templates::Helpers::HtmlHelper 2 | 3 | def init 4 | super 5 | 6 | # Additional javascript that power the additional menus, collapsing, etc. 7 | asset "js/cucumber.js", file("js/cucumber.js",true) 8 | 9 | serialize_object_type :feature 10 | 11 | serialize_object_type :tag 12 | 13 | # Generates the requirements splash page with the 'requirements' template 14 | serialize(YARD::CodeObjects::Cucumber::CUCUMBER_NAMESPACE) 15 | 16 | # Generates a page for step definitions and step transforms with the 'steptransformers' template 17 | serialize(YARD::CodeObjects::Cucumber::CUCUMBER_STEPTRANSFORM_NAMESPACE) 18 | 19 | # Generates the tags page with the 'featuretags' template 20 | serialize(YARD::CodeObjects::Cucumber::CUCUMBER_TAG_NAMESPACE) 21 | 22 | serialize_feature_directories 23 | end 24 | 25 | # 26 | # The top-level feature directories. This is affected by the directories that YARD is told to parse. 27 | # All other features in sub-directories are contained under each of these top-level directories. 28 | # 29 | # @example Generating one feature directory 30 | # 31 | # `yardoc 'example/**/*'` 32 | # 33 | # @example Generating two feature directories 34 | # 35 | # `yardoc 'example/**/*' 'example2/**/*'` 36 | # 37 | # @return the feature directories at the root of the Cucumber Namespace. 38 | # 39 | def root_feature_directories 40 | @root_feature_directories ||= YARD::CodeObjects::Cucumber::CUCUMBER_NAMESPACE.children.find_all {|child| child.is_a?(YARD::CodeObjects::Cucumber::FeatureDirectory)} 41 | end 42 | 43 | # 44 | # Generate pages for the objects if there are objects of this type contained 45 | # within the Registry. 46 | # 47 | def serialize_object_type(type) 48 | objects = Registry.all(type.to_sym) 49 | Array(objects).each {|object| serialize(object) } 50 | end 51 | 52 | # 53 | # Generates pages for the feature directories found. Starting with all root-level feature 54 | # directories and then recursively finding all child feature directories. 55 | # 56 | def serialize_feature_directories 57 | serialize_feature_directories_recursively(root_feature_directories) 58 | root_feature_directories.each {|directory| serialize(directory) } 59 | end 60 | 61 | # 62 | # Generate a page for each Feature Directory. This is called recursively to 63 | # ensure that all feature directories contained as children are rendered to 64 | # pages. 65 | # 66 | def serialize_feature_directories_recursively(namespaces) 67 | namespaces.each do |namespace| 68 | Templates::Engine.with_serializer(namespace, options[:serializer]) do 69 | options[:object] = namespace 70 | T('layout').run(options) 71 | end 72 | serialize_feature_directories_recursively(namespace.children.find_all {|child| child.is_a?(YARD::CodeObjects::Cucumber::FeatureDirectory)}) 73 | end 74 | end 75 | 76 | # Generate feature list 77 | # @note this method is called automatically by YARD based on the menus defined in the layout 78 | def generate_feature_list 79 | features = Registry.all(:feature) 80 | features_ordered_by_name = features.sort {|x,y| x.value.to_s <=> y.value.to_s } 81 | generate_full_list features_ordered_by_name, :features 82 | end 83 | 84 | # Count scenarios for features 85 | def record_feature_scenarios(features) 86 | count_with_examples = 0 87 | features.each do |f| 88 | count_with_examples += f.total_scenarios 89 | end 90 | return count_with_examples 91 | end 92 | 93 | # Count scenarios for tags 94 | def record_tagged_scenarios(tags) 95 | scenario_count = 0 96 | count_with_examples = 0 97 | tags.each do |t| 98 | scenario_count += t.all_scenarios.size 99 | count_with_examples += t.total_scenarios 100 | end 101 | end 102 | 103 | # Generate tag list 104 | # @note this method is called automatically by YARD based on the menus defined in the layout 105 | def generate_tag_list 106 | tags = Registry.all(:tag) 107 | tags_ordered_by_use = Array(tags).sort {|x,y| y.total_scenarios <=> x.total_scenarios } 108 | 109 | record_tagged_scenarios(tags) 110 | 111 | generate_full_list tags_ordered_by_use, :tags 112 | end 113 | 114 | # Generate a step definition list 115 | # @note this menu is not automatically added until yard configuration has this menu added 116 | # See the layout template method that loads the menus 117 | def generate_stepdefinition_list 118 | generate_full_list YARD::Registry.all(:stepdefinition), :stepdefinitions, 119 | :list_title => "Step Definitions List" 120 | end 121 | 122 | # Generate a step list 123 | # @note this menu is not automatically added until yard configuration has this menu added 124 | # See the layout template method that loads the menus 125 | def generate_step_list 126 | generate_full_list YARD::Registry.all(:step), :steps 127 | end 128 | 129 | # Generate feature list 130 | # @note this menu is not automatically added until yard configuration has this menu added 131 | # See the layout template method that loads the menus 132 | def generate_featuredirectories_list 133 | directories_ordered_by_name = root_feature_directories.sort {|x,y| x.value.to_s <=> y.value.to_s } 134 | generate_full_list directories_ordered_by_name, :featuredirectories, 135 | :list_title => "Features by Directory", 136 | :list_filename => "featuredirectories_list.html" 137 | end 138 | 139 | 140 | # Helpler method to generate a full_list page of the specified objects with the 141 | # specified type. 142 | def generate_full_list(objects,type,options = {}) 143 | defaults = { :list_title => "#{type.to_s.capitalize} List", 144 | :css_class => "class", 145 | :list_filename => "#{type.to_s.gsub(/s$/,'')}_list.html" } 146 | 147 | options = defaults.merge(options) 148 | 149 | @items = objects 150 | @list_type = type 151 | @list_title = options[:list_title] 152 | @list_class = options[:css_class] 153 | asset options[:list_filename], erb(:full_list) 154 | end 155 | 156 | # 157 | # @note This method overrides YARD's default template class_list method. 158 | # 159 | # The existing YARD 'Class List' search field contains all the YARD namespace objects. 160 | # We, however, do not want the Cucumber Namespace YARD Object (which holds the features, 161 | # tags, etc.) as it is a meta-object. 162 | # 163 | # This method removes the namespace from the root node, generates the class list, 164 | # and then adds it back into the root node. 165 | # 166 | def class_list(root = Registry.root, tree = TreeContext.new) 167 | return super unless root == Registry.root 168 | 169 | cucumber_namespace = YARD::CodeObjects::Cucumber::CUCUMBER_NAMESPACE 170 | root.instance_eval { children.delete cucumber_namespace } 171 | out = super(root) 172 | root.instance_eval { children.push cucumber_namespace } 173 | out 174 | end 175 | 176 | # 177 | # Generate a link to the 'All Features' in the features_list.html 178 | # 179 | # When there are no feature directories or multiple top-level feature directories 180 | # then we want to link to the 'Requirements' page 181 | # 182 | # When there are is just one feature directory then we want to link to that directory 183 | # 184 | def all_features_link 185 | features = Registry.all(:feature) 186 | count_with_examples = record_feature_scenarios(features) 187 | if root_feature_directories.length == 0 || root_feature_directories.length > 1 188 | linkify YARD::CodeObjects::Cucumber::CUCUMBER_NAMESPACE, "All Features (#{count_with_examples})" 189 | else 190 | linkify root_feature_directories.first, "All Features (#{count_with_examples})" 191 | end 192 | end 193 | 194 | # 195 | # This method is used to generate a feature directory. This template may call 196 | # this method as well to generate any child feature directories as well. 197 | # 198 | # @param directory [FeatureDirectory] this is the FeatureDirectory to display 199 | # @param padding [Fixnum] this is the pixel value to ident as we want to keep 200 | # pushing in the padding to show the parent relationship 201 | # @param row [String] 'odd' or 'even' to correctly color the row 202 | # 203 | def directory_node(directory,padding,row) 204 | @directory = directory 205 | @padding = padding 206 | @row = row 207 | erb(:directories) 208 | end 209 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YARD-Cucumber: A Requirements Documentation Tool 2 | 3 | ## Synopsis 4 | 5 | YARD-Cucumber (formerly Cucumber-In-The-Yard) is a YARD extension that processes 6 | Cucumber features, scenarios, steps, tags, step definitions, and even transforms 7 | to provide documentation similar to what you expect to how YARD displays 8 | classes, methods and constants.This tools bridges the gap of having feature 9 | files found in your source code and true documentation that your team, product 10 | owners and stakeholders can use. 11 | 12 | ## Examples 13 | 14 | I have created a trivial, example project to help provide a quick 15 | visualization of the resulting documentation. I encourage you to look at it as 16 | an example and see if it would assist your project from a multitude of 17 | perspectives: as the project's core developer; another developer or a new 18 | developer; quality assurance engineer; or product owner/stakeholder. 19 | 20 | The implemented example has been deployed at [http://burtlo.github.io/yard-cucumber/](http://burtlo.github.io/yard-cucumber/). 21 | 22 | **1. View Features and Scenarios** [example](http://burtlo.github.io/yard-cucumber/requirements.html) 23 | 24 | **2. Search through [features, scenarios](http://burtlo.github.io/yard-cucumber/feature_list.html), and [tags](http://burtlo.github.io/yard-cucumber/tag_list.html)** 25 | 26 | **3. Dynamic Tag Unions and Intersections** [example](http://burtlo.github.io/yard-cucumber/requirements/tags.html) 27 | 28 | **4. View all features and scenarios by tag** [example](http://burtlo.github.io/yard-cucumber/requirements/tags/bvt.html) 29 | 30 | **5. View Step Definitions and Transforms** [example](http://burtlo.github.io/yard-cucumber/requirements/step_transformers.html) 31 | 32 | **6. All steps [matched](http://burtlo.github.io/yard-cucumber/requirements/step_transformers.html#definition_5-stepdefinition) to step definitions** 33 | 34 | **7. [Steps](http://burtlo.github.io/yard-cucumber/requirements/step_transformers.html#step_transform7-steptransform) that have transforms applied to them** 35 | 36 | **8. [Undefined steps](http://burtlo.github.io/yard-cucumber/requirements/step_transformers.html#undefined_steps) and even [Rubular](http://rubular.com/) links of your step definitions.** 37 | 38 | **9. Feature directories with a README.md will be parsed into the description** [example](http://burtlo.github.io/yard-cucumber/requirements/example/child_feature.html) 39 | 40 | **10. Configurable Menus - want a searchable steps menu and remove the tags menu** 41 | 42 | **11. Step definitions in your language (Ruby 1.9.2 - Internationalization)** 43 | 44 | ## Installation 45 | 46 | YARD-Cucumber requires the following gems installed: 47 | 48 | Gherkin 2.2.9 - http://cukes.info 49 | Cucumber 0.7.5 - http://cukes.info 50 | YARD 0.8.1 - http://yardoc.org 51 | 52 | To install `yard-cucumber` use the following command: 53 | 54 | ```bash 55 | $ gem install yard-cucumber 56 | ``` 57 | 58 | (Add `sudo` if you're installing under a POSIX system as root) 59 | 60 | ## Usage 61 | 62 | YARD supports for automatically including gems with the prefix `yard-` 63 | as a plugin. To enable automatic loading yard-cucumber. 64 | 65 | ```bash 66 | $ mkdir ~/.yard 67 | $ yard config load_plugins true 68 | $ yardoc 'example/**/*.rb' 'example/**/*.feature' 69 | ``` 70 | 71 | Now you can run YARD as you [normally](https://github.com/lsegal/yard) would and 72 | have your features, step definitions and transforms captured. 73 | 74 | An example with the rake task: 75 | 76 | ```ruby 77 | require 'yard' 78 | 79 | YARD::Rake::YardocTask.new do |t| 80 | t.files = ['features/**/*.feature', 'features/**/*.rb'] 81 | t.options = ['--any', '--extra', '--opts'] # optional 82 | end 83 | ``` 84 | 85 | 86 | ## Configuration 87 | 88 | * Adding or Removing search fields (yardoc) 89 | 90 | Be default the yardoc output will generate a search field for features and tags. 91 | This can be configured through the yard configuration file `~/.yard/config` to 92 | add or remove these search fields. 93 | 94 | ```yaml 95 | --- !ruby/hash-with-ivars:SymbolHash 96 | elements: 97 | :load_plugins: true 98 | :ignored_plugins: [] 99 | :autoload_plugins: [] 100 | :safe_mode: false 101 | :"yard-cucumber": 102 | menus: [ 'features', 'directories', 'tags', 'steps', 'step definitions' ] 103 | ivars: 104 | :@symbolize_value: false 105 | ``` 106 | 107 | By default the configuration, yaml format, that is generate by the `yard config` 108 | command will save a `SymbolHash`. You can still edit this file add the entry for 109 | `:"yard-cucumber":` and the sub-entry `menus:` which can contain all of the above 110 | mentioned menus or simply an empty array `[]` if you want no additional menus. 111 | 112 | * Step definitions in your language (Ruby 1.9.2) 113 | 114 | Again the yard configuration file you can define additional step definitions 115 | that can be matched. 116 | 117 | ```yaml 118 | :"yard-cucumber": 119 | language: 120 | step_definitions: [ 'Given', 'When', 'Then', 'And', 'Soit', 'Etantdonné', 'Lorsque', 'Lorsqu', 'Alors', 'Et' ] 121 | ``` 122 | 123 | In this example, I have included the French step definition words alongside the 124 | English step definitions. Even without specifying this feature files in other 125 | languages are found, this provides the ability for the step definitions to match 126 | correctly to step definitions. 127 | 128 | * Exclude features or scenarios from yardoc 129 | 130 | You can exclude any feature or scenario from the yardoc by adding a predefined tags to them. 131 | To define tags that will be excluded, again in yard configuration file: 132 | 133 | ```yaml 134 | :"yard-cucumber": 135 | exclude_tags: [ 'exclude-yardoc', 'also-exclude-yardoc' ] 136 | ``` 137 | 138 | ## Details 139 | 140 | There are two things that I enjoy: a test framework written in my own Domain 141 | Specific Language (DSL) that is easily understood by all those on a project 142 | and the ability for all participants to easily read, search, and view the tests. 143 | 144 | Cucumber is an amazing tool that allowed me to define exercisable requirements. 145 | My biggest obstacle was bringing these requirements to my team, the product 146 | owner, and other stakeholders. 147 | 148 | Initially I tried to expose more of the functionality by providing freshly 149 | authored requirements through email, attachments to JIRA tickets, or linked in 150 | wiki documents. None of these methods were very sustainable or successful. 151 | First, I was continually pushing out the documents to those interested. 152 | Second, the documents were displayed to the user in text without the syntax 153 | highlighting that was exceedingly helpful for quickly understanding the requirements. 154 | 155 | I also found it hard to share the test framework that I had put together with 156 | another developer that joined the team. It was difficult to direct them around 157 | the features, tags, step definitions, and transforms. It was when I started to 158 | convey to them the conventions that I had established that I wished I had a 159 | tool that would allow me to provide documentation like one would find generated 160 | by a great tool like YARD. 161 | 162 | So I set out to integrate Cucumber objects like features, backgrounds, 163 | scenarios, tags, steps, step definitions, and transforms into a YARD template. 164 | From my quick survey of the landscape I can see that the my needs are 165 | different than a lot of others that use Cucumber. The entire project that 166 | spawned this effort was solely to exercise the functionality of a different, 167 | large project and so there is a huge dependence on having the requirements 168 | documented. This is in contrast to other projects that are using this on a 169 | small scale to test the functionality of small software component. Though, 170 | ultimately, I realized that the functionality may provide a valuable tool for 171 | many as I feel it helps more solidly bridge the reporting of the documentation 172 | by putting a coat of paint on it. 173 | 174 | 175 | ## LICENSE 176 | 177 | (The MIT License) 178 | 179 | Copyright (c) 2011 Franklin Webber 180 | 181 | Permission is hereby granted, free of charge, to any person obtaining 182 | a copy of this software and associated documentation files (the 183 | 'Software'), to deal in the Software without restriction, including 184 | without limitation the rights to use, copy, modify, merge, publish, 185 | distribute, sublicense, and/or sell copies of the Software, and to 186 | permit persons to whom the Software is furnished to do so, subject to 187 | the following conditions: 188 | 189 | The above copyright notice and this permission notice shall be 190 | included in all copies or substantial portions of the Software. 191 | 192 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 193 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 194 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 195 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 196 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 197 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 198 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 199 | -------------------------------------------------------------------------------- /History.txt: -------------------------------------------------------------------------------- 1 | === 4.0.0 / 2018-02-25 2 | 3 | * @wellavelino: Adjusts the indentation of several project classes 4 | (https://github.com/burtlo/yard-cucumber/pull/89) 5 | * @elhuang: Adding example tags and scenarios counts 6 | (https://github.com/burtlo/yard-cucumber/pull/87) 7 | * @weh: Update README.md 8 | (https://github.com/burtlo/yard-cucumber/pull/84) 9 | * @weh: Fix Gem Load Error 10 | (https://github.com/burtlo/yard-cucumber/pull/83) 11 | * @Stephen-Kaye: Transforms with CONSTANTS 12 | (https://github.com/burtlo/yard-cucumber/pull/79) 13 | 14 | === 3.1.0/ 2016-12-06 15 | 16 | * Updates to Gherkin 4.0 and Cucumber 2.4.0 17 | * Fixes issues with the formatting that comes from these changes 18 | 19 | === 2.3.2/ 2013-11-08 20 | 21 | * De-duping uses of step definitions when the line number is the same (ipwnstuff) 22 | 23 | === 2.3.1/ 2013-07-12 24 | 25 | * Fixes for Cucumber ~> 1.3 and gherkin ~> 2.12 by loading 26 | a required platform file. 27 | * Added appropriate MIT license 28 | 29 | === 2.3.0/ 2013-07-12 30 | 31 | * Fixes for Cucumber ~> 1.3 and gherkin ~> 2.12 by loading 32 | a required platform file. 33 | 34 | === 2.2.3/ 2013-02-15 35 | 36 | * Step Definitions that are marked as 'pending' will now appear as pending 37 | on the step definition and transforms page. 38 | 39 | === 2.2.2/ 2012-10-07 40 | 41 | * Steps and Definitions have links to step transformer page 42 | * Step definitions in the search menu no longer expand constants 43 | 44 | === 2.2.1/ 2012-09-29 45 | 46 | * Fixes and compatibility with JRuby (Thanks @singram) 47 | 48 | === 2.2.0/ 2012-05-29 49 | 50 | * Fixes and changes to become compatible with YARD 0.8.1 51 | 52 | === 2.1.7/ 2011-11-24 53 | 54 | * Better parsing support for doc_strings (thanks @nikosd) 55 | 56 | === 2.1.6 / 2011-11-24 57 | 58 | * FIX for yard-server (Thanks @msolovyov, @maxigit, @nikosd) 59 | 60 | === 2.1.5 / 2011-10-19 61 | 62 | * FIX for multiple scenario outlines (Thanks @ardavis) 63 | 64 | === 2.1.4 / 2011-10-10 65 | 66 | * Support for multiple scenario outlines (Thanks @jmerrifield) 67 | 68 | === 2.1.3 / 2011-09-24 69 | 70 | * Support for Gherkin ~ 2.5 (Thanks @turboladen) 71 | 72 | === 2.1.2 / 2011-09-16 73 | 74 | * JRuby Bug Fix (Thanks @aledalgrande) 75 | 76 | === 2.1.1 / 2011-06-17 77 | 78 | * Gherkin 2.4.0 Compatibility (Thanks @bowsersenior) 79 | 80 | === 2.1.0 / 2011-05-22 81 | 82 | * YARD 0.7.0 compatibility (removed some monkey-patching) 83 | * Add more menus (Steps and Step Definitions) 84 | * Define step definitions in your language Internalization (Ruby 1.9.2) 85 | 86 | === 2.0.3 / 2011-05-22 87 | 88 | * Updated Cucumber links to the new github organization 89 | 90 | === 2.0.2 / 2011-04-06 91 | 92 | - No Features/Tags will still generate landing pages instead of no pages 93 | - Changed link to current gem 'yard-cucumber' 94 | 95 | === 2.0.1 / 2011-03-14 96 | 97 | - YARD 0.6.5 compatibility 98 | 99 | === 2.0.0 / 2011-02-08 100 | 101 | * Cucumber In The YARD becomes `yard-cucumber` and with that gains the 102 | benefits of being a YARD plugin 103 | 104 | === 1.7.9 / 2011-02-05 105 | 106 | - FIX links to Scenarios and Transforms in various places 107 | - FIX Comments on Step Definitions that may blow up 108 | 109 | === 1.7.8 / 2011-01-30 110 | 111 | * 'All Features' lands you on the root features directory; 'Requirements' 112 | is still accessible through the breadcrumbs. 113 | * Feature directory pages will display all the features in all subdirectories 114 | * Better sorting for features, tags, and step transformers. 115 | 116 | - FIX Feature-Scenario links and Cursors 117 | 118 | === 1.7.7 / 2011-01-06 119 | 120 | * Feature Directories now support a README.md file that can be used to provide 121 | information (with markdown support) about the directory of features. 122 | 123 | === 1.7.6 / 2011-01-06 124 | 125 | * Feature descriptions, multi-line strings, and comments will preserve spacing. 126 | 127 | === 1.7.5 / 2010-12-14 128 | 129 | * FIX Step Transforms were colliding with Step Definitions in Ruby 1.9.2 130 | 131 | === 1.7.4 / 2010-12-11 132 | 133 | * Ruby 1.9.2 Support 134 | * Change to YARD Server support: 135 | yard server -e /path/to/gem/lib/server.b 136 | 137 | === 1.7.3 / 2010-12-06 138 | 139 | * Shortcut Keys (t) for tags and (r) for features (f was taken) 140 | * Step definitions/transforms consolidate steps for easier viewing 141 | and when they have no steps they are colored differently 142 | * Tag filtering supports inverse (~) of tags 143 | 144 | * FIX Scenario Outlines without examples will not crash the parser 145 | * FIX Features, Scenarios, and Directories should display in alphabetic order 146 | * FIX Comments and multiline strings will display correctly 147 | * FIX Feature comments will show 148 | 149 | === 1.7.2 / 2010-11-30 150 | 151 | * Step Definition and Transform page enhancements 152 | * Step Regex links to Rubular 153 | * FIX Steps with HTML encoded characters will display correctly 154 | 155 | === 1.7.1 / 2010-11-28 156 | 157 | * Feature file with layout enhancements 158 | * Scenarios are now linked/linkable 159 | * FIX Scenario Outline error on CentOS, Ruby 1.8.7 160 | * FIX Requiring Cucumber before Gherkin so that correct gherkin is loaded 161 | 162 | === 1.7.0 / 2010-11-18 163 | 164 | * Dynamic Tag Union / Intersection 165 | 166 | === 1.6.4 / 2010-11-16 167 | 168 | * `yard server` can now serve up requirements with YARD 0.6.2 169 | * 'All Tags' page now has a cucumber tag AND filtering tool (beta) 170 | * Step Defs and Transforms will show comments 171 | 172 | * FIX removed warnings by no longer requiring YARD 173 | 174 | === 1.6.2 / 2010-11-07 175 | 176 | * Undefined Steps are displayed on the Step Transformer page 177 | * Defined and Undefined steps are better linked to the Transformer page 178 | 179 | * FIX Requirements tags were conflicting with YARD tags and being displayed 180 | in multiple locations 181 | * FIX Scenario Outlines Examples failed to show their tables or text 182 | 183 | === 1.6.1 / 2010-11-04 184 | 185 | * FIX - Feature Directries with sub-directories aborted serialization 186 | 187 | === 1.6.0 / 2010-11-02 188 | 189 | * Feature directories, subdirectories, and Tags breadcrumb pages are present 190 | * Scenario Outlines now display each example inline when the example is clicked 191 | * 'All Features' and 'All Tags' links in the search fields 192 | 193 | === 1.5.4 / 2010-10-28 194 | 195 | * Optimization - Found that for a large test suite that the processing time was 196 | approaching 2 hours (oh boy!). Reduced that time to a few minutes by optimizing 197 | the feature handler to cache all step definitions and step transforms. 198 | 199 | * Step Transforms will now attempt to unpack any constants found within them. 200 | * FIX - Step Transforms were not being parsed because the regex incorrectly included 201 | the block. 202 | * FIX - Features and Scenarios with multiple tags were not being displayed 203 | 204 | === 1.5.3 / 2010-10-26 205 | 206 | * FIX - Step Definition YARD handler regex was poorly formed and raised exceptions 207 | when it contained a forward-slash (even if escaped). 208 | * FIX - Step highlighting was causing 'Out Of Memory' issues because 209 | gsub could not handle nil values. 210 | * FIX - Step highlighting failed to replace matches properly causing for corrupted 211 | HTML span values to appear in the highlighted step definitions 212 | 213 | === 1.5.2 / 2010-10-26 214 | 215 | * FIX - Step Transform YARD handler regex was poorly formed and raised exceptions 216 | when a transform contained a forward-slash (even if escaped). 217 | 218 | === 1.5.1 / 2010-10-26 219 | 220 | * Unified Feature/Scenario search field that resembles the Namespace-Class field 221 | * Tag search also displays immediate features/scenarios 222 | * Removed Scenario searching field 223 | * Added filename and line number to a number of locations 224 | * Minor Tag template enhancements 225 | * FIX - Scenario Outline Reference was being replaced with the first example 226 | * FIX - Scenario Outlines were not getting counted for tags 227 | 228 | === 1.5 / 2010-10-25 229 | 230 | * Steps that have found their step definitions will also attempt to find their 231 | step transforms (However step transforms are not unpacked yet like step 232 | definitions). 233 | * Scenario Outlines will expand, behind the scenes, and match like traditional 234 | scenarios. 235 | * FIX - duplication of owners that the tag reports has been removed. 236 | * INFO level messaging while processing features and scenarios to help assist 237 | with feedback during the long processing. 238 | 239 | === 1.4 / 2010-10-17 240 | 241 | * Tagusage has been retired 242 | * Tags are found during parsing by name or created and all uses are appended to the exising or newly created object 243 | * Tagusage template renamed and fixed up for just a tag and all the references 244 | * Tags are named tag_TAG (removed the @ as it was encoded during file serialization and I couldn't find an encoder helper) 245 | * Cleaned up all references in the fulldoc, full list to references tags 246 | 247 | * Moved Cucumber CodeObjects, Handlers, and Parsers to a Cucumber sub-directory 248 | * Moved StepDefinition and StepTransforms form extensions.rb to individual StepObject and Handlers files 249 | 250 | * (lsegal advice) Created Cucumber Namespace and sub namespaces for features, tags, and step transformers. 251 | * (lsegal advice) Started to break apart the feature.erb into smaller digestable erb sections 252 | 253 | * Feature namespace now parallels the directory structure of the feature directory (from where it is parsed) 254 | * Feature files have simpler names; i.e. featurefile.html instead of featurefile.feature.html 255 | * Feature page title now states Feature: The Feature ( KEYWORD: FEATURE_TITLE ) 256 | * Moved the templates to lib/templates so I could use yard/templates to override the YARD helpers 257 | 258 | * Changed document parsing to mirror the order imposed by Cucumber, this is to stop features being loaded before step definitions. 259 | This should hopefully address the issue where steps were not being mapped correctly to step definitions. This fixed the issue 260 | in the small and large test suite I ran it against. 261 | 262 | 263 | === 1.3 / 2010-10-13 264 | 265 | * (7rans requested) Step definitions to show their source code; required a better source definition to be stored during creation of the CodeObject 266 | * Fixes to the Feature template which had some unclosed table header elements 267 | * Scenario Outline helper methods added to Scenarios 268 | * Scenario Outlines, Tables, and Multiline Strings had other minor CSS changes 269 | * Step tables and Scenario Example tables are no longer locked to 150px; instead that is the minimum width 270 | * Tables and Multiline Strings (PyStrings) are no longer displayed in the steps frame which was causing ugliness with the striping of the rows 271 | * (lsegal strongly recommended) Removed the pervasive use of filename in the CodeObjects 272 | * Cleaned up the fulldoc setup to remove the redundancy 273 | * Updated all the templates to support the lack of filename 274 | 275 | === 1.2 / 2010-10-10 276 | 277 | * FIX: Empty Step Definitions caused failure during serialization 278 | * FIX: Empty Step Definitions caused failure during rendering HTML 279 | 280 | === 1.1 / 2010-10-10 281 | 282 | * FIX: Feature template had references to old methods for description (when it was an array) 283 | * FIX: Empty feature files caused the documentation to fail; ignoring emptying feature files 284 | * FIX: Tags, Step definitions, and other CodeObjects that were not present caused rendering to fail; Checking for nil 285 | 286 | === 1.0 / 2010-10-10 287 | 288 | * Initial Release 289 | -------------------------------------------------------------------------------- /lib/templates/default/fulldoc/html/js/cucumber.js: -------------------------------------------------------------------------------- 1 | function cucumberKeyboardShortcuts() { 2 | if (window.top.frames.main) return; 3 | $(document).keypress(function(evt) { 4 | if (evt.altKey || evt.ctrlKey || evt.metaKey || evt.shiftKey) return; 5 | if (typeof evt.target !== "undefined" && 6 | (evt.target.nodeName == "INPUT" || 7 | evt.target.nodeName == "TEXTAREA")) return; 8 | switch (evt.charCode) { 9 | case 68: case 100: $('#stepdefinition_list_link').click(); break; // 'd' 10 | case 82: case 114: $('#feature_list_link').click(); break; // 'r' 11 | case 83: case 115: $('#step_list_link').click(); break; // 's' 12 | case 84: case 116: $('#tag_list_link').click(); break; // 't' 13 | } 14 | }); 15 | } 16 | 17 | $(cucumberKeyboardShortcuts); 18 | 19 | $(function() { 20 | 21 | // 22 | // Feature Page - Scenarios 23 | // 24 | $('.scenario div.title').click(function(eventObject) { 25 | if (typeof eventObject.currentTarget !== "undefined") { 26 | toggleScenario( $($(eventObject.currentTarget).parent()) ); 27 | } 28 | }); 29 | 30 | // 31 | // Developer View 32 | // Click + Developer View = toggle the expansion of all tags, location, and comments 33 | // 34 | $('#view').click(function(eventObject) { 35 | 36 | if (typeof eventObject.currentTarget !== "undefined") { 37 | var view = eventObject.currentTarget; 38 | 39 | if (view.innerHTML === '[More Detail]') { 40 | $('.developer').show(500); 41 | view.innerHTML = '[Less Detail]'; 42 | } else { 43 | $('.developer').hide(500); 44 | // Already hidden elements with .developer sub-elements were not getting message 45 | $('.developer').each(function() { 46 | $(this).css('display','none'); 47 | }); 48 | view.innerHTML = '[More Detail]'; 49 | } 50 | } 51 | }); 52 | 53 | // 54 | // Expand/Collapse All 55 | // 56 | $('#expand').click(function(eventObject) { 57 | 58 | if (typeof eventObject.currentTarget !== "undefined") { 59 | if (eventObject.currentTarget.innerHTML === '[Expand All]') { 60 | eventObject.currentTarget.innerHTML = '[Collapse All]'; 61 | $('div.scenario > div.details:hidden').each(function() { 62 | toggleScenario( $($(this).parent()) ); 63 | }); 64 | } else { 65 | eventObject.currentTarget.innerHTML = '[Expand All]'; 66 | $('div.scenario > div.details:visible').each(function() { 67 | toggleScenario( $($(this).parent()) ); 68 | }); 69 | } 70 | } 71 | }); 72 | 73 | // 74 | // Expand/Collapse All 75 | // 76 | $('#stepdefinition,#steptransform').click(function(eventObject) { 77 | 78 | if (typeof eventObject.currentTarget !== "undefined") { 79 | if (eventObject.currentTarget.innerHTML === '[Expand All]') { 80 | eventObject.currentTarget.innerHTML = '[Collapse All]'; 81 | $('div.' + eventObject.currentTarget.id + ' > div.details:hidden').each(function() { 82 | $(this).show(200); 83 | }); 84 | $('div.' + eventObject.currentTarget.id + ' a.stepdef').text("[Collapse]") 85 | } else { 86 | eventObject.currentTarget.innerHTML = '[Expand All]'; 87 | $('div.' + eventObject.currentTarget.id + ' > div.details:visible').each(function() { 88 | $(this).hide(200); 89 | }); 90 | console.log(eventObject.currentTarget.id); 91 | $('div.' + eventObject.currentTarget.id + ' a.stepdef').text('[Expand]'); 92 | } 93 | } 94 | }); 95 | 96 | $('.stepdef').click(function(eventObject) { 97 | if (typeof eventObject.currentTarget !== "undefined") { 98 | if (eventObject.currentTarget.innerHTML === '[Expand]') { 99 | eventObject.currentTarget.innerHTML = '[Collapse]'; 100 | $(eventObject.target).parent().parent().parent().find("div.details:hidden").each(function() { 101 | $(this).show(200); 102 | }); 103 | } else { 104 | eventObject.currentTarget.innerHTML = '[Expand]'; 105 | $(eventObject.target).parent().parent().parent().find("div.details:visible").each(function() { 106 | $(this).hide(200); 107 | }); 108 | } 109 | } 110 | }); 111 | 112 | 113 | // 114 | // Scenario Outlines - Toggle Examples 115 | // 116 | $('.outline table tr').click(function(eventObject) { 117 | 118 | if (typeof eventObject.currentTarget !== "undefined") { 119 | var exampleRow = $(eventObject.currentTarget); 120 | 121 | if (eventObject.currentTarget.className.match(/example\d+-\d+/) == null) { 122 | return false; 123 | } 124 | 125 | var exampleClass = eventObject.currentTarget.className.match(/example\d+-\d+/)[0]; 126 | var example = exampleRow.closest('div.details').find('.' + exampleClass); 127 | 128 | var currentExample = null; 129 | 130 | $('.outline table tr').each(function() { 131 | $(this).removeClass('selected'); 132 | }); 133 | 134 | if ( example[0].style.display == 'none' ) { 135 | currentExample = example[0]; 136 | exampleRow.addClass('selected'); 137 | } else { 138 | currentExample = exampleRow.closest('div.details').find('.steps')[0]; 139 | } 140 | 141 | // hide everything 142 | exampleRow.closest('div.details').find('.steps').each(function() { 143 | $(this).hide(); 144 | }); 145 | 146 | // show the selected 147 | $(currentExample).show(); 148 | } 149 | }); 150 | 151 | 152 | }); 153 | 154 | 155 | function toggleScenario(scenario) { 156 | 157 | var state = scenario.find(".attributes input[name='collapsed']")[0]; 158 | 159 | if (state.value === 'true') { 160 | scenario.find("div.details").each(function() { 161 | $(this).show(500); 162 | }); 163 | state.value = "false"; 164 | scenario.find('a.toggle').each(function() { 165 | this.innerHTML = ' - '; 166 | }); 167 | 168 | } else { 169 | scenario.find("div.details").each(function() { 170 | $(this).hide(500); 171 | }); 172 | state.value = "true"; 173 | scenario.find('a.toggle').each(function() { 174 | this.innerHTML = ' + '; 175 | }); 176 | } 177 | } 178 | 179 | 180 | function updateTagFiltering(tagString) { 181 | var formulaTags = determineTagsUsedInFormula(tagString); 182 | displayExampleCommandLine(formulaTags); 183 | displayQualifyingFeaturesAndScenarios(formulaTags); 184 | fixSectionRowAlternations(); 185 | } 186 | 187 | function clearTagFiltering() { 188 | updateTagFiltering(""); 189 | } 190 | 191 | function determineTagsUsedInFormula(tagString) { 192 | 193 | tagString = tagString.replace(/^(\s+)|(\s+)$/,'').replace(/\s{2,}/,' '); 194 | 195 | var tagGroup = tagString.match(/(?:~)?@\w+(,(?:~)?@\w+)*/g); 196 | 197 | var returnTags = []; 198 | 199 | if (tagGroup) { 200 | tagGroup.forEach(function(tag, index, array) { 201 | console.log("Tag Group: " + tag); 202 | var validTags = removeInvalidTags(tag) 203 | if (validTags != "") { 204 | returnTags.push(validTags); 205 | } 206 | }); 207 | } 208 | 209 | return returnTags; 210 | } 211 | 212 | function removeInvalidTags(tagGroup) { 213 | tagGroup.split(",").forEach(function(tag, index, array) { 214 | 215 | baseTag = tag.match(/^~(.+)/) ? tag.match(/^~(.+)/)[1] : tag; 216 | 217 | //console.log("Validating Tag: " + tag) 218 | if (tag_list.indexOf(baseTag) === -1) { 219 | //console.log("Removing Tag: " + tag); 220 | tagGroup = tagGroup.replace(new RegExp(',?' + tag + ',?'),"") 221 | } 222 | }); 223 | 224 | return tagGroup; 225 | } 226 | 227 | 228 | function displayExampleCommandLine(tags) { 229 | $("#command_example")[0].innerHTML = "cucumber "; 230 | 231 | if (tags.length > 0) { 232 | $("#command_example")[0].innerHTML += "--tags " + tags.join(" --tags "); 233 | } 234 | } 235 | 236 | function fixSectionRowAlternations() { 237 | $(".feature:visible,.scenario:visible").each(function(index){ 238 | $(this).removeClass("odd even").addClass( ((index + 1) % 2 == 0 ? "even" : "odd") ); 239 | }); 240 | } 241 | 242 | function displayQualifyingFeaturesAndScenarios(tags) { 243 | 244 | if (tags.length > 0) { 245 | 246 | $(".feature,.scenario").each(function(feature){ 247 | $(this).hide(); 248 | }); 249 | 250 | var tagSelectors = generateCssSelectorFromTags(tags); 251 | 252 | tagSelectors.forEach(function(selector,selectorIndex,selectorArray) { 253 | var tags = selector; 254 | 255 | $(".feature." + tags).each(function(index) { 256 | $(this).show(); 257 | }); 258 | $(".scenario." + tags).each(function(index) { 259 | $(this).show(); 260 | $(this).parent().prev().show(); 261 | }); 262 | 263 | }); 264 | 265 | if ( $(".feature:visible,.scenario:visible").length == 0 ) { 266 | $("#features div.undefined").show(); 267 | } else { 268 | $("#features div.undefined").hide(); 269 | } 270 | 271 | 272 | } else { 273 | $(".feature:hidden,.scenario:hidden").each(function(feature){ 274 | $(this).show(); 275 | }); 276 | } 277 | 278 | } 279 | 280 | function updateScenarioCount() { 281 | var count = 0; 282 | $(".scenario:visible").each(function(index) { 283 | count += parseInt($(this).attr("count")) 284 | }); 285 | count_header = " (" + count + ")"; 286 | document.getElementById("scenario_count").innerHTML = count_header; 287 | } 288 | 289 | function generateCssSelectorFromTags(tagGroups) { 290 | 291 | var tagSelectors = [ "" ]; 292 | 293 | tagGroups.forEach(function(tagGroup,index,array) { 294 | var newTagSelectors = []; 295 | 296 | tagSelectors.forEach(function(selector,selectorIndex,selectorArray) { 297 | tagGroup.split(",").forEach(function(tag,tagIndex,tagArray) { 298 | 299 | if ( tag.match(/^~@.+$/) ) { 300 | tag = tag.match(/^~(@.+)$/)[1] 301 | //console.log("selector: " + (selector + " :not(" + tag + ")").trim()); 302 | newTagSelectors.push((selector + ":not(." + tag.replace(/@/g,"\\@") +")").trim()); 303 | } else { 304 | //console.log("selector: " + (selector + " " + tag).trim()); 305 | newTagSelectors.push((selector + "." + tag.replace(/@/g,"\\@")).trim()); 306 | } 307 | }); 308 | 309 | }); 310 | 311 | tagSelectors = newTagSelectors; 312 | 313 | }); 314 | 315 | 316 | return tagSelectors; 317 | } 318 | 319 | 320 | function createStepDefinitionLinks() { 321 | // $('.step_instances_list'). 322 | // before("[View steps]"); 323 | $('.toggleSteps').toggle(function() { 324 | $(this).parent().next().slideUp(100); 325 | $(this).text("View " + $(this).attr('alt')); 326 | }, 327 | function() { 328 | $(this).parent().next().slideDown(100); 329 | $(this).text("Hide " + $(this).attr('alt')); 330 | }); 331 | } 332 | 333 | $(createStepDefinitionLinks); 334 | -------------------------------------------------------------------------------- /lib/cucumber/city_builder.rb: -------------------------------------------------------------------------------- 1 | module Cucumber 2 | module Parser 3 | class CityBuilder < ::Gherkin::AstBuilder 4 | 5 | # 6 | # The Gherkin Parser is going to call the various methods within this 7 | # class as it finds items. This is similar to how Cucumber generates 8 | # it's Abstract Syntax Tree (AST). Here instead this generates the 9 | # various YARD::CodeObjects defined within this template. 10 | # 11 | # A namespace is specified and that is the place in the YARD namespacing 12 | # where all cucumber features generated will reside. The namespace specified 13 | # is the root namespaces. 14 | # 15 | # @param [String] file the name of the file which the content belongs 16 | # 17 | def initialize(file) 18 | super() 19 | @namespace = YARD::CodeObjects::Cucumber::CUCUMBER_NAMESPACE 20 | find_or_create_namespace(file) 21 | @file = file 22 | end 23 | 24 | # Return the feature that has been defined. This method is the final 25 | # method that is called when all the work is done. It is called by 26 | # the feature parser to return the complete Feature object that was created 27 | # 28 | # @return [YARD::CodeObject::Cucumber::Feature] the completed feature 29 | # 30 | # @see YARD::Parser::Cucumber::FeatureParser 31 | def ast 32 | feature(get_result) unless @feature 33 | @feature 34 | end 35 | 36 | # 37 | # Feature that are found in sub-directories are considered, in the way 38 | # that I chose to implement it, in another namespace. This is because 39 | # when you execute a cucumber test run on a directory any sub-directories 40 | # of features will be executed with that directory so the file is split 41 | # and then namespaces are generated if they have not already have been. 42 | # 43 | # The other duty that this does is look for a README.md file within the 44 | # specified directory of the file and loads it as the description for the 45 | # namespace. This is useful if you want to give a particular directory 46 | # some flavor or text to describe what is going on. 47 | # 48 | def find_or_create_namespace(file) 49 | @namespace = YARD::CodeObjects::Cucumber::CUCUMBER_NAMESPACE 50 | 51 | File.dirname(file).split('/').each do |directory| 52 | @namespace = @namespace.children.find {|child| child.is_a?(YARD::CodeObjects::Cucumber::FeatureDirectory) && child.name.to_s == directory } || 53 | @namespace = YARD::CodeObjects::Cucumber::FeatureDirectory.new(@namespace,directory) {|dir| dir.add_file(directory)} 54 | end 55 | 56 | if @namespace.description == "" && File.exists?("#{File.dirname(file)}/README.md") 57 | @namespace.description = File.read("#{File.dirname(file)}/README.md") 58 | end 59 | end 60 | 61 | # 62 | # Find the tag if it exists within the YARD Registry, if it doesn't then 63 | # create it. 64 | # 65 | # We note that the tag was used in this file at the current line. 66 | # 67 | # Then we add the tag to the current scenario or feature. We also add the 68 | # feature or scenario to the tag. 69 | # 70 | # @param [String] tag_name the name of the tag 71 | # @param [parent] parent the scenario or feature that is going to adopt 72 | # this tag. 73 | # 74 | def find_or_create_tag(tag_name,parent) 75 | #log.debug "Processing tag #{tag_name}" 76 | tag_code_object = YARD::Registry.all(:tag).find {|tag| tag.value == tag_name } || 77 | YARD::CodeObjects::Cucumber::Tag.new(YARD::CodeObjects::Cucumber::CUCUMBER_TAG_NAMESPACE,tag_name.gsub('@','')) {|t| t.owners = [] ; t.value = tag_name ; t.total_scenarios = 0} 78 | 79 | tag_code_object.add_file(@file,parent.line) 80 | 81 | parent.tags << tag_code_object unless parent.tags.find {|tag| tag == tag_code_object } 82 | tag_code_object.owners << parent unless tag_code_object.owners.find {|owner| owner == parent} 83 | end 84 | 85 | # 86 | # Each feature found will call this method, generating the feature object. 87 | # This is once, as the gherkin parser does not like multiple feature per 88 | # file. 89 | # 90 | def feature(document) 91 | #log.debug "FEATURE" 92 | feature = document[:feature] 93 | return unless document[:feature] 94 | return if has_exclude_tags?(feature[:tags].map { |t| t[:name].gsub(/^@/, '') }) 95 | 96 | @feature = YARD::CodeObjects::Cucumber::Feature.new(@namespace,File.basename(@file.gsub('.feature','').gsub('.','_'))) do |f| 97 | f.comments = feature[:comments] ? feature[:comments].map{|comment| comment[:text]}.join("\n") : '' 98 | f.description = feature[:description] || '' 99 | f.add_file(@file,feature[:location][:line]) 100 | f.keyword = feature[:keyword] 101 | f.value = feature[:name] 102 | f.tags = [] 103 | 104 | feature[:tags].each {|feature_tag| find_or_create_tag(feature_tag[:name],f) } 105 | end 106 | 107 | background(feature[:background]) if feature[:background] 108 | 109 | feature[:children].each do |child| 110 | case child[:type] 111 | when :Background 112 | background(child) 113 | when :ScenarioOutline 114 | outline = scenario_outline(child) 115 | @feature.total_scenarios += outline.scenarios.size 116 | when :Scenario 117 | scenario(child) 118 | end 119 | end 120 | 121 | @feature.tags.each do |feature_tag| 122 | tag_code_object = YARD::Registry.all(:tag).find {|tag| tag.name.to_s == feature_tag[:name].to_s } 123 | tag_code_object.total_scenarios += @feature.total_scenarios 124 | end 125 | end 126 | 127 | # 128 | # Called when a background has been found 129 | # 130 | # @see #feature 131 | def background(background) 132 | #log.debug "BACKGROUND" 133 | 134 | @background = YARD::CodeObjects::Cucumber::Scenario.new(@feature,"background") do |b| 135 | b.comments = background[:comments] ? background[:comments].map{|comment| comment.value}.join("\n") : '' 136 | b.description = background[:description] || '' 137 | b.keyword = background[:keyword] 138 | b.value = background[:name] 139 | b.add_file(@file,background[:location][:line]) 140 | end 141 | 142 | @feature.background = @background 143 | @background.feature = @feature 144 | @step_container = @background 145 | background[:steps].each { |s| 146 | step(s) 147 | } 148 | end 149 | 150 | # 151 | # Called when a scenario has been found 152 | # - create a scenario 153 | # - assign the scenario to the feature 154 | # - assign the feature to the scenario 155 | # - find or create tags associated with the scenario 156 | # 157 | # The scenario is set as the @step_container, which means that any steps 158 | # found before another scenario is defined belong to this scenario 159 | # 160 | # @param [Scenario] statement is a scenario object returned from Gherkin 161 | # @see #find_or_create_tag 162 | # 163 | def scenario(statement) 164 | #log.debug "SCENARIO" 165 | 166 | return if has_exclude_tags?(statement[:tags].map { |t| t[:name].gsub(/^@/, '') }) 167 | 168 | scenario = YARD::CodeObjects::Cucumber::Scenario.new(@feature,"scenario_#{@feature.scenarios.length + 1}") do |s| 169 | s.comments = statement[:comments] ? statement[:comments].map{|comment| comment.value}.join("\n") : '' 170 | s.description = statement[:description] || '' 171 | s.add_file(@file,statement[:location][:line]) 172 | s.keyword = statement[:keyword] 173 | s.value = statement[:name] 174 | 175 | statement[:tags].each {|scenario_tag| find_or_create_tag(scenario_tag[:name],s) } 176 | end 177 | 178 | scenario.feature = @feature 179 | @feature.scenarios << scenario 180 | @step_container = scenario 181 | statement[:steps].each { |s| 182 | step(s) 183 | } 184 | 185 | # count scenarios for scenario level tags 186 | scenario.tags.uniq.each { |scenario_tag| 187 | if !scenario.feature.tags.include?(scenario_tag) 188 | tag_code_object = YARD::Registry.all(:tag).find {|tag| tag.name.to_s == scenario_tag[:name].to_s } 189 | tag_code_object.total_scenarios += 1 190 | end 191 | } 192 | end 193 | 194 | # 195 | # Called when a scenario outline is found. Very similar to a scenario, 196 | # the ScenarioOutline is still a distinct object as it can contain 197 | # multiple different example groups that can contain different values. 198 | # 199 | # @see #scenario 200 | # 201 | def scenario_outline(statement) 202 | #log.debug "SCENARIO OUTLINE" 203 | 204 | return if has_exclude_tags?(statement[:tags].map { |t| t[:name].gsub(/^@/, '') }) 205 | 206 | outline = YARD::CodeObjects::Cucumber::ScenarioOutline.new(@feature,"scenario_#{@feature.scenarios.length + 1}") do |s| 207 | s.comments = statement[:comments] ? statement[:comments].map{|comment| comment.value}.join("\n") : '' 208 | s.description = statement[:description] || '' 209 | s.add_file(@file,statement[:location][:line]) 210 | s.keyword = statement[:keyword] 211 | s.value = statement[:name] 212 | 213 | statement[:tags].each {|scenario_tag| find_or_create_tag(scenario_tag[:name],s) } 214 | end 215 | 216 | outline.feature = @feature 217 | @step_container = outline 218 | statement[:steps].each { |s| 219 | step(s) 220 | } 221 | 222 | statement[:examples].each { |e| 223 | example = examples(e, outline) 224 | } 225 | 226 | @feature.scenarios << outline 227 | 228 | # count scenarios for scenario outline level tags 229 | outline.tags.uniq.each { |outline_tag| 230 | if !outline.feature.tags.include?(outline_tag) 231 | tag_code_object = YARD::Registry.all(:tag).find {|tag| tag.name.to_s == outline_tag[:name].to_s } 232 | tag_code_object.total_scenarios += outline.scenarios.size 233 | end 234 | } 235 | 236 | # count scenarios for example table level tags 237 | outline.examples.each { |example| 238 | unless !example.tags.any? 239 | example.tags.uniq.each { |example_tag| 240 | if !outline.feature.tags.include?(example_tag) && !outline.tags.include?(example_tag) 241 | tag_code_object = YARD::Registry.all(:tag).find {|tag| tag.name.to_s == example_tag[:name].to_s } 242 | tag_code_object.total_scenarios += example.data.size 243 | end 244 | } 245 | end 246 | } 247 | 248 | return outline 249 | end 250 | 251 | 252 | # 253 | # Examples for a scenario outline are called here. This section differs 254 | # from the Cucumber parser because here each of the examples are exploded 255 | # out here as individual scenarios and step definitions. This is so that 256 | # later we can ensure that we have all the variations of the scenario 257 | # outline defined to be displayed. 258 | # 259 | def examples(examples, outline) 260 | #log.debug "EXAMPLES" 261 | return if has_exclude_tags?(examples[:tags].map { |t| t[:name].gsub(/^@/, '') }) 262 | example = YARD::CodeObjects::Cucumber::ScenarioOutline::Examples.new(:keyword => examples[:keyword], 263 | :name => examples[:name], 264 | :line => examples[:location][:line], 265 | :comments => examples[:comments] ? examples.comments.map{|comment| comment.value}.join("\n") : '', 266 | :rows => [], 267 | :tags => [], 268 | :scenario => outline ) 269 | 270 | unless !examples[:tags].any? 271 | examples[:tags].each {|example_tag| find_or_create_tag(example_tag[:name], example)} 272 | end 273 | 274 | example.rows = [examples[:tableHeader][:cells].map{ |c| c[:value] }] if examples[:tableHeader] 275 | example.rows += matrix(examples[:tableBody]) if examples[:tableBody] 276 | 277 | # add the example to the step containers list of examples 278 | 279 | @step_container.examples << example 280 | 281 | # For each example data row we want to generate a new scenario using our 282 | # current scenario as the template. 283 | 284 | example.data.length.times do |row_index| 285 | 286 | # Generate a copy of the scenario. 287 | 288 | scenario = YARD::CodeObjects::Cucumber::Scenario.new(@step_container,"example_#{@step_container.scenarios.length + 1}") do |s| 289 | s.comments = @step_container.comments 290 | s.description = @step_container.description 291 | s.add_file(@file,@step_container.line_number) 292 | s.keyword = @step_container.keyword 293 | s.value = "#{@step_container.value} (#{@step_container.scenarios.length + 1})" 294 | end 295 | 296 | # Generate a copy of the scenario steps. 297 | 298 | @step_container.steps.each do |step| 299 | step_instance = YARD::CodeObjects::Cucumber::Step.new(scenario,step.line_number) do |s| 300 | s.keyword = step.keyword.dup 301 | s.value = step.value.dup 302 | s.add_file(@file,step.line_number) 303 | 304 | s.text = step.text.dup if step.has_text? 305 | s.table = clone_table(step.table) if step.has_table? 306 | end 307 | 308 | # Look at the particular data for the example row and do a simple 309 | # find and replace of the with the associated values. 310 | 311 | example.values_for_row(row_index).each do |key,text| 312 | text ||= "" #handle empty cells in the example table 313 | step_instance.value.gsub!("<#{key}>",text) 314 | step_instance.text.gsub!("<#{key}>",text) if step_instance.has_text? 315 | step_instance.table.each{|row| row.each{|col| col.gsub!("<#{key}>",text)}} if step_instance.has_table? 316 | end 317 | 318 | # Connect these steps that we created to the scenario we created 319 | # and then add the steps to the scenario created. 320 | 321 | step_instance.scenario = scenario 322 | scenario.steps << step_instance 323 | end 324 | 325 | # Add the scenario to the list of scenarios maintained by the feature 326 | # and add the feature to the scenario 327 | 328 | scenario.feature = @feature 329 | @step_container.scenarios << scenario 330 | 331 | end 332 | 333 | return example 334 | end 335 | 336 | # 337 | # Called when a step is found. The step is refered to a table owner, though 338 | # not all steps have a table or multliline arguments associated with them. 339 | # 340 | # If a multiline string is present with the step it is included as the text 341 | # of the step. If the step has a table it is added to the step using the 342 | # same method used by the Cucumber Gherkin model. 343 | # 344 | def step(step) 345 | #log.debug "STEP" 346 | 347 | @table_owner = YARD::CodeObjects::Cucumber::Step.new(@step_container,"#{step[:location][:line]}") do |s| 348 | s.keyword = step[:keyword] 349 | s.value = step[:text] 350 | s.add_file(@file,step[:location][:line]) 351 | end 352 | 353 | @table_owner.comments = step[:comments] ? step[:comments].map{|comment| comment.value}.join("\n") : '' 354 | 355 | multiline_arg = step[:argument] 356 | 357 | case(multiline_arg[:type]) 358 | when :DocString 359 | @table_owner.text = multiline_arg[:content] 360 | when :DataTable 361 | #log.info "Matrix: #{matrix(multiline_arg).collect{|row| row.collect{|cell| cell.class } }.flatten.join("\n")}" 362 | @table_owner.table = matrix(multiline_arg[:rows]) 363 | end if multiline_arg 364 | 365 | @table_owner.scenario = @step_container 366 | @step_container.steps << @table_owner 367 | end 368 | 369 | # Defined in the cucumber version so left here. No events for the end-of-file 370 | def eof 371 | end 372 | 373 | # When a syntax error were to occurr. This parser is not interested in errors 374 | def syntax_error(state, event, legal_events, line) 375 | # raise "SYNTAX ERROR" 376 | end 377 | 378 | private 379 | def matrix(gherkin_table) 380 | gherkin_table.map {|gherkin_row| gherkin_row[:cells].map{ |cell| cell[:value] } } 381 | end 382 | 383 | # 384 | # This helper method is used to deteremine what class is the current 385 | # Gherkin class. 386 | # 387 | # @return [Class] the class that is the current supported Gherkin Model 388 | # for multiline strings. Prior to Gherkin 2.4.0 this was the PyString 389 | # class. As of Gherkin 2.4.0 it is the DocString class. 390 | def gherkin_multiline_string_class 391 | if defined?(Gherkin::Formatter::Model::PyString) 392 | Gherkin::Formatter::Model::PyString 393 | elsif defined?(Gherkin::Formatter::Model::DocString) 394 | Gherkin::Formatter::Model::DocString 395 | else 396 | raise "Unable to find a suitable class in the Gherkin Library to parse the multiline step data." 397 | end 398 | end 399 | 400 | def clone_table(base) 401 | base.map {|row| row.map {|cell| cell.dup }} 402 | end 403 | 404 | def has_exclude_tags?(tags) 405 | if YARD::Config.options["yard-cucumber"] and YARD::Config.options["yard-cucumber"]["exclude_tags"] 406 | return true unless (YARD::Config.options["yard-cucumber"]["exclude_tags"] & tags).empty? 407 | end 408 | end 409 | 410 | end 411 | end 412 | end 413 | -------------------------------------------------------------------------------- /lib/templates/default/fulldoc/html/css/cucumber.css: -------------------------------------------------------------------------------- 1 | .summary { margin: 20px 20px 10px 20px; padding-left: 0px; } 2 | .summary * { padding: 5px 0px 5px 10px; } 3 | .feature {} 4 | .tags { font-family: monospace; font-size: 14px; } 5 | .title { padding: 10px; font-size: 24px; } 6 | 7 | .control { float: right; font-size: 13px; color: #05A; vertical-align: bottom; font-family: monospace; font-size: 14px; cursor: pointer; } 8 | 9 | .title { margin: 20px 20px 10px 20px; padding-left: 0px; border-bottom: 1px solid #E3E3E3; } 10 | .scenario .title { height: 20px; } 11 | .noborder { border: none; } 12 | #header * .title { padding: 10px; margin: 0px; border: none; } 13 | 14 | .meta .file { float: right; margin-top: 5px; margin-right: 20px; } 15 | .title .pre { color: #696969; } 16 | .title .name { font-weight: bold; color: #3F3F3F; } 17 | 18 | .meta { margin-left: 30px; color: gray; } 19 | .scenario .meta .tags { margin-left: 0px; } 20 | .feature .meta .tags { float: right; margin-right: 20px; } 21 | 22 | .suggestion { color: #ff686e; } 23 | 24 | .readme { margin: 0px 20px 0px 30px; padding: 0px; } 25 | .readme h1:first-child { border-top: none; } 26 | 27 | 28 | .feature .description, .requirements .summary { 29 | margin: 10px 20px 0px 30px; 30 | padding: 10px 20px 20px 10px; 31 | color: #343332; 32 | font-size: 16px; 33 | font-family: 'Trebuchet MS', Arial, Helvetica; 34 | } 35 | 36 | .summary .name, .tags .name { color: gray; } 37 | 38 | .odd { background-color: #F0F6F9; } 39 | .even { background-color: #FFFFFF; } 40 | .selected { background-color: #FFCC80; } 41 | 42 | 43 | #features, #scenarios { margin-top: 10px; margin-left: 20px; } 44 | 45 | 46 | #directory { margin-top: 20px; margin-left: 20px; } 47 | 48 | .scenario { margin-top: 40px; margin-left: 20px; } 49 | .requirements * .scenario { margin-top: 0px; margin-left: 0px; } 50 | 51 | .scenario .title, .transformer .title { 52 | font-size: 1.1em; font-weight: normal; font-family: Monaco, Consolas, Courier, monospace; 53 | padding: 6px 10px; margin-top: 18px; 54 | background: #e5e8ff; border: 1px solid #d8d8e5; -moz-border-radius: 3px; -webkit-border-radius: 3px; 55 | } 56 | .nodefinitions .title { 57 | background-color: #F5EADD; border: 1px solid #d8d8e5; -moz-border-radius: 3px; -webkit-border-radius: 3px; 58 | } 59 | .pending .title { 60 | background-color: #ffffc0; border: 1px solid #d8d8e5; -moz-border-radius: 3px; -webkit-border-radius: 3px; 61 | } 62 | 63 | 64 | 65 | .scenario .title { border: none; } 66 | .background .title { background: #cbddeb; } 67 | 68 | #directory .title, #features .title, #scenarios .title { padding-left: 0px; margin-left: 10px; border-bottom: 1px solid #E3E3E3; } 69 | #directory .title .name, #features .title .name, #scenarios .title .name { font-weight: normal; } 70 | 71 | .scenario .title .pre { color: #00AAD2; } 72 | .background .title .pre { color: #346F97; } 73 | 74 | .requirements .tags, .requirements .steptransformers { margin-left: 50px; margin-right: 30px; } 75 | .scenario .tags { float: right; margin-right: 20px; margin-top: 5px; } 76 | 77 | 78 | * ul.alpha { font-size: 1.1em; } 79 | * ul.alpha { padding-bottom: 10px; list-style: none; } 80 | * ul.alpha li.letter { font-size: 1.4em; padding-bottom: 10px; } 81 | * ul.alpha ul { padding-left: 15px; } 82 | * ul small { color: #666; font-size: 0.7em; } 83 | 84 | .scenario .steps { 85 | margin: 0px 20px 0px 30px; 86 | font-size: 12px; 87 | } 88 | 89 | .scenario .step, .steps .none { 90 | margin-top: 4px; 91 | padding: 4px; 92 | font-size: 14px; 93 | font-weight: bold; 94 | } 95 | .steps .none { color: #5D5C5B; } 96 | 97 | .scenario .description { 98 | padding: 10px; 99 | color: #444444; 100 | margin-right: 20px; 101 | margin-left: 20px; 102 | } 103 | 104 | .comments { margin-left: 25px; color: gray; padding: 5px; font-size: 13px; } 105 | .scenario .steps .comments { margin-left: 0px; } 106 | .feature .comments { margin-left: 30px; padding: 10px 20px 20px 10px; } 107 | 108 | .step .predicate { color: #5D5C5B; } 109 | .step .defined {} 110 | div.undefined { padding: 6px; } 111 | .step .definition { float: right; color: #343332; } 112 | .step .details .pre {} 113 | .step .details .name { font-family: monospace; color: black; } 114 | .step .defined .match { color: #346F97; } 115 | 116 | .multiline, .text { margin-top: 10px; margin-left: 20px; } 117 | .text { 118 | padding: 20px; 119 | background-color: #F8F8FF; 120 | color: #444444; 121 | border: 1px solid #DEDEDE; 122 | } 123 | 124 | /* Scenario Outline Table */ 125 | .outline { 126 | margin-top: 20px; 127 | margin-left: 40px; 128 | } 129 | .outline .keyword { 130 | float: left; 131 | margin-top: 20px; 132 | padding: 4px; 133 | font-weight: bold; 134 | font-size: 16px; 135 | } 136 | .outline .example-group-name { 137 | float: left; 138 | margin-top: 20px; 139 | padding: 4px 4px 4px 20px; 140 | font-size: 14px; 141 | color: #343332; 142 | } 143 | .multiline table tr, .outline table tr { 144 | padding: 4px; 145 | } 146 | .multiline table thead tr th, .outline table thead tr th { 147 | text-align: left; 148 | padding: 4px; 149 | background-color: #C4E0C7; 150 | } 151 | .multiline table tr td, .outline table tr td { 152 | min-width: 100px; 153 | padding: 4px 10px 4px 10px; 154 | } 155 | 156 | 157 | /* Tag page */ 158 | #tags #features, #tags #scenarios { margin-right: 20px;} 159 | #tags * .tag { font-family: monospace; font-size: 14px; cursor: pointer; } 160 | 161 | /* Overrides for scenarios in the tag view */ 162 | .tag .feature { margin-left: 20px; } 163 | .tag .scenario { margin-top: 0px; margin-left: 20px; padding-left: 30px; } 164 | .tag .scenario .title { border-bottom: none; } 165 | .tag .scenario .meta { margin-top: 0px; } 166 | 167 | 168 | /* Step Transformer Page */ 169 | .transformer { margin-top: 20px; } 170 | .transformer * .docstring { margin-left: 20px; margin-top: 10px; padding: 5px 5px 5px 10px; } 171 | .transformer .steps, .method_details_list, .steps, .method_details_list, .showSteps { margin: 10px 20px 0px 30px; } 172 | .transformer .steps .step { padding: 6px; font-size: 14px; } 173 | 174 | .showSteps { font-size: 0.9em; } 175 | .showSteps a:link, .showSteps a:visited { text-decoration: none; color: #666; } 176 | 177 | /* Tag Search Page */ 178 | #tags * li { padding: 5px; padding-left: 12px; margin: 0 0 0 10px; font-size: 1.1em; list-style: none; } 179 | #tag_search { margin: 10px 0px 10px 0px; width: 100%; min-width: 200px; height: 20px; } 180 | #tag_filtering { 181 | margin: 10px 20px 0px 30px; 182 | font-weight: bold; 183 | padding: 20px; 184 | background-color: #F8F8FF; 185 | color: #444444; 186 | border: 1px solid #DEDEDE; -moz-border-radius: 3px; -webkit-border-radius: 3px; 187 | } 188 | 189 | #command_example { 190 | margin-top: 10px; 191 | margin-left: 0px; 192 | font-size: 1.1em; font-weight: normal; font-family: Monaco, Consolas, Courier, monospace; 193 | color: #333; 194 | padding: 8px 10px; 195 | background: #A8E795; border: 1px solid #d8d8e5; -moz-border-radius: 3px; -webkit-border-radius: 3px; 196 | } 197 | 198 | 199 | #cukes_links { 200 | margin: 10px auto 10px auto; 201 | border-bottom: 1px solid #E3E3E3; 202 | width: 762px; text-align: center; 203 | padding: 10px; 204 | } 205 | 206 | /* Logo Image and Step Status Images */ 207 | 208 | #cukes_logo { 209 | margin: 0px auto 10px auto; 210 | width: 762px; 211 | height: 190px; 212 | background: url(data:image/png;base64,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%3D) no-repeat center left; 213 | } 214 | 215 | .valid { 216 | width: 15px; 217 | height: 15px; 218 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAIAAAC0tAIdAAAACXBIWXMAAA86AAAPOgGXOdvCAAABVklEQVQoFS2RO1IEMQxEJY+zzSDZlCNwAQIOxJm4DReAnICIIuFTW7U7lsxreTUzHltqtVqyPzw/HQ+3Oaebpc16EospszJ+OfP7/NePh5vHu/sRMfHw2RwxLjHOcQnlJHg42L9+vncIImOPQTbQOXPPHBmRE3o5RQMZwexmvig5pxFTMkqoLllaSZa/5ewgAoA4QEgrgXqXc9ELvbs1iItVNMCh4RgW1GzWoAaHEXV3lJjzwFeiqXD1OAjXUrjmzMw70MbrqFI5WBaTUKUbnDcW4TUTpIugcmBjg5wliRblb0gSoMO1ApxKkWrzXadmtrl6K98UN/takaLJMEGmFMtHmtMRHXCma8kXWd0cN5L7HHtdPIIJ1fAlFevcDcrFJ2JmPFYdVimTJOnQFZq45S9Xeb1tbettU6+a9WqnemLeX6ffl483BcjFdN01eyFFVHK0/FxO/9H/ezlUKYH7AAAAAElFTkSuQmCC) no-repeat center; 219 | 220 | } 221 | 222 | .invalid { 223 | width: 15px; 224 | height: 15px; 225 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAIAAAC0tAIdAAAACXBIWXMAAA86AAAPOgGXOdvCAAABWUlEQVQoFS2SvVEEMQyFJdkspFcCVAAVUREZXVAHHRAwZOQwQ3YM67X4nnwe3+3afn+S18/P9366s5yZ042R6c3c3bRKDw+WwZxf791Pt/3hMcev5QBpFtauPLoBYrYr6zcem/Xr8frUbc4c59zPPoekQeSesTm47IaD74kfTjnDbNqxC5pTzrJfQcCFBQClIh7bIdDc9S+6E8WIQXTl9somfA1S4iJVbUnJmyOhJT+51AMRkXVArMtBdUNLGcpmaReDdSIGHw4m2iyNIiMslTWqBvMuZSVZB4QhMRRp6IUjzYuPctvq3TpWumq2sIfNw/KQ3DwomfIv6aVBT+ZQMFKxhB8uLfhlzkO3Sstlwh1ja3+oEKVuqipGHhMztEGvvlZEQHwyMKPD1NDtrrpBR3jbalv90hWzo05FBr1vlKGXtvGF9fz5HG8vFRpK1a5yV0/KVuZSzO+Pf3G6tVs/+L00AAAAAElFTkSuQmCC) no-repeat center; 226 | } 227 | --------------------------------------------------------------------------------