View Javadoc
1   package org.apache.maven.plugins.surefire.report;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *     http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.File;
23  import java.text.NumberFormat;
24  import java.util.List;
25  import java.util.Locale;
26  import java.util.Map;
27  import java.util.ResourceBundle;
28  import org.apache.maven.doxia.markup.HtmlMarkup;
29  import org.apache.maven.doxia.sink.Sink;
30  import org.apache.maven.doxia.sink.SinkEventAttributeSet;
31  import org.apache.maven.doxia.util.DoxiaUtils;
32  import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
33  import org.apache.maven.reporting.MavenReportException;
34  
35  import static org.apache.maven.doxia.markup.HtmlMarkup.A;
36  import static org.apache.maven.doxia.sink.Sink.JUSTIFY_LEFT;
37  import static org.apache.maven.doxia.sink.SinkEventAttributes.CLASS;
38  import static org.apache.maven.doxia.sink.SinkEventAttributes.HREF;
39  import static org.apache.maven.doxia.sink.SinkEventAttributes.ID;
40  import static org.apache.maven.doxia.sink.SinkEventAttributes.NAME;
41  import static org.apache.maven.doxia.sink.SinkEventAttributes.STYLE;
42  import static org.apache.maven.doxia.sink.SinkEventAttributes.TYPE;
43  import static org.apache.maven.shared.utils.StringUtils.isEmpty;
44  
45  /**
46   * This generator creates HTML Report from Surefire and Failsafe XML Report.
47   */
48  public final class SurefireReportGenerator
49  {
50      private static final int LEFT = JUSTIFY_LEFT;
51  
52      private static final Object[] TAG_TYPE_START = { HtmlMarkup.TAG_TYPE_START };
53  
54      private static final Object[] TAG_TYPE_END = { HtmlMarkup.TAG_TYPE_END };
55  
56      private final SurefireReportParser report;
57  
58      private final boolean showSuccess;
59  
60      private final String xrefLocation;
61      private final String title;
62      private List<ReportTestSuite> testSuites;
63  
64      public SurefireReportGenerator( List<File> reportsDirectories, Locale locale, boolean showSuccess,
65                                      String xrefLocation, ConsoleLogger consoleLogger, String title )
66      {
67          report = new SurefireReportParser( reportsDirectories, locale, consoleLogger );
68          this.showSuccess = showSuccess;
69          this.xrefLocation = xrefLocation;
70          this.title = title;
71      }
72  
73      public SurefireReportGenerator( List<File> reportsDirectories, Locale locale, boolean showSuccess,
74                                      String xrefLocation, ConsoleLogger consoleLogger )
75      {
76          this( reportsDirectories, locale, showSuccess, xrefLocation, consoleLogger, null );
77      }
78  
79      public void doGenerateReport( ResourceBundle bundle, Sink sink )
80          throws MavenReportException
81      {
82          testSuites = report.parseXMLReportFiles();
83  
84          sink.head();
85  
86          sink.title();
87          sink.text( isEmpty( title ) ? bundle.getString( "report.surefire.header" ) : title );
88          sink.title_();
89  
90          sink.head_();
91  
92          sink.body();
93  
94          SinkEventAttributeSet atts = new SinkEventAttributeSet();
95          atts.addAttribute( TYPE, "text/javascript" );
96          sink.unknown( "script", new Object[]{ HtmlMarkup.TAG_TYPE_START }, atts );
97          sink.unknown( "cdata", new Object[]{ HtmlMarkup.CDATA_TYPE, javascriptToggleDisplayCode() }, null );
98          sink.unknown( "script", new Object[]{ HtmlMarkup.TAG_TYPE_END }, null );
99  
100         sink.section1();
101         sink.sectionTitle1();
102         sink.text( isEmpty( title ) ? bundle.getString( "report.surefire.header" ) : title );
103         sink.sectionTitle1_();
104         sink.section1_();
105 
106         constructSummarySection( bundle, sink );
107 
108         Map<String, List<ReportTestSuite>> suitePackages = report.getSuitesGroupByPackage( testSuites );
109         if ( !suitePackages.isEmpty() )
110         {
111             constructPackagesSection( bundle, sink, suitePackages );
112         }
113 
114         if ( !testSuites.isEmpty() )
115         {
116             constructTestCasesSection( bundle, sink );
117         }
118 
119         List<ReportTestCase> failureList = report.getFailureDetails( testSuites );
120         if ( !failureList.isEmpty() )
121         {
122             constructFailureDetails( sink, bundle, failureList );
123         }
124 
125         sink.body_();
126 
127         sink.flush();
128 
129         sink.close();
130     }
131 
132     private void constructSummarySection( ResourceBundle bundle, Sink sink )
133     {
134         Map<String, String> summary = report.getSummary( testSuites );
135 
136         sink.section1();
137         sink.sectionTitle1();
138         sink.text( bundle.getString( "report.surefire.label.summary" ) );
139         sink.sectionTitle1_();
140 
141         sinkAnchor( sink, "Summary" );
142 
143         constructHotLinks( sink, bundle );
144 
145         sinkLineBreak( sink );
146 
147         sink.table();
148 
149         sink.tableRows( new int[]{ LEFT, LEFT, LEFT, LEFT, LEFT, LEFT }, true );
150 
151         sink.tableRow();
152 
153         sinkHeader( sink, bundle.getString( "report.surefire.label.tests" ) );
154 
155         sinkHeader( sink, bundle.getString( "report.surefire.label.errors" ) );
156 
157         sinkHeader( sink, bundle.getString( "report.surefire.label.failures" ) );
158 
159         sinkHeader( sink, bundle.getString( "report.surefire.label.skipped" ) );
160 
161         sinkHeader( sink, bundle.getString( "report.surefire.label.successrate" ) );
162 
163         sinkHeader( sink, bundle.getString( "report.surefire.label.time" ) );
164 
165         sink.tableRow_();
166 
167         sink.tableRow();
168 
169         sinkCell( sink, summary.get( "totalTests" ) );
170 
171         sinkCell( sink, summary.get( "totalErrors" ) );
172 
173         sinkCell( sink, summary.get( "totalFailures" ) );
174 
175         sinkCell( sink, summary.get( "totalSkipped" ) );
176 
177         sinkCell( sink, summary.get( "totalPercentage" ) + "%" );
178 
179         sinkCell( sink, summary.get( "totalElapsedTime" ) );
180 
181         sink.tableRow_();
182 
183         sink.tableRows_();
184 
185         sink.table_();
186 
187         sink.lineBreak();
188 
189         sink.paragraph();
190         sink.text( bundle.getString( "report.surefire.text.note1" ) );
191         sink.paragraph_();
192 
193         sinkLineBreak( sink );
194 
195         sink.section1_();
196     }
197 
198     private void constructPackagesSection( ResourceBundle bundle, Sink sink,
199                                            Map<String, List<ReportTestSuite>> suitePackages )
200     {
201         NumberFormat numberFormat = report.getNumberFormat();
202 
203         sink.section1();
204         sink.sectionTitle1();
205         sink.text( bundle.getString( "report.surefire.label.packagelist" ) );
206         sink.sectionTitle1_();
207 
208         sinkAnchor( sink, "Package_List" );
209 
210         constructHotLinks( sink, bundle );
211 
212         sinkLineBreak( sink );
213 
214         sink.table();
215 
216         sink.tableRows( new int[]{ LEFT, LEFT, LEFT, LEFT, LEFT, LEFT, LEFT }, true );
217 
218         sink.tableRow();
219 
220         sinkHeader( sink, bundle.getString( "report.surefire.label.package" ) );
221 
222         sinkHeader( sink, bundle.getString( "report.surefire.label.tests" ) );
223 
224         sinkHeader( sink, bundle.getString( "report.surefire.label.errors" ) );
225 
226         sinkHeader( sink, bundle.getString( "report.surefire.label.failures" ) );
227 
228         sinkHeader( sink, bundle.getString( "report.surefire.label.skipped" ) );
229 
230         sinkHeader( sink, bundle.getString( "report.surefire.label.successrate" ) );
231 
232         sinkHeader( sink, bundle.getString( "report.surefire.label.time" ) );
233 
234         sink.tableRow_();
235 
236         for ( Map.Entry<String, List<ReportTestSuite>> entry : suitePackages.entrySet() )
237         {
238             sink.tableRow();
239 
240             String packageName = entry.getKey();
241 
242             List<ReportTestSuite> testSuiteList = entry.getValue();
243 
244             Map<String, String> packageSummary = report.getSummary( testSuiteList );
245 
246             sinkCellLink( sink, packageName, "#" + packageName );
247 
248             sinkCell( sink, packageSummary.get( "totalTests" ) );
249 
250             sinkCell( sink, packageSummary.get( "totalErrors" ) );
251 
252             sinkCell( sink, packageSummary.get( "totalFailures" ) );
253 
254             sinkCell( sink, packageSummary.get( "totalSkipped" ) );
255 
256             sinkCell( sink, packageSummary.get( "totalPercentage" ) + "%" );
257 
258             sinkCell( sink, packageSummary.get( "totalElapsedTime" ) );
259 
260             sink.tableRow_();
261         }
262 
263         sink.tableRows_();
264 
265         sink.table_();
266 
267         sink.lineBreak();
268 
269         sink.paragraph();
270         sink.text( bundle.getString( "report.surefire.text.note2" ) );
271         sink.paragraph_();
272 
273         for ( Map.Entry<String, List<ReportTestSuite>> entry : suitePackages.entrySet() )
274         {
275             String packageName = entry.getKey();
276 
277             List<ReportTestSuite> testSuiteList = entry.getValue();
278 
279             sink.section2();
280             sink.sectionTitle2();
281             sink.text( packageName );
282             sink.sectionTitle2_();
283 
284             sinkAnchor( sink, packageName );
285 
286             boolean showTable = false;
287 
288             for ( ReportTestSuite suite : testSuiteList )
289             {
290                 if ( showSuccess || suite.getNumberOfErrors() != 0 || suite.getNumberOfFailures() != 0 )
291                 {
292                     showTable = true;
293 
294                     break;
295                 }
296             }
297 
298             if ( showTable )
299             {
300                 sink.table();
301 
302                 sink.tableRows( new int[]{ LEFT, LEFT, LEFT, LEFT, LEFT, LEFT, LEFT, LEFT }, true );
303 
304                 sink.tableRow();
305 
306                 sinkHeader( sink, "" );
307 
308                 sinkHeader( sink, bundle.getString( "report.surefire.label.class" ) );
309 
310                 sinkHeader( sink, bundle.getString( "report.surefire.label.tests" ) );
311 
312                 sinkHeader( sink, bundle.getString( "report.surefire.label.errors" ) );
313 
314                 sinkHeader( sink, bundle.getString( "report.surefire.label.failures" ) );
315 
316                 sinkHeader( sink, bundle.getString( "report.surefire.label.skipped" ) );
317 
318                 sinkHeader( sink, bundle.getString( "report.surefire.label.successrate" ) );
319 
320                 sinkHeader( sink, bundle.getString( "report.surefire.label.time" ) );
321 
322                 sink.tableRow_();
323 
324                 for ( ReportTestSuite suite : testSuiteList )
325                 {
326                     if ( showSuccess || suite.getNumberOfErrors() != 0 || suite.getNumberOfFailures() != 0 )
327                     {
328                         constructTestSuiteSection( sink, numberFormat, suite );
329                     }
330                 }
331 
332                 sink.tableRows_();
333 
334                 sink.table_();
335             }
336 
337             sink.section2_();
338         }
339 
340         sinkLineBreak( sink );
341 
342         sink.section1_();
343     }
344 
345     private void constructTestSuiteSection( Sink sink, NumberFormat numberFormat, ReportTestSuite suite )
346     {
347         sink.tableRow();
348 
349         sink.tableCell();
350 
351         sink.link( "#" + suite.getPackageName() + '.' + suite.getName() );
352 
353         if ( suite.getNumberOfErrors() > 0 )
354         {
355             sinkIcon( "error", sink );
356         }
357         else if ( suite.getNumberOfFailures() > 0 )
358         {
359             sinkIcon( "junit.framework", sink );
360         }
361         else if ( suite.getNumberOfSkipped() > 0 )
362         {
363             sinkIcon( "skipped", sink );
364         }
365         else
366         {
367             sinkIcon( "success", sink );
368         }
369 
370         sink.link_();
371 
372         sink.tableCell_();
373 
374         sinkCellLink( sink, suite.getName(), "#" + suite.getPackageName() + '.' + suite.getName() );
375 
376         sinkCell( sink, Integer.toString( suite.getNumberOfTests() ) );
377 
378         sinkCell( sink, Integer.toString( suite.getNumberOfErrors() ) );
379 
380         sinkCell( sink, Integer.toString( suite.getNumberOfFailures() ) );
381 
382         sinkCell( sink, Integer.toString( suite.getNumberOfSkipped() ) );
383 
384         String percentage = report.computePercentage( suite.getNumberOfTests(), suite.getNumberOfErrors(),
385                                                       suite.getNumberOfFailures(), suite.getNumberOfSkipped() );
386         sinkCell( sink, percentage + "%" );
387 
388         sinkCell( sink, numberFormat.format( suite.getTimeElapsed() ) );
389 
390         sink.tableRow_();
391     }
392 
393     private void constructTestCasesSection( ResourceBundle bundle, Sink sink )
394     {
395         NumberFormat numberFormat = report.getNumberFormat();
396 
397         sink.section1();
398         sink.sectionTitle1();
399         sink.text( bundle.getString( "report.surefire.label.testcases" ) );
400         sink.sectionTitle1_();
401 
402         sinkAnchor( sink, "Test_Cases" );
403 
404         constructHotLinks( sink, bundle );
405 
406         for ( ReportTestSuite suite : testSuites )
407         {
408             List<ReportTestCase> testCases = suite.getTestCases();
409 
410             if ( !testCases.isEmpty() )
411             {
412                 sink.section2();
413                 sink.sectionTitle2();
414                 sink.text( suite.getName() );
415                 sink.sectionTitle2_();
416 
417                 sinkAnchor( sink, suite.getPackageName() + '.' + suite.getName() );
418 
419                 boolean showTable = false;
420 
421                 for ( ReportTestCase testCase : testCases )
422                 {
423                     if ( !testCase.isSuccessful() || showSuccess )
424                     {
425                         showTable = true;
426 
427                         break;
428                     }
429                 }
430 
431                 if ( showTable )
432                 {
433                     sink.table();
434 
435                     sink.tableRows( new int[]{ LEFT, LEFT, LEFT }, true );
436 
437                     for ( ReportTestCase testCase : testCases )
438                     {
439                         if ( !testCase.isSuccessful() || showSuccess )
440                         {
441                             constructTestCaseSection( sink, numberFormat, testCase );
442                         }
443                     }
444 
445                     sink.tableRows_();
446 
447                     sink.table_();
448                 }
449 
450                 sink.section2_();
451             }
452         }
453 
454         sinkLineBreak( sink );
455 
456         sink.section1_();
457     }
458 
459     private static void constructTestCaseSection( Sink sink, NumberFormat numberFormat, ReportTestCase testCase )
460     {
461         sink.tableRow();
462 
463         sink.tableCell();
464 
465         if ( testCase.getFailureType() != null )
466         {
467             sink.link( "#" + toHtmlId( testCase.getFullName() ) );
468 
469             sinkIcon( testCase.getFailureType(), sink );
470 
471             sink.link_();
472         }
473         else
474         {
475             sinkIcon( "success", sink );
476         }
477 
478         sink.tableCell_();
479 
480         if ( !testCase.isSuccessful() )
481         {
482             sink.tableCell();
483 
484             sinkAnchor( sink, "TC_" + toHtmlId( testCase.getFullName() ) );
485 
486             sinkLink( sink, testCase.getName(), "#" + toHtmlId( testCase.getFullName() ) );
487 
488             SinkEventAttributeSet atts = new SinkEventAttributeSet();
489             atts.addAttribute( CLASS, "detailToggle" );
490             atts.addAttribute( STYLE, "display:inline" );
491             sink.unknown( "div", TAG_TYPE_START, atts );
492 
493             sinkLink( sink, "javascript:toggleDisplay('" + toHtmlId( testCase.getFullName() ) + "');" );
494 
495             atts = new SinkEventAttributeSet();
496             atts.addAttribute( STYLE, "display:inline;" );
497             atts.addAttribute( ID, toHtmlId( testCase.getFullName() ) + "-off" );
498             sink.unknown( "span", TAG_TYPE_START, atts );
499             sink.text( " + " );
500             sink.unknown( "span", TAG_TYPE_END, null );
501 
502             atts = new SinkEventAttributeSet();
503             atts.addAttribute( STYLE, "display:none;" );
504             atts.addAttribute( ID, toHtmlId( testCase.getFullName() ) + "-on" );
505             sink.unknown( "span", TAG_TYPE_START, atts );
506             sink.text( " - " );
507             sink.unknown( "span", TAG_TYPE_END, null );
508 
509             sink.text( "[ Detail ]" );
510             sinkLink_( sink );
511 
512             sink.unknown( "div", TAG_TYPE_END, null );
513 
514             sink.tableCell_();
515         }
516         else
517         {
518             sinkCellAnchor( sink, testCase.getName(), "TC_" + toHtmlId( testCase.getFullName() ) );
519         }
520 
521         sinkCell( sink, numberFormat.format( testCase.getTime() ) );
522 
523         sink.tableRow_();
524 
525         if ( !testCase.isSuccessful() )
526         {
527             sink.tableRow();
528 
529             sinkCell( sink, "" );
530             sinkCell( sink, testCase.getFailureMessage() );
531             sinkCell( sink, "" );
532             sink.tableRow_();
533 
534             String detail = testCase.getFailureDetail();
535             if ( detail != null )
536             {
537                 sink.tableRow();
538                 sinkCell( sink, "" );
539 
540                 sink.tableCell();
541                 SinkEventAttributeSet atts = new SinkEventAttributeSet();
542                 atts.addAttribute( ID, toHtmlId( testCase.getFullName() ) + toHtmlIdFailure( testCase ) );
543                 atts.addAttribute( STYLE, "display:none;" );
544                 sink.unknown( "div", TAG_TYPE_START, atts );
545 
546                 sink.verbatim( null );
547                 sink.text( detail );
548                 sink.verbatim_();
549 
550                 sink.unknown( "div", TAG_TYPE_END, null );
551                 sink.tableCell_();
552 
553                 sinkCell( sink, "" );
554 
555                 sink.tableRow_();
556             }
557         }
558     }
559 
560     private static String toHtmlId( String id )
561     {
562         return DoxiaUtils.isValidId( id ) ? id : DoxiaUtils.encodeId( id, true );
563     }
564 
565     private void constructFailureDetails( Sink sink, ResourceBundle bundle, List<ReportTestCase> failures )
566     {
567         sink.section1();
568         sink.sectionTitle1();
569         sink.text( bundle.getString( "report.surefire.label.failuredetails" ) );
570         sink.sectionTitle1_();
571 
572         sinkAnchor( sink, "Failure_Details" );
573 
574         constructHotLinks( sink, bundle );
575 
576         sinkLineBreak( sink );
577 
578         sink.table();
579 
580         sink.tableRows( new int[]{ LEFT, LEFT }, true );
581 
582         for ( ReportTestCase tCase : failures )
583         {
584             sink.tableRow();
585 
586             sink.tableCell();
587 
588             String type = tCase.getFailureType();
589 
590             sinkIcon( type, sink );
591 
592             sink.tableCell_();
593 
594             sinkCellAnchor( sink, tCase.getName(), toHtmlId( tCase.getFullName() ) );
595 
596             sink.tableRow_();
597 
598             String message = tCase.getFailureMessage();
599 
600             sink.tableRow();
601 
602             sinkCell( sink, "" );
603 
604             sinkCell( sink, message == null ? type : type + ": " + message );
605 
606             sink.tableRow_();
607 
608             String detail = tCase.getFailureDetail();
609             if ( detail != null )
610             {
611                 sink.tableRow();
612 
613                 sinkCell( sink, "" );
614 
615                 sink.tableCell();
616                 SinkEventAttributeSet atts = new SinkEventAttributeSet();
617                 atts.addAttribute( ID, tCase.getName() + toHtmlIdFailure( tCase ) );
618                 sink.unknown( "div", TAG_TYPE_START, atts );
619 
620                 String fullClassName = tCase.getFullClassName();
621                 String errorLineNumber = tCase.getFailureErrorLine();
622                 if ( xrefLocation != null )
623                 {
624                     String path = fullClassName.replace( '.', '/' );
625                     sink.link( xrefLocation + "/" + path + ".html#" + errorLineNumber );
626                 }
627                 sink.text( fullClassName + ":" + errorLineNumber );
628 
629                 if ( xrefLocation != null )
630                 {
631                     sink.link_();
632                 }
633                 sink.unknown( "div", TAG_TYPE_END, null );
634 
635                 sink.tableCell_();
636 
637                 sink.tableRow_();
638             }
639         }
640 
641         sink.tableRows_();
642 
643         sink.table_();
644 
645         sinkLineBreak( sink );
646 
647         sink.section1_();
648     }
649 
650     private void constructHotLinks( Sink sink, ResourceBundle bundle )
651     {
652         if ( !testSuites.isEmpty() )
653         {
654             sink.paragraph();
655 
656             sink.text( "[" );
657             sinkLink( sink, bundle.getString( "report.surefire.label.summary" ), "#Summary" );
658             sink.text( "]" );
659 
660             sink.text( " [" );
661             sinkLink( sink, bundle.getString( "report.surefire.label.packagelist" ), "#Package_List" );
662             sink.text( "]" );
663 
664             sink.text( " [" );
665             sinkLink( sink, bundle.getString( "report.surefire.label.testcases" ), "#Test_Cases" );
666             sink.text( "]" );
667 
668             sink.paragraph_();
669         }
670     }
671 
672     private static String toHtmlIdFailure( ReportTestCase tCase )
673     {
674         return tCase.hasError() ? "-error" : "-failure";
675     }
676 
677     private static void sinkLineBreak( Sink sink )
678     {
679         sink.lineBreak();
680     }
681 
682     private static void sinkIcon( String type, Sink sink )
683     {
684         sink.figure();
685 
686         if ( type.startsWith( "junit.framework" ) || "skipped".equals( type ) )
687         {
688             sink.figureGraphics( "images/icon_warning_sml.gif" );
689         }
690         else if ( type.startsWith( "success" ) )
691         {
692             sink.figureGraphics( "images/icon_success_sml.gif" );
693         }
694         else
695         {
696             sink.figureGraphics( "images/icon_error_sml.gif" );
697         }
698 
699         sink.figure_();
700     }
701 
702     private static void sinkHeader( Sink sink, String header )
703     {
704         sink.tableHeaderCell();
705         sink.text( header );
706         sink.tableHeaderCell_();
707     }
708 
709     private static void sinkCell( Sink sink, String text )
710     {
711         sink.tableCell();
712         sink.text( text );
713         sink.tableCell_();
714     }
715 
716     private static void sinkLink( Sink sink, String text, String link )
717     {
718         sink.link( link );
719         sink.text( text );
720         sink.link_();
721     }
722 
723     private static void sinkCellLink( Sink sink, String text, String link )
724     {
725         sink.tableCell();
726         sinkLink( sink, text, link );
727         sink.tableCell_();
728     }
729 
730     private static void sinkCellAnchor( Sink sink, String text, String anchor )
731     {
732         sink.tableCell();
733         sinkAnchor( sink, anchor );
734         sink.text( text );
735         sink.tableCell_();
736     }
737 
738     private static void sinkAnchor( Sink sink, String anchor )
739     {
740         // Dollar '$' for nested classes is not valid character in sink.anchor() and therefore it is ignored
741         // https://issues.apache.org/jira/browse/SUREFIRE-1443
742         sink.unknown( A.toString(), TAG_TYPE_START, new SinkEventAttributeSet( NAME, anchor ) );
743         sink.unknown( A.toString(), TAG_TYPE_END, null );
744     }
745 
746     private static void sinkLink( Sink sink, String href )
747     {
748         // The "'" argument in this JavaScript function would be escaped to "&apos;"
749         // sink.link( "javascript:toggleDisplay('" + toHtmlId( testCase.getFullName() ) + "');" );
750         sink.unknown( A.toString(), TAG_TYPE_START, new SinkEventAttributeSet( HREF, href ) );
751     }
752 
753     @SuppressWarnings( "checkstyle:methodname" )
754     private static void sinkLink_( Sink sink )
755     {
756         sink.unknown( A.toString(), TAG_TYPE_END, null );
757     }
758 
759     private static String javascriptToggleDisplayCode()
760     {
761 
762         // the javascript code is emitted within a commented CDATA section
763         // so we have to start with a newline and comment the CDATA closing in the end
764 
765         return "\n" + "function toggleDisplay(elementId) {\n"
766                 + " var elm = document.getElementById(elementId + '-error');\n"
767                 + " if (elm == null) {\n"
768                 + "  elm = document.getElementById(elementId + '-failure');\n"
769                 + " }\n"
770                 + " if (elm && typeof elm.style != \"undefined\") {\n"
771                 + "  if (elm.style.display == \"none\") {\n"
772                 + "   elm.style.display = \"\";\n"
773                 + "   document.getElementById(elementId + '-off').style.display = \"none\";\n"
774                 + "   document.getElementById(elementId + '-on').style.display = \"inline\";\n"
775                 + "  } else if (elm.style.display == \"\") {"
776                 + "   elm.style.display = \"none\";\n"
777                 + "   document.getElementById(elementId + '-off').style.display = \"inline\";\n"
778                 + "   document.getElementById(elementId + '-on').style.display = \"none\";\n"
779                 + "  } \n"
780                 + " } \n"
781                 + " }\n"
782                 + "//";
783     }
784 }