|
1 | 1 | import Foundation |
| 2 | +import XCTest |
2 | 3 |
|
3 | 4 | enum RunnerCommandLifecycleState: String { |
4 | 5 | case notAccepted |
@@ -50,7 +51,7 @@ final class RunnerCommandJournal { |
50 | 51 | command: command, |
51 | 52 | state: response.ok ? .completed : .failed, |
52 | 53 | responseOk: response.ok, |
53 | | - responseJson: encodeResponseJson(response), |
| 54 | + responseJson: encodeResponseJson(command: command, response: response), |
54 | 55 | error: response.error |
55 | 56 | ) |
56 | 57 | } |
@@ -131,10 +132,151 @@ final class RunnerCommandJournal { |
131 | 132 | return trimmed.isEmpty ? nil : trimmed |
132 | 133 | } |
133 | 134 |
|
134 | | - private func encodeResponseJson(_ response: Response) -> String? { |
135 | | - guard response.data?.nodes == nil else { return nil } |
| 135 | + private func encodeResponseJson(command: Command, response: Response) -> String? { |
| 136 | + guard shouldRetainResponseJson(command: command) else { return nil } |
136 | 137 | guard let data = try? JSONEncoder().encode(response) else { return nil } |
137 | 138 | guard data.count <= maxResponseJsonBytes else { return nil } |
138 | 139 | return String(data: data, encoding: .utf8) |
139 | 140 | } |
| 141 | + |
| 142 | + private func shouldRetainResponseJson(command: Command) -> Bool { |
| 143 | + switch command.command { |
| 144 | + case .snapshot, .screenshot: |
| 145 | + return false |
| 146 | + case .tap, .mouseClick, .tapSeries, .longPress, .interactionFrame, .drag, .dragSeries, |
| 147 | + .remotePress, .type, .swipe, .findText, .querySelector, .readText, .back, .backInApp, |
| 148 | + .backSystem, .home, .rotate, .appSwitcher, .keyboardDismiss, .keyboardReturn, .alert, |
| 149 | + .pinch, .rotateGesture, .transformGesture, .recordStart, .recordStop, .status, .uptime, |
| 150 | + .shutdown: |
| 151 | + return true |
| 152 | + } |
| 153 | + } |
| 154 | +} |
| 155 | + |
| 156 | +extension RunnerTests { |
| 157 | + func testCommandJournalRetentionPolicy() throws { |
| 158 | + let journal = RunnerCommandJournal() |
| 159 | + |
| 160 | + let uptime = runnerJournalCommand("uptime", id: "small-scalar") |
| 161 | + journal.accept(command: uptime) |
| 162 | + journal.finish( |
| 163 | + command: uptime, |
| 164 | + response: Response(ok: true, data: DataPayload(currentUptimeMs: 12.5)) |
| 165 | + ) |
| 166 | + |
| 167 | + let scalarStatus = journal.status(commandId: "small-scalar") |
| 168 | + XCTAssertEqual(scalarStatus.lifecycleState, RunnerCommandLifecycleState.completed.rawValue) |
| 169 | + XCTAssertEqual(scalarStatus.lifecycleResponseOk, true) |
| 170 | + XCTAssertNotNil(scalarStatus.lifecycleResponseJson) |
| 171 | + let scalarResponse = try decodeRunnerJournalResponse(scalarStatus.lifecycleResponseJson) |
| 172 | + XCTAssertEqual(scalarResponse.data?.currentUptimeMs, 12.5) |
| 173 | + |
| 174 | + let querySelector = runnerJournalCommand("querySelector", id: "small-object") |
| 175 | + journal.accept(command: querySelector) |
| 176 | + journal.finish( |
| 177 | + command: querySelector, |
| 178 | + response: Response(ok: true, data: DataPayload(found: true, nodes: [runnerJournalNode()])) |
| 179 | + ) |
| 180 | + |
| 181 | + let objectStatus = journal.status(commandId: "small-object") |
| 182 | + XCTAssertNotNil(objectStatus.lifecycleResponseJson) |
| 183 | + let objectResponse = try decodeRunnerJournalResponse(objectStatus.lifecycleResponseJson) |
| 184 | + XCTAssertEqual(objectResponse.data?.found, true) |
| 185 | + XCTAssertEqual(objectResponse.data?.nodes?.count, 1) |
| 186 | + |
| 187 | + let snapshot = runnerJournalCommand("snapshot", id: "snapshot-tree") |
| 188 | + journal.accept(command: snapshot) |
| 189 | + journal.finish( |
| 190 | + command: snapshot, |
| 191 | + response: Response(ok: true, data: DataPayload(nodes: [runnerJournalNode()], truncated: false)) |
| 192 | + ) |
| 193 | + |
| 194 | + let snapshotStatus = journal.status(commandId: "snapshot-tree") |
| 195 | + XCTAssertEqual(snapshotStatus.lifecycleState, RunnerCommandLifecycleState.completed.rawValue) |
| 196 | + XCTAssertEqual(snapshotStatus.lifecycleResponseOk, true) |
| 197 | + XCTAssertNil(snapshotStatus.lifecycleResponseJson) |
| 198 | + |
| 199 | + let screenshot = runnerJournalCommand("screenshot", id: "screenshot-artifact") |
| 200 | + journal.accept(command: screenshot) |
| 201 | + journal.finish( |
| 202 | + command: screenshot, |
| 203 | + response: Response(ok: true, data: DataPayload(message: "tmp/screenshot-1.png")) |
| 204 | + ) |
| 205 | + |
| 206 | + let screenshotStatus = journal.status(commandId: "screenshot-artifact") |
| 207 | + XCTAssertEqual(screenshotStatus.lifecycleState, RunnerCommandLifecycleState.completed.rawValue) |
| 208 | + XCTAssertEqual(screenshotStatus.lifecycleResponseOk, true) |
| 209 | + XCTAssertNil(screenshotStatus.lifecycleResponseJson) |
| 210 | + |
| 211 | + let largeRead = runnerJournalCommand("readText", id: "large-read") |
| 212 | + journal.accept(command: largeRead) |
| 213 | + journal.finish( |
| 214 | + command: largeRead, |
| 215 | + response: Response(ok: true, data: DataPayload(text: String(repeating: "x", count: 17 * 1024))) |
| 216 | + ) |
| 217 | + |
| 218 | + let largeReadStatus = journal.status(commandId: "large-read") |
| 219 | + XCTAssertEqual(largeReadStatus.lifecycleState, RunnerCommandLifecycleState.completed.rawValue) |
| 220 | + XCTAssertEqual(largeReadStatus.lifecycleResponseOk, true) |
| 221 | + XCTAssertNil(largeReadStatus.lifecycleResponseJson) |
| 222 | + } |
| 223 | + |
| 224 | + func testCommandJournalKeepsErrorMetadataWhenResponseJsonIsDropped() { |
| 225 | + let journal = RunnerCommandJournal() |
| 226 | + let snapshot = runnerJournalCommand("snapshot", id: "snapshot-error") |
| 227 | + let hint = "Try a smaller read such as snapshot -s <visible label or id> -d 8." |
| 228 | + |
| 229 | + journal.accept(command: snapshot) |
| 230 | + journal.finish( |
| 231 | + command: snapshot, |
| 232 | + response: Response( |
| 233 | + ok: false, |
| 234 | + error: ErrorPayload( |
| 235 | + code: "IOS_AX_SNAPSHOT_FAILED", |
| 236 | + message: "iOS XCTest snapshot failed while serializing the accessibility tree.", |
| 237 | + hint: hint |
| 238 | + ) |
| 239 | + ) |
| 240 | + ) |
| 241 | + |
| 242 | + let status = journal.status(commandId: "snapshot-error") |
| 243 | + XCTAssertEqual(status.lifecycleState, RunnerCommandLifecycleState.failed.rawValue) |
| 244 | + XCTAssertEqual(status.lifecycleResponseOk, false) |
| 245 | + XCTAssertNil(status.lifecycleResponseJson) |
| 246 | + XCTAssertEqual(status.lifecycleErrorCode, "IOS_AX_SNAPSHOT_FAILED") |
| 247 | + XCTAssertEqual( |
| 248 | + status.lifecycleErrorMessage, |
| 249 | + "iOS XCTest snapshot failed while serializing the accessibility tree." |
| 250 | + ) |
| 251 | + XCTAssertEqual(status.lifecycleErrorHint, hint) |
| 252 | + } |
| 253 | + |
| 254 | + private func runnerJournalCommand(_ command: String, id: String) -> Command { |
| 255 | + let json = #"{"command":"\#(command)","commandId":"\#(id)"}"# |
| 256 | + return try! JSONDecoder().decode(Command.self, from: Data(json.utf8)) |
| 257 | + } |
| 258 | + |
| 259 | + private func runnerJournalNode() -> SnapshotNode { |
| 260 | + SnapshotNode( |
| 261 | + index: 0, |
| 262 | + type: "button", |
| 263 | + label: "Continue", |
| 264 | + identifier: "continue", |
| 265 | + value: nil, |
| 266 | + rect: SnapshotRect(x: 10, y: 20, width: 100, height: 44), |
| 267 | + enabled: true, |
| 268 | + focused: nil, |
| 269 | + selected: nil, |
| 270 | + hittable: true, |
| 271 | + depth: 0, |
| 272 | + parentIndex: nil, |
| 273 | + hiddenContentAbove: nil, |
| 274 | + hiddenContentBelow: nil |
| 275 | + ) |
| 276 | + } |
| 277 | + |
| 278 | + private func decodeRunnerJournalResponse(_ responseJson: String?) throws -> Response { |
| 279 | + let responseJson = try XCTUnwrap(responseJson) |
| 280 | + return try JSONDecoder().decode(Response.self, from: Data(responseJson.utf8)) |
| 281 | + } |
140 | 282 | } |
0 commit comments