Commit 434df0a3 authored by jackhou@chromium.org's avatar jackhou@chromium.org

[Mac] Add interactive App Shim test.

This test creates shims in the user data dir and actually starts them up and
expects them to connect to the IPC socket.

BUG=168080, 386024
NOTRY=true

Committed: https://src.chromium.org/viewvc/chrome?view=rev&revision=276368

Committed: https://src.chromium.org/viewvc/chrome?view=rev&revision=277743

Review URL: https://codereview.chromium.org/316493002

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@277955 0039d316-1c4b-4281-b951-d872f2087c98
parent b42a6050
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import <Cocoa/Cocoa.h>
#include <vector>
#include "apps/app_shim/app_shim_handler_mac.h"
#include "apps/app_shim/app_shim_host_manager_mac.h"
#include "apps/app_shim/extension_app_shim_handler_mac.h"
#include "apps/switches.h"
#include "apps/ui/native_app_window.h"
#include "base/auto_reset.h"
#include "base/callback.h"
#include "base/files/file_path_watcher.h"
#include "base/mac/foundation_util.h"
#include "base/mac/launch_services_util.h"
#include "base/mac/scoped_nsobject.h"
#include "base/path_service.h"
#include "base/process/launch.h"
#include "base/strings/sys_string_conversions.h"
#include "base/test/test_timeouts.h"
#include "chrome/browser/apps/app_browsertest_util.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/extensions/extension_test_message_listener.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_applications/web_app_mac.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/mac/app_mode_common.h"
#include "content/public/test/test_utils.h"
#include "extensions/browser/extension_registry.h"
#import "ui/events/test/cocoa_test_event_utils.h"
namespace {
// General end-to-end test for app shims.
class AppShimInteractiveTest : public extensions::PlatformAppBrowserTest {
protected:
AppShimInteractiveTest()
: auto_reset_(&g_app_shims_allow_update_and_launch_in_tests, true) {}
private:
// Temporarily enable app shims.
base::AutoReset<bool> auto_reset_;
DISALLOW_COPY_AND_ASSIGN(AppShimInteractiveTest);
};
// Watches for changes to a file. This is designed to be used from the the UI
// thread.
class WindowedFilePathWatcher
: public base::RefCountedThreadSafe<WindowedFilePathWatcher> {
public:
WindowedFilePathWatcher(const base::FilePath& path) : observed_(false) {
content::BrowserThread::PostTask(
content::BrowserThread::FILE,
FROM_HERE,
base::Bind(&WindowedFilePathWatcher::Watch, this, path));
}
void Wait() {
if (observed_)
return;
run_loop_.reset(new base::RunLoop);
run_loop_->Run();
}
protected:
friend class base::RefCountedThreadSafe<WindowedFilePathWatcher>;
virtual ~WindowedFilePathWatcher() {}
void Watch(const base::FilePath& path) {
watcher_.Watch(
path, false, base::Bind(&WindowedFilePathWatcher::Observe, this));
}
void Observe(const base::FilePath& path, bool error) {
content::BrowserThread::PostTask(
content::BrowserThread::UI,
FROM_HERE,
base::Bind(&WindowedFilePathWatcher::StopRunLoop, this));
}
void StopRunLoop() {
observed_ = true;
if (run_loop_.get())
run_loop_->Quit();
}
private:
base::FilePathWatcher watcher_;
bool observed_;
scoped_ptr<base::RunLoop> run_loop_;
DISALLOW_COPY_AND_ASSIGN(WindowedFilePathWatcher);
};
// Watches for an app shim to connect.
class WindowedAppShimLaunchObserver : public apps::AppShimHandler {
public:
WindowedAppShimLaunchObserver(const std::string& app_id)
: app_mode_id_(app_id),
observed_(false) {
apps::AppShimHandler::RegisterHandler(app_id, this);
}
void Wait() {
if (observed_)
return;
run_loop_.reset(new base::RunLoop);
run_loop_->Run();
}
// AppShimHandler overrides:
virtual void OnShimLaunch(Host* host,
apps::AppShimLaunchType launch_type,
const std::vector<base::FilePath>& files) OVERRIDE {
// Remove self and pass through to the default handler.
apps::AppShimHandler::RemoveHandler(app_mode_id_);
apps::AppShimHandler::GetForAppMode(app_mode_id_)
->OnShimLaunch(host, launch_type, files);
observed_ = true;
if (run_loop_.get())
run_loop_->Quit();
}
virtual void OnShimClose(Host* host) OVERRIDE {}
virtual void OnShimFocus(Host* host,
apps::AppShimFocusType focus_type,
const std::vector<base::FilePath>& files) OVERRIDE {}
virtual void OnShimSetHidden(Host* host, bool hidden) OVERRIDE {}
virtual void OnShimQuit(Host* host) OVERRIDE {}
private:
std::string app_mode_id_;
bool observed_;
scoped_ptr<base::RunLoop> run_loop_;
DISALLOW_COPY_AND_ASSIGN(WindowedAppShimLaunchObserver);
};
NSString* GetBundleID(const base::FilePath& shim_path) {
base::FilePath plist_path = shim_path.Append("Contents").Append("Info.plist");
NSMutableDictionary* plist = [NSMutableDictionary
dictionaryWithContentsOfFile:base::mac::FilePathToNSString(plist_path)];
return [plist objectForKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)];
}
bool HasAppShimHost(Profile* profile, const std::string& app_id) {
return g_browser_process->platform_part()
->app_shim_host_manager()
->extension_app_shim_handler()
->FindHost(profile, app_id);
}
} // namespace
// Watches for NSNotifications from the shared workspace.
@interface WindowedNSNotificationObserver : NSObject {
@private
base::scoped_nsobject<NSString> bundleId_;
BOOL notificationReceived_;
scoped_ptr<base::RunLoop> runLoop_;
}
- (id)initForNotification:(NSString*)name
andBundleId:(NSString*)bundleId;
- (void)observe:(NSNotification*)notification;
- (void)wait;
@end
@implementation WindowedNSNotificationObserver
- (id)initForNotification:(NSString*)name
andBundleId:(NSString*)bundleId {
if (self = [super init]) {
bundleId_.reset([[bundleId copy] retain]);
[[[NSWorkspace sharedWorkspace] notificationCenter]
addObserver:self
selector:@selector(observe:)
name:name
object:nil];
}
return self;
}
- (void)observe:(NSNotification*)notification {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
NSRunningApplication* application =
[[notification userInfo] objectForKey:NSWorkspaceApplicationKey];
if (![[application bundleIdentifier] isEqualToString:bundleId_])
return;
[[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:self];
notificationReceived_ = YES;
if (runLoop_.get())
runLoop_->Quit();
}
- (void)wait {
if (notificationReceived_)
return;
runLoop_.reset(new base::RunLoop);
runLoop_->Run();
}
@end
namespace apps {
// Shims require static libraries http://crbug.com/386024.
#if defined(COMPONENT_BUILD)
#define MAYBE_Launch DISABLED_Launch
#else
#define MAYBE_Launch Launch
#endif
// Test that launching the shim for an app starts the app, and vice versa.
// These two cases are combined because the time to run the test is dominated
// by loading the extension and creating the shim.
IN_PROC_BROWSER_TEST_F(AppShimInteractiveTest, MAYBE_Launch) {
// Install the app.
const extensions::Extension* app = InstallPlatformApp("minimal");
// Use a WebAppShortcutCreator to get the path.
web_app::WebAppShortcutCreator shortcut_creator(
web_app::GetWebAppDataDirectory(profile()->GetPath(), app->id(), GURL()),
web_app::ShortcutInfoForExtensionAndProfile(app, profile()),
extensions::FileHandlersInfo());
base::FilePath shim_path = shortcut_creator.GetInternalShortcutPath();
EXPECT_FALSE(base::PathExists(shim_path));
// Create the internal app shim by simulating an app update. FilePathWatcher
// is used to wait for file operations on the shim to be finished before
// attempting to launch it. Since all of the file operations are done in the
// same event on the FILE thread, everything will be done by the time the
// watcher's callback is executed.
scoped_refptr<WindowedFilePathWatcher> file_watcher =
new WindowedFilePathWatcher(shim_path);
web_app::UpdateAllShortcuts(base::string16(), profile(), app);
file_watcher->Wait();
NSString* bundle_id = GetBundleID(shim_path);
// Case 1: Launch the shim, it should start the app.
{
ExtensionTestMessageListener launched_listener("Launched", false);
CommandLine shim_cmdline(CommandLine::NO_PROGRAM);
shim_cmdline.AppendSwitch(app_mode::kLaunchedForTest);
ProcessSerialNumber shim_psn;
ASSERT_TRUE(base::mac::OpenApplicationWithPath(
shim_path, shim_cmdline, kLSLaunchDefaults, &shim_psn));
ASSERT_TRUE(launched_listener.WaitUntilSatisfied());
ASSERT_TRUE(GetFirstAppWindow());
EXPECT_TRUE(HasAppShimHost(profile(), app->id()));
// If the window is closed, the shim should quit.
pid_t shim_pid;
EXPECT_EQ(noErr, GetProcessPID(&shim_psn, &shim_pid));
GetFirstAppWindow()->GetBaseWindow()->Close();
ASSERT_TRUE(
base::WaitForSingleProcess(shim_pid, TestTimeouts::action_timeout()));
EXPECT_FALSE(GetFirstAppWindow());
EXPECT_FALSE(HasAppShimHost(profile(), app->id()));
}
// Case 2: Launch the app, it should start the shim.
{
base::scoped_nsobject<WindowedNSNotificationObserver> ns_observer;
ns_observer.reset([[WindowedNSNotificationObserver alloc]
initForNotification:NSWorkspaceDidLaunchApplicationNotification
andBundleId:bundle_id]);
WindowedAppShimLaunchObserver observer(app->id());
LaunchPlatformApp(app);
[ns_observer wait];
observer.Wait();
EXPECT_TRUE(GetFirstAppWindow());
EXPECT_TRUE(HasAppShimHost(profile(), app->id()));
// Quitting the shim will eventually cause it to quit. It actually
// intercepts the -terminate, sends an AppShimHostMsg_QuitApp to Chrome,
// and returns NSTerminateLater. Chrome responds by closing all windows of
// the app. Once all windows are closed, Chrome closes the IPC channel,
// which causes the shim to actually terminate.
NSArray* running_shim = [NSRunningApplication
runningApplicationsWithBundleIdentifier:bundle_id];
ASSERT_EQ(1u, [running_shim count]);
ns_observer.reset([[WindowedNSNotificationObserver alloc]
initForNotification:NSWorkspaceDidTerminateApplicationNotification
andBundleId:bundle_id]);
[base::mac::ObjCCastStrict<NSRunningApplication>(
[running_shim objectAtIndex:0]) terminate];
[ns_observer wait];
EXPECT_FALSE(GetFirstAppWindow());
EXPECT_FALSE(HasAppShimHost(profile(), app->id()));
}
}
} // namespace apps
...@@ -611,7 +611,11 @@ int ChromeAppModeStart(const app_mode::ChromeAppModeInfo* info) { ...@@ -611,7 +611,11 @@ int ChromeAppModeStart(const app_mode::ChromeAppModeInfo* info) {
main_message_loop.set_thread_name("MainThread"); main_message_loop.set_thread_name("MainThread");
base::PlatformThread::SetName("CrAppShimMain"); base::PlatformThread::SetName("CrAppShimMain");
if (pid == -1) { // In tests, launching Chrome does nothing, and we won't get a ping response,
// so just assume the socket exists.
if (pid == -1 &&
!CommandLine::ForCurrentProcess()->HasSwitch(
app_mode::kLaunchedForTest)) {
// Launch Chrome if it isn't already running. // Launch Chrome if it isn't already running.
ProcessSerialNumber psn; ProcessSerialNumber psn;
CommandLine command_line(CommandLine::NO_PROGRAM); CommandLine command_line(CommandLine::NO_PROGRAM);
......
...@@ -26,8 +26,6 @@ ...@@ -26,8 +26,6 @@
<string>@APP_MODE_SHORTCUT_URL@</string> <string>@APP_MODE_SHORTCUT_URL@</string>
<key>CrBundleIdentifier</key> <key>CrBundleIdentifier</key>
<string>@APP_MODE_BROWSER_BUNDLE_ID@</string> <string>@APP_MODE_BROWSER_BUNDLE_ID@</string>
<key>LSFileQuarantineEnabled</key>
<true/>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>${MACOSX_DEPLOYMENT_TARGET}.0</string> <string>${MACOSX_DEPLOYMENT_TARGET}.0</string>
<key>NSAppleScriptEnabled</key> <key>NSAppleScriptEnabled</key>
......
...@@ -14,6 +14,11 @@ ...@@ -14,6 +14,11 @@
#include "chrome/browser/web_applications/web_app.h" #include "chrome/browser/web_applications/web_app.h"
#include "extensions/common/manifest_handlers/file_handler_info.h" #include "extensions/common/manifest_handlers/file_handler_info.h"
// Whether to enable update and launch of app shims in tests. (Normally shims
// are never created or launched in tests). Note that update only creates
// internal shim bundles, i.e. it does not create new shims in ~/Applications.
extern bool g_app_shims_allow_update_and_launch_in_tests;
namespace web_app { namespace web_app {
// Returns the full path of the .app shim that would be created by // Returns the full path of the .app shim that would be created by
......
...@@ -47,6 +47,8 @@ ...@@ -47,6 +47,8 @@
#include "ui/base/resource/resource_bundle.h" #include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/image/image_family.h" #include "ui/gfx/image/image_family.h"
bool g_app_shims_allow_update_and_launch_in_tests = false;
namespace { namespace {
// Launch Services Key to run as an agent app, which doesn't launch in the dock. // Launch Services Key to run as an agent app, which doesn't launch in the dock.
...@@ -564,7 +566,11 @@ size_t WebAppShortcutCreator::CreateShortcutsIn( ...@@ -564,7 +566,11 @@ size_t WebAppShortcutCreator::CreateShortcutsIn(
return succeeded; return succeeded;
} }
// Remove the quarantine attribute from both the bundle and the executable.
base::mac::RemoveQuarantineAttribute(dst_path.Append(app_name)); base::mac::RemoveQuarantineAttribute(dst_path.Append(app_name));
base::mac::RemoveQuarantineAttribute(
dst_path.Append(app_name)
.Append("Contents").Append("MacOS").Append("app_mode_loader"));
++succeeded; ++succeeded;
} }
...@@ -863,8 +869,10 @@ base::FilePath GetAppInstallPath(const ShortcutInfo& shortcut_info) { ...@@ -863,8 +869,10 @@ base::FilePath GetAppInstallPath(const ShortcutInfo& shortcut_info) {
} }
void MaybeLaunchShortcut(const ShortcutInfo& shortcut_info) { void MaybeLaunchShortcut(const ShortcutInfo& shortcut_info) {
if (AppShimsDisabledForTest()) if (AppShimsDisabledForTest() &&
!g_app_shims_allow_update_and_launch_in_tests) {
return; return;
}
content::BrowserThread::PostTask( content::BrowserThread::PostTask(
content::BrowserThread::FILE, FROM_HERE, content::BrowserThread::FILE, FROM_HERE,
...@@ -976,8 +984,10 @@ void UpdatePlatformShortcuts( ...@@ -976,8 +984,10 @@ void UpdatePlatformShortcuts(
const ShortcutInfo& shortcut_info, const ShortcutInfo& shortcut_info,
const extensions::FileHandlersInfo& file_handlers_info) { const extensions::FileHandlersInfo& file_handlers_info) {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE)); DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
if (AppShimsDisabledForTest()) if (AppShimsDisabledForTest() &&
!g_app_shims_allow_update_and_launch_in_tests) {
return; return;
}
WebAppShortcutCreator shortcut_creator( WebAppShortcutCreator shortcut_creator(
app_data_path, shortcut_info, file_handlers_info); app_data_path, shortcut_info, file_handlers_info);
......
...@@ -44,6 +44,7 @@ ...@@ -44,6 +44,7 @@
'HAS_OUT_OF_PROC_TEST_RUNNER', 'HAS_OUT_OF_PROC_TEST_RUNNER',
], ],
'sources': [ 'sources': [
'../apps/app_shim/app_shim_interactive_uitest_mac.mm',
'../apps/app_shim/app_shim_quit_interactive_uitest_mac.mm', '../apps/app_shim/app_shim_quit_interactive_uitest_mac.mm',
'../apps/app_window_interactive_uitest.cc', '../apps/app_window_interactive_uitest.cc',
'../ui/base/clipboard/clipboard_unittest.cc', '../ui/base/clipboard/clipboard_unittest.cc',
......
...@@ -37,6 +37,10 @@ extern const char kAppListModeId[]; ...@@ -37,6 +37,10 @@ extern const char kAppListModeId[];
// launch_now = false. This associates the shim without launching the app. // launch_now = false. This associates the shim without launching the app.
extern const char kLaunchedByChromeProcessId[]; extern const char kLaunchedByChromeProcessId[];
// Indicates to the shim that it was launched for a test, so don't attempt to
// launch Chrome.
extern const char kLaunchedForTest[];
// Path to an app shim bundle. Indicates to Chrome that this shim attempted to // Path to an app shim bundle. Indicates to Chrome that this shim attempted to
// launch but failed. // launch but failed.
extern const char kAppShimError[]; extern const char kAppShimError[];
......
...@@ -14,6 +14,7 @@ const char kAppShimSocketSymlinkName[] = "App Shim Socket"; ...@@ -14,6 +14,7 @@ const char kAppShimSocketSymlinkName[] = "App Shim Socket";
const char kAppListModeId[] = "app_list"; const char kAppListModeId[] = "app_list";
const char kLaunchedByChromeProcessId[] = "launched-by-chrome-process-id"; const char kLaunchedByChromeProcessId[] = "launched-by-chrome-process-id";
const char kLaunchedForTest[] = "launched-for-test";
const char kAppShimError[] = "app-shim-error"; const char kAppShimError[] = "app-shim-error";
......
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