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 {
137 .foregroundStyle(.tertiary) 137 .foregroundStyle(.tertiary)
138 } 138 }
139 .disabled(model.working) 139 .disabled(model.working)
140 .accessibilityIdentifier("settings-remove-topic-\(topic)")
140 } 141 }
141 .font(.gbSans(.caption)) 142 .font(.gbSans(.caption))
142 .foregroundStyle(Color.gbAccent) 143 .foregroundStyle(Color.gbAccent)
@@ -281,27 +282,27 @@ struct RepoSettingsView: View {
281 } 282 }
282 } 283 }
283 } 284 }
284 HStack { 285 VStack(spacing: 8) {
285 TextField("Grant user", text: $newGrantUser) 286 TextField("Grant user", text: $newGrantUser)
286 .autocorrectionDisabled() 287 .autocorrectionDisabled()
287 .textInputAutocapitalization(.never) 288 .textInputAutocapitalization(.never)
288 .accessibilityIdentifier("settings-grant-user") 289 .accessibilityIdentifier("settings-grant-user")
289 Picker("", selection: $newGrantRole) { 290 HStack {
290 Text("read").tag("read") 291 Picker("Role", selection: $newGrantRole) {
291 Text("write").tag("write") 292 Text("read").tag("read")
292 Text("admin").tag("admin") 293 Text("write").tag("write")
293 } 294 Text("admin").tag("admin")
294 .labelsHidden() 295 }
295 .fixedSize() 296 .pickerStyle(.segmented)
296 Button { 297 Button("Grant") {
297 let user = newGrantUser 298 let user = newGrantUser
298 newGrantUser = "" 299 newGrantUser = ""
299 Task { await model.grant(user: user, role: newGrantRole) } 300 Task { await model.grant(user: user, role: newGrantRole) }
300 } label: { 301 }
301 Image(systemName: "plus.circle.fill") 302 .buttonStyle(.borderless)
303 .disabled(newGrantUser.trimmingCharacters(in: .whitespaces).isEmpty || model.working)
304 .accessibilityIdentifier("settings-grant-submit")
302 } 305 }
303 .disabled(newGrantUser.trimmingCharacters(in: .whitespaces).isEmpty || model.working)
304 .accessibilityIdentifier("settings-grant-submit")
305 } 306 }
306 } header: { 307 } header: {
307 Text("Access") 308 Text("Access")
gitbayUITests/LiveSmokeUITests.swift +67 −35
@@ -457,10 +457,10 @@ extension LiveSmokeUITests {
457 let hookURL = app.descendants(matching: .any) 457 let hookURL = app.descendants(matching: .any)
458 .matching(identifier: "webhook-url").firstMatch 458 .matching(identifier: "webhook-url").firstMatch
459 XCTAssertTrue(hookURL.waitForExistence(timeout: 5), "webhook sheet did not open") 459 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")
461 app.descendants(matching: .any).matching(identifier: "webhook-submit") 461 app.descendants(matching: .any).matching(identifier: "webhook-submit")
462 .firstMatch.tap() 462 .firstMatch.tap()
463 let hookRow = app.staticTexts["https://example.invalid/ui-smoke"].firstMatch 463 let hookRow = app.staticTexts["https://example.com/ui-smoke"].firstMatch
464 XCTAssertTrue(hookRow.waitForExistence(timeout: 15), "webhook not listed") 464 XCTAssertTrue(hookRow.waitForExistence(timeout: 15), "webhook not listed")
465 hookRow.swipeLeft() 465 hookRow.swipeLeft()
466 app.buttons["Remove"].firstMatch.tap() 466 app.buttons["Remove"].firstMatch.tap()
@@ -495,18 +495,32 @@ extension LiveSmokeUITests {
495 unpin.tap() 495 unpin.tap()
496 496
497 // Bookmark it, find it in the bookmarks list, then unbookmark. 497 // 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.
498 menu.tap() 500 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 }
499 let bookmark = app.buttons["Bookmark"].firstMatch 507 let bookmark = app.buttons["Bookmark"].firstMatch
500 XCTAssertTrue(bookmark.waitForExistence(timeout: 5), "Bookmark action missing") 508 XCTAssertTrue(bookmark.waitForExistence(timeout: 5), "Bookmark action missing")
501 bookmark.tap() 509 bookmark.tap()
502 back() // repo -> list 510 // An active search hides the list's toolbar, so start the list
503 app.descendants(matching: .any).matching(identifier: "repo-bookmarks-button") 511 // fresh: relaunching lands on a list with the bookmarks button.
504 .firstMatch.tap() 512 app.terminate()
505 XCTAssertTrue(app.staticTexts["krz/gitbay-ios"].firstMatch 513 app.launch()
506 .waitForExistence(timeout: 15), "bookmarked repo not in the bookmarks list") 514 selectTab("Repositories")
507 back() // bookmarks -> list 515 let bookmarksButton = app.descendants(matching: .any)
508 XCTAssertTrue(repoRow.waitForExistence(timeout: 10)) 516 .matching(identifier: "repo-bookmarks-button").firstMatch
509 repoRow.tap() 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()
510 XCTAssertTrue(menu.waitForExistence(timeout: 10)) 524 XCTAssertTrue(menu.waitForExistence(timeout: 10))
511 menu.tap() 525 menu.tap()
512 let unbookmark = app.buttons["Unbookmark"].firstMatch 526 let unbookmark = app.buttons["Unbookmark"].firstMatch
@@ -514,7 +528,11 @@ extension LiveSmokeUITests {
514 unbookmark.tap() 528 unbookmark.tap()
515 529
516 // --- settings: topic and merge-rule round-trips --- 530 // --- 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()
518 let addTopic = app.descendants(matching: .any) 536 let addTopic = app.descendants(matching: .any)
519 .matching(identifier: "settings-add-topic").firstMatch 537 .matching(identifier: "settings-add-topic").firstMatch
520 XCTAssertTrue(addTopic.waitForExistence(timeout: 10), "settings did not load") 538 XCTAssertTrue(addTopic.waitForExistence(timeout: 10), "settings did not load")
@@ -523,9 +541,10 @@ extension LiveSmokeUITests {
523 .firstMatch.tap() 541 .firstMatch.tap()
524 let chip = app.staticTexts["ios"].firstMatch 542 let chip = app.staticTexts["ios"].firstMatch
525 XCTAssertTrue(chip.waitForExistence(timeout: 10), "topic did not appear") 543 XCTAssertTrue(chip.waitForExistence(timeout: 10), "topic did not appear")
526 // Remove it again: the chip's own x button is the next button, 544 // Remove it again through the chip's own x button — by name, since
527 // and it asks before removing. 545 // the repository carries other topics — and it asks first.
528 app.scrollViews.buttons.firstMatch.tap() 546 app.descendants(matching: .any).matching(identifier: "settings-remove-topic-ios")
547 .firstMatch.tap()
529 let confirmRemove = app.buttons["Remove"].firstMatch 548 let confirmRemove = app.buttons["Remove"].firstMatch
530 XCTAssertTrue(confirmRemove.waitForExistence(timeout: 5), "topic remove confirmation missing") 549 XCTAssertTrue(confirmRemove.waitForExistence(timeout: 5), "topic remove confirmation missing")
531 confirmRemove.tap() 550 confirmRemove.tap()
@@ -623,8 +642,11 @@ extension LiveSmokeUITests {
623 XCTAssertTrue(app.staticTexts["Files"].firstMatch.waitForExistence(timeout: 10)) 642 XCTAssertTrue(app.staticTexts["Files"].firstMatch.waitForExistence(timeout: 10))
624 643
625 // --- owner profile from the repo screen --- 644 // --- owner profile from the repo screen ---
626 // The row's label merges; match it at any element type. 645 // The row's label merges; match it at any element type. It is
627 app.descendants(matching: .any)["krz"].firstMatch.tap() 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()
628 XCTAssertTrue(app.staticTexts["warez for the public"].firstMatch 650 XCTAssertTrue(app.staticTexts["warez for the public"].firstMatch
629 .waitForExistence(timeout: 15), "profile did not load") 651 .waitForExistence(timeout: 15), "profile did not load")
630 XCTAssertTrue(app.staticTexts 652 XCTAssertTrue(app.staticTexts
@@ -652,15 +674,20 @@ extension LiveSmokeUITests {
652 openRepo("krz/gitbay") 674 openRepo("krz/gitbay")
653 app.staticTexts["Releases"].firstMatch.tap() 675 app.staticTexts["Releases"].firstMatch.tap()
654 676
655 let row = app.staticTexts 677 // Newest first, and the list has grown past the fold, so the
656 .containing(NSPredicate(format: "label CONTAINS 'v1.0.0'")).firstMatch 678 // first row is whichever release is current.
679 let row = app.cells.firstMatch
657 XCTAssertTrue(row.waitForExistence(timeout: 15), "release list empty") 680 XCTAssertTrue(row.waitForExistence(timeout: 15), "release list empty")
658 row.tap() 681 row.tap()
659 682
660 // Notes render and assets carry sizes. 683 // Notes render and assets carry sizes. The assets sit under the
661 XCTAssertTrue(app.staticTexts 684 // notes, which can run past the fold.
662 .containing(NSPredicate(format: "label CONTAINS 'SHA256SUMS'")).firstMatch 685 XCTAssertTrue(app.descendants(matching: .any)
663 .waitForExistence(timeout: 15), "assets missing") 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")
664 691
665 // Edit sheet prefills; saving unchanged content round-trips. 692 // Edit sheet prefills; saving unchanged content round-trips.
666 app.descendants(matching: .any).matching(identifier: "release-edit-button") 693 app.descendants(matching: .any).matching(identifier: "release-edit-button")
@@ -668,8 +695,8 @@ extension LiveSmokeUITests {
668 let title = app.descendants(matching: .any) 695 let title = app.descendants(matching: .any)
669 .matching(identifier: "compose-title").firstMatch 696 .matching(identifier: "compose-title").firstMatch
670 XCTAssertTrue(title.waitForExistence(timeout: 5), "edit sheet did not open") 697 XCTAssertTrue(title.waitForExistence(timeout: 5), "edit sheet did not open")
671 XCTAssertTrue((title.value as? String)?.contains("v1.0.0") == true, 698 XCTAssertFalse((title.value as? String ?? "").isEmpty,
672 "edit sheet did not prefill") 699 "edit sheet did not prefill")
673 app.descendants(matching: .any).matching(identifier: "compose-submit") 700 app.descendants(matching: .any).matching(identifier: "compose-submit")
674 .firstMatch.tap() 701 .firstMatch.tap()
675 XCTAssertTrue(waitForDisappearance(title, timeout: 15), 702 XCTAssertTrue(waitForDisappearance(title, timeout: 15),
@@ -986,12 +1013,6 @@ extension LiveSmokeUITests {
986 XCTAssertTrue(app.staticTexts["Organizations"].firstMatch.exists, 1013 XCTAssertTrue(app.staticTexts["Organizations"].firstMatch.exists,
987 "org memberships missing") 1014 "org memberships missing")
988 XCTAssertTrue(app.staticTexts["krz"].firstMatch.exists, "org row missing") 1015 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")
995 1016
996 // --- an org admin can edit the org's profile --- 1017 // --- an org admin can edit the org's profile ---
997 app.staticTexts["krz"].firstMatch.tap() 1018 app.staticTexts["krz"].firstMatch.tap()
@@ -1000,6 +1021,15 @@ extension LiveSmokeUITests {
1000 .waitForExistence(timeout: 15), "org profile offers no edit to its admin") 1021 .waitForExistence(timeout: 15), "org profile offers no edit to its admin")
1001 back() 1022 back()
1002 1023
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
1003 // --- a log entry opens its commit --- 1033 // --- a log entry opens its commit ---
1004 app.terminate() 1034 app.terminate()
1005 app.launch() 1035 app.launch()
@@ -1009,11 +1039,13 @@ extension LiveSmokeUITests {
1009 XCTAssertTrue(firstCommit.waitForExistence(timeout: 20), "history is empty") 1039 XCTAssertTrue(firstCommit.waitForExistence(timeout: 20), "history is empty")
1010 firstCommit.tap() 1040 firstCommit.tap()
1011 // The commit screen IS the patch: no navigating away to find it. 1041 // The commit screen IS the patch: no navigating away to find it.
1012 // A hunk header (@@) only appears in a rendered diff. 1042 // A hunk header (@@) only appears in a rendered diff, and sits
1013 XCTAssertTrue(app.staticTexts 1043 // below the message and the file list.
1044 let hunk = app.staticTexts
1014 .containing(NSPredicate(format: "label BEGINSWITH '@@'")).firstMatch 1045 .containing(NSPredicate(format: "label BEGINSWITH '@@'")).firstMatch
1015 .waitForExistence(timeout: 20), 1046 _ = hunk.waitForExistence(timeout: 20)
1016 "commit screen did not render the patch inline") 1047 XCTAssertTrue(scrollTo(hunk, swipes: 10),
1048 "commit screen did not render the patch inline")
1017 } 1049 }
1018} 1050}
1019 1051
@@ -1202,7 +1234,7 @@ extension LiveSmokeUITests {
1202 1234
1203 selectTab("My Profile") 1235 selectTab("My Profile")
1204 XCTAssertTrue(app.staticTexts 1236 XCTAssertTrue(app.staticTexts
1205 .containing(NSPredicate(format: "label BEGINSWITH 'Repositories'")).firstMatch 1237 .containing(NSPredicate(format: "label CONTAINS 'Self-Hosting'")).firstMatch
1206 .waitForExistence(timeout: 20), "profile did not load") 1238 .waitForExistence(timeout: 20), "profile did not load")
1207 snap("profile") 1239 snap("profile")
1208 } 1240 }