blob: 6ee5dd39efb1a5ca02956a427a7978e2ba04cbad [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.core.client.GWT;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.ErrorEvent;
import com.google.gwt.event.dom.client.ErrorHandler;
import com.google.gwt.event.dom.client.LoadEvent;
import com.google.gwt.event.dom.client.LoadHandler;
import com.google.gwt.junit.DoNotRunWith;
import com.google.gwt.junit.Platform;
import com.google.gwt.junit.client.GWTTestCase;
import com.google.gwt.junit.client.WithProperties;
import com.google.gwt.junit.client.WithProperties.Property;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.resources.client.ImageResource.ImageOptions;
import com.google.gwt.resources.client.impl.ImageResourcePrototype;
import com.google.gwt.user.client.Timer;
/**
* Tests for the Image widget. Images in both clipped mode and unclipped mode
* are tested, along with the transitions between the two modes.
*/
public class ImageTest extends GWTTestCase {
interface Bundle extends ClientBundle {
ImageResource prettyPiccy();
@Source("prettyPiccy.png")
@ImageOptions(preventInlining = true)
ImageResource prettyPiccyStandalone();
}
private static class TestErrorHandler implements ErrorHandler {
private Image image;
public TestErrorHandler(Image image) {
this.image = image;
}
@Override
public void onError(ErrorEvent event) {
fail("The image " + image.getUrl() + " failed to load.");
}
}
private static class TestImage extends Image {
public TestImage(Element element) {
super(element);
}
public static TestImage wrap(Element element) {
// Assert that the element is attached.
assert Document.get().getBody().isOrHasChild(element);
TestImage image = new TestImage(element);
// Mark it attached and remember it for cleanup.
image.onAttach();
RootPanel.detachOnWindowClose(image);
return image;
}
}
private abstract static class TestLoadHandler implements LoadHandler {
private boolean finished = false;
/**
* Mark the test as finished.
*/
public void finish() {
finished = true;
}
/**
* Returns true if the test has finished.
*/
public boolean isFinished() {
return finished;
}
}
@Deprecated
private abstract static class TestLoadListener implements LoadListener {
private boolean finished = false;
private Image image;
public TestLoadListener(Image image) {
this.image = image;
}
/**
* Mark the test as finished.
*/
public void finish() {
finished = true;
}
/**
* Returns true if the test has finished.
*/
public boolean isFinished() {
return finished;
}
@Override
public void onError(Widget sender) {
fail("The image " + image.getUrl() + " failed to load.");
}
}
/**
* The default timeout of asynchronous tests. This should be larger than
* LOAD_EVENT_TIMEOUT and SYNTHETIC_LOAD_EVENT_TIMEOUT.
*/
private static final int DEFAULT_TEST_TIMEOUT = 10000;
/**
* The amount of time to wait for a load event to fire in milliseconds.
*/
private static final int LOAD_EVENT_TIMEOUT = 7000;
/**
* The amount of time to wait for a clipped image to fire a synthetic load
* event in milliseconds.
*/
private static final int SYNTHETIC_LOAD_EVENT_TIMEOUT = 1000;
/**
* Helper method that allows us to 'peek' at the private <code>state</code>
* field in the Image object, and call the <code>state.getStateName()</code>
* method.
*
* @param image The image instance
* @return "unclipped" if image is in the unclipped state, or "clipped" if the
* image is in the clipped state
*/
public static native String getCurrentImageStateName(Image image) /*-{
var imgState = image.@com.google.gwt.user.client.ui.Image::state;
return imgState.@com.google.gwt.user.client.ui.Image.State::getStateName() ();
}-*/;
private int firedError;
private int firedLoad;
@Override
public String getModuleName() {
return "com.google.gwt.user.UserTest";
}
public void testAltText() {
final String altText = "this is an image";
final Image image = new Image("image.png", 12, 12, 12, 12);
assertEquals("", image.getAltText());
image.setAltText(altText);
assertEquals(altText, image.getAltText());
image.setAltText("");
assertEquals("", image.getAltText());
}
/**
* Test that attaching and immediately detaching an element does not cause an
* error.
*/
public void testAttachDetach() {
final Image image = new Image("counting-forwards.png");
RootPanel.get().add(image);
RootPanel.get().remove(image);
// Wait for the synthetic event to attempt to fire.
delayTestFinish(DEFAULT_TEST_TIMEOUT);
new Timer() {
@Override
public void run() {
// The synthetic event did not cause an error.
finishTest();
}
}.schedule(SYNTHETIC_LOAD_EVENT_TIMEOUT);
}
/**
* Tests the transition from the clipped state to the unclipped state.
*/
@DoNotRunWith(Platform.HtmlUnitUnknown)
public void testChangeClippedImageToUnclipped() {
final Image image = new Image("counting-forwards.png", 12, 13, 8, 8);
assertEquals("clipped", getCurrentImageStateName(image));
delayTestFinish(DEFAULT_TEST_TIMEOUT);
image.addErrorHandler(new TestErrorHandler(image));
image.addLoadHandler(new LoadHandler() {
private int onLoadEventCount = 0;
@Override
public void onLoad(LoadEvent event) {
++onLoadEventCount;
if (onLoadEventCount == 1) { // Set the url after the first image loads
image.setUrl("counting-forwards.png");
} else if (onLoadEventCount == 2) {
assertEquals(0, image.getOriginLeft());
assertEquals(0, image.getOriginTop());
assertEquals(32, image.getWidth());
assertEquals(32, image.getHeight());
assertEquals("unclipped", getCurrentImageStateName(image));
finishTest();
}
}
});
RootPanel.get().add(image);
}
/**
* Tests the transition from the unclipped state to the clipped state.
*/
@DoNotRunWith(Platform.HtmlUnitUnknown)
public void testChangeImageToClipped() {
final Image image = new Image("counting-forwards.png");
assertEquals("unclipped", getCurrentImageStateName(image));
delayTestFinish(DEFAULT_TEST_TIMEOUT);
image.addErrorHandler(new TestErrorHandler(image));
image.addLoadHandler(new LoadHandler() {
private int onLoadEventCount = 0;
@Override
public void onLoad(LoadEvent event) {
if (getCurrentImageStateName(image).equals("unclipped")) {
image.setVisibleRect(12, 13, 8, 8);
}
if (++onLoadEventCount == 2) {
assertEquals(12, image.getOriginLeft());
assertEquals(13, image.getOriginTop());
assertEquals(8, image.getWidth());
assertEquals(8, image.getHeight());
assertEquals("clipped", getCurrentImageStateName(image));
finishTest();
}
}
});
RootPanel.get().add(image);
}
/**
* Tests the transition from the unclipped state to the clipped state
* before a load event fires.
*/
@DoNotRunWith(Platform.HtmlUnitUnknown)
public void testChangeImageToClippedSynchronously() {
final Image image = new Image("counting-forwards.png");
assertEquals("unclipped", getCurrentImageStateName(image));
image.addErrorHandler(new TestErrorHandler(image));
final TestLoadHandler loadHandler = new TestLoadHandler() {
@Override
public void onLoad(LoadEvent event) {
if (isFinished()) {
fail("LoadHandler fired twice. Expected it to fire only once.");
}
assertEquals("clipped", getCurrentImageStateName(image));
assertEquals(12, image.getOriginLeft());
assertEquals(13, image.getOriginTop());
assertEquals(8, image.getWidth());
assertEquals(8, image.getHeight());
finish();
}
};
image.addLoadHandler(loadHandler);
/*
* Change the image to a clipped image before a load event fires. We only
* expect one asynchronous load event to fire for the final state. This is
* consistent with the expected behavior of changing the source URL multiple
* times.
*/
RootPanel.get().add(image);
image.setVisibleRect(12, 13, 8, 8);
assertEquals("clipped", getCurrentImageStateName(image));
delayTestFinish(DEFAULT_TEST_TIMEOUT);
new Timer() {
@Override
public void run() {
assertTrue(loadHandler.isFinished());
finishTest();
}
}.schedule(SYNTHETIC_LOAD_EVENT_TIMEOUT);
}
/**
* Tests the creation of an image in unclipped mode.
*/
public void testCreateImage() {
final Image image = new Image("counting-forwards.png");
delayTestFinish(DEFAULT_TEST_TIMEOUT);
image.addErrorHandler(new TestErrorHandler(image));
image.addLoadHandler(new LoadHandler() {
private int onLoadEventCount = 0;
@Override
public void onLoad(LoadEvent event) {
if (++onLoadEventCount == 1) {
assertEquals(32, image.getWidth());
assertEquals(32, image.getHeight());
finishTest();
}
}
});
RootPanel.get().add(image);
assertEquals(0, image.getOriginLeft());
assertEquals(0, image.getOriginTop());
assertEquals("unclipped", getCurrentImageStateName(image));
}
/**
* Tests the creation of an image that does not exist.
*/
@DoNotRunWith(Platform.HtmlUnitUnknown)
public void testCreateImageWithError() {
final Image image = new Image("imageDoesNotExist.png");
delayTestFinish(DEFAULT_TEST_TIMEOUT);
image.addErrorHandler(new ErrorHandler() {
@Override
public void onError(ErrorEvent event) {
finishTest();
}
});
image.addLoadHandler(new LoadHandler() {
@Override
public void onLoad(LoadEvent event) {
fail("The image " + image.getUrl() + " should have failed to load.");
}
});
RootPanel.get().add(image);
}
/**
* Tests the firing of onload events when
* {@link com.google.gwt.user.client.ui.Image#setUrl(String)} is called on an
* unclipped image.
*/
public void testSetUrlAndLoadEventsOnUnclippedImage() {
final Image image = new Image();
delayTestFinish(DEFAULT_TEST_TIMEOUT);
image.addErrorHandler(new TestErrorHandler(image));
image.addLoadHandler(new LoadHandler() {
private int onLoadEventCount = 0;
@Override
public void onLoad(LoadEvent event) {
if (++onLoadEventCount == 2) {
finishTest();
} else {
image.setUrl("counting-forwards.png");
}
}
});
RootPanel.get().add(image);
image.setUrl("counting-backwards.png");
}
/**
* Tests the behavior of
* <code>setUrlAndVisibleRect(String, int, int, int, int)</code> method on an
* unclipped image, which causes a state transition to the clipped state.
*/
@DoNotRunWith(Platform.HtmlUnitUnknown)
public void testSetUrlAndVisibleRectOnUnclippedImage() {
final Image image = new Image("counting-backwards.png");
assertEquals("unclipped", getCurrentImageStateName(image));
delayTestFinish(DEFAULT_TEST_TIMEOUT);
image.addErrorHandler(new TestErrorHandler(image));
image.addLoadHandler(new LoadHandler() {
private int onLoadEventCount = 0;
@Override
public void onLoad(LoadEvent event) {
if (getCurrentImageStateName(image).equals("unclipped")) {
image.setUrlAndVisibleRect("counting-forwards.png", 0, 16, 16, 16);
}
if (++onLoadEventCount == 2) {
assertEquals(0, image.getOriginLeft());
assertEquals(16, image.getOriginTop());
assertEquals(16, image.getWidth());
assertEquals(16, image.getHeight());
assertEquals("clipped", getCurrentImageStateName(image));
finishTest();
}
}
});
RootPanel.get().add(image);
}
/**
* Tests the creation of an image in clipped mode.
*/
@SuppressWarnings("deprecation")
public void testCreateClippedImage() {
final Image image = new Image("counting-forwards.png", 16, 16, 16, 16);
delayTestFinish(DEFAULT_TEST_TIMEOUT);
final TestLoadListener listener = new TestLoadListener(image) {
private int onLoadEventCount = 0;
@Override
public void onLoad(Widget sender) {
if (++onLoadEventCount == 1) {
assertEquals(16, image.getWidth());
assertEquals(16, image.getHeight());
finish();
}
}
};
image.addLoadListener(listener);
image.addLoadHandler(new LoadHandler() {
private int onLoadEventCount = 0;
@Override
public void onLoad(LoadEvent event) {
if (++onLoadEventCount == 1) {
assertEquals(16, image.getWidth());
assertEquals(16, image.getHeight());
if (listener.isFinished()) {
finishTest();
} else {
fail("Listener did not fire first");
}
}
}
});
image.addErrorHandler(new TestErrorHandler(image));
RootPanel.get().add(image);
assertEquals(16, image.getOriginLeft());
assertEquals(16, image.getOriginTop());
assertEquals("clipped", getCurrentImageStateName(image));
}
@SuppressWarnings("deprecation")
public void testLoadListenerWiring() {
Image im = new Image();
im.addLoadListener(new LoadListener() {
@Override
public void onError(Widget sender) {
++firedError;
}
@Override
public void onLoad(Widget sender) {
++firedLoad;
}
});
im.fireEvent(new LoadEvent() {
// Replaced by Joel's event firing when possible.
});
assertEquals(1, firedLoad);
assertEquals(0, firedError);
im.fireEvent(new ErrorEvent() {
// Replaced by Joel's event firing when possible.
});
assertEquals(1, firedLoad);
assertEquals(1, firedError);
}
/**
* Test that attaching an image multiple times results in only one load event.
*/
public void testMultipleAttach() {
final Image image = new Image("counting-forwards.png");
final TestLoadHandler loadHandler = new TestLoadHandler() {
@Override
public void onLoad(LoadEvent event) {
if (isFinished()) {
fail("LoadHandler fired multiple times.");
}
finish();
}
};
image.addErrorHandler(new TestErrorHandler(image));
image.addLoadHandler(loadHandler);
RootPanel.get().add(image);
RootPanel.get().remove(image);
RootPanel.get().add(image);
RootPanel.get().remove(image);
RootPanel.get().add(image);
RootPanel.get().remove(image);
RootPanel.get().add(image);
delayTestFinish(DEFAULT_TEST_TIMEOUT);
new Timer() {
@Override
public void run() {
assertTrue(loadHandler.isFinished());
finishTest();
}
}.schedule(LOAD_EVENT_TIMEOUT);
}
/**
* Verify that detaching and reattaching an image in a handler does not fire a
* second onload event.
*/
public void testNoEventOnReattachInHandler() {
final Image image = new Image("counting-forwards.png");
delayTestFinish(DEFAULT_TEST_TIMEOUT);
image.addErrorHandler(new TestErrorHandler(image));
image.addLoadHandler(new LoadHandler() {
private int onLoadEventCount = 0;
@Override
public void onLoad(LoadEvent event) {
if (++onLoadEventCount == 1) {
RootPanel.get().remove(image);
RootPanel.get().add(image);
// The extra onLoad would will fire synchronously before finishTest().
finishTest();
} else {
fail("onLoad fired on reattach.");
}
}
});
RootPanel.get().add(image);
}
public void testOneEventOnly() {
final Image image = new Image("counting-forwards.png");
final TestLoadHandler loadHandler = new TestLoadHandler() {
@Override
public void onLoad(LoadEvent event) {
if (isFinished()) {
fail("LoadHandler fired multiple times.");
}
finish();
}
};
image.addErrorHandler(new TestErrorHandler(image));
image.addLoadHandler(loadHandler);
RootPanel.get().add(image);
delayTestFinish(DEFAULT_TEST_TIMEOUT);
new Timer() {
@Override
public void run() {
assertTrue(loadHandler.isFinished());
finishTest();
}
}.schedule(LOAD_EVENT_TIMEOUT);
}
public void testOneEventOnlyClippedImage() {
final Image image = new Image("counting-forwards.png", 12, 13, 8, 8);
final TestLoadHandler loadHandler = new TestLoadHandler() {
@Override
public void onLoad(LoadEvent event) {
if (isFinished()) {
fail("LoadHandler fired multiple times.");
}
finish();
}
};
image.addErrorHandler(new TestErrorHandler(image));
image.addLoadHandler(loadHandler);
RootPanel.get().add(image);
delayTestFinish(DEFAULT_TEST_TIMEOUT);
new Timer() {
@Override
public void run() {
assertTrue(loadHandler.isFinished());
finishTest();
}
}.schedule(LOAD_EVENT_TIMEOUT);
}
@WithProperties({
@Property(name = "ClientBundle.enableInlining", value = "false")
})
public void testResourceConstructor() {
Bundle b = GWT.create(Bundle.class);
Image image = new Image(b.prettyPiccy());
assertResourceWorked(image, b.prettyPiccy());
assertTrue(b.prettyPiccy() instanceof ImageResourcePrototype.Bundle);
assertEquals("clipped", getCurrentImageStateName(image));
}
@WithProperties({
@Property(name = "ClientBundle.enableInlining", value = "false")
})
public void testSetResource() {
Bundle b = GWT.create(Bundle.class);
Image image = new Image();
image.setResource(b.prettyPiccy());
assertResourceWorked(image, b.prettyPiccy());
assertTrue(b.prettyPiccy() instanceof ImageResourcePrototype.Bundle);
assertEquals("clipped", getCurrentImageStateName(image));
}
public void testStandaloneResourceConstructor() {
Bundle b = GWT.create(Bundle.class);
Image image = new Image(b.prettyPiccyStandalone());
assertResourceWorked(image, b.prettyPiccyStandalone());
assertFalse(b.prettyPiccyStandalone() instanceof ImageResourcePrototype.Bundle);
assertEquals("unclipped", getCurrentImageStateName(image));
}
public void testStandaloneSetResource() {
Bundle b = GWT.create(Bundle.class);
Image image = new Image();
image.setResource(b.prettyPiccyStandalone());
assertResourceWorked(image, b.prettyPiccyStandalone());
assertFalse(b.prettyPiccyStandalone() instanceof ImageResourcePrototype.Bundle);
assertEquals("unclipped", getCurrentImageStateName(image));
}
/**
* Tests the behavior of
* {@link com.google.gwt.user.client.ui.Image#setUrlAndVisibleRect(String,int,int,int,int)}
* on a clipped image.
*/
@SuppressWarnings("deprecation")
public void testSetUrlAndVisibleRectOnClippedImage() {
final Image image = new Image("counting-backwards.png", 12, 12, 12, 12);
final TestLoadListener listener = new TestLoadListener(image) {
@Override
public void onLoad(Widget sender) {
if (isFinished()) {
fail("LoadListener fired twice. Expected it to fire only once.");
}
assertEquals(0, image.getOriginLeft());
assertEquals(16, image.getOriginTop());
assertEquals(16, image.getWidth());
assertEquals(16, image.getHeight());
assertEquals("clipped", getCurrentImageStateName(image));
finish();
}
};
image.addLoadListener(listener);
final TestLoadHandler loadHandler = new TestLoadHandler() {
@Override
public void onLoad(LoadEvent event) {
if (isFinished()) {
fail("LoadHandler fired twice. Expected it to fire only once.");
}
if (listener.isFinished()) {
finish();
} else {
fail("Listener did not fire first");
}
}
};
image.addLoadHandler(loadHandler);
image.addErrorHandler(new TestErrorHandler(image));
RootPanel.get().add(image);
assertEquals("clipped", getCurrentImageStateName(image));
/*
* Change the url and visible rect, but we only expect one asynchronous load
* event to fire. This is consistent with the expected behavior of changing
* the source URL in the same event loop.
*/
image.setUrlAndVisibleRect("counting-forwards.png", 0, 16, 16, 16);
delayTestFinish(DEFAULT_TEST_TIMEOUT);
new Timer() {
@Override
public void run() {
assertTrue(loadHandler.isFinished());
finishTest();
}
}.schedule(SYNTHETIC_LOAD_EVENT_TIMEOUT);
}
/**
* Tests the firing of onload events when calling
* {@link com.google.gwt.user.client.ui.Image#setVisibleRect(int,int,int,int)}
* on a clipped image.
*/
@SuppressWarnings("deprecation")
public void testSetVisibleRectAndLoadEventsOnClippedImage() {
final Image image = new Image("counting-backwards.png", 16, 16, 16, 16);
final TestLoadListener listener = new TestLoadListener(image) {
@Override
public void onLoad(Widget sender) {
if (isFinished()) {
fail("LoadListener fired twice. Expected it to fire only once.");
}
finish();
}
};
image.addLoadListener(listener);
final TestLoadHandler loadHandler = new TestLoadHandler() {
@Override
public void onLoad(LoadEvent event) {
if (isFinished()) {
fail("LoadHandler fired twice. Expected it to fire only once.");
}
if (listener.isFinished()) {
finish();
} else {
fail("Listener did not fire first");
}
}
};
image.addLoadHandler(loadHandler);
image.addErrorHandler(new TestErrorHandler(image));
RootPanel.get().add(image);
/*
* Change the visible rect multiple times, but we only expect one
* asynchronous load event to fire after the final change. This is consistent
* with the expected behavior of changing the source URL multiple times.
*/
image.setVisibleRect(0, 0, 16, 16);
image.setVisibleRect(0, 0, 16, 16);
image.setVisibleRect(16, 0, 16, 16);
image.setVisibleRect(16, 8, 8, 8);
delayTestFinish(DEFAULT_TEST_TIMEOUT);
new Timer() {
@Override
public void run() {
assertTrue(loadHandler.isFinished());
finishTest();
}
}.schedule(SYNTHETIC_LOAD_EVENT_TIMEOUT);
}
/**
* Tests that it is possible to make a subclass of Image that can be wrapped.
*/
public void testWrapOfSubclass() {
String uid = Document.get().createUniqueId();
DivElement div = Document.get().createDivElement();
div.setInnerHTML("<img id='" + uid + "' src='counting-forwards.png'>");
Document.get().getBody().appendChild(div);
final TestImage image = TestImage.wrap(Document.get().getElementById(uid));
assertNotNull(image);
// Cleanup.
Document.get().getBody().appendChild(div);
RootPanel.detachNow(image);
}
/**
* Tests that wrapping an existing DOM element works if you call
* setUrlAndVisibleRect() on it.
*/
public void testWrapThenSetUrlAndVisibleRect() {
String uid = Document.get().createUniqueId();
DivElement div = Document.get().createDivElement();
div.setInnerHTML("<img id='" + uid
+ "' src='counting-backwards.png' width='16' height='16'>");
Document.get().getBody().appendChild(div);
final Image image = Image.wrap(Document.get().getElementById(uid));
assertEquals(0, image.getOriginLeft());
assertEquals(0, image.getOriginTop());
assertEquals(16, image.getWidth());
assertEquals(16, image.getHeight());
assertEquals("unclipped", getCurrentImageStateName(image));
image.setUrlAndVisibleRect("counting-forwards.png", 0, 16, 16, 16);
assertEquals(0, image.getOriginLeft());
assertEquals(16, image.getOriginTop());
assertEquals(16, image.getWidth());
assertEquals(16, image.getHeight());
assertEquals("clipped", getCurrentImageStateName(image));
}
private void assertResourceWorked(Image image, ImageResource prettyPiccy) {
assertEquals(prettyPiccy.getSafeUri().asString(), image.getUrl());
assertEquals(prettyPiccy.getTop(), image.getOriginTop());
assertEquals(prettyPiccy.getHeight(), image.getHeight());
assertEquals(prettyPiccy.getLeft(), image.getOriginLeft());
assertEquals(prettyPiccy.getWidth(), image.getWidth());
}
}