| @@ -52,12 +52,107 @@ final class LiveSmokeUITests: XCTestCase { |
| 52 | 52 | if name.contains("testBlameAndEditFlows") { |
| 53 | 53 | try requireFixtureRepo() |
| 54 | 54 | } |
| 55 | if name.contains("testSnippetFlows") { |
| 56 | deleteSmokeSnippets() |
| 57 | } |
| 58 | if name.contains("testOrgLabelAndMilestoneFlows") { |
| 59 | ensureOrgFixtures() |
| 60 | } |
| 61 | if name.contains("testCloseInFavourFlow") { |
| 62 | try requireFixtureBranch() |
| 63 | closeSmokeMergeRequests() |
| 64 | try openSupersedeFixtures() |
| 65 | } |
| 55 | 66 | } |
| 56 | 67 | |
| 57 | 68 | override func tearDownWithError() throws { |
| 58 | 69 | if name.contains("testRepoManagementFlows") { |
| 59 | 70 | deleteScratchRepo() |
| 60 | 71 | } |
| 72 | if name.contains("testSnippetFlows") { |
| 73 | deleteSmokeSnippets() |
| 74 | } |
| 75 | if name.contains("testCloseInFavourFlow") { |
| 76 | closeSmokeMergeRequests() |
| 77 | } |
| 78 | } |
| 79 | |
| 80 | // MARK: - Fixtures for the v1.2.0 flows |
| 81 | |
| 82 | static let smokeSnippetDescription = "UI smoke snippet" |
| 83 | |
| 84 | /// Remove every snippet the flow made, before and after, so a run |
| 85 | /// that died mid-way cannot strand one or hit the per-account cap. |
| 86 | private func deleteSmokeSnippets() { |
| 87 | guard let data = readCommand(["snippet", "list"]), |
| 88 | let envelope = try? JSONSerialization.jsonObject(with: data) as? [String: Any], |
| 89 | let rows = envelope["data"] as? [[String: Any]] else { return } |
| 90 | for row in rows where row["description"] as? String == Self.smokeSnippetDescription { |
| 91 | if let id = row["id"] as? String { |
| 92 | _ = runCommand(["snippet", "delete", id]) |
| 93 | } |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | static let smokeOrg = "krz" |
| 98 | static let smokeOrgLabel = "ui-smoke-label" |
| 99 | static let smokeOrgMilestone = "ui-smoke milestone" |
| 100 | |
| 101 | /// The org milestone is a fixture, reused and left closed: the |
| 102 | /// server has no milestone delete, so creating one per run would |
| 103 | /// pile them up. The label is removed by the flow itself; a stale |
| 104 | /// one is removed here first. |
| 105 | private func ensureOrgFixtures() { |
| 106 | _ = runCommand(["org", "label", "remove", Self.smokeOrg, Self.smokeOrgLabel]) |
| 107 | guard let data = readCommand(["org", "milestone", "list", Self.smokeOrg, "--state", "all"]), |
| 108 | let envelope = try? JSONSerialization.jsonObject(with: data) as? [String: Any] |
| 109 | else { return } |
| 110 | let rows = envelope["data"] as? [[String: Any]] ?? [] |
| 111 | if let row = rows.first(where: { $0["title"] as? String == Self.smokeOrgMilestone }) { |
| 112 | if row["state"] as? String == "open" { |
| 113 | _ = runCommand(["org", "milestone", "close", Self.smokeOrg, Self.smokeOrgMilestone]) |
| 114 | } |
| 115 | } else { |
| 116 | _ = runCommand(["org", "milestone", "create", Self.smokeOrg, Self.smokeOrgMilestone]) |
| 117 | _ = runCommand(["org", "milestone", "close", Self.smokeOrg, Self.smokeOrgMilestone]) |
| 118 | } |
| 119 | } |
| 120 | |
| 121 | static let supersededTitle = "UI smoke: superseded" |
| 122 | static let successorTitle = "UI smoke: successor" |
| 123 | |
| 124 | /// Close in favour needs two open merge requests. The fixture branch |
| 125 | /// gives one (ui-smoke -> main); main has commits ui-smoke lacks, so |
| 126 | /// the reverse (main -> ui-smoke) is the other. Both are the API's |
| 127 | /// to open; the flow closes the first from the app and tearDown |
| 128 | /// closes whatever is left. |
| 129 | private func openSupersedeFixtures() throws { |
| 130 | _ = runCommand(["mr", "create", Self.fixtureRepo, "--source", "ui-smoke", "--target", "main", |
| 131 | "--title", Self.supersededTitle]) |
| 132 | _ = runCommand(["mr", "create", Self.fixtureRepo, "--source", "main", "--target", "ui-smoke", |
| 133 | "--title", Self.successorTitle]) |
| 134 | let open = openSmokeMergeRequests().map { $0.title } |
| 135 | try XCTSkipUnless(open.contains(Self.supersededTitle) && open.contains(Self.successorTitle), """ |
| 136 | could not open both fixture merge requests on krz/gitbay-ios \ |
| 137 | (have \(open)); the ui-smoke branch may be level with main. |
| 138 | """) |
| 139 | } |
| 140 | |
| 141 | private func openSmokeMergeRequests() -> [(number: Int, title: String)] { |
| 142 | guard let data = readCommand(["mr", "list", Self.fixtureRepo, "--state", "open"]), |
| 143 | let envelope = try? JSONSerialization.jsonObject(with: data) as? [String: Any], |
| 144 | let rows = envelope["data"] as? [[String: Any]] else { return [] } |
| 145 | return rows.compactMap { row in |
| 146 | guard let number = row["number"] as? Int, let title = row["title"] as? String, |
| 147 | title.hasPrefix("UI smoke:") else { return nil } |
| 148 | return (number, title) |
| 149 | } |
| 150 | } |
| 151 | |
| 152 | private func closeSmokeMergeRequests() { |
| 153 | for mr in openSmokeMergeRequests() { |
| 154 | _ = runCommand(["mr", "close", Self.fixtureRepo, String(mr.number)]) |
| 155 | } |
| 61 | 156 | } |
| 62 | 157 | |
| 63 | 158 | /// `repo delete` over the JSON API — the app has no delete screen |
| @@ -821,14 +916,14 @@ extension LiveSmokeUITests { |
| 821 | 916 | // Dashboard toolbar -> account screen. |
| 822 | 917 | openAccountScreen() |
| 823 | 918 | |
| 824 | | // Real keys render: SSH fingerprints and the PGP key's UID email. |
| 919 | // Real keys render: SSH fingerprints first. |
| 825 | 920 | XCTAssertTrue(app.staticTexts |
| 826 | 921 | .containing(NSPredicate(format: "label BEGINSWITH 'SHA256:'")).firstMatch |
| 827 | 922 | .waitForExistence(timeout: 15), "SSH keys missing") |
| 828 | | XCTAssertTrue(app.staticTexts["hello@cleberg.net"].firstMatch |
| 829 | | .waitForExistence(timeout: 10), "PGP key UID missing") |
| 830 | 923 | |
| 831 | 924 | // Pasting garbage as an SSH key surfaces the server's validation. |
| 925 | // Done while the SSH section is still on screen: labelled keys |
| 926 | // are two lines each, so what follows sits below the fold. |
| 832 | 927 | app.descendants(matching: .any).matching(identifier: "add-ssh-key") |
| 833 | 928 | .firstMatch.tap() |
| 834 | 929 | let paste = app.descendants(matching: .any) |
| @@ -842,6 +937,11 @@ extension LiveSmokeUITests { |
| 842 | 937 | .waitForExistence(timeout: 15), "invalid-key refusal not surfaced") |
| 843 | 938 | app.buttons["Cancel"].firstMatch.tap() |
| 844 | 939 | |
| 940 | // The PGP key's UID email, below the keys on a List that does not |
| 941 | // build rows it has not shown. |
| 942 | XCTAssertTrue(scrollTo(app.staticTexts["hello@cleberg.net"].firstMatch), |
| 943 | "PGP key UID missing") |
| 944 | |
| 845 | 945 | // Activity mail flips off and back on; the value comes back from |
| 846 | 946 | // the server each time. |
| 847 | 947 | let mail = app.switches["Activity by mail"].firstMatch |
| @@ -1373,3 +1473,315 @@ extension LiveSmokeUITests { |
| 1373 | 1473 | snap("profile") |
| 1374 | 1474 | } |
| 1375 | 1475 | } |
| 1476 | |
| 1477 | extension LiveSmokeUITests { |
| 1478 | |
| 1479 | /// Snippets, a full life on the caller's own account: create, add a |
| 1480 | /// file, replace it, remove it, change visibility, delete. setUp and |
| 1481 | /// tearDown remove anything left by this description. |
| 1482 | func testSnippetFlows() throws { |
| 1483 | selectTab("My Profile") |
| 1484 | app.descendants(matching: .any).matching(identifier: "account-menu").firstMatch.tap() |
| 1485 | let snippets = app.buttons["Snippets"].firstMatch |
| 1486 | XCTAssertTrue(snippets.waitForExistence(timeout: 5), "Snippets missing from the account menu") |
| 1487 | snippets.tap() |
| 1488 | |
| 1489 | // --- create --- |
| 1490 | let create = app.descendants(matching: .any) |
| 1491 | .matching(identifier: "snippet-create-button").firstMatch |
| 1492 | XCTAssertTrue(create.waitForExistence(timeout: 15), "snippet list did not open") |
| 1493 | create.tap() |
| 1494 | let nameField = app.descendants(matching: .any) |
| 1495 | .matching(identifier: "snippet-file-name").firstMatch |
| 1496 | XCTAssertTrue(nameField.waitForExistence(timeout: 5), "create sheet did not open") |
| 1497 | focusAndType(nameField, "ui-smoke.txt") |
| 1498 | focusAndType(app.descendants(matching: .any) |
| 1499 | .matching(identifier: "snippet-description").firstMatch, Self.smokeSnippetDescription) |
| 1500 | let content = app.descendants(matching: .any) |
| 1501 | .matching(identifier: "snippet-content").firstMatch |
| 1502 | focusAndType(content, "hello from the live suite") |
| 1503 | app.descendants(matching: .any).matching(identifier: "snippet-submit").firstMatch.tap() |
| 1504 | XCTAssertTrue(waitForDisappearance(nameField, timeout: 20), "create did not dismiss") |
| 1505 | |
| 1506 | let row = app.staticTexts[Self.smokeSnippetDescription].firstMatch |
| 1507 | XCTAssertTrue(row.waitForExistence(timeout: 15), "created snippet not listed") |
| 1508 | row.tap() |
| 1509 | |
| 1510 | // --- detail: the file renders, unlisted by default --- |
| 1511 | XCTAssertTrue(app.staticTexts["ui-smoke.txt"].firstMatch.waitForExistence(timeout: 15), |
| 1512 | "snippet file missing") |
| 1513 | XCTAssertTrue(app.staticTexts["unlisted"].firstMatch.exists, "default visibility not unlisted") |
| 1514 | |
| 1515 | // --- add a second file --- |
| 1516 | let actions = app.descendants(matching: .any) |
| 1517 | .matching(identifier: "snippet-actions-menu").firstMatch |
| 1518 | XCTAssertTrue(actions.waitForExistence(timeout: 10), "owner actions missing") |
| 1519 | actions.tap() |
| 1520 | let addFile = app.buttons["Add file"].firstMatch |
| 1521 | XCTAssertTrue(addFile.waitForExistence(timeout: 5)) |
| 1522 | addFile.tap() |
| 1523 | let secondName = app.descendants(matching: .any) |
| 1524 | .matching(identifier: "snippet-file-name").firstMatch |
| 1525 | XCTAssertTrue(secondName.waitForExistence(timeout: 5), "add-file sheet did not open") |
| 1526 | focusAndType(secondName, "second.txt") |
| 1527 | focusAndType(app.descendants(matching: .any) |
| 1528 | .matching(identifier: "snippet-content").firstMatch, "two") |
| 1529 | app.descendants(matching: .any).matching(identifier: "snippet-submit").firstMatch.tap() |
| 1530 | XCTAssertTrue(waitForDisappearance(secondName, timeout: 20), "add file did not dismiss") |
| 1531 | let second = app.staticTexts["second.txt"].firstMatch |
| 1532 | XCTAssertTrue(second.waitForExistence(timeout: 15), "added file missing") |
| 1533 | |
| 1534 | // --- replace its content from the file menu --- |
| 1535 | let fileMenu = app.descendants(matching: .any) |
| 1536 | .matching(identifier: "snippet-file-menu-second.txt").firstMatch |
| 1537 | XCTAssertTrue(scrollTo(fileMenu), "file menu not reachable") |
| 1538 | fileMenu.tap() |
| 1539 | let edit = app.buttons["Edit"].firstMatch |
| 1540 | XCTAssertTrue(edit.waitForExistence(timeout: 5)) |
| 1541 | edit.tap() |
| 1542 | let editor = app.descendants(matching: .any) |
| 1543 | .matching(identifier: "snippet-content").firstMatch |
| 1544 | XCTAssertTrue(editor.waitForExistence(timeout: 5), "edit sheet did not open") |
| 1545 | XCTAssertTrue((editor.value as? String)?.contains("two") == true, "edit sheet did not prefill") |
| 1546 | focusAndType(editor, " and three") |
| 1547 | app.descendants(matching: .any).matching(identifier: "snippet-submit").firstMatch.tap() |
| 1548 | XCTAssertTrue(waitForDisappearance(editor, timeout: 20), "file save did not dismiss") |
| 1549 | XCTAssertTrue(app.staticTexts |
| 1550 | .containing(NSPredicate(format: "label CONTAINS 'and three'")).firstMatch |
| 1551 | .waitForExistence(timeout: 15), "replaced content not rendered") |
| 1552 | |
| 1553 | // --- remove it, through its confirmation --- |
| 1554 | XCTAssertTrue(scrollTo(fileMenu), "file menu not reachable after edit") |
| 1555 | fileMenu.tap() |
| 1556 | app.buttons["Remove"].firstMatch.tap() |
| 1557 | let confirmRemove = app.buttons["Remove"].firstMatch |
| 1558 | XCTAssertTrue(confirmRemove.waitForExistence(timeout: 5), "remove confirmation missing") |
| 1559 | confirmRemove.tap() |
| 1560 | XCTAssertTrue(waitForDisappearance(second, timeout: 15), "file not removed") |
| 1561 | |
| 1562 | // --- visibility to public --- |
| 1563 | actions.tap() |
| 1564 | let settings = app.buttons["Description & visibility"].firstMatch |
| 1565 | XCTAssertTrue(settings.waitForExistence(timeout: 5)) |
| 1566 | settings.tap() |
| 1567 | let visibility = app.segmentedControls.firstMatch |
| 1568 | XCTAssertTrue(visibility.waitForExistence(timeout: 5), "settings sheet did not open") |
| 1569 | visibility.buttons["public"].tap() |
| 1570 | app.buttons["Save"].firstMatch.tap() |
| 1571 | XCTAssertTrue(app.staticTexts["public"].firstMatch.waitForExistence(timeout: 15), |
| 1572 | "visibility change not shown") |
| 1573 | |
| 1574 | // --- delete, through its confirmation; the list no longer has it --- |
| 1575 | actions.tap() |
| 1576 | app.buttons["Delete snippet"].firstMatch.tap() |
| 1577 | let confirmDelete = app.buttons["Delete"].firstMatch |
| 1578 | XCTAssertTrue(confirmDelete.waitForExistence(timeout: 5), "delete confirmation missing") |
| 1579 | confirmDelete.tap() |
| 1580 | XCTAssertTrue(create.waitForExistence(timeout: 15), "did not return to the snippet list") |
| 1581 | XCTAssertTrue(waitForDisappearance(row, timeout: 15), "deleted snippet still listed") |
| 1582 | } |
| 1583 | |
| 1584 | /// An SSH key's label round-trips: set from the row's swipe action, |
| 1585 | /// then restored to what it was (or cleared). |
| 1586 | func testSSHKeyLabelFlow() throws { |
| 1587 | openAccountScreen() |
| 1588 | let fingerprint = app.staticTexts |
| 1589 | .containing(NSPredicate(format: "label BEGINSWITH 'SHA256:'")).firstMatch |
| 1590 | XCTAssertTrue(fingerprint.waitForExistence(timeout: 15), "SSH keys missing") |
| 1591 | let fp = fingerprint.label |
| 1592 | |
| 1593 | // What the key is called now, so it can be put back. |
| 1594 | var original = "" |
| 1595 | if let data = readCommand(["keys", "list"]), |
| 1596 | let envelope = try? JSONSerialization.jsonObject(with: data) as? [String: Any], |
| 1597 | let rows = envelope["data"] as? [[String: Any]], |
| 1598 | let row = rows.first(where: { $0["fingerprint"] as? String == fp }) { |
| 1599 | original = row["label"] as? String ?? "" |
| 1600 | } |
| 1601 | |
| 1602 | let relabel: (String) -> Void = { text in |
| 1603 | fingerprint.swipeLeft() |
| 1604 | let label = self.app.buttons["Label"].firstMatch |
| 1605 | XCTAssertTrue(label.waitForExistence(timeout: 5), "Label swipe action missing") |
| 1606 | label.tap() |
| 1607 | let field = self.app.alerts.textFields.firstMatch |
| 1608 | XCTAssertTrue(field.waitForExistence(timeout: 5), "label alert missing") |
| 1609 | self.clearAndType(field, text) |
| 1610 | self.app.alerts.buttons["Save"].firstMatch.tap() |
| 1611 | } |
| 1612 | |
| 1613 | relabel("ui-smoke") |
| 1614 | XCTAssertTrue(app.staticTexts["ui-smoke"].firstMatch.waitForExistence(timeout: 15), |
| 1615 | "label not shown after save") |
| 1616 | |
| 1617 | relabel(original) |
| 1618 | XCTAssertTrue(waitForDisappearance(app.staticTexts["ui-smoke"].firstMatch, timeout: 15), |
| 1619 | "label not restored") |
| 1620 | if !original.isEmpty { |
| 1621 | XCTAssertTrue(app.staticTexts[original].firstMatch.waitForExistence(timeout: 10), |
| 1622 | "original label not back") |
| 1623 | } |
| 1624 | } |
| 1625 | |
| 1626 | /// Org labels and milestones from the org screen: a label is created |
| 1627 | /// and removed; the fixture milestone is reopened and closed again, |
| 1628 | /// and creating it a second time surfaces the server's refusal. |
| 1629 | func testOrgLabelAndMilestoneFlows() throws { |
| 1630 | openAccountScreen() |
| 1631 | let orgRow = app.staticTexts[Self.smokeOrg].firstMatch |
| 1632 | XCTAssertTrue(orgRow.waitForExistence(timeout: 15), "org list missing") |
| 1633 | orgRow.tap() |
| 1634 | XCTAssertTrue(app.staticTexts["cmc"].firstMatch.waitForExistence(timeout: 15), |
| 1635 | "org members missing") |
| 1636 | |
| 1637 | // --- labels --- |
| 1638 | let labels = app.staticTexts["Labels"].firstMatch |
| 1639 | XCTAssertTrue(scrollTo(labels), "Labels link not reachable") |
| 1640 | labels.tap() |
| 1641 | let newName = app.descendants(matching: .any) |
| 1642 | .matching(identifier: "label-new-name").firstMatch |
| 1643 | XCTAssertTrue(newName.waitForExistence(timeout: 15), "org labels did not open") |
| 1644 | focusAndType(newName, Self.smokeOrgLabel) |
| 1645 | app.descendants(matching: .any).matching(identifier: "label-create-submit").firstMatch.tap() |
| 1646 | let chip = app.staticTexts[Self.smokeOrgLabel].firstMatch |
| 1647 | XCTAssertTrue(chip.waitForExistence(timeout: 15), "org label not listed after create") |
| 1648 | |
| 1649 | // The org's own screen manages its rows: the menu is there, not |
| 1650 | // the read-only mark. |
| 1651 | let cell = app.cells.containing(.staticText, identifier: Self.smokeOrgLabel).firstMatch |
| 1652 | let menu = cell.buttons.firstMatch |
| 1653 | XCTAssertTrue(menu.waitForExistence(timeout: 5), "org label has no menu on the org screen") |
| 1654 | menu.tap() |
| 1655 | let remove = app.buttons["Remove"].firstMatch |
| 1656 | XCTAssertTrue(remove.waitForExistence(timeout: 5)) |
| 1657 | remove.tap() |
| 1658 | let confirmRemove = app.buttons["Remove"].firstMatch |
| 1659 | XCTAssertTrue(confirmRemove.waitForExistence(timeout: 5), "remove confirmation missing") |
| 1660 | confirmRemove.tap() |
| 1661 | XCTAssertTrue(waitForDisappearance(chip, timeout: 15), "org label not removed") |
| 1662 | back() |
| 1663 | |
| 1664 | // --- milestones --- |
| 1665 | let milestones = app.staticTexts["Milestones"].firstMatch |
| 1666 | XCTAssertTrue(scrollTo(milestones), "Milestones link not reachable") |
| 1667 | milestones.tap() |
| 1668 | let picker = app.segmentedControls.firstMatch |
| 1669 | XCTAssertTrue(picker.waitForExistence(timeout: 15), "org milestones did not open") |
| 1670 | picker.buttons["Closed"].tap() |
| 1671 | let fixture = app.staticTexts[Self.smokeOrgMilestone].firstMatch |
| 1672 | XCTAssertTrue(fixture.waitForExistence(timeout: 15), "fixture milestone not among closed") |
| 1673 | fixture.swipeLeft() |
| 1674 | let reopen = app.buttons["Reopen"].firstMatch |
| 1675 | XCTAssertTrue(reopen.waitForExistence(timeout: 5), "reopen action missing on the org screen") |
| 1676 | reopen.tap() |
| 1677 | XCTAssertTrue(waitForDisappearance(fixture, timeout: 15), "reopened milestone still among closed") |
| 1678 | picker.buttons["Open"].tap() |
| 1679 | XCTAssertTrue(fixture.waitForExistence(timeout: 15), "reopened milestone not among open") |
| 1680 | |
| 1681 | // Creating it again is refused with the server's sentence. |
| 1682 | app.descendants(matching: .any).matching(identifier: "milestone-create-button").firstMatch.tap() |
| 1683 | let title = app.descendants(matching: .any) |
| 1684 | .matching(identifier: "milestone-title").firstMatch |
| 1685 | XCTAssertTrue(title.waitForExistence(timeout: 5), "create sheet did not open") |
| 1686 | focusAndType(title, Self.smokeOrgMilestone) |
| 1687 | app.descendants(matching: .any).matching(identifier: "milestone-submit").firstMatch.tap() |
| 1688 | XCTAssertTrue(app.staticTexts |
| 1689 | .containing(NSPredicate(format: "label CONTAINS[c] 'already'")).firstMatch |
| 1690 | .waitForExistence(timeout: 15), "duplicate milestone refusal not surfaced") |
| 1691 | app.buttons["Cancel"].firstMatch.tap() |
| 1692 | |
| 1693 | // Back to closed, where the fixture rests. |
| 1694 | XCTAssertTrue(fixture.waitForExistence(timeout: 10)) |
| 1695 | fixture.swipeLeft() |
| 1696 | let close = app.buttons["Close"].firstMatch |
| 1697 | XCTAssertTrue(close.waitForExistence(timeout: 5), "close action missing on the org screen") |
| 1698 | close.tap() |
| 1699 | XCTAssertTrue(waitForDisappearance(fixture, timeout: 15), "closed milestone still among open") |
| 1700 | } |
| 1701 | |
| 1702 | /// Build list filters on a repo with builds: a status narrows the |
| 1703 | /// list, a job narrows it further, and an impossible pair shows the |
| 1704 | /// filtered empty state. Clearing brings the list back. |
| 1705 | func testBuildFilterFlow() throws { |
| 1706 | openRepo("krz/gitbay") |
| 1707 | app.staticTexts["Builds"].firstMatch.tap() |
| 1708 | XCTAssertTrue(app.cells.firstMatch.waitForExistence(timeout: 20), "no builds listed") |
| 1709 | |
| 1710 | let filter = app.descendants(matching: .any) |
| 1711 | .matching(identifier: "build-filter-button").firstMatch |
| 1712 | XCTAssertTrue(filter.waitForExistence(timeout: 10), "filter button missing") |
| 1713 | |
| 1714 | let choose: (String, String) -> Void = { menuId, option in |
| 1715 | let menu = self.app.descendants(matching: .any).matching(identifier: menuId).firstMatch |
| 1716 | XCTAssertTrue(menu.waitForExistence(timeout: 5), "\(menuId) missing") |
| 1717 | menu.tap() |
| 1718 | let choice = self.app.buttons[option].firstMatch |
| 1719 | XCTAssertTrue(choice.waitForExistence(timeout: 5), "\(option) not offered") |
| 1720 | choice.tap() |
| 1721 | } |
| 1722 | |
| 1723 | // status = success: rows remain and every row says success. |
| 1724 | filter.tap() |
| 1725 | choose("build-filter-status-menu", "success") |
| 1726 | app.buttons["Done"].firstMatch.tap() |
| 1727 | XCTAssertTrue(app.cells.firstMatch.waitForExistence(timeout: 20), "no successful builds") |
| 1728 | XCTAssertFalse(app.staticTexts["failure"].firstMatch.exists, "a failed build survived the filter") |
| 1729 | |
| 1730 | // job = test as well: the rows name the job. |
| 1731 | filter.tap() |
| 1732 | choose("build-filter-job-menu", "test") |
| 1733 | app.buttons["Done"].firstMatch.tap() |
| 1734 | XCTAssertTrue(app.staticTexts |
| 1735 | .containing(NSPredicate(format: "label CONTAINS ' test'")).firstMatch |
| 1736 | .waitForExistence(timeout: 20), "job filter did not narrow to test") |
| 1737 | |
| 1738 | // status = pending on a quiet repo: the filtered empty state. |
| 1739 | filter.tap() |
| 1740 | choose("build-filter-status-menu", "pending") |
| 1741 | app.buttons["Done"].firstMatch.tap() |
| 1742 | XCTAssertTrue(app.staticTexts["No builds match these filters."].firstMatch |
| 1743 | .waitForExistence(timeout: 20), "filtered empty state missing") |
| 1744 | |
| 1745 | // Clear: the list is back. |
| 1746 | filter.tap() |
| 1747 | app.buttons["Clear filters"].firstMatch.tap() |
| 1748 | app.buttons["Done"].firstMatch.tap() |
| 1749 | XCTAssertTrue(app.cells.firstMatch.waitForExistence(timeout: 20), "list did not come back") |
| 1750 | } |
| 1751 | |
| 1752 | /// Close in favour of another: the first fixture merge request is |
| 1753 | /// closed from the app naming the second; the page then says so and |
| 1754 | /// links to it. |
| 1755 | func testCloseInFavourFlow() throws { |
| 1756 | openRepo(Self.fixtureRepo) |
| 1757 | app.staticTexts["Merge Requests"].firstMatch.tap() |
| 1758 | let superseded = app.staticTexts[Self.supersededTitle].firstMatch |
| 1759 | XCTAssertTrue(superseded.waitForExistence(timeout: 15), "fixture MR not listed") |
| 1760 | superseded.tap() |
| 1761 | |
| 1762 | app.descendants(matching: .any).matching(identifier: "mr-actions-menu").firstMatch.tap() |
| 1763 | let close = app.buttons["Close"].firstMatch |
| 1764 | XCTAssertTrue(close.waitForExistence(timeout: 5)) |
| 1765 | close.tap() |
| 1766 | let inFavour = app.buttons["Close in favour of another…"].firstMatch |
| 1767 | XCTAssertTrue(inFavour.waitForExistence(timeout: 5), "close dialog has no in-favour choice") |
| 1768 | inFavour.tap() |
| 1769 | |
| 1770 | let successor = app.staticTexts[Self.successorTitle].firstMatch |
| 1771 | XCTAssertTrue(successor.waitForExistence(timeout: 15), "successor not offered") |
| 1772 | successor.tap() |
| 1773 | let confirm = app.buttons["Close"].firstMatch |
| 1774 | XCTAssertTrue(confirm.waitForExistence(timeout: 5), "in-favour confirmation missing") |
| 1775 | confirm.tap() |
| 1776 | |
| 1777 | XCTAssertTrue(app.staticTexts |
| 1778 | .containing(NSPredicate(format: "label BEGINSWITH 'closed in favour of !'")).firstMatch |
| 1779 | .waitForExistence(timeout: 20), "resolution does not name the successor") |
| 1780 | let link = app.staticTexts |
| 1781 | .containing(NSPredicate(format: "label BEGINSWITH 'Superseded by !'")).firstMatch |
| 1782 | XCTAssertTrue(scrollTo(link), "superseded-by link missing") |
| 1783 | link.tap() |
| 1784 | XCTAssertTrue(app.staticTexts[Self.successorTitle].firstMatch.waitForExistence(timeout: 20), |
| 1785 | "link did not open the successor") |
| 1786 | } |
| 1787 | } |