chromium/chrome/test/data/webui/chromeos/settings/os_about_page/os_about_page_test.ts

// Copyright 2019 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/os_settings.js';
import 'chrome://os-settings/lazy_load.js';

import {AboutPageBrowserProxyImpl, BrowserChannel, IronIconElement, LifetimeBrowserProxyImpl, OsAboutPageElement, Router, routes, settingMojom, setUserActionRecorderForTesting, UpdateStatus, userActionRecorderMojom} from 'chrome://os-settings/os_settings.js';
import {webUIListenerCallback} from 'chrome://resources/js/cr.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 {assertDeepEquals, assertEquals, assertFalse, assertNotEquals, assertNull, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {flushTasks, waitAfterNextRender} from 'chrome://webui-test/polymer_test_util.js';
import {eventToPromise, isVisible} from 'chrome://webui-test/test_util.js';

import {FakeUserActionRecorder} from '../fake_user_action_recorder.js';
import {TestLifetimeBrowserProxy} from '../test_os_lifetime_browser_proxy.js';
import {clearBody} from '../utils.js';

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

type UserActionRecorderInterface =
    userActionRecorderMojom.UserActionRecorderInterface;

suite('<os-about-page> AllBuilds', () => {
  const isRevampWayfindingEnabled =
      loadTimeData.getBoolean('isRevampWayfindingEnabled');
  let page: OsAboutPageElement;
  let aboutBrowserProxy: TestAboutPageBrowserProxy;
  let lifetimeBrowserProxy: TestLifetimeBrowserProxy;
  let userActionRecorder: UserActionRecorderInterface;

  const SPINNER_ICON_LIGHT_MODE =
      'chrome://resources/images/throbber_small.svg';
  const SPINNER_ICON_DARK_MODE =
      'chrome://resources/images/throbber_small_dark.svg';

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

    userActionRecorder = new FakeUserActionRecorder();
    setUserActionRecorderForTesting(userActionRecorder);

    lifetimeBrowserProxy = new TestLifetimeBrowserProxy();
    LifetimeBrowserProxyImpl.setInstance(lifetimeBrowserProxy);

    aboutBrowserProxy = new TestAboutPageBrowserProxy();
    AboutPageBrowserProxyImpl.setInstanceForTesting(aboutBrowserProxy);
  });

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

  interface StatusChangeEventOptions {
    progress?: number;
    message?: string;
    rollback?: boolean;
    powerwash?: boolean;
    version?: string;
    size?: string;
  }

  function fireStatusChanged(
      status: UpdateStatus, options: StatusChangeEventOptions = {}): void {
    webUIListenerCallback('update-status-changed', {
      progress: options.progress === undefined ? 1 : options.progress,
      message: options.message,
      status,
      rollback: options.rollback,
      powerwash: options.powerwash,
      version: options.version,
      size: options.size,
    });
  }

  async function initPage(): Promise<void> {
    clearBody();
    page = document.createElement('os-about-page');
    document.body.appendChild(page);

    Router.getInstance().navigateTo(routes.ABOUT);
    await flushTasks();

    await Promise.all([
      aboutBrowserProxy.whenCalled('getChannelInfo'),
      aboutBrowserProxy.whenCalled('refreshUpdateStatus'),
      aboutBrowserProxy.whenCalled('refreshTpmFirmwareUpdateStatus'),
      aboutBrowserProxy.whenCalled('checkInternetConnection'),
    ]);
  }

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

  if (isRevampWayfindingEnabled) {
    test('Crostini settings card is visible', async () => {
      await initPage();
      const crostiniSettingsCard =
          page.shadowRoot!.querySelector('crostini-settings-card');
      assertTrue(isVisible(crostiniSettingsCard));
    });
  }

  ['light', 'dark'].forEach((mode) => {
    suite(`with ${mode} mode active`, () => {
      const isDarkMode = mode === 'dark';

      function setDarkMode(isActive: boolean): void {
        page.set('isDarkModeActive_', isActive);
      }

      /**
       * Test that the OS update status message and icon update according to
       * incoming 'update-status-changed' events, for light and dark mode
       * respectively.
       */
      test('status message and icon update', async () => {
        await initPage();
        setDarkMode(isDarkMode);
        const expectedIcon =
            isDarkMode ? SPINNER_ICON_DARK_MODE : SPINNER_ICON_LIGHT_MODE;

        const updateRowIcon =
            page.shadowRoot!.querySelector<IronIconElement>('#updateRowIcon');
        assertTrue(!!updateRowIcon);
        const statusMessageEl = page.$.updateStatusMessageInner;
        let previousMessageText = statusMessageEl.innerText;

        fireStatusChanged(UpdateStatus.CHECKING);
        assertEquals(expectedIcon, updateRowIcon.src);
        assertNull(updateRowIcon.getAttribute('icon'));
        assertNotEquals(previousMessageText, statusMessageEl.innerText);
        previousMessageText = statusMessageEl.innerText;

        fireStatusChanged(UpdateStatus.UPDATING, {progress: 0});
        assertEquals(expectedIcon, updateRowIcon.src);
        assertNull(updateRowIcon.getAttribute('icon'));
        assertFalse(statusMessageEl.innerText.includes('%'));
        assertNotEquals(previousMessageText, statusMessageEl.innerText);
        previousMessageText = statusMessageEl.innerText;

        fireStatusChanged(UpdateStatus.UPDATING, {progress: 1});
        assertNotEquals(previousMessageText, statusMessageEl.innerText);
        assertTrue(statusMessageEl.innerText.includes('%'));
        previousMessageText = statusMessageEl.innerText;

        fireStatusChanged(UpdateStatus.NEARLY_UPDATED);
        assertNull(updateRowIcon.src);
        assertEquals(
            isRevampWayfindingEnabled ? 'os-settings:about-update-complete' :
                                        'settings:check-circle',
            updateRowIcon.icon);
        assertNotEquals(previousMessageText, statusMessageEl.innerText);
        previousMessageText = statusMessageEl.innerText;

        fireStatusChanged(UpdateStatus.DISABLED_BY_ADMIN);
        assertNull(updateRowIcon.src);
        assertEquals('cr20:domain', updateRowIcon.icon);
        assertNotEquals(previousMessageText, statusMessageEl.innerText);

        fireStatusChanged(UpdateStatus.FAILED);
        assertNull(updateRowIcon.src);
        assertEquals(
            isRevampWayfindingEnabled ? 'os-settings:about-update-error' :
                                        'cr:error-outline',
            updateRowIcon.icon);
        assertEquals(0, statusMessageEl.innerText.trim().length);

        fireStatusChanged(UpdateStatus.DISABLED);
        assertNull(updateRowIcon.src);
        assertNull(updateRowIcon.getAttribute('icon'));
        assertEquals(0, statusMessageEl.innerText.trim().length);
      });
    });
  });

  test('Error HTML is reflected in the update status message', async () => {
    await initPage();
    const htmlError = 'hello<br>there<br>was<pre>an</pre>error';
    fireStatusChanged(UpdateStatus.FAILED, {message: htmlError});
    assertEquals(htmlError, page.$.updateStatusMessageInner.innerHTML);
  });

  test('Learn more link is shown when update fails', async () => {
    await initPage();

    // Check that link is shown when update failed.
    fireStatusChanged(UpdateStatus.FAILED, {message: 'foo'});
    assertTrue(!!page.shadowRoot!.querySelector(
        '#updateStatusMessage a:not([hidden])'));

    // Check that link is hidden when update hasn't failed.
    fireStatusChanged(UpdateStatus.UPDATED, {message: ''});
    assertTrue(
        !!page.shadowRoot!.querySelector('#updateStatusMessage a[hidden]'));
  });

  test('Relaunch', async () => {
    await initPage();

    const relaunchButton = page.$.relaunchButton;
    assertTrue(!!relaunchButton);
    assertFalse(isVisible(relaunchButton));

    fireStatusChanged(UpdateStatus.NEARLY_UPDATED);
    assertTrue(isVisible(relaunchButton));
    relaunchButton.click();
    await lifetimeBrowserProxy.whenCalled('relaunch');
  });

  test('Rollback', async () => {
    const deviceManager = 'google.com';
    loadTimeData.overrideValues({
      deviceManager,
      isManaged: true,
    });
    await initPage();
    const statusMessageEl = page.$.updateStatusMessageInner;

    const progress = 90;
    fireStatusChanged(
        UpdateStatus.UPDATING, {progress, powerwash: true, rollback: true});
    let expectedMessage = page.i18nAdvanced('aboutRollbackInProgress', {
                                substitutions: [deviceManager, `${progress}%`],
                              })
                              .toString();
    assertEquals(expectedMessage, statusMessageEl.innerText);

    fireStatusChanged(
        UpdateStatus.NEARLY_UPDATED, {powerwash: true, rollback: true});
    expectedMessage =
        page.i18nAdvanced(
                'aboutRollbackSuccess', {substitutions: [deviceManager]})
            .toString();
    assertEquals(expectedMessage, statusMessageEl.innerText);

    // Simulate update disallowed to previously installed version after a
    // consumer rollback.
    fireStatusChanged(UpdateStatus.UPDATE_TO_ROLLBACK_VERSION_DISALLOWED);
    expectedMessage = page.i18n('aboutUpdateToRollbackVersionDisallowed');
    assertEquals(expectedMessage, statusMessageEl.innerText);
  });

  test(
      'Warning dialog is shown when attempting to update over metered network',
      async () => {
        await initPage();

        fireStatusChanged(
            UpdateStatus.NEED_PERMISSION_TO_UPDATE,
            {version: '9001.0.0', size: '9999'});
        flush();

        const warningDialog =
            page.shadowRoot!.querySelector('settings-update-warning-dialog');
        assertTrue(!!warningDialog);
        assertTrue(
            warningDialog.$.dialog.open, 'Warning dialog should be open');
      });

  test('Message is shown when there is no internet', async () => {
    await initPage();

    const statusMessageEl = page.$.updateStatusMessageInner;
    assertFalse(isVisible(statusMessageEl));

    aboutBrowserProxy.sendStatusNoInternet();
    flush();
    assertTrue(isVisible(statusMessageEl));
    assertTrue(statusMessageEl.innerText.includes('no internet'));
  });

  suite('Update/Relaunch button', () => {
    /**
     * Regression test for crbug.com/1220294
     */
    test('Update button is shown initially', async () => {
      aboutBrowserProxy.blockRefreshUpdateStatus();
      await initPage();

      assertTrue(isVisible(page.$.checkForUpdatesButton));
    });

    test(
        'Clicking the update button moves focus to status message',
        async () => {
          await initPage();

          const {checkForUpdatesButton, updateStatusMessageInner} = page.$;
          checkForUpdatesButton.click();
          await aboutBrowserProxy.whenCalled('requestUpdate');
          assertEquals(
              updateStatusMessageInner, page.shadowRoot!.activeElement,
              'Update status message should be focused.');
        });

    /**
     * Test that all buttons update according to incoming
     * 'update-status-changed' events for the case where target and current
     * channel are the same.
     */
    test('Button visibility based on update status', async () => {
      await initPage();
      const {checkForUpdatesButton, relaunchButton} = page.$;

      function assertAllHidden() {
        assertTrue(checkForUpdatesButton.hidden);
        assertTrue(relaunchButton.hidden);
        // Ensure that when all buttons are hidden, the container is also
        // hidden.
        assertTrue(page.$.buttonContainer.hidden);
      }

      // Check that |UPDATED| status is ignored if the user has not
      // explicitly checked for updates yet.
      fireStatusChanged(UpdateStatus.UPDATED);
      assertFalse(checkForUpdatesButton.hidden);
      assertTrue(relaunchButton.hidden);

      // Check that the "Check for updates" button gets hidden for certain
      // UpdateStatus values, even if the CHECKING state was never
      // encountered (for example triggering update from crosh command
      // line).
      fireStatusChanged(UpdateStatus.UPDATING);
      assertAllHidden();
      fireStatusChanged(UpdateStatus.NEARLY_UPDATED);
      assertTrue(checkForUpdatesButton.hidden);
      assertFalse(relaunchButton.hidden);

      fireStatusChanged(UpdateStatus.CHECKING);
      assertAllHidden();

      fireStatusChanged(UpdateStatus.UPDATING);
      assertAllHidden();

      fireStatusChanged(UpdateStatus.NEARLY_UPDATED);
      assertTrue(checkForUpdatesButton.hidden);
      assertFalse(relaunchButton.hidden);

      fireStatusChanged(UpdateStatus.UPDATED);
      assertAllHidden();

      fireStatusChanged(UpdateStatus.FAILED);
      assertFalse(checkForUpdatesButton.hidden);
      assertTrue(relaunchButton.hidden);

      fireStatusChanged(UpdateStatus.DISABLED);
      assertAllHidden();

      fireStatusChanged(UpdateStatus.DISABLED_BY_ADMIN);
      assertFalse(checkForUpdatesButton.hidden);
      assertTrue(relaunchButton.hidden);
    });

    /**
     * Test that buttons update according to incoming
     * 'update-status-changed' events for the case where the target channel
     * is more stable than current channel and update will powerwash.
     */
    test('Relaunch button when updating from beta to stable', async () => {
      aboutBrowserProxy.setChannels(BrowserChannel.BETA, BrowserChannel.STABLE);
      await initPage();

      fireStatusChanged(UpdateStatus.NEARLY_UPDATED, {powerwash: true});
      const relaunchButton = page.$.relaunchButton;
      assertTrue(isVisible(relaunchButton));

      assertEquals(
          page.i18n('aboutRelaunchAndPowerwash'), relaunchButton.innerText);

      relaunchButton.click();
      await lifetimeBrowserProxy.whenCalled('relaunch');
    });

    /**
     * Test that buttons update according to incoming
     * 'update-status-changed' events for the case where the target channel
     * is less stable than current channel.
     */
    test('Relaunch button when updating from stable to beta', async () => {
      aboutBrowserProxy.setChannels(BrowserChannel.STABLE, BrowserChannel.BETA);
      await initPage();

      fireStatusChanged(UpdateStatus.NEARLY_UPDATED, {powerwash: false});
      const relaunchButton = page.$.relaunchButton;
      assertTrue(isVisible(relaunchButton));

      assertEquals(page.i18n('aboutRelaunch'), relaunchButton.innerText);

      relaunchButton.click();
      await lifetimeBrowserProxy.whenCalled('relaunch');
    });

    /**
     * The relaunch and powerwash button is shown if the powerwash flag is set
     * in the update status.
     */
    test('Relaunch button when powerwash flag is set', async () => {
      await initPage();

      fireStatusChanged(UpdateStatus.NEARLY_UPDATED, {powerwash: true});
      const relaunchButton = page.$.relaunchButton;
      assertTrue(isVisible(relaunchButton));

      assertEquals(
          page.i18n('aboutRelaunchAndPowerwash'), relaunchButton.innerText);

      relaunchButton.click();
      await lifetimeBrowserProxy.whenCalled('relaunch');
    });
  });

  suite('Release notes', () => {
    function queryReleaseNotesOnline(): HTMLElement|null {
      return page.shadowRoot!.querySelector<HTMLElement>('#releaseNotesOnline');
    }

    function queryReleaseNotesOffline(): HTMLElement|null {
      return page.shadowRoot!.querySelector<HTMLElement>(
          '#releaseNotesOffline');
    }

    suite('when online', () => {
      setup(async () => {
        aboutBrowserProxy.setInternetConnection(true);
        await initPage();
      });

      test('Online release notes are visible', async () => {
        const releaseNotesOnlineButton = queryReleaseNotesOnline();
        assertTrue(!!releaseNotesOnlineButton);
        assertTrue(isVisible(releaseNotesOnlineButton));
        assertFalse(isVisible(queryReleaseNotesOffline()));

        releaseNotesOnlineButton.click();
        await aboutBrowserProxy.whenCalled('launchReleaseNotes');
      });

      test('Deep link to release notes button', async () => {
        const setting = settingMojom.Setting.kSeeWhatsNew;
        deepLinkToSetting(setting);

        const deepLinkElement = queryReleaseNotesOnline();
        assertTrue(!!deepLinkElement);
        await waitAfterNextRender(deepLinkElement);
        assertEquals(
            deepLinkElement, page.shadowRoot!.activeElement,
            `Release notes should be focused for settingId=${setting}.`);
      });
    });

    suite('when offline', () => {
      setup(async () => {
        aboutBrowserProxy.setInternetConnection(false);
        await initPage();
      });

      test('Offline release notes are visible', () => {
        assertFalse(isVisible(queryReleaseNotesOnline()));
        assertTrue(isVisible(queryReleaseNotesOffline()));
      });

      test('Deep link to release notes button', async () => {
        const setting = settingMojom.Setting.kSeeWhatsNew;
        deepLinkToSetting(setting);

        const deepLinkElement = queryReleaseNotesOffline();
        assertTrue(!!deepLinkElement);
        await waitAfterNextRender(deepLinkElement);
        assertEquals(
            deepLinkElement, page.shadowRoot!.activeElement,
            `Release notes should be focused for settingId=${setting}.`);
      });
    });
  });

  suite('Regulatory info', () => {
    const regulatoryInfo = {text: 'foo', url: 'bar'};

    async function checkRegulatoryInfo(isShowing: boolean): Promise<void> {
      await aboutBrowserProxy.whenCalled('getRegulatoryInfo');
      const regulatoryInfoEl = page.$.regulatoryInfo;
      assertEquals(isShowing, isVisible(regulatoryInfoEl));

      if (isShowing) {
        const img = regulatoryInfoEl.querySelector('img');
        assertTrue(!!img);
        assertEquals(regulatoryInfo.text, img.getAttribute('alt'));
        assertEquals(regulatoryInfo.url, img.getAttribute('src'));
      }
    }

    test('Regulatory info is not shown', async () => {
      aboutBrowserProxy.setRegulatoryInfo(null);
      await initPage();
      await checkRegulatoryInfo(false);
    });

    test('Regulatory info is shown', async () => {
      aboutBrowserProxy.setRegulatoryInfo(regulatoryInfo);
      await initPage();
      await checkRegulatoryInfo(true);
    });
  });

  test('TPM firmware update', async () => {
    await initPage();

    const tpmFirmwareUpdateRow =
        page.shadowRoot!.querySelector<HTMLElement>('#aboutTPMFirmwareUpdate');
    assertFalse(isVisible(tpmFirmwareUpdateRow));

    aboutBrowserProxy.setTpmFirmwareUpdateStatus({updateAvailable: true});
    aboutBrowserProxy.refreshTpmFirmwareUpdateStatus();
    assertTrue(!!tpmFirmwareUpdateRow);
    assertTrue(isVisible(tpmFirmwareUpdateRow));

    tpmFirmwareUpdateRow.click();
    await flushTasks();
    const dialog =
        page.shadowRoot!.querySelector('os-settings-powerwash-dialog');
    assertTrue(!!dialog);
    assertTrue(dialog.$.dialog.open);

    dialog.shadowRoot!.querySelector<HTMLElement>('#powerwash')!.click();
    const requestTpmFirmwareUpdate =
        await lifetimeBrowserProxy.whenCalled('factoryReset');
    assertTrue(requestTpmFirmwareUpdate);
  });

  suite('End of life', () => {
    /**
     * Checks the visibility of the end of life message and icon.
     */
    async function assertHasEndOfLife(isShowing: boolean): Promise<void> {
      await aboutBrowserProxy.whenCalled('getEndOfLifeInfo');

      const statusMessageEl = page.$.updateStatusMessageInner;
      const endOfLifeMessage =
          page.shadowRoot!.querySelector<HTMLElement>('#endOfLifeMessage');
      assertTrue(!!endOfLifeMessage);
      assertEquals(isShowing, isVisible(endOfLifeMessage));

      // Update status message should be hidden before user has
      // checked for updates.
      assertFalse(isVisible(statusMessageEl));

      fireStatusChanged(UpdateStatus.CHECKING);
      assertEquals(isShowing, !isVisible(statusMessageEl));

      if (isShowing) {
        const updateRowIcon =
            page.shadowRoot!.querySelector<IronIconElement>('#updateRowIcon');
        assertTrue(!!updateRowIcon);
        assertNull(updateRowIcon.src);
        assertEquals('os-settings:end-of-life', updateRowIcon.icon);

        const {checkForUpdatesButton} = page.$;
        assertTrue(!!checkForUpdatesButton);
        assertTrue(checkForUpdatesButton.hidden);
      }
    }

    test('End of life message is shown', async () => {
      aboutBrowserProxy.setEndOfLifeInfo({
        hasEndOfLife: true,
        aboutPageEndOfLifeMessage: '',
        shouldShowEndOfLifeIncentive: false,
        shouldShowOfferText: false,
        isExtendedUpdatesDatePassed: false,
        isExtendedUpdatesOptInRequired: false,
      });
      await initPage();
      await assertHasEndOfLife(true);
    });

    test('End of life message is not shown', async () => {
      aboutBrowserProxy.setEndOfLifeInfo({
        hasEndOfLife: false,
        aboutPageEndOfLifeMessage: '',
        shouldShowEndOfLifeIncentive: false,
        shouldShowOfferText: false,
        isExtendedUpdatesDatePassed: false,
        isExtendedUpdatesOptInRequired: false,
      });
      await initPage();
      await assertHasEndOfLife(false);
    });

    async function assertEndOfLifeIncentive(isShowing: boolean): Promise<void> {
      await aboutBrowserProxy.whenCalled('getEndOfLifeInfo');
      const eolOfferSection =
          page.shadowRoot!.querySelector('eol-offer-section');
      assertEquals(isShowing, isVisible(eolOfferSection));

      if (isShowing) {
        assertTrue(!!eolOfferSection);
        const eolIncentiveButton =
            eolOfferSection.shadowRoot!.querySelector<HTMLElement>(
                '#eolIncentiveButton');
        assertTrue(!!eolIncentiveButton);
        eolIncentiveButton.click();
        await aboutBrowserProxy.whenCalled('endOfLifeIncentiveButtonClicked');
      }
    }

    test('End of life incentive is not shown', async () => {
      aboutBrowserProxy.setEndOfLifeInfo({
        hasEndOfLife: false,
        aboutPageEndOfLifeMessage: '',
        shouldShowEndOfLifeIncentive: false,
        shouldShowOfferText: false,
        isExtendedUpdatesDatePassed: false,
        isExtendedUpdatesOptInRequired: false,
      });
      await initPage();
      await assertEndOfLifeIncentive(false);
    });

    test('End of life incentive is shown', async () => {
      aboutBrowserProxy.setEndOfLifeInfo({
        hasEndOfLife: false,
        aboutPageEndOfLifeMessage: '',
        shouldShowEndOfLifeIncentive: true,
        shouldShowOfferText: false,
        isExtendedUpdatesDatePassed: false,
        isExtendedUpdatesOptInRequired: false,
      });
      await initPage();
      await assertEndOfLifeIncentive(true);
    });
  });

  test(
      'Detailed build info row is focused when returning from subpage',
      async () => {
        const triggerSelector = '#detailedBuildInfoTrigger';
        const subpageTrigger =
            page.shadowRoot!.querySelector<HTMLElement>(triggerSelector);
        assertTrue(!!subpageTrigger);

        // Sub-page trigger navigates to Detailed build info subpage
        subpageTrigger.click();
        assertEquals(
            routes.ABOUT_DETAILED_BUILD_INFO,
            Router.getInstance().currentRoute);

        // Navigate back
        const popStateEventPromise = eventToPromise('popstate', window);
        Router.getInstance().navigateToPreviousRoute();
        await popStateEventPromise;
        await waitAfterNextRender(page);

        assertEquals(
            subpageTrigger, page.shadowRoot!.activeElement,
            `${triggerSelector} should be focused.`);
      });

  suite('Get help', () => {
    function getHelpRow(): HTMLElement {
      const row = page.shadowRoot!.querySelector<HTMLElement>('#help');
      assertTrue(!!row);
      return row;
    }

    test('Clicking row opens explore app', async () => {
      await initPage();

      getHelpRow().click();
      await aboutBrowserProxy.whenCalled('openOsHelpPage');
    });

    test('Deep link to get help row', async () => {
      await initPage();

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

      const deepLinkElement = getHelpRow();
      await waitAfterNextRender(deepLinkElement);
      assertEquals(
          deepLinkElement, page.shadowRoot!.activeElement,
          `Get help row should be focused for settingId=${setting}.`);
    });
  });

  suite('Diagnostics', () => {
    function getDiagnosticsRow(): HTMLElement {
      const row = page.shadowRoot!.querySelector<HTMLElement>('#diagnostics');
      assertTrue(!!row);
      return row;
    }

    test('Clicking row opens diagnostics app', async () => {
      await initPage();

      getDiagnosticsRow().click();
      await aboutBrowserProxy.whenCalled('openDiagnostics');
    });

    test('Deep link to diagnostics', async () => {
      await initPage();

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

      const deepLinkElement = getDiagnosticsRow();
      await waitAfterNextRender(deepLinkElement);
      assertEquals(
          deepLinkElement, page.shadowRoot!.activeElement,
          `Diagnostics row should be focused for settingId=${setting}.`);
    });
  });

  suite('Firmware updates', () => {
    function getFirmwareUpdateBadge(): IronIconElement {
      const badge = page.shadowRoot!.querySelector<IronIconElement>(
          '#firmwareUpdateBadge');
      assertTrue(!!badge);
      return badge;
    }

    function getFirmwareUpdateBadgeSeparator(): HTMLElement {
      const separator = page.shadowRoot!.querySelector<HTMLElement>(
          '#firmwareUpdateBadgeSeparator');
      assertTrue(!!separator);
      return separator;
    }

    test('Badge is not shown when there are no updates', async () => {
      aboutBrowserProxy.setFirmwareUpdatesCount(0);
      await initPage();
      await aboutBrowserProxy.whenCalled('getFirmwareUpdateCount');

      assertFalse(isVisible(getFirmwareUpdateBadge()));
      assertFalse(isVisible(getFirmwareUpdateBadgeSeparator()));
    });

    test('Badge shows the number of updates', async () => {
      for (let i = 1; i < 10; i++) {
        aboutBrowserProxy.setFirmwareUpdatesCount(i);
        await initPage();
        await aboutBrowserProxy.whenCalled('getFirmwareUpdateCount');

        const badge = getFirmwareUpdateBadge();
        assertTrue(isVisible(badge));
        assertTrue(isVisible(getFirmwareUpdateBadgeSeparator()));
        assertEquals(`os-settings:counter-${i}`, badge.icon);
      }
    });

    test('Badge uses the counter-9 icon for 10 updates', async () => {
      aboutBrowserProxy.setFirmwareUpdatesCount(10);
      await initPage();
      await aboutBrowserProxy.whenCalled('getFirmwareUpdateCount');

      const badge = getFirmwareUpdateBadge();
      assertTrue(isVisible(badge));
      assertTrue(isVisible(getFirmwareUpdateBadgeSeparator()));
      assertEquals('os-settings:counter-9', badge.icon);
    });

    test('Clicking link opens firmware updates page', async () => {
      await initPage();

      const firmwareUpdatesRow =
          page.shadowRoot!.querySelector<HTMLElement>('#firmwareUpdates');
      assertTrue(!!firmwareUpdatesRow);
      firmwareUpdatesRow.click();
      await aboutBrowserProxy.whenCalled('openFirmwareUpdatesPage');
    });

    test('Deep link to firmware updates', async () => {
      await initPage();

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

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

  suite('Extended Updates', () => {
    const EXTENDED_UPDATES_ICON = 'os-settings:about-update-complete';

    function assertExtendedUpdatesVisibility(visible: boolean) {
      const mainMessage = page.shadowRoot!.querySelector<HTMLElement>(
          '#extendedUpdatesMainMessage');
      const secondaryMessage = page.shadowRoot!.querySelector<HTMLElement>(
          '#extendedUpdatesSecondaryMessage');

      assertEquals(visible, isVisible(mainMessage));
      assertEquals(visible, isVisible(secondaryMessage));
      assertEquals(visible, isVisible(page.$.extendedUpdatesButton));
    }

    function getLastEligibilityArgs(): boolean[] {
      return aboutBrowserProxy.getArgs('isExtendedUpdatesOptInEligible').at(-1);
    }

    function fireExtendedUpdatesSettingChanged(): void {
      // This setting only changes when the user opts in,
      // which in turn makes the device no longer eligible for opt-in,
      // so we update the return value to false here.
      aboutBrowserProxy.setExtendedUpdatesOptInEligible(false);
      webUIListenerCallback('extended-updates-setting-changed');
    }

    test('is not shown by default', async () => {
      await initPage();
      assertExtendedUpdatesVisibility(false);
      assertTrue(isVisible(page.$.checkForUpdatesButton));
    });

    test('is shown when eligible and up to date', async () => {
      aboutBrowserProxy.setExtendedUpdatesOptInEligible(true);
      assertEquals(
          0, aboutBrowserProxy.getCallCount('recordExtendedUpdatesShown'));

      await initPage();
      // Extended updates is shown initially if no pending updates.
      assertExtendedUpdatesVisibility(true);
      await aboutBrowserProxy.whenCalled('recordExtendedUpdatesShown');
      assertEquals(
          1, aboutBrowserProxy.getCallCount('recordExtendedUpdatesShown'));

      fireStatusChanged(UpdateStatus.CHECKING);
      assertExtendedUpdatesVisibility(false);

      fireStatusChanged(UpdateStatus.UPDATING);
      assertExtendedUpdatesVisibility(false);

      fireStatusChanged(UpdateStatus.NEARLY_UPDATED);
      assertExtendedUpdatesVisibility(false);

      fireStatusChanged(UpdateStatus.UPDATED);
      assertExtendedUpdatesVisibility(true);
      assertFalse(isVisible(page.$.checkForUpdatesButton));
      // Record call should only happen at most once.
      assertEquals(
          1, aboutBrowserProxy.getCallCount('recordExtendedUpdatesShown'));

      const updateRowIcon =
          page.shadowRoot!.querySelector<IronIconElement>('#updateRowIcon');
      assertTrue(!!updateRowIcon);
      assertNull(updateRowIcon.src);
      assertEquals(EXTENDED_UPDATES_ICON, updateRowIcon.icon);

      assertTrue(isVisible(page.$.extendedUpdatesButton));
      page.$.extendedUpdatesButton.click();
      await aboutBrowserProxy.whenCalled('openExtendedUpdatesDialog');
    });

    test('is not shown after opting in', async () => {
      aboutBrowserProxy.setExtendedUpdatesOptInEligible(true);
      await initPage();
      assertExtendedUpdatesVisibility(true);

      fireExtendedUpdatesSettingChanged();
      await aboutBrowserProxy.whenCalled('isExtendedUpdatesOptInEligible');
      assertExtendedUpdatesVisibility(false);
    });

    suite('when', () => {
      interface ExtendedUpdatesTestCase {
        eolPassed: boolean;
        extDatePassed: boolean;
        optInRequired: boolean;
        expectedVisibility: boolean;
      }
      [{
        eolPassed: true,
        extDatePassed: true,
        optInRequired: true,
        expectedVisibility: false,
      },
       {
         eolPassed: false,
         extDatePassed: true,
         optInRequired: true,
         expectedVisibility: true,
       },
       {
         eolPassed: false,
         extDatePassed: false,
         optInRequired: true,
         expectedVisibility: false,
       },
       {
         eolPassed: false,
         extDatePassed: true,
         optInRequired: false,
         expectedVisibility: false,
       },
      ].forEach((tc: ExtendedUpdatesTestCase) => {
        test(
            `eol has ${tc.eolPassed ? '' : 'not '}passed, ` +
                `extended date has ${tc.extDatePassed ? '' : 'not '}passed, ` +
                `and opt-in is ${tc.optInRequired ? '' : 'not '}required, ` +
                `is ${tc.expectedVisibility ? '' : 'not '}visible`,
            async () => {
              aboutBrowserProxy.setEndOfLifeInfo({
                hasEndOfLife: tc.eolPassed,
                aboutPageEndOfLifeMessage: '',
                shouldShowEndOfLifeIncentive: false,
                shouldShowOfferText: false,
                isExtendedUpdatesDatePassed: tc.extDatePassed,
                isExtendedUpdatesOptInRequired: tc.optInRequired,
              });
              aboutBrowserProxy.setExtendedUpdatesOptInEligible(
                  tc.expectedVisibility);
              await initPage();

              assertDeepEquals(
                  [tc.eolPassed, tc.extDatePassed, tc.optInRequired],
                  getLastEligibilityArgs());
              assertExtendedUpdatesVisibility(tc.expectedVisibility);
            });
      });
    });
  });
});

suite('<os-about-page> OfficialBuild', () => {
  let page: OsAboutPageElement;
  let browserProxy: TestAboutPageBrowserProxy;

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

  async function assertElementIsDeepLinked(element: HTMLElement):
      Promise<void> {
    await waitAfterNextRender(element);
    assertEquals(element, page.shadowRoot!.activeElement);
  }

  setup(async () => {
    browserProxy = new TestAboutPageBrowserProxy();
    AboutPageBrowserProxyImpl.setInstanceForTesting(browserProxy);

    clearBody();
    page = document.createElement('os-about-page');
    document.body.appendChild(page);
    await flushTasks();
  });

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

  test('Report an issue click opens feedback dialog', async () => {
    const reportIssueRow =
        page.shadowRoot!.querySelector<HTMLElement>('#reportIssue');
    assertTrue(!!reportIssueRow);
    reportIssueRow.click();
    await browserProxy.whenCalled('openFeedbackDialog');
  });

  test('Deep link to report an issue', async () => {
    const setting = settingMojom.Setting.kReportAnIssue;
    deepLinkToSetting(setting);

    const reportIssueRow =
        page.shadowRoot!.querySelector<HTMLElement>('#reportIssue');
    assertTrue(!!reportIssueRow);
    await assertElementIsDeepLinked(reportIssueRow);
  });

  test('Deep link to terms of service', async () => {
    const setting = settingMojom.Setting.kTermsOfService;
    deepLinkToSetting(setting);

    const productTosRow =
        page.shadowRoot!.querySelector<HTMLElement>('#aboutProductTos');
    assertTrue(!!productTosRow);
    await assertElementIsDeepLinked(productTosRow);
  });
});