Essays

Essays about Software Development

1 month ago

Monday, February 8, 2010

Using CSS Selectors Instead of XPath As the Default Locator Strategy in Selenium

We use Selenium to run in-browser acceptance tests in all our Rails apps via the Selenium on Rails plugin. With early versions of Selenium you had to use verbose and clunky XPath locators to reference DOM elements in the tests, such as:

//div[@class='content']/div[@class='sidebar']/a

For a while now, though, the cssQuery library has been integrated into Selenium Core, allowing you to use CSS selectors instead. For example, the above XPath locator could be written with CSS selectors as:

css=.content .sidebar a

In addition to being shorter, the Selenium documentation itself points out that most experienced users recommend CSS locators because they’re faster for the Javascript engine to parse, which means faster tests.

Making CSS Locators the Default

While the CSS locators themselves are great, it’s a minor annoyance of mine that they’re not the default locating strategy. As a result, every locator has to be prepended with css=. If you want to change this, apply the following patch to selenium-core/scripts/selenium-browserbot.js:

@@ -1107,7 +1107,7 @@
         if (locator.startsWith('document.')) {
             return this.locateElementByDomTraversal(locator, inDocument, inWindow);
         }
-        return this.locateElementByIdentifier(locator, inDocument, inWindow);
+        return this.locateElementByCss(locator, inDocument);
     };
 }

Minor Annoyances With CSS Locators

The only thing that I don’t like with CSS locators is that indexing specific sibling elements is more verbose—it must be done with nth-child():

.content .sidebar:nth-child(1) a

With XPath you can use a simple pair of brackets:

//div[@class='content']/div[@class='sidebar'][1]/a

It would be really nice if CSS selectors had the same bracket-style indexing syntax as XPath. (Of course, this is a limitation of CSS itself, not Selenium.)

In addition, nth-child() is a little more brittle in that it isn’t constrained to the current selection scope. For example, assume you have markup like so:

<div class="content">
  <div class="something_else">…</div>
  <div class="sidebar">…</div>
</div>

There is now a non-sidebar sibling <div> appearing before the sidebar <div>. In this case, there will be no element that matches .sidebar:nth-child(1)—the sidebar is matched by .sidebar:nth-child(2). This means that adding additional unrelated markup to your page can break your tests if you’re using CSS locators. If you were using the XPath locators in this case, the indexing of the element in question would remain constant because the index doesn’t refer to children, but to elements matching that specific XPath. (If you added additional sidebar <div>’s the XPath would break as well, but all the same it’s still less brittle.)

One final point to note is that there’s also a bug in the cssQuery library that prevents nth-child() from working correctly—it’s Selenium bug #698. Unfortunately, the patch posted on the bug does not fix the problem for me. Instead, you have to resort to suffixing any nth-child() selector with a child or sibling combinator like so:

.content .sidebar:nth-child(1) > a

Despite these minor wrinkles, I find that the CSS locators used with Selenium are less verbose in general and lead to faster running tests.

Comments

8 months ago

Saturday, June 13, 2009

Fixing Maximize Bookmarklets in Safari 4

When Apple released the Safari 4 public beta at the end of February, I discovered an annoying bug: the little Javascript bookmarklet that I use to maximize the browser window didn’t work anymore. I find the behavior of Safari’s green zoom button extremely annoying. It doesn’t expand the browser window to fill the screen, but instead only resizes it to fit the content of the current page. This is typically some weird height and width that causes every subsequent page you view scroll vertically and horizontally.

Of course, Apple’s Human Interface Guidelines for Resizing and Zooming Windows explain that the zoom button isn’t actually supposed to expand the window to fill the screen, but toggle the window between a standard application-defined size and a user-defined size. Regardless of what the human interface guidelines say, though, I still don’t like what it does, and I’m apparently not the only person. There are hundreds of pages asking how to fix it.

In case you’re wondering, the bookmarklet is:

javascript:self.moveTo(0,0);self.resizeTo(screen.availWidth,screen.availHeight);

I simply put the bookmarklet first in my bookmark bar, which lets me maximize Safari by hitting ⌘1. I didn’t write this little snippet of Javascript, but I can’t remember where I found it and there are versions of it all over the Web.

So, back to the real story. Upon further investigation, I determined that the bookmarklet only works when the Safari window contains a single tab. If the window has more than one tab, the bookmarklet doesn’t work. I wasn’t the only one who noticed this bug, as I found the post Safari 4 breaks bookmarklets? on This Is the Green Room.

I figured the problem would be fixed when the final version of Safari 4 was released, but when that happened this past week, the problem remained. Googling a bit, I found WebKit bug 24218, which notes that the Javascript window.moveTo and resizeTo methods do not work in Safari windows with more than one tab.

Since the bug was marked unconfirmed and didn’t have any activity on it for months, I decided to download the WebKit source and try to track down the cause of the problem. I checked out the WebKit code and built it according to the instructions. After digging a bit and setting some breakpoints, I figured out that the following method in WebCore/page/DOMWindow.cpp implements window.moveTo in the Javascript engine:

void DOMWindow::moveTo(float x, float y) const
{
if (!m_frame)
return;

Page* page = m_frame->page();
if (!page)
return;

if (m_frame != page->mainFrame())
return;

FloatRect fr = page->chrome()->windowRect();
FloatRect sr = screenAvailableRect(page->mainFrame()->view());
fr.setLocation(sr.location());
FloatRect update = fr;
update.move(x, y);
// Security check (the spec talks about UniversalBrowserWrite to disable this check…)
adjustWindowRect(sr, fr, update);
page->chrome()->setWindowRect(fr);
}

This method basically does some adjustments with the screen and window rectangles, and then finally invokes Chrome::setWindowRect to perform the actual move of the window. Stepping through the method when clicking the bookmarklet with one and two tabs showed that all the rectangle calculations were resulting in the same values regardless of the number of tabs. The invocation of setWindowRect simply wasn’t moving the window when there was more than one tab.

After a bit of indirection through some other classes, Chrome::setWindowRect calls WebChromeClient::setWindowRect in mac/WebCoreSupport/WebChromeClient.mm:

void WebChromeClient::setWindowRect(const FloatRect& rect)
{
NSRect windowRect = toDeviceSpace(rect, [m_webView window]);
[[m_webView _UIDelegateForwarder] webView:m_webView setFrame:windowRect];
}

This method basically translates the rectangle coordinates between the scale used by the window and the WebView itself, and then sends setFrame:windowRect: to WebUIDelegate. This delegate is the bridge between WebKit’s Javascript engine and the actual Cocoa browser window (I think), and WebKit is passing it the correct coordinates to move the window. WebUIDelegate, though, simply doesn’t move the window as it should when there is more than one tab, which leads me to believe the bug is in Safari itself, not WebKit.

I updated the WebKit bug and filed a new bug for Safari on Radar. I thought for a moment that this behavior might actually be intentional, to prevent pages from resizing the browser window via Javascript when there are other open tabs that would be affected. But since the bookmarklet works in Safari 3, I tend to doubt this new behavior is by design as it would break a lot of existing Javascript. Scripts embedded in an actual page are similarly unable to move or resize the window when there’s more than one tab open, so the problem doesn’t have anything to do with the Javascript being in a bookmarklet.

14 Comments

2 years ago

Tuesday, November 27, 2007

Rails Rake Tasks Plugin

Just like everyone else who develops software, we found ourselves constantly typing the same commands over and over for a variety of little tasks while developing Rails apps. Over the course of a number of projects, we developed a collection of rake tasks to automate many of them.

I find some of the tasks really useful, so we created a plugin called the Sakuzaku Rails Rake Tasks Plugin to make them available to everyone.

Some of the tasks that are particular time-savers for me are:

  • db:remigrate, which drops your database and migrates it from scratch.
  • db:migrate:all, which migrates both your development and test databases at once.
  • db:migrations:test, which runs any uncommitted migrations backwards and forwards to test them.
  • db:migrations:merge, which renames uncommitted migrations as necessary to make them follow any migrations committed while you’ve been working.
  • The testing convenience tasks, which drastically reduce the amount of typing necessary to run tests. For example, you can run all tests for the User model and User controller by doing rake user (instead of ruby test/unit/user_test.rb ; ruby test/functional/user_controller_test.rb), or just the name test on the User model with rake user:name (instead of ruby test/unit/user_test.rb -n test_name).

We’ve added this plugin to a little section of our site called the Sakuzaku Goodie Basket, which contains a growing collection of random, potentially useful, open-source stuff that we’ve developed. We’ll keep adding to the goodie basket as we have the time to pull out and write documentation for all the internal stuff we’ve developed that might be useful to other people. Watch for more additions soon.

Comments

2 years ago

Friday, October 5, 2007

I Am Building Ruby, Rails, Subversion, Mongrel, and MySQL on Mac OS X — Automatically (And So Can You!)

Update: This script has been given a permanent home in the Sakuzaku Goodie Basket. Please head over there to get the latest documentation for it.

Here’s a script to automatically download, build, and install the OS X versions of the following software:

  • Readline (5.2)
  • Ruby (1.8.6)
  • Rubygems (0.9.4)
  • Subversion (1.4.5)
  • MySQL (5.0.45)
  • Rails (latest version)
  • Mongrel (latest version)
  • Capistrano (latest version)

The steps it takes mirror those explained in Hivelogic’s great post Building Ruby, Rails, Subversion, Mongrel, and MySQL on Mac OS X. If you’re of the variety that can’t easily understand the instructions in HiveLogic’s post (for some reason your humorless software-engineer–partner insists you run the Rails app on your local machine for development), or if you’re the variety that has to do this installation samba prance dance on a frequent basis, then this might come in handy for you.

Notes

  • Before you run the script, you’ll need to have Xcode installed. Unfortunately, I can’t make the script download and install it automatically, since you need to login to with an ADC account to download it. It’s an almost 1 GB download, so plan accordingly. But real developers have Xcode installed, so it’s OK.
  • While installing the gems, you’ll probably be prompted to choose which version to install for a number of the gems. Choose the option for the highest Ruby version listed (i.e., not the ‘mswin32’ version).
  • If the script complains when installing the MySQL gem, see our post about Installing the Ruby mysql-2.7 gem on OS X 10.4.
  • Since you can only really run this script once, I have only tested it a few times, on a Powermac and a few MacBook Pros running OS X 10.4.10. If you run into any problems, send me a bug report so I can improve the script.

There’s no easy way to make the script automatically download the latest versions of the software involved (except for the gems), so I’ll try to keep the script up-to-date as new versions of the software it installs are released. (If I fall behind, just email me and I’ll post an update, and maybe send you a Sakuzaku pin badge.)

Download

Usage

  1. Open Terminal.
  2. Type:
    bash /directory/where/i/downloaded/the/file/install.sh

Sudo will prompt you for your administrator password after you run the script, and then blast off with a flurry of downloading and compiling. You’ll have to do some clickety-clicking to help MySQL install itself. Then sit back — you just saved 15 minutes (or 4 hours).

12 Comments

2 years ago

Saturday, September 29, 2007

Creating a Self-signed SSL Certificate for Apache 2 on Debian lenny

While setting up SSL for Apache 2 on Debian lenny for our development server, I got stuck when all the instructions I could find (1, 2) said to use the script apache2-ssl-certificate, which wasn’t installed on the system.

After some more Googling I found Ubuntu bug #77675 in Ubuntu’s apache2 package, which notes that apache2-ssl-certificate is missing. Debian bug #398520 indicates it’s also a problem in Debian. Both bugs indicate that the solution is to use make-ssl-cert instead. So, this is what I originally did.

Yesterday, however, the certificate expired, and I noticed it had only been created to be valid for 30 days. So, I went through the trouble of regoogling everything I just mentioned above in order to refresh my memory about how to create a certificate. This time, however, I read to the bottom of both bug reports. They mention that the default lifetime for make-ssl-cert is only 30 days. Debian bug #293821 mentions the problem, and even seems to include a patch for make-ssl-cert that gives it a -days option. The patch doesn’t seem to have made it into lenny, though.

So, I copied /usr/sbin/make-ssl-cert to /etc/apache2 and modified it myself. All you need to do is add the -days option to the openssl line near the bottom:

94c118
<     openssl req -config $TMPFILE -new -x509 -nodes -out $output -keyout $output -days 1095 > /dev/null 2>&1

>     openssl req -config $TMPFILE -new -x509 -nodes -out $output -keyout $output > /dev/null 2>&1

While I was at it, I copied /usr/share/ssl-cert/ssleay.cnf and modified it, too, by removing the required country, state, locality, and organizational unit fields in the certificate, since I didn’t have anything useful to enter for them:

1
2
3
4
5
6
13a14,16
> countryName                     = @CountryName@
> stateOrProvinceName             = @StateName@
> localityName                    = @LocalityName@
14a18
> organizationalUnitName          = @OUName@

I also changed the relevant lines in my new make-ssl-cert:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
12c12
<     templates="organisationname hostname email"

>     templates="countryname statename localityname organisationname ouname hostname email"
23a24,35
>      db_get make-ssl-cert/countryname
>      CountryName="$RET"
>      db_fset make-ssl-cert/countryname seen false
> 
>      db_get make-ssl-cert/statename
>      StateName="$RET"
>      db_fset make-ssl-cert/statename seen false
> 
>      db_get make-ssl-cert/localityname
>      LocalityName="$RET"
>      db_fset make-ssl-cert/localityname seen false
> 
27a40,43
>      db_get make-ssl-cert/ouname
>      OUName="$RET"
>      db_fset make-ssl-cert/ouname seen false
> 
37a54,56
>      CountryName="XX"
>      StateName="There is no such thing outside US"
>      LocalityName="Everywhere"
38a58
>      OUName="Office for Complication of Otherwise Simple Affairs"
44c64,68
<     sed -e s#@OrganisationName@#"$OrganisationName"# 

>     sed -e s#@CountryName@#"$CountryName"# 
>       -e s#@StateName@#"$StateName"# 
>       -e s#@LocalityName@#"$LocalityName"# 
>       -e s#@OrganisationName@#"$OrganisationName"# 
>       -e s#@OUName@#"$OUName"# 

Then, all you have to do is run the following:

/etc/apache2/make-ssl-cert /etc/apache2/ssleay.cnf /etc/apache2/ssl

This should create the necessary certificate in /etc/apache2/ssl as explained by all the other instructions.

The patched scripts are available in their entirety below. I also posted the diffs above, in case you have different versions of ssleay.cnf and make-ssl-cert and you want to patch them.

2 Comments

Next Page