The Dialect Diary – Element Definitions

Dialect needs to support the idea of element definitions. Like assertion definitions, this is part of the generator concept, where methods are generated as part of Dialect’s operation. This is a core part of Dialect’s design that has to be as right as possible since pretty much everything else will flow from this.

Element definitions are methods that can be called directly on a page or activity definition. These methods assert a particular aspect of interaction, either with a web page or a web activity (like an API call). A method is generated by Dialect when an element definition is encountered. That method will act to hook the underlying driver library into the object represented by the element definition.

For this post, I’ll be talking about the feature/element-generator branch. For this example, I’m going to be using the Stardate Calculator page of my Dialogic test app. This page has a series of elements that can be put to the test. The first thing you have to do, upon visiting the page, is click a checkbox to enable the form. So I first have to handle finding this checkbox and then manipulating it. Before I consider that, however, I have to allow a test script writer the means by which to specify that the checkbox is (or should be) on the page. That’s where the element definition concept comes in.

Let’s consider how I could reference this checkbox via the Watir-WebDriver library.

Here I get a reference to the element, I call the set() method on that element and then I call the set?() predicate method to check if the element was set. Notice the locator part, (id: ‘enableForm’), which is critical. Normally I would probably give the above a friendly identifier that I could use to refer to that particular element:

The Element Definition

So here’s what I want to support, in terms of an element definition being specified as part of the page definition:

Notice there that a script writer first provides the web element, in this case checkbox. Then a friendly name is provided as a symbol. You could also do this as a string. Given how Ruby handles strings and symbols, you could do this:

Then a locator is used, which tells a browser driver how to recognize the element.

Here, much like url_is() and title_is(), which I discussed in the evaluators post, checkbox() is actually a method. That method is in the elements generator module (elements.rb). As with the assertion module, you can see by looking at dialect.rb (line 24), that I extend the element module.

With that in place, let’s try this script:

Here you can see how how that’s working via the expectations. But let’s give the simple view:

So basically all I’ve done is attach Watir-WebDriver directly to the friendly name of an element. The friendly name is standing in for the way Watir-WebDriver would expect you to locate the element. So this:

becomes:

You can see that the locator gets wrapped up in a friendly name element that is on a page definition. The page definition itself wraps up the driver. What this means is that Dialect can insulate you from the specifics of the library in use, but can also allow you to make calls on that library.

A framework design choice comes into play here. Should I provide for alternative actions? For example, what if I wanted someone to be able to use actions like “check” and “uncheck” for a checkbox? In other words, I want to allow something like this:

I could attempt to just add to the libary itself using Ruby’s mechanism for open classes. So I could do this:

There’s a possible downside to this. It ties me into the specific library and the specific implementation of the library. Then again, I’m already supporting the Watir-WebDriver platform so if there were changes to it, presumably the above would make me no worse off. The other thing I could do is provide my own methods that are variations on calling the friendly name. I actually explored this idea a bit in my other test frameworks, Symbiont and Fluent. So, for example, I could generate a method for any checkboxes that let you do this:

The last underscore contained an “addition” to the friendly name. The more I look at it, I can’t believe I went that route with my other tools, however. It seems so … messy. It conflates the friendly name with an additive bit that you have to remember. I even allowed certain methods to be prefixed rather than suffixed. So the above could also have been:

Note that I did not allow:

That’s probably “obvious” when I explain it here but iamgine how someone has to learn all that. And if you have different prefix/suffix methods for different elements, you complicate your API. Again, looking at it now, I’m amazed I did something so stupid. (Then again, it’s far from the first time I’ve done something stupid and, unfortunately, probably far from the last time as well.) Incidentally, this is all very similar to the way the page-object gem works, which is why I’ve come to not recommend that gem to people.

Incidentally, this does bring up a point: how would I handle uncheck? I can’t just alias set like I did because it would have to check if it’s not checked, right? Well, try it.

Now use this logic:

Yup, as suspected: doesn’t work. But I could do something like this:

Of course, looking into the Watir API, there’s an easier way: I could just alias the clear method, which appears to use set(false). So I could do this:

Handling a checked? method is easy. Just alias the set? predicate method.

Now the test script code reads:

I should note as well that someone can always use that old standby of “clicking” something.

Another design question comes up. Should I provide default actions? For example, what if someone does this?

With Dialect, that just returns an object reference. In Symbiont and Fluent, this would often have a default action. For example, if the element was a button the above would click it. If the above was a text field, the above would grab the value from it.

Again, here’s an area where it seems like I was confusing people with the API. When you saw the line above, you had no idea — from that line alone — what the default action was (or even necessarily that there was a “default action”). You had to look up into the page definition and see the object and then remember that if the object was a button, the default action was click. It actually worked really well — if your framework was just a tight API for only you to use!

I’m starting to realize, as part of this Dialect project, that I way overcomplicated the API of Symbiont and thus of Fluent. I actually would have made it harder for people who already knew Watir-WebDriver or Selenium-WebDriver to use my framework. They would have had certain expectations that my frameworks either did not adhere to or adhered to, but in a modified way.

Other Elements … Streamline the Code

Currently radios, text fields, links and buttons work pretty much the same way. A friendly name for the object wraps around the driver so that you can make any calls on that friendly name that you would make directly on an instance of the element when using Watir-WebDriver. And here I get into an interesting aspect of design. Originally in elements.rb, I had methods like this:

And in platform_object.rb I had:

I quickly realized that this would lead me to define a new method for any widget that ended up getting supported by Watir. If you compare this with what I had in Fluent’s generators.rb and platform_object.rb, as just one example, you’ll see what a mess the code can become if you don’t take care to think about your framework design. Again, I keep finding myself a little bit in shock over how frightfully bad some of my past design decisions were.

Contrast that with the platform_object.rb in Dialect, specifically lines 53 to 57. I do something similar in elements.rb, lines 5 to 12. This makes for some very lean code.

Is this Working?

A little bit of my instinct keeps trying to tell me that I’m painting myself into a corner with Dialect — if one of my goals is to allow for multiple libraries under one umbrella. Meaning, I keep wrapping up what seems to be Watir specific support and I don’t know if that’s ruling me out of Selenium or Capybara or whatnot. That said, low-level Selenium is always a mistake to me. That’s why Watir and Capybara exist: to make Selenium much less of a pain to work with. And, as I showed with the evaluators design, it’s possible for me to support the language of another library without having to support the library directly.

As I said before, the above open class approach tied me a bit to Watir-WebDriver, which was fine since the framework is in fact predicated upon Watir-WebDriver. But I also mentioned in an earlier post that I’m toying around with supporting Selenium-WebDriver directly as well, rather than just relying on the fact that Watir is a higher-level API on top of Selenium. However, one thing that’s kind of cool about the above open class concept is that I don’t have to provide that logic as part of Dialect. That could be left to people who are using Dialect. Or I could provide an “Extended” module or something as part of Dialect that people could use if they wanted, but didn’t have to.

Do note that if I built in the open class mechanism AND if I supported Selenium-WebDriver directly, just as I do Watir-WebDriver, that would mean I would, at all times, require the same open classing against both libraries so that the Dialect API could be the same across drivers.

In any event, this element definition was a core part of the Dialect design. This seems to work well and the current design has certainly streamlined the excuse I had for a design in my other frameworks. My next steps are to build off of this idea, adding more elements for Dialect to support as well as consideration for elements that are in a context, such as a modal window or a frame.

Share

About Jeff Nyman

Anything I put here is an approximation of the truth. You're getting a particular view of myself ... and it's the view I'm choosing to present to you. If you've never met me before in person, please realize I'm not the same in person as I am in writing. That's because I can only put part of myself down into words. If you have met me before in person then I'd ask you to consider that the view you've formed that way and the view you come to by reading what I say here may, in fact, both be true. I'd advise that you not automatically discard either viewpoint when they conflict or accept either as truth when they agree.
This entry was posted in Dialect. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *