| @@ -52,12 +52,107 @@ final class LiveSmokeUITests: XCTestCase { |
| 52 | if name.contains("testBlameAndEditFlows") { |
52 | if name.contains("testBlameAndEditFlows") { |
| 53 | try requireFixtureRepo() |
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 | override func tearDownWithError() throws { |
68 | override func tearDownWithError() throws { |
| 58 | if name.contains("testRepoManagementFlows") { |
69 | if name.contains("testRepoManagementFlows") { |
| 59 | deleteScratchRepo() |
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 | /// `repo delete` over the JSON API — the app has no delete screen |
158 | /// `repo delete` over the JSON API — the app has no delete screen |
| @@ -821,14 +916,14 @@ extension LiveSmokeUITests { |
| 821 | // Dashboard toolbar -> account screen. |
916 | // Dashboard toolbar -> account screen. |
| 822 | openAccountScreen() |
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 | XCTAssertTrue(app.staticTexts |
920 | XCTAssertTrue(app.staticTexts |
| 826 | .containing(NSPredicate(format: "label BEGINSWITH 'SHA256:'")).firstMatch |
921 | .containing(NSPredicate(format: "label BEGINSWITH 'SHA256:'")).firstMatch |
| 827 | .waitForExistence(timeout: 15), "SSH keys missing") |
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 | // Pasting garbage as an SSH key surfaces the server's validation. |
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 | app.descendants(matching: .any).matching(identifier: "add-ssh-key") |
927 | app.descendants(matching: .any).matching(identifier: "add-ssh-key") |
| 833 | .firstMatch.tap() |
928 | .firstMatch.tap() |
| 834 | let paste = app.descendants(matching: .any) |
929 | let paste = app.descendants(matching: .any) |
| @@ -842,6 +937,11 @@ extension LiveSmokeUITests { |
| 842 | .waitForExistence(timeout: 15), "invalid-key refusal not surfaced") |
937 | .waitForExistence(timeout: 15), "invalid-key refusal not surfaced") |
| 843 | app.buttons["Cancel"].firstMatch.tap() |
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 | // Activity mail flips off and back on; the value comes back from |
945 | // Activity mail flips off and back on; the value comes back from |
| 846 | // the server each time. |
946 | // the server each time. |
| 847 | let mail = app.switches["Activity by mail"].firstMatch |
947 | let mail = app.switches["Activity by mail"].firstMatch |
| @@ -1373,3 +1473,315 @@ extension LiveSmokeUITests { |
| 1373 | snap("profile") |
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 | } |