/*
 * VirtualConsoleTests.java
 *
 * Copyright (C) 2022 by Posit Software, PBC
 *
 * Unless you have received this program directly from Posit Software pursuant
 * to the terms of a commercial license agreement with Posit Software, then
 * this program is licensed to you under the terms of version 3 of the
 * GNU Affero General Public License. This program is distributed WITHOUT
 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
 *
 */
package org.rstudio.core.client;

import org.rstudio.studio.client.workbench.prefs.model.UserPrefs;

import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.PreElement;
import com.google.gwt.junit.client.GWTTestCase;

import junit.framework.Assert;

// Note on test coverage: the VirtualConsole has two side effects; it operates on a
// DOM, if supplied, and also maintains an internal string with the output results.
// The "consolify" tests are testing the internal string only, and most of the other
// tests are only testing the DOM. Ideally these should all be extended to do both.

public class VirtualConsoleTests extends GWTTestCase
{
   @Override
   public String getModuleName()
   {
      return "org.rstudio.studio.RStudioTests";
   }

   private static class FakePrefs implements VirtualConsole.Preferences
   {
      @Override
      public int truncateLongLinesInConsoleHistory()
      {
         return truncateLines_;
      }

      @Override
      public String consoleAnsiMode()
      {
         return ansiMode_;
      }

      @Override
      public boolean screenReaderEnabled()
      {
         return screenReaderEnabled_;
      }

      public int truncateLines_ = 1000;
      public String ansiMode_ = UserPrefs.ANSI_CONSOLE_MODE_ON;
      public boolean screenReaderEnabled_ = false;
   }

   private static String consolify(String text)
   {
      VirtualConsole console = new VirtualConsole(null, new FakePrefs());
      console.submit(text);
      return console.toString();
   }

   private VirtualConsole getVC(Element ele)
   {
      return new VirtualConsole(ele, new FakePrefs());
   }

   private static String setCsiCode(int code)
   {
      return setCsiCode(String.valueOf(code));
   }

   private static String setCsiCode(String code)
   {
      return AnsiCode.CSI + code + AnsiCode.SGR;
   }

   private static String setForegroundIndex(int color)
   {
      return setCsiCode(AnsiCode.FOREGROUND_EXT + ";" + AnsiCode.EXT_BY_INDEX + ";" + color);
   }

   private static String setBackgroundIndex(int color)
   {
      return setCsiCode(AnsiCode.BACKGROUND_EXT + ";" + AnsiCode.EXT_BY_INDEX + ";" + color);
   }

   private static String inverseOn()
   {
      return setCsiCode(AnsiCode.INVERSE);
   }

   private static String inverseOff()
   {
      return setCsiCode(AnsiCode.INVERSE_OFF);
   }

   // ---- tests start here ----

   public void testSimpleText()
   {
      String simple = consolify("foo");
      Assert.assertEquals("foo", simple);
   }

   public void testEmbeddedBackspace()
   {
      String backspace = consolify("bool\bk");
      Assert.assertEquals("book", backspace);
   }

   public void testTrailingBackspace()
   {
      String backspace = consolify("bool\bk");
      Assert.assertEquals("book", backspace);
   }

   public void testCarriageReturn()
   {
      String cr = consolify("hello\rj");
      Assert.assertEquals("jello", cr);
   }

   public void testNewlineCarriageReturn()
   {
      String cr = consolify("L1\nL2\rL3");
      Assert.assertEquals("L1\nL3", cr);
   }

   public void testSimpleColor()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("Error", "error");
      Assert.assertEquals(
            "<span class=\"error\">Error</span>",
            ele.getInnerHTML());
   }

   public void testTwoColors()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("Output 1", "one");
      vc.submit("Output 2", "two");
      Assert.assertEquals(
            "<span class=\"one\">Output 1</span>" +
            "<span class=\"two\">Output 2</span>",
            ele.getInnerHTML());
   }

   public void testColorOverwrite()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("XXXX\r", "X");
      vc.submit("YY", "Y");
      Assert.assertEquals(
            "<span class=\"Y\">YY</span>" +
            "<span class=\"X\">XX</span>",
            ele.getInnerHTML());
   }

   public void testColorSplit()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("123456");
      vc.submit("\b\b\b\bXX", "X");
      Assert.assertEquals(
            "<span>12</span>" +
            "<span class=\"X\">XX</span>" +
            "<span>56</span>",
            ele.getInnerHTML());
   }

   public void testColorOverlap()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("123", "A");
      vc.submit("456", "B");
      vc.submit("\b\b\b\bXX", "X");
      Assert.assertEquals(
            "<span class=\"A\">12</span>" +
            "<span class=\"X\">XX</span>" +
            "<span class=\"B\">56</span>",
            ele.getInnerHTML());
   }

   public void testFormFeed()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("Sample1\n");
      vc.submit("Sample2\n");
      vc.submit("Sample3\f");
      vc.submit("Sample4");
      Assert.assertEquals("<span>Sample4</span>", ele.getInnerHTML());
   }

   public void testCarriageReturnWithStyleChange()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);

      vc.submit("World", "a");

      // replace first character of line with green 'X', then change
      // second character to red 'Y'
      vc.submit("\r\033[32mX\033[31mY\033[0m", "a");
      Assert.assertEquals(
            "<span class=\"a xtermColor2\">X</span>" +
            "<span class=\"a xtermColor1\">Y</span>" +
            "<span class=\"a\">rld</span>",
            ele.getInnerHTML());
   }

   public void testCarriageReturnWithStyleChange2()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);

      vc.submit("Hello\nWorld", "a");

      // replace first character of current line with green 'X', then change
      // second character to red 'Y'
      vc.submit("\r\033[32mX\033[31mY\033[0m", "a");
      Assert.assertEquals(
            "<span class=\"a\">Hello\n</span>" +
            "<span class=\"a xtermColor2\">X</span>" +
            "<span class=\"a xtermColor1\">Y</span>" +
            "<span class=\"a\">rld</span>",
            ele.getInnerHTML());
   }

   public void testCarriageReturnWithStyleChange3()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);

      vc.submit("Hello\nWorld", "a");

      vc.submit("\r\033[32m123\033[31m45\033[0m", "a");
      Assert.assertEquals(
            "<span class=\"a\">Hello\n</span>" +
            "<span class=\"a xtermColor2\">123</span>" +
            "<span class=\"a\"></span>" +
            "<span class=\"a xtermColor1\">45</span>",
            ele.getInnerHTML());
   }

   public void testAnsiColorStyleHelper()
   {
      Assert.assertEquals("xtermColor0",
            AnsiCode.clazzForColor(AnsiCode.FOREGROUND_MIN));
      Assert.assertEquals("xtermColor7",
            AnsiCode.clazzForColor(AnsiCode.FOREGROUND_MAX));
      Assert.assertEquals("xtermBgColor0",
            AnsiCode.clazzForBgColor(AnsiCode.BACKGROUND_MIN));
      Assert.assertEquals("xtermBgColor7",
            AnsiCode.clazzForBgColor(AnsiCode.BACKGROUND_MAX));
   }

   public void testAnsiIntenseColorStyleHelper()
   {
      Assert.assertEquals("xtermColor8",
            AnsiCode.clazzForColor(AnsiCode.FOREGROUND_INTENSE_MIN));
      Assert.assertEquals("xtermColor15",
            AnsiCode.clazzForColor(AnsiCode.FOREGROUND_INTENSE_MAX));
      Assert.assertEquals("xtermBgColor8",
            AnsiCode.clazzForBgColor(AnsiCode.BACKGROUND_INTENSE_MIN));
      Assert.assertEquals("xtermBgColor15",
            AnsiCode.clazzForBgColor(AnsiCode.BACKGROUND_INTENSE_MAX));
   }

   public void testMinAnsiForegroundColor()
   {
      int color = AnsiCode.FOREGROUND_MIN;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(setCsiCode(color) + "Hello");
      String expected ="<span class=\"xtermColor0\">Hello</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testMaxAnsiForegroundColor()
   {
      int color = AnsiCode.FOREGROUND_MAX;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(setCsiCode(color) + "Hello World");
      String expected ="<span class=\"xtermColor7\">Hello World</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testMinAnsiBgColor()
   {
      int color = AnsiCode.BACKGROUND_MIN;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(setCsiCode(color) + "pretty");
      String expected ="<span class=\"" +
            AnsiCode.clazzForBgColor(color) + "\">pretty</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testMaxAnsiBgColor()
   {
      int color = AnsiCode.BACKGROUND_MAX;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(setCsiCode(color) + "colors");
      String expected ="<span class=\"" +
            AnsiCode.clazzForBgColor(color) + "\">colors</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testResetAnsiForegroundColor()
   {
      int color = AnsiCode.ForeColorNum.RED;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(
            setCsiCode(color) + "Hello " +
            setCsiCode(AnsiCode.RESET_FOREGROUND) + "World");
      String expected =
            "<span class=\"" +
            AnsiCode.clazzForColor(color) + "\">Hello </span>" +
            "<span>World</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testResetAnsiBgColor()
   {
      int color = AnsiCode.BackColorNum.GREEN;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(
            setCsiCode(color) + "Sunny " +
            setCsiCode(AnsiCode.RESET_BACKGROUND) + "Days");
      String expected = "<span class=\"" + AnsiCode.clazzForBgColor(color) +
            "\">Sunny </span><span>Days</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testAnsiForegroundAndBgColor()
   {
      int color = AnsiCode.ForeColorNum.MAGENTA;
      int bgColor = AnsiCode.BackColorNum.GREEN;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(setCsiCode(color) + setCsiCode(bgColor) + "Hello");
      String expected ="<span class=\"" +
            AnsiCode.clazzForColor(color) + " " +
            AnsiCode.clazzForBgColor(bgColor) + "\">Hello</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testResetAnsiColors()
   {
      int color = AnsiCode.ForeColorNum.YELLOW;
      int bgColor = AnsiCode.BackColorNum.CYAN;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(
            setCsiCode(color) +
            setCsiCode(bgColor) + "Colorful " +
            setCsiCode(AnsiCode.RESET) + "Bland");
      String expected ="<span class=\"" +
            AnsiCode.clazzForColor(color) + " " +
            AnsiCode.clazzForBgColor(bgColor) +
            "\">Colorful </span><span>Bland</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testMinAnsiIntenseForegroundColor()
   {
      int color = AnsiCode.FOREGROUND_INTENSE_MIN;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(setCsiCode(color) + "Hello");
      String expected ="<span class=\"" +
            AnsiCode.clazzForColor(color) +
            "\">Hello</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testMaxAnsiIntenseForegroundColor()
   {
      int color = AnsiCode.FOREGROUND_INTENSE_MAX;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(setCsiCode(color) + "Hello World");
      String expected ="<span class=\"" +
            AnsiCode.clazzForColor(color) + "\">Hello World</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testMinAnsiIntenseBgColor()
   {
      int color = AnsiCode.BACKGROUND_INTENSE_MIN;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(setCsiCode(color) + "pretty");
      String expected ="<span class=\"" +
            AnsiCode.clazzForBgColor(color) + "\">pretty</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testMaxAnsiIntenseBgColor()
   {
      int color = AnsiCode.BACKGROUND_INTENSE_MAX;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(AnsiCode.CSI + color + AnsiCode.SGR + "colors");
      String expected ="<span class=\"" +
            AnsiCode.clazzForBgColor(color) + "\">colors</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testResetAnsiIntenseForegroundColor()
   {
      int color = AnsiCode.FOREGROUND_INTENSE_MIN + 1;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(
            AnsiCode.CSI + color + AnsiCode.SGR + "Hello " +
            AnsiCode.CSI + AnsiCode.RESET_FOREGROUND + AnsiCode.SGR + "World");
      String expected =
            "<span class=\"" +
            AnsiCode.clazzForColor(color) + "\">Hello </span>" +
            "<span>World</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testResetAnsiIntenseBgColor()
   {
      int color = AnsiCode.BACKGROUND_INTENSE_MIN + 1;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(
            AnsiCode.CSI + color + AnsiCode.SGR + "Sunny " +
            AnsiCode.CSI + AnsiCode.RESET_BACKGROUND + AnsiCode.SGR + "Days");
      String expected = "<span class=\"" + AnsiCode.clazzForBgColor(color) +
            "\">Sunny </span><span>Days</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testAnsiInvertDefaultColors()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(inverseOn() + "Sunny Days");
      String expected = "<span class=\"xtermInvertColor xtermInvertBgColor\"" +
            ">Sunny Days</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testAnsiInvertRevertDefaultColors()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(
            inverseOn() + "Sunny " +
            inverseOff() + "Days");
      String expected = "<span class=\"xtermInvertColor xtermInvertBgColor\"" +
            ">Sunny </span><span>Days</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testAnsiInvertCustomBgColor()
   {
      int bgColor = AnsiCode.BackColorNum.GREEN;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(
            AnsiCode.CSI + bgColor + AnsiCode.SGR +
            inverseOn() + "Sunny Days");
      String expected = "<span class=\"xtermColor2 xtermInvertBgColor\"" +
            ">Sunny Days</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testAnsiInvertCustomFgColor()
   {
      int fgColor = AnsiCode.ForeColorNum.BLUE;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(
            AnsiCode.CSI + fgColor + AnsiCode.SGR +
            inverseOn() + "Sunny Days");
      String expected = "<span class=\"xtermInvertColor xtermBgColor4\"" +
            ">Sunny Days</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testAnsiForeground256Color()
   {
      int color = 120;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(setForegroundIndex(color) + "Hello World");
      String expected ="<span class=\"xtermColor120\">Hello World</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testAnsiBackground256Color()
   {
      int color = 213;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(setBackgroundIndex(color) + "Hello World");
      String expected ="<span class=\"xtermBgColor213\">Hello World</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testAnsi256Color()
   {
      int color = 65;
      int bgColor = 252;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(setForegroundIndex(color) + setBackgroundIndex(bgColor) + "Hello World");
      String expected ="<span class=\"xtermColor65 xtermBgColor252\">Hello World</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testAnsiInvert256Color()
   {
      int color = 61;
      int bgColor = 129;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(setForegroundIndex(color) + setBackgroundIndex(bgColor) +
            inverseOn() + "Hello World");
      String expected ="<span class=\"xtermColor129 xtermBgColor61\">Hello World</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testAnsiInvert256FgColor()
   {
      int color = 77;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(AnsiCode.CSI + AnsiCode.FOREGROUND_EXT + ";" +
            AnsiCode.EXT_BY_INDEX + ";" + color + AnsiCode.SGR +
            inverseOn() + "Hello World");
      String expected ="<span class=\"xtermInvertColor xtermBgColor77\">Hello World</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testAnsiInvert256BgColor()
   {
      int bgColor = 234;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(AnsiCode.CSI + AnsiCode.BACKGROUND_EXT + ";" +
            AnsiCode.EXT_BY_INDEX + ";" + bgColor + AnsiCode.SGR +
            inverseOn() + "Hello World");
      String expected ="<span class=\"xtermColor234 xtermInvertBgColor\">Hello World</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testAnsiComplexSequence1()
   {
      int fgColor = AnsiCode.ForeColorNum.GREEN;
      int bgColor = 230;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(
         AnsiCode.CSI + fgColor + ";" +
         AnsiCode.BACKGROUND_EXT + ";" + AnsiCode.EXT_BY_INDEX + ";" + bgColor + ";" +
         AnsiCode.BOLD + ";" + AnsiCode.UNDERLINE + ";" + AnsiCode.BOLD_BLURRED_OFF + ";" +
         AnsiCode.INVERSE_OFF + AnsiCode.SGR + "Hello World");
      String expected ="<span class=\"xtermColor2 xtermBgColor230 xtermUnderline\">Hello World</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testAnsiComplexSequence2()
   {
      int fgColor = AnsiCode.ForeColorNum.GREEN;
      int bgColor = 230;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(
         AnsiCode.CSI + fgColor + ";" +
         AnsiCode.BACKGROUND_EXT + ";" + AnsiCode.EXT_BY_INDEX + ";" + bgColor + ";" +
         AnsiCode.BOLD + ";" + AnsiCode.UNDERLINE + ";" + AnsiCode.BOLD_BLURRED_OFF + ";" +
         AnsiCode.INVERSE_OFF + AnsiCode.SGR + "Hello World",
         "existingStyle moreExisting");
      String expected ="<span class=\"existingStyle moreExisting xtermColor2 xtermBgColor230 xtermUnderline\">Hello World</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testAnsiUnsupportedRGBExtendedColorScheme()
   {
      // don't currently support specifying color via RGB, which is
      // ESC[38;2;r;g;bm or ESC[48;2;r;g;bm
      // in those cases want to ignore the RGB sequence and carry on
      // processing following codes
      int red = 123;
      int green = 231;
      int blue = 121;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(
         AnsiCode.CSI + AnsiCode.FOREGROUND_EXT + ";" + AnsiCode.EXT_BY_RGB + ";" +
         red + ";" + green + ";" + blue + ";" + AnsiCode.BOLD + AnsiCode.SGR +
         "Hello World");
      String expected ="<span class=\"xtermBold\">Hello World</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testAnsiMultiUnsupportedRGBExtendedColorScheme()
   {
      int red = 123;
      int green = 231;
      int blue = 121;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(
         AnsiCode.CSI + AnsiCode.BOLD + ";" +
         AnsiCode.FOREGROUND_EXT + ";" + AnsiCode.EXT_BY_RGB + ";" +
         red + ";" + green + ";" + blue + ";" +
         AnsiCode.UNDERLINE + ";" +
         AnsiCode.BACKGROUND_EXT + ";" + AnsiCode.EXT_BY_RGB + ";" +
         red + ";" + green + ";" + blue + ";" +
         AnsiCode.FOREGROUND_EXT + ";" + AnsiCode.EXT_BY_INDEX + ";" + 255 +
         AnsiCode.SGR +
         "Hello World");
      String expected ="<span class=\"xtermBold xtermUnderline xtermColor255\">Hello World</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testAnsiCodeSplitAcrossSubmits()
   {
      int color = AnsiCode.ForeColorNum.MAGENTA;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(AnsiCode.CSI + color);
      vc.submit(AnsiCode.SGR + "Hello");
      String expected ="<span class=\"" +
            AnsiCode.clazzForColor(color) + "\">Hello</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testAnsiCodeSplitAcrossSubmitsWithParentStyle()
   {
      int color = AnsiCode.ForeColorNum.MAGENTA;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(AnsiCode.CSI + color, "myStyle");
      vc.submit(AnsiCode.SGR + "Hello", "myStyle");
      String expected ="<span class=\"myStyle " +
            AnsiCode.clazzForColor(color) + "\">Hello</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testMultipleAnsiCodesSplitsAcrossSubmits()
   {
      int color = AnsiCode.ForeColorNum.MAGENTA;
      int bgColor = AnsiCode.BackColorNum.GREEN;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(AnsiCode.CSI + color);
      vc.submit(AnsiCode.SGR + AnsiCode.CSI);
      vc.submit(Integer.toString(bgColor));
      vc.submit(AnsiCode.SGR + "Hello");
      String expected ="<span class=\"" +
            AnsiCode.clazzForColor(color) + " " +
            AnsiCode.clazzForBgColor(bgColor) + "\">Hello</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testAnsiCodeAtEndOfSubmitCall()
   {
      int color = AnsiCode.ForeColorNum.MAGENTA;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(AnsiCode.CSI + color);
      vc.submit(AnsiCode.SGR);
      vc.submit("Hello");
      String expected ="<span class=\"" +
            AnsiCode.clazzForColor(color) + "\">Hello</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testSplitAnsiCodeFollowedByMoreTextSubmits()
   {
      int color = AnsiCode.ForeColorNum.MAGENTA;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(AnsiCode.CSI + color);
      vc.submit(AnsiCode.SGR);
      vc.submit("Hello");
      vc.submit(" World");
      String expected ="<span class=\"" +
            AnsiCode.clazzForColor(color) + "\">Hello World</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testAnsiCodeFollowedByMoreTextSubmits()
   {
      int color = AnsiCode.ForeColorNum.RED;
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(AnsiCode.CSI + color + AnsiCode.SGR, "someClazz");
      vc.submit("Hello World", "someClazz");
      String expected ="<span class=\"someClazz " +
            AnsiCode.clazzForColor(color) + "\">Hello World</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testAnsiComplexSequence1WithStripping()
   {
      int fgColor = AnsiCode.ForeColorNum.GREEN;
      int bgColor = 230;
      PreElement ele = Document.get().createPreElement();
      FakePrefs prefs = new FakePrefs();
      prefs.ansiMode_ = UserPrefs.ANSI_CONSOLE_MODE_STRIP;
      VirtualConsole vc = new VirtualConsole(ele, prefs);
      vc.submit(
         AnsiCode.CSI + fgColor + ";" +
         AnsiCode.BACKGROUND_EXT + ";" + AnsiCode.EXT_BY_INDEX + ";" + bgColor + ";" +
         AnsiCode.BOLD + ";" + AnsiCode.UNDERLINE + ";" + AnsiCode.BOLD_BLURRED_OFF + ";" +
         AnsiCode.INVERSE_OFF + AnsiCode.SGR + "Hello World", "myOriginalStyle");
      String expected ="<span class=\"myOriginalStyle\">Hello World</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testAnsiComplexSequence1WithPassThrough()
   {
      int fgColor = AnsiCode.ForeColorNum.GREEN;
      int bgColor = 230;
      PreElement ele = Document.get().createPreElement();
      FakePrefs prefs = new FakePrefs();
      prefs.ansiMode_ = UserPrefs.ANSI_CONSOLE_MODE_OFF;
      VirtualConsole vc = new VirtualConsole(ele, prefs);
      vc.submit(
         AnsiCode.CSI + fgColor + ";" +
         AnsiCode.BACKGROUND_EXT + ";" + AnsiCode.EXT_BY_INDEX + ";" + bgColor + ";" +
         AnsiCode.BOLD + ";" + AnsiCode.UNDERLINE + ";" + AnsiCode.BOLD_BLURRED_OFF + ";" +
         AnsiCode.INVERSE_OFF + AnsiCode.SGR + "Hello World", "myOriginalStyle");
      String expected ="<span class=\"myOriginalStyle\"><ESC>[32;48;5;230;1;4;22;27mHello World</span>";
      Assert.assertEquals(expected, AnsiCode.prettyPrint(ele.getInnerHTML()));
   }

   public void testMultipleAnsiCodesSplitsAcrossSubmitsWithStripping()
   {
      int color = AnsiCode.ForeColorNum.MAGENTA;
      int bgColor = AnsiCode.BackColorNum.GREEN;
      PreElement ele = Document.get().createPreElement();
      FakePrefs prefs = new FakePrefs();
      prefs.ansiMode_ = UserPrefs.ANSI_CONSOLE_MODE_STRIP;
      VirtualConsole vc = new VirtualConsole(ele, prefs);
      vc.submit(AnsiCode.CSI + color);
      vc.submit(AnsiCode.SGR + AnsiCode.CSI);
      vc.submit(Integer.toString(bgColor));
      vc.submit(AnsiCode.SGR + "Hello");
      String expected ="<span>Hello</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testAnsiCodeSplitAcrossSubmitsWithParentStyleWithPassthrough()
   {
      int color = AnsiCode.ForeColorNum.MAGENTA;
      PreElement ele = Document.get().createPreElement();
      FakePrefs prefs = new FakePrefs();
      prefs.ansiMode_ = UserPrefs.ANSI_CONSOLE_MODE_OFF;
      VirtualConsole vc = new VirtualConsole(ele, prefs);
      vc.submit(AnsiCode.CSI + color, "myStyle");
      vc.submit(AnsiCode.SGR + "Hello", "myStyle");
      String expected ="<span class=\"myStyle\"><ESC>[35mHello</span>";
      Assert.assertEquals(expected, AnsiCode.prettyPrint(ele.getInnerHTML()));
   }

   public void testMultipleCarriageReturn()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("hello world\r           \rgoodbye");
      Assert.assertEquals("<span>goodbye    </span>", ele.getInnerHTML());
      Assert.assertEquals("goodbye    ", vc.toString());
   }

   public void testNonDestructiveBackspace()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("hello world\b\b\b\b\b");
      Assert.assertEquals("<span>hello world</span>", ele.getInnerHTML());
      Assert.assertEquals("hello world", vc.toString());
   }

   public void testSingleSupportedANSICodes()
   {
      // https://github.com/rstudio/rstudio/issues/2248
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(AnsiCode.CSI + "?25lBuilding sites \342\200\246 " +
                AnsiCode.CSI + "?25h\r" + AnsiCode.CSI + "[K");
      Assert.assertEquals("<span>Building sites \342\200\246 </span>", ele.getInnerHTML());
      Assert.assertEquals("Building sites \342\200\246 ", vc.toString());
   }

   public void testMultipleUnknownANSICodes()
   {
      // https://github.com/rstudio/rstudio/issues/2248
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("We are " + AnsiCode.CSI + "?25lbuilding sites \342\200\246" +
                AnsiCode.CSI + "?25h\r" + AnsiCode.CSI + "[K");
      Assert.assertEquals("<span>We are building sites \342\200\246</span>", ele.getInnerHTML());
      Assert.assertEquals("We are building sites \342\200\246", vc.toString());
   }

   public void testMultiCarriageReturnsWithoutColorsMultiSubmits()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("123");
      vc.submit("x \r");
      vc.submit("456");
      vc.submit("x \r");
      vc.submit("789");
      Assert.assertEquals("<span>789x </span>", ele.getInnerHTML());
      Assert.assertEquals("789x ", vc.toString());
   }

   public void testMultiCarriageReturnWithoutColors()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("123" + "x \r" + "456" + "x \r" + "789");
      Assert.assertEquals("<span>789x </span>", ele.getInnerHTML());
      Assert.assertEquals("789x ", vc.toString());
   }

   public void testMultiCarriageReturnWithColors()
   {
      // https://github.com/rstudio/rstudio/issues/2387
      // cat(c(crayon::red("123"), "x \r", crayon::red("456"), "x \r", crayon::red("789\n")), sep = "")
      String red = AnsiCode.CSI + AnsiCode.ForeColorNum.RED + AnsiCode.SGR;
      String reset = AnsiCode.CSI + AnsiCode.RESET_FOREGROUND + AnsiCode.SGR;

      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit(red + "123" + reset + "x \r" +
                red + "456" + reset + "x \r" +
                red + "789");

      String expected =
            "<span class=\"" + AnsiCode.clazzForColor(AnsiCode.ForeColorNum.RED) +
            "\">789</span><span>x </span>";

      Assert.assertEquals(expected, ele.getInnerHTML());
      Assert.assertEquals("789x ", vc.toString());
   }


   public void testCRNoColorsIssue2665Part1()
   {
      // https://github.com/rstudio/rstudio/issues/2665
      // this checks the output without colors involved
      // cat("✔ xxx", "yyy", "xxx")
      // cat("\r")
      // cat("✔xxx", "yyy", "zzz")
      // cat("\n")

      // test writing over a previously written line (via \r) with
      // no colors involved, the second line shorter than the first,
      // ending with a \n
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("✔ xxx yyy xxx");
      vc.submit("\r");
      vc.submit("✔xxx yyy zzz");
      vc.submit("\n");

      String expected = "<span>✔xxx yyy zzzx\n</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
      Assert.assertEquals("✔xxx yyy zzzx\n", vc.toString());
   }

   public void testCRNoColorsIssue2665Part2()
   {
      // https://github.com/rstudio/rstudio/issues/2665
      // this checks the output without colors involved
      // cat("✔ xxx", "yyy", "xxx")
      // cat("\r")
      // cat("✔ xxx", "yyy", "zzz")
      // cat("\n")

      // test writing over a previously written line (via \r) with
      // no colors involved, and the second line the same length as
      // the first, ending with a \n
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("✔ xxx yyy xxx");
      vc.submit("\r");
      vc.submit("✔ xxx yyy zzz");
      vc.submit("\n");

      String expected = "<span>✔ xxx yyy zzz\n</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
      Assert.assertEquals("✔ xxx yyy zzz\n", vc.toString());
   }

   public void testCRColorsIssue2665Part1()
   {
      // https://github.com/rstudio/rstudio/issues/2665
      // cat("✔ xxx", crayon::blue("yyy"), "xxx")
      // cat("\r")
      // cat("✔xxx", crayon::red("yyy"), "zzz")
      // cat("\n")

      // test writing over a previously written line (via \r) with
      // colors involved, and the second line shorter than the first line,
      // ending with \n
      String red = AnsiCode.CSI + AnsiCode.ForeColorNum.RED + AnsiCode.SGR;
      String redCode = "\"" + AnsiCode.clazzForColor(AnsiCode.ForeColorNum.RED) + "\"";
      String blue = AnsiCode.CSI + AnsiCode.ForeColorNum.BLUE + AnsiCode.SGR;
      String blueCode = "\"" + AnsiCode.clazzForColor(AnsiCode.ForeColorNum.BLUE) + "\"";
      String reset = AnsiCode.CSI + AnsiCode.RESET_FOREGROUND + AnsiCode.SGR;

      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("✔ xxx " + blue + "yyy" + reset + " xxx");
      vc.submit("\r");
      vc.submit("✔xxx " + red + "yyy" + reset + " zzz");
      vc.submit("\n");

      String expected =
            "<span>✔xxx </span>" +
            "<span class=" + redCode + ">yyy</span>" +
            "<span class=" + blueCode + "></span>" +
            "<span></span><span> zzzx\n</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
      Assert.assertEquals("✔xxx yyy zzzx\n", vc.toString());
   }

   public void testProgressBar4777Part1()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);

      vc.submit("=>---------- 1.11 kB/s");
      vc.submit("\r");
      vc.submit("==>--------- 1.12 kB/s");
      vc.submit("\r");
      vc.submit("==>--------- 1.13 kB/s");
      vc.submit("\r");
      vc.submit("===>-------- 1.14 kB/s");
      vc.submit("\r");
      vc.submit("====>------- 1.15 kB/s");
      vc.submit("\r");
      vc.submit("====>-------- 1.2 kB/s");
      vc.submit("\r");
      vc.submit("======>----- 1.21 kB/s");
      vc.submit("\r");
      vc.submit("=======>---- 1.22 kB/s");
      vc.submit("\r");
      vc.submit("========>--- 1.23 kB/s");
      vc.submit("\r");
      vc.submit("=========>--- 1.3 kB/s");
      vc.submit("\r");
      vc.submit("==========>- 1.32 kB/s");
      vc.submit("\r");
      String lastLine = "===========> 1.33 kB/s";
      vc.submit(lastLine);

      String expected = "<span>===========&gt; 1.33 kB/s</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
      Assert.assertEquals(lastLine, vc.toString());
   }

   public void testProgressBar4777Part2()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);

      String color = "\033[32m";
      String restore = "\033[39m";

      vc.submit("=>---------- " + color + "1.11 kB/s " + restore);
      vc.submit("\r");
      vc.submit("==>--------- " + color + "1.12 kB/s " + restore);
      vc.submit("\r");
      vc.submit("==>--------- " + color + "1.13 kB/s " + restore);
      vc.submit("\r");
      vc.submit("===>-------- " + color + "1.14 kB/s " + restore);
      vc.submit("\r");
      vc.submit("====>------- " + color + "1.15 kB/s " + restore);
      vc.submit("\r");
      vc.submit("====>------- " + color + "1.20 kB/s " + restore);
      vc.submit("\r");
      vc.submit("======>----- " + color + "1.21 kB/s " + restore);
      vc.submit("\r");
      vc.submit("=======>---- " + color + "1.22 kB/s " + restore);
      vc.submit("\r");
      vc.submit("========>--- " + color + "1.23 kB/s " + restore);
      vc.submit("\r");
      vc.submit("=========>-- " + color + "1.30 kB/s " + restore);
      vc.submit("\r");
      vc.submit("==========>- " + color + "1.32 kB/s " + restore);
      vc.submit("\r");
      vc.submit("===========> " + color + "1.33 kB/s " + restore);

      Assert.assertTrue(true);
      String expected = "<span>===========&gt; </span><span class=\"xtermColor2\">1.33 kB/s </span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
      Assert.assertEquals("===========> 1.33 kB/s " , vc.toString());
   }

   public void testProgressBar4777Part3()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);

      String color = "\033[32m";
      String restore = "\033[39m";

      vc.submit("aa " + color + "11 " + restore);
      vc.submit("\r");
      vc.submit("bbb " + color + "2 " + restore);
      vc.submit("\r");
      vc.submit("cc " + color + "33 " + restore);

      String expected = "<span>cc </span><span class=\"xtermColor2\"></span><span class=\"xtermColor2\">33 </span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
      Assert.assertEquals("cc 33 ", vc.toString());
   }

   public void testScreenReaderOffNoTextNotCaptured()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("Hello World", "someclass", false, true);
      Assert.assertEquals(vc.getNewText(), "");
   }

   public void testScreenReaderOnTextCaptured()
   {
      PreElement ele = Document.get().createPreElement();
      FakePrefs prefs = new FakePrefs();
      prefs.screenReaderEnabled_ = true;
      VirtualConsole vc = new VirtualConsole(ele, prefs);
      String text = "Hello World\nHow are you?";
      vc.submit(text, "someclass", false, true);
      String newText = vc.getNewText();
      Assert.assertEquals(newText, text);
   }

   public void testBackground255Carryover6092()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);

      String testInput =
            setForegroundIndex(232) + setBackgroundIndex(255) + "one" +
            setCsiCode(AnsiCode.RESET_BACKGROUND) + setCsiCode(AnsiCode.RESET_FOREGROUND) + " two";

      vc.submit(testInput);
      String expected = "<span class=\"xtermColor232 xtermBgColor255\">one</span><span> two</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
      Assert.assertEquals("one two", vc.toString());
   }

   public void testForeground255Carryover6092()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);

      String testInput =
            setForegroundIndex(255) + setBackgroundIndex(232) + "one" +
                  setCsiCode(AnsiCode.RESET_BACKGROUND) + setCsiCode(AnsiCode.RESET_FOREGROUND) + " two";

      vc.submit(testInput);
      String expected = "<span class=\"xtermColor255 xtermBgColor232\">one</span><span> two</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
      Assert.assertEquals("one two", vc.toString());
   }

   public void testInverseFgHandling()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);

      String testInput = inverseOn() + setCsiCode(AnsiCode.ForeColorNum.RED) +
         "This is visible in both " + setCsiCode(AnsiCode.RESET_FOREGROUND) +
         "This is visible only in the terminal" + inverseOff();

      vc.submit(testInput);
      String expected = "<span class=\"xtermInvertColor xtermBgColor1\">This is visible in both </span>" +
                        "<span class=\"xtermInvertColor xtermInvertBgColor\">This is visible only in the terminal</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
      Assert.assertEquals("This is visible in both This is visible only in the terminal", vc.toString());
   }

   public void testInverseBgHandling()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);

      String testInput = inverseOn() + setCsiCode(103 /*bright yellow background*/) +
         "Part One " + setCsiCode(AnsiCode.RESET_BACKGROUND) +
         "Other Part" + inverseOff();

      vc.submit(testInput);
      String expected = "<span class=\"xtermInvertBgColor xtermColor11\">Part One </span>" +
         "<span class=\"xtermInvertBgColor xtermInvertColor\">Other Part</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
      Assert.assertEquals("Part One Other Part", vc.toString());
   }

   public void testInverseFgBgHandling()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);

      String testInput = inverseOn() + setCsiCode(103 /*bright yellow background*/) + setCsiCode(AnsiCode.ForeColorNum.RED) +
         "Part One " + setCsiCode(AnsiCode.RESET_BACKGROUND) + setCsiCode(AnsiCode.RESET_FOREGROUND) +
         "Other Part" + inverseOff();

      vc.submit(testInput);
      String expected = "<span class=\"xtermColor11 xtermBgColor1\">Part One </span>" +
         "<span class=\"xtermInvertColor xtermInvertBgColor\">Other Part</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
      Assert.assertEquals("Part One Other Part", vc.toString());
   }

   public void testInverse256FgHandling()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);

      String testInput = inverseOn() + setForegroundIndex(196 /*red*/) +
         "Inverted with red background " + setCsiCode(AnsiCode.RESET_FOREGROUND) +
         "Inverted with default background" + inverseOff();

      vc.submit(testInput);
      String expected = "<span class=\"xtermInvertColor xtermBgColor196\">Inverted with red background </span>" +
         "<span class=\"xtermInvertColor xtermInvertBgColor\">Inverted with default background</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
      Assert.assertEquals("Inverted with red background Inverted with default background", vc.toString());
   }

   public void testInverse256FgBgHandling()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);

      String testInput = inverseOn() + setForegroundIndex(196 /*red*/) + setBackgroundIndex(228 /*yellow*/) +
         "Inverted with red background yellow foreground " + setCsiCode(AnsiCode.RESET_FOREGROUND) +
         setCsiCode(AnsiCode.RESET_BACKGROUND) + "Inverted with default colors" + inverseOff();

      vc.submit(testInput);
      String expected = "<span class=\"xtermBgColor196 xtermColor228\">Inverted with red background yellow foreground </span>" +
         "<span class=\"xtermInvertBgColor xtermInvertColor\">Inverted with default colors</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
      Assert.assertEquals("Inverted with red background yellow foreground Inverted with default colors", vc.toString());
   }

   public void testCarriageReturnPartialEndOverwrite()
   {
      // Case where a span is partially overwritten after a carriage-return
      // and thus must be moved to a new position; it was being moved to an incorrect location
      // causing output to be lost.
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);

      String testInput = "⠹ [ \033[32mPASS\033[39m x632 \033[31mFAIL\033[39m x16 \033[35mWARN\033[39m x4" +
         "\r" +
         "\033[32m✓\033[39m |   4       | reporter-zzz\033[36m [\033[0m";

      vc.submit(testInput);
      String expected = "<span class=\"xtermColor2\">✓</span><span> |   4       | reporter-zzz</span>" +
         "<span></span><span class=\"xtermColor6\"> [</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
      Assert.assertEquals("✓ |   4       | reporter-zzz [", vc.toString());
   }

   public void testBlurred()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      String testInput = "\033[2mtext\033[22m";
      vc.submit(testInput);
      String expected = "<span class=\"xtermBlur\">text</span>";
      Assert.assertEquals(expected, ele.getInnerHTML());
   }

   public void testHyperlinkNoParams()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      String testInput = "\u001b]8;;http://www.example.com\7text\u001b]8;;\7";
      vc.submit(testInput);
      Assert.assertTrue(ele.getInnerHTML().matches("^<a class=\".*\">text</a>"));
   }

   public void testHyperlinkNoParamsColorOutside()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      String testInput = "\033[31m\u001b]8;;http://www.example.com\7text\u001b]8;;\7\033[39m";
      vc.submit(testInput);
      Assert.assertTrue(ele.getInnerHTML().matches("^<a class=\".*xtermColor1.*\">text</a>"));
   }

   public void testHyperlinkNoParamsColorInside()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      String testInput = "\u001b]8;;http://www.example.com\7\033[31mtext\033[39m\u001b]8;;\7";
      vc.submit(testInput);
      Assert.assertTrue(ele.getInnerHTML().matches("^<a class=\".*xtermColor1.*\">text</a>"));
   }

   public void testHyperlinkWithParams()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      String testInput = "\u001b]8;name1=value1:name2=value2;http://www.example.com\7text\u001b]8;;\7";
      vc.submit(testInput);
      Assert.assertTrue(ele.getInnerHTML().matches("^<a class=\".*\">text</a>"));
   }

   public void testIssue9846()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("i Some intermediate step with a \033[32mfield\033[39m-----------------------\r+++++++++++++++++++++++++++++++++++++\r\033[33m!\033[39m An alert message which is long enough\ni Some intermediate step with a \033[32mfield\033[39m\n");
      Assert.assertEquals("<span class=\"xtermColor3\">!</span><span> An alert message which is long enough</span><span>---------------------\ni Some intermediate step with a </span><span class=\"xtermColor2\">field</span><span>\n</span>", ele.getInnerHTML());
   }
   
   public void testCsiCursorMovement()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("Hello world!\033[10DLL\033[4CRL");
      Assert.assertEquals(ele.getInnerText(), "HeLLo woRLd!");
   }
   
   public void testEraseInLineMode0Default()
   {
      // \033[K with no param defaults to mode 0 (erase from cursor to end of line)
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("Hello World\033[5D\033[K");
      Assert.assertEquals("Hello ", vc.toString());
      Assert.assertEquals("<span>Hello </span>", ele.getInnerHTML());
   }

   public void testEraseInLineMode0Explicit()
   {
      // \033[0K is the same as \033[K
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("Hello World\033[5D\033[0K");
      Assert.assertEquals("Hello ", vc.toString());
      Assert.assertEquals("<span>Hello </span>", ele.getInnerHTML());
   }

   public void testEraseInLineMode0WithColors()
   {
      // Erase colored text from cursor to end of line
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      String red = AnsiCode.CSI + AnsiCode.ForeColorNum.RED + AnsiCode.SGR;
      String reset = AnsiCode.CSI + AnsiCode.RESET_FOREGROUND + AnsiCode.SGR;
      vc.submit("Hello " + red + "World" + reset + "\033[5D\033[K");
      Assert.assertEquals("Hello ", vc.toString());
      Assert.assertEquals("<span>Hello </span>", ele.getInnerHTML());
   }

   public void testEraseInLineMode0StraddlingColorRange()
   {
      // Erase from the middle of a colored range (optimization path)
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      String red = AnsiCode.CSI + AnsiCode.ForeColorNum.RED + AnsiCode.SGR;
      String reset = AnsiCode.CSI + AnsiCode.RESET_FOREGROUND + AnsiCode.SGR;
      // "He" plain, "llo World" red -- cursor back 5 lands inside the red range
      vc.submit("He" + red + "llo World" + reset + "\033[5D\033[K");
      Assert.assertEquals("He" + "llo ", vc.toString());
      Assert.assertEquals(
         "<span>He</span>" +
         "<span class=\"xtermColor1\">llo </span>",
         ele.getInnerHTML());
   }

   public void testEraseInLineMode1()
   {
      // \033[1K erases from beginning of line to cursor (inclusive)
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("Hello World\033[5D\033[1K");
      Assert.assertEquals("       orld", vc.toString());
      Assert.assertEquals("<span>       orld</span>", ele.getInnerHTML());
   }

   public void testEraseInLineMode2()
   {
      // \033[2K erases the entire line
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("Hello World\033[5D\033[2K");
      Assert.assertEquals("           ", vc.toString());
      Assert.assertEquals("<span>           </span>", ele.getInnerHTML());
   }

   public void testEraseInLineMultiLine()
   {
      // EL should not cross newline boundaries
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("Line1\nHello World\033[5D\033[K");
      Assert.assertEquals("Line1\nHello ", vc.toString());
      Assert.assertEquals("<span>Line1\nHello </span>", ele.getInnerHTML());
   }

   public void testEraseInLineMode0MiddleLine()
   {
      // Mode 0 on a non-trailing line erases via the space-overwrite path.
      // Cursor up (CSI A) reaches line 2, landing on the space before "World"
      // (column preserved from "Line3"); erase-to-EOL then clears " World".
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("Line1\nHello World\nLine3\033[1A\033[K");
      Assert.assertEquals("Line1\nHello      \nLine3", vc.toString());
      Assert.assertEquals("<span>Line1\nHello      \nLine3</span>", ele.getInnerHTML());
   }

   public void testEraseInLineMode1AtEnd()
   {
      // Mode 1 with cursor at end of line erases the entire line
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("Hello World\033[1K");
      Assert.assertEquals("           ", vc.toString());
      Assert.assertEquals("<span>           </span>", ele.getInnerHTML());
   }

   public void testEraseInLineMode2MultiLine()
   {
      // Mode 2 on a middle line of a multi-line buffer. Cursor up (CSI A)
      // lands on line 2; mode 2 erases the whole line regardless of column.
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("Line1\nHello World\nLine3\033[1A\033[2K");
      Assert.assertEquals("Line1\n           \nLine3", vc.toString());
      Assert.assertEquals("<span>Line1\n           \nLine3</span>", ele.getInnerHTML());
   }

   public void testEraseInLineThenWrite()
   {
      // Verify cursor is not corrupted after erase -- writing resumes at correct position
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("Hello World\033[5D\033[K");
      vc.submit("Earth");
      Assert.assertEquals("Hello Earth", vc.toString());
      Assert.assertEquals("<span>Hello Earth</span>", ele.getInnerHTML());
   }

   public void testEraseInLineProgressBar()
   {
      // Realistic progress bar pattern: \r + text + \033[K
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("Progress: 50% done!!!");
      vc.submit("\rProgress: 100%\033[K");
      Assert.assertEquals("Progress: 100%", vc.toString());
      Assert.assertEquals("<span>Progress: 100%</span>", ele.getInnerHTML());
   }

   public void testCsiCursorUpRewritesPreviousLine()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("line1\nline2\u001b[1A\rLINE1");
      Assert.assertEquals("LINE1\nline2", vc.toString());
   }

   public void testCsiCursorUpPreservesColumn()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("abcdef\nghijkl\u001b[3D\u001b[1AXY");
      Assert.assertEquals("abcXYf\nghijkl", vc.toString());
   }

   public void testCsiCursorUpClampsAtTop()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("only line\u001b[5A\rOVER");
      Assert.assertEquals("OVER line", vc.toString());
   }

   public void testCsiCursorUpDefaultCount()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("line1\nline2\u001b[A\rLINE1");
      Assert.assertEquals("LINE1\nline2", vc.toString());
   }

   public void testCsiCursorDownMovesToNextLine()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("line1\nline2\nline3\u001b[2A\u001b[1B\rLINE2");
      Assert.assertEquals("line1\nLINE2\nline3", vc.toString());
   }

   public void testCsiCursorDownClampsAtBottom()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("only line\u001b[5B!");
      Assert.assertEquals("only line!", vc.toString());
   }

   public void testCsiMultiProgressBarUpdate()
   {
      // Mirrors the output cli emits when rendering two progress bars on
      // separate lines: each update moves up with \033[1A, redraws bar 1,
      // moves down via \n, then redraws bar 2. Without CSI A support the
      // second bar would clobber the first.
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("\rDownloads  10%\u001b[K\n\rProcessing 10%\u001b[K\r");
      vc.submit("\u001b[1A\rDownloads  50%\u001b[K\n\rProcessing 50%\u001b[K\r");
      Assert.assertEquals("Downloads  50%\nProcessing 50%", vc.toString());
   }

   public void testCsiCursorUpWriteLongerThanLinePreservesNewline()
   {
      // Regression guard: a write that begins on an earlier line via CSI A
      // and is longer than that line's existing content must not run past
      // the trailing newline and corrupt the line below it. The buffer
      // newline stays where the writer expects, with the upper line
      // extended to fit the new content.
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("Downloads 10%\nProcessing 10%");
      vc.submit("\u001b[1A\rDownloads NEW 50%\u001b[K\n\rProcessing 50%\u001b[K\r");
      Assert.assertEquals("Downloads NEW 50%\nProcessing 50%", vc.toString());
   }

   public void testCsiCursorUpOntoEmptyLinePreservesNewline()
   {
      // Regression guard for the cursor-on-newline case: CSI A from a
      // following line can land the cursor directly on an empty line's
      // terminating newline (lineStart == lineEnd). A subsequent non-newline
      // write must not consume that newline and merge the lines below.
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("Header\n\nFooter");
      vc.submit("\u001b[1AX");
      Assert.assertEquals("Header\nX\nFooter", vc.toString());
   }

   public void testCsiCursorDownDefaultCount()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("line1\nline2\nline3\u001b[2A\u001b[B\rLINE2");
      Assert.assertEquals("line1\nLINE2\nline3", vc.toString());
   }

   public void testCsiCursorUpClampsColumnToLineLength()
   {
      // Pin the absolute-position behavior without a \r chaser: when the
      // source column exceeds the target line's length, the cursor lands
      // on the line's last character (not past it) so subsequent ops like
      // \r and eraseInLine stay on the intended line. See maxCursorColumn.
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("short\nlongerline\u001b[1AX");
      Assert.assertEquals("shorX\nlongerline", vc.toString());
   }

   public void testCsiCursorUpPreservesColorWhenRewriting()
   {
      // Verifies extendLineBefore keeps the DOM in sync: a CSI A overwrite
      // that extends a colored line must produce HTML whose color span
      // still covers the original characters and is followed by a new,
      // uncolored span for the appended content.
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("\u001b[32mbar1\u001b[39m\nbar2");
      vc.submit("\u001b[1A\rbar1 EXTRA\u001b[K");
      Assert.assertEquals("bar1 EXTRA\nbar2", vc.toString());
      // The DOM must still contain the colored span (the green bar1 prefix)
      // and the \n separator must survive intact.
      String html = ele.getInnerHTML();
      Assert.assertTrue("expected xtermColor2 span, got: " + html,
         html.indexOf("xtermColor2") != -1);
      Assert.assertTrue("expected newline preserved in DOM, got: " + html,
         html.indexOf("\n") != -1);
   }

   public void testIncompleteAnsiCodes()
   {
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      String text = "\u001b[34mhello\u001b[0m";
      for (int i = 0, n = text.length(); i < n; i++)
         vc.submit(text.substring(i, i + 1));
      
      Assert.assertEquals(ele.getInnerText(), "hello");
   }

   public void testCsiCursorToColumnDefault()
   {
      // \033[G with no param defaults to column 1 (start of line)
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("abc\033[GX");
      Assert.assertEquals("Xbc", vc.toString());
   }

   public void testCsiCursorToColumnExplicit()
   {
      // \033[7G moves to column 7 (1-based), i.e. the 'W' in "Hello World"
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("Hello World\033[7GXYZ");
      Assert.assertEquals("Hello XYZld", vc.toString());
   }

   public void testCsiCursorToColumnClampsBeyondLine()
   {
      // A column past the end of the (unterminated) line clamps to the end
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("abc\033[99GX");
      Assert.assertEquals("abcX", vc.toString());
   }

   public void testCsiCursorToColumnMultiLine()
   {
      // CHA operates on the line the cursor is currently on
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("line1\nline2\033[3GX");
      Assert.assertEquals("line1\nliXe2", vc.toString());
   }

   public void testCsiCursorToColumnEndOfNonFinalLine()
   {
      // CHA must be able to address the column just past the content of a
      // terminated (non-final) line. After moving up to "abc", \033[4G lands
      // after 'c' (not on it), so the write appends instead of clobbering
      // 'c' or merging into the line below.
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("abc\ndef\033[1A\033[4GZ");
      Assert.assertEquals("abcZ\ndef", vc.toString());
   }

   public void testCsiCursorToColumnEndOfLineThenCarriageReturn()
   {
      // A cursor left on a terminated line's '\n' by CHA must still belong to
      // that line: \r returns to the start of "abc", not down to "def".
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("abc\ndef\033[1A\033[4G\rX");
      Assert.assertEquals("Xbc\ndef", vc.toString());
   }

   public void testCsiCursorToColumnEndOfLineThenEraseLine()
   {
      // Likewise CSI K must operate on the line whose '\n' the cursor sits on.
      // \033[2K erases "abc" (leaving spaces), not "def".
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("abc\ndef\033[1A\033[4G\033[2K");
      Assert.assertEquals("   \ndef", vc.toString());
   }

   public void testCsiCursorBackClampsAtLineStart()
   {
      // CUB must not cross the preceding '\n': moving left 4 from the end of
      // "def" stops at the start of "def", so the write overwrites 'd'.
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("abc\ndef\033[4DX");
      Assert.assertEquals("abc\nXef", vc.toString());
   }

   public void testCsiCursorBackThenCarriageReturn()
   {
      // After CUB clamps at the start of "def", \r stays on "def" (it does not
      // resolve to the previous line). Regression guard for cursor-backward
      // interacting with the newline-resolution in carriageReturn().
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("abc\ndef\033[4D\rX");
      Assert.assertEquals("abc\nXef", vc.toString());
   }

   public void testCsiCursorBackThenEraseLine()
   {
      // Same for CSI K: it erases "def" (the line CUB stayed on), not "abc".
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("abc\ndef\033[4D\033[2K");
      Assert.assertEquals("abc\n   ", vc.toString());
   }

   public void testBackspaceClampsAtLineStart()
   {
      // Plain backspace must also stay within the current line.
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("abc\ndef\b\b\b\bX");
      Assert.assertEquals("abc\nXef", vc.toString());
   }

   public void testCsiCursorForwardClampsAtLineEnd()
   {
      // CUF must not cross into the next line: moving right past the end of
      // "abc" stops at its end, so the write appends to "abc".
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("abc\ndef\033[1A\033[9CX");
      Assert.assertEquals("abcX\ndef", vc.toString());
   }

   public void testCsiCursorToColumnProgressBar()
   {
      // Realistic progress pattern: redraw the line via CHA + erase-to-EOL
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("Downloading 45%");
      vc.submit("\033[GDownloading 100%\033[K");
      Assert.assertEquals("Downloading 100%", vc.toString());
      Assert.assertEquals("<span>Downloading 100%</span>", ele.getInnerHTML());
   }

   public void testCsiCursorNextLine()
   {
      // \033[2A up to the first line, then \033[1E to the start of line 2
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("aaa\nbbb\nccc\033[2A\033[1EX");
      Assert.assertEquals("aaa\nXbb\nccc", vc.toString());
   }

   public void testCsiCursorNextLineDefaultCount()
   {
      // \033[E with no param defaults to moving down one line
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("aaa\nbbb\nccc\033[2A\033[EX");
      Assert.assertEquals("aaa\nXbb\nccc", vc.toString());
   }

   public void testCsiCursorNextLineClampsAtBottom()
   {
      // CNL on the last line cannot move down but still resets to column 0
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("abc\033[3EX");
      Assert.assertEquals("Xbc", vc.toString());
   }

   public void testCsiCursorPreviousLine()
   {
      // \033[1F moves to the start of the line above
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("aaa\nbbb\nccc\033[1FX");
      Assert.assertEquals("aaa\nXbb\nccc", vc.toString());
   }

   public void testCsiCursorPreviousLineDefaultCount()
   {
      // \033[F with no param defaults to moving up one line
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("aaa\nbbb\nccc\033[FX");
      Assert.assertEquals("aaa\nXbb\nccc", vc.toString());
   }

   public void testCsiCursorPreviousLineClampsAtTop()
   {
      // CPL on the first line cannot move up but still resets to column 0
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("abc\033[3FX");
      Assert.assertEquals("Xbc", vc.toString());
   }

   public void testCsiCursorPreviousLineResetsColumn()
   {
      // Unlike CSI A (which preserves the column), CPL snaps to column 0
      PreElement ele = Document.get().createPreElement();
      VirtualConsole vc = getVC(ele);
      vc.submit("abcdef\nghijkl\033[1FX");
      Assert.assertEquals("Xbcdef\nghijkl", vc.toString());
   }
}
