# frozen_string_literal: true require 'cucumber/gherkin/formatter/ansi_escapes' require 'cucumber/core/test/data_table' require 'cucumber/deprecate' module Cucumber module Glue # Defines the basic API methods availlable in all Cucumber step definitions. # # You can, and probably should, extend this API with your own methods that # make sense in your domain. For more on that, see {Cucumber::Glue::Dsl#World} module ProtoWorld # Run a single Gherkin step # @example Call another step # step "I am logged in" # @example Call a step with quotes in the name # step %{the user "Dave" is logged in} # @example Passing a table # step "the following users exist:", table(%{ # | name | email | # | Matt | matt@matt.com | # | Aslak | aslak@aslak.com | # }) # @example Passing a multiline string # step "the email should contain:", "Dear sir,\nYou've won a prize!\n" # @param [String] name The name of the step # @param [String,Cucumber::Test::DocString,Cucumber::Ast::Table] multiline_argument def step(name, raw_multiline_arg = nil) super end # Run a snippet of Gherkin # @example # steps %{ # Given the user "Susan" exists # And I am logged in as "Susan" # } # @param [String] steps_text The Gherkin snippet to run def steps(steps_text) super end # Parse Gherkin into a {Cucumber::Ast::Table} object. # # Useful in conjunction with the #step method. # @example Create a table # users = table(%{ # | name | email | # | Matt | matt@matt.com | # | Aslak | aslak@aslak.com | # }) # @param [String] text_or_table The Gherkin string that represents the table # Returns a Cucumber::MultilineArgument::DataTable for +text_or_table+, which can either # be a String: # # table(%{ # | account | description | amount | # | INT-100 | Taxi | 114 | # | CUC-101 | Peeler | 22 | # }) # # or a 2D Array: # # table([ # %w{ account description amount }, # %w{ INT-100 Taxi 114 }, # %w{ CUC-101 Peeler 22 } # ]) # def table(text_or_table) MultilineArgument::DataTable.from(text_or_table) end # Print a message to the output. # # @note Cucumber might surprise you with the behaviour of this method. Instead # of sending the output directly to STDOUT, Cucumber will intercept and cache # the message until the current step has finished, and then display it. # # If you'd prefer to see the message immediately, call {Kernel.puts} instead. def puts(*messages) Cucumber.deprecate( 'Messages emitted with "puts" will no longer be caught by Cucumber ' \ 'and sent to the formatter. If you want message to be in the formatted output, ' \ "please use log(message) instead.\n" \ 'If you simply want it in the console, '\ 'keep using "puts" (or Kernel.puts to avoid this message)', 'puts(message)', '5.0.0' ) messages.each { |message| log(message.to_s) } end # Pause the tests and ask the operator for input def ask(question, timeout_seconds = 60) super end # Embed an image in the output def embed(file, mime_type, _label = 'Screenshot') Cucumber.deprecate( 'Please use attach(file, media_type) instead', 'embed(file, mime_type, label)', '5.0.0' ) attach(file, mime_type) end def log(*messages) messages.each { |message| attach(message.to_s.dup, 'text/x.cucumber.log+plain') } end def attach(file, media_type) super end # Mark the matched step as pending. def pending(message = 'TODO') raise Pending, message unless block_given? begin yield rescue Exception # rubocop:disable Lint/RescueException raise Pending, message end raise Pending, "Expected pending '#{message}' to fail. No Error was raised. No longer pending?" end # Skips this step and the remaining steps in the scenario def skip_this_scenario(message = 'Scenario skipped') raise Core::Test::Result::Skipped, message end # Prints the list of modules that are included in the World def inspect super end # see {#inspect} def to_s inspect end # Dynamially generate the API module, closuring the dependencies def self.for(runtime, language) # rubocop:disable Metrics/MethodLength Module.new do # rubocop:disable Metrics/BlockLength def self.extended(object) # wrap the dynamically generated module so that we can document the methods # for yardoc, which doesn't like define_method. object.extend(ProtoWorld) end # TODO: pass these in when building the module, instead of mutating them later # Extend the World with user-defined modules def add_modules!(world_modules, namespaced_world_modules) add_world_modules!(world_modules) add_namespaced_modules!(namespaced_world_modules) end define_method(:step) do |name, raw_multiline_arg = nil| location = Core::Test::Location.of_caller runtime.invoke_dynamic_step(name, MultilineArgument.from(raw_multiline_arg, location)) end define_method(:steps) do |steps_text| location = Core::Test::Location.of_caller runtime.invoke_dynamic_steps(steps_text, language, location) end define_method(:ask) do |question, timeout_seconds = 60| runtime.ask(question, timeout_seconds) end define_method(:attach) do |file, media_type| runtime.attach(file, media_type) end # Prints the list of modules that are included in the World def inspect modules = [self.class] (class << self; self; end).instance_eval do modules += included_modules end modules << stringify_namespaced_modules format('#<%s:0x%x>', modules: modules.join('+'), object_id: object_id) end private # @private def add_world_modules!(modules) modules.each do |world_module| extend(world_module) end end # @private def add_namespaced_modules!(modules) @__namespaced_modules = modules modules.each do |namespace, world_modules| world_modules.each do |world_module| variable_name = "@__#{namespace}_world" inner_world = if self.class.respond_to?(namespace) instance_variable_get(variable_name) else Object.new end instance_variable_set(variable_name, inner_world.extend(world_module)) self.class.send(:define_method, namespace) do instance_variable_get(variable_name) end end end end # @private def stringify_namespaced_modules @__namespaced_modules.map { |k, v| "#{v.join(',')} (as #{k})" }.join('+') end end end # @private AnsiEscapes = Cucumber::Gherkin::Formatter::AnsiEscapes end end end