chromium/chrome/test/data/webui/chromeos/personalization_app/personalization_app_test_utils.ts

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

/**
 * @fileoverview A set of utilities to help test the chrome://personalization
 * SWA.
 */

import {emptyState, getSeaPenStore, PersonalizationState, SeaPenStoreAdapter, setAmbientProviderForTesting, setKeyboardBacklightProviderForTesting, setSeaPenProviderForTesting, setThemeProviderForTesting, setUserProviderForTesting, setWallpaperProviderForTesting} from 'chrome://personalization/js/personalization_app.js';
import {flush, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {flushTasks, waitAfterNextRender} from 'chrome://webui-test/polymer_test_util.js';

import {TestAmbientProvider} from './test_ambient_interface_provider.js';
import {TestKeyboardBacklightProvider} from './test_keyboard_backlight_interface_provider.js';
import {TestPersonalizationStore} from './test_personalization_store.js';
import {TestSeaPenProvider} from './test_sea_pen_interface_provider.js';
import {TestThemeProvider} from './test_theme_interface_provider.js';
import {TestUserProvider} from './test_user_interface_provider.js';
import {TestWallpaperProvider} from './test_wallpaper_interface_provider.js';

/**
 * Constructs the given element with properties and appends it to body.
 */
export function initElement<T extends PolymerElement>(
    cls: {new (): T, is: string}, properties = {}): T {
  const element = document.createElement(cls.is) as T & HTMLElement;
  for (const [key, value] of Object.entries(properties)) {
    (element as any)[key] = value;
  }
  document.body.appendChild(element);
  flush();
  return element;
}

/**
 * Tear down an element. Remove from dom and call |flushTasks| to finish any
 * async cleanup in polymer and execute pending promises.
 */
export async function teardownElement(element: HTMLElement|null) {
  if (!element) {
    return;
  }
  element.remove();
  await flushTasks();
}

/**
 * Sets up the test wallpaper provider, test personalization store, and clears
 * the page.
 */
export function baseSetup(initialState: PersonalizationState = emptyState()) {
  const wallpaperProvider = new TestWallpaperProvider();
  setWallpaperProviderForTesting(wallpaperProvider);
  const ambientProvider = new TestAmbientProvider();
  setAmbientProviderForTesting(ambientProvider);
  const keyboardBacklightProvider = new TestKeyboardBacklightProvider();
  setKeyboardBacklightProviderForTesting(keyboardBacklightProvider);
  const themeProvider = new TestThemeProvider();
  setThemeProviderForTesting(themeProvider);
  const userProvider = new TestUserProvider();
  setUserProviderForTesting(userProvider);
  const seaPenProvider = new TestSeaPenProvider();
  setSeaPenProviderForTesting(seaPenProvider);
  const personalizationStore = new TestPersonalizationStore(initialState);
  personalizationStore.replaceSingleton();
  // Re-init SeaPenStoreAdapter so that it sees TestPersonalizationStore.
  SeaPenStoreAdapter.initSeaPenStore();
  document.body.innerHTML = window.trustedTypes!.emptyHTML;
  return {
    ambientProvider,
    keyboardBacklightProvider,
    seaPenProvider,
    themeProvider,
    userProvider,
    wallpaperProvider,
    personalizationStore,
    seaPenStore: getSeaPenStore(),
  };
}

/**
 * Returns a svg data url. This is useful in tests to force img on-load events
 * to fire so that wallpaper-grid-item resolves its loading state.
 */
export function createSvgDataUrl(id: string): string {
  return 'data:image/svg+xml;utf8,' +
      '<svg xmlns="http://www.w3.org/2000/svg" ' +
      `height="100px" width="100px" id="${id}">` +
      '<rect fill="red" height="100px" width="100px"></rect>' +
      '</svg>';
}

/**
 * Waits for the specified |element| to be the active element in
 * the containing element's shadow DOM.
 */
export async function waitForActiveElement(
    targetElement: Element, elementContainer: HTMLElement) {
  while (elementContainer.shadowRoot!.activeElement !== targetElement) {
    await waitAfterNextRender(elementContainer!);
  }
}

/** Dispatches a keydown event to |element| for the specified |key|. */
export function dispatchKeydown(element: HTMLElement, key: string) {
  const init: KeyboardEventInit = {bubbles: true, key};
  switch (key) {
    case 'ArrowDown':
      init.keyCode = 40;
      break;
    case 'ArrowRight':
      init.keyCode = 39;
      break;
    case 'ArrowLeft':
      init.keyCode = 37;
      break;
    case 'ArrowUp':
      init.keyCode = 38;
      break;
  }
  element.dispatchEvent(new KeyboardEvent('keydown', init));
}

/** Returns the active element in the given element's shadow DOM. */
export function getActiveElement(element: Element): HTMLElement {
  return (element.shadowRoot!.activeElement as HTMLElement);
}

/**
 * Get a sub-property in obj. Splits on '.'
 */
function getProperty(obj: object, key: string): unknown {
  let ref: any = obj;
  for (const part of key.split('.')) {
    ref = ref[part];
  }
  return ref;
}

/**
 * Returns a function that returns only nested subproperties in state.
 */
export function filterAndFlattenState(keys: string[]): (state: any) => any {
  return (state) => {
    const result: any = {};
    for (const key of keys) {
      result[key] = getProperty(state, key);
    }
    return result;
  };
}

/**
 * Forces typescript compiler to check that an anonymous value is a specific
 * type.
 */
export function typeCheck<T>(value: T): T {
  return value;
}