/* * Copyright 2011 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.dom.builder.shared; import com.google.gwt.dom.builder.client.DomBuilderFactory; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.junit.client.GWTTestCase; import com.google.gwt.safehtml.shared.SafeHtmlUtils; /** * Base tests for subclasses of {@link ElementBuilderBase}. * * @param <T> the type of builder being tested */ public abstract class ElementBuilderTestBase<T extends ElementBuilderBase<?>> extends GWTTestCase { /** * A command that operates on a builder. */ protected interface BuilderCommand<T extends ElementBuilderBase<?>> { /** * Execute an action. * * @param builder the builder being tested */ void execute(T builder); } /** * The GWT module name when running tests on the client. */ protected static final String GWT_MODULE_NAME = "com.google.gwt.dom.builder.DomBuilder"; /** * Indicates whether or not the element under test supports child elements. */ private boolean isChildElementSupported; /** * Indicates whether or not the element under test supports an end tag. */ private boolean isEndTagForbidden; /** * Indicates whether or not the element under test supports inner html. */ private boolean isInnerHtmlSupported; /** * Indicates whether or not the element under test supports inner text. */ private boolean isInnerTextSupported; @Override public String getModuleName() { // Default to JVM implementation. return null; } /** * Test that you cannot append text, html, or elements after setting the text. */ public void testAppendAfterHtml() { // Skip this test if inner html is not supported. if (!isInnerHtmlSupported) { return; } for (ElementBuilderFactory factory : getFactories()) { T builder = createElementBuilder(factory); builder.html(SafeHtmlUtils.fromString("Hello World")); try { builder.text("moretext"); fail("Expected IllegalStateException: setting text after setting html"); } catch (IllegalStateException e) { // Expected. } try { builder.html(SafeHtmlUtils.fromString("morehtml")); fail("Expected IllegalStateException: setting html twice"); } catch (IllegalStateException e) { // Expected. } try { builder.startDiv(); fail("Expected IllegalStateException: appending a div after setting html"); } catch (IllegalStateException e) { // Expected. } } } /** * Test that you cannot append text, html, or elements after setting the text. */ public void testAppendAfterText() { // Skip this test if inner text is not supported. if (!isInnerTextSupported) { return; } for (ElementBuilderFactory factory : getFactories()) { T builder = createElementBuilder(factory); builder.text("Hello World"); try { builder.text("moretext"); fail("Expected IllegalStateException: setting text twice"); } catch (IllegalStateException e) { // Expected. } if (isInnerHtmlSupported) { try { builder.html(SafeHtmlUtils.fromString("morehtml")); fail("Expected IllegalStateException: setting html after setting text"); } catch (IllegalStateException e) { // Expected. } } if (isChildElementSupported) { try { builder.startDiv(); fail("Expected IllegalStateException: appending a div after setting text"); } catch (IllegalStateException e) { // Expected. } } } } public void testAttributeAfterAppendHtml() { // String value. assertActionFailsAfterAppendHtml(new BuilderCommand<T>() { @Override public void execute(T builder) { builder.attribute("name", "value"); } }, "Cannot add attribute after appending html"); // Int value. assertActionFailsAfterAppendHtml(new BuilderCommand<T>() { @Override public void execute(T builder) { builder.attribute("name", 1); } }, "Cannot add attribute after appending html"); } public void testAttributeAfterEnd() { assertActionFailsAfterEnd(new BuilderCommand<T>() { @Override public void execute(T builder) { builder.attribute("name", "value"); } }, "Cannot add attribute after adding a child element"); } public void testClassNameAfterAppendHtml() { assertActionFailsAfterAppendHtml(new BuilderCommand<T>() { @Override public void execute(T builder) { builder.className("value"); } }, "Cannot add attribute after appending html"); } public void testDirNameAfterAppendHtml() { assertActionFailsAfterAppendHtml(new BuilderCommand<T>() { @Override public void execute(T builder) { builder.dir("value"); } }, "Cannot add attribute after appending html"); } public void testDraggableNameAfterAppendHtml() { assertActionFailsAfterAppendHtml(new BuilderCommand<T>() { @Override public void execute(T builder) { builder.draggable("value"); } }, "Cannot add attribute after appending html"); } public void testEnd() { for (ElementBuilderFactory factory : getFactories()) { // Test that a builder can be ended if it comes directly from the factory. { T builder = createElementBuilder(factory); builder.id("myid"); endElement(builder); } /* * Test that a builder can be ended if it was started from another * builder. * * Skip this test if child elements are not supported. */ if (isChildElementSupported) { T builder = createElementBuilder(factory); T elem = startElement(builder); endElement(elem); builder.end(); } } } public void testEndUnmatchedTagName() { // Skip this test if child elements are not supported. if (!isChildElementSupported) { return; } for (ElementBuilderFactory factory : getFactories()) { T builder = createElementBuilder(factory); T child = startElement(builder); try { child.end("notamatch"); fail("Expected IllegalStateException: end tag does not match start tag"); } catch (IllegalStateException e) { // Expected. } } } public void testHtmlAfterAppend() { assertActionFailsAfterAppendHtml(new BuilderCommand<T>() { @Override public void execute(T builder) { builder.html(SafeHtmlUtils.fromString("hello world")); } }, "Cannot set html after appending a child element"); } public void testHtmlAfterEnd() { if (!isInnerHtmlSupported) { return; } assertActionFailsAfterEnd(new BuilderCommand<T>() { @Override public void execute(T builder) { builder.html(SafeHtmlUtils.fromString("hello world")); } }, "Cannot set html after adding a child element"); } /** * Test that HTML can be set after ending one element and starting another. */ public void testHtmlAfterRestart() { if (!isInnerHtmlSupported || !isChildElementSupported) { return; } for (ElementBuilderFactory factory : getFactories()) { T builder = createElementBuilder(factory); builder.startDiv().id("test").html(SafeHtmlUtils.fromString("test")).end(); // Should not cause any errors. builder.startDiv().html(SafeHtmlUtils.fromString("hello")); } } /** * Test that all implementations of a builder are consistent in their support * of setting inner html. */ public void testHtmlConsistent() { boolean expected = false; ElementBuilderFactory[] factories = getFactories(); for (int i = 0; i < factories.length; i++) { T builder = createElementBuilder(factories[0]); boolean isSupported = true; try { builder.html(SafeHtmlUtils.EMPTY_SAFE_HTML); } catch (UnsupportedOperationException e) { // Child elements are not supported. isSupported = false; } assertEquals(isInnerHtmlSupported, isSupported); } } public void testIdNameAfterAppendHtml() { assertActionFailsAfterAppendHtml(new BuilderCommand<T>() { @Override public void execute(T builder) { builder.id("value"); } }, "Cannot add attribute after appending html"); } public void testIsEndTagForbidden() { // Skip this test if the end tag is allowed. if (!isEndTagForbidden) { return; } for (ElementBuilderFactory factory : getFactories()) { try { T builder = createElementBuilder(factory); builder.html(SafeHtmlUtils.fromString("html is not allowed")); fail("Expected UnsupportedOperationException: cannot set html if end tag is forbidden"); } catch (UnsupportedOperationException e) { // Expected. } try { T builder = createElementBuilder(factory); builder.text("text is not allowed"); fail("Expected UnsupportedOperationException: cannot set text if end tag is forbidden"); } catch (UnsupportedOperationException e) { // Expected. } try { T builder = createElementBuilder(factory); builder.startDiv(); fail("Expected UnsupportedOperationException: " + "cannot append a child if end tag is forbidden"); } catch (UnsupportedOperationException e) { // Expected. } } } /** * Test that all implementations of a builder return the same value from * {@link ElementBuilderBase#isEndTagForbidden()}. */ public void testIsEndTagForbiddenConsistent() { boolean expected = false; ElementBuilderFactory[] factories = getFactories(); for (int i = 0; i < factories.length; i++) { T builder = createElementBuilder(factories[0]); assertEquals(isEndTagForbidden, builder.isEndTagForbidden()); } } public void testLangAfterAppendHtml() { assertActionFailsAfterAppendHtml(new BuilderCommand<T>() { @Override public void execute(T builder) { builder.lang("value"); } }, "Cannot add attribute after appending html"); } /** * Test that all implementations of a builder are consistent in their support * of appending child elements. */ public void testStartConsistent() { boolean expected = false; ElementBuilderFactory[] factories = getFactories(); for (int i = 0; i < factories.length; i++) { T builder = createElementBuilder(factories[0]); assertEquals(isChildElementSupported, builder.isChildElementSupported()); } } public void testStartSecondTopLevelElement() { for (ElementBuilderFactory factory : getFactories()) { T builder = createElementBuilder(factory); builder.end(); // Close the top level attribute. try { startElement(builder); fail("Expected IllegalStateException: Cannot start multiple top level attributes"); } catch (IllegalStateException e) { // Expected. } } } public void testStylePropertyAfterAppendHtml() { assertActionFailsAfterAppendHtml(new BuilderCommand<T>() { @Override public void execute(T builder) { // Accessing the style builder is allowed. StylesBuilder style; try { style = builder.style(); } catch (RuntimeException e) { fail("Accessing StyleBuilder should not trigger an error: " + e.getMessage()); throw e; } // Using it is not. style.trustedProperty("name", "value"); } }, "Cannot access StyleBuilder appending html"); } public void testStylePropertyAfterEnd() { assertActionFailsAfterEnd(new BuilderCommand<T>() { @Override public void execute(T builder) { // Accessing the style builder is allowed. StylesBuilder style; try { style = builder.style(); } catch (RuntimeException e) { fail("Accessing StyleBuilder should not trigger an error: " + e.getMessage()); throw e; } // Using it is not. style.trustedProperty("name", "value"); } }, "Cannot access StyleBuilder appending html"); } /** * Test that you cannot add style properties after interrupting them with an * attribute. */ public void testStyleTwice() { for (ElementBuilderFactory factory : getFactories()) { T builder = createElementBuilder(factory); // Access style first time. StylesBuilder style = builder.style().borderWidth(1.0, Unit.PX).fontSize(10.0, Unit.PX); // Access style again, without interruption. builder.style().trustedColor("red"); // Add an attribute. builder.id("id"); // Accessing style after interruption is allowed. StylesBuilder style0 = builder.style(); // Using it is not. try { style0.left(1.0, Unit.PX); fail("Expected IllegalStateException: Cannot access StyleBuilder after interruption"); } catch (IllegalStateException e) { // Expected. } // Reuse existing style after interruption. try { style.left(1.0, Unit.PX); fail("Expected IllegalStateException: Cannot access StyleBuilder after interruption"); } catch (IllegalStateException e) { // Expected. } } } public void testTabIndexAfterAppendHtml() { assertActionFailsAfterAppendHtml(new BuilderCommand<T>() { @Override public void execute(T builder) { builder.tabIndex(1); } }, "Cannot add attribute after appending html"); } public void testTextAfterAppend() { assertActionFailsAfterAppendHtml(new BuilderCommand<T>() { @Override public void execute(T builder) { builder.text("hello world"); } }, "Cannot set html after appending a child element"); } public void testTextAfterEnd() { if (!isInnerTextSupported) { return; } assertActionFailsAfterEnd(new BuilderCommand<T>() { @Override public void execute(T builder) { builder.text("hello world"); } }, "Cannot set text after adding a child element"); } /** * Test that text can be set after ending one element and starting another. */ public void testTextAfterRestart() { if (!isInnerTextSupported || !isChildElementSupported) { return; } for (ElementBuilderFactory factory : getFactories()) { T builder = createElementBuilder(factory); builder.startDiv().id("test").text("test").end(); // Should not cause any errors. builder.startDiv().text("hello"); } } /** * Test that all implementations of a builder are consistent in their support * of setting inner text. */ public void testTextConsistent() { boolean expected = false; ElementBuilderFactory[] factories = getFactories(); for (int i = 0; i < factories.length; i++) { T builder = createElementBuilder(factories[0]); boolean isSupported = true; try { builder.text(""); } catch (UnsupportedOperationException e) { // Child elements are not supported. isSupported = false; } assertEquals(isInnerTextSupported, isSupported); } } public void testTitleAfterAppendHtml() { assertActionFailsAfterAppendHtml(new BuilderCommand<T>() { @Override public void execute(T builder) { builder.title("value"); } }, "Cannot add attribute after appending html"); } /** * Test that the specified command triggers an {@link IllegalStateException} * after appending html to the element. * * @param action the command to execute * @param message the failure message if the test fails */ protected void assertActionFailsAfterAppendHtml(BuilderCommand<T> action, String message) { // Skip this test if inner html is not supported. if (!isInnerHtmlSupported) { return; } for (ElementBuilderFactory factory : getFactories()) { T builder = createElementBuilder(factory); builder.html(SafeHtmlUtils.EMPTY_SAFE_HTML); try { action.execute(builder); fail("Expected IllegalStateException: " + message); } catch (IllegalStateException e) { // Expected. } } } /** * Test that the specified command triggers an {@link IllegalStateException} * after ending a child element. * * @param action the command to execute * @param message the failure message if the test fails */ protected void assertActionFailsAfterEnd(BuilderCommand<T> action, String message) { // Skip this test if child elements are not supported. if (!isChildElementSupported) { return; } for (ElementBuilderFactory factory : getFactories()) { T builder = createElementBuilder(factory); // Add a child. builder.startDiv().end(); try { action.execute(builder); fail("Expected IllegalStateException: " + message); } catch (IllegalStateException e) { // Expected. } } } /** * Create an {@link ElementBuilderBase} to test. * * @param factory the {@link ElementBuilderFactory} used to create the element */ protected abstract T createElementBuilder(ElementBuilderFactory factory); /** * End the element within an existing builder. * * @param builder the existing builder */ protected abstract void endElement(ElementBuilderBase<?> builder); /** * Get the array of factories to test. * * @return an array of factories to test. */ protected ElementBuilderFactory[] getFactories() { if (getModuleName() == null) { // JRE tests only work with HtmlBuilderFactory. return new ElementBuilderFactory[]{HtmlBuilderFactory.get()}; } else { // GWT tests work with both implementations. return new ElementBuilderFactory[]{HtmlBuilderFactory.get(), DomBuilderFactory.get()}; } } @Override protected void gwtSetUp() throws Exception { isChildElementSupported = createElementBuilder(getFactories()[0]).isChildElementSupported(); isEndTagForbidden = createElementBuilder(getFactories()[0]).isEndTagForbidden(); isInnerHtmlSupported = true; isInnerTextSupported = true; try { createElementBuilder(getFactories()[0]).html(SafeHtmlUtils.EMPTY_SAFE_HTML); } catch (UnsupportedOperationException e) { isInnerHtmlSupported = false; } try { createElementBuilder(getFactories()[0]).text(""); } catch (UnsupportedOperationException e) { isInnerTextSupported = false; } } protected boolean isInnerHtmlSupported() { return true; } /** * Start a new element within an existing builder. * * @param builder the existing builder * @return the builder for the new element */ protected abstract T startElement(ElementBuilderBase<?> builder); }