chromium/chrome/test/data/webui/settings/appearance_page_test.ts

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import type {AppearanceBrowserProxy, /*CrButtonElement,*/ CustomizeColorSchemeModeClientRemote, SettingsAppearancePageElement, SettingsDropdownMenuElement} from 'chrome://settings/settings.js';
import {AppearanceBrowserProxyImpl, ColorSchemeMode, CustomizeColorSchemeModeBrowserProxy, CustomizeColorSchemeModeClientCallbackRouter, CustomizeColorSchemeModeHandlerRemote, SystemTheme} from 'chrome://settings/settings.js';
import {assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {TestBrowserProxy} from 'chrome://webui-test/test_browser_proxy.js';
import {TestMock} from 'chrome://webui-test/test_mock.js';
import {isVisible, microtasksFinished} from 'chrome://webui-test/test_util.js';

class TestAppearanceBrowserProxy extends TestBrowserProxy implements
    AppearanceBrowserProxy {
  private defaultZoom_: number = 1;
  private isChildAccount_: boolean = false;
  private isHomeUrlValid_: boolean = true;
  private pinnedToolbarActionsAreDefaultResponse_: boolean = true;

  constructor() {
    super([
      'getDefaultZoom',
      'getThemeInfo',
      'isChildAccount',
      'openCustomizeChrome',
      'openCustomizeChromeToolbarSection',
      'recordHoverCardImagesEnabledChanged',
      'resetPinnedToolbarActions',
      'useDefaultTheme',
      // <if expr="is_linux">
      'useGtkTheme',
      'useQtTheme',
      // </if>
      'validateStartupPage',
      'pinnedToolbarActionsAreDefault',
    ]);
  }

  getDefaultZoom() {
    this.methodCalled('getDefaultZoom');
    return Promise.resolve(this.defaultZoom_);
  }

  getThemeInfo(themeId: string) {
    this.methodCalled('getThemeInfo', themeId);
    return Promise.resolve({
      id: '',
      name: 'Sports car red',
      shortName: '',
      description: '',
      version: '',
      mayDisable: false,
      enabled: false,
      isApp: false,
      offlineEnabled: false,
      optionsUrl: '',
      permissions: [],
      hostPermissions: [],
    });
  }

  isChildAccount() {
    this.methodCalled('isChildAccount');
    return this.isChildAccount_;
  }

  openCustomizeChrome() {
    this.methodCalled('openCustomizeChrome');
  }

  openCustomizeChromeToolbarSection() {
    this.methodCalled('openCustomizeChromeToolbarSection');
  }

  recordHoverCardImagesEnabledChanged(enabled: boolean) {
    this.methodCalled('recordHoverCardImagesEnabledChanged', enabled);
  }

  resetPinnedToolbarActions() {
    this.methodCalled('resetPinnedToolbarActions');
  }

  useDefaultTheme() {
    this.methodCalled('useDefaultTheme');
  }

  // <if expr="is_linux">
  useGtkTheme() {
    this.methodCalled('useGtkTheme');
  }

  useQtTheme() {
    this.methodCalled('useQtTheme');
  }
  // </if>

  setDefaultZoom(defaultZoom: number) {
    this.defaultZoom_ = defaultZoom;
  }

  setIsChildAccount(isChildAccount: boolean) {
    this.isChildAccount_ = isChildAccount;
  }

  validateStartupPage(url: string) {
    this.methodCalled('validateStartupPage', url);
    return Promise.resolve(this.isHomeUrlValid_);
  }

  setValidStartupPageResponse(isValid: boolean) {
    this.isHomeUrlValid_ = isValid;
  }

  pinnedToolbarActionsAreDefault() {
    this.methodCalled('pinnedToolbarActionsAreDefault');
    return Promise.resolve(this.pinnedToolbarActionsAreDefaultResponse_);
  }

  setPinnedToolbarActionsAreDefaultResponse(areDefault: boolean) {
    this.pinnedToolbarActionsAreDefaultResponse_ = areDefault;
  }
}

let appearancePage: SettingsAppearancePageElement;
let appearanceBrowserProxy: TestAppearanceBrowserProxy;
let colorSchemeHandler: TestMock<CustomizeColorSchemeModeHandlerRemote>&
    CustomizeColorSchemeModeHandlerRemote;
let colorSchemeCallbackRouter: CustomizeColorSchemeModeClientRemote;

function createAppearancePage() {
  appearanceBrowserProxy.reset();
  document.body.innerHTML = window.trustedTypes!.emptyHTML;

  colorSchemeHandler =
      TestMock.fromClass(CustomizeColorSchemeModeHandlerRemote);
  CustomizeColorSchemeModeBrowserProxy.setInstance(
      colorSchemeHandler, new CustomizeColorSchemeModeClientCallbackRouter());
  colorSchemeCallbackRouter = CustomizeColorSchemeModeBrowserProxy.getInstance()
                                  .callbackRouter.$.bindNewPipeAndPassRemote();

  appearancePage = document.createElement('settings-appearance-page');
  appearancePage.set('prefs', {
    autogenerated: {
      theme: {
        policy: {
          color: {
            type: chrome.settingsPrivate.PrefType.NUMBER,
            value: 0,
          },
        },
      },
    },
    browser: {
      show_forward_button: {
        type: chrome.settingsPrivate.PrefType.BOOLEAN,
        value: true,
      },
      show_home_button: {
        type: chrome.settingsPrivate.PrefType.BOOLEAN,
        value: false,
      },
    },
    extensions: {
      theme: {
        id: {
          type: chrome.settingsPrivate.PrefType.STRING,
          value: '',
        },
        system_theme: {
          type: chrome.settingsPrivate.PrefType.NUMBER,
          value: SystemTheme.DEFAULT,
        },
      },
    },
    tab_search: {
      is_right_aligned: {
        type: chrome.settingsPrivate.PrefType.BOOLEAN,
        value: false,
      },
    },
  });

  appearancePage.set('pageVisibility', {
    setWallpaper: true,
  });

  document.body.appendChild(appearancePage);
  flush();
}

suite('AppearanceHandler', function() {
  setup(function() {
    appearanceBrowserProxy = new TestAppearanceBrowserProxy();
    AppearanceBrowserProxyImpl.setInstance(appearanceBrowserProxy);

    createAppearancePage();
  });

  teardown(function() {
    appearancePage.remove();
  });

  const THEME_ID_PREF = 'prefs.extensions.theme.id.value';

  // <if expr="is_linux">
  const SYSTEM_THEME_PREF = 'prefs.extensions.theme.system_theme.value';

  test('useDefaultThemeLinux', async () => {
    await colorSchemeHandler.whenCalled('initializeColorSchemeMode');

    assertFalse(!!appearancePage.get(THEME_ID_PREF));
    assertEquals(appearancePage.get(SYSTEM_THEME_PREF), SystemTheme.DEFAULT);
    // No custom nor system theme in use; "USE CLASSIC" should be hidden.
    assertFalse(!!appearancePage.shadowRoot!.querySelector('#useDefault'));
    // The color scheme toggle should be visible when the classic theme is used.
    assertTrue(isVisible(appearancePage.$.colorSchemeModeRow));

    appearancePage.set(SYSTEM_THEME_PREF, SystemTheme.GTK);
    flush();
    // If the system theme is in use, "USE CLASSIC" should show.
    assertTrue(!!appearancePage.shadowRoot!.querySelector('#useDefault'));
    // The color scheme toggle should be hidden when the GTK theme is used.
    assertFalse(isVisible(appearancePage.$.colorSchemeModeRow));

    appearancePage.set(SYSTEM_THEME_PREF, SystemTheme.DEFAULT);
    appearancePage.set(THEME_ID_PREF, 'fake theme id');
    flush();

    // With a custom theme installed, "USE CLASSIC" should show.
    const button =
        appearancePage.shadowRoot!.querySelector<HTMLElement>('#useDefault');
    assertTrue(!!button);

    button.click();
    return appearanceBrowserProxy.whenCalled('useDefaultTheme');
  });

  test('useGtkThemeLinux', async () => {
    await colorSchemeHandler.whenCalled('initializeColorSchemeMode');

    assertFalse(!!appearancePage.get(THEME_ID_PREF));
    appearancePage.set(SYSTEM_THEME_PREF, SystemTheme.GTK);
    flush();
    // The "USE GTK+" button shouldn't be showing if it's already in use.
    assertFalse(!!appearancePage.shadowRoot!.querySelector('#useGtk'));
    // The color scheme toggle should be hidden when the GTK theme is used.
    assertFalse(isVisible(appearancePage.$.colorSchemeModeRow));

    appearanceBrowserProxy.setIsChildAccount(true);
    appearancePage.set(SYSTEM_THEME_PREF, SystemTheme.DEFAULT);
    flush();
    // Child account users have their own theme and can't use GTK+ theme.
    assertFalse(!!appearancePage.shadowRoot!.querySelector('#useDefault'));
    assertFalse(!!appearancePage.shadowRoot!.querySelector('#useGtk'));
    // If there's no "USE" buttons, the container should be hidden.
    assertTrue(
        appearancePage.shadowRoot!
            .querySelector<HTMLElement>('#themesSecondaryActions')!.hidden);
    // The color scheme toggle should be visible when the classic theme is used,
    // for child accounts.
    assertTrue(isVisible(appearancePage.$.colorSchemeModeRow));

    appearanceBrowserProxy.setIsChildAccount(false);
    appearancePage.set(THEME_ID_PREF, 'fake theme id');
    flush();
    // If there's "USE" buttons again, the container should be visible.
    assertTrue(!!appearancePage.shadowRoot!.querySelector('#useDefault'));
    assertFalse(
        appearancePage.shadowRoot!
            .querySelector<HTMLElement>('#themesSecondaryActions')!.hidden);
    // The color scheme toggle should be visible when a custom theme is used.
    assertTrue(isVisible(appearancePage.$.colorSchemeModeRow));

    const button =
        appearancePage.shadowRoot!.querySelector<HTMLElement>('#useGtk');
    assertTrue(!!button);

    button.click();
    return appearanceBrowserProxy.whenCalled('useGtkTheme');
  });
  // </if>

  // <if expr="not is_linux">
  test('useDefaultTheme', function() {
    assertFalse(!!appearancePage.get(THEME_ID_PREF));
    assertFalse(!!appearancePage.shadowRoot!.querySelector('#useDefault'));

    appearancePage.set(THEME_ID_PREF, 'fake theme id');
    flush();

    // With a custom theme installed, "RESET TO DEFAULT" should show.
    const button =
        appearancePage.shadowRoot!.querySelector<HTMLElement>('#useDefault');
    assertTrue(!!button);

    button.click();
    return appearanceBrowserProxy.whenCalled('useDefaultTheme');
  });

  test('useDefaultThemeWithPolicy', function() {
    const POLICY_THEME_COLOR_PREF = 'prefs.autogenerated.theme.policy.color';
    assertFalse(!!appearancePage.shadowRoot!.querySelector('#useDefault'));

    // "Reset to default" button doesn't appear as result of a policy theme.
    appearancePage.set(POLICY_THEME_COLOR_PREF, {controlledBy: 'PRIMARY_USER'});
    flush();

    assertFalse(!!appearancePage.shadowRoot!.querySelector('#useDefault'));

    // Unset policy theme and set custom theme to get button to show.
    appearancePage.set(POLICY_THEME_COLOR_PREF, {});
    appearancePage.set(THEME_ID_PREF, 'fake theme id');
    flush();

    let button =
        appearancePage.shadowRoot!.querySelector<HTMLElement>('#useDefault');
    assertTrue(!!button);

    // Clicking "Reset to default" button when a policy theme is applied
    // causes the managed theme dialog to appear.
    appearancePage.set(POLICY_THEME_COLOR_PREF, {controlledBy: 'PRIMARY_USER'});
    flush();

    button =
        appearancePage.shadowRoot!.querySelector<HTMLElement>('#useDefault');
    assertTrue(!!button);
    assertEquals(
        null, appearancePage.shadowRoot!.querySelector('managed-dialog'));

    button.click();
    flush();

    assertFalse(
        appearancePage.shadowRoot!.querySelector('managed-dialog')!.hidden);
  });
  // </if>

  test('openCustomizeChrome', function() {
    loadTimeData.overrideValues({
      toolbarPinningEnabled: true,
    });
    createAppearancePage();
    const button =
        appearancePage.shadowRoot!.querySelector<HTMLElement>('#openTheme');
    assertTrue(!!button);

    button.click();
    return appearanceBrowserProxy.whenCalled('openCustomizeChrome');
  });

  test('openCustomizeChromeToolbarSection', function() {
    loadTimeData.overrideValues({
      toolbarPinningEnabled: true,
    });
    createAppearancePage();
    const button = appearancePage.shadowRoot!.querySelector<HTMLElement>(
        '#customizeToolbar');
    assertTrue(!!button);

    button.click();
    return appearanceBrowserProxy.whenCalled(
        'openCustomizeChromeToolbarSection');
  });

  test('resetPinnedToolbarActions', async function() {
    loadTimeData.overrideValues({
      toolbarPinningEnabled: true,
    });
    appearanceBrowserProxy.setPinnedToolbarActionsAreDefaultResponse(false);
    createAppearancePage();
    await microtasksFinished();

    const button = appearancePage.shadowRoot!.querySelector<HTMLElement>(
        '#resetPinnedToolbarActions');
    assertTrue(!!button);

    button.click();
    return appearanceBrowserProxy.whenCalled('resetPinnedToolbarActions');
  });

  test('resetHiddenWhenNoPinnedActions', async function() {
    loadTimeData.overrideValues({
      toolbarPinningEnabled: true,
    });
    appearanceBrowserProxy.setPinnedToolbarActionsAreDefaultResponse(true);
    createAppearancePage();
    await microtasksFinished();

    const button = appearancePage.shadowRoot!.querySelector<HTMLElement>(
        '#resetPinnedToolbarActions');
    assertFalse(!!button);
  });

  test('ColorSchemeMode', async () => {
    assertFalse(isVisible(appearancePage.$.colorSchemeModeRow));

    colorSchemeHandler.reset();
    createAppearancePage();
    await colorSchemeHandler.whenCalled('initializeColorSchemeMode');

    assertTrue(isVisible(appearancePage.$.colorSchemeModeRow));
    assertEquals(
        1, colorSchemeHandler.getCallCount('initializeColorSchemeMode'));

    // Assert that changes to the color scheme mode updates the select menu.
    colorSchemeCallbackRouter.setColorSchemeMode(ColorSchemeMode.kLight);
    assertEquals(
        `${ColorSchemeMode.kLight}`,
        appearancePage.$.colorSchemeModeSelect.value);

    // Assert that changing the select menu updates the color scheme.
    appearancePage.$.colorSchemeModeSelect.value = `${ColorSchemeMode.kDark}`;
    appearancePage.$.colorSchemeModeSelect.dispatchEvent(new Event('change'));
    const handlerArg =
        await colorSchemeHandler.whenCalled('setColorSchemeMode');
    assertEquals(ColorSchemeMode.kDark, handlerArg);
  });

  test('default zoom handling', async function() {
    function getDefaultZoomText() {
      const zoomLevel = appearancePage.$.zoomLevel;
      return zoomLevel.options[zoomLevel.selectedIndex]!.textContent!.trim();
    }

    await appearanceBrowserProxy.whenCalled('getDefaultZoom');

    assertEquals('100%', getDefaultZoomText());

    appearanceBrowserProxy.setDefaultZoom(2 / 3);
    createAppearancePage();
    await appearanceBrowserProxy.whenCalled('getDefaultZoom');

    assertEquals('67%', getDefaultZoomText());

    appearanceBrowserProxy.setDefaultZoom(11 / 10);
    createAppearancePage();
    await appearanceBrowserProxy.whenCalled('getDefaultZoom');

    assertEquals('110%', getDefaultZoomText());

    appearanceBrowserProxy.setDefaultZoom(1.7499999999999);
    createAppearancePage();
    await appearanceBrowserProxy.whenCalled('getDefaultZoom');

    assertEquals('175%', getDefaultZoomText());
  });

  test('show home button toggling', function() {
    assertFalse(
        !!appearancePage.shadowRoot!.querySelector('#home-button-options'));
    appearancePage.set('prefs', {
      autogenerated: {theme: {policy: {color: {value: 0}}}},
      browser: {show_home_button: {value: true}},
      extensions: {theme: {id: {value: ''}}},
      toolbar: {pinned_actions: {value: []}},
    });
    flush();

    assertTrue(
        !!appearancePage.shadowRoot!.querySelector('#home-button-options'));
  });

  test('show side panel options', function() {
    createAppearancePage();
    assertTrue(
        !!appearancePage.shadowRoot!.querySelector('#sidePanelPosition'));
  });

  test('show tab search options', async function() {
    loadTimeData.overrideValues({
      showTabSearchPositionSettings: true,
    });
    createAppearancePage();
    await microtasksFinished();
    assertTrue(
        !!appearancePage.shadowRoot!.querySelector('#tabSearchPositionRow'));
  });

  test('hide tab search options', async function() {
    loadTimeData.overrideValues({
      showTabSearchPositionSettings: false,
    });
    createAppearancePage();
    await microtasksFinished();
    assertTrue(
        !appearancePage.shadowRoot!.querySelector('#tabSearchPositionRow'));
  });

  test('ShowSavedTabGroupsToggleVisible', async function() {
    loadTimeData.overrideValues({
      tabGroupsSaveUIUpdateEnabled: true,
    });
    createAppearancePage();
    await microtasksFinished();
    assertTrue(isVisible(appearancePage.$.showSavedTabGroups));
  });

  test('ShowSavedTabGroupsToggleHidden', async function() {
    loadTimeData.overrideValues({
      tabGroupsSaveUIUpdateEnabled: false,
    });
    createAppearancePage();
    await microtasksFinished();
    assertFalse(isVisible(appearancePage.$.showSavedTabGroups));
  });

  test('ShowAutoPinNewTabGroupsToggleVisible', async function() {
    loadTimeData.overrideValues({
      tabGroupsSaveUIUpdateEnabled: true,
    });
    createAppearancePage();
    await microtasksFinished();
    assertTrue(isVisible(appearancePage.$.autoPinNewTabGroups));
  });

  test('ShowAutoPinNewTabGroupsToggleHidden', async function() {
    loadTimeData.overrideValues({
      tabGroupsSaveUIUpdateEnabled: false,
    });
    createAppearancePage();
    await microtasksFinished();
    assertFalse(isVisible(appearancePage.$.autoPinNewTabGroups));
  });
});

suite('TabSearchPositionSettings', () => {
  const TAB_SEARCH_IS_RIGHT_ALIGNED_PREF_PATH = 'tab_search.is_right_aligned';
  const DEFAULT_TAB_SEARCH_IS_RIGHT_ALIGNED = false;
  const UI_FEATURE_ALIGN_LEFT = 'foo';
  const UI_FEATURE_ALIGN_RIGHT = 'bar';
  const FALSEY_STRING = 'false';
  const TRUTHY_STRING = 'true';

  async function buildPage(startupPref: boolean, currentPref: boolean) {
    loadTimeData.overrideValues({
      uiFeatureAlignLeft: UI_FEATURE_ALIGN_LEFT,
      uiFeatureAlignRight: UI_FEATURE_ALIGN_RIGHT,
      showTabSearchPositionSettings: true,
      tabSearchIsRightAlignedAtStartup: startupPref,
    });

    createAppearancePage();

    appearancePage.setPrefValue(
        TAB_SEARCH_IS_RIGHT_ALIGNED_PREF_PATH, currentPref);
    flush();
    await microtasksFinished();
  }

  function getTabSearchDropdown(): SettingsDropdownMenuElement|null {
    return appearancePage.shadowRoot!
        .querySelector<SettingsDropdownMenuElement>(
            '#tabSearchPositionDropdown');
  }

  function getTabSearchRestartButton(): HTMLElement|null {
    return appearancePage.shadowRoot!.querySelector(
        '#tabSearchPositionRestart');
  }

  async function userClicksDropdownForOption(userChoice: boolean) {
    const dropdown: SettingsDropdownMenuElement|null = getTabSearchDropdown();
    if (dropdown === null) {
      return;
    }

    dropdown.$.dropdownMenu.value = userChoice ? TRUTHY_STRING : FALSEY_STRING;
    dropdown.dispatchEvent(new CustomEvent('change'));

    // simulate the pref changing in the backend, This doesnt get triggered
    // because the prefs are hardcoded.
    appearancePage.setPrefValue(
        TAB_SEARCH_IS_RIGHT_ALIGNED_PREF_PATH, userChoice);
    flush();
    await microtasksFinished();
  }

  setup(async () => {
    await buildPage(
        DEFAULT_TAB_SEARCH_IS_RIGHT_ALIGNED,
        DEFAULT_TAB_SEARCH_IS_RIGHT_ALIGNED);
  });

  test('shows when showTabSearchPositionSettings is true', () => {
    assertTrue(!!getTabSearchDropdown());
  });

  test('dropdown has expected options', () => {
    const dropdown: SettingsDropdownMenuElement|null = getTabSearchDropdown();

    assertTrue(!!dropdown);
    assertEquals(2, dropdown?.menuOptions.length);
    assertTrue(!!dropdown?.menuOptions.some(
        option => option.name === UI_FEATURE_ALIGN_LEFT &&
            option.value === FALSEY_STRING));
    assertTrue(!!dropdown?.menuOptions.some(
        option => option.name === UI_FEATURE_ALIGN_RIGHT &&
            option.value === TRUTHY_STRING));
  });

  test('dropdown sets the value', async () => {
    const dropdown: SettingsDropdownMenuElement|null = getTabSearchDropdown();

    // Should be set to initial option of "False" based on pref.
    assertEquals(FALSEY_STRING, dropdown?.getSelectedValue());

    // on user click of true, the dropdown should now show truthy
    await userClicksDropdownForOption(/*userChoice=*/ true);
    assertEquals(TRUTHY_STRING, dropdown?.getSelectedValue());

    // on user click of false, the dropdown should now show falsey
    await userClicksDropdownForOption(/*userChoice=*/ false);
    assertEquals(FALSEY_STRING, dropdown?.getSelectedValue());
  });

  test('restart button A11y', async () => {
    await buildPage(/*startupPref=*/ false, /*currentPref=*/ true);
    const button = getTabSearchRestartButton();
    assertTrue(!!button);

    // The restart button needs to have the "alert" aria attribute.
    assertEquals('alert', button.role);
  });

  test('restart button steady state', async () => {
    await buildPage(/*startupPref=*/ false, /*currentPref=*/ false);
    assertFalse(!!getTabSearchRestartButton());

    await buildPage(/*startupPref=*/ false, /*currentPref=*/ true);
    assertTrue(!!getTabSearchRestartButton());

    await buildPage(/*startupPref=*/ true, /*currentPref=*/ false);
    assertTrue(!!getTabSearchRestartButton());

    await buildPage(/*startupPref=*/ true, /*currentPref=*/ true);
    assertFalse(!!getTabSearchRestartButton());
  });

  test('restart button shows on change', async () => {
    assertFalse(!!getTabSearchRestartButton());

    await userClicksDropdownForOption(/*userChoice=*/ true);
    assertTrue(!!getTabSearchRestartButton());

    await userClicksDropdownForOption(/*userChoice=*/ false);
    assertFalse(!!getTabSearchRestartButton());
  });
});