From 57bde0934ffdce25b2cb79f058de973fe9643f39 Mon Sep 17 00:00:00 2001 From: Stefan Hausotte Date: Thu, 7 May 2026 19:17:49 +0200 Subject: [PATCH] refactor: small name changes --- Forji/Forji/Helpers/WorkflowStatus.swift | 9 ++++--- Forji/Forji/Views/WorkflowJobView.swift | 27 +++++++++++++------ Forji/Forji/Views/WorkflowRunDetailView.swift | 2 -- .../ForjiTests/WorkflowStatusIconTests.swift | 26 +++++++++++------- Forji/ForjiUITests/ActionsUITests.swift | 8 +++--- README.md | 2 +- 6 files changed, 46 insertions(+), 28 deletions(-) diff --git a/Forji/Forji/Helpers/WorkflowStatus.swift b/Forji/Forji/Helpers/WorkflowStatus.swift index 950a68e..c8f8b29 100644 --- a/Forji/Forji/Helpers/WorkflowStatus.swift +++ b/Forji/Forji/Helpers/WorkflowStatus.swift @@ -35,10 +35,11 @@ struct WorkflowStatusStyle { let isAnimated: Bool let label: String - /// Maps Forgejo's single status value to a UI style. Forgejo's known + /// 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 /// statuses: success, failure, cancelled, skipped, running, waiting, /// blocked, unknown. Anything else falls back to a question-mark glyph. - static func forRun(status: String) -> WorkflowStatusStyle { + static func forStatus(_ status: String) -> WorkflowStatusStyle { switch status { case "success": .init(symbol: "checkmark.circle.fill", tint: .green, isAnimated: false, label: "Success") @@ -66,7 +67,7 @@ struct WorkflowStatusIcon: View { let status: String var body: some View { - let style = WorkflowStatusStyle.forRun(status: status) + let style = WorkflowStatusStyle.forStatus(status) Image(systemName: style.symbol) .foregroundStyle(style.tint) .symbolEffect( @@ -79,7 +80,7 @@ struct WorkflowStatusIcon: View { extension WorkflowRun { var statusStyle: WorkflowStatusStyle { - WorkflowStatusStyle.forRun(status: status) + WorkflowStatusStyle.forStatus(status) } var shortCommitSha: String? { diff --git a/Forji/Forji/Views/WorkflowJobView.swift b/Forji/Forji/Views/WorkflowJobView.swift index dff0f99..936b3fd 100644 --- a/Forji/Forji/Views/WorkflowJobView.swift +++ b/Forji/Forji/Views/WorkflowJobView.swift @@ -12,6 +12,7 @@ struct WorkflowJobView: View { @State private var authService: AuthenticationService @State private var view: WorkflowRunView? + @State private var stepLogs: [Int: [WorkflowRunViewLogLine]] = [:] @State private var isLoading = false @State private var errorMessage: String? @State private var showError = false @@ -37,7 +38,7 @@ struct WorkflowJobView: View { var body: some View { List { if let view { - stepsSection(steps: view.state.currentJob.steps, logs: view.logs.stepsLog) + stepsSection(steps: view.state.currentJob.steps) } else if isLoading { LoadingListSection() } else { @@ -64,7 +65,7 @@ struct WorkflowJobView: View { } @ViewBuilder - private func stepsSection(steps: [WorkflowRunViewStep], logs: [WorkflowRunViewStepLog]) -> some View { + private func stepsSection(steps: [WorkflowRunViewStep]) -> some View { if steps.isEmpty { Section { ContentUnavailableView { @@ -92,7 +93,7 @@ struct WorkflowJobView: View { } }, ), - logLines: logs.first(where: { $0.step == index })?.lines ?? [], + logLines: stepLogs[index] ?? [], ) .accessibilityIdentifier("workflow-step-\(index)") } @@ -101,7 +102,7 @@ struct WorkflowJobView: View { } footer: { Text( "Steps and logs come from Forgejo's web UI endpoint, not the public API. " - + "Output may change between Forgejo versions.", + + "Output may change between Forgejo versions.", ) .font(.caption2) .foregroundStyle(.tertiary) @@ -125,13 +126,15 @@ struct WorkflowJobView: View { errorMessage = nil do { - view = try await workflowService.fetchRunView( + let fetched = try await workflowService.fetchRunView( owner: repository.owner, repo: repository.repoName, runIndex: runIndex, jobIndex: jobIndex, logCursors: [], ) + view = fetched + mergeLogs(from: fetched) hasLoaded = true } catch is CancellationError { // Ignore cancellation @@ -146,17 +149,19 @@ struct WorkflowJobView: View { private func loadLogs(for step: Int) async { guard let workflowService else { return } // Already loaded? - if view?.logs.stepsLog.contains(where: { $0.step == step }) == true { return } + if stepLogs[step] != nil { return } do { - let updated = try await workflowService.fetchRunView( + let fetched = try await workflowService.fetchRunView( owner: repository.owner, repo: repository.repoName, runIndex: runIndex, jobIndex: jobIndex, logCursors: [WorkflowLogCursor(step: step, cursor: 0, expanded: true)], ) - view = updated + // Each fetch returns logs only for the requested cursor(s); merge + // into our running map so previously-expanded steps don't go blank. + mergeLogs(from: fetched) } catch is CancellationError { // Ignore } catch { @@ -164,6 +169,12 @@ struct WorkflowJobView: View { showError = true } } + + private func mergeLogs(from runView: WorkflowRunView) { + for stepLog in runView.logs.stepsLog { + stepLogs[stepLog.step] = stepLog.lines + } + } } private struct StepDisclosureRow: View { diff --git a/Forji/Forji/Views/WorkflowRunDetailView.swift b/Forji/Forji/Views/WorkflowRunDetailView.swift index 86bf4f8..ed6f0cf 100644 --- a/Forji/Forji/Views/WorkflowRunDetailView.swift +++ b/Forji/Forji/Views/WorkflowRunDetailView.swift @@ -55,7 +55,6 @@ struct WorkflowRunDetailView: View { .errorAlert(message: $errorMessage, isPresented: $showError) } - @ViewBuilder private func headerSection(run: WorkflowRun) -> some View { Section { HStack(spacing: 10) { @@ -89,7 +88,6 @@ struct WorkflowRunDetailView: View { } } - @ViewBuilder private func metadataSection(run: WorkflowRun) -> some View { Section("Details") { if let workflow = run.workflowId { diff --git a/Forji/ForjiTests/WorkflowStatusIconTests.swift b/Forji/ForjiTests/WorkflowStatusIconTests.swift index 63b79d8..c969c46 100644 --- a/Forji/ForjiTests/WorkflowStatusIconTests.swift +++ b/Forji/ForjiTests/WorkflowStatusIconTests.swift @@ -7,51 +7,51 @@ import Testing struct WorkflowStatusIconTests { @Test func successShowsCheckmark() { - let style = WorkflowStatusStyle.forRun(status: "success") + let style = WorkflowStatusStyle.forStatus("success") #expect(style.symbol == "checkmark.circle.fill") #expect(style.isAnimated == false) #expect(style.label == "Success") } @Test func failureShowsXmark() { - let style = WorkflowStatusStyle.forRun(status: "failure") + let style = WorkflowStatusStyle.forStatus("failure") #expect(style.symbol == "xmark.circle.fill") #expect(style.label == "Failed") } @Test func cancelledShowsSlash() { - let style = WorkflowStatusStyle.forRun(status: "cancelled") + let style = WorkflowStatusStyle.forStatus("cancelled") #expect(style.symbol == "slash.circle.fill") } @Test func skippedShowsForward() { - let style = WorkflowStatusStyle.forRun(status: "skipped") + let style = WorkflowStatusStyle.forStatus("skipped") #expect(style.symbol == "forward.circle.fill") } @Test func runningIsAnimated() { - let style = WorkflowStatusStyle.forRun(status: "running") + let style = WorkflowStatusStyle.forStatus("running") #expect(style.symbol == "circle.dotted") #expect(style.isAnimated == true) } @Test func waitingShowsClock() { - let style = WorkflowStatusStyle.forRun(status: "waiting") + let style = WorkflowStatusStyle.forStatus("waiting") #expect(style.symbol == "clock") } @Test func blockedShowsHand() { - let style = WorkflowStatusStyle.forRun(status: "blocked") + let style = WorkflowStatusStyle.forStatus("blocked") #expect(style.symbol == "hand.raised.fill") } @Test func unknownStatusShowsQuestionMark() { - let style = WorkflowStatusStyle.forRun(status: "unknown") + let style = WorkflowStatusStyle.forStatus("unknown") #expect(style.symbol == "questionmark.circle") } @Test func arbitraryStatusFallsBackToQuestionMark() { - let style = WorkflowStatusStyle.forRun(status: "weird") + let style = WorkflowStatusStyle.forStatus("weird") #expect(style.symbol == "questionmark.circle") #expect(style.label == "Weird") } @@ -92,6 +92,14 @@ struct WorkflowStatusIconTests { #expect(run.displayName == "ci.yml") } + @Test func displayNameFallsBackWhenTitleIsEmpty() { + let run = WorkflowRun( + id: 1, title: "", status: "running", + workflowId: "ci.yml", indexInRepo: 5, + ) + #expect(run.displayName == "ci.yml") + } + @Test func displayNameFallsBackToIndex() { let run = WorkflowRun( id: 1, title: nil, status: "running", indexInRepo: 7, diff --git a/Forji/ForjiUITests/ActionsUITests.swift b/Forji/ForjiUITests/ActionsUITests.swift index 4d90b55..0353404 100644 --- a/Forji/ForjiUITests/ActionsUITests.swift +++ b/Forji/ForjiUITests/ActionsUITests.swift @@ -8,8 +8,8 @@ final class ActionsUITests: ForgejoReadOnlyUITestBase { } /// The Actions tab is only present for repos with `has_actions: true`. - /// Until the seed enables actions on a test repo, this checks that — when - /// the tab appears at all — selecting it renders the section picker. + /// The seed leaves `test-repo` at the Forgejo-15 default (actions enabled), + /// so the tab is expected to render and produce the workflow status filter. @MainActor func testActionsTabRendersWhenAvailable() throws { navigateToRepoDetail("test-repo") @@ -18,9 +18,9 @@ final class ActionsUITests: ForgejoReadOnlyUITestBase { XCTAssertTrue(tabPicker.waitForExistence(timeout: 10), "Tab picker should appear") let actionsTab = tabPicker.buttons["Actions"] - try XCTSkipUnless( + XCTAssertTrue( actionsTab.waitForExistence(timeout: 3), - "Actions tab not exposed on this seed repo (has_actions disabled)", + "Actions tab should be exposed for test-repo (Forgejo 15 default has_actions: true)", ) actionsTab.tap() diff --git a/README.md b/README.md index 3f86624..867383b 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Foji is availbe on the Apple App Store for free: [Forji App Store](https://apps. ### Actions - Browse Forgejo Actions workflows defined in a repository -- View workflow runs filtered by status (running, completed, queued) +- View workflow runs filtered by status (running, success, failed) - Inspect a run's jobs with status and duration - Read job logs