chromium/ash/webui/help_app_ui/test/guest_query_receiver.ts

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

import {TestMessageResponseData, TestMessageRunTestCase} from './driver_api.js';
import {TEST_ONLY} from './receiver.js';

const {parentMessagePipe} = TEST_ONLY;

interface GenericErrorResponse {
  name: string;
  message: string;
  stack: string;
}

/** Test cases registered by GUEST_TEST. */
const guestTestCases = new Map<string, () => unknown>();

/** Acts on TestMessageRunTestCase. */
async function runTestCase(data: TestMessageRunTestCase):
  Promise<TestMessageResponseData> {
  const testCase = guestTestCases.get(data.testCase);
  if (!testCase) {
    throw new Error(`Unknown test case: '${data.testCase}'`);
  }
  await testCase();  // Propagate exceptions to the MessagePipe handler.
  return {testQueryResult: 'success'};
}

/**
 * Registers a test that runs in the guest context. To indicate failure, the
 * test throws an exception (e.g. via assertEquals).
 */
// eslint-disable-next-line @typescript-eslint/naming-convention
export function GUEST_TEST(testName: string, testCase: () => unknown) {
  guestTestCases.set(testName, testCase);
}

/**
 * Tells the test driver the guest test message handlers are installed. This
 * requires the test handler that receives the signal to be set up. The order
 * that this occurs can not be guaranteed. So this function retries until the
 * signal is handled, which requires the 'test-handlers-ready' handler to be
 * registered in driver.js.
 */
async function signalTestHandlersReady() {
  const EXPECTED_ERROR =
      /No handler registered for message type 'test-handlers-ready'/;
  // Attempt to signal to the driver that we are ready to run tests, give up
  // after 10 tries and assume something went wrong so we don't spam the error
  // log too much.
  let attempts = 10;
  while (--attempts >= 0) {
    try {
      // Try to limit log output from message pipe errors.
      await new Promise(resolve => setTimeout(resolve, 100));
      await parentMessagePipe.sendMessage('test-handlers-ready', {});
      return;
    } catch (error: unknown) {
      const e = error as GenericErrorResponse;
      if (!EXPECTED_ERROR.test(e.message)) {
        console.error('Unexpected error in signalTestHandlersReady', e);
        return;
      }
    }
  }
  console.error('signalTestHandlersReady failed to signal.');
}

/** Installs the MessagePipe handlers for receiving test queries. */
function installTestHandlers() {
  // Turn off error rethrowing for tests so the test runner doesn't mark
  // our error handling tests as failed.
  parentMessagePipe.rethrowErrors = false;

  parentMessagePipe.registerHandler(
    'run-test-case', (data: TestMessageRunTestCase) => {
    return runTestCase(data);
  });

  // Log errors, rather than send them to console.error. This allows the error
  // handling tests to work correctly, and is also required for
  // signalTestHandlersReady() to operate without failing tests.
  parentMessagePipe.logClientError = (error: unknown) =>
      console.log(JSON.stringify(error));

  signalTestHandlersReady();
}

// Ensure content and all scripts have loaded before installing test handlers.
if (document.readyState !== 'complete') {
  window.addEventListener('load', installTestHandlers);
} else {
  installTestHandlers();
}