Commit 604f2da53e
Unsigned
Layout: unified · split
gitbayUITests/LiveSmokeUITests.swift +41 −23
| @@ -11,9 +11,10 @@ import XCTest | |||
| 11 | /// -only-testing:gitbayUITests ... | 11 | /// -only-testing:gitbayUITests ... |
| 12 | /// | 12 | /// |
| 13 | /// They run as the `ios-smoke` account, a krz admin that owns the | 13 | /// They run as the `ios-smoke` account, a krz admin that owns the |
| 14 | /// scratch and fixture repositories (smoke-account.sh provisions it), and | 14 | /// scratch and fixture repositories (smoke-account.sh provisions it). |
| 15 | /// assume branch `ui-smoke` exists on krz/gitbay-ios. A signed-out device | 15 | /// Everything they write goes to those, never to krz/gitbay-ios. A |
| 16 | /// is signed in at setUp from TEST_RUNNER_GITBAY_UITEST_TOKEN. | 16 | /// signed-out device is signed in at setUp from |
| 17 | /// TEST_RUNNER_GITBAY_UITEST_TOKEN. | ||
| 17 | final class LiveSmokeUITests: XCTestCase { | 18 | final class LiveSmokeUITests: XCTestCase { |
| 18 | 19 | ||
| 19 | private var app: XCUIApplication! | 20 | private var app: XCUIApplication! |
| @@ -32,7 +33,11 @@ final class LiveSmokeUITests: XCTestCase { | |||
| 32 | static let smokeUser = "ios-smoke" | 33 | static let smokeUser = "ios-smoke" |
| 33 | static let smokeProfileDescription = "Live UI smoke account for gitbay-ios" | 34 | static let smokeProfileDescription = "Live UI smoke account for gitbay-ios" |
| 34 | static let smokePGPEmail = "ios-smoke+pgp@gitbay.org" | 35 | static let smokePGPEmail = "ios-smoke+pgp@gitbay.org" |
| 35 | static let editRepo = "\(smokeUser)/ui-smoke-edit" | 36 | |
| 37 | /// The repository the write flows work in: the fixture issue and | ||
| 38 | /// merge requests, and the file testBlameAndEditFlows edits. | ||
| 39 | /// smoke-account.sh seeds it, with `main` and `ui-smoke` divergent. | ||
| 40 | static let fixtureRepo = "\(smokeUser)/ui-smoke-edit" | ||
| 36 | 41 | ||
| 37 | override func setUpWithError() throws { | 42 | override func setUpWithError() throws { |
| 38 | try XCTSkipUnless( | 43 | try XCTSkipUnless( |
| @@ -56,6 +61,8 @@ final class LiveSmokeUITests: XCTestCase { | |||
| 56 | deleteScratchRepo() | 61 | deleteScratchRepo() |
| 57 | } | 62 | } |
| 58 | if name.contains("testCreationAndEditingFlows") { | 63 | if name.contains("testCreationAndEditingFlows") { |
| 64 | try requireFixtureRepo() | ||
| 65 | ensureFixtureMilestone() | ||
| 59 | ensureFixtureIssue() | 66 | ensureFixtureIssue() |
| 60 | try requireFixtureBranch() | 67 | try requireFixtureBranch() |
| 61 | } | 68 | } |
| @@ -69,6 +76,7 @@ final class LiveSmokeUITests: XCTestCase { | |||
| 69 | ensureOrgFixtures() | 76 | ensureOrgFixtures() |
| 70 | } | 77 | } |
| 71 | if name.contains("testCloseInFavourFlow") { | 78 | if name.contains("testCloseInFavourFlow") { |
| 79 | try requireFixtureRepo() | ||
| 72 | try requireFixtureBranch() | 80 | try requireFixtureBranch() |
| 73 | closeSmokeMergeRequests() | 81 | closeSmokeMergeRequests() |
| 74 | try openSupersedeFixtures() | 82 | try openSupersedeFixtures() |
| @@ -143,7 +151,7 @@ final class LiveSmokeUITests: XCTestCase { | |||
| 143 | "--title", Self.successorTitle]) | 151 | "--title", Self.successorTitle]) |
| 144 | let open = openSmokeMergeRequests().map { $0.title } | 152 | let open = openSmokeMergeRequests().map { $0.title } |
| 145 | try XCTSkipUnless(open.contains(Self.supersededTitle) && open.contains(Self.successorTitle), """ | 153 | try XCTSkipUnless(open.contains(Self.supersededTitle) && open.contains(Self.successorTitle), """ |
| 146 | could not open both fixture merge requests on krz/gitbay-ios \ | 154 | could not open both fixture merge requests on \(Self.fixtureRepo) \ |
| 147 | (have \(open)); the ui-smoke branch may be level with main. | 155 | (have \(open)); the ui-smoke branch may be level with main. |
| 148 | """) | 156 | """) |
| 149 | } | 157 | } |
| @@ -175,6 +183,19 @@ final class LiveSmokeUITests: XCTestCase { | |||
| 175 | _ = runCommand(["repo", "delete", Self.scratchRepo + "-2", "--yes"]) | 183 | _ = runCommand(["repo", "delete", Self.scratchRepo + "-2", "--yes"]) |
| 176 | } | 184 | } |
| 177 | 185 | ||
| 186 | /// The milestone the editing flow assigns. Created rather than | ||
| 187 | /// required: unlike a branch, the API can make one. | ||
| 188 | static let fixtureMilestone = "v1.0.0" | ||
| 189 | |||
| 190 | private func ensureFixtureMilestone() { | ||
| 191 | guard let data = readCommand(["milestone", "list", Self.fixtureRepo, "--state", "all"]), | ||
| 192 | let envelope = try? JSONSerialization.jsonObject(with: data) as? [String: Any] | ||
| 193 | else { return } | ||
| 194 | let rows = envelope["data"] as? [[String: Any]] ?? [] | ||
| 195 | if rows.contains(where: { $0["title"] as? String == Self.fixtureMilestone }) { return } | ||
| 196 | _ = runCommand(["milestone", "create", Self.fixtureRepo, Self.fixtureMilestone]) | ||
| 197 | } | ||
| 198 | |||
| 178 | /// The issue the editing flow works on. It is a fixture, not a real | 199 | /// The issue the editing flow works on. It is a fixture, not a real |
| 179 | /// issue: pinning the test to whatever issue happened to be open put | 200 | /// issue: pinning the test to whatever issue happened to be open put |
| 180 | /// it at the mercy of the project moving on, and it duly broke when | 201 | /// it at the mercy of the project moving on, and it duly broke when |
| @@ -200,8 +221,6 @@ final class LiveSmokeUITests: XCTestCase { | |||
| 200 | ) | 221 | ) |
| 201 | } | 222 | } |
| 202 | 223 | ||
| 203 | static let fixtureRepo = "krz/gitbay-ios" | ||
| 204 | |||
| 205 | /// The MR flow needs a source branch carrying a commit main does not | 224 | /// The MR flow needs a source branch carrying a commit main does not |
| 206 | /// have. Nothing in the app or the API can create a branch — that is | 225 | /// have. Nothing in the app or the API can create a branch — that is |
| 207 | /// a push — so this only checks, and says so plainly rather than | 226 | /// a push — so this only checks, and says so plainly rather than |
| @@ -214,9 +233,9 @@ final class LiveSmokeUITests: XCTestCase { | |||
| 214 | else { return } // no token, or the instance is unreachable | 233 | else { return } // no token, or the instance is unreachable |
| 215 | let names = branches.compactMap { $0["name"] as? String } | 234 | let names = branches.compactMap { $0["name"] as? String } |
| 216 | try XCTSkipUnless(names.contains("ui-smoke"), """ | 235 | try XCTSkipUnless(names.contains("ui-smoke"), """ |
| 217 | krz/gitbay-ios has no ui-smoke branch. The MR flow opens a \ | 236 | \(Self.fixtureRepo) has no ui-smoke branch. The MR flows open \ |
| 218 | merge request from it; recreate it with a commit main does \ | 237 | one merge request each way between it and main, so each side \ |
| 219 | not have (see .gitbay/ui-smoke.md on that branch). | 238 | needs a commit the other lacks; smoke-account.sh seeds both. |
| 220 | """) | 239 | """) |
| 221 | } | 240 | } |
| 222 | 241 | ||
| @@ -265,9 +284,9 @@ final class LiveSmokeUITests: XCTestCase { | |||
| 265 | /// MR create, MR close. One test so the flows share navigation and | 284 | /// MR create, MR close. One test so the flows share navigation and |
| 266 | /// the writes happen exactly once. | 285 | /// the writes happen exactly once. |
| 267 | func testCreationAndEditingFlows() throws { | 286 | func testCreationAndEditingFlows() throws { |
| 268 | openRepo("krz/gitbay-ios") | 287 | openRepo(Self.fixtureRepo) |
| 269 | 288 | ||
| 270 | // --- the fixture issue: milestone picker assigns v1.0.0 --- | 289 | // --- the fixture issue: milestone picker assigns the fixture --- |
| 271 | app.staticTexts["Issues"].firstMatch.tap() | 290 | app.staticTexts["Issues"].firstMatch.tap() |
| 272 | let issueRow = app.staticTexts | 291 | let issueRow = app.staticTexts |
| 273 | .containing(NSPredicate(format: "label CONTAINS 'ui-smoke fixture'")).firstMatch | 292 | .containing(NSPredicate(format: "label CONTAINS 'ui-smoke fixture'")).firstMatch |
| @@ -280,11 +299,11 @@ final class LiveSmokeUITests: XCTestCase { | |||
| 280 | XCTAssertTrue(milestoneMenu.waitForExistence(timeout: 10)) | 299 | XCTAssertTrue(milestoneMenu.waitForExistence(timeout: 10)) |
| 281 | milestoneMenu.tap() | 300 | milestoneMenu.tap() |
| 282 | let milestoneChoice = app.buttons | 301 | let milestoneChoice = app.buttons |
| 283 | .containing(NSPredicate(format: "label BEGINSWITH 'v1.0.0'")).firstMatch | 302 | .containing(NSPredicate(format: "label BEGINSWITH %@", Self.fixtureMilestone)).firstMatch |
| 284 | XCTAssertTrue(milestoneChoice.waitForExistence(timeout: 5), "milestone menu did not open") | 303 | XCTAssertTrue(milestoneChoice.waitForExistence(timeout: 5), "milestone menu did not open") |
| 285 | milestoneChoice.tap() | 304 | milestoneChoice.tap() |
| 286 | // The reload renders the assigned milestone in the header row. | 305 | // The reload renders the assigned milestone in the header row. |
| 287 | XCTAssertTrue(app.staticTexts["v1.0.0"].firstMatch | 306 | XCTAssertTrue(app.staticTexts[Self.fixtureMilestone].firstMatch |
| 288 | .waitForExistence(timeout: 10), "milestone not shown after assign") | 307 | .waitForExistence(timeout: 10), "milestone not shown after assign") |
| 289 | 308 | ||
| 290 | // --- the fixture issue: edit sheet saves (content unchanged) --- | 309 | // --- the fixture issue: edit sheet saves (content unchanged) --- |
| @@ -415,18 +434,17 @@ final class LiveSmokeUITests: XCTestCase { | |||
| 415 | keys.tap() | 434 | keys.tap() |
| 416 | } | 435 | } |
| 417 | 436 | ||
| 418 | /// The edit flow saves a change to a real file. Seeding a | 437 | /// Seeding a repository's first commit is a push, which no test can |
| 419 | /// repository's first commit is a push, which no test can make, so | 438 | /// make, so this only checks — and says why, rather than failing |
| 420 | /// this only checks — and says why, rather than failing later at | 439 | /// later at "ios-smoke/ui-smoke-edit not in the repo list". |
| 421 | /// "ios-smoke/ui-smoke-edit not in the repo list". | ||
| 422 | private func requireFixtureRepo() throws { | 440 | private func requireFixtureRepo() throws { |
| 423 | guard let data = readCommand(["repo", "show", Self.editRepo]), | 441 | guard let data = readCommand(["repo", "show", Self.fixtureRepo]), |
| 424 | let envelope = try? JSONSerialization.jsonObject(with: data) as? [String: Any] | 442 | let envelope = try? JSONSerialization.jsonObject(with: data) as? [String: Any] |
| 425 | else { return } // no token, or the instance is unreachable | 443 | else { return } // no token, or the instance is unreachable |
| 426 | try XCTSkipUnless(envelope["data"] != nil, """ | 444 | try XCTSkipUnless(envelope["data"] != nil, """ |
| 427 | \(Self.editRepo) is missing. The edit flow saves a change to \ | 445 | \(Self.fixtureRepo) is missing. The write flows work in it; \ |
| 428 | notes.txt in it; gitbayUITests/smoke-account.sh seeds it (or \ | 446 | gitbayUITests/smoke-account.sh seeds it (or push it by hand, \ |
| 429 | push it by hand, see that script) and run again. | 447 | see that script) and run again. |
| 430 | """) | 448 | """) |
| 431 | } | 449 | } |
| 432 | 450 | ||
| @@ -1186,7 +1204,7 @@ extension LiveSmokeUITests { | |||
| 1186 | // previous query, and clearing it from the harness is unreliable. | 1204 | // previous query, and clearing it from the harness is unreliable. |
| 1187 | app.terminate() | 1205 | app.terminate() |
| 1188 | app.launch() | 1206 | app.launch() |
| 1189 | openRepo(Self.editRepo) | 1207 | openRepo(Self.fixtureRepo) |
| 1190 | app.staticTexts["Files"].firstMatch.tap() | 1208 | app.staticTexts["Files"].firstMatch.tap() |
| 1191 | let notes = app.staticTexts["notes.txt"].firstMatch | 1209 | let notes = app.staticTexts["notes.txt"].firstMatch |
| 1192 | XCTAssertTrue(notes.waitForExistence(timeout: 15), "notes.txt not in the tree") | 1210 | XCTAssertTrue(notes.waitForExistence(timeout: 15), "notes.txt not in the tree") |
gitbayUITests/smoke-account.sh +53 −16
| @@ -4,7 +4,9 @@ | |||
| 4 | # what already exists, so a failed run is rerun as is. It creates the | 4 | # what already exists, so a failed run is rerun as is. It creates the |
| 5 | # account with a fresh SSH key (~/.ssh/gitbay-ios-smoke), makes it a krz | 5 | # account with a fresh SSH key (~/.ssh/gitbay-ios-smoke), makes it a krz |
| 6 | # admin, sets its profile description, registers a PGP key, and pushes | 6 | # admin, sets its profile description, registers a PGP key, and pushes |
| 7 | # the ios-smoke/ui-smoke-edit fixture that testBlameAndEditFlows edits. | 7 | # the ios-smoke/ui-smoke-edit fixture the write flows work in: notes.txt |
| 8 | # for testBlameAndEditFlows, and a ui-smoke branch divergent from main | ||
| 9 | # for the merge-request flows. | ||
| 8 | # LiveSmokeUITests asserts on the same values, so keep the two in step. | 10 | # LiveSmokeUITests asserts on the same values, so keep the two in step. |
| 9 | set -eu | 11 | set -eu |
| 10 | 12 | ||
| @@ -39,37 +41,72 @@ if ! as_smoke pgp list --json | grep -q "$pgp_email"; then | |||
| 39 | rm -rf "$gnupg" | 41 | rm -rf "$gnupg" |
| 40 | fi | 42 | fi |
| 41 | 43 | ||
| 42 | if as_smoke repo show "$user/ui-smoke-edit" --json > /dev/null 2>&1; then | 44 | repo=$user/ui-smoke-edit |
| 43 | echo "$user/ui-smoke-edit exists" | 45 | url=ssh://git@gitbay.org/$repo.git |
| 44 | exit 0 | ||
| 45 | fi | ||
| 46 | as_smoke repo create "$user/ui-smoke-edit" | ||
| 47 | work=$(mktemp -d) | 46 | work=$(mktemp -d) |
| 48 | git -C "$work" init -q -b main | 47 | trap 'rm -rf "$work"' EXIT |
| 49 | cat > "$work/README.md" <<'EOF' | 48 | |
| 49 | commit_in_work() { | ||
| 50 | git -C "$work" add -A | ||
| 51 | git -C "$work" -c user.name="$user" -c user.email="$user@gitbay.org" \ | ||
| 52 | -c commit.gpgsign=false commit -q -m "$1" | ||
| 53 | } | ||
| 54 | |||
| 55 | push_work() { | ||
| 56 | GIT_SSH_COMMAND="$smoke_ssh" git -C "$work" push -q "$url" "$1" | ||
| 57 | } | ||
| 58 | |||
| 59 | if as_smoke repo show "$repo" --json > /dev/null 2>&1; then | ||
| 60 | echo "$repo exists" | ||
| 61 | GIT_SSH_COMMAND="$smoke_ssh" git clone -q "$url" "$work" | ||
| 62 | else | ||
| 63 | as_smoke repo create "$repo" | ||
| 64 | git -C "$work" init -q -b main | ||
| 65 | cat > "$work/README.md" <<'EOF' | ||
| 50 | # ui-smoke-edit | 66 | # ui-smoke-edit |
| 51 | 67 | ||
| 52 | A fixture repository for the gitbay-ios live UI smoke suite. `notes.txt` | 68 | A fixture repository for the gitbay-ios live UI smoke suite. `notes.txt` |
| 53 | is edited by `testBlameAndEditFlows` on every run. | 69 | is edited by `testBlameAndEditFlows` on every run, and the `ui-smoke` |
| 70 | branch is what the merge-request flows open merge requests from. | ||
| 54 | 71 | ||
| 55 | Nothing in the app or the JSON API can seed a repository's first commit | 72 | Nothing in the app or the JSON API can seed a repository's first commit |
| 56 | (that is a push), so the suite only checks that this repo exists and | 73 | (that is a push), so the suite only checks that this repo exists and |
| 57 | skips when it does not. `gitbayUITests/smoke-account.sh` in krz/gitbay-ios | 74 | skips when it does not. `gitbayUITests/smoke-account.sh` in krz/gitbay-ios |
| 58 | is what created it. | 75 | is what created it. |
| 59 | EOF | 76 | EOF |
| 60 | cat > "$work/notes.txt" <<'EOF' | 77 | cat > "$work/notes.txt" <<'EOF' |
| 61 | Scratch content for the live UI smoke suite. | 78 | Scratch content for the live UI smoke suite. |
| 62 | 79 | ||
| 63 | testBlameAndEditFlows opens this file and saves an edit through the app, | 80 | testBlameAndEditFlows opens this file and saves an edit through the app, |
| 64 | which is how the file-editing path is verified end to end. The text does | 81 | which is how the file-editing path is verified end to end. The text does |
| 65 | not matter; the file existing does. | 82 | not matter; the file existing does. |
| 66 | EOF | 83 | EOF |
| 67 | git -C "$work" add -A | 84 | commit_in_work 'fixture repo for the live smoke suite' |
| 68 | git -C "$work" -c user.name="$user" -c user.email="$user@gitbay.org" -c commit.gpgsign=false \ | 85 | push_work main |
| 69 | commit -q -m 'fixture repo for the live smoke suite' | 86 | fi |
| 70 | GIT_SSH_COMMAND="$smoke_ssh" \ | 87 | |
| 71 | git -C "$work" push -q "ssh://git@gitbay.org/$user/ui-smoke-edit.git" main | 88 | # The MR flows open one merge request each way between ui-smoke and |
| 72 | rm -rf "$work" | 89 | # main, so each side needs a commit the other lacks. Only a push can |
| 90 | # make a branch, which is why this lives here and not in the suite. | ||
| 91 | if ! as_smoke repo refs "$repo" --json | grep -q '"ui-smoke"'; then | ||
| 92 | git -C "$work" checkout -q -b ui-smoke | ||
| 93 | cat > "$work/ui-smoke.md" <<'EOF' | ||
| 94 | This branch exists so the live UI smoke suite has a merge request to | ||
| 95 | open. It is never merged: the suite opens merge requests both ways | ||
| 96 | between it and main, and closes them again. | ||
| 97 | EOF | ||
| 98 | commit_in_work 'ui-smoke: a commit main does not have' | ||
| 99 | push_work ui-smoke | ||
| 100 | |||
| 101 | git -C "$work" checkout -q main | ||
| 102 | cat > "$work/main.md" <<'EOF' | ||
| 103 | The counterpart of ui-smoke.md: a commit on main that the ui-smoke | ||
| 104 | branch does not have, so a merge request from main to ui-smoke has | ||
| 105 | something to carry. | ||
| 106 | EOF | ||
| 107 | commit_in_work 'main: a commit ui-smoke does not have' | ||
| 108 | push_work main | ||
| 109 | fi | ||
| 73 | 110 | ||
| 74 | echo "Mint the suite's token with:" | 111 | echo "Mint the suite's token with:" |
| 75 | echo " $smoke_ssh git@gitbay.org token create --name ui-smoke --scope full --ttl 30d" | 112 | echo " $smoke_ssh git@gitbay.org token create --name ui-smoke --scope full --ttl 30d" |