Commit fffc51c75c

fffc51c75cc7490c4c70a939d82bd0a5ffeaa1ae

parent: b1290cb6a0

Unsigned

cmc <hello@cleberg.net> · 2026-08-27 14:30 UTC

ui tests: live smoke flows by accessibility, compose autocorrect off

gitbayUITests drives the flows the coordinate-based simulator input
could not be trusted to verify: milestone assignment, edit-sheet save,
MR create against a real branch, MR close through its confirmation
dialog. The suite writes to the live instance, so it is opt-in behind
TEST_RUNNER_GITBAY_UITEST_LIVE=1 and assumes a signed-in simulator.
Interactive controls carry accessibility identifiers.

Compose fields disable autocorrection: issue and MR text names code —
paths, branches, commands — and the keyboard rewrote krz/ to its/ in a
live body.

First pass of #5.

Ref #11

Layout: unified · split

gitbay.xcodeproj/project.pbxproj +110
@@ -18,11 +18,19 @@
18 remoteGlobalIDString = 8B289811303FF3E5001BF44A; 18 remoteGlobalIDString = 8B289811303FF3E5001BF44A;
19 remoteInfo = gitbay; 19 remoteInfo = gitbay;
20 }; 20 };
21 8B2898C7303FF3E5001BF44A /* PBXContainerItemProxy */ = {
22 isa = PBXContainerItemProxy;
23 containerPortal = 8B28980A303FF3E5001BF44A /* Project object */;
24 proxyType = 1;
25 remoteGlobalIDString = 8B289811303FF3E5001BF44A;
26 remoteInfo = gitbay;
27 };
21/* End PBXContainerItemProxy section */ 28/* End PBXContainerItemProxy section */
22 29
23/* Begin PBXFileReference section */ 30/* Begin PBXFileReference section */
24 8B289812303FF3E5001BF44A /* gitbay.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = gitbay.app; sourceTree = BUILT_PRODUCTS_DIR; }; 31 8B289812303FF3E5001BF44A /* gitbay.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = gitbay.app; sourceTree = BUILT_PRODUCTS_DIR; };
25 8B2898A2303FF3E5001BF44A /* gitbayTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = gitbayTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 32 8B2898A2303FF3E5001BF44A /* gitbayTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = gitbayTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
33 8B2898C2303FF3E5001BF44A /* gitbayUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = gitbayUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
26/* End PBXFileReference section */ 34/* End PBXFileReference section */
27 35
28/* Begin PBXFileSystemSynchronizedRootGroup section */ 36/* Begin PBXFileSystemSynchronizedRootGroup section */
@@ -36,6 +44,11 @@
36 path = gitbayTests; 44 path = gitbayTests;
37 sourceTree = "<group>"; 45 sourceTree = "<group>";
38 }; 46 };
47 8B2898C0303FF3E5001BF44A /* gitbayUITests */ = {
48 isa = PBXFileSystemSynchronizedRootGroup;
49 path = gitbayUITests;
50 sourceTree = "<group>";
51 };
39/* End PBXFileSystemSynchronizedRootGroup section */ 52/* End PBXFileSystemSynchronizedRootGroup section */
40 53
41/* Begin PBXFrameworksBuildPhase section */ 54/* Begin PBXFrameworksBuildPhase section */
@@ -54,6 +67,13 @@
54 ); 67 );
55 runOnlyForDeploymentPostprocessing = 0; 68 runOnlyForDeploymentPostprocessing = 0;
56 }; 69 };
70 8B2898C4303FF3E5001BF44A /* Frameworks */ = {
71 isa = PBXFrameworksBuildPhase;
72 buildActionMask = 2147483647;
73 files = (
74 );
75 runOnlyForDeploymentPostprocessing = 0;
76 };
57/* End PBXFrameworksBuildPhase section */ 77/* End PBXFrameworksBuildPhase section */
58 78
59/* Begin PBXGroup section */ 79/* Begin PBXGroup section */
@@ -62,6 +82,7 @@
62 children = ( 82 children = (
63 8B289814303FF3E5001BF44A /* gitbay */, 83 8B289814303FF3E5001BF44A /* gitbay */,
64 8B2898A0303FF3E5001BF44A /* gitbayTests */, 84 8B2898A0303FF3E5001BF44A /* gitbayTests */,
85 8B2898C0303FF3E5001BF44A /* gitbayUITests */,
65 8B289813303FF3E5001BF44A /* Products */, 86 8B289813303FF3E5001BF44A /* Products */,
66 ); 87 );
67 sourceTree = "<group>"; 88 sourceTree = "<group>";
@@ -71,6 +92,7 @@
71 children = ( 92 children = (
72 8B289812303FF3E5001BF44A /* gitbay.app */, 93 8B289812303FF3E5001BF44A /* gitbay.app */,
73 8B2898A2303FF3E5001BF44A /* gitbayTests.xctest */, 94 8B2898A2303FF3E5001BF44A /* gitbayTests.xctest */,
95 8B2898C2303FF3E5001BF44A /* gitbayUITests.xctest */,
74 ); 96 );
75 name = Products; 97 name = Products;
76 sourceTree = "<group>"; 98 sourceTree = "<group>";
@@ -124,6 +146,29 @@
124 productReference = 8B2898A2303FF3E5001BF44A /* gitbayTests.xctest */; 146 productReference = 8B2898A2303FF3E5001BF44A /* gitbayTests.xctest */;
125 productType = "com.apple.product-type.bundle.unit-test"; 147 productType = "com.apple.product-type.bundle.unit-test";
126 }; 148 };
149 8B2898C1303FF3E5001BF44A /* gitbayUITests */ = {
150 isa = PBXNativeTarget;
151 buildConfigurationList = 8B2898CA303FF3E5001BF44A /* Build configuration list for PBXNativeTarget "gitbayUITests" */;
152 buildPhases = (
153 8B2898C3303FF3E5001BF44A /* Sources */,
154 8B2898C4303FF3E5001BF44A /* Frameworks */,
155 8B2898C5303FF3E5001BF44A /* Resources */,
156 );
157 buildRules = (
158 );
159 dependencies = (
160 8B2898C6303FF3E5001BF44A /* PBXTargetDependency */,
161 );
162 fileSystemSynchronizedGroups = (
163 8B2898C0303FF3E5001BF44A /* gitbayUITests */,
164 );
165 name = gitbayUITests;
166 packageProductDependencies = (
167 );
168 productName = gitbayUITests;
169 productReference = 8B2898C2303FF3E5001BF44A /* gitbayUITests.xctest */;
170 productType = "com.apple.product-type.bundle.ui-testing";
171 };
127/* End PBXNativeTarget section */ 172/* End PBXNativeTarget section */
128 173
129/* Begin PBXProject section */ 174/* Begin PBXProject section */
@@ -141,6 +186,10 @@
141 CreatedOnToolsVersion = 26.6; 186 CreatedOnToolsVersion = 26.6;
142 TestTargetID = 8B289811303FF3E5001BF44A; 187 TestTargetID = 8B289811303FF3E5001BF44A;
143 }; 188 };
189 8B2898C1303FF3E5001BF44A = {
190 CreatedOnToolsVersion = 26.6;
191 TestTargetID = 8B289811303FF3E5001BF44A;
192 };
144 }; 193 };
145 }; 194 };
146 buildConfigurationList = 8B28980D303FF3E5001BF44A /* Build configuration list for PBXProject "gitbay" */; 195 buildConfigurationList = 8B28980D303FF3E5001BF44A /* Build configuration list for PBXProject "gitbay" */;
@@ -162,6 +211,7 @@
162 targets = ( 211 targets = (
163 8B289811303FF3E5001BF44A /* gitbay */, 212 8B289811303FF3E5001BF44A /* gitbay */,
164 8B2898A1303FF3E5001BF44A /* gitbayTests */, 213 8B2898A1303FF3E5001BF44A /* gitbayTests */,
214 8B2898C1303FF3E5001BF44A /* gitbayUITests */,
165 ); 215 );
166 }; 216 };
167/* End PBXProject section */ 217/* End PBXProject section */
@@ -181,6 +231,13 @@
181 ); 231 );
182 runOnlyForDeploymentPostprocessing = 0; 232 runOnlyForDeploymentPostprocessing = 0;
183 }; 233 };
234 8B2898C5303FF3E5001BF44A /* Resources */ = {
235 isa = PBXResourcesBuildPhase;
236 buildActionMask = 2147483647;
237 files = (
238 );
239 runOnlyForDeploymentPostprocessing = 0;
240 };
184/* End PBXResourcesBuildPhase section */ 241/* End PBXResourcesBuildPhase section */
185 242
186/* Begin PBXSourcesBuildPhase section */ 243/* Begin PBXSourcesBuildPhase section */
@@ -198,6 +255,13 @@
198 ); 255 );
199 runOnlyForDeploymentPostprocessing = 0; 256 runOnlyForDeploymentPostprocessing = 0;
200 }; 257 };
258 8B2898C3303FF3E5001BF44A /* Sources */ = {
259 isa = PBXSourcesBuildPhase;
260 buildActionMask = 2147483647;
261 files = (
262 );
263 runOnlyForDeploymentPostprocessing = 0;
264 };
201/* End PBXSourcesBuildPhase section */ 265/* End PBXSourcesBuildPhase section */
202 266
203/* Begin PBXTargetDependency section */ 267/* Begin PBXTargetDependency section */
@@ -206,6 +270,11 @@
206 target = 8B289811303FF3E5001BF44A /* gitbay */; 270 target = 8B289811303FF3E5001BF44A /* gitbay */;
207 targetProxy = 8B2898A7303FF3E5001BF44A /* PBXContainerItemProxy */; 271 targetProxy = 8B2898A7303FF3E5001BF44A /* PBXContainerItemProxy */;
208 }; 272 };
273 8B2898C6303FF3E5001BF44A /* PBXTargetDependency */ = {
274 isa = PBXTargetDependency;
275 target = 8B289811303FF3E5001BF44A /* gitbay */;
276 targetProxy = 8B2898C7303FF3E5001BF44A /* PBXContainerItemProxy */;
277 };
209/* End PBXTargetDependency section */ 278/* End PBXTargetDependency section */
210 279
211/* Begin XCBuildConfiguration section */ 280/* Begin XCBuildConfiguration section */
@@ -434,6 +503,38 @@
434 }; 503 };
435 name = Release; 504 name = Release;
436 }; 505 };
506 8B2898C8303FF3E5001BF44A /* Debug */ = {
507 isa = XCBuildConfiguration;
508 buildSettings = {
509 CURRENT_PROJECT_VERSION = 1;
510 DEVELOPMENT_TEAM = ZCNAX3VL9D;
511 GENERATE_INFOPLIST_FILE = YES;
512 MARKETING_VERSION = 1.0;
513 PRODUCT_BUNDLE_IDENTIFIER = org.gitbay.gitbayUITests;
514 PRODUCT_NAME = "$(TARGET_NAME)";
515 SWIFT_EMIT_LOC_STRINGS = NO;
516 SWIFT_VERSION = 6.0;
517 TARGETED_DEVICE_FAMILY = "1,2";
518 TEST_TARGET_NAME = gitbay;
519 };
520 name = Debug;
521 };
522 8B2898C9303FF3E5001BF44A /* Release */ = {
523 isa = XCBuildConfiguration;
524 buildSettings = {
525 CURRENT_PROJECT_VERSION = 1;
526 DEVELOPMENT_TEAM = ZCNAX3VL9D;
527 GENERATE_INFOPLIST_FILE = YES;
528 MARKETING_VERSION = 1.0;
529 PRODUCT_BUNDLE_IDENTIFIER = org.gitbay.gitbayUITests;
530 PRODUCT_NAME = "$(TARGET_NAME)";
531 SWIFT_EMIT_LOC_STRINGS = NO;
532 SWIFT_VERSION = 6.0;
533 TARGETED_DEVICE_FAMILY = "1,2";
534 TEST_TARGET_NAME = gitbay;
535 };
536 name = Release;
537 };
437/* End XCBuildConfiguration section */ 538/* End XCBuildConfiguration section */
438 539
439/* Begin XCConfigurationList section */ 540/* Begin XCConfigurationList section */
@@ -464,6 +565,15 @@
464 defaultConfigurationIsVisible = 0; 565 defaultConfigurationIsVisible = 0;
465 defaultConfigurationName = Release; 566 defaultConfigurationName = Release;
466 }; 567 };
568 8B2898CA303FF3E5001BF44A /* Build configuration list for PBXNativeTarget "gitbayUITests" */ = {
569 isa = XCConfigurationList;
570 buildConfigurations = (
571 8B2898C8303FF3E5001BF44A /* Debug */,
572 8B2898C9303FF3E5001BF44A /* Release */,
573 );
574 defaultConfigurationIsVisible = 0;
575 defaultConfigurationName = Release;
576 };
467/* End XCConfigurationList section */ 577/* End XCConfigurationList section */
468 578
469/* Begin XCRemoteSwiftPackageReference section */ 579/* Begin XCRemoteSwiftPackageReference section */
gitbay/Views/Issues/IssueListView.swift +1
@@ -41,6 +41,7 @@ struct IssueListView: View {
41 } label: { 41 } label: {
42 Image(systemName: "plus") 42 Image(systemName: "plus")
43 } 43 }
44 .accessibilityIdentifier("issue-create-button")
44 } 45 }
45 } 46 }
46 .sheet(isPresented: $composing) { 47 .sheet(isPresented: $composing) {
gitbay/Views/Issues/IssueView.swift +2
@@ -147,6 +147,7 @@ struct IssueView: View {
147 } 147 }
148 .disabled(model.working) 148 .disabled(model.working)
149 .task { await model.loadMilestones() } 149 .task { await model.loadMilestones() }
150 .accessibilityIdentifier("milestone-menu")
150 } 151 }
151 } 152 }
152 153
@@ -232,6 +233,7 @@ struct IssueView: View {
232 } 233 }
233 } 234 }
234 .disabled(model.working) 235 .disabled(model.working)
236 .accessibilityIdentifier("issue-actions-menu")
235 } 237 }
236 } 238 }
237 } 239 }
gitbay/Views/MRs/MRListView.swift +8
@@ -39,6 +39,7 @@ struct MRListView: View {
39 } label: { 39 } label: {
40 Image(systemName: "plus") 40 Image(systemName: "plus")
41 } 41 }
42 .accessibilityIdentifier("mr-create-button")
42 } 43 }
43 } 44 }
44 .sheet(isPresented: $composing) { 45 .sheet(isPresented: $composing) {
@@ -72,17 +73,23 @@ private struct MRCreateSheet: View {
72 TextField("Source branch", text: $source) 73 TextField("Source branch", text: $source)
73 .autocorrectionDisabled() 74 .autocorrectionDisabled()
74 .textInputAutocapitalization(.never) 75 .textInputAutocapitalization(.never)
76 .accessibilityIdentifier("mr-source")
75 TextField("Target branch", text: $target) 77 TextField("Target branch", text: $target)
76 .autocorrectionDisabled() 78 .autocorrectionDisabled()
77 .textInputAutocapitalization(.never) 79 .textInputAutocapitalization(.never)
80 .accessibilityIdentifier("mr-target")
78 } 81 }
79 Section("Title") { 82 Section("Title") {
80 TextField("Title", text: $title, axis: .vertical) 83 TextField("Title", text: $title, axis: .vertical)
81 .lineLimit(1...3) 84 .lineLimit(1...3)
85 .autocorrectionDisabled()
86 .accessibilityIdentifier("mr-title")
82 } 87 }
83 Section("Body") { 88 Section("Body") {
84 TextEditor(text: $bodyText) 89 TextEditor(text: $bodyText)
85 .frame(minHeight: 120) 90 .frame(minHeight: 120)
91 .autocorrectionDisabled()
92 .accessibilityIdentifier("mr-body")
86 } 93 }
87 if let error = model.errorMessage { 94 if let error = model.errorMessage {
88 Section { 95 Section {
@@ -117,6 +124,7 @@ private struct MRCreateSheet: View {
117 || target.trimmingCharacters(in: .whitespaces).isEmpty 124 || target.trimmingCharacters(in: .whitespaces).isEmpty
118 || title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty 125 || title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
119 ) 126 )
127 .accessibilityIdentifier("mr-submit")
120 } 128 }
121 } 129 }
122 } 130 }
gitbay/Views/MRs/MRView.swift +1
@@ -274,6 +274,7 @@ struct MRView: View {
274 } 274 }
275 } 275 }
276 .disabled(model.working) 276 .disabled(model.working)
277 .accessibilityIdentifier("mr-actions-menu")
277 } 278 }
278 } 279 }
279 } 280 }
gitbay/Views/Shared/ComposeSheet.swift +7
@@ -19,11 +19,17 @@ struct ComposeSheet: View {
19 Section("Title") { 19 Section("Title") {
20 TextField("Title", text: $title, axis: .vertical) 20 TextField("Title", text: $title, axis: .vertical)
21 .lineLimit(1...3) 21 .lineLimit(1...3)
22 // Titles name code: paths, commands, branch names.
23 // Autocorrection rewrites them.
24 .autocorrectionDisabled()
25 .accessibilityIdentifier("compose-title")
22 } 26 }
23 Section("Body") { 27 Section("Body") {
24 TextEditor(text: $bodyText) 28 TextEditor(text: $bodyText)
25 .frame(minHeight: 160) 29 .frame(minHeight: 160)
26 .font(.body) 30 .font(.body)
31 .autocorrectionDisabled()
32 .accessibilityIdentifier("compose-body")
27 } 33 }
28 if let errorMessage { 34 if let errorMessage {
29 Section { 35 Section {
@@ -45,6 +51,7 @@ struct ComposeSheet: View {
45 } else { 51 } else {
46 Button(submitLabel, action: onSubmit) 52 Button(submitLabel, action: onSubmit)
47 .disabled(title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) 53 .disabled(title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
54 .accessibilityIdentifier("compose-submit")
48 } 55 }
49 } 56 }
50 } 57 }
gitbayUITests/LiveSmokeUITests.swift added +142
@@ -0,0 +1,142 @@
1import XCTest
2
3/// Live smoke flows against gitbay.org, driven by accessibility — the
4/// coordinate-free verification the simulator's input pipeline cannot be
5/// trusted to do (krz/gitbay-ios#5).
6///
7/// These WRITE to the live instance (an MR is opened and closed, a
8/// milestone assigned), so they only run when explicitly asked:
9///
10/// TEST_RUNNER_GITBAY_UITEST_LIVE=1 xcodebuild test \
11/// -only-testing:gitbayUITests ...
12///
13/// They assume the simulator is already signed in as an account that can
14/// write to krz/gitbay-ios, and that branch `ui-smoke` exists.
15final class LiveSmokeUITests: XCTestCase {
16
17 private var app: XCUIApplication!
18
19 override func setUpWithError() throws {
20 try XCTSkipUnless(
21 ProcessInfo.processInfo.environment["GITBAY_UITEST_LIVE"] == "1",
22 "live UI smoke is opt-in; set TEST_RUNNER_GITBAY_UITEST_LIVE=1"
23 )
24 continueAfterFailure = false
25 app = XCUIApplication()
26 app.launch()
27 }
28
29 /// Everything in one ordered pass: milestone assign, edit-save,
30 /// MR create, MR close. One test so the flows share navigation and
31 /// the writes happen exactly once.
32 func testCreationAndEditingFlows() throws {
33 openRepo("krz/gitbay-ios")
34
35 // --- Issue #4: milestone picker assigns v1.0.0 ---
36 app.staticTexts["Issues"].firstMatch.tap()
37 let issueRow = app.staticTexts
38 .containing(NSPredicate(format: "label CONTAINS 'README.org'")).firstMatch
39 XCTAssertTrue(issueRow.waitForExistence(timeout: 10), "issue #4 not in the list")
40 issueRow.tap()
41
42 let milestoneMenu = app.descendants(matching: .any)
43 .matching(identifier: "milestone-menu").firstMatch
44 XCTAssertTrue(milestoneMenu.waitForExistence(timeout: 10))
45 milestoneMenu.tap()
46 let milestoneChoice = app.buttons
47 .containing(NSPredicate(format: "label BEGINSWITH 'v1.0.0'")).firstMatch
48 XCTAssertTrue(milestoneChoice.waitForExistence(timeout: 5), "milestone menu did not open")
49 milestoneChoice.tap()
50 // The reload renders the assigned milestone in the header row.
51 XCTAssertTrue(app.staticTexts["v1.0.0"].firstMatch
52 .waitForExistence(timeout: 10), "milestone not shown after assign")
53
54 // --- Issue #4: edit sheet saves (content unchanged) ---
55 app.descendants(matching: .any).matching(identifier: "issue-actions-menu")
56 .firstMatch.tap()
57 let edit = app.buttons["Edit"].firstMatch
58 XCTAssertTrue(edit.waitForExistence(timeout: 5))
59 edit.tap()
60 let title = app.descendants(matching: .any)
61 .matching(identifier: "compose-title").firstMatch
62 XCTAssertTrue(title.waitForExistence(timeout: 5), "edit sheet did not open")
63 XCTAssertTrue((title.value as? String)?.contains("README.org") == true,
64 "edit sheet did not prefill")
65 app.descendants(matching: .any).matching(identifier: "compose-submit")
66 .firstMatch.tap()
67 // Sheet dismissal proves the save round-tripped without error.
68 XCTAssertTrue(waitForDisappearance(title, timeout: 15), "edit save did not dismiss")
69
70 back() // to issues list
71 back() // to repo screen
72
73 // --- MR: create from ui-smoke, then close it ---
74 app.staticTexts["Merge Requests"].firstMatch.tap()
75 app.descendants(matching: .any).matching(identifier: "mr-create-button")
76 .firstMatch.tap()
77
78 let source = app.descendants(matching: .any)
79 .matching(identifier: "mr-source").firstMatch
80 XCTAssertTrue(source.waitForExistence(timeout: 5), "MR create sheet did not open")
81 source.tap()
82 source.typeText("ui-smoke")
83
84 let target = app.descendants(matching: .any)
85 .matching(identifier: "mr-target").firstMatch
86 // The default branch prefilled while the sheet loaded.
87 XCTAssertEqual(target.value as? String, "main", "target did not prefill")
88
89 let mrTitle = app.descendants(matching: .any)
90 .matching(identifier: "mr-title").firstMatch
91 mrTitle.tap()
92 mrTitle.typeText("UI smoke: mr create from the app")
93
94 app.descendants(matching: .any).matching(identifier: "mr-submit")
95 .firstMatch.tap()
96
97 let createdRow = app.staticTexts
98 .containing(NSPredicate(format: "label CONTAINS 'UI smoke'")).firstMatch
99 XCTAssertTrue(createdRow.waitForExistence(timeout: 15), "created MR not in the list")
100 createdRow.tap()
101
102 app.descendants(matching: .any).matching(identifier: "mr-actions-menu")
103 .firstMatch.tap()
104 let close = app.buttons["Close"].firstMatch
105 XCTAssertTrue(close.waitForExistence(timeout: 5))
106 close.tap()
107 // The confirmation dialog's destructive Close.
108 let confirm = app.buttons["Close"].firstMatch
109 XCTAssertTrue(confirm.waitForExistence(timeout: 5), "close confirmation missing")
110 confirm.tap()
111
112 XCTAssertTrue(app.staticTexts["closed"].firstMatch
113 .waitForExistence(timeout: 15), "MR did not show closed after close")
114 }
115
116 // MARK: - Helpers
117
118 private func openRepo(_ path: String) {
119 let tab = app.buttons["Repositories"].firstMatch
120 XCTAssertTrue(tab.waitForExistence(timeout: 10))
121 tab.tap()
122 let search = app.searchFields.firstMatch
123 XCTAssertTrue(search.waitForExistence(timeout: 10))
124 search.tap()
125 search.typeText(path)
126 let row = app.staticTexts[path].firstMatch
127 XCTAssertTrue(row.waitForExistence(timeout: 15), "\(path) not in the repo list")
128 row.tap()
129 // Repo screen is loaded once its links render.
130 XCTAssertTrue(app.staticTexts["Files"].firstMatch.waitForExistence(timeout: 10))
131 }
132
133 private func back() {
134 app.navigationBars.buttons.firstMatch.tap()
135 }
136
137 private func waitForDisappearance(_ element: XCUIElement, timeout: TimeInterval) -> Bool {
138 let predicate = NSPredicate(format: "exists == false")
139 let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element)
140 return XCTWaiter().wait(for: [expectation], timeout: timeout) == .completed
141 }
142}