Skip to main content

Notice: this Wiki will be going read only early in 2024 and edits will no longer be possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.

Jump to: navigation, search

Difference between revisions of "RapUITesting"

m
Line 244: Line 244:
  
 
We need some time to investigate how this could be implemented.
 
We need some time to investigate how this could be implemented.
 +
 +
[[Category:RAP]]

Revision as of 05:17, 21 August 2007

| RAP wiki home | RAP project home |

THIS IS EXPERIMENTAL! WE MAKE OUR RESEARCH RESULTS AVAILABLE TO ENABLE FEEDBACK

Writing UI Tests for RAP applications

This document will describe one possible way to do UI tests for RAP applications. It currently requires to alter the RWT source in one place, but we will be looking into how to provide this kind of testability without the need to patch. With this approach we use the combination of JUnit, Selenium and Selenium RC to have automated UI tests which can be integrated into your JUnit testsuite.

Below is a simple example application to demonstrate the usage of the tool chain.

Requirements

If you don't have it, you need at least these files in order to follow the tutorial:

Extract these files in a directory of your choice.

Creating the sample application

First we need something to test. Therefor I put together a little example application to demonstrate the UI testing approach.

Just create a new plugin project, add org.eclipse.rap.ui.workbench as a dependency and create the following class:

package org.eclipse.rap.demo.ui.tests;

import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.lifecycle.IEntryPoint;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;

public class TestApp implements IEntryPoint {

	public Display createUI() {
		Display d = new Display();
		final Shell s = new Shell(d, SWT.SHELL_TRIM);
		s.setLayout(new GridLayout());
		s.setText("App Title");
		final Button b1 = new Button(s, SWT.PUSH);
		b1.setText("Before");
		b1.addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent e) {
				MessageDialog.openInformation(s, "MessageBox",
						"Changing the button text now...", null);
				b1.setText("After");
			}
		});
		s.pack();
		s.open();
		return d;
	}

}

As you can see, this is a really tiny RAP example - but big enough to be worth to test it.

Preparing the tests

Now create a new java project and add the JUnit library. Additionally, you need to add the selenium-java-client-driver.jar to your project in order to use the Selenium Remote Control.

As RAP applications have a little bit other nature than normal web applications, we need to work around some techniques of selenium. First, you can use commands like click against a target element on your page. The target defined as in id element of your (x)html source. As Qooxdoo doesn't provide ids for their widgets, we need the mentioned Selenium User Extension for Qooxdoo. With this, all targets prefixed with "qx=" will now use another "find-the-target" algorithm based on UserData provided by Qooxdoo widgets.

As we have an internal identifier for each widget in RAP which is known by the server and the client side, we can use this to put it into the UserData field. Apply the following patch to the org.eclipse.rap.rwt component in order to have the UserData:

### Eclipse Workspace Patch 1.0
#P org.eclipse.rap.rwt
Index: src/org/eclipse/swt/lifecycle/ControlLCAUtil.java
===================================================================
RCS file: /cvsroot/technology/org.eclipse.rap/org.eclipse.rap.rwt/src/org/eclipse/swt/lifecycle/ControlLCAUtil.java,v
retrieving revision 1.1
diff -u -r1.1 ControlLCAUtil.java
--- src/org/eclipse/swt/lifecycle/ControlLCAUtil.java	26 Apr 2007 06:46:37 -0000	1.1
+++ src/org/eclipse/swt/lifecycle/ControlLCAUtil.java	29 May 2007 16:21:01 -0000
@@ -94,6 +94,12 @@
     }
   }
   
+  public static void writeUserData(final Control control) throws IOException {
+		JSWriter writer = JSWriter.getWriterFor(control);
+		String newValue = WidgetUtil.getId( control );
+		writer.set("UserData", new String[] {newValue, "selenium"});
+	}
+  
   // TODO [rh] there seems to be a qooxdoo problem when trying to change the
   //      visibility of a newly created widget (no flushGlobalQueues was called)
   //      MSG: Modification of property "visibility" failed with exception: 
@@ -126,6 +132,7 @@
     writeFont( control );
     writeToolTip( control );
     writeMenu( control );
+    writeUserData( control );
     writeActivateListener( control );
     writeFocusListener( control );
   }

At the moment, all widget ids are looking like "w1", "w2", "w3". As these ids are generated in the order the widgets are created, it isn't a very good way to handle it. Imagine: You wrote hundred of testcases and then you see: Ah, my application needs an additional label somewhere, all the widgets created after this label have another ID. We are thinking about reinventing the IDs in order to provide more meaningful values which can be used in the tests. But as this is the first attemp, let's go on with what we have.

To not prefix all your IDs with the "qx=" special locator, copy this class to your test project to use it as your Selenium connector:

package org.eclipse.rap.demo.ui.tests;

import com.thoughtworks.selenium.CommandProcessor;
import com.thoughtworks.selenium.DefaultSelenium;


public class RAPSelenium extends DefaultSelenium {

	public RAPSelenium(CommandProcessor processor) {
		super(processor);
	}

	public RAPSelenium(String serverHost, int serverPort,
			String browserStartCommand, String browserURL) {
		super(serverHost, serverPort, browserStartCommand, browserURL);
	}

	public void click(String locator) {
		commandProcessor.doCommand("qxClickAt", new String[] { "qx=" + locator});
	}
	
	public String getText(String locator) {
		return super.getText("qx=" + locator);
	}
	
	public void waitForElementPresent(String locator) {
		for (int second = 0;; second++) {
			if (second >= 60) System.out.println("timeout");
			try { if (isElementPresent("qx=" + locator)) break; } catch (Exception e) {}
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	
	public void clickAndWait(String locator) {
		click(locator);
		try {
			Thread.sleep(1*1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	public String getShellText(String locator) {
		String text = getText(locator);
		return text.split("\n")[0];
		
	}
}

This class is a simple wrapper around the existing Selenium class which helps you with some sort of problems. Be careful: As it's extends the base Selenium class, you'r able to use other methods of the original class which are not overridden by the RAPSelenium class. If you forget to add the "qx=" locator yourself, you'll not get any useful return result.

Now we are ready to take off and write the first testcase...

First RAP UI Testcase

import junit.framework.TestCase;

public class AppTest extends TestCase {

	private RAPSelenium sel;

	private static final String MAIN_SHELL = "w2";
	private static final String BUTTON = "w3";
	private static final String MESSAGEBOX = "w4";
	private static final String MESSAGEBOX_MESSAGE = "w6";
	private static final String MESSAGEBOX_OK_BUTTON = "w10";

	protected void setUp() throws Exception {
		sel = new RAPSelenium("localhost", 4444,
				"*firefox /usr/lib/firefox/firefox-bin",
				"http://localhost:8080/rap");
		sel.start();
	}

	public void testButton() {
		sel.open("http://localhost:8080/rap?w4t_startup=foo");

		sel.waitForElementPresent(BUTTON);

		// checking shell title
		assertEquals("App Title", sel.getShellText(MAIN_SHELL));

		// checking button
		assertEquals("Before", sel.getText(BUTTON));

		// checking message dialog
		sel.click(BUTTON);

		sel.waitForElementPresent(MESSAGEBOX);
		assertEquals("MessageBox", sel.getShellText(MESSAGEBOX));
		assertEquals("Changing the button text now...", sel
				.getText(MESSAGEBOX_MESSAGE));
		sel.clickAndWait(MESSAGEBOX_OK_BUTTON);
		
		// check button afterwards
		assertEquals("After", sel.getText(BUTTON));
	}

	protected void tearDown() throws Exception {
		sel.stop();
	}

}

Running the Selenium RC server

The Selenium RC server is a little server written in Java which cares about the interaction between your JUnit tests and the browser instances. As it offers some webservices for us, we need to start it before running our tests.

Just call this on our favorite shell/commandline:

 $ cd <path-to-selenium-rc>/server
 $ java -jar selenium-server.jar -userExtensions <path-to-qooxdoo-user-extension.js>
...
...
INFO: Started SocketListener on 0.0.0.0:4444
May 29, 2007 5:42:01 PM org.mortbay.util.Container start
INFO: Started org.mortbay.jetty.Server@a62fc3

If you don't get an output like this in the last lines, be sure you have access to the port 4444 or change it with the commandline paramter of the selenium server. See Command Line Options


And now?

As you see, all that stuff is really hacky and should be used carefully. As this was the first try to combine RAP applications and UI Tests, there is much work to do in this area. When we have some time in the future, we will consider working on an own RAP User Extensions for Selenium with some improvements and also on a - at least - simple infrastructure for your UI tests.

  • AFAIK the Selenium "type" command is not yet working with qooxdoo textboxes. So be patient...

If you have great ideas or thoughts how we could improve the whole story, don't hesitate to contact us in the RAP newsgroup or add your thoughts to the Ideas section of this page.

Ideas

As the idea of using the internal widget ID isn't very helpful and additionally very dangerous, we thought about some other ways to get a reference to a specific widget. One of the best ways to do this would be something similar to a XPath expression. These could look something like the following examples:

  • //button[@text="OK"]
  • //shell[@active]/toolbar[2]/toolitem[4]

We need some time to investigate how this could be implemented.

Back to the top