chromium/chrome/test/data/webui/chromeos/settings/os_privacy_page/privacy_hub_geolocation_subpage_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 {PrivacyHubBrowserProxyImpl, SettingsPrivacyHubGeolocationSubpage} from 'chrome://os-settings/lazy_load.js';
import {appPermissionHandlerMojom, ControlledButtonElement, ControlledRadioButtonElement, CrDialogElement, CrLinkRowElement, CrTooltipIconElement, GeolocationAccessLevel, OpenWindowProxyImpl, Router, routes, ScheduleType, setAppPermissionProviderForTesting, SettingsPrivacyHubSystemServiceRow} from 'chrome://os-settings/os_settings.js';
import {CrButtonElement} from 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import {PermissionType, TriState} from 'chrome://resources/cr_components/app_management/app_management.mojom-webui.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {DomRepeat} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {assertEquals, assertLT, assertNotReached, assertNull, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {flushTasks} from 'chrome://webui-test/polymer_test_util.js';
import {TestOpenWindowProxy} from 'chrome://webui-test/test_open_window_proxy.js';

import {FakeMetricsPrivate} from '../fake_metrics_private.js';

import {FakeAppPermissionHandler} from './fake_app_permission_handler.js';
import {createApp, createFakeMetricsPrivate, getSystemServiceName, getSystemServicePermissionText, getSystemServicesFromSubpage} from './privacy_hub_app_permission_test_util.js';
import {TestPrivacyHubBrowserProxy} from './test_privacy_hub_browser_proxy.js';

type App = appPermissionHandlerMojom.App;

suite('<settings-privacy-hub-geolocation-subpage>', () => {
  let fakeHandler: FakeAppPermissionHandler;
  let metrics: FakeMetricsPrivate;
  let privacyHubGeolocationSubpage: SettingsPrivacyHubGeolocationSubpage;
  let privacyHubBrowserProxy: TestPrivacyHubBrowserProxy;
  let openWindowProxy: TestOpenWindowProxy;

  async function initPage() {
    privacyHubGeolocationSubpage =
        document.createElement('settings-privacy-hub-geolocation-subpage');
    const prefs = {
      ash: {
        user: {
          geolocation_access_level: {
            key: 'ash.user.geolocation_access_level',
            type: chrome.settingsPrivate.PrefType.NUMBER,
            value: GeolocationAccessLevel.ALLOWED,
          },
        },
        dark_mode: {
          schedule_type: {
            key: 'ash.user.dark_mode.schedule_type',
            type: chrome.settingsPrivate.PrefType.NUMBER,
            value: ScheduleType.SUNSET_TO_SUNRISE,
          },
        },
        night_light: {
          schedule_type: {
            key: 'ash.night_light.schedule_type',
            type: chrome.settingsPrivate.PrefType.NUMBER,
            value: ScheduleType.SUNSET_TO_SUNRISE,
          },
        },
      },
      generated: {
        resolve_timezone_by_geolocation_on_off: {
          key: 'generated.resolve_timezone_by_geolocation_on_off',
          type: chrome.settingsPrivate.PrefType.BOOLEAN,
          value: true,
        },
      },
      settings: {
        ambient_mode: {
          enabled: {
            value: true,
          },
        },
      },
    };
    privacyHubGeolocationSubpage.prefs = prefs;
    document.body.appendChild(privacyHubGeolocationSubpage);
    await flushTasks();
  }


  setup(() => {
    fakeHandler = new FakeAppPermissionHandler();
    setAppPermissionProviderForTesting(fakeHandler);
    metrics = createFakeMetricsPrivate();
    privacyHubBrowserProxy = new TestPrivacyHubBrowserProxy();
    PrivacyHubBrowserProxyImpl.setInstanceForTesting(privacyHubBrowserProxy);
    openWindowProxy = new TestOpenWindowProxy();
    OpenWindowProxyImpl.setInstance(openWindowProxy);

    Router.getInstance().navigateTo(routes.PRIVACY_HUB_GEOLOCATION);
  });

  teardown(() => {
    privacyHubGeolocationSubpage.remove();
    Router.getInstance().resetRouteForTesting();
    openWindowProxy.reset();
  });

  function histogram(): string {
    return 'ChromeOS.PrivacyHub.Geolocation.AccessLevelChanged.SystemSettings';
  }

  function getGeolocationLabel(): string {
    return privacyHubGeolocationSubpage.shadowRoot!
        .querySelector<HTMLDivElement>('#geolocationStatus')!.innerText;
  }

  function getGeolocationSubLabel(): string {
    return privacyHubGeolocationSubpage.shadowRoot!
        .querySelector<HTMLDivElement>(
            '#geolocationStatusDescription')!.innerText;
  }

  async function setGeolocationAccessLevelPref(
      accessLevel: GeolocationAccessLevel) {
    privacyHubGeolocationSubpage.prefs.ash.user.geolocation_access_level.value =
        accessLevel;
    privacyHubGeolocationSubpage.notifyPath(
        'prefs.ash.user.geolocation_access_level', accessLevel);
    await flushTasks();
  }

  function getGeolocationAccessLevel(): GeolocationAccessLevel {
    const geolocationAccessLevel = getGeolocationLabel();
    assertTrue(!!geolocationAccessLevel);

    switch (geolocationAccessLevel) {
      case privacyHubGeolocationSubpage.i18n(
          'geolocationAccessLevelDisallowed'):
        return GeolocationAccessLevel.DISALLOWED;
      case privacyHubGeolocationSubpage.i18n('geolocationAccessLevelAllowed'):
        return GeolocationAccessLevel.ALLOWED;
      case privacyHubGeolocationSubpage.i18n(
          'geolocationAccessLevelOnlyAllowedForSystem'):
        return GeolocationAccessLevel.ONLY_ALLOWED_FOR_SYSTEM;
    }

    assertNotReached('Invalid GeolocationAccessLevel value detected');
  }

  function getNoAppHasAccessTextSection(): HTMLElement|null {
    return privacyHubGeolocationSubpage.shadowRoot!.querySelector(
        '#noAppHasAccessText');
  }

  function getAppList(): DomRepeat|null {
    return privacyHubGeolocationSubpage.shadowRoot!.querySelector('#appList');
  }

  function getNthAppName(i: number): string|null {
    const appPermissionRows =
        privacyHubGeolocationSubpage.shadowRoot!.querySelectorAll(
            '#appsSection > div > settings-privacy-hub-app-permission-row');
    assertLT(i, appPermissionRows.length);

    return appPermissionRows[i]!.shadowRoot!.querySelector(
                                                '#appName')!.textContent;
  }

  interface TextOptions {
    notConfiguredText: string;
    allowedText: string;
    blockedText: string;
  }

  function checkService(
      systemService: SettingsPrivacyHubSystemServiceRow, expectedName: string,
      isConfiguredToUseGeolocation: boolean,
      expectedDescriptions: TextOptions) {
    // Check  service name.
    assertEquals(expectedName, getSystemServiceName(systemService));

    // Check subtext:
    // Check when the system service is not configured to use geolocation (e.g.
    // time zone is selected from the static list).
    if (!isConfiguredToUseGeolocation) {
      assertEquals(
          expectedDescriptions['notConfiguredText'],
          getSystemServicePermissionText(systemService));
      return;
    }
    // Check when the system service is using geolocation.
    switch (getGeolocationAccessLevel()) {
      case GeolocationAccessLevel.DISALLOWED:
        assertEquals(
            expectedDescriptions['blockedText'],
            getSystemServicePermissionText(systemService));
        break;
      case GeolocationAccessLevel.ALLOWED:
        // Falls through to ONLY_ALLOWED_FOR_SYSTEM
      case GeolocationAccessLevel.ONLY_ALLOWED_FOR_SYSTEM:
        assertEquals(
            expectedDescriptions['allowedText'],
            getSystemServicePermissionText(systemService));
        break;
    }
  }

  async function setAutomaticTimeZoneEnabled(enabled: boolean) {
    privacyHubGeolocationSubpage.set(
        'prefs.generated.resolve_timezone_by_geolocation_on_off.value',
        enabled);
    await flushTasks();
  }

  async function setNightLightScheduleType(scheduleType: ScheduleType) {
    privacyHubGeolocationSubpage.set(
        'prefs.ash.night_light.schedule_type.value', scheduleType);
    await flushTasks();
  }

  async function setLocalWeatherEnabled(enabled: boolean) {
    privacyHubGeolocationSubpage.set(
        'prefs.settings.ambient_mode.enabled.value', enabled);
    await flushTasks();
  }

  async function setDarkThemeScheduleType(scheduleType: ScheduleType) {
    privacyHubGeolocationSubpage.set(
        'prefs.ash.dark_mode.schedule_type.value', scheduleType);
    await flushTasks();
  }


  async function checkServiceSection() {
    assertEquals(
        privacyHubGeolocationSubpage.i18n(
            'privacyHubSystemServicesSectionTitle'),
        privacyHubGeolocationSubpage.shadowRoot!
            .querySelector<HTMLElement>(
                '#systemServicesSectionTitle')!.innerText!.trim());

    const systemServices =
        getSystemServicesFromSubpage(privacyHubGeolocationSubpage);
    assertEquals(4, systemServices.length);

    for (const timeZoneAutomaticSetting of [true, false]) {
      await setAutomaticTimeZoneEnabled(timeZoneAutomaticSetting);

      checkService(
          systemServices[0]!,
          privacyHubGeolocationSubpage.i18n(
              'privacyHubSystemServicesAutomaticTimeZoneName'),
          timeZoneAutomaticSetting, {
            notConfiguredText: privacyHubGeolocationSubpage.i18n(
                'privacyHubSystemServicesGeolocationNotConfigured'),
            allowedText: privacyHubGeolocationSubpage.i18n(
                'privacyHubSystemServicesAllowedText'),
            blockedText: 'Blocked. Time zone is currently set to ' +
                'Test Time Zone' +
                ' and can only be updated manually.',
          });
    }

    const allScheduleTypes: ScheduleType[] =
        Object.values(ScheduleType)
            .filter(value => typeof value === 'number') as ScheduleType[];
    // Test Night Light
    for (const scheduleType of allScheduleTypes) {
      await setNightLightScheduleType(scheduleType as ScheduleType);

      checkService(
          systemServices[1]!,
          privacyHubGeolocationSubpage.i18n(
              'privacyHubSystemServicesSunsetScheduleName'),
          scheduleType === ScheduleType.SUNSET_TO_SUNRISE, {
            notConfiguredText: privacyHubGeolocationSubpage.i18n(
                'privacyHubSystemServicesGeolocationNotConfigured'),
            allowedText: privacyHubGeolocationSubpage.i18n(
                'privacyHubSystemServicesAllowedText'),
            blockedText:
                'Blocked. Schedule is currently set to 7:00AM - 8:00PM' +
                ' and can only be updated manually.',
          });
    }

    // Test Dark Theme
    for (const scheduleType of allScheduleTypes) {
      await setDarkThemeScheduleType(scheduleType as ScheduleType);

      checkService(
          systemServices[2]!,
          privacyHubGeolocationSubpage.i18n(
              'privacyHubSystemServicesDarkThemeName'),
          scheduleType === ScheduleType.SUNSET_TO_SUNRISE, {
            notConfiguredText: privacyHubGeolocationSubpage.i18n(
                'privacyHubSystemServicesGeolocationNotConfigured'),
            allowedText: privacyHubGeolocationSubpage.i18n(
                'privacyHubSystemServicesAllowedText'),
            blockedText:
                'Blocked. Schedule is currently set to 7:00AM - 8:00PM' +
                ' and can only be updated manually.',
          });
    }

    // Test Local Weather
    for (const localWeatherEnabled of [true, false]) {
      await setLocalWeatherEnabled(localWeatherEnabled);

      checkService(
          systemServices[3]!,
          privacyHubGeolocationSubpage.i18n(
              'privacyHubSystemServicesLocalWeatherName'),
          localWeatherEnabled, {
            notConfiguredText: privacyHubGeolocationSubpage.i18n(
                'privacyHubSystemServicesGeolocationNotConfigured'),
            allowedText: privacyHubGeolocationSubpage.i18n(
                'privacyHubSystemServicesAllowedText'),
            blockedText: 'Blocked',
          });
    }
  }

  test('Geolocation sub-label updates on location change', async () => {
    await initPage();

    // Check "Allowed"
    assertTrue(getGeolocationAccessLevel() === GeolocationAccessLevel.ALLOWED);
    assertEquals(
        privacyHubGeolocationSubpage.i18n('geolocationAccessLevelAllowed'),
        getGeolocationLabel());
    assertEquals(
        privacyHubGeolocationSubpage.i18n('geolocationAllowedModeDescription'),
        getGeolocationSubLabel());

    // Check "Allowed For System Services"
    setGeolocationAccessLevelPref(
        GeolocationAccessLevel.ONLY_ALLOWED_FOR_SYSTEM);
    assertTrue(
        getGeolocationAccessLevel() ===
        GeolocationAccessLevel.ONLY_ALLOWED_FOR_SYSTEM);
    assertEquals(
        privacyHubGeolocationSubpage.i18n(
            'geolocationAccessLevelOnlyAllowedForSystem'),
        getGeolocationLabel());
    assertEquals(
        privacyHubGeolocationSubpage.i18n(
            'geolocationOnlyAllowedForSystemModeDescription'),
        getGeolocationSubLabel());

    // Check "Blocked for all"
    setGeolocationAccessLevelPref(GeolocationAccessLevel.DISALLOWED);
    assertTrue(
        getGeolocationAccessLevel() === GeolocationAccessLevel.DISALLOWED);
    assertEquals(
        privacyHubGeolocationSubpage.i18n('geolocationAccessLevelDisallowed'),
        getGeolocationLabel());
    assertEquals(
        privacyHubGeolocationSubpage.i18n('geolocationBlockedModeDescription'),
        getGeolocationSubLabel());
  });

  function getGeolocationSelectorDialog(): CrDialogElement|null {
    return privacyHubGeolocationSubpage.shadowRoot!
        .querySelector<CrDialogElement>('#systemGeolocationDialog');
  }

  async function selectGeolocationAccessLevelInDialog(
      accessLevel: GeolocationAccessLevel) {
    // Exhaustive list of radio button id's of the dialog mapped to its
    // corresponding  geolocation access level.
    const accessLevelToRadioButtonIdMap = {
      [GeolocationAccessLevel.ALLOWED]: 'systemGeolocationAccessAllowedForAll',
      [GeolocationAccessLevel.ONLY_ALLOWED_FOR_SYSTEM]:
          'systemGeolocationAccessOnlyAllowedForSystem',
      [GeolocationAccessLevel.DISALLOWED]: 'systemGeolocationAccessBlocked',
    };
    assertEquals(
        GeolocationAccessLevel.MAX_VALUE + 1,
        Object.keys(accessLevelToRadioButtonIdMap).length);

    // Click change button.
    const changeGeolocationButton =
        privacyHubGeolocationSubpage.shadowRoot!.querySelector<CrButtonElement>(
            '#changeAccessButton');
    assertTrue(!!changeGeolocationButton, 'Change button is missing');
    changeGeolocationButton.click();
    await flushTasks();

    // Check that geolocation selector dialog appears.
    const geolocationSelectorDialog = getGeolocationSelectorDialog();
    assertTrue(
        !!geolocationSelectorDialog, 'Geolocation selector dialog is missing');

    // Check that the Dialog radio-buttons represent all access levels.
    const radioButtonList =
        geolocationSelectorDialog.querySelectorAll('controlled-radio-button');
    assertEquals(
        GeolocationAccessLevel.MAX_VALUE + 1, radioButtonList.length,
        'Radio buttons don\'t match with the access levels');

    // Select the intended radio button and click submit.
    const radioButton: ControlledRadioButtonElement =
        Array.from(radioButtonList)
            .find(
                (elem: ControlledRadioButtonElement) =>
                    elem.id === accessLevelToRadioButtonIdMap[accessLevel])!;
    radioButton.click();
    await flushTasks();
    const confirmButton: CrButtonElement =
        geolocationSelectorDialog.querySelector<CrButtonElement>(
            '#confirmButton')!;
    confirmButton.click();
    await flushTasks();

    // Check that the dialog disappears.
    assertNull(
        getGeolocationSelectorDialog(),
        'Geolocation selector dialog is not dismissed');
  }

  test('Select geolocation access level from dialog', async () => {
    await initPage();

    // Geolocation selector dialog should be hidden.
    assertNull(getGeolocationSelectorDialog());

    for (const accessLevel of Object.values(GeolocationAccessLevel)
             .filter(v => typeof v === 'number')) {
      await selectGeolocationAccessLevelInDialog(accessLevel);
      assertEquals(
          accessLevel,
          privacyHubGeolocationSubpage
              .getPref('ash.user.geolocation_access_level')
              .value,
          'Preference didn\'t update');
    }
  });

  test('App list displayed when geolocation allowed', async () => {
    await initPage();
    assertEquals(
        privacyHubGeolocationSubpage.i18n('privacyHubAppsSectionTitle'),
        privacyHubGeolocationSubpage.shadowRoot!
            .querySelector<HTMLElement>(
                '#appsSectionTitle')!.innerText!.trim());
    assertTrue(!!getAppList());
    assertNull(getNoAppHasAccessTextSection());
  });

  test('App list not displayed when geolocation not allowed', async () => {
    await initPage();
    // Disable geolocation access.
    setGeolocationAccessLevelPref(GeolocationAccessLevel.DISALLOWED);

    assertNull(getAppList());
    assertTrue(!!getNoAppHasAccessTextSection());
    assertEquals(
        privacyHubGeolocationSubpage.i18n('noAppCanUseGeolocationText'),
        getNoAppHasAccessTextSection()!.innerText!.trim());

    // Setting location permission to "Only allowed for system" should have
    // similar effect.
    setGeolocationAccessLevelPref(
        GeolocationAccessLevel.ONLY_ALLOWED_FOR_SYSTEM);
    assertNull(getAppList());
    assertTrue(!!getNoAppHasAccessTextSection());
    assertEquals(
        privacyHubGeolocationSubpage.i18n('noAppCanUseGeolocationText'),
        getNoAppHasAccessTextSection()!.innerText!.trim());
  });

  function initializeObserver(): Promise<void> {
    return fakeHandler.whenCalled('addObserver');
  }

  function simulateAppUpdate(app: App): void {
    fakeHandler.getObserverRemote().onAppUpdated(app);
  }

  function simulateAppRemoval(id: string): void {
    fakeHandler.getObserverRemote().onAppRemoved(id);
  }

  test('AppList displays all apps with geolocation permission', async () => {
    await initPage();
    const app1 = createApp(
        'app1_id', 'app1_name', PermissionType.kLocation, TriState.kAllow);
    const app2 = createApp(
        'app2_id', 'app2_name', PermissionType.kMicrophone, TriState.kAllow);
    const app3 = createApp(
        'app3_id', 'app3_name', PermissionType.kLocation, TriState.kAsk);
    const app4 = createApp(
        'app4_id', 'app4_name', PermissionType.kCamera, TriState.kBlock);


    await initializeObserver();
    simulateAppUpdate(app1);
    simulateAppUpdate(app2);
    simulateAppUpdate(app3);
    simulateAppUpdate(app4);
    await flushTasks();

    assertEquals(2, getAppList()!.items!.length);
  });

  test('Removed app are removed from appList', async () => {
    await initPage();
    const app1 = createApp(
        'app1_id', 'app1_name', PermissionType.kLocation, TriState.kAllow);
    const app2 = createApp(
        'app2_id', 'app2_name', PermissionType.kMicrophone, TriState.kAllow);

    await initializeObserver();
    simulateAppUpdate(app1);
    simulateAppUpdate(app2);
    await flushTasks();

    assertEquals(1, getAppList()!.items!.length);

    simulateAppRemoval(app2.id);
    await flushTasks();

    assertEquals(1, getAppList()!.items!.length);

    simulateAppRemoval(app1.id);
    await flushTasks();

    assertEquals(0, getAppList()!.items!.length);
  });

  test('AppList is alphabetically sorted', async () => {
    await initPage();
    const app1 = createApp(
        'app1_id', 'app1_name', PermissionType.kLocation, TriState.kAllow);
    const app2 = createApp(
        'app2_id', 'app2_name', PermissionType.kLocation, TriState.kAsk);
    const app3 = createApp(
        'app3_id', 'app3_name', PermissionType.kLocation, TriState.kAsk);
    const app4 = createApp(
        'app4_id', 'app4_name', PermissionType.kLocation, TriState.kBlock);


    await initializeObserver();
    simulateAppUpdate(app3);
    simulateAppUpdate(app1);
    simulateAppUpdate(app4);
    simulateAppUpdate(app2);
    await flushTasks();

    assertEquals(4, getAppList()!.items!.length);
    assertEquals(app1.name, getNthAppName(0));
    assertEquals(app2.name, getNthAppName(1));
    assertEquals(app3.name, getNthAppName(2));
    assertEquals(app4.name, getNthAppName(3));
  });

  test('Metric recorded when clicked', async () => {
    await initPage();

    assertEquals(
        0,
        metrics.countMetricValue(
            histogram(), GeolocationAccessLevel.DISALLOWED));
    assertEquals(
        0,
        metrics.countMetricValue(histogram(), GeolocationAccessLevel.ALLOWED));
    assertEquals(
        0,
        metrics.countMetricValue(
            histogram(), GeolocationAccessLevel.ONLY_ALLOWED_FOR_SYSTEM));

    // Default access level should be ALLOWED.
    assertEquals(GeolocationAccessLevel.ALLOWED, getGeolocationAccessLevel());

    // Change dropdown and check the corresponding metric is recorded.
    await selectGeolocationAccessLevelInDialog(
        GeolocationAccessLevel.DISALLOWED);
    assertEquals(
        1,
        metrics.countMetricValue(
            histogram(), GeolocationAccessLevel.DISALLOWED));

    // Change dropdown and check the corresponding metric is recorded.
    await selectGeolocationAccessLevelInDialog(
        GeolocationAccessLevel.ONLY_ALLOWED_FOR_SYSTEM);
    assertEquals(
        1,
        metrics.countMetricValue(
            histogram(), GeolocationAccessLevel.ONLY_ALLOWED_FOR_SYSTEM));

    // Change dropdown and check the corresponding metric is recorded.
    await selectGeolocationAccessLevelInDialog(GeolocationAccessLevel.ALLOWED);
    assertEquals(
        1,
        metrics.countMetricValue(histogram(), GeolocationAccessLevel.ALLOWED));
  });

  function getManagePermissionsInChromeRow(): CrLinkRowElement|null {
    return privacyHubGeolocationSubpage.shadowRoot!
        .querySelector<CrLinkRowElement>('#managePermissionsInChromeRow');
  }

  function getNoWebsiteHasAccessTextRow(): HTMLElement|null {
    return privacyHubGeolocationSubpage.shadowRoot!.querySelector<HTMLElement>(
        '#noWebsiteHasAccessText');
  }

  test('Websites section texts', async () => {
    await initPage();
    assertEquals(
        privacyHubGeolocationSubpage.i18n('websitesSectionTitle'),
        privacyHubGeolocationSubpage.shadowRoot!
            .querySelector<HTMLElement>(
                '#websitesSectionTitle')!.innerText!.trim()),
        'problem 1';

    assertEquals(
        privacyHubGeolocationSubpage.i18n(
            'manageLocationPermissionsInChromeText'),
        getManagePermissionsInChromeRow()!.label, 'problem 2');

    // Disable geolocation access.
    setGeolocationAccessLevelPref(GeolocationAccessLevel.DISALLOWED);
    assertEquals(
        privacyHubGeolocationSubpage.i18n('noWebsiteCanUseLocationText'),
        getNoWebsiteHasAccessTextRow()!.innerText!.trim(), 'problem 3');

    // Setting location to "only allowed for system services" should have same
    // effect as disabling.
    setGeolocationAccessLevelPref(
        GeolocationAccessLevel.ONLY_ALLOWED_FOR_SYSTEM);
    assertEquals(
        privacyHubGeolocationSubpage.i18n('noWebsiteCanUseLocationText'),
        getNoWebsiteHasAccessTextRow()!.innerText!.trim(), 'problem 4');
  });

  test(
      'Websites section with external link to Chrome Settings is shown when ' +
          'location is allowed',
      async () => {
        await initPage();
        // Geolocation is set to Allowed by default.
        assertEquals(
            GeolocationAccessLevel.ALLOWED, getGeolocationAccessLevel());

        // Check that the external link is present
        assertTrue(!!getManagePermissionsInChromeRow());
        assertNull(getNoWebsiteHasAccessTextRow());
      });

  test(
      'Clicking Chrome row opens Chrome browser location permission settings',
      async () => {
        await initPage();

        assertEquals(
            PermissionType.kUnknown,
            fakeHandler.getLastOpenedBrowserPermissionSettingsType());

        getManagePermissionsInChromeRow()!.click();
        await fakeHandler.whenCalled('openBrowserPermissionSettings');

        assertEquals(
            PermissionType.kLocation,
            fakeHandler.getLastOpenedBrowserPermissionSettingsType());
      });

  test('Websites section is hidden when location is not allowed', async () => {
    await initPage();
    // Disable location access.
    setGeolocationAccessLevelPref(GeolocationAccessLevel.DISALLOWED);
    assertNull(getManagePermissionsInChromeRow());
    assertTrue(!!getNoWebsiteHasAccessTextRow());

    // Set location to "only allowed for system", UI should remain the same.
    setGeolocationAccessLevelPref(
        GeolocationAccessLevel.ONLY_ALLOWED_FOR_SYSTEM);
    assertNull(getManagePermissionsInChromeRow());
    assertTrue(!!getNoWebsiteHasAccessTextRow());
  });

  test('System services section', async () => {
    await initPage();

    await setGeolocationAccessLevelPref(
        GeolocationAccessLevel.ONLY_ALLOWED_FOR_SYSTEM);
    await checkServiceSection();

    await setGeolocationAccessLevelPref(GeolocationAccessLevel.DISALLOWED);
    await checkServiceSection();

    await setGeolocationAccessLevelPref(GeolocationAccessLevel.ALLOWED);
    await checkServiceSection();
  });

  test('System services navigate to its settings page on click', async () => {
    await initPage();
    const systemServices =
        getSystemServicesFromSubpage(privacyHubGeolocationSubpage);

    // Check all 4 system services are listed.
    assertEquals(4, systemServices.length);

    // Check Timezone navigation.
    const timezone = systemServices[0]!;
    timezone.click();
    await flushTasks();
    assertEquals(
        routes.DATETIME_TIMEZONE_SUBPAGE.path,
        Router.getInstance().currentRoute.path);

    // Check NightLight navigation.
    const nightLight = systemServices[1]!;
    nightLight.click();
    await flushTasks();
    assertEquals(routes.DISPLAY.path, Router.getInstance().currentRoute.path);


    // Check DarkTheme navigation.
    const darkTheme = systemServices[2]!;
    darkTheme.click();
    assertEquals(
        loadTimeData.getString('personalizationAppUrl'),
        await openWindowProxy.whenCalled('openUrl'));
    await flushTasks();

    // Reset promise resolvers to test again on LocalWeather.
    openWindowProxy.reset();

    // Check LocalWeather navigation.
    const localWeather = systemServices[3]!;
    localWeather.click();
    assertEquals(
        loadTimeData.getString('personalizationAppUrl') +
            loadTimeData.getString('ambientSubpageRelativeUrl'),
        await openWindowProxy.whenCalled('openUrl'));
  });

  test('Timezone update in system services section', async () => {
    await initPage();
    setGeolocationAccessLevelPref(GeolocationAccessLevel.DISALLOWED);
    const systemServices =
        getSystemServicesFromSubpage(privacyHubGeolocationSubpage);

    assertEquals(4, systemServices.length);

    const timeZoneString = (tz: string) =>
        ('Blocked. Time zone is currently set to ' + tz +
         ' and can only be updated manually.');

    const sunsetScheduleString = (interval: string) =>
        'Blocked. Schedule is currently set to ' + interval +
        ' and can only be updated manually.';

    assertEquals(
        timeZoneString('Test Time Zone'),
        getSystemServicePermissionText(systemServices[0]!));
    assertEquals(
        sunsetScheduleString('7:00AM - 8:00PM'),
        getSystemServicePermissionText(systemServices[1]!));

    // Simulate timezone-changed event.
    const secondTimeZone = 'Some Other Time Zone';
    const secondSunsetSchedule = '5:00AM - 10:00PM';
    privacyHubBrowserProxy.currentTimeZoneName = secondTimeZone;
    privacyHubBrowserProxy.currentSunRiseTime = '5:00AM';
    privacyHubBrowserProxy.currentSunSetTime = '10:00PM';
    privacyHubGeolocationSubpage.notifyPath(
        'prefs.cros.system.timezone', secondTimeZone);


    // Wait for all observers to be notified.
    // This statement puts this currently executed async task at the end of the
    // JS event loop.
    await flushTasks();

    // The warning strings should now look differently.
    assertEquals(
        timeZoneString(secondTimeZone),
        getSystemServicePermissionText(systemServices[0]!));
    assertEquals(
        sunsetScheduleString(secondSunsetSchedule),
        getSystemServicePermissionText(systemServices[1]!));
  });

  test('Location controls is disabled when managed by policy', async () => {
    await initPage();
    privacyHubGeolocationSubpage.prefs.ash.user.geolocation_access_level
        .enforcement = chrome.settingsPrivate.Enforcement.ENFORCED;
    privacyHubGeolocationSubpage.prefs.ash.user.geolocation_access_level
        .controlledBy = chrome.settingsPrivate.ControlledBy.USER_POLICY;
    privacyHubGeolocationSubpage.notifyPath(
        'prefs.ash.user.geolocation_access_level.enforcement');
    privacyHubGeolocationSubpage.notifyPath(
        'prefs.ash.user.geolocation_access_level.controlledBy');
    await flushTasks();

    // Change button should be disabled.
    const changeButton =
        privacyHubGeolocationSubpage.shadowRoot!
            .querySelector<ControlledButtonElement>('#changeAccessButton')!;
    assertTrue(changeButton.disabled, 'Change button not disabled');

    // Policy indicator should be shown.
    assertTrue(
        !!changeButton.shadowRoot!.querySelector('cr-policy-pref-indicator'),
        'Policy indicator missing');
  });

  test('Location control is disabled for secondary users', async () => {
    // Simulate secondary user flow.
    loadTimeData.overrideValues({
      isSecondaryUser: true,
    });
    await initPage();

    // Change button should be disabled.
    const changeButton =
        privacyHubGeolocationSubpage.shadowRoot!
            .querySelector<ControlledButtonElement>('#changeAccessButton')!;
    assertTrue(changeButton.disabled);

    // Managed icon with tooltip should be present.
    assertTrue(!!privacyHubGeolocationSubpage.shadowRoot!
                     .querySelector<CrTooltipIconElement>('cr-tooltip-icon'));
  });
});