View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.html;
16  
17  import java.lang.reflect.Method;
18  import java.util.ArrayList;
19  import java.util.HashSet;
20  import java.util.List;
21  import java.util.Locale;
22  import java.util.stream.Stream;
23  
24  import org.htmlunit.BrowserVersion;
25  import org.htmlunit.MockWebConnection;
26  import org.htmlunit.WebClient;
27  import org.htmlunit.WebTestCase;
28  import org.htmlunit.html.parser.HTMLParser;
29  import org.junit.jupiter.api.Assertions;
30  import org.junit.jupiter.api.DynamicTest;
31  import org.junit.jupiter.api.TestFactory;
32  
33  /**
34   * <p>Tests for all the generated attribute accessors. This test case will
35   * dynamically generate tests for all the various attributes. The code
36   * is fairly complicated but doing it this way is much easier than writing
37   * individual tests for all the attributes.</p>
38   *
39   * <p>With the new custom DOM, this test has somewhat lost its significance.
40   * We simply set and get the attributes and compare the results.</p>
41   *
42   * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
43   * @author Christian Sell
44   * @author Marc Guillemot
45   * @author Ahmed Ashour
46   * @author Ronald Brill
47   * @author Frank Danek
48   */
49  public class AttributesTest {
50  
51      private static final List<String> EXCLUDED_METHODS = new ArrayList<>();
52      static {
53          EXCLUDED_METHODS.add("getHtmlElementsByAttribute");
54          EXCLUDED_METHODS.add("getOneHtmlElementByAttribute");
55          EXCLUDED_METHODS.add("getAttribute");
56          EXCLUDED_METHODS.add("getElementsByAttribute");
57      }
58  
59      /**
60       * Creates dynamic tests for each attribute on each element.
61       *
62       * @return stream of dynamic tests
63       * @throws Exception if the tests cannot be created
64       */
65      @TestFactory
66      Stream<DynamicTest> attributeTests() throws Exception {
67          final String[] classesToTest = {
68              "HtmlAbbreviated", "HtmlAcronym",
69              "HtmlAnchor", "HtmlAddress", "HtmlArea",
70              "HtmlArticle", "HtmlAside", "HtmlAudio",
71              "HtmlBackgroundSound", "HtmlBase", "HtmlBaseFont",
72              "HtmlBidirectionalIsolation",
73              "HtmlBidirectionalOverride", "HtmlBig",
74              "HtmlBlockQuote", "HtmlBody", "HtmlBold",
75              "HtmlBreak", "HtmlButton", "HtmlCanvas", "HtmlCaption",
76              "HtmlCenter", "HtmlCitation", "HtmlCode", "DomComment",
77              "HtmlData", "HtmlDataList",
78              "HtmlDefinition", "HtmlDefinitionDescription",
79              "HtmlDeletedText", "HtmlDetails", "HtmlDialog", "HtmlDirectory",
80              "HtmlDivision", "HtmlDefinitionList",
81              "HtmlDefinitionTerm", "HtmlEmbed",
82              "HtmlEmphasis",
83              "HtmlFieldSet", "HtmlFigureCaption", "HtmlFigure",
84              "HtmlFont", "HtmlForm", "HtmlFooter",
85              "HtmlFrame", "HtmlFrameSet",
86              "HtmlHead", "HtmlHeader",
87              "HtmlHeading1", "HtmlHeading2", "HtmlHeading3",
88              "HtmlHeading4", "HtmlHeading5", "HtmlHeading6",
89              "HtmlHorizontalRule", "HtmlHtml", "HtmlInlineFrame",
90              "HtmlInlineQuotation",
91              "HtmlImage", "HtmlImage", "HtmlInsertedText",
92              "HtmlItalic", "HtmlKeyboard", "HtmlLabel", "HtmlLayer",
93              "HtmlLegend", "HtmlListing", "HtmlListItem",
94              "HtmlLink",
95              "HtmlMap", "HtmlMain", "HtmlMark", "HtmlMarquee",
96              "HtmlMenu", "HtmlMeta", "HtmlMeter",
97              "HtmlNav",
98              "HtmlNoBreak", "HtmlNoEmbed", "HtmlNoFrames", "HtmlNoLayer",
99              "HtmlNoScript", "HtmlObject", "HtmlOrderedList",
100             "HtmlOptionGroup", "HtmlOption", "HtmlOutput",
101             "HtmlParagraph",
102             "HtmlParameter", "HtmlPicture", "HtmlPlainText", "HtmlPreformattedText",
103             "HtmlProgress",
104             "HtmlRb", "HtmlRp", "HtmlRt", "HtmlRtc", "HtmlRuby",
105             "HtmlS", "HtmlSample",
106             "HtmlScript", "HtmlSection", "HtmlSelect", "HtmlSlot", "HtmlSmall",
107             "HtmlSource", "HtmlSpan",
108             "HtmlStrike", "HtmlStrong", "HtmlStyle",
109             "HtmlSubscript", "HtmlSummary", "HtmlSuperscript",
110             "HtmlSvg",
111             "HtmlTable", "HtmlTableColumn", "HtmlTableColumnGroup",
112             "HtmlTableBody", "HtmlTableDataCell", "HtmlTableHeaderCell",
113             "HtmlTableRow", "HtmlTextArea", "HtmlTableFooter",
114             "HtmlTableHeader", "HtmlTeletype", "HtmlTemplate", "HtmlTrack",
115             "HtmlTime", "HtmlTitle",
116             "HtmlUnderlined", "HtmlUnorderedList",
117             "HtmlVariable", "HtmlVideo",
118             "HtmlWordBreak", "HtmlExample"
119         };
120 
121         final HashSet<String> supportedTags = new HashSet<>(DefaultElementFactory.SUPPORTED_TAGS_);
122         final List<DynamicTest> tests = new ArrayList<>();
123 
124         for (final String testClass : classesToTest) {
125             final Class<?> clazz = Class.forName("org.htmlunit.html." + testClass);
126             tests.addAll(createTestsForClass(clazz));
127 
128             String tag;
129             if (DomComment.class == clazz) {
130                 tag = "comment";
131             }
132             else {
133                 tag = (String) clazz.getField("TAG_NAME").get(null);
134             }
135             supportedTags.remove(tag);
136             try {
137                 tag = (String) clazz.getField("TAG_NAME2").get(null);
138                 supportedTags.remove(tag);
139             }
140             catch (final NoSuchFieldException ignored) {
141                 // ignore
142             }
143         }
144 
145         supportedTags.remove("input");
146 
147         if (!supportedTags.isEmpty()) {
148             throw new RuntimeException("Missing tag class(es) " + supportedTags);
149         }
150         return tests.stream();
151     }
152 
153     /**
154      * Creates all the tests for a given class.
155      *
156      * @param clazz the class to create tests for
157      * @throws Exception if the tests cannot be created
158      */
159     private List<DynamicTest> createTestsForClass(final Class<?> clazz) throws Exception {
160         final List<DynamicTest> tests = new ArrayList<>();
161         final Method[] methods = clazz.getMethods();
162 
163         for (final Method method : methods) {
164             final String methodName = method.getName();
165             if (methodName.startsWith("get")
166                 && methodName.endsWith("Attribute")
167                 && !EXCLUDED_METHODS.contains(methodName)) {
168 
169                 final String attributeName =
170                         normalizeAttributeName(
171                                 methodName.substring(3, methodName.length() - 9).toLowerCase(Locale.ROOT));
172 
173                 final String testName = createTestName(clazz, method);
174                 tests.add(DynamicTest.dynamicTest(testName, () -> {
175                     executeAttributeTest(clazz, method, attributeName);
176                 }));
177             }
178         }
179         return tests;
180     }
181 
182     /**
183      * Normalizes attribute names for special cases.
184      */
185     private static String normalizeAttributeName(final String attributeName) {
186         switch (attributeName) {
187             case "xmllang": return "xml:lang";
188             case "columns": return "cols";
189             case "columnspan": return "colspan";
190             case "textdirection": return "dir";
191             case "httpequiv": return "http-equiv";
192             case "acceptcharset": return "accept-charset";
193             case "htmlfor": return "for";
194             default: return attributeName;
195         }
196     }
197 
198     /**
199      * Executes the actual test for a specific attribute.
200      * @param classUnderTest the class under test
201      * @param method the getter method
202      * @param attributeName the attribute name
203      * @throws Exception if the test fails
204      */
205     private static void executeAttributeTest(final Class<?> classUnderTest,
206             final Method method, final String attributeName) throws Exception {
207         try (WebClient webClient = new WebClient(BrowserVersion.BEST_SUPPORTED)) {
208             final MockWebConnection connection = new MockWebConnection();
209             connection.setDefaultResponse("<html><head><title>foo</title></head><body></body></html>");
210             webClient.setWebConnection(connection);
211             final HtmlPage page = webClient.getPage(WebTestCase.URL_FIRST);
212             final String value = "value";
213             final DomElement objectToTest = getNewInstanceForClassUnderTest(classUnderTest, page);
214             objectToTest.setAttribute(attributeName, value);
215             final Object[] noObjects = new Object[0];
216             final Object result = method.invoke(objectToTest, noObjects);
217             Assertions.assertSame(value, result);
218         }
219     }
220 
221 
222     /**
223      * Creates a name for this particular test that reflects the attribute being tested.
224      * @param clazz the class containing the attribute
225      * @param method the getter method for the attribute
226      * @return the test name
227      */
228     private static String createTestName(final Class<?> clazz, final Method method) {
229         String className = clazz.getName();
230         final int index = className.lastIndexOf('.');
231         className = className.substring(index + 1);
232 
233         return "testAttributes_" + className + '_' + method.getName();
234     }
235 
236     /**
237      * Creates a new instance of the class being tested.
238      * @return the new instance
239      * @throws Exception if the new object cannot be created
240      */
241     private static DomElement getNewInstanceForClassUnderTest(
242                         final Class<?> classUnderTest, final HtmlPage page) throws Exception {
243         final HTMLParser htmlParser = page.getWebClient().getPageCreator().getHtmlParser();
244         final DomElement newInstance;
245         if (classUnderTest == HtmlTableRow.class) {
246             newInstance = htmlParser.getFactory(HtmlTableRow.TAG_NAME)
247                     .createElement(page, HtmlTableRow.TAG_NAME, null);
248         }
249         else if (classUnderTest == HtmlTableHeaderCell.class) {
250             newInstance = htmlParser.getFactory(HtmlTableHeaderCell.TAG_NAME)
251                     .createElement(page, HtmlTableHeaderCell.TAG_NAME, null);
252         }
253         else if (classUnderTest == HtmlTableDataCell.class) {
254             newInstance = htmlParser.getFactory(HtmlTableDataCell.TAG_NAME)
255                     .createElement(page, HtmlTableDataCell.TAG_NAME, null);
256         }
257         else {
258             final String tagName = (String) classUnderTest.getField("TAG_NAME").get(null);
259             newInstance = htmlParser.getFactory(tagName).createElement(page, tagName, null);
260         }
261 
262         return newInstance;
263     }
264 }