chromium/chrome/test/data/webui/chromeos/settings/utils.ts

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

import {assertTrue} from 'chrome://webui-test/chai_assert.js';

const RETRY_INTERVAL_MILLISECONDS = 0.2 * 1000;
const RETRY_TIMEOUT_MILLISECONDS = 10 * 1000;
const SATISFACTION_MILLISECONDS = 1 * 1000;

export function hasProperty<X extends {}, Y extends PropertyKey>(
    obj: X, prop: Y): obj is X&Record<Y, unknown> {
  return prop in obj;
}

export function hasBooleanProperty<X extends {}, Y extends PropertyKey>(
    obj: X, prop: Y): obj is X&Record<Y, boolean> {
  return hasProperty(obj, prop) && typeof obj[prop] === 'boolean';
}

export function hasStringProperty<X extends {}, Y extends PropertyKey>(
    obj: X, prop: Y): obj is X&Record<Y, string> {
  return hasProperty(obj, prop) && typeof obj[prop] === 'string';
}

export function sleep(milliseconds: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, milliseconds));
}

export type Lazy<T> = () => T;

// Repeatedly evaluates a lazy |value| until it evaluates without throwing an
// exception, or if a timeout is exceeded.
export async function retry<T>(
    value: Lazy<T>,
    timeoutMilliseconds: number = RETRY_TIMEOUT_MILLISECONDS): Promise<T> {
  while (true) {
    try {
      const val = value();
      return val;
    } catch (err) {
      if (timeoutMilliseconds <= 0) {
        throw err;
      }
    }

    const sleepMilliseconds =
        Math.min(RETRY_INTERVAL_MILLISECONDS, timeoutMilliseconds);
    await sleep(sleepMilliseconds);
    timeoutMilliseconds -= sleepMilliseconds;
  }
}

// Repeatedly evaluates a lazy |value| until it evaluates without throwing an
// exception to something !== null, or if a timeout is exceeded.
export async function retryUntilSome<T>(
    value: Lazy<T|null>,
    timeoutMilliseconds: number = RETRY_TIMEOUT_MILLISECONDS): Promise<T> {
  return await retry(() => {
    const val = value();
    assertTrue(val !== null);
    return val;
  }, timeoutMilliseconds);
}

// Repeatedly evaluates a lazy |property| until it holds or a timeout is
// exceeded.
export async function assertAsync(
    property: Lazy<boolean>,
    timeoutMilliseconds: number = RETRY_TIMEOUT_MILLISECONDS): Promise<void> {
  return await retry(() => assertTrue(property()), timeoutMilliseconds);
}

// Repeatedly asserts that a |property| holds for a (short) duration.
export async function assertForDuration(
    property: Lazy<boolean>,
    satisfactionMilliseconds: number =
        SATISFACTION_MILLISECONDS): Promise<void> {
  while (true) {
    assertTrue(property());
    if (satisfactionMilliseconds <= 0) {
      return;
    }

    const sleepMilliseconds =
        Math.min(RETRY_INTERVAL_MILLISECONDS, satisfactionMilliseconds);
    await sleep(sleepMilliseconds);
    satisfactionMilliseconds -= sleepMilliseconds;
  }
}

// Finds an element that is nested inside shadow roots using a sequence of
// query selectors. The first query is run from |root|. Subsequent queries are
// run within the |shadowRoot| of the previous result. Returns |null| if any of
// the queries did not yield a result.
export function querySelectorShadow(
    root: DocumentFragment|Element, selectors: string[]): Element|null {
  assertTrue(selectors.length > 0);

  const initSelectors = selectors.slice(0, selectors.length - 1);
  const lastSelector = selectors[selectors.length - 1];
  assertTrue(lastSelector !== undefined);
  for (const selector of initSelectors) {
    const el = root.querySelector(selector);
    if (el === null || el.shadowRoot === null) {
      return null;
    }
    root = el.shadowRoot;
  }
  return root.querySelector(lastSelector);
}

/**
 * Clears the document body HTML.
 */
export function clearBody() {
  document.body.innerHTML = window.trustedTypes!.emptyHTML;
}