From 69f7923a522db001052ec86cb22729ceba413f2c Mon Sep 17 00:00:00 2001 From: Stefan Hausotte Date: Mon, 15 Jun 2026 12:19:00 +0200 Subject: [PATCH] chore: formatting and lints --- .swiftformat | 1 + Forji/Forji.xcodeproj/project.pbxproj | 21 ++++++++------- .../xcshareddata/swiftpm/Package.resolved | 9 +++++++ Forji/Forji/App/ContentView.swift | 2 +- Forji/Forji/Helpers/MermaidParser.swift | 2 +- Forji/Forji/Helpers/WorkflowStatus.swift | 4 +-- .../Services/AuthenticationService.swift | 4 +-- Forji/Forji/Views/AttachmentGallery.swift | 6 ++--- Forji/Forji/Views/CommitHistoryView.swift | 2 +- Forji/Forji/Views/HomeView.swift | 2 +- Forji/Forji/Views/IssueListView.swift | 2 +- Forji/Forji/Views/MarkdownToolbar.swift | 4 +-- Forji/Forji/Views/PullRequestListView.swift | 2 +- Forji/Forji/Views/RepositoryDetailView.swift | 4 +-- .../Forji/Views/SearchableOverviewView.swift | 4 +-- Forji/Forji/Views/WorkflowJobView.swift | 2 +- Forji/Forji/Views/WorkflowRunDetailView.swift | 6 ++--- Forji/ForjiTests/KeychainManagerTests.swift | 4 +-- .../ForjiTests/MarkdownComponentsTests.swift | 2 +- .../NotificationPollingIntegrationTests.swift | 26 +++++++++---------- Forji/ForjiTests/PaginationStateTests.swift | 20 +++++++------- Forji/ForjiUITests/AttachmentUITests.swift | 10 +++---- .../ForgejoReadOnlyUITestBase.swift | 4 +-- .../ForjiUITestsLaunchTests.swift | 2 +- Forji/ForjiUITests/IssueMutatingUITests.swift | 2 +- .../ForjiUITests/MergedInstanceUITests.swift | 2 +- .../OverviewCreateMutatingUITests.swift | 2 +- .../ForjiUITests/OverviewCreateUITests.swift | 2 +- Forji/ForjiUITests/PaginationUITests.swift | 2 +- Forji/ForjiUITests/PermissionUITests.swift | 4 +-- Forji/ForjiUITests/PullRequestUITests.swift | 4 +-- .../RepositoryMutatingUITests.swift | 2 +- Forji/ForjiUITests/RepositoryUITests.swift | 4 +-- README.md | 16 +++++++----- 34 files changed, 99 insertions(+), 86 deletions(-) diff --git a/.swiftformat b/.swiftformat index e11af52..dd1bb2c 100644 --- a/.swiftformat +++ b/.swiftformat @@ -1,2 +1,3 @@ --swiftversion 6.2 --disable redundantMemberwiseInit +--disable swiftTestingTestCaseNames diff --git a/Forji/Forji.xcodeproj/project.pbxproj b/Forji/Forji.xcodeproj/project.pbxproj index 10be1f4..3545c79 100644 --- a/Forji/Forji.xcodeproj/project.pbxproj +++ b/Forji/Forji.xcodeproj/project.pbxproj @@ -230,7 +230,7 @@ mainGroup = DEC49F182F3CE05200E7DD54; minimizedProjectReferenceProxies = 1; packageReferences = ( - DE00000000000005000000AA /* XCLocalSwiftPackageReference "../../ForgejoKit" */, + DE00000000000005000000AA /* XCRemoteSwiftPackageReference "ForgejoKit" */, DEC49F6B2F3D00C700E7DD54 /* XCRemoteSwiftPackageReference "textual" */, DEC49F812F3D173F00E7DD54 /* XCRemoteSwiftPackageReference "HighlightSwift" */, ); @@ -633,14 +633,15 @@ }; /* End XCConfigurationList section */ -/* Begin XCLocalSwiftPackageReference section */ - DE00000000000005000000AA /* XCLocalSwiftPackageReference "../../ForgejoKit" */ = { - isa = XCLocalSwiftPackageReference; - relativePath = ../../ForgejoKit; - }; -/* End XCLocalSwiftPackageReference section */ - /* Begin XCRemoteSwiftPackageReference section */ + DE00000000000005000000AA /* XCRemoteSwiftPackageReference "ForgejoKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://codeberg.org/secana/ForgejoKit.git"; + requirement = { + kind = exactVersion; + version = 0.8.0; + }; + }; DEC49F6B2F3D00C700E7DD54 /* XCRemoteSwiftPackageReference "textual" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/gonzalezreal/textual"; @@ -662,12 +663,12 @@ /* Begin XCSwiftPackageProductDependency section */ DE00000000000002000000AA /* ForgejoKit */ = { isa = XCSwiftPackageProductDependency; - package = DE00000000000005000000AA /* XCLocalSwiftPackageReference "../../ForgejoKit" */; + package = DE00000000000005000000AA /* XCRemoteSwiftPackageReference "ForgejoKit" */; productName = ForgejoKit; }; DE00000000000004000000AA /* ForgejoKit */ = { isa = XCSwiftPackageProductDependency; - package = DE00000000000005000000AA /* XCLocalSwiftPackageReference "../../ForgejoKit" */; + package = DE00000000000005000000AA /* XCRemoteSwiftPackageReference "ForgejoKit" */; productName = ForgejoKit; }; DEC49F6D2F3D023400E7DD54 /* Textual */ = { diff --git a/Forji/Forji.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Forji/Forji.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 363434d..78791f3 100644 --- a/Forji/Forji.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Forji/Forji.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,15 @@ { "originHash" : "931ec0beeaf4e6a5eaa0afab6f815f97bc126bda7a2c9f001c10de58585e766f", "pins" : [ + { + "identity" : "forgejokit", + "kind" : "remoteSourceControl", + "location" : "https://codeberg.org/secana/ForgejoKit.git", + "state" : { + "revision" : "485c147e7f9762c06f51029de47464fb41abc4af", + "version" : "0.8.0" + } + }, { "identity" : "highlightswift", "kind" : "remoteSourceControl", diff --git a/Forji/Forji/App/ContentView.swift b/Forji/Forji/App/ContentView.swift index 72b0a9f..16e5bbc 100644 --- a/Forji/Forji/App/ContentView.swift +++ b/Forji/Forji/App/ContentView.swift @@ -150,7 +150,7 @@ struct ContentView: View { defaultInstance.lastUsed = Date() try? modelContext.save() } catch { - // Auto-login failed — fall through to instance list + // Auto-login failed, fall through to instance list } } diff --git a/Forji/Forji/Helpers/MermaidParser.swift b/Forji/Forji/Helpers/MermaidParser.swift index 057ebf4..ee5b800 100644 --- a/Forji/Forji/Helpers/MermaidParser.swift +++ b/Forji/Forji/Helpers/MermaidParser.swift @@ -152,7 +152,7 @@ enum HTMLParser { } } - // 2. Inline HTML — check paragraphs for inline tags + // 2. Inline HTML, check paragraphs for inline tags if let inlinePattern = inlineHTMLPattern { let paragraphs = splitIntoParagraphs(masked) for para in paragraphs { diff --git a/Forji/Forji/Helpers/WorkflowStatus.swift b/Forji/Forji/Helpers/WorkflowStatus.swift index c8f8b29..a6f9d1e 100644 --- a/Forji/Forji/Helpers/WorkflowStatus.swift +++ b/Forji/Forji/Helpers/WorkflowStatus.swift @@ -36,7 +36,7 @@ struct WorkflowStatusStyle { let label: String /// Maps Forgejo's single status value to a UI style. Used for runs, jobs, - /// and steps — they share the same status vocabulary. Forgejo's known + /// and steps, they share the same status vocabulary. Forgejo's known /// statuses: success, failure, cancelled, skipped, running, waiting, /// blocked, unknown. Anything else falls back to a question-mark glyph. static func forStatus(_ status: String) -> WorkflowStatusStyle { @@ -122,7 +122,7 @@ extension WorkflowRun { } /// Elapsed time as TimeInterval. Forgejo serialises a Go `time.Duration` - /// — int64 nanoseconds — so divide by 1_000_000_000. Falls back to the + /// , int64 nanoseconds, so divide by 1_000_000_000. Falls back to the /// timestamps only when both are sane (post-2000). var duration: TimeInterval? { if let nanos = durationNanos, nanos > 0 { diff --git a/Forji/Forji/Services/AuthenticationService.swift b/Forji/Forji/Services/AuthenticationService.swift index 99f180c..64ca743 100644 --- a/Forji/Forji/Services/AuthenticationService.swift +++ b/Forji/Forji/Services/AuthenticationService.swift @@ -116,7 +116,7 @@ class AuthenticationService { username: instance.username, ) } catch { - // Keychain delete failed — log in debug builds + // Keychain delete failed, log in debug builds #if DEBUG print("Keychain delete failed during logout: \(error)") #endif @@ -171,7 +171,7 @@ class AuthenticationService { isAuthenticated = true return } catch { - // Token is invalid/expired — only fall through to password if this is not a token-only instance + // Token is invalid/expired, only fall through to password if this is not a token-only instance if useTokenAuth { throw SessionRestoreError.fromTokenValidationError(error) } diff --git a/Forji/Forji/Views/AttachmentGallery.swift b/Forji/Forji/Views/AttachmentGallery.swift index 7ee7121..76d4de4 100644 --- a/Forji/Forji/Views/AttachmentGallery.swift +++ b/Forji/Forji/Views/AttachmentGallery.swift @@ -2,9 +2,9 @@ import ForgejoKit import SwiftUI /// Markdown to insert when an attachment is uploaded. Images use embed syntax -/// (`![name](url)`) so they render inline; everything else (logs, PDFs, archives, -/// …) uses link syntax (`[name](url)`) so it appears as a clickable link rather -/// than a broken inline image. +/// (`![name](url)`) so they render inline; every other file type (logs, PDFs, +/// archives, and so on) uses link syntax (`[name](url)`) so it appears as a +/// clickable link rather than a broken inline image. func attachmentMarkdown(for attachment: Attachment) -> String { if attachment.isImage { "![\(attachment.name)](\(attachment.browserDownloadUrl))" diff --git a/Forji/Forji/Views/CommitHistoryView.swift b/Forji/Forji/Views/CommitHistoryView.swift index d6e842c..a9d132e 100644 --- a/Forji/Forji/Views/CommitHistoryView.swift +++ b/Forji/Forji/Views/CommitHistoryView.swift @@ -146,7 +146,7 @@ struct CommitHistoryView: View { repo: repo, ) } catch { - // Non-critical — branch selector stays disabled + // Non-critical, branch selector stays disabled } } diff --git a/Forji/Forji/Views/HomeView.swift b/Forji/Forji/Views/HomeView.swift index 8a321f5..70b3544 100644 --- a/Forji/Forji/Views/HomeView.swift +++ b/Forji/Forji/Views/HomeView.swift @@ -100,7 +100,7 @@ struct HomeView: View { do { unreadCount = try await notificationService.fetchUnreadCount() } catch { - // Silently ignore — badge is non-critical, keep the prior count + // Silently ignore, badge is non-critical, keep the prior count return } } else { diff --git a/Forji/Forji/Views/IssueListView.swift b/Forji/Forji/Views/IssueListView.swift index c691996..3beea3d 100644 --- a/Forji/Forji/Views/IssueListView.swift +++ b/Forji/Forji/Views/IssueListView.swift @@ -55,7 +55,7 @@ struct IssueListView: View { } description: { Text( stateFilter == .open - ? "All clear — no open issues to review." + ? "All clear, no open issues to review." : "No closed issues found.", ) } diff --git a/Forji/Forji/Views/MarkdownToolbar.swift b/Forji/Forji/Views/MarkdownToolbar.swift index 1bb3e8d..18acba4 100644 --- a/Forji/Forji/Views/MarkdownToolbar.swift +++ b/Forji/Forji/Views/MarkdownToolbar.swift @@ -106,7 +106,7 @@ struct MarkdownToolbar: View { private func uploadTestImage() async { guard let onUploadImage else { return } - // 1×1 transparent PNG — used in UI tests to skip the Photos picker. + // 1×1 transparent PNG, used in UI tests to skip the Photos picker. let base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" guard let pngData = Data(base64Encoded: base64) else { return } await runUpload { try await onUploadImage(pngData, "test.png", "image/png") } @@ -114,7 +114,7 @@ struct MarkdownToolbar: View { private func uploadTestFile() async { guard let onUploadImage else { return } - // Plain-text log file — used in UI tests to exercise the non-image + // Plain-text log file, used in UI tests to exercise the non-image // attachment path (link markdown + file-row display). let logData = Data("line1 ERROR boom\nline2 WARN stuff\nline3 done\n".utf8) await runUpload { try await onUploadImage(logData, "server.log", "text/plain") } diff --git a/Forji/Forji/Views/PullRequestListView.swift b/Forji/Forji/Views/PullRequestListView.swift index 353be51..b480fd8 100644 --- a/Forji/Forji/Views/PullRequestListView.swift +++ b/Forji/Forji/Views/PullRequestListView.swift @@ -55,7 +55,7 @@ struct PullRequestListView: View { } description: { Text( stateFilter == .open - ? "All clear — no open pull requests." + ? "All clear, no open pull requests." : "No closed pull requests found.", ) } diff --git a/Forji/Forji/Views/RepositoryDetailView.swift b/Forji/Forji/Views/RepositoryDetailView.swift index 58328aa..d3171d2 100644 --- a/Forji/Forji/Views/RepositoryDetailView.swift +++ b/Forji/Forji/Views/RepositoryDetailView.swift @@ -185,7 +185,7 @@ struct RepositoryDetailView: View { repo: repository.repoName, ) } catch { - // Non-critical — branch selector stays disabled + // Non-critical, branch selector stays disabled } } .toolbar { @@ -504,7 +504,7 @@ struct RepositoryCodeView: View { ) readmeContent = fileContent.decodedContent } catch { - // README exists in listing but couldn't be fetched — ignore + // README exists in listing but couldn't be fetched, ignore } } diff --git a/Forji/Forji/Views/SearchableOverviewView.swift b/Forji/Forji/Views/SearchableOverviewView.swift index f746bc3..a0e36e6 100644 --- a/Forji/Forji/Views/SearchableOverviewView.swift +++ b/Forji/Forji/Views/SearchableOverviewView.swift @@ -69,7 +69,7 @@ struct SearchableOverviewView: View { return "No \(itemNoun) matching your search." } if stateFilter == .open { - return "All clear — no open \(itemNoun) to review." + return "All clear, no open \(itemNoun) to review." } let prefix = stateFilter == .all ? "" : stateFilter.rawValue + " " return "No \(prefix)\(itemNoun) found." @@ -85,7 +85,7 @@ struct SearchableOverviewView: View { case .closed: "Closed" case .all: "All" } - // Scope is ignored during search — only show state + // Scope is ignored during search, only show state if !searchText.isEmpty || (stateFilter == .open && involvementScope == .involved) { return stateLabel } diff --git a/Forji/Forji/Views/WorkflowJobView.swift b/Forji/Forji/Views/WorkflowJobView.swift index 936b3fd..24afe9c 100644 --- a/Forji/Forji/Views/WorkflowJobView.swift +++ b/Forji/Forji/Views/WorkflowJobView.swift @@ -72,7 +72,7 @@ struct WorkflowJobView: View { Label("No Steps Recorded", systemImage: "tray") .foregroundStyle(.secondary) } description: { - Text("This job has no recorded steps — it likely never started.") + Text("This job has no recorded steps, it likely never started.") } } header: { experimentalHeader diff --git a/Forji/Forji/Views/WorkflowRunDetailView.swift b/Forji/Forji/Views/WorkflowRunDetailView.swift index 8ed91f4..19104ec 100644 --- a/Forji/Forji/Views/WorkflowRunDetailView.swift +++ b/Forji/Forji/Views/WorkflowRunDetailView.swift @@ -196,7 +196,7 @@ struct WorkflowRunDetailView: View { isLoading = false - // Best-effort experimental fetch — don't surface errors if it fails + // Best-effort experimental fetch, don't surface errors if it fails // (e.g. older Forgejo versions without this route, or auth quirks). if let runIndex = run?.indexInRepo { await loadRunView(runIndex: runIndex) @@ -214,7 +214,7 @@ struct WorkflowRunDetailView: View { logCursors: [], ) } catch { - // Silent — experimental endpoint failure shouldn't block run detail. + // Silent, experimental endpoint failure shouldn't block run detail. } } @@ -222,7 +222,7 @@ struct WorkflowRunDetailView: View { let formatter = DateComponentsFormatter() formatter.allowedUnits = [.hour, .minute, .second] formatter.unitsStyle = .abbreviated - return formatter.string(from: seconds) ?? "—" + return formatter.string(from: seconds) ?? "-" } } diff --git a/Forji/ForjiTests/KeychainManagerTests.swift b/Forji/ForjiTests/KeychainManagerTests.swift index 9ce5324..4c3b18c 100644 --- a/Forji/ForjiTests/KeychainManagerTests.swift +++ b/Forji/ForjiTests/KeychainManagerTests.swift @@ -127,7 +127,7 @@ struct KeychainManagerTests { _ = try await KeychainManager.shared.getPassword(for: raw, username: "user") Issue.record("Should not find password with non-normalized key") } catch is KeychainError { - // Expected — keys differ + // Expected, keys differ } // Clean up @@ -156,7 +156,7 @@ struct KeychainManagerTests { _ = try await KeychainManager.shared.getToken(for: server, username: username) Issue.record("Expected KeychainError.notFound for token after deleteCredentials") } catch is KeychainError { - // Expected — this is the assertion that fails under the old leak + // Expected, this is the assertion that fails under the old leak } catch { Issue.record("Unexpected error: \(error)") } diff --git a/Forji/ForjiTests/MarkdownComponentsTests.swift b/Forji/ForjiTests/MarkdownComponentsTests.swift index 046b682..84459d2 100644 --- a/Forji/ForjiTests/MarkdownComponentsTests.swift +++ b/Forji/ForjiTests/MarkdownComponentsTests.swift @@ -44,7 +44,7 @@ struct MarkdownComponentsTests { } @Test func repoRelativePathReturnsNilForBranchOnly() { - // URL ends right after the ref — no file path follows + // URL ends right after the ref, no file path follows let url = URL(string: "https://forgejo.example.com/owner/repo/src/branch/main")! #expect(repoRelativePath(from: url) == nil) } diff --git a/Forji/ForjiTests/NotificationPollingIntegrationTests.swift b/Forji/ForjiTests/NotificationPollingIntegrationTests.swift index b609c52..5809fee 100644 --- a/Forji/ForjiTests/NotificationPollingIntegrationTests.swift +++ b/Forji/ForjiTests/NotificationPollingIntegrationTests.swift @@ -71,12 +71,12 @@ struct NotificationPollingIntegrationTests { let ids = Set(notifications.map(\.id)) #expect(!ids.isEmpty) - // Seed — should record all current IDs + // Seed, should record all current IDs store.seed(ids: ids, for: normalizedURL, username: Self.username) #expect(store.isSeeded(for: normalizedURL, username: Self.username)) #expect(store.seenIDs(for: normalizedURL, username: Self.username) == ids) - // Simulate a poll returning the same IDs — no new notifications + // Simulate a poll returning the same IDs, no new notifications let newIDs = ids.subtracting(store.seenIDs(for: normalizedURL, username: Self.username)) #expect(newIDs.isEmpty, "After seeding with current IDs, diff should be empty") } @@ -93,11 +93,11 @@ struct NotificationPollingIntegrationTests { let allIDs = Set(notifications.map(\.id)) #expect(allIDs.count >= 2, "Need at least 2 notifications for this test") - // Seed with only a subset — simulates having seen all but one + // Seed with only a subset, simulates having seen all but one let partial = Set(allIDs.dropFirst()) store.seed(ids: partial, for: normalizedURL, username: Self.username) - // Now diff — the first ID should show as "new" + // Now diff, the first ID should show as "new" let seenIDs = store.seenIDs(for: normalizedURL, username: Self.username) let newIDs = allIDs.subtracting(seenIDs) #expect(newIDs.count == 1, "Expected exactly 1 new notification after partial seed") @@ -123,7 +123,7 @@ struct NotificationPollingIntegrationTests { store.seed(ids: seeded, for: normalizedURL, username: Self.username) #expect(store.seenIDs(for: normalizedURL, username: Self.username).contains(999_999)) - // Prune — only keep IDs that the server still reports as unread + // Prune, only keep IDs that the server still reports as unread store.prune(keeping: serverIDs, for: normalizedURL, username: Self.username) #expect(!store.seenIDs(for: normalizedURL, username: Self.username).contains(999_999)) #expect(store.seenIDs(for: normalizedURL, username: Self.username) == serverIDs) @@ -150,7 +150,7 @@ struct NotificationPollingIntegrationTests { let store = SeenNotificationStore(suiteName: "de.hausotte.Forji.test.\(UUID().uuidString)") #expect(!store.isSeeded(for: normalizedURL, username: Self.username)) - // Seed the store — first poll seeds without posting notifications + // Seed the store, first poll seeds without posting notifications let firstPollIDs = Set(notifications.map(\.id)) store.seed(ids: firstPollIDs, for: normalizedURL, username: Self.username) @@ -191,7 +191,7 @@ struct NotificationPollingIntegrationTests { // Save with current (post-migration) accessibility try await KeychainManager.shared.saveToken(token, for: server, username: user) - // Run migration — should read + delete + re-save without losing data + // Run migration, should read + delete + re-save without losing data await KeychainManager.shared.migrateAccessibility(for: server, username: user) // Verify token is still accessible @@ -283,7 +283,7 @@ struct NotificationPollingIntegrationTests { let service = NotificationService(client: client) let store = SeenNotificationStore(suiteName: "de.hausotte.Forji.test.\(UUID().uuidString)") - // Initial poll — seed seen store + // Initial poll, seed seen store let initial = try await service.fetchNotifications( statusTypes: ["unread"], page: 1, limit: 50 ) @@ -295,14 +295,14 @@ struct NotificationPollingIntegrationTests { let markedID = initial[0].id try await service.markAsRead(id: markedID) - // Next poll — fewer unreads + // Next poll, fewer unreads let afterMark = try await service.fetchNotifications( statusTypes: ["unread"], page: 1, limit: 50 ) let afterIDs = Set(afterMark.map(\.id)) #expect(!afterIDs.contains(markedID), "Marked notification should no longer be unread") - // Prune — the marked ID should be removed from seen store + // Prune, the marked ID should be removed from seen store store.prune(keeping: afterIDs, for: normalizedURL, username: Self.username) #expect( !store.seenIDs(for: normalizedURL, username: Self.username).contains(markedID), @@ -319,7 +319,7 @@ struct NotificationPollingIntegrationTests { let server = "https://test.example.com" let user = "testuser" - // Seed with partial IDs — ID 1 is "seen", ID 2 is "new" + // Seed with partial IDs, ID 1 is "seen", ID 2 is "new" store.seed(ids: [1], for: server, username: user) // Simulate a poll returning IDs [1, 2] @@ -460,12 +460,12 @@ struct NotificationPollingIntegrationTests { let allIDs = Set(notifications.map(\.id)) #expect(allIDs.count >= 2, "Need at least 2 notifications") - // Seed with a subset — simulate having seen all but one + // Seed with a subset, simulate having seen all but one let store = SeenNotificationStore(suiteName: "de.hausotte.Forji.test.\(UUID().uuidString)") let partial = Set(allIDs.dropFirst()) store.seed(ids: partial, for: normalizedURL, username: Self.username) - // Diff — should detect the missing ID as new + // Diff, should detect the missing ID as new let seenIDs = store.seenIDs(for: normalizedURL, username: Self.username) let newIDs = allIDs.subtracting(seenIDs) #expect(newIDs.count == 1, "Expected exactly 1 new notification") diff --git a/Forji/ForjiTests/PaginationStateTests.swift b/Forji/ForjiTests/PaginationStateTests.swift index b71a9e5..1bb898d 100644 --- a/Forji/ForjiTests/PaginationStateTests.swift +++ b/Forji/ForjiTests/PaginationStateTests.swift @@ -147,13 +147,13 @@ struct PaginationStateConcurrentTests { } await yieldUntil { fetcherA.isPending } - // Start reload B — cancels A's internal task + // Start reload B, cancels A's internal task let taskB = pagination.reload { page, limit in try await fetcherB.fetch(page: page, limit: limit) } await yieldUntil { fetcherB.isPending } - // Complete both — only B's results should be applied + // Complete both, only B's results should be applied fetcherA.complete(returning: ["stale"]) fetcherB.complete(returning: ["fresh"]) await taskB.value @@ -177,7 +177,7 @@ struct PaginationStateConcurrentTests { } await yieldUntil { fetcherB.isPending } - // Complete A first (stale, its task was cancelled) — must be discarded + // Complete A first (stale, its task was cancelled), must be discarded fetcherA.complete(returning: ["stale"]) await yieldUntil { fetcherA.callCount == 1 } @@ -213,7 +213,7 @@ struct PaginationStateConcurrentTests { } await yieldUntil { fetcherC.isPending } - // Complete in order A, B, C — only C should be applied + // Complete in order A, B, C, only C should be applied fetcherA.complete(returning: ["a"]) fetcherB.complete(returning: ["b"]) fetcherC.complete(returning: ["c"]) @@ -238,7 +238,7 @@ struct PaginationStateConcurrentTests { } await yieldUntil { fetcherB.isPending } - // A fails with error — but its task was cancelled, so no alert + // A fails with error, but its task was cancelled, so no alert fetcherA.complete(throwing: URLError(.badServerResponse)) fetcherB.complete(returning: ["ok"]) await taskB.value @@ -264,7 +264,7 @@ struct PaginationStateConcurrentTests { await yieldUntil { fetcherB.isPending } #expect(pagination.isLoading) - // Complete stale A — isLoading must REMAIN true (B is still in flight) + // Complete stale A, isLoading must REMAIN true (B is still in flight) fetcherA.complete(returning: ["stale"]) await yieldUntil { fetcherA.callCount == 1 } #expect(pagination.isLoading, "isLoading must stay true while latest reload (B) is pending") @@ -287,7 +287,7 @@ struct PaginationStateConcurrentTests { } await yieldUntil { initialFetcher.isPending } - // Simulate onChange: user changed filter — just call reload again + // Simulate onChange: user changed filter, just call reload again let filterTask = pagination.reload(clearItems: true) { page, limit in try await filterFetcher.fetch(page: page, limit: limit) } @@ -463,11 +463,11 @@ struct PaginationStateLoadMoreTests { } await yieldUntil { freshFetcher.isPending } - // loadMore completes — should be discarded (task was cancelled) + // loadMore completes, should be discarded (task was cancelled) moreFetcher.complete(returning: ["stale-more"]) await moreTask.value - // reload completes — should be applied + // reload completes, should be applied freshFetcher.complete(returning: ["fresh"]) await reloadTask.value @@ -485,7 +485,7 @@ struct PaginationStateDedupeTests { pagination.dedupeKey = { $0 } await pagination.reload { _, _ in ["a", "b"] }.value - // "b" resurfaces on page 2 — only "c" is new + // "b" resurfaces on page 2, only "c" is new await pagination.loadMore { _, _ in ["b", "c"] } #expect(pagination.items == ["a", "b", "c"]) #expect(pagination.hasMore) // the page contributed a new item diff --git a/Forji/ForjiUITests/AttachmentUITests.swift b/Forji/ForjiUITests/AttachmentUITests.swift index 594892c..305546d 100644 --- a/Forji/ForjiUITests/AttachmentUITests.swift +++ b/Forji/ForjiUITests/AttachmentUITests.swift @@ -25,7 +25,7 @@ final class AttachmentUITests: ForgejoUITestBase { XCTAssertTrue(commentButton.waitForExistence(timeout: 5)) commentButton.tap() - // Tap the image upload button twice — in test mode each tap uploads a + // Tap the image upload button twice, in test mode each tap uploads a // hardcoded PNG instead of presenting the Photos picker. Two taps produce // two distinct attachments and two markdown references. let imageButton = app.buttons["markdown-toolbar-image"] @@ -33,13 +33,13 @@ final class AttachmentUITests: ForgejoUITestBase { let textEditor = app.textViews["markdown-text-editor"].firstMatch XCTAssertTrue(textEditor.waitForExistence(timeout: 5)) - // First upload — wait until one markdown reference is inserted before tapping + // First upload, wait until one markdown reference is inserted before tapping // again, so the second upload appends rather than racing the first. imageButton.tap() expectation(for: NSPredicate(format: "value MATCHES '.*!\\\\[.*'"), evaluatedWith: textEditor) waitForExpectations(timeout: 15, handler: nil) - // Second upload — wait until two markdown references are present. + // Second upload, wait until two markdown references are present. imageButton.tap() let twoRefs = NSPredicate(format: "value MATCHES '(.*!\\\\[.*){2,}'") expectation(for: twoRefs, evaluatedWith: textEditor) @@ -120,7 +120,7 @@ final class AttachmentUITests: ForgejoUITestBase { XCTAssertTrue(commentButton.waitForExistence(timeout: 5)) commentButton.tap() - // Tap the file upload button — in test mode this uploads a hardcoded server.log. + // Tap the file upload button, in test mode this uploads a hardcoded server.log. let fileButton = app.buttons["markdown-toolbar-file"] XCTAssertTrue(fileButton.waitForExistence(timeout: 5)) let textEditor = app.textViews["markdown-text-editor"].firstMatch @@ -132,7 +132,7 @@ final class AttachmentUITests: ForgejoUITestBase { waitForExpectations(timeout: 15, handler: nil) // A non-image attachment must use link syntax [name](url), NOT image-embed - // syntax ![name](url) — otherwise it would render as a broken inline image. + // syntax ![name](url), otherwise it would render as a broken inline image. let editorValue = textEditor.value as? String ?? "" XCTAssertTrue( editorValue.contains("[server.log]"), diff --git a/Forji/ForjiUITests/ForgejoReadOnlyUITestBase.swift b/Forji/ForjiUITests/ForgejoReadOnlyUITestBase.swift index ec8f555..e011d6d 100644 --- a/Forji/ForjiUITests/ForgejoReadOnlyUITestBase.swift +++ b/Forji/ForjiUITests/ForgejoReadOnlyUITestBase.swift @@ -14,7 +14,7 @@ class ForgejoReadOnlyUITestBase: XCTestCase, UITestNavigating { guard let url = ForgejoUITestBase.resolveTestServerURL(), !url.isEmpty else { - // Can't XCTSkip from class setUp — individual tests will skip + // Can't XCTSkip from class setUp, individual tests will skip return } sharedServerURL = url @@ -60,7 +60,7 @@ class ForgejoReadOnlyUITestBase: XCTestCase, UITestNavigating { // Ensure we're at the home screen (Repositories tab as anchor) let reposTab = app.tabBars.buttons["Repositories"] if !reposTab.waitForExistence(timeout: 5) { - // Recovery: app may have crashed or navigated away — relaunch + // Recovery: app may have crashed or navigated away, relaunch let freshApp = XCUIApplication() freshApp.launchArguments += [ "-dev_serverURL", Self.sharedServerURL, diff --git a/Forji/ForjiUITests/ForjiUITestsLaunchTests.swift b/Forji/ForjiUITests/ForjiUITestsLaunchTests.swift index bdb3d7a..b9c7d97 100644 --- a/Forji/ForjiUITests/ForjiUITestsLaunchTests.swift +++ b/Forji/ForjiUITests/ForjiUITestsLaunchTests.swift @@ -9,7 +9,7 @@ final class ForjiUITestsLaunchTests: XCTestCase { override func setUpWithError() throws { continueAfterFailure = false - // Skip during integration test runs — these tests need manual credentials + // Skip during integration test runs, these tests need manual credentials if FileManager.default.fileExists(atPath: "/tmp/forgejo_test_url.txt") { throw XCTSkip("Skipping launch tests during integration test run") } diff --git a/Forji/ForjiUITests/IssueMutatingUITests.swift b/Forji/ForjiUITests/IssueMutatingUITests.swift index 8c37279..84f8ecc 100644 --- a/Forji/ForjiUITests/IssueMutatingUITests.swift +++ b/Forji/ForjiUITests/IssueMutatingUITests.swift @@ -26,7 +26,7 @@ final class IssueMutatingUITests: ForgejoUITestBase { openButton.tap() XCTAssertTrue(app.staticTexts["Test issue 1 from integration tests"].waitForExistence(timeout: 10)) - // Issue detail — title, comments, label, milestone, assignee + // Issue detail, title, comments, label, milestone, assignee let issueCell = app.staticTexts["Test issue 1 from integration tests"].firstMatch issueCell.tap() diff --git a/Forji/ForjiUITests/MergedInstanceUITests.swift b/Forji/ForjiUITests/MergedInstanceUITests.swift index daba8ab..9cb98a8 100644 --- a/Forji/ForjiUITests/MergedInstanceUITests.swift +++ b/Forji/ForjiUITests/MergedInstanceUITests.swift @@ -36,7 +36,7 @@ final class MergedInstanceUITests: ForgejoUITestBase { XCTAssertTrue(addButton.waitForExistence(timeout: 15), "Instance list did not appear") addButton.tap() - // Form is pre-filled from dev launch args — scroll and tap login + // Form is pre-filled from dev launch args, scroll and tap login app.swipeUp() let loginButton = app.buttons["login-button"] diff --git a/Forji/ForjiUITests/OverviewCreateMutatingUITests.swift b/Forji/ForjiUITests/OverviewCreateMutatingUITests.swift index a5200e1..d434f0f 100644 --- a/Forji/ForjiUITests/OverviewCreateMutatingUITests.swift +++ b/Forji/ForjiUITests/OverviewCreateMutatingUITests.swift @@ -2,7 +2,7 @@ import XCTest final class OverviewCreateMutatingUITests: ForgejoUITestBase { - // MARK: - Create Issue from Issues Overview (mutates — creates an issue) + // MARK: - Create Issue from Issues Overview (mutates, creates an issue) @MainActor func testCreateIssueFromOverview() throws { diff --git a/Forji/ForjiUITests/OverviewCreateUITests.swift b/Forji/ForjiUITests/OverviewCreateUITests.swift index 00d87a1..fe2dc4c 100644 --- a/Forji/ForjiUITests/OverviewCreateUITests.swift +++ b/Forji/ForjiUITests/OverviewCreateUITests.swift @@ -13,7 +13,7 @@ final class OverviewCreateUITests: ForgejoReadOnlyUITestBase { func testCreatePRFromOverview() throws { app.tabBars.buttons["Pull Requests"].tap() - // Default scope is "All involvement" — testadmin is assigned to PR #4 + // Default scope is "All involvement", testadmin is assigned to PR #4 XCTAssertTrue(app.staticTexts["Add feature file"].waitForExistence(timeout: 10)) // Tap floating create button diff --git a/Forji/ForjiUITests/PaginationUITests.swift b/Forji/ForjiUITests/PaginationUITests.swift index 9f4bae5..6cc1cf2 100644 --- a/Forji/ForjiUITests/PaginationUITests.swift +++ b/Forji/ForjiUITests/PaginationUITests.swift @@ -53,7 +53,7 @@ final class PaginationUITests: ForgejoReadOnlyUITestBase { XCTAssertTrue(closedButton.waitForExistence(timeout: 5)) closedButton.tap() - // Only 1 closed issue (#3) — should not show load-more + // Only 1 closed issue (#3), should not show load-more XCTAssertTrue(app.staticTexts["Test issue 3 from integration tests"].waitForExistence(timeout: 10)) let loadMore = app.activityIndicators["load-more-indicator"] diff --git a/Forji/ForjiUITests/PermissionUITests.swift b/Forji/ForjiUITests/PermissionUITests.swift index 63c753e..b51fab9 100644 --- a/Forji/ForjiUITests/PermissionUITests.swift +++ b/Forji/ForjiUITests/PermissionUITests.swift @@ -14,7 +14,7 @@ final class PermissionUITests: ForgejoUITestBase { ] } - // MARK: - Issue Detail — read-only user + // MARK: - Issue Detail, read-only user @MainActor func testIssueDetailHidesActionsForReadOnlyUser() throws { @@ -43,7 +43,7 @@ final class PermissionUITests: ForgejoUITestBase { XCTAssertFalse(toggleStateButton.exists, "Close/Reopen button should be hidden for read-only user") } - // MARK: - PR Detail — read-only user + // MARK: - PR Detail, read-only user @MainActor func testPRDetailHidesActionsForReadOnlyUser() throws { diff --git a/Forji/ForjiUITests/PullRequestUITests.swift b/Forji/ForjiUITests/PullRequestUITests.swift index 7e2b82f..bfe16bd 100644 --- a/Forji/ForjiUITests/PullRequestUITests.swift +++ b/Forji/ForjiUITests/PullRequestUITests.swift @@ -29,7 +29,7 @@ final class PullRequestUITests: ForgejoReadOnlyUITestBase { openButton.tap() XCTAssertTrue(app.staticTexts["Add feature file"].waitForExistence(timeout: 10)) - // PR detail — title, branches + // PR detail, title, branches let prCell = app.staticTexts["Add feature file"].firstMatch prCell.tap() @@ -55,7 +55,7 @@ final class PullRequestUITests: ForgejoReadOnlyUITestBase { XCTAssertTrue(app.staticTexts["testbot"].waitForExistence(timeout: 10)) } - // MARK: - Diff View — context line has no comment button + // MARK: - Diff View, context line has no comment button @MainActor func testDiffViewContextLineHasNoCommentButton() throws { diff --git a/Forji/ForjiUITests/RepositoryMutatingUITests.swift b/Forji/ForjiUITests/RepositoryMutatingUITests.swift index cf0d462..5d59a2e 100644 --- a/Forji/ForjiUITests/RepositoryMutatingUITests.swift +++ b/Forji/ForjiUITests/RepositoryMutatingUITests.swift @@ -33,7 +33,7 @@ final class RepositoryMutatingUITests: ForgejoUITestBase { toggleButton.tap() toggleButton.tap() - // File viewer — tap hello.py + // File viewer, tap hello.py let fileCell = app.staticTexts["hello.py"].firstMatch XCTAssertTrue(fileCell.waitForExistence(timeout: 10)) fileCell.tap() diff --git a/Forji/ForjiUITests/RepositoryUITests.swift b/Forji/ForjiUITests/RepositoryUITests.swift index c533568..87f5495 100644 --- a/Forji/ForjiUITests/RepositoryUITests.swift +++ b/Forji/ForjiUITests/RepositoryUITests.swift @@ -38,7 +38,7 @@ final class RepositoryUITests: ForgejoReadOnlyUITestBase { searchField.buttons["Clear text"].tap() XCTAssertTrue(app.staticTexts["test-repo"].waitForExistence(timeout: 10)) - // Star toggle (last — tap triggers NavigationLink, only tests tappability) + // Star toggle (last, tap triggers NavigationLink, only tests tappability) let starButton = repoList.buttons["star-button"].firstMatch XCTAssertTrue(starButton.waitForExistence(timeout: 5), "No star button found in repo list") starButton.tap() @@ -130,7 +130,7 @@ final class RepositoryUITests: ForgejoReadOnlyUITestBase { // Wait for repo detail to load XCTAssertTrue(app.staticTexts["html-readme-repo"].waitForExistence(timeout: 10)) - // Raw HTML tags should NOT appear as plain text — they should be rendered + // Raw HTML tags should NOT appear as plain text, they should be rendered XCTAssertFalse( app.staticTexts["
"].waitForExistence(timeout: 5), "Raw
tag should not be visible as plain text") diff --git a/README.md b/README.md index baa236c..c5c3d1d 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Forji is available on the Apple App Store for free: [Forji App Store](https://ap - Create, edit, and close/reopen issues - Manage labels, milestones, and assignees - Comment with Markdown support +- Attach and view image and file attachments in descriptions and comments ### Pull Requests - View PRs across all repositories or per-repo @@ -47,6 +48,7 @@ Forji is available on the Apple App Store for free: [Forji App Store](https://ap - Submit reviews (comment, approve, request changes) - Merge with merge commit, rebase, or squash - Close, reopen, and edit PRs +- Attach and view image and file attachments in descriptions and comments ### Actions - Browse Forgejo Actions workflows defined in a repository @@ -110,13 +112,13 @@ The Forji logo is based on the [Forgejo logo](https://codeberg.org/forgejo/forge ### Libraries -- [ForgejoKit](https://codeberg.org/secana/ForgejoKit) — Forgejo API client (MIT) -- [Textual](https://github.com/gonzalezreal/textual) — Markdown rendering (MIT) -- [HighlightSwift](https://github.com/appstefan/HighlightSwift) — Code syntax highlighting (MIT) -- [mermaid](https://github.com/mermaid-js/mermaid) — Diagram rendering (MIT) -- [marked](https://github.com/markedjs/marked) — Markdown parser (MIT) -- [DOMPurify](https://github.com/cure53/DOMPurify) — HTML sanitizer (Apache 2.0 / MPL 2.0) -- [github-markdown-css](https://github.com/sindresorhus/github-markdown-css) — GitHub-style Markdown styling (MIT) +- [ForgejoKit](https://codeberg.org/secana/ForgejoKit), Forgejo API client (MIT) +- [Textual](https://github.com/gonzalezreal/textual), Markdown rendering (MIT) +- [HighlightSwift](https://github.com/appstefan/HighlightSwift), Code syntax highlighting (MIT) +- [mermaid](https://github.com/mermaid-js/mermaid), Diagram rendering (MIT) +- [marked](https://github.com/markedjs/marked), Markdown parser (MIT) +- [DOMPurify](https://github.com/cure53/DOMPurify), HTML sanitizer (Apache 2.0 / MPL 2.0) +- [github-markdown-css](https://github.com/sindresorhus/github-markdown-css), GitHub-style Markdown styling (MIT) ## Contributing