chromium/chrome/test/data/webui/compose/compose_app_focus_test.ts

// Copyright 2023 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-untrusted://compose/app.js';

import {loadTimeData} from '//resources/js/load_time_data.js';
import type {ComposeAppElement} from 'chrome-untrusted://compose/app.js';
import {StyleModifier} from 'chrome-untrusted://compose/compose.mojom-webui.js';
import {ComposeApiProxyImpl} from 'chrome-untrusted://compose/compose_api_proxy.js';
import {ComposeStatus} from 'chrome-untrusted://compose/compose_enums.mojom-webui.js';
import {assertEquals} from 'chrome-untrusted://webui-test/chai_assert.js';
import {flushTasks} from 'chrome-untrusted://webui-test/polymer_test_util.js';

import {TestComposeApiProxy} from './test_compose_api_proxy.js';

suite('ComposeApp', function() {
  let testProxy: TestComposeApiProxy;

  async function createApp(): Promise<ComposeAppElement> {
    const app = document.createElement('compose-app');
    document.body.appendChild(app);

    await testProxy.whenCalled('requestInitialState');
    await flushTasks();
    return app;
  }

  setup(async () => {
    testProxy = new TestComposeApiProxy();
    ComposeApiProxyImpl.setInstance(testProxy);
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
  });

  function mockResponse(triggeredFromModifier: boolean = false): Promise<void> {
    testProxy.remote.responseReceived({
      status: ComposeStatus.kOk,
      result: 'some response',
      undoAvailable: false,
      redoAvailable: false,
      providedByUser: false,
      onDeviceEvaluationUsed: false,
      triggeredFromModifier,
    });
    return testProxy.remote.$.flushForTesting();
  }

  test('RefocusesInputOnInvalidate', async () => {
    const app = await createApp();
    app.$.textarea.value = 'short';
    app.$.textarea.dispatchEvent(new CustomEvent('value-changed'));
    app.$.submitButton.focus();
    app.$.submitButton.click();
    await flushTasks();
    assertEquals(app.$.textarea, app.shadowRoot!.activeElement);
  });

  test('FocusesEditInput', async () => {
    testProxy.setOpenMetadata({}, {
      webuiState: JSON.stringify({
        input: 'some input',
        isEditingSubmittedInput: true,
      }),
    });
    const app = await createApp();
    assertEquals(app.$.editTextarea, app.shadowRoot!.activeElement);
  });

  test('FocusesRefreshButtonAfterRefreshRewrite', async () => {
    // This test is only useful for non-refinement UI.
    loadTimeData.overrideValues({
      enableRefinedUi: false,
    });

    const app = await createApp();
    app.$.textarea.value = 'test value one';
    app.$.submitButton.click();
    await mockResponse();

    app.$.refreshButton.click();
    await testProxy.whenCalled('rewrite');
    await mockResponse(true);

    assertEquals(app.$.refreshButton, app.shadowRoot!.activeElement);
  });

  test('FocusesEditInputAfterSubmitInput', async () => {
    const app = await createApp();
    app.$.textarea.value = 'test value one';
    app.$.submitButton.click();

    await testProxy.whenCalled('compose');
    await mockResponse();

    assertEquals(app.$.textarea, app.shadowRoot!.activeElement);
  });

  test('FocusesLengthMenuAfterLengthRewrite', async () => {
    // This test is only useful for non-refinement UI.
    loadTimeData.overrideValues({
      enableRefinedUi: false,
    });

    const app = await createApp();
    app.$.textarea.value = 'test value';
    app.$.submitButton.click();
    await mockResponse();

    app.$.lengthMenu.value = `${StyleModifier.kLonger}`;
    app.$.lengthMenu.dispatchEvent(new CustomEvent('change'));

    await testProxy.whenCalled('rewrite');
    await mockResponse(true);

    assertEquals(app.$.lengthMenu, app.shadowRoot!.activeElement);
  });

  test('FocusesToneMenuAfterToneRewrite', async () => {
    const app = await createApp();
    app.$.textarea.value = 'test value';
    app.$.submitButton.click();
    await mockResponse();

    app.$.toneMenu.value = `${StyleModifier.kCasual}`;
    app.$.toneMenu.dispatchEvent(new CustomEvent('change'));

    await testProxy.whenCalled('rewrite');
    await mockResponse(true);

    assertEquals(app.$.toneMenu, app.shadowRoot!.activeElement);
  });

  test('FocusesUndoButtonAfterUndoClick', async () => {
    // This test is only useful for non-refinement UI.
    loadTimeData.overrideValues({
      enableRefinedUi: false,
    });
    // Set up initial state to show undo button and mock up a previous state.
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    testProxy.setOpenMetadata({}, {
      hasPendingRequest: false,
      response: {
        status: ComposeStatus.kOk,
        undoAvailable: true,
        redoAvailable: false,
        providedByUser: false,
        result: 'here is a result',
        onDeviceEvaluationUsed: false,
        triggeredFromModifier: false,
      },
    });
    const appWithUndo = document.createElement('compose-app');
    document.body.appendChild(appWithUndo);
    await testProxy.whenCalled('requestInitialState');

    // The undo button keeps focus after it is clicked.
    testProxy.setUndoResponseWithUndoAndRedo(true, false);
    appWithUndo.$.undoButton.click();
    await testProxy.whenCalled('undo');
    await flushTasks();

    assertEquals(
        appWithUndo.$.undoButton, appWithUndo.shadowRoot!.activeElement);
  });

  test('FocusesUndoOrRedoButtonAfterUndoClick', async () => {
    // This test is only useful for Refinements UI.
    loadTimeData.overrideValues({
      enableRefinedUi: true,
    });
    // Set up initial state to show undo/redo buttons and mock up a previous
    // state.
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    testProxy.setOpenMetadata({}, {
      hasPendingRequest: false,
      response: {
        status: ComposeStatus.kOk,
        undoAvailable: true,
        redoAvailable: true,
        providedByUser: false,
        result: 'here is a result',
        onDeviceEvaluationUsed: false,
        triggeredFromModifier: false,
      },
    });
    const appWithUndo = document.createElement('compose-app');
    document.body.appendChild(appWithUndo);
    await testProxy.whenCalled('requestInitialState');

    // If undo is enabled after the undo action, the undo button keeps focus.
    testProxy.setUndoResponseWithUndoAndRedo(true, false);
    appWithUndo.$.undoButtonRefined.click();
    await testProxy.whenCalled('undo');
    await flushTasks();
    assertEquals(
        appWithUndo.$.undoButtonRefined, appWithUndo.shadowRoot!.activeElement);

    // If undo is disabled after the undo action, the redo button gains
    // focus.
    testProxy.setUndoResponseWithUndoAndRedo(false, true);
    appWithUndo.$.undoButtonRefined.click();
    await testProxy.whenCalled('undo');
    await flushTasks();
    assertEquals(
        appWithUndo.$.redoButton, appWithUndo.shadowRoot!.activeElement);
  });

  test('FocusesUndoOrRedoButtonAfterRedoClick', async () => {
    // This test is only useful for Refinements UI.
    loadTimeData.overrideValues({
      enableRefinedUi: true,
    });
    // Set up initial state to show undo/redo buttons and mock up a previous
    // state.
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    testProxy.setOpenMetadata({}, {
      hasPendingRequest: false,
      response: {
        status: ComposeStatus.kOk,
        undoAvailable: true,
        redoAvailable: true,
        providedByUser: false,
        result: 'here is a result',
        onDeviceEvaluationUsed: false,
        triggeredFromModifier: false,
      },
    });
    const appWithRedo = document.createElement('compose-app');
    document.body.appendChild(appWithRedo);
    await testProxy.whenCalled('requestInitialState');

    // If redo is enabled after the redo action, the redo button keeps focus.
    testProxy.setRedoResponseWithUndoAndRedo(false, true);
    appWithRedo.$.redoButton.click();
    await testProxy.whenCalled('redo');
    await flushTasks();
    assertEquals(
        appWithRedo.$.redoButton, appWithRedo.shadowRoot!.activeElement);

    // If redo is disabled after the redo action, the undo button gains focus.
    testProxy.setRedoResponseWithUndoAndRedo(true, false);
    appWithRedo.$.redoButton.click();
    await testProxy.whenCalled('redo');
    await flushTasks();
    assertEquals(
        appWithRedo.$.undoButtonRefined, appWithRedo.shadowRoot!.activeElement);
  });

});