Commit ebe0e42d13
Unsigned
Layout: unified · split
gitbay/Views/Repos/RepoSettingsView.swift +17 −16
| @@ -137,6 +137,7 @@ struct RepoSettingsView: View { | ||
| 137 | 137 | .foregroundStyle(.tertiary) |
| 138 | 138 | } |
| 139 | 139 | .disabled(model.working) |
| 140 | .accessibilityIdentifier("settings-remove-topic-\(topic)") | |
| 140 | 141 | } |
| 141 | 142 | .font(.gbSans(.caption)) |
| 142 | 143 | .foregroundStyle(Color.gbAccent) |
| @@ -281,27 +282,27 @@ struct RepoSettingsView: View { | ||
| 281 | 282 | } |
| 282 | 283 | } |
| 283 | 284 | } |
| 284 | HStack { | |
| 285 | VStack(spacing: 8) { | |
| 285 | 286 | TextField("Grant user", text: $newGrantUser) |
| 286 | 287 | .autocorrectionDisabled() |
| 287 | 288 | .textInputAutocapitalization(.never) |
| 288 | 289 | .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") | |
| 302 | 305 | } |
| 303 | .disabled(newGrantUser.trimmingCharacters(in: .whitespaces).isEmpty || model.working) | |
| 304 | .accessibilityIdentifier("settings-grant-submit") | |
| 305 | 306 | } |
| 306 | 307 | } header: { |
| 307 | 308 | Text("Access") |
gitbayUITests/LiveSmokeUITests.swift +67 −35
| @@ -457,10 +457,10 @@ extension LiveSmokeUITests { | ||
| 457 | 457 | let hookURL = app.descendants(matching: .any) |
| 458 | 458 | .matching(identifier: "webhook-url").firstMatch |
| 459 | 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 | 461 | app.descendants(matching: .any).matching(identifier: "webhook-submit") |
| 462 | 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 | 464 | XCTAssertTrue(hookRow.waitForExistence(timeout: 15), "webhook not listed") |
| 465 | 465 | hookRow.swipeLeft() |
| 466 | 466 | app.buttons["Remove"].firstMatch.tap() |
| @@ -495,18 +495,32 @@ extension LiveSmokeUITests { | ||
| 495 | 495 | unpin.tap() |
| 496 | 496 | |
| 497 | 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 | 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 | 507 | let bookmark = app.buttons["Bookmark"].firstMatch |
| 500 | 508 | XCTAssertTrue(bookmark.waitForExistence(timeout: 5), "Bookmark action missing") |
| 501 | 509 | 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() | |
| 510 | 524 | XCTAssertTrue(menu.waitForExistence(timeout: 10)) |
| 511 | 525 | menu.tap() |
| 512 | 526 | let unbookmark = app.buttons["Unbookmark"].firstMatch |
| @@ -514,7 +528,11 @@ extension LiveSmokeUITests { | ||
| 514 | 528 | unbookmark.tap() |
| 515 | 529 | |
| 516 | 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 | 536 | let addTopic = app.descendants(matching: .any) |
| 519 | 537 | .matching(identifier: "settings-add-topic").firstMatch |
| 520 | 538 | XCTAssertTrue(addTopic.waitForExistence(timeout: 10), "settings did not load") |
| @@ -523,9 +541,10 @@ extension LiveSmokeUITests { | ||
| 523 | 541 | .firstMatch.tap() |
| 524 | 542 | let chip = app.staticTexts["ios"].firstMatch |
| 525 | 543 | 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() | |
| 529 | 548 | let confirmRemove = app.buttons["Remove"].firstMatch |
| 530 | 549 | XCTAssertTrue(confirmRemove.waitForExistence(timeout: 5), "topic remove confirmation missing") |
| 531 | 550 | confirmRemove.tap() |
| @@ -623,8 +642,11 @@ extension LiveSmokeUITests { | ||
| 623 | 642 | XCTAssertTrue(app.staticTexts["Files"].firstMatch.waitForExistence(timeout: 10)) |
| 624 | 643 | |
| 625 | 644 | // --- 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() | |
| 628 | 650 | XCTAssertTrue(app.staticTexts["warez for the public"].firstMatch |
| 629 | 651 | .waitForExistence(timeout: 15), "profile did not load") |
| 630 | 652 | XCTAssertTrue(app.staticTexts |
| @@ -652,15 +674,20 @@ extension LiveSmokeUITests { | ||
| 652 | 674 | openRepo("krz/gitbay") |
| 653 | 675 | app.staticTexts["Releases"].firstMatch.tap() |
| 654 | 676 | |
| 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 | |
| 657 | 680 | XCTAssertTrue(row.waitForExistence(timeout: 15), "release list empty") |
| 658 | 681 | row.tap() |
| 659 | 682 | |
| 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") | |
| 664 | 691 | |
| 665 | 692 | // Edit sheet prefills; saving unchanged content round-trips. |
| 666 | 693 | app.descendants(matching: .any).matching(identifier: "release-edit-button") |
| @@ -668,8 +695,8 @@ extension LiveSmokeUITests { | ||
| 668 | 695 | let title = app.descendants(matching: .any) |
| 669 | 696 | .matching(identifier: "compose-title").firstMatch |
| 670 | 697 | 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") | |
| 673 | 700 | app.descendants(matching: .any).matching(identifier: "compose-submit") |
| 674 | 701 | .firstMatch.tap() |
| 675 | 702 | XCTAssertTrue(waitForDisappearance(title, timeout: 15), |
| @@ -986,12 +1013,6 @@ extension LiveSmokeUITests { | ||
| 986 | 1013 | XCTAssertTrue(app.staticTexts["Organizations"].firstMatch.exists, |
| 987 | 1014 | "org memberships missing") |
| 988 | 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 | 1017 | // --- an org admin can edit the org's profile --- |
| 997 | 1018 | app.staticTexts["krz"].firstMatch.tap() |
| @@ -1000,6 +1021,15 @@ extension LiveSmokeUITests { | ||
| 1000 | 1021 | .waitForExistence(timeout: 15), "org profile offers no edit to its admin") |
| 1001 | 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 | 1033 | // --- a log entry opens its commit --- |
| 1004 | 1034 | app.terminate() |
| 1005 | 1035 | app.launch() |
| @@ -1009,11 +1039,13 @@ extension LiveSmokeUITests { | ||
| 1009 | 1039 | XCTAssertTrue(firstCommit.waitForExistence(timeout: 20), "history is empty") |
| 1010 | 1040 | firstCommit.tap() |
| 1011 | 1041 | // 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 | |
| 1014 | 1045 | .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") | |
| 1017 | 1049 | } |
| 1018 | 1050 | } |
| 1019 | 1051 | |
| @@ -1202,7 +1234,7 @@ extension LiveSmokeUITests { | ||
| 1202 | 1234 | |
| 1203 | 1235 | selectTab("My Profile") |
| 1204 | 1236 | XCTAssertTrue(app.staticTexts |
| 1205 | .containing(NSPredicate(format: "label BEGINSWITH 'Repositories'")).firstMatch | |
| 1237 | .containing(NSPredicate(format: "label CONTAINS 'Self-Hosting'")).firstMatch | |
| 1206 | 1238 | .waitForExistence(timeout: 20), "profile did not load") |
| 1207 | 1239 | snap("profile") |
| 1208 | 1240 | } |