Testing Seaside Components

C. David Shaffer
Shaffer Consulting

Contents

  1. Getting started
  2. A Simple Test
  3. Following Anchors
  4. Forms, Input Components And Checking For An Answer
  5. The infamous back button
  6. Finding XHTML elements
  7. Testing javascript-enabled components using an external browser
  8. Testing remote applications
  9. Miscellaneous topics

Getting started

You should be familiar with Seaside especially through the Seaside Book and also with the SUnit framework. You should load SeasideTesting into your Smalltalk image:

The framework (and your test cases) must be in the same image as your Seaside server. Later in this tutorial we will show how to test a remote application.

SUnitToo users: There is a package called SeasideTesting-VW-SUnitToo that adds an abstract test class for that framework.

A simple test

Let's go test first...we wish to create a component which satisfies the following requirement:

The HelloComponent will display an HTML element with id 'main' which will contain the text 'hello'.

First we write a test...the SeasideTesting framework contains an abstract subclass of TestCase called STTestCase. It adds browser-like actions (follow link, submit form, back, refresh), searching (find a HTML element satisfying some criteria) and support for configuring and accessing the component being tested in the Seaside server. Here is the class definition for our first test case:

Smalltalk defineClass: #HelloComponentTest
	superclass: #{Seaside.STTestCase}
	indexedType: #none
	private: false
	instanceVariableNames: ''
	classInstanceVariableNames: ''
	imports: ''
	category: ''
and a method which checks that our requirement is satisfied:
HelloComponentTest>>testMainElementContainsHello
	self newApplicationWithRootClass: HelloComponent.
	self establishSession.
	self assert: (self lastResponse stringWithId: 'main') = 'hello'

Line 2 of testMainElementContainsHello creats a new Seaside application entry point whose root is HelloComponent. We haven't created this class yet so you may have to tell your Smalltalk IDE to "leave it undeclared."

Line 3 establishes a Seaside session with the application by requesting the root page. The method establishSession answers an instance of STSeasideResponse. This response can be later retrieved using self lastResponse.

Finally line 4 uses the stringWithId: convenience method to find the HTML element whose ID is "main" and pull out its string contents, asserting that this string is 'hello'.

At this point you should run this test case in a standard SUnit TestRunner. The test should fail, of course, since we haven't created the component yet.

Now let's implement a component to pass this test

Smalltalk defineClass: #HelloComponent
	superclass: #{Seaside.WAComponent}
	indexedType: #none
	private: false
	instanceVariableNames: ''
	classInstanceVariableNames: ''
	imports: ''
	category: ''

renderContentOn: html 
	html span
          id: 'main';
          with: 'hello'

Run the HelloComponentTest again to verify that it passes. You may want to add a your own entry point with this component as the root so you can view the component in your browser. This is not a requirement of SeasideTesting...there is no need for each of your components to have an entry point. When I'm developing tests I like to avoid thinking of Seaside components as visual entities, and focus on functional requirements for as long as possible so normally I avoid creating this entry point until I really need to look at the component in a browser.

Following anchors

OK, what about a component that has anchors? Let's add the following requirement:
The HelloComponent will display an anchor labeled with 'Leaving' which, when clicked, will display a new page containing the word 'Goodbye'.
And our corresponding test:
HelloComponentTest>>testClickingLeavingAnchorShowsGoodbye
	self newApplicationWithRootClass: HelloComponent.
	self establishSession.
	(self lastResponse anchorWithLabel: 'Leaving') click.
	self assert: (self lastResponse containsString: 'Goodbye')

Notice the duplication between this and the previous test method. In a later section we'll pull out these duplicate lines and put them in the standard SUnit setUp method. On line 4 we see how to handle clicking on an anchor. We ask the last response for the anchor with the specified label and we send the resulting object the click message. In response to click, SeasideTesting will simulate the browser's request to the application and the response (a STSeasideResponse) is available by sending self lastResponse. Finally we test that this response contains the required string using the containsString: convenience method.

Run this test and observe that it fails (as expected!) with an error indicating that the requested anchor doesn't exist.

Let's modify our component to satisfy this requirement. Replace the original HelloComponent>>renderContentOn: with the following:

HelloComponent>>renderContentOn: html 
	html span
          id: 'main';
          with: 'hello'.
	html anchor
		callback: [self inform: 'Goodbye'];
		with: 'Leaving'

Run your tests (this one and the previous one) to verify that they both pass.

A note before we go on: searching the DOM

The method STSeasideResponse>>anchorWithLabel: is one of many search methods in the SeasideTesting. These methods traverse the DOM and return one or more, depending on the method, XHTML elements which we call entities in SeasideTesting. These are concrete STSeasideEntity instances. The actual type of entity depends on the type of the XHTML element (anchor elements produce STSeasideAnchor instances, for example). We will see a few other ways to find elements in the DOM so don't be surprised (or disgusted) as we incrementally reveal these methods. In Finding XHTML elements we will go over many of the commonly used search methods.

Forms, Input Components And Checking For An Answer

In this section we will take the test-first approach to creating a simple editor for concert information. Since we are only interested in testing the user interface, the domain model is just given here:

Smalltalk defineClass: #Concert
	superclass: #{Core.Object}
	indexedType: #none
	private: false
	instanceVariableNames: 'title description date time location artist free '
	classInstanceVariableNames: ''
	imports: ''
	category: ''

artist
    ^ artist

artist: anObject
    artist := anObject

date
    ^ date

date: anObject
    date := anObject

description
    ^ description

description: anObject
    description := anObject

location
    ^ location

location: anObject
    location := anObject

time
    ^ time

time: anObject
    time := anObject

title
    ^ title

title: anObject
    title := anObject

makeFree
    free := true

makePaymentRequired
    free := false

isFree
    ^free ifNil: [false]
Now, our test case:
Smalltalk defineClass: #ConcertEditorTest
	superclass: #{Seaside.STTestCase}
	indexedType: #none
	private: false
	instanceVariableNames: ''
	classInstanceVariableNames: ''
	imports: ''
	category: ''

All of our test methods will be testing the same kind of component so it makes sense to override the SUnit TestCase>>setUp method to register the application and establish a session:

ConcertEditorTest>>setUp
   self newApplicationWithRootClass: ConcertEditor.
   self establishSession

Note, we haven't defined the class ConcertEditor yet so you may have to fight with your Smalltalk IDE to force it to accept the definition of this method.

Buttons

We will begin with requirements that help us illustrate manipulating submit button elements in SeasideTesting.

The concert editor will have two buttons. One labeled "Save" and the other labeled "Cancel".
ConcertEditorTest>>testHasSaveAndCancelButtons
   self assert:
      (self lastResponse buttonWithValue: 'Save') notNil.
   self assert:
      (self lastResponse buttonWithValue: 'Cancel') notNil.

Note for XHTML buttons the text displayed on the button is called its "value", so the method buttonWithValue: searches the DOM for a single XHTML INPUT tag with the specified value. The notNil check is useless since buttonWithValue: would signal an error if the button didn't exist but I feel safer leaving it in.

Verify that this test fails (we haven't even created the component yet!) and then implement the code to satisfy this requirement:

Smalltalk defineClass: #ConcertEditor
	superclass: #{Seaside.WAComponent}
	indexedType: #none
	private: false
	instanceVariableNames: ''
	classInstanceVariableNames: ''
	imports: ''
	category: ''

renderContentOn: html

	html submitButton value: 'Cancel'.
	html submitButton value: 'Save'
When the cancel button is pressed, the editor will answer nil.
ConcertEditorTest>>testCancelAnswersNil
	(self lastResponse buttonWithValue: 'Cancel') click.
	self assert: self answer isNil

Just as with anchors, sending click to a submit button causes SeasideTesting to simulate the browser's response to a user's click on this button. The method STTestCase>>answer signals an error if the component hasn't answered, otherwise it returns the component's answer. Run this test and observe the failure message "form cannot be nil." SeasideTesting is telling us that it can only click on a button if it is in a form. We need to put our buttons inside a form and make the "Cancel" button answer nil:

renderContentOn: html

	html form with:
		[html submitButton 
			callback: [self answer: nil];
			value: 'Cancel'.
		html submitButton value: 'Save']

Now run the test, it should pass. Finally we need to have some requirements about how the editor modifies a Concert instance. Rather than writing detailed requirements, for the sake of this tutorial we just give a rather vague one:

When the save button is pressed, the editor will answer a Concert instance (hereafter referred to as "the Concert").
ConcertEditorTest>>testSaveAnswersConcert
	(self lastResponse buttonWithValue: 'Save') click.
	self assert: self answer notNil.
	self assert: (self answer isKindOf: Concert)
We've given a loose interpretation of the word "edited" here. Later requirements will make this more explicit. Run this test...it should fail with a message "component did not answer." To pass this test we modify the ConcertEditor to have a concert i-var, to initialize it properly and to answer it when "Save" is clicked. Here is the entire ConcertEditor class:
Smalltalk defineClass: #ConcertEditor
	superclass: #{Seaside.WAComponent}
	indexedType: #none
	private: false
	instanceVariableNames: 'concert '
	classInstanceVariableNames: ''
	imports: ''
	category: ''

initialize

	super initialize.
	concert := Concert new

renderContentOn: html

	html form with:
		[html submitButton 
			callback: [self answer: nil];
			value: 'Cancel'.
		html submitButton 
			callback: [self answer: concert];
			value: 'Save']
You should run your test cases at this point to verify that this component conforms to the requirements.

Text Inputs

Now lets give the user the ability to specify an artist and description.
The component must provide two text inputs whose values are stored in the Concert's artist and description instance variables.
ConcertEditorTest>>testArtistAndDescription
	| resp |
	resp := self lastResponse.
	(resp entityWithId: 'artist') value: 'Bob Dylan'.
	(resp entityWithId: 'description') value: 'Folk/rock concert'.
	(resp buttonWithValue: 'Save') click.
	self assert: self answer artist = 'Bob Dylan'.
	self assert: self answer description = 'Folk/rock concert'

Here we retrieve the text input elements from the form using STSeasideResponse>>entityWithId:. This method searches the DOM for a single XHTML element with the specified id. As these are XHTML input elements, SeasideTesting will answer instances of STSeasideTextEntity. We then specify a new value for these inputs using the STHtmlInputComponent>>value: with this new one). Clicking the Save button should submit the page and update the Concert instance which we verify. Now the component changes:

ConcertEditor>>renderContentOn: html

	html form with:
		[html textInput
			id: 'artist';
			on: #artist of: concert.
		html textInput
			id: 'description';
			on: #description of: concert.
		html submitButton 
			callback: [self answer: nil];
			value: 'Cancel'.
		html submitButton 
			callback: [self answer: concert];
			value: 'Save']

If you run the tests now they should all pass. At this point you might feel like looking at the component in a web browser. I discourage you from doing this since it is easy to become distracted into fiddling with the appearance of the component when we're supposed to be focused on the functionality. Still, if you insist, you can do one or both of the following:

Comboboxes (select and option)

Let's continue with requirements that illustrate testing other types of XHTML components.

The component will include a drop down (or "combo box") with id "location" which allows the user to select from the following list of locations: "New York", "London", "Paris". The Concert answered by pressing the "Save" button will answer the selected city, as a string, in response to location.
ConcertEditorTest>>testLocation
	
	| selectInput labels |
	selectInput := self lastResponse entityWithId: 'location'.
	self assert: selectInput options size = 3.
	labels := selectInput options collect: [:each | each label].
	self
		assert:
			(#('New York' 'London' 'Paris') allSatisfy: [:city | labels includes: city]).
	selectInput value: 'London'.
	(self lastResponse buttonWithValue: 'Save') click.
	self assert: self answer location = 'London'

We find the entity with CSS id 'location', which we expect is an XHTML select input. In SeasideTesting these are represented by instances of STSelectInput. Working with XHTML select inputs is fairly straightforward. We can query them for a list of options using the STSelectInput>>options method which answers a collection of STSelectOption instances. Each option knows its text contents which can be obtained by STSelectOption>>label. We test that all of the expected labels are included as options. We use STSelectInput>>value: to select the "London" option, and subsequently submit the form, verifying that the Concert object answered when Save was pressed was updated correctly.

We now update the ConcertEditor>>renderContentOn: method to display the appropriate XHTML select input:

ConcertEditor>>renderContentOn: html

	html form with:
		[html textInput
			id: 'artist';
			on: #artist of: concert.
		html textInput
			id: 'description';
			on: #description of: concert.
		html select
			id: 'location';
			on: #location of: concert;
			list: #('New York' 'London' 'Paris').
		html submitButton 
			callback: [self answer: nil];
			value: 'Cancel'.
		html submitButton 
			callback: [self answer: concert];
			value: 'Save']

As always, run your tests to make sure they all pass. Some notes on select inputs:

Checkboxes

As a final example of working with form inputs we test the admission fee aspect of ConcertEditor.

The editor will provide a checkbox for the user to indicate if the concert admission is free or requires payment and it will update the Concert instance based on the users input. The checkbox will default to unchecked.
ConcertEditorTest>>testFree
	| checkbox |
	checkbox := self lastResponse entityWithId: 'free'.
	self deny: checkbox value. "default to false"
	checkbox value: true.
	(self lastResponse buttonWithValue: 'Save') click.
	self assert: self answer isFree

ConcertEditorTest>>testRequiresPayment
	| checkbox |
	checkbox := self lastResponse entityWithId: 'free'.
	checkbox value: false.  "doesn't really change value"
	(self lastResponse buttonWithValue: 'Save') click.
	self deny: self answer isFree
Now we implement a version of ConcertEditor>>renderContentOn: to pass this test:
ConcertEditor>>renderContentOn: html
    html form with:
        [html text: 'Artist:'.
        html textInput id: 'artist'; on: #artist of: concert.
        html text: 'Description:'.
        html textInput id: 'description'; on: #description of: concert.
        html text: 'Location:'.
        html select
            id: 'location';
            on: #location of: concert;
            list: #('New York' 'London' 'Paris').
        html checkbox
            id: 'free';
            onTrue: [concert makeFree] onFalse: [concert makePaymentRequired].
        html submitButton callback: [self answer: concert]; value: 'Save'.
        html submitButton callback: [self answer: nil]; value: 'Cancel']
    

The infamous back button

SeasideTesting can simulate the user's use of the back button. For the purposes of this discussion, rather than creating our own component, let's just write a test case for WACounter (VW7.7 users will need to load the Seaside-Examples parcel). If you've never used that component, create an application for it now and play with it a bit. We start by testing that clicking the "++" link increments the counter and clicking the "--" link decrements it (nothing to do with the back button yet).

Smalltalk defineClass: #WACounterTest
	superclass: #{Seaside.STTestCase}
	indexedType: #none
	private: false
	instanceVariableNames: ''
	classInstanceVariableNames: ''
	imports: ''
	category: ''

testIncrement

	self newApplicationWithRootClass: Seaside.WACounter.
	self establishSession.
	(self lastResponse anchorWithLabel: '++') click.
	self assert: self component count = 1.
	(self lastResponse anchorWithLabel: '++') click.
	self assert: self component count = 2

You can see that we can access the current component via the component method. This method returns the root component used to render the last request (although this component may have a delegate which is rendering the current view -- more on that later). Now, let's make sure that pressing the back button works as desired. Backing up in the browser should back up the state of the counter as well (look at WACounter>>states to see why):

testBack
	self newApplicationWithRootClass: Seaside.WACounter.
	self establishSession.
	(self lastResponse anchorWithLabel: '++') click.
	(self lastResponse anchorWithLabel: '++') click.
	self assert: self component count = 2.
	self back.
	(self lastResponse anchorWithLabel: '++') click.
	self assert: self component count = 2

The back method simply returns the STSeasideResponse which was retrieved by the request before the last one. It also causes subsequent calls to lastResponse to return the corresponding response. It does not ask the server for a fresh view of that page. To get a fresh view of the current page (simulating the browser refresh button) simply send refresh in your test case. Finally, since some browsers might not use a cache for the pages in their back buffer, you can simulate going back and reloading the current page by sending backAndRefresh which just combines the two previously discussed methods.

To round out these tests you should add testDecrement and testInitialState which do just what their names imply. Be sure to run the tests!

Finding XHTML elements

This section is less tutorial-oriented than the rest of this document. I won't provide many examples, I just want to discuss how to pick apart the DOM since finding the XHTML element you're querying is often half the battle.

To begin, open a class browser on the class hierarchy rooted at STSeasideEntity. All XHTML elements and the response object (the lastResponse) are descendants of this class. Among other things STSeasideEntity contains our search methods. This makes it possible to restrict searches to being within elements. For example, suppose I have a component that produces the following structure:

<div class="common">
  NO
</div>
<div id="unique">
  <div class="common">
    YES
  </div>
  <div class="common">
    YES AGAIN
  </div>
</div>

Suppose that I want to find the occurrences of elements with class common, but only the ones inside the DIV with id unique. The following code would do the trick:

|uniqueDiv innerCommons|
uniqueDiv := self lastResponse entityWithId: 'unique'.
innerCommons := uniqueDiv entitiesWithClass: 'common'.

Notice that, on the last line, we queried uniqueDiv rather than the entire document.

The search-* method categories of STSeasideEntity contain search methods. It should be clear from the method name whether or not the method returns a single or multiple elements. If the method returns a single element, and you aren't using the ifAbsent: argument, failure to find the element will signal an error (not answer nil!). Also, if an search method is supposed to return a single element and there are multiple elements that meet the criteria, it will signal an error. Here are some commonly-used search methods:

Type of search Method
All children children
All descendants allEntities
Elements with tag entitiesNamed:
Elements with class entitiesWithClass:
Element with ID entityWithId:, entityWithId:ifAbsent:

In addition to these methods, there are type-specific search methods. These methods behave as above but, in addition, ensure that the XHTML element being produced is the correct kind. So, for example, anchorWithId: would signal an error if the XHTML element with the specified ID was not an anchor. These methods have been added to SeasideTesting on an as-needed basis. I don't plan to continue to grow this protocol. Here are some commonly-used type-specific search methods:

Type of search Method
All anchors anchors
Anchors which include specified text inside it anchorsIncludingString:
Anchors which have exactly specified string inside them anchorsWithLabel:
Unique anchor with exactly specified string inside it anchorWithLabel:
All buttons buttons
Button with specified label buttonWithValue:
All input components inputs
Labels with given text labelsWithText:

Testing javascript-enabled components using an external browser

SeasideTesting performs its job by simulating a web browser. This simulation does not include running Javascript so, in order to test components that make use of Javascript, I provide a mechanism where SeasideTesting uses an external web browser through a simple remote-control mechanism (currently only tested with Firefox!).

How it works

Let me start by saying that everything you've learned so far will be useful in external-browser based test cases. Still, understanding some of the differences between the simulated-browser and external-browser frameworks is helpful to prevent you from running into subtle problems.

In simulated-browser test cases SeasideTesting makes HTTP requests to the Seaside server whenever you tell it to click on an anchor or a button. SeasideTesting reads the server's response and therefore always has an up-to-date view of the HTML being "displayed" on the web page.

In external-browser test cases SeasideTesting asks an external web browser (Firefox) to simulate user gestures like clicking, typing etc. In response to these gestures, the external browser may make requests to Seaside and/or modify the DOM by the execution of Javascript code. If SeasideTesting wants to know the XHTML being displayed by the external browser, it asks the external browser to send it. I refer to this as a "page fetch" and it is triggered automatically by some events but there are times when it must be triggered manually.

Even in external-browser test cases, all of the test code runs in the Smalltalk image. For example, rather than asking Firefox "does the page contain the word frog?" we simply ask Firefox for a copy of the page and use our usual Smalltalk tools to examine its contents. This sets SeasideTesting apart, for better or worse, from many of the in-browser testing tools (Albatros, Selenium, and watir). This means that the full suite of SUnit tools used by simulated-browser tests are available to external-browser tests. The chief drawback of this is that SeasideTesting must periodically refresh its model of the page being shown in the browser via a page fetch.

When using external-browser tests, please keep in mind that the browser could be executing Javascript which may continue to change the contents of the page, even after it is fetched. This is especially important for asynchronous requests which I will discuss in detail later.

Make sure things are working

Just to make sure the external browser support is working, run the tests in the package SeasideTesting-Tests-ExtBrowserFunctional. As of this time there is one test that fails under Seaside 2.8 with an error (STExtDialogTest>>testAlert) representing work in progress. There are 6 failures/errors in Seaside 3.0. The additional 5 errors are due to a bug in Seaside's error handling. If the web browser doesn't open and/or other tests fail, there is no point in going forward...time to look for help. If a browser other than Firefox opens you may need to make Firefox your default browser. Under UNIX/Linux, at least, you can specify a different browser with:

"Change /usr/bin/firefox as needed for your OS"
UnixBrowserLaunchService currentExternalBrowser: '/usr/bin/firefox'

This might be useful if you prefer to not have Firefox be your default browser in your desktop environment. I assume something similar can be done in Windows.

A simple example

SeasideTesting includes the STExtTestCase class which performs the setup necessary to make a test case use an external browser. Let's build a Javascript-enabled version of our "hello component":

The AjaxHelloComponent will display the word "Hello" in an H1 tag.
The AjaxHelloComponent will include a anchor labeled "hide" which, when clicked, will hide the "Hello" greeting by adding the CSS class "hidden" to the H1 tag from AHC 1. This action will occur without a request to the server (it will be client-side Javascript only).

Notice that we describe how the greeting will be hidden (via a CSS tag) since it is not possible for SeasideTesting to ensure that the item is actually hidden from the user's view. Generally tests of the actual appearance must be performed by a human. (See Screenshots subsection below for ways in which SeasideTesting can help with this.) OK...now the tests:

Smalltalk defineClass: #AjaxHelloComponentTest
	superclass: #{Seaside.STExtTestCase}
	indexedType: #none
	private: false
	instanceVariableNames: ''
	classInstanceVariableNames: ''
	imports: ''
	category: ''

setUp
	
	| app |
	super setUp.
	app := self newApplicationWithRootClass: AjaxHelloComponent.
	app addLibrary: Scriptaculous.SULibrary
        "Note: for VW7.7/Seaside 3.0 the line above needs to be:
        add 
           addLibrary: Scriptaculous.PTDeploymentLibrary;
           addLibrary: Scriptaculous.SUDeploymentLibrary."


heading
	"Answer the first H1 tag on the page"
	
	^(self lastResponse entitiesNamed: 'h1') first

testShowsGreeting
	
	self establishSession.
	self assert: (self heading containsString: 'Hello').
	self deny: (self heading cssClasses includes: 'hidden')

testHideAnchorHidesGreeting
	
	self establishSession.
	(self lastResponse anchorWithLabel: 'hide') click.
	self assert: (self heading cssClasses includes: 'hidden')

As always, run the tests to see that they fail. These look pretty much like normal SeasideTesting test cases. Since the component will use Scriptaculous, we've had to add SULibrary to the application entry point that SeasideTesting created. Another important point to note is that the click message, in addition to causing the external web browser to "click" on the anchor, also caused it to send an updated version of the page to SeasideTesting.

Now we need to write the component:

Smalltalk defineClass: #AjaxHelloComponent
	superclass: #{Seaside.WAComponent}
	indexedType: #none
	private: false
	instanceVariableNames: ''
	classInstanceVariableNames: ''
	imports: ''
	category: ''

style
	^'
.hidden {
	display: none;
}
'

renderContentOn: html

	html heading
		level: 1;
		id: #greeting;
		with: 'Hello'.

	html horizontalRule.
	html anchor
		onClick: (html scriptaculous element id: #greeting; addClassName: 'hidden');
		with: 'hide'

Re-run the tests...they should pass.

A is for Asynchronous

Quite often components perform an AJAX request in response to events like a mouse click. These AJAX requests run asynchronously once they have been dispatched to the server. That is, the browser doesn't wait for a response before returning from the click event handler, for example. This means that SeasideTesting may ask the browser to send a copy of the page before the AJAX call has completed. Subsequent assertions about the contents of this page may lead to incorrect test results.

The solution provided by SeasideTesting is a simple polling loop: poll the browser until some criteria is met. This is implemented via the STExtTestCase>>assertEventually: method. Let's work through an example where this might be used.

The AjaxHelloComponent will have an anchor labeled "Change". When clicked this anchor will modify the H1 tag to contain a greeting from the list (cycling back to the beginning once the list is exhausted):
  1. G'day
  2. Salut
  3. Bonjour
  4. Hello
This action will be performed without a full-page refresh.
OK, so here's the test:
testAnotherAnchorChangesGreeting

	self establishSession.
	self assert: (self heading containsString: 'Hello').
	(self lastResponse anchorWithLabel: 'Change') click.
	self assertEventually: [self heading contentString = 'G''day'].
	(self lastResponse anchorWithLabel: 'Change') click.
	self assertEventually: [self heading contentString = 'Salut'].
	(self lastResponse anchorWithLabel: 'Change') click.
	self assertEventually: [self heading contentString = 'Bonjour'].
	(self lastResponse anchorWithLabel: 'Change') click.
	self assertEventually: [self heading contentString = 'Hello'].

The assertEventually: call tests the condition block (first argument), if this condition is false, it refetches the page from the browser and repeats until either the default timeout (500ms, see STTestCase>>defaultJavascriptTimeout) is reached or the condition block is satisfied.

Be careful:

With that out of the way, let's pass the tests. Here is the complete source to AjaxHelloComponent (note position i-var added):

Smalltalk defineClass: #AjaxHelloComponent
	superclass: #{Seaside.WAComponent}
	indexedType: #none
	private: false
	instanceVariableNames: 'position '
	classInstanceVariableNames: ''
	imports: ''
	category: ''

initialize

	super initialize.
	position := 0.

greetings

	^#('Hello' 'G''day' 'Salut' 'Bonjour') collect: [:each | each asString]

nextGreeting

	position := position + 1 \\ self greetings size.

renderContentOn: html

	html heading
		level: 1;
		id: #greeting;
		with: [self renderGreetingOn: html].

	html horizontalRule.
	html anchor
		onClick: (html scriptaculous element id: #greeting; addClassName: 'hidden');
		with: 'hide'.
	html anchor
		onClick: (html updater id: #greeting; callback: [:r | self nextGreeting. self renderGreetingOn: r]);
		with: 'Change'

renderGreetingOn: html

	html text: (self greetings at: position + 1)

style
	^'
.hidden {
	display: none;
}
'

In renderContentOn: you can see that I used the Scriptaculous updater to update the H1 element to contain the new greeting.

Wait for page to load

When an anchor or submit button causes a full-page update (rather than an AJAX update), you probably want your test case to wait until the new page is loaded before continuing. This can be done using waitForPageToLoad. Here's an example:

The AjaxHelloComponent will include an anchor labeled "Goodbye". When clicked this anchor will show a new page with the text "Bye" on it.
And the test:
AjaxHelloComponentTests>>testGoodbyeAnchorWorks

	self establishSession.
	self deny: (self lastResponse containsString: 'Bye').
	(self lastResponse anchorWithLabel: 'Goodbye') click.
	self waitForPageToLoad.
	self assert: (self lastResponse containsString: 'Bye').

and the rendering code to make it pass:

AjaxHelloComponent>>renderContentOn: html

	html heading
		level: 1;
		id: #greeting;
		with: [self renderGreetingOn: html].

	html horizontalRule.
	html anchor
		onClick: (html scriptaculous element id: #greeting; addClassName: 'hidden');
		with: 'hide'.
	html anchor
		onClick: (html updater id: #greeting; callback: [:r | self nextGreeting. self renderGreetingOn: r]);
		with: 'Change'.
	html anchor
		callback: [self inform: 'Bye'];
		with: 'Goodbye'

Keep in mind that waitForPageToLoad only works for full page updates. You can't use it to wait for some javascript update to complete, for example.

Events

Javascript-enabled components can respond to a variety of events. Some of these events can be generated by SeasideTesting. The primitive event generation methods are in the event generation method category of STSeasideEntity. These methods cause the browser to invoke the corresponding javascript event on the specified element (the receiver). This is done using the jQuery library's event generation support. In the future I would like to make it possible to not only generate these events but also specify the various parameters of the javascript event objects so that one could, for example, simulate drag-and-drop operations. Here is an example of testing if the example text on a text input disappears when that text input gets focus (this code was pulled from the integration tests for SeasideTesting):

Smalltalk.Seaside defineClass: #ExampleTextTest
	superclass: #{Seaside.STExtTestCase}
	indexedType: #none
	private: false
	instanceVariableNames: ''
	classInstanceVariableNames: ''
	imports: ''
	category: ''

setUp
	|app|

	super setUp.

	app := self newApplicationWithRootClass: ExampleTextComponent.
	app addLibrary: Scriptaculous.SULibrary.
	self establishSession

testExampleTextDisappearsOnFocus

	self assertEventually: [(self lastResponse entityWithId: #exampleTextInput) value = 'example text'].
	(self lastResponse entityWithId: #exampleTextInput) doFocus.
	(self lastResponse entityWithId: #exampleTextInput) doBlur.
	self 
		assertEventually: [(self lastResponse entityWithId: #exampleTextInput) value = '']
		failInMS: 2000

a the component:

Smalltalk defineClass: #ExampleTextComponent
	superclass: #{Seaside.WAComponent}
	indexedType: #none
	private: false
	instanceVariableNames: ''
	classInstanceVariableNames: ''
	imports: ''
	category: ''

renderContentOn: html
	html form:
		[html textInput id: #exampleTextInput; exampleText: 'example text']

SeasideTesting also includes a higher-level API for event generation, called "actions". These are used to simulate user gestures and often involve one or more of the primitive events as well as typically a page fetch after the event is generated. These can be found in the actions method category of STSeasideEntity. This is needed because jQuery's event generation doesn't always correspond to the expected user gesture. Here are some details:

STSeasideEntity>>click

For the typical XHTML element the click message is simply a doClick followed by a fetchPage. Unfortunately jQuery doesn't behave properly when sending click to an anchor. In this case SeasideTesting has a special javascript method for simulating a click.

STSeasideEntity>>type:, typeBackspace, typeBackspaces:

As with click events, after a string is "typed" the page is fetched. The jQuery key event generation code doesn't do what is needed by SeasideTesting so, again, I have provided separate javascript for generation of these events. This was taken and modified from the Selenium project (under the terms of the apache license). The details can be found in STExternalBrowserLibrary>>htmlUtilsJs. Since keycode generation is platform and browser dependent, there may be problems with simulating various international characters. Please let me know if you have problems (send complete details!) and I'll try to solve them. I would really like SeasideTesting to be as language/encoding friendly.

Screenshots

TBD (basically I use import from imagemagick)

Testing remote applications

Most of SeasideTesting's functionality is encapsulated in the class STWebAppTester which I refer to as "the tester". The abstract STTestCase simply provides convenience methods many of which delegate directly to the tester. STWebAppTester is a subclass of STRemoteAppTester and adds tools that are useful when testing an application that resides in the same Smalltalk image as the test case. The superclass STRemoteAppTester is useful in its own right as it can test Seaside applications that are running in other images and/or on other machines. For example, one of my end-to-end test cases looks roughly like this:

testDeployAndStartProvidesLoginPage
	(Deployer forServer: TestServer new) deploy: MyApplication new.
	self tester establishSession.
	self assert: (self tester lastResponse containsString: 'Login').

tester
	^tester ifNil:
		[tester := STRemoteAppTester new.
		tester rootUrl: self testServerUrl]

testServerUrl
	^'http://some.test.server/'

This is just a sketch but it shows basically how one could test a "remote" application. I don't expect this to be generally useful except in writing full functional tests like the one above. Please keep in mind that SeasideTesting currently only supports testing remote Seaside applications (I use the Smalltalk platform's XML parser so, at the very least, the remote application needs to produce well-formed XHTML but there are probably other dependencies on Seaside-generated XHTML).

As a trivial example, just to demonstrate this functionality, consider testing the anchor labeled "Screenshots" on http://seaside.st. Clicking on this anchor should show a page whose heading is "Screenshots":

Smalltalk defineClass: #SeasideDotStTest
	superclass: #{XProgramming.SUnit.TestCase}
	indexedType: #none
	private: false
	instanceVariableNames: ''
	classInstanceVariableNames: ''
	imports: ''
	category: ''

testScreenshotsAnchor
	"Make sure that there is an anchor labeled 'Screenshots' and that clicking on it brings you to 
	a page with a heading element that contains 'Screenshots'"

	| tester anchor headings |
	tester := Seaside.STRemoteAppTester new.
	tester rootUrl: 'http://seaside.st/'.
	tester establishSession.
	anchor := tester lastResponse anchorWithLabel: 'Screenshots'.
	anchor click.
	headings := tester lastResponse entitiesNamed: 'h1'.
	self assert: (headings anySatisfy: [:heading | heading containsString: 'Screenshots'])

You can even put this section together with the last and write tests of remote applications that run through an external web browser. This just involves setting the browser (STRemoteAppTester>>browser:). Look for senders of this message for examples.

Miscellaneous topics

Integrating SeasideTesting into your own test heirarchy

As mentioned in a previous section, most of SeasideTesting's functionality is encapsulated in the class STWebAppTester (the "tester"). If you prefer not to subclass STTestCase you can use a "tester" in your own test heirarchy. For example, we can write a test of the WACounter class that is included with Seaside without subclassing STTestCase:
Smalltalk defineClass: #WACounterTest
	superclass: #{XProgramming.SUnit.TestCase}
	indexedType: #none
	private: false
	instanceVariableNames: ''
	classInstanceVariableNames: ''
	imports: ''
	category: ''

testIncrement

	| tester |
	tester := Seaside.STWebAppTester new.
	tester newApplicationWithRootClass: Seaside.WACounter path: 'WACounter_test'.
	tester establishSession.
	self assert: ((tester lastResponse entitiesNamed: 'h1') first containsString: '0').
	(tester lastResponse anchorWithLabel: '++') click.
	self assert: ((tester lastResponse entitiesNamed: 'h1') first containsString: '1').

Configuring a component

Many components are designed to be created and initialized before being "called:" from Seaside. For example, suppose you have a view which you normally expect to use as follows:

showEditorFor: someObject
	| editorView |
	editorView := EditorView new.
	editorView objectToBeEdited: someObject.
	self call: editorView

The problem which comes when testing EditorView is that the testing framework makes the class the root class for a Seaside application but Seaside constructs an instance of the class when a request is submitted from your test case. You are not given a chance to initialize this instance. To get around this problem SeasideTesting allows you to specify an initialization block which will be evaluated after your component has been created:

testSimpleEdit
	self newApplicationWithRooClass: EditorView
		initializeWith: [:view | view objectToBeEdited: SomeClass new].
	self establishSession.
	...

As you can see, the initialization block is passed the component instance which was created by the Seaside server.

Testing a specific instance

Since SeasideTesting registers your component as the root of a Seaside application, the default way to create your component will be to send it new. In some cases components must be created using a constructor other than #new. For example, suppose I have a component with a class side method in a class called EditorView:

edit: someObject
	^self new initializeEditorFor: someObject

It is clear that we should use this method to constructor instances of EditorView using this method. SeasideTesting permits you to create the component yourself using the following method:

testSimpleEdit
	| someObject |
	someObject := SomeClass new.
	self newApplicationWithRooClass: EditorView
		createWith: [EditorView edit: someObject].
	self establishSession.
	...

The front most component and delegation

Following delegation chains is a common exercise when testing components. Suppose, for example, that you want to ask a question about the front-most component rather than the root. I have provided STTestCase>>frontMostComponent for that purpose. Look at the sample code SCSampleComponentTest>>testFrontMost for an example of using this method. If you need to look deeper into a components structure you can look at the source code of that method as a first example. Also, keep in mind that this testing framework works best when testing at the component level. If you are using a component which is a composite of several other components, you should write tests for the smaller components first and then limit your tests of the larger component to features which emphasize its API.

Customizing the environment ("configuration") of the component under test

In Seaside, entry points to applications are instances of WAApplication. These "applications" specify some low-level Seaside configuration and permit users to add their own configuration information. In SeasideTesting if you want to use your own configuration classes, set values for some of the configuration parameters, and/or add libraries to your component under test simply do it after creating the application (a WAApplication) with the newApplication* methods that we have used throughout this tutorial:

HelloComponentTest>>testComponentUsesMailer
	app := self newApplicationWithRootClass: SomeComponentClass.
	app configuration addAncestor: MyCustomConfiguration new.
	app preferenceAt: #sessionClass put: MyCustomSession.
	app preferenceAt: #mailerFactory put: FakeMailer.
	self establishSession.
	...

Testing error handlers

Not yet supported.

Multiple browsers

You many have as many separate browser sessions as you like in a test method. The default implementations of establishSession just passes the message on to an instance of STWebAppTester (the tester). If you wish to have several simultaneous sessions in a test method, just create your own tester instances and send them the appropriate messages. I don't currently have an example.

Asserting that rendering or callback causes error

This doesn't seem that generally useful but it is possible to assert that a component raises an unhandled error during rendering or callback processing. In the event of such an error, SeasideTesting raises STComponentError and records the original exception as the cause. As an example, suppose I have the following component (this code is from the SeasideTesting's integration test suite):
Smalltalk.Seaside defineClass: #STErrorHandlingComponent
	superclass: #{Seaside.WAComponent}
	indexedType: #none
	private: false
	instanceVariableNames: 'renderingError '
	classInstanceVariableNames: ''
	imports: ''
	category: ''

initialize
	super initialize.
	renderingError := false

haveRenderingError
	renderingError := true

renderContentOn: html
	html anchor callback: [self haveRenderingError]; id: #renderError; with: 'Have rendering error'.
	html break.
	renderingError ifTrue: [1 / 0].
	html anchor callback: [(Array new: 0) at: 1]; id: #callbackError; with: 'Have callback error'.
	html text: 'blah'
I can test that ZeroDivide gets raised via the following test:
Smalltalk.Seaside defineClass: #STErrorHandlingTest
	superclass: #{Seaside.STTestCase}
	indexedType: #none
	private: false
	instanceVariableNames: ''
	classInstanceVariableNames: ''
	imports: ''
	category: ''

setUp
	super setUp.
	self newApplicationWithRootClass: STErrorHandlingComponent.
	self establishSession.

testRenderingErrorCause
	| theException |
	theException := nil.
	[(self lastResponse anchorWithLabel: 'Have rendering error') click] on: STComponentError do: [:ex |
		self assert: (ex cause isKindOf: ZeroDivide).
		theException := ex].
	self assert: theException notNil.
See STErrorHandlingTest for more examples.