/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.apache.wicket.util.tester; import static org.apache.wicket.markup.parser.filter.HtmlHandler.requiresCloseTag; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Stack; import java.util.function.Function; import java.util.regex.Pattern; import org.apache.wicket.WicketRuntimeException; import org.apache.wicket.markup.parser.XmlPullParser; import org.apache.wicket.markup.parser.XmlTag; import org.apache.wicket.util.lang.Args; import org.apache.wicket.util.string.Strings; import org.apache.wicket.util.value.IValueMap; /** * Tag tester is used to test that a generated markup tag contains the correct attributes, values * etc. This can be done instead of comparing generated markup with some expected markup. The * advantage of this is that a lot of tests don't fail when the generated markup changes just a * little bit. * <p> * It also gives a more programmatic way of testing the generated output, by not having to worry * about precisely how the markup looks instead of which attributes exists on the given tags, and * what values they have. * <p> * Example: * * <pre> * ... * TagTester tagTester = application.getTagByWicketId("form"); * assertTrue(tag.hasAttribute("action")); * ... * </pre> * * @since 1.2.6 */ public class TagTester { private static final Pattern AJAX_COMPONENT_CDATA_OPEN = Pattern.compile("<component.*?><!\\[CDATA\\["); private static final Pattern AJAX_COMPONENT_CDATA_CLOSE = Pattern.compile("\\]\\]></component>"); private final XmlTag openTag; private final XmlTag closeTag; private final XmlPullParser parser; /** * Constructor. * * @param parser * an <code>XmlPullParser</code> * @param openTag * an opening XML tag * @param closeTag * a closing XML tag */ private TagTester(XmlPullParser parser, XmlTag openTag, XmlTag closeTag) { this.parser = parser; this.openTag = openTag; this.closeTag = closeTag; } /** * Gets the tag's name. * * @return the tag name */ public String getName() { return openTag.getName(); } /** * Tests if the tag contains the given attribute. Please note that this is non case-sensitive, * because attributes in HTML may be non case-sensitive. * * @param attribute * an attribute to look for in the tag * @return <code>true</code> if the tag has the attribute, <code>false</code> if not. */ public boolean hasAttribute(String attribute) { boolean hasAttribute = false; if (getAttribute(attribute) != null) { hasAttribute = true; } return hasAttribute; } /** * Gets the value for a given attribute. Please note that this is non case-sensitive, because * attributes in HTML may be non case-sensitive. * * @param attribute * an attribute to look for in the tag * @return the value of the attribute or <code>null</code> if it isn't found. */ public String getAttribute(String attribute) { String value = null; IValueMap attributeMap = openTag.getAttributes(); if (attributeMap != null) { for (String attr : attributeMap.keySet()) { if (attr.equalsIgnoreCase(attribute)) { value = attributeMap.getString(attr); } } } return value; } /** * Checks if an attribute contains the specified partial value. * <p> * For example: * * <p> * <b>Markup:</b> * * <pre> * <span wicket:id="helloComp" class="style1 style2">Hello</span> * </pre> * * <p> * <b>Test:</b> * * <pre> * TagTester tester = application.getTagByWicketId("helloComp"); * assertTrue(tester.getAttributeContains("class", "style2")); * </pre> * * @param attribute * the attribute to test on * @param partialValue * the partial value to test if the attribute value contains it * @return <code>true</code> if the attribute value contains the partial value */ public boolean getAttributeContains(String attribute, String partialValue) { boolean contains = false; if (partialValue != null) { String value = getAttribute(attribute); if (value != null) { if (value.contains(partialValue)) { contains = true; } } } return contains; } /** * Checks if an attribute's value is the exact same as the given value. * * @param attribute * an attribute to test * @param expected * the value which should be the same at the attribute's value * @return <code>true</code> if the attribute's value is the same as the given value */ public boolean getAttributeIs(String attribute, String expected) { boolean is = false; String val = getAttribute(attribute); if (val == null && expected == null || expected != null && expected.equals(val)) { is = true; } return is; } /** * Checks if an attribute's value ends with the given parameter. * * @param attribute * an attribute to test * @param expected * the expected value * @return <code>true</code> if the attribute's value ends with the expected value */ public boolean getAttributeEndsWith(String attribute, String expected) { boolean endsWith = false; if (expected != null) { String val = getAttribute(attribute); if (val != null) { if (val.endsWith(expected)) { endsWith = true; } } } return endsWith; } /** * Checks if the tag has a child with the given <code>tagName</code>. * * @param tagName * the tag name to search for * @return <code>true</code> if this tag has a child with the given <code>tagName</code>. */ public boolean hasChildTag(String tagName) { Args.notEmpty(tagName, "tagName"); boolean hasChild = false; if (openTag.isOpen()) { try { // Get the content of the tag int startPos = openTag.getPos() + openTag.getLength(); int endPos = closeTag.getPos(); String markup = parser.getInput(startPos, endPos).toString(); if (Strings.isEmpty(markup) == false) { XmlPullParser p = new XmlPullParser(); p.parse(markup); XmlTag tag; while ((tag = p.nextTag()) != null) { if (tagName.equalsIgnoreCase(tag.getName())) { hasChild = true; break; } } } } catch (Exception e) { throw new WicketRuntimeException(e); } } return hasChild; } /** * Checks if the tag has a child with the given <code>tagName</code>. * * @param tagName * the tag name to search for * @return <code>true</code> if this tag has a child with the given <code>tagName</code>. */ public TagTester getChild(String tagName) { Args.notNull(tagName, "tagName"); TagTester childTagTester = null; if (openTag.isOpen()) { // Get the content of the tag int startPos = openTag.getPos() + openTag.getLength(); int endPos = closeTag.getPos(); String markup = parser.getInput(startPos, endPos).toString(); childTagTester = createTagByAttribute(markup, tagName); } return childTagTester; } /** * Gets a child tag for testing. If this tag contains child tags, you can get one of them as a * {@link TagTester} instance. * * @param attribute * an attribute on the child tag to search for * @param value * a value that the attribute must have * @return the <code>TagTester</code> for the child tag */ public TagTester getChild(String attribute, String value) { TagTester childTag = null; if (openTag.isOpen()) { // Generate the markup for this tag String markup = getMarkup(); if (Strings.isEmpty(markup) == false) { childTag = TagTester.createTagByAttribute(markup, attribute, value); } } return childTag; } /** * Gets the markup for this tag. This includes all markup between the open tag and the close * tag. * * @return all the markup between the open tag and the close tag */ public String getMarkup() { int openPos = openTag.getPos(); int closePos = closeTag.getPos() + closeTag.getLength(); return parser.getInput(openPos, closePos).toString(); } /** * Returns the value for this tag. This includes all data between the open tag and the close * tag. * * @return all the data between the open tag and the close tag * @since 1.3 */ public String getValue() { if (openTag == closeTag) { return null; } int openPos = openTag.getPos() + openTag.getLength(); int closePos = closeTag.getPos(); return parser.getInput(openPos, closePos).toString(); } /** * Static factory method for creating a <code>TagTester</code> based on a tag name. Please note * that it will return the first tag which matches the criteria. * * @param markup * the markup to look for the tag to create the <code>TagTester</code> from the value * which the attribute must have * @return the <code>TagTester</code> which matches the tag by name in the markup */ public static TagTester createTagByName(String markup, String tagName) { List<TagTester> tester = createTags(markup, xmlTag -> xmlTag.getName().equalsIgnoreCase(tagName), true); if ((tester == null) || (tester.size() == 0)) { return null; } return tester.get(0); } /** * Static factory method for creating a <code>TagTester</code> based on a tag name. Please note * that it will return the first tag which matches the criteria. * * @param markup * the markup to look for the tag to create the <code>TagTester</code> from the value * which the attribute must have * @return the <code>TagTester</code> which matches the tag by name in the markup * * @deprecated use {@link #createTagByName(String, String)} instead */ @Deprecated public static TagTester createTagByAttribute(String markup, String tagName) { return createTagByName(markup, tagName); } /** * Static factory method for creating a <code>TagTester</code> based on a tag found by an * attribute with a specific value. Please note that it will return the first tag which matches * the criteria. It's therefore good for attributes such as "id" or "wicket:id", but only if * "wicket:id" is unique in the specified markup. * * @param markup * the markup to look for the tag to create the <code>TagTester</code> from * @param attribute * the attribute which should be on the tag in the markup * @param value * the value which the attribute must have * @return the <code>TagTester</code> which matches the tag in the markup, that has the given * value on the given attribute */ public static TagTester createTagByAttribute(String markup, String attribute, String value) { List<TagTester> tester = createTagsByAttribute(markup, attribute, value, true); if ((tester == null) || (tester.size() == 0)) { return null; } return tester.get(0); } /** * Static factory method for creating a <code>TagTester</code> based on a tag found by an * attribute with a specific value. Please note that it will return the first tag which matches * the criteria. It's therefore good for attributes suck as "id" or "wicket:id", but only if * "wicket:id" is unique in the specified markup. * * @param markup * the markup to look for the tag to create the <code>TagTester</code> from * @param attribute * the attribute which should be on the tag in the markup * @param value * the value which the attribute must have * @return the <code>TagTester</code> which matches the tag in the markup, that has the given * value on the given attribute */ @Deprecated public static TagTester createTagsByAttribute(String markup, String attribute, String value) { return createTagByAttribute(markup, attribute, value); } /** * find the correct openTag to the given closeTag and remove all unclosed openTags between both * in given array {@code stack} * * @param closeTag * tag to search for corresponding openTag * @param stack * array of unclosed openTags * @return corresponding openTag or {@code null} */ private static XmlTag findOpenTag(XmlTag closeTag, Stack<XmlTag> stack) { while (stack.size() > 0) { XmlTag popped = stack.pop(); if (popped.getName().equals(closeTag.getName())) { return popped; } } return null; } /** * Static factory method for creating a <code>TagTester</code> based on a tag found by an * attribute with a specific value. Please note that it will return the first tag which matches * the criteria. It's therefore good for attributes suck as "id" or "wicket:id", but only if * "wicket:id" is unique in the specified markup. * * @param markup * the markup to look for the tag to create the <code>TagTester</code> from * @param attribute * the attribute which should be on the tag in the markup * @param value * the value which the attribute must have * @param stopAfterFirst * if true search will stop after the first match * @return the <code>TagTester</code> which matches the tag in the markup, that has the given * value on the given attribute */ public static List<TagTester> createTagsByAttribute(String markup, String attribute, String value, boolean stopAfterFirst) { if (Strings.isEmpty(attribute) || Strings.isEmpty(value)) { return Collections.emptyList(); } return createTags(markup, xmlTag -> value.equals(xmlTag.getAttributes().get(attribute)), stopAfterFirst); } public static List<TagTester> createTags(String markup, Function<XmlTag, Boolean> accept, boolean stopAfterFirst) { List<TagTester> testers = new ArrayList<>(); if ((Strings.isEmpty(markup) == false)) { try { // remove the CDATA and // the id attribute of the component because it is often the same as the element's id markup = AJAX_COMPONENT_CDATA_OPEN.matcher(markup).replaceAll("<component>"); markup = AJAX_COMPONENT_CDATA_CLOSE.matcher(markup).replaceAll("</component>"); XmlPullParser parser = new XmlPullParser(); parser.parse(markup); XmlTag openTag = null; XmlTag closeTag = null; // temporary Tag-Hierarchy after openTag Stack<XmlTag> stack = new Stack<>(); while (true) { XmlTag xmlTag = parser.nextTag(); if (xmlTag == null) { break; } if (openTag == null) { if (accept.apply(xmlTag)) { if (xmlTag.isOpen()) { openTag = xmlTag; } else if (xmlTag.isOpenClose()) { openTag = xmlTag; closeTag = xmlTag; } } } else { if (xmlTag.isOpen() && !xmlTag.isOpenClose()) { stack.push(xmlTag); } if (xmlTag.isClose()) { XmlTag foundTag = findOpenTag(xmlTag, stack); if (foundTag == null) { if (xmlTag.getName().equals(openTag.getName())) { closeTag = xmlTag; closeTag.setOpenTag(openTag); } else if (requiresCloseTag(openTag.getName()) == false) { // no closeTag for current openTag (allowed) closeTag = openTag; } else { // no closeTag for current openTag (invalid structure) // thus reset state openTag = null; closeTag = null; } } } } if ((openTag != null) && (closeTag != null)) { TagTester tester = new TagTester(parser, openTag, closeTag); testers.add(tester); openTag = null; closeTag = null; } if (stopAfterFirst && (closeTag != null)) { break; } } } catch (Exception e) { throw new WicketRuntimeException(e); } } return testers; } }