0

I'm using SitePrism to create some POM tests. One of my page classes looks like this:

class HomePage < SitePrism::Page
    set_url '/index.html'
    element :red_colour_cell, "div[id='colour-cell-red']"
    element :green_colour_cell, "div[id='colour-cell-green']"
    element :blue_colour_cell, "div[id='colour-cell-blue']"

    def click_colour_cell(colour)
        case colour
            when 'red'
                has_red_colour_cell?
                red_colour_cell.click
            when 'green'
                has_green_colour_cell?
                green_colour_cell.click
            when 'blue'
                has_blue_colour_cell?
                blue_colour_cell.click
        end
    end
end

The method click_colour_cell() get its string value passed from a Capybara test step that calls this method. If I need to create additional similar methods in the future, it can become rather tedious and unwieldy having so many case switches to determine the code flow.

Is there some way I can create a variable that is dynamically named by the string value of another variable? For example, I would like to do something for click_colour_cell() that resembles the following:

    def click_colour_cell(colour)
        has_@colour_colour_cell?
        @colour_colour_cell.click
    end

where @colour represents the value of the passed value, colour and would be interpreted by Ruby:

    def click_colour_cell('blue')
        has_blue_colour_cell?
        blue_colour_cell.click
    end

Isn't this what instance variables are used for? I've tried the above proposal as a solution, but I receive the ambiguous error:

syntax error, unexpected end, expecting ':'
    end
    ^~~ (SyntaxError)

If it is an instance variable that I need to use, then I'm not sure I'm using it correctly. if it's something else I need to use, please advise.

2 Answers 2

3

Instance variables are used define properties of an object.

Instead you can achieve through the method send and string interpolation.

Try the below:

def click_colour_cell(colour)
  send("has_#{colour}_colour_cell?")
  send("#{colour}_colour_cell").click
end

About Send:

send is the method defined in the Object class (parent class for all the classes).

As the documentation says, it invokes the method identified by the given String or Symbol. You can also pass arguments to the methods you are trying to invoke.

On the below snippet, send will search for a method named testing and invokes it.

class SendTest
  def testing
    puts 'Hey there!'
  end
end


obj = SendTest.new
obj.send("testing")
obj.send(:testing)

OUTPUT

Hey there!
Hey there!

In your case, Consider the argument passed for colour is blue,

"has_#{colour}_colour_cell?" will return the string"has_blue_colour_cell?" and send will dynamically invoke the method named has_blue_colour_cell?. Same is the case for method blue_colour_cell

Sign up to request clarification or add additional context in comments.

2 Comments

I can confirm that this does work. But how? I am not familiar with the send method and the provided documentation describes a different purpose for send that what I may be using it for. I am really interested in understanding more on how this actually works.
Thanks for the explanation. I found this solution to be the most useful for my situation so far. However, I would like to limit my use of send() in the future and consider other solutions if offered.
1

Direct answer to your question

You can dynamically get/set instance vars with:

instance_variable_get("@build_string_as_you_see_fit")
instance_variable_set("@build_string_as_you_see_fit", value_for_ivar)

But...

A Warning!

I think dynamically creating variables here and/or using things like string-building method names to send are a bad idea that will greatly hinder future maintainability.

Think of it this way: any time you see method names like this:

click_blue_button
click_red_button
click_green_button

it's the same thing as doing:

add_one_to(1)   // instead of 1 + 1, i.e. 1.+(1)
add_two_to(1)   // instead of 1 + 2, i.e. 1.+(2)
add_three_to(1) // instead of 1 + 3, i.e. i.+(3)

Instead of passing a meaningful argument into a method, you've ended up hard-coding values into the method name! Continue this and eventually your whole codebase will have to deal with "values" that have been hard-coded into the names of methods.

A Better Way

Here's what you should do instead:

class HomePage < SitePrism::Page
  set_url '/index.html'

  elements :color_cells, "div[id^='colour-cell-']"

  def click_cell(color)
    cell = color_cells.find_by(id: "colour-cell-#{color}") # just an example, I don't know how to do element queries in site-prism
    cell.click
  end
end

Or if you must have them as individual elements:

class HomePage < SitePrism::Page
  set_url '/index.html'

  COLORS = %i[red green blue]

  COLORS.each do |color|
    element :"#{color}_colour_cell", "div[id='colour-cell-#{color}']"
  end

  def cell(color:)                 # every other usage should call this method instead
    @cells ||= COLORS.index_with do |color|
      send("#{color}_colour_cell") # do the dynamic `send` in only ONE place
    end
    @cells.fetch(color)
  end
end

home_page.cell(color: :red).click

3 Comments

I think this may be a more suitable approach in a situation where I have multiple elements that follow a pattern, or a single element that can receive a dynamically named CSS selector as its ID. I already have such a situation. I agree that for maintainability purposes, this may be more efficient (and elegant) going forward. I will try it and report my results.
The only problem with this is it reduces the value of what SitePrism is for. Although it may help with the maintenance of methods, It becomes increasingly difficult to maintain the elements, should new ones be introduced into the pattern or existing ones changed.
As the current maintainer of siteprism I'd say this is a reasonable way to go. It's not perfect and it's not the way I would exactly code it. But it's a good start. If you're finding you have 5/10/15 colours and you want to generate large amounts of elements for these, I would store them in a code-agnostic way (So a yml file), then pull them in (Instead of using a constant). This way you keep the bit that is less code relevant (How many colours you have), away from the bit that is code-relevant (The elements and helpers for each one).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.