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.
4
5#include "extensions/browser/app_window/app_window_geometry_cache.h"
6
7#include "base/files/file_path.h"
8#include "base/memory/scoped_ptr.h"
9#include "base/prefs/mock_pref_change_callback.h"
10#include "base/prefs/pref_service_factory.h"
11#include "base/prefs/testing_pref_store.h"
12#include "base/strings/string_number_conversions.h"
13#include "components/pref_registry/pref_registry_syncable.h"
14#include "content/public/test/test_browser_context.h"
15#include "content/public/test/test_browser_thread.h"
16#include "content/public/test/test_utils.h"
17#include "extensions/browser/extension_pref_value_map.h"
18#include "extensions/browser/extension_prefs.h"
19#include "extensions/browser/extensions_test.h"
20#include "extensions/browser/null_app_sorting.h"
21#include "extensions/common/extension_builder.h"
22#include "extensions/common/value_builder.h"
23#include "testing/gtest/include/gtest/gtest.h"
24
25using content::BrowserThread;
26
27namespace extensions {
28
29namespace {
30const char kWindowId[] = "windowid";
31const char kWindowId2[] = "windowid2";
32
33// Create a very simple extension with id.
34scoped_refptr<Extension> CreateExtension(const std::string& id) {
35  return ExtensionBuilder()
36      .SetManifest(DictionaryBuilder().Set("name", "test").Set(
37          "version", "0.1"))
38      .SetID(id)
39      .Build();
40}
41
42}  // namespace
43
44// Base class for tests.
45class AppWindowGeometryCacheTest : public ExtensionsTest {
46 public:
47  AppWindowGeometryCacheTest()
48      : ui_thread_(BrowserThread::UI, &ui_message_loop_) {}
49
50  // testing::Test overrides:
51  virtual void SetUp() OVERRIDE;
52  virtual void TearDown() OVERRIDE;
53
54  void AddGeometryAndLoadExtension(const std::string& extension_id,
55                                   const std::string& window_id,
56                                   const gfx::Rect& bounds,
57                                   const gfx::Rect& screen_bounds,
58                                   ui::WindowShowState state);
59
60  // Spins the UI threads' message loops to make sure any task
61  // posted to sync the geometry to the value store gets a chance to run.
62  void WaitForSync();
63
64  void LoadExtension(const std::string& extension_id);
65  void UnloadExtension(const std::string& extension_id);
66
67  // Creates and adds an extension with associated prefs. Returns the extension
68  // ID.
69  std::string AddExtensionWithPrefs(const std::string& name);
70
71 protected:
72  base::MessageLoopForUI ui_message_loop_;
73  content::TestBrowserThread ui_thread_;
74  scoped_ptr<ExtensionPrefValueMap> extension_pref_value_map_;
75  scoped_ptr<PrefService> pref_service_;
76  scoped_ptr<ExtensionPrefs> extension_prefs_;
77  scoped_ptr<AppWindowGeometryCache> cache_;
78};
79
80void AppWindowGeometryCacheTest::SetUp() {
81  ExtensionsTest::SetUp();
82
83  // Set up all the dependencies of ExtensionPrefs.
84  extension_pref_value_map_.reset(new ExtensionPrefValueMap);
85  base::PrefServiceFactory factory;
86  factory.set_user_prefs(new TestingPrefStore);
87  factory.set_extension_prefs(new TestingPrefStore);
88  user_prefs::PrefRegistrySyncable* pref_registry =
89      new user_prefs::PrefRegistrySyncable;
90  // Prefs should be registered before the PrefService is created.
91  ExtensionPrefs::RegisterProfilePrefs(pref_registry);
92  pref_service_ = factory.Create(pref_registry).Pass();
93
94  extension_prefs_.reset(ExtensionPrefs::Create(
95      pref_service_.get(),
96      browser_context()->GetPath().AppendASCII("Extensions"),
97      extension_pref_value_map_.get(),
98      scoped_ptr<AppSorting>(new NullAppSorting),
99      false /* extensions_disabled */,
100      std::vector<ExtensionPrefsObserver*>()));
101
102  cache_.reset(
103      new AppWindowGeometryCache(browser_context(), extension_prefs_.get()));
104  cache_->SetSyncDelayForTests(0);
105}
106
107void AppWindowGeometryCacheTest::TearDown() {
108  cache_.reset();
109  extension_prefs_.reset();
110  pref_service_.reset();
111  extension_pref_value_map_.reset();
112
113  ExtensionsTest::TearDown();
114}
115
116void AppWindowGeometryCacheTest::AddGeometryAndLoadExtension(
117    const std::string& extension_id,
118    const std::string& window_id,
119    const gfx::Rect& bounds,
120    const gfx::Rect& screen_bounds,
121    ui::WindowShowState state) {
122  scoped_ptr<base::DictionaryValue> dict(new base::DictionaryValue);
123  base::DictionaryValue* value = new base::DictionaryValue;
124  value->SetInteger("x", bounds.x());
125  value->SetInteger("y", bounds.y());
126  value->SetInteger("w", bounds.width());
127  value->SetInteger("h", bounds.height());
128  value->SetInteger("screen_bounds_x", screen_bounds.x());
129  value->SetInteger("screen_bounds_y", screen_bounds.y());
130  value->SetInteger("screen_bounds_w", screen_bounds.width());
131  value->SetInteger("screen_bounds_h", screen_bounds.height());
132  value->SetInteger("state", state);
133  dict->SetWithoutPathExpansion(window_id, value);
134  extension_prefs_->SetGeometryCache(extension_id, dict.Pass());
135  LoadExtension(extension_id);
136}
137
138void AppWindowGeometryCacheTest::WaitForSync() {
139  content::RunAllPendingInMessageLoop();
140}
141
142void AppWindowGeometryCacheTest::LoadExtension(
143    const std::string& extension_id) {
144  cache_->LoadGeometryFromStorage(extension_id);
145  WaitForSync();
146}
147
148void AppWindowGeometryCacheTest::UnloadExtension(
149    const std::string& extension_id) {
150  scoped_refptr<Extension> extension = CreateExtension(extension_id);
151  cache_->OnExtensionUnloaded(browser_context(),
152                              extension.get(),
153                              UnloadedExtensionInfo::REASON_DISABLE);
154  WaitForSync();
155}
156
157std::string AppWindowGeometryCacheTest::AddExtensionWithPrefs(
158    const std::string& name) {
159  // Generate the extension with a path based on the name so that extensions
160  // with different names will have different IDs.
161  base::FilePath path =
162      browser_context()->GetPath().AppendASCII("Extensions").AppendASCII(name);
163  scoped_refptr<Extension> extension =
164      ExtensionBuilder()
165          .SetManifest(
166               DictionaryBuilder().Set("name", "test").Set("version", "0.1"))
167          .SetPath(path)
168          .Build();
169
170  extension_prefs_->OnExtensionInstalled(
171      extension.get(),
172      Extension::ENABLED,
173      syncer::StringOrdinal::CreateInitialOrdinal(),
174      std::string());
175  return extension->id();
176}
177
178// Test getting geometry from an empty store.
179TEST_F(AppWindowGeometryCacheTest, GetGeometryEmptyStore) {
180  const std::string extension_id = AddExtensionWithPrefs("ext1");
181  ASSERT_FALSE(cache_->GetGeometry(extension_id, kWindowId, NULL, NULL, NULL));
182}
183
184// Test getting geometry for an unknown extension.
185TEST_F(AppWindowGeometryCacheTest, GetGeometryUnkownExtension) {
186  const std::string extension_id1 = AddExtensionWithPrefs("ext1");
187  const std::string extension_id2 = AddExtensionWithPrefs("ext2");
188  AddGeometryAndLoadExtension(extension_id1,
189                              kWindowId,
190                              gfx::Rect(4, 5, 31, 43),
191                              gfx::Rect(0, 0, 1600, 900),
192                              ui::SHOW_STATE_NORMAL);
193  ASSERT_FALSE(cache_->GetGeometry(extension_id2, kWindowId, NULL, NULL, NULL));
194}
195
196// Test getting geometry for an unknown window in a known extension.
197TEST_F(AppWindowGeometryCacheTest, GetGeometryUnkownWindow) {
198  const std::string extension_id = AddExtensionWithPrefs("ext1");
199  AddGeometryAndLoadExtension(extension_id,
200                              kWindowId,
201                              gfx::Rect(4, 5, 31, 43),
202                              gfx::Rect(0, 0, 1600, 900),
203                              ui::SHOW_STATE_NORMAL);
204  ASSERT_FALSE(cache_->GetGeometry(extension_id, kWindowId2, NULL, NULL, NULL));
205}
206
207// Test that loading geometry, screen_bounds and state from the store works
208// correctly.
209TEST_F(AppWindowGeometryCacheTest, GetGeometryAndStateFromStore) {
210  const std::string extension_id = AddExtensionWithPrefs("ext1");
211  gfx::Rect bounds(4, 5, 31, 43);
212  gfx::Rect screen_bounds(0, 0, 1600, 900);
213  ui::WindowShowState state = ui::SHOW_STATE_NORMAL;
214  AddGeometryAndLoadExtension(
215      extension_id, kWindowId, bounds, screen_bounds, state);
216  gfx::Rect new_bounds;
217  gfx::Rect new_screen_bounds;
218  ui::WindowShowState new_state = ui::SHOW_STATE_DEFAULT;
219  ASSERT_TRUE(cache_->GetGeometry(
220      extension_id, kWindowId, &new_bounds, &new_screen_bounds, &new_state));
221  ASSERT_EQ(bounds, new_bounds);
222  ASSERT_EQ(screen_bounds, new_screen_bounds);
223  ASSERT_EQ(state, new_state);
224}
225
226// Test corrupt bounds will not be loaded.
227TEST_F(AppWindowGeometryCacheTest, CorruptBounds) {
228  const std::string extension_id = AddExtensionWithPrefs("ext1");
229  gfx::Rect bounds;
230  gfx::Rect screen_bounds(0, 0, 1600, 900);
231  ui::WindowShowState state = ui::SHOW_STATE_NORMAL;
232  AddGeometryAndLoadExtension(
233      extension_id, kWindowId, bounds, screen_bounds, state);
234  gfx::Rect new_bounds;
235  gfx::Rect new_screen_bounds;
236  ui::WindowShowState new_state = ui::SHOW_STATE_DEFAULT;
237  ASSERT_FALSE(cache_->GetGeometry(
238      extension_id, kWindowId, &new_bounds, &new_screen_bounds, &new_state));
239  ASSERT_TRUE(new_bounds.IsEmpty());
240  ASSERT_TRUE(new_screen_bounds.IsEmpty());
241  ASSERT_EQ(new_state, ui::SHOW_STATE_DEFAULT);
242}
243
244// Test corrupt screen bounds will not be loaded.
245TEST_F(AppWindowGeometryCacheTest, CorruptScreenBounds) {
246  const std::string extension_id = AddExtensionWithPrefs("ext1");
247  gfx::Rect bounds(4, 5, 31, 43);
248  gfx::Rect screen_bounds;
249  ui::WindowShowState state = ui::SHOW_STATE_NORMAL;
250  AddGeometryAndLoadExtension(
251      extension_id, kWindowId, bounds, screen_bounds, state);
252  gfx::Rect new_bounds;
253  gfx::Rect new_screen_bounds;
254  ui::WindowShowState new_state = ui::SHOW_STATE_DEFAULT;
255  ASSERT_FALSE(cache_->GetGeometry(
256      extension_id, kWindowId, &new_bounds, &new_screen_bounds, &new_state));
257  ASSERT_TRUE(new_bounds.IsEmpty());
258  ASSERT_TRUE(new_screen_bounds.IsEmpty());
259  ASSERT_EQ(new_state, ui::SHOW_STATE_DEFAULT);
260}
261
262// Test corrupt state will not be loaded.
263TEST_F(AppWindowGeometryCacheTest, CorruptState) {
264  const std::string extension_id = AddExtensionWithPrefs("ext1");
265  gfx::Rect bounds(4, 5, 31, 43);
266  gfx::Rect screen_bounds(0, 0, 1600, 900);
267  ui::WindowShowState state = ui::SHOW_STATE_DEFAULT;
268  AddGeometryAndLoadExtension(
269      extension_id, kWindowId, bounds, screen_bounds, state);
270  gfx::Rect new_bounds;
271  gfx::Rect new_screen_bounds;
272  ui::WindowShowState new_state = ui::SHOW_STATE_DEFAULT;
273  ASSERT_FALSE(cache_->GetGeometry(
274      extension_id, kWindowId, &new_bounds, &new_screen_bounds, &new_state));
275  ASSERT_TRUE(new_bounds.IsEmpty());
276  ASSERT_TRUE(new_screen_bounds.IsEmpty());
277  ASSERT_EQ(new_state, ui::SHOW_STATE_DEFAULT);
278}
279
280// Test saving geometry, screen_bounds and state to the cache and state store,
281// and reading it back.
282TEST_F(AppWindowGeometryCacheTest, SaveGeometryAndStateToStore) {
283  const std::string extension_id = AddExtensionWithPrefs("ext1");
284  const std::string window_id(kWindowId);
285
286  // inform cache of extension
287  LoadExtension(extension_id);
288
289  // update geometry stored in cache
290  gfx::Rect bounds(4, 5, 31, 43);
291  gfx::Rect screen_bounds(0, 0, 1600, 900);
292  ui::WindowShowState state = ui::SHOW_STATE_NORMAL;
293  cache_->SaveGeometry(extension_id, window_id, bounds, screen_bounds, state);
294
295  // make sure that immediately reading back geometry works
296  gfx::Rect new_bounds;
297  gfx::Rect new_screen_bounds;
298  ui::WindowShowState new_state = ui::SHOW_STATE_DEFAULT;
299  ASSERT_TRUE(cache_->GetGeometry(
300      extension_id, window_id, &new_bounds, &new_screen_bounds, &new_state));
301  ASSERT_EQ(bounds, new_bounds);
302  ASSERT_EQ(screen_bounds, new_screen_bounds);
303  ASSERT_EQ(state, new_state);
304
305  // unload extension to force cache to save data to the state store
306  UnloadExtension(extension_id);
307
308  // check if geometry got stored correctly in the state store
309  const base::DictionaryValue* dict =
310      extension_prefs_->GetGeometryCache(extension_id);
311  ASSERT_TRUE(dict);
312
313  ASSERT_TRUE(dict->HasKey(window_id));
314  int v;
315  ASSERT_TRUE(dict->GetInteger(window_id + ".x", &v));
316  ASSERT_EQ(bounds.x(), v);
317  ASSERT_TRUE(dict->GetInteger(window_id + ".y", &v));
318  ASSERT_EQ(bounds.y(), v);
319  ASSERT_TRUE(dict->GetInteger(window_id + ".w", &v));
320  ASSERT_EQ(bounds.width(), v);
321  ASSERT_TRUE(dict->GetInteger(window_id + ".h", &v));
322  ASSERT_EQ(bounds.height(), v);
323  ASSERT_TRUE(dict->GetInteger(window_id + ".screen_bounds_x", &v));
324  ASSERT_EQ(screen_bounds.x(), v);
325  ASSERT_TRUE(dict->GetInteger(window_id + ".screen_bounds_y", &v));
326  ASSERT_EQ(screen_bounds.y(), v);
327  ASSERT_TRUE(dict->GetInteger(window_id + ".screen_bounds_w", &v));
328  ASSERT_EQ(screen_bounds.width(), v);
329  ASSERT_TRUE(dict->GetInteger(window_id + ".screen_bounds_h", &v));
330  ASSERT_EQ(screen_bounds.height(), v);
331  ASSERT_TRUE(dict->GetInteger(window_id + ".state", &v));
332  ASSERT_EQ(state, v);
333
334  // reload extension
335  LoadExtension(extension_id);
336  // and make sure the geometry got reloaded properly too
337  ASSERT_TRUE(cache_->GetGeometry(
338      extension_id, window_id, &new_bounds, &new_screen_bounds, &new_state));
339  ASSERT_EQ(bounds, new_bounds);
340  ASSERT_EQ(screen_bounds, new_screen_bounds);
341  ASSERT_EQ(state, new_state);
342}
343
344// Tests that we won't do writes to the state store for SaveGeometry calls
345// which don't change the state we already have.
346TEST_F(AppWindowGeometryCacheTest, NoDuplicateWrites) {
347  using testing::_;
348  using testing::Mock;
349
350  const std::string extension_id = AddExtensionWithPrefs("ext1");
351  gfx::Rect bounds1(100, 200, 300, 400);
352  gfx::Rect bounds2(200, 400, 600, 800);
353  gfx::Rect bounds2_duplicate(200, 400, 600, 800);
354
355  gfx::Rect screen_bounds1(0, 0, 1600, 900);
356  gfx::Rect screen_bounds2(0, 0, 1366, 768);
357  gfx::Rect screen_bounds2_duplicate(0, 0, 1366, 768);
358
359  MockPrefChangeCallback observer(pref_service_.get());
360  PrefChangeRegistrar registrar;
361  registrar.Init(pref_service_.get());
362  registrar.Add("extensions.settings", observer.GetCallback());
363
364  // Write the first bounds - it should do > 0 writes.
365  EXPECT_CALL(observer, OnPreferenceChanged(_));
366  cache_->SaveGeometry(
367      extension_id, kWindowId, bounds1, screen_bounds1, ui::SHOW_STATE_NORMAL);
368  WaitForSync();
369  Mock::VerifyAndClearExpectations(&observer);
370
371  // Write a different bounds - it should also do > 0 writes.
372  EXPECT_CALL(observer, OnPreferenceChanged(_));
373  cache_->SaveGeometry(
374      extension_id, kWindowId, bounds2, screen_bounds1, ui::SHOW_STATE_NORMAL);
375  WaitForSync();
376  Mock::VerifyAndClearExpectations(&observer);
377
378  // Write a different screen bounds - it should also do > 0 writes.
379  EXPECT_CALL(observer, OnPreferenceChanged(_));
380  cache_->SaveGeometry(
381      extension_id, kWindowId, bounds2, screen_bounds2, ui::SHOW_STATE_NORMAL);
382  WaitForSync();
383  Mock::VerifyAndClearExpectations(&observer);
384
385  // Write a different state - it should also do > 0 writes.
386  EXPECT_CALL(observer, OnPreferenceChanged(_));
387  cache_->SaveGeometry(extension_id,
388                       kWindowId,
389                       bounds2,
390                       screen_bounds2,
391                       ui::SHOW_STATE_MAXIMIZED);
392  WaitForSync();
393  Mock::VerifyAndClearExpectations(&observer);
394
395  // Write a bounds, screen bounds and state that's a duplicate of what we
396  // already have. This should not do any writes.
397  EXPECT_CALL(observer, OnPreferenceChanged(_)).Times(0);
398  cache_->SaveGeometry(extension_id,
399                       kWindowId,
400                       bounds2_duplicate,
401                       screen_bounds2_duplicate,
402                       ui::SHOW_STATE_MAXIMIZED);
403  WaitForSync();
404  Mock::VerifyAndClearExpectations(&observer);
405}
406
407// Tests that no more than kMaxCachedWindows windows will be cached.
408TEST_F(AppWindowGeometryCacheTest, MaxWindows) {
409  const std::string extension_id = AddExtensionWithPrefs("ext1");
410  // inform cache of extension
411  LoadExtension(extension_id);
412
413  gfx::Rect bounds(4, 5, 31, 43);
414  gfx::Rect screen_bounds(0, 0, 1600, 900);
415  for (size_t i = 0; i < AppWindowGeometryCache::kMaxCachedWindows + 1; ++i) {
416    std::string window_id = "window_" + base::IntToString(i);
417    cache_->SaveGeometry(
418        extension_id, window_id, bounds, screen_bounds, ui::SHOW_STATE_NORMAL);
419  }
420
421  // The first added window should no longer have cached geometry.
422  EXPECT_FALSE(cache_->GetGeometry(extension_id, "window_0", NULL, NULL, NULL));
423  // All other windows should still exist.
424  for (size_t i = 1; i < AppWindowGeometryCache::kMaxCachedWindows + 1; ++i) {
425    std::string window_id = "window_" + base::IntToString(i);
426    EXPECT_TRUE(cache_->GetGeometry(extension_id, window_id, NULL, NULL, NULL));
427  }
428}
429
430}  // namespace extensions
431