mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-07 08:18:41 +09:00
Dactyloidae iOS initial commit
This commit is contained in:
parent
daa6179d22
commit
7154a0497e
2123 changed files with 197052 additions and 0 deletions
284
mobile/ios/ClientTests/ActivityStreamTests.swift
Normal file
284
mobile/ios/ClientTests/ActivityStreamTests.swift
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import UIKit
|
||||
import XCTest
|
||||
@testable import Client
|
||||
import Shared
|
||||
import Storage
|
||||
import Deferred
|
||||
import SyncTelemetry
|
||||
|
||||
class ActivityStreamTests: XCTestCase {
|
||||
var profile: MockProfile!
|
||||
var panel: ActivityStreamPanel!
|
||||
var mockPingClient: MockPingClient!
|
||||
var telemetry: ActivityStreamTracker!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
self.profile = MockProfile()
|
||||
self.telemetry = ActivityStreamTracker(eventsTracker: MockPingClient(), sessionsTracker: MockPingClient())
|
||||
self.panel = ActivityStreamPanel(profile: profile, telemetry: self.telemetry)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
mockPingClient = nil
|
||||
}
|
||||
|
||||
func testDeletionOfSingleSuggestedSite() {
|
||||
let siteToDelete = panel.defaultTopSites()[0]
|
||||
|
||||
panel.hideURLFromTopSites(siteToDelete)
|
||||
let newSites = panel.defaultTopSites()
|
||||
|
||||
XCTAssertFalse(newSites.contains(siteToDelete, f: { (a, b) -> Bool in
|
||||
return a.url == b.url
|
||||
}))
|
||||
}
|
||||
|
||||
func testDeletionOfAllDefaultSites() {
|
||||
let defaultSites = panel.defaultTopSites()
|
||||
defaultSites.forEach({
|
||||
panel.hideURLFromTopSites($0)
|
||||
})
|
||||
|
||||
let newSites = panel.defaultTopSites()
|
||||
XCTAssertTrue(newSites.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Telemetry Tests
|
||||
extension ActivityStreamTests {
|
||||
fileprivate func expectedPayloadForEvent(_ event: String, source: String, position: Int) -> [String: Any] {
|
||||
return [
|
||||
"event": event,
|
||||
"page": "NEW_TAB",
|
||||
"source": source,
|
||||
"action_position": position,
|
||||
"app_version": AppInfo.appVersion,
|
||||
"build": AppInfo.buildNumber,
|
||||
"locale": Locale.current.identifier,
|
||||
"release_channel": AppConstants.BuildChannel.rawValue
|
||||
]
|
||||
}
|
||||
|
||||
fileprivate func expectedBadStatePayload(state: String, source: String) -> [String: Any] {
|
||||
return [
|
||||
"event": state,
|
||||
"page": "NEW_TAB",
|
||||
"source": source,
|
||||
"app_version": AppInfo.appVersion,
|
||||
"build": AppInfo.buildNumber,
|
||||
"locale": Locale.current.identifier,
|
||||
"release_channel": AppConstants.BuildChannel.rawValue
|
||||
]
|
||||
}
|
||||
|
||||
fileprivate func assertPayload(_ actual: [String: Any], matches: [String: Any]) {
|
||||
XCTAssertTrue(actual.count == matches.count)
|
||||
actual.enumerated().forEach { index, element in
|
||||
if let actualValue = element.1 as? Int,
|
||||
let matchesValue = matches[element.0] as? Int {
|
||||
XCTAssertTrue(actualValue == matchesValue)
|
||||
}
|
||||
|
||||
if let actualValue = element.1 as? String,
|
||||
let matchesValue = matches[element.0] as? String {
|
||||
XCTAssertTrue(actualValue == matchesValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testHighlightEmitsEventOnTap() {
|
||||
let mockSite = Site(url: "http://mozilla.org", title: "Mozilla")
|
||||
panel.highlights = [mockSite]
|
||||
panel.selectItemAtIndex(0, inSection: .highlights)
|
||||
|
||||
let pingsSent = (telemetry.eventsTracker as! MockPingClient).pingsReceived
|
||||
XCTAssertEqual(pingsSent.count, 1)
|
||||
let eventPing = pingsSent[0]
|
||||
assertPayload(eventPing, matches: expectedPayloadForEvent("CLICK", source: "HIGHLIGHTS", position: 0))
|
||||
}
|
||||
|
||||
func testContextMenuOnTopSiteEmitsRemoveEvent() {
|
||||
let mockSite = Site(url: "http://mozilla.org", title: "Mozilla")
|
||||
let topSitesContextMenu = panel.contextMenu(for: mockSite, with: IndexPath(item: 0, section: ActivityStreamPanel.Section.topSites.rawValue))
|
||||
|
||||
let removeAction = topSitesContextMenu?.actions[0].find { $0.title == Strings.RemoveContextMenuTitle }
|
||||
removeAction?.handler?(removeAction!)
|
||||
|
||||
let pingsSent = (telemetry.eventsTracker as! MockPingClient).pingsReceived
|
||||
XCTAssertEqual(pingsSent.count, 1)
|
||||
let removePing = pingsSent[0]
|
||||
assertPayload(removePing, matches: expectedPayloadForEvent("REMOVE", source: "TOP_SITES", position: 0))
|
||||
}
|
||||
|
||||
func testContextMenuOnHighlightsEmitsRemoveDismissEvents() {
|
||||
let mockSite = Site(url: "http://mozilla.org", title: "Mozilla")
|
||||
let highlightsContextMenu = panel.contextMenu(for: mockSite, with: IndexPath(row: 0, section: ActivityStreamPanel.Section.highlights.rawValue))
|
||||
|
||||
let dismiss = highlightsContextMenu?.actions[0].find { $0.title == Strings.RemoveContextMenuTitle }
|
||||
let delete = highlightsContextMenu?.actions[0].find { $0.title == Strings.DeleteFromHistoryContextMenuTitle }
|
||||
|
||||
dismiss?.handler?(dismiss!)
|
||||
delete?.handler?(delete!)
|
||||
|
||||
// Check to see that they emitted telemetry events
|
||||
let pingsSent = (telemetry.eventsTracker as! MockPingClient).pingsReceived
|
||||
XCTAssertEqual(pingsSent.count, 2)
|
||||
|
||||
let dismissPing = pingsSent[0]
|
||||
assertPayload(dismissPing, matches: expectedPayloadForEvent("DISMISS", source: "HIGHLIGHTS", position: 0))
|
||||
|
||||
let deletePing = pingsSent[1]
|
||||
assertPayload(deletePing, matches: expectedPayloadForEvent("DELETE", source: "HIGHLIGHTS", position: 0))
|
||||
}
|
||||
|
||||
func testSessionReportedWhenViewAppearsAndDisappears() {
|
||||
// Simulate the panel opening and closing with a second in between for some session_duration
|
||||
panel.viewWillAppear(false)
|
||||
var pingsSent = (telemetry.sessionsTracker as! MockPingClient).pingsReceived
|
||||
XCTAssertEqual(pingsSent.count, 0)
|
||||
|
||||
wait(1)
|
||||
panel.viewDidDisappear(false)
|
||||
|
||||
pingsSent = (telemetry.sessionsTracker as! MockPingClient).pingsReceived
|
||||
XCTAssertEqual(pingsSent.count, 1)
|
||||
|
||||
let eventPing = pingsSent[0]
|
||||
XCTAssertNotNil(eventPing["session_duration"])
|
||||
}
|
||||
|
||||
func testBadStateEventsForHighlights() {
|
||||
let goodSite = Site(url: "http://mozilla.org", title: "Mozilla")
|
||||
goodSite.icon = Favicon(url: "http://image", date: Date(), type: .local)
|
||||
goodSite.metadata = PageMetadata(id: nil,
|
||||
siteURL: "http://mozilla.org",
|
||||
mediaURL: "http://image",
|
||||
title: "Mozilla",
|
||||
description: "Web",
|
||||
type: nil,
|
||||
providerName: nil,
|
||||
mediaDataURI: nil)
|
||||
let badSite = Site(url: "http://mozilla.org", title: "Mozilla")
|
||||
profile.recommendations = MockRecommender(highlights: [goodSite, badSite])
|
||||
|
||||
// Since invalidateHighlights calls back into the main thread, we can't
|
||||
// simply call .value on this to block since the app will dead lock when
|
||||
// trying to call back onto a blocked main thread.
|
||||
let expect = XCTestExpectation(description: "Sent bad highlight pings")
|
||||
panel.getHighlights() >>> {
|
||||
expect.fulfill()
|
||||
}
|
||||
|
||||
wait(for: [expect], timeout: 3)
|
||||
let pingsSent = (self.telemetry.eventsTracker as! MockPingClient).pingsReceived
|
||||
XCTAssertEqual(pingsSent.count, 2)
|
||||
assertPayload(pingsSent[0],
|
||||
matches: expectedBadStatePayload(state: "MISSING_METADATA_IMAGE", source: "HIGHLIGHTS"))
|
||||
assertPayload(pingsSent[1],
|
||||
matches: expectedBadStatePayload(state: "MISSING_FAVICON", source: "HIGHLIGHTS"))
|
||||
}
|
||||
|
||||
func testBadStateEventsForTopSites() {
|
||||
let goodSite = Site(url: "http://mozilla.org", title: "Mozilla")
|
||||
goodSite.icon = Favicon(url: "http://image", date: Date(), type: .local)
|
||||
goodSite.metadata = PageMetadata(id: nil,
|
||||
siteURL: "http://mozilla.org",
|
||||
mediaURL: "http://image",
|
||||
title: "Mozilla",
|
||||
description: "Web",
|
||||
type: nil,
|
||||
providerName: nil,
|
||||
mediaDataURI: nil)
|
||||
let badSite = Site(url: "http://mozilla.org", title: "Mozilla")
|
||||
profile.history = MockTopSitesHistory(sites: [goodSite, badSite])
|
||||
|
||||
// Since invalidateHighlights calls back into the main thread, we can't
|
||||
// simply call .value on this to block since the app will dead lock when
|
||||
// trying to call back onto a blocked main thread.
|
||||
let expect = XCTestExpectation(description: "Sent bad top site pings")
|
||||
panel.getTopSites() >>> {
|
||||
expect.fulfill()
|
||||
}
|
||||
|
||||
wait(for: [expect], timeout: 3)
|
||||
let pingsSent = (self.telemetry.eventsTracker as! MockPingClient).pingsReceived
|
||||
XCTAssertEqual(pingsSent.count, 2)
|
||||
assertPayload(pingsSent[0],
|
||||
matches: expectedBadStatePayload(state: "MISSING_METADATA_IMAGE", source: "TOP_SITES"))
|
||||
assertPayload(pingsSent[1],
|
||||
matches: expectedBadStatePayload(state: "MISSING_FAVICON", source: "TOP_SITES"))
|
||||
}
|
||||
}
|
||||
|
||||
class MockPingClient: PingCentreClient {
|
||||
|
||||
var pingsReceived: [[String: Any]] = []
|
||||
|
||||
public func sendPing(_ data: [String : Any], validate: Bool) -> Success {
|
||||
pingsReceived.append(data)
|
||||
return succeed()
|
||||
}
|
||||
|
||||
public func sendBatch(_ data: [[String : Any]], validate: Bool) -> Success {
|
||||
pingsReceived += data
|
||||
return succeed()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate class MockRecommender: HistoryRecommendations {
|
||||
func repopulateHighlights() -> Success {
|
||||
return succeed()
|
||||
}
|
||||
|
||||
var highlights: [Site]
|
||||
|
||||
init(highlights: [Site]) {
|
||||
self.highlights = highlights
|
||||
}
|
||||
|
||||
func getHighlights() -> Deferred<Maybe<Cursor<Site>>> {
|
||||
return deferMaybe(ArrayCursor(data: highlights))
|
||||
}
|
||||
|
||||
func getRecentBookmarks(_ limit: Int) -> Deferred<Maybe<Cursor<Site>>> {
|
||||
return deferMaybe(ArrayCursor(data: []))
|
||||
}
|
||||
|
||||
func repopulate(invalidateTopSites shouldInvalidateTopSites: Bool, invalidateHighlights shouldInvalidateHighlights: Bool) -> Success {
|
||||
return succeed()
|
||||
}
|
||||
|
||||
func removeHighlightForURL(_ url: String) -> Success {
|
||||
guard let foundSite = highlights.filter({ $0.url == url }).first else {
|
||||
return succeed()
|
||||
}
|
||||
let foundIndex = highlights.index(of: foundSite)!
|
||||
highlights.remove(at: foundIndex)
|
||||
return succeed()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate class MockTopSitesHistory: MockableHistory {
|
||||
let mockTopSites: [Site]
|
||||
|
||||
init(sites: [Site]) {
|
||||
mockTopSites = sites
|
||||
}
|
||||
|
||||
override func getTopSitesWithLimit(_ limit: Int) -> Deferred<Maybe<Cursor<Site>>> {
|
||||
return deferMaybe(ArrayCursor(data: mockTopSites))
|
||||
}
|
||||
|
||||
override func getPinnedTopSites() -> Deferred<Maybe<Cursor<Site>>> {
|
||||
return deferMaybe(ArrayCursor(data: []))
|
||||
}
|
||||
|
||||
override func updateTopSitesCacheIfInvalidated() -> Deferred<Maybe<Bool>> {
|
||||
return deferMaybe(true)
|
||||
}
|
||||
}
|
||||
173
mobile/ios/ClientTests/AuthenticatorTests.swift
Normal file
173
mobile/ios/ClientTests/AuthenticatorTests.swift
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
import Shared
|
||||
import Deferred
|
||||
@testable import Client
|
||||
@testable import Storage
|
||||
|
||||
class MockFiles: FileAccessor {
|
||||
init() {
|
||||
let docPath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0]
|
||||
super.init(rootPath: (docPath as NSString).appendingPathComponent("testing"))
|
||||
}
|
||||
}
|
||||
|
||||
class MockChallengeSender: NSObject, URLAuthenticationChallengeSender {
|
||||
func use(_ credential: URLCredential, for challenge: URLAuthenticationChallenge) {}
|
||||
func continueWithoutCredential(for challenge: URLAuthenticationChallenge) {}
|
||||
func cancel(_ challenge: URLAuthenticationChallenge) {}
|
||||
}
|
||||
|
||||
class MockMalformableLogin: LoginData {
|
||||
var guid: String
|
||||
var credentials: URLCredential
|
||||
var protectionSpace: URLProtectionSpace
|
||||
var hostname: String
|
||||
var username: String?
|
||||
var password: String
|
||||
var httpRealm: String?
|
||||
var formSubmitURL: String?
|
||||
var usernameField: String?
|
||||
var passwordField: String?
|
||||
var hasMalformedHostname = true
|
||||
var isValid = Maybe(success: ())
|
||||
|
||||
static func createWithHostname(_ hostname: String, username: String, password: String, formSubmitURL: String) -> MockMalformableLogin {
|
||||
return self.init(guid: Bytes.generateGUID(), hostname: hostname, username: username, password: password, formSubmitURL: formSubmitURL)
|
||||
}
|
||||
|
||||
required init(guid: String, hostname: String, username: String, password: String, formSubmitURL: String) {
|
||||
self.guid = guid
|
||||
self.credentials = URLCredential(user: username, password: password, persistence: URLCredential.Persistence.none)
|
||||
self.hostname = hostname
|
||||
self.password = password
|
||||
self.username = username
|
||||
self.protectionSpace = URLProtectionSpace(host: hostname, port: 0, protocol: nil, realm: nil, authenticationMethod: nil)
|
||||
self.formSubmitURL = formSubmitURL
|
||||
}
|
||||
|
||||
func toDict() -> [String: String] {
|
||||
// Not used for this mock
|
||||
return [String: String]()
|
||||
}
|
||||
|
||||
func isSignificantlyDifferentFrom(_ login: LoginData) -> Bool {
|
||||
// Not used for this mock
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private let MainLoginColumns = "guid, username, password, hostname, httpRealm, formSubmitURL, usernameField, passwordField"
|
||||
|
||||
class AuthenticatorTests: XCTestCase {
|
||||
|
||||
fileprivate var db: BrowserDB!
|
||||
fileprivate var logins: SQLiteLogins!
|
||||
fileprivate var mockVC = UIViewController()
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
self.db = BrowserDB(filename: "testsqlitelogins.db", schema: LoginsSchema(), files: MockFiles())
|
||||
self.logins = SQLiteLogins(db: self.db)
|
||||
self.logins.removeAll().succeeded()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
self.logins.removeAll().succeeded()
|
||||
}
|
||||
|
||||
fileprivate func mockChallengeForURL(_ url: URL, username: String, password: String) -> URLAuthenticationChallenge {
|
||||
let scheme = url.scheme
|
||||
let host = url.host ?? ""
|
||||
let port = (url as NSURL).port?.intValue ?? 80
|
||||
|
||||
let credential = URLCredential(user: username, password: password, persistence: .none)
|
||||
let protectionSpace = URLProtectionSpace(host: host,
|
||||
port: port,
|
||||
protocol: scheme,
|
||||
realm: "Secure Site",
|
||||
authenticationMethod: nil)
|
||||
return URLAuthenticationChallenge(protectionSpace: protectionSpace,
|
||||
proposedCredential: credential,
|
||||
previousFailureCount: 0,
|
||||
failureResponse: nil,
|
||||
error: nil,
|
||||
sender: MockChallengeSender())
|
||||
}
|
||||
|
||||
fileprivate func hostnameFactory(_ row: SDRow) -> String {
|
||||
return row["hostname"] as! String
|
||||
}
|
||||
|
||||
fileprivate func rawQueryForAllLogins() -> Deferred<Maybe<Cursor<String>>> {
|
||||
let projection = MainLoginColumns
|
||||
let sql =
|
||||
"SELECT \(projection) FROM " +
|
||||
"\(TableLoginsLocal) WHERE is_deleted = 0 " +
|
||||
"UNION ALL " +
|
||||
"SELECT \(projection) FROM " +
|
||||
"\(TableLoginsMirror) WHERE is_overridden = 0 " +
|
||||
"ORDER BY hostname ASC"
|
||||
return db.runQuery(sql, args: nil, factory: hostnameFactory)
|
||||
}
|
||||
|
||||
func testChallengeMatchesLoginEntry() {
|
||||
let login = Login.createWithHostname("https://securesite.com", username: "username", password: "password", formSubmitURL: "https://submit.me")
|
||||
logins.addLogin(login).succeeded()
|
||||
let challenge = mockChallengeForURL(URL(string: "https://securesite.com")!, username: "username", password: "password")
|
||||
let result = Authenticator.findMatchingCredentialsForChallenge(challenge, fromLoginsProvider: logins).value.successValue!
|
||||
XCTAssertNotNil(result)
|
||||
XCTAssertEqual(result?.user, "username")
|
||||
XCTAssertEqual(result?.password, "password")
|
||||
}
|
||||
|
||||
func testChallengeMatchesSingleMalformedLoginEntry() {
|
||||
// Since Login has been updated to not store schemeless URL, write directly to simulate a malformed URL
|
||||
let malformedLogin = MockMalformableLogin.createWithHostname("malformed.com", username: "username", password: "password", formSubmitURL: "https://submit.me")
|
||||
logins.addLogin(malformedLogin).succeeded()
|
||||
|
||||
// Pre-condition: Check that the hostname is malformed
|
||||
let oldHostname = rawQueryForAllLogins().value.successValue![0]
|
||||
XCTAssertEqual(oldHostname, "malformed.com")
|
||||
|
||||
let challenge = mockChallengeForURL(URL(string: "https://malformed.com")!, username: "username", password: "password")
|
||||
let result = Authenticator.findMatchingCredentialsForChallenge(challenge, fromLoginsProvider: logins).value.successValue!
|
||||
XCTAssertNotNil(result)
|
||||
XCTAssertEqual(result?.user, "username")
|
||||
XCTAssertEqual(result?.password, "password")
|
||||
|
||||
// Post-condition: Check that we updated the hostname to be not malformed
|
||||
let newHostname = rawQueryForAllLogins().value.successValue![0]
|
||||
XCTAssertEqual(newHostname, "https://malformed.com")
|
||||
}
|
||||
|
||||
func testChallengeMatchesDuplicateLoginEntries() {
|
||||
// Since Login has been updated to not store schemeless URL, write directly to simulate a malformed URL
|
||||
let malformedLogin = MockMalformableLogin.createWithHostname("malformed.com", username: "malformed_username", password: "malformed_password", formSubmitURL: "https://submit.me")
|
||||
logins.addLogin(malformedLogin).succeeded()
|
||||
|
||||
let login = Login.createWithHostname("https://malformed.com", username: "good_username", password: "good_password", formSubmitURL: "https://submit.me")
|
||||
logins.addLogin(login).succeeded()
|
||||
|
||||
// Pre-condition: Verify that both logins were stored
|
||||
let hostnames = rawQueryForAllLogins().value.successValue!
|
||||
XCTAssertEqual(hostnames.count, 2)
|
||||
XCTAssertEqual(hostnames[0], "https://malformed.com")
|
||||
XCTAssertEqual(hostnames[1], "malformed.com")
|
||||
|
||||
let challenge = mockChallengeForURL(URL(string: "https://malformed.com")!, username: "username", password: "password")
|
||||
let result = Authenticator.findMatchingCredentialsForChallenge(challenge, fromLoginsProvider: logins).value.successValue!
|
||||
XCTAssertNotNil(result)
|
||||
XCTAssertEqual(result?.user, "good_username")
|
||||
XCTAssertEqual(result?.password, "good_password")
|
||||
|
||||
// Post-condition: Verify that malformed URL was removed
|
||||
let newHostnames = rawQueryForAllLogins().value.successValue!
|
||||
XCTAssertEqual(newHostnames.count, 1)
|
||||
XCTAssertEqual(newHostnames[0], "https://malformed.com")
|
||||
}
|
||||
}
|
||||
94
mobile/ios/ClientTests/ClientTests.swift
Normal file
94
mobile/ios/ClientTests/ClientTests.swift
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import UIKit
|
||||
import XCTest
|
||||
|
||||
import Shared
|
||||
import Storage
|
||||
import WebKit
|
||||
import Alamofire
|
||||
@testable import Client
|
||||
|
||||
class ClientTests: XCTestCase {
|
||||
|
||||
func testSyncUA() {
|
||||
let ua = UserAgent.syncUserAgent
|
||||
let device = DeviceInfo.deviceModel()
|
||||
let systemVersion = UIDevice.current.systemVersion
|
||||
let expectedRegex = "^Firefox-iOS-Sync/[0-9\\.]+b[0-9]* \\(\(device); iPhone OS \(systemVersion)\\) \\([-_A-Za-z0-9= \\(\\)]+\\)$"
|
||||
let loc = ua.range(of: expectedRegex, options: NSString.CompareOptions.regularExpression)
|
||||
XCTAssertTrue(loc != nil, "Sync UA is as expected. Was \(ua)")
|
||||
}
|
||||
|
||||
// Simple test to make sure the WKWebView UA matches the expected FxiOS pattern.
|
||||
func testUserAgent() {
|
||||
let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String
|
||||
|
||||
let compare: (String) -> Bool = { ua in
|
||||
let range = ua.range(of: "^Mozilla/5\\.0 \\(.+\\) AppleWebKit/[0-9\\.]+ \\(KHTML, like Gecko\\) FxiOS/\(appVersion)b[0-9]* Mobile/[A-Za-z0-9]+ Safari/[0-9\\.]+$", options: NSString.CompareOptions.regularExpression)
|
||||
return range != nil
|
||||
}
|
||||
|
||||
XCTAssertTrue(compare(UserAgent.defaultUserAgent()), "User agent computes correctly.")
|
||||
XCTAssertTrue(compare(UserAgent.cachedUserAgent(checkiOSVersion: true)!), "User agent is cached correctly.")
|
||||
|
||||
let expectation = self.expectation(description: "Found Firefox user agent")
|
||||
|
||||
let webView = WKWebView()
|
||||
webView.evaluateJavaScript("navigator.userAgent") { result, error in
|
||||
let userAgent = result as! String
|
||||
if compare(userAgent) {
|
||||
expectation.fulfill()
|
||||
} else {
|
||||
XCTFail("User agent did not match expected pattern! \(userAgent)")
|
||||
}
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 5, handler: nil)
|
||||
}
|
||||
|
||||
func testDesktopUserAgent() {
|
||||
let compare: (String) -> Bool = { ua in
|
||||
let range = ua.range(of: "^Mozilla/5\\.0 \\(Macintosh; Intel Mac OS X [0-9_]+\\) AppleWebKit/[0-9\\.]+ \\(KHTML, like Gecko\\) Safari/[0-9\\.]+$", options: NSString.CompareOptions.regularExpression)
|
||||
return range != nil
|
||||
}
|
||||
|
||||
XCTAssertTrue(compare(UserAgent.desktopUserAgent()), "Desktop user agent computes correctly.")
|
||||
}
|
||||
|
||||
/// Our local server should only accept whitelisted hosts (localhost and 127.0.0.1).
|
||||
/// All other localhost equivalents should return 403.
|
||||
func testDisallowLocalhostAliases() {
|
||||
// Allowed local hosts. The first two are equivalent since iOS forwards an
|
||||
// empty host to localhost.
|
||||
[ "localhost",
|
||||
"",
|
||||
"127.0.0.1",
|
||||
].forEach { XCTAssert(hostIsValid($0), "\($0) host should be valid.") }
|
||||
|
||||
// Disallowed local hosts. WKWebView will direct them to our server, but the server
|
||||
// should reject them.
|
||||
[ "[::1]",
|
||||
"2130706433",
|
||||
"0",
|
||||
"127.00.00.01",
|
||||
"017700000001",
|
||||
"0x7f.0x0.0x0.0x1"
|
||||
].forEach { XCTAssertFalse(hostIsValid($0), "\($0) host should not be valid.") }
|
||||
}
|
||||
|
||||
fileprivate func hostIsValid(_ host: String) -> Bool {
|
||||
let expectation = self.expectation(description: "Validate host for \(host)")
|
||||
let request = URLRequest(url: URL(string: "http://\(host):6571/about/license")!)
|
||||
var response: HTTPURLResponse?
|
||||
Alamofire.request(request).authenticate(usingCredential: WebServer.sharedInstance.credentials).response { (res) -> Void in
|
||||
response = res.response
|
||||
expectation.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 100, handler: nil)
|
||||
return response?.statusCode == 200
|
||||
}
|
||||
|
||||
}
|
||||
60
mobile/ios/ClientTests/CustomSearchEnginesTest.swift
Normal file
60
mobile/ios/ClientTests/CustomSearchEnginesTest.swift
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
@testable import Client
|
||||
import Shared
|
||||
import Storage
|
||||
import Sync
|
||||
import UIKit
|
||||
|
||||
import XCTest
|
||||
|
||||
class CustomSearchEnginesTest: XCTestCase {
|
||||
|
||||
func testgetSearchTemplate() {
|
||||
let profile = MockBrowserProfile(localName: "customSearchTests")
|
||||
let customSearchEngineForm = CustomSearchViewController()
|
||||
customSearchEngineForm.profile = profile
|
||||
|
||||
let template = customSearchEngineForm.getSearchTemplate(withString: "https://github.com/search=%s")
|
||||
XCTAssertEqual(template, "https://github.com/search={searchTerms}")
|
||||
|
||||
let badTemplate = customSearchEngineForm.getSearchTemplate(withString: "https://github.com/search=blah")
|
||||
XCTAssertNil(badTemplate)
|
||||
}
|
||||
|
||||
func testaddSearchEngine() {
|
||||
let profile = MockBrowserProfile(localName: "customSearchTests")
|
||||
let customSearchEngineForm = CustomSearchViewController()
|
||||
customSearchEngineForm.profile = profile
|
||||
let q = "http://www.google.ca/?#q=%s"
|
||||
let title = "YASE"
|
||||
|
||||
let expectation = self.expectation(description: "Waiting on favicon fetching")
|
||||
customSearchEngineForm.createEngine(forQuery: q, andName: title).uponQueue(DispatchQueue.main) { result in
|
||||
XCTAssertNotNil(result.successValue, "Make sure the new engine is not nil")
|
||||
let engine = result.successValue!
|
||||
XCTAssertEqual(engine.shortName, title)
|
||||
XCTAssertNotNil(engine.image)
|
||||
XCTAssertEqual(engine.searchTemplate, "http://www.google.ca/?#q={searchTerms}")
|
||||
expectation.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 5, handler: nil)
|
||||
}
|
||||
|
||||
func testaddSearchEngineFailure() {
|
||||
let profile = MockBrowserProfile(localName: "customSearchTests")
|
||||
let customSearchEngineForm = CustomSearchViewController()
|
||||
customSearchEngineForm.profile = profile
|
||||
let q = "isthisvalid.com/hhh%s"
|
||||
let title = "YASE"
|
||||
|
||||
let expectation = self.expectation(description: "Waiting on favicon fetching")
|
||||
customSearchEngineForm.createEngine(forQuery: q, andName: title).uponQueue(DispatchQueue.main) { result in
|
||||
XCTAssertNil(result.successValue, "Make sure the new engine is nil")
|
||||
expectation.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 5, handler: nil)
|
||||
}
|
||||
}
|
||||
76
mobile/ios/ClientTests/FileAccessorTests.swift
Normal file
76
mobile/ios/ClientTests/FileAccessorTests.swift
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import Storage
|
||||
import XCTest
|
||||
|
||||
class FileAccessorTests: XCTestCase {
|
||||
fileprivate var testDir: String!
|
||||
fileprivate var files: FileAccessor!
|
||||
|
||||
override func setUp() {
|
||||
let docPath: NSString = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0] as NSString
|
||||
files = FileAccessor(rootPath: docPath.appendingPathComponent("filetest"))
|
||||
|
||||
testDir = try! files.getAndEnsureDirectory()
|
||||
try! files.removeFilesInDirectory()
|
||||
}
|
||||
|
||||
func testFileAccessor() {
|
||||
// Test existence.
|
||||
XCTAssertFalse(files.exists("foo"), "File doesn't exist")
|
||||
createFile("foo")
|
||||
XCTAssertTrue(files.exists("foo"), "File exists")
|
||||
|
||||
// Test moving.
|
||||
do {
|
||||
try files.move("foo", toRelativePath: "bar")
|
||||
XCTAssertFalse(files.exists("foo"), "Old doesn't exist")
|
||||
XCTAssertTrue(files.exists("bar"), "New file exists")
|
||||
} catch {
|
||||
XCTFail("Unable to move 'foo' to 'bar' \(error)")
|
||||
}
|
||||
|
||||
do {
|
||||
try files.move("bar", toRelativePath: "foo/bar")
|
||||
XCTAssertFalse(files.exists("bar"), "Old doesn't exist")
|
||||
XCTAssertTrue(files.exists("foo/bar"), "New file exists")
|
||||
} catch {
|
||||
XCTFail("Unable to move 'bar' to 'foo/bar' \(error)")
|
||||
}
|
||||
|
||||
// Test removal.
|
||||
do {
|
||||
XCTAssertTrue(files.exists("foo"), "File exists")
|
||||
try files.remove("foo")
|
||||
XCTAssertFalse(files.exists("foo"), "File removed")
|
||||
} catch {
|
||||
XCTFail("Unable to remove 'foo' \(error)")
|
||||
}
|
||||
|
||||
// Test directory creation and path.
|
||||
do {
|
||||
XCTAssertFalse(files.exists("foo"), "Directory doesn't exist")
|
||||
let path = try files.getAndEnsureDirectory("foo")
|
||||
var isDirectory = ObjCBool(false)
|
||||
FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory)
|
||||
XCTAssertTrue(isDirectory.boolValue, "Directory exists")
|
||||
} catch {
|
||||
XCTFail("Unable to find directory 'foo' \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func createFile(_ filename: String) {
|
||||
let path = (testDir as NSString).appendingPathComponent(filename)
|
||||
let success: Bool
|
||||
do {
|
||||
try "foo".write(toFile: path, atomically: false, encoding: String.Encoding.utf8)
|
||||
success = true
|
||||
} catch _ {
|
||||
success = false
|
||||
}
|
||||
XCTAssertTrue(success, "Wrote to \(path)")
|
||||
}
|
||||
}
|
||||
42
mobile/ios/ClientTests/FxADeepLinkingTests.swift
Normal file
42
mobile/ios/ClientTests/FxADeepLinkingTests.swift
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import UIKit
|
||||
import XCTest
|
||||
@testable import Client
|
||||
import Shared
|
||||
|
||||
class FxADeepLinkingTests: XCTestCase {
|
||||
var profile: MockProfile!
|
||||
var vc: FxAContentViewController!
|
||||
var expectUrl = URL(string: "https://accounts.firefox.com/signin?service=sync&context=fx_ios_v1&signin=test&utm_source=somesource&entrypoint=one")
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
self.profile = MockProfile()
|
||||
self.vc = FxAContentViewController(profile: self.profile)
|
||||
}
|
||||
|
||||
func testLaunchWithNilOptions() {
|
||||
let testUrl = self.vc.createFxAURLWith(nil, profile: self.profile)
|
||||
// Should use default urls for nil options
|
||||
XCTAssertEqual(testUrl, self.vc.profile.accountConfiguration.signInURL)
|
||||
}
|
||||
|
||||
func testLaunchWithOptions() {
|
||||
let url = URL(string: "firefox://fxa-signin?signin=test&utm_source=somesource&entrypoint=one&ignore=this")
|
||||
let query = url!.getQuery()
|
||||
let fxaOptions = FxALaunchParams(query: query)
|
||||
let testUrl = self.vc.createFxAURLWith(fxaOptions, profile: self.profile)
|
||||
XCTAssertEqual(testUrl, expectUrl!)
|
||||
}
|
||||
|
||||
func testDoesntOverrideServiceContext() {
|
||||
let url = URL(string: "firefox://fxa-signin?service=asdf&context=123&signin=test&entrypoint=one&utm_source=somesource&ignore=this")
|
||||
let query = url!.getQuery()
|
||||
let fxaOptions = FxALaunchParams(query: query)
|
||||
let testUrl = self.vc.createFxAURLWith(fxaOptions, profile: self.profile)
|
||||
XCTAssertEqual(testUrl, expectUrl!)
|
||||
}
|
||||
}
|
||||
153
mobile/ios/ClientTests/FxAPushMessageTest.swift
Normal file
153
mobile/ios/ClientTests/FxAPushMessageTest.swift
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
@testable import Account
|
||||
@testable import Client
|
||||
import Foundation
|
||||
import FxA
|
||||
import SwiftyJSON
|
||||
import XCTest
|
||||
|
||||
class FxAPushMessageTest: XCTestCase {
|
||||
func testMessage_subscriptionDecryption() {
|
||||
var userInfo: [AnyHashable: Any] = [
|
||||
"chid": "034f52789f7b44ecaf119cc59231cdc1",
|
||||
"enc": "keyid=p256dh;salt=mYBHM3B_oXlEjV0HfgHZ1A",
|
||||
"body": "_tZf65gKC23STnTNuhtSSbrg1LScGiLjO4GOIuHlCGIFFEzcwsB-J-s3pe3qu2d24A-sKwVyolmShlMBEvEX_34f6FgXMs3k35g4u5STKgJMQxZ8VFDjtQqQfxfSIEt35pdKaPwXKH2zbs0xHC3qEJ0YMc60Eq8uAuQF7FQZ7ts",
|
||||
"cryptokey": "keyid=p256dh;dh=BNLZ2IWMNGioofzMBnSijySib0Pa-lwgBfLYIhqvvmKxprKgEh6JCDB2DBUmj9BuJCk6xJvBbPd-4x_8tb-qIPM;p256ecdsa=BP-OR33RzQSlrzD7_d1kYE9i9WjSIQAKTuhHxYNiPEF0i-wxeNIIwxthwU7zBTbumyxFUeydrmxcVKXugjBImRU",
|
||||
"con": "aesgcm",
|
||||
"ver": "gAAAAABY5hUl_Pi1bVI4ptqKzPWq_aulqXEO1eOx_ncUuwoz8zItWWLA4Ix1ZbDLTOkurqK1vE3N3y1ZXsp_MZ69QJYRuWg5V_3T_XUNVBxj8dnDcNAE-ep9_M5xeKiNdngdww-cqqMOxf-tI5ZoR2nQFqWSs51XGwMMIdsGNmUPqnR4w2Rzt6FYz-AsGCrHXJ3bmYbwpq4P",
|
||||
]
|
||||
|
||||
let subscription = PushSubscription(channelID: "channel",
|
||||
endpoint: URL(string: "https://example.ru")!,
|
||||
p256dhPrivateKey: "UDnicgor_Il7cLNTqSt--SrEblNBbgPA2yXAQ_b31xI",
|
||||
p256dhPublicKey: "BB4XdAhpVVU45NYSXHpRiubMYoaeb0A-y5aSGE437YKGHQihlvlZMv5D0ebK6WmFqzpIr217Kv9oCbZVDp1KGK4",
|
||||
authKey: "0gH7RiYYMhfHDQ1L1X4RMw")
|
||||
|
||||
let body = userInfo["body"] as! String
|
||||
let enc = userInfo["enc"] as! String
|
||||
let cryptokey = userInfo["cryptokey"] as! String
|
||||
|
||||
guard let plaintext = subscription.aesgcm(payload: body, encryptionHeader: enc, cryptoHeader: cryptokey) else {
|
||||
return XCTFail("Decryption failed")
|
||||
}
|
||||
|
||||
let json = JSON(parseJSON: plaintext)
|
||||
|
||||
XCTAssertEqual(json["command"].stringValue, "fxaccounts:device_connected")
|
||||
}
|
||||
|
||||
func testMessageHandler() {
|
||||
let subscription = PushSubscription(channelID: "channel",
|
||||
endpoint: URL(string: "https://example.ru")!,
|
||||
p256dhPrivateKey: "t7dcZIN4w37UYjE6u3lBLB0WOShxqelkbJFKKzMDSsE",
|
||||
p256dhPublicKey: "BIXFDlhppL2lc5GcIXbGPa1iVdJn5ULYaF1ltJY9Qm17-tIC_9eEZXtalPpMRXsFmEKhdn2ttg3KQ3t-ztQ3ShQ",
|
||||
authKey: "a8E3EO5F6FFWdv4hAGyGyw")
|
||||
|
||||
let userInfo: [AnyHashable: Any] = [
|
||||
AnyHashable("chid"): "f6defa012a6249e58bbfbf2995f8d425",
|
||||
AnyHashable("enc"): "keyid=p256dh;salt=nslkEsUqUg5sQ7_sRZfOjg",
|
||||
AnyHashable("body"): "Q1t-ttSwQMxUI64Ls3vOU-hE_qg1AIUzyLQSpEkx-8JITh5UJ7oq25faEc8XPoTYQoaHQ2d--QIK_yVorbt-0Yr7IO4BmtSSX-e4kSx76fWzqKjEpEt7Vr3Av5seEBeoAT2FZRzehkFjNVWoTw",
|
||||
AnyHashable("cryptokey"): "keyid=p256dh;dh=BNfUPK_8eUTZGOyXq07lthBfHeIxC2B7L_gF3cMGK1jVfDe9tlgxpHD_mbKrt3p12d7_O__wizhne2a1Eb7pZgk;p256ecdsa=BFSuld8S4PbRcgGe3OQPN9NyIOXx-ccUIMb0q6nIpH7Qf894wz0TIQTXQ7I7pWjZiN9KCdYVjNhyPtr1--37ois",
|
||||
AnyHashable("con"): "aesgcm",
|
||||
AnyHashable("ver"): "gAAAAABZLbG-m7EHhcMdrqs51SkESIZHsZjvw2QIu8LOeXxcKEy6wDVCprOKFAfJU44cinfJcDtCnO9EEyzpFt5e0HBDCLybGyThoZzmiod6zTLhTfAKZe-SyElSVCL0UDpJ_-U3UTUUHUaJXeRf0z6NvFM-uL39Jy-dwr3cuJoSDIcTPdChRPFiIS1hwokqMlxOn36azxOi",
|
||||
]
|
||||
|
||||
let profile = MockProfile()
|
||||
|
||||
let account = FirefoxAccount(
|
||||
configuration: FirefoxAccountConfigurationLabel.production.toConfiguration(),
|
||||
email: "testtest@test.com",
|
||||
uid: "uid",
|
||||
deviceRegistration: nil,
|
||||
stateKeyLabel: "xxx",
|
||||
state: SeparatedState())
|
||||
|
||||
let registration = PushRegistration(uaid: "uaid", secret: "secret", subscription: subscription)
|
||||
|
||||
account.pushRegistration = registration
|
||||
profile.setAccount(account)
|
||||
|
||||
let handler = FxAPushMessageHandler(with: profile)
|
||||
|
||||
let expectation = XCTestExpectation()
|
||||
handler.handle(userInfo: userInfo).upon { maybe in
|
||||
XCTAssertTrue(maybe.isSuccess)
|
||||
XCTAssertEqual(maybe.successValue!, PushMessage.collectionChanged(collections: ["clients", "tabs"]))
|
||||
expectation.fulfill()
|
||||
}
|
||||
wait(for: [expectation], timeout: 10)
|
||||
}
|
||||
|
||||
func createHandler(_ profile: Profile = MockProfile()) -> FxAPushMessageHandler {
|
||||
let account = FirefoxAccount(
|
||||
configuration: FirefoxAccountConfigurationLabel.production.toConfiguration(),
|
||||
email: "testtest@test.com",
|
||||
uid: "uid",
|
||||
deviceRegistration: nil,
|
||||
stateKeyLabel: "xxx",
|
||||
state: SeparatedState())
|
||||
|
||||
profile.setAccount(account)
|
||||
|
||||
return FxAPushMessageHandler(with: profile)
|
||||
}
|
||||
|
||||
func test_deviceConnected() {
|
||||
let handler = createHandler()
|
||||
|
||||
let expectation = XCTestExpectation()
|
||||
handler.handle(plaintext: "{\"command\":\"fxaccounts:device_connected\",\"data\":{\"deviceName\": \"Use Nightly on Desktop\"}}").upon { maybe in
|
||||
XCTAssertTrue(maybe.isSuccess)
|
||||
guard let message = maybe.successValue else {
|
||||
return expectation.fulfill()
|
||||
}
|
||||
XCTAssertEqual(message, PushMessage.deviceConnected("Use Nightly on Desktop"))
|
||||
expectation.fulfill()
|
||||
}
|
||||
wait(for: [expectation], timeout: 10)
|
||||
}
|
||||
|
||||
func test_deviceDisconnected() {
|
||||
let profile = MockProfile()
|
||||
let handler = createHandler(profile)
|
||||
let prefs = profile.prefs
|
||||
|
||||
let expectation = XCTestExpectation()
|
||||
handler.handle(plaintext: "{\"command\":\"fxaccounts:device_disconnected\",\"data\":{\"id\": \"not_this_device\"}}").upon { maybe in
|
||||
XCTAssertTrue(maybe.isSuccess)
|
||||
guard let message = maybe.successValue else {
|
||||
return expectation.fulfill()
|
||||
}
|
||||
XCTAssertEqual(message.messageType, .deviceDisconnected)
|
||||
XCTAssertFalse(prefs.boolForKey(PendingAccountDisconnectedKey) ?? false)
|
||||
expectation.fulfill()
|
||||
}
|
||||
wait(for: [expectation], timeout: 10)
|
||||
|
||||
}
|
||||
|
||||
func test_thisDeviceDisconnected() {
|
||||
let profile = MockProfile()
|
||||
let handler = createHandler(profile)
|
||||
|
||||
let deviceRegistration = FxADeviceRegistration(id: "this-device-id", version: 1, lastRegistered: 0)
|
||||
profile.account?.deviceRegistration = deviceRegistration
|
||||
|
||||
let prefs = profile.prefs
|
||||
|
||||
let expectation = XCTestExpectation()
|
||||
handler.handle(plaintext: "{\"command\":\"fxaccounts:device_disconnected\",\"data\":{\"id\": \"\(deviceRegistration.id)\"}}").upon { maybe in
|
||||
guard let message = maybe.successValue else {
|
||||
return expectation.fulfill()
|
||||
}
|
||||
XCTAssertEqual(message, PushMessage.thisDeviceDisconnected)
|
||||
XCTAssertTrue(prefs.boolForKey(PendingAccountDisconnectedKey) ?? false)
|
||||
expectation.fulfill()
|
||||
}
|
||||
wait(for: [expectation], timeout: 10)
|
||||
|
||||
}
|
||||
}
|
||||
16
mobile/ios/ClientTests/HomePageTests.swift
Normal file
16
mobile/ios/ClientTests/HomePageTests.swift
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
@testable import Client
|
||||
import XCTest
|
||||
|
||||
class HomePageTests: XCTestCase {
|
||||
let prefs = NSUserDefaultsPrefs(prefix: "PrefsTests")
|
||||
|
||||
func testHomePageSettingForInternalURLs() {
|
||||
let helper = HomePageHelper(prefs: prefs)
|
||||
helper.currentURL = URL(string: "http://localhost:6571")
|
||||
XCTAssertNil(prefs.stringForKey(HomePageConstants.HomePageURLPrefKey))
|
||||
}
|
||||
}
|
||||
24
mobile/ios/ClientTests/Info.plist
Normal file
24
mobile/ios/ClientTests/Info.plist
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?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>en</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>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>10.6</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</plist>
|
||||
235
mobile/ios/ClientTests/MockProfile.swift
Normal file
235
mobile/ios/ClientTests/MockProfile.swift
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
@testable import Client
|
||||
import Foundation
|
||||
import Account
|
||||
import ReadingList
|
||||
import Shared
|
||||
import Storage
|
||||
import Sync
|
||||
import XCTest
|
||||
import Deferred
|
||||
|
||||
open class MockSyncManager: SyncManager {
|
||||
open var isSyncing = false
|
||||
open var lastSyncFinishTime: Timestamp?
|
||||
open var syncDisplayState: SyncDisplayState?
|
||||
|
||||
open func hasSyncedHistory() -> Deferred<Maybe<Bool>> {
|
||||
return deferMaybe(true)
|
||||
}
|
||||
|
||||
private func completedWithStats(collection: String) -> Deferred<Maybe<SyncStatus>> {
|
||||
return deferMaybe(SyncStatus.completed(SyncEngineStatsSession(collection: collection)))
|
||||
}
|
||||
|
||||
open func syncClients() -> SyncResult { return completedWithStats(collection: "mock_clients") }
|
||||
open func syncClientsThenTabs() -> SyncResult { return completedWithStats(collection: "mock_clientsandtabs") }
|
||||
open func syncHistory() -> SyncResult { return completedWithStats(collection: "mock_history") }
|
||||
open func syncLogins() -> SyncResult { return completedWithStats(collection: "mock_logins") }
|
||||
open func mirrorBookmarks() -> SyncResult { return completedWithStats(collection: "mock_bookmarks") }
|
||||
open func syncEverything(why: SyncReason) -> Success {
|
||||
return succeed()
|
||||
}
|
||||
open func syncNamedCollections(why: SyncReason, names: [String]) -> Success {
|
||||
return succeed()
|
||||
}
|
||||
open func beginTimedSyncs() {}
|
||||
open func endTimedSyncs() {}
|
||||
open func applicationDidBecomeActive() {
|
||||
self.beginTimedSyncs()
|
||||
}
|
||||
open func applicationDidEnterBackground() {
|
||||
self.endTimedSyncs()
|
||||
}
|
||||
|
||||
open func onNewProfile() {
|
||||
}
|
||||
|
||||
open func onAddedAccount() -> Success {
|
||||
return succeed()
|
||||
}
|
||||
open func onRemovedAccount(_ account: FirefoxAccount?) -> Success {
|
||||
return succeed()
|
||||
}
|
||||
|
||||
open func hasSyncedLogins() -> Deferred<Maybe<Bool>> {
|
||||
return deferMaybe(true)
|
||||
}
|
||||
}
|
||||
|
||||
open class MockTabQueue: TabQueue {
|
||||
open func addToQueue(_ tab: ShareItem) -> Success {
|
||||
return succeed()
|
||||
}
|
||||
|
||||
open func getQueuedTabs() -> Deferred<Maybe<Cursor<ShareItem>>> {
|
||||
return deferMaybe(ArrayCursor<ShareItem>(data: []))
|
||||
}
|
||||
|
||||
open func clearQueuedTabs() -> Success {
|
||||
return succeed()
|
||||
}
|
||||
}
|
||||
|
||||
open class MockPanelDataObservers: PanelDataObservers {
|
||||
override init(profile: Profile) {
|
||||
super.init(profile: profile)
|
||||
self.activityStream = MockActivityStreamDataObserver(profile: profile)
|
||||
}
|
||||
}
|
||||
|
||||
open class MockActivityStreamDataObserver: DataObserver {
|
||||
public var profile: Profile
|
||||
public weak var delegate: DataObserverDelegate?
|
||||
|
||||
init(profile: Profile) {
|
||||
self.profile = profile
|
||||
}
|
||||
|
||||
public func refreshIfNeeded(forceHighlights highlights: Bool, forceTopSites topsites: Bool) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
open class MockProfile: Profile {
|
||||
// Read/Writeable properties for mocking
|
||||
public var recommendations: HistoryRecommendations
|
||||
public var places: BrowserHistory & Favicons & SyncableHistory & ResettableSyncStorage & HistoryRecommendations
|
||||
public var files: FileAccessor
|
||||
public var history: BrowserHistory & SyncableHistory & ResettableSyncStorage
|
||||
public var logins: BrowserLogins & SyncableLogins & ResettableSyncStorage
|
||||
public var syncManager: SyncManager!
|
||||
|
||||
public lazy var panelDataObservers: PanelDataObservers = {
|
||||
return MockPanelDataObservers(profile: self)
|
||||
}()
|
||||
|
||||
var db: BrowserDB
|
||||
|
||||
fileprivate let name: String = "mockaccount"
|
||||
|
||||
init() {
|
||||
files = MockFiles()
|
||||
syncManager = MockSyncManager()
|
||||
logins = MockLogins(files: files)
|
||||
db = BrowserDB(filename: "mock.db", schema: BrowserSchema(), files: files)
|
||||
places = SQLiteHistory(db: self.db, prefs: MockProfilePrefs())
|
||||
recommendations = places
|
||||
history = places
|
||||
}
|
||||
|
||||
public func localName() -> String {
|
||||
return name
|
||||
}
|
||||
|
||||
public func reopen() {
|
||||
}
|
||||
|
||||
public func shutdown() {
|
||||
}
|
||||
|
||||
public var isShutdown: Bool = false
|
||||
|
||||
public var favicons: Favicons {
|
||||
return self.places
|
||||
}
|
||||
|
||||
lazy public var queue: TabQueue = {
|
||||
return MockTabQueue()
|
||||
}()
|
||||
|
||||
lazy public var metadata: Metadata = {
|
||||
return SQLiteMetadata(db: self.db)
|
||||
}()
|
||||
|
||||
lazy public var isChinaEdition: Bool = {
|
||||
return Locale.current.identifier == "zh_CN"
|
||||
}()
|
||||
|
||||
lazy public var certStore: CertStore = {
|
||||
return CertStore()
|
||||
}()
|
||||
|
||||
lazy public var bookmarks: BookmarksModelFactorySource & KeywordSearchSource & SyncableBookmarks & LocalItemSource & MirrorItemSource & ShareToDestination = {
|
||||
// Make sure the rest of our tables are initialized before we try to read them!
|
||||
// This expression is for side-effects only.
|
||||
let p = self.places
|
||||
|
||||
return MergedSQLiteBookmarks(db: self.db)
|
||||
}()
|
||||
|
||||
lazy public var searchEngines: SearchEngines = {
|
||||
return SearchEngines(prefs: self.prefs, files: self.files)
|
||||
}()
|
||||
|
||||
lazy public var prefs: Prefs = {
|
||||
return MockProfilePrefs()
|
||||
}()
|
||||
|
||||
lazy public var readingList: ReadingListService? = {
|
||||
return ReadingListService(profileStoragePath: self.files.rootPath as String)
|
||||
}()
|
||||
|
||||
lazy public var recentlyClosedTabs: ClosedTabsStore = {
|
||||
return ClosedTabsStore(prefs: self.prefs)
|
||||
}()
|
||||
|
||||
internal lazy var remoteClientsAndTabs: RemoteClientsAndTabs = {
|
||||
return SQLiteRemoteClientsAndTabs(db: self.db)
|
||||
}()
|
||||
|
||||
fileprivate lazy var syncCommands: SyncCommands = {
|
||||
return SQLiteRemoteClientsAndTabs(db: self.db)
|
||||
}()
|
||||
|
||||
public let accountConfiguration: FirefoxAccountConfiguration = ProductionFirefoxAccountConfiguration()
|
||||
var account: FirefoxAccount?
|
||||
|
||||
public func hasAccount() -> Bool {
|
||||
return account != nil
|
||||
}
|
||||
|
||||
public func hasSyncableAccount() -> Bool {
|
||||
return account?.actionNeeded == FxAActionNeeded.none
|
||||
}
|
||||
|
||||
public func getAccount() -> FirefoxAccount? {
|
||||
return account
|
||||
}
|
||||
|
||||
public func setAccount(_ account: FirefoxAccount) {
|
||||
self.account = account
|
||||
self.syncManager.onAddedAccount()
|
||||
}
|
||||
|
||||
public func flushAccount() {}
|
||||
|
||||
public func removeAccount() {
|
||||
let old = self.account
|
||||
self.account = nil
|
||||
self.syncManager.onRemovedAccount(old)
|
||||
}
|
||||
|
||||
public func getClients() -> Deferred<Maybe<[RemoteClient]>> {
|
||||
return deferMaybe([])
|
||||
}
|
||||
|
||||
public func getClientsAndTabs() -> Deferred<Maybe<[ClientAndTabs]>> {
|
||||
return deferMaybe([])
|
||||
}
|
||||
|
||||
public func getCachedClientsAndTabs() -> Deferred<Maybe<[ClientAndTabs]>> {
|
||||
return deferMaybe([])
|
||||
}
|
||||
|
||||
public func storeTabs(_ tabs: [RemoteTab]) -> Deferred<Maybe<Int>> {
|
||||
return deferMaybe(0)
|
||||
}
|
||||
|
||||
public func sendItems(_ items: [ShareItem], toClients clients: [RemoteClient]) -> Deferred<Maybe<SyncStatus>> {
|
||||
return deferMaybe(SyncStatus.notStarted(.offline))
|
||||
}
|
||||
}
|
||||
45
mobile/ios/ClientTests/MockableHistory.swift
Normal file
45
mobile/ios/ClientTests/MockableHistory.swift
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import Storage
|
||||
import Deferred
|
||||
import Shared
|
||||
|
||||
/*
|
||||
* A class that adheres to all the requirements for a profile's history property
|
||||
* with all of the methods set to fatalError. Use this class if you're looking to
|
||||
* mock out parts of the history API
|
||||
*/
|
||||
class MockableHistory: BrowserHistory, SyncableHistory, ResettableSyncStorage {
|
||||
func getFrecentHistory() -> FrecentHistory { fatalError() }
|
||||
func getTopSitesWithLimit(_ limit: Int) -> Deferred<Maybe<Cursor<Site>>> { fatalError() }
|
||||
func addLocalVisit(_ visit: SiteVisit) -> Success { fatalError() }
|
||||
func clearHistory() -> Success { fatalError() }
|
||||
func removeHistoryForURL(_ url: String) -> Success { fatalError() }
|
||||
func removeSiteFromTopSites(_ site: Site) -> Success { fatalError() }
|
||||
func removeHostFromTopSites(_ host: String) -> Success { fatalError() }
|
||||
func clearTopSitesCache() -> Success { fatalError() }
|
||||
func removeFromPinnedTopSites(_ site: Site) -> Success { fatalError() }
|
||||
func addPinnedTopSite(_ site: Site) -> Success { fatalError() }
|
||||
func getPinnedTopSites() -> Deferred<Maybe<Cursor<Site>>> { fatalError() }
|
||||
func getSitesByLastVisit(_ limit: Int) -> Deferred<Maybe<Cursor<Site>>> { fatalError() }
|
||||
func setTopSitesNeedsInvalidation() { fatalError() }
|
||||
func updateTopSitesCacheIfInvalidated() -> Deferred<Maybe<Bool>> { fatalError() }
|
||||
func setTopSitesCacheSize(_ size: Int32) { fatalError() }
|
||||
func onRemovedAccount() -> Success { fatalError() }
|
||||
func ensurePlaceWithURL(_ url: String, hasGUID guid: GUID) -> Success { fatalError() }
|
||||
func deleteByGUID(_ guid: GUID, deletedAt: Timestamp) -> Success { fatalError() }
|
||||
func storeRemoteVisits(_ visits: [Visit], forGUID guid: GUID) -> Success { fatalError() }
|
||||
func insertOrUpdatePlace(_ place: Place, modified: Timestamp) -> Deferred<Maybe<GUID>> { fatalError() }
|
||||
func getModifiedHistoryToUpload() -> Deferred<Maybe<[(Place, [Visit])]>> { fatalError() }
|
||||
func getDeletedHistoryToUpload() -> Deferred<Maybe<[GUID]>> { fatalError() }
|
||||
func markAsSynchronized(_: [GUID], modified: Timestamp) -> Deferred<Maybe<Timestamp>> { fatalError() }
|
||||
func markAsDeleted(_ guids: [GUID]) -> Success { fatalError() }
|
||||
func doneApplyingRecordsAfterDownload() -> Success { fatalError() }
|
||||
func doneUpdatingMetadataAfterUpload() -> Success { fatalError() }
|
||||
func hasSyncedHistory() -> Deferred<Maybe<Bool>> { fatalError() }
|
||||
func resetClient() -> Success { fatalError() }
|
||||
}
|
||||
|
||||
84
mobile/ios/ClientTests/PanelDataObserversTests.swift
Normal file
84
mobile/ios/ClientTests/PanelDataObserversTests.swift
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import Shared
|
||||
import XCTest
|
||||
@testable import Client
|
||||
|
||||
private class MockDataObserverDelegate: DataObserverDelegate {
|
||||
var didInvalidateCount = 0
|
||||
var willInvalidateCount = 0
|
||||
var highlightsRefreshCount = 0
|
||||
var topSitesRefreshCount = 0
|
||||
|
||||
func didInvalidateDataSources(refresh forced: Bool, highlightsRefreshed: Bool, topSitesRefreshed: Bool) {
|
||||
didInvalidateCount += 1
|
||||
if highlightsRefreshed {
|
||||
highlightsRefreshCount += 1
|
||||
}
|
||||
|
||||
if topSitesRefreshed {
|
||||
topSitesRefreshCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
func willInvalidateDataSources(forceHighlights highlights: Bool, forceTopSites topSites: Bool) {
|
||||
willInvalidateCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
class PanelDataObserversTests: XCTestCase {
|
||||
func testActivityStreamDelegates() {
|
||||
let profile = MockProfile()
|
||||
let observer = ActivityStreamDataObserver(profile: profile)
|
||||
let delegate = MockDataObserverDelegate()
|
||||
observer.delegate = delegate
|
||||
|
||||
NotificationCenter.default.post(name: NotificationFirefoxAccountChanged,
|
||||
object: nil)
|
||||
NotificationCenter.default.post(name: NotificationProfileDidFinishSyncing,
|
||||
object: nil)
|
||||
NotificationCenter.default.post(name: NotificationPrivateDataClearedHistory,
|
||||
object: nil)
|
||||
|
||||
waitForCondition(timeout: 5) { delegate.didInvalidateCount == 3 && delegate.willInvalidateCount == 3 }
|
||||
}
|
||||
|
||||
func testHighlightsCacheInvalidation20Min() {
|
||||
let profile = MockProfile()
|
||||
let observer = ActivityStreamDataObserver(profile: profile)
|
||||
let delegate = MockDataObserverDelegate()
|
||||
observer.delegate = delegate
|
||||
|
||||
// Set to 20min since refresh
|
||||
profile.prefs.setLong(Date.now() - (OneMinuteInMilliseconds * 20), forKey: PrefsKeys.ASLastInvalidation)
|
||||
observer.refreshIfNeeded(forceHighlights: false, forceTopSites: false)
|
||||
waitForCondition(timeout: 5) { delegate.highlightsRefreshCount == 1 }
|
||||
}
|
||||
|
||||
func testHighlightEmptyCache() {
|
||||
let profile = MockProfile()
|
||||
let observer = ActivityStreamDataObserver(profile: profile)
|
||||
let delegate = MockDataObserverDelegate()
|
||||
observer.delegate = delegate
|
||||
|
||||
// Set to no validation key
|
||||
profile.prefs.removeObjectForKey(PrefsKeys.ASLastInvalidation)
|
||||
observer.refreshIfNeeded(forceHighlights: false, forceTopSites: false)
|
||||
waitForCondition(timeout: 5) { delegate.highlightsRefreshCount == 1 }
|
||||
}
|
||||
|
||||
func testHighlightActiveCache() {
|
||||
let profile = MockProfile()
|
||||
let observer = ActivityStreamDataObserver(profile: profile)
|
||||
let delegate = MockDataObserverDelegate()
|
||||
observer.delegate = delegate
|
||||
|
||||
// Set to 10min since refresh
|
||||
profile.prefs.setLong(Date.now() - (OneMinuteInMilliseconds * 10), forKey: PrefsKeys.ASLastInvalidation)
|
||||
observer.refreshIfNeeded(forceHighlights: false, forceTopSites: false)
|
||||
waitForCondition(timeout: 5) { delegate.didInvalidateCount == 1 && delegate.highlightsRefreshCount == 0 }
|
||||
}
|
||||
}
|
||||
104
mobile/ios/ClientTests/PingCentreTests.swift
Normal file
104
mobile/ios/ClientTests/PingCentreTests.swift
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
@testable import Client
|
||||
import XCTest
|
||||
import JSONSchema
|
||||
import Alamofire
|
||||
import Deferred
|
||||
import Shared
|
||||
@testable import SyncTelemetry
|
||||
|
||||
private let mockTopic = PingCentreTopic(name: "ios-mock", schema: Schema([
|
||||
"type": "object",
|
||||
"properties": [
|
||||
"title": ["type": "string"]
|
||||
],
|
||||
"required": [
|
||||
"title"
|
||||
]
|
||||
]))
|
||||
|
||||
private var receivedNetworkRequests = [URLRequest]()
|
||||
|
||||
// Used to mock the network so we don't need to rely on the interweb for our unit tests.
|
||||
class MockingURLProtocol: URLProtocol {
|
||||
override class func canInit(with request: URLRequest) -> Bool {
|
||||
return request.url?.scheme == "https" && request.httpMethod == "POST"
|
||||
}
|
||||
|
||||
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
|
||||
return request
|
||||
}
|
||||
|
||||
override func startLoading() {
|
||||
receivedNetworkRequests.append(request)
|
||||
let response = HTTPURLResponse(url: request.url!,
|
||||
statusCode: 200,
|
||||
httpVersion: "HTTP/1.1",
|
||||
headerFields: [:])
|
||||
|
||||
self.client?.urlProtocol(self, didReceive: response!, cacheStoragePolicy: .notAllowed)
|
||||
self.client?.urlProtocol(self, didLoad: "".data(using: String.Encoding.utf8)!)
|
||||
self.client?.urlProtocolDidFinishLoading(self)
|
||||
}
|
||||
|
||||
override func stopLoading() {
|
||||
//no-op
|
||||
}
|
||||
}
|
||||
|
||||
class PingCentreTests: XCTestCase {
|
||||
var manager: SessionManager!
|
||||
var client: PingCentreClient!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
let configuration = URLSessionConfiguration.default
|
||||
configuration.protocolClasses!.insert(MockingURLProtocol.self, at: 0)
|
||||
|
||||
self.manager = SessionManager(configuration: configuration)
|
||||
self.client = DefaultPingCentreImpl(topic: mockTopic, endpoint: .staging, clientID: "fakeID", manager: self.manager)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
receivedNetworkRequests = []
|
||||
}
|
||||
|
||||
func testSendPing() {
|
||||
let validPing = [
|
||||
"title": "Test!"
|
||||
]
|
||||
let invalidPing = [String: AnyObject]()
|
||||
|
||||
client.sendPing(validPing, validate: true).succeeded()
|
||||
let validationError = client.sendPing(invalidPing, validate: true).value
|
||||
XCTAssertNotNil(validationError.failureValue)
|
||||
XCTAssertTrue(validationError.failureValue! is PingValidationError)
|
||||
|
||||
// Double check that we actually sent the successful ping and not the invalid one
|
||||
XCTAssertTrue(receivedNetworkRequests.count == 1)
|
||||
}
|
||||
|
||||
func testSendBatch() {
|
||||
let validPingA = ["title": "A"]
|
||||
let validPingB = ["title": "B"]
|
||||
let invalidPingC = [String: AnyObject]()
|
||||
|
||||
client.sendBatch([validPingA, validPingB], validate: true).succeeded()
|
||||
let validationError = client.sendBatch([validPingA, invalidPingC], validate: true).value
|
||||
|
||||
XCTAssertNotNil(validationError.failureValue)
|
||||
XCTAssertTrue(validationError.failureValue! is PingValidationError)
|
||||
|
||||
// Double check that we actually sent the successful ping and not the invalid one
|
||||
XCTAssertTrue(receivedNetworkRequests.count == 1)
|
||||
|
||||
client.sendBatch([], validate: true).succeeded()
|
||||
|
||||
// Double check that we didn't send the empty payloads request
|
||||
XCTAssertTrue(receivedNetworkRequests.count == 1)
|
||||
}
|
||||
}
|
||||
67
mobile/ios/ClientTests/PocketFeedTests.swift
Normal file
67
mobile/ios/ClientTests/PocketFeedTests.swift
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import UIKit
|
||||
import GCDWebServers
|
||||
import XCTest
|
||||
|
||||
@testable import Client
|
||||
|
||||
class PocketStoriesTests: XCTestCase {
|
||||
|
||||
var pocketAPI: String!
|
||||
let webServer: GCDWebServer = GCDWebServer()
|
||||
|
||||
/// Setup a basic web server that binds to a random port and that has one default handler on /hello
|
||||
fileprivate func setupWebServer() {
|
||||
let path = Bundle(for: type(of: self)).path(forResource: "pocketglobalfeed", ofType: "json")
|
||||
let data = try! Data(contentsOf: URL(fileURLWithPath: path!))
|
||||
|
||||
webServer.addHandler(forMethod: "GET", path: "/pocketglobalfeed", request: GCDWebServerRequest.self) { (request) -> GCDWebServerResponse! in
|
||||
return GCDWebServerDataResponse(data: data, contentType: "application/json")
|
||||
}
|
||||
|
||||
if webServer.start(withPort: 0, bonjourName: nil) == false {
|
||||
XCTFail("Can't start the GCDWebServer")
|
||||
}
|
||||
pocketAPI = "http://localhost:\(webServer.port)/pocketglobalfeed"
|
||||
}
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
setupWebServer()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testPocketStoriesCaching() {
|
||||
let expect = expectation(description: "Pocket")
|
||||
let PocketFeed = Pocket(endPoint: pocketAPI)
|
||||
|
||||
PocketFeed.globalFeed(items: 4).upon { result in
|
||||
let items = result
|
||||
XCTAssertEqual(items.count, 2, "We are fetching a static feed. There are only 2 items in it")
|
||||
self.webServer.stop() // Stop the webserver so we can check caching
|
||||
|
||||
// Try again now that the webserver is down
|
||||
PocketFeed.globalFeed(items: 4).upon { result in
|
||||
let items = result
|
||||
XCTAssertEqual(items.count, 2, "We are fetching a static feed. There are only 2 items in it")
|
||||
let item = items.first
|
||||
//These are all not optional so they should never be nil.
|
||||
//But lets check in case someone decides to change something
|
||||
XCTAssertNotNil(item?.domain, "Why")
|
||||
XCTAssertNotNil(item?.imageURL, "You")
|
||||
XCTAssertNotNil(item?.storyDescription, "Do")
|
||||
XCTAssertNotNil(item?.title, "This")
|
||||
XCTAssertNotNil(item?.url, "?")
|
||||
expect.fulfill()
|
||||
}
|
||||
}
|
||||
waitForExpectations(timeout: 10, handler: nil)
|
||||
}
|
||||
|
||||
}
|
||||
100
mobile/ios/ClientTests/PrefsTests.swift
Normal file
100
mobile/ios/ClientTests/PrefsTests.swift
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
@testable import Client
|
||||
import Foundation
|
||||
import Shared
|
||||
|
||||
import XCTest
|
||||
|
||||
class PrefsTests: XCTestCase {
|
||||
var prefs: NSUserDefaultsPrefs!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
prefs = NSUserDefaultsPrefs(prefix: "PrefsTests")
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
prefs.clearAll()
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testClearPrefs() {
|
||||
prefs.setObject("foo", forKey: "bar")
|
||||
XCTAssertEqual(prefs.stringForKey("bar")!, "foo")
|
||||
|
||||
// Ensure clearing prefs is branch-specific.
|
||||
let otherPrefs = NSUserDefaultsPrefs(prefix: "othermockaccount")
|
||||
otherPrefs.clearAll()
|
||||
XCTAssertEqual(prefs.stringForKey("bar")!, "foo")
|
||||
|
||||
prefs.clearAll()
|
||||
XCTAssertNil(prefs.stringForKey("bar"))
|
||||
}
|
||||
|
||||
func testStringForKey() {
|
||||
XCTAssertNil(prefs.stringForKey("key"))
|
||||
prefs.setObject("value", forKey: "key")
|
||||
XCTAssertEqual(prefs.stringForKey("key")!, "value")
|
||||
// Non-String values return nil.
|
||||
prefs.setObject(1, forKey: "key")
|
||||
XCTAssertNil(prefs.stringForKey("key"))
|
||||
}
|
||||
|
||||
func testBoolForKey() {
|
||||
XCTAssertNil(prefs.boolForKey("key"))
|
||||
prefs.setObject(true, forKey: "key")
|
||||
XCTAssertEqual(prefs.boolForKey("key")!, true)
|
||||
prefs.setObject(false, forKey: "key")
|
||||
XCTAssertEqual(prefs.boolForKey("key")!, false)
|
||||
// We would like non-Bool values to return nil, but I can't figure out how to differentiate.
|
||||
// Instead, this documents the undesired behaviour.
|
||||
prefs.setObject(1, forKey: "key")
|
||||
XCTAssertEqual(prefs.boolForKey("key")!, true)
|
||||
prefs.setObject("1", forKey: "key")
|
||||
XCTAssertNil(prefs.boolForKey("key"))
|
||||
prefs.setObject("x", forKey: "key")
|
||||
XCTAssertNil(prefs.boolForKey("key"))
|
||||
}
|
||||
|
||||
func testStringArrayForKey() {
|
||||
XCTAssertNil(prefs.stringArrayForKey("key"))
|
||||
prefs.setObject(["value1", "value2"], forKey: "key")
|
||||
XCTAssertEqual(prefs.stringArrayForKey("key")!, ["value1", "value2"])
|
||||
// Non-[String] values return nil.
|
||||
prefs.setObject(1, forKey: "key")
|
||||
XCTAssertNil(prefs.stringArrayForKey("key"))
|
||||
// [Non-String] values return nil.
|
||||
prefs.setObject([1, 2], forKey: "key")
|
||||
XCTAssertNil(prefs.stringArrayForKey("key"))
|
||||
}
|
||||
|
||||
func testMockProfilePrefsRoundtripsTimestamps() {
|
||||
let prefs = MockProfilePrefs().branch("baz")
|
||||
let val: Timestamp = Date.now()
|
||||
prefs.setLong(val, forKey: "foobar")
|
||||
XCTAssertEqual(val, prefs.unsignedLongForKey("foobar")!)
|
||||
}
|
||||
|
||||
func testMockProfilePrefsKeys() {
|
||||
let prefs = MockProfilePrefs().branch("baz") as! MockProfilePrefs
|
||||
let val: Timestamp = Date.now()
|
||||
prefs.setLong(val, forKey: "foobar")
|
||||
XCTAssertEqual(val, (prefs.things["baz.foobar"] as! NSNumber).uint64Value)
|
||||
}
|
||||
|
||||
func testMockProfilePrefsClearAll() {
|
||||
let prefs1 = MockProfilePrefs().branch("bar") as! MockProfilePrefs
|
||||
let prefs2 = MockProfilePrefs().branch("baz") as! MockProfilePrefs
|
||||
|
||||
// Ensure clearing prefs is branch-specific.
|
||||
prefs1.setInt(123, forKey: "foo")
|
||||
prefs2.clearAll()
|
||||
XCTAssertEqual(123, prefs1.intForKey("foo")!)
|
||||
|
||||
prefs1.clearAll()
|
||||
XCTAssertNil(prefs1.intForKey("foo") as AnyObject?)
|
||||
}
|
||||
}
|
||||
28
mobile/ios/ClientTests/ProfileTest.swift
Normal file
28
mobile/ios/ClientTests/ProfileTest.swift
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
@testable import Client
|
||||
import Foundation
|
||||
import Shared
|
||||
import Storage
|
||||
import SwiftKeychainWrapper
|
||||
|
||||
import XCTest
|
||||
|
||||
/*
|
||||
* A base test type for tests that need a profile.
|
||||
*/
|
||||
class ProfileTest: XCTestCase {
|
||||
func withTestProfile(_ callback: (_ profile: Profile) -> Void) {
|
||||
callback(MockProfile())
|
||||
}
|
||||
|
||||
func testNewProfileClearsExistingAuthenticationInfo() {
|
||||
let authInfo = AuthenticationKeychainInfo(passcode: "1234")
|
||||
KeychainWrapper.sharedAppContainerKeychain.setAuthenticationInfo(authInfo)
|
||||
XCTAssertNotNil(KeychainWrapper.sharedAppContainerKeychain.authenticationInfo())
|
||||
let _ = BrowserProfile(localName: "my_profile", clear: true)
|
||||
XCTAssertNil(KeychainWrapper.sharedAppContainerKeychain.authenticationInfo())
|
||||
}
|
||||
}
|
||||
40
mobile/ios/ClientTests/RelativeDatesTests.swift
Normal file
40
mobile/ios/ClientTests/RelativeDatesTests.swift
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
class RelativeDatesTests: XCTestCase {
|
||||
func testRelativeDates() {
|
||||
let dateOrig = Date()
|
||||
var date = Date(timeInterval: 0, since: dateOrig)
|
||||
|
||||
XCTAssertEqual(date.toRelativeTimeString(), "just now")
|
||||
|
||||
date = Date(timeInterval: -10, since: dateOrig)
|
||||
XCTAssertEqual(date.toRelativeTimeString(), "just now")
|
||||
|
||||
date = Date(timeInterval: -60, since: dateOrig)
|
||||
XCTAssertEqual(date.toRelativeTimeString(), ("today at " + DateFormatter.localizedString(from: date, dateStyle: DateFormatter.Style.none, timeStyle: DateFormatter.Style.short)))
|
||||
|
||||
let calendar = Calendar.autoupdatingCurrent
|
||||
date = calendar.date(byAdding: .day, value: -1, to: dateOrig)!
|
||||
XCTAssertEqual(date.toRelativeTimeString(), "yesterday")
|
||||
|
||||
date = calendar.date(byAdding: .day, value: -2, to: dateOrig)!
|
||||
XCTAssertEqual(date.toRelativeTimeString(), "this week")
|
||||
|
||||
date = calendar.date(byAdding: .day, value: -7, to: dateOrig)!
|
||||
XCTAssertEqual(date.toRelativeTimeString(), "more than a week ago")
|
||||
|
||||
date = calendar.date(byAdding: .day, value: -7 * 5, to: dateOrig)!
|
||||
XCTAssertEqual(date.toRelativeTimeString(), "more than a month ago")
|
||||
|
||||
date = Date(timeInterval: -60 * 60 * 24 * 7 * 5 * 2, since: dateOrig)
|
||||
XCTAssertEqual(date.toRelativeTimeString(), DateFormatter.localizedString(from: date, dateStyle: DateFormatter.Style.short, timeStyle: DateFormatter.Style.short))
|
||||
|
||||
date = Date(timeInterval: -60 * 60 * 24 * 7 * 5 * 12 * 2, since: dateOrig)
|
||||
XCTAssertEqual(date.toRelativeTimeString(), DateFormatter.localizedString(from: date, dateStyle: DateFormatter.Style.short, timeStyle: DateFormatter.Style.short))
|
||||
}
|
||||
}
|
||||
78
mobile/ios/ClientTests/ResetTests.swift
Normal file
78
mobile/ios/ClientTests/ResetTests.swift
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
@testable import Client
|
||||
import Shared
|
||||
@testable import Storage
|
||||
import Sync
|
||||
import UIKit
|
||||
|
||||
import XCTest
|
||||
|
||||
class MockBrowserProfile: BrowserProfile {
|
||||
var peekSyncManager: BrowserSyncManager {
|
||||
return self.syncManager as! BrowserSyncManager
|
||||
}
|
||||
|
||||
var peekTabs: SQLiteRemoteClientsAndTabs {
|
||||
return self.remoteClientsAndTabs as! SQLiteRemoteClientsAndTabs
|
||||
}
|
||||
}
|
||||
|
||||
class MockEngineStateChanges: EngineStateChanges {
|
||||
var collections: [String] = []
|
||||
var enabled: [String] = []
|
||||
var disabled: [String] = []
|
||||
var clearWasCalled: Bool = false
|
||||
|
||||
func collectionsThatNeedLocalReset() -> [String] {
|
||||
return self.collections
|
||||
}
|
||||
|
||||
func enginesEnabled() -> [String] {
|
||||
return self.enabled
|
||||
}
|
||||
|
||||
func enginesDisabled() -> [String] {
|
||||
return self.disabled
|
||||
}
|
||||
|
||||
func clearLocalCommands() {
|
||||
clearWasCalled = true
|
||||
}
|
||||
}
|
||||
|
||||
func assertClientsHaveGUIDsFromStorage(_ storage: RemoteClientsAndTabs, expected: [GUID]) {
|
||||
let recs = storage.getClients().value.successValue
|
||||
XCTAssertNotNil(recs)
|
||||
XCTAssertEqual(expected, recs!.map { $0.guid! })
|
||||
}
|
||||
|
||||
class ResetTests: XCTestCase {
|
||||
func testResetting() {
|
||||
let profile = MockBrowserProfile(localName: "testResetTests")
|
||||
|
||||
// Add a client.
|
||||
let tabs = profile.peekTabs
|
||||
XCTAssertTrue(tabs.insertOrUpdateClient(RemoteClient(guid: "abcdefghijkl", name: "Remote", modified: Date.now(), type: "mobile", formfactor: "tablet", os: "Windows", version: "55.0.1a", fxaDeviceId: "fxa1")).value.isSuccess)
|
||||
_ = tabs.replaceRemoteDevices([RemoteDevice(id: "fxa1", name: "Device 1", type: "desktop", isCurrentDevice: false, lastAccessTime: 123)]).succeeded()
|
||||
|
||||
// Verify that it's there.
|
||||
assertClientsHaveGUIDsFromStorage(tabs, expected: ["abcdefghijkl"])
|
||||
|
||||
// Tell the sync manager that "clients" has changed syncID.
|
||||
let e = MockEngineStateChanges()
|
||||
e.collections.append("clients")
|
||||
|
||||
XCTAssertTrue(profile.peekSyncManager.takeActionsOnEngineStateChanges(e).value.isSuccess)
|
||||
|
||||
// We threw away the command.
|
||||
XCTAssertTrue(e.clearWasCalled)
|
||||
|
||||
// And now we have no local clients.
|
||||
let empty = tabs.getClients().value.successValue
|
||||
XCTAssertNotNil(empty)
|
||||
XCTAssertEqual(empty!, [])
|
||||
}
|
||||
}
|
||||
174
mobile/ios/ClientTests/SearchEnginesTests.swift
Normal file
174
mobile/ios/ClientTests/SearchEnginesTests.swift
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
@testable import Client
|
||||
import Foundation
|
||||
import XCTest
|
||||
import Shared
|
||||
|
||||
private let DefaultSearchEngineName = "Google"
|
||||
private let ExpectedEngineNames = ["Amazon.com", "Bing", "DuckDuckGo", "Google", "Twitter", "Wikipedia", "Yahoo"]
|
||||
|
||||
class SearchEnginesTests: XCTestCase {
|
||||
|
||||
func testIncludesExpectedEngines() {
|
||||
// Verify that the set of shipped engines includes the expected subset.
|
||||
let profile = MockProfile()
|
||||
let engines = SearchEngines(prefs: profile.prefs, files: profile.files).orderedEngines
|
||||
XCTAssertTrue((engines?.count)! >= ExpectedEngineNames.count)
|
||||
|
||||
for engineName in ExpectedEngineNames {
|
||||
XCTAssertTrue(((engines?.filter { engine in engine.shortName == engineName })?.count)! > 0)
|
||||
}
|
||||
}
|
||||
|
||||
func testDefaultEngineOnStartup() {
|
||||
// If this is our first run, Yahoo should be first for the en locale.
|
||||
let profile = MockProfile()
|
||||
let engines = SearchEngines(prefs: profile.prefs, files: profile.files)
|
||||
XCTAssertEqual(engines.defaultEngine.shortName, DefaultSearchEngineName)
|
||||
XCTAssertEqual(engines.orderedEngines[0].shortName, DefaultSearchEngineName)
|
||||
}
|
||||
|
||||
func testAddingAndDeletingCustomEngines() {
|
||||
let testEngine = OpenSearchEngine(engineID: "ATester", shortName: "ATester", image: UIImage(), searchTemplate: "http://firefox.com/find?q={searchTerm}", suggestTemplate: nil, isCustomEngine: true)
|
||||
let profile = MockProfile()
|
||||
let engines = SearchEngines(prefs: profile.prefs, files: profile.files)
|
||||
engines.addSearchEngine(testEngine)
|
||||
XCTAssertEqual(engines.orderedEngines[1].engineID, testEngine.engineID)
|
||||
|
||||
engines.deleteCustomEngine(testEngine)
|
||||
let deleted = engines.orderedEngines.filter {$0 == testEngine}
|
||||
XCTAssertEqual(deleted, [])
|
||||
}
|
||||
|
||||
func testDefaultEngine() {
|
||||
let profile = MockProfile()
|
||||
let engines = SearchEngines(prefs: profile.prefs, files: profile.files)
|
||||
let engineSet = engines.orderedEngines
|
||||
|
||||
engines.defaultEngine = (engineSet?[0])!
|
||||
XCTAssertTrue(engines.isEngineDefault((engineSet?[0])!))
|
||||
XCTAssertFalse(engines.isEngineDefault((engineSet?[1])!))
|
||||
// The first ordered engine is the default.
|
||||
XCTAssertEqual(engines.orderedEngines[0].shortName, engineSet?[0].shortName)
|
||||
|
||||
engines.defaultEngine = (engineSet?[1])!
|
||||
XCTAssertFalse(engines.isEngineDefault((engineSet?[0])!))
|
||||
XCTAssertTrue(engines.isEngineDefault((engineSet?[1])!))
|
||||
// The first ordered engine is the default.
|
||||
XCTAssertEqual(engines.orderedEngines[0].shortName, engineSet?[1].shortName)
|
||||
|
||||
let engines2 = SearchEngines(prefs: profile.prefs, files: profile.files)
|
||||
// The default engine should have been persisted.
|
||||
XCTAssertTrue(engines2.isEngineDefault((engineSet?[1])!))
|
||||
// The first ordered engine is the default.
|
||||
XCTAssertEqual(engines.orderedEngines[0].shortName, engineSet?[1].shortName)
|
||||
}
|
||||
|
||||
func testOrderedEngines() {
|
||||
let profile = MockProfile()
|
||||
let engines = SearchEngines(prefs: profile.prefs, files: profile.files)
|
||||
|
||||
engines.orderedEngines = [ExpectedEngineNames[4], ExpectedEngineNames[2], ExpectedEngineNames[0]].map { name in
|
||||
for engine in engines.orderedEngines {
|
||||
if engine.shortName == name {
|
||||
return engine
|
||||
}
|
||||
}
|
||||
XCTFail("Could not find engine: \(name)")
|
||||
return engines.orderedEngines.first!
|
||||
}
|
||||
XCTAssertEqual(engines.orderedEngines[0].shortName, ExpectedEngineNames[4])
|
||||
XCTAssertEqual(engines.orderedEngines[1].shortName, ExpectedEngineNames[2])
|
||||
XCTAssertEqual(engines.orderedEngines[2].shortName, ExpectedEngineNames[0])
|
||||
|
||||
let engines2 = SearchEngines(prefs: profile.prefs, files: profile.files)
|
||||
// The ordering should have been persisted.
|
||||
XCTAssertEqual(engines2.orderedEngines[0].shortName, ExpectedEngineNames[4])
|
||||
XCTAssertEqual(engines2.orderedEngines[1].shortName, ExpectedEngineNames[2])
|
||||
XCTAssertEqual(engines2.orderedEngines[2].shortName, ExpectedEngineNames[0])
|
||||
|
||||
// Remaining engines should be appended in alphabetical order.
|
||||
XCTAssertEqual(engines2.orderedEngines[3].shortName, ExpectedEngineNames[1])
|
||||
XCTAssertEqual(engines2.orderedEngines[4].shortName, ExpectedEngineNames[3])
|
||||
XCTAssertEqual(engines2.orderedEngines[5].shortName, ExpectedEngineNames[5])
|
||||
XCTAssertEqual(engines2.orderedEngines[6].shortName, ExpectedEngineNames[6])
|
||||
}
|
||||
|
||||
func testQuickSearchEngines() {
|
||||
let profile = MockProfile()
|
||||
let engines = SearchEngines(prefs: profile.prefs, files: profile.files)
|
||||
let engineSet = engines.orderedEngines
|
||||
|
||||
// You can't disable the default engine.
|
||||
engines.defaultEngine = (engineSet?[1])!
|
||||
engines.disableEngine((engineSet?[1])!)
|
||||
XCTAssertTrue(engines.isEngineEnabled((engineSet?[1])!))
|
||||
|
||||
// The default engine is not included in the quick search engines.
|
||||
XCTAssertEqual(0, engines.quickSearchEngines.filter { engine in engine.shortName == engineSet?[1].shortName }.count)
|
||||
|
||||
// Enable and disable work.
|
||||
engines.enableEngine((engineSet?[0])!)
|
||||
XCTAssertTrue(engines.isEngineEnabled((engineSet?[0])!))
|
||||
XCTAssertEqual(1, engines.quickSearchEngines.filter { engine in engine.shortName == engineSet?[0].shortName }.count)
|
||||
|
||||
engines.disableEngine((engineSet?[0])!)
|
||||
XCTAssertFalse(engines.isEngineEnabled((engineSet?[0])!))
|
||||
XCTAssertEqual(0, engines.quickSearchEngines.filter { engine in engine.shortName == engineSet?[0].shortName }.count)
|
||||
|
||||
// Setting the default engine enables it.
|
||||
engines.defaultEngine = (engineSet?[0])!
|
||||
XCTAssertTrue(engines.isEngineEnabled((engineSet?[1])!))
|
||||
|
||||
// Setting the order may change the default engine, which enables it.
|
||||
engines.orderedEngines = [(engineSet?[2])!, (engineSet?[1])!, (engineSet?[0])!]
|
||||
XCTAssertTrue(engines.isEngineDefault((engineSet?[2])!))
|
||||
XCTAssertTrue(engines.isEngineEnabled((engineSet?[2])!))
|
||||
|
||||
// The enabling should be persisted.
|
||||
engines.enableEngine((engineSet?[2])!)
|
||||
engines.disableEngine((engineSet?[1])!)
|
||||
engines.enableEngine((engineSet?[0])!)
|
||||
|
||||
let engines2 = SearchEngines(prefs: profile.prefs, files: profile.files)
|
||||
XCTAssertTrue(engines2.isEngineEnabled((engineSet?[2])!))
|
||||
XCTAssertFalse(engines2.isEngineEnabled((engineSet?[1])!))
|
||||
XCTAssertTrue(engines2.isEngineEnabled((engineSet?[0])!))
|
||||
}
|
||||
|
||||
func testSearchSuggestionSettings() {
|
||||
let profile = MockProfile()
|
||||
let engines = SearchEngines(prefs: profile.prefs, files: profile.files)
|
||||
|
||||
// By default, you should see search suggestions
|
||||
XCTAssertTrue(engines.shouldShowSearchSuggestions)
|
||||
|
||||
// Setting should be persisted.
|
||||
engines.shouldShowSearchSuggestions = false
|
||||
|
||||
let engines2 = SearchEngines(prefs: profile.prefs, files: profile.files)
|
||||
XCTAssertFalse(engines2.shouldShowSearchSuggestions)
|
||||
}
|
||||
|
||||
func testDirectoriesForLanguageIdentifier() {
|
||||
XCTAssertEqual(
|
||||
SearchEngines.directoriesForLanguageIdentifier("nl", basePath: "/tmp", fallbackIdentifier: "en"),
|
||||
["/tmp/nl", "/tmp/en"]
|
||||
)
|
||||
XCTAssertEqual(
|
||||
SearchEngines.directoriesForLanguageIdentifier("en-US", basePath: "/tmp", fallbackIdentifier: "en"),
|
||||
["/tmp/en-US", "/tmp/en"]
|
||||
)
|
||||
XCTAssertEqual(
|
||||
SearchEngines.directoriesForLanguageIdentifier("es-MX", basePath: "/tmp", fallbackIdentifier: "en"),
|
||||
["/tmp/es-MX", "/tmp/es", "/tmp/en"]
|
||||
)
|
||||
XCTAssertEqual(
|
||||
SearchEngines.directoriesForLanguageIdentifier("zh-Hans-CN", basePath: "/tmp", fallbackIdentifier: "en"),
|
||||
["/tmp/zh-Hans-CN", "/tmp/zh-CN", "/tmp/zh", "/tmp/en"]
|
||||
)
|
||||
}
|
||||
}
|
||||
155
mobile/ios/ClientTests/SearchTests.swift
Normal file
155
mobile/ios/ClientTests/SearchTests.swift
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import GCDWebServers
|
||||
@testable import Client
|
||||
import UIKit
|
||||
|
||||
import XCTest
|
||||
|
||||
class SearchTests: XCTestCase {
|
||||
func testParsing() {
|
||||
let parser = OpenSearchParser(pluginMode: true)
|
||||
let file = Bundle.main.path(forResource: "google", ofType: "xml", inDirectory: "SearchPlugins/en")
|
||||
let engine: OpenSearchEngine! = parser.parse(file!, engineID: "google")
|
||||
XCTAssertEqual(engine.shortName, "Google")
|
||||
|
||||
// Test regular search queries.
|
||||
XCTAssertEqual(engine.searchURLForQuery("foobar")!.absoluteString, "https://www.google.com/search?q=foobar&ie=utf-8&oe=utf-8&client=firefox-b")
|
||||
|
||||
// Test search suggestion queries.
|
||||
XCTAssertEqual(engine.suggestURLForQuery("foobar")!.absoluteString, "https://www.google.com/complete/search?client=firefox&q=foobar")
|
||||
}
|
||||
|
||||
func testURIFixup() {
|
||||
// Check valid URLs. We can load these after some fixup.
|
||||
checkValidURL("http://www.mozilla.org", afterFixup: "http://www.mozilla.org")
|
||||
checkValidURL("about:", afterFixup: "about:")
|
||||
checkValidURL("about:config", afterFixup: "about:config")
|
||||
checkValidURL("about: config", afterFixup: "about:%20config")
|
||||
checkValidURL("file:///f/o/o", afterFixup: "file:///f/o/o")
|
||||
checkValidURL("ftp://ftp.mozilla.org", afterFixup: "ftp://ftp.mozilla.org")
|
||||
checkValidURL("foo.bar", afterFixup: "http://foo.bar")
|
||||
checkValidURL(" foo.bar ", afterFixup: "http://foo.bar")
|
||||
checkValidURL("1.2.3", afterFixup: "http://1.2.3")
|
||||
|
||||
// Check invalid URLs. These are passed along to the default search engine.
|
||||
checkInvalidURL("foobar")
|
||||
checkInvalidURL("foo bar")
|
||||
checkInvalidURL("mozilla. org")
|
||||
checkInvalidURL("123")
|
||||
checkInvalidURL("a/b")
|
||||
checkInvalidURL("创业咖啡")
|
||||
checkInvalidURL("创业咖啡 中国")
|
||||
checkInvalidURL("创业咖啡. 中国")
|
||||
}
|
||||
|
||||
func testURIFixupPunyCode() {
|
||||
checkValidURL("http://创业咖啡.中国/", afterFixup: "http://xn--vhq70hq9bhxa.xn--fiqs8s/")
|
||||
checkValidURL("创业咖啡.中国", afterFixup: "http://xn--vhq70hq9bhxa.xn--fiqs8s")
|
||||
checkValidURL(" 创业咖啡.中国 ", afterFixup: "http://xn--vhq70hq9bhxa.xn--fiqs8s")
|
||||
}
|
||||
|
||||
fileprivate func checkValidURL(_ beforeFixup: String, afterFixup: String) {
|
||||
XCTAssertEqual(URIFixup.getURL(beforeFixup)!.absoluteString, afterFixup)
|
||||
}
|
||||
|
||||
fileprivate func checkInvalidURL(_ beforeFixup: String) {
|
||||
XCTAssertNil(URIFixup.getURL(beforeFixup))
|
||||
}
|
||||
|
||||
func testSuggestClient() {
|
||||
let webServerBase = startMockSuggestServer()
|
||||
let engine = OpenSearchEngine(engineID: "mock", shortName: "Mock engine", image: UIImage(), searchTemplate: "", suggestTemplate: "\(webServerBase)?q={searchTerms}",
|
||||
isCustomEngine: false)
|
||||
let client = SearchSuggestClient(searchEngine: engine, userAgent: "Fx-testSuggestClient")
|
||||
|
||||
let query1 = self.expectation(description: "foo query")
|
||||
client.query("foo", callback: { response, error in
|
||||
withExtendedLifetime(client) {
|
||||
if error != nil {
|
||||
XCTFail("Error: \(error?.description ?? "nil")")
|
||||
}
|
||||
|
||||
XCTAssertEqual(response![0], "foo")
|
||||
XCTAssertEqual(response![1], "foo2")
|
||||
XCTAssertEqual(response![2], "foo you")
|
||||
|
||||
query1.fulfill()
|
||||
}
|
||||
})
|
||||
waitForExpectations(timeout: 10, handler: nil)
|
||||
|
||||
let query2 = self.expectation(description: "foo bar query")
|
||||
client.query("foo bar", callback: { response, error in
|
||||
withExtendedLifetime(client) {
|
||||
if error != nil {
|
||||
XCTFail("Error: \(error?.description ?? "nil")")
|
||||
}
|
||||
|
||||
XCTAssertEqual(response![0], "foo bar soap")
|
||||
XCTAssertEqual(response![1], "foo barstool")
|
||||
XCTAssertEqual(response![2], "foo bartender")
|
||||
|
||||
query2.fulfill()
|
||||
}
|
||||
})
|
||||
waitForExpectations(timeout: 10, handler: nil)
|
||||
}
|
||||
|
||||
func testExtractingOfSearchTermsFromURL() {
|
||||
let parser = OpenSearchParser(pluginMode: true)
|
||||
var file = Bundle.main.path(forResource: "google", ofType: "xml", inDirectory: "SearchPlugins/en")
|
||||
let googleEngine: OpenSearchEngine! = parser.parse(file!, engineID: "google")
|
||||
|
||||
// create URL
|
||||
let searchTerm = "Foo Bar"
|
||||
let encodedSeachTerm = searchTerm.replacingOccurrences(of: " ", with: "+")
|
||||
let googleSearchURL = URL(string: "https://www.google.com/search?q=\(encodedSeachTerm)&ie=utf-8&oe=utf-8&gws_rd=cr&ei=I0UyVp_qK4HtUoytjagM")
|
||||
let duckDuckGoSearchURL = URL(string: "https://duckduckgo.com/?q=\(encodedSeachTerm)&ia=about")
|
||||
let invalidSearchURL = URL(string: "https://www.google.co.uk")
|
||||
|
||||
// check it correctly matches google search term given google config
|
||||
XCTAssertEqual(searchTerm, googleEngine.queryForSearchURL(googleSearchURL))
|
||||
|
||||
// check it doesn't match when the URL is not a search URL
|
||||
XCTAssertNil(googleEngine.queryForSearchURL(invalidSearchURL))
|
||||
|
||||
// check that it matches given a different configuration
|
||||
file = Bundle.main.path(forResource: "duckduckgo", ofType: "xml", inDirectory: "SearchPlugins/en")
|
||||
let duckDuckGoEngine: OpenSearchEngine! = parser.parse(file!, engineID: "duckduckgo")
|
||||
XCTAssertEqual(searchTerm, duckDuckGoEngine.queryForSearchURL(duckDuckGoSearchURL))
|
||||
|
||||
// check it doesn't match search URLs for different configurations
|
||||
XCTAssertNil(duckDuckGoEngine.queryForSearchURL(googleSearchURL))
|
||||
|
||||
// check that if you pass in a nil URL that everything works
|
||||
XCTAssertNil(duckDuckGoEngine.queryForSearchURL(nil))
|
||||
}
|
||||
|
||||
fileprivate func startMockSuggestServer() -> String {
|
||||
let webServer: GCDWebServer = GCDWebServer()
|
||||
|
||||
webServer.addHandler(forMethod: "GET", path: "/", request: GCDWebServerRequest.self) { (request) -> GCDWebServerResponse! in
|
||||
var suggestions: [String]!
|
||||
let query = request?.query["q"] as! String
|
||||
switch query {
|
||||
case "foo":
|
||||
suggestions = ["foo", "foo2", "foo you"]
|
||||
case "foo bar":
|
||||
suggestions = ["foo bar soap", "foo barstool", "foo bartender"]
|
||||
default:
|
||||
XCTFail("Unexpected query: \(query)")
|
||||
}
|
||||
return GCDWebServerDataResponse(jsonObject: [query, suggestions])
|
||||
}
|
||||
|
||||
if !webServer.start(withPort: 0, bonjourName: nil) {
|
||||
XCTFail("Can't start the GCDWebServer")
|
||||
}
|
||||
|
||||
return "http://localhost:\(webServer.port)"
|
||||
}
|
||||
}
|
||||
74
mobile/ios/ClientTests/StringExtensionsTests.swift
Normal file
74
mobile/ios/ClientTests/StringExtensionsTests.swift
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
class StringExtensionsTests: XCTestCase {
|
||||
|
||||
func testStartsWith() {
|
||||
XCTAssertTrue("abcde".startsWith("abcde"))
|
||||
XCTAssertTrue("abcde".startsWith(""))
|
||||
XCTAssertTrue("abcde".startsWith("a"))
|
||||
XCTAssertTrue("abcdea".startsWith("a"))
|
||||
XCTAssertFalse("abcde".startsWith("fa"))
|
||||
XCTAssertFalse("abcde".startsWith("af"))
|
||||
XCTAssertFalse("abcde".startsWith("b"))
|
||||
}
|
||||
|
||||
func testEndsWith() {
|
||||
XCTAssertTrue("abcde".endsWith("abcde"))
|
||||
XCTAssertTrue("abcde".endsWith(""))
|
||||
XCTAssertTrue("abcde".endsWith("e"))
|
||||
XCTAssertTrue("abcdea".endsWith("a"))
|
||||
XCTAssertFalse("abcde".endsWith("fe"))
|
||||
XCTAssertFalse("abcde".endsWith("ef"))
|
||||
XCTAssertFalse("abcde".endsWith("d"))
|
||||
}
|
||||
|
||||
func testEllipsize() {
|
||||
// Odd maxLength. Note that we ellipsize with a Unicode join character to avoid wrapping.
|
||||
XCTAssertEqual("abcd…\u{2060}fgh", "abcdefgh".ellipsize(maxLength: 7))
|
||||
|
||||
// Even maxLength.
|
||||
XCTAssertEqual("abcd…\u{2060}ijkl", "abcdefghijkl".ellipsize(maxLength: 8))
|
||||
|
||||
// String shorter than maxLength.
|
||||
XCTAssertEqual("abcd", "abcd".ellipsize(maxLength: 7))
|
||||
|
||||
// Empty String.
|
||||
XCTAssertEqual("", "".ellipsize(maxLength: 8))
|
||||
|
||||
// maxLength < 2.
|
||||
XCTAssertEqual("abcdefgh", "abcdefgh".ellipsize(maxLength: 0))
|
||||
}
|
||||
|
||||
func testStringByTrimmingLeadingCharactersInSet() {
|
||||
XCTAssertEqual("foo ", " foo ".stringByTrimmingLeadingCharactersInSet(CharacterSet.whitespaces))
|
||||
XCTAssertEqual("foo456", "123foo456".stringByTrimmingLeadingCharactersInSet(CharacterSet.decimalDigits))
|
||||
XCTAssertEqual("", "123456".stringByTrimmingLeadingCharactersInSet(CharacterSet.decimalDigits))
|
||||
}
|
||||
|
||||
func testStringSplitWithNewline() {
|
||||
XCTAssertEqual("", "".stringSplitWithNewline())
|
||||
XCTAssertEqual("foo", "foo".stringSplitWithNewline())
|
||||
XCTAssertEqual("aaa\n bbb", "aaa bbb".stringSplitWithNewline())
|
||||
XCTAssertEqual("Mark as\n Read", "Mark as Read".stringSplitWithNewline())
|
||||
XCTAssertEqual("aa\n bbbbbb", "aa bbbbbb".stringSplitWithNewline())
|
||||
}
|
||||
|
||||
func testPercentEscaping() {
|
||||
func roundtripTest(_ input: String, _ expected: String, file: StaticString = #file, line: UInt = #line) {
|
||||
let observed = input.escape()!
|
||||
XCTAssertEqual(observed, expected, "input is \(input)", file: file, line: line)
|
||||
let roundtrip = observed.unescape()
|
||||
XCTAssertEqual(roundtrip, input, "encoded is \(observed)", file: file, line: line)
|
||||
}
|
||||
|
||||
roundtripTest("https://mozilla.com", "https://mozilla.com")
|
||||
roundtripTest("http://www.cnn.com/2017/09/25/politics/north-korea-fm-us-bombers/index.html", "http://www.cnn.com/2017/09/25/politics/north-korea-fm-us-bombers/index.html")
|
||||
roundtripTest("http://mozilla.com/?a=foo&b=bar", "http://mozilla.com/%3Fa%3Dfoo%26b%3Dbar")
|
||||
}
|
||||
|
||||
}
|
||||
109
mobile/ios/ClientTests/SyncStatusResolverTests.swift
Normal file
109
mobile/ios/ClientTests/SyncStatusResolverTests.swift
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
@testable import Client
|
||||
@testable import Sync
|
||||
|
||||
import Shared
|
||||
import Storage
|
||||
import XCTest
|
||||
|
||||
private class RandomError: MaybeErrorType {
|
||||
var description = "random_error"
|
||||
}
|
||||
|
||||
class SyncStatusResolverTests: XCTestCase {
|
||||
|
||||
private func mockStatsForCollection(collection: String) -> SyncEngineStatsSession {
|
||||
return SyncEngineStatsSession(collection: collection)
|
||||
}
|
||||
|
||||
func testAllCompleted() {
|
||||
let results: EngineResults = [
|
||||
("tabs", .completed(mockStatsForCollection(collection: "tabs"))),
|
||||
("clients", .completed(mockStatsForCollection(collection: "clients")))
|
||||
]
|
||||
let maybeResults = Maybe(success: results)
|
||||
|
||||
let resolver = SyncStatusResolver(engineResults: maybeResults)
|
||||
XCTAssertTrue(resolver.resolveResults() == SyncDisplayState.good)
|
||||
}
|
||||
|
||||
func testAllCompletedExceptOneDisabledRemotely() {
|
||||
let results: EngineResults = [
|
||||
("tabs", .completed(mockStatsForCollection(collection: "tabs"))),
|
||||
("clients", .notStarted(.engineRemotelyNotEnabled(collection: "clients")))
|
||||
]
|
||||
let maybeResults = Maybe(success: results)
|
||||
|
||||
let resolver = SyncStatusResolver(engineResults: maybeResults)
|
||||
XCTAssertTrue(resolver.resolveResults() == SyncDisplayState.good)
|
||||
}
|
||||
|
||||
func testAllCompletedExceptNotStartedBecauseNoAccount() {
|
||||
let results: EngineResults = [
|
||||
("tabs", .completed(mockStatsForCollection(collection: "tabs"))),
|
||||
("clients", .notStarted(.noAccount))
|
||||
]
|
||||
let maybeResults = Maybe(success: results)
|
||||
|
||||
let resolver = SyncStatusResolver(engineResults: maybeResults)
|
||||
XCTAssertTrue(resolver.resolveResults() == SyncDisplayState.warning(message: Strings.FirefoxSyncOfflineTitle))
|
||||
}
|
||||
|
||||
func testAllCompletedExceptNotStartedBecauseOffline() {
|
||||
let results: EngineResults = [
|
||||
("tabs", .completed(mockStatsForCollection(collection: "tabs"))),
|
||||
("clients", .notStarted(.offline))
|
||||
]
|
||||
let maybeResults = Maybe(success: results)
|
||||
|
||||
let resolver = SyncStatusResolver(engineResults: maybeResults)
|
||||
XCTAssertTrue(resolver.resolveResults() == SyncDisplayState.bad(message: Strings.FirefoxSyncOfflineTitle))
|
||||
}
|
||||
|
||||
func testOfflineAndNoAccount() {
|
||||
let results: EngineResults = [
|
||||
("tabs", .notStarted(.noAccount)),
|
||||
("clients", .notStarted(.offline))
|
||||
]
|
||||
|
||||
let maybeResults = Maybe(success: results)
|
||||
|
||||
let resolver = SyncStatusResolver(engineResults: maybeResults)
|
||||
XCTAssertTrue(resolver.resolveResults() == SyncDisplayState.bad(message: Strings.FirefoxSyncOfflineTitle))
|
||||
}
|
||||
|
||||
func testAllPartial() {
|
||||
let results: EngineResults = [
|
||||
("tabs", .partial(SyncEngineStatsSession(collection: "tabs"))),
|
||||
("clients", .partial(SyncEngineStatsSession(collection: "clients")))
|
||||
]
|
||||
let maybeResults = Maybe(success: results)
|
||||
|
||||
let resolver = SyncStatusResolver(engineResults: maybeResults)
|
||||
XCTAssertTrue(resolver.resolveResults() == SyncDisplayState.good)
|
||||
}
|
||||
|
||||
func testBookmarkMergeError() {
|
||||
let maybeResults: Maybe<EngineResults> = Maybe(failure: BookmarksMergeError())
|
||||
let resolver = SyncStatusResolver(engineResults: maybeResults)
|
||||
let expected = SyncDisplayState.warning(message: String(format: Strings.FirefoxSyncPartialTitle, Strings.localizedStringForSyncComponent("bookmarks") ?? ""))
|
||||
XCTAssertTrue(resolver.resolveResults() == expected)
|
||||
}
|
||||
|
||||
func testBufferInvalidError() {
|
||||
let maybeResults: Maybe<EngineResults> = Maybe(failure: BufferInvalidError(inconsistencies: [:], validationDuration: 0))
|
||||
let resolver = SyncStatusResolver(engineResults: maybeResults)
|
||||
let expected = SyncDisplayState.warning(message: String(format: Strings.FirefoxSyncPartialTitle, Strings.localizedStringForSyncComponent("bookmarks") ?? ""))
|
||||
XCTAssertTrue(resolver.resolveResults() == expected)
|
||||
}
|
||||
|
||||
func testRandomFailure() {
|
||||
let maybeResults: Maybe<EngineResults> = Maybe(failure: RandomError())
|
||||
let resolver = SyncStatusResolver(engineResults: maybeResults)
|
||||
let expected = SyncDisplayState.bad(message: nil)
|
||||
XCTAssertTrue(resolver.resolveResults() == expected)
|
||||
}
|
||||
}
|
||||
87
mobile/ios/ClientTests/TabEventHandlerTests.swift
Normal file
87
mobile/ios/ClientTests/TabEventHandlerTests.swift
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
@testable import Client
|
||||
import WebKit
|
||||
|
||||
import XCTest
|
||||
|
||||
class TabEventHandlerTests: XCTestCase {
|
||||
|
||||
func testEventDelivery() {
|
||||
let tab = Tab(configuration: WKWebViewConfiguration())
|
||||
let handler = DummyHandler()
|
||||
|
||||
XCTAssertNil(handler.isFocused)
|
||||
|
||||
TabEvent.post(.didGainFocus, for: tab)
|
||||
XCTAssertTrue(handler.isFocused!)
|
||||
|
||||
TabEvent.post(.didLoseFocus, for: tab)
|
||||
XCTAssertFalse(handler.isFocused!)
|
||||
}
|
||||
|
||||
func testUnregistration() {
|
||||
let tab = Tab(configuration: WKWebViewConfiguration())
|
||||
let handler = DummyHandler()
|
||||
|
||||
XCTAssertNil(handler.isFocused)
|
||||
|
||||
TabEvent.post(.didGainFocus, for: tab)
|
||||
XCTAssertTrue(handler.isFocused!)
|
||||
|
||||
handler.doUnregister()
|
||||
TabEvent.post(.didLoseFocus, for: tab)
|
||||
// The event didn't reach us, so we should still be focused.
|
||||
XCTAssertTrue(handler.isFocused!)
|
||||
}
|
||||
|
||||
func testOnlyRegisteredForEvents() {
|
||||
let tab = Tab(configuration: WKWebViewConfiguration())
|
||||
let handler = DummyHandler()
|
||||
handler.doUnregister()
|
||||
|
||||
let tabObservers = handler.registerFor(.didGainFocus)
|
||||
|
||||
XCTAssertNil(handler.isFocused)
|
||||
|
||||
TabEvent.post(.didGainFocus, for: tab)
|
||||
XCTAssertTrue(handler.isFocused!)
|
||||
|
||||
TabEvent.post(.didLoseFocus, for: tab)
|
||||
XCTAssertTrue(handler.isFocused!)
|
||||
|
||||
handler.unregister(tabObservers)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class DummyHandler: TabEventHandler {
|
||||
var tabObservers: TabObservers!
|
||||
|
||||
// This is not how this should be written in production — the handler shouldn't be keeping track
|
||||
// of individual tab state.
|
||||
var isFocused: Bool? = nil
|
||||
|
||||
init() {
|
||||
tabObservers = registerFor(.didGainFocus, .didLoseFocus)
|
||||
}
|
||||
|
||||
deinit {
|
||||
doUnregister()
|
||||
}
|
||||
|
||||
fileprivate func doUnregister() {
|
||||
unregister(tabObservers)
|
||||
}
|
||||
|
||||
func tabDidGainFocus(_ tab: Tab) {
|
||||
isFocused = true
|
||||
}
|
||||
|
||||
func tabDidLoseFocus(_ tab: Tab) {
|
||||
isFocused = false
|
||||
}
|
||||
}
|
||||
457
mobile/ios/ClientTests/TabManagerTests.swift
Normal file
457
mobile/ios/ClientTests/TabManagerTests.swift
Normal file
|
|
@ -0,0 +1,457 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
@testable import Client
|
||||
import Shared
|
||||
import Storage
|
||||
import UIKit
|
||||
import WebKit
|
||||
import Deferred
|
||||
|
||||
import XCTest
|
||||
|
||||
open class TabManagerMockProfile: MockProfile {
|
||||
var numberOfTabsStored = 0
|
||||
override public func storeTabs(_ tabs: [RemoteTab]) -> Deferred<Maybe<Int>> {
|
||||
numberOfTabsStored = tabs.count
|
||||
return deferMaybe(tabs.count)
|
||||
}
|
||||
}
|
||||
|
||||
open class MockTabManagerStateDelegate: TabManagerStateDelegate {
|
||||
var numberOfTabsStored = 0
|
||||
public func tabManagerWillStoreTabs(_ tabs: [Tab]) {
|
||||
numberOfTabsStored = tabs.count
|
||||
}
|
||||
}
|
||||
|
||||
struct MethodSpy {
|
||||
let functionName: String
|
||||
let method: ((_ tabs: [Tab?]) -> Void)?
|
||||
|
||||
init(functionName: String) {
|
||||
self.functionName = functionName
|
||||
self.method = nil
|
||||
}
|
||||
|
||||
init(functionName: String, method: ((_ tabs: [Tab?]) -> Void)?) {
|
||||
self.functionName = functionName
|
||||
self.method = method
|
||||
}
|
||||
}
|
||||
|
||||
open class MockTabManagerDelegate: TabManagerDelegate {
|
||||
|
||||
//this array represents the order in which delegate methods should be called.
|
||||
//each delegate method will pop the first struct from the array. If the method name doesn't match the struct then the order is incorrect
|
||||
//Then it evaluates the method closure which will return true/false depending on if the tabs are correct
|
||||
var methodCatchers: [MethodSpy] = []
|
||||
|
||||
func expect(_ methods: [MethodSpy]) {
|
||||
self.methodCatchers = methods
|
||||
}
|
||||
|
||||
func verify(_ message: String) {
|
||||
XCTAssertTrue(methodCatchers.isEmpty, message)
|
||||
}
|
||||
|
||||
func testDelegateMethodWithName(_ name: String, tabs: [Tab?]) {
|
||||
guard let spy = self.methodCatchers.first else {
|
||||
XCTAssert(false, "No method was availible in the queue. For the delegate method \(name) to use")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(spy.functionName, name)
|
||||
if let methodCheck = spy.method {
|
||||
methodCheck(tabs)
|
||||
}
|
||||
methodCatchers.removeFirst()
|
||||
}
|
||||
|
||||
public func tabManager(_ tabManager: TabManager, didSelectedTabChange selected: Tab?, previous: Tab?) {
|
||||
testDelegateMethodWithName(#function, tabs: [selected, previous])
|
||||
}
|
||||
|
||||
public func tabManager(_ tabManager: TabManager, didAddTab tab: Tab) {
|
||||
testDelegateMethodWithName(#function, tabs: [tab])
|
||||
}
|
||||
|
||||
public func tabManager(_ tabManager: TabManager, didRemoveTab tab: Tab) {
|
||||
testDelegateMethodWithName(#function, tabs: [tab])
|
||||
}
|
||||
|
||||
public func tabManagerDidRestoreTabs(_ tabManager: TabManager) {
|
||||
testDelegateMethodWithName(#function, tabs: [])
|
||||
}
|
||||
|
||||
public func tabManager(_ tabManager: TabManager, willRemoveTab tab: Tab) {
|
||||
testDelegateMethodWithName(#function, tabs: [tab])
|
||||
}
|
||||
|
||||
public func tabManager(_ tabManager: TabManager, willAddTab tab: Tab) {
|
||||
testDelegateMethodWithName(#function, tabs: [tab])
|
||||
}
|
||||
|
||||
public func tabManagerDidAddTabs(_ tabManager: TabManager) {
|
||||
testDelegateMethodWithName(#function, tabs: [])
|
||||
}
|
||||
|
||||
public func tabManagerDidRemoveAllTabs(_ tabManager: TabManager, toast: ButtonToast?) {
|
||||
testDelegateMethodWithName(#function, tabs: [])
|
||||
}
|
||||
}
|
||||
|
||||
class TabManagerTests: XCTestCase {
|
||||
|
||||
let willRemove = MethodSpy(functionName: "tabManager(_:willRemoveTab:)")
|
||||
let didRemove = MethodSpy(functionName: "tabManager(_:didRemoveTab:)")
|
||||
let willAdd = MethodSpy(functionName: "tabManager(_:willAddTab:)")
|
||||
let didAdd = MethodSpy(functionName: "tabManager(_:didAddTab:)")
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testTabManagerCallsTabManagerStateDelegateOnStoreChangesWithNormalTabs() {
|
||||
let profile = TabManagerMockProfile()
|
||||
let manager = TabManager(prefs: profile.prefs, imageStore: nil)
|
||||
let stateDelegate = MockTabManagerStateDelegate()
|
||||
manager.stateDelegate = stateDelegate
|
||||
let configuration = WKWebViewConfiguration()
|
||||
configuration.processPool = WKProcessPool()
|
||||
|
||||
// test that non-private tabs are saved to the db
|
||||
// add some non-private tabs to the tab manager
|
||||
for _ in 0..<3 {
|
||||
let tab = Tab(configuration: configuration)
|
||||
tab.url = URL(string: "http://yahoo.com")!
|
||||
manager.configureTab(tab, request: URLRequest(url: tab.url!), flushToDisk: false, zombie: false)
|
||||
}
|
||||
|
||||
manager.storeChanges()
|
||||
|
||||
XCTAssertEqual(stateDelegate.numberOfTabsStored, 3, "Expected state delegate to have been called with 3 tabs, but called with \(stateDelegate.numberOfTabsStored)")
|
||||
}
|
||||
|
||||
func testTabManagerDoesNotCallTabManagerStateDelegateOnStoreChangesWithPrivateTabs() {
|
||||
let profile = TabManagerMockProfile()
|
||||
let manager = TabManager(prefs: profile.prefs, imageStore: nil)
|
||||
let stateDelegate = MockTabManagerStateDelegate()
|
||||
manager.stateDelegate = stateDelegate
|
||||
let configuration = WKWebViewConfiguration()
|
||||
configuration.processPool = WKProcessPool()
|
||||
|
||||
// test that non-private tabs are saved to the db
|
||||
// add some non-private tabs to the tab manager
|
||||
for _ in 0..<3 {
|
||||
let tab = Tab(configuration: configuration, isPrivate: true)
|
||||
tab.url = URL(string: "http://yahoo.com")!
|
||||
manager.configureTab(tab, request: URLRequest(url: tab.url!), flushToDisk: false, zombie: false)
|
||||
}
|
||||
|
||||
manager.storeChanges()
|
||||
|
||||
XCTAssertEqual(stateDelegate.numberOfTabsStored, 0, "Expected state delegate to have been called with 3 tabs, but called with \(stateDelegate.numberOfTabsStored)")
|
||||
}
|
||||
|
||||
func testAddTab() {
|
||||
let profile = TabManagerMockProfile()
|
||||
let manager = TabManager(prefs: profile.prefs, imageStore: nil)
|
||||
let delegate = MockTabManagerDelegate()
|
||||
manager.addDelegate(delegate)
|
||||
|
||||
delegate.expect([willAdd, didAdd])
|
||||
manager.addTab()
|
||||
delegate.verify("Not all delegate methods were called")
|
||||
}
|
||||
|
||||
func testDidDeleteLastTab() {
|
||||
let profile = TabManagerMockProfile()
|
||||
let manager = TabManager(prefs: profile.prefs, imageStore: nil)
|
||||
let delegate = MockTabManagerDelegate()
|
||||
|
||||
//create the tab before adding the mock delegate. So we don't have to check delegate calls we dont care about
|
||||
let tab = manager.addTab()
|
||||
manager.selectTab(tab)
|
||||
manager.addDelegate(delegate)
|
||||
|
||||
let didSelect = MethodSpy(functionName: "tabManager(_:didSelectedTabChange:previous:)") { tabs in
|
||||
let next = tabs[0]!
|
||||
let previous = tabs[1]!
|
||||
XCTAssertTrue(previous != next)
|
||||
XCTAssertTrue(previous == tab)
|
||||
XCTAssertFalse(next.isPrivate)
|
||||
}
|
||||
delegate.expect([willRemove, didRemove, willAdd, didAdd, didSelect])
|
||||
manager.removeTab(tab)
|
||||
delegate.verify("Not all delegate methods were called")
|
||||
}
|
||||
|
||||
func testDidDeleteLastPrivateTab() {
|
||||
let profile = TabManagerMockProfile()
|
||||
let manager = TabManager(prefs: profile.prefs, imageStore: nil)
|
||||
let delegate = MockTabManagerDelegate()
|
||||
|
||||
//create the tab before adding the mock delegate. So we don't have to check delegate calls we dont care about
|
||||
let tab = manager.addTab()
|
||||
manager.selectTab(tab)
|
||||
let privateTab = manager.addTab(isPrivate: true)
|
||||
manager.selectTab(privateTab)
|
||||
manager.addDelegate(delegate)
|
||||
|
||||
let didSelect = MethodSpy(functionName: "tabManager(_:didSelectedTabChange:previous:)") { tabs in
|
||||
let next = tabs[0]!
|
||||
let previous = tabs[1]!
|
||||
XCTAssertTrue(previous != next)
|
||||
XCTAssertTrue(previous == privateTab)
|
||||
XCTAssertTrue(next == tab)
|
||||
XCTAssertTrue(previous.isPrivate)
|
||||
XCTAssertTrue(manager.selectedTab == next)
|
||||
}
|
||||
delegate.expect([willRemove, didRemove, didSelect])
|
||||
manager.removeTab(privateTab)
|
||||
delegate.verify("Not all delegate methods were called")
|
||||
}
|
||||
|
||||
func testDeletePrivateTabsOnExit() {
|
||||
//setup
|
||||
let profile = TabManagerMockProfile()
|
||||
let manager = TabManager(prefs: profile.prefs, imageStore: nil)
|
||||
profile.prefs.setBool(true, forKey: "settings.closePrivateTabs")
|
||||
|
||||
// create one private and one normal tab
|
||||
let tab = manager.addTab()
|
||||
manager.selectTab(tab)
|
||||
manager.selectTab(manager.addTab(isPrivate: true))
|
||||
|
||||
XCTAssertEqual(manager.selectedTab?.isPrivate, true, "The selected tab should be the private tab")
|
||||
XCTAssertEqual(manager.privateTabs.count, 1, "There should only be one private tab")
|
||||
|
||||
manager.selectTab(tab)
|
||||
XCTAssertEqual(manager.privateTabs.count, 0, "If the normal tab is selected the private tab should have been deleted")
|
||||
XCTAssertEqual(manager.normalTabs.count, 1, "The regular tab should stil be around")
|
||||
|
||||
manager.selectTab(manager.addTab(isPrivate: true))
|
||||
XCTAssertEqual(manager.privateTabs.count, 1, "There should be one new private tab")
|
||||
manager.willSwitchTabMode()
|
||||
XCTAssertEqual(manager.privateTabs.count, 0, "After willSwitchTabMode there should be no more private tabs")
|
||||
|
||||
manager.selectTab(manager.addTab(isPrivate: true))
|
||||
manager.selectTab(manager.addTab(isPrivate: true))
|
||||
XCTAssertEqual(manager.privateTabs.count, 2, "Private tabs should not be deleted when another one is added")
|
||||
manager.selectTab(manager.addTab())
|
||||
XCTAssertEqual(manager.privateTabs.count, 0, "But once we add a normal tab we've switched out of private mode. Private tabs should be deleted")
|
||||
XCTAssertEqual(manager.normalTabs.count, 2, "The original normal tab and the new one should both still exist")
|
||||
|
||||
profile.prefs.setBool(false, forKey: "settings.closePrivateTabs")
|
||||
manager.selectTab(manager.addTab(isPrivate: true))
|
||||
manager.selectTab(tab)
|
||||
XCTAssertEqual(manager.selectedTab?.isPrivate, false, "The selected tab should not be private")
|
||||
XCTAssertEqual(manager.privateTabs.count, 1, "If the flag is false then private tabs should still exist")
|
||||
}
|
||||
|
||||
func testDeleteNonSelectedTab() {
|
||||
let profile = TabManagerMockProfile()
|
||||
let manager = TabManager(prefs: profile.prefs, imageStore: nil)
|
||||
let delegate = MockTabManagerDelegate()
|
||||
|
||||
//create the tab before adding the mock delegate. So we don't have to check delegate calls we dont care about
|
||||
let tab = manager.addTab()
|
||||
manager.selectTab(tab)
|
||||
manager.addTab()
|
||||
let deleteTab = manager.addTab()
|
||||
manager.addDelegate(delegate)
|
||||
|
||||
delegate.expect([willRemove, didRemove])
|
||||
manager.removeTab(deleteTab)
|
||||
|
||||
delegate.verify("Not all delegate methods were called")
|
||||
}
|
||||
|
||||
func testDeleteSelectedTab() {
|
||||
let profile = TabManagerMockProfile()
|
||||
let manager = TabManager(prefs: profile.prefs, imageStore: nil)
|
||||
let delegate = MockTabManagerDelegate()
|
||||
|
||||
func addTab(_ load: Bool) -> Tab {
|
||||
let tab = manager.addTab()
|
||||
if load {
|
||||
tab.lastExecutedTime = Date.now()
|
||||
}
|
||||
return tab
|
||||
}
|
||||
|
||||
let tab0 = addTab(false) // not loaded
|
||||
let tab1 = addTab(true)
|
||||
let tab2 = addTab(true)
|
||||
let tab3 = addTab(false) // not loaded
|
||||
let tab4 = addTab(true)
|
||||
|
||||
// starting at tab2, we should be selecting
|
||||
// [ tab4, tab1, tab3, tab0 ]
|
||||
|
||||
manager.selectTab(tab2)
|
||||
manager.removeTab(manager.selectedTab!)
|
||||
// Rule: most recently loaded.
|
||||
XCTAssertEqual(manager.selectedTab, tab4)
|
||||
|
||||
manager.removeTab(manager.selectedTab!)
|
||||
// Rule: most recently loaded.
|
||||
XCTAssertEqual(manager.selectedTab, tab1)
|
||||
|
||||
manager.removeTab(manager.selectedTab!)
|
||||
// Rule: next to the right.
|
||||
XCTAssertEqual(manager.selectedTab, tab3)
|
||||
|
||||
manager.removeTab(manager.selectedTab!)
|
||||
// Rule: last one left.
|
||||
XCTAssertEqual(manager.selectedTab, tab0)
|
||||
}
|
||||
|
||||
func testDeleteLastTab() {
|
||||
let profile = TabManagerMockProfile()
|
||||
let manager = TabManager(prefs: profile.prefs, imageStore: nil)
|
||||
let delegate = MockTabManagerDelegate()
|
||||
|
||||
//create the tab before adding the mock delegate. So we don't have to check delegate calls we dont care about
|
||||
(0..<10).forEach {_ in manager.addTab() }
|
||||
manager.selectTab(manager.tabs.last)
|
||||
let deleteTab = manager.tabs.last
|
||||
let newSelectedTab = manager.tabs[8]
|
||||
manager.addDelegate(delegate)
|
||||
|
||||
let didSelect = MethodSpy(functionName: "tabManager(_:didSelectedTabChange:previous:)") { tabs in
|
||||
let next = tabs[0]!
|
||||
let previous = tabs[1]!
|
||||
XCTAssertEqual(deleteTab, previous)
|
||||
XCTAssertEqual(next, newSelectedTab)
|
||||
}
|
||||
delegate.expect([willRemove, didRemove, didSelect])
|
||||
manager.removeTab(manager.tabs.last!)
|
||||
|
||||
delegate.verify("Not all delegate methods were called")
|
||||
}
|
||||
|
||||
func testDelegatesCalledWhenRemovingPrivateTabs() {
|
||||
//setup
|
||||
let profile = TabManagerMockProfile()
|
||||
let delegate = MockTabManagerDelegate()
|
||||
let manager = TabManager(prefs: profile.prefs, imageStore: nil)
|
||||
profile.prefs.setBool(true, forKey: "settings.closePrivateTabs")
|
||||
|
||||
// create one private and one normal tab
|
||||
let tab = manager.addTab()
|
||||
let newTab = manager.addTab()
|
||||
manager.selectTab(tab)
|
||||
manager.selectTab(manager.addTab(isPrivate: true))
|
||||
manager.addDelegate(delegate)
|
||||
|
||||
// Double check a few things
|
||||
XCTAssertEqual(manager.selectedTab?.isPrivate, true, "The selected tab should be the private tab")
|
||||
XCTAssertEqual(manager.privateTabs.count, 1, "There should only be one private tab")
|
||||
|
||||
// switch to normal mode. Which should delete the private tabs
|
||||
manager.willSwitchTabMode()
|
||||
|
||||
//make sure tabs are cleared properly and indexes are reset
|
||||
XCTAssertEqual(manager.privateTabs.count, 0, "Private tab should have been deleted")
|
||||
XCTAssertEqual(manager.selectedIndex, -1, "The selected index should have been reset")
|
||||
|
||||
// didSelect should still be called when switching between a nil tab
|
||||
let didSelect = MethodSpy(functionName: "tabManager(_:didSelectedTabChange:previous:)") { tabs in
|
||||
XCTAssertNil(tabs[1], "there should be no previous tab")
|
||||
let next = tabs[0]!
|
||||
XCTAssertFalse(next.isPrivate)
|
||||
}
|
||||
|
||||
// make sure delegate method is actually called
|
||||
delegate.expect([didSelect])
|
||||
|
||||
// select the new tab to trigger the delegate methods
|
||||
manager.selectTab(newTab)
|
||||
|
||||
// check
|
||||
delegate.verify("Not all delegate methods were called")
|
||||
}
|
||||
|
||||
func testDeleteFirstTab() {
|
||||
let profile = TabManagerMockProfile()
|
||||
let manager = TabManager(prefs: profile.prefs, imageStore: nil)
|
||||
let delegate = MockTabManagerDelegate()
|
||||
|
||||
//create the tab before adding the mock delegate. So we don't have to check delegate calls we dont care about
|
||||
(0..<10).forEach {_ in manager.addTab() }
|
||||
manager.selectTab(manager.tabs.first)
|
||||
let deleteTab = manager.tabs.first
|
||||
let newSelectedTab = manager.tabs[1]
|
||||
manager.addDelegate(delegate)
|
||||
|
||||
let didSelect = MethodSpy(functionName: "tabManager(_:didSelectedTabChange:previous:)") { tabs in
|
||||
let next = tabs[0]!
|
||||
let previous = tabs[1]!
|
||||
XCTAssertEqual(deleteTab, previous)
|
||||
XCTAssertEqual(next, newSelectedTab)
|
||||
}
|
||||
delegate.expect([willRemove, didRemove, didSelect])
|
||||
manager.removeTab(manager.tabs.first!)
|
||||
delegate.verify("Not all delegate methods were called")
|
||||
}
|
||||
|
||||
// Private tabs and regular tabs are in the same tabs array.
|
||||
// Make sure that when a private tab is added inbetween regular tabs it isnt accidently selected when removing a regular tab
|
||||
func testTabsIndex() {
|
||||
let profile = TabManagerMockProfile()
|
||||
let manager = TabManager(prefs: profile.prefs, imageStore: nil)
|
||||
let delegate = MockTabManagerDelegate()
|
||||
|
||||
// We add 2 tabs. Then a private one before adding another normal tab and selecting it.
|
||||
// Make sure that when the last one is deleted we dont switch to the private tab
|
||||
manager.addTab()
|
||||
let newSelected = manager.addTab()
|
||||
manager.addTab(isPrivate: true)
|
||||
let deleted = manager.addTab()
|
||||
manager.selectTab(manager.tabs.last)
|
||||
manager.addDelegate(delegate)
|
||||
|
||||
let didSelect = MethodSpy(functionName: "tabManager(_:didSelectedTabChange:previous:)") { tabs in
|
||||
let next = tabs[0]!
|
||||
let previous = tabs[1]!
|
||||
XCTAssertEqual(deleted, previous)
|
||||
XCTAssertEqual(next, newSelected)
|
||||
}
|
||||
delegate.expect([willRemove, didRemove, didSelect])
|
||||
manager.removeTab(manager.tabs.last!)
|
||||
|
||||
delegate.verify("Not all delegate methods were called")
|
||||
}
|
||||
|
||||
func testTabsIndexClosingFirst() {
|
||||
let profile = TabManagerMockProfile()
|
||||
let manager = TabManager(prefs: profile.prefs, imageStore: nil)
|
||||
let delegate = MockTabManagerDelegate()
|
||||
|
||||
// We add 2 tabs. Then a private one before adding another normal tab and selecting the first.
|
||||
// Make sure that when the last one is deleted we dont switch to the private tab
|
||||
let deleted = manager.addTab()
|
||||
let newSelected = manager.addTab()
|
||||
manager.addTab(isPrivate: true)
|
||||
manager.addTab()
|
||||
manager.selectTab(manager.tabs.first)
|
||||
manager.addDelegate(delegate)
|
||||
|
||||
let didSelect = MethodSpy(functionName: "tabManager(_:didSelectedTabChange:previous:)") { tabs in
|
||||
let next = tabs[0]!
|
||||
let previous = tabs[1]!
|
||||
XCTAssertEqual(deleted, previous)
|
||||
XCTAssertEqual(next, newSelected)
|
||||
}
|
||||
delegate.expect([willRemove, didRemove, didSelect])
|
||||
manager.removeTab(manager.tabs.first!)
|
||||
delegate.verify("Not all delegate methods were called")
|
||||
}
|
||||
|
||||
}
|
||||
42
mobile/ios/ClientTests/TestBookmarks.swift
Normal file
42
mobile/ios/ClientTests/TestBookmarks.swift
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
@testable import Client
|
||||
import Foundation
|
||||
import Shared
|
||||
import Storage
|
||||
|
||||
import XCTest
|
||||
|
||||
class TestBookmarks: ProfileTest {
|
||||
func testBookmarks() {
|
||||
withTestProfile { profile -> Void in
|
||||
for i in 0...10 {
|
||||
let bookmark = ShareItem(url: "http://www.example.com/\(i)", title: "Example \(i)", favicon: nil)
|
||||
profile.bookmarks.shareItem(bookmark)
|
||||
}
|
||||
|
||||
let expectation = self.expectation(description: "asynchronous request")
|
||||
profile.bookmarks.modelFactory >>== {
|
||||
$0.modelForFolder(BookmarkRoots.MobileFolderGUID).upon { result in
|
||||
guard let model = result.successValue else {
|
||||
XCTFail("Should not have failed to get mock bookmarks.")
|
||||
expectation.fulfill()
|
||||
return
|
||||
}
|
||||
// 11 bookmarks plus our two suggested sites.
|
||||
XCTAssertEqual(model.current.count, 11, "We create \(model.current.count) stub bookmarks in the Mobile Bookmarks folder.")
|
||||
let bookmark = model.current[0]
|
||||
XCTAssertTrue(bookmark is BookmarkItem)
|
||||
XCTAssertTrue((bookmark as! BookmarkItem).url.hasPrefix("http://www.example.com/"), "Example URL found.")
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
self.waitForExpectations(timeout: 10.0, handler:nil)
|
||||
// This'll do.
|
||||
try! profile.files.remove("mock.db")
|
||||
}
|
||||
}
|
||||
}
|
||||
57
mobile/ios/ClientTests/TestFavicons.swift
Normal file
57
mobile/ios/ClientTests/TestFavicons.swift
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
import Storage
|
||||
import SDWebImage
|
||||
@testable import Client
|
||||
import Shared
|
||||
|
||||
class TestFavicons: ProfileTest {
|
||||
|
||||
fileprivate func addSite(_ favicons: Favicons, url: String, s: Bool = true) {
|
||||
let expectation = self.expectation(description: "Wait for history")
|
||||
let site = Site(url: url, title: "")
|
||||
let icon = Favicon(url: url + "/icon.png", type: IconType.icon)
|
||||
favicons.addFavicon(icon, forSite: site).upon {
|
||||
XCTAssertEqual($0.isSuccess, s, "Icon added \(url)")
|
||||
expectation.fulfill()
|
||||
}
|
||||
self.waitForExpectations(timeout: 100, handler: nil)
|
||||
}
|
||||
|
||||
func testFaviconFetcherParse() {
|
||||
let expectation = self.expectation(description: "Wait for Favicons to be fetched")
|
||||
|
||||
let profile = MockProfile()
|
||||
// I want a site that also has an iOS app so I can get "apple-touch-icon-precomposed" icons as well
|
||||
let url = URL(string: "https://instagram.com")
|
||||
FaviconFetcher.getForURL(url!, profile: profile).uponQueue(DispatchQueue.main) { result in
|
||||
guard let favicons = result.successValue, favicons.count > 0, let url = favicons.first?.url.asURL else {
|
||||
XCTFail("Favicons were not found.")
|
||||
return expectation.fulfill()
|
||||
}
|
||||
XCTAssertGreaterThan(favicons.count, 1, "Instagram should have more than one Favicon.")
|
||||
SDWebImageManager.shared().loadImage(with: url, options: .retryFailed, progress: nil, completed: { (img, _, _, _, _, _) in
|
||||
guard let image = img else {
|
||||
XCTFail("Not a valid URL provided for a favicon.")
|
||||
return expectation.fulfill()
|
||||
}
|
||||
XCTAssertNotEqual(image.size, CGSize(width: 0, height: 0))
|
||||
expectation.fulfill()
|
||||
})
|
||||
|
||||
}
|
||||
self.waitForExpectations(timeout: 3000, handler: nil)
|
||||
}
|
||||
|
||||
func testDefaultFavicons() {
|
||||
let icon = FaviconFetcher.getDefaultIconForURL(url: URL(string: "http://www.google.de")!)
|
||||
XCTAssertNotNil(icon)
|
||||
let craigsListIcon = FaviconFetcher.getDefaultIconForURL(url: URL(string: "http://vancouver.craigslist.ca")!)
|
||||
XCTAssertNotNil(craigsListIcon)
|
||||
|
||||
}
|
||||
}
|
||||
19
mobile/ios/ClientTests/TestHashExtensions.swift
Normal file
19
mobile/ios/ClientTests/TestHashExtensions.swift
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
class TestHashExtensions: XCTestCase {
|
||||
func testSha1() {
|
||||
XCTAssertEqual("test1test2".sha1.hexEncodedString, "dff964f6e3c1761b6288f5c75c319d36fb09b2b9")
|
||||
XCTAssertEqual("test2test3".sha1.hexEncodedString, "66cdfcbbf4ad73f40ae06140460ff9bb0aabaf5c")
|
||||
}
|
||||
|
||||
func testSha256() {
|
||||
let data1: Data = "4f980b6f9baa6965f760d0bf2b2ccbee483032e5df01d77bbd9e25f7517a06b9".hexDecodedData
|
||||
XCTAssertEqual("test1test2".sha256, data1)
|
||||
XCTAssertEqual("test2test3".sha256, "fc3ea28dc1801e4180cec1022b55bee7795cf3c9fd430fb5237c9d8054218e81".hexDecodedData)
|
||||
}
|
||||
}
|
||||
219
mobile/ios/ClientTests/TestHistory.swift
Normal file
219
mobile/ios/ClientTests/TestHistory.swift
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
@testable import Client
|
||||
import Foundation
|
||||
import Storage
|
||||
|
||||
import XCTest
|
||||
|
||||
class TestHistory: ProfileTest {
|
||||
fileprivate func addSite(_ history: BrowserHistory, url: String, title: String, s: Bool = true) {
|
||||
let site = Site(url: url, title: title)
|
||||
let visit = SiteVisit(site: site, date: Date.nowMicroseconds())
|
||||
XCTAssertEqual(s, history.addLocalVisit(visit).value.isSuccess, "Site added: \(url).")
|
||||
}
|
||||
|
||||
fileprivate func innerCheckSites(_ history: BrowserHistory, callback: @escaping (_ cursor: Cursor<Site>) -> Void) {
|
||||
// Retrieve the entry
|
||||
history.getSitesByLastVisit(100).upon {
|
||||
XCTAssertTrue($0.isSuccess)
|
||||
callback($0.successValue!)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func checkSites(_ history: BrowserHistory, urls: [String: String], s: Bool = true) {
|
||||
// Retrieve the entry.
|
||||
if let cursor = history.getSitesByLastVisit(100).value.successValue {
|
||||
XCTAssertEqual(cursor.status, CursorStatus.success, "Returned success \(cursor.statusMessage).")
|
||||
XCTAssertEqual(cursor.count, urls.count, "Cursor has \(urls.count) entries.")
|
||||
|
||||
for index in 0..<cursor.count {
|
||||
let s = cursor[index]!
|
||||
XCTAssertNotNil(s, "Cursor has a site for entry.")
|
||||
let title = urls[s.url]
|
||||
XCTAssertNotNil(title, "Found right URL.")
|
||||
XCTAssertEqual(s.title, title!, "Found right title.")
|
||||
}
|
||||
} else {
|
||||
XCTFail("Couldn't get cursor.")
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func clear(_ history: BrowserHistory) {
|
||||
XCTAssertTrue(history.clearHistory().value.isSuccess, "History cleared.")
|
||||
}
|
||||
|
||||
fileprivate func checkVisits(_ history: BrowserHistory, url: String) {
|
||||
let expectation = self.expectation(description: "Wait for history")
|
||||
history.getSitesByLastVisit(100).upon { result in
|
||||
XCTAssertTrue(result.isSuccess)
|
||||
history.getFrecentHistory().getSites(whereURLContains: url, historyLimit: 100, bookmarksLimit: 0).upon { result in
|
||||
XCTAssertTrue(result.isSuccess)
|
||||
let cursor = result.successValue!
|
||||
XCTAssertEqual(cursor.status, CursorStatus.success, "returned success \(cursor.statusMessage)")
|
||||
// XXX - We don't allow querying much info about visits here anymore, so there isn't a lot to do
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
self.waitForExpectations(timeout: 100, handler: nil)
|
||||
}
|
||||
|
||||
// This is a very basic test. Adds an entry. Retrieves it, and then clears the database
|
||||
func testHistory() {
|
||||
withTestProfile { profile -> Void in
|
||||
let h = profile.history
|
||||
self.addSite(h, url: "http://url1/", title: "title")
|
||||
self.addSite(h, url: "http://url1/", title: "title")
|
||||
self.addSite(h, url: "http://url1/", title: "title 2")
|
||||
self.addSite(h, url: "https://url2/", title: "title")
|
||||
self.addSite(h, url: "https://url2/", title: "title")
|
||||
self.checkSites(h, urls: ["http://url1/": "title 2", "https://url2/": "title"])
|
||||
self.checkVisits(h, url: "http://url1/")
|
||||
self.checkVisits(h, url: "https://url2/")
|
||||
self.clear(h)
|
||||
}
|
||||
}
|
||||
|
||||
func testAboutUrls() {
|
||||
withTestProfile { (profile) -> Void in
|
||||
let h = profile.history
|
||||
self.addSite(h, url: "about:home", title: "About Home", s: false)
|
||||
self.clear(h)
|
||||
}
|
||||
}
|
||||
|
||||
let NumThreads = 5
|
||||
let NumCmds = 10
|
||||
|
||||
func testInsertPerformance() {
|
||||
withTestProfile { profile -> Void in
|
||||
let h = profile.history
|
||||
var j = 0
|
||||
|
||||
self.measure({ () -> Void in
|
||||
for _ in 0...self.NumCmds {
|
||||
self.addSite(h, url: "https://someurl\(j).com/", title: "title \(j)")
|
||||
j += 1
|
||||
}
|
||||
self.clear(h)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testGetPerformance() {
|
||||
withTestProfile { profile -> Void in
|
||||
let h = profile.history
|
||||
var j = 0
|
||||
var urls = [String: String]()
|
||||
|
||||
self.clear(h)
|
||||
for _ in 0...self.NumCmds {
|
||||
self.addSite(h, url: "https://someurl\(j).com/", title: "title \(j)")
|
||||
urls["https://someurl\(j).com/"] = "title \(j)"
|
||||
j += 1
|
||||
}
|
||||
|
||||
self.measure({ () -> Void in
|
||||
self.checkSites(h, urls: urls)
|
||||
return
|
||||
})
|
||||
|
||||
self.clear(h)
|
||||
}
|
||||
}
|
||||
|
||||
// Fuzzing tests. These fire random insert/query/clear commands into the history database from threads. The don't check
|
||||
// the results. Just look for crashes.
|
||||
func testRandomThreading() {
|
||||
withTestProfile { profile -> Void in
|
||||
let queue = DispatchQueue(label: "My Queue", qos: DispatchQoS.default, attributes: DispatchQueue.Attributes.concurrent, autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency.inherit, target: nil)
|
||||
var counter = 0
|
||||
|
||||
let expectation = self.expectation(description: "Wait for history")
|
||||
for _ in 0..<self.NumThreads {
|
||||
var history = profile.history as BrowserHistory
|
||||
self.runRandom(&history, queue: queue, cb: { () -> Void in
|
||||
counter += 1
|
||||
if counter == self.NumThreads {
|
||||
self.clear(history)
|
||||
expectation.fulfill()
|
||||
}
|
||||
})
|
||||
}
|
||||
self.waitForExpectations(timeout: 10, handler: nil)
|
||||
}
|
||||
}
|
||||
|
||||
// Same as testRandomThreading, but uses one history connection for all threads
|
||||
func testRandomThreading2() {
|
||||
withTestProfile { profile -> Void in
|
||||
let queue = DispatchQueue(label: "My Queue", qos: DispatchQoS.default, attributes: DispatchQueue.Attributes.concurrent, autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency.inherit, target: nil)
|
||||
var history = profile.history as BrowserHistory
|
||||
var counter = 0
|
||||
|
||||
let expectation = self.expectation(description: "Wait for history")
|
||||
for _ in 0..<self.NumThreads {
|
||||
self.runRandom(&history, queue: queue, cb: { () -> Void in
|
||||
counter += 1
|
||||
if counter == self.NumThreads {
|
||||
self.clear(history)
|
||||
expectation.fulfill()
|
||||
}
|
||||
})
|
||||
}
|
||||
self.waitForExpectations(timeout: 10, handler: nil)
|
||||
}
|
||||
}
|
||||
|
||||
// Runs a random command on a database. Calls cb when finished.
|
||||
fileprivate func runRandom(_ history: inout BrowserHistory, cmdIn: Int, cb: @escaping () -> Void) {
|
||||
var cmd = cmdIn
|
||||
if cmd < 0 {
|
||||
cmd = Int(arc4random() % 5)
|
||||
}
|
||||
|
||||
switch cmd {
|
||||
case 0...1:
|
||||
let url = "https://randomurl.com/\(arc4random() % 100)"
|
||||
let title = "title \(arc4random() % 100)"
|
||||
addSite(history, url: url, title: title)
|
||||
cb()
|
||||
case 2...3:
|
||||
innerCheckSites(history) { cursor in
|
||||
for site in cursor {
|
||||
_ = site!
|
||||
}
|
||||
}
|
||||
cb()
|
||||
default:
|
||||
history.clearHistory().upon() { success in cb() }
|
||||
}
|
||||
}
|
||||
|
||||
// Calls numCmds random methods on this database. val is a counter used by this interally (i.e. always pass zero for it).
|
||||
// Calls cb when finished.
|
||||
fileprivate func runMultiRandom(_ history: inout BrowserHistory, val: Int, numCmds: Int, cb: @escaping () -> Void) {
|
||||
if val == numCmds {
|
||||
cb()
|
||||
return
|
||||
} else {
|
||||
runRandom(&history, cmdIn: -1) { [history]_ in
|
||||
var history = history
|
||||
self.runMultiRandom(&history, val: val+1, numCmds: numCmds, cb: cb)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper for starting a new thread running NumCmds random methods on it. Calls cb when done.
|
||||
fileprivate func runRandom(_ history: inout BrowserHistory, queue: DispatchQueue, cb: @escaping () -> Void) {
|
||||
queue.async { [history] in
|
||||
var history = history
|
||||
// Each thread creates its own history provider
|
||||
self.runMultiRandom(&history, val: 0, numCmds: self.NumCmds) { _ in
|
||||
DispatchQueue.main.async(execute: cb)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
94
mobile/ios/ClientTests/UIImageViewExtensionsTests.swift
Normal file
94
mobile/ios/ClientTests/UIImageViewExtensionsTests.swift
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import XCTest
|
||||
import Storage
|
||||
import SDWebImage
|
||||
import GCDWebServers
|
||||
import Shared
|
||||
|
||||
@testable import Client
|
||||
|
||||
class UIImageViewExtensionsTests: XCTestCase {
|
||||
|
||||
override func setUp() {
|
||||
SDWebImageDownloader.shared().urlCredential = WebServer.sharedInstance.credentials
|
||||
}
|
||||
|
||||
func testsetIcon() {
|
||||
let url = URL(string: "http://mozilla.com")
|
||||
let imageView = UIImageView()
|
||||
|
||||
let goodIcon = FaviconFetcher.getDefaultFavicon(url!)
|
||||
let correctColor = FaviconFetcher.getDefaultColor(url!)
|
||||
imageView.setIcon(nil, forURL: url)
|
||||
XCTAssertEqual(imageView.image!, goodIcon, "The correct default favicon should be applied")
|
||||
XCTAssertEqual(imageView.backgroundColor, correctColor, "The correct default color should be applied")
|
||||
|
||||
imageView.setIcon(nil, forURL: URL(string: "http://mozilla.com/blahblah"))
|
||||
XCTAssertEqual(imageView.image!, goodIcon, "The same icon should be applied to all urls with the same domain")
|
||||
|
||||
imageView.setIcon(nil, forURL: URL(string: "b"))
|
||||
XCTAssertEqual(imageView.image, FaviconFetcher.defaultFavicon, "The default favicon should be applied when no information is given about the icon")
|
||||
}
|
||||
|
||||
func testAsyncSetIcon() {
|
||||
let originalImage = UIImage(named: "fxLogo")!
|
||||
|
||||
WebServer.sharedInstance.registerHandlerForMethod("GET", module: "favicon", resource: "icon") { (request) -> GCDWebServerResponse! in
|
||||
return GCDWebServerDataResponse(data: UIImagePNGRepresentation(originalImage), contentType: "image/png")
|
||||
}
|
||||
|
||||
let favImageView = UIImageView()
|
||||
favImageView.setIcon(Favicon(url: "http://localhost:6571/favicon/icon", type: .guess), forURL: URL(string: "http://localhost:6571"))
|
||||
|
||||
let expect = expectation(description: "UIImageView async load")
|
||||
let time = Int64(2 * Double(NSEC_PER_SEC))
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(time) / Double(NSEC_PER_SEC)) {
|
||||
XCTAssert(originalImage.isStrictlyEqual(to: favImageView.image!), "The correct favicon should be applied to the UIImageView")
|
||||
expect.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 5, handler: nil)
|
||||
}
|
||||
|
||||
func testAsyncSetIconFail() {
|
||||
let favImageView = UIImageView()
|
||||
|
||||
let gFavURL = URL(string: "https://www.nofavicon.com/noicon.ico")
|
||||
let gURL = URL(string: "http://nofavicon.com")
|
||||
let correctImage = FaviconFetcher.getDefaultFavicon(gURL!)
|
||||
|
||||
favImageView.setIcon(Favicon(url: gFavURL!.absoluteString, type: .guess), forURL: gURL)
|
||||
|
||||
let expect = expectation(description: "UIImageView async load")
|
||||
let time = Int64(2 * Double(NSEC_PER_SEC))
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(time) / Double(NSEC_PER_SEC)) {
|
||||
XCTAssert(correctImage.isStrictlyEqual(to: favImageView.image!), "The correct default favicon should be applied to the UIImageView")
|
||||
expect.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 5, handler: nil)
|
||||
}
|
||||
|
||||
func testDefaultIcons() {
|
||||
let favImageView = UIImageView()
|
||||
|
||||
let gFavURL = URL(string: "https://www.facebook.com/fav") //This will be fetched from tippy top sites
|
||||
let gURL = URL(string: "http://www.facebook.com")!
|
||||
let defaultItem = FaviconFetcher.defaultIcons[gURL.baseDomain!]!
|
||||
let correctImage = UIImage(contentsOfFile: defaultItem.url)!
|
||||
|
||||
favImageView.setIcon(Favicon(url: gFavURL!.absoluteString, type: .guess), forURL: gURL)
|
||||
|
||||
let expect = expectation(description: "UIImageView async load")
|
||||
let time = Int64(2 * Double(NSEC_PER_SEC))
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(time) / Double(NSEC_PER_SEC)) {
|
||||
XCTAssertEqual(favImageView.backgroundColor, defaultItem.color)
|
||||
XCTAssert(correctImage.isStrictlyEqual(to: favImageView.image!), "The correct default favicon should be applied to the UIImageView")
|
||||
expect.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 5, handler: nil)
|
||||
}
|
||||
}
|
||||
47
mobile/ios/ClientTests/UIPasteboardExtensionsTests.swift
Normal file
47
mobile/ios/ClientTests/UIPasteboardExtensionsTests.swift
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import MobileCoreServices
|
||||
import UIKit
|
||||
import XCTest
|
||||
|
||||
class UIPasteboardExtensionsTests: XCTestCase {
|
||||
|
||||
fileprivate var pasteboard: UIPasteboard!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
pasteboard = UIPasteboard.withUniqueName()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
UIPasteboard.remove(withName: pasteboard.name)
|
||||
}
|
||||
|
||||
func testAddPNGImage() {
|
||||
let path = Bundle(for: self.classForCoder).path(forResource: "image", ofType: "png")!
|
||||
let data = try! Data(contentsOf: URL(fileURLWithPath: path))
|
||||
let url = URL(string: "http://foo.bar")!
|
||||
pasteboard.addImageWithData(data, forURL: url)
|
||||
verifyPasteboard(expectedURL: url, expectedImageTypeKey: kUTTypePNG)
|
||||
}
|
||||
|
||||
func testAddGIFImage() {
|
||||
let path = Bundle(for: self.classForCoder).path(forResource: "image", ofType: "gif")!
|
||||
let data = try! Data(contentsOf: URL(fileURLWithPath: path))
|
||||
let url = URL(string: "http://foo.bar")!
|
||||
pasteboard.addImageWithData(data, forURL: url)
|
||||
verifyPasteboard(expectedURL: url, expectedImageTypeKey: kUTTypeGIF)
|
||||
}
|
||||
|
||||
fileprivate func verifyPasteboard(expectedURL: URL, expectedImageTypeKey: CFString) {
|
||||
XCTAssertEqual(pasteboard.items.count, 1)
|
||||
XCTAssertEqual(pasteboard.items[0].count, 2)
|
||||
XCTAssertEqual(pasteboard.items[0][kUTTypeURL as String] as? URL, expectedURL)
|
||||
XCTAssertNotNil(pasteboard.items[0][expectedImageTypeKey as String])
|
||||
}
|
||||
|
||||
}
|
||||
49
mobile/ios/ClientTests/WebServerTests.swift
Normal file
49
mobile/ios/ClientTests/WebServerTests.swift
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import UIKit
|
||||
import GCDWebServers
|
||||
import XCTest
|
||||
|
||||
/// Minimal web server tests. This class can be used as a base class for tests that need a real web server.
|
||||
/// Simply add additional handlers your test class' setUp() method.
|
||||
class WebServerTests: XCTestCase {
|
||||
let webServer: GCDWebServer = GCDWebServer()
|
||||
var webServerBase: String!
|
||||
|
||||
/// Setup a basic web server that binds to a random port and that has one default handler on /hello
|
||||
fileprivate func setupWebServer() {
|
||||
webServer.addHandler(forMethod: "GET", path: "/hello", request: GCDWebServerRequest.self) { (request) -> GCDWebServerResponse! in
|
||||
return GCDWebServerDataResponse(html: "<html><body><p>Hello World</p></body></html>")
|
||||
}
|
||||
if webServer.start(withPort: 0, bonjourName: nil) == false {
|
||||
XCTFail("Can't start the GCDWebServer")
|
||||
}
|
||||
webServerBase = "http://localhost:\(webServer.port)"
|
||||
}
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
setupWebServer()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testWebServerIsRunning() {
|
||||
XCTAssertTrue(webServer.isRunning)
|
||||
}
|
||||
|
||||
func testWebServerIsServingRequests() {
|
||||
let response: NSString?
|
||||
do {
|
||||
response = try NSString(contentsOf: URL(string: "\(webServerBase!)/hello")!, encoding: String.Encoding.utf8.rawValue)
|
||||
} catch _ {
|
||||
response = nil
|
||||
}
|
||||
XCTAssertNotNil(response)
|
||||
XCTAssertTrue(response == "<html><body><p>Hello World</p></body></html>")
|
||||
}
|
||||
}
|
||||
30
mobile/ios/ClientTests/XCTestCaseExtensions.swift
Normal file
30
mobile/ios/ClientTests/XCTestCaseExtensions.swift
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
extension XCTestCase {
|
||||
func wait(_ time: TimeInterval) {
|
||||
let expectation = self.expectation(description: "Wait")
|
||||
let delayTime = DispatchTime.now() + Double(Int64(time * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)
|
||||
DispatchQueue.main.asyncAfter(deadline: delayTime) {
|
||||
expectation.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: time + 1, handler: nil)
|
||||
}
|
||||
|
||||
func waitForCondition(timeout: TimeInterval = 10, condition: () -> Bool) {
|
||||
let timeoutTime = Date.timeIntervalSinceReferenceDate + timeout
|
||||
|
||||
while !condition() {
|
||||
if Date.timeIntervalSinceReferenceDate > timeoutTime {
|
||||
XCTFail("Condition timed out")
|
||||
return
|
||||
}
|
||||
|
||||
wait(0.1)
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
mobile/ios/ClientTests/image.gif
Normal file
BIN
mobile/ios/ClientTests/image.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 B |
BIN
mobile/ios/ClientTests/image.png
Normal file
BIN
mobile/ios/ClientTests/image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 150 B |
24
mobile/ios/ClientTests/pocketglobalfeed.json
Normal file
24
mobile/ios/ClientTests/pocketglobalfeed.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"status": 1,
|
||||
"list": [{
|
||||
"id": 2092,
|
||||
"url": "https:\/\/pocket.co\/xMnD5u",
|
||||
"dedupe_url": "https:\/\/www.wired.com\/story\/turn-off-your-push-notifications\/",
|
||||
"title": "Turn Off Your Push Notifications. All of Them",
|
||||
"excerpt": "Push notifications are ruining my life. Yours too, I bet. Download more than a few apps and the notifications become a non-stop, cacophonous waterfall of nonsense. Here's just part of an afternoon on my phone:",
|
||||
"domain": "wired.com",
|
||||
"image_src": "https:\/\/d33ypg4xwx0n86.cloudfront.net\/direct?url=https%3A%2F%2Fmedia.wired.com%2Fphotos%2F597267fd023c38366e1ae497%2Fmaster%2Fw_1200%2Cc_limit%2Fno_notifications-TA.gif&resize=w450",
|
||||
"published_timestamp": "1332306000",
|
||||
"sort_id": 0
|
||||
}, {
|
||||
"id": 2091,
|
||||
"url": "https:\/\/pocket.co\/sMnD5m",
|
||||
"dedupe_url": "http:\/\/www.latimes.com\/business\/la-fi-agenda-best-buy-20170717-htmlstory.html",
|
||||
"title": "Why the grim reaper of retail hasn't come to claim Best Buy",
|
||||
"excerpt": "Five years ago Best Buy Co. looked like a retail dinosaur, another victim of e-commerce juggernaut Amazon.com and other online sellers.",
|
||||
"domain": "latimes.com",
|
||||
"image_src": "https:\/\/d33ypg4xwx0n86.cloudfront.net\/direct?url=http%3A%2F%2Fwww.trbimg.com%2Fimg-547cc080%2Fturbine%2Fla-fi-mh-radio-shack-20141201-004%2F&resize=w450",
|
||||
"published_timestamp": "1500267600",
|
||||
"sort_id": 1
|
||||
}]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue