blob: e8e5f9695811a06c9f9030ce06c13f1a523c0b27 [file] [log] [blame]
// 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 {DialogType} from '../dialog_type.js';
import {addEntries, ENTRIES, repeatUntil, RootPath, sendTestMessage} from '../test_util.js';
import {testcase} from '../testcase.js';
import {navigateWithDirectoryTree, openNewWindow, remoteCall, setupAndWaitUntilReady} from './background.js';
import {DOWNLOADS_FAKE_TASKS} from './tasks.js';
import {BASIC_ANDROID_ENTRY_SET, BASIC_DRIVE_ENTRY_SET, BASIC_LOCAL_ENTRY_SET, NESTED_ENTRY_SET} from './test_data.js';
/**
* Clicks the enabled and visible move to trash button and ensures the delete
* button is hidden.
* @param {string} appId
*/
async function clickTrashButton(appId) {
await remoteCall.waitForElement(appId, '#delete-button[hidden]');
await remoteCall.waitAndClickElement(
appId, '#move-to-trash-button:not([hidden]):not([disabled])');
}
/**
* Clicks the enabled and visible delete button and ensures the move to trash
* button is hidden.
* @param {string} appId
*/
async function clickDeleteButton(appId) {
await remoteCall.waitForElement(appId, '#move-to-trash[hidden]');
await remoteCall.waitAndClickElement(
appId, '#delete-button:not([hidden]):not([disabled])');
}
/**
* Confirm the deletion happens and assert the dialog has the correct text.
* @param {string} appId
* @param {string} okText expected OK text
*/
async function confirmPermanentDeletion(appId, okText) {
// Check: the delete confirm dialog should appear.
await remoteCall.waitForElement(appId, '.cr-dialog-container.shown');
// Check: the dialog 'Cancel' button should be focused by default.
const dialogDefaultButton =
await remoteCall.waitForElement(appId, '.cr-dialog-cancel:focus');
chrome.test.assertEq('Cancel', dialogDefaultButton.text);
// Click the delete confirm dialog 'Delete' button.
const dialogDeleteButton =
await remoteCall.waitAndClickElement(appId, '.cr-dialog-ok');
chrome.test.assertEq(okText, dialogDeleteButton.text);
// Wait for completion of file deletion.
await remoteCall.waitForElementLost(
appId, '#file-list [file-name="hello.txt"]');
}
/**
* Clicks the delete button and confirms the deletion.
* @param {string} appId
*/
async function clickDeleteButtonAndConfirmDeletion(appId) {
await clickDeleteButton(appId);
await confirmPermanentDeletion(appId, 'Delete forever');
}
/**
* Shows hidden files to facilitate tests again the .Trash directory.
* @param {string} appId
*/
async function showHiddenFiles(appId) {
// Open the gear menu by clicking the gear button.
chrome.test.assertTrue(await remoteCall.callRemoteTestUtil(
'fakeMouseClick', appId, ['#gear-button']));
// Wait for menu to not be hidden.
await remoteCall.waitForElement(appId, '#gear-menu:not([hidden])');
// Wait for menu item to appear.
await remoteCall.waitForElement(
appId, '#gear-menu-toggle-hidden-files:not([disabled]):not([checked])');
// Click the menu item.
await remoteCall.callRemoteTestUtil(
'fakeMouseClick', appId, ['#gear-menu-toggle-hidden-files']);
}
/**
* Delete files in MyFiles and ensure they are moved to /.Trash.
* Then delete items from /.Trash/files and /.Trash/info, then delete /.Trash.
*/
testcase.trashMoveToTrash = async () => {
const appId = await setupAndWaitUntilReady(
RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
// Select hello.txt.
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name="hello.txt"]');
// Delete item and wait for it to be removed (no dialog).
await clickTrashButton(appId);
await remoteCall.waitForElementLost(
appId, '#file-list [file-name="hello.txt"]');
// Enable hidden files to be shown.
await showHiddenFiles(appId);
// Navigate to /My files/Downloads/.Trash/files.
await navigateWithDirectoryTree(appId, '/My files/Downloads/.Trash/files');
// Select hello.txt.
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name="hello.txt"]');
// Delete selected item.
await clickDeleteButtonAndConfirmDeletion(appId);
await remoteCall.waitForElementLost(
appId, '#file-list [file-name="hello.txt"]');
// Navigate to /My files/Downloads/.Trash/info.
await navigateWithDirectoryTree(appId, '/My files/Downloads/.Trash/info');
// Select hello.txt.trashinfo.
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name="hello.txt.trashinfo"]');
// Delete selected item.
await clickDeleteButtonAndConfirmDeletion(appId);
// Wait for completion of file deletion.
await remoteCall.waitForElementLost(
appId, '#file-list [file-name="hello.txt.trashinfo"]');
// Navigate to /My files/Downloads.
await navigateWithDirectoryTree(appId, '/My files/Downloads');
// Select .Trash.
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name=".Trash"]');
// Delete selected item.
await clickDeleteButtonAndConfirmDeletion(appId);
// Wait for completion of file deletion.
await remoteCall.waitForElementLost(appId, '#file-list [file-name=".Trash"]');
// Delete photos dir (no dialog),
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name="photos"]');
await clickTrashButton(appId);
// Wait for photos to be removed, and .Trash to be recreated.
await remoteCall.waitForElementLost(appId, '#file-list [file-name="photos"]');
await remoteCall.waitForElement(appId, '#file-list [file-name=".Trash"]');
};
/**
* Permanently delete files in Downloads.
*/
testcase.trashPermanentlyDelete = async () => {
const appId = await setupAndWaitUntilReady(
RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
// Select hello.txt.
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name="hello.txt"]');
// Send Shift+Delete to permanently delete, shows delete confirm dialog.
const shiftDeleteKey = ['#quick-view', 'Delete', false, true, false];
chrome.test.assertTrue(
await remoteCall.callRemoteTestUtil('fakeKeyDown', appId, shiftDeleteKey),
'Pressing Shift+Delete failed.');
// Confirm the permanent deletion of the "hello.txt" file.
await confirmPermanentDeletion(appId, 'Delete forever');
};
/**
* Files send to the Trash from ~/MyFiles should be able to be deleted once they
* are in Trash.
*/
testcase.trashDeleteFromTrashOriginallyFromMyFiles = async () => {
const appId = await setupAndWaitUntilReady(
RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
// Select hello.txt.
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name="hello.txt"]');
// Cut the file "hello.txt" in preparation to move to ~/MyFiles.
chrome.test.assertTrue(
await remoteCall.callRemoteTestUtil('execCommand', appId, ['cut']));
await navigateWithDirectoryTree(appId, '/My files');
// Paste the file.
chrome.test.assertTrue(
await remoteCall.callRemoteTestUtil('execCommand', appId, ['paste']));
// Select hello.txt.
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name="hello.txt"]');
// Delete item and wait for it to be removed (no dialog).
await clickTrashButton(appId);
await remoteCall.waitForElementLost(
appId, '#file-list [file-name="hello.txt"]');
// Navigate to /Trash and ensure the file is shown.
await navigateWithDirectoryTree(appId, '/Trash');
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name="hello.txt"]');
// Delete selected item.
await clickDeleteButtonAndConfirmDeletion(appId);
};
/**
* Delete files then restore via progress center panel button 'Undo'.
*/
testcase.trashRestoreFromToast = async () => {
const appId = await setupAndWaitUntilReady(
RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
// Select hello.txt.
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name="hello.txt"]');
// Delete item and wait for it to be removed (no dialog).
await clickTrashButton(appId);
await remoteCall.waitForElementLost(
appId, '#file-list [file-name="hello.txt"]');
// Wait for the a success progress panel item to appear.
await remoteCall.waitForElement(
appId, ['#progress-panel', 'xf-panel-item[status="success"]']);
// Press the "Undo"" button on the success feedback panel.
chrome.test.assertTrue(await remoteCall.callRemoteTestUtil(
'fakeMouseClick', appId,
[['#progress-panel', 'xf-panel-item', 'xf-button#primary-action']]));
// Wait for file to reappear in list.
await remoteCall.waitForElement(appId, '#file-list [file-name="hello.txt"]');
};
/**
* Delete files then restore via Trash file context menu.
*/
testcase.trashRestoreFromTrash = async () => {
const appId = await setupAndWaitUntilReady(
RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
// Select hello.txt.
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name="hello.txt"]');
// Delete item and wait for it to be removed (no dialog).
await clickTrashButton(appId);
await remoteCall.waitForElementLost(
appId, '#file-list [file-name="hello.txt"]');
// Navigate to /Trash and ensure the file is shown.
await navigateWithDirectoryTree(appId, '/Trash');
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name="hello.txt"]');
// Right-click the selected file to validate context menu.
chrome.test.assertTrue(!!await remoteCall.callRemoteTestUtil(
'fakeMouseRightClick', appId, ['.table-row[selected]']));
await remoteCall.waitForElement(appId, '#file-context-menu:not([hidden])');
// Check that 'Restore from Trash' and 'Delete' are shown.
const checkMenu = async command => {
await remoteCall.waitForElement(
appId,
`#file-context-menu:not([hidden]) [command="${
command}"]:not([hidden])`);
};
await checkMenu('#restore-from-trash');
await checkMenu('#delete');
// Restore item.
await remoteCall.waitAndClickElement(appId, '#restore-from-trash-button');
// Wait for completion of file restore.
await remoteCall.waitForElementLost(
appId, '#file-list [file-name="hello.txt"]');
// Navigate to /My files/Downloads and ensure the file is shown.
await navigateWithDirectoryTree(appId, '/My files/Downloads');
await remoteCall.waitForElement(appId, '#file-list [file-name="hello.txt"]');
};
/**
* Delete files then restore via keyboard shortcut.
*/
testcase.trashRestoreFromTrashShortcut = async () => {
const appId = await setupAndWaitUntilReady(
RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
// Select hello.txt.
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name="hello.txt"]');
// Delete item and wait for it to be removed (no dialog).
await clickTrashButton(appId);
await remoteCall.waitForElementLost(
appId, '#file-list [file-name="hello.txt"]');
// Navigate to /Trash.
await navigateWithDirectoryTree(appId, '/Trash');
// Select file.
await remoteCall.waitUntilSelected(appId, 'hello.txt');
// Press 'Delete' key.
chrome.test.assertTrue(!!await remoteCall.callRemoteTestUtil(
'fakeKeyDown', appId, ['#file-list', 'Delete', false, false, false]));
// Wait for completion of file restore.
await remoteCall.waitForElementLost(
appId, '.tre-row input [file-name="hello.txt"]');
// Navigate to /My files/Downloads and ensure the file is shown.
await navigateWithDirectoryTree(appId, '/My files/Downloads');
await remoteCall.waitForElement(appId, '#file-list [file-name="hello.txt"]');
};
/**
* Delete files (move them into trash) then empty trash using the banner.
*/
testcase.trashEmptyTrash = async () => {
const appId = await setupAndWaitUntilReady(
RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
// Select hello.txt.
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name="hello.txt"]');
// Delete item and wait for it to be removed (no dialog).
await clickTrashButton(appId);
await remoteCall.waitForElementLost(
appId, '#file-list [file-name="hello.txt"]');
// Navigate to /Trash and ensure the file is shown.
await navigateWithDirectoryTree(appId, '/Trash');
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name="hello.txt"]');
// Empty trash and confirm delete (dialog shown).
await remoteCall.waitAndClickElement(
appId, ['trash-banner', 'cr-button[command="#empty-trash"]']);
await remoteCall.waitAndClickElement(
appId, '.files-confirm-dialog .cr-dialog-ok');
// Wait for completion of file deletion.
await remoteCall.waitForElementLost(
appId, '#file-list [file-name="hello.txt"]');
};
/**
* Delete files (move them into trash) then empty trash using shortcut.
*/
testcase.trashEmptyTrashShortcut = async () => {
const appId = await setupAndWaitUntilReady(
RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
// Select hello.txt.
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name="hello.txt"]');
// Delete item and wait for it to be removed (no dialog).
await clickTrashButton(appId);
await remoteCall.waitForElementLost(
appId, '#file-list [file-name="hello.txt"]');
// Navigate to /Trash and ensure the file is shown.
await navigateWithDirectoryTree(appId, '/Trash');
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name="hello.txt"]');
// Press Ctrl+Shift+Delete key.
chrome.test.assertTrue(!!await remoteCall.callRemoteTestUtil(
'fakeKeyDown', appId,
['#file-list', 'Delete', /*ctrl=*/ true, /*shift=*/ true, false]));
// Confirm dialog.
await remoteCall.waitAndClickElement(
appId, '.files-confirm-dialog .cr-dialog-ok');
// Wait for completion of file deletion.
await remoteCall.waitForElementLost(
appId, '#file-list [file-name="hello.txt"]');
};
/**
* Delete files (move them into trash) then permanently delete.
*/
testcase.trashDeleteFromTrash = async () => {
const appId = await setupAndWaitUntilReady(
RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
// Select hello.txt.
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name="hello.txt"]');
// Delete item and wait for it to be removed (no dialog).
await clickTrashButton(appId);
await remoteCall.waitForElementLost(
appId, '#file-list [file-name="hello.txt"]');
// Navigate to /Trash and ensure the file is shown.
await navigateWithDirectoryTree(appId, '/Trash');
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name="hello.txt"]');
// Delete selected item.
await clickDeleteButtonAndConfirmDeletion(appId);
};
/**
* Delete files (move them into trash) then permanently delete.
*/
testcase.trashDeleteFromTrashOriginallyFromDrive = async () => {
const appId =
await setupAndWaitUntilReady(RootPath.DRIVE, [], [ENTRIES.hello]);
// Select hello.txt.
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name="hello.txt"]');
// Delete item and wait for it to be removed (no dialog).
await clickTrashButton(appId);
await remoteCall.waitForElementLost(
appId, '#file-list [file-name="hello.txt"]');
// Navigate to /Trash and ensure the file is shown.
await navigateWithDirectoryTree(appId, '/Trash');
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name="hello.txt"]');
// Confirm the permanent deletion of the file does not say 'forever'.
await clickDeleteButton(appId);
await confirmPermanentDeletion(appId, 'Delete');
};
/**
* When selecting items whilst in the trash root, no files tasks should be
* available.
*/
testcase.trashNoTasksInTrashRoot = async () => {
const appId = await setupAndWaitUntilReady(
RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
await remoteCall.callRemoteTestUtil(
'overrideTasks', appId, [DOWNLOADS_FAKE_TASKS]);
// Select hello.txt and make sure tasks are visible.
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name="hello.txt"]');
await remoteCall.waitForElement(appId, '#tasks:not([hidden])');
// Delete item and wait for it to be removed (no dialog).
await clickTrashButton(appId);
await remoteCall.waitForElementLost(
appId, '#file-list [file-name="hello.txt"]');
// Navigate to /Trash and ensure the file is shown and the tasks button is
// hidden.
await navigateWithDirectoryTree(appId, '/Trash');
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name="hello.txt"]');
await remoteCall.waitForElement(appId, '#tasks[hidden]');
};
/**
* Double clicking on a file while in Trash shows a disallowed alert dialog.
*/
testcase.trashDoubleClickOnFileInTrashRootShowsDialog = async () => {
const appId = await setupAndWaitUntilReady(
RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
await remoteCall.callRemoteTestUtil(
'overrideTasks', appId, [DOWNLOADS_FAKE_TASKS]);
// Select hello.txt and make sure a default task is executed when double
// clicking.
await remoteCall.waitForElement(appId, '#file-list [file-name="hello.txt"]');
chrome.test.assertTrue(!!await remoteCall.callRemoteTestUtil(
'fakeMouseDoubleClick', appId, ['#file-list [file-name="hello.txt"]']));
await remoteCall.waitUntilTaskExecutes(
appId, DOWNLOADS_FAKE_TASKS[0].descriptor, ['hello.txt']);
// Delete item and wait for it to be removed (no dialog).
await clickTrashButton(appId);
await remoteCall.waitForElementLost(
appId, '#file-list [file-name="hello.txt"]');
// Navigate to /Trash and ensure the file is shown.
await navigateWithDirectoryTree(appId, '/Trash');
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name="hello.txt"]');
await remoteCall.waitForElement(appId, '#tasks[hidden]');
// Double-click the file and ensure an alert dialog is displayed.
await remoteCall.callRemoteTestUtil(
'fakeMouseDoubleClick', appId, ['#file-list [file-name="hello.txt"]']);
await remoteCall.waitForElement(appId, '.files-confirm-dialog');
};
/**
* Pressing Enter on a file while in Trash shows a disallowed confirm dialog
* with a restore button that performs restoration on the file.
*/
testcase.trashPressingEnterOnFileInTrashRootShowsDialogWithRestoreButton =
async () => {
const appId = await setupAndWaitUntilReady(
RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
await remoteCall.callRemoteTestUtil(
'overrideTasks', appId, [DOWNLOADS_FAKE_TASKS]);
// Delete item and wait for it to be removed (no dialog).
await remoteCall.waitUntilSelected(appId, 'hello.txt');
await clickTrashButton(appId);
await remoteCall.waitForElementLost(
appId, '#file-list [file-name="hello.txt"]');
// Navigate to /Trash and ensure the file is shown.
await navigateWithDirectoryTree(appId, '/Trash');
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name="hello.txt"]');
await remoteCall.waitForElement(appId, '#tasks[hidden]');
// Press "Enter" on the file and ensure an alert dialog is displayed.
const enterKey = ['#file-list', 'Enter', false, false, false];
await remoteCall.fakeKeyDown(appId, ...enterKey);
await remoteCall.waitForElement(appId, '.files-confirm-dialog');
// Click the "Restore" button on the error message and ensure it restores the
// file back to the Downloads directory.
await remoteCall.waitAndClickElement(appId, '.cr-dialog-ok');
await navigateWithDirectoryTree(appId, '/My files/Downloads');
await remoteCall.waitForElement(appId, '#file-list [file-name="hello.txt"]');
};
/**
* Double clicking on a file while in Trash shows a disallowed alert dialog.
*/
testcase.trashTraversingFolderShowsDisallowedDialog = async () => {
const appId = await setupAndWaitUntilReady(
RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
// Select the Photos folder and trash the whole thing.
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name="photos"]');
await clickTrashButton(appId);
await remoteCall.waitForElementLost(appId, '#file-list [file-name="photos"]');
// Navigate to /Trash and ensure the "photos" folder is shown.
await navigateWithDirectoryTree(appId, '/Trash');
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name="photos"]');
// Double-click the folder and ensure an alert dialog is displayed.
await remoteCall.callRemoteTestUtil(
'fakeMouseDoubleClick', appId, ['#file-list [file-name="photos"]']);
await remoteCall.waitForElement(appId, '.files-confirm-dialog');
// Dismiss the alert dialog.
await remoteCall.waitAndClickElement(appId, '.cr-dialog-cancel');
// Select the element and press enter, outside of Trash this would navigate to
// the folder but in Trash it should show a confirm dialog.
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name="photos"]');
const enterKey = ['#file-list', 'Enter', false, false, false];
await remoteCall.fakeKeyDown(appId, ...enterKey);
await remoteCall.waitForElement(appId, '.files-confirm-dialog');
};
/**
* Tests that dragging an accepted file over Trash shows that it accepts the
* action and performs a trash operation (move a move).
*/
testcase.trashDragDropRootAcceptsEntries = async () => {
const appId = await setupAndWaitUntilReady(
RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
// The drag has to start in the file list column "name" text, otherwise it
// starts a drag-selection instead of a drag operation.
const source = '#file-list [file-name="hello.txt"] .entry-name';
// Select the source file.
await remoteCall.waitAndClickElement(appId, source);
// Wait for the directory tree target.
const target = '#directory-tree [entry-label="Trash"]';
await remoteCall.waitForElement(appId, target);
// Drag the source and hover it over the target.
const skipDrop = true;
chrome.test.assertTrue(
await remoteCall.callRemoteTestUtil(
'fakeDragAndDrop', appId, [source, target, skipDrop]),
'fakeDragAndDrop failed');
// Check: drag hovering should navigate the file list.
await remoteCall.waitUntilCurrentDirectoryIsChanged(appId, '/Trash');
// Check: the target should have accepts class.
const willAcceptDrop = '#directory-tree [entry-label="Trash"].accepts';
await remoteCall.waitForElement(appId, willAcceptDrop);
// Check: the target should not have denies class.
const willDenyDrop = '#directory-tree [entry-label="Trash"].denies';
await remoteCall.waitForElementLost(appId, willDenyDrop);
// Send a dragdrop event to the target to start a trash operation.
const dragLeave = false;
chrome.test.assertTrue(
await remoteCall.callRemoteTestUtil(
'fakeDragLeaveOrDrop', appId, ['#file-list', target, dragLeave]),
'fakeDragLeaveOrDrop failed');
// The Trash root should not have either accepts nor denies after stopping the
// dragdrop event.
await remoteCall.waitForElementLost(appId, willAcceptDrop);
await remoteCall.waitForElementLost(appId, willDenyDrop);
};
/**
* Tests that dragging a file from a location that Trash is not enabled (Android
* files in this case) shows it is denied.
*/
testcase.trashDragDropFromDisallowedRootsFails = async () => {
// Open Files app on Play Files.
await addEntries(['android_files'], BASIC_ANDROID_ENTRY_SET);
const appId = await openNewWindow(RootPath.ANDROID_FILES);
// Wait for the file list to appear.
await remoteCall.waitForElement(appId, '#file-list');
// Set the source of the drag event to the name of the file.
const source = `#file-list li[file-name="${
BASIC_ANDROID_ENTRY_SET[0].nameText}"] .entry-name`;
// Select the source file.
await remoteCall.waitAndClickElement(appId, source);
// Wait for the directory tree target to be visible.
const target = '#directory-tree [entry-label="Trash"]';
await remoteCall.waitForElement(appId, target);
// Drag the source and hover it over the target.
const skipDrop = true;
chrome.test.assertTrue(
await remoteCall.callRemoteTestUtil(
'fakeDragAndDrop', appId, [source, target, skipDrop]),
'fakeDragAndDrop failed');
// Wait for the directory to change to the Trash root.
await remoteCall.waitUntilCurrentDirectoryIsChanged(appId, '/Trash');
// The Trash root in the directory tree shouldn't have the accepts class.
const willAcceptDrop = '#directory-tree [entry-label="Trash"].accepts';
await remoteCall.waitForElementLost(appId, willAcceptDrop);
// The Trash root should have a denies class.
const willDenyDrop = '#directory-tree [entry-label="Trash"].denies';
await remoteCall.waitForElement(appId, willDenyDrop);
// Send a dragleave event to the target to stop the drag event.
const dragLeave = true;
chrome.test.assertTrue(
await remoteCall.callRemoteTestUtil(
'fakeDragLeaveOrDrop', appId, ['#file-list', target, dragLeave]),
'fakeDragLeaveOrDrop failed');
// The Trash root should not have either accepts nor denies after stopping the
// dragdrop event.
await remoteCall.waitForElementLost(appId, willAcceptDrop);
await remoteCall.waitForElementLost(appId, willDenyDrop);
};
/**
* Tests that dragging and dropping on the Trash root actually trashes the item
* and it appears in Trash after drop completed.
*/
testcase.trashDragDropRootPerformsTrashAction = async () => {
const appId = await setupAndWaitUntilReady(
RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
// The drag has to start in the file list column "name" text, otherwise it
// starts a drag-selection instead of a drag operation.
const source = '#file-list [file-name="hello.txt"] .entry-name';
// Select the source file.
await remoteCall.waitAndClickElement(appId, source);
// Wait for the directory tree target.
const target = '#directory-tree [entry-label="Trash"]';
await remoteCall.waitForElement(appId, target);
// Send a dragdrop event to the target to start a trash operation.
const skipDrop = false;
chrome.test.assertTrue(
await remoteCall.callRemoteTestUtil(
'fakeDragAndDrop', appId, [source, target, skipDrop]),
'fakeDragLeaveOrDrop failed');
// Wait for element to disappear from the "Downloads" view, this indicates it
// should be in trash.
await remoteCall.waitForElementLost(appId, source);
// Navigate to the Trash root.
await navigateWithDirectoryTree(appId, '/Trash');
// Wait for the element to appear in the Trash.
await remoteCall.waitForElement(appId, '#file-list [file-name="hello.txt"]');
};
/**
* Tests that dragging an entry that is non-modifiable (Downloads in this case)
* should not be allowed despite residing in a trashable location.
*/
testcase.trashDragDropNonModifiableEntriesCantBeTrashed = async () => {
const appId = await setupAndWaitUntilReady(
RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
// Navigate to My files.
await navigateWithDirectoryTree(appId, '/My files');
// Use Downloads entry as the drag source. Although this is technically a
// folder and resides on a trashable location "My files", it is a special
// entry so we should disallow this from being acceptable as a drop target on
// Trash.
const source = '#file-list [file-name="Downloads"] .entry-name';
// Select the source file.
await remoteCall.waitAndClickElement(appId, source);
// Wait for the directory tree target.
const target = '#directory-tree [entry-label="Trash"]';
await remoteCall.waitForElement(appId, target);
// Send a dragdrop event to the target to try Trash the downloads folder.
const skipDrop = false;
chrome.test.assertTrue(
await remoteCall.callRemoteTestUtil(
'fakeDragAndDrop', appId, [source, target, skipDrop]),
'fakeDragLeaveOrDrop failed');
// Navigate to Trash to ensure Downloads wasn't sent there.
await navigateWithDirectoryTree(appId, '/Trash');
// Ensure the Downloads entry doesn't exist in Trash.
await remoteCall.waitForElement(appId, `[scan-completed="Trash"]`);
await remoteCall.waitForFiles(appId, []);
};
/**
* Tests the Trash root is not visible when opening Files app as a select file
* dialog.
*/
testcase.trashDontShowTrashRootOnSelectFileDialog = async () => {
// Open Files app on Downloads as a select file dialog.
const appId = await setupAndWaitUntilReady(
RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, [],
{type: DialogType.SELECT_OPEN_FILE});
// Navigate to the My files directory to ensure the directory tree has fully
// loaded and wait for My files to finish scanning.
await navigateWithDirectoryTree(appId, '/My files');
await remoteCall.waitForElement(appId, `[scan-completed="My files"]`);
// Ensure the Trash root entry is not visible on the page.
await remoteCall.waitForElementLost(
appId, '#directory-tree [entry-label="Trash"]');
};
/**
* Tests the Trash root is not visible when Files app is used as a select file
* dialog within Android applications.
*/
testcase.trashDontShowTrashRootWhenOpeningAsAndroidFilePicker = async () => {
// Open Files app on Downloads as an Android file picker.
const appId = await setupAndWaitUntilReady(
RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, [],
{volumeFilter: ['media-store-files-only']});
// Navigate to the My files directory to ensure the directory tree has fully
// loaded and wait for My files to finish scanning.
await navigateWithDirectoryTree(appId, '/My files');
await remoteCall.waitForElement(appId, `[scan-completed="My files"]`);
// Ensure the Trash root entry is not visible on the page.
await remoteCall.waitForElementLost(
appId, '#directory-tree [entry-label="Trash"]');
};
/**
* Tests that a trashed file with a deletion date >30 days gets permanently
* removed.
*/
testcase.trashEnsureOldEntriesArePeriodicallyRemoved = async () => {
const appId =
await setupAndWaitUntilReady(RootPath.DOWNLOADS, [ENTRIES.hello], []);
const fileNameSelector = '#file-list [file-name="hello.txt"]';
// Select hello.txt and make sure a default task is executed when double
// clicking.
await remoteCall.waitAndClickElement(appId, fileNameSelector);
await clickTrashButton(appId);
await remoteCall.waitForElementLost(appId, fileNameSelector);
// Navigate to /Trash and ensure the file is there and has not been deleted,
// the deletion date is well within the periodic deletion boundaries.
await navigateWithDirectoryTree(appId, '/Trash');
await remoteCall.waitForElement(appId, fileNameSelector);
// Navigate away from /Trash (to /My files) as the periodic removal will only
// be kicked off on initial directory scan.
await navigateWithDirectoryTree(appId, '/My files');
// Overwrite the existing .trashinfo file with an older one that is outside
// the 30 day window and should trigger periodic removal.
await addEntries(['local'], [
ENTRIES.trashRootDirectory,
ENTRIES.trashInfoDirectory,
ENTRIES.oldTrashInfoFile,
]);
// Navigate to /Trash and ensure the file has been removed.
await navigateWithDirectoryTree(appId, '/Trash');
await remoteCall.waitForElement(appId, `[scan-completed="Trash"]`);
await remoteCall.waitForElementLost(appId, fileNameSelector);
// Expect no feedback panel element to appear as the IOTask was kicked off
// with notifications disabled.
await remoteCall.waitForElementLost(
appId, ['#progress-panel', 'xf-panel-item']);
};
/**
* Tests that dragging and dropping out of the Trash root restore files to the
* location that was requested (i.e. the drop target).
*/
testcase.trashDragDropOutOfTrashPerformsRestoration = async () => {
const appId = await setupAndWaitUntilReady(
RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
// Select hello.txt and send it to the Trash.
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name="hello.txt"]');
await clickTrashButton(appId);
await remoteCall.waitForElementLost(
appId, '#file-list [file-name="hello.txt"]');
// Navigate to the Trash root.
await navigateWithDirectoryTree(appId, '/Trash');
// Wait for the element to appear in the Trash.
await remoteCall.waitForElement(appId, '#file-list [file-name="hello.txt"]');
// Send a dragdrop event that emulates dragging the hello.txt onto the "My
// files" root in the directory tree.
const skipDrop = false;
chrome.test.assertTrue(
await remoteCall.callRemoteTestUtil(
'fakeDragAndDrop', appId,
[
'#file-list [file-name="hello.txt"] .entry-name',
'#directory-tree [entry-label="My files"]',
skipDrop,
]),
'fakeDragLeaveOrDrop failed');
// Wait for the element to disappear from the file list.
await remoteCall.waitForElementLost(
appId, '#file-list [file-name="hello.txt"]');
// A feedback panel item should be created to indicate the restoring function.
await remoteCall.waitForElement(appId, ['#progress-panel', 'xf-panel-item']);
// Navigate to the "My files" root and ensure the file exists there now.
await navigateWithDirectoryTree(appId, '/My files');
await remoteCall.waitForElement(appId, '#file-list [file-name="hello.txt"]');
};
/**
* Tests that the "Moving to trash" visual signal that is shown whilst a trash
* operation is in progress, does not contain the "Undo" button.
*/
testcase.trashRestorationDialogInProgressDoesntShowUndo = async () => {
const appId = await setupAndWaitUntilReady(
RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
// Tell the progress center to never finish the operation which leaves the in
// progress visual signal visible.
await remoteCall.callRemoteTestUtil(
'progressCenterNeverNotifyCompleted', appId, []);
// Select hello.txt and send it to the Trash.
await remoteCall.waitAndClickElement(
appId, '#file-list [file-name="hello.txt"]');
await clickTrashButton(appId);
await remoteCall.waitForElementLost(
appId, '#file-list [file-name="hello.txt"]');
// A feedback panel item should be created to indicate the item is being sent
// to the trash with only a secondary action.
const cancelButton = await remoteCall.waitForElement(
appId,
['#progress-panel', 'xf-panel-item', 'xf-button#secondary-action']);
await remoteCall.waitForElementLost(
appId, ['#progress-panel', 'xf-panel-item', 'xf-button#primary-action']);
// Ensure the secondary action is of the category cancel.
chrome.test.assertEq(cancelButton.attributes['data-category'], 'cancel');
};
/**
* Tests that the `TrashEnabled` preference adds and removes the trash root
* from the directory tree.
*/
testcase.trashTogglingTrashEnabledPrefUpdatesDirectoryTree = async () => {
const appId = await setupAndWaitUntilReady(
RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
// Select hello.txt and send it to the Trash, this file should not be removed
// in between enabling and disabling the feature.
await remoteCall.waitUntilSelected(appId, 'hello.txt');
await clickTrashButton(appId);
await remoteCall.waitForElementLost(
appId, '#file-list [file-name="hello.txt"]');
// Wait for Trash root to be visible.
await remoteCall.waitForElement(
appId, '#directory-tree [entry-label="Trash"]');
// Disable trash.
await sendTestMessage({name: 'setTrashEnabled', enabled: false});
// Wait for the Trash root to disappear.
await remoteCall.waitForElementLost(
appId, '#directory-tree [entry-label="Trash"]');
// Ensure the delete button shows up instead of the move to trash button.
await remoteCall.waitUntilSelected(appId, 'world.ogv');
await clickDeleteButton(appId);
await remoteCall.waitForElement(appId, '.cr-dialog-container.shown');
// Cancel the dialog.
await remoteCall.waitAndClickElement(appId, '.cr-dialog-cancel');
// Enable trash.
await sendTestMessage({name: 'setTrashEnabled', enabled: true});
// Wait for the Trash root to appear again.
await remoteCall.waitForElement(
appId, '#directory-tree [entry-label="Trash"]');
// Navigate to the "Trash" root and ensure the file exists there now.
await navigateWithDirectoryTree(appId, '/Trash');
await remoteCall.waitForElement(appId, '#file-list [file-name="hello.txt"]');
};
/**
* Tests that the `TrashEnabled` preference adds and removes the trash root
* from the directory tree and when navigated on the Trash root, removal
* navigates the user back to My files.
*/
testcase.trashTogglingTrashEnabledNavigatesAwayFromTrashRoot = async () => {
const appId = await setupAndWaitUntilReady(
RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
// Navigate to the Trash root.
await navigateWithDirectoryTree(appId, '/Trash');
// Disable trash.
await sendTestMessage({name: 'setTrashEnabled', enabled: false});
// Wait for the Trash root to disappear.
await remoteCall.waitForElementLost(
appId, '#directory-tree [entry-label="Trash"]');
// Ensure the new root is now at My files.
await remoteCall.waitUntilCurrentDirectoryIsChanged(appId, '/My files');
};
/**
* Verify that files that have their parents trashed show an alert dialog to
* indicate that restoration is not possible.
*/
testcase.trashCantRestoreWhenParentDoesntExist = async () => {
const appId =
await setupAndWaitUntilReady(RootPath.DOWNLOADS, NESTED_ENTRY_SET, []);
// Navigate to the "A" directory.
await navigateWithDirectoryTree(appId, '/My files/Downloads/A');
// Ensure the "B" directory exists within "A".
await remoteCall.waitForFiles(appId, [ENTRIES.directoryB.getExpectedRow()]);
// Select the "B" directory.
await remoteCall.waitAndClickElement(appId, '#file-list [file-name="B"]');
// Delete item and wait for it to be removed (no dialog).
await clickTrashButton(appId);
await remoteCall.waitForElementLost(appId, '#file-list [file-name="B"]');
// Navigate to /My files/Downloads and click the "A" directory.
await navigateWithDirectoryTree(appId, '/My files/Downloads');
await remoteCall.waitAndClickElement(appId, '#file-list [file-name="A"]');
// Delete item and wait for it to be removed (no dialog).
await clickTrashButton(appId);
await remoteCall.waitForElementLost(appId, '#file-list [file-name="A"]');
// Navigate to Trash and click the "B" directory of which the parent "A"
// directory has been removed.
await navigateWithDirectoryTree(appId, '/Trash');
await remoteCall.waitAndClickElement(appId, '#file-list [file-name="B"]');
// Right-click the selected file to validate context menu.
await remoteCall.waitAndRightClick(appId, '.table-row[selected]');
await remoteCall.waitForElement(appId, '#file-context-menu:not([hidden])');
// Restore item and expect the alert dialog is shown as the parent has been
// removed.
await remoteCall.waitAndClickElement(appId, '#restore-from-trash-button');
await remoteCall.waitForElement(appId, '.files-alert-dialog');
};
/**
* Verify that infeasible actions within Trash root are disabled and hidden when
* right clicking a file. Verify that feasible actions are enabled.
*/
testcase.trashInfeasibleActionsForFileDisabledAndHiddenInTrashRoot =
async () => {
const appId =
await setupAndWaitUntilReady(RootPath.DOWNLOADS, [ENTRIES.hello], []);
const fileSelector = '#file-list [file-name="hello.txt"]';
// Select hello.txt and send it to the Trash.
await remoteCall.waitAndClickElement(appId, fileSelector);
await clickTrashButton(appId);
await remoteCall.waitForElementLost(appId, fileSelector);
// Navigate to the Trash root.
await navigateWithDirectoryTree(appId, '/Trash');
// Wait for the element to appear in the Trash and right click it to get
// access to the context menu.
await remoteCall.waitAndRightClick(appId, fileSelector);
const contextMenuSelector = '#file-context-menu:not([hidden])';
await remoteCall.waitForElement(appId, contextMenuSelector);
// Ensure each infeasible action is disabled and hidden.
const infeasibleActions = [
'rename',
'new-folder',
'paste',
'copy',
'zip-selection',
'set-wallpaper',
];
for (const action of infeasibleActions) {
await remoteCall.waitForElement(
appId,
contextMenuSelector + ' [command="#' + action + '"][disabled][hidden]');
}
// Ensure each feasible action not disabled and not hidden.
const feasibleActions = [
'cut',
'get-info',
'delete',
'restore-from-trash',
];
for (const action of feasibleActions) {
await remoteCall.waitForElement(
appId,
contextMenuSelector + ' [command="#' + action +
'"]:not([disabled]):not([hidden])');
}
};
/**
* Verify that infeasible actions within Trash root are disabled and hidden when
* right clicking a folder. Verify that feasible actions are enabled.
*/
testcase.trashInfeasibleActionsForFolderDisabledAndHiddenInTrashRoot =
async () => {
const appId = await setupAndWaitUntilReady(
RootPath.DOWNLOADS, [ENTRIES.hello, ENTRIES.directoryA], []);
// Select and copy hello.txt into the clipboard to check the Paste Into Folder
// action.
await remoteCall.waitUntilSelected(appId, ENTRIES.hello.nameText);
chrome.test.assertTrue(
!!await remoteCall.callRemoteTestUtil('execCommand', appId, ['copy']),
'execCommand failed');
const fileSelector = '#file-list [file-name="A"]';
// Select folder A and send it to the Trash.
await remoteCall.waitAndClickElement(appId, fileSelector);
await clickTrashButton(appId);
await remoteCall.waitForElementLost(appId, fileSelector);
// Navigate to the Trash root.
await navigateWithDirectoryTree(appId, '/Trash');
// Wait for the element to appear in the Trash and right click it to get
// access to the context menu.
await remoteCall.waitAndRightClick(appId, fileSelector);
const contextMenuSelector = '#file-context-menu:not([hidden])';
await remoteCall.waitForElement(appId, contextMenuSelector);
// Ensure each infeasible action is disabled and hidden.
const infeasibleActions = [
'rename',
'new-folder',
'copy',
'zip-selection',
'paste-into-folder',
];
for (const action of infeasibleActions) {
await remoteCall.waitForElement(
appId,
contextMenuSelector + ' [command="#' + action + '"][disabled][hidden]');
}
// Ensure each feasible action not disabled and not hidden.
const feasibleActions = [
'cut',
'get-info',
'delete',
'restore-from-trash',
];
for (const action of feasibleActions) {
await remoteCall.waitForElement(
appId,
contextMenuSelector + ' [command="#' + action +
'"]:not([disabled]):not([hidden])');
}
};
/**
* Verify that Extract All within Trash root is disabled and hidden when right
* clicking a zip file.
*/
testcase.trashExtractAllForZipHiddenAndDisabledInTrashRoot = async () => {
const appId = await setupAndWaitUntilReady(
RootPath.DOWNLOADS, [ENTRIES.zipArchive], []);
const fileSelector = '#file-list [file-name="archive.zip"]';
// Select tera.zip and send it to the Trash.
await remoteCall.waitAndClickElement(appId, fileSelector);
await clickTrashButton(appId);
await remoteCall.waitForElementLost(appId, fileSelector);
// Navigate to the Trash root.
await navigateWithDirectoryTree(appId, '/Trash');
// Wait for the element to appear in the Trash and right click it to get
// access to the context menu.
await remoteCall.waitAndRightClick(appId, fileSelector);
const contextMenuSelector = '#file-context-menu:not([hidden])';
await remoteCall.waitForElement(appId, contextMenuSelector);
// Ensure extract all action is disabled and hidden.
await remoteCall.waitForElement(
appId,
contextMenuSelector + ' [command="#extract-all"][disabled][hidden]');
};
/**
* Verify that infeasible actions within Trash root are disabled and hidden when
* right clicking a blank space. Verify that Cut is disabled but not hidden.
*/
testcase.trashAllActionsDisabledForBlankSpaceInTrashRoot = async () => {
const appId =
await setupAndWaitUntilReady(RootPath.DOWNLOADS, [ENTRIES.hello], []);
// Select and copy hello.txt into the clipboard to check the Paste action.
await remoteCall.waitUntilSelected(appId, ENTRIES.hello.nameText);
chrome.test.assertTrue(
!!await remoteCall.callRemoteTestUtil('execCommand', appId, ['copy']),
'execCommand failed');
// Navigate to the Trash root.
await navigateWithDirectoryTree(appId, '/Trash');
// Click blank space.
await remoteCall.rightClickFileListBlankSpace(appId);
// Ensure the context menu is hidden.
const contextMenuSelector = '#file-context-menu:not([hidden])';
await remoteCall.waitForElement(appId, contextMenuSelector);
// Ensure each infeasible action is disabled and hidden.
const infeasibleActions = [
'paste',
'new-folder',
'copy',
];
for (const action of infeasibleActions) {
await remoteCall.waitForElement(
appId,
contextMenuSelector + ' [command="#' + action + '"][disabled][hidden]');
}
// Ensure Cut is disabled and not hidden.
await remoteCall.waitForElement(
appId, contextMenuSelector + ' [command="#cut"][disabled]:not([hidden])');
};
/**
* Tests that the trash nudge is shown on the first trash but is not shown on
* subsequent trashes.
* NOTE: The nudge has an expiry period, this will override the expiry period.
*/
testcase.trashNudgeShownOnFirstTrashOperation = async () => {
const appId = await setupAndWaitUntilReady(
RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
// Disable the nudge expiry.
await remoteCall.disableNudgeExpiry(appId);
// Select hello.txt and send it to the Trash, this file should not be removed
// in between enabling and disabling the feature.
await remoteCall.waitUntilSelected(appId, 'hello.txt');
await clickTrashButton(appId);
await remoteCall.waitForElementLost(
appId, '#file-list [file-name="hello.txt"]');
// Verify the dot has been placed somewhere visible.
let nudgeDot = await remoteCall.waitForElementStyles(
appId, ['xf-nudge', '#dot'], ['left']);
chrome.test.assertTrue(nudgeDot.renderedLeft > 0);
chrome.test.assertTrue(nudgeDot.renderedTop > 0);
// The nudge is dismissed through keyboard and mouse events which are "faked"
// in the integration test harness. So send a blur event to the anchor to
// ensure the nudge gets removed instead.
chrome.test.assertTrue(await remoteCall.callRemoteTestUtil(
'fakeEvent', appId, ['span[root-type-icon="trash"]', 'blur']));
await repeatUntil(async () => {
const nudgeDot = await remoteCall.waitForElementStyles(
appId, ['xf-nudge', '#dot'], ['left']);
return nudgeDot.renderedLeft < 0;
});
// Select and trash the file "world.ogv".
await remoteCall.waitUntilSelected(appId, 'world.ogv');
await clickTrashButton(appId);
await remoteCall.waitForElementLost(
appId, '#file-list [file-name="world.ogv"]');
// Ensure the nudge doesn't show up again after the first trash.
nudgeDot = await remoteCall.waitForElementStyles(
appId, ['xf-nudge', '#dot'], ['left']);
chrome.test.assertTrue(nudgeDot.renderedLeft < 0);
};
testcase.trashStaleTrashInfoFilesAreRemovedAfterOneHour = async () => {
const appId = await setupAndWaitUntilReady(
RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
const fileSelector = '#file-list [file-name="hello.txt"]';
const trashInfoSelector = '#file-list [file-name="hello.txt.trashinfo"]';
// Select hello.txt and send it to the Trash.
await remoteCall.waitAndClickElement(appId, fileSelector);
await clickTrashButton(appId);
await remoteCall.waitForElementLost(appId, fileSelector);
// Enable hidden files to be shown.
await showHiddenFiles(appId);
// Navigate to /My files/Downloads/.Trash/files.
await navigateWithDirectoryTree(appId, '/My files/Downloads/.Trash/files');
// Select hello.txt.
await remoteCall.waitAndClickElement(appId, fileSelector);
// Delete selected item.
await clickDeleteButtonAndConfirmDeletion(appId);
await remoteCall.waitForElementLost(appId, fileSelector);
// Navigate to /My files/Downloads/.Trash/info and ensure the .trashinfo file
// is still there.
await navigateWithDirectoryTree(appId, '/My files/Downloads/.Trash/info');
await remoteCall.waitForElement(appId, trashInfoSelector);
// Update the modification date for the .trashinfo file.
chrome.test.assertEq(
await sendTestMessage({
name: 'updateModificationDate',
localPath: 'Downloads/.Trash/info/hello.txt.trashinfo',
modificationDate: ((new Date()).getTime() - 120 * 60 * 1000),
}),
'true');
// Navigate to the Trash directory which should kick off the removal of the
// stale .trashinfo file.
await navigateWithDirectoryTree(appId, '/Trash');
await remoteCall.waitForElementLost(appId, fileSelector);
// Navigate back to the .Trash/info directory and ensure the .trashinfo file
// has been removed.
await navigateWithDirectoryTree(appId, '/My files/Downloads/.Trash/info');
await remoteCall.waitForElementLost(appId, trashInfoSelector);
};