Add tests for 1.1.0 features
This commit is contained in:
@@ -17,7 +17,11 @@ struct BackupRecord: Identifiable {
|
|||||||
class BackupService {
|
class BackupService {
|
||||||
static let shared = BackupService()
|
static let shared = BackupService()
|
||||||
|
|
||||||
private let fileManager = FileManager.default
|
private let fileManager: FileManager
|
||||||
|
private let dateProvider: () -> Date
|
||||||
|
private let localBaseDirectoryProvider: () -> URL?
|
||||||
|
private let iCloudBaseDirectoryProvider: () -> URL?
|
||||||
|
private let exportProvider: () -> String
|
||||||
private let dateFormatter: DateFormatter = {
|
private let dateFormatter: DateFormatter = {
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.dateFormat = "yyyyMMdd-HHmmss"
|
formatter.dateFormat = "yyyyMMdd-HHmmss"
|
||||||
@@ -25,16 +29,42 @@ class BackupService {
|
|||||||
return formatter
|
return formatter
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private init() {}
|
private init() {
|
||||||
|
fileManager = .default
|
||||||
|
dateProvider = Date.init
|
||||||
|
localBaseDirectoryProvider = { [fileManager] in
|
||||||
|
fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
|
||||||
|
}
|
||||||
|
iCloudBaseDirectoryProvider = { [fileManager] in
|
||||||
|
fileManager.url(forUbiquityContainerIdentifier: nil)
|
||||||
|
}
|
||||||
|
exportProvider = {
|
||||||
|
let context = CoreDataStack.shared.viewContext
|
||||||
|
let sources = BackupService.fetchAllSources(in: context)
|
||||||
|
let categories = BackupService.fetchAllCategories(in: context)
|
||||||
|
return ExportService.shared.exportToJSON(sources: sources, categories: categories)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(
|
||||||
|
fileManager: FileManager = .default,
|
||||||
|
dateProvider: @escaping () -> Date,
|
||||||
|
localBaseDirectoryProvider: @escaping () -> URL?,
|
||||||
|
iCloudBaseDirectoryProvider: @escaping () -> URL?,
|
||||||
|
exportProvider: @escaping () -> String
|
||||||
|
) {
|
||||||
|
self.fileManager = fileManager
|
||||||
|
self.dateProvider = dateProvider
|
||||||
|
self.localBaseDirectoryProvider = localBaseDirectoryProvider
|
||||||
|
self.iCloudBaseDirectoryProvider = iCloudBaseDirectoryProvider
|
||||||
|
self.exportProvider = exportProvider
|
||||||
|
}
|
||||||
|
|
||||||
func createBackup(retentionCount: Int, includeICloud: Bool) -> [BackupRecord] {
|
func createBackup(retentionCount: Int, includeICloud: Bool) -> [BackupRecord] {
|
||||||
let timestamp = dateFormatter.string(from: Date())
|
let timestamp = dateFormatter.string(from: dateProvider())
|
||||||
let fileName = "backup-\(timestamp).json"
|
let fileName = "backup-\(timestamp).json"
|
||||||
|
|
||||||
let context = CoreDataStack.shared.viewContext
|
let content = exportProvider()
|
||||||
let sources = fetchAllSources(in: context)
|
|
||||||
let categories = fetchAllCategories(in: context)
|
|
||||||
let content = ExportService.shared.exportToJSON(sources: sources, categories: categories)
|
|
||||||
|
|
||||||
var records: [BackupRecord] = []
|
var records: [BackupRecord] = []
|
||||||
|
|
||||||
@@ -67,7 +97,7 @@ class BackupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func backupDirectory() -> URL? {
|
private func backupDirectory() -> URL? {
|
||||||
guard let base = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
|
guard let base = localBaseDirectoryProvider() else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let dir = base.appendingPathComponent("Backups", isDirectory: true)
|
let dir = base.appendingPathComponent("Backups", isDirectory: true)
|
||||||
@@ -76,7 +106,7 @@ class BackupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func iCloudBackupDirectory() -> URL? {
|
private func iCloudBackupDirectory() -> URL? {
|
||||||
guard let base = fileManager.url(forUbiquityContainerIdentifier: nil) else { return nil }
|
guard let base = iCloudBaseDirectoryProvider() else { return nil }
|
||||||
let dir = base.appendingPathComponent("Documents/Backups", isDirectory: true)
|
let dir = base.appendingPathComponent("Documents/Backups", isDirectory: true)
|
||||||
ensureDirectoryExists(dir)
|
ensureDirectoryExists(dir)
|
||||||
return dir
|
return dir
|
||||||
@@ -137,13 +167,13 @@ class BackupService {
|
|||||||
return dateFormatter.date(from: dateString)
|
return dateFormatter.date(from: dateString)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fetchAllSources(in context: NSManagedObjectContext) -> [InvestmentSource] {
|
private static func fetchAllSources(in context: NSManagedObjectContext) -> [InvestmentSource] {
|
||||||
let request: NSFetchRequest<InvestmentSource> = InvestmentSource.fetchRequest()
|
let request: NSFetchRequest<InvestmentSource> = InvestmentSource.fetchRequest()
|
||||||
request.sortDescriptors = [NSSortDescriptor(keyPath: \InvestmentSource.name, ascending: true)]
|
request.sortDescriptors = [NSSortDescriptor(keyPath: \InvestmentSource.name, ascending: true)]
|
||||||
return (try? context.fetch(request)) ?? []
|
return (try? context.fetch(request)) ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fetchAllCategories(in context: NSManagedObjectContext) -> [Category] {
|
private static func fetchAllCategories(in context: NSManagedObjectContext) -> [Category] {
|
||||||
let request: NSFetchRequest<Category> = Category.fetchRequest()
|
let request: NSFetchRequest<Category> = Category.fetchRequest()
|
||||||
request.sortDescriptors = [NSSortDescriptor(keyPath: \Category.name, ascending: true)]
|
request.sortDescriptors = [NSSortDescriptor(keyPath: \Category.name, ascending: true)]
|
||||||
return (try? context.fetch(request)) ?? []
|
return (try? context.fetch(request)) ?? []
|
||||||
|
|||||||
@@ -49,6 +49,13 @@ class IAPService: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
func setPremiumForTesting(_ value: Bool, familyShared: Bool = false) {
|
||||||
|
isPremium = value
|
||||||
|
isFamilyShared = familyShared
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
updateListenerTask?.cancel()
|
updateListenerTask?.cancel()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,38 +8,59 @@ final class ReviewPromptService {
|
|||||||
private let checkInCountKey = "reviewPromptCheckinCount"
|
private let checkInCountKey = "reviewPromptCheckinCount"
|
||||||
private let minCheckInsBetweenPrompts = 3
|
private let minCheckInsBetweenPrompts = 3
|
||||||
private let minDaysBetweenPrompts = 90
|
private let minDaysBetweenPrompts = 90
|
||||||
|
private let userDefaults: UserDefaults
|
||||||
|
private let dateProvider: () -> Date
|
||||||
|
private let reviewRequestHandler: () -> Void
|
||||||
|
|
||||||
private init() {}
|
private init() {
|
||||||
|
userDefaults = .standard
|
||||||
|
dateProvider = Date.init
|
||||||
|
reviewRequestHandler = ReviewPromptService.defaultReviewRequestHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
init(
|
||||||
|
userDefaults: UserDefaults,
|
||||||
|
dateProvider: @escaping () -> Date,
|
||||||
|
reviewRequestHandler: @escaping () -> Void
|
||||||
|
) {
|
||||||
|
self.userDefaults = userDefaults
|
||||||
|
self.dateProvider = dateProvider
|
||||||
|
self.reviewRequestHandler = reviewRequestHandler
|
||||||
|
}
|
||||||
|
|
||||||
func recordMonthlyCheckInCompleted() {
|
func recordMonthlyCheckInCompleted() {
|
||||||
let currentCount = UserDefaults.standard.integer(forKey: checkInCountKey)
|
let currentCount = userDefaults.integer(forKey: checkInCountKey)
|
||||||
UserDefaults.standard.set(currentCount + 1, forKey: checkInCountKey)
|
userDefaults.set(currentCount + 1, forKey: checkInCountKey)
|
||||||
requestReviewIfEligible()
|
requestReviewIfEligible()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func requestReviewIfEligible() {
|
private func requestReviewIfEligible() {
|
||||||
let now = Date()
|
let now = dateProvider()
|
||||||
if let lastPrompt = UserDefaults.standard.object(forKey: lastPromptKey) as? Date {
|
if let lastPrompt = userDefaults.object(forKey: lastPromptKey) as? Date {
|
||||||
let daysSince = now.timeIntervalSince(lastPrompt) / 86_400
|
let daysSince = now.timeIntervalSince(lastPrompt) / 86_400
|
||||||
if daysSince < Double(minDaysBetweenPrompts) {
|
if daysSince < Double(minDaysBetweenPrompts) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let count = UserDefaults.standard.integer(forKey: checkInCountKey)
|
let count = userDefaults.integer(forKey: checkInCountKey)
|
||||||
guard count >= minCheckInsBetweenPrompts else { return }
|
guard count >= minCheckInsBetweenPrompts else { return }
|
||||||
|
|
||||||
requestReview()
|
requestReview()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func requestReview() {
|
private func requestReview() {
|
||||||
|
reviewRequestHandler()
|
||||||
|
userDefaults.set(dateProvider(), forKey: lastPromptKey)
|
||||||
|
userDefaults.set(0, forKey: checkInCountKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func defaultReviewRequestHandler() {
|
||||||
guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return }
|
guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return }
|
||||||
if #available(iOS 18.0, *) {
|
if #available(iOS 18.0, *) {
|
||||||
AppStore.requestReview(in: scene)
|
AppStore.requestReview(in: scene)
|
||||||
} else {
|
} else {
|
||||||
SKStoreReviewController.requestReview(in: scene)
|
SKStoreReviewController.requestReview(in: scene)
|
||||||
}
|
}
|
||||||
UserDefaults.standard.set(Date(), forKey: lastPromptKey)
|
|
||||||
UserDefaults.standard.set(0, forKey: checkInCountKey)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,18 @@ class ShareService {
|
|||||||
|
|
||||||
private init() {}
|
private init() {}
|
||||||
|
|
||||||
|
static func buildMonthlyCheckInShareText(summary: MonthlySummary, appName: String) -> String {
|
||||||
|
"""
|
||||||
|
\(summary.periodLabel) Check-in
|
||||||
|
Starting: \(summary.formattedStartingValue)
|
||||||
|
Ending: \(summary.formattedEndingValue)
|
||||||
|
Contributions: \(summary.formattedContributions)
|
||||||
|
Net performance: \(summary.formattedNetPerformance) (\(summary.formattedNetPerformancePercentage))
|
||||||
|
|
||||||
|
Shared from \(appName)
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
func shareText(_ content: String) {
|
func shareText(_ content: String) {
|
||||||
guard let viewController = ShareService.topViewController() else { return }
|
guard let viewController = ShareService.topViewController() else { return }
|
||||||
|
|
||||||
|
|||||||
@@ -457,15 +457,10 @@ struct MonthlyCheckInView: View {
|
|||||||
|
|
||||||
private func shareMonthlyCheckIn() {
|
private func shareMonthlyCheckIn() {
|
||||||
let summary = viewModel.monthlySummary
|
let summary = viewModel.monthlySummary
|
||||||
let text = """
|
let text = ShareService.buildMonthlyCheckInShareText(
|
||||||
\(summary.periodLabel) Check-in
|
summary: summary,
|
||||||
Starting: \(summary.formattedStartingValue)
|
appName: appDisplayName
|
||||||
Ending: \(summary.formattedEndingValue)
|
)
|
||||||
Contributions: \(summary.formattedContributions)
|
|
||||||
Net performance: \(summary.formattedNetPerformance) (\(summary.formattedNetPerformancePercentage))
|
|
||||||
|
|
||||||
Shared from \(appDisplayName)
|
|
||||||
"""
|
|
||||||
ShareService.shared.shareText(text)
|
ShareService.shared.shareText(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -317,9 +317,13 @@ struct OnboardingQuickStartView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func openAppSettings() {
|
private func openAppSettings() {
|
||||||
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
|
guard let url = Self.appSettingsURL() else { return }
|
||||||
UIApplication.shared.open(url)
|
UIApplication.shared.open(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func appSettingsURL() -> URL? {
|
||||||
|
URL(string: UIApplication.openSettingsURLString)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - First Launch Welcome
|
// MARK: - First Launch Welcome
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import XCTest
|
||||||
|
@testable import PortfolioJournal
|
||||||
|
|
||||||
|
final class BackupServiceTests: XCTestCase {
|
||||||
|
private var tempRoot: URL!
|
||||||
|
|
||||||
|
override func setUp() {
|
||||||
|
super.setUp()
|
||||||
|
tempRoot = FileManager.default.temporaryDirectory
|
||||||
|
.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
||||||
|
try? FileManager.default.createDirectory(at: tempRoot, withIntermediateDirectories: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tearDown() {
|
||||||
|
if let tempRoot {
|
||||||
|
try? FileManager.default.removeItem(at: tempRoot)
|
||||||
|
}
|
||||||
|
super.tearDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCreateBackupPrunesToRetentionCount() {
|
||||||
|
let localBase = tempRoot.appendingPathComponent("Local", isDirectory: true)
|
||||||
|
try? FileManager.default.createDirectory(at: localBase, withIntermediateDirectories: true)
|
||||||
|
|
||||||
|
var dates = [
|
||||||
|
Date(timeIntervalSince1970: 100),
|
||||||
|
Date(timeIntervalSince1970: 200)
|
||||||
|
]
|
||||||
|
|
||||||
|
let service = BackupService(
|
||||||
|
fileManager: .default,
|
||||||
|
dateProvider: { dates.removeFirst() },
|
||||||
|
localBaseDirectoryProvider: { localBase },
|
||||||
|
iCloudBaseDirectoryProvider: { nil },
|
||||||
|
exportProvider: { "{}" }
|
||||||
|
)
|
||||||
|
|
||||||
|
_ = service.createBackup(retentionCount: 1, includeICloud: false)
|
||||||
|
let records = service.createBackup(retentionCount: 1, includeICloud: false)
|
||||||
|
|
||||||
|
XCTAssertEqual(records.count, 1)
|
||||||
|
XCTAssertEqual(records.first?.location, .local)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCreateBackupIncludesICloudWhenEnabled() {
|
||||||
|
let localBase = tempRoot.appendingPathComponent("Local", isDirectory: true)
|
||||||
|
let iCloudBase = tempRoot.appendingPathComponent("iCloud", isDirectory: true)
|
||||||
|
try? FileManager.default.createDirectory(at: localBase, withIntermediateDirectories: true)
|
||||||
|
try? FileManager.default.createDirectory(at: iCloudBase, withIntermediateDirectories: true)
|
||||||
|
|
||||||
|
let service = BackupService(
|
||||||
|
fileManager: .default,
|
||||||
|
dateProvider: { Date(timeIntervalSince1970: 300) },
|
||||||
|
localBaseDirectoryProvider: { localBase },
|
||||||
|
iCloudBaseDirectoryProvider: { iCloudBase },
|
||||||
|
exportProvider: { "{}" }
|
||||||
|
)
|
||||||
|
|
||||||
|
let records = service.createBackup(retentionCount: 5, includeICloud: true)
|
||||||
|
let locations = Set(records.map { $0.location })
|
||||||
|
|
||||||
|
XCTAssertTrue(locations.contains(.local))
|
||||||
|
XCTAssertTrue(locations.contains(.iCloud))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import XCTest
|
||||||
|
@testable import PortfolioJournal
|
||||||
|
|
||||||
|
final class ReviewPromptServiceTests: XCTestCase {
|
||||||
|
private var defaults: UserDefaults!
|
||||||
|
private var suiteName: String!
|
||||||
|
|
||||||
|
override func setUp() {
|
||||||
|
super.setUp()
|
||||||
|
suiteName = UUID().uuidString
|
||||||
|
defaults = UserDefaults(suiteName: suiteName)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tearDown() {
|
||||||
|
if let suiteName {
|
||||||
|
defaults.removePersistentDomain(forName: suiteName)
|
||||||
|
}
|
||||||
|
super.tearDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTriggersAfterMinimumCheckIns() {
|
||||||
|
var didRequest = false
|
||||||
|
let service = ReviewPromptService(
|
||||||
|
userDefaults: defaults,
|
||||||
|
dateProvider: { Date(timeIntervalSince1970: 1_000) },
|
||||||
|
reviewRequestHandler: { didRequest = true }
|
||||||
|
)
|
||||||
|
|
||||||
|
service.recordMonthlyCheckInCompleted()
|
||||||
|
service.recordMonthlyCheckInCompleted()
|
||||||
|
XCTAssertFalse(didRequest)
|
||||||
|
|
||||||
|
service.recordMonthlyCheckInCompleted()
|
||||||
|
XCTAssertTrue(didRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDoesNotTriggerIfPromptedRecently() {
|
||||||
|
var didRequest = false
|
||||||
|
let now = Date(timeIntervalSince1970: 10_000)
|
||||||
|
defaults.set(now, forKey: "reviewPromptLastDate")
|
||||||
|
|
||||||
|
let service = ReviewPromptService(
|
||||||
|
userDefaults: defaults,
|
||||||
|
dateProvider: { now.addingTimeInterval(60 * 60 * 24 * 10) },
|
||||||
|
reviewRequestHandler: { didRequest = true }
|
||||||
|
)
|
||||||
|
|
||||||
|
service.recordMonthlyCheckInCompleted()
|
||||||
|
service.recordMonthlyCheckInCompleted()
|
||||||
|
service.recordMonthlyCheckInCompleted()
|
||||||
|
|
||||||
|
XCTAssertFalse(didRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import XCTest
|
||||||
|
@testable import PortfolioJournal
|
||||||
|
|
||||||
|
final class ShareServiceTests: XCTestCase {
|
||||||
|
func testBuildMonthlyCheckInShareText() {
|
||||||
|
let summary = MonthlySummary(
|
||||||
|
periodLabel: "January 2026",
|
||||||
|
startDate: Date(timeIntervalSince1970: 0),
|
||||||
|
endDate: Date(timeIntervalSince1970: 0),
|
||||||
|
startingValue: 1000,
|
||||||
|
endingValue: 1200,
|
||||||
|
contributions: 150,
|
||||||
|
netPerformance: 50
|
||||||
|
)
|
||||||
|
|
||||||
|
let text = ShareService.buildMonthlyCheckInShareText(
|
||||||
|
summary: summary,
|
||||||
|
appName: "Portfolio Journal"
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertTrue(text.contains("January 2026 Check-in"))
|
||||||
|
XCTAssertTrue(text.contains("Starting:"))
|
||||||
|
XCTAssertTrue(text.contains("Ending:"))
|
||||||
|
XCTAssertTrue(text.contains("Contributions:"))
|
||||||
|
XCTAssertTrue(text.contains("Net performance:"))
|
||||||
|
XCTAssertTrue(text.contains("Shared from Portfolio Journal"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import XCTest
|
||||||
|
@testable import PortfolioJournal
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class SettingsViewModelTests: XCTestCase {
|
||||||
|
override func tearDown() {
|
||||||
|
UserDefaults.standard.removeObject(forKey: "backupsEnabled")
|
||||||
|
UserDefaults.standard.removeObject(forKey: "backupRetentionCount")
|
||||||
|
UserDefaults.standard.removeObject(forKey: "debugPremiumOverride")
|
||||||
|
super.tearDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSetBackupsEnabledNonPremiumShowsPaywall() {
|
||||||
|
let iap = IAPService()
|
||||||
|
#if DEBUG
|
||||||
|
iap.setPremiumForTesting(false)
|
||||||
|
#endif
|
||||||
|
let viewModel = SettingsViewModel(iapService: iap)
|
||||||
|
|
||||||
|
viewModel.setBackupsEnabled(true)
|
||||||
|
|
||||||
|
XCTAssertFalse(viewModel.backupsEnabled)
|
||||||
|
XCTAssertTrue(viewModel.showingPaywall)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSetBackupsEnabledPremiumPersistsFlag() {
|
||||||
|
#if DEBUG
|
||||||
|
UserDefaults.standard.set(true, forKey: "debugPremiumOverride")
|
||||||
|
#endif
|
||||||
|
let iap = IAPService()
|
||||||
|
#if DEBUG
|
||||||
|
iap.setDebugPremiumOverride(true)
|
||||||
|
#endif
|
||||||
|
let viewModel = SettingsViewModel(iapService: iap)
|
||||||
|
|
||||||
|
viewModel.setBackupsEnabled(true)
|
||||||
|
|
||||||
|
XCTAssertTrue(viewModel.backupsEnabled)
|
||||||
|
XCTAssertFalse(viewModel.showingPaywall)
|
||||||
|
XCTAssertTrue(UserDefaults.standard.bool(forKey: "backupsEnabled"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import XCTest
|
||||||
|
import UIKit
|
||||||
|
@testable import PortfolioJournal
|
||||||
|
|
||||||
|
final class OnboardingViewTests: XCTestCase {
|
||||||
|
func testAppSettingsURLMatchesSystemConstant() {
|
||||||
|
let expected = URL(string: UIApplication.openSettingsURLString)
|
||||||
|
XCTAssertEqual(OnboardingQuickStartView.appSettingsURL(), expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user