chromium/ash/webui/help_app_ui/test/help_app_guest_ui_browsertest.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.

/** @fileoverview Test suite for chrome-untrusted://help-app. */

import {GUEST_TEST} from './guest_query_receiver.js';

// Test that language is set correctly on the guest frame.
GUEST_TEST('GuestHasLang', () => {
  assertEquals(document.documentElement.lang, 'en-US');
});

GUEST_TEST('GuestLoadsLoadTimeData', () => {
  // TODO(b/313562946): Add types for `sandboxed_load_time_data.js`.
  const loadTimeData = (window as any)['loadTimeData'];
  // Check `LoadTimeData` exists on the global window object.
  chai.assert.isTrue(loadTimeData !== undefined);
  assertEquals(loadTimeData.getString('appLocale'), 'en-US');
});

/**
 * Waits for the app's initial index update to complete. This prevents it from
 * interfering with test code. After the update completes, there will be at
 * least one search result for the query "Chrome".
 *
 * Returns the app's client API delegate.
 */
async function waitForInitialIndexUpdate() {
  const delegate = window.customLaunchData.delegate;

  for (let numTries = 0; numTries < 50; numTries++) {
    // 'Chrome' appears in the mock app's fake search results, and should appear
    // in the real app's search results.
    const response = await delegate.findInSearchIndex('Chrome');
    if (response && response.results && response.results.length > 0) {
      break;
    }
    await new Promise(resolve => {
      setTimeout(resolve, 50);
    });
  }
  return delegate;
}

// Test that search (add, find) works when called from the guest frame,
// and it works for searchable items with and without subheadings.
GUEST_TEST('GuestCanSearchWithHeadings', async () => {
  const delegate = await waitForInitialIndexUpdate();

  await delegate.addOrUpdateSearchIndex([
    {
      // Title match. No subheadings.
      id: 'test-id-1',
      title: 'Title with verycomplicatedsearchtoken',
      body: 'Body text',
      mainCategoryName: 'Help',
      locale: 'en-US',
    },
    {
      // Subheading match.
      id: 'test-id-2',
      title: 'Title 2',
      subheadings: [
        'Subheading 1',
        'verycomplicatedsearchtoken in subheading. Verycomplicatedsearchtoken',
        'Another subheading with verycomplicatedsearchtoken',
      ],
      body: 'Body text',
      mainCategoryName: 'Help',
      locale: 'en-US',
    },
    {
      // Should not appear in the results.
      id: 'test-id-3',
      title: 'Title of irrelevant article',
      body: 'Body text',
      mainCategoryName: 'Help',
      locale: 'en-US',
    },
  ]);

  // Keep polling until the index finishes updating or too much time has passed.
  let response: FindResponse | undefined;
  for (let numTries = 0; numTries < 50; numTries++) {
    // This search query was chosen because it is unlikely to show any search
    // results for the real app's data.
    response = await delegate.findInSearchIndex('verycomplicatedsearchtoken');
    if (response && response.results && response.results.length > 0) {
      break;
    }
    await new Promise(resolve => {
      setTimeout(resolve, 50);
    });
  }

  assertDeepEquals(response!.results, [
    // The first result only matches on the title.
    {
      id: 'test-id-1',
      titlePositions: [{start: 11, length: 26}],
      subheadingIndex: null,
      subheadingPositions: null,
      bodyPositions: [],
    },
    // The second result only matches on the second and third subheadings, and
    // it uses the subheading with the most matches in the snippet.
    {
      id: 'test-id-2',
      titlePositions: [],
      subheadingIndex: 1,
      subheadingPositions: [
        {start: 0, length: 26},
        {start: 42, length: 26},
      ],
      bodyPositions: [],
    },
  ]);
});

// Test that search works for the categories and subcategories of searchable
// items.
GUEST_TEST('GuestCanSearchWithCategories', async () => {
  const delegate = await waitForInitialIndexUpdate();

  await delegate.addOrUpdateSearchIndex([
    {
      // Main category match. No subcategories.
      id: 'test-id-1',
      title: 'Title with of article',
      body: 'Body text',
      mainCategoryName: 'Verycomplicatedsearchtoken',
      locale: 'en-US',
    },
    {
      // Subcategory match.
      id: 'test-id-2',
      title: 'Title 2',
      subcategoryNames: [
        'Subcategory 1',
        'verycomplicatedsearchtoken in subcategory. Verycomplicatedsearchtoken',
        'Another subcategory with verycomplicatedsearchtoken',
      ],
      body: 'Body text',
      mainCategoryName: 'Help',
      locale: 'en-US',
    },
    {
      // Should not appear in the results.
      id: 'test-id-3',
      title: 'Title of irrelevant article',
      body: 'Body text',
      mainCategoryName: 'Help',
      locale: 'en-US',
    },
  ]);

  // Keep polling until the index finishes updating or too much time has passed.
  let response: FindResponse | undefined;
  for (let numTries = 0; numTries < 50; numTries++) {
    // This search query was chosen because it is unlikely to show any search
    // results for the real app's data.
    response = await delegate.findInSearchIndex('verycomplicatedsearchtoken');
    if (response && response.results && response.results.length > 0) {
      break;
    }
    await new Promise(resolve => {
      setTimeout(resolve, 50);
    });
  }

  // Don't test the ordering of search results because they should have similar
  // relevance.
  chai.assert.sameDeepMembers(response!.results, [
    // This result only matches on the main category.
    {
      id: 'test-id-1',
      titlePositions: [],
      subheadingIndex: null,
      subheadingPositions: null,
      bodyPositions: [],
    },
    // This result only matches on the second and third subcategories.
    {
      id: 'test-id-2',
      titlePositions: [],
      subheadingIndex: null,
      subheadingPositions: null,
      bodyPositions: [],
    },
  ]);
});

// Test that the number of search results is reduced when maxResults is
// provided.
GUEST_TEST('GuestCanLimitMaxSearchResults', async () => {
  const delegate = await waitForInitialIndexUpdate();

  await delegate.addOrUpdateSearchIndex([
    {
      // Main category match. No subcategories.
      id: 'test-id-1',
      title: 'Title with of article',
      body: 'Body text',
      mainCategoryName: 'Verycomplicatedsearchtoken',
      locale: 'en-US',
    },
    {
      // Subcategory match.
      id: 'test-id-2',
      title: 'Title 2',
      subcategoryNames: [
        'Subcategory 1',
        'verycomplicatedsearchtoken in subcategory.',
      ],
      body: 'Body text',
      mainCategoryName: 'Help',
      locale: 'en-US',
    },
  ]);

  // Limit to 1 result. This search query was chosen because it is unlikely to
  // show any search results for the real app's data.
  const res = await delegate.findInSearchIndex('verycomplicatedsearchtoken', 1);

  assertDeepEquals(res.results, [
    {
      id: 'test-id-1',
      titlePositions: [],
      subheadingIndex: null,
      subheadingPositions: null,
      bodyPositions: [],
    },
  ]);
});

// Test that the guest frame can clear the search index.
GUEST_TEST('GuestCanClearSearchIndex', async () => {
  const delegate = await waitForInitialIndexUpdate();

  // Clear resolves after the index finishes clearing, so we don't need to try
  // finding multiple times.
  await delegate.clearSearchIndex();

  const res = await delegate.findInSearchIndex('Chrome');
  assertDeepEquals(res, {results: null});
});

// Test that the guest frame can get device info.
GUEST_TEST('GuestCanGetDeviceInfo', async () => {
  const delegate = window.customLaunchData.delegate;

  const deviceInfo = await delegate.getDeviceInfo();
  assertEquals(typeof deviceInfo.board, 'string');
  assertEquals(typeof deviceInfo.model, 'string');
  assertEquals(typeof deviceInfo.userType, 'string');

  assertEquals(typeof deviceInfo.isSteamAllowed, 'boolean');
});