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.javascript.regexp;
16  
17  import org.htmlunit.WebDriverTestCase;
18  import org.htmlunit.junit.annotation.Alerts;
19  import org.htmlunit.junit.annotation.HtmlUnitNYI;
20  import org.junit.jupiter.api.Test;
21  import org.openqa.selenium.WebDriver;
22  
23  /**
24   * Tests for the RegEx support.
25   *
26   * @author Ronald Brill
27   */
28  public class RegExpTest extends WebDriverTestCase {
29  
30      /**
31       * @throws Exception if an error occurs
32       */
33      @Test
34      @Alerts({"g", "true", "false", "false", "false", "false"})
35      public void globalCtor() throws Exception {
36          testEvaluateProperties("new RegExp('foo', 'g');");
37      }
38  
39      /**
40       * @throws Exception if an error occurs
41       */
42      @Test
43      @Alerts({"g", "true", "false", "false", "false", "false"})
44      public void global() throws Exception {
45          testEvaluateProperties("/foo/g;");
46      }
47  
48      /**
49       * @throws Exception if an error occurs
50       */
51      @Test
52      @Alerts({"i", "false", "true", "false", "false", "false"})
53      public void ignoreCaseCtor() throws Exception {
54          testEvaluateProperties("new RegExp('foo', 'i');");
55      }
56  
57      /**
58       * @throws Exception if an error occurs
59       */
60      @Test
61      @Alerts({"i", "false", "true", "false", "false", "false"})
62      public void ignoreCase() throws Exception {
63          testEvaluateProperties("/foo/i;");
64      }
65  
66      /**
67       * @throws Exception if an error occurs
68       */
69      @Test
70      @Alerts({"m", "false", "false", "true", "false", "false"})
71      public void multilineCtor() throws Exception {
72          testEvaluateProperties("new RegExp('foo', 'm');");
73      }
74  
75      /**
76       * @throws Exception if an error occurs
77       */
78      @Test
79      @Alerts({"m", "false", "false", "true", "false", "false"})
80      public void multiline() throws Exception {
81          testEvaluateProperties("/foo/m;");
82      }
83  
84      /**
85       * @throws Exception if an error occurs
86       */
87      @Test
88      @Alerts({"y", "false", "false", "false", "true", "false"})
89      public void stickyCtor() throws Exception {
90          testEvaluateProperties("new RegExp('foo', 'y');");
91      }
92  
93      /**
94       * @throws Exception if an error occurs
95       */
96      @Test
97      @Alerts({"y", "false", "false", "false", "true", "false"})
98      public void sticky() throws Exception {
99          testEvaluateProperties("/foo/y;");
100     }
101 
102     /**
103      * @throws Exception if an error occurs
104      */
105     @Test
106     @Alerts({"gm", "true", "false", "true", "false", "false"})
107     public void globalMultilineCtor() throws Exception {
108         testEvaluateProperties("new RegExp('foo', 'gm');");
109     }
110 
111     /**
112      * @throws Exception if an error occurs
113      */
114     @Test
115     @Alerts({"gm", "true", "false", "true", "false", "false"})
116     public void globalMultiline() throws Exception {
117         testEvaluateProperties("/foo/gm;");
118     }
119 
120     /**
121      * @throws Exception if an error occurs
122      */
123     @Test
124     @Alerts({"gi", "true", "true", "false", "false", "false"})
125     public void globalIgnoreCaseCtor() throws Exception {
126         testEvaluateProperties("new RegExp('foo', 'ig');");
127     }
128 
129     /**
130      * @throws Exception if an error occurs
131      */
132     @Test
133     @Alerts({"gi", "true", "true", "false", "false", "false"})
134     public void globalIgnoreCase() throws Exception {
135         testEvaluateProperties("/foo/ig;");
136     }
137 
138     /**
139      * @throws Exception if an error occurs
140      */
141     @Test
142     @Alerts({"u", "false", "false", "false", "false", "true"})
143     public void unicodeCaseCtor() throws Exception {
144         testEvaluateProperties("new RegExp('foo', 'u');");
145     }
146 
147     /**
148      * @throws Exception if an error occurs
149      */
150     @Test
151     @Alerts({"u", "false", "false", "false", "false", "true"})
152     public void unicodeCase() throws Exception {
153         testEvaluateProperties("/foo/u;");
154     }
155 
156     /**
157      * @throws Exception if an error occurs
158      */
159     @Test
160     @Alerts({"mu", "false", "false", "true", "false", "true"})
161     public void multilineUnicodeCtor() throws Exception {
162         testEvaluateProperties("new RegExp('foo', 'mu');");
163     }
164 
165     /**
166      * @throws Exception if an error occurs
167      */
168     @Test
169     @Alerts({"mu", "false", "false", "true", "false", "true"})
170     public void multilineUnicode() throws Exception {
171         testEvaluateProperties("/foo/mu;");
172     }
173 
174     /**
175      * @throws Exception if an error occurs
176      */
177     @Test
178     @Alerts({"gy", "true", "false", "false", "true", "false"})
179     public void globalStickyCtor() throws Exception {
180         testEvaluateProperties("new RegExp('foo', 'gy');");
181     }
182 
183     /**
184      * @throws Exception if an error occurs
185      */
186     @Test
187     @Alerts({"gy", "true", "false", "false", "true", "false"})
188     public void globalSticky() throws Exception {
189         testEvaluateProperties("/foo/gy;");
190     }
191 
192     /**
193      * @throws Exception if an error occurs
194      */
195     @Test
196     @Alerts({"gim", "true", "true", "true", "false", "false"})
197     public void globalMultilineIgnoreCaseCtor() throws Exception {
198         testEvaluateProperties("new RegExp('foo', 'mig');");
199     }
200 
201     /**
202      * @throws Exception if an error occurs
203      */
204     @Test
205     @Alerts({"gim", "true", "true", "true", "false", "false"})
206     public void globalMultilineIgnoreCase() throws Exception {
207         testEvaluateProperties("/foo/gmi;");
208     }
209 
210     /**
211      * @throws Exception if an error occurs
212      */
213     @Test
214     @Alerts({"giy", "true", "true", "false", "true", "false"})
215     public void globalIgnoreCaseStickyCtor() throws Exception {
216         testEvaluateProperties("new RegExp('foo', 'yig');");
217     }
218 
219     /**
220      * @throws Exception if an error occurs
221      */
222     @Test
223     @Alerts({"giy", "true", "true", "false", "true", "false"})
224     public void globalIgnoreCaseSticky() throws Exception {
225         testEvaluateProperties("/foo/ygi;");
226     }
227 
228     /**
229      * @throws Exception if an error occurs
230      */
231     @Test
232     @Alerts({"gmy", "true", "false", "true", "true", "false"})
233     public void globalMultilineStickyCtor() throws Exception {
234         testEvaluateProperties("new RegExp('foo', 'gmy');");
235     }
236 
237     /**
238      * @throws Exception if an error occurs
239      */
240     @Test
241     @Alerts({"gmy", "true", "false", "true", "true", "false"})
242     public void globalMultilineSticky() throws Exception {
243         testEvaluateProperties("/foo/gmy;");
244     }
245 
246     /**
247      * @throws Exception if an error occurs
248      */
249     @Test
250     @Alerts({"im", "false", "true", "true", "false", "false"})
251     public void ignoreCaseMultilineCtor() throws Exception {
252         testEvaluateProperties("new RegExp('foo', 'im');");
253     }
254 
255     /**
256      * @throws Exception if an error occurs
257      */
258     @Test
259     @Alerts({"im", "false", "true", "true", "false", "false"})
260     public void ignoreCaseMultiline() throws Exception {
261         testEvaluateProperties("/foo/mi;");
262     }
263 
264     /**
265      * @throws Exception if an error occurs
266      */
267     @Test
268     @Alerts({"iy", "false", "true", "false", "true", "false"})
269     public void ignoreCaseStickyCtor() throws Exception {
270         testEvaluateProperties("new RegExp('foo', 'yi');");
271     }
272 
273     /**
274      * @throws Exception if an error occurs
275      */
276     @Test
277     @Alerts({"iy", "false", "true", "false", "true", "false"})
278     public void ignoreCaseSticky() throws Exception {
279         testEvaluateProperties("/foo/iy;");
280     }
281 
282     /**
283      * @throws Exception if an error occurs
284      */
285     @Test
286     @Alerts({"my", "false", "false", "true", "true", "false"})
287     public void multilineStickyCtor() throws Exception {
288         testEvaluateProperties("new RegExp('foo', 'my');");
289     }
290 
291     /**
292      * @throws Exception if an error occurs
293      */
294     @Test
295     @Alerts({"my", "false", "false", "true", "true", "false"})
296     public void multilineSticky() throws Exception {
297         testEvaluateProperties("/foo/my;");
298     }
299 
300     private void testEvaluateProperties(final String script) throws Exception {
301         final String html =
302                 "<html>\n"
303                 + "<head>\n"
304                 + "<script>\n"
305                 + LOG_TEXTAREA_FUNCTION
306                 + "function test() {\n"
307                 + "  var regex = " + script + "\n"
308                 + "  log(regex.flags);\n"
309                 + "  log(regex.global);\n"
310                 + "  log(regex.ignoreCase);\n"
311                 + "  log(regex.multiline);\n"
312                 + "  log(regex.sticky);\n"
313                 + "  log(regex.unicode);\n"
314                 + "}\n"
315                 + "</script>\n"
316                 + "</head>\n"
317                 + "<body onload='test()'>\n"
318                 + LOG_TEXTAREA
319                 + "</body></html>";
320 
321         loadPageVerifyTextArea2(html);
322     }
323 
324     /**
325      * @throws Exception if an error occurs
326      */
327     @Test
328     @Alerts("false")
329     public void stickyStartOfLine() throws Exception {
330         final String script =
331                 "var regex = /^foo/y;\n"
332                 + "regex.lastIndex = 2;\n"
333                 + "log(regex.test('..foo'));";
334         testEvaluate(script);
335     }
336 
337     /**
338      * @throws Exception if an error occurs
339      */
340     @Test
341     @Alerts({"false", "true"})
342     public void stickyStartOfLineMultiline() throws Exception {
343         final String script =
344                 "var regex = /^foo/my;\n"
345                 + "regex.lastIndex = 2;\n"
346                 + "log(regex.test('..foo'))\n"
347                 + "regex.lastIndex = 2;\n"
348                 + "log(regex.test('.\\nfoo'));";
349         testEvaluate(script);
350     }
351 
352     /**
353      * @throws Exception if an error occurs
354      */
355     @Test
356     @Alerts({"2", "a", "a"})
357     public void stickyAndGlobal() throws Exception {
358         final String script =
359                 "var result = 'aaba'.match(/a/yg);\n"
360                 + "log(result.length);\n"
361                 + "log(result[0]);\n"
362                 + "log(result[1]);\n";
363         testEvaluate(script);
364     }
365 
366     /**
367      * @throws Exception if an error occurs
368      */
369     @Test
370     @Alerts({"1", "a\na"})
371     public void dotAll() throws Exception {
372         final String script =
373                 "var result = 'a\\naba'.match(/a.a/s);\n"
374                 + "log(result.length);\n"
375                 + "log(result[0]);\n";
376         testEvaluate(script);
377     }
378 
379     /**
380      * @throws Exception if an error occurs
381      */
382     @Test
383     @Alerts({"1", "a\na"})
384     public void dotAllAndGlobal() throws Exception {
385         final String script =
386                 "var result = 'a\\naba'.match(/a.a/sg);\n"
387                 + "log(result.length);\n"
388                 + "log(result[0]);\n";
389         testEvaluate(script);
390     }
391 
392     /**
393      * @throws Exception if an error occurs
394      */
395     @Test
396     @Alerts({"2", "3f", "3f"})
397     public void unicode() throws Exception {
398         final String script =
399                 "var result = 'Angle α measures π radians.'.match(/[Α-Ωα-ω]/g);\n"
400                 + "log(result.length);\n"
401                 + "log(result[0].charCodeAt(0).toString(16));\n"
402                 + "log(result[1].charCodeAt(0).toString(16));";
403         testEvaluate(script);
404     }
405 
406     /**
407      * @throws Exception if an error occurs
408      */
409     @Test
410     @Alerts({"0", "undefined", "true", "false", "undefined"})
411     @HtmlUnitNYI(CHROME = {"get undefined", "", "false", "false", "false"},
412             EDGE = {"get undefined", "", "false", "false", "false"},
413             FF = {"get undefined", "", "false", "false", "false"},
414             FF_ESR = {"get undefined", "", "false", "false", "false"})
415     public void flagsProperty() throws Exception {
416         testProperty("flags");
417     }
418 
419     /**
420      * @throws Exception if an error occurs
421      */
422     @Test
423     @Alerts({"0", "undefined", "true", "false", "undefined"})
424     @HtmlUnitNYI(CHROME = {"get undefined", "false", "false", "false", "false"},
425             EDGE = {"get undefined", "false", "false", "false", "false"},
426             FF = {"get undefined", "false", "false", "false", "false"},
427             FF_ESR = {"get undefined", "false", "false", "false", "false"})
428     public void globalProperty() throws Exception {
429         testProperty("global");
430     }
431 
432     /**
433      * @throws Exception if an error occurs
434      */
435     @Test
436     @Alerts({"0", "undefined", "true", "false", "undefined"})
437     @HtmlUnitNYI(CHROME = {"get undefined", "false", "false", "false", "false"},
438             EDGE = {"get undefined", "false", "false", "false", "false"},
439             FF = {"get undefined", "false", "false", "false", "false"},
440             FF_ESR = {"get undefined", "false", "false", "false", "false"})
441     public void ignoreCaseProperty() throws Exception {
442         testProperty("ignoreCase");
443     }
444 
445     /**
446      * @throws Exception if an error occurs
447      */
448     @Test
449     @Alerts({"0", "undefined", "true", "false", "undefined"})
450     @HtmlUnitNYI(CHROME = {"get undefined", "false", "false", "false", "false"},
451             EDGE = {"get undefined", "false", "false", "false", "false"},
452             FF = {"get undefined", "false", "false", "false", "false"},
453             FF_ESR = {"get undefined", "false", "false", "false", "false"})
454     public void multilineProperty() throws Exception {
455         testProperty("multiline");
456     }
457 
458     /**
459      * @throws Exception if an error occurs
460      */
461     @Test
462     @Alerts({"0", "undefined", "true", "false", "undefined"})
463     @HtmlUnitNYI(CHROME = {"get undefined", "false", "false", "false", "false"},
464             EDGE = {"get undefined", "false", "false", "false", "false"},
465             FF = {"get undefined", "false", "false", "false", "false"},
466             FF_ESR = {"get undefined", "false", "false", "false", "false"})
467     public void stickyProperty() throws Exception {
468         testProperty("sticky");
469     }
470 
471     /**
472      * @throws Exception if an error occurs
473      */
474     @Test
475     @Alerts({"0", "undefined", "true", "false", "undefined"})
476     @HtmlUnitNYI(CHROME = {"get undefined", "false", "false", "false", "false"},
477             EDGE = {"get undefined", "false", "false", "false", "false"},
478             FF = {"get undefined", "false", "false", "false", "false"},
479             FF_ESR = {"get undefined", "false", "false", "false", "false"})
480     public void dotAllProperty() throws Exception {
481         testProperty("dotAll");
482     }
483 
484 
485     /**
486      * @throws Exception if an error occurs
487      */
488     @Test
489     @Alerts({"0", "undefined", "true", "false", "undefined"})
490     @HtmlUnitNYI(CHROME = {"get undefined", "false", "false", "false", "false"},
491             EDGE = {"get undefined", "false", "false", "false", "false"},
492             FF = {"get undefined", "false", "false", "false", "false"},
493             FF_ESR = {"get undefined", "false", "false", "false", "false"})
494     public void unicodeProperty() throws Exception {
495         testProperty("unicode");
496     }
497 
498 
499     /**
500      * @throws Exception if an error occurs
501      */
502     @Test
503     @Alerts({"0", "undefined", "true", "false", "undefined"})
504     @HtmlUnitNYI(CHROME = {"get undefined", "", "false", "false", "false"},
505             EDGE = {"get undefined", "", "false", "false", "false"},
506             FF = {"get undefined", "", "false", "false", "false"},
507             FF_ESR = {"get undefined", "", "false", "false", "false"})
508     public void sourceProperty() throws Exception {
509         testProperty("source");
510     }
511 
512     private void testProperty(final String property) throws Exception {
513         final String script =
514                 "var get = Object.getOwnPropertyDescriptor(RegExp.prototype, '" + property + "');\n"
515                 + "log(get.get == undefined ? 'get undefined' : get.get.length);\n"
516                 + "log(get.value);\n"
517                 + "log(get.configurable);\n"
518                 + "log(get.enumerable);\n"
519                 + "log(get.writable);\n";
520 
521         testEvaluate(script);
522     }
523 
524     /**
525      * @throws Exception if an error occurs
526      */
527     @Test
528     public void stringReplace() throws Exception {
529         testEvaluate("xyz", "''.replace('', 'xyz')");
530         testEvaluate("1", "'121'.replace('21', '')");
531         testEvaluate("xyz121", "'121'.replace('', 'xyz')");
532         testEvaluate("a$c21", "'121'.replace('1', 'a$c')");
533         testEvaluate("a121", "'121'.replace('1', 'a$&')");
534         testEvaluate("a$c21", "'121'.replace('1', 'a$$c')");
535         testEvaluate("abaabe", "'abcde'.replace('cd', 'a$`')");
536         testEvaluate("a21", "'121'.replace('1', 'a$`')");
537         testEvaluate("abaee", "'abcde'.replace('cd', \"a$'\")");
538         testEvaluate("aba", "'abcd'.replace('cd', \"a$'\")");
539         testEvaluate("aba$0", "'abcd'.replace('cd', 'a$0')");
540         testEvaluate("aba$1", "'abcd'.replace('cd', 'a$1')");
541         testEvaluate("abCD", "'abcd'.replace('cd', function (matched) { return matched.toUpperCase() })");
542         testEvaluate("", "'123456'.replace(/\\d+/, '')");
543         testEvaluate("123ABCD321abcd",
544                 "'123abcd321abcd'.replace(/[a-z]+/, function (matched) { return matched.toUpperCase() })");
545     }
546 
547     /**
548      * @throws Exception if an error occurs
549      */
550     @Test
551     public void stringReplaceAll() throws Exception {
552         testEvaluate("xyz", "''.replaceAll('', 'xyz')");
553         testEvaluate("1", "'12121'.replaceAll('21', '')");
554         testEvaluate("xyz1xyz2xyz1xyz", "'121'.replaceAll('', 'xyz')");
555         testEvaluate("a$c2a$c", "'121'.replaceAll('1', 'a$c')");
556         testEvaluate("a12a1", "'121'.replaceAll('1', 'a$&')");
557         testEvaluate("a$c2a$c", "'121'.replaceAll('1', 'a$$c')");
558         testEvaluate("aaadaaabcda", "'abcdabc'.replaceAll('bc', 'a$`')");
559         testEvaluate("a2a12", "'121'.replaceAll('1', 'a$`')");
560         testEvaluate("aadabcdaa", "'abcdabc'.replaceAll('bc', \"a$'\")");
561         testEvaluate("aadabcdaa", "'abcdabc'.replaceAll('bc', \"a$'\")");
562         testEvaluate("aa$0daa$0", "'abcdabc'.replaceAll('bc', 'a$0')");
563         testEvaluate("aa$1daa$1", "'abcdabc'.replaceAll('bc', 'a$1')");
564         testEvaluate("", "'123456'.replaceAll(/\\d+/g, '')");
565         testEvaluate("123456", "'123456'.replaceAll(undefined, '')");
566         testEvaluate("afoobarb", "'afoob'.replaceAll(/(foo)/g, '$1bar')");
567         testEvaluate("foobarb", "'foob'.replaceAll(/(foo)/gy, '$1bar')");
568         testEvaluate("hllo", "'hello'.replaceAll(/(h)e/gy, '$1')");
569         testEvaluate("$1llo", "'hello'.replaceAll(/he/g, '$1')");
570         testEvaluate("I$want$these$periods$to$be$$s", "'I.want.these.periods.to.be.$s'.replaceAll(/\\./g, '$')");
571         testEvaluate("food bar", "'foo bar'.replaceAll(/foo/g, '$&d')");
572         testEvaluate("foo foo ", "'foo bar'.replaceAll(/bar/g, '$`')");
573         testEvaluate(" bar bar", "'foo bar'.replaceAll(/foo/g, '$\\'')");
574         testEvaluate("$' bar", "'foo bar'.replaceAll(/foo/g, '$$\\'')");
575         testEvaluate("123FOOBAR321BARFOO123",
576                 "'123foobar321barfoo123'.replace(/[a-z]+/g, function (matched) { return matched.toUpperCase() })");
577 
578         testEvaluate(
579                 "TypeError: replaceAll must be called with a global RegExp",
580                 "'hello'.replaceAll(/he/i, 'x')");
581     }
582 
583     /**
584      * @throws Exception if an error occurs
585      */
586     @Test
587     @Alerts({"ad$0db", "ad$0db", "ad$0dbd$0dc"})
588     public void stringReplaceAllBrowserSpecific() throws Exception {
589         testEvaluate(getExpectedAlerts()[0], "'afoob'.replaceAll(/(foo)/g, 'd$0d')");
590         testEvaluate(getExpectedAlerts()[1], "'afkxxxkob'.replace(/(f)k(.*)k(o)/g, 'd$0d')");
591         testEvaluate(getExpectedAlerts()[2], "'afoobfuoc'.replaceAll(/(f.o)/g, 'd$0d')");
592     }
593 
594     private void testEvaluate(final String expected, final String script) throws Exception {
595         final String html =
596                 "<html>\n"
597                 + "<head>\n"
598                 + "<script>\n"
599                 + LOG_TEXTAREA_FUNCTION
600                 + "function test() {\n"
601                 + "  try {\n"
602                 + "    log(" + script + ");\n"
603                 + "  } catch(e) { log(e); }\n"
604                 + "}\n"
605                 + "</script>\n"
606                 + "</head>\n"
607                 + "<body onload='test()'>\n"
608                 + LOG_TEXTAREA
609                 + "</body></html>";
610 
611         final WebDriver driver = loadPage2(html);
612         verifyTextArea2(driver, expected);
613     }
614 
615     private void testEvaluate(final String script) throws Exception {
616         final String html =
617                 "<html>\n"
618                 + "<head>\n"
619                 + "<script>\n"
620                 + LOG_TEXTAREA_FUNCTION
621                 + "function test() {\n"
622                 + script
623                 + "}\n"
624                 + "</script>\n"
625                 + "</head>\n"
626                 + "<body onload='test()'>\n"
627                 + LOG_TEXTAREA
628                 + "</body></html>";
629 
630         loadPageVerifyTextArea2(html);
631     }
632 }