summaryrefslogtreecommitdiffstats
path: root/apps/app_shim/app_shim_interactive_uitest_mac.mm
blob: 1c4cc114ae83c5737464c4d9bd2d2eee8a32ff76 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
// 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 {

// 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, 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