| @@ -310,7 +310,7 @@ final class LiveSmokeUITests: XCTestCase { |
| 310 | 310 | openRepo(Self.fixtureRepo) |
| 311 | 311 | |
| 312 | 312 | // --- the fixture issue: milestone picker assigns the fixture --- |
| 313 | | app.staticTexts["Issues"].firstMatch.tap() |
| 313 | repoLink("issues").tap() |
| 314 | 314 | let issueRow = app.staticTexts |
| 315 | 315 | .containing(NSPredicate(format: "label CONTAINS 'ui-smoke fixture'")).firstMatch |
| 316 | 316 | XCTAssertTrue(issueRow.waitForExistence(timeout: 10), |
| @@ -367,7 +367,7 @@ final class LiveSmokeUITests: XCTestCase { |
| 367 | 367 | back() // to repo screen |
| 368 | 368 | |
| 369 | 369 | // --- MR: create from ui-smoke, then close it --- |
| 370 | | app.staticTexts["Merge Requests"].firstMatch.tap() |
| 370 | repoLink("merge-requests").tap() |
| 371 | 371 | app.descendants(matching: .any).matching(identifier: "mr-create-button") |
| 372 | 372 | .firstMatch.tap() |
| 373 | 373 | |
| @@ -434,7 +434,7 @@ final class LiveSmokeUITests: XCTestCase { |
| 434 | 434 | XCTAssertTrue(row.waitForExistence(timeout: 15), "\(path) not in the repo list") |
| 435 | 435 | row.tap() |
| 436 | 436 | // Repo screen is loaded once its links render. |
| 437 | | XCTAssertTrue(app.staticTexts["Files"].firstMatch.waitForExistence(timeout: 10), |
| 437 | XCTAssertTrue(repoLink("files").waitForExistence(timeout: 10), |
| 438 | 438 | "repo screen did not open") |
| 439 | 439 | } |
| 440 | 440 | |
| @@ -442,6 +442,34 @@ final class LiveSmokeUITests: XCTestCase { |
| 442 | 442 | app.navigationBars.buttons.firstMatch.tap() |
| 443 | 443 | } |
| 444 | 444 | |
| 445 | /// One of the four links in the grid at the top of the repo screen. |
| 446 | private func repoLink(_ id: String) -> XCUIElement { |
| 447 | app.descendants(matching: .any).matching(identifier: "repo-link-\(id)").firstMatch |
| 448 | } |
| 449 | |
| 450 | /// A link in the repo screen's More section, below the README. |
| 451 | /// Expands the section when it is still collapsed. |
| 452 | private func moreLink(_ title: String) -> XCUIElement { |
| 453 | let link = app.staticTexts[title].firstMatch |
| 454 | if link.exists { return link } |
| 455 | let more = app.descendants(matching: .any).matching(identifier: "repo-more").firstMatch |
| 456 | XCTAssertTrue(scrollTo(more, swipes: 20), "More section not reachable") |
| 457 | more.tap() |
| 458 | XCTAssertTrue(scrollTo(link), "\(title) not under More") |
| 459 | return link |
| 460 | } |
| 461 | |
| 462 | /// Settings is in the repo screen's actions menu. |
| 463 | private func openRepoSettings() { |
| 464 | let menu = app.descendants(matching: .any) |
| 465 | .matching(identifier: "repo-actions-menu").firstMatch |
| 466 | XCTAssertTrue(menu.waitForExistence(timeout: 10), "repo actions menu missing") |
| 467 | menu.tap() |
| 468 | let settings = app.buttons["Settings"].firstMatch |
| 469 | XCTAssertTrue(settings.waitForExistence(timeout: 5), "Settings missing from the actions menu") |
| 470 | settings.tap() |
| 471 | } |
| 472 | |
| 445 | 473 | /// Keys, PGP and email live behind the account menu, which is on the |
| 446 | 474 | /// My Profile tab and nowhere else. |
| 447 | 475 | func openAccountScreen(file: StaticString = #filePath, line: UInt = #line) { |
| @@ -589,8 +617,8 @@ extension LiveSmokeUITests { |
| 589 | 617 | |
| 590 | 618 | // --- access: grant a user on the scratch repo, then revoke --- |
| 591 | 619 | app.staticTexts[Self.scratchRepo].firstMatch.tap() |
| 592 | | XCTAssertTrue(app.staticTexts["Files"].firstMatch.waitForExistence(timeout: 10)) |
| 593 | | app.staticTexts["Settings"].firstMatch.tap() |
| 620 | XCTAssertTrue(repoLink("files").waitForExistence(timeout: 10)) |
| 621 | openRepoSettings() |
| 594 | 622 | let grantUser = app.descendants(matching: .any) |
| 595 | 623 | .matching(identifier: "settings-grant-user").firstMatch |
| 596 | 624 | XCTAssertTrue(scrollTo(grantUser, swipes: 8), "access section not reachable") |
| @@ -639,12 +667,7 @@ extension LiveSmokeUITests { |
| 639 | 667 | if firstRename { |
| 640 | 668 | firstRename = false // already on the settings screen |
| 641 | 669 | } else { |
| 642 | | // Fresh repo screen: scroll to the top so no row hides |
| 643 | | // under the navigation bar, then reach Settings. |
| 644 | | self.app.swipeDown() |
| 645 | | let settingsLink = self.app.descendants(matching: .any).matching(identifier: "repo-settings-link").firstMatch |
| 646 | | XCTAssertTrue(self.scrollTo(settingsLink), "settings link not reachable") |
| 647 | | settingsLink.tap() |
| 670 | self.openRepoSettings() |
| 648 | 671 | } |
| 649 | 672 | let renameButton = self.app.descendants(matching: .any) |
| 650 | 673 | .matching(identifier: "settings-rename-button").firstMatch |
| @@ -664,8 +687,10 @@ extension LiveSmokeUITests { |
| 664 | 687 | for _ in 0..<6 where !renamedLink.exists { self.app.swipeDown() } |
| 665 | 688 | XCTAssertTrue(renamedLink.waitForExistence(timeout: 10), "rename did not land") |
| 666 | 689 | renamedLink.tap() |
| 667 | | XCTAssertTrue(self.app.staticTexts[expectedPath].firstMatch.waitForExistence(timeout: 15), |
| 668 | | "renamed repo did not open as \(expectedPath)") |
| 690 | // The repo screen's title is the name alone; the header |
| 691 | // splits the path into the owner link and the rest. |
| 692 | XCTAssertTrue(self.app.navigationBars.staticTexts[newName].firstMatch |
| 693 | .waitForExistence(timeout: 15), "renamed repo did not open as \(expectedPath)") |
| 669 | 694 | } |
| 670 | 695 | rename("ui-smoke-2", Self.scratchRepo + "-2") |
| 671 | 696 | rename("ui-smoke", Self.scratchRepo) |
| @@ -681,7 +706,7 @@ extension LiveSmokeUITests { |
| 681 | 706 | let repoRow = app.staticTexts["krz/gitbay-ios"].firstMatch |
| 682 | 707 | XCTAssertTrue(repoRow.waitForExistence(timeout: 15)) |
| 683 | 708 | repoRow.tap() |
| 684 | | XCTAssertTrue(app.staticTexts["Files"].firstMatch.waitForExistence(timeout: 10)) |
| 709 | XCTAssertTrue(repoLink("files").waitForExistence(timeout: 10)) |
| 685 | 710 | let menu = app.descendants(matching: .any) |
| 686 | 711 | .matching(identifier: "repo-actions-menu").firstMatch |
| 687 | 712 | XCTAssertTrue(menu.waitForExistence(timeout: 10)) |
| @@ -729,12 +754,7 @@ extension LiveSmokeUITests { |
| 729 | 754 | unbookmark.tap() |
| 730 | 755 | |
| 731 | 756 | // --- settings: topic and merge-rule round-trips --- |
| 732 | | // The links section can sit below the README fold, and the row's |
| 733 | | // label is not always exposed as a static text. |
| 734 | | let settingsLink = app.descendants(matching: .any) |
| 735 | | .matching(identifier: "repo-settings-link").firstMatch |
| 736 | | XCTAssertTrue(scrollTo(settingsLink), "settings link not reachable") |
| 737 | | settingsLink.tap() |
| 757 | openRepoSettings() |
| 738 | 758 | let addTopic = app.descendants(matching: .any) |
| 739 | 759 | .matching(identifier: "settings-add-topic").firstMatch |
| 740 | 760 | XCTAssertTrue(addTopic.waitForExistence(timeout: 10), "settings did not load") |
| @@ -822,14 +842,11 @@ extension LiveSmokeUITests { |
| 822 | 842 | |
| 823 | 843 | |
| 824 | 844 | back() // settings -> repo |
| 825 | | // The repo screen keeps the scroll that reached Settings, which |
| 826 | | // can leave Builds under the navigation bar. |
| 827 | | app.swipeDown() |
| 828 | 845 | |
| 829 | 846 | // --- nothing to trigger without a job file --- |
| 830 | 847 | // This repo has no .gitbay/ci.yml, so `build jobs` returns none |
| 831 | 848 | // and the control is gated rather than failing after a guess. |
| 832 | | app.staticTexts["Builds"].firstMatch.tap() |
| 849 | repoLink("builds").tap() |
| 833 | 850 | let trigger = app.descendants(matching: .any) |
| 834 | 851 | .matching(identifier: "build-trigger-button").firstMatch |
| 835 | 852 | XCTAssertTrue(trigger.waitForExistence(timeout: 15), "trigger control missing") |
| @@ -877,13 +894,12 @@ extension LiveSmokeUITests { |
| 877 | 894 | XCTAssertTrue(hit.waitForExistence(timeout: 15), |
| 878 | 895 | "server-side topic search found nothing") |
| 879 | 896 | hit.tap() |
| 880 | | XCTAssertTrue(app.staticTexts["Files"].firstMatch.waitForExistence(timeout: 10)) |
| 897 | XCTAssertTrue(repoLink("files").waitForExistence(timeout: 10)) |
| 881 | 898 | |
| 882 | 899 | // --- owner profile from the repo screen --- |
| 883 | | // The row's label merges; match it at any element type. It is |
| 884 | | // the last row of the links section, below the fold. |
| 885 | | let ownerLink = app.descendants(matching: .any)["krz"].firstMatch |
| 886 | | XCTAssertTrue(scrollTo(ownerLink), "owner link not reachable") |
| 900 | let ownerLink = app.descendants(matching: .any) |
| 901 | .matching(identifier: "repo-owner-link").firstMatch |
| 902 | XCTAssertTrue(ownerLink.waitForExistence(timeout: 10), "owner link missing") |
| 887 | 903 | ownerLink.tap() |
| 888 | 904 | XCTAssertTrue(app.staticTexts["warez for the public"].firstMatch |
| 889 | 905 | .waitForExistence(timeout: 15), "profile did not load") |
| @@ -893,7 +909,7 @@ extension LiveSmokeUITests { |
| 893 | 909 | back() |
| 894 | 910 | |
| 895 | 911 | // --- grep inside the repo, last: its search UI owns the screen --- |
| 896 | | app.staticTexts["Search in Files"].firstMatch.tap() |
| 912 | moreLink("Search in Files").tap() |
| 897 | 913 | let grepField = app.searchFields.firstMatch |
| 898 | 914 | XCTAssertTrue(grepField.waitForExistence(timeout: 10)) |
| 899 | 915 | focusAndType(grepField, "space") |
| @@ -910,7 +926,7 @@ extension LiveSmokeUITests { |
| 910 | 926 | /// missing-tag refusal on create. Nothing changes state. |
| 911 | 927 | func testReleaseFlows() throws { |
| 912 | 928 | openRepo("krz/gitbay") |
| 913 | | app.staticTexts["Releases"].firstMatch.tap() |
| 929 | moreLink("Releases").tap() |
| 914 | 930 | |
| 915 | 931 | // Newest first, and the list has grown past the fold, so the |
| 916 | 932 | // first row is whichever release is current. |
| @@ -1119,7 +1135,7 @@ extension LiveSmokeUITests { |
| 1119 | 1135 | openRepo("krz/gitbay") |
| 1120 | 1136 | snap("repo") |
| 1121 | 1137 | |
| 1122 | | app.staticTexts["Merge Requests"].firstMatch.tap() |
| 1138 | repoLink("merge-requests").tap() |
| 1123 | 1139 | let all = app.segmentedControls.buttons["All"].firstMatch |
| 1124 | 1140 | XCTAssertTrue(all.waitForExistence(timeout: 10), "state picker missing") |
| 1125 | 1141 | all.tap() |
| @@ -1208,7 +1224,7 @@ extension LiveSmokeUITests { |
| 1208 | 1224 | func testBlameAndEditFlows() throws { |
| 1209 | 1225 | // --- blame on a real file --- |
| 1210 | 1226 | openRepo("krz/gitbay") |
| 1211 | | app.staticTexts["Files"].firstMatch.tap() |
| 1227 | repoLink("files").tap() |
| 1212 | 1228 | let goMod = app.staticTexts["go.mod"].firstMatch |
| 1213 | 1229 | XCTAssertTrue(goMod.waitForExistence(timeout: 15), "go.mod not in the tree") |
| 1214 | 1230 | goMod.tap() |
| @@ -1228,7 +1244,7 @@ extension LiveSmokeUITests { |
| 1228 | 1244 | app.terminate() |
| 1229 | 1245 | app.launch() |
| 1230 | 1246 | openRepo(Self.fixtureRepo) |
| 1231 | | app.staticTexts["Files"].firstMatch.tap() |
| 1247 | repoLink("files").tap() |
| 1232 | 1248 | let notes = app.staticTexts["notes.txt"].firstMatch |
| 1233 | 1249 | XCTAssertTrue(notes.waitForExistence(timeout: 15), "notes.txt not in the tree") |
| 1234 | 1250 | notes.tap() |
| @@ -1344,7 +1360,7 @@ extension LiveSmokeUITests { |
| 1344 | 1360 | app.terminate() |
| 1345 | 1361 | app.launch() |
| 1346 | 1362 | openRepo("krz/gitbay") |
| 1347 | | app.staticTexts["History"].firstMatch.tap() |
| 1363 | moreLink("History").tap() |
| 1348 | 1364 | let firstCommit = app.cells.firstMatch |
| 1349 | 1365 | XCTAssertTrue(firstCommit.waitForExistence(timeout: 20), "history is empty") |
| 1350 | 1366 | firstCommit.tap() |
| @@ -1368,7 +1384,7 @@ extension LiveSmokeUITests { |
| 1368 | 1384 | openRepo("krz/gitbay") |
| 1369 | 1385 | |
| 1370 | 1386 | // --- branches and tags, and browsing at a ref --- |
| 1371 | | app.staticTexts["Branches & Tags"].firstMatch.tap() |
| 1387 | moreLink("Branches & Tags").tap() |
| 1372 | 1388 | let main = app.staticTexts["main"].firstMatch |
| 1373 | 1389 | XCTAssertTrue(main.waitForExistence(timeout: 20), "refs did not load") |
| 1374 | 1390 | XCTAssertTrue(app.staticTexts["default"].firstMatch.exists, |
| @@ -1382,7 +1398,7 @@ extension LiveSmokeUITests { |
| 1382 | 1398 | back() |
| 1383 | 1399 | |
| 1384 | 1400 | // --- milestones with progress --- |
| 1385 | | app.staticTexts["Milestones"].firstMatch.tap() |
| 1401 | moreLink("Milestones").tap() |
| 1386 | 1402 | XCTAssertTrue(app.segmentedControls.firstMatch.waitForExistence(timeout: 15), |
| 1387 | 1403 | "milestones did not load") |
| 1388 | 1404 | app.segmentedControls.buttons["All"].firstMatch.tap() |
| @@ -1392,7 +1408,7 @@ extension LiveSmokeUITests { |
| 1392 | 1408 | back() |
| 1393 | 1409 | |
| 1394 | 1410 | // --- an MR's milestone reads back, and can be cleared --- |
| 1395 | | app.staticTexts["Merge Requests"].firstMatch.tap() |
| 1411 | repoLink("merge-requests").tap() |
| 1396 | 1412 | app.segmentedControls.buttons["All"].firstMatch.tap() |
| 1397 | 1413 | let mrNumber = app.staticTexts |
| 1398 | 1414 | .containing(NSPredicate(format: "label BEGINSWITH '!'")).firstMatch |
| @@ -1427,7 +1443,7 @@ extension LiveSmokeUITests { |
| 1427 | 1443 | .containing(NSPredicate(format: "label CONTAINS 'audit-labs/'")).firstMatch |
| 1428 | 1444 | .waitForExistence(timeout: 10), "explore is not the public listing") |
| 1429 | 1445 | firstRepo.tap() |
| 1430 | | XCTAssertTrue(app.staticTexts["Files"].firstMatch.waitForExistence(timeout: 20), |
| 1446 | XCTAssertTrue(repoLink("files").waitForExistence(timeout: 20), |
| 1431 | 1447 | "explore row did not open its repo") |
| 1432 | 1448 | |
| 1433 | 1449 | // --- history at a ref --- |
| @@ -1436,7 +1452,7 @@ extension LiveSmokeUITests { |
| 1436 | 1452 | openRepo("krz/gitbay") |
| 1437 | 1453 | // Browse a ref, then ask for its history — the move the tree page |
| 1438 | 1454 | // offers on the web. |
| 1439 | | app.staticTexts["Branches & Tags"].firstMatch.tap() |
| 1455 | moreLink("Branches & Tags").tap() |
| 1440 | 1456 | let main = app.staticTexts["main"].firstMatch |
| 1441 | 1457 | XCTAssertTrue(main.waitForExistence(timeout: 20), "refs did not load") |
| 1442 | 1458 | main.tap() |
| @@ -1458,7 +1474,7 @@ extension LiveSmokeUITests { |
| 1458 | 1474 | /// krz/gitbay#48. Read-only; editing is a push, on every surface. |
| 1459 | 1475 | func testWikiFlows() throws { |
| 1460 | 1476 | openRepo("krz/gitbay") |
| 1461 | | app.staticTexts["Wiki"].firstMatch.tap() |
| 1477 | moreLink("Wiki").tap() |
| 1462 | 1478 | |
| 1463 | 1479 | // The landing page leads and is marked. |
| 1464 | 1480 | let home = app.staticTexts["Home"].firstMatch |
| @@ -1488,7 +1504,7 @@ extension LiveSmokeUITests { |
| 1488 | 1504 | /// server names, and the detail screen says more than the log. |
| 1489 | 1505 | func testBuildJobsAndDetail() throws { |
| 1490 | 1506 | openRepo("krz/gitbay") |
| 1491 | | app.staticTexts["Builds"].firstMatch.tap() |
| 1507 | repoLink("builds").tap() |
| 1492 | 1508 | |
| 1493 | 1509 | // The picker lists jobs from `build jobs` — no name to type. |
| 1494 | 1510 | let trigger = app.descendants(matching: .any) |
| @@ -1535,7 +1551,7 @@ extension LiveSmokeUITests { |
| 1535 | 1551 | let firstRepo = app.cells.firstMatch |
| 1536 | 1552 | XCTAssertTrue(firstRepo.waitForExistence(timeout: 20), "explore listed nothing") |
| 1537 | 1553 | firstRepo.tap() |
| 1538 | | XCTAssertTrue(app.staticTexts["Files"].firstMatch.waitForExistence(timeout: 20), |
| 1554 | XCTAssertTrue(repoLink("files").waitForExistence(timeout: 20), |
| 1539 | 1555 | "repo screen did not open") |
| 1540 | 1556 | snap("repo") |
| 1541 | 1557 | |
| @@ -1782,7 +1798,7 @@ extension LiveSmokeUITests { |
| 1782 | 1798 | /// filtered empty state. Clearing brings the list back. |
| 1783 | 1799 | func testBuildFilterFlow() throws { |
| 1784 | 1800 | openRepo("krz/gitbay") |
| 1785 | | app.staticTexts["Builds"].firstMatch.tap() |
| 1801 | repoLink("builds").tap() |
| 1786 | 1802 | XCTAssertTrue(app.cells.firstMatch.waitForExistence(timeout: 20), "no builds listed") |
| 1787 | 1803 | |
| 1788 | 1804 | let filter = app.descendants(matching: .any) |
| @@ -1837,7 +1853,7 @@ extension LiveSmokeUITests { |
| 1837 | 1853 | else { throw XCTSkip("could not read krz/gitbay's builds over the API") } |
| 1838 | 1854 | |
| 1839 | 1855 | openRepo("krz/gitbay") |
| 1840 | | app.staticTexts["Builds"].firstMatch.tap() |
| 1856 | repoLink("builds").tap() |
| 1841 | 1857 | XCTAssertTrue(app.cells.firstMatch.waitForExistence(timeout: 20), "no builds listed") |
| 1842 | 1858 | let thirtyFirst = app.staticTexts |
| 1843 | 1859 | .containing(NSPredicate(format: "label BEGINSWITH %@", "#\(number) ")).firstMatch |
| @@ -1850,7 +1866,7 @@ extension LiveSmokeUITests { |
| 1850 | 1866 | /// links to it. |
| 1851 | 1867 | func testCloseInFavourFlow() throws { |
| 1852 | 1868 | openRepo(Self.fixtureRepo) |
| 1853 | | app.staticTexts["Merge Requests"].firstMatch.tap() |
| 1869 | repoLink("merge-requests").tap() |
| 1854 | 1870 | let superseded = app.staticTexts[Self.supersededTitle].firstMatch |
| 1855 | 1871 | XCTAssertTrue(superseded.waitForExistence(timeout: 15), "fixture MR not listed") |
| 1856 | 1872 | superseded.tap() |
| @@ -1885,7 +1901,7 @@ extension LiveSmokeUITests { |
| 1885 | 1901 | /// list filters to it, and removing it clears the chip. |
| 1886 | 1902 | func testMRLabelFlow() throws { |
| 1887 | 1903 | openRepo(Self.fixtureRepo) |
| 1888 | | app.staticTexts["Merge Requests"].firstMatch.tap() |
| 1904 | repoLink("merge-requests").tap() |
| 1889 | 1905 | let row = app.staticTexts[Self.labelledTitle].firstMatch |
| 1890 | 1906 | XCTAssertTrue(row.waitForExistence(timeout: 15), "fixture MR not listed") |
| 1891 | 1907 | row.tap() |
| @@ -1923,7 +1939,7 @@ extension LiveSmokeUITests { |
| 1923 | 1939 | /// the bold markers are gone and the word remains. Nothing is posted. |
| 1924 | 1940 | func testMarkupPreviewFlow() throws { |
| 1925 | 1941 | openRepo(Self.fixtureRepo) |
| 1926 | | app.staticTexts["Issues"].firstMatch.tap() |
| 1942 | repoLink("issues").tap() |
| 1927 | 1943 | let issue = app.staticTexts[Self.fixtureIssueTitle].firstMatch |
| 1928 | 1944 | XCTAssertTrue(issue.waitForExistence(timeout: 15), "fixture issue not listed") |
| 1929 | 1945 | issue.tap() |