refactor: small name changes

This commit is contained in:
Stefan Hausotte 2026-05-07 19:17:49 +02:00
parent 2e07f6da4d
commit 57bde0934f
6 changed files with 46 additions and 28 deletions

View file

@ -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? {

View file

@ -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 {

View file

@ -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 {

View file

@ -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,

View file

@ -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()

View file

@ -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