Check USB device path access when prompting users to select a device.
[chromium-blink-merge.git] / chrome / browser / apps / app_shim / app_shim_interactive_uitest_mac.mm
bloba17fb0100e61460e3b59ea30cc697fc91da97476
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import <Cocoa/Cocoa.h>
6 #include <vector>
8 #include "apps/app_lifetime_monitor_factory.h"
9 #include "apps/switches.h"
10 #include "base/auto_reset.h"
11 #include "base/callback.h"
12 #include "base/files/file_path_watcher.h"
13 #include "base/mac/foundation_util.h"
14 #include "base/mac/launch_services_util.h"
15 #include "base/mac/mac_util.h"
16 #include "base/mac/scoped_nsobject.h"
17 #include "base/path_service.h"
18 #include "base/process/launch.h"
19 #include "base/strings/sys_string_conversions.h"
20 #include "base/test/test_timeouts.h"
21 #include "chrome/browser/apps/app_browsertest_util.h"
22 #include "chrome/browser/apps/app_shim/app_shim_handler_mac.h"
23 #include "chrome/browser/apps/app_shim/app_shim_host_manager_mac.h"
24 #include "chrome/browser/apps/app_shim/extension_app_shim_handler_mac.h"
25 #include "chrome/browser/browser_process.h"
26 #include "chrome/browser/extensions/launch_util.h"
27 #include "chrome/browser/profiles/profile.h"
28 #include "chrome/browser/ui/browser_list.h"
29 #include "chrome/browser/ui/browser_window.h"
30 #include "chrome/browser/web_applications/web_app_mac.h"
31 #include "chrome/common/chrome_paths.h"
32 #include "chrome/common/chrome_switches.h"
33 #include "chrome/common/mac/app_mode_common.h"
34 #include "content/public/test/test_utils.h"
35 #include "extensions/browser/app_window/native_app_window.h"
36 #include "extensions/browser/extension_prefs.h"
37 #include "extensions/test/extension_test_message_listener.h"
38 #import "ui/events/test/cocoa_test_event_utils.h"
40 namespace {
42 // General end-to-end test for app shims.
43 class AppShimInteractiveTest : public extensions::PlatformAppBrowserTest {
44  protected:
45   AppShimInteractiveTest()
46       : auto_reset_(&g_app_shims_allow_update_and_launch_in_tests, true) {}
48   void SetUpCommandLine(base::CommandLine* command_line) override {
49     PlatformAppBrowserTest::SetUpCommandLine(command_line);
50     command_line->AppendSwitch(switches::kEnableNewBookmarkApps);
51   }
53  private:
54   // Temporarily enable app shims.
55   base::AutoReset<bool> auto_reset_;
57   DISALLOW_COPY_AND_ASSIGN(AppShimInteractiveTest);
60 // Watches for changes to a file. This is designed to be used from the the UI
61 // thread.
62 class WindowedFilePathWatcher
63     : public base::RefCountedThreadSafe<WindowedFilePathWatcher> {
64  public:
65   WindowedFilePathWatcher(const base::FilePath& path)
66       : path_(path), observed_(false) {
67     content::BrowserThread::PostTask(
68         content::BrowserThread::FILE,
69         FROM_HERE,
70         base::Bind(&WindowedFilePathWatcher::Watch, this));
71   }
73   void Wait() {
74     if (observed_)
75       return;
77     run_loop_.reset(new base::RunLoop);
78     run_loop_->Run();
79   }
81  protected:
82   friend class base::RefCountedThreadSafe<WindowedFilePathWatcher>;
83   virtual ~WindowedFilePathWatcher() {}
85   void Watch() {
86     watcher_.reset(new base::FilePathWatcher);
87     watcher_->Watch(path_.DirName(),
88                     false,
89                     base::Bind(&WindowedFilePathWatcher::Observe, this));
90   }
92   void Observe(const base::FilePath& path, bool error) {
93     DCHECK(!error);
94     if (base::PathExists(path_)) {
95       watcher_.reset();
96       content::BrowserThread::PostTask(
97           content::BrowserThread::UI,
98           FROM_HERE,
99           base::Bind(&WindowedFilePathWatcher::StopRunLoop, this));
100     }
101   }
103   void StopRunLoop() {
104     observed_ = true;
105     if (run_loop_.get())
106       run_loop_->Quit();
107   }
109  private:
110   const base::FilePath path_;
111   scoped_ptr<base::FilePathWatcher> watcher_;
112   bool observed_;
113   scoped_ptr<base::RunLoop> run_loop_;
115   DISALLOW_COPY_AND_ASSIGN(WindowedFilePathWatcher);
118 // Watches for an app shim to connect.
119 class WindowedAppShimLaunchObserver : public apps::AppShimHandler {
120  public:
121   WindowedAppShimLaunchObserver(const std::string& app_id)
122       : app_mode_id_(app_id),
123         observed_(false) {
124     apps::AppShimHandler::RegisterHandler(app_id, this);
125   }
127   void Wait() {
128     if (observed_)
129       return;
131     run_loop_.reset(new base::RunLoop);
132     run_loop_->Run();
133   }
135   // AppShimHandler overrides:
136   void OnShimLaunch(Host* host,
137                     apps::AppShimLaunchType launch_type,
138                     const std::vector<base::FilePath>& files) override {
139     // Remove self and pass through to the default handler.
140     apps::AppShimHandler::RemoveHandler(app_mode_id_);
141     apps::AppShimHandler::GetForAppMode(app_mode_id_)
142         ->OnShimLaunch(host, launch_type, files);
143     observed_ = true;
144     if (run_loop_.get())
145       run_loop_->Quit();
146   }
147   void OnShimClose(Host* host) override {}
148   void OnShimFocus(Host* host,
149                    apps::AppShimFocusType focus_type,
150                    const std::vector<base::FilePath>& files) override {}
151   void OnShimSetHidden(Host* host, bool hidden) override {}
152   void OnShimQuit(Host* host) override {}
154  private:
155   std::string app_mode_id_;
156   bool observed_;
157   scoped_ptr<base::RunLoop> run_loop_;
159   DISALLOW_COPY_AND_ASSIGN(WindowedAppShimLaunchObserver);
162 // Watches for a hosted app browser window to open.
163 class HostedAppBrowserListObserver : public chrome::BrowserListObserver {
164  public:
165   explicit HostedAppBrowserListObserver(const std::string& app_id)
166       : app_id_(app_id), observed_add_(false), observed_removed_(false) {
167     BrowserList::AddObserver(this);
168   }
170   ~HostedAppBrowserListObserver() override {
171     BrowserList::RemoveObserver(this);
172   }
174   void WaitUntilAdded() {
175     if (observed_add_)
176       return;
178     run_loop_.reset(new base::RunLoop);
179     run_loop_->Run();
180   }
182   void WaitUntilRemoved() {
183     if (observed_removed_)
184       return;
186     run_loop_.reset(new base::RunLoop);
187     run_loop_->Run();
188   }
190   // BrowserListObserver overrides:
191   void OnBrowserAdded(Browser* browser) override {
192     const extensions::Extension* app =
193         apps::ExtensionAppShimHandler::GetAppForBrowser(browser);
194     if (app && app->id() == app_id_) {
195       observed_add_ = true;
196       if (run_loop_.get())
197         run_loop_->Quit();
198     }
199   }
201   void OnBrowserRemoved(Browser* browser) override {
202     const extensions::Extension* app =
203         apps::ExtensionAppShimHandler::GetAppForBrowser(browser);
204     if (app && app->id() == app_id_) {
205       observed_removed_ = true;
206       if (run_loop_.get())
207         run_loop_->Quit();
208     }
209   }
211  private:
212   std::string app_id_;
213   bool observed_add_;
214   bool observed_removed_;
215   scoped_ptr<base::RunLoop> run_loop_;
217   DISALLOW_COPY_AND_ASSIGN(HostedAppBrowserListObserver);
220 class AppLifetimeMonitorObserver : public apps::AppLifetimeMonitor::Observer {
221  public:
222   AppLifetimeMonitorObserver(Profile* profile)
223       : profile_(profile), activated_count_(0), deactivated_count_(0) {
224     apps::AppLifetimeMonitorFactory::GetForProfile(profile_)->AddObserver(this);
225   }
226   ~AppLifetimeMonitorObserver() override {
227     apps::AppLifetimeMonitorFactory::GetForProfile(profile_)
228         ->RemoveObserver(this);
229   }
231   int activated_count() { return activated_count_; }
232   int deactivated_count() { return deactivated_count_; }
234  protected:
235   // AppLifetimeMonitor::Observer overrides:
236   void OnAppActivated(Profile* profile, const std::string& app_id) override {
237     ++activated_count_;
238   }
239   void OnAppDeactivated(Profile* profile, const std::string& app_id) override {
240     ++deactivated_count_;
241   }
243  private:
244   Profile* profile_;
245   int activated_count_;
246   int deactivated_count_;
248   DISALLOW_COPY_AND_ASSIGN(AppLifetimeMonitorObserver);
251 NSString* GetBundleID(const base::FilePath& shim_path) {
252   base::FilePath plist_path = shim_path.Append("Contents").Append("Info.plist");
253   NSMutableDictionary* plist = [NSMutableDictionary
254       dictionaryWithContentsOfFile:base::mac::FilePathToNSString(plist_path)];
255   return [plist objectForKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)];
258 bool HasAppShimHost(Profile* profile, const std::string& app_id) {
259   return g_browser_process->platform_part()
260       ->app_shim_host_manager()
261       ->extension_app_shim_handler()
262       ->FindHost(profile, app_id);
265 base::FilePath GetAppShimPath(Profile* profile,
266                               const extensions::Extension* app) {
267   // Use a WebAppShortcutCreator to get the path.
268   web_app::WebAppShortcutCreator shortcut_creator(
269       web_app::GetWebAppDataDirectory(profile->GetPath(), app->id(), GURL()),
270       web_app::ShortcutInfoForExtensionAndProfile(app, profile),
271       extensions::FileHandlersInfo());
272   return shortcut_creator.GetInternalShortcutPath();
275 void UpdateAppAndAwaitShimCreation(Profile* profile,
276                                    const extensions::Extension* app,
277                                    const base::FilePath& shim_path) {
278   // Create the internal app shim by simulating an app update. FilePathWatcher
279   // is used to wait for file operations on the shim to be finished before
280   // attempting to launch it. Since all of the file operations are done in the
281   // same event on the FILE thread, everything will be done by the time the
282   // watcher's callback is executed.
283   scoped_refptr<WindowedFilePathWatcher> file_watcher =
284       new WindowedFilePathWatcher(shim_path);
285   web_app::UpdateAllShortcuts(base::string16(), profile, app);
286   file_watcher->Wait();
289 Browser* GetFirstHostedAppWindow() {
290   BrowserList* browsers =
291       BrowserList::GetInstance(chrome::HOST_DESKTOP_TYPE_NATIVE);
292   for (Browser* browser : *browsers) {
293     const extensions::Extension* extension =
294         apps::ExtensionAppShimHandler::GetAppForBrowser(browser);
295     if (extension && extension->is_hosted_app())
296       return browser;
297   }
298   return nullptr;
301 }  // namespace
303 // Watches for NSNotifications from the shared workspace.
304 @interface WindowedNSNotificationObserver : NSObject {
305  @private
306   base::scoped_nsobject<NSString> bundleId_;
307   BOOL notificationReceived_;
308   scoped_ptr<base::RunLoop> runLoop_;
311 - (id)initForNotification:(NSString*)name
312               andBundleId:(NSString*)bundleId;
313 - (void)observe:(NSNotification*)notification;
314 - (void)wait;
315 @end
317 @implementation WindowedNSNotificationObserver
319 - (id)initForNotification:(NSString*)name
320               andBundleId:(NSString*)bundleId {
321   if (self = [super init]) {
322     bundleId_.reset([[bundleId copy] retain]);
323     [[[NSWorkspace sharedWorkspace] notificationCenter]
324         addObserver:self
325            selector:@selector(observe:)
326                name:name
327              object:nil];
328   }
329   return self;
332 - (void)observe:(NSNotification*)notification {
333   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
335   NSRunningApplication* application =
336       [[notification userInfo] objectForKey:NSWorkspaceApplicationKey];
337   if (![[application bundleIdentifier] isEqualToString:bundleId_])
338     return;
340   [[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:self];
341   notificationReceived_ = YES;
342   if (runLoop_.get())
343     runLoop_->Quit();
346 - (void)wait {
347   if (notificationReceived_)
348     return;
350   runLoop_.reset(new base::RunLoop);
351   runLoop_->Run();
354 @end
356 namespace apps {
358 // Shims require static libraries http://crbug.com/386024.
359 #if defined(COMPONENT_BUILD)
360 #define MAYBE_Launch DISABLED_Launch
361 #define MAYBE_HostedAppLaunch DISABLED_HostedAppLaunch
362 #define MAYBE_ShowWindow DISABLED_ShowWindow
363 #define MAYBE_RebuildShim DISABLED_RebuildShim
364 #else
365 #define MAYBE_Launch Launch
366 #define MAYBE_HostedAppLaunch HostedAppLaunch
367 #define MAYBE_ShowWindow ShowWindow
368 #define MAYBE_RebuildShim RebuildShim
369 #endif
371 IN_PROC_BROWSER_TEST_F(AppShimInteractiveTest, MAYBE_HostedAppLaunch) {
372   const extensions::Extension* app = InstallHostedApp();
374   base::FilePath shim_path = GetAppShimPath(profile(), app);
375   EXPECT_FALSE(base::PathExists(shim_path));
377   UpdateAppAndAwaitShimCreation(profile(), app, shim_path);
378   ASSERT_TRUE(base::PathExists(shim_path));
379   NSString* bundle_id = GetBundleID(shim_path);
381   // Explicitly set the launch type to open in a new window.
382   extensions::SetLaunchType(profile(), app->id(),
383                             extensions::LAUNCH_TYPE_WINDOW);
385   // Case 1: Launch the hosted app, it should start the shim.
386   {
387     base::scoped_nsobject<WindowedNSNotificationObserver> ns_observer;
388     ns_observer.reset([[WindowedNSNotificationObserver alloc]
389         initForNotification:NSWorkspaceDidLaunchApplicationNotification
390                 andBundleId:bundle_id]);
391     WindowedAppShimLaunchObserver observer(app->id());
392     LaunchHostedApp(app);
393     [ns_observer wait];
394     observer.Wait();
396     EXPECT_TRUE(HasAppShimHost(profile(), app->id()));
397     EXPECT_TRUE(GetFirstHostedAppWindow());
399     NSArray* running_shim = [NSRunningApplication
400         runningApplicationsWithBundleIdentifier:bundle_id];
401     ASSERT_EQ(1u, [running_shim count]);
403     ns_observer.reset([[WindowedNSNotificationObserver alloc]
404         initForNotification:NSWorkspaceDidTerminateApplicationNotification
405                 andBundleId:bundle_id]);
406     [base::mac::ObjCCastStrict<NSRunningApplication>(
407         [running_shim objectAtIndex:0]) terminate];
408     [ns_observer wait];
410     EXPECT_FALSE(GetFirstHostedAppWindow());
411     EXPECT_FALSE(HasAppShimHost(profile(), app->id()));
412   }
414   // Case 2: Launch the shim, it should start the hosted app.
415   {
416     HostedAppBrowserListObserver listener(app->id());
417     base::CommandLine shim_cmdline(base::CommandLine::NO_PROGRAM);
418     shim_cmdline.AppendSwitch(app_mode::kLaunchedForTest);
419     ProcessSerialNumber shim_psn;
420     ASSERT_TRUE(base::mac::OpenApplicationWithPath(
421         shim_path, shim_cmdline, kLSLaunchDefaults, &shim_psn));
422     listener.WaitUntilAdded();
424     ASSERT_TRUE(GetFirstHostedAppWindow());
425     EXPECT_TRUE(HasAppShimHost(profile(), app->id()));
427     // If the window is closed, the shim should quit.
428     pid_t shim_pid;
429     EXPECT_EQ(noErr, GetProcessPID(&shim_psn, &shim_pid));
430     GetFirstHostedAppWindow()->window()->Close();
431     // Wait for the window to be closed.
432     listener.WaitUntilRemoved();
433     base::Process shim_process(shim_pid);
434     int exit_code;
435     ASSERT_TRUE(shim_process.WaitForExitWithTimeout(
436                     TestTimeouts::action_timeout(), &exit_code));
438     EXPECT_FALSE(GetFirstHostedAppWindow());
439     EXPECT_FALSE(HasAppShimHost(profile(), app->id()));
440   }
443 // Test that launching the shim for an app starts the app, and vice versa.
444 // These two cases are combined because the time to run the test is dominated
445 // by loading the extension and creating the shim.
446 IN_PROC_BROWSER_TEST_F(AppShimInteractiveTest, MAYBE_Launch) {
447   const extensions::Extension* app = InstallPlatformApp("minimal");
449   base::FilePath shim_path = GetAppShimPath(profile(), app);
450   EXPECT_FALSE(base::PathExists(shim_path));
452   UpdateAppAndAwaitShimCreation(profile(), app, shim_path);
453   ASSERT_TRUE(base::PathExists(shim_path));
454   NSString* bundle_id = GetBundleID(shim_path);
456   // Case 1: Launch the app, it should start the shim.
457   {
458     base::scoped_nsobject<WindowedNSNotificationObserver> ns_observer;
459     ns_observer.reset([[WindowedNSNotificationObserver alloc]
460         initForNotification:NSWorkspaceDidLaunchApplicationNotification
461                 andBundleId:bundle_id]);
462     WindowedAppShimLaunchObserver observer(app->id());
463     LaunchPlatformApp(app);
464     [ns_observer wait];
465     observer.Wait();
467     EXPECT_TRUE(GetFirstAppWindow());
468     EXPECT_TRUE(HasAppShimHost(profile(), app->id()));
470     // Quitting the shim will eventually cause it to quit. It actually
471     // intercepts the -terminate, sends an AppShimHostMsg_QuitApp to Chrome,
472     // and returns NSTerminateLater. Chrome responds by closing all windows of
473     // the app. Once all windows are closed, Chrome closes the IPC channel,
474     // which causes the shim to actually terminate.
475     NSArray* running_shim = [NSRunningApplication
476         runningApplicationsWithBundleIdentifier:bundle_id];
477     ASSERT_EQ(1u, [running_shim count]);
479     ns_observer.reset([[WindowedNSNotificationObserver alloc]
480         initForNotification:NSWorkspaceDidTerminateApplicationNotification
481                 andBundleId:bundle_id]);
482     [base::mac::ObjCCastStrict<NSRunningApplication>(
483         [running_shim objectAtIndex:0]) terminate];
484     [ns_observer wait];
486     EXPECT_FALSE(GetFirstAppWindow());
487     EXPECT_FALSE(HasAppShimHost(profile(), app->id()));
488   }
490   // Case 2: Launch the shim, it should start the app.
491   {
492     ExtensionTestMessageListener launched_listener("Launched", false);
493     base::CommandLine shim_cmdline(base::CommandLine::NO_PROGRAM);
494     shim_cmdline.AppendSwitch(app_mode::kLaunchedForTest);
495     ProcessSerialNumber shim_psn;
496     ASSERT_TRUE(base::mac::OpenApplicationWithPath(
497         shim_path, shim_cmdline, kLSLaunchDefaults, &shim_psn));
498     ASSERT_TRUE(launched_listener.WaitUntilSatisfied());
500     ASSERT_TRUE(GetFirstAppWindow());
501     EXPECT_TRUE(HasAppShimHost(profile(), app->id()));
503     // If the window is closed, the shim should quit.
504     pid_t shim_pid;
505     EXPECT_EQ(noErr, GetProcessPID(&shim_psn, &shim_pid));
506     GetFirstAppWindow()->GetBaseWindow()->Close();
507     base::Process shim_process(shim_pid);
508     int exit_code;
509     ASSERT_TRUE(shim_process.WaitForExitWithTimeout(
510                     TestTimeouts::action_timeout(), &exit_code));
512     EXPECT_FALSE(GetFirstAppWindow());
513     EXPECT_FALSE(HasAppShimHost(profile(), app->id()));
514   }
517 // Test that the shim's lifetime depends on the visibility of windows. I.e. the
518 // shim is only active when there are visible windows.
519 IN_PROC_BROWSER_TEST_F(AppShimInteractiveTest, MAYBE_ShowWindow) {
520   const extensions::Extension* app = InstallPlatformApp("hidden");
522   base::FilePath shim_path = GetAppShimPath(profile(), app);
523   EXPECT_FALSE(base::PathExists(shim_path));
525   UpdateAppAndAwaitShimCreation(profile(), app, shim_path);
526   ASSERT_TRUE(base::PathExists(shim_path));
527   NSString* bundle_id = GetBundleID(shim_path);
529   // It's impractical to confirm that the shim did not launch by timing out, so
530   // instead we watch AppLifetimeMonitor::Observer::OnAppActivated.
531   AppLifetimeMonitorObserver lifetime_observer(profile());
533   // Launch the app. It should create a hidden window, but the shim should not
534   // launch.
535   {
536     ExtensionTestMessageListener launched_listener("Launched", false);
537     LaunchPlatformApp(app);
538     EXPECT_TRUE(launched_listener.WaitUntilSatisfied());
539   }
540   extensions::AppWindow* window_1 = GetFirstAppWindow();
541   ASSERT_TRUE(window_1);
542   EXPECT_TRUE(window_1->is_hidden());
543   EXPECT_FALSE(HasAppShimHost(profile(), app->id()));
544   EXPECT_EQ(0, lifetime_observer.activated_count());
546   // Showing the window causes the shim to launch.
547   {
548     base::scoped_nsobject<WindowedNSNotificationObserver> ns_observer(
549         [[WindowedNSNotificationObserver alloc]
550             initForNotification:NSWorkspaceDidLaunchApplicationNotification
551                     andBundleId:bundle_id]);
552     WindowedAppShimLaunchObserver observer(app->id());
553     window_1->Show(extensions::AppWindow::SHOW_INACTIVE);
554     [ns_observer wait];
555     observer.Wait();
556     EXPECT_EQ(1, lifetime_observer.activated_count());
557     EXPECT_TRUE(HasAppShimHost(profile(), app->id()));
558   }
560   // Hiding the window causes the shim to quit.
561   {
562     base::scoped_nsobject<WindowedNSNotificationObserver> ns_observer(
563         [[WindowedNSNotificationObserver alloc]
564             initForNotification:NSWorkspaceDidTerminateApplicationNotification
565                     andBundleId:bundle_id]);
566     window_1->Hide();
567     [ns_observer wait];
568     EXPECT_FALSE(HasAppShimHost(profile(), app->id()));
569   }
571   // Launch a second window. It should not launch the shim.
572   {
573     ExtensionTestMessageListener launched_listener("Launched", false);
574     LaunchPlatformApp(app);
575     EXPECT_TRUE(launched_listener.WaitUntilSatisfied());
576   }
577   const extensions::AppWindowRegistry::AppWindowList& app_windows =
578       extensions::AppWindowRegistry::Get(profile())->app_windows();
579   EXPECT_EQ(2u, app_windows.size());
580   extensions::AppWindow* window_2 = app_windows.front();
581   EXPECT_NE(window_1, window_2);
582   ASSERT_TRUE(window_2);
583   EXPECT_TRUE(window_2->is_hidden());
584   EXPECT_FALSE(HasAppShimHost(profile(), app->id()));
585   EXPECT_EQ(1, lifetime_observer.activated_count());
587   // Showing one of the windows should launch the shim.
588   {
589     base::scoped_nsobject<WindowedNSNotificationObserver> ns_observer(
590         [[WindowedNSNotificationObserver alloc]
591             initForNotification:NSWorkspaceDidLaunchApplicationNotification
592                     andBundleId:bundle_id]);
593     WindowedAppShimLaunchObserver observer(app->id());
594     window_1->Show(extensions::AppWindow::SHOW_INACTIVE);
595     [ns_observer wait];
596     observer.Wait();
597     EXPECT_EQ(2, lifetime_observer.activated_count());
598     EXPECT_TRUE(HasAppShimHost(profile(), app->id()));
599     EXPECT_TRUE(window_2->is_hidden());
600   }
602   // Showing the other window does nothing.
603   {
604     window_2->Show(extensions::AppWindow::SHOW_INACTIVE);
605     EXPECT_EQ(2, lifetime_observer.activated_count());
606   }
608   // Showing an already visible window does nothing.
609   {
610     window_1->Show(extensions::AppWindow::SHOW_INACTIVE);
611     EXPECT_EQ(2, lifetime_observer.activated_count());
612   }
614   // Hiding one window does nothing.
615   {
616     AppLifetimeMonitorObserver deactivate_observer(profile());
617     window_1->Hide();
618     EXPECT_EQ(0, deactivate_observer.deactivated_count());
619   }
621   // Hiding other window causes the shim to quit.
622   {
623     AppLifetimeMonitorObserver deactivate_observer(profile());
624     EXPECT_TRUE(HasAppShimHost(profile(), app->id()));
625     base::scoped_nsobject<WindowedNSNotificationObserver> ns_observer(
626         [[WindowedNSNotificationObserver alloc]
627             initForNotification:NSWorkspaceDidTerminateApplicationNotification
628                     andBundleId:bundle_id]);
629     window_2->Hide();
630     [ns_observer wait];
631     EXPECT_EQ(1, deactivate_observer.deactivated_count());
632     EXPECT_FALSE(HasAppShimHost(profile(), app->id()));
633   }
636 #if defined(ARCH_CPU_64_BITS)
638 // Tests that a 32 bit shim attempting to launch 64 bit Chrome will eventually
639 // be rebuilt.
640 IN_PROC_BROWSER_TEST_F(AppShimInteractiveTest, MAYBE_RebuildShim) {
641   // Get the 32 bit shim.
642   base::FilePath test_data_dir;
643   PathService::Get(chrome::DIR_TEST_DATA, &test_data_dir);
644   base::FilePath shim_path_32 =
645       test_data_dir.Append("app_shim").Append("app_shim_32_bit.app");
646   EXPECT_TRUE(base::PathExists(shim_path_32));
648   // Install test app.
649   const extensions::Extension* app = InstallPlatformApp("minimal");
651   // Use WebAppShortcutCreator to create a 64 bit shim.
652   web_app::WebAppShortcutCreator shortcut_creator(
653       web_app::GetWebAppDataDirectory(profile()->GetPath(), app->id(), GURL()),
654       web_app::ShortcutInfoForExtensionAndProfile(app, profile()),
655       extensions::FileHandlersInfo());
656   shortcut_creator.UpdateShortcuts();
657   base::FilePath shim_path = shortcut_creator.GetInternalShortcutPath();
658   NSMutableDictionary* plist_64 = [NSMutableDictionary
659       dictionaryWithContentsOfFile:base::mac::FilePathToNSString(
660           shim_path.Append("Contents").Append("Info.plist"))];
662   // Copy 32 bit shim to where it's expected to be.
663   // CopyDirectory doesn't seem to work when copying and renaming in one go.
664   ASSERT_TRUE(base::DeleteFile(shim_path, true));
665   ASSERT_TRUE(base::PathExists(shim_path.DirName()));
666   ASSERT_TRUE(base::CopyDirectory(shim_path_32, shim_path.DirName(), true));
667   ASSERT_TRUE(base::Move(shim_path.DirName().Append(shim_path_32.BaseName()),
668                          shim_path));
669   ASSERT_TRUE(base::PathExists(
670       shim_path.Append("Contents").Append("MacOS").Append("app_mode_loader")));
672   // Fix up the plist so that it matches the installed test app.
673   NSString* plist_path = base::mac::FilePathToNSString(
674       shim_path.Append("Contents").Append("Info.plist"));
675   NSMutableDictionary* plist =
676       [NSMutableDictionary dictionaryWithContentsOfFile:plist_path];
678   NSArray* keys_to_copy = @[
679     base::mac::CFToNSCast(kCFBundleIdentifierKey),
680     base::mac::CFToNSCast(kCFBundleNameKey),
681     app_mode::kCrAppModeShortcutIDKey,
682     app_mode::kCrAppModeUserDataDirKey,
683     app_mode::kBrowserBundleIDKey
684   ];
685   for (NSString* key in keys_to_copy) {
686     [plist setObject:[plist_64 objectForKey:key]
687               forKey:key];
688   }
689   [plist writeToFile:plist_path
690           atomically:YES];
692   base::mac::RemoveQuarantineAttribute(shim_path);
694   // Launch the shim, it should start the app and ultimately connect over IPC.
695   // This actually happens in multiple launches of the shim:
696   // (1) The shim will fail and instead launch Chrome with --app-id so that the
697   //     app starts.
698   // (2) Chrome launches the shim in response to an app starting, this time the
699   //     shim launches Chrome with --app-shim-error, which causes Chrome to
700   //     rebuild the shim.
701   // (3) After rebuilding, Chrome again launches the shim and expects it to
702   //     behave normally.
703   ExtensionTestMessageListener launched_listener("Launched", false);
704   base::CommandLine shim_cmdline(base::CommandLine::NO_PROGRAM);
705   ASSERT_TRUE(base::mac::OpenApplicationWithPath(
706       shim_path, shim_cmdline, kLSLaunchDefaults, NULL));
708   // Wait for the app to start (1). At this point there is no shim host.
709   ASSERT_TRUE(launched_listener.WaitUntilSatisfied());
710   EXPECT_FALSE(HasAppShimHost(profile(), app->id()));
712   // Wait for the rebuilt shim to connect (3). This does not race with the app
713   // starting (1) because Chrome only launches the shim (2) after the app
714   // starts. Then Chrome must handle --app-shim-error on the UI thread before
715   // the shim is rebuilt.
716   WindowedAppShimLaunchObserver(app->id()).Wait();
718   EXPECT_TRUE(GetFirstAppWindow());
719   EXPECT_TRUE(HasAppShimHost(profile(), app->id()));
722 #endif  // defined(ARCH_CPU_64_BITS)
724 }  // namespace apps