chromium/chrome/test/data/webui/metrics_internals/field_trials_test.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 'chrome://metrics-internals/app.js';

import {MetricsInternalsBrowserProxyImpl} from 'chrome://metrics-internals/browser_proxy.js';
import type {FieldTrialState, HashNameMap, KeyValue, MetricsInternalsBrowserProxy, Trial} from 'chrome://metrics-internals/browser_proxy.js';
import type {FieldTrialsAppElement} from 'chrome://metrics-internals/field_trials.js';
import {assertDeepEquals, assertEquals, assertNotEquals, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {TestBrowserProxy} from 'chrome://webui-test/test_browser_proxy.js';

function wait(): Promise<void> {
  return new Promise(resolve => {
    window.setTimeout(() => {
      resolve();
    }, 1);
  });
}

class FakeBrowser extends TestBrowserProxy implements
    MetricsInternalsBrowserProxy {
  trialState: FieldTrialState = {
    trials: [],
    restartRequired: false,
  };
  lookupTrialOrGroupNameResult: HashNameMap = {};

  constructor() {
    super([
      'getUmaLogData',
      'fetchVariationsSummary',
      'fetchUmaSummary',
      'isUsingMetricsServiceObserver',
      'setTrialEnrollState',
      'fetchTrialState',
      'lookupTrialOrGroupName',
      'restart',
    ]);
  }

  // Note: Each browser proxy method uses `wait()` so that the returned
  // promise isn't immediately resolved. This mirrors the actual browser
  // proxy more accurately.

  async getUmaLogData(includeLogProtoData: boolean): Promise<string> {
    this.methodCalled('getUmaLogData', includeLogProtoData);
    await wait();
    return '';
  }

  async fetchVariationsSummary(): Promise<KeyValue[]> {
    this.methodCalled('fetchVariationsSummary');
    await wait();
    return [];
  }

  async fetchUmaSummary(): Promise<KeyValue[]> {
    this.methodCalled('fetchUmaSummary');
    await wait();
    return [];
  }

  async isUsingMetricsServiceObserver(): Promise<boolean> {
    this.methodCalled('isUsingMetricsServiceObserver');
    await wait();
    return false;
  }

  async setTrialEnrollState(
      trialHash: string, groupHash: string,
      forceEnable: boolean): Promise<boolean> {
    this.methodCalled('setTrialEnrollState', trialHash, groupHash, forceEnable);
    await wait();
    return false;
  }

  async fetchTrialState(): Promise<FieldTrialState> {
    this.methodCalled('fetchTrialState');
    await wait();
    return this.trialState;
  }

  async lookupTrialOrGroupName(name: string): Promise<HashNameMap> {
    this.methodCalled('lookupTrialOrGroupName', name);
    await wait();
    return this.lookupTrialOrGroupNameResult;
  }

  async restart(): Promise<void> {
    this.methodCalled('restart');
    await wait();
  }
}

function makeTrial(name: string): Trial {
  return {
    name,
    hash: btoa(name),
    groups: [
      {
        name: 'On',
        hash: '1',
        enabled: false,
        forceEnabled: false,
      },
      {
        name: 'Off',
        hash: '2',
        enabled: false,
        forceEnabled: false,
      },
    ],
  };
}

interface DisplayedTrial {
  title: string;
  groups: DisplayedGroup[];
}

interface DisplayedGroup {
  title: string;
  enrolled: boolean;
  overridden: boolean;
}


suite('FieldTrialsTest', function() {
  let app: FieldTrialsAppElement;
  let fakeBrowser: FakeBrowser;

  /** Returns the list of field trials currently displayed. */
  const getDisplayedTrials = async(): Promise<DisplayedTrial[]> => {
    await waitForUpdate();
    const displayedTrials: DisplayedTrial[] = [];
    for (const trialDiv of app.shadowRoot!.querySelectorAll<HTMLElement>(
             '#field-trial-list .trial-row')) {
      if (!trialDiv.checkVisibility()) {
        continue;
      }
      const groups: DisplayedGroup[] =
          Array.from(trialDiv.querySelectorAll<HTMLElement>('.experiment-row'))
              .map(experimentRow => ({
                     title: experimentRow.querySelector('.experiment-name')!
                                .textContent!.trim(),
                     enrolled: experimentRow.dataset['enrolled'] === '1',
                     overridden: experimentRow
                                     .querySelector<HTMLInputElement>(
                                         '.override input')!.checked,
                   }));
      if (groups.length) {
        displayedTrials.push({
          title: trialDiv.querySelector('.trial-header')!.textContent!.trim(),
          groups,
        });
      }
    }
    return displayedTrials;
  };

  const filterInputElement = (): HTMLInputElement => {
    const filterInput = app.shadowRoot!.getElementById('filter');
    assertNotEquals(filterInput, null);
    return filterInput as HTMLInputElement;
  };

  async function waitForUpdate() {
    while (app.dirty) {
      await new Promise<void>(resolve => {
        app.onUpdateForTesting = () => {
          resolve();
        };
      });
    }
  }

  async function makeApp() {
    app = document.createElement('field-trials-app');
    document.body.replaceChildren(app);
    await waitForUpdate();
  }

  setup(() => {
    MetricsInternalsBrowserProxyImpl.setInstance(
        fakeBrowser = new FakeBrowser());
    localStorage.clear();
  });

  teardown(() => {
    MetricsInternalsBrowserProxyImpl.setInstance(
        new MetricsInternalsBrowserProxyImpl());
  });

  test('page loads with real proxy', async function() {
    MetricsInternalsBrowserProxyImpl.setInstance(
        new MetricsInternalsBrowserProxyImpl());
    await makeApp();
    assertTrue(app != null);
    assertTrue(app.shadowRoot != null);
    const blurbContainer = app.shadowRoot.querySelector('.blurb-container');
    assertTrue(blurbContainer != null);
    const blurbText = (blurbContainer as HTMLElement).innerText;
    assertTrue(
        blurbText.includes('WARNING: EXPERIMENTAL FEATURES AHEAD!'),
        'Found text: ' + blurbText);
  });

  test('page loads', async function() {
    await makeApp();
    assertTrue(app != null);
    assertTrue(app.shadowRoot != null);
    const blurbContainer = app.shadowRoot.querySelector('.blurb-container');
    assertTrue(blurbContainer != null);
    const blurbText = (blurbContainer as HTMLElement).innerText;
    assertTrue(
        blurbText.includes('WARNING: EXPERIMENTAL FEATURES AHEAD!'),
        'Found text: ' + blurbText);
  });

  test('populateState shows trials', async () => {
    const red = makeTrial('Red');
    red.groups[0]!.enabled = true;
    const blue = makeTrial('Blue');
    fakeBrowser.trialState = {
      trials: [blue, red],
      restartRequired: false,
    };
    await makeApp();
    const displayedTrials = await getDisplayedTrials();
    assertDeepEquals(
        [
          {
            title: 'Blue (#Qmx1ZQ==)',
            groups: [
              {
                title: 'On (#1)',
                enrolled: false,
                overridden: false,
              },
              {
                title: 'Off (#2)',
                enrolled: false,
                overridden: false,
              },
            ],
          },
          {
            title: 'Red (#UmVk)',
            groups: [
              {
                title: 'On (#1)',
                enrolled: true,
                overridden: false,
              },
              {
                title: 'Off (#2)',
                enrolled: false,
                overridden: false,
              },
            ],
          },
        ],
        displayedTrials, 'got ' + JSON.stringify(displayedTrials));
  });

  test('force enroll tells browser', async function() {
    fakeBrowser.trialState = {
      trials: [makeTrial('Red')],
      restartRequired: false,
    };
    await makeApp();
    // Click the first enroll checkbox.
    app.shadowRoot!.querySelector<HTMLInputElement>('.override input')!.click();
    assertDeepEquals(
        ['UmVk', '1', true],
        await fakeBrowser.whenCalled('setTrialEnrollState'));
  });

  test('force enroll allows one per trial', async function() {
    fakeBrowser.trialState = {
      trials: [makeTrial('Red')],
      restartRequired: false,
    };
    await makeApp();

    // Click the first and then the second enroll checkboxes.
    const checkboxes = Array.from(
        app.shadowRoot!.querySelectorAll<HTMLInputElement>('.override input'));
    assertEquals(checkboxes.length, 2);
    checkboxes[0]!.click();
    assertDeepEquals(
        ['UmVk', '1', true],
        await fakeBrowser.whenCalled('setTrialEnrollState'),
        'after clicking first box');

    fakeBrowser.reset();
    checkboxes[1]!.click();
    assertDeepEquals(
        ['UmVk', '2', true],
        await fakeBrowser.whenCalled('setTrialEnrollState'),
        'after clicking second box');

    assertEquals(checkboxes[0]!.checked, false);
    assertEquals(checkboxes[1]!.checked, true);
  });

  test('filter by trial name matches', async function() {
    const trial = makeTrial('Red');
    trial.hash = 'somehash';
    fakeBrowser.trialState = {
      trials: [trial],
      restartRequired: false,
    };
    await makeApp();

    filterInputElement().value = 'Red';
    filterInputElement().dispatchEvent(new Event('input'));

    assertEquals((await getDisplayedTrials()).length, 1);
  });

  test('filter by trial hash matches', async function() {
    const trial = makeTrial('Red');
    trial.hash = 'somehash';
    fakeBrowser.trialState = {
      trials: [trial],
      restartRequired: false,
    };
    await makeApp();

    filterInputElement().value = 'somehash';
    filterInputElement().dispatchEvent(new Event('input'));

    assertEquals((await getDisplayedTrials()).length, 1);
  });

  test('filter no match', async function() {
    const trial = makeTrial('Red');
    trial.hash = 'somehash';
    fakeBrowser.trialState = {
      trials: [trial],
      restartRequired: false,
    };
    await makeApp();

    filterInputElement().value = 'Blue';
    filterInputElement().dispatchEvent(new Event('input'));

    assertEquals((await getDisplayedTrials()).length, 0);
  });

  test('populate without names displays hash only', async function() {
    const trial = makeTrial('Red');
    trial.name = undefined;
    trial.hash = 'somehash';
    fakeBrowser.trialState = {
      trials: [trial],
      restartRequired: false,
    };
    await makeApp();

    const displayedTrials = await getDisplayedTrials();
    assertEquals('#somehash', displayedTrials[0]!.title);
  });

  test('filter by name can unmask hash', async function() {
    const trial = makeTrial('Red');
    trial.name = undefined;
    trial.hash = 'somehash';
    fakeBrowser.trialState = {
      trials: [trial],
      restartRequired: false,
    };
    await makeApp();

    fakeBrowser.reset();
    fakeBrowser.lookupTrialOrGroupNameResult = {'somehash': 'Red'};

    filterInputElement().value = 'Red';
    filterInputElement().dispatchEvent(new Event('input'));

    assertEquals('Red', await fakeBrowser.whenCalled('lookupTrialOrGroupName'));

    const displayedTrials = await getDisplayedTrials();
    assertEquals(
        'Red (#somehash)', displayedTrials[0]!.title,
        JSON.stringify(displayedTrials));
  });
});