Commit ebe0e42d13
Unsigned
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 | } |