chromium/chrome/test/data/webui/lens/overlay/object_selection_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-untrusted://lens/selection_overlay.js';

import type {RectF} from '//resources/mojo/ui/gfx/geometry/mojom/geometry.mojom-webui.js';
import {BrowserProxyImpl} from 'chrome-untrusted://lens/browser_proxy.js';
import type {CenterRotatedBox} from 'chrome-untrusted://lens/geometry.mojom-webui.js';
import type {LensPageRemote} from 'chrome-untrusted://lens/lens.mojom-webui.js';
import {UserAction} from 'chrome-untrusted://lens/lens.mojom-webui.js';
import type {OverlayObject} from 'chrome-untrusted://lens/overlay_object.mojom-webui.js';
import type {SelectionOverlayElement} from 'chrome-untrusted://lens/selection_overlay.js';
import {loadTimeData} from 'chrome-untrusted://resources/js/load_time_data.js';
import {assertEquals} from 'chrome-untrusted://webui-test/chai_assert.js';
import type {MetricsTracker} from 'chrome-untrusted://webui-test/metrics_test_support.js';
import {fakeMetricsPrivate} from 'chrome-untrusted://webui-test/metrics_test_support.js';
import {flushTasks} from 'chrome-untrusted://webui-test/polymer_test_util.js';

import {assertBoxesWithinThreshold, createObject} from '../utils/object_utils.js';
import {simulateClick} from '../utils/selection_utils.js';

import {TestLensOverlayBrowserProxy} from './test_overlay_browser_proxy.js';


suite('ObjectSelection', function() {
  let testBrowserProxy: TestLensOverlayBrowserProxy;
  let selectionOverlayElement: SelectionOverlayElement;
  let callbackRouterRemote: LensPageRemote;
  let objects: OverlayObject[];
  let metrics: MetricsTracker;

  setup(async () => {
    // Resetting the HTML needs to be the first thing we do in setup to
    // guarantee that any singleton instances don't change while any UI is still
    // attached to the DOM.
    document.body.innerHTML = window.trustedTypes!.emptyHTML;

    testBrowserProxy = new TestLensOverlayBrowserProxy();
    callbackRouterRemote =
        testBrowserProxy.callbackRouter.$.bindNewPipeAndPassRemote();
    BrowserProxyImpl.setInstance(testBrowserProxy);

    // Turn off the shimmer. Since the shimmer is resource intensive, turn off
    // to prevent from causing issues in the tests.
    loadTimeData.overrideValues({'enableShimmer': false});

    selectionOverlayElement = document.createElement('lens-selection-overlay');
    document.body.appendChild(selectionOverlayElement);
    // Since the size of the Selection Overlay is based on the screenshot which
    // is not loaded in the test, we need to force the overlay to take up the
    // viewport.
    selectionOverlayElement.$.selectionOverlay.style.width = '100%';
    selectionOverlayElement.$.selectionOverlay.style.height = '100%';
    metrics = fakeMetricsPrivate();
    await flushTasks();
    await addObjects();
  });

  // Normalizes the given values to the size of selection overlay.
  function normalizedBox(box: RectF): RectF {
    const boundingRect = selectionOverlayElement.getBoundingClientRect();
    return {
      x: box.x / boundingRect.width,
      y: box.y / boundingRect.height,
      width: box.width / boundingRect.width,
      height: box.height / boundingRect.height,
    };
  }

  function addObjects() {
    // The object at index 0 will have a segmentation mask.
    objects =
        [
          {x: 20, y: 15, width: 20, height: 10},
          {x: 120, y: 15, width: 30, height: 10},
          {x: 70, y: 35, width: 50, height: 20},
          {x: 320, y: 50, width: 80, height: 30},
          {x: 320, y: 50, width: 40, height: 20},
          {x: 320, y: 50, width: 60, height: 25},
        ]
            .map(
                (rect, i) => createObject(
                    i.toString(), normalizedBox(rect),
                    /*includeSegmentationMask=*/ i === 0));
    callbackRouterRemote.objectsReceived(objects);
    return flushTasks();
  }

  function getRenderedObjects() {
    return selectionOverlayElement.$.objectSelectionLayer
        .getObjectNodesForTesting();
  }

  async function verifyLastReceievedObjectRequest(
      expectedRegion: CenterRotatedBox, expectedIsMaskClick: boolean) {
    await testBrowserProxy.handler.whenCalled('issueLensObjectRequest');
    const requestRegion =
        testBrowserProxy.handler.getArgs('issueLensObjectRequest')[0][0];
    const isMaskClick =
        testBrowserProxy.handler.getArgs('issueLensObjectRequest')[0][1];
    assertBoxesWithinThreshold(expectedRegion, requestRegion);
    assertEquals(expectedIsMaskClick, isMaskClick);
  }

  test('verify that objects render on the page', () => {
    const objectsOnPage = getRenderedObjects();

    assertEquals(6, objectsOnPage.length);
  });

  test(
      `verify that tapping an object issues lens request via mojo`,
      async () => {
        await simulateClick(selectionOverlayElement, {x: 120, y: 15});

        await verifyLastReceievedObjectRequest(
            objects[1]!.geometry.boundingBox, /*expectedIsMaskClick=*/ false);
        assertEquals(1, metrics.count('Lens.Overlay.Overlay.UserAction'));
        const action = await testBrowserProxy.handler.whenCalled(
            'recordUkmAndTaskCompletionForLensOverlayInteraction');
        assertEquals(UserAction.kObjectClick, action);
        assertEquals(
            1,
            metrics.count(
                'Lens.Overlay.Overlay.UserAction', UserAction.kObjectClick));
        assertEquals(
            1,
            metrics.count(
                'Lens.Overlay.Overlay.ByInvocationSource.AppMenu.UserAction',
                UserAction.kObjectClick));
      });

  test(
      `verify that smaller objects have priority over larger objects`,
      async () => {
        await simulateClick(selectionOverlayElement, {x: 320, y: 50});

        await verifyLastReceievedObjectRequest(
            objects[4]!.geometry.boundingBox, /*expectedIsMaskClick=*/ false);
      });

  test(
      `verify that tapping on objects with masks sets the mask click flag`,
      async () => {
        // Tap on the object at index 0, which has a mask.
        await simulateClick(selectionOverlayElement, {x: 21, y: 16});

        await verifyLastReceievedObjectRequest(
            objects[0]!.geometry.boundingBox, /*expectedIsMaskClick=*/ true);
      });

  test(
      'verify that tapping an object calls closePreselectionBubble',
      async () => {
        await simulateClick(selectionOverlayElement, {x: 320, y: 50});
        await testBrowserProxy.handler.whenCalled('closePreselectionBubble');
        assertEquals(
            1,
            testBrowserProxy.handler.getCallCount('closePreselectionBubble'));
      });
});