Commit dee2de1f authored by Joel Hockey's avatar Joel Hockey Committed by Commit Bot

Crostini: remount persisted shared folders at startup

Note that this feature is being added to read any persisted shares,
however support to write shares as persisted will not be added
until the management UI where shares can be removed is completed.

* prefs: crostini.shared_paths will be a list of filesystem path strings.
* New function CrostiniSharePath::GetSharedPaths() reads from prefs.
* New fileManagerPrivate.getCrostiniSharedPaths executes
  callback to receive list of Entry.
* FileManager.setupCrostini_ loads persisted shares at startup.
* FileManagerApiTest.Crostini verifies reading from prefs and correct
  conversion of filesystem paths to FileEntry.
* New integration test for fileManagerPrivate.getCrostiniSharedPaths().


Bug: 878324
Cq-Include-Trybots: luci.chromium.try:closure_compilation
Change-Id: I5c8ee8193eb2fda94e5505d79b449797fbab9449
Reviewed-on: https://chromium-review.googlesource.com/1220411Reviewed-by: default avatarBen Wells <benwells@chromium.org>
Reviewed-by: default avatarLuciano Pacheco <lucmult@chromium.org>
Reviewed-by: default avatarNicholas Verne <nverne@chromium.org>
Commit-Queue: Joel Hockey <joelhockey@chromium.org>
Cr-Commit-Position: refs/heads/master@{#592208}
parent 18126ed5
...@@ -12,16 +12,19 @@ namespace prefs { ...@@ -12,16 +12,19 @@ namespace prefs {
// A boolean preference representing whether a user has opted in to use // A boolean preference representing whether a user has opted in to use
// Crostini (Called "Linux Apps" in UI). // Crostini (Called "Linux Apps" in UI).
const char kCrostiniEnabled[] = "crostini.enabled"; const char kCrostiniEnabled[] = "crostini.enabled";
const char kCrostiniRegistry[] = "crostini.registry";
const char kCrostiniMimeTypes[] = "crostini.mime_types"; const char kCrostiniMimeTypes[] = "crostini.mime_types";
const char kCrostiniRegistry[] = "crostini.registry";
// List of filesystem paths that are shared with the crostini container.
const char kCrostiniSharedPaths[] = "crostini.shared_paths";
// A boolean preference representing a user level enterprise policy to enable // A boolean preference representing a user level enterprise policy to enable
// Crostini use. // Crostini use.
const char kUserCrostiniAllowedByPolicy[] = "crostini.user_allowed_by_policy"; const char kUserCrostiniAllowedByPolicy[] = "crostini.user_allowed_by_policy";
void RegisterProfilePrefs(PrefRegistrySimple* registry) { void RegisterProfilePrefs(PrefRegistrySimple* registry) {
registry->RegisterBooleanPref(kCrostiniEnabled, false); registry->RegisterBooleanPref(kCrostiniEnabled, false);
registry->RegisterDictionaryPref(kCrostiniRegistry);
registry->RegisterDictionaryPref(kCrostiniMimeTypes); registry->RegisterDictionaryPref(kCrostiniMimeTypes);
registry->RegisterDictionaryPref(kCrostiniRegistry);
registry->RegisterListPref(kCrostiniSharedPaths);
registry->RegisterBooleanPref(kUserCrostiniAllowedByPolicy, true); registry->RegisterBooleanPref(kUserCrostiniAllowedByPolicy, true);
} }
......
...@@ -11,8 +11,9 @@ namespace crostini { ...@@ -11,8 +11,9 @@ namespace crostini {
namespace prefs { namespace prefs {
extern const char kCrostiniEnabled[]; extern const char kCrostiniEnabled[];
extern const char kCrostiniRegistry[];
extern const char kCrostiniMimeTypes[]; extern const char kCrostiniMimeTypes[];
extern const char kCrostiniRegistry[];
extern const char kCrostiniSharedPaths[];
extern const char kUserCrostiniAllowedByPolicy[]; extern const char kUserCrostiniAllowedByPolicy[];
void RegisterProfilePrefs(PrefRegistrySimple* registry); void RegisterProfilePrefs(PrefRegistrySimple* registry);
......
...@@ -7,11 +7,13 @@ ...@@ -7,11 +7,13 @@
#include "base/bind.h" #include "base/bind.h"
#include "base/optional.h" #include "base/optional.h"
#include "chrome/browser/chromeos/crostini/crostini_manager.h" #include "chrome/browser/chromeos/crostini/crostini_manager.h"
#include "chrome/browser/chromeos/crostini/crostini_pref_names.h"
#include "chrome/browser/chromeos/crostini/crostini_util.h" #include "chrome/browser/chromeos/crostini/crostini_util.h"
#include "chrome/browser/profiles/profile.h" #include "chrome/browser/profiles/profile.h"
#include "chromeos/dbus/concierge/service.pb.h" #include "chromeos/dbus/concierge/service.pb.h"
#include "chromeos/dbus/dbus_thread_manager.h" #include "chromeos/dbus/dbus_thread_manager.h"
#include "chromeos/dbus/seneschal_client.h" #include "chromeos/dbus/seneschal_client.h"
#include "components/prefs/pref_service.h"
namespace crostini { namespace crostini {
...@@ -28,6 +30,7 @@ void CrostiniSharePath::SharePath( ...@@ -28,6 +30,7 @@ void CrostiniSharePath::SharePath(
std::string vm_name, std::string vm_name,
std::string path, std::string path,
base::OnceCallback<void(bool, std::string)> callback) { base::OnceCallback<void(bool, std::string)> callback) {
// TODO(joelhockey): Save new path into prefs once management UI is ready.
base::Optional<vm_tools::concierge::VmInfo> vm_info = base::Optional<vm_tools::concierge::VmInfo> vm_info =
crostini::CrostiniManager::GetForProfile(profile)->GetVmInfo( crostini::CrostiniManager::GetForProfile(profile)->GetVmInfo(
std::move(vm_name)); std::move(vm_name));
...@@ -61,4 +64,14 @@ void CrostiniSharePath::OnSharePathResponse( ...@@ -61,4 +64,14 @@ void CrostiniSharePath::OnSharePathResponse(
response.value().failure_reason()); response.value().failure_reason());
} }
std::vector<std::string> CrostiniSharePath::GetSharedPaths(Profile* profile) {
std::vector<std::string> result;
const base::ListValue* shared_paths =
profile->GetPrefs()->GetList(prefs::kCrostiniSharedPaths);
for (const auto& path : *shared_paths) {
result.emplace_back(path.GetString());
}
return result;
}
} // namespace crostini } // namespace crostini
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
#ifndef CHROME_BROWSER_CHROMEOS_CROSTINI_CROSTINI_SHARE_PATH_H_ #ifndef CHROME_BROWSER_CHROMEOS_CROSTINI_CROSTINI_SHARE_PATH_H_
#define CHROME_BROWSER_CHROMEOS_CROSTINI_CROSTINI_SHARE_PATH_H_ #define CHROME_BROWSER_CHROMEOS_CROSTINI_CROSTINI_SHARE_PATH_H_
#include <vector>
#include "base/macros.h" #include "base/macros.h"
#include "base/memory/singleton.h" #include "base/memory/singleton.h"
#include "base/memory/weak_ptr.h" #include "base/memory/weak_ptr.h"
...@@ -20,6 +22,7 @@ class CrostiniSharePath { ...@@ -20,6 +22,7 @@ class CrostiniSharePath {
// Returns the singleton instance of CrostiniSharePath. // Returns the singleton instance of CrostiniSharePath.
static CrostiniSharePath* GetInstance(); static CrostiniSharePath* GetInstance();
// Share specified path with vm.
void SharePath(Profile* profile, void SharePath(Profile* profile,
std::string vm_name, std::string vm_name,
std::string path, std::string path,
...@@ -29,6 +32,9 @@ class CrostiniSharePath { ...@@ -29,6 +32,9 @@ class CrostiniSharePath {
base::OnceCallback<void(bool, std::string)> callback, base::OnceCallback<void(bool, std::string)> callback,
base::Optional<vm_tools::seneschal::SharePathResponse> response) const; base::Optional<vm_tools::seneschal::SharePathResponse> response) const;
// Get list of all shared paths for the default crostini container.
std::vector<std::string> GetSharedPaths(Profile* profile);
private: private:
friend struct base::DefaultSingletonTraits<CrostiniSharePath>; friend struct base::DefaultSingletonTraits<CrostiniSharePath>;
......
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
#include "chrome/common/chrome_features.h" #include "chrome/common/chrome_features.h"
#include "chrome/common/extensions/api/file_system_provider_capabilities/file_system_provider_capabilities_handler.h" #include "chrome/common/extensions/api/file_system_provider_capabilities/file_system_provider_capabilities_handler.h"
#include "chrome/test/base/testing_profile.h" #include "chrome/test/base/testing_profile.h"
#include "chromeos/dbus/concierge/service.pb.h"
#include "chromeos/dbus/cros_disks_client.h" #include "chromeos/dbus/cros_disks_client.h"
#include "chromeos/disks/disk.h" #include "chromeos/disks/disk.h"
#include "chromeos/disks/mock_disk_mount_manager.h" #include "chromeos/disks/mock_disk_mount_manager.h"
...@@ -560,6 +561,17 @@ IN_PROC_BROWSER_TEST_F(FileManagerPrivateApiTest, Crostini) { ...@@ -560,6 +561,17 @@ IN_PROC_BROWSER_TEST_F(FileManagerPrivateApiTest, Crostini) {
&downloads)); &downloads));
ASSERT_TRUE(base::CreateDirectory(downloads.AppendASCII("share_dir"))); ASSERT_TRUE(base::CreateDirectory(downloads.AppendASCII("share_dir")));
// Setup prefs crostini.shared_paths.
base::FilePath shared1 = downloads.AppendASCII("shared1");
base::FilePath shared2 = downloads.AppendASCII("shared2");
ASSERT_TRUE(base::CreateDirectory(shared1));
ASSERT_TRUE(base::CreateDirectory(shared2));
base::ListValue shared_paths;
shared_paths.AppendString(shared1.value());
shared_paths.AppendString(shared2.value());
browser()->profile()->GetPrefs()->Set(crostini::prefs::kCrostiniSharedPaths,
shared_paths);
ASSERT_TRUE(RunComponentExtensionTest("file_browser/crostini_test")); ASSERT_TRUE(RunComponentExtensionTest("file_browser/crostini_test"));
} }
......
...@@ -719,6 +719,45 @@ void FileManagerPrivateInternalSharePathWithCrostiniFunction::SharePathCallback( ...@@ -719,6 +719,45 @@ void FileManagerPrivateInternalSharePathWithCrostiniFunction::SharePathCallback(
Respond(success ? NoArguments() : Error(failure_reason)); Respond(success ? NoArguments() : Error(failure_reason));
} }
ExtensionFunction::ResponseAction
FileManagerPrivateInternalGetCrostiniSharedPathsFunction::Run() {
Profile* profile = Profile::FromBrowserContext(browser_context());
file_manager::util::FileDefinitionList file_definition_list;
auto shared_paths =
crostini::CrostiniSharePath::GetInstance()->GetSharedPaths(profile);
for (const std::string& path : shared_paths) {
file_manager::util::FileDefinition file_definition;
// All shared paths should be directories. Even if this is not true, it
// is fine for foreground/js/crostini.js class to think so.
// We verify that the paths are in fact valid directories before calling
// seneschal/9p.
file_definition.is_directory = true;
if (file_manager::util::ConvertAbsoluteFilePathToRelativeFileSystemPath(
profile, extension_id(), base::FilePath(path),
&file_definition.virtual_path)) {
file_definition_list.emplace_back(std::move(file_definition));
}
}
file_manager::util::ConvertFileDefinitionListToEntryDefinitionList(
profile, extension_id(),
file_definition_list, // Safe, since copied internally.
base::Bind(&FileManagerPrivateInternalGetCrostiniSharedPathsFunction::
OnConvertFileDefinitionListToEntryDefinitionList,
this));
return RespondLater();
}
void FileManagerPrivateInternalGetCrostiniSharedPathsFunction::
OnConvertFileDefinitionListToEntryDefinitionList(
std::unique_ptr<file_manager::util::EntryDefinitionList>
entry_definition_list) {
DCHECK(entry_definition_list);
Respond(OneArgument(file_manager::util::ConvertEntryDefinitionListToListValue(
*entry_definition_list)));
}
ExtensionFunction::ResponseAction ExtensionFunction::ResponseAction
FileManagerPrivateInternalInstallLinuxPackageFunction::Run() { FileManagerPrivateInternalInstallLinuxPackageFunction::Run() {
using extensions::api::file_manager_private_internal::InstallLinuxPackage:: using extensions::api::file_manager_private_internal::InstallLinuxPackage::
...@@ -909,16 +948,13 @@ void FileManagerPrivateInternalGetRecentFilesFunction::OnGetRecentFiles( ...@@ -909,16 +948,13 @@ void FileManagerPrivateInternalGetRecentFilesFunction::OnGetRecentFiles(
continue; continue;
file_manager::util::FileDefinition file_definition; file_manager::util::FileDefinition file_definition;
const bool result =
file_manager::util::ConvertAbsoluteFilePathToRelativeFileSystemPath(
chrome_details_.GetProfile(), extension_id(), file.url().path(),
&file_definition.virtual_path);
if (!result)
continue;
// Recent file system only lists regular files, not directories. // Recent file system only lists regular files, not directories.
file_definition.is_directory = false; file_definition.is_directory = false;
file_definition_list.emplace_back(std::move(file_definition)); if (file_manager::util::ConvertAbsoluteFilePathToRelativeFileSystemPath(
chrome_details_.GetProfile(), extension_id(), file.url().path(),
&file_definition.virtual_path)) {
file_definition_list.emplace_back(std::move(file_definition));
}
} }
file_manager::util::ConvertFileDefinitionListToEntryDefinitionList( file_manager::util::ConvertFileDefinitionListToEntryDefinitionList(
......
...@@ -272,11 +272,13 @@ class FileManagerPrivateIsCrostiniEnabledFunction ...@@ -272,11 +272,13 @@ class FileManagerPrivateIsCrostiniEnabledFunction
public: public:
DECLARE_EXTENSION_FUNCTION("fileManagerPrivate.isCrostiniEnabled", DECLARE_EXTENSION_FUNCTION("fileManagerPrivate.isCrostiniEnabled",
FILEMANAGERPRIVATE_ISCROSTINIENABLED) FILEMANAGERPRIVATE_ISCROSTINIENABLED)
FileManagerPrivateIsCrostiniEnabledFunction() = default;
protected: protected:
~FileManagerPrivateIsCrostiniEnabledFunction() override = default; ~FileManagerPrivateIsCrostiniEnabledFunction() override = default;
ResponseAction Run() override; ResponseAction Run() override;
DISALLOW_COPY_AND_ASSIGN(FileManagerPrivateIsCrostiniEnabledFunction);
}; };
// Implements the chrome.fileManagerPrivate.mountCrostini method. // Implements the chrome.fileManagerPrivate.mountCrostini method.
...@@ -297,6 +299,7 @@ class FileManagerPrivateMountCrostiniFunction ...@@ -297,6 +299,7 @@ class FileManagerPrivateMountCrostiniFunction
private: private:
std::string source_path_; std::string source_path_;
std::string mount_label_; std::string mount_label_;
DISALLOW_COPY_AND_ASSIGN(FileManagerPrivateMountCrostiniFunction);
}; };
// Implements the chrome.fileManagerPrivate.sharePathWithCrostini // Implements the chrome.fileManagerPrivate.sharePathWithCrostini
...@@ -306,6 +309,7 @@ class FileManagerPrivateInternalSharePathWithCrostiniFunction ...@@ -306,6 +309,7 @@ class FileManagerPrivateInternalSharePathWithCrostiniFunction
public: public:
DECLARE_EXTENSION_FUNCTION("fileManagerPrivateInternal.sharePathWithCrostini", DECLARE_EXTENSION_FUNCTION("fileManagerPrivateInternal.sharePathWithCrostini",
FILEMANAGERPRIVATEINTERNAL_SHAREPATHWITHCROSTINI) FILEMANAGERPRIVATEINTERNAL_SHAREPATHWITHCROSTINI)
FileManagerPrivateInternalSharePathWithCrostiniFunction() = default;
protected: protected:
~FileManagerPrivateInternalSharePathWithCrostiniFunction() override = default; ~FileManagerPrivateInternalSharePathWithCrostiniFunction() override = default;
...@@ -313,6 +317,31 @@ class FileManagerPrivateInternalSharePathWithCrostiniFunction ...@@ -313,6 +317,31 @@ class FileManagerPrivateInternalSharePathWithCrostiniFunction
private: private:
ResponseAction Run() override; ResponseAction Run() override;
void SharePathCallback(bool success, std::string failure_reason); void SharePathCallback(bool success, std::string failure_reason);
DISALLOW_COPY_AND_ASSIGN(
FileManagerPrivateInternalSharePathWithCrostiniFunction);
};
// Implements the chrome.fileManagerPrivate.getCrostiniSharedPaths
// method. Returns list of file entries.
class FileManagerPrivateInternalGetCrostiniSharedPathsFunction
: public UIThreadExtensionFunction {
public:
DECLARE_EXTENSION_FUNCTION(
"fileManagerPrivateInternal.getCrostiniSharedPaths",
FILEMANAGERPRIVATEINTERNAL_GETCROSTINISHAREDPATHS)
FileManagerPrivateInternalGetCrostiniSharedPathsFunction() = default;
protected:
~FileManagerPrivateInternalGetCrostiniSharedPathsFunction() override =
default;
private:
ResponseAction Run() override;
void OnConvertFileDefinitionListToEntryDefinitionList(
std::unique_ptr<file_manager::util::EntryDefinitionList>
entry_definition_list);
DISALLOW_COPY_AND_ASSIGN(
FileManagerPrivateInternalGetCrostiniSharedPathsFunction);
}; };
// Implements the chrome.fileManagerPrivate.installLinuxPackage method. // Implements the chrome.fileManagerPrivate.installLinuxPackage method.
...@@ -322,6 +351,7 @@ class FileManagerPrivateInternalInstallLinuxPackageFunction ...@@ -322,6 +351,7 @@ class FileManagerPrivateInternalInstallLinuxPackageFunction
public: public:
DECLARE_EXTENSION_FUNCTION("fileManagerPrivateInternal.installLinuxPackage", DECLARE_EXTENSION_FUNCTION("fileManagerPrivateInternal.installLinuxPackage",
FILEMANAGERPRIVATEINTERNAL_INSTALLLINUXPACKAGE) FILEMANAGERPRIVATEINTERNAL_INSTALLLINUXPACKAGE)
FileManagerPrivateInternalInstallLinuxPackageFunction() = default;
protected: protected:
~FileManagerPrivateInternalInstallLinuxPackageFunction() override = default; ~FileManagerPrivateInternalInstallLinuxPackageFunction() override = default;
...@@ -330,6 +360,8 @@ class FileManagerPrivateInternalInstallLinuxPackageFunction ...@@ -330,6 +360,8 @@ class FileManagerPrivateInternalInstallLinuxPackageFunction
ResponseAction Run() override; ResponseAction Run() override;
void OnInstallLinuxPackage(crostini::ConciergeClientResult result, void OnInstallLinuxPackage(crostini::ConciergeClientResult result,
const std::string& failure_reason); const std::string& failure_reason);
DISALLOW_COPY_AND_ASSIGN(
FileManagerPrivateInternalInstallLinuxPackageFunction);
}; };
// Implements the chrome.fileManagerPrivate.getCustomActions method. // Implements the chrome.fileManagerPrivate.getCustomActions method.
......
...@@ -579,9 +579,11 @@ WRAPPED_INSTANTIATE_TEST_CASE_P( ...@@ -579,9 +579,11 @@ WRAPPED_INSTANTIATE_TEST_CASE_P(
TestCase("showToggleHiddenAndroidFoldersGearMenuItemsInMyFiles"), TestCase("showToggleHiddenAndroidFoldersGearMenuItemsInMyFiles"),
TestCase("enableToggleHiddenAndroidFoldersShowsHiddenFiles"))); TestCase("enableToggleHiddenAndroidFoldersShowsHiddenFiles")));
WRAPPED_INSTANTIATE_TEST_CASE_P(Crostini, /* crostini.js */ WRAPPED_INSTANTIATE_TEST_CASE_P(
FilesAppBrowserTest, Crostini, /* crostini.js */
::testing::Values(TestCase("mountCrostini"))); FilesAppBrowserTest,
::testing::Values(TestCase("mountCrostini"),
TestCase("sharePathWithCrostini")));
WRAPPED_INSTANTIATE_TEST_CASE_P( WRAPPED_INSTANTIATE_TEST_CASE_P(
MyFiles, /* my_files.js */ MyFiles, /* my_files.js */
......
...@@ -24,6 +24,7 @@ ...@@ -24,6 +24,7 @@
#include "chrome/browser/browser_process.h" #include "chrome/browser/browser_process.h"
#include "chrome/browser/chromeos/crostini/crostini_manager.h" #include "chrome/browser/chromeos/crostini/crostini_manager.h"
#include "chrome/browser/chromeos/crostini/crostini_pref_names.h" #include "chrome/browser/chromeos/crostini/crostini_pref_names.h"
#include "chrome/browser/chromeos/crostini/crostini_util.h"
#include "chrome/browser/chromeos/drive/file_system_util.h" #include "chrome/browser/chromeos/drive/file_system_util.h"
#include "chrome/browser/chromeos/file_manager/file_manager_test_util.h" #include "chrome/browser/chromeos/file_manager/file_manager_test_util.h"
#include "chrome/browser/chromeos/file_manager/mount_test_util.h" #include "chrome/browser/chromeos/file_manager/mount_test_util.h"
...@@ -40,6 +41,7 @@ ...@@ -40,6 +41,7 @@
#include "chromeos/chromeos_switches.h" #include "chromeos/chromeos_switches.h"
#include "chromeos/components/drivefs/drivefs_host.h" #include "chromeos/components/drivefs/drivefs_host.h"
#include "chromeos/components/drivefs/fake_drivefs.h" #include "chromeos/components/drivefs/fake_drivefs.h"
#include "chromeos/dbus/concierge/service.pb.h"
#include "chromeos/dbus/dbus_thread_manager.h" #include "chromeos/dbus/dbus_thread_manager.h"
#include "chromeos/dbus/fake_cros_disks_client.h" #include "chromeos/dbus/fake_cros_disks_client.h"
#include "components/drive/chromeos/file_system_interface.h" #include "components/drive/chromeos/file_system_interface.h"
...@@ -977,6 +979,7 @@ void FileManagerBrowserTestBase::SetUpCommandLine( ...@@ -977,6 +979,7 @@ void FileManagerBrowserTestBase::SetUpCommandLine(
if (!IsGuestModeTest()) { if (!IsGuestModeTest()) {
enabled_features.emplace_back(features::kCrostini); enabled_features.emplace_back(features::kCrostini);
enabled_features.emplace_back(features::kExperimentalCrostiniUI); enabled_features.emplace_back(features::kExperimentalCrostiniUI);
command_line->AppendSwitch(chromeos::switches::kCrostiniFiles);
} }
if (IsDriveFsTest()) { if (IsDriveFsTest()) {
enabled_features.emplace_back(chromeos::features::kDriveFs); enabled_features.emplace_back(chromeos::features::kDriveFs);
...@@ -1347,7 +1350,13 @@ base::FilePath FileManagerBrowserTestBase::MaybeMountCrostini( ...@@ -1347,7 +1350,13 @@ base::FilePath FileManagerBrowserTestBase::MaybeMountCrostini(
if (source_url.scheme() != "sshfs") { if (source_url.scheme() != "sshfs") {
return {}; return {};
} }
// Mount crostini volume, and set VM now running for CrostiniManager.
CHECK(crostini_volume_->Mount(profile())); CHECK(crostini_volume_->Mount(profile()));
crostini::CrostiniManager* crostini_manager =
crostini::CrostiniManager::GetForProfile(profile()->GetOriginalProfile());
vm_tools::concierge::VmInfo vm_info;
crostini_manager->AddRunningVmForTesting(kCrostiniDefaultVmName,
std::move(vm_info));
return crostini_volume_->mount_path(); return crostini_volume_->mount_path();
} }
......
...@@ -736,6 +736,10 @@ callback GetDirectorySizeCallback = void(double size); ...@@ -736,6 +736,10 @@ callback GetDirectorySizeCallback = void(double size);
// |entries| Recently modified entries. // |entries| Recently modified entries.
callback GetRecentFilesCallback = void([instanceOf=Entry] object[] entries); callback GetRecentFilesCallback = void([instanceOf=Entry] object[] entries);
// |entries| Entries shared with crostini container.
callback GetCrostiniSharedPathsCallback =
void([instanceOf = Entry] object[] entries);
// |status| Result of starting the install // |status| Result of starting the install
// |failure_reason| Reason for failure for a 'failed' status // |failure_reason| Reason for failure for a 'failed' status
callback InstallLinuxPackageCallback = void( callback InstallLinuxPackageCallback = void(
...@@ -1115,6 +1119,10 @@ interface Functions { ...@@ -1115,6 +1119,10 @@ interface Functions {
static void sharePathWithCrostini( static void sharePathWithCrostini(
[instanceof=DirectoryEntry] object entry, SimpleCallback callback); [instanceof=DirectoryEntry] object entry, SimpleCallback callback);
// Returns list of paths shared with crostini container.
[nocompile]
static void getCrostiniSharedPaths(GetCrostiniSharedPathsCallback callback);
// Starts installation of a Linux package. // Starts installation of a Linux package.
[nocompile] [nocompile]
static void installLinuxPackage([instanceof=Entry] object entry, static void installLinuxPackage([instanceof=Entry] object entry,
......
...@@ -32,6 +32,7 @@ namespace fileManagerPrivateInternal { ...@@ -32,6 +32,7 @@ namespace fileManagerPrivateInternal {
callback ValidatePathNameLengthCallback = void(boolean result); callback ValidatePathNameLengthCallback = void(boolean result);
callback GetDirectorySizeCallback = void(double size); callback GetDirectorySizeCallback = void(double size);
callback GetRecentFilesCallback = void(EntryDescription[] entries); callback GetRecentFilesCallback = void(EntryDescription[] entries);
callback GetCrostiniSharedPathsCallback = void(EntryDescription[] entries);
callback InstallLinuxPackageCallback = callback InstallLinuxPackageCallback =
void(fileManagerPrivate.InstallLinuxPackageResponse response, void(fileManagerPrivate.InstallLinuxPackageResponse response,
optional DOMString failure_reason); optional DOMString failure_reason);
...@@ -102,6 +103,8 @@ namespace fileManagerPrivateInternal { ...@@ -102,6 +103,8 @@ namespace fileManagerPrivateInternal {
GetRecentFilesCallback callback); GetRecentFilesCallback callback);
static void sharePathWithCrostini(DOMString url, static void sharePathWithCrostini(DOMString url,
SimpleCallback callback); SimpleCallback callback);
static void getCrostiniSharedPaths(
GetCrostiniSharedPathsCallback callback);
static void installLinuxPackage(DOMString url, static void installLinuxPackage(DOMString url,
InstallLinuxPackageCallback callback); InstallLinuxPackageCallback callback);
static void getThumbnail(DOMString url, static void getThumbnail(DOMString url,
......
...@@ -224,6 +224,16 @@ binding.registerCustomHook(function(bindingsAPI) { ...@@ -224,6 +224,16 @@ binding.registerCustomHook(function(bindingsAPI) {
url, callback); url, callback);
}); });
apiFunctions.setHandleRequest(
'getCrostiniSharedPaths', function(callback) {
fileManagerPrivateInternal.getCrostiniSharedPaths(
function(entryDescriptions) {
callback(entryDescriptions.map(function(description) {
return GetExternalFileEntry(description);
}));
});
});
apiFunctions.setHandleRequest('installLinuxPackage', function( apiFunctions.setHandleRequest('installLinuxPackage', function(
entry, callback) { entry, callback) {
var url = fileManagerPrivateNatives.GetEntryURL(entry); var url = fileManagerPrivateNatives.GetEntryURL(entry);
......
...@@ -2,6 +2,11 @@ ...@@ -2,6 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
// This api testing extension's ID. Files referenced as Entry will
// have this as part of their URL.
const TEST_EXTENSION_ID = 'pkplfbidichfdicaijlchgnapepdginl';
/** /**
* Get specified entry. * Get specified entry.
* @param {string} volumeType volume type for entry. * @param {string} volumeType volume type for entry.
...@@ -47,4 +52,18 @@ chrome.test.runTests([ ...@@ -47,4 +52,18 @@ chrome.test.runTests([
'Share with Linux only allowed for directories within Downloads.')); 'Share with Linux only allowed for directories within Downloads.'));
}); });
}, },
function testGetCrostiniSharedPaths() {
const urlPrefix = 'filesystem:chrome-extension://' + TEST_EXTENSION_ID +
'/external/Downloads-user';
chrome.fileManagerPrivate.getCrostiniSharedPaths(
chrome.test.callbackPass((entries) => {
chrome.test.assertEq(2, entries.length);
chrome.test.assertEq(urlPrefix + '/shared1', entries[0].toURL());
chrome.test.assertTrue(entries[0].isDirectory);
chrome.test.assertEq('/shared1', entries[0].fullPath);
chrome.test.assertEq(urlPrefix + '/shared2', entries[1].toURL());
chrome.test.assertTrue(entries[1].isDirectory);
chrome.test.assertEq('/shared2', entries[1].fullPath);
}));
}
]); ]);
...@@ -1339,6 +1339,7 @@ enum HistogramValue { ...@@ -1339,6 +1339,7 @@ enum HistogramValue {
WEBVIEWINTERNAL_SETSPATIALNAVIGATIONENABLED = 1276, WEBVIEWINTERNAL_SETSPATIALNAVIGATIONENABLED = 1276,
WEBVIEWINTERNAL_ISSPATIALNAVIGATIONENABLED = 1277, WEBVIEWINTERNAL_ISSPATIALNAVIGATIONENABLED = 1277,
FILEMANAGERPRIVATEINTERNAL_GETTHUMBNAIL = 1278, FILEMANAGERPRIVATEINTERNAL_GETTHUMBNAIL = 1278,
FILEMANAGERPRIVATEINTERNAL_GETCROSTINISHAREDPATHS = 1279,
// Last entry: Add new entries above, then run: // Last entry: Add new entries above, then run:
// python tools/metrics/histograms/update_extension_histograms.py // python tools/metrics/histograms/update_extension_histograms.py
ENUM_BOUNDARY ENUM_BOUNDARY
......
...@@ -944,6 +944,12 @@ chrome.fileManagerPrivate.mountCrostini = function(callback) {}; ...@@ -944,6 +944,12 @@ chrome.fileManagerPrivate.mountCrostini = function(callback) {};
chrome.fileManagerPrivate.sharePathWithCrostini = function( chrome.fileManagerPrivate.sharePathWithCrostini = function(
entry, callback) {}; entry, callback) {};
/**
* Returns list of paths shared with the crostini container.
* @param {function(!Array<!Entry>)} callback
*/
chrome.fileManagerPrivate.getCrostiniSharedPaths = function(callback) {};
/** /**
* Begin installation of a Linux package. * Begin installation of a Linux package.
* @param {!Entry} entry * @param {!Entry} entry
......
...@@ -16764,6 +16764,7 @@ Called by update_net_error_codes.py.--> ...@@ -16764,6 +16764,7 @@ Called by update_net_error_codes.py.-->
<int value="1276" label="WEBVIEWINTERNAL_SETSPATIALNAVIGATIONENABLED"/> <int value="1276" label="WEBVIEWINTERNAL_SETSPATIALNAVIGATIONENABLED"/>
<int value="1277" label="WEBVIEWINTERNAL_ISSPATIALNAVIGATIONENABLED"/> <int value="1277" label="WEBVIEWINTERNAL_ISSPATIALNAVIGATIONENABLED"/>
<int value="1278" label="FILEMANAGERPRIVATEINTERNAL_GETTHUMBNAIL"/> <int value="1278" label="FILEMANAGERPRIVATEINTERNAL_GETTHUMBNAIL"/>
<int value="1279" label="FILEMANAGERPRIVATEINTERNAL_GETCROSTINISHAREDPATHS"/>
</enum> </enum>
<enum name="ExtensionIconState"> <enum name="ExtensionIconState">
...@@ -13,20 +13,37 @@ const Crostini = {}; ...@@ -13,20 +13,37 @@ const Crostini = {};
Crostini.SHARED_PATHS_ = {}; Crostini.SHARED_PATHS_ = {};
/** /**
* Add entry as a shared path. * Registers an entry as a shared path.
* @param {!Entry} entry * @param {!Entry} entry
* @param {!VolumeManager} volumeManager * @param {!VolumeManager} volumeManager
*/ */
Crostini.addSharedPath = function(entry, volumeManager) { Crostini.registerSharedPath = function(entry, volumeManager) {
const root = volumeManager.getLocationInfo(entry).rootType; const info = volumeManager.getLocationInfo(entry);
let paths = Crostini.SHARED_PATHS_[root]; if (!info)
return;
let paths = Crostini.SHARED_PATHS_[info.rootType];
if (!paths) { if (!paths) {
paths = {}; paths = {};
Crostini.SHARED_PATHS_[root] = paths; Crostini.SHARED_PATHS_[info.rootType] = paths;
} }
paths[entry.fullPath] = true; paths[entry.fullPath] = true;
}; };
/**
* Unregisters entry as a shared path.
* @param {!Entry} entry
* @param {!VolumeManager} volumeManager
*/
Crostini.unregisterSharedPath = function(entry, volumeManager) {
const info = volumeManager.getLocationInfo(entry);
if (!info)
return;
const paths = Crostini.SHARED_PATHS_[info.rootType];
if (paths) {
delete paths[entry.fullPath];
}
};
/** /**
* Returns true if entry is shared. * Returns true if entry is shared.
* @param {!Entry} entry * @param {!Entry} entry
......
...@@ -18,12 +18,15 @@ function testIsPathShared() { ...@@ -18,12 +18,15 @@ function testIsPathShared() {
assertFalse(Crostini.isPathShared(foo1, volumeManager)); assertFalse(Crostini.isPathShared(foo1, volumeManager));
Crostini.addSharedPath(foo1, volumeManager); Crostini.registerSharedPath(foo1, volumeManager);
assertFalse(Crostini.isPathShared(root, volumeManager)); assertFalse(Crostini.isPathShared(root, volumeManager));
assertTrue(Crostini.isPathShared(foo1, volumeManager)); assertTrue(Crostini.isPathShared(foo1, volumeManager));
assertTrue(Crostini.isPathShared(foobar1, volumeManager)); assertTrue(Crostini.isPathShared(foobar1, volumeManager));
Crostini.addSharedPath(foobar2, volumeManager); Crostini.registerSharedPath(foobar2, volumeManager);
assertFalse(Crostini.isPathShared(foo2, volumeManager)); assertFalse(Crostini.isPathShared(foo2, volumeManager));
assertTrue(Crostini.isPathShared(foobar2, volumeManager)); assertTrue(Crostini.isPathShared(foobar2, volumeManager));
Crostini.unregisterSharedPath(foobar2, volumeManager);
assertFalse(Crostini.isPathShared(foobar2, volumeManager));
} }
...@@ -1229,7 +1229,19 @@ FileManager.prototype = /** @struct */ { ...@@ -1229,7 +1229,19 @@ FileManager.prototype = /** @struct */ {
str('LINUX_FILES_ROOT_LABEL'), str('LINUX_FILES_ROOT_LABEL'),
VolumeManagerCommon.RootType.CROSTINI, true)) : VolumeManagerCommon.RootType.CROSTINI, true)) :
null; null;
// Redraw the tree even if not enabled. This is required for testing.
this.directoryTree.redraw(false); this.directoryTree.redraw(false);
if (!enabled)
return;
// Load any existing shared paths.
chrome.fileManagerPrivate.getCrostiniSharedPaths((entries) => {
for (let i = 0; i < entries.length; i++) {
Crostini.registerSharedPath(entries[i], assert(this.volumeManager_));
}
});
}); });
}; };
......
...@@ -1664,7 +1664,7 @@ CommandHandler.COMMANDS_['share-with-linux'] = /** @type {Command} */ ({ ...@@ -1664,7 +1664,7 @@ CommandHandler.COMMANDS_['share-with-linux'] = /** @type {Command} */ ({
'Error sharing with linux: ' + 'Error sharing with linux: ' +
chrome.runtime.lastError.message); chrome.runtime.lastError.message);
} else { } else {
Crostini.addSharedPath(dir, assert(fileManager.volumeManager)); Crostini.registerSharedPath(dir, fileManager.volumeManager);
} }
}); });
} }
......
...@@ -74,6 +74,7 @@ js_library("check_select") { ...@@ -74,6 +74,7 @@ js_library("check_select") {
js_library("crostini") { js_library("crostini") {
deps = [ deps = [
"../foreground/js:crostini",
"js:test_util", "js:test_util",
"//ui/webui/resources/js:webui_resource_test", "//ui/webui/resources/js:webui_resource_test",
] ]
......
...@@ -255,16 +255,15 @@ crostini.testSharePathCrostiniSuccess = (done) => { ...@@ -255,16 +255,15 @@ crostini.testSharePathCrostiniSuccess = (done) => {
.then(() => { .then(() => {
// Right-click 'photos' directory. // Right-click 'photos' directory.
// Check 'Share with Linux' is shown in menu. // Check 'Share with Linux' is shown in menu.
test.selectFile('photos');
assertTrue( assertTrue(
test.fakeMouseRightClick('#file-list li[selected]'), test.fakeMouseRightClick('#file-list [file-name="photos"]'),
'right-click photos'); 'right-click photos');
return test.waitForElement( return test.waitForElement(
'#file-context-menu:not([hidden]) ' + '#file-context-menu:not([hidden]) ' +
'[command="#share-with-linux"]:not([hidden]):not([disabled])'); '[command="#share-with-linux"]:not([hidden]):not([disabled])');
}) })
.then(() => { .then(() => {
// Click on 'Start with Linux'. // Click on 'Share with Linux'.
assertTrue( assertTrue(
test.fakeMouseClick( test.fakeMouseClick(
'#file-context-menu [command="#share-with-linux"]'), '#file-context-menu [command="#share-with-linux"]'),
...@@ -280,8 +279,9 @@ crostini.testSharePathCrostiniSuccess = (done) => { ...@@ -280,8 +279,9 @@ crostini.testSharePathCrostiniSuccess = (done) => {
// Verify right-click menu with 'Share with Linux' is not shown for: // Verify right-click menu with 'Share with Linux' is not shown for:
// * Files (not directory) // * Files (not directory)
// * Any folder already shared
// * Root Downloads folder // * Root Downloads folder
// * Any folder outside of downloads (e.g. crostini or drive) // * Any folder outside of downloads (e.g. crostini or orive)
crostini.testSharePathNotShown = (done) => { crostini.testSharePathNotShown = (done) => {
const myFiles = '#directory-tree .tree-item [root-type-icon="my_files"]'; const myFiles = '#directory-tree .tree-item [root-type-icon="my_files"]';
const downloads = '#file-list li [file-type-icon="downloads"]'; const downloads = '#file-list li [file-type-icon="downloads"]';
...@@ -289,14 +289,27 @@ crostini.testSharePathNotShown = (done) => { ...@@ -289,14 +289,27 @@ crostini.testSharePathNotShown = (done) => {
const googleDrive = '#directory-tree .tree-item [volume-type-icon="drive"]'; const googleDrive = '#directory-tree .tree-item [volume-type-icon="drive"]';
const menuNoShareWithLinux = '#file-context-menu:not([hidden]) ' + const menuNoShareWithLinux = '#file-context-menu:not([hidden]) ' +
'[command="#share-with-linux"][hidden][disabled="disabled"]'; '[command="#share-with-linux"][hidden][disabled="disabled"]';
let alreadySharedPhotosDir;
test.setupAndWaitUntilReady() test.setupAndWaitUntilReady()
.then(() => { .then(() => {
// Right-click 'hello.txt' file. // Right-click 'hello.txt' file.
// Check 'Share with Linux' is not shown in menu. // Check 'Share with Linux' is not shown in menu.
test.selectFile('hello.txt');
assertTrue( assertTrue(
test.fakeMouseRightClick('#file-list li[selected]'), test.fakeMouseRightClick('#file-list [file-name="hello.txt"]'),
'right-click hello.txt');
return test.waitForElement(menuNoShareWithLinux);
})
.then(() => {
// Set a folder as already shared.
alreadySharedPhotosDir =
mockVolumeManager
.getCurrentProfileVolumeInfo(
VolumeManagerCommon.VolumeType.DOWNLOADS)
.fileSystem.entries['/photos'];
Crostini.registerSharedPath(alreadySharedPhotosDir, mockVolumeManager);
assertTrue(
test.fakeMouseRightClick('#file-list [file-name="photos"]'),
'right-click hello.txt'); 'right-click hello.txt');
return test.waitForElement(menuNoShareWithLinux); return test.waitForElement(menuNoShareWithLinux);
}) })
...@@ -323,9 +336,8 @@ crostini.testSharePathNotShown = (done) => { ...@@ -323,9 +336,8 @@ crostini.testSharePathNotShown = (done) => {
}) })
.then(() => { .then(() => {
// Check 'Share with Linux' is not shown in menu. // Check 'Share with Linux' is not shown in menu.
test.selectFile('A');
assertTrue( assertTrue(
test.fakeMouseRightClick('#file-list li[selected]'), test.fakeMouseRightClick('#file-list [file-name="A"]'),
'right-click directory A'); 'right-click directory A');
return test.waitForElement(menuNoShareWithLinux); return test.waitForElement(menuNoShareWithLinux);
}) })
...@@ -340,9 +352,8 @@ crostini.testSharePathNotShown = (done) => { ...@@ -340,9 +352,8 @@ crostini.testSharePathNotShown = (done) => {
}) })
.then(() => { .then(() => {
// Check 'Share with Linux' is not shown in menu. // Check 'Share with Linux' is not shown in menu.
test.selectFile('photos');
assertTrue( assertTrue(
test.fakeMouseRightClick('#file-list li[selected]'), test.fakeMouseRightClick('#file-list [file-name="photos"]'),
'right-click photos'); 'right-click photos');
return test.waitForElement(menuNoShareWithLinux); return test.waitForElement(menuNoShareWithLinux);
}) })
...@@ -353,6 +364,9 @@ crostini.testSharePathNotShown = (done) => { ...@@ -353,6 +364,9 @@ crostini.testSharePathNotShown = (done) => {
'#directory-tree .tree-item [root-type-icon="crostini"]'); '#directory-tree .tree-item [root-type-icon="crostini"]');
}) })
.then(() => { .then(() => {
// Clear Crostini shared folders.
Crostini.unregisterSharedPath(
alreadySharedPhotosDir, mockVolumeManager);
done(); done();
}); });
}; };
...@@ -89,6 +89,10 @@ chrome.fileManagerPrivate = { ...@@ -89,6 +89,10 @@ chrome.fileManagerPrivate = {
} }
setTimeout(callback, 0, results); setTimeout(callback, 0, results);
}, },
getCrostiniSharedPaths: (callback) => {
// Returns Entry[].
setTimeout(callback, 0, []);
},
getPreferences: (callback) => { getPreferences: (callback) => {
setTimeout(callback, 0, chrome.fileManagerPrivate.preferences_); setTimeout(callback, 0, chrome.fileManagerPrivate.preferences_);
}, },
......
...@@ -9,6 +9,9 @@ loadTimeData.data = $GRDP; ...@@ -9,6 +9,9 @@ loadTimeData.data = $GRDP;
// Extend with additional fields not found in grdp files. // Extend with additional fields not found in grdp files.
Object.setPrototypeOf(loadTimeData.data_, { Object.setPrototypeOf(loadTimeData.data_, {
'CHROMEOS_RELEASE_BOARD': 'unknown',
'GOOGLE_DRIVE_REDEEM_URL': 'http://www.google.com/intl/en/chrome/devices' +
'/goodies.html?utm_source=filesapp&utm_medium=banner&utm_campaign=gsg',
'HIDE_SPACE_INFO': false, 'HIDE_SPACE_INFO': false,
'UI_LOCALE': 'en_US', 'UI_LOCALE': 'en_US',
'language': 'en-US', 'language': 'en-US',
......
...@@ -5,8 +5,8 @@ ...@@ -5,8 +5,8 @@
'use strict'; 'use strict';
testcase.mountCrostini = function() { testcase.mountCrostini = function() {
const fake = '#directory-tree .tree-item [root-type-icon="crostini"]'; const fakeLinuxFiles = '#directory-tree [root-type-icon="crostini"]';
const real = '#directory-tree .tree-item [volume-type-icon="crostini"]'; const realLinxuFiles = '#directory-tree [volume-type-icon="crostini"]';
let appId; let appId;
StepsRunner.run([ StepsRunner.run([
...@@ -14,28 +14,86 @@ testcase.mountCrostini = function() { ...@@ -14,28 +14,86 @@ testcase.mountCrostini = function() {
setupAndWaitUntilReady( setupAndWaitUntilReady(
null, RootPath.DOWNLOADS, this.next, [ENTRIES.hello], []); null, RootPath.DOWNLOADS, this.next, [ENTRIES.hello], []);
}, },
// Add entries to crostini volume, but do not mount.
function(results) { function(results) {
// Add entries to crostini volume, but do not mount.
appId = results.windowId; appId = results.windowId;
addEntries(['crostini'], BASIC_CROSTINI_ENTRY_SET, this.next); addEntries(['crostini'], BASIC_CROSTINI_ENTRY_SET, this.next);
}, },
// Linux files fake root is shown.
function() { function() {
// Linux files fake root is shown. remoteCall.waitForElement(appId, fakeLinuxFiles).then(this.next);
remoteCall.waitForElement(appId, fake).then(this.next);
}, },
// Mount crostini, and ensure real root and files are shown.
function() { function() {
// Mount crostini, and ensure real root and files are shown. remoteCall.callRemoteTestUtil('fakeMouseClick', appId, [fakeLinuxFiles]);
remoteCall.callRemoteTestUtil('fakeMouseClick', appId, [fake]); remoteCall.waitForElement(appId, realLinxuFiles).then(this.next);
remoteCall.waitForElement(appId, real).then(this.next);
}, },
function() { function() {
const files = TestEntryInfo.getExpectedRows(BASIC_CROSTINI_ENTRY_SET); const files = TestEntryInfo.getExpectedRows(BASIC_CROSTINI_ENTRY_SET);
remoteCall.waitForFiles(appId, files).then(this.next); remoteCall.waitForFiles(appId, files).then(this.next);
}, },
// Unmount and ensure fake root is shown.
function() { function() {
// Unmount and ensure fake root is shown. // Unmount and ensure fake root is shown.
remoteCall.callRemoteTestUtil('unmount', null, ['crostini']); remoteCall.callRemoteTestUtil('unmount', null, ['crostini']);
remoteCall.waitForElement(appId, fake).then(this.next); remoteCall.waitForElement(appId, fakeLinuxFiles).then(this.next);
},
function() {
checkIfNoErrorsOccured(this.next);
},
]);
};
testcase.sharePathWithCrostini = function() {
const fakeLinuxFiles = '#directory-tree [root-type-icon="crostini"]';
const realLinuxFiles = '#directory-tree [volume-type-icon="crostini"]';
const downloads = '#directory-tree [volume-type-icon="downloads"]';
const photos = '#file-list [file-name="photos"]';
const menuShareWithLinux = '#file-context-menu:not([hidden]) ' +
'[command="#share-with-linux"]:not([hidden]):not([disabled])';
const menuNoShareWithLinux = '#file-context-menu:not([hidden]) ' +
'[command="#share-with-linux"][hidden][disabled="disabled"]';
let appId;
StepsRunner.run([
function() {
setupAndWaitUntilReady(
null, RootPath.DOWNLOADS, this.next, [ENTRIES.photos], []);
},
// Ensure fake Linux files root is shown.
function(results) {
appId = results.windowId;
remoteCall.waitForElement(appId, fakeLinuxFiles).then(this.next);
},
// Mount crostini, and ensure real root is shown.
function() {
remoteCall.callRemoteTestUtil('fakeMouseClick', appId, [fakeLinuxFiles]);
remoteCall.waitForElement(appId, realLinuxFiles).then(this.next);
},
// Go back to downloads, wait for photos dir to be shown.
function(results) {
remoteCall.callRemoteTestUtil('fakeMouseClick', appId, [downloads]);
remoteCall.waitForElement(appId, photos).then(this.next);
},
// Right-click 'photos' directory, ensure 'Share with Linux' is shown.
function(results) {
remoteCall.callRemoteTestUtil('fakeMouseRightClick', appId, [photos]);
remoteCall.waitForElement(appId, menuShareWithLinux).then(this.next);
},
// Click on 'Share with Linux', ensure menu is closed.
function() {
remoteCall.callRemoteTestUtil(
'fakeMouseClick', appId,
['#file-context-menu [command="#share-with-linux"]'], this.next);
remoteCall.waitForElement(appId, '#file-context-menu[hidden]')
.then(this.next);
},
// Right-click 'photos' directory, ensure 'Share with Linux' is not shown.
function() {
remoteCall.callRemoteTestUtil(
'fakeMouseRightClick', appId, ['#file-list [file-name="photos"'],
this.next);
remoteCall.waitForElement(appId, menuNoShareWithLinux).then(this.next);
}, },
function() { function() {
checkIfNoErrorsOccured(this.next); checkIfNoErrorsOccured(this.next);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment