chromium/chrome/test/data/webui/chromeos/enterprise_reporting/enterprise_reporting_test.ts

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview Unittests for the chrome://enterprise-reporting element.
 */

import {EnterpriseReportingBrowserProxy} from 'chrome://enterprise-reporting/browser_proxy.js';
import {ErpHistoryData, ErpHistoryEvent, ErpHistoryEventParameter, PageCallbackRouter, PageHandlerRemote, PageRemote} from 'chrome://enterprise-reporting/enterprise_reporting.mojom-webui.js';
import {ReportingHistoryElement} from 'chrome://enterprise-reporting/reporting_history.js';
import {assertEquals} from 'chrome://webui-test/chai_assert.js';
import {flushTasks} from 'chrome://webui-test/polymer_test_util.js';
import {TestMock} from 'chrome://webui-test/test_mock.js';

suite('enterprise_reporting', function() {
  let reportingHistoryElement: ReportingHistoryElement;
  let callbackRouterRemote: PageRemote;
  let handler: TestMock<PageHandlerRemote>;

  // Number of cells in HTML row representing a single event, produced by
  // `chrome://enterprise_reporting/reporting_history.ts`:
  // `call`, `parameters`, `status` and `timestamp`.
  const numCellsInRow = 4;

  type Constructor<T> = new (...args: any[]) => T;
  type Installer<T> = (instance: T) => void;

  function installMock<T extends object>(
      clazz: Constructor<T>, installer?: Installer<T>): TestMock<T> {
    installer = installer ||
        (clazz as unknown as {setInstance: Installer<T>}).setInstance;
    const mock = TestMock.fromClass(clazz);
    installer!(mock);
    return mock;
  }

  function getHistoryTable(): HTMLElement {
    return reportingHistoryElement.$.body;
  }

  function timestampToString(timestampSeconds: bigint): string {
    // Multiply by 1000 since the constructor expects milliseconds, but the
    // timestamps are in seconds.
    const timestamp: Date = new Date(Number(timestampSeconds) * 1000);

    // For today's timestamp, show time only.
    const now: Date = new Date();
    if (timestamp.getDate() === now.getDate()) {
      return timestamp.toLocaleTimeString();
    }

    // Otherwise show whole timestamp.
    return timestamp.toLocaleString();
  }

  function parametersMatch(
      expectedParameters: ErpHistoryEventParameter[],
      parameters: HTMLTableCellElement) {
    const lines = parameters.querySelectorAll<HTMLLIElement>('li');
    assertEquals(expectedParameters.length, lines.length);
    lines.forEach((line, index) => {
      assertEquals(
          (expectedParameters[index]!.name + ': ' +
           expectedParameters[index]!.value),
          line.innerText);
    });
  }

  function cellMatches(
      expectedEvent: ErpHistoryEvent, row: HTMLTableRowElement) {
    // Enumerate and match cells in the row.
    const cells = row.querySelectorAll<HTMLTableCellElement>('td');
    assertEquals(numCellsInRow, cells.length);
    assertEquals(expectedEvent.call, cells[0]!.innerText);
    parametersMatch(expectedEvent.parameters, cells[1]!);
    assertEquals(expectedEvent.status, cells[2]!.innerText);
    assertEquals(timestampToString(expectedEvent.time), cells[3]!.innerText);
  }

  function rowsMatchInReverse(
      expectedHistory: ErpHistoryData, rows: NodeListOf<HTMLTableRowElement>) {
    // Enumerate and match all rows.
    assertEquals(expectedHistory.events.length, rows.length);
    rows.forEach((row, index) => {
      cellMatches(
          expectedHistory.events[expectedHistory.events.length - index - 1]!,
          row);
    });
  }

  function emptyMatch(rows: NodeListOf<HTMLTableRowElement>) {
    // Check for empty case.
    assertEquals(1, rows.length);
    // Enumerate the cells.
    const cells = rows[0]!.querySelectorAll<HTMLTableCellElement>('td');
    assertEquals(numCellsInRow, cells.length);
    assertEquals('No events', cells[0]!.innerText);
    assertEquals('', cells[1]!.innerText);
    assertEquals('', cells[2]!.innerText);
    assertEquals('', cells[3]!.innerText);
  }

  async function setInitialSettings() {
    callbackRouterRemote.setErpHistoryData({events: []});
    handler.setResultFor('getDebugState', Promise.resolve({state: true}));
    handler.setResultFor('getErpHistoryData', Promise.resolve({events: []}));
    reportingHistoryElement =
        document.createElement(ReportingHistoryElement.is);
    document.body.appendChild(reportingHistoryElement);
    await handler.whenCalled('getDebugState');
    await handler.whenCalled('getErpHistoryData');
  }

  setup(() => {
    handler = installMock(
        PageHandlerRemote,
        (mock: PageHandlerRemote) =>
            EnterpriseReportingBrowserProxy.createInstanceForTest(
                mock, new PageCallbackRouter()));
    callbackRouterRemote = EnterpriseReportingBrowserProxy.getInstance()
                               .callbackRouter.$.bindNewPipeAndPassRemote();
  });

  teardown(async () => {
    await callbackRouterRemote.$.flushForTesting();
    await flushTasks();
    reportingHistoryElement.remove();
  });

  test('create History Element and see that it is empty', async () => {
    await setInitialSettings();
    await callbackRouterRemote.$.flushForTesting();
    await handler.whenCalled('getErpHistoryData');
    await flushTasks();

    const table = getHistoryTable();
    emptyMatch(table.querySelectorAll<HTMLTableRowElement>('tr'));
  });

  test('create History Element and update it with data', async () => {
    await setInitialSettings();
    await callbackRouterRemote.$.flushForTesting();

    const event1: ErpHistoryEvent = {
      call: 'call',
      parameters: [{name: 'seq_id', value: '345'} as ErpHistoryEventParameter],
      status: 'OK',
      time: BigInt(123456789),
    };
    const event2: ErpHistoryEvent = {
      call: 'recall',
      parameters: [
        {name: 'seq_id', value: '123'} as ErpHistoryEventParameter,
        {name: 'count', value: '777'} as ErpHistoryEventParameter,
      ],
      status: 'Error',
      time: BigInt(987654321),
    };
    const event3: ErpHistoryEvent = {
      call: 'upload',
      parameters: [
        {name: 'seq_id', value: '123'} as ErpHistoryEventParameter,
        {name: 'seq_id', value: '456'} as ErpHistoryEventParameter,
        {name: 'seq_id', value: '789'} as ErpHistoryEventParameter,
      ],
      status: 'Success',
      time: BigInt(555666777),
    };
    const history: ErpHistoryData = {events: [event1, event2, event3]};
    callbackRouterRemote.setErpHistoryData(history);
    await handler.whenCalled('getErpHistoryData');
    await flushTasks();

    const table = getHistoryTable();
    rowsMatchInReverse(
        history, table.querySelectorAll<HTMLTableRowElement>('tr'));
  });
});