chromium/chrome/test/data/webui/chromeos/settings/os_apps_page/app_management_page/supported_links_item_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.

import 'chrome://os-settings/lazy_load.js';

import {AppManagementSupportedLinksOverlappingAppsDialogElement} from 'chrome://os-settings/lazy_load.js';
import {AppManagementStore, AppManagementSupportedLinksItemElement, CrRadioButtonElement, CrRadioGroupElement, updateSelectedAppId} from 'chrome://os-settings/os_settings.js';
import {App, AppType, WindowMode} from 'chrome://resources/cr_components/app_management/app_management.mojom-webui.js';
import {AppMap} from 'chrome://resources/cr_components/app_management/constants.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {assertEquals, assertFalse, assertNull, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {flushTasks, waitAfterNextRender} from 'chrome://webui-test/polymer_test_util.js';
import {isVisible} from 'chrome://webui-test/test_util.js';

import {FakePageHandler} from '../../app_management/fake_page_handler.js';
import {createApp, replaceBody, replaceStore, setupFakeHandler} from '../../app_management/test_util.js';
import {clearBody} from '../../utils.js';

type AppConfig = Partial<App>;
suite('<app-management-supported-links-item>', () => {
  let supportedLinksItem: AppManagementSupportedLinksItemElement;
  let fakeHandler: FakePageHandler;

  setup(() => {
    fakeHandler = setupFakeHandler();
    replaceStore();

    supportedLinksItem =
        document.createElement('app-management-supported-links-item');
  });

  teardown(() => {
    supportedLinksItem.remove();
  });

  function createSupportedLinksItemForApp(app: App): void {
    supportedLinksItem.app = app;
    supportedLinksItem.apps = AppManagementStore.getInstance().data.apps;
  }

  test('PWA - preferred -> browser', async () => {
    const pwaOptions = {
      type: AppType.kWeb,
      isPreferredApp: true,
      supportedLinks: ['google.com'],
    };

    // Add PWA app, and make it the currently selected app.
    const app = await fakeHandler.addApp('app1', pwaOptions);

    AppManagementStore.getInstance().dispatch(updateSelectedAppId(app.id));

    await fakeHandler.flushPipesForTesting();

    assertTrue(!!AppManagementStore.getInstance().data.apps[app.id]);

    createSupportedLinksItemForApp(app);

    replaceBody(supportedLinksItem);
    await fakeHandler.flushPipesForTesting();
    flushTasks();

    let radioGroup =
        supportedLinksItem.shadowRoot!.querySelector('cr-radio-group');
    assertTrue(!!radioGroup);
    assertEquals('preferred', radioGroup.selected);

    const browserRadioButton =
        supportedLinksItem.shadowRoot!.querySelector<CrRadioButtonElement>(
            '#browserRadioButton');
    assertTrue(!!browserRadioButton);
    await browserRadioButton.click();
    await fakeHandler.whenCalled('setPreferredApp');
    await flushTasks();

    const selectedApp = AppManagementStore.getInstance().data.apps[app.id];
    assertTrue(!!selectedApp);
    assertFalse(selectedApp.isPreferredApp);

    radioGroup = supportedLinksItem.shadowRoot!.querySelector('cr-radio-group');
    assertTrue(!!radioGroup);
    assertEquals('browser', radioGroup.selected);
  });

  test('ARC - browser -> preferred', async () => {
    const arcOptions = {
      type: AppType.kArc,
      isPreferredApp: false,
      supportedLinks: ['google.com', 'gmail.com'],
    };

    // Add ARC app, and make it the currently selected app.
    const app = await fakeHandler.addApp('app1', arcOptions);

    AppManagementStore.getInstance().dispatch(updateSelectedAppId(app.id));

    await fakeHandler.flushPipesForTesting();

    assertTrue(!!AppManagementStore.getInstance().data.apps[app.id]);

    createSupportedLinksItemForApp(app);

    replaceBody(supportedLinksItem);
    await fakeHandler.flushPipesForTesting();
    flushTasks();

    let radioGroup =
        supportedLinksItem.shadowRoot!.querySelector('cr-radio-group');
    assertTrue(!!radioGroup);
    assertEquals('browser', radioGroup.selected);

    const preferredRadioButton =
        supportedLinksItem.shadowRoot!.querySelector<CrRadioButtonElement>(
            '#preferredRadioButton');
    assertTrue(!!preferredRadioButton);
    await preferredRadioButton.click();
    await fakeHandler.whenCalled('setPreferredApp');
    await flushTasks();

    const selectedApp = AppManagementStore.getInstance().data.apps[app.id];
    assertTrue(!!selectedApp);
    assertTrue(selectedApp.isPreferredApp);

    radioGroup = supportedLinksItem.shadowRoot!.querySelector('cr-radio-group');
    assertTrue(!!radioGroup);
    assertEquals('preferred', radioGroup.selected);
  });

  // TODO(crbug.com/40199350): Race condition when closing the dialog makes this
  // test flaky.
  test.skip('overlap dialog is shown and cancelled', async () => {
    const pwaOptions = {
      type: AppType.kWeb,
      isPreferredApp: false,
      supportedLinks: ['google.com'],
    };

    // Add PWA app, and make it the currently selected app.
    const app1 = await fakeHandler.addApp('app1', pwaOptions);
    await fakeHandler.addApp('app2', pwaOptions);
    fakeHandler.overlappingAppIds = ['app2'];

    AppManagementStore.getInstance().dispatch(updateSelectedAppId(app1.id));

    await fakeHandler.flushPipesForTesting();

    assertTrue(!!AppManagementStore.getInstance().data.apps[app1.id]);
    createSupportedLinksItemForApp(app1);
    replaceBody(supportedLinksItem);
    await fakeHandler.flushPipesForTesting();
    await flushTasks();

    // Pre-test checks
    let overlapDialog = supportedLinksItem.querySelector('#overlapDialog');
    assertNull(overlapDialog);

    const browserRadioButton =
        supportedLinksItem.shadowRoot!.querySelector<CrRadioButtonElement>(
            '#browserRadioButton');
    assertTrue(!!browserRadioButton);
    assertTrue(browserRadioButton.checked);

    // Open dialog
    const promise = fakeHandler.whenCalled('getOverlappingPreferredApps');
    const preferredRadioButton =
        supportedLinksItem.shadowRoot!.querySelector<CrRadioButtonElement>(
            '#preferredRadioButton');
    assertTrue(!!preferredRadioButton);
    await preferredRadioButton.click();
    await promise;
    await fakeHandler.flushPipesForTesting();
    await flushTasks();

    overlapDialog =
        supportedLinksItem.shadowRoot!.querySelector('#overlapDialog');
    assertTrue(!!overlapDialog);

    // Close dialog
    const cancelButton =
        overlapDialog.shadowRoot!.querySelector<HTMLButtonElement>('#cancel');
    assertTrue(!!cancelButton);
    cancelButton.click();
    await fakeHandler.flushPipesForTesting();
    await flushTasks();

    overlapDialog =
        supportedLinksItem.shadowRoot!.querySelector('#overlapDialog');
    assertNull(overlapDialog);

    const selectedApp = AppManagementStore.getInstance().data.apps[app1.id];
    assertTrue(!!selectedApp);
    assertFalse(selectedApp.isPreferredApp);
    const radioGroup =
        supportedLinksItem.shadowRoot!.querySelector('cr-radio-group');
    assertTrue(!!radioGroup);
    assertEquals('browser', radioGroup.selected);
  });

  test('overlap dialog is shown and accepted', async () => {
    const pwaOptions = {
      type: AppType.kWeb,
      isPreferredApp: false,
      supportedLinks: ['google.com'],
    };

    // Add PWA app, and make it the currently selected app.
    const app1 = await fakeHandler.addApp('app1', pwaOptions);
    await fakeHandler.addApp('app2', pwaOptions);
    fakeHandler.overlappingAppIds = ['app2'];

    AppManagementStore.getInstance().dispatch(updateSelectedAppId(app1.id));

    await fakeHandler.flushPipesForTesting();

    assertTrue(!!AppManagementStore.getInstance().data.apps[app1.id]);
    createSupportedLinksItemForApp(app1);
    replaceBody(supportedLinksItem);
    await fakeHandler.flushPipesForTesting();
    await flushTasks();

    // Pre-test checks
    assertNull(supportedLinksItem.querySelector('#overlapDialog'));
    const browserRadioButton =
        supportedLinksItem.shadowRoot!.querySelector<CrRadioButtonElement>(
            '#browserRadioButton');
    assertTrue(!!browserRadioButton);
    assertTrue(browserRadioButton.checked);

    // Open dialog
    let promise = fakeHandler.whenCalled('getOverlappingPreferredApps');
    const preferredRadioButton =
        supportedLinksItem.shadowRoot!.querySelector<CrRadioButtonElement>(
            '#preferredRadioButton');
    assertTrue(!!preferredRadioButton);
    await preferredRadioButton.click();
    await promise;
    await fakeHandler.flushPipesForTesting();
    await flushTasks();
    assertTrue(
        !!supportedLinksItem.shadowRoot!.querySelector('#overlapDialog'));

    // Accept change
    promise = fakeHandler.whenCalled('setPreferredApp');
    const overlapDialog = supportedLinksItem.shadowRoot!.querySelector<
        AppManagementSupportedLinksOverlappingAppsDialogElement>(
        '#overlapDialog');
    assertTrue(!!overlapDialog);
    overlapDialog.$.dialog.close();
    await promise;
    await fakeHandler.flushPipesForTesting();
    await flushTasks();

    assertNull(supportedLinksItem.shadowRoot!.querySelector('#overlapDialog'));

    const selectedApp = AppManagementStore.getInstance().data.apps[app1.id];
    assertTrue(!!selectedApp);
    assertTrue(selectedApp.isPreferredApp);
    const radioGroup =
        supportedLinksItem.shadowRoot!.querySelector('cr-radio-group');
    assertTrue(!!radioGroup);
    assertEquals('preferred', radioGroup.selected);
  });

  test('overlap warning isnt shown when not selected', async () => {
    const pwaOptions1 = {
      type: AppType.kWeb,
      isPreferredApp: true,
      supportedLinks: ['google.com', 'gmail.com'],
    };

    const pwaOptions2 = {
      type: AppType.kWeb,
      isPreferredApp: false,
      supportedLinks: ['google.com'],
    };

    // Add PWA app, and make it the currently selected app.
    const app1 = await fakeHandler.addApp('app1', pwaOptions1);
    await fakeHandler.addApp('app2', pwaOptions2);
    fakeHandler.overlappingAppIds = ['app2'];

    AppManagementStore.getInstance().dispatch(updateSelectedAppId(app1.id));
    await fakeHandler.flushPipesForTesting();

    assertTrue(!!AppManagementStore.getInstance().data.apps[app1.id]);
    createSupportedLinksItemForApp(app1);
    replaceBody(supportedLinksItem);
    await fakeHandler.flushPipesForTesting();
    await flushTasks();

    assertNull(supportedLinksItem.shadowRoot!.querySelector('#overlapWarning'));
  });

  test('overlap warning is shown', async () => {
    const pwaOptions1 = {
      type: AppType.kWeb,
      isPreferredApp: false,
      supportedLinks: ['google.com', 'gmail.com'],
    };

    const pwaOptions2 = {
      type: AppType.kWeb,
      isPreferredApp: true,
      supportedLinks: ['google.com'],
    };

    // Add PWA app, and make it the currently selected app.
    const app1 = await fakeHandler.addApp('app1', pwaOptions1);
    const app2 = await fakeHandler.addApp('app2', pwaOptions2);
    fakeHandler.overlappingAppIds = ['app2'];

    AppManagementStore.getInstance().dispatch(updateSelectedAppId(app1.id));
    await fakeHandler.flushPipesForTesting();
    await flushTasks();

    assertTrue(!!AppManagementStore.getInstance().data.apps[app1.id]);
    assertTrue(!!AppManagementStore.getInstance().data.apps[app2.id]);
    createSupportedLinksItemForApp(app1);
    replaceBody(supportedLinksItem);
    await fakeHandler.flushPipesForTesting();
    await flushTasks();

    assertTrue(
        !!supportedLinksItem.shadowRoot!.querySelector('#overlapWarning'));
  });
});

suite('AppManagementSupportedLinksItemElement', function() {
  let supportedLinksItem: AppManagementSupportedLinksItemElement;
  let apps: AppMap;

  setup(function() {
    clearBody();
    apps = {};
    setupFakeHandler();

    loadTimeData.resetForTesting({
      cancel: 'Cancel',
      close: 'Close',
      appManagementIntentSettingsDialogTitle: 'Supported Links',
      appManagementIntentSettingsTitle: '<a href="#">Supported Links</a>',
      appManagementIntentOverlapDialogTitle: 'Change as preferred app',
      appManagementIntentOverlapChangeButton: 'Change',
      appManagementIntentSharingOpenBrowserLabel: 'Open in Chrome browser',
      appManagementIntentSharingOpenAppLabel: 'Open in App',
      appManagementIntentSharingTabExplanation:
          'App is set to open in a new browser tab, supported links will also open in the browser.',
    });
  });

  async function setUpSupportedLinksComponent(
      id: string, optConfig?: AppConfig): Promise<App> {
    const app = createApp(id, optConfig);
    apps[app.id] = app;
    supportedLinksItem =
        document.createElement('app-management-supported-links-item');
    supportedLinksItem.app = app;
    supportedLinksItem.apps = apps;
    document.body.appendChild(supportedLinksItem);
    await waitAfterNextRender(supportedLinksItem);
    return app;
  }

  test('No supported links', async () => {
    const appOptions = {
      type: AppType.kWeb,
      isPreferredApp: true,
      supportedLinks: [],  // Explicitly empty.
    };
    await setUpSupportedLinksComponent('app1', appOptions);
    assertFalse(isVisible(supportedLinksItem));
  });

  test('Window/Tab mode', async () => {
    const appOptions = {
      type: AppType.kWeb,
      isPreferredApp: true,
      windowMode: WindowMode.kBrowser,
      supportedLinks: ['google.com'],
    };

    await setUpSupportedLinksComponent('app1', appOptions);
    assertTrue(!!supportedLinksItem.shadowRoot!.querySelector(
        '#disabledExplanationText'));

    const radioGroup =
        supportedLinksItem.shadowRoot!.querySelector<CrRadioGroupElement>(
            '#radioGroup');
    assertTrue(!!radioGroup);
    assertTrue(!!radioGroup.disabled);
  });

  test('can open and close supported links list dialog', async () => {
    const supportedLink = 'google.com';
    const appOptions = {
      type: AppType.kWeb,
      isPreferredApp: true,
      supportedLinks: [supportedLink],
    };

    await setUpSupportedLinksComponent('app1', appOptions);
    let supportedLinksDialog =
        supportedLinksItem.shadowRoot!.querySelector<HTMLElement>('#dialog');
    assertNull(supportedLinksDialog);

    // Open dialog.
    const heading = supportedLinksItem.shadowRoot!.querySelector('#heading');
    assertTrue(!!heading);
    const link = heading.shadowRoot!.querySelector('a');
    assertTrue(!!link);
    link.click();
    await flushTasks();

    supportedLinksDialog =
        supportedLinksItem.shadowRoot!.querySelector<HTMLElement>('#dialog');
    assertTrue(!!supportedLinksDialog);
    const innerDialog =
        supportedLinksDialog.shadowRoot!.querySelector<HTMLDialogElement>(
            '#dialog');
    assertTrue(!!innerDialog);
    assertTrue(innerDialog.open);

    // Confirm google.com shows up.
    const list = supportedLinksDialog.shadowRoot!.querySelector('#list');
    assertTrue(!!list);
    const item = list.getElementsByClassName('list-item')[0] as HTMLElement;
    assertTrue(!!item);
    assertEquals(supportedLink, item.innerText);

    // Close dialog.
    const closeButton =
        innerDialog.shadowRoot!.querySelector<HTMLButtonElement>('#close');
    assertTrue(!!closeButton);
    closeButton.click();
    await flushTasks();

    // Wait for the stamped dialog to be destroyed.
    await waitAfterNextRender(supportedLinksDialog);
    supportedLinksDialog =
        supportedLinksItem.shadowRoot!.querySelector('#dialog');
    assertNull(supportedLinksDialog);
  });
});