-
Notifications
You must be signed in to change notification settings - Fork 0
Browser simulation
In UI tests, you'll probably want to check that expected behaviors do occur when the user interacts with the UI components, like clicking on a button or filling a text box.
gwt-test-utils provides ways to simulate this user interaction and the corresponding DOM events. So let's talk about how a web browser is simulated !
In the previous sample, we wanted to test that a message is displayed when the user fills the textbox and then clicks on the button. We can achieve this by simulating the 'fill text' action and a DOM 'click' event using the static methods Browser.fillText()
and Browser.click()
:
@Test
public void clickOnButtonShouldDisplayMessageInLabel() {
// Arrange
SampleView view = new SampleView();
// ensure the widgets state at init
assertThat(view.label).isNotVisible();
assertThat(view.button).isVisible().isNotEnabled();
// Act
Browser.fillText(view.textBox, "Jack Shephard");
// the button should be enabled now
Browser.click(view.button);
// Assert: label should be visible and filled
assertThat(view.label).isVisible().textEquals("Hello Jack Shephard");
}
Browser
provides many predefined methods to simulate various events, like blur()
, change()
, etc.
Your application might also react to some slightly more complex events, like pressing a combination of keys.
For such a case, you can simulate this kind of event by using an EventBuilder
to build it, and then firing it with the dispatchEvent()
method of Browser
:
@Test
public void complexClickOnButtonShouldDisplayMessageInLabel() {
// Arrange
SampleView view = new SampleView();
view.textBox.setText("Ben Linus");
view.button.setEnabled(true);
assertThat(view.label).isNotVisible();
// Act : simulate a complex event (ctrl + shift + click)
Event clickEvent = EventBuilder.create(Event.ONCLICK)
.setCtrlKey(true)
.setShiftKey(true)
.build();
Browser.dispatchEvent(button, clickEvent);
// Assert: label should be visible and filled
assertThat(view.label).isVisible().textEquals("Hello Ben Linus");
}
In real life, your favorite web browser won't allow you to trigger any event on a disabled or display:none UI components, such as text boxes, buttons, etc.
The Browser
class does the same by checking the targeted widget state before dispatching any event on it.
There is a special case for attached / detached widgets. Real browsers won't let you trigger events on detached widgets, since they aren't displayed on screen. But Browser
class by default will be able to dispatch any event on a detached widget.
Actually, we decided that having to add a widget we want to unit test to the GWT RootPanel
(or RootLayoutPanel
) so it will be attached to the fake DOM was an unnecessary constraint.
This behavior can easily be overrided if you want Browser
to check for a widget attached / detached state before dispatching events on it. In your test class, just use the GwtTest.setCanDispatchEventsOnDetachedWidgets(...)
protected method like that:
@Before
public void setupBrowser() {
setCanDispatchEventsOnDetachedWidgets(false);
}
Just keep in mind this feature is reseted to true
between each tests.
With JavaScript, most events are asynchronous. When an asynchronous event occurs, it gets into the Event queue.
Real web browsers have an inner loop, called Event Loop, which checks the queue and processes events, executes functions etc.
gwt-test-utils browser implementation doesn't have a real Event Loop, since your tests are executed in a thread without any pause to notify this loop it can processed queued events, DOM modifications, ScheduledCommand
, async callbacks, etc.
So, when you attach some Widget to the DOM, it's immediately attached.
But this 'synchronous' behavior is not relevant in some cases, where you explicitly want to invoke code with some delay :
- using Scheduler.scheduleDeferred(ScheduledCommand)
- using Scheduler.scheduleFinally(RepeatingCommand) or scheduleFinally(ScheduledCommand)
- using Scheduler.scheduleEntry(RepeatingCommand) or Scheduler.scheduleEntry(ScheduledCommand)
- for remote services
AsynchCallback
execution, which should be done after each other synchronous code has been executed.
For those cases, gwt-test-utils use an internal queue of commands which are not executed until they are explicitly told.
If you got confused, don't worry. Code always explains things better :
@GwtModule("com.googlecode.gwt.test.sample.Sample")
public class SampleViewTest extends GwtTest {
@Test
public void deferredCommandShouldNotBeTriggerSynchronously() {
// Arrange
final StringBuilder sb = new StringBuilder();
ScheduledCommand cmd = new ScheduledCommand() {
public void execute() {
sb.append("triggered!");
}
};
// Act
Scheduler.get().scheduleDeferred(cmd);
// Assert the cmd is not yet triggered
assertThat(sb.toString()).isEmpty();
}
}
This unit test is pretty straightfoward. But running it, you'll get a GwtTestException
:
com.googlecode.gwt.test.exceptions.GwtTestException: One exception thrown during gwt-test-utils cleanup phase :
at com.googlecode.gwt.test.GwtTest.tearDownGwtTest(GwtTest.java:93)
...
Caused by: com.googlecode.gwt.test.exceptions.GwtTestException: 1 pending scheduledDeferred ScheduledCommand(s) must be triggered manually by calling SampleViewTest.getBrowserSimulator().fireLoopEnd() before making your test assertions
at com.googlecode.gwt.test.internal.BrowserSimulatorImpl.afterTest(BrowserSimulatorImpl.java:78)
at com.googlecode.gwt.test.internal.AfterTestCallbackManager.executeCallback(AfterTestCallbackManager.java:95)
at com.googlecode.gwt.test.internal.AfterTestCallbackManager.triggerCallbacks(AfterTestCallbackManager.java:67)
at com.googlecode.gwt.test.GwtTest.tearDownGwtTest(GwtTest.java:85)
In fact, your test ran successfully. But after each @Test
methods, gwt-test-utils verify there is no pending command command to be executed, and would throw an exception if it find one, telling you it must be triggered manually. Let's see how to do this !
A BrowserSimulator interface is provided to be able to notify each queued command to be executed by simulating the end of an Event Loop.
An instance of this interface is simply returned by calling the protected method getBrowserSimulator()
in your GwtTest
subclasses. So let's correct our unit test :
@Test
public void deferredCommandShouldNotBeTriggerSynchronously() {
// Arrange
final StringBuilder sb = new StringBuilder();
ScheduledCommand cmd = new ScheduledCommand() {
public void execute() {
sb.append("triggered!");
}
};
// Assert the cmd is not yet triggered
assertThat(sb.toString()).isEmpty();
// Act
Scheduler.get().scheduleDeferred(cmd);
// simulate an event loop end
getBrowserSimulator().fireLoopEnd();
// Assert
assertThat(sb.toString()).isEqualTo("triggered!");
}
You should never care about the gwt-test-utils internal implementation of the BrowserSimulator
interface. But keep in mind it's there every commands and asyncCallbacks are queued. This implementation is used to fire an event loop end automatically whenever it seems relevant, like before and after you trigger an DOM event using Browser
utilities.
There should be very few cases where you'll need to use getBrowserSimulator().fireLoopEnd()
manually.