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

// Copyright 2024 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 extensions-mv2-deprecation-panel. */
import 'chrome://extensions/extensions.js';

import type {ExtensionsMv2DeprecationPanelElement} from 'chrome://extensions/extensions.js';
import type {CrButtonElement} from 'chrome://resources/cr_elements/cr_button/cr_button.js';
import type {CrIconButtonElement} from 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.js';
import {assertDeepEquals, assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {flushTasks} from 'chrome://webui-test/polymer_test_util.js';
import {isVisible} from 'chrome://webui-test/test_util.js';

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

suite('ExtensionsMV2DeprecationPanel_WarningStage', function() {
  let panelElement: ExtensionsMv2DeprecationPanelElement;
  let mockDelegate: TestService;

  setup(function() {
    mockDelegate = new TestService();

    panelElement = document.createElement('extensions-mv2-deprecation-panel');
    panelElement.extensions = [createExtensionInfo({
      name: 'Extension A',
      id: 'a'.repeat(32),
      isAffectedByMV2Deprecation: true,
      mustRemainInstalled: false,
    })];
    // Stage 1 represents Mv2ExperimentStage.WARNING.
    panelElement.mv2ExperimentStage = 1;
    panelElement.delegate = mockDelegate;
    document.body.appendChild(panelElement);

    return flushTasks();
  });

  /**
   * Returns the only extension in the panel. Will fail if there are no
   * extensions, or more than one.
   */
  function getExtension(): Element {
    const extensionRows =
        panelElement.shadowRoot!.querySelectorAll('.panel-extension-row');
    assertEquals(1, extensionRows.length);
    const extension = extensionRows[0];
    assertTrue(!!extension);
    return extension;
  }

  test('header content is always visible', function() {
    assertTrue(isVisible(
        panelElement.shadowRoot!.querySelector('.panel-header-text')));
    assertTrue(
        isVisible(panelElement.shadowRoot!.querySelector('.header-button')));
  });

  test('correct number of extension rows', async function() {
    // Verify there is one extension row for the extension added at setup.
    let extensionRows =
        panelElement.shadowRoot!.querySelectorAll('.panel-extension-row');
    assertEquals(1, extensionRows.length);
    let infoA =
        extensionRows[0]!.querySelector<HTMLElement>('.panel-extension-info');
    assertTrue(!!infoA);
    assertEquals('Extension A', infoA.textContent!.trim());

    // Add a new extension to the panel.
    panelElement.push('extensions', createExtensionInfo({
                        name: 'Extension B',
                        isAffectedByMV2Deprecation: true,
                      }));
    await flushTasks();

    // Verify there are two extension rows.
    extensionRows =
        panelElement.shadowRoot!.querySelectorAll('.panel-extension-row');
    assertEquals(extensionRows.length, 2);
    infoA =
        extensionRows[0]!.querySelector<HTMLElement>('.panel-extension-info');
    assertTrue(!!infoA);
    assertEquals('Extension A', infoA.textContent!.trim());
    const infoB =
        extensionRows[1]!.querySelector<HTMLElement>('.panel-extension-info');
    assertTrue(!!infoB);
    assertEquals('Extension B', infoB.textContent!.trim());
  });

  test(
      'dismiss button triggers the warning dismissal when clicked',
      async function() {
        const dismissButton =
            panelElement.shadowRoot!.querySelector<CrButtonElement>(
                '.header-button');
        assertTrue(!!dismissButton);

        dismissButton.click();
        await mockDelegate.whenCalled('dismissMv2DeprecationNotice');
        assertEquals(
            1, mockDelegate.getCallCount('dismissMv2DeprecationNotice'));
      });

  test(
      'find alternative button is visible if extension has recommendations' +
          'url, and opens url when clicked',
      async function() {
        // Find alternative button is hidden when the extension doesn't have a
        // recommendations url.
        let extension = getExtension();
        let findAlternativeButton = extension.querySelector<CrButtonElement>(
            '.find-alternative-button');
        assertFalse(isVisible(findAlternativeButton));

        // Add a recommendations url to the existent extension.
        const id = 'a'.repeat(32);
        const recommendationsUrl =
            `https://chromewebstore.google.com/detail/${id}` +
            `/related-recommendations`;
        panelElement.set('extensions.0', createExtensionInfo({
                           name: 'Extension A',
                           id,
                           isAffectedByMV2Deprecation: true,
                           recommendationsUrl,
                         }));
        await flushTasks();

        // Find alternative button is visible when the extension has a
        // recommendations url.
        extension = getExtension();
        findAlternativeButton = extension.querySelector<CrButtonElement>(
            '.find-alternative-button');
        assertTrue(isVisible(findAlternativeButton));

        // Click on the find alternative button, and verify it triggered the
        // correct delegate call.
        findAlternativeButton?.click();
        await mockDelegate.whenCalled('openUrl');
        assertEquals(1, mockDelegate.getCallCount('openUrl'));
        assertDeepEquals([recommendationsUrl], mockDelegate.getArgs('openUrl'));
      });

  test('remove button is always hidden', function() {
    const extension = getExtension();
    const removeButton =
        extension.querySelector<CrIconButtonElement>('#removeButton');
    assertFalse(isVisible(removeButton));
  });

  test('find alternative action is always hidden', function() {
    // Open the extension's action menu.
    const extension = getExtension();
    const actionButton =
        extension.querySelector<CrIconButtonElement>('#actionMenuButton');
    assertTrue(!!actionButton);
    actionButton.click();

    // Find alternative action is always hidden.
    const findAlternativeAction =
        panelElement.shadowRoot!.querySelector<HTMLElement>(
            '#findAlternativeAction');
    assertFalse(isVisible(findAlternativeAction));
  });

  test(
      'remove action is visible if extension can be removed, and triggers' +
          'the extension removal when clicked',
      async function() {
        // Open the extension's action menu button.
        let extension = getExtension();
        let actionButton =
            extension.querySelector<CrIconButtonElement>('#actionMenuButton');
        assertTrue(!!actionButton);
        actionButton.click();

        // Remove button is visible when the extension doesn't need to remain
        // installed.
        let removeAction = panelElement.shadowRoot!.querySelector<HTMLElement>(
            '#removeAction');
        assertTrue(isVisible(removeAction));

        // Click on the remove button in the action menu, and verify it
        // triggered the correct delegate call.
        removeAction?.click();
        await mockDelegate.whenCalled('deleteItem');
        assertEquals(1, mockDelegate.getCallCount('deleteItem'));
        assertDeepEquals(
            [panelElement.extensions[0]?.id],
            mockDelegate.getArgs('deleteItem'));

        // Set the extension property to be force installed.
        panelElement.set('extensions.0', createExtensionInfo({
                           name: 'Extension A',
                           id: 'a'.repeat(32),
                           isAffectedByMV2Deprecation: true,
                           mustRemainInstalled: true,
                         }));
        await flushTasks();

        // Open the extension's action menu button again, since clicking on the
        // action closed the menu.
        extension = getExtension();
        actionButton =
            extension.querySelector<CrIconButtonElement>('#actionMenuButton');
        assertTrue(!!actionButton);
        actionButton.click();

        // Remove action is hidden when the extension must remain installed.
        removeAction = panelElement.shadowRoot!.querySelector<HTMLElement>(
            '#removeAction');
        assertFalse(isVisible(removeAction));
      });

  test(
      'keep action menu button triggers a warning dismissal for the extension' +
          'when clicked',
      async function() {
        // Open the extension's action menu.
        const extension = getExtension();
        const actionButton =
            extension.querySelector<CrIconButtonElement>('#actionMenuButton');
        assertTrue(!!actionButton);
        actionButton.click();

        // Keep action is always visible.
        const keepAction =
            panelElement.shadowRoot!.querySelector<HTMLElement>('#keepAction');
        assertTrue(!!keepAction);
        assertTrue(isVisible(keepAction));

        // Click on keep action and verify it triggers the correct call.
        keepAction.click();
        await mockDelegate.whenCalled(
            'dismissMv2DeprecationNoticeForExtension');
        assertEquals(
            1,
            mockDelegate.getCallCount(
                'dismissMv2DeprecationNoticeForExtension'));
        assertDeepEquals(
            [panelElement.extensions[0]?.id],
            mockDelegate.getArgs('dismissMv2DeprecationNoticeForExtension'));
      });
});