chromium/chrome/test/data/webui/new_tab_page/modules/v2/calendar/calendar_event_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 {CalendarAction, CalendarEventElement} from 'chrome://new-tab-page/lazy_load.js';
import {$$, WindowProxy} from 'chrome://new-tab-page/new_tab_page.js';
import {assertEquals, assertFalse, 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 type {TestMock} from 'chrome://webui-test/test_mock.js';
import {eventToPromise, isVisible, microtasksFinished} from 'chrome://webui-test/test_util.js';

import {installMock} from '../../../test_support.js';

import {createAttachments, createEvent} from './test_support.js';

// Microseconds between windows and unix epoch.
const kWindowsToUnixEpochOffset: bigint = 11644473600000000n;

suite('NewTabPageModulesCalendarEventTest', () => {
  let element: CalendarEventElement;
  let windowProxy: TestMock<WindowProxy>;

  const startTime = (new Date('2024-07-01T03:00:00')).valueOf();

  setup(async () => {
    windowProxy = installMock(WindowProxy);
    windowProxy.setResultFor('now', startTime);
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    document.body.style.width = '300px';
    element = new CalendarEventElement();
    element.event = createEvent(1);
    element.expanded = false;
    document.body.append(element);
    await microtasksFinished();
  });

  suite('general', () => {
    test('event is visible', async () => {
      // Create a test startTime and turn it into microseconds from
      // Windows epoch.
      const startTime = (new Date('02 Feb 2024 03:04')).valueOf();
      const convertedStartTime =
          (BigInt(startTime) * 1000n) + kWindowsToUnixEpochOffset;

      element.event =
          createEvent(1, {startTime: {internalValue: convertedStartTime}});
      await microtasksFinished();

      // Assert.
      assertTrue(isVisible(element));
      assertTrue(isVisible(element.$.title));
      assertTrue(isVisible(element.$.startTime));
      assertEquals(element.$.title.textContent, 'Test Event 1');
      assertEquals(element.$.startTime.textContent, '3:04am');
      assertEquals(element.$.header.href, element.event.url.url);
    });

    test('time status hidden if not expanded', async () => {
      const startTimeFromUnixEpoch =
          (BigInt(startTime) * 1000n) + kWindowsToUnixEpochOffset;

      element.event =
          createEvent(1, {startTime: {internalValue: startTimeFromUnixEpoch}});
      await microtasksFinished();

      // Assert.
      assertFalse(isVisible(element.$.timeStatus));
      assertEquals('', element.$.timeStatus.textContent!.trim());
    });

    test('time status displays correctly', async () => {
      let startTimeFromUnixEpoch =
          (BigInt(startTime) * 1000n) + kWindowsToUnixEpochOffset;

      element.event =
          createEvent(1, {startTime: {internalValue: startTimeFromUnixEpoch}});
      element.expanded = true;
      await microtasksFinished();

      assertTrue(isVisible(element.$.timeStatus));
      assertEquals(element.$.timeStatus.innerText, 'In Progress');

      // Make the event 5 minutes in the future.
      startTimeFromUnixEpoch += (1000000n * 60n * 5n);
      element.event =
          createEvent(1, {startTime: {internalValue: startTimeFromUnixEpoch}});
      await microtasksFinished();

      assertEquals(element.$.timeStatus.innerText, 'In 5 min');

      // Make the event 1 hour in the future.
      startTimeFromUnixEpoch += (1000000n * 60n * 60n);
      element.event =
          createEvent(1, {startTime: {internalValue: startTimeFromUnixEpoch}});
      await microtasksFinished();

      assertEquals(element.$.timeStatus.innerText, 'In 1 hr');
    });

    test('Expanded event shows extra info', async () => {
      element.expanded = true;
      await microtasksFinished();

      // Assert.
      const locationElement = $$(element, '#location');
      const attachmentsElement = $$(element, '#attachments');
      const conferenceElement = $$(element, '#conference');
      assertTrue(!!locationElement);
      assertTrue(!!attachmentsElement);
      assertTrue(!!conferenceElement);
      assertTrue(isVisible(locationElement));
      assertTrue(isVisible(attachmentsElement));
      assertTrue(isVisible(conferenceElement));

      const attachmentChips = element.shadowRoot!.querySelectorAll('cr-chip');
      assertEquals(attachmentChips.length, 3);
    });

    test('Non-expanded event hides extra info', async () => {
      // Assert.
      const locationElement = $$(element, '#location');
      const attachmentsElement = $$(element, '#attachments');
      const conferenceElement = $$(element, '#conference');
      assertTrue(!locationElement);
      assertTrue(!attachmentsElement);
      assertTrue(!conferenceElement);
    });

    test('location hidden if empty', async () => {
      element.expanded = true;
      element.event = createEvent(1, {location: ''});
      await microtasksFinished();

      // Assert.
      const locationElement = $$(element, '#location');
      assertTrue(!!locationElement);
      assertFalse(isVisible(locationElement));
    });

    test('attachments hidden if empty', async () => {
      element.expanded = true;
      element.event = createEvent(1, {attachments: []});
      await microtasksFinished();

      // Assert.
      const attachmentsElement = $$(element, '#attachments');
      assertTrue(!!attachmentsElement);
      assertFalse(isVisible(attachmentsElement));
    });

    test('attachments fade on edge of scroll', async () => {
      element.expanded = true;
      element.event = createEvent(1, {attachments: createAttachments(10)});
      await microtasksFinished();

      // Assert.
      const attachmentListElement = $$(element, '#attachmentList');
      assertTrue(!!attachmentListElement);
      assertEquals(attachmentListElement.children.length, 10);

      function whenVisibilityChanged(e: Element): Promise<void> {
        return new Promise(resolve => {
          const pageObserver =
              new IntersectionObserver((_entries, observer) => {
                resolve();
                observer.unobserve(e);
              }, {root: document.documentElement});
          pageObserver.observe(e);
        });
      }

      // Act.
      attachmentListElement.scrollTo({top: 0, left: 0, behavior: 'instant'});
      await whenVisibilityChanged(attachmentListElement.children[0]!);
      await microtasksFinished();

      // Assert.
      assertEquals(attachmentListElement.className, 'scrollable-right');

      // Act.
      attachmentListElement.scrollTo({top: 0, left: 100, behavior: 'instant'});
      await whenVisibilityChanged(attachmentListElement.children[0]!);
      await microtasksFinished();

      // Assert.
      assertEquals(attachmentListElement.className, 'scrollable');

      // Act.
      const maxLeft =
          attachmentListElement.scrollWidth - attachmentListElement.clientWidth;
      attachmentListElement.scrollTo(
          {top: 0, left: maxLeft, behavior: 'instant'});
      await whenVisibilityChanged(attachmentListElement.children[9]!);
      await microtasksFinished();

      // Assert.
      assertEquals(attachmentListElement.className, 'scrollable-left');
    });

    test('conference button hidden if empty', async () => {
      element.expanded = true;
      element.event = createEvent(1, {conferenceUrl: {url: ''}});
      await microtasksFinished();

      // Assert.
      const conferenceElement = $$(element, '#conference');
      assertTrue(!!conferenceElement);
      assertFalse(isVisible(conferenceElement));
    });
  });

  suite('metrics', () => {
    let metrics: MetricsTracker;

    setup(() => {
      metrics = fakeMetricsPrivate();
    });

    test('basic event click', async () => {
      const usagePromise = eventToPromise('usage', element);
      const moduleName = 'GoogleCalendar';
      element.event = createEvent(1);
      element.moduleName = moduleName;
      element.index = 1;
      await microtasksFinished();

      // Act.
      // Prevent navigating to href.
      element.$.header.addEventListener('click', (e) => e.preventDefault());
      element.$.header.click();

      // Assert.
      const usageEvent: Event = await usagePromise;
      assertTrue(!!usageEvent);
      assertEquals(
          1,
          metrics.count(
              `NewTabPage.${moduleName}.UserAction`,
              CalendarAction.BASIC_EVENT_HEADER_CLICKED));
      assertEquals(
          1,
          metrics.count(
              `NewTabPage.${moduleName}.EventClickIndex`, element.index));
    });

    test('expanded event click', async () => {
      const usagePromise = eventToPromise('usage', element);
      const moduleName = 'GoogleCalendar';
      element.expanded = true;
      element.event = createEvent(1);
      element.moduleName = moduleName;
      element.index = 3;
      await microtasksFinished();

      // Act.
      // Prevent navigating to href.
      element.$.header.addEventListener('click', (e) => e.preventDefault());
      element.$.header.click();

      // Assert.
      const usageEvent: Event = await usagePromise;
      assertTrue(!!usageEvent);
      assertEquals(
          1,
          metrics.count(
              `NewTabPage.${moduleName}.UserAction`,
              CalendarAction.EXPANDED_EVENT_HEADER_CLICKED));
      assertEquals(
          1,
          metrics.count(
              `NewTabPage.${moduleName}.EventClickIndex`, element.index));
    });

    test('double booked event click', async () => {
      const usagePromise = eventToPromise('usage', element);
      const moduleName = 'GoogleCalendar';
      element.doubleBooked = true;
      element.event = createEvent(1);
      element.moduleName = moduleName;
      element.index = 0;
      await microtasksFinished();

      // Act.
      // Prevent navigating to href.
      element.$.header.addEventListener('click', (e) => e.preventDefault());
      element.$.header.click();

      // Assert.
      const usageEvent: Event = await usagePromise;
      assertTrue(!!usageEvent);
      assertEquals(
          1,
          metrics.count(
              `NewTabPage.${moduleName}.UserAction`,
              CalendarAction.DOUBLE_BOOKED_EVENT_HEADER_CLICKED));
      assertEquals(
          1,
          metrics.count(
              `NewTabPage.${moduleName}.EventClickIndex`, element.index));
    });

    test('attachment click', async () => {
      const usagePromise = eventToPromise('usage', element);
      const moduleName = 'GoogleCalendar';
      element.expanded = true;
      element.event = createEvent(1, {attachments: createAttachments(3)});
      element.moduleName = moduleName;
      await microtasksFinished();

      // Act.
      const attachments = element.renderRoot!.querySelectorAll('.attachment');
      assertEquals(attachments.length, 3);
      (attachments[1]! as HTMLElement).click();

      // Assert.
      const usageEvent: Event = await usagePromise;
      assertTrue(!!usageEvent);
      assertEquals(
          1,
          metrics.count(
              `NewTabPage.${moduleName}.UserAction`,
              CalendarAction.ATTACHMENT_CLICKED));
    });

    test('conference call click', async () => {
      const usagePromise = eventToPromise('usage', element);
      const moduleName = 'GoogleCalendar';
      element.expanded = true;
      element.event =
          createEvent(1, {conferenceUrl: {url: 'https://google.com/'}});
      element.moduleName = moduleName;
      await microtasksFinished();

      // Act.
      const conference =
          element.renderRoot!.querySelector('#conference cr-button');
      assertTrue(!!conference);
      (conference! as HTMLElement).click();

      // Assert.
      const usageEvent: Event = await usagePromise;
      assertTrue(!!usageEvent);
      assertEquals(
          1,
          metrics.count(
              `NewTabPage.${moduleName}.UserAction`,
              CalendarAction.CONFERENCE_CALL_CLICKED));
    });
  });
});