chromium/ui/file_manager/file_manager/state/ducks/navigation_unittest.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 {assertDeepEquals, assertEquals} from 'chrome://webui-test/chromeos/chai_assert.js';

import {MockVolumeManager} from '../../background/js/mock_volume_manager.js';
import {EntryList, FakeEntryImpl, VolumeEntry} from '../../common/js/files_app_entry_types.js';
import {MockFileEntry, MockFileSystem} from '../../common/js/mock_entry.js';
import {TrashRootEntry} from '../../common/js/trash.js';
import {RootType, VolumeType} from '../../common/js/volume_manager_types.js';
import {ICON_TYPES, ODFS_EXTENSION_ID} from '../../foreground/js/constants.js';
import {type AndroidApp, type FileData, type MaterializedView, type NavigationRoot, NavigationSection, NavigationType, type State, type Volume} from '../../state/state.js';
import {convertEntryToFileData} from '../ducks/all_entries.js';
import {createFakeVolumeMetadata, setUpFileManagerOnWindow, setupStore, waitDeepEquals} from '../for_tests.js';
import {getEmptyState, getFileData} from '../store.js';

import {updateMaterializedViews} from './materialized_views.js';
import {refreshNavigationRoots} from './navigation.js';
import {convertVolumeInfoAndMetadataToVolume, driveRootEntryListKey, myFilesEntryListKey, recentRootKey, trashRootKey} from './volumes.js';

export function setUp() {
  setUpFileManagerOnWindow();
}

/** Create FileData for recent entry. */
function createRecentFileData(): FileData {
  const recentEntry = new FakeEntryImpl(
      'Recent', RootType.RECENT,
      chrome.fileManagerPrivate.SourceRestriction.ANY_SOURCE,
      chrome.fileManagerPrivate.FileCategory.ALL);
  return convertEntryToFileData(recentEntry);
}

/** Create FileData for shortcut entry. */
function createShortcutEntryFileData(
    fileSystemName: string, entryName: string, label: string): FileData {
  const fakeFs = new MockFileSystem(fileSystemName);
  const shortcutEntry = MockFileEntry.create(fakeFs, `/root/${entryName}`);
  return {
    ...convertEntryToFileData(shortcutEntry),
    label,
  };
}

/** Create FileData for MyFiles entry. */
function createMyFilesEntryFileData(): {fileData: FileData, volume: Volume} {
  const {volumeManager} = window.fileManager;
  const downloadsVolumeInfo =
      volumeManager.getCurrentProfileVolumeInfo(VolumeType.DOWNLOADS)!;
  const myFilesEntry = new VolumeEntry(downloadsVolumeInfo);
  const fileData = convertEntryToFileData(myFilesEntry);
  const volume = convertVolumeInfoAndMetadataToVolume(
      downloadsVolumeInfo, createFakeVolumeMetadata(downloadsVolumeInfo));
  return {fileData, volume};
}

/** Create FileData for drive root entry. */
function createDriveRootEntryListFileData(): FileData {
  const driveRootEntryList =
      new EntryList('Google Drive', RootType.DRIVE_FAKE_ROOT);
  return convertEntryToFileData(driveRootEntryList);
}

/** Create FileData for trash entry. */
function createTrashEntryFileData(): FileData {
  const trashEntry = new TrashRootEntry();
  return convertEntryToFileData(trashEntry);
}

/** Create android apps. */
function createAndroidApps(): [AndroidApp, AndroidApp] {
  return [
    {
      name: 'App 1',
      packageName: 'com.test.app1',
      activityName: 'Activity1',
      iconSet: {icon16x16Url: 'url1', icon32x32Url: 'url2'},
      icon: {icon16x16Url: 'url1', icon32x32Url: 'url2'},
    },
    {
      name: 'App 2',
      packageName: 'com.test.app2',
      activityName: 'Activity2',
      iconSet: {icon16x16Url: '', icon32x32Url: ''},
      icon: ICON_TYPES.GENERIC,
    },
  ];
}

/** Create file data and volume data for volume. */
function createVolumeFileData(
    volumeType: VolumeType, volumeId: string, label: string = '',
    devicePath: string = '',
    providerId: string = ''): {fileData: FileData, volume: Volume} {
  const volumeInfo = MockVolumeManager.createMockVolumeInfo(
      volumeType, volumeId, label, devicePath, providerId);
  const {volumeManager} = window.fileManager;
  volumeManager.volumeInfoList.add(volumeInfo);
  const volumeEntry = new VolumeEntry(volumeInfo);
  const fileData = convertEntryToFileData(volumeEntry);
  const volume = convertVolumeInfoAndMetadataToVolume(
      volumeInfo, createFakeVolumeMetadata(volumeInfo));
  return {fileData, volume};
}

/**
 * Tests that navigation roots with all different types:
 * 1. produces the expected order of volumes.
 * 2. manages NavigationSection for the relevant volumes.
 * 3. keeps MTP/Archive/Removable volumes on the original order.
 */
export async function testNavigationRoots(done: () => void) {
  const initialState = getEmptyState();
  // Put recent entry in the store.
  const recentEntryFileData = createRecentFileData();
  initialState.allEntries[recentRootKey] = recentEntryFileData;
  // Put 2 shortcut entries in the store.
  const shortcutEntryFileData1 =
      createShortcutEntryFileData('drive', 'shortcut1', 'Shortcut 1');
  initialState.allEntries[shortcutEntryFileData1.key] = shortcutEntryFileData1;
  initialState.folderShortcuts.push(shortcutEntryFileData1.key);
  const shortcutEntryFileData2 =
      createShortcutEntryFileData('drive', 'shortcut2', 'Shortcut 2');
  initialState.allEntries[shortcutEntryFileData2.key] = shortcutEntryFileData2;
  initialState.folderShortcuts.push(shortcutEntryFileData2.key);
  // Put MyFiles entry in the store.
  const myFilesVolume = createMyFilesEntryFileData();
  initialState.allEntries[myFilesVolume.fileData.key] = myFilesVolume.fileData;
  initialState.volumes[myFilesVolume.volume.volumeId] = myFilesVolume.volume;
  // Put drive entry in the store.
  const driveRootEntryFileData = createDriveRootEntryListFileData();
  initialState.allEntries[driveRootEntryListKey] = driveRootEntryFileData;
  initialState.uiEntries.push(driveRootEntryListKey);
  // Put trash entry in the store.
  const trashEntryFileData = createTrashEntryFileData();
  initialState.allEntries[trashRootKey] = trashEntryFileData;
  initialState.uiEntries.push(trashRootKey);
  // Put the android apps in the store.
  const androidAppsData = createAndroidApps();
  initialState.androidApps[androidAppsData[0].packageName] = androidAppsData[0];
  initialState.androidApps[androidAppsData[1].packageName] = androidAppsData[1];

  // Create different volumes.
  const providerVolume1 =
      createVolumeFileData(VolumeType.PROVIDED, 'provided:prov1');
  initialState.allEntries[providerVolume1.fileData.key] =
      providerVolume1.fileData;
  initialState.volumes[providerVolume1.volume.volumeId] =
      providerVolume1.volume;

  // Set the device paths of the removable volumes to different strings to
  // test the behavior of two physically separate external devices.
  const hogeVolume = createVolumeFileData(
      VolumeType.REMOVABLE, 'removable:hoge', 'Hoge', 'device/path/1');
  initialState.allEntries[hogeVolume.fileData.key] = hogeVolume.fileData;
  initialState.volumes[hogeVolume.volume.volumeId] = hogeVolume.volume;

  const fugaVolume = createVolumeFileData(
      VolumeType.REMOVABLE, 'removable:fuga', 'Fuga', 'device/path/2');
  initialState.allEntries[fugaVolume.fileData.key] = fugaVolume.fileData;
  initialState.volumes[fugaVolume.volume.volumeId] = fugaVolume.volume;

  const archiveVolume =
      createVolumeFileData(VolumeType.ARCHIVE, 'archive:a-rar');
  initialState.allEntries[archiveVolume.fileData.key] = archiveVolume.fileData;
  initialState.volumes[archiveVolume.volume.volumeId] = archiveVolume.volume;

  const mtpVolume = createVolumeFileData(VolumeType.MTP, 'mtp:a-phone');
  initialState.allEntries[mtpVolume.fileData.key] = mtpVolume.fileData;
  initialState.volumes[mtpVolume.volume.volumeId] = mtpVolume.volume;

  const providerVolume2 =
      createVolumeFileData(VolumeType.PROVIDED, 'provided:prov2');
  initialState.allEntries[providerVolume2.fileData.key] =
      providerVolume2.fileData;
  initialState.volumes[providerVolume2.volume.volumeId] =
      providerVolume2.volume;

  const androidFilesVolume =
      createVolumeFileData(VolumeType.ANDROID_FILES, 'android_files:droid');
  androidFilesVolume.volume.prefixKey = myFilesVolume.fileData.key;
  initialState.allEntries[androidFilesVolume.fileData.key] =
      androidFilesVolume.fileData;
  initialState.volumes[androidFilesVolume.volume.volumeId] =
      androidFilesVolume.volume;

  const smbVolume = createVolumeFileData(VolumeType.SMB, 'smb:file-share');
  initialState.allEntries[smbVolume.fileData.key] = smbVolume.fileData;
  initialState.volumes[smbVolume.volume.volumeId] = smbVolume.volume;

  const odfsVolume = createVolumeFileData(
      VolumeType.PROVIDED, 'provided:odfs', '', '', ODFS_EXTENSION_ID);
  initialState.allEntries[odfsVolume.fileData.key] = odfsVolume.fileData;
  initialState.volumes[odfsVolume.volume.volumeId] = odfsVolume.volume;

  const store = setupStore(initialState);

  // Dispatch an action to refresh navigation roots.
  store.dispatch(refreshNavigationRoots());

  // Expect navigation roots being built in the store:
  //  1.  fake-entry://recent
  //  2.  /root/shortcut1
  //  3.  /root/shortcut2
  //  4.  My files
  //      * Android files - won't be included as root because it's inside
  //      MyFiles.
  //  5.  Google Drive
  //  6.  ODFS
  //  7.  smb:file-share
  //  8.  provided:prov1
  //  9.  provided:prov2
  //
  // 10.  removable:hoge
  // 11.  removable:fuga
  // 12.  archive:a-rar  - mounted as archive
  // 13.  mtp:a-phone
  //
  // 14.  android:app1
  // 15.  android:app2
  //
  // 16.  Trash

  // Check items order and that MTP/Archive/Removable respect the original
  // order.
  const want: State['navigation']['roots'] = [
    // recent.
    {
      key: recentEntryFileData.key,
      section: NavigationSection.TOP,
      separator: false,
      type: NavigationType.RECENT,
    },
    // shortcut1.
    {
      key: shortcutEntryFileData1.key,
      section: NavigationSection.TOP,
      separator: false,
      type: NavigationType.SHORTCUT,
    },
    // shortcut2.
    {
      key: shortcutEntryFileData2.key,
      section: NavigationSection.TOP,
      separator: false,
      type: NavigationType.SHORTCUT,
    },
    // My Files.
    {
      key: myFilesVolume.fileData.key,
      section: NavigationSection.MY_FILES,
      separator: true,
      type: NavigationType.VOLUME,
    },
    // Drive.
    {
      key: driveRootEntryFileData.key,
      section: NavigationSection.GOOGLE_DRIVE,
      separator: true,
      type: NavigationType.DRIVE,
    },
    // ODFS
    {
      key: odfsVolume.fileData.key,
      section: NavigationSection.ODFS,
      separator: true,
      type: NavigationType.VOLUME,
    },
    // Other FSP, and SMB are grouped together.
    // smb:file-share.
    {
      key: smbVolume.fileData.key,
      section: NavigationSection.CLOUD,
      separator: true,
      type: NavigationType.VOLUME,
    },
    // provided:prov1.
    {
      key: providerVolume1.fileData.key,
      section: NavigationSection.CLOUD,
      separator: false,
      type: NavigationType.VOLUME,
    },
    // provided:prov2.
    {
      key: providerVolume2.fileData.key,
      section: NavigationSection.CLOUD,
      separator: false,
      type: NavigationType.VOLUME,
    },
    // MTP/Archive/Removable are grouped together.
    // removable:hoge.
    {
      key: hogeVolume.fileData.key,
      section: NavigationSection.REMOVABLE,
      separator: true,
      type: NavigationType.VOLUME,
    },
    // removable:fuga.
    {
      key: fugaVolume.fileData.key,
      section: NavigationSection.REMOVABLE,
      separator: false,
      type: NavigationType.VOLUME,
    },
    // archive:a-rar.
    {
      key: archiveVolume.fileData.key,
      section: NavigationSection.REMOVABLE,
      separator: false,
      type: NavigationType.VOLUME,
    },
    // mtp:a-phone.
    {
      key: mtpVolume.fileData.key,
      section: NavigationSection.REMOVABLE,
      separator: false,
      type: NavigationType.VOLUME,
    },
    // android:app1.
    {
      key: androidAppsData[0].packageName,
      section: NavigationSection.ANDROID_APPS,
      separator: true,
      type: NavigationType.ANDROID_APPS,
    },
    // android:app2.
    {
      key: androidAppsData[1].packageName,
      section: NavigationSection.ANDROID_APPS,
      separator: false,
      type: NavigationType.ANDROID_APPS,
    },
    // Trash.
    {
      key: trashEntryFileData.key,
      section: NavigationSection.TRASH,
      separator: true,
      type: NavigationType.TRASH,
    },
  ];
  await waitDeepEquals(store, want, (state) => state.navigation.roots);

  done();
}

/**
 * Tests navigation roots with no Recents.
 */
export async function testNavigationRootsWithoutRecents(done: () => void) {
  const initialState = getEmptyState();
  // Put shortcut entry in the store.
  const shortcutEntryFileData =
      createShortcutEntryFileData('drive', 'shortcut', 'Shortcut');
  initialState.allEntries[shortcutEntryFileData.key] = shortcutEntryFileData;
  initialState.folderShortcuts.push(shortcutEntryFileData.key);
  // Put MyFiles entry in the store.
  const myFilesVolume = createMyFilesEntryFileData();
  initialState.allEntries[myFilesVolume.fileData.key] = myFilesVolume.fileData;
  initialState.volumes[myFilesVolume.volume.volumeId] = myFilesVolume.volume;

  const store = setupStore(initialState);

  // Dispatch an action to refresh navigation roots.
  store.dispatch(refreshNavigationRoots());

  // Expect 2 navigation roots.
  const want: State['navigation']['roots'] = [
    // shortcut.
    {
      key: shortcutEntryFileData.key,
      section: NavigationSection.TOP,
      separator: false,
      type: NavigationType.SHORTCUT,
    },
    // My Files volume.
    {
      key: myFilesVolume.fileData.key,
      section: NavigationSection.MY_FILES,
      separator: true,
      type: NavigationType.VOLUME,
    },
  ];
  await waitDeepEquals(store, want, (state) => state.navigation.roots);

  done();
}

/**
 * Tests navigation roots with fake MyFiles.
 */
export async function testNavigationRootsWithFakeMyFiles(done: () => void) {
  const initialState = getEmptyState();
  // Put recent entry in the store.
  const recentEntryFileData = createRecentFileData();
  initialState.allEntries[recentRootKey] = recentEntryFileData;
  // Put MyFiles entry in the store.
  const myFilesEntryList = new EntryList('My files', RootType.MY_FILES);
  initialState.allEntries[myFilesEntryList.toURL()] =
      convertEntryToFileData(myFilesEntryList);

  const store = setupStore(initialState);

  // Dispatch an action to refresh navigation roots.
  store.dispatch(refreshNavigationRoots());

  // Expect 2 navigation roots.
  const want: State['navigation']['roots'] = [
    // recent.
    {
      key: recentEntryFileData.key,
      section: NavigationSection.TOP,
      separator: false,
      type: NavigationType.RECENT,
    },
    // My Files entry list.
    {
      key: myFilesEntryList.toURL(),
      section: NavigationSection.MY_FILES,
      separator: true,
      type: NavigationType.ENTRY_LIST,
    },
  ];
  await waitDeepEquals(store, want, (state) => state.navigation.roots);

  done();
}

/**
 * Tests navigation roots with volumes.
 */
export async function testNavigationRootsWithVolumes(done: () => void) {
  const initialState = getEmptyState();
  // Put recent entry in the store.
  const recentEntryFileData = createRecentFileData();
  initialState.allEntries[recentRootKey] = recentEntryFileData;
  // Put MyFiles entry in the store.
  const myFilesVolume = createMyFilesEntryFileData();
  initialState.allEntries[myFilesVolume.fileData.key] = myFilesVolume.fileData;
  initialState.volumes[myFilesVolume.volume.volumeId] = myFilesVolume.volume;
  // Put drive entry in the store.
  const driveRootEntryFileData = createDriveRootEntryListFileData();
  initialState.allEntries[driveRootEntryListKey] = driveRootEntryFileData;
  initialState.uiEntries.push(driveRootEntryListKey);

  // Put removable volume 'hoge' in the store.
  const hogeVolume = createVolumeFileData(
      VolumeType.REMOVABLE, 'removable:hoge', 'Hoge', 'device/path/1');
  initialState.allEntries[hogeVolume.fileData.key] = hogeVolume.fileData;
  initialState.volumes[hogeVolume.volume.volumeId] = hogeVolume.volume;

  // Create a shortcut for the 'hoge' volume in the store.
  const hogeShortcutEntryFileData = createShortcutEntryFileData(
      hogeVolume.volume.volumeId, 'shortcut-hoge', 'Hoge shortcut');
  initialState.allEntries[hogeShortcutEntryFileData.key] =
      hogeShortcutEntryFileData;
  initialState.folderShortcuts.push(hogeShortcutEntryFileData.key);

  // Put removable volume 'fuga' in the store. Not a partition, so set a
  // different device path to 'hoge'.
  const fugaVolume = createVolumeFileData(
      VolumeType.REMOVABLE, 'removable:fuga', 'Fuga', 'device/path/2');
  initialState.allEntries[fugaVolume.fileData.key] = fugaVolume.fileData;
  initialState.volumes[fugaVolume.volume.volumeId] = fugaVolume.volume;

  const store = setupStore(initialState);

  // Dispatch an action to refresh navigation roots.
  store.dispatch(refreshNavigationRoots());

  // Expect 6 navigation roots.
  const want: State['navigation']['roots'] = [
    // recent.
    {
      key: recentEntryFileData.key,
      section: NavigationSection.TOP,
      separator: false,
      type: NavigationType.RECENT,
    },
    // hoge shortcut.
    {
      key: hogeShortcutEntryFileData.key,
      section: NavigationSection.TOP,
      separator: false,
      type: NavigationType.SHORTCUT,
    },
    // My Files.
    {
      key: myFilesVolume.fileData.key,
      section: NavigationSection.MY_FILES,
      separator: true,
      type: NavigationType.VOLUME,
    },
    // Drive.
    {
      key: driveRootEntryFileData.key,
      section: NavigationSection.GOOGLE_DRIVE,
      separator: true,
      type: NavigationType.DRIVE,
    },
    // hoge volume.
    {
      key: hogeVolume.fileData.key,
      section: NavigationSection.REMOVABLE,
      separator: true,
      type: NavigationType.VOLUME,
    },
    // fuga volume.
    {
      key: fugaVolume.fileData.key,
      section: NavigationSection.REMOVABLE,
      separator: false,
      type: NavigationType.VOLUME,
    },
  ];
  await waitDeepEquals(store, want, (state) => state.navigation.roots);

  done();
}

/**
 * Tests that for multiple partition volumes, only the parent entry will be
 * added to the navigation roots.
 */
export async function testMultipleUsbPartitionsGrouping(done: () => void) {
  const initialState = getEmptyState();

  // Add parent entry list to the store.
  const devicePath = 'device/path/1';
  const parentEntry =
      new EntryList('Partition wrap', RootType.REMOVABLE, devicePath);
  initialState.allEntries[parentEntry.toURL()] =
      convertEntryToFileData(parentEntry);
  // Create 3 volumes with the same device path so the partitions are grouped.
  const partitionVolume1 = createVolumeFileData(
      VolumeType.REMOVABLE, 'removable:partition1', 'partition1', devicePath);
  partitionVolume1.volume.prefixKey = parentEntry.toURL();
  initialState.allEntries[partitionVolume1.fileData.key] =
      partitionVolume1.fileData;
  initialState.volumes[partitionVolume1.volume.volumeId] =
      partitionVolume1.volume;
  const partitionVolume2 = createVolumeFileData(
      VolumeType.REMOVABLE, 'removable:partition2', 'partition2', devicePath);
  initialState.allEntries[partitionVolume2.fileData.key] =
      partitionVolume2.fileData;
  initialState.volumes[partitionVolume2.volume.volumeId] =
      partitionVolume2.volume;
  partitionVolume2.volume.prefixKey = parentEntry.toURL();
  const partitionVolume3 = createVolumeFileData(
      VolumeType.REMOVABLE, 'removable:partition3', 'partition3', devicePath);
  initialState.allEntries[partitionVolume3.fileData.key] =
      partitionVolume3.fileData;
  initialState.volumes[partitionVolume3.volume.volumeId] =
      partitionVolume3.volume;
  partitionVolume3.volume.prefixKey = parentEntry.toURL();

  const store = setupStore(initialState);

  // Dispatch an action to refresh navigation roots.
  store.dispatch(refreshNavigationRoots());

  // Expect only the parent entry and MyFiles being added to the navigation
  // roots.
  const want: State['navigation']['roots'] = [
    // My Files entry list.
    {
      key: myFilesEntryListKey,
      section: NavigationSection.MY_FILES,
      separator: false,
      type: NavigationType.ENTRY_LIST,
    },
    // parent entry for all removable partitions.
    {
      key: parentEntry.toURL(),
      section: NavigationSection.REMOVABLE,
      separator: true,
      type: NavigationType.VOLUME,
    },
  ];
  await waitDeepEquals(store, want, (state) => state.navigation.roots);

  done();
}

/**
 * Tests that the volumes filtered by the volume manager won't be shown in the
 * navigation roots.
 */
export async function testNavigationRootsWithFilteredVolume(done: () => void) {
  const initialState = getEmptyState();
  // Put volume1 in the store.
  const volume1 = createVolumeFileData(VolumeType.REMOVABLE, 'removable1');
  initialState.allEntries[volume1.fileData.key] = volume1.fileData;
  initialState.volumes[volume1.volume.volumeId] = volume1.volume;
  // Put volume2 in the store.
  const volumeInfo2 = MockVolumeManager.createMockVolumeInfo(
      VolumeType.REMOVABLE, 'removable2');
  const volumeEntry2 = new VolumeEntry(volumeInfo2);
  initialState.allEntries[volumeEntry2.toURL()] =
      convertEntryToFileData(volumeEntry2);
  initialState.volumes[volumeInfo2.volumeId] =
      convertVolumeInfoAndMetadataToVolume(
          volumeInfo2, createFakeVolumeMetadata(volumeInfo2));
  // Mark volume2 as not allowed volume.
  const {volumeManager} = window.fileManager;
  volumeManager.isAllowedVolume = (volumeInfo) => volumeInfo !== volumeInfo2;

  const store = setupStore(initialState);

  // Dispatch an action to refresh navigation roots.
  store.dispatch(refreshNavigationRoots());

  // Expect only volume1 and MyFiles in the navigation roots.
  const want: State['navigation']['roots'] = [
    // My Files entry list.
    {
      key: myFilesEntryListKey,
      section: NavigationSection.MY_FILES,
      separator: false,
      type: NavigationType.ENTRY_LIST,
    },
    // volume1.
    {
      key: volume1.fileData.key,
      section: NavigationSection.REMOVABLE,
      separator: true,
      type: NavigationType.VOLUME,
    },
  ];
  await waitDeepEquals(store, want, (state) => state.navigation.roots);

  done();
}

/**
 * Tests that materialized views add navigation roots.
 */
export async function testNavigationRootsWithMaterializedViews(
    done: () => void) {
  const initialState = getEmptyState();
  const store = setupStore(initialState);

  // Roots and materialized views start empty.
  assertDeepEquals(store.getState().materializedViews, []);
  assertDeepEquals(store.getState().navigation.roots, []);

  // Dispatch an action to refresh navigation roots.
  store.dispatch(refreshNavigationRoots());

  // Everything is still empty.
  assertDeepEquals(
      store.getState().materializedViews, [], 'expected empty views');
  // It contains My files by default.
  assertEquals(store.getState().navigation.roots.length, 1);
  assertEquals(
      getFileData(store.getState(), store.getState().navigation.roots[0]!.key)
          ?.label,
      'My files');
  const myFilesRoot = store.getState().navigation.roots[0]!;

  // Add a view.
  store.dispatch(updateMaterializedViews({
    materializedViews: [
      {
        viewId: 1,
        name: 'test view',
      },
    ],
  }));

  const viewKey = 'materialized-view://1/';
  const wantView: MaterializedView = {
    id: '1',
    key: viewKey,
    label: 'test view',
    icon: ICON_TYPES.STAR,
    isRoot: true,
  };
  await waitDeepEquals(store, [wantView], (state) => state.materializedViews);

  // Refresh to pick up the new view.
  store.dispatch(refreshNavigationRoots());
  const wantViewRoot: NavigationRoot = {
    key: viewKey,
    section: NavigationSection.TOP,
    separator: false,
    type: NavigationType.MATERIALIZED_VIEW,
  };
  await waitDeepEquals(
      store, [wantViewRoot, myFilesRoot], (state) => state.navigation.roots);

  done();
}