geb/geb — pull request #25 by perezrathke (2012-05-10 19:17:23+00:00)
Title: OnLoad Client Side Dom Processing

On many pages with very large DOMs (10K+ elements), we've found that Geb navigation selectors alone can be cripplingly slow. However, CSS selectors may not produce sufficiently narrow results.

For example, if we have a large page with with the following segment:

```html

Book Title
Grails in Action

Publisher
Manning Publications Co

Author
Glen Smith and Peter Ledbrook

```
When writing a test or automation script for this page, we are more likely to care about the value of the `td` elements, but something writing an ordinal selector like `$('#book-details td', 0)` or `$('#book-details td', 1)` is often too rigid. For example, if the 'Publisher' row only shows up if it's not null, all the proceeding ordinals will be thrown off.

Of course, Geb is a rock-solid DSL designed to handle these scenarios elegantly. If performance weren't an issue, we could always do `$('#book-details th', text: startsWith('Author')).next('td)` but, as far as we can tell, this roughly translates to:

* Iterate over all the `th` elements in `#book-details`
* Pass each element, one at a time, from the browser to Geb for runtime evaluation of `text: startsWith('Author')`

If your selection list is relatively small and your browser is local, performance isn't too bad. But if we're iterating over 100s of similar elements and the browser sits across a network (e.g. Selenium Grid), we've found this sort of thing to be painfully slow.

In these cases, we've found ourselves doing something like:

```

js.exec("""
$('#book-details th')​​​​​​​​​.each(function(i, el) {
var fieldName = $(el).text()
.replace(/\s/g, '-')
.toLowerCase();
$(el).siblings('td').addClass('__' + fieldName + '_value');
});
""")

$('#book-details .__author_value')
```

Effectively, we're preprocessing the DOM to create prettier, more queryable elements. Since all the preprocessing logic sits client-side, Geb only needs to make one round trip to find the right element. We think this approach can be pretty useful when working with remote test execution. We also think it might be useful to have this functionality baked into Geb core as an option.

To this end, we hope to push more integrated support for JavaScript DOM processing to the Geb Page model. The goals are to

1. Allow an optional and simple mechanism for the client to specify which javascript files are to be used for processing the page
2. When the Page is loaded, the javascripts are executed
3. Add a hook "decorateDom()" which the client may use to force the javascripts to be processed

We have outlined several approaches to achieve this goal:

## Option 1. Create DomDecoratingSupport delegate and add to Page API using @Delegate annotation _(current implementation for this pull request)_

The delegate would have a constructor that takes the Page instance. The delegate would expose a function decorateDom() and various getter methods for specifying the dom decoration script names. If no scripts are specified, the delegate will default to checking to see if a script with the same class name as the client Page exists. If a non-empty list of scripts to execute is found, the delegate will execute these scripts, else no scripts will be executed.

To satisfy the goal of executing scripts when the page is loaded, this can be facilitated in two ways:

1. Create a PageOnLoadListener interface that can be registered with each Page. The Browser then calls page.informPageOnLoadListeners() when switching pages. In practice, the DomDecoratingSupportDelegate will register a listener when constructed. The listener in turn will call decorateDom() when the page is loaded. This listener based approach also allows future delegate classes to hook the page loaded callback. Note, this is the implementation we have chosen for this pull request.

1. Instead, the Page onLoad() member method could also be overloaded to automatically call decorateDom() whenever the page is loaded. A drawback is that the Page class is now coupled to the DomDecoratingSupport delegate via the explicit decorateDom() call within the Page onLoad() method.

## Option 2. Create a DomDecoratingSupport mixin using @Mixin annotation

The Groovy @Mixin annotation allows arbitrary methods to be "mixed-in" from a parameter class object. Similar to the delegate approach, the mixin class would contain a decorateDom() method which uses reflection to check for the existence of a static getJsFileNames() method. The mixin would also expose a new operator 'q' which is analogous to '$' except that 'q' also implicitly calls decorateDom() before evaluating a selector pattern.

Some drawbacks are that the @Mixin annotation is a runtime manipulation of the metaclass and therefore is not reverse compatible into a java project. Furthermore, I have not found a way to robustly overload the Page onLoad() method (hence the existence of the 'q' operator). It should be noted that this solution avoids coupling the Page to the mixin. Additionally, if there is a way to robustly overload Page onLoad() via the mixin, then this would be a very modular groovy-only solution.

## Option 3. Create a local AST Transform

The goal of this approach would be to introduce an annotation @DecorateDomWith([List of Filenames]) for use with derived Page classes. The annotation would use AST transforms to add the decorateDom() method as well as robustly overload the Page onLoad() method.

The drawbacks are that it is unclear how much AST transforms are coupled to specific versions of Groovy. The AST may be fairly stable, in which case using raw transforms may be okay. However, if AST builders are used, these may not be compatible to version of Groovy less than 1.7.

## Next Steps

We are interested in your thoughts, particularly in regards to whether DomDecoration is of interest to the Geb project and, if so, whether you have any alternative approaches for implementation. Also, we're not 100% certain what to call these features - DomDecorating is simply our current favorite. However, this functionality is somewhat generic as it's simply a mechanism for specifying javascripts to execute when a page is loaded.