Commit ebe0e42d13

ebe0e42d13a7ff34da3947a8023e5023dbf793f9

parent: 4e6a7fb976

Unsigned

cmc <hello@cleberg.net> · 2026-09-07 18:40 UTC

Live suite: run green against today's forge (!80)

All 14 live smoke tests pass on iPhone 17 Pro against gitbay.org.

App: the access grant row uses a text button beside a segmented role picker (the icon beside a menu picker never received the tap); topic chips carry settings-remove-topic-<name> so a test can remove the one it added.

Tests: scroll for rows below the fold (owner link, activity graph, repositories, release assets, commit hunk, settings link); take the newest release rather than v1.0.0; relaunch before the bookmarks list since an active search hides the toolbar; undo a leftover bookmark from a dead run; use https://example.com for the webhook fixture because the server resolves the host before accepting it.

Layout: unified · split

gitbay/Views/Repos/RepoSettingsView.swift +17 −16
@@ -137,6 +137,7 @@ struct RepoSettingsView: View {
137137 .foregroundStyle(.tertiary)
138138 }
139139 .disabled(model.working)
140 .accessibilityIdentifier("settings-remove-topic-\(topic)")
140141 }
141142 .font(.gbSans(.caption))
142143 .foregroundStyle(Color.gbAccent)
@@ -281,27 +282,27 @@ struct RepoSettingsView: View {
281282 }
282283 }
283284 }
284 HStack {
285 VStack(spacing: 8) {
285286 TextField("Grant user", text: $newGrantUser)
286287 .autocorrectionDisabled()
287288 .textInputAutocapitalization(.never)
288289 .accessibilityIdentifier("settings-grant-user")
289 Picker("", selection: $newGrantRole) {
290 Text("read").tag("read")
291 Text("write").tag("write")
292 Text("admin").tag("admin")
293 }
294 .labelsHidden()
295 .fixedSize()
296 Button {
297 let user = newGrantUser
298 newGrantUser = ""
299 Task { await model.grant(user: user, role: newGrantRole) }
300 } label: {
301 Image(systemName: "plus.circle.fill")
290 HStack {
291 Picker("Role", selection: $newGrantRole) {
292 Text("read").tag("read")
293 Text("write").tag("write")
294 Text("admin").tag("admin")
295 }
296 .pickerStyle(.segmented)
297 Button("Grant") {
298 let user = newGrantUser
299 newGrantUser = ""
300 Task { await model.grant(user: user, role: newGrantRole) }
301 }
302 .buttonStyle(.borderless)
303 .disabled(newGrantUser.trimmingCharacters(in: .whitespaces).isEmpty || model.working)
304 .accessibilityIdentifier("settings-grant-submit")
302305 }
303 .disabled(newGrantUser.trimmingCharacters(in: .whitespaces).isEmpty || model.working)
304 .accessibilityIdentifier("settings-grant-submit")
305306 }
306307 } header: {
307308 Text("Access")
gitbayUITests/LiveSmokeUITests.swift +67 −35
@@ -457,10 +457,10 @@ extension LiveSmokeUITests {
457457 let hookURL = app.descendants(matching: .any)
458458 .matching(identifier: "webhook-url").firstMatch
459459 XCTAssertTrue(hookURL.waitForExistence(timeout: 5), "webhook sheet did not open")
460 focusAndType(hookURL, "https://example.invalid/ui-smoke")
460 focusAndType(hookURL, "https://example.com/ui-smoke")
461461 app.descendants(matching: .any).matching(identifier: "webhook-submit")
462462 .firstMatch.tap()
463 let hookRow = app.staticTexts["https://example.invalid/ui-smoke"].firstMatch
463 let hookRow = app.staticTexts["https://example.com/ui-smoke"].firstMatch
464464 XCTAssertTrue(hookRow.waitForExistence(timeout: 15), "webhook not listed")
465465 hookRow.swipeLeft()
466466 app.buttons["Remove"].firstMatch.tap()
@@ -495,18 +495,32 @@ extension LiveSmokeUITests {
495495 unpin.tap()
496496
497497 // Bookmark it, find it in the bookmarks list, then unbookmark.
498 // A run that died between the two leaves it bookmarked; undo
499 // that first so the step starts from the same state every time.
498500 menu.tap()
501 let leftover = app.buttons["Unbookmark"].firstMatch
502 if leftover.waitForExistence(timeout: 3) {
503 leftover.tap()
504 XCTAssertTrue(waitForDisappearance(leftover, timeout: 10))
505 menu.tap()
506 }
499507 let bookmark = app.buttons["Bookmark"].firstMatch
500508 XCTAssertTrue(bookmark.waitForExistence(timeout: 5), "Bookmark action missing")
501509 bookmark.tap()
502 back() // repo -> list
503 app.descendants(matching: .any).matching(identifier: "repo-bookmarks-button")
504 .firstMatch.tap()
505 XCTAssertTrue(app.staticTexts["krz/gitbay-ios"].firstMatch
506 .waitForExistence(timeout: 15), "bookmarked repo not in the bookmarks list")
507 back() // bookmarks -> list
508 XCTAssertTrue(repoRow.waitForExistence(timeout: 10))
509 repoRow.tap()
510 // An active search hides the list's toolbar, so start the list
511 // fresh: relaunching lands on a list with the bookmarks button.
512 app.terminate()
513 app.launch()
514 selectTab("Repositories")
515 let bookmarksButton = app.descendants(matching: .any)
516 .matching(identifier: "repo-bookmarks-button").firstMatch
517 XCTAssertTrue(bookmarksButton.waitForExistence(timeout: 15), "bookmarks button missing")
518 bookmarksButton.tap()
519 let bookmarkedRow = app.staticTexts["krz/gitbay-ios"].firstMatch
520 XCTAssertTrue(bookmarkedRow.waitForExistence(timeout: 15),
521 "bookmarked repo not in the bookmarks list")
522 // Open it from the bookmarks list and undo the bookmark there.
523 bookmarkedRow.tap()
510524 XCTAssertTrue(menu.waitForExistence(timeout: 10))
511525 menu.tap()
512526 let unbookmark = app.buttons["Unbookmark"].firstMatch
@@ -514,7 +528,11 @@ extension LiveSmokeUITests {
514528 unbookmark.tap()
515529
516530 // --- settings: topic and merge-rule round-trips ---
517 app.staticTexts["Settings"].firstMatch.tap()
531 // The links section can sit below the README fold, and the row's
532 // label is not always exposed as a static text.
533 let settingsLink = app.descendants(matching: .any)["Settings"].firstMatch
534 XCTAssertTrue(scrollTo(settingsLink), "settings link not reachable")
535 settingsLink.tap()
518536 let addTopic = app.descendants(matching: .any)
519537 .matching(identifier: "settings-add-topic").firstMatch
520538 XCTAssertTrue(addTopic.waitForExistence(timeout: 10), "settings did not load")
@@ -523,9 +541,10 @@ extension LiveSmokeUITests {
523541 .firstMatch.tap()
524542 let chip = app.staticTexts["ios"].firstMatch
525543 XCTAssertTrue(chip.waitForExistence(timeout: 10), "topic did not appear")
526 // Remove it again: the chip's own x button is the next button,
527 // and it asks before removing.
528 app.scrollViews.buttons.firstMatch.tap()
544 // Remove it again through the chip's own x button — by name, since
545 // the repository carries other topics — and it asks first.
546 app.descendants(matching: .any).matching(identifier: "settings-remove-topic-ios")
547 .firstMatch.tap()
529548 let confirmRemove = app.buttons["Remove"].firstMatch
530549 XCTAssertTrue(confirmRemove.waitForExistence(timeout: 5), "topic remove confirmation missing")
531550 confirmRemove.tap()
@@ -623,8 +642,11 @@ extension LiveSmokeUITests {
623642 XCTAssertTrue(app.staticTexts["Files"].firstMatch.waitForExistence(timeout: 10))
624643
625644 // --- owner profile from the repo screen ---
626 // The row's label merges; match it at any element type.
627 app.descendants(matching: .any)["krz"].firstMatch.tap()
645 // The row's label merges; match it at any element type. It is
646 // the last row of the links section, below the fold.
647 let ownerLink = app.descendants(matching: .any)["krz"].firstMatch
648 XCTAssertTrue(scrollTo(ownerLink), "owner link not reachable")
649 ownerLink.tap()
628650 XCTAssertTrue(app.staticTexts["warez for the public"].firstMatch
629651 .waitForExistence(timeout: 15), "profile did not load")
630652 XCTAssertTrue(app.staticTexts
@@ -652,15 +674,20 @@ extension LiveSmokeUITests {
652674 openRepo("krz/gitbay")
653675 app.staticTexts["Releases"].firstMatch.tap()
654676
655 let row = app.staticTexts
656 .containing(NSPredicate(format: "label CONTAINS 'v1.0.0'")).firstMatch
677 // Newest first, and the list has grown past the fold, so the
678 // first row is whichever release is current.
679 let row = app.cells.firstMatch
657680 XCTAssertTrue(row.waitForExistence(timeout: 15), "release list empty")
658681 row.tap()
659682
660 // Notes render and assets carry sizes.
661 XCTAssertTrue(app.staticTexts
662 .containing(NSPredicate(format: "label CONTAINS 'SHA256SUMS'")).firstMatch
663 .waitForExistence(timeout: 15), "assets missing")
683 // Notes render and assets carry sizes. The assets sit under the
684 // notes, which can run past the fold.
685 XCTAssertTrue(app.descendants(matching: .any)
686 .matching(identifier: "release-edit-button").firstMatch
687 .waitForExistence(timeout: 15), "release did not open")
688 XCTAssertTrue(scrollTo(app.staticTexts
689 .containing(NSPredicate(format: "label CONTAINS 'SHA256SUMS'")).firstMatch, swipes: 10),
690 "assets missing")
664691
665692 // Edit sheet prefills; saving unchanged content round-trips.
666693 app.descendants(matching: .any).matching(identifier: "release-edit-button")
@@ -668,8 +695,8 @@ extension LiveSmokeUITests {
668695 let title = app.descendants(matching: .any)
669696 .matching(identifier: "compose-title").firstMatch
670697 XCTAssertTrue(title.waitForExistence(timeout: 5), "edit sheet did not open")
671 XCTAssertTrue((title.value as? String)?.contains("v1.0.0") == true,
672 "edit sheet did not prefill")
698 XCTAssertFalse((title.value as? String ?? "").isEmpty,
699 "edit sheet did not prefill")
673700 app.descendants(matching: .any).matching(identifier: "compose-submit")
674701 .firstMatch.tap()
675702 XCTAssertTrue(waitForDisappearance(title, timeout: 15),
@@ -986,12 +1013,6 @@ extension LiveSmokeUITests {
9861013 XCTAssertTrue(app.staticTexts["Organizations"].firstMatch.exists,
9871014 "org memberships missing")
9881015 XCTAssertTrue(app.staticTexts["krz"].firstMatch.exists, "org row missing")
989 XCTAssertTrue(app.staticTexts
990 .containing(NSPredicate(format: "label CONTAINS 'contributions in the last year'")).firstMatch
991 .exists, "activity graph missing")
992 XCTAssertTrue(app.staticTexts
993 .containing(NSPredicate(format: "label BEGINSWITH 'Repositories'")).firstMatch
994 .exists, "repositories missing")
9951016
9961017 // --- an org admin can edit the org's profile ---
9971018 app.staticTexts["krz"].firstMatch.tap()
@@ -1000,6 +1021,15 @@ extension LiveSmokeUITests {
10001021 .waitForExistence(timeout: 15), "org profile offers no edit to its admin")
10011022 back()
10021023
1024 // The graph and the repositories sit below the orgs and links,
1025 // and a List does not build rows it has not shown.
1026 XCTAssertTrue(scrollTo(app.staticTexts
1027 .containing(NSPredicate(format: "label CONTAINS 'contributions in the last year'")).firstMatch),
1028 "activity graph missing")
1029 XCTAssertTrue(scrollTo(app.staticTexts
1030 .containing(NSPredicate(format: "label BEGINSWITH 'Repositories'")).firstMatch),
1031 "repositories missing")
1032
10031033 // --- a log entry opens its commit ---
10041034 app.terminate()
10051035 app.launch()
@@ -1009,11 +1039,13 @@ extension LiveSmokeUITests {
10091039 XCTAssertTrue(firstCommit.waitForExistence(timeout: 20), "history is empty")
10101040 firstCommit.tap()
10111041 // The commit screen IS the patch: no navigating away to find it.
1012 // A hunk header (@@) only appears in a rendered diff.
1013 XCTAssertTrue(app.staticTexts
1042 // A hunk header (@@) only appears in a rendered diff, and sits
1043 // below the message and the file list.
1044 let hunk = app.staticTexts
10141045 .containing(NSPredicate(format: "label BEGINSWITH '@@'")).firstMatch
1015 .waitForExistence(timeout: 20),
1016 "commit screen did not render the patch inline")
1046 _ = hunk.waitForExistence(timeout: 20)
1047 XCTAssertTrue(scrollTo(hunk, swipes: 10),
1048 "commit screen did not render the patch inline")
10171049 }
10181050}
10191051
@@ -1202,7 +1234,7 @@ extension LiveSmokeUITests {
12021234
12031235 selectTab("My Profile")
12041236 XCTAssertTrue(app.staticTexts
1205 .containing(NSPredicate(format: "label BEGINSWITH 'Repositories'")).firstMatch
1237 .containing(NSPredicate(format: "label CONTAINS 'Self-Hosting'")).firstMatch
12061238 .waitForExistence(timeout: 20), "profile did not load")
12071239 snap("profile")
12081240 }