chromium/chrome/test/data/webui/downloads/manager_test.ts

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

import type {CrToastManagerElement, DownloadsManagerElement, PageRemote} from 'chrome://downloads/downloads.js';
import {BrowserProxy, DangerType, loadTimeData, State} from 'chrome://downloads/downloads.js';
import {stringToMojoString16, stringToMojoUrl} from 'chrome://resources/js/mojo_type_util.js';
import {isMac} from 'chrome://resources/js/platform.js';
import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {assertEquals, assertFalse, assertLT, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {keyDownOn} from 'chrome://webui-test/keyboard_mock_interactions.js';
import {isVisible} from 'chrome://webui-test/test_util.js';

import {createDownload, TestDownloadsProxy} from './test_support.js';

suite('manager tests', function() {
  let manager: DownloadsManagerElement;
  let testBrowserProxy: TestDownloadsProxy;
  let callbackRouterRemote: PageRemote;
  let toastManager: CrToastManagerElement;

  setup(function() {
    loadTimeData.overrideValues({'improvedDownloadWarningsUX': true});
    document.body.innerHTML = window.trustedTypes!.emptyHTML;

    testBrowserProxy = new TestDownloadsProxy();
    callbackRouterRemote = testBrowserProxy.callbackRouterRemote;
    BrowserProxy.setInstance(testBrowserProxy);

    manager = document.createElement('downloads-manager');
    document.body.appendChild(manager);

    toastManager = manager.shadowRoot!.querySelector('cr-toast-manager')!;
    assertTrue(!!toastManager);
  });

  test('long URLs don\'t elide', async () => {
    const url = 'https://' +
        'a'.repeat(1000) + '.com/document.pdf';
    const displayUrl = 'https://' +
        '啊'.repeat(1000) + '.com/document.pdf';
    callbackRouterRemote.insertItems(
        0, [createDownload({
          fileName: 'file name',
          state: State.kComplete,
          sinceString: 'Today',
          url: stringToMojoUrl(url),
          displayUrl: stringToMojoString16(displayUrl),
        })]);
    await callbackRouterRemote.$.flushForTesting();
    flush();

    const item = manager.shadowRoot!.querySelector('downloads-item')!;
    assertLT(item.$.url.offsetWidth, item.offsetWidth);
    assertEquals(displayUrl, item.$.url.textContent);
    assertEquals(url, item.$.url.href);
    assertEquals(url, item.$['file-link'].href);
    assertEquals(url, item.$.url.href);
  });

  test('inserting items at beginning render dates correctly', async () => {
    const countDates = () => {
      const items = manager.shadowRoot!.querySelectorAll('downloads-item');
      return Array.from(items).reduce((soFar, item) => {
        return item.shadowRoot!.querySelector('div[id=date]:not(:empty)') ?
            soFar + 1 :
            soFar;
      }, 0);
    };

    const download1 = createDownload();
    const download2 = createDownload();

    callbackRouterRemote.insertItems(0, [download1, download2]);
    await callbackRouterRemote.$.flushForTesting();
    flush();
    assertEquals(1, countDates());

    callbackRouterRemote.removeItem(0);
    await callbackRouterRemote.$.flushForTesting();
    flush();
    assertEquals(1, countDates());

    callbackRouterRemote.insertItems(0, [download1]);
    await callbackRouterRemote.$.flushForTesting();
    flush();
    assertEquals(1, countDates());
  });

  test('update', async () => {
    const dangerousDownload = createDownload({
      dangerType: DangerType.kDangerousFile,
      state: State.kDangerous,
    });
    callbackRouterRemote.insertItems(0, [dangerousDownload]);
    await callbackRouterRemote.$.flushForTesting();
    flush();
    assertTrue(!!manager.shadowRoot!.querySelector('downloads-item')!
                     .shadowRoot!.querySelector('.dangerous'));

    const safeDownload = Object.assign({}, dangerousDownload, {
      dangerType: DangerType.kNoApplicableDangerType,
      state: State.kComplete,
    });
    callbackRouterRemote.updateItem(0, safeDownload);
    await callbackRouterRemote.$.flushForTesting();
    flush();
    assertFalse(!!manager.shadowRoot!.querySelector('downloads-item')!
                      .shadowRoot!.querySelector('.dangerous'));
  });

  test('remove', async () => {
    callbackRouterRemote.insertItems(0, [createDownload({
                                       fileName: 'file name',
                                       state: State.kComplete,
                                       sinceString: 'Today',
                                       url: stringToMojoUrl('a'.repeat(1000)),
                                     })]);
    await callbackRouterRemote.$.flushForTesting();
    flush();
    const item = manager.shadowRoot!.querySelector('downloads-item')!;

    const quickRemoveButton =
        item.shadowRoot!.querySelector<HTMLElement>('#quick-remove');
    assertTrue(!!quickRemoveButton);
    quickRemoveButton.click();
    await testBrowserProxy.handler.whenCalled('remove');
    flush();
    const list = manager.shadowRoot!.querySelector('iron-list')!;
    assertTrue(list.hidden);
    assertTrue(toastManager.isToastOpen);
  });

  test('toolbar hasClearableDownloads set correctly', async () => {
    const clearable = createDownload();
    callbackRouterRemote.insertItems(0, [clearable]);
    const checkClearable = async (state: State) => {
      const download = createDownload({state: state});
      callbackRouterRemote.updateItem(0, download);
      await callbackRouterRemote.$.flushForTesting();
      assertTrue(manager.$.toolbar.hasClearableDownloads);
    };
    await checkClearable(State.kDangerous);
    await checkClearable(State.kInProgress);
    await checkClearable(State.kPaused);
    await checkClearable(State.kComplete);

    callbackRouterRemote.removeItem(0);
    await callbackRouterRemote.$.flushForTesting();
    assertFalse(manager.$.toolbar.hasClearableDownloads);
  });

  test('loadTimeData contains isManaged and browserManagedByOrg', function() {
    // Check that loadTimeData contains these values.
    loadTimeData.getBoolean('isManaged');
    loadTimeData.getString('browserManagedByOrg');
  });

  test('toast is shown when clear-all-command is fired', async () => {
    // Add a download entry so that clear-all-command is applicable.
    callbackRouterRemote.insertItems(0, [createDownload({
                                       fileName: 'file name',
                                       state: State.kComplete,
                                       sinceString: 'Today',
                                       url: stringToMojoUrl('a'.repeat(1000)),
                                     })]);
    await callbackRouterRemote.$.flushForTesting();

    assertFalse(toastManager.isToastOpen);

    // Simulate 'alt+c' key combo.
    keyDownOn(document.documentElement, 0, 'alt', isMac ? 'ç' : 'c');
    assertTrue(toastManager.isToastOpen);
  });

  test('toast is hidden when undo-command is fired', () => {
    toastManager.show('');
    assertTrue(toastManager.isToastOpen);

    // Simulate 'ctrl+z' key combo (or meta+z for Mac).
    keyDownOn(document.documentElement, 0, isMac ? 'meta' : 'ctrl', 'z');
    assertFalse(toastManager.isToastOpen);
  });

  test('toast is hidden when undo is clicked', () => {
    toastManager.show('');
    assertTrue(toastManager.isToastOpen);
    manager.shadowRoot!
        .querySelector<HTMLElement>('cr-toast-manager cr-button')!.click();
    assertFalse(toastManager.isToastOpen);
  });

  test('toast is not hidden when itself is clicked', () => {
    toastManager.show('');
    assertTrue(toastManager.isToastOpen);
    toastManager.shadowRoot!.querySelector<HTMLElement>('#toast')!.click();
    assertTrue(toastManager.isToastOpen);
  });

  test('toast is hidden when page is clicked', () => {
    toastManager.show('');
    assertTrue(toastManager.isToastOpen);

    document.body.click();
    assertFalse(toastManager.isToastOpen);
  });

  test('undo is not shown when removing only dangerous items', async () => {
    callbackRouterRemote.insertItems(0, [
      createDownload({isDangerous: true}),
      createDownload({isInsecure: true}),
    ]);
    await callbackRouterRemote.$.flushForTesting();
    toastManager.show('', /* hideSlotted= */ false);
    assertFalse(toastManager.slottedHidden);
    keyDownOn(document.documentElement, 0, 'alt', isMac ? 'ç' : 'c');
    assertTrue(toastManager.slottedHidden);
  });

  test('undo is shown when removing items', async () => {
    callbackRouterRemote.insertItems(0, [
      createDownload(),
      createDownload({isDangerous: true}),
      createDownload({isInsecure: true}),
    ]);
    await callbackRouterRemote.$.flushForTesting();
    toastManager.show('', /* hideSlotted= */ true);
    assertTrue(toastManager.slottedHidden);
    keyDownOn(document.documentElement, 0, 'alt', isMac ? 'ç' : 'c');
    assertFalse(toastManager.slottedHidden);
  });

  test(
      'bypass warning confirmation dialog shown on save-dangerous-click',
      async () => {
        document.body.innerHTML = window.trustedTypes!.emptyHTML;
        loadTimeData.overrideValues({dangerousDownloadInterstitial: false});
        manager = document.createElement('downloads-manager');
        document.body.appendChild(manager);
        callbackRouterRemote.insertItems(0, [
          createDownload({
            id: 'itemId',
            fileName: 'item.pdf',
            state: State.kDangerous,
            isDangerous: true,
            dangerType: DangerType.kDangerousUrl,
          }),
        ]);
        await callbackRouterRemote.$.flushForTesting();
        flush();
        const item = manager.shadowRoot!.querySelector('downloads-item');
        assertTrue(!!item);
        item.dispatchEvent(new CustomEvent('save-dangerous-click', {
          bubbles: true,
          composed: true,
          detail: {id: item.data.id},
        }));
        await callbackRouterRemote.$.flushForTesting();
        flush();
        const recordOpenId = await testBrowserProxy.handler.whenCalled(
            'recordOpenBypassWarningDialog');
        assertEquals('itemId', recordOpenId);
        const dialog = manager.shadowRoot!.querySelector(
            'downloads-bypass-warning-confirmation-dialog');
        assertTrue(!!dialog);
        assertTrue(dialog.$.dialog.open);
        assertEquals('item.pdf', dialog.fileName);
        // Confirm the dialog to download the dangerous file.
        dialog.$.dialog.close();
        await callbackRouterRemote.$.flushForTesting();
        flush();
        const saveDangerousId = await testBrowserProxy.handler.whenCalled(
            'saveDangerousFromDialogRequiringGesture');
        assertEquals('itemId', saveDangerousId);
        assertFalse(dialog.$.dialog.open);
      });

  test('bypass warning confirmation dialog records cancellation', async () => {
    callbackRouterRemote.insertItems(0, [
      createDownload({
        id: 'itemId',
        fileName: 'item.pdf',
        state: State.kDangerous,
        isDangerous: true,
        dangerType: DangerType.kDangerousUrl,
      }),
    ]);
    await callbackRouterRemote.$.flushForTesting();
    flush();
    const item = manager.shadowRoot!.querySelector('downloads-item');
    assertTrue(!!item);
    item.dispatchEvent(new CustomEvent('save-dangerous-click', {
      bubbles: true,
      composed: true,
      detail: {id: item.data.id},
    }));
    await callbackRouterRemote.$.flushForTesting();
    flush();
    const recordOpenId = await testBrowserProxy.handler.whenCalled(
        'recordOpenBypassWarningDialog');
    assertEquals('itemId', recordOpenId);
    const dialog = manager.shadowRoot!.querySelector(
        'downloads-bypass-warning-confirmation-dialog');
    assertTrue(!!dialog);
    assertTrue(dialog.$.dialog.open);
    assertEquals('item.pdf', dialog.fileName);
    // Cancel the dialog and check that it's recorded.
    dialog.$.dialog.cancel();
    await callbackRouterRemote.$.flushForTesting();
    flush();
    const recordCancelId = await testBrowserProxy.handler.whenCalled(
        'recordCancelBypassWarningDialog');
    assertEquals('itemId', recordCancelId);
    assertFalse(dialog.$.dialog.open);
  });

  test(
      'bypass warning confirmation dialog closed when file removed',
      async () => {
        callbackRouterRemote.insertItems(0, [
          createDownload({
            id: 'itemId',
            state: State.kDangerous,
            isDangerous: true,
            dangerType: DangerType.kDangerousUrl,
          }),
        ]);
        await callbackRouterRemote.$.flushForTesting();
        flush();
        const item = manager.shadowRoot!.querySelector('downloads-item')!;
        assertTrue(!!item);
        item.dispatchEvent(new CustomEvent('save-dangerous-click', {
          bubbles: true,
          composed: true,
          detail: {id: item.data.id},
        }));
        flush();
        const dialog = manager.shadowRoot!.querySelector(
            'downloads-bypass-warning-confirmation-dialog');
        assertTrue(!!dialog);
        assertTrue(dialog.$.dialog.open);
        // Remove the file and check that the dialog is hidden.
        callbackRouterRemote.removeItem(0);
        await callbackRouterRemote.$.flushForTesting();
        flush();
        assertFalse(isVisible(dialog));
      });

  test(
      'interstitial shown when dangerousDownloadInterstitial enabled',
      async () => {
        document.body.innerHTML = window.trustedTypes!.emptyHTML;
        loadTimeData.overrideValues({dangerousDownloadInterstitial: true});
        manager = document.createElement('downloads-manager');
        document.body.appendChild(manager);
        callbackRouterRemote.insertItems(0, [
          createDownload({
            id: 'itemId',
            state: State.kDangerous,
            isDangerous: true,
            dangerType: DangerType.kDangerousUrl,
          }),
        ]);
        await callbackRouterRemote.$.flushForTesting();
        flush();
        const saveDangerousButton =
            manager.shadowRoot!.querySelector('downloads-item')!.shadowRoot!
                .querySelector('cr-action-menu')!.querySelector<HTMLElement>(
                    '#save-dangerous');
        assertTrue(!!saveDangerousButton);
        saveDangerousButton.click();
        flush();
        const recordOpenId = await testBrowserProxy.handler.whenCalled(
            'recordOpenBypassWarningInterstitial');
        assertEquals('itemId', recordOpenId);
        const interstitial = manager.shadowRoot!.querySelector(
            'downloads-dangerous-download-interstitial');
        assertTrue(!!interstitial);
        assertTrue(interstitial.$.dialog.open);

        const surveyResponse = 'kNoResponse';
        interstitial.$.dialog.close(surveyResponse);
        interstitial.dispatchEvent(new CustomEvent('close', {
          bubbles: true,
          composed: true,
        }));
        const saveDangerousId = await testBrowserProxy.handler.whenCalled(
            'saveDangerousFromInterstitialNeedGesture');
        assertEquals(surveyResponse, interstitial.$.dialog.returnValue);
        assertEquals('itemId', saveDangerousId);
        assertFalse(interstitial.$.dialog.open);
      });

  test('dangerousDownloadInterstitial records cancellation', async () => {
    callbackRouterRemote.insertItems(0, [
      createDownload({
        id: 'itemId',
        state: State.kDangerous,
        isDangerous: true,
        dangerType: DangerType.kDangerousUrl,
      }),
    ]);
    await callbackRouterRemote.$.flushForTesting();
    flush();
    const saveDangerousButton =
        manager.shadowRoot!.querySelector('downloads-item')!.shadowRoot!
            .querySelector('cr-action-menu')!.querySelector<HTMLElement>(
                '#save-dangerous');
    assertTrue(!!saveDangerousButton);
    saveDangerousButton.click();
    flush();
    const recordOpenId = await testBrowserProxy.handler.whenCalled(
        'recordOpenBypassWarningInterstitial');
    assertEquals('itemId', recordOpenId);
    const interstitial = manager.shadowRoot!.querySelector(
        'downloads-dangerous-download-interstitial');
    assertTrue(!!interstitial);
    assertTrue(interstitial.$.dialog.open);

    interstitial.$.dialog.close();
    interstitial.dispatchEvent(new CustomEvent('cancel', {
      bubbles: true,
      composed: true,
    }));
    const recordCancelId = await testBrowserProxy.handler.whenCalled(
        'recordCancelBypassWarningInterstitial');
    assertEquals('itemId', recordCancelId);
    assertFalse(interstitial.$.dialog.open);
  });

  test(
      'bypass warning confirmation interstitial closed when file removed',
      async () => {
        callbackRouterRemote.insertItems(0, [
          createDownload({
            id: 'itemId',
            state: State.kDangerous,
            isDangerous: true,
            dangerType: DangerType.kDangerousUrl,
          }),
        ]);
        await callbackRouterRemote.$.flushForTesting();
        flush();
        const item = manager.shadowRoot!.querySelector('downloads-item');
        assertTrue(!!item);
        const saveDangerousButton =
            manager.shadowRoot!.querySelector('downloads-item')!.shadowRoot!
                .querySelector('cr-action-menu')!.querySelector<HTMLElement>(
                    '#save-dangerous');
        assertTrue(!!saveDangerousButton);
        saveDangerousButton.click();
        flush();
        const interstitial = manager.shadowRoot!.querySelector(
            'downloads-dangerous-download-interstitial');
        assertTrue(!!interstitial);
        assertTrue(interstitial.$.dialog.open);
        // Remove the file and check that the interstitial is hidden.
        callbackRouterRemote.removeItem(0);
        await callbackRouterRemote.$.flushForTesting();
        flush();
        assertFalse(isVisible(interstitial));
      });

  // <if expr="_google_chrome">
  test(
      'shouldShowEsbPromotion returns true on first dangerous download',
      async () => {
        document.body.removeChild(manager);
        loadTimeData.overrideValues({esbDownloadRowPromo: true});
        testBrowserProxy.handler.setEligbleForEsbPromo(true);
        manager = document.createElement('downloads-manager');
        document.body.appendChild(manager);
        const dangerousDownload = createDownload({
          dangerType: DangerType.kDangerousFile,
          state: State.kDangerous,
          isDangerous: true,
        });
        callbackRouterRemote.insertItems(0, [dangerousDownload]);
        await callbackRouterRemote.$.flushForTesting();
        flush();

        const item = manager.shadowRoot!.querySelector('downloads-item');
        assertTrue(!!item);
        assertTrue(item.showEsbPromotion);
      });

  test(
      'shouldShowEsbPromotion returns true on most recent dangerous download',
      async () => {
        document.body.removeChild(manager);
        loadTimeData.overrideValues({esbDownloadRowPromo: true});
        testBrowserProxy.handler.setEligbleForEsbPromo(true);
        manager = document.createElement('downloads-manager');
        document.body.appendChild(manager);
        const dangerousDownload = createDownload({
          dangerType: DangerType.kDangerousFile,
          state: State.kDangerous,
          isDangerous: true,
          id: 'dangerousdownload1',
        });
        const dangerousDownloadTwo = createDownload({
          dangerType: DangerType.kDangerousFile,
          state: State.kDangerous,
          isDangerous: true,
          url: stringToMojoUrl('http://evil.com'),
          id: 'dangerousdownload2',
        });
        callbackRouterRemote.insertItems(
            0, [dangerousDownload, dangerousDownloadTwo]);
        await callbackRouterRemote.$.flushForTesting();
        flush();
        const itemList = manager.shadowRoot!.querySelectorAll('downloads-item');
        assertEquals(itemList.length, 2);
        assertTrue(itemList[0]!.showEsbPromotion);
        assertFalse(itemList[1]!.showEsbPromotion);
      });

  test(
      'calls logsEsbPromotionRowViewed when promo row is in first 5 downloads',
      async () => {
        document.body.removeChild(manager);
        loadTimeData.overrideValues({esbDownloadRowPromo: true});
        testBrowserProxy.handler.setEligbleForEsbPromo(true);
        manager = document.createElement('downloads-manager');
        document.body.appendChild(manager);
        const dangerousDownload = createDownload({
          dangerType: DangerType.kDangerousFile,
          state: State.kDangerous,
          isDangerous: true,
          id: 'dangerousdownload',
        });
        const downloadTwo = createDownload({
          dangerType: DangerType.kNoApplicableDangerType,
          state: State.kComplete,
          id: 'download2',
        });
        const downloadThree = createDownload({
          dangerType: DangerType.kNoApplicableDangerType,
          state: State.kComplete,
          id: 'download3',
        });
        const downloadFour = createDownload({
          dangerType: DangerType.kNoApplicableDangerType,
          state: State.kComplete,
          id: 'download4',
        });
        const downloadFive = createDownload({
          dangerType: DangerType.kNoApplicableDangerType,
          state: State.kComplete,
          id: 'download5',
        });
        callbackRouterRemote.insertItems(0, [
          downloadTwo,
          downloadThree,
          downloadFour,
          downloadFive,
          dangerousDownload,
        ]);
        await callbackRouterRemote.$.flushForTesting();
        flush();
        await testBrowserProxy.handler.whenCalled('logEsbPromotionRowViewed');
      });
  // </if>
});