mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
- Bug-Fix: bulk added source previews were shown out of order
This commit is contained in:
@@ -315,10 +315,13 @@ private extension AddSourceViewController
|
||||
self.viewModel.$sourceAddress
|
||||
.map { [weak self] in
|
||||
guard let self else { return [] }
|
||||
print("\n\nStarting pipeline processing...\n\n")
|
||||
|
||||
let lines = $0.split(whereSeparator: { $0.isWhitespace }).map(String.init).compactMap(self.sourceURL)
|
||||
return lines
|
||||
// Preserve order of parsed URLs
|
||||
let lines = $0.split(whereSeparator: { $0.isWhitespace })
|
||||
.map(String.init)
|
||||
.compactMap(self.sourceURL)
|
||||
|
||||
return NSOrderedSet(array: lines).array as! [URL] // de-duplicate while preserving order
|
||||
}
|
||||
.assign(to: &self.viewModel.$sourceURLs)
|
||||
|
||||
@@ -344,16 +347,26 @@ private extension AddSourceViewController
|
||||
|
||||
self.viewModel.isLoadingPreview = true
|
||||
|
||||
let publishers = sourceURLs.map { sourceURL in
|
||||
print("Creating preview for source:", sourceURL, " ...")
|
||||
return self.fetchSourcePreview(sourceURL: sourceURL)
|
||||
// Create publishers maintaining order
|
||||
let publishers = sourceURLs.enumerated().map { index, sourceURL in
|
||||
self.fetchSourcePreview(sourceURL: sourceURL)
|
||||
.map { result in
|
||||
// Add index to maintain order
|
||||
(index: index, result: result)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
// since network requests are concurrent, we sort the values when they are received
|
||||
return publishers.isEmpty
|
||||
? Just([]).eraseToAnyPublisher()
|
||||
: Publishers.MergeMany(publishers)
|
||||
.collect()
|
||||
.collect() // await all publishers to emit the results
|
||||
.map { results in // perform sorting of the collected results
|
||||
// Sort by original index before returning
|
||||
results.sorted { $0.index < $1.index }
|
||||
.map { $0.result }
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.sink { [weak self] sourcePreviewResults in
|
||||
@@ -386,49 +399,20 @@ private extension AddSourceViewController
|
||||
}
|
||||
.store(in: &self.cancellables)
|
||||
|
||||
// self.viewModel.$sourcePreviewResults
|
||||
// .map { sourcePreviewResults -> [Source] in
|
||||
// // Process the full array:
|
||||
// // - For each tuple, extract the `result` (the second element)
|
||||
// // - For each result, convert it to a Managed<Source> if it's successful
|
||||
// // - Remove any nil values from failed results
|
||||
// let managedSources = sourcePreviewResults.compactMap { previewResult -> Managed<Source>? in
|
||||
// switch previewResult.result {
|
||||
// case .success(let source):
|
||||
// return source
|
||||
// case .failure:
|
||||
// return nil
|
||||
// }
|
||||
// }
|
||||
// // Optionally, remove duplicates based on identifier:
|
||||
// // (This groups by identifier and keeps the first occurrence.)
|
||||
// let uniqueManagedSources = Dictionary(grouping: managedSources, by: { $0.identifier })
|
||||
// .compactMap { $0.value.first }
|
||||
//
|
||||
// // Unwrap Managed<Source> into Source (assuming Managed<Source> has a wrappedValue property)
|
||||
// let sources = uniqueManagedSources.map { $0.wrappedValue }
|
||||
// return sources
|
||||
// }
|
||||
// .receive(on: RunLoop.main)
|
||||
// .sink { [weak self] sources in
|
||||
// self?.updateSourcesPreview(for: sources)
|
||||
// }
|
||||
// .store(in: &self.cancellables)
|
||||
|
||||
self.viewModel.$sourcePreviewResults
|
||||
.map { sourcePreviewResults -> [Source] in
|
||||
var seenIdentifiers = Set<String>()
|
||||
let orderedSources = sourcePreviewResults.compactMap { previewResult -> Source? in
|
||||
switch previewResult.result {
|
||||
case .success(let managedSource):
|
||||
let id = managedSource.identifier
|
||||
guard !seenIdentifiers.contains(id) else { return nil }
|
||||
seenIdentifiers.insert(id)
|
||||
return managedSource.wrappedValue
|
||||
case .failure:
|
||||
// Maintain order based on original sourceURLs array
|
||||
let orderedSources = self.viewModel.sourceURLs.compactMap { sourceURL -> Source? in
|
||||
// Find the preview result matching this URL
|
||||
guard let previewResult = sourcePreviewResults.first(where: { $0.sourceURL == sourceURL }),
|
||||
case .success(let managedSource) = previewResult.result
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return managedSource.wrappedValue
|
||||
}
|
||||
|
||||
return orderedSources
|
||||
}
|
||||
.receive(on: RunLoop.main)
|
||||
@@ -615,6 +599,9 @@ private extension AddSourceViewController
|
||||
cell.bannerView.button.tintColor = .clear
|
||||
cell.bannerView.button.isHidden = false
|
||||
|
||||
// mark the button with label (useful for accessibility and for UITests)
|
||||
cell.bannerView.button.accessibilityIdentifier = "add"
|
||||
|
||||
func setButtonIcon()
|
||||
{
|
||||
Task<Void, Never>(priority: .userInitiated) { [weak cell] in
|
||||
|
||||
@@ -35,6 +35,51 @@ final class UITests: XCTestCase {
|
||||
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"]
|
||||
|
||||
XCTAssertTrue(systemAlert.waitForExistence(timeout: 0.2), "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"]
|
||||
|
||||
XCTAssertTrue(systemAlert.waitForExistence(timeout: 0.2), "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 deleteMyApp() {
|
||||
XCUIApplication().terminate()
|
||||
dismissSpringboardAlerts()
|
||||
@@ -60,33 +105,7 @@ final class UITests: XCTestCase {
|
||||
springboard_app.tap()
|
||||
}
|
||||
|
||||
// @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"]
|
||||
|
||||
XCTAssertTrue(systemAlert.waitForExistence(timeout: 0.2), "Notifications alert did not appear")
|
||||
systemAlert.scrollViews.otherElements.buttons["Allow"].tap()
|
||||
|
||||
// Do the actual validation
|
||||
try performBulkAddingRecommendedSources(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()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
private extension UITests {
|
||||
|
||||
class func dismissSpringboardAlerts() {
|
||||
for alert in springboard_app.alerts.allElementsBoundByIndex {
|
||||
if alert.exists {
|
||||
@@ -102,9 +121,75 @@ private extension UITests {
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 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()
|
||||
|
||||
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()
|
||||
@@ -120,7 +205,7 @@ private extension UITests {
|
||||
("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", true),
|
||||
("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),
|
||||
@@ -151,3 +236,21 @@ private extension UITests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user