chromium/ui/file_manager/file_manager/foreground/js/file_type_filters_controller_unittest.ts

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {NativeEventTarget as EventTarget} from 'chrome://resources/ash/common/event_target.js';
import {assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chromeos/chai_assert.js';

import type {FakeEntry, FilesAppDirEntry} from '../../common/js/files_app_entry_types.js';
import {EntryList, FakeEntryImpl} from '../../common/js/files_app_entry_types.js';
import {installMockChrome} from '../../common/js/mock_chrome.js';
import {RootType} from '../../common/js/volume_manager_types.js';

import type {DirectoryModel} from './directory_model.js';
import {FileTypeFiltersController} from './file_type_filters_controller.js';
import type {A11yAnnounce} from './ui/a11y_announce.js';

/**
 */
let container: HTMLElement;

/**
 */
let directoryModel: DirectoryModel;

/**
 */
let recentEntry: FakeEntry;

/**
 */
let myFilesEntry: EntryList;

let isScanCalled = false;

const TOTAL_FILTER_BUTTON_COUNT = 5;

export function setUp() {
  installMockChrome({});
  isScanCalled = false;
  class MockDirectoryModel extends EventTarget {
    currentDirEntry: DirectoryEntry|FilesAppDirEntry|null;

    constructor() {
      super();

      this.currentDirEntry = null;
      isScanCalled = false;
    }

    clearCurrentDirAndScan() {
      isScanCalled = true;
    }

    changeDirectoryEntry(dirEntry: DirectoryEntry|FilesAppDirEntry) {
      // Change the directory model's current directory to |dirEntry|.
      const previousDirEntry = this.currentDirEntry;
      this.currentDirEntry = dirEntry;

      // Emit 'directory-changed' event synchronously to simplify testing.
      const event = new CustomEvent('directory-changed', {
        detail: {
          previousDirEntry,
          newDirEntry: this.currentDirEntry,
        },
      });
      this.dispatchEvent(event);
    }

    static create() {
      const model = new MockDirectoryModel();
      return model as any as DirectoryModel;
    }
  }

  const mockA11y = {
    speakA11yMessage: () => {},
  } as A11yAnnounce;

  // Create FileTypeFiltersController instance with dependencies.
  container = document.createElement('div');
  directoryModel = MockDirectoryModel.create();
  recentEntry = new FakeEntryImpl(
      'Recent', RootType.RECENT,
      chrome.fileManagerPrivate.SourceRestriction.ANY_SOURCE,
      chrome.fileManagerPrivate.FileCategory.ALL);
  new FileTypeFiltersController(
      container, directoryModel, recentEntry, mockA11y);

  // Create a directory entry which is not Recents to simulate directory change.
  myFilesEntry = new EntryList('My Files', RootType.MY_FILES);
}

/**
 * Tests that creating FileTypeFiltersController generates four buttons in the
 * given container element.
 */
export function testCreatedButtonLabels() {
  const buttons = Array.from(container.children) as HTMLButtonElement[];
  assertEquals(buttons.length, TOTAL_FILTER_BUTTON_COUNT);

  assertEquals(buttons[0]?.textContent, 'All');
  assertEquals(buttons[1]?.textContent, 'Audio');
  assertEquals(buttons[2]?.textContent, 'Documents');
  assertEquals(buttons[3]?.textContent, 'Images');
  assertEquals(buttons[4]?.textContent, 'Videos');
}

/**
 * Tests that initial states of all buttons inside container are inactive
 * except the first button (button with label "All").
 */
export function testButtonInitialActiveState() {
  const buttons = Array.from(container.children) as HTMLButtonElement[];
  assertEquals(buttons.length, TOTAL_FILTER_BUTTON_COUNT);

  assertTrue(!!buttons[0]?.classList.contains('active'));
  assertFalse(!!buttons[1]?.classList.contains('active'));
  assertFalse(!!buttons[2]?.classList.contains('active'));
  assertFalse(!!buttons[3]?.classList.contains('active'));
  assertFalse(!!buttons[4]?.classList.contains('active'));
}

/**
 * Tests that click events can toggle button state (active <-> inactive),
 * if the button is already active, make it inactive and make "All" button
 * active.
 */
export function testButtonToggleState() {
  const buttons = Array.from(container.children) as HTMLButtonElement[];
  assertEquals(buttons.length, TOTAL_FILTER_BUTTON_COUNT);

  // State change: inactive -> active -> inactive.
  assertFalse(!!buttons[1]?.classList.contains('active'));
  buttons[1]?.click();
  assertTrue(!!buttons[1]?.classList.contains('active'));
  buttons[1]?.click();
  assertFalse(!!buttons[1]?.classList.contains('active'));
  assertTrue(!!buttons[0]?.classList.contains('active'));
  // Clicking active "All" does nothing.
  buttons[0]?.click();
  assertTrue(!!buttons[0]?.classList.contains('active'));
}

/**
 * Tests that only one button can be active.
 * If button_1 is clicked when button_0 is active, button_0 becomes inactive and
 * button_1 becomes active.
 */
export function testOnlyOneButtonCanActive() {
  const buttons = Array.from(container.children) as HTMLButtonElement[];
  assertEquals(buttons.length, TOTAL_FILTER_BUTTON_COUNT);

  assertTrue(!!buttons[0]?.classList.contains('active'));

  assertFalse(!!buttons[1]?.classList.contains('active'));
  buttons[1]?.click();
  assertFalse(!!buttons[0]?.classList.contains('active'));
  assertTrue(!!buttons[1]?.classList.contains('active'));
  assertFalse(!!buttons[2]?.classList.contains('active'));
  assertFalse(!!buttons[3]?.classList.contains('active'));
  assertFalse(!!buttons[4]?.classList.contains('active'));

  buttons[2]?.click();
  assertFalse(!!buttons[0]?.classList.contains('active'));
  assertFalse(!!buttons[1]?.classList.contains('active'));
  assertTrue(!!buttons[2]?.classList.contains('active'));
  assertFalse(!!buttons[3]?.classList.contains('active'));
  assertFalse(!!buttons[4]?.classList.contains('active'));

  buttons[3]?.click();
  assertFalse(!!buttons[0]?.classList.contains('active'));
  assertFalse(!!buttons[1]?.classList.contains('active'));
  assertFalse(!!buttons[2]?.classList.contains('active'));
  assertTrue(!!buttons[3]?.classList.contains('active'));
  assertFalse(!!buttons[4]?.classList.contains('active'));

  buttons[0]?.click();
  assertTrue(!!buttons[0]?.classList.contains('active'));
  assertFalse(!!buttons[1]?.classList.contains('active'));
  assertFalse(!!buttons[2]?.classList.contains('active'));
  assertFalse(!!buttons[3]?.classList.contains('active'));
  assertFalse(!!buttons[4]?.classList.contains('active'));
}

/**
 * Tests that container element is visible only when the current directory is
 * Recents view.
 */
export function testContainerIsShownOnlyInRecents() {
  container.hidden = true;
  directoryModel.changeDirectoryEntry(recentEntry);
  assertFalse(container.hidden);
  directoryModel.changeDirectoryEntry(myFilesEntry);
  assertTrue(container.hidden);
}

/**
 * Tests that button's active state is reset when the user leaves
 * Recents view and go back again.
 */
export function testActiveButtonIsResetOnLeavingRecents() {
  const buttons = Array.from(container.children) as HTMLButtonElement[];
  assertEquals(buttons.length, TOTAL_FILTER_BUTTON_COUNT);

  directoryModel.changeDirectoryEntry(recentEntry);

  buttons[1]?.click();
  assertFalse(!!buttons[0]?.classList.contains('active'));
  assertTrue(!!buttons[1]?.classList.contains('active'));
  assertFalse(!!buttons[2]?.classList.contains('active'));
  assertFalse(!!buttons[3]?.classList.contains('active'));
  assertFalse(!!buttons[4]?.classList.contains('active'));

  // Changing directory to the same Recent doesn't reset states.
  directoryModel.changeDirectoryEntry(recentEntry);
  assertFalse(!!buttons[0]?.classList.contains('active'));
  assertTrue(!!buttons[1]?.classList.contains('active'));
  assertFalse(!!buttons[2]?.classList.contains('active'));
  assertFalse(!!buttons[3]?.classList.contains('active'));
  assertFalse(!!buttons[4]?.classList.contains('active'));

  directoryModel.changeDirectoryEntry(myFilesEntry);
  assertTrue(!!buttons[0]?.classList.contains('active'));
  assertFalse(!!buttons[1]?.classList.contains('active'));
  assertFalse(!!buttons[2]?.classList.contains('active'));
  assertFalse(!!buttons[3]?.classList.contains('active'));
  assertFalse(!!buttons[4]?.classList.contains('active'));

  directoryModel.changeDirectoryEntry(recentEntry);
  assertTrue(!!buttons[0]?.classList.contains('active'));
  assertFalse(!!buttons[1]?.classList.contains('active'));
  assertFalse(!!buttons[2]?.classList.contains('active'));
  assertFalse(!!buttons[3]?.classList.contains('active'));
  assertFalse(!!buttons[4]?.classList.contains('active'));
}

/**
 * Tests that the active state of each button is reflected to the Recent entry's
 * fileCategory property, and DirectoryModel.rescan() is called after the
 * Recent entry's property is modified.
 */
export function testAppliedFilters() {
  const buttons = Array.from(container.children) as HTMLButtonElement[];
  assertEquals(buttons.length, TOTAL_FILTER_BUTTON_COUNT);

  directoryModel.changeDirectoryEntry(recentEntry);

  buttons[1]?.click();
  assertEquals(
      recentEntry.fileCategory, chrome.fileManagerPrivate.FileCategory.AUDIO);
  assertTrue(isScanCalled);
  isScanCalled = false;

  // Clicking an active button will trigger a scan for "All".
  buttons[1]?.click();
  assertEquals(
      recentEntry.fileCategory, chrome.fileManagerPrivate.FileCategory.ALL);
  assertTrue(isScanCalled);
  isScanCalled = false;

  buttons[2]?.click();
  assertEquals(
      recentEntry.fileCategory,
      chrome.fileManagerPrivate.FileCategory.DOCUMENT);
  assertTrue(isScanCalled);
  isScanCalled = false;

  buttons[3]?.click();
  assertEquals(
      recentEntry.fileCategory, chrome.fileManagerPrivate.FileCategory.IMAGE);
  assertTrue(isScanCalled);
  isScanCalled = false;

  buttons[4]?.click();
  assertEquals(
      recentEntry.fileCategory, chrome.fileManagerPrivate.FileCategory.VIDEO);
  assertTrue(isScanCalled);
  isScanCalled = false;

  buttons[0]?.click();
  assertEquals(
      recentEntry.fileCategory, chrome.fileManagerPrivate.FileCategory.ALL);
  assertTrue(isScanCalled);
  isScanCalled = false;
}