chromium/chrome/test/data/webui/cr_components/theme_color_picker/theme_color_picker_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://customize-chrome-side-panel.top-chrome/strings.m.js';
import 'chrome://resources/cr_components/theme_color_picker/theme_color_picker.js';

import type {ManagedDialogElement} from 'chrome://resources/cr_components/managed_dialog/managed_dialog.js';
import {ThemeColorPickerBrowserProxy} from 'chrome://resources/cr_components/theme_color_picker/browser_proxy.js';
import type {Color} from 'chrome://resources/cr_components/theme_color_picker/color_utils.js';
import {DARK_BASELINE_BLUE_COLOR, DARK_BASELINE_GREY_COLOR, LIGHT_BASELINE_BLUE_COLOR, LIGHT_BASELINE_GREY_COLOR} from 'chrome://resources/cr_components/theme_color_picker/color_utils.js';
import type {ThemeColorElement} from 'chrome://resources/cr_components/theme_color_picker/theme_color.js';
import type {ThemeColorPickerElement} from 'chrome://resources/cr_components/theme_color_picker/theme_color_picker.js';
import type {ChromeColor, Theme, ThemeColorPickerClientRemote} from 'chrome://resources/cr_components/theme_color_picker/theme_color_picker.mojom-webui.js';
import {ThemeColorPickerClientCallbackRouter, ThemeColorPickerHandlerRemote} from 'chrome://resources/cr_components/theme_color_picker/theme_color_picker.mojom-webui.js';
import {PromiseResolver} from 'chrome://resources/js/promise_resolver.js';
import {BrowserColorVariant} from 'chrome://resources/mojo/ui/base/mojom/themes.mojom-webui.js';
import {assertDeepEquals, assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {TestMock} from 'chrome://webui-test/test_mock.js';
import {$$, hasStyle, microtasksFinished} from 'chrome://webui-test/test_util.js';

function createTheme(isDarkMode = false): Theme {
  return {
    hasBackgroundImage: false,
    hasThirdPartyTheme: false,
    backgroundImageMainColor: null,
    isDarkMode,
    seedColor: {value: 0xff0000ff},
    seedColorHue: 0,
    backgroundColor: {value: 0xffff0000},
    foregroundColor: null,
    colorPickerIconColor: {value: 0xffff0000},
    colorsManagedByPolicy: false,
    isGreyBaseline: false,
    browserColorVariant: BrowserColorVariant.kTonalSpot,
    followDeviceTheme: false,
  };
}

suite('CrComponentsThemeColorPickerTest', () => {
  let colorsElement: ThemeColorPickerElement;
  let handler: TestMock<ThemeColorPickerHandlerRemote>&
      ThemeColorPickerHandlerRemote;
  let callbackRouter: ThemeColorPickerClientRemote;
  let chromeColorsResolver: PromiseResolver<{colors: ChromeColor[]}>;

  setup(() => {
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    handler = TestMock.fromClass(ThemeColorPickerHandlerRemote);
    ThemeColorPickerBrowserProxy.setInstance(
        handler, new ThemeColorPickerClientCallbackRouter());
    callbackRouter = ThemeColorPickerBrowserProxy.getInstance()
                         .callbackRouter.$.bindNewPipeAndPassRemote();
    chromeColorsResolver = new PromiseResolver();
  });

  function initializeElement() {
    handler.setResultFor('getChromeColors', chromeColorsResolver.promise);
    colorsElement = document.createElement('cr-theme-color-picker');
    document.body.appendChild(colorsElement);
  }

  ([
    [true, DARK_BASELINE_BLUE_COLOR],
    [false, LIGHT_BASELINE_BLUE_COLOR],
  ] as Array<[boolean, Color]>)
      .forEach(([isDarkMode, defaultColor]) => {
        test(
            `render DarkMode ${isDarkMode} default color`, async () => {
              initializeElement();
              const theme: Theme = createTheme(isDarkMode);

              callbackRouter.setTheme(theme);
              await callbackRouter.$.flushForTesting();

              const defaultColorElement =
                  $$<ThemeColorElement>(colorsElement, '#defaultColor')!;
              assertDeepEquals(
                  defaultColor.foreground, defaultColorElement.foregroundColor);
              assertDeepEquals(
                  defaultColor.background, defaultColorElement.backgroundColor);
              assertDeepEquals(
                  defaultColor.base, defaultColorElement.baseColor);
            });
      });

  ([
    [true, DARK_BASELINE_GREY_COLOR],
    [false, LIGHT_BASELINE_GREY_COLOR],
  ] as Array<[boolean, Color]>)
      .forEach(([isDarkMode, greyDefaultColor]) => {
        test(`render DarkMode ${isDarkMode} grey default color`, async () => {
          initializeElement();
          const theme: Theme = createTheme(isDarkMode);

          callbackRouter.setTheme(theme);
          await callbackRouter.$.flushForTesting();

          const greyDefaultColorElement =
              $$<ThemeColorElement>(colorsElement, '#greyDefaultColor')!;
          assertDeepEquals(
              greyDefaultColor.foreground,
              greyDefaultColorElement.foregroundColor);
          assertDeepEquals(
              greyDefaultColor.background,
              greyDefaultColorElement.backgroundColor);
          assertDeepEquals(
              greyDefaultColor.base, greyDefaultColorElement.baseColor);
        });
      });

  test('sets default color', async () => {
    initializeElement();
    const theme = createTheme();
    theme.foregroundColor = null;
    callbackRouter.setTheme(theme);
    await callbackRouter.$.flushForTesting();

    $$<HTMLElement>(colorsElement, '#defaultColor')!.click();

    assertEquals(1, handler.getCallCount('setDefaultColor'));
  });

  test('sets grey default color', async () => {
    initializeElement();
    const theme = createTheme();
    theme.foregroundColor = null;
    callbackRouter.setTheme(theme);
    await callbackRouter.$.flushForTesting();

    $$<HTMLElement>(colorsElement, '#greyDefaultColor')!.click();

    assertEquals(1, handler.getCallCount('setGreyDefaultColor'));
  });

  test('renders chrome colors', async () => {
    const theme = createTheme();
    callbackRouter.setTheme(theme);
    initializeElement();
    const colors = {
      colors: [
        {
          id: 1,
          name: 'foo',
          seed: {value: 5},
          background: {value: 1},
          foreground: {value: 2},
          base: {value: 3},
          variant: BrowserColorVariant.kTonalSpot,
        },
        {
          id: 2,
          name: 'bar',
          seed: {value: 6},
          background: {value: 3},
          foreground: {value: 4},
          base: {value: 5},
          variant: BrowserColorVariant.kNeutral,
        },
      ],
    };

    chromeColorsResolver.resolve(colors);
    await microtasksFinished();

    const colorElements =
        colorsElement.shadowRoot!.querySelectorAll<ThemeColorElement>(
            '.chrome-color');
    assertEquals(2, colorElements.length);
    assertDeepEquals({value: 1}, colorElements[0]!.backgroundColor);
    assertDeepEquals({value: 2}, colorElements[0]!.foregroundColor);
    assertDeepEquals({value: 3}, colorElements[0]!.baseColor);
    assertEquals('foo', colorElements[0]!.title);
    assertDeepEquals({value: 3}, colorElements[1]!.backgroundColor);
    assertDeepEquals({value: 4}, colorElements[1]!.foregroundColor);
    assertDeepEquals({value: 5}, colorElements[1]!.baseColor);
    assertEquals('bar', colorElements[1]!.title);
  });

  test('sets chrome color', async () => {
    const theme = createTheme();
    callbackRouter.setTheme(theme);
    initializeElement();
    const colors = {
      colors: [
        {
          id: 1,
          name: 'foo',
          seed: {value: 3},
          background: {value: 1},
          foreground: {value: 2},
          base: {value: 4},
          variant: BrowserColorVariant.kNeutral,
        },
      ],
    };

    chromeColorsResolver.resolve(colors);
    await microtasksFinished();
    colorsElement.shadowRoot!.querySelector<ThemeColorElement>(
                                 '.chrome-color')!.click();

    const args = handler.getArgs('setSeedColor')[0];
    assertEquals(1, handler.getCallCount('setSeedColor'));
    assertEquals(3, args[0].value);
    assertEquals(BrowserColorVariant.kNeutral, args[1]);
  });

  test('sets custom color from hue slider', async () => {
    initializeElement();
    callbackRouter.setTheme(Object.assign(createTheme(), {seedColorHue: 10}));
    await callbackRouter.$.flushForTesting();

    colorsElement.$.hueSlider.selectedHue = 10;
    colorsElement.$.hueSlider.dispatchEvent(new Event('selected-hue-changed'));
    assertEquals(0, handler.getCallCount('setSeedColorFromHue'));

    colorsElement.$.hueSlider.selectedHue = 30;
    colorsElement.$.hueSlider.dispatchEvent(new Event('selected-hue-changed'));
    assertEquals(1, handler.getCallCount('setSeedColorFromHue'));
    assertEquals(30, handler.getArgs('setSeedColorFromHue')[0]);
  });

  test('updates custom color for theme', async () => {
    initializeElement();
    const colors = {
      colors: [
        {
          id: 1,
          name: 'foo',
          seed: {value: 3},
          background: {value: 1},
          foreground: {value: 2},
          base: {value: 3},
          variant: BrowserColorVariant.kTonalSpot,
        },
      ],
    };
    chromeColorsResolver.resolve(colors);

    // Set a custom color theme.
    const customColortheme = createTheme();
    customColortheme.seedColor = {value: 0xffffff00};
    customColortheme.backgroundColor = {value: 0xffff0000};
    customColortheme.foregroundColor = {value: 0xff00ff00};
    customColortheme.colorPickerIconColor = {value: 0xff0000ff};
    callbackRouter.setTheme(customColortheme);
    await callbackRouter.$.flushForTesting();

    // Custom color circle should be updated.
    assertEquals(0xffff0000, colorsElement.$.customColor.backgroundColor.value);
    assertEquals(0xff00ff00, colorsElement.$.customColor.foregroundColor.value);
    assertTrue(hasStyle(
        colorsElement.$.colorPickerIcon, 'background-color', 'rgb(0, 0, 255)'));

    // Set a theme that is not a custom color theme.
    const otherTheme = createTheme();
    otherTheme.seedColor = {value: 0xff00ff00};
    otherTheme.backgroundColor = {value: 0xffffffff};
    otherTheme.foregroundColor = null;  // Makes a default theme.
    otherTheme.colorPickerIconColor = {value: 0xffffffff};
    callbackRouter.setTheme(otherTheme);
    await callbackRouter.$.flushForTesting();

    // Custom color circle should be not be updated.
    assertEquals(0xffff0000, colorsElement.$.customColor.backgroundColor.value);
    assertEquals(0xff00ff00, colorsElement.$.customColor.foregroundColor.value);
    assertTrue(hasStyle(
        colorsElement.$.colorPickerIcon, 'background-color', 'rgb(0, 0, 255)'));
  });

  test('update colorPicker value for theme', async () => {
    initializeElement();
    const colors = {
      colors: [
        {
          id: 1,
          name: 'foo',
          seed: {value: 3},
          background: {value: 1},
          foreground: {value: 2},
          base: {value: 3},
          variant: BrowserColorVariant.kTonalSpot,
        },
      ],
    };
    chromeColorsResolver.resolve(colors);

    // Set a custom color theme.
    const customColortheme = createTheme();
    customColortheme.seedColorHue = 22;
    customColortheme.backgroundColor = {value: 0xffff0000};
    customColortheme.foregroundColor = {value: 0xff00ff00};
    customColortheme.colorPickerIconColor = {value: 0xff0000ff};
    callbackRouter.setTheme(customColortheme);
    await callbackRouter.$.flushForTesting();

    // Custom hue slider value should be updated.
    assertEquals(
        colorsElement.$.hueSlider.selectedHue, customColortheme.seedColorHue);

    // Set a theme that is not a custom color theme.
    const otherTheme = createTheme();
    otherTheme.backgroundColor = {value: 0xffffffff};
    otherTheme.foregroundColor = null;  // Makes a default theme.
    otherTheme.colorPickerIconColor = {value: 0xffffffff};
    callbackRouter.setTheme(otherTheme);
    await callbackRouter.$.flushForTesting();

    // Custom hue slider value should still be the same.
    assertEquals(
        colorsElement.$.hueSlider.selectedHue, customColortheme.seedColorHue);
  });

  test('checks selected color', async () => {
    initializeElement();
    const colors = {
      colors: [
        {
          id: 1,
          name: 'foo',
          seed: {value: 5},
          background: {value: 1},
          foreground: {value: 2},
          base: {value: 3},
          variant: BrowserColorVariant.kTonalSpot,
        },
        {
          id: 2,
          name: 'bar',
          seed: {value: 6},
          background: {value: 3},
          foreground: {value: 4},
          base: {value: 5},
          variant: BrowserColorVariant.kNeutral,
        },
      ],
    };
    chromeColorsResolver.resolve(colors);
    const theme = createTheme();

    // Set default color.
    theme.foregroundColor = null;
    callbackRouter.setTheme(theme);
    await callbackRouter.$.flushForTesting();

    // Check default color selected.
    const defaultColorElement =
        $$<ThemeColorElement>(colorsElement, '#defaultColor')!;
    let checkedColors =
        colorsElement.shadowRoot!.querySelectorAll<ThemeColorElement>(
            '[checked]');
    assertEquals(1, checkedColors.length);
    assertEquals(defaultColorElement, checkedColors[0]);
    assertEquals(defaultColorElement.getAttribute('aria-checked'), 'true');
    let indexedColors =
        colorsElement.shadowRoot!.querySelectorAll('[tabindex="0"]');
    assertEquals(1, indexedColors.length);
    assertEquals(defaultColorElement, indexedColors[0]);

    // Set Chrome color.
    theme.seedColor = {value: 5};
    theme.foregroundColor = {value: 2};
    callbackRouter.setTheme(theme);
    await callbackRouter.$.flushForTesting();

    // Check Chrome color selected.
    checkedColors =
        colorsElement.shadowRoot!.querySelectorAll<ThemeColorElement>(
            '[checked]');
    assertEquals(1, checkedColors.length);
    assertEquals('chrome-color', checkedColors[0]!.className);
    assertEquals(checkedColors[0]!.getAttribute('aria-checked'), 'true');
    assertEquals(2, checkedColors[0]!.foregroundColor.value);
    assertEquals(3, checkedColors[0]!.baseColor!.value);
    indexedColors =
        colorsElement.shadowRoot!.querySelectorAll('[tabindex="0"]');
    assertEquals(1, indexedColors.length);
    assertEquals('chrome-color', indexedColors[0]!.className);

    // Set custom color.
    theme.seedColor = {value: 10};
    theme.foregroundColor = {value: 5};
    callbackRouter.setTheme(theme);
    await callbackRouter.$.flushForTesting();

    // Check custom color selected.
    checkedColors = colorsElement.shadowRoot!.querySelectorAll('[checked]');
    assertEquals(1, checkedColors.length);
    assertEquals(colorsElement.$.customColor, checkedColors[0]);
    assertEquals(
        colorsElement.$.customColorContainer.getAttribute('aria-checked'),
        'true');
    indexedColors =
        colorsElement.shadowRoot!.querySelectorAll('[tabindex="0"]');
    assertEquals(1, indexedColors.length);
    assertEquals(colorsElement.$.customColorContainer, indexedColors[0]);
  });

  [false, true].forEach(hasBackgroundImage => {
    test(
        `background color visible if theme has image ${hasBackgroundImage}`,
        async () => {
          initializeElement();
          const theme = createTheme();
          theme.hasBackgroundImage = hasBackgroundImage;
          callbackRouter.setTheme(theme);
          await callbackRouter.$.flushForTesting();

          const colors =
              colorsElement.shadowRoot!.querySelectorAll('cr-theme-color');
          for (const color of colors) {
            if (color.id === 'customColor') {
              assertTrue(color.backgroundColorHidden);
            } else {
              assertFalse(color.backgroundColorHidden);
            }
          }
        });
  });

  ([
    ['#defaultColor', null],
    ['.chrome-color', 3],
    ['#customColor', 10],
  ] as Array<[string, number]>)
      .forEach(([selector, foregroundColor]) => {
        test(`respects policy for ${selector}`, async () => {
          initializeElement();
          const colors = {
            colors: [
              {
                id: 1,
                name: 'foo',
                seed: {value: 3},
                background: {value: 1},
                foreground: {value: 2},
                base: {value: 3},
                variant: BrowserColorVariant.kTonalSpot,
              },
            ],
          };
          chromeColorsResolver.resolve(colors);
          const theme = createTheme();
          if (foregroundColor) {
            theme.foregroundColor = {value: foregroundColor};
          }
          theme.hasBackgroundImage = true;
          theme.colorsManagedByPolicy = true;
          callbackRouter.setTheme(theme);
          await callbackRouter.$.flushForTesting();

          $$<HTMLElement>(colorsElement, selector)!.click();
          await microtasksFinished();

          const managedDialog =
              $$<ManagedDialogElement>(colorsElement, 'managed-dialog');
          assertTrue(!!managedDialog);
          assertTrue(managedDialog.$.dialog.open);
          assertEquals(0, handler.getCallCount('setDefaultColor'));
          assertEquals(0, handler.getCallCount('setSeedColor'));
        });
      });
});