chromium/chrome/test/data/webui/app_settings/app_test.ts

// Copyright 2022 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://app-settings/web_app_settings.js';

import type {App, AppElement, PermissionItemElement, PermissionTypeIndex, SupportedLinksItemElement, SupportedLinksOverlappingAppsDialogElement, ToggleRowElement} from 'chrome://app-settings/web_app_settings.js';
import {AppType, BrowserProxy, createTriStatePermission, getPermissionValueBool, InstallReason, InstallSource, PermissionType, RunOnOsLoginMode, TriState, WindowMode} from 'chrome://app-settings/web_app_settings.js';
import type {CrIconButtonElement} from 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.js';
import type {CrRadioButtonElement} from 'chrome://resources/cr_elements/cr_radio_button/cr_radio_button.js';
import {getDeepActiveElement} from 'chrome://resources/js/util.js';
import {assertEquals, assertFalse, assertNull, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {keyDownOn} from 'chrome://webui-test/keyboard_mock_interactions.js';
import {eventToPromise, microtasksFinished} from 'chrome://webui-test/test_util.js';

import {TestAppManagementBrowserProxy} from './test_app_management_browser_proxy.js';

type AppConfig = Partial<App>;

suite('AppSettingsAppTest', () => {
  let appSettingsApp: AppElement;
  let app: App;
  let testProxy: TestAppManagementBrowserProxy;

  function createApp(id: string, optConfig?: AppConfig): App {
    const app: App = {
      id: id,
      type: AppType.kWeb,
      title: 'App Title',
      description: '',
      version: '5.1',
      size: '9.0MB',
      installReason: InstallReason.kUser,
      permissions: {},
      hideMoreSettings: false,
      hidePinToShelf: false,
      isPreferredApp: false,
      windowMode: WindowMode.kWindow,
      hideWindowMode: false,
      resizeLocked: false,
      hideResizeLocked: true,
      supportedLinks: [],
      runOnOsLogin: {loginMode: RunOnOsLoginMode.kNotRun, isManaged: false},
      fileHandlingState: {
        enabled: false,
        isManaged: false,
        userVisibleTypes: 'TXT',
        userVisibleTypesLabel: 'Supported type: TXT',
        learnMoreUrl: {url: 'https://google.com/'},
      },
      installSource: InstallSource.kUnknown,
      appSize: '',
      dataSize: '',
      publisherId: '',
      formattedOrigin: '',
      scopeExtensions: [],
      supportedLocales: [],
      isPinned: null,
      isPolicyPinned: null,
      selectedLocale: null,
      showSystemNotificationsSettingsLink: false,
      allowUninstall: true,
    };

    if (optConfig) {
      Object.assign(app, optConfig);
    }

    const permissionTypes = [
      PermissionType.kLocation,
      PermissionType.kNotifications,
      PermissionType.kMicrophone,
      PermissionType.kCamera,
    ];

    for (const permissionType of permissionTypes) {
      const permissionValue = TriState.kAsk;
      const isManaged = false;
      app.permissions[permissionType] =
          createTriStatePermission(permissionType, permissionValue, isManaged);
    }

    return app;
  }

  function fakeHandler() {
    return testProxy.fakeHandler;
  }

  function getSupportedLinksElement(): SupportedLinksItemElement|null {
    return appSettingsApp.shadowRoot!.querySelector<SupportedLinksItemElement>(
        'app-management-supported-links-item');
  }

  async function reloadPage() {
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    appSettingsApp = document.createElement('web-app-settings-app');
    document.body.appendChild(appSettingsApp);
    await microtasksFinished();
  }

  setup(async () => {
    app = createApp('test');
    testProxy = new TestAppManagementBrowserProxy(app);
    BrowserProxy.setInstance(testProxy);

    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    appSettingsApp = document.createElement('web-app-settings-app');
    document.body.appendChild(appSettingsApp);
    await microtasksFinished();
  });

  test('Elements are present', function() {
    assertEquals(
        appSettingsApp.shadowRoot!.querySelector('.cr-title-text')!.textContent,
        app.title);

    assertTrue(!!appSettingsApp.shadowRoot!.querySelector('#title-icon'));

    assertTrue(!!appSettingsApp.shadowRoot!.querySelector(
        'app-management-uninstall-button'));

    assertTrue(!!appSettingsApp.shadowRoot!.querySelector(
        'app-management-more-permissions-item'));
  });

  test('Toggle Run on OS Login', async function() {
    const runOnOsLoginItem = appSettingsApp.shadowRoot!.querySelector(
        'app-management-run-on-os-login-item')!;
    assertTrue(!!runOnOsLoginItem);
    assertEquals(
        runOnOsLoginItem.app.runOnOsLogin!.loginMode, RunOnOsLoginMode.kNotRun);

    runOnOsLoginItem.click();
    await eventToPromise('change', runOnOsLoginItem);
    assertEquals(
        runOnOsLoginItem.app.runOnOsLogin!.loginMode,
        RunOnOsLoginMode.kWindowed);

    runOnOsLoginItem.click();
    await eventToPromise('change', runOnOsLoginItem);
    assertEquals(
        runOnOsLoginItem.app.runOnOsLogin!.loginMode, RunOnOsLoginMode.kNotRun);
  });

  // Serves as a basic test of the presence of the File Handling item. More
  // comprehensive tests are located in the cross platform app_management test.
  test('Toggle File Handling', async function() {
    const fileHandlingItem = appSettingsApp.shadowRoot!.querySelector(
        'app-management-file-handling-item')!;
    assertTrue(!!fileHandlingItem);
    assertEquals(fileHandlingItem.app.fileHandlingState!.enabled, false);

    const toggleRow =
        fileHandlingItem.shadowRoot!.querySelector<ToggleRowElement>(
            '#toggle-row')!;
    assertTrue(!!toggleRow);
    toggleRow.click();
    await eventToPromise('change', toggleRow);
    assertEquals(fileHandlingItem.app.fileHandlingState!.enabled, true);

    toggleRow.click();
    await eventToPromise('change', toggleRow);
    assertEquals(fileHandlingItem.app.fileHandlingState!.enabled, false);
  });

  test('Toggle window mode', async function() {
    const windowModeItem =
        appSettingsApp.shadowRoot!.querySelector('app-management-window-mode-item')!;
    assertTrue(!!windowModeItem);
    assertEquals(windowModeItem.app.windowMode, WindowMode.kWindow);

    windowModeItem.click();
    await eventToPromise('change', windowModeItem);
    assertEquals(windowModeItem.app.windowMode, WindowMode.kBrowser);
  });

  test('Toggle permissions', async function() {
    const permsisionTypes: PermissionTypeIndex[] =
        ['kNotifications', 'kLocation', 'kCamera', 'kMicrophone'];
    for (const permissionType of permsisionTypes) {
      const permissionItem =
          appSettingsApp.shadowRoot!.querySelector<PermissionItemElement>(
              `app-management-permission-item[permission-type=${
                  permissionType}]`)!;
      assertTrue(!!permissionItem);
      assertFalse(getPermissionValueBool(permissionItem.app, permissionType));

      permissionItem.click();
      await eventToPromise('change', permissionItem);
      assertTrue(getPermissionValueBool(permissionItem.app, permissionType));

      permissionItem.click();
      await eventToPromise('change', permissionItem);
      assertFalse(getPermissionValueBool(permissionItem.app, permissionType));
    }
  });

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

    // Add PWA app, and make it the currently selected app.
    await fakeHandler().setApp(createApp('app1', appOptions));
    await fakeHandler().flushPipesForTesting();
    await reloadPage();

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

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

    const selectedApp = await fakeHandler().getApp('app1');
    assertTrue(!!selectedApp.app);
    assertFalse(selectedApp.app.isPreferredApp);

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

  test('supported links change browser -> preferred', async () => {
    const appOptions = {
      type: AppType.kWeb,
      isPreferredApp: false,
      supportedLinks: ['google.com'],
    };

    // Add PWA app, and make it the currently selected app.
    await fakeHandler().setApp(createApp('app1', appOptions));
    await fakeHandler().flushPipesForTesting();
    await reloadPage();

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

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

    const selectedApp = await fakeHandler().getApp('app1');
    assertTrue(!!selectedApp.app);
    assertTrue(selectedApp.app.isPreferredApp);

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

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

    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    // Add PWA app, and make it the currently selected app.
    await fakeHandler().setApp(createApp('app1', appOptions));
    await fakeHandler().addApp(createApp('app2', appOptions));
    fakeHandler().setOverlappingAppsForTesting(['app2']);
    await fakeHandler().flushPipesForTesting();
    await reloadPage();

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

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

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

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

    const selectedApp = await fakeHandler().getApp('app1');
    assertTrue(!!selectedApp.app);
    assertTrue(selectedApp.app.isPreferredApp);
    const radioGroup =
        getSupportedLinksElement()!.shadowRoot!.querySelector('cr-radio-group');
    assertTrue(!!radioGroup);
    assertEquals('preferred', radioGroup.selected);
  });

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

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

    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    // Add PWA app, and make it the currently selected app.
    await fakeHandler().setApp(createApp('app1', pwaOptions1));
    await fakeHandler().addApp(createApp('app2', pwaOptions2));
    fakeHandler().setOverlappingAppsForTesting(['app2']);
    await fakeHandler().flushPipesForTesting();
    await reloadPage();

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

  test('overlap warning is shown', async () => {
    // Since pwaOptions1 is not a preferred app, the overlap warning should be
    // shown.
    const pwaOptions1 = {
      type: AppType.kWeb,
      isPreferredApp: false,
      supportedLinks: ['google.com', 'gmail.com'],
    };

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

    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    // Add PWA app, and make it the currently selected app.
    await fakeHandler().setApp(createApp('app1', pwaOptions1));
    await fakeHandler().addApp(createApp('app2', pwaOptions2));
    fakeHandler().setOverlappingAppsForTesting(['app2']);
    await fakeHandler().flushPipesForTesting();
    await reloadPage();

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

  test('Origin URL is present in Permissions header', async () => {
    const appOptions = {
      type: AppType.kWeb,
      formattedOrigin: 'abc.com',
    };

    // Add PWA app, and make it the currently selected app.
    await fakeHandler().setApp(createApp('app1', appOptions));
    await fakeHandler().flushPipesForTesting();
    await reloadPage();

    assertEquals(
        appSettingsApp.shadowRoot!.querySelector(
                                      '.header-text')!.textContent!.trim(),
        'Permissions (abc.com)');
  });

  // Check that the app content element is not hidden when there are
  // scope_extensions entries.
  test('App Content element is present', async () => {
    const appOptions = {
      type: AppType.kWeb,
      scopeExtensions: ['*.abc.com', 'def.com', 'ghi.com'],
    };

    // Add PWA app, and make it the currently selected app.
    await fakeHandler().setApp(createApp('app1', appOptions));
    await fakeHandler().flushPipesForTesting();
    await reloadPage();

    const appContentItem = appSettingsApp.shadowRoot!.querySelector(
        'app-management-app-content-item')!;
    assertTrue(!!appContentItem);

    assertFalse(!!appContentItem.hidden);
  });

  // Check that the app content element is hidden when there are no
  // scope_extensions entries.
  test('App Content element is not present', async () => {
    const appOptions = {
      type: AppType.kWeb,
      scopeExtensions: [],
    };

    // Add PWA app, and make it the currently selected app.
    await fakeHandler().setApp(createApp('app1', appOptions));
    await fakeHandler().flushPipesForTesting();
    await reloadPage();

    const appContentItem = appSettingsApp.shadowRoot!.querySelector(
        'app-management-app-content-item')!;
    assertTrue(!!appContentItem);

    assertTrue(appContentItem.hidden);
  });

  test('App Content dialog is shown', async () => {
    const appOptions = {
      type: AppType.kWeb,
      scopeExtensions: ['*.abc.com', 'def.com', 'ghi.com'],
    };

    // Add PWA app, and make it the currently selected app.
    await fakeHandler().setApp(createApp('app1', appOptions));
    await fakeHandler().flushPipesForTesting();
    await reloadPage();

    const appContentItem = appSettingsApp.shadowRoot!.querySelector(
        'app-management-app-content-item')!;
    assertTrue(!!appContentItem);

    // Check that the dialog is not shown initially.
    assertFalse(appContentItem.showAppContentDialog);
    assertFalse(!!appContentItem.shadowRoot!.querySelector(
        'app-management-app-content-dialog'));

    const clickableAppContentElement =
        appContentItem.shadowRoot!.querySelector<HTMLElement>('#appContent')!;

    await clickableAppContentElement.click();

    // Check that the dialog is shown after clicking on the app content row.
    assertTrue(appContentItem.showAppContentDialog);
    const appContentDialogElement = appContentItem.shadowRoot!.querySelector(
        'app-management-app-content-dialog');
    assertTrue(!!appContentDialogElement);

    const dialog = appContentDialogElement.shadowRoot!.querySelector('#dialog');
    assertTrue(!!dialog);
    const closeButton =
        dialog.shadowRoot!.querySelector<CrIconButtonElement>('#close');
    assertTrue(!!closeButton);

    // Check that the focus stays on the close button.
    keyDownOn(appContentDialogElement, 0, undefined, 'Tab');
    assertEquals(getDeepActiveElement(), closeButton);
  });
});