chromium/chrome/test/data/webui/extensions/activity_log_stream_test.ts

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

/** @fileoverview Suite of tests for activity-log-stream. */

import 'chrome://extensions/extensions.js';

import type {ActivityLogStreamElement} from 'chrome://extensions/extensions.js';
import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {assertEquals, assertTrue} from 'chrome://webui-test/chai_assert.js';

import {TestService} from './test_service.js';
import {testVisible} from './test_util.js';

suite('ExtensionsActivityLogStreamTest', function() {
  /**
   * Backing extension id, same id as the one in createExtensionInfo
   */
  const EXTENSION_ID: string = 'a'.repeat(32);

  const activity1 = {
    extensionId: EXTENSION_ID,
    activityType: chrome.activityLogPrivate.ExtensionActivityType.API_CALL,
    time: 1550101623113,
    args: JSON.stringify([null]),
    apiCall: 'testAPI.testMethod',
  };

  const activity2 = {
    extensionId: EXTENSION_ID,
    activityType: chrome.activityLogPrivate.ExtensionActivityType.DOM_EVENT,
    time: 1550101623116,
    args: JSON.stringify(['testArg']),
    apiCall: 'testAPI.DOMMethod',
    pageUrl: 'testUrl',
  };

  const contentScriptActivity = {
    extensionId: EXTENSION_ID,
    activityType:
        chrome.activityLogPrivate.ExtensionActivityType.CONTENT_SCRIPT,
    time: 1550101623115,
    args: JSON.stringify(['script1.js', 'script2.js']),
    apiCall: '',
  };

  /**
   * Extension activityLogStream created before each test.
   */
  let activityLogStream: ActivityLogStreamElement;
  let proxyDelegate: TestService;
  let boundTestVisible: (selector: string, expectedVisible: boolean) => void;

  // Initialize an extension activity log item before each test.
  setup(function() {
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    proxyDelegate = new TestService();

    activityLogStream = document.createElement('activity-log-stream');

    activityLogStream.extensionId = EXTENSION_ID;
    activityLogStream.delegate = proxyDelegate;
    boundTestVisible = testVisible.bind(null, activityLogStream);

    document.body.appendChild(activityLogStream);
  });

  teardown(function() {
    activityLogStream.remove();
  });

  // Returns a list of visible stream items. The not([hidden]) selector is
  // needed for iron-list as it reuses components but hides them when not in
  // use.
  function getStreamItems(): NodeListOf<HTMLElement> {
    return activityLogStream.shadowRoot!.querySelectorAll<HTMLElement>(
        'activity-log-stream-item:not([hidden])');
  }

  test('button toggles stream on/off', function() {
    flush();

    // Stream should be on when element is first attached to the DOM.
    boundTestVisible('.activity-subpage-header', true);
    boundTestVisible('#empty-stream-message', true);
    boundTestVisible('#stream-started-message', true);

    activityLogStream.shadowRoot!
        .querySelector<HTMLElement>('#toggle-stream-button')!.click();
    boundTestVisible('#stream-stopped-message', true);
  });

  test(
      'new activity events are only shown while the stream is started',
      function() {
        flush();
        proxyDelegate.getOnExtensionActivity().callListeners(activity1);

        flush();
        // One event coming in. Since the stream is on, we should be able to see
        // it.
        let streamItems = getStreamItems();
        assertEquals(1, streamItems.length);

        // Pause the stream.
        activityLogStream.shadowRoot!
            .querySelector<HTMLElement>('#toggle-stream-button')!.click();
        proxyDelegate.getOnExtensionActivity().callListeners(
            contentScriptActivity);

        flush();
        // One event was fired but the stream was paused, we should still see
        // only one item.
        streamItems = getStreamItems();
        assertEquals(1, streamItems.length);

        // Resume the stream.
        activityLogStream.shadowRoot!
            .querySelector<HTMLElement>('#toggle-stream-button')!.click();
        proxyDelegate.getOnExtensionActivity().callListeners(activity2);

        flush();
        streamItems = getStreamItems();
        assertEquals(2, streamItems.length);

        assertEquals(
            streamItems[0]!.shadowRoot!
                .querySelector<HTMLElement>('#activity-name')!.innerText!,
            'testAPI.testMethod');
        assertEquals(
            streamItems[1]!.shadowRoot!
                .querySelector<HTMLElement>('#activity-name')!.innerText!,
            'testAPI.DOMMethod');
      });

  test('activities shown match search query', function() {
    flush();
    boundTestVisible('#empty-stream-message', true);

    proxyDelegate.getOnExtensionActivity().callListeners(activity1);
    proxyDelegate.getOnExtensionActivity().callListeners(activity2);

    flush();
    assertEquals(2, getStreamItems().length);

    const search =
        activityLogStream.shadowRoot!.querySelector('cr-search-field');
    assertTrue(!!search);

    // Search for the apiCall of |activity1|.
    search!.setValue('testMethod');
    flush();

    const filteredStreamItems = getStreamItems();
    assertEquals(1, getStreamItems().length);
    assertEquals(
        filteredStreamItems[0]!.shadowRoot!
            .querySelector<HTMLElement>('#activity-name')!.innerText!,
        'testAPI.testMethod');

    // search again, expect none
    search!.setValue('not expecting any activities to match');
    flush();

    assertEquals(0, getStreamItems().length);
    boundTestVisible('#empty-stream-message', false);
    boundTestVisible('#empty-search-message', true);

    // Another activity comes in while the stream is listening but search
    // returns no results.
    proxyDelegate.getOnExtensionActivity().callListeners(contentScriptActivity);

    search!.shadowRoot!.querySelector<HTMLElement>('#clearSearch')!.click();
    flush();

    // We expect 4 activities to appear as |contentScriptActivity| (which is
    // split into 2 items) should be processed and stored in the stream
    // regardless of the search input.
    assertEquals(4, getStreamItems().length);
  });

  test('content script events are split by content script names', function() {
    proxyDelegate.getOnExtensionActivity().callListeners(contentScriptActivity);

    flush();
    const streamItems = getStreamItems();
    assertEquals(2, streamItems.length);

    // We should see two items: one for every script called.
    assertEquals(
        streamItems[0]!.shadowRoot!
            .querySelector<HTMLElement>('#activity-name')!.innerText!,
        'script1.js');
    assertEquals(
        streamItems[1]!.shadowRoot!
            .querySelector<HTMLElement>('#activity-name')!.innerText!,
        'script2.js');
  });

  test('clicking on clear button clears the activity log stream', function() {
    proxyDelegate.getOnExtensionActivity().callListeners(activity1);

    flush();
    assertEquals(1, getStreamItems().length);
    boundTestVisible('.activity-table-headings', true);
    activityLogStream.shadowRoot!
        .querySelector<HTMLElement>('.clear-activities-button')!.click();

    flush();
    assertEquals(0, getStreamItems().length);
    boundTestVisible('.activity-table-headings', false);
  });
});