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.junit;
16  
17  import java.lang.annotation.ElementType;
18  import java.lang.annotation.Retention;
19  import java.lang.annotation.RetentionPolicy;
20  import java.lang.annotation.Target;
21  import java.text.MessageFormat;
22  import java.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.Collections;
25  import java.util.List;
26  import java.util.Set;
27  
28  import org.htmlunit.BrowserVersion;
29  import org.htmlunit.WebDriverTestCase;
30  import org.htmlunit.WebTestCase;
31  import org.junit.runner.Runner;
32  import org.junit.runner.manipulation.Filter;
33  import org.junit.runner.manipulation.Filterable;
34  import org.junit.runner.manipulation.NoTestsRemainException;
35  import org.junit.runners.Parameterized.Parameters;
36  import org.junit.runners.Suite;
37  import org.junit.runners.model.FrameworkMethod;
38  import org.junit.runners.model.TestClass;
39  import org.junit.runners.parameterized.TestWithParameters;
40  
41  /**
42   * The custom runner <code>BrowserParameterizedRunner</code> combines the behavior of both
43   * {@link org.htmlunit.junit.BrowserRunner} and {@link org.junit.runners.Parameterized}.
44   *
45   * It uses {@link org.junit.runners.Parameterized.Parameter} for field injection.
46   *
47   * You must define a single {@link Default} method, which has global
48   * {@link org.htmlunit.junit.annotation.Alerts}.
49   * You can add other specific tests, which will not be parameterized, and they can have other
50   * {@link org.htmlunit.junit.annotation.Alerts} or
51   * {@link org.htmlunit.junit.annotation.NotYetImplemented}.
52   *
53   * The method name will start with underscore '_' and have the parameters joined by an underscore.
54   * If the method of a data entry already exists, then it will not be considered, as the actual method will override it.
55   *
56   * For example, you can have:
57   * <pre>
58     &#064;RunWith(BrowserParameterizedRunner.class)
59     public class SomeTest extends WebDriverTestCase {
60  
61        &#064;Parameters
62        public static Iterable&lt;Object[]&gt; data() {
63            return Arrays.asList(new Object[][] { { 0, 0 }, { 1, 1 }, { 2, 1 },
64                     /&#042; will be overridden, see below &#042;/ { 3, 2 },
65                     { 4, 3 }, { 5, 5 }, { 6, 8 } });
66        }
67  
68        &#064;Parameter
69        public int param1;
70  
71        &#064;Parameter(1)
72        public int param2;
73  
74        &#064;Test
75        &#064;Alerts("some alert")
76        &#064;Default
77        public void test() throws Exception {
78           loadPageWithAlerts2("some HTML with " + param1 + " " + param2);
79        }
80  
81        /&#042;&#042;
82         &#042; This method will override the <tt>{ 3, 2 }</tt> entry.
83         &#042;/
84        &#064;Test
85        &#064;Alerts("another alert")
86        &#064;NotYetImplemented
87        public void _3_2() throws Exception {
88           loadPageWithAlerts2("some HTML without the parameters, since it is not the &#064;Default");
89        }
90  
91        &#064;Test
92        &#064;Alerts("another alert")
93        &#064;NotYetImplemented
94        public void anotherTest() throws Exception {
95           loadPageWithAlerts2("some HTML without the parameters, since it is not the &#064;Default");
96        }
97     }
98     </pre>
99   *
100  * @author Ahmed Ashour
101  */
102 public class BrowserParameterizedRunner extends Suite {
103 
104     /**
105      * Annotation for a methods which is the default one to be executed for all parameters.
106      */
107     @Retention(RetentionPolicy.RUNTIME)
108     @Target(ElementType.METHOD)
109     public @interface Default {
110     }
111 
112     private final ArrayList<Runner> runners_ = new ArrayList<>();
113 
114     /**
115      * Only called reflectively. Do not use programmatically.
116      * @param klass the class
117      * @throws Throwable if an error occurs
118      */
119     public BrowserParameterizedRunner(final Class<WebTestCase> klass) throws Throwable {
120         super(klass, Collections.<Runner>emptyList());
121 
122         verifyDefaultMEthod();
123 
124         final Parameters parameters = getParametersMethod().getAnnotation(Parameters.class);
125 
126         final List<TestWithParameters> tests = createTestsForParameters(
127                 allParameters(), parameters.name());
128 
129         if (BrowserVersionClassRunner.containsTestMethods(klass)) {
130             final Set<String> browsers = WebDriverTestCase.getBrowsersProperties();
131             if (WebDriverTestCase.class.isAssignableFrom(klass)) {
132                 if (browsers.contains(BrowserRunner.REAL_CHROME)) {
133                     runners_.add(new BrowserVersionClassRunnerWithParameters(
134                             klass, BrowserVersion.CHROME, true, tests));
135                 }
136                 if (browsers.contains(BrowserRunner.REAL_FIREFOX_ESR)) {
137                     runners_.add(new BrowserVersionClassRunnerWithParameters(
138                             klass, BrowserVersion.FIREFOX_ESR, true, tests));
139                 }
140                 if (browsers.contains(BrowserRunner.REAL_FIREFOX)) {
141                     runners_.add(new BrowserVersionClassRunnerWithParameters(
142                             klass, BrowserVersion.FIREFOX, true, tests));
143                 }
144                 if (browsers.contains(BrowserRunner.REAL_EDGE)) {
145                     runners_.add(new BrowserVersionClassRunnerWithParameters(
146                             klass, BrowserVersion.EDGE, true, tests));
147                 }
148             }
149 
150             if (browsers.contains(BrowserRunner.HTMLUNIT_CHROME)) {
151                 runners_.add(new BrowserVersionClassRunnerWithParameters(
152                         klass, BrowserVersion.CHROME, false, tests));
153             }
154             if (browsers.contains(BrowserRunner.HTMLUNIT_FIREFOX_ESR)) {
155                 runners_.add(new BrowserVersionClassRunnerWithParameters(
156                         klass, BrowserVersion.FIREFOX_ESR, false, tests));
157             }
158             if (browsers.contains(BrowserRunner.HTMLUNIT_FIREFOX)) {
159                 runners_.add(new BrowserVersionClassRunnerWithParameters(
160                         klass, BrowserVersion.FIREFOX, false, tests));
161             }
162             if (browsers.contains(BrowserRunner.HTMLUNIT_EDGE)) {
163                 runners_.add(new BrowserVersionClassRunnerWithParameters(
164                         klass, BrowserVersion.EDGE, false, tests));
165             }
166         }
167         else {
168             throw new IllegalStateException("No @Test method found");
169         }
170     }
171 
172     /**
173      * {@inheritDoc}
174      */
175     @Override
176     protected List<Runner> getChildren() {
177         return runners_;
178     }
179 
180     private TestWithParameters createTestWithNotNormalizedParameters(
181             final String pattern, final int index, final Object parametersOrSingleParameter) {
182         final Object[] parameters = (parametersOrSingleParameter instanceof Object[])
183                 ? (Object[]) parametersOrSingleParameter
184                 : new Object[] {parametersOrSingleParameter};
185         return createTestWithParameters(getTestClass(), pattern, index,
186                 parameters);
187     }
188 
189     @SuppressWarnings("unchecked")
190     private Iterable<Object> allParameters() throws Throwable {
191         final Object parameters = getParametersMethod().invokeExplosively(null);
192         if (parameters instanceof Iterable) {
193             return (Iterable<Object>) parameters;
194         }
195         else if (parameters instanceof Object[]) {
196             return Arrays.asList((Object[]) parameters);
197         }
198         else {
199             throw parametersMethodReturnedWrongType();
200         }
201     }
202 
203     private FrameworkMethod getParametersMethod() throws Exception {
204         final List<FrameworkMethod> methods = getTestClass().getAnnotatedMethods(
205                 Parameters.class);
206         for (final FrameworkMethod each : methods) {
207             if (each.isStatic() && each.isPublic()) {
208                 return each;
209             }
210         }
211 
212         throw new Exception("No public static parameters method on class "
213                 + getTestClass().getName());
214     }
215 
216     private List<TestWithParameters> createTestsForParameters(
217             final Iterable<Object> allParameters, final String namePattern) {
218         int i = 0;
219         final List<TestWithParameters> children = new ArrayList<>();
220         for (final Object parametersOfSingleTest : allParameters) {
221             children.add(createTestWithNotNormalizedParameters(namePattern,
222                     i++, parametersOfSingleTest));
223         }
224         return children;
225     }
226 
227     private Exception parametersMethodReturnedWrongType() throws Exception {
228         final String className = getTestClass().getName();
229         final String methodName = getParametersMethod().getName();
230         final String message = MessageFormat.format(
231                 "{0}.{1}() must return an Iterable of arrays.",
232                 className, methodName);
233         return new Exception(message);
234     }
235 
236     private static TestWithParameters createTestWithParameters(
237             final TestClass testClass, final String pattern, final int index, final Object[] parameters) {
238         final String finalPattern = pattern.replaceAll("\\{index\\}",
239                 Integer.toString(index));
240         final String name = MessageFormat.format(finalPattern, parameters);
241         return new TestWithParameters("[" + name + "]", testClass,
242                 Arrays.asList(parameters));
243     }
244 
245     private void verifyDefaultMEthod() throws Exception {
246         final List<FrameworkMethod> methods = getTestClass().getAnnotatedMethods(Default.class);
247         if (methods.size() != 1) {
248             throw new Exception("A single method with @Default must exist in class "
249                 + getTestClass().getName());
250         }
251     }
252 
253     /**
254      * {@inheritDoc}
255      */
256     @Override
257     public void filter(final Filter filter) throws NoTestsRemainException {
258         boolean atLeastOne = false;
259         for (final Runner runner : getChildren()) {
260             try {
261                 if (runner instanceof Filterable) {
262                     ((Filterable) runner).filter(filter);
263                     atLeastOne = true;
264                 }
265             }
266             catch (final NoTestsRemainException e) {
267                 // nothing
268             }
269         }
270 
271         if (!atLeastOne) {
272             throw new NoTestsRemainException();
273         }
274     }
275 }