blob: 5df0b7f361ca137ea994bb14972be8d74d15b917 [file] [log] [blame]
/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.gwt.user.client.ui;
import com.google.gwt.dom.client.AnchorElement;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.junit.DoNotRunWith;
import com.google.gwt.junit.Platform;
import com.google.gwt.junit.client.GWTTestCase;
import com.google.gwt.user.client.History;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import java.util.ArrayList;
/**
* Tests for the history system.
*
* TODO: find a way to test unescaping of the initial hash value.
*/
public class HistoryTest extends GWTTestCase {
private static String getCurrentLocationHash() {
// Firefox automatically decodes location.hash so parse it from Window.Location.getHref
String href = Window.Location.getHref();
String[] split = href.split("#");
if (split.length != 2) {
fail("can not read history token");
}
return split[1];
}
private HandlerRegistration handlerRegistration;
private Timer timer;
@Override
public String getModuleName() {
return "com.google.gwt.user.User";
}
protected String getHistoryToken2() {
return "token 2";
}
protected String getHistoryToken2_encoded() {
return "token%202";
}
// TODO(dankurka): Fix up HTML unit hash change handling
@DoNotRunWith(Platform.HtmlUnitUnknown)
public void testClickLink() {
AnchorElement anchorElement = Document.get().createAnchorElement();
anchorElement.setHref("#href1");
Document.get().getBody().appendChild(anchorElement);
try {
History.newItem("something_as_base");
addHistoryListenerImpl(new ValueChangeHandler<String>() {
@Override
public void onValueChange(ValueChangeEvent<String> event) {
assertEquals("href1", event.getValue());
finishTest();
}
});
delayTestFinish(5000);
NativeEvent clickEvent =
Document.get().createClickEvent(0, 0, 0, 0, 0, false, false, false, false);
anchorElement.dispatchEvent(clickEvent);
} finally {
Document.get().getBody().removeChild(anchorElement);
}
}
/* Tests against issue #572: Double unescaping of history tokens. */
public void testDoubleEscaping() {
final String escToken = "%24%24%24";
delayTestFinish(5000);
addHistoryListenerImpl(new ValueChangeHandler<String>() {
@Override
public void onValueChange(ValueChangeEvent<String> event) {
assertEquals(escToken, event.getValue());
finishTest();
}
});
History.newItem(escToken);
}
/*
* Tests against issue #879: Ensure that empty history tokens do not add
* additional characters after the '#' symbol in the URL.
*/
public void testEmptyHistoryTokens() {
delayTestFinish(5000);
addHistoryListenerImpl(new ValueChangeHandler<String>() {
@Override
public void onValueChange(ValueChangeEvent<String> event) {
String historyToken = event.getValue();
if (historyToken == null) {
fail("historyToken should not be null");
}
if (historyToken.equals("foobar")) {
History.newItem("");
} else {
assertEquals("", historyToken);
finishTest();
}
}
});
// We must first start out with a non-blank history token. Adding a blank
// history token in the initial state will not cause an onHistoryChanged
// event to fire.
History.newItem("foobar");
}
/**
* Verify that no events are issued via newItem if there were not reqeuested.
*/
public void testNoEvents() {
delayTestFinish(5000);
addHistoryListenerImpl(new ValueChangeHandler<String>() {
@Override
public void onValueChange(ValueChangeEvent<String> event) {
fail("onHistoryChanged should not have been called");
}
});
History.newItem("testNoEvents", false);
timer = new Timer() {
@Override
public void run() {
finishTest();
}
};
timer.schedule(500);
}
/*
* Ensure that non-url-safe strings (such as those containing spaces) are
* encoded/decoded correctly, and that programmatic 'back' works.
*/
@DoNotRunWith(Platform.HtmlUnitUnknown)
public void testHistory() {
/*
* Sentinel token which should only be seen if tokens are lost during the
* rest of the test. Without this, History.back() might send the browser too
* far back, i.e. back to before the web app containing our test module.
*/
History.newItem("if-you-see-this-then-history-went-back-too-far");
final String historyToken1 = "token1";
final String historyToken2 = getHistoryToken2();
delayTestFinish(10000);
addHistoryListenerImpl(new ValueChangeHandler<String>() {
private int state = 0;
@Override
public void onValueChange(ValueChangeEvent<String> event) {
String historyToken = event.getValue();
switch (state) {
case 0: {
if (!historyToken.equals(historyToken1)) {
fail("Expecting token '" + historyToken1 + "', but got: " + historyToken);
}
state = 1;
History.newItem(historyToken2);
break;
}
case 1: {
if (!historyToken.equals(historyToken2)) {
fail("Expecting token '" + historyToken2 + "', but got: " + historyToken);
}
state = 2;
History.back();
break;
}
case 2: {
if (!historyToken.equals(historyToken1)) {
fail("Expecting token '" + historyToken1 + "', but got: " + historyToken);
}
finishTest();
break;
}
}
}
});
History.newItem(historyToken1);
}
/**
* Verify that {@link ValueChangeHandler#onValueChange(ValueChangeEvent)}
* is only called once per {@link History#newItem(String)}.
*/
public void testHistoryChangedCount() {
delayTestFinish(5000);
timer = new Timer() {
private int count = 0;
@Override
public void run() {
if (count++ == 0) {
// verify that duplicates don't issue another event
History.newItem("testHistoryChangedCount");
timer.schedule(500);
} else {
finishTest();
}
}
};
addHistoryListenerImpl(new ValueChangeHandler<String>() {
private int count = 0;
@Override
public void onValueChange(ValueChangeEvent<String> event) {
if (count++ != 0) {
fail("onHistoryChanged called multiple times");
}
// wait 500ms to see if we get called multiple times
timer.schedule(500);
}
});
History.newItem("testHistoryChangedCount");
}
@DoNotRunWith(Platform.HtmlUnitUnknown)
public void testReplaceItem() {
/*
* Sentinel token which should only be seen if tokens are lost during the rest of the test.
* Without this, History.back() might send the browser too far back, i.e. back to before the web
* app containing our test module.
*/
History.newItem("if-you-see-this-then-history-went-back-too-far");
final String historyToken1 = "token1";
final String historyToken2 = getHistoryToken2();
final String historyToken3 = "token3";
delayTestFinish(10000);
addHistoryListenerImpl(new ValueChangeHandler<String>() {
private int state = 0;
@Override
public void onValueChange(ValueChangeEvent<String> event) {
String historyToken = event.getValue();
switch (state) {
case 0: {
if (!historyToken.equals(historyToken1)) {
fail("Expecting token '" + historyToken1 + "', but got: " + historyToken);
}
state = 1;
History.newItem(historyToken2);
break;
}
case 1: {
if (!historyToken.equals(historyToken2)) {
fail("Expecting token '" + historyToken2 + "', but got: " + historyToken);
}
state = 2;
History.replaceItem(historyToken3, true);
break;
}
case 2: {
if (!historyToken.equals(historyToken3)) {
fail("Expecting token '" + historyToken3 + "', but got: " + historyToken);
}
state = 3;
History.back();
break;
}
case 3: {
if (!historyToken.equals(historyToken1)) {
fail("Expecting token '" + historyToken1 + "', but got: " + historyToken);
}
finishTest();
}
}
}
});
History.newItem(historyToken1);
}
/*
* HtmlUnit tends to fire events after adding a new item with fireEvent set
* to false. This makes 'testEmptyHistoryTokens' randomly fail after running
* this test.
*/
@DoNotRunWith(Platform.HtmlUnitBug)
public void testReplaceItemNoEvent() {
/*
* Sentinel token which should only be seen if tokens are lost during the rest of the test.
* Without this, History.back() might send the browser too far back, i.e. back to before the web
* app containing our test module.
*/
History.newItem("if-you-see-this-then-history-went-back-too-far");
final String historyToken1 = "token1";
final String historyToken2 = getHistoryToken2();
final String historyToken2_encoded = getHistoryToken2_encoded();
History.newItem(historyToken1);
addHistoryListenerImpl(new ValueChangeHandler<String>() {
@Override
public void onValueChange(ValueChangeEvent<String> event) {
fail("No event expected");
}
});
History.replaceItem(historyToken2, false);
assertEquals(historyToken2, History.getToken());
delayTestFinish(500);
timer = new Timer() {
@Override
public void run() {
// Make sure that we have updated the URL properly.
assertEquals(historyToken2_encoded, getCurrentLocationHash());
finishTest();
}
};
timer.schedule(200);
}
public void testTokenEscaping() {
final String shouldBeEncoded = "% ^[]|\"<>{}\\";
final String shouldBeEncodedAs = "%25%20%5E%5B%5D%7C%22%3C%3E%7B%7D%5C";
delayTestFinish(5000);
addHistoryListenerImpl(new ValueChangeHandler<String>() {
@Override
public void onValueChange(ValueChangeEvent<String> event) {
assertEquals(shouldBeEncodedAs, getCurrentLocationHash());
assertEquals(shouldBeEncoded, event.getValue());
finishTest();
}
});
History.newItem(shouldBeEncoded);
}
/**
* Test to make sure that there is no double unescaping of hash values.
* See https://bugzilla.mozilla.org/show_bug.cgi?id=483304
*/
@DoNotRunWith(Platform.HtmlUnitUnknown)
public void testNoDoubleTokenUnEscaping() {
final String shouldBeEncoded = "abc%20abc";
delayTestFinish(5000);
History.newItem(shouldBeEncoded);
History.newItem("someOtherToken");
History.back();
// allow browser to update the url
timer = new Timer() {
@Override
public void run() {
// make sure that value in url actually matches the original token
assertEquals(shouldBeEncoded, History.getToken());
finishTest();
}
};
timer.schedule(200);
}
public void testTokenNonescaping() {
final String shouldNotChange = "abc;,/?:@&=+$-_.!~*()ABC123foo";
delayTestFinish(5000);
addHistoryListenerImpl(new ValueChangeHandler<String>() {
@Override
public void onValueChange(ValueChangeEvent<String> event) {
assertEquals(shouldNotChange, event.getValue());
finishTest();
}
});
History.newItem(shouldNotChange);
}
/*
* Test against issue #2500. IE6 has a bug that causes it to not report any
* part of the current fragment after a '?' when read from location.hash; make
* sure that on affected browsers, we're not relying on this.
*/
public void testTokenWithQuestionmark() {
delayTestFinish(5000);
final String token = "foo?bar";
addHistoryListenerImpl(new ValueChangeHandler<String>() {
@Override
public void onValueChange(ValueChangeEvent<String> event) {
String historyToken = event.getValue();
if (historyToken == null) {
fail("historyToken should not be null");
}
assertEquals(token, historyToken);
finishTest();
}
});
History.newItem(token);
}
/**
* Test that using an empty history token works properly. There have been
* problems (see issue 2905) with this in the past on Safari.
* <p>
* Seems like a HtmlUnit bug. Need more investigation.
*/
@DoNotRunWith(Platform.HtmlUnitBug)
public void testEmptyHistoryToken() {
final ArrayList<Object> counter = new ArrayList<Object>();
addHistoryListenerImpl(new ValueChangeHandler<String>() {
@Override
public void onValueChange(ValueChangeEvent<String> event) {
counter.add(new Object());
assertFalse("Browser is borked by empty history token", isBorked());
}
});
History.newItem("x");
History.newItem("");
assertEquals("Expected two history events", 2, counter.size());
}
// Used by testEmptyHistoryToken() to catch a bizarre failure mode on Safari.
private static boolean isBorked() {
Element e = Document.get().createDivElement();
e.setInnerHTML("string");
return e.getInnerHTML().length() == 0;
}
@Override
protected void gwtTearDown() throws Exception {
if (handlerRegistration != null) {
handlerRegistration.removeHandler();
handlerRegistration = null;
}
if (timer != null) {
timer.cancel();
timer = null;
}
}
private void addHistoryListenerImpl(ValueChangeHandler<String> handler) {
this.handlerRegistration = History.addValueChangeHandler(handler);
}
private native void click(Element el) /*-{
el.click();
}-*/;
}