chromium/chrome/test/data/webui/settings/protocol_handlers_test.ts

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

// clang-format off
import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import type {AppProtocolEntry, HandlerEntry, ProtocolEntry, ProtocolHandlersElement} from 'chrome://settings/lazy_load.js';
import {SiteSettingsPrefsBrowserProxyImpl} from 'chrome://settings/lazy_load.js';
import {assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';

import {TestSiteSettingsPrefsBrowserProxy} from './test_site_settings_prefs_browser_proxy.js';
// clang-format on

/** @fileoverview Suite of tests for protocol_handlers. */
suite('ProtocolHandlers', function() {
  /**
   * A dummy protocol handler element created before each test.
   */
  let testElement: ProtocolHandlersElement;

  /**
   * A list of ProtocolEntry fixtures.
   */
  const protocols: ProtocolEntry[] = [
    {
      handlers: [{
        host: 'www.google.com',
        protocol: 'mailto',
        protocol_display_name: 'email',
        spec: 'http://www.google.com/%s',
        is_default: true,
      }],
      protocol: 'mailto',
      protocol_display_name: 'email',
    },
    {
      handlers: [
        {
          host: 'www.google1.com',
          protocol: 'webcal',
          protocol_display_name: 'web calendar',
          spec: 'http://www.google1.com/%s',
          is_default: true,
        },
        {
          host: 'www.google2.com',
          protocol: 'webcal',
          protocol_display_name: 'web calendar',
          spec: 'http://www.google2.com/%s',
          is_default: false,
        },
      ],
      protocol: 'webcal',
      protocol_display_name: 'web calendar',
    },
  ];

  /**
   * A list of AppProtocolEntry fixtures.
   */
  const appAllowedProtocols: AppProtocolEntry[] = [
    {
      handlers: [{
        host: 'www.google.com',
        protocol: 'mailto',
        protocol_display_name: 'email',
        spec: 'http://www.google.com/%s',
        app_id: 'testID',
      }],
      protocol: 'mailto',
      protocol_display_name: 'email',
    },
    {
      handlers: [
        {
          host: 'www.google1.com',
          protocol: 'webcal',
          protocol_display_name: 'web calendar',
          spec: 'http://www.google1.com/%s',
          app_id: 'testID1',
        },
        {
          host: 'www.google2.com',
          protocol: 'webcal',
          protocol_display_name: 'web calendar',
          spec: 'http://www.google2.com/%s',
          app_id: 'testID2',
        },
      ],
      protocol: 'webcal',
      protocol_display_name: 'web calendar',
    },
  ];

  /**
   * A list of AppProtocolEntry fixtures. This list should only contain
   * entries that do not overlap `appAllowedProtocols`.
   */
  const appDisallowedProtocols: AppProtocolEntry[] = [
    {
      handlers: [{
        host: 'www.google1.com',
        protocol: 'mailto',
        protocol_display_name: 'email',
        spec: 'http://www.google1.com/%s',
        app_id: 'testID1',
      }],
      protocol: 'mailto',
      protocol_display_name: 'email',
    },
    {
      handlers: [
        {
          host: 'www.google.com',
          protocol: 'webcal',
          protocol_display_name: 'web calendar',
          spec: 'http://www.google.com/%s',
          app_id: 'testID',
        },
        {
          host: 'www.google3.com',
          protocol: 'webcal',
          protocol_display_name: 'web calendar',
          spec: 'http://www.google3.com/%s',
          app_id: 'testID3',
        },
      ],
      protocol: 'webcal',
      protocol_display_name: 'web calendar',
    },
  ];

  /**
   * A list of IgnoredProtocolEntry fixtures.
   */
  const ignoredProtocols: HandlerEntry[] = [{
    host: 'www.google.com',
    protocol: 'web+ignored',
    protocol_display_name: 'web+ignored',
    spec: 'https://www.google.com/search?q=ignored+%s',
    is_default: false,
  }];

  /**
   * The mock proxy object to use during test.
   */
  let browserProxy: TestSiteSettingsPrefsBrowserProxy;

  setup(async function() {
    browserProxy = new TestSiteSettingsPrefsBrowserProxy();
    SiteSettingsPrefsBrowserProxyImpl.setInstance(browserProxy);
  });

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

  /** @return {!Promise} */
  async function initPage() {
    browserProxy.reset();
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    testElement = document.createElement('protocol-handlers');
    document.body.appendChild(testElement);
    await browserProxy.whenCalled('observeAppProtocolHandlers');
    flush();
  }

  test('set protocol handlers default called', async () => {
    await initPage();
    testElement.shadowRoot!
        .querySelector<HTMLElement>('#protcolHandlersRadioBlock')!.click();
    await browserProxy.whenCalled('setProtocolHandlerDefault');
  });

  test('empty list', async function() {
    await initPage();
    const listFrames = testElement.shadowRoot!.querySelectorAll('.list-frame');
    assertEquals(0, listFrames.length);
  });

  test('non-empty list', async function() {
    browserProxy.setProtocolHandlers(protocols);

    await initPage();
    const listFrames = testElement.shadowRoot!.querySelectorAll('.list-frame');
    const listItems = testElement.shadowRoot!.querySelectorAll('.list-item');
    // There are two protocols: ["mailto", "webcal"].
    assertEquals(2, listFrames.length);
    // There are three total handlers within the two protocols.
    assertEquals(3, listItems.length);

    // Check that item hosts are rendered correctly.
    const hosts = testElement.shadowRoot!.querySelectorAll('.protocol-host');
    assertEquals('www.google.com', hosts[0]!.textContent!.trim());
    assertEquals('www.google1.com', hosts[1]!.textContent!.trim());
    assertEquals('www.google2.com', hosts[2]!.textContent!.trim());

    // Check that item default subtexts are rendered correctly.
    const defText = testElement.shadowRoot!.querySelectorAll<HTMLElement>(
        '.protocol-default');
    assertFalse(defText[0]!.hidden);
    assertFalse(defText[1]!.hidden);
    assertTrue(defText[2]!.hidden);
  });

  test('non-empty ignored protocols', async () => {
    browserProxy.setIgnoredProtocols(ignoredProtocols);

    await initPage();
    const listFrames = testElement.shadowRoot!.querySelectorAll('.list-frame');
    const listItems = testElement.shadowRoot!.querySelectorAll('.list-item');
    // There is a single blocked protocols section
    assertEquals(1, listFrames.length);
    // There is one total handlers within the two protocols.
    assertEquals(1, listItems.length);

    // Check that item hosts are rendered correctly.
    const hosts = testElement.shadowRoot!.querySelectorAll('.protocol-host');
    assertEquals('www.google.com', hosts[0]!.textContent!.trim());

    // Check that item default subtexts are rendered correctly.
    const defText = testElement.shadowRoot!.querySelectorAll<HTMLElement>(
        '.protocol-protocol');
    assertFalse(defText[0]!.hidden);
  });

  /**
   * A reusable function to test different action buttons.
   * @param button id of the button to test.
   * @param handler name of browserProxy handler to react.
   */
  async function testButtonFlow(button: string, browserProxyHandler: string) {
    await initPage();

    // Initiating the elements
    const menuButtons = testElement.shadowRoot!.querySelectorAll<HTMLElement>(
        'cr-icon-button.icon-more-vert');
    assertEquals(3, menuButtons.length);
    const dialog = testElement.shadowRoot!.querySelector('cr-action-menu')!;
    const indexPairs = [[0, 0], [1, 0], [1, 1]];

    for (let menuIndex = 0; menuIndex < indexPairs.length; menuIndex++) {
      const protocolIndex = indexPairs[menuIndex]![0]!;
      const handlerIndex = indexPairs[menuIndex]![1]!;
      // Test the button for the first protocol handler
      browserProxy.reset();
      assertFalse(dialog.open);
      menuButtons[menuIndex]!.click();
      assertTrue(dialog.open);
      if (testElement.$.defaultButton.disabled) {
        testElement.shadowRoot!.querySelector('cr-action-menu')!.close();
        assertFalse(dialog.open);
        continue;
      }

      testElement.shadowRoot!.querySelector<HTMLElement>(`#${button}`)!.click();
      assertFalse(dialog.open);
      const [protocol, url] =
          await browserProxy.whenCalled(browserProxyHandler);
      // BrowserProxy's handler is expected to be called with
      // arguments as [protocol, url].
      assertEquals(protocols[protocolIndex]!.protocol, protocol);
      assertEquals(protocols[protocolIndex]!.handlers[handlerIndex]!.spec, url);
    }
  }

  test('remove button works', function() {
    browserProxy.setProtocolHandlers(protocols);
    return testButtonFlow('removeButton', 'removeProtocolHandler');
  });

  test('default button works', function() {
    browserProxy.setProtocolHandlers(protocols);
    return testButtonFlow('defaultButton', 'setProtocolDefault').then(() => {
      const menuButtons = testElement.shadowRoot!.querySelectorAll<HTMLElement>(
          'cr-icon-button.icon-more-vert');
      const closeMenu = () =>
          testElement.shadowRoot!.querySelector('cr-action-menu')!.close();
      menuButtons[0]!.click();
      flush();
      assertTrue(testElement.$.defaultButton.disabled);
      closeMenu();
      menuButtons[1]!.click();
      flush();
      assertTrue(testElement.$.defaultButton.disabled);
      closeMenu();
      menuButtons[2]!.click();
      flush();
      assertFalse(testElement.$.defaultButton.disabled);
    });
  });

  test('remove button for ignored works', async () => {
    browserProxy.setIgnoredProtocols(ignoredProtocols);
    await initPage();

    testElement.shadowRoot!.querySelector<HTMLElement>(
                               '#removeIgnoredButton')!.click();
    const args = await browserProxy.whenCalled('removeProtocolHandler');

    const protocol = args[0];
    const url = args[1];
    // BrowserProxy's handler is expected to be called with arguments as
    // [protocol, url].
    assertEquals(ignoredProtocols[0]!.protocol, protocol);
    assertEquals(ignoredProtocols[0]!.spec, url);
  });

  test('non-empty web app allowed protocols', async () => {
    browserProxy.setAppAllowedProtocolHandlers(appAllowedProtocols);
    await initPage();
    const listFrames = testElement.shadowRoot!.querySelectorAll('.list-frame');
    const listItems = testElement.shadowRoot!.querySelectorAll('.list-item');
    // There are two protocols: ["mailto", "webcal"].
    assertEquals(2, listFrames.length);
    // There are three total handlers within the two protocols.
    assertEquals(3, listItems.length);

    // Check that item hosts are rendered correctly.
    const hosts = testElement.shadowRoot!.querySelectorAll('.protocol-host');
    assertEquals('www.google.com', hosts[0]!.textContent!.trim());
    assertEquals('www.google1.com', hosts[1]!.textContent!.trim());
    assertEquals('www.google2.com', hosts[2]!.textContent!.trim());
  });

  test('remove web app allowed protocols', async () => {
    browserProxy.setAppAllowedProtocolHandlers(appAllowedProtocols);
    await initPage();
    // Remove the first app protocol.
    testElement.shadowRoot!
        .querySelector<HTMLElement>('#removeAppHandlerButton')!.click();
    const args = await browserProxy.whenCalled('removeAppAllowedHandler');

    // BrowserProxy's handler is expected to be called with
    // arguments as [protocol, url, app_id].
    assertEquals(appAllowedProtocols[0]!.protocol, args[0]);
    assertEquals(appAllowedProtocols[0]!.handlers[0]!.spec, args[1]);
    assertEquals(appAllowedProtocols[0]!.handlers[0]!.app_id, args[2]);
  });

  test('non-empty web app disallowed protocols', async () => {
    browserProxy.setAppDisallowedProtocolHandlers(appDisallowedProtocols);
    await initPage();
    const listFrames = testElement.shadowRoot!.querySelectorAll('.list-frame');
    const listItems = testElement.shadowRoot!.querySelectorAll('.list-item');
    // There are two protocols: ["mailto", "webcal"].
    assertEquals(2, listFrames.length);
    // There are three total handlers within the two protocols.
    assertEquals(3, listItems.length);

    // Check that item hosts are rendered correctly.
    const hosts = testElement.shadowRoot!.querySelectorAll('.protocol-host');
    assertEquals('www.google1.com', hosts[0]!.textContent!.trim());
    assertEquals('www.google.com', hosts[1]!.textContent!.trim());
    assertEquals('www.google3.com', hosts[2]!.textContent!.trim());
  });

  test('remove web app disallowed protocols', async () => {
    browserProxy.setAppDisallowedProtocolHandlers(appDisallowedProtocols);
    await initPage();
    // Remove the first app protocol.
    testElement.shadowRoot!
        .querySelector<HTMLElement>('#removeAppHandlerButton')!.click();
    const args = await browserProxy.whenCalled('removeAppDisallowedHandler');

    // BrowserProxy's handler is expected to be called with
    // arguments as [protocol, url, app_id].
    assertEquals(appDisallowedProtocols[0]!.protocol, args[0]);
    assertEquals(appDisallowedProtocols[0]!.handlers[0]!.spec, args[1]);
    assertEquals(appDisallowedProtocols[0]!.handlers[0]!.app_id, args[2]);
  });

  test('non-empty web app allowed and disallowed protocols', async () => {
    browserProxy.setAppAllowedProtocolHandlers(appAllowedProtocols);
    browserProxy.setAppDisallowedProtocolHandlers(appDisallowedProtocols);
    await initPage();
    const listFrames = testElement.shadowRoot!.querySelectorAll('.list-frame');
    const listItems = testElement.shadowRoot!.querySelectorAll('.list-item');
    // There are two protocols ["mailto", "webcal"] for both allowed,
    // and disallowed lists.
    assertEquals(4, listFrames.length);
    // There are three total handlers within the two protocols in both
    // the allowed and disallowed lists.
    assertEquals(6, listItems.length);

    // Check that item hosts are rendered correctly.
    const hosts = testElement.shadowRoot!.querySelectorAll('.protocol-host');

    // Allowed list.
    assertEquals('www.google.com', hosts[0]!.textContent!.trim());
    assertEquals('www.google1.com', hosts[1]!.textContent!.trim());
    assertEquals('www.google2.com', hosts[2]!.textContent!.trim());

    // Disallowed list.
    assertEquals('www.google1.com', hosts[3]!.textContent!.trim());
    assertEquals('www.google.com', hosts[4]!.textContent!.trim());
    assertEquals('www.google3.com', hosts[5]!.textContent!.trim());
  });

  test('remove web app allowed then disallowed protocols', async () => {
    browserProxy.setAppAllowedProtocolHandlers(appAllowedProtocols);
    browserProxy.setAppDisallowedProtocolHandlers(appDisallowedProtocols);
    await initPage();

    const removeButtons = testElement.shadowRoot!.querySelectorAll<HTMLElement>(
        'cr-icon-button.icon-clear');
    assertEquals(6, removeButtons.length);

    // Remove the first allowed app protocol.
    removeButtons[0]!.click();
    const args1 = await browserProxy.whenCalled('removeAppAllowedHandler');
    assertEquals(appAllowedProtocols[0]!.protocol, args1[0]);
    assertEquals(appAllowedProtocols[0]!.handlers[0]!.spec, args1[1]);
    assertEquals(appAllowedProtocols[0]!.handlers[0]!.app_id, args1[2]);

    // Remove the first disallowed app protocol.
    removeButtons[3]!.click();
    const args2 = await browserProxy.whenCalled('removeAppDisallowedHandler');
    assertEquals(appDisallowedProtocols[0]!.protocol, args2[0]);
    assertEquals(appDisallowedProtocols[0]!.handlers[0]!.spec, args2[1]);
    assertEquals(appDisallowedProtocols[0]!.handlers[0]!.app_id, args2[2]);
  });
});