Files
SideStore/SideStore/Tests/UITests/UITests.swift

325 lines
14 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// UITests.swift
// UITests
//
// Created by Magesh K on 21/02/25.
// Copyright © 2025 SideStore. All rights reserved.
//
import XCTest
import Foundation
final class UITests: XCTestCase {
// Handle to the homescreen UI
private static let springboard_app = XCUIApplication(bundleIdentifier: "com.apple.springboard")
private static let spotlight_app = XCUIApplication(bundleIdentifier: "com.apple.Spotlight")
private static let searchBar = spotlight_app.textFields["SpotlightSearchField"]
private static let APP_NAME = "SideStore"
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
// Self.dismissSpotlight()
Self.dismissSpringboardAlerts()
// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false
// In UI tests its important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
// Self.deleteMyApp()
Self.deleteMyApp2()
super.tearDown()
}
// @MainActor // Xcode 16.2 bug: UITest Record Button Disabled with @MainActor, see: https://stackoverflow.com/a/79445950/11971304
func testBulkAddRecommendedSources() throws {
let app = XCUIApplication()
app.launch()
let systemAlert = Self.springboard_app.alerts["\(Self.APP_NAME)” Would Like to Send You Notifications"]
// if it exists keep going immediately else wait for upto 5 sec with polling every 1 sec for existence
XCTAssertTrue(systemAlert.exists || systemAlert.waitForExistence(timeout: 5), "Notifications alert did not appear")
systemAlert.scrollViews.otherElements.buttons["Allow"].tap()
// Do the actual validation
try performBulkAddingRecommendedSources(for: app)
}
func testBulkAddInputSources() throws {
let app = XCUIApplication()
app.launch()
let systemAlert = Self.springboard_app.alerts["\(Self.APP_NAME)” Would Like to Send You Notifications"]
// if it exists keep going immediately else wait for upto 5 sec with polling every 1 sec for existence
XCTAssertTrue(systemAlert.exists || systemAlert.waitForExistence(timeout: 5), "Notifications alert did not appear")
systemAlert.scrollViews.otherElements.buttons["Allow"].tap()
// Do the actual validation
try performBulkAddingInputSources(for: app)
}
// @MainActor
// func testLaunchPerformance() throws {
// if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
// // This measures how long it takes to launch your application.
// measure(metrics: [XCTApplicationLaunchMetric()]) {
// XCUIApplication().launch()
// }
// }
// }
}
// Helpers
private extension UITests {
class func dismissSpotlight(){
// ignore spotlight if it was shown
if searchBar.exists {
let clearButton = searchBar.buttons["Clear text"]
if clearButton.exists{
clearButton.tap()
}
}
springboard_app.tap()
}
class func deleteMyApp() {
XCUIApplication().terminate()
dismissSpringboardAlerts()
// XCUIDevice.shared.press(.home)
springboard_app.swipeDown()
let searchBar = Self.searchBar
_ = searchBar.exists || searchBar.waitForExistence(timeout: 5)
searchBar.typeText(APP_NAME)
// Rest of the deletion flow...
let appIcon = spotlight_app.icons[APP_NAME]
// if it exists keep going immediately else wait for upto 5 sec with polling every 1 sec for existence
if appIcon.exists || appIcon.waitForExistence(timeout: 5) {
appIcon.press(forDuration: 1)
let deleteAppButton = spotlight_app.buttons["Delete App"]
_ = deleteAppButton.exists || deleteAppButton.waitForExistence(timeout: 5)
deleteAppButton.tap()
let confirmDeleteButton = springboard_app.alerts["Delete “\(APP_NAME)”?"]
_ = confirmDeleteButton.exists || confirmDeleteButton.waitForExistence(timeout: 5)
confirmDeleteButton.scrollViews.otherElements.buttons["Delete"].tap()
}
let clearButton = searchBar.buttons["Clear text"]
_ = clearButton.exists || clearButton.waitForExistence(timeout: 5)
clearButton.tap()
springboard_app.tap()
}
class func deleteMyApp2() {
XCUIApplication().terminate()
dismissSpringboardAlerts()
// Rest of the deletion flow...
let appIcon = springboard_app.icons[APP_NAME]
// if it exists keep going immediately else wait for upto 5 sec with polling every 1 sec for existence
if appIcon.exists || appIcon.waitForExistence(timeout: 5) {
appIcon.press(forDuration: 1)
do {
let button = springboard_app.buttons["Remove App"]
_ = button.exists || button.waitForExistence(timeout: 5)
button.tap()
}
do {
let button = springboard_app.buttons["Delete App"]
_ = button.waitForExistence(timeout: 0.3)
button.tap()
}
do {
let button = springboard_app.buttons["Delete"]
_ = button.waitForExistence(timeout: 0.3)
button.tap()
}
// Press home once to make the icons stop wiggling
XCUIDevice.shared.press(.home)
}
}
class func dismissSpringboardAlerts() {
for alert in springboard_app.alerts.allElementsBoundByIndex {
if alert.exists {
// If there's a "Cancel" button, tap it; otherwise, tap the first button.
if alert.buttons["Cancel"].exists {
alert.buttons["Cancel"].tap()
} else if let firstButton = alert.buttons.allElementsBoundByIndex.first {
firstButton.tap()
}
}
}
}
}
// Test guts (definition)
private extension UITests {
private func performBulkAddingInputSources(for app: XCUIApplication) throws {
// set content into clipboard (for bulk add (paste))
// NOTE: THIS IS AN ORDERED SEQUENCE AND MUST MATCH THE ORDER in textInputSources BELOW (Remember to take this into account when adding more entries)
UIPasteboard.general.string = """
https://alts.lao.sb
https://taurine.app/altstore/taurinestore.json
https://randomblock1.com/altstore/apps.json
https://burritosoftware.github.io/altstore/channels/burritosource.json
https://bit.ly/40Isul6
https://bit.ly/wuxuslibraryplus
https://bit.ly/Quantumsource-plus
https://bit.ly/Altstore-complete
https://bit.ly/Quantumsource
""".trimmedIndentation
let app = XCUIApplication()
app.tabBars["Tab Bar"].buttons["Sources"].tap()
app.navigationBars["Sources"].buttons["Add"].tap()
let collectionViewsQuery = app.collectionViews
let appsSidestoreIoTextField = collectionViewsQuery.textFields["apps.sidestore.io"]
appsSidestoreIoTextField.tap()
appsSidestoreIoTextField.tap()
collectionViewsQuery.staticTexts["Paste"].tap()
// if app.keyboards.buttons["Return"].exists {
// app.keyboards.buttons["Return"].tap()
// } else if app.keyboards.buttons["Done"].exists {
// app.keyboards.buttons["Done"].tap()
// } else {
// // if still exists try tapping outside of text field focus
// app.tap()
// }
if app.keyboards.count > 0 {
appsSidestoreIoTextField.typeText("\n") // Fallback to newline so that soft kb is dismissed
}
let cellsQuery = collectionViewsQuery.cells
// Data model for recommended sources. NOTE: This list order is required to be the same as that of "Add Source" Screen
let textInputSources: [(identifier: String, alertTitle: String, requiresSwipe: Bool)] = [
("Laoalts\nalts.lao.sb", "Laoalts", false),
("Taurine\ntaurine.app/altstore/taurinestore.json", "Taurine", false),
("RandomSource\nrandomblock1.com/altstore/apps.json", "RandomSource", false),
("Burrito's AltStore\nburritosoftware.github.io/altstore/channels/burritosource.json", "Burrito's AltStore", true),
("Qn_'s AltStore Repo\nbit.ly/40Isul6", "Qn_'s AltStore Repo", false),
("WuXu's Library++\nThe Most Up-To-Date IPA Library on AltStore.", "WuXu's Library++", false),
("Quantum Source++\nContains tweaked apps, free streaming, cracked apps, and more.", "Quantum Source++", false),
("AltStore Complete\nContains tweaked apps, free streaming, cracked apps, and more.", "AltStore Complete", false),
("Quantum Source\nContains all of your favorite emulators, games, jailbreaks, utilities, and more.", "Quantum Source", false),
]
// Tap on each textInputSources source's "add" button.
for source in textInputSources {
let sourceButton = cellsQuery.otherElements
.containing(.button, identifier: source.identifier)
.children(matching: .button)[source.identifier]
// let addButton = sourceButton.children(matching: .button).firstMatch
let addButton = sourceButton.children(matching: .button)["add"]
addButton.tap()
if source.requiresSwipe {
sourceButton.swipeUp(velocity: .slow) // Swipe up if needed.
}
}
// Commit the changes by tapping "Done".
app.navigationBars["Add Source"].buttons["Done"].tap()
// Accept each source addition via alert.
for source in textInputSources {
let alertIdentifier = "Would you like to add the source “\(source.alertTitle)”?"
app.alerts[alertIdentifier]
.scrollViews.otherElements.buttons["Add Source"]
.tap()
}
}
private func performBulkAddingRecommendedSources(for app: XCUIApplication) throws {
// Navigate to the Sources screen and open the Add Source view.
app.tabBars["Tab Bar"].buttons["Sources"].tap()
app.navigationBars["Sources"].buttons["Add"].tap()
let cellsQuery = app.collectionViews.cells
// Data model for recommended sources. NOTE: This list order is required to be the same as that of "Add Source" Screen
let recommendedSources: [(identifier: String, alertTitle: String, requiresSwipe: Bool)] = [
("SideStore Team Picks\ncommunity-apps.sidestore.io/sidecommunity.json", "SideStore Team Picks", false),
("Provenance EMU\nprovenance-emu.com/apps.json", "Provenance EMU", false),
("Countdown Respository\nneoarz.github.io/Countdown-App/Countdown.json", "Countdown Respository", false),
("OatmealDome's AltStore Source\naltstore.oatmealdome.me", "OatmealDome's AltStore Source", false),
("UTM Repository\nVirtual machines for iOS", "UTM Repository", true),
("Flyinghead\nflyinghead.github.io/flycast-builds/altstore.json", "Flyinghead", false),
("PojavLauncher Repository\nalt.crystall1ne.dev", "PojavLauncher Repository", false),
("PokeMMO\npokemmo.eu/altstore/", "PokeMMO", false),
("Odyssey\ntheodyssey.dev/altstore/odysseysource.json", "Odyssey", false),
("Yattee\nrepos.yattee.stream/alt/apps.json", "Yattee", false),
("ThatStella7922 Source\nThe home for all apps ThatStella7922", "ThatStella7922 Source", false)
]
// Tap on each recommended source's "add" button.
for source in recommendedSources {
let sourceButton = cellsQuery.otherElements
.containing(.button, identifier: source.identifier)
.children(matching: .button)[source.identifier]
let addButton = sourceButton.children(matching: .button)["add"]
addButton.tap()
if source.requiresSwipe {
sourceButton.swipeUp() // Swipe up if needed.
}
}
// Commit the changes by tapping "Done".
app.navigationBars["Add Source"].buttons["Done"].tap()
// Accept each source addition via alert.
for source in recommendedSources {
let alertIdentifier = "Would you like to add the source “\(source.alertTitle)”?"
app.alerts[alertIdentifier]
.scrollViews.otherElements.buttons["Add Source"]
.tap()
}
}
}
extension String {
var trimmedIndentation: String {
let lines = self.split(separator: "\n", omittingEmptySubsequences: false)
let minIndent = lines
.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty } // Ignore empty lines
.map { $0.prefix { $0.isWhitespace }.count }
.min() ?? 0
return lines.map { line in
String(line.dropFirst(minIndent))
}.joined(separator: "\n")
}
}