001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.wicket.util.tester; 018 019import static org.apache.wicket.markup.parser.filter.HtmlHandler.requiresCloseTag; 020 021import java.util.ArrayList; 022import java.util.Collections; 023import java.util.List; 024import java.util.Objects; 025import java.util.Stack; 026import java.util.function.Function; 027import java.util.regex.Pattern; 028 029import org.apache.wicket.WicketRuntimeException; 030import org.apache.wicket.markup.parser.XmlPullParser; 031import org.apache.wicket.markup.parser.XmlTag; 032import org.apache.wicket.util.lang.Args; 033import org.apache.wicket.util.string.Strings; 034import org.apache.wicket.util.value.IValueMap; 035 036/** 037 * Tag tester is used to test that a generated markup tag contains the correct attributes, values 038 * etc. This can be done instead of comparing generated markup with some expected markup. The 039 * advantage of this is that a lot of tests don't fail when the generated markup changes just a 040 * little bit. 041 * <p> 042 * It also gives a more programmatic way of testing the generated output, by not having to worry 043 * about precisely how the markup looks instead of which attributes exists on the given tags, and 044 * what values they have. 045 * <p> 046 * Example: 047 * 048 * <pre> 049 * ... 050 * TagTester tagTester = application.getTagByWicketId("form"); 051 * assertTrue(tag.hasAttribute("action")); 052 * ... 053 * </pre> 054 * 055 * @since 1.2.6 056 */ 057public class TagTester 058{ 059 private static final Pattern AJAX_COMPONENT_CDATA_OPEN = Pattern.compile("<component.*?><!\\[CDATA\\["); 060 private static final Pattern AJAX_COMPONENT_CDATA_CLOSE = Pattern.compile("\\]\\]></component>"); 061 062 private final XmlTag openTag; 063 064 private final XmlTag closeTag; 065 066 private final XmlPullParser parser; 067 068 /** 069 * Constructor. 070 * 071 * @param parser 072 * an <code>XmlPullParser</code> 073 * @param openTag 074 * an opening XML tag 075 * @param closeTag 076 * a closing XML tag 077 */ 078 private TagTester(XmlPullParser parser, XmlTag openTag, XmlTag closeTag) 079 { 080 this.parser = parser; 081 this.openTag = openTag; 082 this.closeTag = closeTag; 083 } 084 085 /** 086 * Gets the tag's name. 087 * 088 * @return the tag name 089 */ 090 public String getName() 091 { 092 return openTag.getName(); 093 } 094 095 /** 096 * Tests if the tag contains the given attribute. Please note that this is non case-sensitive, 097 * because attributes in HTML may be non case-sensitive. 098 * 099 * @param attribute 100 * an attribute to look for in the tag 101 * @return <code>true</code> if the tag has the attribute, <code>false</code> if not. 102 */ 103 public boolean hasAttribute(String attribute) 104 { 105 boolean hasAttribute = false; 106 107 if (getAttribute(attribute) != null) 108 { 109 hasAttribute = true; 110 } 111 112 return hasAttribute; 113 } 114 115 /** 116 * Gets the value for a given attribute. Please note that this is non case-sensitive, because 117 * attributes in HTML may be non case-sensitive. 118 * 119 * @param attribute 120 * an attribute to look for in the tag 121 * @return the value of the attribute or <code>null</code> if it isn't found. 122 */ 123 public String getAttribute(String attribute) 124 { 125 String value = null; 126 127 IValueMap attributeMap = openTag.getAttributes(); 128 129 if (attributeMap != null) 130 { 131 for (String attr : attributeMap.keySet()) 132 { 133 if (attr.equalsIgnoreCase(attribute)) 134 { 135 value = attributeMap.getString(attr); 136 } 137 } 138 } 139 140 return value; 141 } 142 143 /** 144 * Checks if an attribute contains the specified partial value. 145 * <p> 146 * For example: 147 * 148 * <p> 149 * <b>Markup:</b> 150 * 151 * <pre> 152 * <span wicket:id="helloComp" class="style1 style2">Hello</span> 153 * </pre> 154 * 155 * <p> 156 * <b>Test:</b> 157 * 158 * <pre> 159 * TagTester tester = application.getTagByWicketId("helloComp"); 160 * assertTrue(tester.getAttributeContains("class", "style2")); 161 * </pre> 162 * 163 * @param attribute 164 * the attribute to test on 165 * @param partialValue 166 * the partial value to test if the attribute value contains it 167 * @return <code>true</code> if the attribute value contains the partial value 168 */ 169 public boolean getAttributeContains(String attribute, String partialValue) 170 { 171 boolean contains = false; 172 173 if (partialValue != null) 174 { 175 String value = getAttribute(attribute); 176 177 if (value != null) 178 { 179 if (value.contains(partialValue)) 180 { 181 contains = true; 182 } 183 } 184 } 185 186 return contains; 187 } 188 189 /** 190 * Checks if an attribute's value is the exact same as the given value. 191 * 192 * @param attribute 193 * an attribute to test 194 * @param expected 195 * the value which should be the same at the attribute's value 196 * @return <code>true</code> if the attribute's value is the same as the given value 197 */ 198 public boolean getAttributeIs(String attribute, String expected) 199 { 200 boolean is = false; 201 202 String val = getAttribute(attribute); 203 204 if (val == null && expected == null || expected != null && expected.equals(val)) 205 { 206 is = true; 207 } 208 209 return is; 210 } 211 212 /** 213 * Checks if an attribute's value ends with the given parameter. 214 * 215 * @param attribute 216 * an attribute to test 217 * @param expected 218 * the expected value 219 * @return <code>true</code> if the attribute's value ends with the expected value 220 */ 221 public boolean getAttributeEndsWith(String attribute, String expected) 222 { 223 boolean endsWith = false; 224 225 if (expected != null) 226 { 227 String val = getAttribute(attribute); 228 229 if (val != null) 230 { 231 if (val.endsWith(expected)) 232 { 233 endsWith = true; 234 } 235 } 236 } 237 238 return endsWith; 239 } 240 241 /** 242 * Checks if the tag has a child with the given <code>tagName</code>. 243 * 244 * @param tagName 245 * the tag name to search for 246 * @return <code>true</code> if this tag has a child with the given <code>tagName</code>. 247 */ 248 public boolean hasChildTag(String tagName) 249 { 250 Args.notEmpty(tagName, "tagName"); 251 252 boolean hasChild = false; 253 254 if (openTag.isOpen()) 255 { 256 try 257 { 258 // Get the content of the tag 259 int startPos = openTag.getPos() + openTag.getLength(); 260 int endPos = closeTag.getPos(); 261 String markup = parser.getInput(startPos, endPos).toString(); 262 263 if (Strings.isEmpty(markup) == false) 264 { 265 XmlPullParser p = new XmlPullParser(); 266 p.parse(markup); 267 268 XmlTag tag; 269 while ((tag = p.nextTag()) != null) 270 { 271 if (tagName.equalsIgnoreCase(tag.getName())) 272 { 273 hasChild = true; 274 break; 275 } 276 } 277 } 278 } 279 catch (Exception e) 280 { 281 throw new WicketRuntimeException(e); 282 } 283 } 284 285 return hasChild; 286 } 287 288 /** 289 * Checks if the tag has a child with the given <code>tagName</code>. 290 * 291 * @param tagName 292 * the tag name to search for 293 * @return <code>true</code> if this tag has a child with the given <code>tagName</code>. 294 */ 295 public TagTester getChild(String tagName) 296 { 297 Args.notNull(tagName, "tagName"); 298 299 TagTester childTagTester = null; 300 301 if (openTag.isOpen()) 302 { 303 // Get the content of the tag 304 int startPos = openTag.getPos() + openTag.getLength(); 305 int endPos = closeTag.getPos(); 306 String markup = parser.getInput(startPos, endPos).toString(); 307 308 childTagTester = createTagByName(markup, tagName); 309 } 310 311 return childTagTester; 312 } 313 314 /** 315 * Gets a child tag for testing. If this tag contains child tags, you can get one of them as a 316 * {@link TagTester} instance. 317 * 318 * @param attribute 319 * an attribute on the child tag to search for 320 * @param value 321 * a value that the attribute must have 322 * @return the <code>TagTester</code> for the child tag 323 */ 324 public TagTester getChild(String attribute, String value) 325 { 326 TagTester childTag = null; 327 328 if (openTag.isOpen()) 329 { 330 // Generate the markup for this tag 331 String markup = getMarkup(); 332 333 if (Strings.isEmpty(markup) == false) 334 { 335 childTag = TagTester.createTagByAttribute(markup, attribute, value); 336 } 337 } 338 339 return childTag; 340 } 341 342 /** 343 * Gets the markup for this tag. This includes all markup between the open tag and the close 344 * tag. 345 * 346 * @return all the markup between the open tag and the close tag 347 */ 348 public String getMarkup() 349 { 350 int openPos = openTag.getPos(); 351 int closePos = closeTag.getPos() + closeTag.getLength(); 352 353 return parser.getInput(openPos, closePos).toString(); 354 } 355 356 /** 357 * Returns the value for this tag. This includes all data between the open tag and the close 358 * tag. 359 * 360 * @return all the data between the open tag and the close tag 361 * @since 1.3 362 */ 363 public String getValue() 364 { 365 if (openTag == closeTag) 366 { 367 return null; 368 } 369 370 int openPos = openTag.getPos() + openTag.getLength(); 371 int closePos = closeTag.getPos(); 372 373 return parser.getInput(openPos, closePos).toString(); 374 } 375 376 /** 377 * Static factory method for creating a <code>TagTester</code> based on a tag name. Please note 378 * that it will return the first tag which matches the criteria. 379 * 380 * @param markup 381 * the markup to look for the tag to create the <code>TagTester</code> from the value 382 * which the attribute must have 383 * @return the <code>TagTester</code> which matches the tag by name in the markup 384 */ 385 public static TagTester createTagByName(String markup, String tagName) 386 { 387 List<TagTester> tester = createTags(markup, xmlTag -> xmlTag.getName().equalsIgnoreCase(tagName), true); 388 if ((tester == null) || (tester.size() == 0)) 389 { 390 return null; 391 } 392 return tester.get(0); 393 } 394 395 /** 396 * Static factory method for creating a <code>TagTester</code> based on a tag found by an 397 * attribute with a specific value. Please note that it will return the first tag which matches 398 * the criteria. It's therefore good for attributes such as "id" or "wicket:id", but only if 399 * "wicket:id" is unique in the specified markup. 400 * 401 * @param markup 402 * the markup to look for the tag to create the <code>TagTester</code> from 403 * @param attribute 404 * the attribute which should be on the tag in the markup 405 * @param value 406 * the value which the attribute must have 407 * @return the <code>TagTester</code> which matches the tag in the markup, that has the given 408 * value on the given attribute 409 */ 410 public static TagTester createTagByAttribute(String markup, String attribute, String value) 411 { 412 List<TagTester> tester = createTagsByAttribute(markup, attribute, value, true); 413 if ((tester == null) || (tester.size() == 0)) 414 { 415 return null; 416 } 417 return tester.get(0); 418 } 419 420 /** 421 * find the correct openTag to the given closeTag and remove all unclosed openTags between both 422 * in given array {@code stack} 423 * 424 * @param closeTag 425 * tag to search for corresponding openTag 426 * @param stack 427 * array of unclosed openTags 428 * @return corresponding openTag or {@code null} 429 */ 430 private static XmlTag findOpenTag(XmlTag closeTag, Stack<XmlTag> stack) 431 { 432 while (stack.size() > 0) 433 { 434 XmlTag popped = stack.pop(); 435 if (popped.getName().equals(closeTag.getName())) 436 { 437 return popped; 438 } 439 } 440 return null; 441 } 442 443 /** 444 * Static factory method for creating a <code>TagTester</code> based on tags found by an 445 * attribute with a specific value. 446 * 447 * @param markup 448 * the markup to look for the tag to create the <code>TagTester</code> from 449 * @param attribute 450 * the attribute which should be on the tag in the markup 451 * @param value 452 * the value which the attribute must have 453 * @param stopAfterFirst 454 * if true search will stop after the first match 455 * @return list of <code>TagTester</code>s matching the tags in the markup, that have the given 456 * value on the given attribute 457 */ 458 public static List<TagTester> createTagsByAttribute(String markup, String attribute, String value, boolean stopAfterFirst) 459 { 460 if (Strings.isEmpty(attribute)) { 461 return Collections.emptyList(); 462 } 463 464 return createTags(markup, xmlTag -> Objects.equals(value, xmlTag.getAttributes().get(attribute)), stopAfterFirst); 465 } 466 467 public static List<TagTester> createTags(String markup, Function<XmlTag, Boolean> accept, boolean stopAfterFirst) 468 { 469 List<TagTester> testers = new ArrayList<>(); 470 471 if ((Strings.isEmpty(markup) == false)) 472 { 473 try 474 { 475 // remove the CDATA and 476 // the id attribute of the component because it is often the same as the element's id 477 markup = AJAX_COMPONENT_CDATA_OPEN.matcher(markup).replaceAll("<component>"); 478 markup = AJAX_COMPONENT_CDATA_CLOSE.matcher(markup).replaceAll("</component>"); 479 480 XmlPullParser parser = new XmlPullParser(); 481 parser.parse(markup); 482 483 XmlTag openTag = null; 484 XmlTag closeTag = null; 485 486 // temporary Tag-Hierarchy after openTag 487 Stack<XmlTag> stack = new Stack<>(); 488 489 while (true) 490 { 491 XmlTag xmlTag = parser.nextTag(); 492 if (xmlTag == null) 493 { 494 break; 495 } 496 497 if (openTag == null) 498 { 499 if (accept.apply(xmlTag)) 500 { 501 if (xmlTag.isOpen()) 502 { 503 openTag = xmlTag; 504 } 505 else if (xmlTag.isOpenClose()) 506 { 507 openTag = xmlTag; 508 closeTag = xmlTag; 509 } 510 } 511 } 512 else 513 { 514 if (xmlTag.isOpen() && !xmlTag.isOpenClose()) 515 { 516 stack.push(xmlTag); 517 } 518 if (xmlTag.isClose()) 519 { 520 XmlTag foundTag = findOpenTag(xmlTag, stack); 521 if (foundTag == null) 522 { 523 if (xmlTag.getName().equals(openTag.getName())) 524 { 525 closeTag = xmlTag; 526 closeTag.setOpenTag(openTag); 527 } 528 else if (requiresCloseTag(openTag.getName()) == false) 529 { 530 // no closeTag for current openTag (allowed) 531 closeTag = openTag; 532 } 533 else 534 { 535 // no closeTag for current openTag (invalid structure) 536 // thus reset state 537 openTag = null; 538 closeTag = null; 539 } 540 } 541 } 542 } 543 544 if ((openTag != null) && (closeTag != null)) 545 { 546 TagTester tester = new TagTester(parser, openTag, closeTag); 547 testers.add(tester); 548 openTag = null; 549 closeTag = null; 550 } 551 552 if (stopAfterFirst && (closeTag != null)) 553 { 554 break; 555 } 556 } 557 } 558 catch (Exception e) 559 { 560 throw new WicketRuntimeException(e); 561 } 562 } 563 564 return testers; 565 } 566}