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(&quot;form&quot;);
051 *  assertTrue(tag.hasAttribute(&quot;action&quot;));
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         *  &lt;span wicket:id=&quot;helloComp&quot; class=&quot;style1 style2&quot;&gt;Hello&lt;/span&gt;
153         * </pre>
154         * 
155         * <p>
156         * <b>Test:</b>
157         * 
158         * <pre>
159         * TagTester tester = application.getTagByWicketId(&quot;helloComp&quot;);
160         * assertTrue(tester.getAttributeContains(&quot;class&quot;, &quot;style2&quot;));
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}