1 /*
2  * ConsoleOutputWriterTests.java
3  *
4  * Copyright (C) 2021 by RStudio, PBC
5  *
6  * Unless you have received this program directly from RStudio pursuant
7  * to the terms of a commercial license agreement with RStudio, then
8  * this program is licensed to you under the terms of version 3 of the
9  * GNU Affero General Public License. This program is distributed WITHOUT
10  * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11  * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12  * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13  *
14  */
15 package org.rstudio.core.client;
16 
17 import java.util.List;
18 
19 import org.rstudio.core.client.dom.DomUtils;
20 import org.rstudio.studio.client.workbench.prefs.model.UserPrefs;
21 
22 import com.google.gwt.dom.client.Element;
23 import com.google.gwt.dom.client.SpanElement;
24 import com.google.gwt.dom.client.Text;
25 import com.google.gwt.junit.client.GWTTestCase;
26 
27 import junit.framework.Assert;
28 
29 public class ConsoleOutputWriterTests extends GWTTestCase
30 {
31    private final String nullClazz = null;
32    private final boolean notError = false;
33    private final boolean isError = true;
34    private final boolean checkLineCount = false;
35    private final boolean ignoreLineCount = true;
36    private final String myClass = "myClass";
37    private final String myErrorClass = "myErrorClass";
38 
39    private final String newlineErrorSpan = "<span class=\"myErrorClass\">\n</span>";
40 
numberedLine(int i)41    private String numberedLine(int i)
42    {
43       return i + "\n";
44    }
45 
getInnerHTML(ConsoleOutputWriter output)46    private String getInnerHTML(ConsoleOutputWriter output)
47    {
48       SpanElement outerSpan = SpanElement.as(output.getElement().getFirstChildElement());
49       return outerSpan.getInnerHTML();
50    }
51 
52    private static class FakePrefs implements VirtualConsole.Preferences
53    {
54       @Override
truncateLongLinesInConsoleHistory()55       public int truncateLongLinesInConsoleHistory()
56       {
57          return truncateLines_;
58       }
59 
60       @Override
consoleAnsiMode()61       public String consoleAnsiMode()
62       {
63          return ansiMode_;
64       }
65 
66       @Override
screenReaderEnabled()67       public boolean screenReaderEnabled()
68       {
69          return screenReaderEnabled_;
70       }
71 
72       @Override
limitConsoleVisible()73       public boolean limitConsoleVisible()
74       {
75          return limitConsoleVisible_;
76       }
77 
78       public boolean limitConsoleVisible_ = false;
79       public final int truncateLines_ = 1000;
80       public final String ansiMode_ = UserPrefs.ANSI_CONSOLE_MODE_ON;
81       public final boolean screenReaderEnabled_ = false;
82    }
83 
84    private static class VCFactory implements VirtualConsoleFactory
85    {
86       @Override
create(Element elem)87       public VirtualConsole create(Element elem)
88       {
89          return new VirtualConsole(elem, new FakePrefs());
90       }
91    }
92 
getCOW()93    private ConsoleOutputWriter getCOW()
94    {
95       return new ConsoleOutputWriter(new VCFactory(), null);
96    }
97 
98    @Override
getModuleName()99    public String getModuleName()
100    {
101       return "org.rstudio.studio.RStudioTests";
102    }
103 
testCreation()104    public void testCreation()
105    {
106       ConsoleOutputWriter output = getCOW();
107       Assert.assertNotNull(output);
108       Assert.assertNotNull(output.getWidget());
109    }
110 
testSetGetMaxLines()111    public void testSetGetMaxLines()
112    {
113       ConsoleOutputWriter output = getCOW();
114       Assert.assertEquals(-1,  output.getMaxOutputLines());
115       output.setMaxOutputLines(1000);
116       Assert.assertEquals(1000, output.getMaxOutputLines());
117    }
118 
testSimpleLineCount()119    public void testSimpleLineCount()
120    {
121       ConsoleOutputWriter output = getCOW();
122       Assert.assertEquals(0, output.getCurrentLines());
123       Assert.assertTrue(output.outputToConsole("Hello World",
124             nullClazz, notError, checkLineCount, false));
125       Assert.assertEquals(0,  output.getCurrentLines());
126       Assert.assertTrue(output.outputToConsole(" more on same line",
127             nullClazz, notError, checkLineCount, false));
128       Assert.assertEquals(0,  output.getCurrentLines());
129       Assert.assertTrue(output.outputToConsole("next line starts now\n",
130             nullClazz, notError, checkLineCount, false));
131       Assert.assertEquals(1,  output.getCurrentLines());
132       Assert.assertEquals(1, DomUtils.countLines(output.getElement(), true));
133    }
134 
testTrimming()135    public void testTrimming()
136    {
137       // this test is rather bulky; it tests that lines get trimmed, but also
138       // some basic cross-checking of the output structure, behavior of
139       // trimOutput, and lack of a class attribute when no clazz was specified
140       ConsoleOutputWriter output = getCOW();
141       final int maxLines = 25;
142       output.setMaxOutputLines(maxLines);
143 
144       // write just below the maximum
145       for (int i = 0; i < maxLines; i++)
146       {
147          Assert.assertTrue(output.outputToConsole(numberedLine(i),
148                nullClazz, notError, checkLineCount, false));
149          Assert.assertEquals(i + 1, output.getCurrentLines());
150       }
151 
152       // trim should be a no-op when below the limit
153       Assert.assertEquals(maxLines, output.getCurrentLines());
154       Assert.assertFalse(output.trimExcess());
155       Assert.assertEquals(maxLines, output.getCurrentLines());
156 
157       // go over the limit
158       Assert.assertFalse(output.outputToConsole(numberedLine(maxLines),
159             nullClazz, notError, checkLineCount, false));
160       Assert.assertEquals(maxLines, output.getCurrentLines());
161 
162       // go over the limit again
163       Assert.assertFalse(output.outputToConsole(numberedLine(maxLines + 1),
164             nullClazz, notError, checkLineCount, false));
165       Assert.assertEquals(maxLines, output.getCurrentLines());
166 
167       // verify DOM matches expectations; first two output lines (0 and 1)
168       // should have been removed; since clazz has been the same, all lines
169       // should be in a single child <span> under the initial <span>
170       Element parent = output.getElement();
171       Assert.assertEquals(1, parent.getChildCount());
172       SpanElement outerSpan = SpanElement.as(parent.getFirstChildElement());
173       Assert.assertEquals(1, outerSpan.getChildCount());
174       SpanElement span = SpanElement.as(outerSpan.getFirstChildElement());
175       Assert.assertEquals(1, span.getChildCount());
176       Text text = Text.as(span.getChild(0));
177 
178       StringBuilder expected = new StringBuilder();
179       for (int i = 2; i <= maxLines + 1; i++)
180       {
181          expected.append(numberedLine(i));
182       }
183       Assert.assertEquals(expected.toString(), text.getData());
184       Assert.assertEquals(span.getClassName(), "");
185 
186       String expectedInnerHtml = "<span>" + expected.toString() + "</span>";
187       Assert.assertEquals(expectedInnerHtml, getInnerHTML(output));
188       Assert.assertEquals(maxLines, DomUtils.countLines(output.getElement(), true));
189    }
190 
testBulkAddThenTrim()191    public void testBulkAddThenTrim()
192    {
193       // bulk adding lets you add more lines than the maximum, but defer
194       // trimming until the end
195       ConsoleOutputWriter output = getCOW();
196       final int maxLines = 50;
197       output.setMaxOutputLines(maxLines);
198 
199       for (int i = 0; i < maxLines + 10; i++)
200       {
201          Assert.assertTrue(output.outputToConsole(numberedLine(i),
202                myClass, notError, ignoreLineCount, false));
203          Assert.assertEquals(i + 1, output.getCurrentLines());
204       }
205 
206       Assert.assertEquals(maxLines + 10, output.getCurrentLines());
207       Assert.assertTrue(output.trimExcess());
208       Assert.assertEquals(maxLines, output.getCurrentLines());
209       Assert.assertEquals(maxLines, DomUtils.countLines(output.getElement(), true));
210 
211       StringBuilder expected = new StringBuilder();
212       expected.append("<span class=\"" + myClass + "\">");
213       for (int i = 10; i < maxLines + 10; i++)
214       {
215          expected.append(numberedLine(i));
216       }
217       expected.append("</span>");
218 
219       Assert.assertEquals(expected.toString(), getInnerHTML(output));
220    }
221 
testWriteSimpleError()222    public void testWriteSimpleError()
223    {
224       ConsoleOutputWriter output = getCOW();
225       String errorMsg = "Oh no, an error!!";
226 
227       Assert.assertTrue(output.outputToConsole(errorMsg,
228                myErrorClass, isError, ignoreLineCount, false));
229 
230       Assert.assertEquals(0,  output.getCurrentLines());
231 
232       String expected = "<span class=\"myErrorClass\">" + errorMsg + "</span>";
233       Assert.assertEquals(expected, getInnerHTML(output));
234    }
235 
testWriteSimpleErrorWithNewline()236    public void testWriteSimpleErrorWithNewline()
237    {
238       ConsoleOutputWriter output = getCOW();
239       String errorMsg = "Oh no, an error!!\n";
240 
241       Assert.assertTrue(output.outputToConsole(errorMsg,
242                myErrorClass, isError, ignoreLineCount, false));
243 
244       Assert.assertEquals(1, output.getCurrentLines());
245       String expected = "<span class=\"myErrorClass\">" + errorMsg + "</span>";
246       Assert.assertEquals(expected, getInnerHTML(output));
247    }
248 
249    ////////////////////////////////////////////////////////////////////////////
250    // Below here are a bunch of tests I had written in R and was checking by
251    // eyeball directly in RStudio. https://github.com/gtritchie/console_tests
252    // These test a variety of issues that came up in the product during
253    // development, with a mixture of Ansi colors, regular output, error output,
254    // and so on.
255    ////////////////////////////////////////////////////////////////////////////
256 
test1()257    public void test1()
258    {
259       // output multiple errors with same style, ensure each goes into its own
260       // span and the final one is captured and available via getNewElements
261       ConsoleOutputWriter output = getCOW();
262 
263       output.outputToConsole("1\n", myErrorClass, isError, ignoreLineCount, false);
264       output.outputToConsole("2\n", myErrorClass, isError, ignoreLineCount, false);
265       output.outputToConsole("3\n", myErrorClass, isError, ignoreLineCount, false);
266       output.outputToConsole("4\n", myErrorClass, isError, ignoreLineCount, false);
267 
268       String lastError = "Error in h() : An error! Oh No!";
269       output.outputToConsole(lastError, myErrorClass, isError, ignoreLineCount, false);
270 
271       String lastErrorSpan = "<span class=\"myErrorClass\">" + lastError + "</span>";
272       String expected =
273             "<span class=\"myErrorClass\">1\n</span>" +
274             "<span class=\"myErrorClass\">2\n</span>" +
275             "<span class=\"myErrorClass\">3\n</span>" +
276             "<span class=\"myErrorClass\">4\n</span>" +
277             lastErrorSpan;
278 
279       Assert.assertEquals(4, output.getCurrentLines());
280       Assert.assertEquals(expected, getInnerHTML(output));
281 
282       List<Element> newElements = output.getNewElements();
283       Assert.assertFalse(newElements.isEmpty());
284       Assert.assertEquals(1, newElements.size());
285       Assert.assertEquals(lastErrorSpan, newElements.get(0).getString());
286    }
287 
test2()288    public void test2()
289    {
290       // output multiple errors with varying styles via Ansi codes, ensure
291       // each goes into its own span with correct styles, and the final one,
292       // which also has multiple styles and spans three lines is
293       // captured and available via getNewElements.
294       ConsoleOutputWriter output = getCOW();
295 
296       output.outputToConsole("1\n", myErrorClass, isError, ignoreLineCount, false);
297       output.outputToConsole("2\n", myErrorClass, isError, ignoreLineCount, false);
298       output.outputToConsole("3\n", myErrorClass, isError, ignoreLineCount, false);
299       output.outputToConsole("4\n", myErrorClass, isError, ignoreLineCount, false);
300 
301       String lastError1 = "Error in h2() : An error!\n";
302       String lastError2 = "\033[31mOh No!\n\033[39m";
303       String lastError3 = "\033[43m\033[31mWow!\033[39m\033[49m";
304 
305       output.outputToConsole(lastError1 + lastError2 + lastError3,
306             myErrorClass, isError, ignoreLineCount, false);
307 
308       String lastError1Span = "<span class=\"myErrorClass\">" +
309                   lastError1 + "</span>";
310       String lastError2Span = "<span class=\"myErrorClass xtermColor1\">" +
311                   "Oh No!" + "</span>";
312       String lastError3Span = "<span class=\"myErrorClass xtermBgColor3 xtermColor1\">" +
313                   "Wow!" + "</span>";
314 
315       String expected =
316             "<span class=\"myErrorClass\">1\n</span>" +
317             "<span class=\"myErrorClass\">2\n</span>" +
318             "<span class=\"myErrorClass\">3\n</span>" +
319             "<span class=\"myErrorClass\">4\n</span>" +
320             lastError1Span + lastError2Span + newlineErrorSpan + lastError3Span;
321 
322       Assert.assertEquals(6, output.getCurrentLines());
323       Assert.assertEquals(expected, getInnerHTML(output));
324 
325       List<Element> newElements = output.getNewElements();
326       Assert.assertFalse(newElements.isEmpty());
327       Assert.assertEquals(4, newElements.size());
328       Assert.assertEquals(lastError1Span, newElements.get(0).getString());
329       Assert.assertEquals(lastError2Span, newElements.get(1).getString());
330       Assert.assertEquals(newlineErrorSpan, newElements.get(2).getString());
331       Assert.assertEquals(lastError3Span, newElements.get(3).getString());
332    }
333 
test3()334    public void test3()
335    {
336       // output two regular strings, without a newline, and make sure
337       // they turn into a single span of text
338       ConsoleOutputWriter output = getCOW();
339 
340       output.outputToConsole("Hello", myClass, notError, ignoreLineCount, false);
341       output.outputToConsole("World", myClass, notError, ignoreLineCount, false);
342 
343       Assert.assertEquals(0, output.getCurrentLines());
344 
345       Assert.assertEquals("<span class=\"myClass\">HelloWorld</span>",
346             getInnerHTML(output));
347    }
348 
test4()349    public void test4()
350    {
351       // output 4 regular strings followed by newlines; should end up in a
352       // single span
353       ConsoleOutputWriter output = getCOW();
354 
355       output.outputToConsole("One\n", myClass, notError, ignoreLineCount, false);
356       output.outputToConsole("Two\n", myClass, notError, ignoreLineCount, false);
357       output.outputToConsole("Three\n", myClass, notError, ignoreLineCount, false);
358       output.outputToConsole("Four\n", myClass, notError, ignoreLineCount, false);
359 
360       Assert.assertEquals(4, output.getCurrentLines());
361 
362       Assert.assertEquals("<span class=\"myClass\">One\nTwo\nThree\nFour\n</span>",
363             getInnerHTML(output));
364 
365    }
366 
test5()367    public void test5()
368    {
369       // output two error strings, then one regular string, then another error
370       // and make sure DOM ends up as expected
371       ConsoleOutputWriter output = getCOW();
372 
373       output.outputToConsole("1\n", myErrorClass, isError, ignoreLineCount, false);
374       output.outputToConsole("2\n", myErrorClass, isError, ignoreLineCount, false);
375       output.outputToConsole("Hello ", myClass, notError, ignoreLineCount, false);
376       output.outputToConsole("world\n", myClass, notError, ignoreLineCount, false);
377       output.outputToConsole("3\n", myErrorClass, isError, ignoreLineCount, false);
378 
379       Assert.assertEquals(4, output.getCurrentLines());
380 
381       Assert.assertEquals("<span class=\"myErrorClass\">1\n</span>" +
382             "<span class=\"myErrorClass\">2\n</span>" +
383             "<span class=\"myClass\">Hello world\n</span>" +
384             "<span class=\"myErrorClass\">3\n</span>",
385             getInnerHTML(output));
386 
387       List<Element> newElements = output.getNewElements();
388       Assert.assertEquals(1, newElements.size());
389       Assert.assertEquals("<span class=\"myErrorClass\">3\n</span>",
390             newElements.get(0).getString());
391    }
392 
test6()393    public void test6()
394    {
395       // output 4 error messages without ansi codes; each should end up in its
396       // own span, and the final should be captured
397       ConsoleOutputWriter output = getCOW();
398 
399       output.outputToConsole("1\n", myErrorClass, isError, ignoreLineCount, false);
400       output.outputToConsole("2\n", myErrorClass, isError, ignoreLineCount, false);
401       output.outputToConsole("3\n", myErrorClass, isError, ignoreLineCount, false);
402       output.outputToConsole("4", myErrorClass, isError, ignoreLineCount, false);
403 
404       Assert.assertEquals(3, output.getCurrentLines());
405 
406       Assert.assertEquals(
407             "<span class=\"myErrorClass\">1\n</span>" +
408             "<span class=\"myErrorClass\">2\n</span>" +
409             "<span class=\"myErrorClass\">3\n</span>" +
410             "<span class=\"myErrorClass\">4</span>",
411             getInnerHTML(output));
412 
413       List<Element> newElements = output.getNewElements();
414       Assert.assertEquals(1, newElements.size());
415       Assert.assertEquals("<span class=\"myErrorClass\">4</span>",
416             newElements.get(0).getString());
417    }
418 
test7()419    public void test7()
420    {
421       // output a single error message with ansi code in it
422       ConsoleOutputWriter output = getCOW();
423 
424       output.outputToConsole("Error in test7a() : \033[32mHi\033[39m", myErrorClass,
425             isError, ignoreLineCount, false);
426 
427       Assert.assertEquals(0, output.getCurrentLines());
428 
429       Assert.assertEquals(
430             "<span class=\"myErrorClass\">Error in test7a() : </span>" +
431             "<span class=\"myErrorClass xtermColor2\">Hi</span>",
432             getInnerHTML(output));
433 
434       List<Element> newElements = output.getNewElements();
435       Assert.assertEquals(2, newElements.size());
436       Assert.assertEquals("<span class=\"myErrorClass\">Error in test7a() : </span>",
437             newElements.get(0).getString());
438       Assert.assertEquals("<span class=\"myErrorClass xtermColor2\">Hi</span>",
439             newElements.get(1).getString());
440    }
441 
test8()442    public void test8()
443    {
444       // Write four error lines, then a complex multi-line error message with
445       // ansi codes.
446       ConsoleOutputWriter output = getCOW();
447 
448       output.outputToConsole("1\n", myErrorClass, isError, ignoreLineCount, false);
449       output.outputToConsole("2\n", myErrorClass, isError, ignoreLineCount, false);
450       output.outputToConsole("3\n", myErrorClass, isError, ignoreLineCount, false);
451       output.outputToConsole("4\n", myErrorClass, isError, ignoreLineCount, false);
452       output.outputToConsole(
453             "Error in test8b() : An error!\n" +
454             "\033[31mOh No!\n\033[39m" +
455             "\033[43m\033[31mA multiline error with colors.\n\033[39m\033[49m" +
456             "\033[7mAnd some inverse text.\n\033[27m" +
457             "\033[1m\033[3mThe Horror!!\033[23m\033[22m",
458             myErrorClass, isError, ignoreLineCount, false);
459 
460       Assert.assertEquals(8, output.getCurrentLines());
461 
462       Assert.assertEquals(
463             "<span class=\"myErrorClass\">1\n</span>" +
464             "<span class=\"myErrorClass\">2\n</span>" +
465             "<span class=\"myErrorClass\">3\n</span>" +
466             "<span class=\"myErrorClass\">4\n</span>" +
467             "<span class=\"myErrorClass\">Error in test8b() : An error!\n</span>" +
468             "<span class=\"myErrorClass xtermColor1\">Oh No!</span>" +
469             newlineErrorSpan +
470             "<span class=\"myErrorClass xtermBgColor3 xtermColor1\">A multiline error with colors.</span>" +
471             newlineErrorSpan +
472             "<span class=\"myErrorClass xtermInvertColor xtermInvertBgColor\">And some inverse text.</span>" +
473             newlineErrorSpan +
474             "<span class=\"myErrorClass xtermBold xtermItalic\">The Horror!!</span>",
475             getInnerHTML(output));
476 
477       List<Element> newElements = output.getNewElements();
478       Assert.assertEquals(8, newElements.size());
479 
480       Assert.assertEquals("<span class=\"myErrorClass\">Error in test8b() : An error!\n</span>",
481             newElements.get(0).getString());
482       Assert.assertEquals("<span class=\"myErrorClass xtermColor1\">Oh No!</span>",
483             newElements.get(1).getString());
484       Assert.assertEquals(newlineErrorSpan, newElements.get(2).getString());
485       Assert.assertEquals("<span class=\"myErrorClass xtermBgColor3 xtermColor1\">A multiline error with colors.</span>" ,
486             newElements.get(3).getString());
487       Assert.assertEquals(newlineErrorSpan, newElements.get(4).getString());
488       Assert.assertEquals("<span class=\"myErrorClass xtermInvertColor xtermInvertBgColor\">And some inverse text.</span>",
489             newElements.get(5).getString());
490       Assert.assertEquals(newlineErrorSpan, newElements.get(6).getString());
491       Assert.assertEquals("<span class=\"myErrorClass xtermBold xtermItalic\">The Horror!!</span>",
492             newElements.get(7).getString());
493    }
494 
test9()495    public void test9()
496    {
497       // write a single-line error followed by a multi-line error, no ansi codes
498       ConsoleOutputWriter output = getCOW();
499 
500       output.outputToConsole("Hello\n", myErrorClass, isError, ignoreLineCount, false);
501       output.outputToConsole("A\nB\nC", myErrorClass, isError, ignoreLineCount, false);
502 
503       Assert.assertEquals(3, output.getCurrentLines());
504 
505       Assert.assertEquals(
506             "<span class=\"myErrorClass\">Hello\n</span>" +
507             "<span class=\"myErrorClass\">A\nB\nC</span>",
508             getInnerHTML(output));
509 
510       List<Element> newElements = output.getNewElements();
511       Assert.assertEquals(1, newElements.size());
512       Assert.assertEquals("<span class=\"myErrorClass\">A\nB\nC</span>",
513             newElements.get(0).getString());
514    }
515 
test10()516    public void test10()
517    {
518       // inline editing via \r without ansi codes
519       ConsoleOutputWriter output = getCOW();
520       output.outputToConsole("\rfoobar", myClass, notError, ignoreLineCount, false);
521       output.outputToConsole("\rX foobar\n", myClass, notError, ignoreLineCount, false);
522 
523       Assert.assertEquals(1, output.getCurrentLines());
524       Assert.assertEquals(
525             "<span class=\"myClass\">X foobar\n</span>",
526             getInnerHTML(output));
527    }
528 
test11()529    public void test11()
530    {
531       // inline editing via \r with ansi codes
532       ConsoleOutputWriter output = getCOW();
533       output.outputToConsole("\rfoobar", myClass, notError, ignoreLineCount, false);
534       output.outputToConsole("\r\033[32mX\033[39m \033[31mfoobar\033[39m\n",
535             myClass, notError, ignoreLineCount, false);
536 
537       Assert.assertEquals(1, output.getCurrentLines());
538       Assert.assertEquals(
539             "<span class=\"myClass xtermColor2\">X</span>" +
540             "<span class=\"myClass\"> </span>" +
541             "<span class=\"myClass xtermColor1\">foobar</span>" +
542             "<span class=\"myClass\">\n</span>",
543             getInnerHTML(output));
544    }
545 
test12()546    public void test12()
547    {
548       // inline editing via \r with ansi codes; multiple output lines, don't
549       // overwrite entire original output
550       ConsoleOutputWriter output = getCOW();
551       output.outputToConsole("Hello\nWorld", myClass, notError, ignoreLineCount, false);
552       output.outputToConsole("\r\033[32mX\033[39m \033[31mY\033[39m\n",
553             myClass, notError, ignoreLineCount, false);
554 
555       Assert.assertEquals(2, output.getCurrentLines());
556       Assert.assertEquals(
557             "<span class=\"myClass\">Hello\n</span>" +
558             "<span class=\"myClass xtermColor2\">X</span>" +
559             "<span class=\"myClass\"> </span>" +
560             "<span class=\"myClass xtermColor1\">Y</span>" +
561             "<span class=\"myClass\">ld\n</span>",
562             getInnerHTML(output));
563    }
564 
test13()565    public void test13()
566    {
567       // inline editing via \r with ansi codes; multiple output lines,
568       // overwrite entire original output
569       ConsoleOutputWriter output = getCOW();
570       output.outputToConsole("Hello\nWorld", myClass, notError, ignoreLineCount, false);
571       output.outputToConsole("\r\033[32m123\033[39m\033[31m45\033[39m\n",
572             myClass, notError, ignoreLineCount, false);
573 
574       Assert.assertEquals(2, output.getCurrentLines());
575       Assert.assertEquals(
576             "<span class=\"myClass\">Hello\n</span>" +
577             "<span class=\"myClass xtermColor2\">123</span>" +
578             "<span class=\"myClass\"></span>" +
579             "<span class=\"myClass xtermColor1\">45</span>" +
580             "<span class=\"myClass\">\n</span>",
581             getInnerHTML(output));
582    }
583 
test14()584    public void test14()
585    {
586       // output mixture of normal and error output, make sure DOM is correct
587       ConsoleOutputWriter output = getCOW();
588 
589       output.outputToConsole("Beginning\n", myClass, notError, ignoreLineCount, false);
590       output.outputToConsole("1\n", myErrorClass, isError, ignoreLineCount, false);
591       output.outputToConsole("2\n", myErrorClass, isError, ignoreLineCount, false);
592       output.outputToConsole("Hello ", myClass, notError, ignoreLineCount, false);
593       output.outputToConsole("world\n", myClass, notError, ignoreLineCount, false);
594       output.outputToConsole("3\n", myErrorClass, isError, ignoreLineCount, false);
595       output.outputToConsole("END", myClass, notError, ignoreLineCount, false);
596 
597       Assert.assertEquals(5, output.getCurrentLines());
598 
599       Assert.assertEquals("<span class=\"myClass\">Beginning\n</span>" +
600             "<span class=\"myErrorClass\">1\n</span>" +
601             "<span class=\"myErrorClass\">2\n</span>" +
602             "<span class=\"myClass\">Hello world\n</span>" +
603             "<span class=\"myErrorClass\">3\n</span>" +
604             "<span class=\"myClass\">END</span>",
605             getInnerHTML(output));
606 
607    }
608 
test15()609    public void test15()
610    {
611       // write multiple error lines followed by a multi-line error, no ansi codes
612       ConsoleOutputWriter output = getCOW();
613 
614       output.outputToConsole("1\n", myErrorClass, isError, ignoreLineCount, false);
615       output.outputToConsole("2\n", myErrorClass, isError, ignoreLineCount, false);
616       output.outputToConsole(
617             "Error in h15() : A multiline error without colors!\nOh No!",
618             myErrorClass, isError, ignoreLineCount, false);
619 
620       Assert.assertEquals(3, output.getCurrentLines());
621 
622       Assert.assertEquals(
623             "<span class=\"myErrorClass\">1\n</span>" +
624             "<span class=\"myErrorClass\">2\n</span>" +
625             "<span class=\"myErrorClass\">Error in h15() : A multiline error " +
626                "without colors!\nOh No!</span>",
627             getInnerHTML(output));
628 
629       List<Element> newElements = output.getNewElements();
630       Assert.assertEquals(1, newElements.size());
631       Assert.assertEquals("<span class=\"myErrorClass\">Error in h15() : A " +
632          "multiline error without colors!\nOh No!</span>",
633             newElements.get(0).getString());
634    }
635 
test16()636    public void test16()
637    {
638       // write several lines of regular output
639       ConsoleOutputWriter output = getCOW();
640 
641       output.outputToConsole("one\n", myClass, notError, ignoreLineCount, false);
642       output.outputToConsole("two\n", myClass, notError, ignoreLineCount, false);
643       output.outputToConsole("three", myClass, notError, ignoreLineCount, false);
644 
645       Assert.assertEquals(2, output.getCurrentLines());
646 
647       Assert.assertEquals(
648             "<span class=\"myClass\">one\ntwo\nthree</span>",
649             getInnerHTML(output));
650    }
651 
test17()652    public void test17()
653    {
654       // inline editing via \r with ansi codes; multiple output lines, don't
655       // overwrite entire original output
656       ConsoleOutputWriter output = getCOW();
657       output.outputToConsole("✔ xxx \033[34myyy\033[39m xxx", myClass, notError, ignoreLineCount, false);
658       output.outputToConsole("\r", myClass, notError, ignoreLineCount, false);
659       output.outputToConsole("✔xxx \033[31myyy\033[39m zzz", myClass, notError, ignoreLineCount, false);
660       output.outputToConsole("\n", myClass, notError, ignoreLineCount, false);
661       output.outputToConsole("\n", myClass, notError, ignoreLineCount, false);
662       output.outputToConsole("✔ xxx \033[34myyy\033[39m xxx", myClass, notError, ignoreLineCount, false);
663       output.outputToConsole("\r", myClass, notError, ignoreLineCount, false);
664       output.outputToConsole("✔ xxx \033[31myyy\033[39m zzz", myClass, notError, ignoreLineCount, false);
665       output.outputToConsole("\n", myClass, notError, ignoreLineCount, false);
666       Assert.assertEquals(3, output.getCurrentLines());
667       Assert.assertEquals(
668          "<span class=\"myClass\">✔xxx </span>" +
669          "<span class=\"myClass xtermColor1\"></span>" + // redundant
670          "<span class=\"myClass\"></span>" + // redundant
671          "<span class=\"myClass xtermColor1\">yyy</span>" +
672          "<span class=\"myClass xtermColor4\"></span>" + // redundant
673          "<span class=\"myClass\"></span>" + // redundant
674          "<span class=\"myClass\"> zzzx\n\n✔ xxx </span>" +
675          "<span class=\"myClass xtermColor4\"></span>" + // redundant
676          "<span class=\"myClass xtermColor1\">yyy</span>" +
677          "<span class=\"myClass\"> zzz\n</span>",
678          getInnerHTML(output));
679    }
680 
test18()681    public void test18()
682    {
683       // https://github.com/rstudio/rstudio/issues/7278
684       ConsoleOutputWriter output = getCOW();
685       output.outputToConsole("B\033[31mx\033[39mA", myClass, notError, ignoreLineCount, false);
686       output.outputToConsole("\r   ", myClass, notError, ignoreLineCount, false);
687       output.outputToConsole("\rMessage\n", myClass, notError, ignoreLineCount, false);
688       Assert.assertEquals(1, output.getCurrentLines());
689       Assert.assertEquals("<span class=\"myClass\">Message\n</span>", getInnerHTML(output));
690    }
691 }
692