[iOS] Add test support for ChromeTableViewController

This CL adds a class to ease testing of subclasses of
ChromeTableViewController, in the same way CollectionViewControllerTest
is easing the testing of subclasses of CollectionViewController.

Bug: 894791
Cq-Include-Trybots: luci.chromium.try:ios-simulator-cronet;luci.chromium.try:ios-simulator-full-configs
Change-Id: I159849612b16d5475d581c79601f93de04e6b705
Reviewed-on: https://chromium-review.googlesource.com/c/1283025
Commit-Queue: Gauthier Ambard <gambard@chromium.org>
Reviewed-by: Sergio Collazos <sczs@chromium.org>
Cr-Commit-Position: refs/heads/master@{#600706}
diff --git a/ios/chrome/browser/ui/table_view/BUILD.gn b/ios/chrome/browser/ui/table_view/BUILD.gn
index 7b48a48..7a23b74 100644
--- a/ios/chrome/browser/ui/table_view/BUILD.gn
+++ b/ios/chrome/browser/ui/table_view/BUILD.gn
@@ -77,6 +77,23 @@
   ]
 }
 
+source_set("test_support") {
+  configs += [ "//build/config/compiler:enable_arc" ]
+  testonly = true
+  sources = [
+    "chrome_table_view_controller_test.h",
+    "chrome_table_view_controller_test.mm",
+  ]
+  deps = [
+    ":table_view",
+    "//base",
+    "//ios/chrome/browser/ui/table_view/cells",
+    "//ios/chrome/test:test_support",
+    "//testing/gtest",
+    "//ui/base",
+  ]
+}
+
 source_set("unit_tests") {
   configs += [ "//build/config/compiler:enable_arc" ]
   testonly = true
@@ -85,6 +102,7 @@
   ]
   deps = [
     ":table_view",
+    ":test_support",
     "//ios/chrome/browser/ui/table_view/cells",
     "//testing/gtest",
   ]
diff --git a/ios/chrome/browser/ui/table_view/chrome_table_view_controller_test.h b/ios/chrome/browser/ui/table_view/chrome_table_view_controller_test.h
new file mode 100644
index 0000000..f74a1a8
--- /dev/null
+++ b/ios/chrome/browser/ui/table_view/chrome_table_view_controller_test.h
@@ -0,0 +1,115 @@
+// Copyright 2018 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.
+
+#ifndef IOS_CHROME_BROWSER_UI_TABLE_VIEW_CHROME_TABLE_VIEW_CONTROLLER_TEST_H_
+#define IOS_CHROME_BROWSER_UI_TABLE_VIEW_CHROME_TABLE_VIEW_CONTROLLER_TEST_H_
+
+#import <UIKit/UIKit.h>
+
+#include "base/compiler_specific.h"
+#import "base/ios/block_types.h"
+#import "ios/chrome/test/block_cleanup_test.h"
+
+@class ChromeTableViewController;
+
+class ChromeTableViewControllerTest : public BlockCleanupTest {
+ public:
+  ChromeTableViewControllerTest();
+  ~ChromeTableViewControllerTest() override;
+
+ protected:
+  void TearDown() override;
+
+  // Derived classes allocate their controller here.
+  virtual ChromeTableViewController* InstantiateController() = 0;
+
+  // Tests should call this function to create their controller for testing.
+  void CreateController();
+
+  // Will call CreateController() if |controller_| is nil.
+  ChromeTableViewController* controller();
+
+  // Deletes the controller.
+  void ResetController();
+
+  // Tests that the controller's view, model, tableView, and delegate are
+  // valid. Also tests that the model is the tableView's data source.
+  void CheckController();
+
+  // Returns the number of sections in the tableView.
+  int NumberOfSections();
+
+  // Returns the number of items in |section|.
+  int NumberOfItemsInSection(int section);
+
+  // Returns the collection view item at |item| in |section|.
+  id GetTableViewItem(int section, int item);
+
+  // Verifies that the title matches |expected_title|.
+  void CheckTitle(NSString* expected_title);
+
+  // Verifies that the title matches the l10n string for |expected_title_id|.
+  void CheckTitleWithId(int expected_title_id);
+
+  // Verifies that the section footer at |section| matches the |expected_text|.
+  void CheckSectionFooter(NSString* expected_text, int section);
+
+  // Verifies that the section footer at |section| matches the l10n string for
+  // |expected_text_id|.
+  void CheckSectionFooterWithId(int expected_text_id, int section);
+
+  // Verifies that the text cell at |item| in |section| has a text property
+  // which matches |expected_text|.
+  void CheckTextCellText(NSString* expected_text, int section, int item);
+
+  // Verifies that the text cell at |item| in |section| has a text property
+  // which matches the l10n string for |expected_text_id|.
+  void CheckTextCellTextWithId(int expected_text_id, int section, int item);
+
+  // Verifies that the text cell at |item| in |section| has a text and
+  // detailText properties which match strings for |expected_text| and
+  // |expected_detail_text|, respectively.
+  void CheckTextCellTextAndDetailText(NSString* expected_text,
+                                      NSString* expected_detail_text,
+                                      int section,
+                                      int item);
+
+  // Verifies that the text cell at |item| in |section| has a text and
+  // detailText properties which match strings for |expected_text| and
+  // |expected_detail_text|, respectively.
+  void CheckDetailItemTextWithIds(int expected_text_id,
+                                  int expected_detail_text_id,
+                                  int section_id,
+                                  int item_id);
+
+  // Verifies that the switch cell at |item| in |section| has a title which
+  // matches |expected_title| and is currently in |state|.
+  void CheckSwitchCellStateAndTitle(BOOL expected_state,
+                                    NSString* expected_title,
+                                    int section,
+                                    int item);
+
+  // Verifies that the switch cell at |item| in |section| has a title which
+  // matches the l10n string for |expected_title_id| and is currently in
+  // |state|.
+  void CheckSwitchCellStateAndTitleWithId(BOOL expected_state,
+                                          int expected_title_id,
+                                          int section,
+                                          int item);
+
+  // Verifies that the cell at |item| in |section| has the given
+  // |accessory_type|.
+  void CheckAccessoryType(UITableViewCellAccessoryType accessory_type,
+                          int section,
+                          int item);
+
+  // For |section|, deletes the item at |item|. |completion_block| is called at
+  // the end of the call to -performBatchUpdates:completion:.
+  void DeleteItem(int section, int item, ProceduralBlock completion_block);
+
+ private:
+  ChromeTableViewController* controller_;
+};
+
+#endif  // IOS_CHROME_BROWSER_UI_TABLE_VIEW_CHROME_TABLE_VIEW_CONTROLLER_TEST_H_
diff --git a/ios/chrome/browser/ui/table_view/chrome_table_view_controller_test.mm b/ios/chrome/browser/ui/table_view/chrome_table_view_controller_test.mm
new file mode 100644
index 0000000..9e1e395
--- /dev/null
+++ b/ios/chrome/browser/ui/table_view/chrome_table_view_controller_test.mm
@@ -0,0 +1,225 @@
+// Copyright 2018 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 "ios/chrome/browser/ui/table_view/chrome_table_view_controller_test.h"
+
+#include "base/logging.h"
+#import "base/mac/foundation_util.h"
+#import "ios/chrome/browser/ui/table_view/cells/table_view_text_item.h"
+#import "ios/chrome/browser/ui/table_view/chrome_table_view_controller.h"
+
+#include "testing/gtest_mac.h"
+#include "ui/base/l10n/l10n_util.h"
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+// Add selectors to be tested in the helpers.
+@interface TableViewItem (DetailTextAddition)
+- (NSString*)text;
+- (NSString*)detailText;
+@end
+
+ChromeTableViewControllerTest::ChromeTableViewControllerTest() {}
+
+ChromeTableViewControllerTest::~ChromeTableViewControllerTest() {}
+
+void ChromeTableViewControllerTest::TearDown() {
+  // Delete the controller before deleting other test variables, such as a
+  // profile, to ensure things are cleaned up in the same order as in Chrome.
+  controller_ = nil;
+  BlockCleanupTest::TearDown();
+}
+
+void ChromeTableViewControllerTest::CreateController() {
+  DCHECK(!controller_);
+  controller_ = InstantiateController();
+  // Force the model to be loaded.
+  [controller_ loadModel];
+  // Force the tableView to be built.
+  EXPECT_TRUE([controller_ view]);
+}
+
+ChromeTableViewController* ChromeTableViewControllerTest::controller() {
+  if (!controller_)
+    CreateController();
+  return controller_;
+}
+
+void ChromeTableViewControllerTest::ResetController() {
+  controller_ = nil;
+}
+
+void ChromeTableViewControllerTest::CheckController() {
+  EXPECT_TRUE([controller_ view]);
+  EXPECT_TRUE([controller_ tableView]);
+  EXPECT_TRUE([controller_ tableViewModel]);
+  EXPECT_EQ(controller_, [controller_ tableView].dataSource);
+  EXPECT_EQ(controller_, [controller_ tableView].delegate);
+}
+
+int ChromeTableViewControllerTest::NumberOfSections() {
+  return [[controller_ tableViewModel] numberOfSections];
+}
+
+int ChromeTableViewControllerTest::NumberOfItemsInSection(int section) {
+  return [[controller_ tableViewModel] numberOfItemsInSection:section];
+}
+
+id ChromeTableViewControllerTest::GetTableViewItem(int section, int item) {
+  TableViewModel* model = [controller_ tableViewModel];
+  NSIndexPath* index_path =
+      [NSIndexPath indexPathForItem:item inSection:section];
+  TableViewItem* collection_view_item = [model hasItemAtIndexPath:index_path]
+                                            ? [model itemAtIndexPath:index_path]
+                                            : nil;
+  EXPECT_TRUE(collection_view_item);
+  return collection_view_item;
+}
+
+void ChromeTableViewControllerTest::CheckTitle(NSString* expected_title) {
+  EXPECT_NSEQ(expected_title, [controller_ title]);
+}
+
+void ChromeTableViewControllerTest::CheckTitleWithId(int expected_title_id) {
+  CheckTitle(l10n_util::GetNSString(expected_title_id));
+}
+
+void ChromeTableViewControllerTest::CheckSectionFooter(NSString* expected_text,
+                                                       int section) {
+  // TODO(crbug.com/894791): Implement this.
+  NOTREACHED();
+}
+
+void ChromeTableViewControllerTest::CheckSectionFooterWithId(
+    int expected_text_id,
+    int section) {
+  return CheckSectionFooter(l10n_util::GetNSString(expected_text_id), section);
+}
+
+void ChromeTableViewControllerTest::CheckTextCellText(NSString* expected_text,
+                                                      int section,
+                                                      int item) {
+  id cell = GetTableViewItem(section, item);
+  ASSERT_TRUE([cell respondsToSelector:@selector(text)]);
+  ASSERT_TRUE([cell respondsToSelector:@selector(detailText)]);
+  EXPECT_NSEQ(expected_text, [cell text]);
+  EXPECT_FALSE([cell detailText]);
+}
+
+void ChromeTableViewControllerTest::CheckTextCellTextWithId(
+    int expected_text_id,
+    int section,
+    int item) {
+  CheckTextCellText(l10n_util::GetNSString(expected_text_id), section, item);
+}
+
+void ChromeTableViewControllerTest::CheckTextCellTextAndDetailText(
+    NSString* expected_text,
+    NSString* expected_detail_text,
+    int section,
+    int item) {
+  id cell = GetTableViewItem(section, item);
+  ASSERT_TRUE([cell respondsToSelector:@selector(text)]);
+  ASSERT_TRUE([cell respondsToSelector:@selector(detailText)]);
+  EXPECT_NSEQ(expected_text, [cell text]);
+  EXPECT_NSEQ(expected_detail_text, [cell detailText]);
+}
+
+void ChromeTableViewControllerTest::CheckDetailItemTextWithIds(
+    int expected_text_id,
+    int expected_detail_text_id,
+    int section_id,
+    int item_id) {
+  id item = GetTableViewItem(section_id, item_id);
+  ASSERT_TRUE([item respondsToSelector:@selector(text)]);
+  ASSERT_TRUE([item respondsToSelector:@selector(detailText)]);
+  EXPECT_NSEQ(l10n_util::GetNSString(expected_text_id), [item text]);
+  EXPECT_NSEQ(l10n_util::GetNSString(expected_detail_text_id),
+              [item detailText]);
+}
+
+void ChromeTableViewControllerTest::CheckSwitchCellStateAndTitle(
+    BOOL expected_state,
+    NSString* expected_title,
+    int section,
+    int item) {
+  // TODO(crbug.com/894791): Implement this.
+  NOTREACHED();
+}
+
+void ChromeTableViewControllerTest::CheckSwitchCellStateAndTitleWithId(
+    BOOL expected_state,
+    int expected_title_id,
+    int section,
+    int item) {
+  CheckSwitchCellStateAndTitle(
+      expected_state, l10n_util::GetNSString(expected_title_id), section, item);
+}
+
+void ChromeTableViewControllerTest::CheckAccessoryType(
+    UITableViewCellAccessoryType accessory_type,
+    int section,
+    int item) {
+  id text_item = GetTableViewItem(section, item);
+  EXPECT_TRUE([text_item respondsToSelector:@selector(accessoryType)]);
+  EXPECT_EQ(accessory_type, [text_item accessoryType]);
+}
+
+void ChromeTableViewControllerTest::DeleteItem(
+    int section,
+    int item,
+    ProceduralBlock completion_block) {
+  NSIndexPath* index_path =
+      [NSIndexPath indexPathForItem:item inSection:section];
+  __weak ChromeTableViewController* weak_controller = controller_;
+  void (^batch_updates)() = ^{
+    ChromeTableViewController* strong_controller = weak_controller;
+    if (!strong_controller)
+      return;
+    // Delete data in the model.
+    TableViewModel* model = strong_controller.tableViewModel;
+    NSInteger section_ID =
+        [model sectionIdentifierForSection:index_path.section];
+    NSInteger item_type = [model itemTypeForIndexPath:index_path];
+    NSUInteger index = [model indexInItemTypeForIndexPath:index_path];
+    [model removeItemWithType:item_type
+        fromSectionWithIdentifier:section_ID
+                          atIndex:index];
+
+    // Delete in the table view.
+    [[strong_controller tableView]
+        deleteRowsAtIndexPaths:@[ index_path ]
+              withRowAnimation:UITableViewRowAnimationNone];
+  };
+
+  void (^completion)(BOOL finished) = ^(BOOL finished) {
+    if (completion_block) {
+      completion_block();
+    }
+  };
+  if (@available(iOS 11.0, *)) {
+    [[controller_ tableView] performBatchUpdates:batch_updates
+                                      completion:completion];
+  } else {
+    TableViewModel* model = controller_.tableViewModel;
+    NSInteger section_ID =
+        [model sectionIdentifierForSection:index_path.section];
+    NSInteger item_type = [model itemTypeForIndexPath:index_path];
+    NSUInteger index = [model indexInItemTypeForIndexPath:index_path];
+    [model removeItemWithType:item_type
+        fromSectionWithIdentifier:section_ID
+                          atIndex:index];
+
+    // Delete in the table view.
+    [[controller_ tableView]
+        deleteRowsAtIndexPaths:@[ index_path ]
+              withRowAnimation:UITableViewRowAnimationNone];
+
+    if (completion_block) {
+      completion_block();
+    }
+  }
+}