chromium/chrome/test/data/webui/chromeos/settings/os_privacy_page/privacy_hub_app_permission_row_test.ts

// Copyright 2023 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 {appPermissionHandlerMojom, CrIconButtonElement, CrToggleElement, PrivacyHubSensorSubpageUserAction, setAppPermissionProviderForTesting, SettingsPrivacyHubAppPermissionRow} from 'chrome://os-settings/os_settings.js';
import {AppType, Permission, PermissionType, TriState} from 'chrome://resources/cr_components/app_management/app_management.mojom-webui.js';
import {PermissionTypeIndex} from 'chrome://resources/cr_components/app_management/permission_constants.js';
import {createTriStatePermission, isTriStateValue} from 'chrome://resources/cr_components/app_management/permission_util.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.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 {FakeMetricsPrivate} from '../fake_metrics_private.js';

import {FakeAppPermissionHandler} from './fake_app_permission_handler.js';
import {createFakeMetricsPrivate} from './privacy_hub_app_permission_test_util.js';

type App = appPermissionHandlerMojom.App;
type PermissionMap = Partial<Record<PermissionType, Permission>>;

suite('<settings-privacy-hub-app-permission-row>', () => {
  let fakeHandler: FakeAppPermissionHandler;
  let metrics: FakeMetricsPrivate;
  let testRow: SettingsPrivacyHubAppPermissionRow;
  let app: App;
  const permissionType: PermissionTypeIndex = 'kMicrophone';

  setup(() => {
    loadTimeData.overrideValues({
      isArcReadOnlyPermissionsEnabled: false,
    });

    fakeHandler = new FakeAppPermissionHandler();
    setAppPermissionProviderForTesting(fakeHandler);

    metrics = createFakeMetricsPrivate();

    testRow = document.createElement('settings-privacy-hub-app-permission-row');
    testRow.permissionType = permissionType;
    app = {
      id: 'test_app_id',
      name: 'test_app_name',
      type: AppType.kWeb,
      permissions: {},
    };
    app.permissions[PermissionType[permissionType]] = createTriStatePermission(
        PermissionType[permissionType], TriState.kAsk, /*is_managed=*/ false);
    testRow.app = app;
    document.body.appendChild(testRow);
    flush();
  });

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

  async function changePermissionValue(value: TriState): Promise<void> {
    const permissions: PermissionMap = {};
    permissions[PermissionType[permissionType]] = createTriStatePermission(
        PermissionType[permissionType], value, /*is_managed=*/ false);
    testRow.set('app.permissions', permissions);
    await waitAfterNextRender(testRow);
  }

  function getAppName(): string {
    return testRow.shadowRoot!.querySelector('#appName')!.textContent!.trim();
  }

  function getPermissionText(): string {
    return testRow.shadowRoot!.querySelector(
                                  '#permissionText')!.textContent!.trim();
  }

  function getPermissionToggle(): CrToggleElement {
    return testRow.shadowRoot!.querySelector<CrToggleElement>(
        '#permissionToggle')!;
  }

  test('Displays the app data appropriately', () => {
    assertEquals('test_app_name', getAppName());
    assertEquals(
        testRow.i18n('appManagementPermissionAsk'), getPermissionText());
    assertFalse(getPermissionToggle().checked);
  });

  test('Changing permission changes the subtext and toggle', async () => {
    const triStateDescription:
        {[key in TriState]: {text: string, isEnabled: boolean}} = {
          [TriState.kAllow]: {
            text: testRow.i18n('appManagementPermissionAllowed'),
            isEnabled: true,
          },
          [TriState.kAsk]: {
            text: testRow.i18n('appManagementPermissionAsk'),
            isEnabled: false,
          },
          [TriState.kBlock]: {
            text: testRow.i18n('appManagementPermissionDenied'),
            isEnabled: false,
          },
        };

    const permissionValues = [
      TriState.kBlock,
      TriState.kAllow,
      TriState.kAsk,
      TriState.kAllow,
      TriState.kBlock,
      TriState.kAsk,
    ];

    for (let i = 0; i < permissionValues.length; ++i) {
      const value = permissionValues[i]!;
      await changePermissionValue(value);
      assertEquals(triStateDescription[value].text, getPermissionText());
      assertEquals(
          triStateDescription[value].isEnabled, getPermissionToggle().checked);
    }
  });

  test('Type of permission changed is correct', async () => {
    assertEquals(
        PermissionType.kUnknown,
        fakeHandler.getLastUpdatedPermission().permissionType);

    testRow.click();
    await fakeHandler.whenCalled('setPermission');

    const updatedPermission = fakeHandler.getLastUpdatedPermission();
    assertEquals(
        permissionType, PermissionType[updatedPermission.permissionType]);
    assertTrue(isTriStateValue(updatedPermission.value));
    assertEquals(TriState.kAllow, updatedPermission.value.tristateValue);
  });

  function getPermissionChangeCount(): number {
    return metrics.countMetricValue(
        'ChromeOS.PrivacyHub.MicrophoneSubpage.UserAction',
        PrivacyHubSensorSubpageUserAction.APP_PERMISSION_CHANGED);
  }

  test('Clicking on the toggle button triggers permission change', async () => {
    assertEquals(0, getPermissionChangeCount());

    getPermissionToggle().click();
    await fakeHandler.whenCalled('setPermission');

    assertEquals(1, getPermissionChangeCount());
  });

  test('Clicking anywhere on the row triggers permission change', async () => {
    assertEquals(0, getPermissionChangeCount());

    testRow.click();
    await fakeHandler.whenCalled('setPermission');

    assertEquals(1, getPermissionChangeCount());
  });

  test('Toggle button reacts to Enter and Space keyboard events', async () => {
    const keyBoardEvents = [
      {
        event: new KeyboardEvent('keydown', {key: 'Enter'}),
        shouldTogglePermission: true,
      },
      {
        event: new KeyboardEvent('keyup', {key: 'Enter'}),
        shouldTogglePermission: false,
      },
      {
        event: new KeyboardEvent('keydown', {key: ' '}),
        shouldTogglePermission: false,
      },
      {
        event: new KeyboardEvent('keyup', {key: ' '}),
        shouldTogglePermission: true,
      },
    ];

    let changeCount = 0;

    for (const e of keyBoardEvents) {
      assertEquals(changeCount, getPermissionChangeCount());

      getPermissionToggle().dispatchEvent(e.event);
      if (e.shouldTogglePermission) {
        changeCount++;
      }

      await flushTasks();

      assertEquals(changeCount, getPermissionChangeCount());
    }
  });

  function isPermissionManaged(): boolean {
    const permission = app.permissions[PermissionType[permissionType]];
    assertTrue(!!permission);
    return permission.isManaged;
  }


  test('Managed icon displayed when permission is managed', () => {
    assertFalse(isPermissionManaged());
    assertNull(testRow.shadowRoot!.querySelector('cr-policy-indicator'));
    assertFalse(getPermissionToggle().disabled);

    // Toggle managed state.
    testRow.set(
        'app.permissions.' + PermissionType[permissionType] + '.isManaged',
        true);
    flush();

    assertTrue(isPermissionManaged());
    assertTrue(!!testRow.shadowRoot!.querySelector('cr-policy-indicator'));
    assertTrue(getPermissionToggle().disabled);
  });

  test('Clicking on the row is no-op when permission is managed', async () => {
    assertFalse(isPermissionManaged());

    // Toggle managed state.
    testRow.set(
        'app.permissions.' + PermissionType[permissionType] + '.isManaged',
        true);
    flush();

    assertTrue(isPermissionManaged());
    assertEquals(0, getPermissionChangeCount());

    testRow.click();
    await flushTasks();

    assertEquals(0, getPermissionChangeCount());
  });

  function getAndroidSettingsLinkButton(): CrIconButtonElement|null {
    return testRow.shadowRoot!.querySelector('cr-icon-button');
  }

  test('Link to android settings displayed', async () => {
    assertFalse(isVisible(getAndroidSettingsLinkButton()));

    loadTimeData.overrideValues({
      isArcReadOnlyPermissionsEnabled: true,
    });
    testRow.set('app.type', AppType.kArc);
    flush();

    assertTrue(isVisible(getAndroidSettingsLinkButton()));
  });

  test('Android settings link click metric recorded', async () => {
    loadTimeData.overrideValues({
      isArcReadOnlyPermissionsEnabled: true,
    });
    testRow.set('app.type', AppType.kArc);
    flush();

    assertEquals(
        0,
        metrics.countMetricValue(
            'ChromeOS.PrivacyHub.MicrophoneSubpage.UserAction',
            PrivacyHubSensorSubpageUserAction.ANDROID_SETTINGS_LINK_CLICKED));

    testRow.click();
    await fakeHandler.whenCalled('openNativeSettings');

    assertEquals(
        1,
        metrics.countMetricValue(
            'ChromeOS.PrivacyHub.MicrophoneSubpage.UserAction',
            PrivacyHubSensorSubpageUserAction.ANDROID_SETTINGS_LINK_CLICKED));
  });

  test(
      'Clicking the android settings link opens android settings', async () => {
        loadTimeData.overrideValues({
          isArcReadOnlyPermissionsEnabled: true,
        });
        testRow.set('app.type', AppType.kArc);
        flush();

        assertEquals(0, fakeHandler.getNativeSettingsOpenedCount());

        const linkButton = getAndroidSettingsLinkButton();
        assertTrue(!!linkButton);
        linkButton.click();
        await fakeHandler.whenCalled('openNativeSettings');

        assertEquals(1, fakeHandler.getNativeSettingsOpenedCount());
      });

  test('Clicking anywhere on the row opens android settings', async () => {
    loadTimeData.overrideValues({
      isArcReadOnlyPermissionsEnabled: true,
    });
    testRow.set('app.type', AppType.kArc);
    flush();

    assertEquals(0, fakeHandler.getNativeSettingsOpenedCount());

    testRow.click();
    await fakeHandler.whenCalled('openNativeSettings');

    assertEquals(1, fakeHandler.getNativeSettingsOpenedCount());
  });
});