File: tab_loader_unittest.cc

package info (click to toggle)
chromium 138.0.7204.183-1~deb12u1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm-proposed-updates
  • size: 6,080,960 kB
  • sloc: cpp: 34,937,079; ansic: 7,176,967; javascript: 4,110,704; python: 1,419,954; asm: 946,768; xml: 739,971; pascal: 187,324; sh: 89,623; perl: 88,663; objc: 79,944; sql: 50,304; cs: 41,786; fortran: 24,137; makefile: 21,811; php: 13,980; tcl: 13,166; yacc: 8,925; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (476 lines) | stat: -rw-r--r-- 19,279 bytes parent folder | download | duplicates (4)
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
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/sessions/tab_loader.h"

#include <vector>

#include "base/containers/flat_set.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/test/simple_test_tick_clock.h"
#include "base/time/time.h"
#include "chrome/browser/resource_coordinator/tab_helper.h"
#include "chrome/browser/resource_coordinator/tab_manager_features.h"
#include "chrome/browser/sessions/session_restore_test_utils.h"
#include "chrome/browser/sessions/tab_loader_tester.h"
#include "chrome/browser/ui/tabs/tab_model.h"
#include "chrome/test/base/browser_with_test_window_test.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/site_instance.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_web_contents_factory.h"
#include "content/public/test/web_contents_tester.h"
#include "testing/gtest/include/gtest/gtest.h"

using resource_coordinator::TabLoadTracker;
using resource_coordinator::ResourceCoordinatorTabHelper;
using LoadingState = TabLoadTracker::LoadingState;

class TabLoaderTest : public BrowserWithTestWindowTest {
 protected:
  using Super = BrowserWithTestWindowTest;
  using RestoredTab = SessionRestoreDelegate::RestoredTab;

  TabLoaderTest() : max_simultaneous_loads_(1) {}

  TabLoaderTest(const TabLoaderTest&) = delete;
  TabLoaderTest& operator=(const TabLoaderTest&) = delete;

  void OnTabLoaderCreated(TabLoader* tab_loader) {
    tab_loader_.SetTabLoader(tab_loader);
    tab_loader_.SetTickClockForTesting(&clock_);
    if (max_simultaneous_loads_ != 0)
      tab_loader_.SetMaxSimultaneousLoadsForTesting(max_simultaneous_loads_);
  }

  // testing::Test:
  void SetUp() override {
    Super::SetUp();
    construction_callback_ = base::BindRepeating(
        &TabLoaderTest::OnTabLoaderCreated, base::Unretained(this));
    TabLoaderTester::SetConstructionCallbackForTesting(&construction_callback_);
    test_policy_ =
        std::make_unique<testing::ScopedAlwaysLoadSessionRestoreTestPolicy>();
  }

  void TearDown() override {
    if (TabLoaderTester::shared_tab_loader() != nullptr) {
      // Expect the TabLoader to detach after all tabs have loaded.
      SimulateLoadedAll();
      EXPECT_TRUE(TabLoaderTester::shared_tab_loader() == nullptr);
    }

    TabLoaderTester::SetConstructionCallbackForTesting(nullptr);
    task_environment()->RunUntilIdle();
    test_policy_.reset();
    Super::TearDown();
  }

  void SimulateLoadTimeout() {
    // Unfortunately there's no mock time in BrowserTaskEnvironment.
    // Fast-forward things and simulate firing the timer.
    // TODO(crbug.com/40602467): TaskEnvironment::TimeSource::MOCK_TIME now
    // supports this.
    EXPECT_TRUE(tab_loader_.force_load_timer().IsRunning());
    clock_.SetNowTicks(tab_loader_.force_load_time());
    tab_loader_.force_load_timer().Stop();
    tab_loader_.ForceLoadTimerFired();
    SimulatePrimaryPageChangedIfNecessary();
  }

  void SimulateStartedToLoad(size_t tab_index) {
    auto* contents = restored_tabs_[tab_index].contents();
    auto* tracker = TabLoadTracker::Get();
    tracker->TransitionStateForTesting(contents, LoadingState::LOADING);
    SimulatePrimaryPageChangedIfNecessary();
  }

  void SimulateLoaded(size_t tab_index) {
    // Transition to a LOADED state. This has to pass through the LOADING state
    // in order to satisfy the internal logic of SessionRestoreStatsCollector.
    auto* contents = restored_tabs_[tab_index].contents();
    auto* tracker = TabLoadTracker::Get();
    if (tracker->GetLoadingState(contents) != LoadingState::LOADING)
      tracker->TransitionStateForTesting(contents, LoadingState::LOADING);
    tracker->TransitionStateForTesting(contents, LoadingState::LOADED);
    SimulatePrimaryPageChangedIfNecessary();
  }

  void SimulateLoadedAll() {
    for (size_t i = 0; i < restored_tabs_.size(); ++i)
      SimulateLoaded(i);
  }

  content::WebContents* CreateRestoredWebContents(bool is_active) {
    std::unique_ptr<content::WebContents> test_contents =
        content::WebContentsTester::CreateTestWebContents(
            profile(), content::SiteInstance::Create(profile()));
    auto* raw_contents = test_contents.get();
    std::vector<std::unique_ptr<content::NavigationEntry>> entries;
    entries.push_back(content::NavigationEntry::Create());
    test_contents->GetController().Restore(0, content::RestoreType::kRestored,
                                           &entries);
    // TabLoadTracker needs the resource_coordinator WebContentsData to be
    // initialized.
    ResourceCoordinatorTabHelper::CreateForWebContents(raw_contents);
    restored_tabs_.push_back(
        RestoredTab(raw_contents, is_active /* is_active */, false /* is_app */,
                    false /* is_pinned */, std::nullopt /* group */));

    // Add the contents to the tab strip model, which becomes the owner.
    auto* tab_strip_model = browser()->tab_strip_model();
    tab_strip_model->AppendWebContents(std::move(test_contents), is_active);

    if (is_active) {
      // If the tab is active start "loading" it right away for consistency with
      // session restore code.
      raw_contents->GetController().LoadIfNecessary();
    }

    return raw_contents;
  }

  void CreateMultipleRestoredWebContents(size_t num_active,
                                         size_t num_inactive) {
    // At least one active tab must be created.
    DCHECK_LT(0u, num_active);
    for (size_t i = 0; i < num_active; ++i)
      CreateRestoredWebContents(true);
    for (size_t i = 0; i < num_inactive; ++i)
      CreateRestoredWebContents(false);
  }

  // Since it couldn't get PrimaryPageChanged() by loading, it simulates
  // PrimaryPageChanged() to update the status.
  void SimulatePrimaryPageChanged(content::WebContents* web_contents) {
    auto* helper = ResourceCoordinatorTabHelper::FromWebContents(web_contents);
    helper->PrimaryPageChanged(web_contents->GetPrimaryPage());
  }

  // If the tab initiates loading, TransitionState is updated by
  // PrimaryPageChanged() in a normal browser flow. Since this is a unit test,
  // we simulate SimulatePrimaryPageChanged() for the loading initiated tabs.
  void SimulatePrimaryPageChangedIfNecessary() {
    if (!TabLoaderTester::shared_tab_loader())
      return;

    // Copy because the set can change while calling
    // SimulatePrimaryPageChanged() and the iteration is invalidated.
    base::flat_set<raw_ptr<content::WebContents, CtnExperimental>>
        load_initiated = tab_loader_.tabs_load_initiated();
    for (content::WebContents* web_contents : load_initiated) {
      SimulatePrimaryPageChanged(web_contents);
    }
  }

  void StartTabLoader() {
    // Call PrimaryPageChanged() that would be caused by LoadIfNecessary() from
    // CreateRestoredWebContents().
    for (auto& tab : restored_tabs_) {
      if (tab.is_active())
        SimulatePrimaryPageChanged(tab.contents());
    }

    TabLoader::RestoreTabs(restored_tabs_, clock_.NowTicks());
    EXPECT_TRUE(tab_loader_.IsSharedTabLoader());
    EXPECT_FALSE(tab_loader_.IsLoadingEnabled());
    tab_loader_.WaitForTabLoadingEnabled();
  }

  // The number of loading slots to use. This needs to be set before the
  // TabLoader is created in order to be picked up by it.
  size_t max_simultaneous_loads_;

  // Set of restored tabs that is populated by calls to
  // CreateRestoredWebContents.
  std::vector<RestoredTab> restored_tabs_;

  // Automatically attaches to the tab loader that is created by the test.
  TabLoaderTester tab_loader_;

  // The tick clock that is injected into the tab loader.
  base::SimpleTestTickClock clock_;

  // The post-construction testing seam that is invoked by TabLoader.
  base::RepeatingCallback<void(TabLoader*)> construction_callback_;

  std::unique_ptr<testing::ScopedAlwaysLoadSessionRestoreTestPolicy>
      test_policy_;
};

TEST_F(TabLoaderTest, AllLoadingSlotsUsed) {
  // Create 2 active tabs and 4 inactive tabs.
  CreateMultipleRestoredWebContents(2, 4);

  // Use 4 loading slots. The active tabs will only use 2 which means 2 of the
  // inactive tabs should immediately be scheduled to load as well.
  max_simultaneous_loads_ = 4;

  StartTabLoader();

  // The loader should be enabled, with 2 tabs loading and 4 tabs left to go.
  // The initial load should exclusively allow active tabs time to load, and
  // fill up the rest of the loading slots.
  EXPECT_TRUE(tab_loader_.IsLoadingEnabled());
  EXPECT_EQ(4u, tab_loader_.tabs_to_load().size());
  EXPECT_EQ(2u, tab_loader_.scheduled_to_load_count());
  EXPECT_EQ(2u, TabLoadTracker::Get()->GetLoadingTabCount());

  // Trying to load another tab should do nothing as no tab has yet finished
  // loading.
  tab_loader_.MaybeLoadSomeTabsForTesting();
  EXPECT_EQ(4u, tab_loader_.tabs_to_load().size());
  EXPECT_EQ(2u, tab_loader_.scheduled_to_load_count());
  EXPECT_EQ(2u, TabLoadTracker::Get()->GetLoadingTabCount());
  EXPECT_TRUE(tab_loader_.IsSharedTabLoader());

  // Mark an active tab as having finished loading. This marks the end of the
  // exclusive loading period and all slots should be full now.
  SimulateLoaded(0);
  EXPECT_EQ(1u, tab_loader_.tabs_to_load().size());
  EXPECT_EQ(5u, tab_loader_.scheduled_to_load_count());
  EXPECT_EQ(4u, TabLoadTracker::Get()->GetLoadingTabCount());
  EXPECT_TRUE(tab_loader_.IsSharedTabLoader());

  // Trying to load more tabs should still do nothing.
  tab_loader_.MaybeLoadSomeTabsForTesting();
  EXPECT_EQ(1u, tab_loader_.tabs_to_load().size());
  EXPECT_EQ(5u, tab_loader_.scheduled_to_load_count());
  EXPECT_EQ(4u, TabLoadTracker::Get()->GetLoadingTabCount());
  EXPECT_TRUE(tab_loader_.IsSharedTabLoader());
}

TEST_F(TabLoaderTest, ForceLoadTimer) {
  // Create 1 active tab and 1 inactive tab with 1 loading slot.
  CreateMultipleRestoredWebContents(1, 1);
  max_simultaneous_loads_ = 1;

  StartTabLoader();

  // The loader should be enabled, with 1 tab loading and 1 tab left to go.
  EXPECT_TRUE(tab_loader_.IsLoadingEnabled());
  EXPECT_EQ(1u, tab_loader_.tabs_to_load().size());
  EXPECT_EQ(1u, tab_loader_.scheduled_to_load_count());
  EXPECT_EQ(1u, TabLoadTracker::Get()->GetLoadingTabCount());
  EXPECT_TRUE(tab_loader_.IsSharedTabLoader());

  SimulateLoadTimeout();
  EXPECT_FALSE(tab_loader_.HasTimedOutLoads());

  // Expect all tabs to be loading. Note that this also validates that
  // force-loads can exceed the number of loadingslots.
  EXPECT_TRUE(tab_loader_.IsLoadingEnabled());
  EXPECT_TRUE(tab_loader_.tabs_to_load().empty());
  EXPECT_EQ(2u, tab_loader_.scheduled_to_load_count());
  EXPECT_EQ(2u, TabLoadTracker::Get()->GetLoadingTabCount());
  EXPECT_TRUE(tab_loader_.IsSharedTabLoader());
}

TEST_F(TabLoaderTest, LoadsAreStaggered) {
  // Create 1 active tab and 1 inactive tab with 1 loading slot.
  CreateMultipleRestoredWebContents(1, 1);
  max_simultaneous_loads_ = 1;

  StartTabLoader();

  // The loader should be enabled, with 1 tab loading and 1 tab left to go.
  EXPECT_TRUE(tab_loader_.IsLoadingEnabled());
  EXPECT_EQ(1u, tab_loader_.tabs_to_load().size());
  EXPECT_EQ(1u, tab_loader_.scheduled_to_load_count());
  EXPECT_EQ(1u, TabLoadTracker::Get()->GetLoadingTabCount());
  EXPECT_TRUE(tab_loader_.IsSharedTabLoader());

  // Simulate the first tab finishing loading.
  SimulateLoaded(0);

  // Expect all tabs to be loaded/loading.
  EXPECT_TRUE(tab_loader_.IsLoadingEnabled());
  EXPECT_TRUE(tab_loader_.tabs_to_load().empty());
  EXPECT_EQ(2u, tab_loader_.scheduled_to_load_count());
  EXPECT_EQ(1u, TabLoadTracker::Get()->GetLoadedTabCount());
  EXPECT_EQ(1u, TabLoadTracker::Get()->GetLoadingTabCount());
  EXPECT_TRUE(tab_loader_.IsSharedTabLoader());
}

TEST_F(TabLoaderTest, OnMemoryPressure) {
  // Multiple contents are necessary to make sure that the tab loader
  // doesn't immediately kick off loading of all tabs and detach.
  CreateMultipleRestoredWebContents(1, 2);

  max_simultaneous_loads_ = 1;
  StartTabLoader();
  EXPECT_EQ(1u, tab_loader_.scheduled_to_load_count());

  // Simulate memory pressure and expect the tab loader to disable loading.
  EXPECT_TRUE(tab_loader_.IsLoadingEnabled());
  tab_loader_.OnMemoryPressure(
      base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE);
  EXPECT_FALSE(tab_loader_.IsLoadingEnabled());

  // Finish loading the tab and expect the tab loader to disconnect.
  SimulateLoaded(0);
  EXPECT_TRUE(TabLoaderTester::shared_tab_loader() == nullptr);
}

TEST_F(TabLoaderTest, TimeoutCanExceedLoadingSlots) {
  CreateMultipleRestoredWebContents(1, 4);

  // Create the tab loader with 2 loading slots. This should initially start
  // loading 1 tab, due to exclusive initial loading of active tabs.
  max_simultaneous_loads_ = 2;
  StartTabLoader();
  EXPECT_EQ(4u, tab_loader_.tabs_to_load().size());
  EXPECT_EQ(1u, tab_loader_.scheduled_to_load_count());

  // Simulate a timeout and expect there to be 2 loading tabs and 3 left to
  // load.
  SimulateLoadTimeout();
  EXPECT_FALSE(tab_loader_.HasTimedOutLoads());
  EXPECT_EQ(3u, tab_loader_.tabs_to_load().size());
  EXPECT_EQ(2u, tab_loader_.scheduled_to_load_count());
  EXPECT_EQ(2u, tab_loader_.force_load_delay_multiplier());
  EXPECT_TRUE(tab_loader_.IsSharedTabLoader());

  // Do it again and expect 3 tabs to be loading.
  SimulateLoadTimeout();
  EXPECT_FALSE(tab_loader_.HasTimedOutLoads());
  EXPECT_EQ(2u, tab_loader_.tabs_to_load().size());
  EXPECT_EQ(3u, tab_loader_.scheduled_to_load_count());
  EXPECT_EQ(4u, tab_loader_.force_load_delay_multiplier());
  EXPECT_TRUE(tab_loader_.IsSharedTabLoader());

  // Do it again and expect 4 tabs to be loading.
  SimulateLoadTimeout();
  EXPECT_FALSE(tab_loader_.HasTimedOutLoads());
  EXPECT_EQ(1u, tab_loader_.tabs_to_load().size());
  EXPECT_EQ(4u, tab_loader_.scheduled_to_load_count());
  EXPECT_EQ(8u, tab_loader_.force_load_delay_multiplier());
  EXPECT_TRUE(tab_loader_.IsSharedTabLoader());

  // Simulate the first tab finishing loading and don't expect more tabs to
  // start loading.
  SimulateLoaded(0);
  EXPECT_EQ(1u, tab_loader_.tabs_to_load().size());
  EXPECT_EQ(4u, tab_loader_.scheduled_to_load_count());
  EXPECT_TRUE(tab_loader_.IsSharedTabLoader());

  // Simulate the second tab finishing loading and don't expect more tabs to
  // start loading.
  SimulateLoaded(1);
  EXPECT_EQ(1u, tab_loader_.tabs_to_load().size());
  EXPECT_EQ(4u, tab_loader_.scheduled_to_load_count());
  EXPECT_TRUE(tab_loader_.IsSharedTabLoader());

  // Simulate the third tab finishing loading and this time expect the last tab
  // load to be initiated. There are no tabs left so the TabLoader should also
  // have initiated a self-destroy.
  SimulateLoaded(2);
  EXPECT_TRUE(tab_loader_.tabs_to_load().empty());
  EXPECT_EQ(5u, tab_loader_.scheduled_to_load_count());
  EXPECT_TRUE(tab_loader_.IsSharedTabLoader());
}

TEST_F(TabLoaderTest, DelegatePolicyIsApplied) {
  namespace rc = resource_coordinator;

  test_policy_.reset();

  // Don't directly configure the max simultaneous loads, but rather let it be
  // configured via the policy engine.
  max_simultaneous_loads_ = 0;

  // Create 5 tabs to restore, 1 foreground and 4 background.
  CreateMultipleRestoredWebContents(1, 4);

  // Create the tab loader. This should initially start loading 1 tab, due to
  // exclusive initial loading of active tabs.
  StartTabLoader();
  EXPECT_EQ(4u, tab_loader_.tabs_to_load().size());
  EXPECT_EQ(1u, tab_loader_.scheduled_to_load_count());

  // Configure the policy engine explicitly. Values of zero disable those
  // particular aspects of the policy engine.
  auto* policy = tab_loader_.GetPolicy();
  policy->MinSimultaneousTabLoadsForTesting() = 2;
  policy->MaxSimultaneousTabLoadsForTesting() = 2;
  policy->CoresPerSimultaneousTabLoadForTesting() = 0;
  policy->MinTabsToRestoreForTesting() = 1;
  policy->MaxTabsToRestoreForTesting() = 3;
  policy->MbFreeMemoryPerTabToRestoreForTesting() = 0;
  policy->MaxTimeSinceLastUseToRestoreForTesting() = base::TimeDelta();
  policy->MinSiteEngagementToRestoreForTesting() = 0;
  policy->CalculateSimultaneousTabLoadsForTesting();

  // Simulate the first tab as having loaded. Another 2 should start loading.
  SimulateLoaded(0);
  EXPECT_EQ(2u, tab_loader_.tabs_to_load().size());
  EXPECT_EQ(3u, tab_loader_.scheduled_to_load_count());
  EXPECT_TRUE(tab_loader_.IsSharedTabLoader());

  // Simulate another tab as having loaded. The last 2 tabs should be deferred
  // (still need reloads) and the tab loader should detach.
  SimulateLoaded(1);
  SimulateLoaded(2);
  EXPECT_TRUE(restored_tabs_[3].contents()->GetController().NeedsReload());
  EXPECT_TRUE(restored_tabs_[4].contents()->GetController().NeedsReload());
  EXPECT_TRUE(TabLoaderTester::shared_tab_loader() == nullptr);
}

TEST_F(TabLoaderTest, ObservesExternallyInitiatedLoads) {
  CreateMultipleRestoredWebContents(1, 2);

  // Create the tab loader with 1 loading slots. This should initially start
  // loading 1 tab, due to exclusive initial loading of active tabs.
  max_simultaneous_loads_ = 1;
  StartTabLoader();
  EXPECT_EQ(2u, tab_loader_.tabs_to_load().size());
  EXPECT_EQ(1u, tab_loader_.scheduled_to_load_count());

  // Manually initiate the load on one of the tabs, as would occur if a user
  // focused a tab. The tab should no longer be in the scheduled to load bucket.
  SimulateStartedToLoad(1);
  EXPECT_EQ(1u, tab_loader_.tabs_to_load().size());
  EXPECT_EQ(2u, tab_loader_.scheduled_to_load_count());
  EXPECT_TRUE(tab_loader_.IsSharedTabLoader());
}

TEST_F(TabLoaderTest, CloseAllTabs) {
  CreateMultipleRestoredWebContents(1, 2);

  // Create the tab loader with 1 loading slots. This should initially start
  // loading 1 tab, due to exclusive initial loading of active tabs.
  max_simultaneous_loads_ = 1;
  StartTabLoader();
  EXPECT_EQ(2u, tab_loader_.tabs_to_load().size());
  EXPECT_EQ(1u, tab_loader_.scheduled_to_load_count());

  // The loader should entirely disconnect when all tabs are closed.
  browser()->tab_strip_model()->CloseAllTabs();
  EXPECT_TRUE(TabLoaderTester::shared_tab_loader() == nullptr);
}

TEST_F(TabLoaderTest, RemoveFromTabStrip) {
  CreateMultipleRestoredWebContents(1, 1);

  // Create the tab loader with 1 loading slots. This should initially start
  // loading 1 tab, due to exclusive initial loading of active tabs.
  max_simultaneous_loads_ = 1;
  StartTabLoader();
  EXPECT_EQ(1u, tab_loader_.tabs_to_load().size());
  EXPECT_EQ(1u, tab_loader_.scheduled_to_load_count());

  // Remove the second tab from the tab strip model.
  browser()->tab_strip_model()->DetachAndDeleteWebContentsAt(1);

  // The tab being removed won't be noticed by the loader until some state
  // change it cares about occurs. Simulate the first tab finishing loading, at
  // which point the loader should realize the other tab is no longer attached
  // to a tab strip, and destroy itself because it has no work left to do.
  SimulateLoaded(0);
  EXPECT_TRUE(TabLoaderTester::shared_tab_loader() == nullptr);
}