chromium/chrome/test/data/webui/side_panel/commerce/price_tracking_section_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://shopping-insights-side-panel.top-chrome/app.js';

import {BrowserProxyImpl} from 'chrome://resources/cr_components/commerce/browser_proxy.js';
import type {BookmarkProductInfo, PageRemote, PriceInsightsInfo, ProductInfo} from 'chrome://resources/cr_components/commerce/shopping_service.mojom-webui.js';
import {PageCallbackRouter, PriceInsightsInfo_PriceBucket} from 'chrome://resources/cr_components/commerce/shopping_service.mojom-webui.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {stringToMojoString16} from 'chrome://resources/js/mojo_type_util.js';
import type {PriceTrackingSection} from 'chrome://shopping-insights-side-panel.top-chrome/price_tracking_section.js';
import {assertEquals, assertTrue} from 'chrome://webui-test/chai_assert.js';
import type {MetricsTracker} from 'chrome://webui-test/metrics_test_support.js';
import {fakeMetricsPrivate} from 'chrome://webui-test/metrics_test_support.js';
import {flushTasks} from 'chrome://webui-test/polymer_test_util.js';
import {TestMock} from 'chrome://webui-test/test_mock.js';

suite('PriceTrackingSectionTest', () => {
  let priceTrackingSection: PriceTrackingSection;
  let callbackRouter: PageCallbackRouter;
  let callbackRouterRemote: PageRemote;
  const shoppingServiceApi = TestMock.fromClass(BrowserProxyImpl);
  let metrics: MetricsTracker;

  const productInfo: ProductInfo = {
    title: 'Product Foo',
    clusterTitle: 'Product Cluster Foo',
    domain: 'foo.com',
    imageUrl: {url: 'https://foo.com/image'},
    productUrl: {url: 'https://foo.com/product'},
    currentPrice: '$12',
    previousPrice: '$34',
    clusterId: BigInt(12345),
    categoryLabels: [],
  };

  const priceInsights: PriceInsightsInfo = {
    clusterId: BigInt(12345),
    typicalLowPrice: '$100',
    typicalHighPrice: '$200',
    catalogAttributes: 'Unlocked, 4GB',
    jackpot: {url: 'https://foo.com/jackpot'},
    bucket: PriceInsightsInfo_PriceBucket.kLow,
    hasMultipleCatalogs: true,
    history: [{
      date: '2021-01-01',
      price: 100,
      formattedPrice: '$100',
    }],
    locale: 'en-us',
    currencyCode: 'usd',
  };

  const bookmarkProductInfo: BookmarkProductInfo = {
    bookmarkId: BigInt(3),
    info: productInfo,
  };

  function checkPriceTrackingSectionRendering(tracked: boolean) {
    assertEquals(
        priceTrackingSection.$.toggleTitle!.textContent,
        loadTimeData.getString('trackPriceTitle'));

    if (tracked) {
      checkAnnotationHasText(
          loadTimeData.getStringF('trackPriceSaveDescription'));
      checkAnnotationHasText(
          loadTimeData.getStringF('trackPriceSaveLocation', 'Parent folder'));
    } else {
      checkAnnotationHasText(loadTimeData.getString('trackPriceDescription'));
    }

    assertEquals(
        priceTrackingSection.$.toggle!.getAttribute('aria-pressed')!,
        tracked ? 'true' : 'false');
  }

  // Check whether the annotation element contains the provided text. The intent
  // of this utility is to ignore extra whitespace generated by the structure in
  // the HTML.
  function checkAnnotationHasText(expected: string) {
    const annotationText = priceTrackingSection.$.toggleAnnotation!.textContent;
    assertTrue(
        annotationText?.trim().length !== 0,
        'Annotation text context was empty!');
    assertTrue(
        annotationText?.includes(expected) === true,
        'Annotation section did not contain string: ' + expected +
            ' Actual: ' + annotationText);
  }

  setup(async () => {
    document.body.innerHTML = window.trustedTypes!.emptyHTML;

    shoppingServiceApi.reset();
    callbackRouter = new PageCallbackRouter();
    shoppingServiceApi.setResultFor('getCallbackRouter', callbackRouter);
    shoppingServiceApi.setResultFor(
        'getParentBookmarkFolderNameForCurrentUrl',
        Promise.resolve({name: stringToMojoString16('Parent folder')}));

    callbackRouterRemote = callbackRouter.$.bindNewPipeAndPassRemote();

    BrowserProxyImpl.setInstance(shoppingServiceApi);

    priceTrackingSection = document.createElement('price-tracking-section');
    priceTrackingSection.productInfo = productInfo;
    priceTrackingSection.priceInsightsInfo = priceInsights;

    metrics = fakeMetricsPrivate();
  });

  [true, false].forEach((tracked) => {
    test(
        `PriceTracking section rendering when tracked is ${tracked}`,
        async () => {
          priceTrackingSection.isProductTracked = tracked;

          document.body.appendChild(priceTrackingSection);
          await flushTasks();

          checkPriceTrackingSectionRendering(tracked);
        });

    test(`Toggle price tracking when tracked is ${tracked}`, async () => {
      priceTrackingSection.isProductTracked = tracked;

      document.body.appendChild(priceTrackingSection);
      await flushTasks();

      priceTrackingSection.$.toggle!.click();

      const tracking = await shoppingServiceApi.whenCalled(
          'setPriceTrackingStatusForCurrentUrl');
      assertEquals(!tracking, tracked);
      if (tracking) {
        assertEquals(
            1,
            metrics.count(
                'Commerce.PriceTracking.PriceInsightsSidePanel.Track',
                PriceInsightsInfo_PriceBucket.kLow));
      } else {
        assertEquals(
            1,
            metrics.count(
                'Commerce.PriceTracking.PriceInsightsSidePanel.Untrack',
                PriceInsightsInfo_PriceBucket.kLow));
      }
    });

    test(`Ignore unrealted product tracking status change`, async () => {
      priceTrackingSection.isProductTracked = tracked;

      document.body.appendChild(priceTrackingSection);
      await flushTasks();

      // Create a unrelated product.
      const otherProductInfo: ProductInfo = {
        title: 'Product Bar',
        clusterTitle: 'Product Cluster Bar',
        domain: 'bar.com',
        imageUrl: {url: 'https://bar.com/image'},
        productUrl: {url: 'https://bar.com/product'},
        currentPrice: '$12',
        previousPrice: '$34',
        clusterId: BigInt(54321),
        categoryLabels: [],
      };

      const otherBookmarkProductInfo: BookmarkProductInfo = {
        bookmarkId: BigInt(4),
        info: otherProductInfo,
      };

      if (tracked) {
        callbackRouterRemote.priceUntrackedForBookmark(
            otherBookmarkProductInfo);
      } else {
        callbackRouterRemote.priceTrackedForBookmark(otherBookmarkProductInfo);
      }
      await flushTasks();
      checkPriceTrackingSectionRendering(tracked);
    });
  });

  test(`Observe current product tracking status change`, async () => {
    priceTrackingSection.isProductTracked = false;

    document.body.appendChild(priceTrackingSection);
    await flushTasks();

    callbackRouterRemote.priceTrackedForBookmark(bookmarkProductInfo);
    await flushTasks();
    checkPriceTrackingSectionRendering(true);

    callbackRouterRemote.priceUntrackedForBookmark(bookmarkProductInfo);
    await flushTasks();
    checkPriceTrackingSectionRendering(false);
  });

  test(`Trigger bookmark editor`, async () => {
    priceTrackingSection.isProductTracked = true;

    document.body.appendChild(priceTrackingSection);
    await flushTasks();
    checkPriceTrackingSectionRendering(true);

    const folder = priceTrackingSection.shadowRoot!.querySelector<HTMLElement>(
        '#toggleAnnotationButton');
    assertTrue(!!folder);
    folder.click();

    await shoppingServiceApi.whenCalled('showBookmarkEditorForCurrentUrl');
    assertEquals(
        1,
        metrics.count(
            'Commerce.PriceTracking.' +
            'EditedBookmarkFolderFromPriceInsightsSidePanel'));
  });

  test(`Render error message`, async () => {
    priceTrackingSection.isProductTracked = false;

    document.body.appendChild(priceTrackingSection);
    await flushTasks();

    callbackRouterRemote.operationFailedForBookmark(bookmarkProductInfo, true);
    await flushTasks();

    assertEquals(
        priceTrackingSection.$.toggleTitle!.textContent,
        loadTimeData.getString('trackPriceTitle'));
    assertEquals(
        priceTrackingSection.$.toggleAnnotation!.textContent?.trim(),
        loadTimeData.getString('trackPriceError'));
    assertEquals(
        priceTrackingSection.$.toggle!.getAttribute('aria-pressed'), 'false');

    callbackRouterRemote.operationFailedForBookmark(bookmarkProductInfo, false);
    await flushTasks();

    assertEquals(
        priceTrackingSection.$.toggleTitle!.textContent,
        loadTimeData.getString('trackPriceTitle'));
    assertEquals(
        priceTrackingSection.$.toggleAnnotation!.textContent?.trim(),
        loadTimeData.getString('trackPriceError'));
    assertEquals(
        priceTrackingSection.$.toggle!.getAttribute('aria-pressed'), 'true');
  });

  test(`Observe product bookmark move event`, async () => {
    priceTrackingSection.isProductTracked = true;

    document.body.appendChild(priceTrackingSection);
    await flushTasks();
    checkPriceTrackingSectionRendering(true);
    let expectedAnnotation =
        loadTimeData.getStringF('trackPriceSaveDescription');
    let expectedSaveLocationText =
        loadTimeData.getStringF('trackPriceSaveLocation', 'Parent folder');
    checkAnnotationHasText(expectedAnnotation);
    checkAnnotationHasText(expectedSaveLocationText);

    shoppingServiceApi.setResultFor(
        'getParentBookmarkFolderNameForCurrentUrl',
        Promise.resolve({name: stringToMojoString16('New folder')}));
    callbackRouterRemote.onProductBookmarkMoved(bookmarkProductInfo);
    await flushTasks();

    expectedAnnotation = loadTimeData.getStringF('trackPriceSaveDescription');
    expectedSaveLocationText =
        loadTimeData.getStringF('trackPriceSaveLocation', 'New folder');
    checkAnnotationHasText(expectedAnnotation);
    checkAnnotationHasText(expectedSaveLocationText);
  });
});