chromium/chrome/test/data/webui/support_tool/support_tool_test.ts

// Copyright 2021 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 the Support Tool UI elements. It will be
 * executed by support_tool_browsertest.js.
 */

import 'chrome://support-tool/support_tool.js';
import 'chrome://support-tool/url_generator.js';

import type {CrButtonElement} from 'chrome://resources/cr_elements/cr_button/cr_button.js';
import type {CrCheckboxElement} from 'chrome://resources/cr_elements/cr_checkbox/cr_checkbox.js';
import type {CrInputElement} from 'chrome://resources/cr_elements/cr_input/cr_input.js';
import type {CrToastElement} from 'chrome://resources/cr_elements/cr_toast/cr_toast.js';
import {webUIListenerCallback} from 'chrome://resources/js/cr.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import type {BrowserProxy, DataCollectorItem, IssueDetails, PiiDataItem, SupportTokenGenerationResult} from 'chrome://support-tool/browser_proxy.js';
import {BrowserProxyImpl} from 'chrome://support-tool/browser_proxy.js';
import type {ScreenshotElement} from 'chrome://support-tool/screenshot.js';
import type {DataExportResult, SupportToolElement} from 'chrome://support-tool/support_tool.js';
import {SupportToolPageIndex} from 'chrome://support-tool/support_tool.js';
import type {UrlGeneratorElement} from 'chrome://support-tool/url_generator.js';
import {assertEquals, assertFalse, assertNotEquals, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {track} from 'chrome://webui-test/mouse_mock_interactions.js';
import {waitAfterNextRender} from 'chrome://webui-test/polymer_test_util.js';
import {TestBrowserProxy} from 'chrome://webui-test/test_browser_proxy.js';

const SCREENSHOT_BASE64: string =
    'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgMCAgMDAwME' +
    'AwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT' +
    '/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB' +
    'QUFBQUFBQUFBQUFBQUFBT/wAARCABkAGQDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAA' +
    'AAAAAj/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFgEBAQEAAAAAAAAAAAAAAAAAAAcJ/8QA' +
    'FBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AnQBDGqYAAAAAAAAAAAAAAAAAAAA' +
    'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
    'AAAAD/2Q==';

const EMAIL_ADDRESSES: string[] =
    ['[email protected]', '[email protected]'];

const DATA_COLLECTORS: DataCollectorItem[] = [
  {name: 'data collector 1', isIncluded: false, protoEnum: 1},
  {name: 'data collector 2', isIncluded: true, protoEnum: 2},
  {name: 'data collector 3', isIncluded: false, protoEnum: 3},
];

const ALL_DATA_COLLECTORS: DataCollectorItem[] = [
  {name: 'data collector 1', isIncluded: false, protoEnum: 1},
  {name: 'data collector 2', isIncluded: false, protoEnum: 2},
  {name: 'data collector 3', isIncluded: false, protoEnum: 3},
  {name: 'data collector 4', isIncluded: false, protoEnum: 4},
  {name: 'data collector 5', isIncluded: false, protoEnum: 5},
];

const PII_ITEMS: PiiDataItem[] = [
  {
    piiTypeDescription: 'IP Address',
    piiType: 0,
    detectedData: ['255.255.155.2', '255.255.155.255', '172.11.5.5'],
    count: 3,
    keep: false,
    expandDetails: true,
  },
  {
    piiTypeDescription: 'Hash',
    piiType: 1,
    detectedData: ['27540283740a0897ab7c8de0f809add2bacde78f'],
    count: 1,
    keep: false,
    expandDetails: true,
  },
  {
    piiTypeDescription: 'URL',
    piiType: 2,
    detectedData: [
      'chrome://resources/f?user=bar',
      'chrome-extension://nkoccljplnhpfnfiajclkommnmllphnl/foobar.js?bar=x',
      'http://tets.com',
    ],
    count: 3,
    keep: false,
    expandDetails: true,
  },
];

/**
 * A test version of SupportToolBrowserProxy.
 * Provides helper methods for allowing tests to know when a method was called,
 * as well as specifying mock responses.
 */
class TestSupportToolBrowserProxy extends TestBrowserProxy implements
    BrowserProxy {
  private supportTokenGenerationResult_: SupportTokenGenerationResult = {
    success: false,
    token: '',
    errorMessage: '',
  };

  constructor() {
    super([
      'takeScreenshot',
      'getEmailAddresses',
      'getDataCollectors',
      'startDataCollection',
      'cancelDataCollection',
      'startDataExport',
      'showExportedDataInFolder',
      'getAllDataCollectors',
      'generateCustomizedUrl',
      'generateSupportToken',
    ]);
  }

  takeScreenshot() {
    this.methodCalled('takeScreenshot');
  }

  getEmailAddresses() {
    this.methodCalled('getEmailAddresses');
    // We don't return EMAIL_ADDRESSES directly since we don't want the caller
    // to be able to modify the const array as it's possible in Typescript to
    // change the values of the contents of const arrays.
    return Promise.resolve(Array.from(EMAIL_ADDRESSES));
  }

  getDataCollectors() {
    this.methodCalled('getDataCollectors');
    return Promise.resolve(DATA_COLLECTORS);
  }

  getAllDataCollectors() {
    this.methodCalled('getAllDataCollectors');
    return Promise.resolve(ALL_DATA_COLLECTORS);
  }

  startDataCollection(
      issueDetails: IssueDetails, selectedDataCollectors: DataCollectorItem[],
      screenshot: string) {
    this.methodCalled(
        'startDataCollection', issueDetails, selectedDataCollectors,
        screenshot);
    // Return result with success for testing.
    const result = {success: true, errorMessage: ''};
    return Promise.resolve(result);
  }

  cancelDataCollection() {
    this.methodCalled('cancelDataCollection');
  }

  startDataExport(piiDataItems: PiiDataItem[]) {
    this.methodCalled('startDataExport', [piiDataItems]);
  }

  showExportedDataInFolder() {
    this.methodCalled('showExportedDataInFolder');
  }

  setSupportTokenGenerationResult(result: SupportTokenGenerationResult) {
    this.supportTokenGenerationResult_ = result;
  }

  // Returns this.supportTokenGenerationResult_ as response. Please call
  // this.setSupportTokenGenerationResult() before using this function in tests.
  generateCustomizedUrl(caseId: string, dataCollectors: DataCollectorItem[]) {
    this.methodCalled('generateCustomizedUrl', caseId, dataCollectors);
    return Promise.resolve(this.supportTokenGenerationResult_);
  }

  // Returns this.supportTokenGenerationResult_ as response. Please call
  // this.setSupportTokenGenerationResult() before using this function in tests.
  generateSupportToken(dataCollectors: DataCollectorItem[]) {
    this.methodCalled('generateSupportToken', dataCollectors);
    return Promise.resolve(this.supportTokenGenerationResult_);
  }
}

// Waits until `toast` is opened. Waits for 3 seconds and polls every second by
// default. Resolves to `true` if toast is opened. Returns error otherwise.
async function waitForToastOpened(
    toast: CrToastElement, intervalMs: number = 100,
    timeoutMs: number = 301): Promise<NonNullable<boolean>> {
  let rejectTimer: number|null = null;
  let pollTimer: number|null = null;

  function cleanup() {
    if (rejectTimer) {
      window.clearTimeout(rejectTimer);
    }
    if (pollTimer) {
      window.clearInterval(pollTimer);
    }
  }

  return new Promise((resolve, reject) => {
    rejectTimer = window.setTimeout(() => {
      cleanup();
      reject(new Error('Toast element did not get opened on time'));
    }, timeoutMs);

    pollTimer = window.setInterval(() => {
      if (toast.open) {
        cleanup();
        resolve(true);
      }
    }, intervalMs);
  });
}

suite('SupportToolTest', function() {
  let supportTool: SupportToolElement;
  let browserProxy: TestSupportToolBrowserProxy;

  const strings = {
    caseId: 'testcaseid',
  };

  setup(async function() {
    loadTimeData.overrideValues(strings);
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    browserProxy = new TestSupportToolBrowserProxy();
    BrowserProxyImpl.setInstance(browserProxy);
    supportTool = document.createElement('support-tool');
    document.body.appendChild(supportTool);
    await waitAfterNextRender(supportTool);
  });

  test('support tool pages navigation', () => {
    const pages = supportTool.shadowRoot!.querySelector('cr-page-selector');
    assertTrue(!!pages);

    // The selected page index must be 0, which means initial page is
    // IssueDetails.
    assertEquals(pages.selected, SupportToolPageIndex.ISSUE_DETAILS);
    // Only continue button container must be visible in initial page.
    assertFalse(
        supportTool.shadowRoot!.getElementById(
                                   'continueButtonContainer')!.hidden);
    // Click on continue button to move onto data collector selection page.
    supportTool.shadowRoot!.getElementById('continueButton')!.click();
    assertEquals(pages.selected, SupportToolPageIndex.DATA_COLLECTOR_SELECTION);
    // Click on continue button to start data collection.
    supportTool.shadowRoot!.getElementById('continueButton')!.click();
    browserProxy.whenCalled('startDataCollection').then(function([
      issueDetails,
      selectedDataCollectors,
    ]) {
      assertEquals(issueDetails.caseId, 'testcaseid');
      assertEquals(selectedDataCollectors, DATA_COLLECTORS);
    });
  });

  test('issue details page', () => {
    // Check the contents of data collectors page.
    const issueDetails = supportTool.$.issueDetails;
    assertEquals(
        issueDetails.shadowRoot!.querySelector('cr-input')!.value,
        'testcaseid');
    const emailOptions = issueDetails.shadowRoot!.querySelectorAll('option');
    // IssueDetailsElement adds DONT_INCLUDE_EMAIL string to the email addresses
    // options as for use to give the option to not include email address.
    assertEquals(EMAIL_ADDRESSES.length + 1, emailOptions.length);
  });

  test('data collector selection page', async () => {
    // Check the contents of data collectors page.
    const ironListItems =
        supportTool.$.dataCollectors.shadowRoot!.querySelector(
                                                    'dom-repeat')!.items!;
    assertEquals(ironListItems.length, DATA_COLLECTORS.length);
    for (let i = 0; i < ironListItems.length; i++) {
      const listItem = ironListItems[i];
      assertEquals(listItem.name, DATA_COLLECTORS[i]!.name);
      assertEquals(listItem.isIncluded, DATA_COLLECTORS[i]!.isIncluded);
      assertEquals(listItem.protoEnum, DATA_COLLECTORS[i]!.protoEnum);
    }

    const selectAllCheckbox =
        supportTool.$.dataCollectors.shadowRoot!.getElementById(
            'selectAllCheckbox')! as CrCheckboxElement;

    // Verify that the select all functionality works.
    selectAllCheckbox.click();
    await selectAllCheckbox.updateComplete;
    for (let i = 0; i < ironListItems.length; i++) {
      assertTrue(ironListItems[i].isIncluded);
    }

    // Verify that the unselect all functionality works.
    selectAllCheckbox.click();
    await selectAllCheckbox.updateComplete;
    for (let i = 0; i < ironListItems.length; i++) {
      assertFalse(ironListItems[i].isIncluded);
    }
  });

  test('take and remove screenshot', async () => {
    // Go to the data collector selection page.
    supportTool.shadowRoot!.getElementById('continueButton')!.click();
    assertEquals(
        supportTool.shadowRoot!.querySelector('cr-page-selector')!.selected,
        SupportToolPageIndex.DATA_COLLECTOR_SELECTION);
    // Take screenshot.
    const screenshot = supportTool.$.dataCollectors.shadowRoot!
                           .querySelector<ScreenshotElement>('#screenshot')!;
    const takeScreenshotButton =
        screenshot.shadowRoot!.getElementById('takeScreenshot')!;
    const removeButton =
        screenshot.shadowRoot!.getElementById('removeScreenshot')!;
    const hideInfoButton = screenshot.shadowRoot!.getElementById('hideInfo')!;
    takeScreenshotButton.click();
    await browserProxy.whenCalled('takeScreenshot');
    webUIListenerCallback('screenshot-received', SCREENSHOT_BASE64);
    assertFalse(removeButton.hidden);
    assertFalse(hideInfoButton.hidden);
    assertTrue(takeScreenshotButton.hidden);
    assertNotEquals('', screenshot.getEditedScreenshotBase64());
    // Remove screenshot.
    screenshot.shadowRoot!.getElementById('removeScreenshot')!.click();
    assertTrue(removeButton.hidden);
    assertTrue(hideInfoButton.hidden);
    assertFalse(takeScreenshotButton.hidden);
    assertEquals('', screenshot.getEditedScreenshotBase64());
  });

  test('take and edit screenshot', async () => {
    // Go to the data collector selection page.
    supportTool.shadowRoot!.getElementById('continueButton')!.click();
    assertEquals(
        supportTool.shadowRoot!.querySelector('cr-page-selector')!.selected,
        SupportToolPageIndex.DATA_COLLECTOR_SELECTION);
    // Take a screenshot.
    const screenshot = supportTool.$.dataCollectors.shadowRoot!
                           .querySelector<ScreenshotElement>('#screenshot')!;
    const takeScreenshotButton =
        screenshot.shadowRoot!.getElementById('takeScreenshot')!;
    const removeButton =
        screenshot.shadowRoot!.getElementById('removeScreenshot')!;
    const hideInfoButton = screenshot.shadowRoot!.getElementById('hideInfo')!;
    takeScreenshotButton.click();
    await browserProxy.whenCalled('takeScreenshot');
    webUIListenerCallback('screenshot-received', SCREENSHOT_BASE64);
    assertFalse(
        screenshot.shadowRoot!.getElementById('screenshotPreview')!.hidden);
    assertFalse(removeButton.hidden);
    assertFalse(hideInfoButton.hidden);
    assertTrue(takeScreenshotButton.hidden);
    assertNotEquals('', screenshot.getEditedScreenshotBase64());
    const originalScreenshot = screenshot.getOriginalScreenshotBase64();

    // Edit the screenshot.
    hideInfoButton.click();
    await waitAfterNextRender(screenshot);
    const canvas = screenshot.shadowRoot!.querySelector<HTMLCanvasElement>(
        '#screenshotCanvas')!;
    const confirmButton = screenshot.shadowRoot!.getElementById('confirmEdit')!;

    // After clicking the confirm button, the image is changed.
    track(canvas, canvas.width / 4, canvas.height / 4, 1);
    confirmButton.click();
    await waitAfterNextRender(screenshot);
    assertNotEquals(originalScreenshot, screenshot.getEditedScreenshotBase64());
  });

  test('spinner page', () => {
    // Check the contents of spinner page.
    const spinner = supportTool.$.spinnerPage;
    spinner.shadowRoot!.getElementById('cancelButton')!.click();
    browserProxy.whenCalled('cancelDataCollection').then(function() {
      webUIListenerCallback('data-collection-cancelled');
      flush();
      // Make sure the issue details page is displayed after cancelling data
      // collection.
      assertEquals(
          supportTool.shadowRoot!.querySelector('cr-page-selector')!.selected,
          SupportToolPageIndex.ISSUE_DETAILS);
    });
    assertEquals(browserProxy.getCallCount('cancelDataCollection'), 1);
  });

  test('PII selection page', async () => {
    // Go to the data collector selection page and start data collection by
    // clicking continue button twice so that the PII selection page gets
    // filled.
    supportTool.shadowRoot!.getElementById('continueButton')!.click();
    assertEquals(
        supportTool.shadowRoot!.querySelector('cr-page-selector')!.selected,
        SupportToolPageIndex.DATA_COLLECTOR_SELECTION);
    supportTool.shadowRoot!.getElementById('continueButton')!.click();
    // Check the contents of PII selection page.
    const piiSelection = supportTool.$.piiSelection;
    browserProxy.whenCalled('startDataCollection').then(function() {
      webUIListenerCallback('data-collection-completed', PII_ITEMS);
      flush();
      const items =
          piiSelection.shadowRoot!.querySelector('dom-repeat')!.items!;
      assertEquals(items, PII_ITEMS);
    });
    assertEquals(browserProxy.getCallCount('startDataCollection'), 1);
    piiSelection.shadowRoot!.getElementById('exportButton')!.click();
    await browserProxy.whenCalled('startDataExport');
    webUIListenerCallback('support-data-export-started');
    flush();
    assertEquals(
        supportTool.shadowRoot!.querySelector('cr-page-selector')!.selected,
        SupportToolPageIndex.EXPORT_SPINNER);
    const exportResult: DataExportResult = {
      success: true,
      path: '/usr/testuser/downloads/fake_support_packet_path.zip',
      error: '',
    };
    webUIListenerCallback('data-export-completed', exportResult);
    flush();
    assertEquals(
        supportTool.shadowRoot!.querySelector('cr-page-selector')!.selected,
        SupportToolPageIndex.DATA_EXPORT_DONE);
  });
});

suite('UrlGeneratorTest', function() {
  let urlGenerator: UrlGeneratorElement;
  let browserProxy: TestSupportToolBrowserProxy;

  setup(async function() {
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    browserProxy = new TestSupportToolBrowserProxy();
    BrowserProxyImpl.setInstance(browserProxy);
    urlGenerator = document.createElement('url-generator');
    document.body.appendChild(urlGenerator);
    await waitAfterNextRender(urlGenerator);
  });

  test('url generation success', async () => {
    // Ensure the button is disabled when we open the page.
    const copyLinkButton = urlGenerator.shadowRoot!.getElementById(
                               'copyURLButton')! as CrButtonElement;
    assertTrue(copyLinkButton.disabled);
    const caseIdInput = urlGenerator.shadowRoot!.getElementById(
                            'caseIdInput')! as CrInputElement;
    caseIdInput.value = 'test123';
    const dataCollectors =
        urlGenerator.shadowRoot!.querySelectorAll('cr-checkbox');
    // Select one of data collectors to enable the button.
    const firstDataCollector = dataCollectors[0]! as CrCheckboxElement;
    firstDataCollector.click();
    await firstDataCollector.updateComplete;
    // Ensure the button is enabled after we select at least one data collector.
    assertFalse(copyLinkButton.disabled);
    const expectedToken = 'chrome://support-tool/?case_id=test123&module=jekhh';
    // Set the expected result of URL generation to successful.
    const expectedResult: SupportTokenGenerationResult = {
      success: true,
      token: expectedToken,
      errorMessage: '',
    };
    browserProxy.setSupportTokenGenerationResult(expectedResult);
    // Click the button to generate URL and copy to clipboard.
    copyLinkButton.click();
    await browserProxy.whenCalled('generateCustomizedUrl');
    // A toast message needs to be opened when the copy link button is clicked.
    assertTrue(await waitForToastOpened(urlGenerator.$.copyToast));
    // Check the URL value copied to clipboard if it's as expected.
    const copiedToken = await navigator.clipboard.readText();
    assertEquals(copiedToken, expectedToken);
  });

  test('url generation fail', async () => {
    // Set the expected result of URL generation to error.
    const expectedResult: SupportTokenGenerationResult = {
      success: false,
      token: '',
      errorMessage: 'Test error message',
    };
    browserProxy.setSupportTokenGenerationResult(expectedResult);
    const copyLinkButton = urlGenerator.shadowRoot!.getElementById(
                               'copyURLButton')! as CrButtonElement;
    // Enable the button for testing. The input fields are not important as
    // we're testing for the error message.
    copyLinkButton.disabled = false;
    // Click the button to generate URL.
    copyLinkButton!.click();
    await browserProxy.whenCalled('generateCustomizedUrl');
    // Check that there's an error message shown to user.
    assertTrue(await waitForToastOpened(urlGenerator.$.errorMessageToast));
  });

  test('token generation success', async () => {
    // Ensure the button is disabled when we open the page.
    const copyTokenButton = urlGenerator.shadowRoot!.getElementById(
                                'copyTokenButton')! as CrButtonElement;
    assertTrue(copyTokenButton.disabled);
    const dataCollectors =
        urlGenerator.shadowRoot!.querySelectorAll('cr-checkbox');
    // Select one of data collectors to enable the button.
    const firstDataCollector = dataCollectors[0]! as CrCheckboxElement;
    firstDataCollector.click();
    await firstDataCollector.updateComplete;
    // Ensure the button is enabled after we select at least one data collector.
    assertFalse(copyTokenButton.disabled);
    const expectedToken = 'jekhh';
    // Set the expected result of token generation to successful.
    const expectedResult: SupportTokenGenerationResult = {
      success: true,
      token: expectedToken,
      errorMessage: '',
    };
    browserProxy.setSupportTokenGenerationResult(expectedResult);
    // Click the button to generate URL and copy to clipboard.
    copyTokenButton.click();
    await browserProxy.whenCalled('generateSupportToken');
    // A toast message needs to be opened when the copy token button is clicked.
    assertTrue(await waitForToastOpened(urlGenerator.$.copyToast));
    // Check the token value copied to clipboard if it's as expected.
    const copiedToken = await navigator.clipboard.readText();
    assertEquals(copiedToken, expectedToken);
  });
});