[logging] Migrate LogRow component to lit

LogRow still uses the global styling. Once we migrate LogList, we can
inline the css.

Change-Id: Ib5bae5fb98b23985c6df7cd1687e7eb228678a29
Reviewed-on: https://fuchsia-review.googlesource.com/c/vscode-plugins/+/753863
Reviewed-by: Miguel Flores <miguelfrde@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/media/webview-logging.css b/media/webview-logging.css
index f07c533..6dd643f 100644
--- a/media/webview-logging.css
+++ b/media/webview-logging.css
@@ -12,7 +12,7 @@
   flex-direction: column;
 }
 
-th {
+#logs-table-header>div {
   background-color: var(--vscode-editor-background);
   border-bottom: 2px solid var(--vscode-panel-border);
   padding-bottom: 5px;
@@ -20,26 +20,40 @@
   top: 0;
 }
 
-td,
-th {
+#logs-table-header,
+log-view-row {
+  display: table-row-group;
+}
+
+log-view-row[hidden] {
+  display: none;
+}
+
+log-view-row>div {
+  display: table-row;
+}
+
+.log-list-cell,
+#logs-table-header>div {
   overflow: clip;
   text-align: left;
   text-overflow: ellipsis;
   vertical-align: top;
   white-space: nowrap;
+  display: table-cell;
 }
 
 td#moniker {
   direction: rtl;
 }
 
-table[wrap-log-text] .msg-cell,
-table:not([wrap-log-text]) .msg-cell:hover {
+#log-list[wrap-log-text] .msg-cell,
+#log-list:not([wrap-log-text]) .msg-cell:hover {
   text-overflow: clip;
   white-space: normal;
 }
 
-table:not([wrap-log-text]) .msg-cell:hover {
+#log-list:not([wrap-log-text]) .msg-cell:hover {
   background: var(--vscode-list-hoverBackground);
 }
 
@@ -50,26 +64,27 @@
 }
 
 #log-list {
+  display: table;
   table-layout: fixed;
   width: 100%;
   word-wrap: normal;
 }
 
-.log-entry {
+log-view-row {
   font-family: var(--vscode-editor-font-family);
   font-size: var(--vscode-editor-font-size);
   font-weight: var(--vscode-editor-font-weight);
 }
 
-.log-entry[data-severity="warn"] {
+log-view-row div[data-severity="warn"] {
   color: var(--vscode-list-warningForeground);
 }
 
-.log-entry[data-severity="error"] {
+log-view-row div[data-severity="error"] {
   color: var(--vscode-list-errorForeground);
 }
 
-.log-entry[data-moniker="<VSCode>"] {
+log-view-row div[data-moniker="<VSCode>"] {
   line-height: 75%;
   opacity: 70%;
 }
diff --git a/webviews/logging/components/log_header.ts b/webviews/logging/components/log_header.ts
index 438601f..4a028dc 100644
--- a/webviews/logging/components/log_header.ts
+++ b/webviews/logging/components/log_header.ts
@@ -16,7 +16,7 @@
     this.tableHeader = this.initTableHeader();
     for (let i = 0; i < keys.length; i++) {
       const header = keys[i] as LogField;
-      const headerCell = document.createElement('th');
+      const headerCell = document.createElement('div');
       this.maxWidths[header] = 0;
       headerCell.id = header;
       headerCell.style.width = fields[header].width;
@@ -44,8 +44,8 @@
    *
    * @return the header element.
    */
-  private initTableHeader(): HTMLTableRowElement {
-    const tableHeader = document.createElement('tr');
+  private initTableHeader(): HTMLDivElement {
+    const tableHeader = document.createElement('div');
     tableHeader.id = 'logs-table-header';
     return tableHeader;
   }
diff --git a/webviews/logging/components/log_list.ts b/webviews/logging/components/log_list.ts
index 892208e..cb2f47c 100644
--- a/webviews/logging/components/log_list.ts
+++ b/webviews/logging/components/log_list.ts
@@ -13,7 +13,7 @@
 export const WRAP_LOG_TEXT_ATTR = 'wrap-log-text';
 
 export class LogList {
-  private logList: HTMLTableElement;
+  private logList: HTMLDivElement;
   private logHeader: LogHeader;
   private hasPreviousLog: boolean = false;
   private maxWidths: Array<number>;
@@ -35,7 +35,7 @@
         }
       } else {
         for (const element of logRows) {
-          if (filter.accepts(element)) {
+          if (filter.accepts(element.children[0])) {
             element.removeAttribute('hidden');
           } else {
             element.setAttribute('hidden', '');
@@ -88,8 +88,8 @@
    *
    * @return the log list element.
    */
-  private initLogList(): HTMLTableElement {
-    let logList = document.createElement('table');
+  private initLogList(): HTMLDivElement {
+    let logList = document.createElement('div');
     logList.appendChild(this.logHeader.element);
     logList.id = 'log-list';
     return logList;
@@ -107,8 +107,7 @@
   }
 
   private appendLogElement(log: FfxLogData) {
-    const newRow = new LogRow(log, this.hasPreviousLog);
-    const element = newRow.element;
+    const element = new LogRow(log, this.hasPreviousLog);
     if (!this.filtersAllow(element)) {
       element.setAttribute('hidden', '');
     }
diff --git a/webviews/logging/components/log_row.ts b/webviews/logging/components/log_row.ts
index e951969..28f5515 100644
--- a/webviews/logging/components/log_row.ts
+++ b/webviews/logging/components/log_row.ts
@@ -3,6 +3,9 @@
 // found in the LICENSE file.
 
 import * as constant from '../src/constants';
+import {ChildPart, html, LitElement} from 'lit';
+import {Directive, directive} from 'lit/directive.js';
+import {customElement, property, state} from 'lit/decorators.js';
 import { LogField } from '../src/fields';
 import {
   formatLogPayload, formatMalformedLog,
@@ -10,12 +13,33 @@
 } from '../src/format_log_text';
 import { FfxEventData, FfxLogData, LogData } from '../src/log_data';
 
-export class LogRow {
-  private logRow: HTMLElement;
-  private symbolized: string | undefined;
+class AttributeSetter extends Directive {
+  update(part: ChildPart, [attributes]: [any]) {
+    const domElement = (part.parentNode as Element);
+    for (const attr in attributes) {
+      const value = attributes[attr];
+      domElement.setAttribute(`data-${attr}`, value);
+    }
+    return this.render(attributes);
+  }
 
-  constructor(private log: FfxLogData, hasPreviousLog: boolean) {
-    this.logRow = this.initLogRow();
+  render(_attributes: any) {
+    return '';
+  }
+}
+@customElement('log-view-row')
+export class LogRow extends LitElement {
+  @property({attribute: true, type: Object})
+  public log: FfxLogData | undefined = undefined;
+
+  @state()
+  private symbolized: string | undefined;
+  private fields!: Record<string, any>;
+  private dataAttributes!: Record<string, any>;
+
+  constructor(log: FfxLogData, hasPreviousLog: boolean) {
+    super();
+    this.log = log;
     if (this.log.data.TargetLog !== undefined) {
       this.formatRow(this.log.data.TargetLog);
     } else if (this.log.data.FfxEvent !== undefined) {
@@ -30,48 +54,29 @@
     }
   }
 
-  /**
-   * Returns the core element.
-   */
-  get element() {
-    return this.logRow;
+  createRenderRoot() {
+    return this;
   }
 
-  /**
-   * Creates log row element containing useful metadata in `data-*` attributes.
-   *
-   * @param attributes the map of attribute name to value.
-   * @returns the log row html element.
-   */
-  private createLogRowAttributes(attributes: Record<string, any>) {
-    for (const attributeName in attributes) {
-      this.logRow.setAttribute(`data-${attributeName}`, attributes[attributeName]);
-    }
+  render() {
+    const attributeSetter = directive(AttributeSetter);
+    return html`
+      <div class="log-entry">
+        ${attributeSetter(this.dataAttributes)}
+        ${Object.keys(constant.LOGS_HEADERS).map(this.htmlForFieldKey.bind(this))}
+      </div>`;
   }
 
-  /**
-   * Creates the inner html of the fields.
-   *
-   * @param fields fields of the log element.
-   * @returns the inner html string with present fields filled.
-   */
-  private createLogInnerHtml(fields: Record<string, any>) {
-    let fieldKey: LogField;
-    for (fieldKey in constant.LOGS_HEADERS) {
-      let cell = document.createElement('td');
-      let content = '';
-      if (fields[fieldKey]) {
-        content = fields[fieldKey];
-      }
-      if (fieldKey === 'message') {
-        cell.classList.add('msg-cell');
-      } else {
-        cell.title = content;
-      }
-      cell.innerText = content;
-      cell.setAttribute('id', fieldKey);
-      this.logRow.appendChild(cell);
-    }
+  private htmlForFieldKey(fieldKey: string) {
+    const content = this.fields[fieldKey] ?? '';
+    return html`
+      <div
+        id="${fieldKey}"
+        title="${fieldKey === 'message' ? '' : content}"
+        class="log-list-cell${fieldKey === 'message' ? ' msg-cell' : ''}"
+      >
+        ${content}
+      </div>`;
   }
 
   /**
@@ -79,9 +84,9 @@
    */
   private formatFfxEventRow(event: FfxEventData, hasPreviousLog: boolean) {
     const fields =
-      { 'moniker': constant.FFX_MONIKER, 'message': messageForEvent(event, hasPreviousLog) };
-    this.createLogRowAttributes(fields);
-    this.createLogInnerHtml(fields);
+      {'moniker': constant.FFX_MONIKER, 'message': messageForEvent(event, hasPreviousLog)};
+    this.dataAttributes = fields;
+    this.fields = fields;
   }
 
   /**
@@ -89,9 +94,9 @@
    * @param malformedLog the malformed log to render as a message.
    */
   private formatMalformedRow(malformedLog: string) {
-    const fields = { 'message': formatMalformedLog(malformedLog) };
-    this.createLogRowAttributes(fields);
-    this.createLogInnerHtml(fields);
+    const fields = {'message': formatMalformedLog(malformedLog)};
+    this.dataAttributes = fields;
+    this.fields = fields;
   }
 
   /**
@@ -99,9 +104,9 @@
    * @param message  the message to display.
    */
   private formatViewerEventRow(message: string) {
-    const fields = { message, moniker: constant.VSCODE_SYNTHETIC_MONIKER };
-    this.createLogRowAttributes(fields);
-    this.createLogInnerHtml(fields);
+    const fields = {message, moniker: constant.VSCODE_SYNTHETIC_MONIKER};
+    this.dataAttributes = fields;
+    this.fields = fields;
   }
 
   /**
@@ -109,15 +114,15 @@
    */
   private formatRow(log: LogData) {
     let fields = {} as Record<string, any>;
-    const attributes = createAttributes(log);
-    this.createLogRowAttributes(attributes);
+    this.dataAttributes = createAttributes(log);
 
     let header: LogField;
     for (header in constant.LOGS_HEADERS) {
       let field = header.toLowerCase();
       fields[field] = this.getLogCell(log, field);
     }
-    this.createLogInnerHtml(fields);
+
+    this.fields = fields;
   }
 
   /**
@@ -151,15 +156,6 @@
         return '';
     }
   }
-
-  /**
-   * Initialize LogRow element.
-   */
-  private initLogRow(): HTMLElement {
-    let logRow = document.createElement('tr');
-    logRow.classList.add('log-entry');
-    return logRow;
-  }
 }
 
 const COMPONENT_URL_REGEX =
diff --git a/webviews/logging/test/filter.test.ts b/webviews/logging/test/filter.test.ts
index 6fc3991..233c48b 100644
--- a/webviews/logging/test/filter.test.ts
+++ b/webviews/logging/test/filter.test.ts
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 import chai from 'chai'; // not esm
+import {LitElement} from 'lit';
 import { LogRow } from '../components/log_row';
 import { FilterExpression, OrExpression, parseFilter } from '../src/filter';
 
@@ -11,9 +12,11 @@
 });
 
 describe('filtering', () => {
+  let testLitElements: LitElement[] = [];
   let testElements: HTMLElement[] = [];
-  before(() => {
-    testElements = [
+
+  before(async () => {
+    testLitElements = [
       new LogRow(
         {
           data: {
@@ -47,7 +50,7 @@
           },
           timestamp: 12345,
           version: 1
-        }, false).element,
+        }, false),
       new LogRow( //2
         {
           data: {
@@ -83,7 +86,7 @@
           },
           timestamp: 12345,
           version: 1
-        }, false).element,
+        }, false),
       new LogRow(
         {
           data: {
@@ -119,8 +122,20 @@
           },
           timestamp: 12345,
           version: 1
-        }, false).element,
+        }, false),
     ];
+
+    for (const index in testLitElements) {
+      document.body.appendChild(testLitElements[index]);
+      await testLitElements[index].updateComplete;
+      testElements.push(testLitElements[index].children[0] as HTMLElement);
+    }
+  });
+
+  after(() => {
+    for (const index in testLitElements) {
+      testLitElements[index].remove();
+    }
   });
 
   describe('FilterExpression', () => {
diff --git a/webviews/logging/test/log_list.test.ts b/webviews/logging/test/log_list.test.ts
index d50d5d0..bd38503 100644
--- a/webviews/logging/test/log_list.test.ts
+++ b/webviews/logging/test/log_list.test.ts
@@ -10,6 +10,7 @@
 import { Filter } from '../src/filter';
 import { LogList, WRAP_LOG_TEXT_ATTR } from '../components/log_list';
 import { State } from '../src/state';
+import {LitElement} from 'lit';
 
 before(function () {
   chai.should();
@@ -49,28 +50,31 @@
   });
 
   describe('#addLog', () => {
-    it('appends a log', () => {
+    it('appends a log', async () => {
       const view = new LogList(state);
       const logsList = view.element;
+      document.body.appendChild(logsList);
 
       logsList.children.length.should.equal(1);
       logsList.children[0].id.should.equal('logs-table-header');
       view.addLog(logDataForTest('core/foo'));
       logsList.children.length.should.equal(2);
 
-      const logLine = logsList.children[1] as HTMLElement;
-      logLine.should.have.class('log-entry');
-      logLine.should.have.attr('data-moniker', 'core/foo');
-      logLine.should.have.attr('data-pid', '123');
-      logLine.should.have.attr('data-tid', '456');
-      logLine.hidden.should.be.false;
+      const logLine = logsList.children[1] as LitElement;
+      await logLine.updateComplete;
+      logLine.children[0].should.have.class('log-entry');
+      logLine.children[0].should.have.attr('data-moniker', 'core/foo');
+      logLine.children[0].should.have.attr('data-pid', '123');
+      logLine.children[0].should.have.attr('data-tid', '456');
+      (logLine.children[0] as HTMLElement).hidden.should.be.false;
 
       const testFields = ['000000.12', '123', '456', 'core/foo', 'my_tag', 'Info', 'msg'];
       let field: keyof typeof testFields;
       for (field in testFields) {
-        const logField = logLine.children[field] as HTMLElement;
+        const logField = logLine.children[0].children[field] as HTMLElement;
         logField.innerText.should.equal(testFields[field]);
       }
+      logsList.remove();
     });
 
 
@@ -92,44 +96,50 @@
       logLine.hidden.should.be.true;
     });
 
-    it('handles ffx events', () => {
+    it('handles ffx events', async () => {
       const view = new LogList(state);
       let moniker = constant.FFX_MONIKER;
       const msg = 'Logger lost connection to target. Retrying...';
       view.addLog(ffxEventForTest('TargetDisconnected'));
 
       const logsList = view.element;
+      document.body.appendChild(logsList);
       logsList.children.length.should.equal(2);
       logsList.children[0].id.should.equal('logs-table-header');
 
-      const logLine = logsList.children[1] as HTMLElement;
+      const logLine = logsList.children[1] as LitElement;
+      await logLine.updateComplete;
 
-      logLine.children.length.should.equal(7);
-      logLine.should.have.class('log-entry');
-      logLine.should.have.attr('data-moniker', '<ffx>');
-      logLine.hidden.should.be.false;
+      logLine.children[0].children.length.should.equal(7);
+      logLine.children[0].should.have.class('log-entry');
+      logLine.children[0].should.have.attr('data-moniker', '<ffx>');
+      (logLine.children[0] as HTMLElement).hidden.should.be.false;
 
-      let monikerEl = logLine.children[3] as HTMLElement;
-      let msgEl = logLine.children[6] as HTMLElement;
+      let monikerEl = logLine.children[0].children[3] as HTMLElement;
+      let msgEl = logLine.children[0].children[6] as HTMLElement;
       monikerEl.innerText.should.equal(moniker);
       msgEl.innerText.should.equal(msg);
+      logsList.remove();
     });
 
-    it('handles malformed logs', () => {
+    it('handles malformed logs', async () => {
       const view = new LogList(state);
       const msg = 'Malformed target log: oh no something went wrong';
       view.addLog(malformedLogForTest('oh no something went wrong'));
 
       const logsList = view.element;
+      document.body.appendChild(logsList);
       logsList.children.length.should.equal(2);
       logsList.children[0].id.should.equal('logs-table-header');
 
-      const logLine = logsList.children[1] as HTMLElement;
-      logLine.children.length.should.equal(7);
-      logLine.should.have.class('log-entry');
-      logLine.hidden.should.be.false;
-      let msgEl = logLine.children[6] as HTMLElement;
+      const logLine = logsList.children[1] as LitElement;
+      await logLine.updateComplete;
+      logLine.children[0].children.length.should.equal(7);
+      logLine.children[0].should.have.class('log-entry');
+      (logLine.children[0] as HTMLElement).hidden.should.be.false;
+      let msgEl = logLine.children[0].children[6] as HTMLElement;
       msgEl.innerText.should.equal(msg);
+      logsList.remove();
     });
 
     it('detects user hovering logs', () => {
diff --git a/webviews/logging/test/log_row.test.ts b/webviews/logging/test/log_row.test.ts
index efad7f7..8141bac 100644
--- a/webviews/logging/test/log_row.test.ts
+++ b/webviews/logging/test/log_row.test.ts
@@ -47,7 +47,7 @@
 
 describe('LogRow', () => {
   describe('#constructor', () => {
-    it('adds title to to non-message cells', () => {
+    it('adds title to to non-message cells', async () => {
       const logRow = new LogRow(logDataForTest({
         root: {
           message: {
@@ -56,19 +56,22 @@
           keys: null,
           printf: null,
         }
-      }), false).element;
+      }), false);
+      document.body.appendChild(logRow);
+      await logRow.updateComplete;
       const testFields = ['000012.35', '123', '456', 'core/foo', 'bar,baz', 'Info', 'hello vscode'];
       let field: keyof typeof testFields;
       for (field in testFields) {
         if (testFields[field] === 'hello vscode') {
           return;
         }
-        const logField = logRow.children[field] as HTMLElement;
+        const logField = logRow.children[0].children[field] as HTMLElement;
         logField.title.should.equal(testFields[field]);
       }
+      logRow.remove();
     });
 
-    it('formats target logs', () => {
+    it('formats target logs', async () => {
       const logRow = new LogRow(logDataForTest({
         root: {
           message: {
@@ -77,16 +80,19 @@
           keys: null,
           printf: null,
         }
-      }), false).element;
+      }), false);
+      document.body.appendChild(logRow);
+      await logRow.updateComplete;
       const testFields = ['000012.35', '123', '456', 'core/foo', 'bar,baz', 'Info', 'hello vscode'];
       let field: keyof typeof testFields;
       for (field in testFields) {
-        const logField = logRow.children[field] as HTMLElement;
+        const logField = logRow.children[0].children[field] as HTMLElement;
         logField.innerText.should.equal(testFields[field]);
       }
+      logRow.remove();
     });
 
-    it('formats structured logs', () => {
+    it('formats structured logs', async () => {
       const logRow = new LogRow(logDataForTest({
         root: {
           message: {
@@ -98,18 +104,21 @@
           },
           printf: null,
         }
-      }), false).element;
+      }), false);
+      document.body.appendChild(logRow);
+      await logRow.updateComplete;
       const testFields =
         ['000012.35', '123', '456', 'core/foo', 'bar,baz',
           'Info', 'hello vscodeos=fuchsia number=1'];
       let field: keyof typeof testFields;
       for (field in testFields) {
-        const logField = logRow.children[field] as HTMLElement;
+        const logField = logRow.children[0].children[field] as HTMLElement;
         logField.innerText.should.equal(testFields[field]);
       }
+      logRow.remove();
     });
 
-    it('formats printf logs', () => {
+    it('formats printf logs', async () => {
       const logRow = new LogRow(logDataForTest({
         root: {
           message: null,
@@ -119,17 +128,20 @@
             args: ['Fuchsia', 1]
           },
         }
-      }), false).element;
+      }), false);
+      document.body.appendChild(logRow);
+      await logRow.updateComplete;
       const testFields =
         ['000012.35', '123', '456', 'core/foo', 'bar,baz', 'Info', '%s is #%d args=[Fuchsia, 1]'];
       let field: keyof typeof testFields;
       for (field in testFields) {
-        const logField = logRow.children[field] as HTMLElement;
+        const logField = logRow.children[0].children[field] as HTMLElement;
         logField.innerText.should.equal(testFields[field]);
       }
+      logRow.remove();
     });
 
-    it('formats symbolized log', () => {
+    it('formats symbolized log', async () => {
       const logRow = new LogRow(symbolizedDataForTest({
         root: {
           message: {
@@ -138,54 +150,69 @@
           keys: null,
           printf: null,
         }
-      }), false).element;
+      }), false);
+      document.body.appendChild(logRow);
+      await logRow.updateComplete;
       const testFields =
         ['000000.12', '123', '456', 'core/foo', 'my_tag', 'Info', 'symbolized'];
       let field: keyof typeof testFields;
       for (field in testFields) {
-        const logField = logRow.children[field] as HTMLElement;
+        const logField = logRow.children[0].children[field] as HTMLElement;
         logField.innerText.should.equal(testFields[field]);
       }
+      logRow.remove();
     });
 
-    it('formats logging started events', () => {
-      let logRow = new LogRow(ffxEventForTest('LoggingStarted'), false).element;
+    it('formats logging started events', async () => {
+      let logRow = new LogRow(ffxEventForTest('LoggingStarted'), false);
+      document.body.appendChild(logRow);
+      await logRow.updateComplete;
       let moniker = constant.FFX_MONIKER;
       let msg = messageForEvent('LoggingStarted', false);
-      let monikerEl = logRow.children[3] as HTMLElement;
-      let msgEl = logRow.children[6] as HTMLElement;
+      let monikerEl = logRow.children[0].children[3] as HTMLElement;
+      let msgEl = logRow.children[0].children[6] as HTMLElement;
 
       monikerEl.innerText.should.equal(moniker);
       msgEl.innerText.should.equal(msg);
+      logRow.remove();
 
-      logRow = new LogRow(ffxEventForTest('LoggingStarted'), true).element;
+      logRow = new LogRow(ffxEventForTest('LoggingStarted'), true);
+      document.body.appendChild(logRow);
+      await logRow.updateComplete;
       msg = messageForEvent('LoggingStarted', true);
-      monikerEl = logRow.children[3] as HTMLElement;
-      msgEl = logRow.children[6] as HTMLElement;
+      monikerEl = logRow.children[0].children[3] as HTMLElement;
+      msgEl = logRow.children[0].children[6] as HTMLElement;
 
       monikerEl.innerText.should.equal(moniker);
       msgEl.innerText.should.equal(msg);
+      logRow.remove();
     });
 
-    it('formats target disconnected events', () => {
-      const logRow = new LogRow(ffxEventForTest('TargetDisconnected'), false).element;
+    it('formats target disconnected events', async () => {
+      const logRow = new LogRow(ffxEventForTest('TargetDisconnected'), false);
+      document.body.appendChild(logRow);
+      await logRow.updateComplete;
       let moniker = constant.FFX_MONIKER;
       let msg = messageForEvent('TargetDisconnected', true);
-      let monikerEl = logRow.children[3] as HTMLElement;
-      let msgEl = logRow.children[6] as HTMLElement;
+      let monikerEl = logRow.children[0].children[3] as HTMLElement;
+      let msgEl = logRow.children[0].children[6] as HTMLElement;
 
       monikerEl.innerText.should.equal(moniker);
       msgEl.innerText.should.equal(msg);
+      logRow.remove();
     });
 
-    it('formats malformed log', () => {
-      const logRow = new LogRow(malformedLogForTest('hello world'), false).element;
+    it('formats malformed log', async () => {
+      const logRow = new LogRow(malformedLogForTest('hello world'), false);
+      document.body.appendChild(logRow);
+      await logRow.updateComplete;
       let msg = 'Malformed target log: hello world';
-      let msgEl = logRow.children[6] as HTMLElement;
+      let msgEl = logRow.children[0].children[6] as HTMLElement;
       msgEl.innerText.should.equal(msg);
+      logRow.remove();
     });
 
-    it('formats viewer synthesized messages', () => {
+    it('formats viewer synthesized messages', async () => {
       const msg = 'Logs are cool';
       const data = {
         data: {
@@ -195,11 +222,14 @@
         timestamp: 0,
         version: 0
       };
-      const logRow = new LogRow(data, false).element;
-      const monikerEl = logRow.children[3] as HTMLElement;
-      const msgEl = logRow.children[6] as HTMLElement;
+      const logRow = new LogRow(data, false);
+      document.body.appendChild(logRow);
+      await logRow.updateComplete;
+      const monikerEl = logRow.children[0].children[3] as HTMLElement;
+      const msgEl = logRow.children[0].children[6] as HTMLElement;
       monikerEl.innerText.should.equal(constant.VSCODE_SYNTHETIC_MONIKER);
       msgEl.innerText.should.equal(msg);
+      logRow.remove();
     });
   });
 });