Skip to content

Commit

Permalink
Create a simple UI test using KIF to ensure the app always starts ok
Browse files Browse the repository at this point in the history
  • Loading branch information
pietrocaselani committed May 18, 2019
1 parent e9e622d commit 3979a83
Show file tree
Hide file tree
Showing 19 changed files with 816 additions and 22 deletions.
2 changes: 2 additions & 0 deletions .swiftlint.yml
Expand Up @@ -3,6 +3,8 @@ excluded:
- GeneratedFiles
- vendor
- CouchTrackerCoreTests
- CouchTrackerUITests
- CouchTrackerAppTestable
- TraktSwiftTests
- TraktSwiftTestable
- TMDBSwiftTests
Expand Down
409 changes: 405 additions & 4 deletions CouchTracker.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Expand Up @@ -29,6 +29,16 @@
codeCoverageEnabled = "YES"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "827BC4A3228E615000459ECD"
BuildableName = "CouchTrackerUITests.xctest"
BlueprintName = "CouchTrackerUITests"
ReferencedContainer = "container:CouchTracker.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
Expand Down
28 changes: 28 additions & 0 deletions CouchTrackerApp/AppEnvironment.swift
@@ -0,0 +1,28 @@
import CouchTrackerCore
import CouchTrackerPersistence
import Moya
import TMDBSwift
import TraktSwift
import TVDBSwift

public struct AppEnvironment {
public var trakt: TraktProvider = Environment.instance.trakt
public var tmdb: TMDBProvider = Environment.instance.tmdb
public var tvdb: TVDBProvider = Environment.instance.tvdb
public var schedulers: Schedulers = Environment.instance.schedulers
public var realmProvider: RealmProvider = Environment.instance.realmProvider
public var buildConfig: BuildConfig = Environment.instance.buildConfig
public var appStateManager: AppStateManager = Environment.instance.appStateManager
public var showsSynchronizer: WatchedShowsSynchronizer = Environment.instance.showsSynchronizer
public var showSynchronizer: WatchedShowSynchronizer = Environment.instance.showSynchronizer
public var watchedShowEntitiesObservable: WatchedShowEntitiesObservable = Environment.instance.watchedShowEntitiesObservable
public var watchedShowEntityObserable: WatchedShowEntityObserable = Environment.instance.watchedShowEntityObserable
public var userDefaults: UserDefaults = Environment.instance.userDefaults
public var genreRepository: GenreRepository = Environment.instance.genreRepository
public var imageRepository: ImageRepository = Environment.instance.imageRepository
public var configurationRepository: ConfigurationCachedRepository = Environment.instance.configurationRepository
public var movieImageRepository: MovieImageCachedRepository = Environment.instance.movieImageRepository
public var showImageRepository: ShowImageCachedRepository = Environment.instance.showImageRepository
public var episodeImageRepository: EpisodeImageCachedRepository = Environment.instance.episodeImageRepository
public var syncStateObservable: SyncStateObservable = Environment.instance.syncStateObservable
}
32 changes: 31 additions & 1 deletion CouchTrackerApp/Environment.swift
Expand Up @@ -35,8 +35,38 @@ public final class Environment {
return UserDefaultsAppStateDataHolder.currentAppConfig(userDefaults)
}

init(trakt: TraktProvider, tmdb: TMDBProvider, tvdb: TVDBProvider,
schedulers: Schedulers, realmProvider: RealmProvider, buildConfig: BuildConfig,
appStateManager: AppStateManager, showsSynchronizer: WatchedShowsSynchronizer,
showSynchronizer: WatchedShowSynchronizer, watchedShowEntitiesObservable: WatchedShowEntitiesObservable,
watchedShowEntityObserable: WatchedShowEntityObserable, userDefaults: UserDefaults,
genreRepository: GenreRepository, imageRepository: ImageRepository,
configurationRepository: ConfigurationCachedRepository,
movieImageRepository: MovieImageCachedRepository, showImageRepository: ShowImageCachedRepository,
episodeImageRepository: EpisodeImageCachedRepository, syncStateObservable: SyncStateObservable) {
self.trakt = trakt
self.tmdb = tmdb
self.tvdb = tvdb
self.schedulers = schedulers
self.realmProvider = realmProvider
self.buildConfig = buildConfig
self.appStateManager = appStateManager
self.showsSynchronizer = showsSynchronizer
self.showSynchronizer = showSynchronizer
self.watchedShowEntitiesObservable = watchedShowEntitiesObservable
self.watchedShowEntityObserable = watchedShowEntityObserable
self.userDefaults = userDefaults
self.genreRepository = genreRepository
self.imageRepository = imageRepository
self.configurationRepository = configurationRepository
self.movieImageRepository = movieImageRepository
self.showImageRepository = showImageRepository
self.episodeImageRepository = episodeImageRepository
self.syncStateObservable = syncStateObservable
}

// swiftlint:disable function_body_length
private init() {
init() {
userDefaults = UserDefaults.standard
let schedulers = DefaultSchedulers.instance

Expand Down
10 changes: 10 additions & 0 deletions CouchTrackerAppTestable/AppEnvironmentMock.swift
@@ -0,0 +1,10 @@
import CouchTrackerCore
import CouchTrackerPersistence
import Moya
import TMDBSwift
import TraktSwift
import TVDBSwift

@testable import CouchTrackerApp

extension AppEnvironment {}
19 changes: 19 additions & 0 deletions CouchTrackerAppTestable/CouchTrackerAppTestable.h
@@ -0,0 +1,19 @@
//
// CouchTrackerAppTestable.h
// CouchTrackerAppTestable
//
// Created by Pietro Caselani on 17/05/19.
// Copyright © 2019 Pietro Caselani. All rights reserved.
//

#import <UIKit/UIKit.h>

//! Project version number for CouchTrackerAppTestable.
FOUNDATION_EXPORT double CouchTrackerAppTestableVersionNumber;

//! Project version string for CouchTrackerAppTestable.
FOUNDATION_EXPORT const unsigned char CouchTrackerAppTestableVersionString[];

// In this header, you should import all the public headers of your framework using statements like #import <CouchTrackerAppTestable/PublicHeader.h>


22 changes: 22 additions & 0 deletions CouchTrackerAppTestable/Info.plist
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>0.0.1</string>
<key>CFBundleVersion</key>
<string>37</string>
</dict>
</plist>
207 changes: 207 additions & 0 deletions CouchTrackerAppTestable/KIFExtensions.swift
@@ -0,0 +1,207 @@
import Foundation
import KIF
import XCTest

public extension XCTestCase {
func tester(file: String = #file, _ line: Int = #line) -> KIFUITestActor {
return KIFUITestActor(inFile: file, atLine: line, delegate: self)
}

func system(file: String = #file, _ line: Int = #line) -> KIFSystemTestActor {
return KIFSystemTestActor(inFile: file, atLine: line, delegate: self)
}
}

public extension KIFTestActor {
func tester(file: String = #file, _ line: Int = #line) -> KIFUITestActor {
return KIFUITestActor(inFile: file, atLine: line, delegate: self)
}

func system(file: String = #file, _ line: Int = #line) -> KIFSystemTestActor {
return KIFSystemTestActor(inFile: file, atLine: line, delegate: self)
}
}

extension UIView {
var keyWindow: UIWindow? { return UIApplication.shared.keyWindow }

public func isVisible() -> Bool {
guard let rect = superview?.convert(frame, to: nil) else { return false }
let isInWindow = keyWindow?.bounds.contains(rect) ?? false
return isInWindow
&& !isHidden
&& alpha > 0
}
}

extension UITableView {
public func scrollToTop() {
setContentOffset(.zero, animated: false)
}

public func scrollToBottom() {
setContentOffset(CGPoint(x: 0, y: contentSize.height), animated: false)
}
}

extension KIFUITestActor {
var keyWindow: UIWindow? { return UIApplication.shared.keyWindow }

// MARK: - Instant matchers

public func element(withAccessibilityHint hint: String) -> UIAccessibilityElement? {
return keyWindow?.accessibilityElement { $0?.accessibilityHint == hint }
}

public func element(withAccessibilityIdentifier identifier: String) -> UIAccessibilityElement? {
return keyWindow?.accessibilityElement { $0?.accessibilityIdentifier == identifier }
}

public func view(withAccessibilityHint hint: String) -> UIView? {
return keyWindow?.view { $0?.accessibilityHint == hint }
}

public func view(withAccessibilityLabel label: String) -> UIView? {
return keyWindow?.view { $0?.accessibilityLabel == label }
}

public func view(withAccessibilityIdentifier identifier: String) -> UIView? {
return keyWindow?.view { $0?.accessibilityIdentifier == identifier }
}

// MARK: - Partial instant matchers

public func element(withAccessibilityLabelThatContains label: String) -> UIAccessibilityElement? {
return keyWindow?.accessibilityElement { $0?.accessibilityLabel?.contains(label) ?? false }
}

public func view(withAccessibilityLabelThatContains string: String) -> UIView? {
return keyWindow?.view { $0?.accessibilityLabel?.contains(string) ?? false }
}

public func view(withAccessibilityHintThatContains string: String) -> UIView? {
return keyWindow?.view { $0?.accessibilityHint?.contains(string) ?? false }
}

// MARK: - Partial tap matchers

public func tapElement(withAccessibilityLabelThatContains string: String) {
var element: UIAccessibilityElement?
wait(for: &element,
view: nil,
withElementMatching: NSPredicate(format: "accessibilityLabel CONTAINS %@", string),
tappable: true)

if let e = element {
tap(element: e)
} else {
XCTFail("Failed to find element that contains: \(string)")
}
}

public func tapView(withAccessibilityIdentifier identifier: String) {
var view: UIView?
wait(for: nil, view: &view, withIdentifier: identifier, tappable: true)
view?.tap()
}

public func tapNavigationBackButton() {
tester().tapScreen(at: CGPoint(x: 15, y: 25))
}

// MARK: - Swipe actions

public func swipeView(withAccessibilityIdentifier identifier: String, inDirection direction: KIFSwipeDirection) {
var view: UIView?
var element: UIAccessibilityElement?

wait(for: &element, view: &view, withIdentifier: identifier, tappable: true)
swipeAccessibilityElement(element, in: view, in: direction)
}

// MARK: - TableView/Scroll actions

public func firstTableView() -> UITableView? {
return keyWindow?.view { $0 is UITableView } as! UITableView?
}

public func scroll(_ tableView: UITableView?, toIndexPath indexPath: IndexPath) {
guard let tableView = tableView else { return XCTFail("A tableView was not provided.") }
tableView.scrollToRow(at: indexPath, at: .middle, animated: false)
}

// MARK: - Condition waiters

public func wait(for timeout: TimeInterval? = nil, untilTrue block: @escaping () -> Bool) {
let waitBlock: KIFTestExecutionBlock = { _ in block() ? .success : .wait }
if let t = timeout {
run(waitBlock, timeout: t)
} else {
run(waitBlock)
}
}

// MARK: - View waiters

@discardableResult
public func waitForView(withAccessibilityIdentifier identifier: String) -> UIView? {
var view: UIView?
wait(for: nil,
view: &view,
withElementMatching: NSPredicate(format: "accessibilityIdentifier == %@", identifier),
tappable: false)

return view
}

@discardableResult
public func waitForView(withAccessibilityLabelThatContains label: String, tappable: Bool = true) -> UIView? {
var view: UIView?
wait(for: nil,
view: &view,
withElementMatching: NSPredicate(format: "accessibilityLabel CONTAINS %@", label),
tappable: tappable)

return view
}

public func waitForAbsenceOfView(withAccessibilityIdentifier identifier: String) {
waitForAbsenceOfViewWithElement(matching: NSPredicate(format: "accessibilityIdentifer = %@", identifier))
}

public func waitForAbsenceOfView(withAccessibilityHint hint: String) {
waitForAbsenceOfViewWithElement(matching: NSPredicate(format: "accessibilityHint = %@", hint))
}
}

// MARK: - Private methods

extension KIFUITestActor {
func tap(element: NSObject) {
if let view = element as? UIView {
view.tap()
} else {
let center = CGPoint(x: element.accessibilityFrame.origin.x + element.accessibilityFrame.size.width / 2,
y: element.accessibilityFrame.origin.y + element.accessibilityFrame.size.height / 2)
tapScreen(at: center)
}
}
}

extension KIFUITestActor {
public func speedUp() { keyWindow?.layer.speed = 1e2 }
public func restoreSpeed() { keyWindow?.layer.speed = 1 }
}

extension UIWindow {
public func view(matching matchBlock: @escaping ((UIView?) -> Bool)) -> UIView? {
let element = accessibilityElement(matching: { (element) -> Bool in
if let view = (element as Any?) as? UIView {
return matchBlock(view)
}
return false
})

return (element as Any?) as! UIView?
}
}
2 changes: 1 addition & 1 deletion CouchTrackerCore/Core/BundleProvider.swift
Expand Up @@ -53,7 +53,7 @@ public final class DefaultBundleProvider: BundleProvider, LanguageProvider {
private static func languageBundleName(for language: SupportedLanguages) -> String {
switch language {
case .englishUS: return "en"
case .portugueseBrazil: return "pt-BR"
case .portugueseBR: return "pt-BR"
}
}
}
2 changes: 1 addition & 1 deletion CouchTrackerCore/Localization/SupportedLanguages.swift
Expand Up @@ -2,7 +2,7 @@ import Foundation

public enum SupportedLanguages: String, Hashable, CaseIterable {
case englishUS = "en_US"
case portugueseBrazil = "pt_BR"
case portugueseBR = "pt_BR"

var asLocale: Locale {
return Locale(identifier: rawValue)
Expand Down
Expand Up @@ -18,9 +18,9 @@ final class DatePresentableTests: XCTestCase {
}

func testDatePresentable_inPortugueseBrazil() {
DefaultBundleProvider.update(language: .portugueseBrazil)
DefaultBundleProvider.update(language: .portugueseBR)

XCTAssertEqual(DefaultBundleProvider.instance.currentLanguage, .portugueseBrazil)
XCTAssertEqual(DefaultBundleProvider.instance.currentLanguage, .portugueseBR)

let date = Date(timeIntervalSince1970: 1_557_187_200) // 2019-05-07 00:00:00 +0000
let expectedString = "06 de maio"
Expand Down
11 changes: 11 additions & 0 deletions CouchTrackerUITests/AppStartupEnglishUSTests.swift
@@ -0,0 +1,11 @@
import CouchTrackerAppTestable
import CouchTrackerCore
import KIF

final class AppStartupEnglishUSTests: KIFTestCase {
func testChangeTabs_enUS() {
tester().tapView(withAccessibilityLabel: "Shows")
tester().tapView(withAccessibilityLabel: "Settings")
tester().tapView(withAccessibilityLabel: "Movies")
}
}

0 comments on commit 3979a83

Please sign in to comment.