chromium/chrome/test/data/webui/chromeos/settings/os_about_page/detailed_build_info_subpage_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.

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

import {SettingsDetailedBuildInfoSubpageElement} from 'chrome://os-settings/lazy_load.js';
import {AboutPageBrowserProxyImpl, CrPolicyIndicatorElement, DeviceNameBrowserProxyImpl, DeviceNameState, Router, routes, settingMojom} from 'chrome://os-settings/os_settings.js';
import {CrPolicyIndicatorType} from 'chrome://resources/ash/common/cr_policy_indicator_behavior.js';
import {webUIListenerCallback} from 'chrome://resources/js/cr.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {PromiseResolver} from 'chrome://resources/js/promise_resolver.js';
import {assertEquals, assertFalse, 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 {clearBody} from '../utils.js';

import {TestAboutPageBrowserProxy} from './test_about_page_browser_proxy.js';
import {TestDeviceNameBrowserProxy} from './test_device_name_browser_proxy.js';

suite('<detailed-build-info-subpage>', () => {
  let page: SettingsDetailedBuildInfoSubpageElement;
  let browserProxy: TestAboutPageBrowserProxy;
  let deviceNameBrowserProxy: TestDeviceNameBrowserProxy;

  async function createPage(): Promise<void> {
    clearBody();
    page = document.createElement('settings-detailed-build-info-subpage');
    document.body.appendChild(page);
    await flushTasks();
  }

  function deepLinkToSetting(setting: settingMojom.Setting): void {
    const params = new URLSearchParams();
    params.append('settingId', setting.toString());
    Router.getInstance().navigateTo(routes.ABOUT_DETAILED_BUILD_INFO, params);
  }

  function getChangeChannelButton(): HTMLButtonElement {
    const button = page.shadowRoot!.querySelector<HTMLButtonElement>(
        '#changeChannelButton');
    assertTrue(!!button);
    return button;
  }

  function queryChangeChannelPolicyIndicator(): CrPolicyIndicatorElement|null {
    return page.shadowRoot!.querySelector<CrPolicyIndicatorElement>(
        '#changeChannelPolicyIndicator');
  }

  function getEditHostnameButton(): HTMLButtonElement {
    const button = page.shadowRoot!.querySelector<HTMLButtonElement>(
        '#editHostnameButton');
    assertTrue(!!button);
    return button;
  }

  setup(() => {
    loadTimeData.overrideValues({
      aboutEnterpriseManaged: false,
      isHostnameSettingEnabled: true,
      isManaged: false,
    });

    browserProxy = new TestAboutPageBrowserProxy();
    AboutPageBrowserProxyImpl.setInstanceForTesting(browserProxy);
    deviceNameBrowserProxy = new TestDeviceNameBrowserProxy();
    DeviceNameBrowserProxyImpl.setInstanceForTesting(deviceNameBrowserProxy);

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

  teardown(() => {
    Router.getInstance().resetRouteForTesting();
  });

  test('Browser proxy methods are called on initialization', async () => {
    await createPage();

    await Promise.all([
      browserProxy.whenCalled('pageReady'),
      browserProxy.whenCalled('isConsumerAutoUpdateEnabled'),
      browserProxy.whenCalled('setConsumerAutoUpdate'),
      browserProxy.whenCalled('canChangeChannel'),
      browserProxy.whenCalled('getChannelInfo'),
      browserProxy.whenCalled('getVersionInfo'),
    ]);
  });

  test(
      'Browser proxy methods are called on initialization for managed device',
      async () => {
        loadTimeData.overrideValues({
          isManaged: true,
        });
        await createPage();

        await Promise.all([
          browserProxy.whenCalled('pageReady'),
          browserProxy.whenCalled('isManagedAutoUpdateEnabled'),
          browserProxy.whenCalled('canChangeChannel'),
          browserProxy.whenCalled('getChannelInfo'),
          browserProxy.whenCalled('getVersionInfo'),
        ]);
      });

  [true, false].forEach((canChangeChannel) => {
    suite(
        `When user ${canChangeChannel ? 'can' : 'cannot'} change channel`,
        () => {
          setup(() => {
            browserProxy.setCanChangeChannel(canChangeChannel);
          });

          function assertChangeChannelButtonEnabledState(): void {
            const changeChannelButton = getChangeChannelButton();
            assertEquals(canChangeChannel, !changeChannelButton.disabled);
          }

          function assertChangeChannelPolicyIndicatorVisibility(
              isManaged = false): void {
            const policyIndicator = queryChangeChannelPolicyIndicator();

            if (canChangeChannel) {
              assertFalse(isVisible(policyIndicator));
            } else {
              assertTrue(!!policyIndicator);
              assertTrue(isVisible(policyIndicator));
              assertEquals(
                  isManaged ? CrPolicyIndicatorType.DEVICE_POLICY :
                              CrPolicyIndicatorType.OWNER,
                  policyIndicator.indicatorType);
            }
          }

          test('Change channel button enabled state', async () => {
            await createPage();
            await browserProxy.whenCalled('canChangeChannel');

            assertChangeChannelButtonEnabledState();
          });

          test('Change channel policy indicator visibility', async () => {
            await createPage();
            await browserProxy.whenCalled('canChangeChannel');

            assertChangeChannelPolicyIndicatorVisibility();
          });

          if (!canChangeChannel) {
            test(
                'Change channel policy indicator is visible for managed device',
                async () => {
                  loadTimeData.overrideValues({
                    aboutEnterpriseManaged: true,
                  });
                  await createPage();
                  await browserProxy.whenCalled('canChangeChannel');

                  assertChangeChannelPolicyIndicatorVisibility(
                      /*isManaged=*/ true);
                });
          }

          /**
           * Checks whether the "change channel" button state (enabled/disabled)
           * is correct before getChannelInfo() returns
           * (see https://crbug.com/848750). Here, getChannelInfo() is blocked
           * manually until after the button check.
           */
          test(
              'Change channel button state before getChannelInfo() returns',
              async () => {
                const fakeDelayPromise = new PromiseResolver();
                browserProxy.fakeChannelInfoDelay = fakeDelayPromise;
                await createPage();

                assertChangeChannelButtonEnabledState();
                fakeDelayPromise.resolve(null);
                await browserProxy.whenCalled('getChannelInfo');
              });

          test('Deep link to change channel button', async () => {
            await createPage();

            const setting = settingMojom.Setting.kChangeChromeChannel;
            deepLinkToSetting(setting);

            const changeChannelButton = getChangeChannelButton();
            await waitAfterNextRender(changeChannelButton);
            assertEquals(
                changeChannelButton, page.shadowRoot!.activeElement,
                `Change channel button should be focused for settingId=${
                    setting}.`);
          });
        });
  });

  test('Copy build details button copies info to clipboard', async () => {
    await createPage();
    await Promise.all([
      browserProxy.whenCalled('getVersionInfo'),
      browserProxy.whenCalled('getChannelInfo'),
      browserProxy.whenCalled('canChangeChannel'),
    ]);

    const copyBuildDetailsButton =
        page.shadowRoot!.querySelector<HTMLElement>('#copyBuildDetailsButton');
    assertTrue(!!copyBuildDetailsButton);

    const {osVersion, osFirmware, arcVersion} =
        browserProxy.getVersionInfoForTesting();
    const {targetChannel} = browserProxy.getChannelInfoForTesting();
    const expectedClipBoardText =
        `${loadTimeData.getString('application_label')}: ` +
        `${loadTimeData.getString('aboutBrowserVersion')}\n` +
        `Platform: ${osVersion}\n` +
        `Channel: ${targetChannel}\n` +
        `Firmware Version: ${osFirmware}\n` +
        `ARC Enabled: ${loadTimeData.getBoolean('aboutIsArcEnabled')}\n` +
        `ARC: ${arcVersion}\n` +
        'Enterprise Enrolled: ' +
        `${loadTimeData.getBoolean('aboutEnterpriseManaged')}\n` +
        'Developer Mode: ' +
        `${loadTimeData.getBoolean('aboutIsDeveloperMode')}`;

    let clipboardText = await navigator.clipboard.readText();
    assertEquals('', clipboardText);

    copyBuildDetailsButton.click();
    clipboardText = await navigator.clipboard.readText();
    assertEquals(expectedClipBoardText, clipboardText);
  });

  test('Deep link to change device name', async () => {
    await createPage();

    const setting = settingMojom.Setting.kChangeDeviceName;
    deepLinkToSetting(setting);

    const deepLinkElement =
        page.shadowRoot!.querySelector<HTMLElement>('#editHostnameButton');
    assertTrue(!!deepLinkElement);
    await waitAfterNextRender(deepLinkElement);
    assertEquals(
        deepLinkElement, page.shadowRoot!.activeElement,
        `Change device name button should be focused for settingId=${
            setting}.`);
  });

  /**
   * Checks whether the "edit device name" button state (enabled/disabled)
   * correctly reflects whether the user is allowed to edit the name.
   */
  async function checkDeviceNameMetadata(
      expectedDeviceName: string,
      deviceNameState: DeviceNameState): Promise<void> {
    webUIListenerCallback(
        'settings.updateDeviceNameMetadata',
        {deviceName: expectedDeviceName, deviceNameState});
    await flushTasks();

    const actualDeviceName =
        page.shadowRoot!.querySelector<HTMLElement>('#deviceName')!.innerText;
    assertEquals(expectedDeviceName, actualDeviceName);

    let canEditDeviceName: boolean;
    switch (deviceNameState) {
      case (DeviceNameState.CAN_BE_MODIFIED):
        canEditDeviceName = true;
        break;
      default:
        canEditDeviceName = false;
    }

    const editHostnameButton = getEditHostnameButton();
    assertEquals(canEditDeviceName, !editHostnameButton.disabled);

    const policyIndicator =
        page.shadowRoot!.querySelector<CrPolicyIndicatorElement>(
            '#editHostnamePolicyIndicator');
    if (deviceNameState === DeviceNameState.CAN_BE_MODIFIED) {
      assertFalse(isVisible(policyIndicator));
    } else if (
        deviceNameState ===
        DeviceNameState.CANNOT_BE_MODIFIED_BECAUSE_OF_POLICIES) {
      assertTrue(!!policyIndicator);
      assertTrue(isVisible(policyIndicator));
      assertEquals(
          CrPolicyIndicatorType.DEVICE_POLICY, policyIndicator.indicatorType);
    } else if (
        deviceNameState ===
        DeviceNameState.CANNOT_BE_MODIFIED_BECAUSE_NOT_DEVICE_OWNER) {
      assertTrue(!!policyIndicator);
      assertTrue(isVisible(policyIndicator));
      assertEquals(CrPolicyIndicatorType.OWNER, policyIndicator.indicatorType);
    }
  }

  test('Device name metadata', async () => {
    createPage();
    await deviceNameBrowserProxy.whenCalled('notifyReadyForDeviceName');

    checkDeviceNameMetadata('TestDeviceName1', DeviceNameState.CAN_BE_MODIFIED);

    // Verify that we can still make changes to device name metadata even
    // if notifyReadyForDeviceName() is not called again.
    checkDeviceNameMetadata(
        'TestDeviceName2',
        DeviceNameState.CANNOT_BE_MODIFIED_BECAUSE_OF_POLICIES);
    checkDeviceNameMetadata(
        'TestDeviceName3',
        DeviceNameState.CANNOT_BE_MODIFIED_BECAUSE_NOT_DEVICE_OWNER);
  });

  test('Edit hostname dialog can be opened', async () => {
    await createPage();
    await deviceNameBrowserProxy.whenCalled('notifyReadyForDeviceName');

    getEditHostnameButton().click();
    await flushTasks();

    const dialog = page.shadowRoot!.querySelector('edit-hostname-dialog');
    assertTrue(!!dialog);
    assertTrue(dialog.$.dialog.open);
  });
});