From b276cb56726c49901f7c034a00d18d2a9fae5a2b Mon Sep 17 00:00:00 2001 From: Jorge Leandro Perez Date: Mon, 2 Jul 2018 12:55:00 -0300 Subject: [PATCH 1/7] Implements Site DataModel --- Networking/Networking/Model/Site.swift | 60 ++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 Networking/Networking/Model/Site.swift diff --git a/Networking/Networking/Model/Site.swift b/Networking/Networking/Model/Site.swift new file mode 100644 index 00000000000..0a1c1f52b57 --- /dev/null +++ b/Networking/Networking/Model/Site.swift @@ -0,0 +1,60 @@ +import Foundation + + +/// Represents a WordPress.com Site. +/// +public struct Site: Decodable { + + /// WordPress.com Site Identifier. + /// + let siteID: Int + + /// Site's Name. + /// + let name: String + + /// Site's Description. + /// + let description: String + + /// Site's URL. + /// + let url: String + + /// Indicates if this site hosts a WordPress Store. + /// + let isWordPressStore: Bool + + + /// Designated Initializer. + /// + public init(from decoder: Decoder) throws { + let siteContainer = try decoder.container(keyedBy: SiteKeys.self) + + siteID = try siteContainer.decode(Int.self, forKey: .siteID) + name = try siteContainer.decode(String.self, forKey: .name) + description = try siteContainer.decode(String.self, forKey: .description) + url = try siteContainer.decode(String.self, forKey: .url) + + let optionsContainer = try siteContainer.nestedContainer(keyedBy: OptionKeys.self, forKey: .options) + isWordPressStore = try optionsContainer.decode(Bool.self, forKey: .isWordPressStore) + } +} + + +/// Defines all of the Site CodingKeys. +/// +private extension Site { + + enum SiteKeys: String, CodingKey { + case siteID = "ID" + case name = "name" + case description = "description" + case url = "URL" + case options = "options" + } + + enum OptionKeys: String, CodingKey { + case isWordPressStore = "is_wpcom_store" + } +} From f421cae403deca2228634cea4c97b158b883bdb0 Mon Sep 17 00:00:00 2001 From: Jorge Leandro Perez Date: Mon, 2 Jul 2018 12:55:09 -0300 Subject: [PATCH 2/7] Implements SiteListMapper --- .../Networking/Mapper/SiteListMapper.swift | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 Networking/Networking/Mapper/SiteListMapper.swift diff --git a/Networking/Networking/Mapper/SiteListMapper.swift b/Networking/Networking/Mapper/SiteListMapper.swift new file mode 100644 index 00000000000..61c51d322ad --- /dev/null +++ b/Networking/Networking/Mapper/SiteListMapper.swift @@ -0,0 +1,29 @@ +import Foundation + + +/// Mapper: SiteList +/// +class SiteListMapper: Mapper { + + /// (Attempts) to convert a dictionary into [Site]. + /// + func map(response: Data) throws -> [Site] { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .formatted(DateFormatter.Defaults.dateTimeFormatter) + + return try decoder.decode(SiteListEnvelope.self, from: response).sites + } +} + + +/// SiteList Disposable Entity: +/// `Load All Sites` endpoint returns all of its orders within the `sites` key. This entity +/// allows us to do parse all the things with JSONDecoder. +/// +private struct SiteListEnvelope: Decodable { + let sites: [Site] + + private enum CodingKeys: String, CodingKey { + case sites = "sites" + } +} From 394a320ac20394a2f1097286ca37a3d61bd54c62 Mon Sep 17 00:00:00 2001 From: Jorge Leandro Perez Date: Mon, 2 Jul 2018 12:55:19 -0300 Subject: [PATCH 3/7] AccountRemote: New loadSites API --- Networking/Networking/Remote/AccountRemote.swift | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Networking/Networking/Remote/AccountRemote.swift b/Networking/Networking/Remote/AccountRemote.swift index bdb1e2b22eb..e7c19715082 100644 --- a/Networking/Networking/Remote/AccountRemote.swift +++ b/Networking/Networking/Remote/AccountRemote.swift @@ -15,4 +15,19 @@ public class AccountRemote: Remote { enqueue(request, mapper: mapper, completion: completion) } + + + /// Loads the Sites collection associated with the WordPress.com User. + /// + public func loadSites(completion: @escaping ([Site]?, Error?) -> Void) { + let path = "me/sites" + let parameters = [ + "fields": "ID,name,description,URL,options" + ] + + let request = DotcomRequest(wordpressApiVersion: .mark1_1, method: .get, path: path, parameters: parameters) + let mapper = SiteListMapper() + + enqueue(request, mapper: mapper, completion: completion) + } } From e32fd13e4459144806290deb1108270b4f78e9de Mon Sep 17 00:00:00 2001 From: Jorge Leandro Perez Date: Mon, 2 Jul 2018 12:55:30 -0300 Subject: [PATCH 4/7] New LoadSites Unit Tests --- .../Mapper/AccountMapperTests.swift | 33 ++- .../NetworkingTests/Responses/sites.json | 219 ++++++++++++++++++ 2 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 Networking/NetworkingTests/Responses/sites.json diff --git a/Networking/NetworkingTests/Mapper/AccountMapperTests.swift b/Networking/NetworkingTests/Mapper/AccountMapperTests.swift index 3a66ad4a97b..a0bbb7b6fb7 100644 --- a/Networking/NetworkingTests/Mapper/AccountMapperTests.swift +++ b/Networking/NetworkingTests/Mapper/AccountMapperTests.swift @@ -20,6 +20,27 @@ class AccountMapperTests: XCTestCase { XCTAssertEqual(account.userID, 78972699) XCTAssertEqual(account.username, "apiexamples") } + + /// Verifies that all of the Site fields are properly parsed. + /// + func testSiteFieldsAreProperlyParsed() { + let sites = mapLoadSitesResponse() + XCTAssert(sites?.count == 2) + + let first = sites!.first! + XCTAssertEqual(first.siteID, 1112233334444555) + XCTAssertEqual(first.name, "Testing Blog") + XCTAssertEqual(first.description, "Testing Tagline") + XCTAssertEqual(first.url, "https://some-testing-url.testing.blog") + XCTAssertEqual(first.isWordPressStore, true) + + let second = sites!.last! + XCTAssertEqual(second.siteID, 11122333344446666) + XCTAssertEqual(second.name, "Thoughts") + XCTAssertEqual(second.description, "Your Favorite Blog") + XCTAssertEqual(second.url, "https://thoughts.testing.blog") + XCTAssertEqual(second.isWordPressStore, false) + } } @@ -28,7 +49,7 @@ class AccountMapperTests: XCTestCase { // private extension AccountMapperTests { - /// Returns the AccountMapper output upon receiving `me` (Data Encoded) + /// Returns the AccountMapper output upon receiving `me` mockup response (Data Encoded). /// func mapLoadAccountResponse() -> Account? { guard let response = Loader.contentsOf("me") else { @@ -37,4 +58,14 @@ private extension AccountMapperTests { return try? AccountMapper().map(response: response) } + + /// Returns the SiteListMapper output upon receiving `me/sites` mockup response (Data Encoded). + /// + func mapLoadSitesResponse() -> [Site]? { + guard let response = Loader.contentsOf("sites") else { + return nil + } + + return try? SiteListMapper().map(response: response) + } } diff --git a/Networking/NetworkingTests/Responses/sites.json b/Networking/NetworkingTests/Responses/sites.json new file mode 100644 index 00000000000..1ac6b3ee09d --- /dev/null +++ b/Networking/NetworkingTests/Responses/sites.json @@ -0,0 +1,219 @@ +{ + "sites": [ + { + "ID": 1112233334444555, + "name": "Testing Blog", + "description": "Testing Tagline", + "URL": "https:\/\/some-testing-url.testing.blog", + "options": { + "timezone": "", + "gmt_offset": 0, + "blog_public": 1, + "videopress_enabled": false, + "upgraded_filetypes_enabled": true, + "login_url": "https:\/\/some-testing-url.here\/wp-login.php", + "admin_url": "https:\/\/some-testing-url.here\/wp-admin\/", + "is_mapped_domain": true, + "is_redirect": false, + "unmapped_url": "https:\/\/some-testing-url.here", + "featured_images_enabled": false, + "theme_slug": "storefront", + "header_image": false, + "background_color": false, + "image_default_link_type": "file", + "image_thumbnail_width": 150, + "image_thumbnail_height": 150, + "image_thumbnail_crop": 0, + "image_medium_width": 300, + "image_medium_height": 300, + "image_large_width": 1024, + "image_large_height": 1024, + "permalink_structure": "\/%year%\/%monthnum%\/%day%\/%postname%\/", + "post_formats": [], + "default_post_format": "0", + "default_category": 12, + "allowed_file_types": [ + "jpg", + "jpeg", + "png", + "gif", + "pdf", + "doc", + "ppt", + "odt", + "pptx", + "docx", + "pps", + "ppsx", + "xls", + "xlsx", + "key", + "ogv", + "mp4", + "m4v", + "mov", + "wmv", + "avi", + "mpg", + "3gp", + "3g2" + ], + "show_on_front": "page", + "default_likes_enabled": true, + "default_sharing_status": true, + "default_comment_status": true, + "default_ping_status": true, + "software_version": "4.9.6", + "created_at": "2014-03-28T15:03:03+00:00", + "wordads": false, + "publicize_permanently_disabled": false, + "frame_nonce": "73ae7163d8", + "page_on_front": 1455, + "page_for_posts": 0, + "headstart": false, + "headstart_is_fresh": false, + "ak_vp_bundle_enabled": false, + "advanced_seo_front_page_description": "", + "advanced_seo_title_formats": [], + "verification_services_codes": null, + "podcasting_archive": null, + "is_domain_only": false, + "is_automated_transfer": true, + "is_wpcom_store": true, + "woocommerce_is_active": true, + "design_type": null, + "site_goals": null, + "jetpack_version": "6.2.1", + "main_network_site": "https:\/\/some-testing-url.here", + "active_modules": [ + "after-the-deadline", + "contact-form", + "custom-content-types", + "custom-css", + "enhanced-distribution", + "gravatar-hovercards", + "json-api", + "latex", + "manage", + "notes", + "post-by-email", + "protect", + "publicize", + "sharedaddy", + "shortcodes", + "shortlinks", + "sitemaps", + "stats", + "subscriptions", + "verification-tools", + "widget-visibility", + "widgets", + "sso", + "tiled-gallery", + "likes", + "comments", + "comment-likes", + "videopress", + "carousel", + "photon", + "seo-tools", + "google-analytics", + "infinite-scroll", + "masterbar", + "vaultpress" + ], + "max_upload_size": false, + "wp_memory_limit": "268435456", + "wp_max_memory_limit": "268435456", + "is_multi_network": false, + "is_multi_site": false, + "file_mod_disabled": false + }, + "updates": { + "plugins": 3, + "themes": 1, + "wordpress": 0, + "translations": 0, + "total": 4 + } + }, + { + "ID": 11122333344446666, + "name": "Thoughts", + "description": "Your Favorite Blog", + "URL": "https:\/\/thoughts.testing.blog", + "options": { + "timezone": "", + "gmt_offset": 0, + "blog_public": 1, + "videopress_enabled": false, + "upgraded_filetypes_enabled": false, + "login_url": "https:\/\/thoughts.testing.blog\/wp-login.php", + "admin_url": "https:\/\/thoughts.testing.blog\/wp-admin\/", + "is_mapped_domain": false, + "is_redirect": false, + "unmapped_url": "https:\/\/thoughts.testing.blog", + "featured_images_enabled": false, + "theme_slug": "pub\/p2-breathe", + "header_image": { + "thumbnail_url": "https:\/\/thoughts.testing.blog\/2016\/07\/blur.jpg?resize=520,108.33333333333", + "url": "https:\/\/thoughts.testing.blog\/2016\/07\/blur.jpg?resize=1200,250", + "description": "Blurred Lights" + }, + "background_color": "2b2b2b", + "image_default_link_type": "file", + "image_thumbnail_width": 150, + "image_thumbnail_height": 150, + "image_thumbnail_crop": 0, + "image_medium_width": 300, + "image_medium_height": 300, + "image_large_width": 1024, + "image_large_height": 1024, + "permalink_structure": "\/%year%\/%monthnum%\/%day%\/%postname%\/", + "post_formats": [], + "default_post_format": "standard", + "default_category": 1, + "allowed_file_types": [ + "jpg", + "jpeg", + "png", + "gif", + "pdf", + "doc", + "ppt", + "odt", + "pptx", + "docx", + "pps", + "ppsx", + "xls", + "xlsx", + "key" + ], + "show_on_front": "posts", + "default_likes_enabled": true, + "default_sharing_status": false, + "default_comment_status": true, + "default_ping_status": true, + "software_version": "4.9.7-alpha-43298", + "created_at": "2013-08-05T16:39:48+00:00", + "wordads": false, + "publicize_permanently_disabled": false, + "frame_nonce": "e7bfd785f0", + "headstart": false, + "headstart_is_fresh": false, + "ak_vp_bundle_enabled": null, + "advanced_seo_front_page_description": "", + "advanced_seo_title_formats": [], + "verification_services_codes": null, + "podcasting_archive": null, + "is_domain_only": false, + "is_automated_transfer": false, + "is_wpcom_store": false, + "woocommerce_is_active": false, + "design_type": null, + "site_goals": null + } + } + ] +} From 9d4928cee9e91d613d19e7e33c96ee924761d7f2 Mon Sep 17 00:00:00 2001 From: Jorge Leandro Perez Date: Mon, 2 Jul 2018 12:55:36 -0300 Subject: [PATCH 5/7] Updates Project --- Networking/Networking.xcodeproj/project.pbxproj | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Networking/Networking.xcodeproj/project.pbxproj b/Networking/Networking.xcodeproj/project.pbxproj index 97d17ffc0c7..287124a7250 100644 --- a/Networking/Networking.xcodeproj/project.pbxproj +++ b/Networking/Networking.xcodeproj/project.pbxproj @@ -39,6 +39,9 @@ B567AF2F20A0FB8F00AB6C62 /* AuthenticatedRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B567AF2C20A0FB8F00AB6C62 /* AuthenticatedRequestTests.swift */; }; B567AF3020A0FB8F00AB6C62 /* DotcomRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B567AF2D20A0FB8F00AB6C62 /* DotcomRequestTests.swift */; }; B567AF3120A0FB8F00AB6C62 /* JetpackRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B567AF2E20A0FB8F00AB6C62 /* JetpackRequestTests.swift */; }; + B56C1EB620EA757B00D749F9 /* SiteListMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56C1EB520EA757B00D749F9 /* SiteListMapper.swift */; }; + B56C1EB820EA76F500D749F9 /* Site.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56C1EB720EA76F500D749F9 /* Site.swift */; }; + B56C1EBA20EA7D2C00D749F9 /* sites.json in Resources */ = {isa = PBXBuildFile; fileRef = B56C1EB920EA7D2C00D749F9 /* sites.json */; }; B5969E1520A47F99005E9DF1 /* RemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5969E1420A47F99005E9DF1 /* RemoteTests.swift */; }; B5BB1D0C20A2050300112D92 /* DateFormatter+Woo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BB1D0B20A2050300112D92 /* DateFormatter+Woo.swift */; }; B5BB1D1020A237FB00112D92 /* Address.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BB1D0F20A237FB00112D92 /* Address.swift */; }; @@ -97,6 +100,9 @@ B567AF2C20A0FB8F00AB6C62 /* AuthenticatedRequestTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticatedRequestTests.swift; sourceTree = ""; }; B567AF2D20A0FB8F00AB6C62 /* DotcomRequestTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DotcomRequestTests.swift; sourceTree = ""; }; B567AF2E20A0FB8F00AB6C62 /* JetpackRequestTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JetpackRequestTests.swift; sourceTree = ""; }; + B56C1EB520EA757B00D749F9 /* SiteListMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteListMapper.swift; sourceTree = ""; }; + B56C1EB720EA76F500D749F9 /* Site.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Site.swift; sourceTree = ""; }; + B56C1EB920EA7D2C00D749F9 /* sites.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = sites.json; sourceTree = ""; }; B5969E1420A47F99005E9DF1 /* RemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteTests.swift; sourceTree = ""; }; B5BB1D0B20A2050300112D92 /* DateFormatter+Woo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateFormatter+Woo.swift"; sourceTree = ""; }; B5BB1D0F20A237FB00112D92 /* Address.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Address.swift; sourceTree = ""; }; @@ -280,6 +286,7 @@ B557DA1C20979E7D005962F4 /* Order.swift */, B5BB1D1120A255EC00112D92 /* OrderStatus.swift */, B5C6FCCE20A3592900A4F8E4 /* OrderItem.swift */, + B56C1EB720EA76F500D749F9 /* Site.swift */, ); path = Model; sourceTree = ""; @@ -291,6 +298,7 @@ B559EBA920A0B5CD00836CD4 /* orders-load-all.json */, B5C6FCD520A3768900A4F8E4 /* order.json */, CE20179220E3EFA7005B4C18 /* broken-order.json */, + B56C1EB920EA7D2C00D749F9 /* sites.json */, ); path = Responses; sourceTree = ""; @@ -302,6 +310,7 @@ B505F6CC20BEE37E00BB1B69 /* AccountMapper.swift */, B5C6FCD320A373BA00A4F8E4 /* OrderMapper.swift */, B567AF2A20A0FA4200AB6C62 /* OrderListMapper.swift */, + B56C1EB520EA757B00D749F9 /* SiteListMapper.swift */, ); path = Mapper; sourceTree = ""; @@ -447,6 +456,7 @@ B505F6D520BEE4E700BB1B69 /* me.json in Resources */, B5C6FCD620A3768900A4F8E4 /* order.json in Resources */, B559EBAA20A0B5CD00836CD4 /* orders-load-all.json in Resources */, + B56C1EBA20EA7D2C00D749F9 /* sites.json in Resources */, CE20179320E3EFA7005B4C18 /* broken-order.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -517,11 +527,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B56C1EB620EA757B00D749F9 /* SiteListMapper.swift in Sources */, B557DA1A20979D66005962F4 /* Settings.swift in Sources */, B5BB1D0C20A2050300112D92 /* DateFormatter+Woo.swift in Sources */, B567AF2520A0CCA300AB6C62 /* AuthenticatedRequest.swift in Sources */, B505F6EA20BEFC3700BB1B69 /* MockupNetwork.swift in Sources */, B557DA0220975500005962F4 /* JetpackRequest.swift in Sources */, + B56C1EB820EA76F500D749F9 /* Site.swift in Sources */, B505F6CD20BEE37E00BB1B69 /* AccountMapper.swift in Sources */, B557DA0D20975DB1005962F4 /* WordPressAPIVersion.swift in Sources */, B557DA1D20979E7D005962F4 /* Order.swift in Sources */, From 867bc0d1164a2f760091f8829277708d06254ab5 Mon Sep 17 00:00:00 2001 From: Jorge Leandro Perez Date: Thu, 5 Jul 2018 11:03:01 -0300 Subject: [PATCH 6/7] Nukes AccountAction.retrieveAccount --- Yosemite/Yosemite/Actions/AccountAction.swift | 1 - Yosemite/Yosemite/Stores/AccountStore.swift | 9 --------- 2 files changed, 10 deletions(-) diff --git a/Yosemite/Yosemite/Actions/AccountAction.swift b/Yosemite/Yosemite/Actions/AccountAction.swift index 10034099ce7..9e451ae836f 100644 --- a/Yosemite/Yosemite/Actions/AccountAction.swift +++ b/Yosemite/Yosemite/Actions/AccountAction.swift @@ -7,5 +7,4 @@ import Networking // public enum AccountAction: Action { case synchronizeAccount(onCompletion: (Account?, Error?) -> Void) - case retrieveAccount(userId: Int, onCompletion: (Account?) -> Void) } diff --git a/Yosemite/Yosemite/Stores/AccountStore.swift b/Yosemite/Yosemite/Stores/AccountStore.swift index e6efaf6b96e..933b99b9224 100644 --- a/Yosemite/Yosemite/Stores/AccountStore.swift +++ b/Yosemite/Yosemite/Stores/AccountStore.swift @@ -25,8 +25,6 @@ public class AccountStore: Store { switch action { case .synchronizeAccount(let onCompletion): synchronizeAccount(onCompletion: onCompletion) - case .retrieveAccount(let userId, let onCompletion): - retrieveAccount(userId: userId, onCompletion: onCompletion) } } } @@ -51,13 +49,6 @@ extension AccountStore { onCompletion(account, nil) } } - - /// Retrieves the account associated with a given User ID (if any!). - /// - func retrieveAccount(userId: Int, onCompletion: (Account?) -> Void) { - let account = loadStoredAccount(userId: userId)?.toReadOnly() - onCompletion(account) - } } From 6f923356d4adedbbaa582bac72a80c02bd0f12b5 Mon Sep 17 00:00:00 2001 From: Jorge Leandro Perez Date: Thu, 5 Jul 2018 11:04:37 -0300 Subject: [PATCH 7/7] AccountStoreTests: Nukes dead test --- .../Stores/AccountStoreTests.swift | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/Yosemite/YosemiteTests/Stores/AccountStoreTests.swift b/Yosemite/YosemiteTests/Stores/AccountStoreTests.swift index 517034cd825..54e0febeba9 100644 --- a/Yosemite/YosemiteTests/Stores/AccountStoreTests.swift +++ b/Yosemite/YosemiteTests/Stores/AccountStoreTests.swift @@ -122,33 +122,6 @@ class AccountStoreTests: XCTestCase { let storageAccount = accountStore.loadStoredAccount(userId: remoteAccount.userID)! compare(storageAccount: storageAccount, remoteAccount: remoteAccount) } - - /// Verifies that AccountAction.retrieveAccount returns the expected Account. - /// - func testRetrieveAccountReturnsEntityWithExpectedFields() { - let accountStore = AccountStore(dispatcher: dispatcher, storageManager: storageManager, network: network) - let sampleAccount = sampleAccountPristine() - - let expectation = self.expectation(description: "Synchronize") - accountStore.upsertStoredAccount(remote: sampleAccount) - - let retrieveAccountAction = AccountAction.retrieveAccount(userId: sampleAccount.userID) { account in - guard let retrieved = account else { - XCTFail() - return - } - - XCTAssertEqual(retrieved.displayName, sampleAccount.displayName) - XCTAssertEqual(retrieved.email, sampleAccount.email) - XCTAssertEqual(retrieved.gravatarUrl, sampleAccount.gravatarUrl) - XCTAssertEqual(retrieved.userID, sampleAccount.userID) - XCTAssertEqual(retrieved.username, sampleAccount.username) - expectation.fulfill() - } - - accountStore.onAction(retrieveAccountAction) - wait(for: [expectation], timeout: Constants.expectationTimeout) - } }