Add restore and delete actions in Trash root view
http://go/chrome-ss/8aec11c357971886d745aa9ea5b89356399956d2
Added restore-from-trash command which is shown in the Trash root as
both a top bar action, and a file context menu command.
Modified delete action so that it shows for Trash root (even though the
Trash root FakeEntry is read-only), and added remove() and
removeRecursively() functions to TrashEntry which deletes both the
filesEntry and infoEntry.
Background restore() code previously expected the supplied TrashItem
would come only from the TrashItem returned by removeFileOrDirectory(),
but now that it can be set directly from the restore-from-trash
command, it requires a change to the TrashConfig lookup and path prefix
validation.
Bug: 953310
Change-Id: I2cdf018fd1daab8f57f0ab7f679d47eeba2109b0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2564841
Reviewed-by: Luciano Pacheco <lucmult@chromium.org>
Commit-Queue: Joel Hockey <joelhockey@chromium.org>
Cr-Commit-Position: refs/heads/master@{#832700}
diff --git a/chrome/browser/chromeos/file_manager/file_manager_browsertest.cc b/chrome/browser/chromeos/file_manager/file_manager_browsertest.cc
index 341589a5..3259daa 100644
--- a/chrome/browser/chromeos/file_manager/file_manager_browsertest.cc
+++ b/chrome/browser/chromeos/file_manager/file_manager_browsertest.cc
@@ -1091,6 +1091,8 @@
Trash, /* trash.js */
FilesAppBrowserTest,
::testing::Values(TestCase("trashMoveToTrash").EnableTrash(),
- TestCase("trashRestore").EnableTrash()));
+ TestCase("trashRestoreFromToast").EnableTrash(),
+ TestCase("trashRestoreFromTrash").EnableTrash(),
+ TestCase("trashDeleteFromTrash").EnableTrash()));
} // namespace file_manager
diff --git a/chrome/browser/chromeos/file_manager/file_manager_string_util.cc b/chrome/browser/chromeos/file_manager/file_manager_string_util.cc
index 1f37f159..30ea6bde 100644
--- a/chrome/browser/chromeos/file_manager/file_manager_string_util.cc
+++ b/chrome/browser/chromeos/file_manager/file_manager_string_util.cc
@@ -823,6 +823,8 @@
IDS_FILE_BROWSER_REPARTITION_DIALOG_CONFIRM_LABEL);
SET_STRING("REPARTITION_DIALOG_MESSAGE",
IDS_FILE_BROWSER_REPARTITION_DIALOG_MESSAGE);
+ SET_STRING("RESTORE_FROM_TRASH_BUTTON_LABEL",
+ IDS_FILE_BROWSER_RESTORE_FROM_TRASH_BUTTON_LABEL);
SET_STRING("UNPIN_FOLDER_BUTTON_LABEL",
IDS_FILE_BROWSER_UNPIN_FOLDER_BUTTON_LABEL);
SET_STRING("RENAME_BUTTON_LABEL", IDS_FILE_BROWSER_RENAME_BUTTON_LABEL);
diff --git a/ui/chromeos/file_manager_strings.grdp b/ui/chromeos/file_manager_strings.grdp
index e13010e..39d0677 100644
--- a/ui/chromeos/file_manager_strings.grdp
+++ b/ui/chromeos/file_manager_strings.grdp
@@ -544,6 +544,9 @@
<message name="IDS_FILE_BROWSER_DELETE_BUTTON_LABEL" desc="Context menu item that deletes the currently-selected file(s).">
Delete
</message>
+ <message name="IDS_FILE_BROWSER_RESTORE_FROM_TRASH_BUTTON_LABEL" desc="Context menu item that restores the currently-selected file(s) from the trash / recycle bin.">
+ Restore from trash
+ </message>
<message name="IDS_FILE_BROWSER_PASTE_BUTTON_LABEL" desc="Context menu item that pastes the file(s) in the clipboard.">
Paste
</message>
diff --git a/ui/chromeos/file_manager_strings_grdp/IDS_FILE_BROWSER_RESTORE_FROM_TRASH_BUTTON_LABEL.png.sha1 b/ui/chromeos/file_manager_strings_grdp/IDS_FILE_BROWSER_RESTORE_FROM_TRASH_BUTTON_LABEL.png.sha1
new file mode 100644
index 0000000..1bc894f
--- /dev/null
+++ b/ui/chromeos/file_manager_strings_grdp/IDS_FILE_BROWSER_RESTORE_FROM_TRASH_BUTTON_LABEL.png.sha1
@@ -0,0 +1 @@
+8aec11c357971886d745aa9ea5b89356399956d2
\ No newline at end of file
diff --git a/ui/file_manager/file_manager/background/js/file_operation_manager.js b/ui/file_manager/file_manager/background/js/file_operation_manager.js
index 2d6d5b6..ca3d692 100644
--- a/ui/file_manager/file_manager/background/js/file_operation_manager.js
+++ b/ui/file_manager/file_manager/background/js/file_operation_manager.js
@@ -531,14 +531,22 @@
/**
* Restores files from trash.
*
- * @param {Array<!fileOperationUtil.TrashEntry>} trashEntries The trash items.
+ * @param {Array<!fileOperationUtil.TrashEntry>} trashEntries The trash
+ * entries.
*/
restoreDeleted(trashEntries) {
- const volumeManager = assert(this.volumeManager_);
+ if (!this.volumeManager_) {
+ volumeManagerFactory.getInstance().then(volumeManager => {
+ this.volumeManager_ = volumeManager;
+ this.restoreDeleted(trashEntries);
+ });
+ return;
+ }
+
while (trashEntries.length) {
this.trash_
.restore(
- volumeManager,
+ assert(this.volumeManager_),
/** @type {!TrashEntry} */ (trashEntries.pop()))
.catch(e => console.error('Error restoring deleted file', e));
}
diff --git a/ui/file_manager/file_manager/background/js/trash.js b/ui/file_manager/file_manager/background/js/trash.js
index 592df98..9a86427 100644
--- a/ui/file_manager/file_manager/background/js/trash.js
+++ b/ui/file_manager/file_manager/background/js/trash.js
@@ -32,6 +32,34 @@
}
/**
+ * Get the TrashConfig for the trash that this entry would be placed in, if
+ * any. Initializes TrashConfig with pathPrefix if required.
+ *
+ * @param {!VolumeManager} volumeManager
+ * @param {!Entry} entry The entry to find a matching TrashConfig for.
+ * @return {?TrashConfig} TrashConfig for entry or null.
+ * @private
+ */
+ getConfig_(volumeManager, entry) {
+ const info = volumeManager.getLocationInfo(entry);
+ if (!loadTimeData.getBoolean('FILES_TRASH_ENABLED') || !info) {
+ return null;
+ }
+ const fullPathSlash = entry.fullPath + '/';
+ for (const config of TrashConfig.CONFIG) {
+ const entryInVolume = fullPathSlash.startsWith(config.topDir);
+ if (config.volumeType === info.volumeInfo.volumeType && entryInVolume) {
+ if (config.prefixPathWithRemoteMount &&
+ info.volumeInfo.remoteMountPath) {
+ config.pathPrefix = info.volumeInfo.remoteMountPath;
+ }
+ return config;
+ }
+ }
+ return null;
+ }
+
+ /**
* Only move to trash if feature is on, and entry is in one of the supported
* volumes, but not already in the trash.
*
@@ -41,19 +69,12 @@
* else null if item should be permanently deleted.
*/
shouldMoveToTrash(volumeManager, entry) {
- const info = volumeManager.getLocationInfo(entry);
- if (!loadTimeData.getBoolean('FILES_TRASH_ENABLED') || !info) {
- return null;
- }
- const fullPathSlash = entry.fullPath + '/';
- for (const config of TrashConfig.CONFIG) {
- const entryInVolume = fullPathSlash.startsWith(config.topDir);
- if (config.volumeType === info.volumeInfo.volumeType && entryInVolume) {
- if (config.prefixPathWithRemoteMount) {
- config.pathPrefix = info.volumeInfo.remoteMountPath;
- }
- const entryInTrash = fullPathSlash.startsWith(config.trashDir);
- return entryInTrash ? null : config;
+ const config = this.getConfig_(volumeManager, entry);
+ if (config) {
+ const fullPathSlash = entry.fullPath + '/';
+ const entryInTrash = fullPathSlash.startsWith(config.trashDir);
+ if (!entryInTrash) {
+ return config;
}
}
return null;
@@ -200,8 +221,7 @@
const infoEntry = await this.writeTrashInfoFile_(
trashDirs.info, trashInfoName, path, deletionDate);
const filesEntry = await this.moveTo_(entry, trashDirs.files, name);
- return new TrashEntry(
- entry.name, deletionDate, filesEntry, infoEntry, config.pathPrefix);
+ return new TrashEntry(entry.name, deletionDate, filesEntry, infoEntry);
}
/**
@@ -212,20 +232,25 @@
* @return {Promise<void>} Promise which resolves when file is restored.
*/
async restore(volumeManager, trashEntry) {
+ const infoEntry = trashEntry.infoEntry;
+ const config = this.getConfig_(volumeManager, infoEntry);
+ if (!config) {
+ throw new Error(`No TrashConfig for ${infoEntry.toURL()}`);
+ }
+
// Read Path from info entry.
- const file = await new Promise(
- (resolve, reject) => trashEntry.infoEntry.file(resolve, reject));
+ const file =
+ await new Promise((resolve, reject) => infoEntry.file(resolve, reject));
const text = await file.text();
const path = TrashEntry.parsePath(text);
if (!path) {
- throw new Error(`No Path found to restore in ${
- trashEntry.infoEntry.fullPath}, text=${text}`);
- } else if (!path.startsWith(trashEntry.pathPrefix)) {
+ throw new Error(
+ `No Path found to restore in ${infoEntry.fullPath}, text=${text}`);
+ } else if (!path.startsWith(config.pathPrefix)) {
throw new Error(`Path does not match expected prefix in ${
- trashEntry.infoEntry.fullPath}, prefix=${
- trashEntry.pathPrefix}, text=${text}`);
+ infoEntry.fullPath}, prefix=${config.pathPrefix}, text=${text}`);
}
- const pathNoLeadingSlash = path.substring(trashEntry.pathPrefix.length + 1);
+ const pathNoLeadingSlash = path.substring(config.pathPrefix.length + 1);
const parts = pathNoLeadingSlash.split('/');
// Move to last directory in path, making sure dirs are created if needed.
@@ -241,7 +266,7 @@
const name =
await fileOperationUtil.deduplicatePath(dir, parts[parts.length - 1]);
await this.moveTo_(trashEntry.filesEntry, dir, name);
- await this.permanentlyDeleteFileOrDirectory_(trashEntry.infoEntry);
+ await this.permanentlyDeleteFileOrDirectory_(infoEntry);
}
/**
diff --git a/ui/file_manager/file_manager/common/js/trash.js b/ui/file_manager/file_manager/common/js/trash.js
index 13dc480..fcfed09 100644
--- a/ui/file_manager/file_manager/common/js/trash.js
+++ b/ui/file_manager/file_manager/common/js/trash.js
@@ -123,14 +123,11 @@
* @param {!Date} deletionDate DeletionDate of deleted file from infoEntry.
* @param {!Entry} filesEntry trash files entry.
* @param {!FileEntry} infoEntry trash info entry.
- * @param {string=} pathPrefix Optional prefix for 'Path=' in *.trashinfo. For
- * crostini, this is the user's homedir (/home/<username>).
*/
- constructor(name, deletionDate, filesEntry, infoEntry, pathPrefix = '') {
+ constructor(name, deletionDate, filesEntry, infoEntry) {
this.name = name;
this.filesEntry = filesEntry;
this.infoEntry = infoEntry;
- this.pathPrefix = pathPrefix;
/** @private */
this.deletionDate_ = deletionDate;
@@ -147,9 +144,6 @@
/** @override Entry */
this.isFile = filesEntry.isFile;
- /** @override FileEntry */
- this.file = filesEntry.file;
-
/** @override FilesAppEntry */
this.rootType = VolumeManagerCommon.RootType.TRASH;
@@ -157,9 +151,14 @@
this.type_name = 'TrashEntry';
}
- /** @override Entry */
+ /**
+ * Use filesEntry toURL() so this entry can be used as that file to view,
+ * copy, etc.
+ *
+ * @override Entry
+ */
toURL() {
- return 'trash://' + this.infoEntry.toURL();
+ return this.filesEntry.toURL();
}
/**
@@ -179,14 +178,83 @@
return null;
}
- /** @override FilesAppEntry */
+ /**
+ * Remove filesEntry first, then remove infoEntry. Overrides Entry.
+ *
+ * @param {function()} success
+ * @param {function(!FileError)=} error
+ */
+ remove(success, error) {
+ this.filesEntry.remove(() => this.infoEntry.remove(success, error), error);
+ }
+
+ /**
+ * Pass through to filesEntry. Overrides FileEntry.
+ *
+ * @param {function(!File)} success
+ * @param {function(!FileError)=} error
+ */
+ file(success, error) {
+ this.filesEntry.file(success, error);
+ }
+
+ /**
+ * Pass through to filesEntry. Overrides DirectoryEntry.
+ *
+ * @return {!DirectoryReader}
+ */
+ createReader() {
+ return this.filesEntry.createReader();
+ }
+
+ /**
+ * Pass through to filesEntry. Overrides DirectoryEntry.
+ *
+ * @param {string} path
+ * @param {!Object} options
+ * @param {function(!File)} success
+ * @param {function(!FileError)=} error
+ */
+ getDirectory(path, options, success, error) {
+ this.filesEntry.createReader(path, options, success, error);
+ }
+
+ /**
+ * Pass through to filesEntry. Overrides DirectoryEntry.
+ *
+ * @param {string} path
+ * @param {!Object} options
+ * @param {function(!File)} success
+ * @param {function(!FileError)=} error
+ */
+ getFile(path, options, success, error) {
+ this.filesEntry.getFile(path, options, success, error);
+ }
+
+ /**
+ * Remove filesEntry first, then remove infoEntry. Overrides DirectoryEntry.
+ *
+ * @param {function()} success
+ * @param {function(!FileError)=} error
+ */
+ removeRecursively(success, error) {
+ this.filesEntry.removeRecursively(
+ () => this.infoEntry.remove(success, error), error);
+ }
+
+ /**
+ * We must set entry.isNativeType to true, so that this is not considered a
+ * FakeEntry, and we are allowed to delete the item.
+ *
+ * @override FilesAppEntry
+ */
get isNativeType() {
- return false;
+ return true;
}
/** @override FilesAppEntry */
getNativeEntry() {
- return null;
+ return this.filesEntry;
}
/**
diff --git a/ui/file_manager/file_manager/foreground/css/file_manager.css b/ui/file_manager/file_manager/foreground/css/file_manager.css
index 33f537e..c4ef51b4 100644
--- a/ui/file_manager/file_manager/foreground/css/file_manager.css
+++ b/ui/file_manager/file_manager/foreground/css/file_manager.css
@@ -784,6 +784,10 @@
background-image: url(../images/files/ui/delete.svg);
}
+.dialog-header #restore-from-trash-button > .icon {
+ -webkit-mask-image: url(../images/files/ui/restore.svg);
+}
+
.dialog-header.files-ng #refresh-button > .icon {
-webkit-mask-image: url(../images/files/ui/refresh.svg);
}
diff --git a/ui/file_manager/file_manager/foreground/images/files/ui/restore.svg b/ui/file_manager/file_manager/foreground/images/files/ui/restore.svg
new file mode 100644
index 0000000..96bfddd
--- /dev/null
+++ b/ui/file_manager/file_manager/foreground/images/files/ui/restore.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M13 3a9 9 0 00-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42A8.954 8.954 0 0013 21a9 9 0 000-18zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z"/></svg>
diff --git a/ui/file_manager/file_manager/foreground/js/directory_model.js b/ui/file_manager/file_manager/foreground/js/directory_model.js
index a18b7ea7..097e9f446 100644
--- a/ui/file_manager/file_manager/foreground/js/directory_model.js
+++ b/ui/file_manager/file_manager/foreground/js/directory_model.js
@@ -177,6 +177,20 @@
}
/**
+ * @return {boolean} True if entries in the current directory can be deleted.
+ * Similar to !isReadOnly() except that we allow items in the read-only
+ * Trash root to be deleted. If there is no entry set, then returns false.
+ */
+ canDeleteEntries() {
+ const currentDirEntry = this.getCurrentDirEntry();
+ if (currentDirEntry &&
+ currentDirEntry.rootType === VolumeManagerCommon.RootType.TRASH) {
+ return true;
+ }
+ return !this.isReadOnly();
+ }
+
+ /**
* @return {boolean} True if the a scan is active.
*/
isScanning() {
diff --git a/ui/file_manager/file_manager/foreground/js/file_manager_commands.js b/ui/file_manager/file_manager/foreground/js/file_manager_commands.js
index 760675d..bf048d42 100644
--- a/ui/file_manager/file_manager/foreground/js/file_manager_commands.js
+++ b/ui/file_manager/file_manager/foreground/js/file_manager_commands.js
@@ -1146,7 +1146,7 @@
canDeleteEntries_(entries, fileManager) {
return entries.length > 0 &&
!this.containsReadOnlyEntry_(entries, fileManager) &&
- !fileManager.directoryModel.isReadOnly() &&
+ fileManager.directoryModel.canDeleteEntries() &&
CommandUtil.hasCapability(fileManager, entries, 'canDelete');
}
@@ -1216,6 +1216,33 @@
};
/**
+ * Restores selected files from trash.
+ *
+ * @suppress {invalidCasts} See FilesAppEntry in files_app_entry_interfaces.js
+ * for explanation of why FilesAppEntry cannot extend Entry.
+ */
+CommandHandler.COMMANDS_['restore-from-trash'] = new class extends Command {
+ execute(event, fileManager) {
+ const entries = CommandUtil.getCommandEntries(fileManager, event.target);
+ fileManager.fileOperationManager.restoreDeleted(entries.map(e => {
+ return /** @type {!TrashEntry} */ (e);
+ }));
+ fileManager.directoryModel.rescanSoon(/*refresh=*/ false);
+ }
+
+ /** @override */
+ canExecute(event, fileManager) {
+ const entries = CommandUtil.getCommandEntries(fileManager, event.target);
+
+ const enabled = entries.length > 0 && entries.every(e => {
+ return e.rootType && e.rootType === VolumeManagerCommon.RootType.TRASH;
+ });
+ event.canExecute = enabled;
+ event.command.setHidden(!enabled);
+ }
+};
+
+/**
* Pastes files from clipboard.
*/
CommandHandler.COMMANDS_['paste'] = new class extends Command {
diff --git a/ui/file_manager/file_manager/foreground/js/toolbar_controller.js b/ui/file_manager/file_manager/foreground/js/toolbar_controller.js
index b0be5de..fd299a2 100644
--- a/ui/file_manager/file_manager/foreground/js/toolbar_controller.js
+++ b/ui/file_manager/file_manager/foreground/js/toolbar_controller.js
@@ -60,6 +60,13 @@
* @private {!HTMLElement}
* @const
*/
+ this.restoreFromTrashButton_ =
+ queryRequiredElement('#restore-from-trash-button', this.toolbar_);
+
+ /**
+ * @private {!HTMLElement}
+ * @const
+ */
this.readOnlyIndicator_ =
queryRequiredElement('#read-only-indicator', this.toolbar_);
@@ -89,6 +96,15 @@
* @private {!cr.ui.Command}
* @const
*/
+ this.restoreFromTrashCommand_ = assertInstanceof(
+ queryRequiredElement(
+ '#restore-from-trash', assert(this.toolbar_.ownerDocument.body)),
+ cr.ui.Command);
+
+ /**
+ * @private {!cr.ui.Command}
+ * @const
+ */
this.refreshCommand_ = assertInstanceof(
queryRequiredElement(
'#refresh', assert(this.toolbar_.ownerDocument.body)),
@@ -183,6 +199,9 @@
this.deleteButton_.addEventListener(
'click', this.onDeleteButtonClicked_.bind(this));
+ this.restoreFromTrashButton_.addEventListener(
+ 'click', this.onRestoreFromTrashButtonClicked_.bind(this));
+
if (util.isFilesNg()) {
this.togglePinnedCommand_.addEventListener(
'checkedChange', this.updatePinnedToggle_.bind(this));
@@ -280,11 +299,17 @@
// Update visibility of the delete button.
this.deleteButton_.hidden =
- (selection.totalCount === 0 || this.directoryModel_.isReadOnly() ||
+ (selection.totalCount === 0 ||
+ !this.directoryModel_.canDeleteEntries() ||
selection.hasReadOnlyEntry() ||
selection.entries.some(
entry => util.isNonModifiable(this.volumeManager_, entry)));
+ // Update visibility of the restore-from-trash button.
+ this.restoreFromTrashButton_.hidden = (selection.totalCount == 0) ||
+ this.directoryModel_.getCurrentRootType() !==
+ VolumeManagerCommon.RootType.TRASH;
+
if (util.isFilesNg()) {
this.togglePinnedCommand_.canExecuteChange(
this.listContainer_.currentList);
@@ -330,6 +355,17 @@
}
/**
+ * Handles click event for restore from trash button to execute the restore
+ * command.
+ * @private
+ */
+ onRestoreFromTrashButtonClicked_() {
+ this.restoreFromTrashCommand_.canExecuteChange(
+ this.listContainer_.currentList);
+ this.restoreFromTrashCommand_.execute(this.listContainer_.currentList);
+ }
+
+ /**
* Handles the relayout event occurred on the navigation list.
* @private
*/
diff --git a/ui/file_manager/file_manager/main.html b/ui/file_manager/file_manager/main.html
index 525f133a..57f73c5 100644
--- a/ui/file_manager/file_manager/main.html
+++ b/ui/file_manager/file_manager/main.html
@@ -76,6 +76,7 @@
<command id="rename" label="$i18n{RENAME_BUTTON_LABEL}"
shortcut="Enter|Ctrl">
<command id="delete" shortcut="Backspace|Alt Delete">
+ <command id="restore-from-trash" label="$i18n{RESTORE_FROM_TRASH_BUTTON_LABEL}">
<command id="refresh" label="$i18n{REFRESH_BUTTON_LABEL}"
shortcut="b|Ctrl" hidden>
<command id="pin-folder"
@@ -205,6 +206,7 @@
<cr-menu-item command="#rename"></cr-menu-item>
<cr-menu-item command="#pin-folder"></cr-menu-item>
<cr-menu-item command="#delete" class="hide-on-toolbar">$i18n{DELETE_BUTTON_LABEL}</cr-menu-item>
+ <cr-menu-item command="#restore-from-trash"></cr-menu-item>
<cr-menu-item command="#zip-selection"></cr-menu-item>
<cr-menu-item command="#toggle-holding-space" visibleif="full-page">
</cr-menu-item>
@@ -399,6 +401,13 @@
<files-ripple></files-ripple>
<div class="icon"></div>
</cr-button>
+ <cr-button id="restore-from-trash-button" class="icon-button menu-button" tabindex="0" hidden
+ aria-label="$i18n{RESTORE_FROM_TRASH_BUTTON_LABEL}"
+ visibleif="full-page"
+ has-tooltip>
+ <files-ripple></files-ripple>
+ <div class="icon"></div>
+ </cr-button>
<div id="search-wrapper">
<cr-button id="search-button" class="icon-button menu-button" tabindex="0"
aria-label="$i18n{SEARCH_TEXT_LABEL}"
diff --git a/ui/file_manager/integration_tests/file_manager/trash.js b/ui/file_manager/integration_tests/file_manager/trash.js
index 374412a..472b686 100644
--- a/ui/file_manager/integration_tests/file_manager/trash.js
+++ b/ui/file_manager/integration_tests/file_manager/trash.js
@@ -21,11 +21,6 @@
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.waitForElement(
- appId, '#file-list [file-name="My files › Downloads › hello.txt"]');
-
// Open the gear menu by clicking the gear button.
chrome.test.assertTrue(await remoteCall.callRemoteTestUtil(
'fakeMouseClick', appId, ['#gear-button']));
@@ -101,7 +96,7 @@
/**
* Delete files then restore via toast 'Undo'.
*/
-testcase.trashRestore = async () => {
+testcase.trashRestoreFromToast = async () => {
const appId = await setupAndWaitUntilReady(
RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
@@ -121,3 +116,67 @@
// 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 remoteCall.waitAndClickElement(appId, '#delete-button');
+ 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="My files › Downloads › hello.txt"]');
+
+ // Restore item.
+ await remoteCall.waitAndClickElement(appId, '#restore-from-trash-button');
+
+ // Wait for completion of file restore.
+ await remoteCall.waitForElementLost(
+ appId, '#file-list [file-name="My files › Downloads › 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 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 remoteCall.waitAndClickElement(appId, '#delete-button');
+ 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="My files › Downloads › hello.txt"]');
+
+ // Delete item and confirm delete (dialog shown).
+ await remoteCall.waitAndClickElement(appId, '#delete-button');
+ await remoteCall.waitAndClickElement(
+ appId, '.files-confirm-dialog .cr-dialog-ok');
+
+ // Wait for completion of file deletion.
+ await remoteCall.waitForElementLost(
+ appId, '#file-list [file-name="My files › Downloads › hello.txt"]');
+};