Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions StreamChatSwiftUITestsAppTests/Pages/PhotoLibrary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@ enum PhotoLibrary {
XCTFail("Could not write the video: \(error)", file: file, line: line)
return
}
requestAddAuthorization()
guard requestAddAuthorization() == .authorized else {
XCTFail(
"Photo library access is denied, reset it with `xcrun simctl privacy <udid> reset photos-add io.getstream.iOS.StreamChatSwiftUITestsAppTests.xctrunner`",
file: file,
line: line
)
return
}

let saved = XCTestExpectation(description: "The video is saved to the photo library")
nonisolated(unsafe) var saveError: Error?
Expand All @@ -31,16 +38,20 @@ enum PhotoLibrary {
XCTAssertNil(saveError, "Could not save the video: \(String(describing: saveError))", file: file, line: line)
}

private static func requestAddAuthorization() {
guard PHPhotoLibrary.authorizationStatus(for: .addOnly) != .authorized else { return }
let authorized = XCTestExpectation(description: "Photo library access is granted")
PHPhotoLibrary.requestAuthorization(for: .addOnly) { _ in authorized.fulfill() }
// CI grants access up front (`allow_photo_library_access` lane), local runs may get the system prompt.
private static func requestAddAuthorization() -> PHAuthorizationStatus {
let status = PHPhotoLibrary.authorizationStatus(for: .addOnly)
guard status == .notDetermined else { return status }

let answered = XCTestExpectation(description: "The photo library prompt is answered")
PHPhotoLibrary.requestAuthorization(for: .addOnly) { _ in answered.fulfill() }
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
let allowButton = springboard.buttons.matching(NSPredicate(format: "label BEGINSWITH 'Allow'")).firstMatch
if allowButton.waitForExistence(timeout: 5) {
if allowButton.waitForExistence(timeout: 15) {
allowButton.tap()
}
_ = XCTWaiter.wait(for: [authorized], timeout: 10)
_ = XCTWaiter.wait(for: [answered], timeout: 10)
return PHPhotoLibrary.authorizationStatus(for: .addOnly)
}

private static func writeVideo(to url: URL) throws {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ extension UserRobot {
@discardableResult
func openContextMenu(forMessageWithText text: String) -> Self {
let cell = MessageListPage.cell(withText: text).wait()
attributes.messageBubble(in: cell).waitForHitPoint().press(forDuration: 1)
_ = attributes.messageBubble(in: cell).waitForHitPoint()
longPressMessageBubble(in: cell)
return self
}

Expand Down
22 changes: 20 additions & 2 deletions StreamChatSwiftUITestsAppTests/Robots/UserRobot+Reactions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ extension UserRobot {
attributes.reactionButton(in: messageCell).wait().safeTap()
return self
}

@discardableResult
func closeReactionAuthors() -> Self {
let grabber = MessageListPage.Reactions.authorsSheetGrabber.wait()
grabber.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5))
.press(forDuration: 0.1, thenDragTo: app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.98)))
MessageListPage.Reactions.authors.firstMatch.waitForDisappearance()
return self
}
}

// MARK: Asserts
Expand All @@ -47,11 +56,14 @@ extension UserRobot {
@discardableResult
func assertReactionAuthor(
_ authorName: String,
isPresent: Bool = true,
file: StaticString = #filePath,
line: UInt = #line
) -> Self {
let author = MessageListPage.Reactions.author(authorName).wait()
XCTAssertTrue(author.exists, "Reaction author \(authorName) is not shown", file: file, line: line)
let author = MessageListPage.Reactions.author(authorName)
_ = isPresent ? author.wait() : author.waitForDisappearance()
let errMessage = isPresent ? "Reaction author \(authorName) is not shown" : "Reaction author \(authorName) is shown"
XCTAssertEqual(isPresent, author.exists, errMessage, file: file, line: line)
return self
}
}
Expand All @@ -65,6 +77,12 @@ extension MessageListPage.Reactions {
app.buttons.matching(NSPredicate(format: "label == %@", type.emoji)).firstMatch
}

static let currentUserAuthorName = "You"

static var authors: XCUIElementQuery { app.buttons.matching(identifier: "ReactionAuthorView") }

static var authorsSheetGrabber: XCUIElement { app.buttons["Sheet Grabber"].firstMatch }

static func author(_ name: String) -> XCUIElement {
app.buttons.matching(NSPredicate(format: "identifier == 'ReactionAuthorView' AND label CONTAINS %@", name)).firstMatch
}
Expand Down
20 changes: 18 additions & 2 deletions StreamChatSwiftUITestsAppTests/Robots/UserRobot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,27 @@ extension UserRobot {
@discardableResult
func openContextMenu(messageCellIndex: Int = 0) -> Self {
let messageCell = messageCell(withIndex: messageCellIndex)
let messageBubble = attributes.messageBubble(in: messageCell)
messageBubble.press(forDuration: 1)
longPressMessageBubble(in: messageCell)
return self
}

// Before iOS 18 the message's long press isn't high priority, so the top reactions take presses
// within ~15pt of them and open the reactions sheet instead of the message actions.
func longPressMessageBubble(in messageCell: XCUIElement) {
let bubble = attributes.messageBubble(in: messageCell)
let reactions = attributes.reactionButton(in: messageCell)
guard reactions.exists else {
bubble.press(forDuration: 1)
return
}

let frame = bubble.frame
let y = min(max(frame.midY, reactions.frame.maxY + 30), frame.maxY - 4)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
bubble.coordinate(withNormalizedOffset: .zero)
.withOffset(CGVector(dx: frame.width / 2, dy: y - frame.minY))
.press(forDuration: 1)
}

@discardableResult
func typeText(_ text: String, obtainKeyboardFocus: Bool = true) -> Self {
if obtainKeyboardFocus {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,16 @@ extension StreamTestCase {
add(attachment)
}

// Without a handler, XCTest dismisses an alert with "Don't Allow", and the denial sticks for later tests.
private func alertHandler() {
let title = "Push Notification Alert"
let title = "Permission Alert"
_ = addUIInterruptionMonitor(withDescription: title) { (alert: XCUIElement) -> Bool in
let allowButton = alert.buttons["Allow"]
if allowButton.exists {
allowButton.tap()
return true
for label in ["Allow", "Allow Full Access"] {
let allowButton = alert.buttons[label]
if allowButton.exists {
allowButton.tap()
return true
}
}
return false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ extension Reactions_Tests {
}
}

func test_reactionAuthorsSheetIsShown_whenUserTapsOnReaction() {
func test_reactionAuthorsSheetIsShown_whenUserTapsOnReaction() throws {
linkToScenario(withId: 12042)

let message = "test message"
Expand All @@ -42,16 +42,39 @@ extension Reactions_Tests {
participantRobot.sendMessage(message)
userRobot.assertMessage(message)
}
AND("user adds the reaction") {
userRobot
.addReaction(type: .wow)
.assertReaction(type: .wow, isPresent: true)
}
AND("participant adds the reaction") {
participantRobot.addReaction(type: .love)
userRobot.assertReaction(type: .love, isPresent: true)
}
WHEN("user taps on the message reaction") {
userRobot.tapOnMessageReactions()
}
THEN("the reaction authors sheet shows the participant") {
userRobot.assertReactionAuthor(participantRobot.name)
THEN("the reaction authors sheet shows the participant and the user") {
userRobot
.assertReactionAuthor(participantRobot.name)
.assertReactionAuthor(MessageListPage.Reactions.currentUserAuthorName)
}

// Skipped because of https://linear.app/stream/issue/IOS-2191
//
// WHEN("participant removes the reaction") {
// userRobot.closeReactionAuthors()
// participantRobot.deleteReaction(type: .love)
// userRobot.assertReaction(type: .love, isPresent: false)
// }
// AND("user taps on the message reaction") {
// userRobot.tapOnMessageReactions()
// }
// THEN("the reaction authors sheet shows only the user") {
// userRobot
// .assertReactionAuthor(MessageListPage.Reactions.currentUserAuthorName)
// .assertReactionAuthor(participantRobot.name, isPresent: false)
// }
}

func test_userAddsReactionWhileOffline() throws {
Expand Down
28 changes: 24 additions & 4 deletions StreamChatSwiftUITestsAppTests/Tests/Reactions_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,36 @@ final class Reactions_Tests: StreamTestCase {
WHEN("user sends the message: '\(message)'") {
userRobot.sendMessage(message)
}
AND("participant adds the reaction") {
participantRobot.addReaction(type: .love)
userRobot.assertReaction(type: .love, isPresent: true)
}
AND("user adds the reaction") {
userRobot
.addReaction(type: .wow)
.waitForNewReaction()
.assertReaction(type: .wow, isPresent: true)
}
AND("user removes the reaction") {
userRobot.deleteReaction(type: .wow)
AND("user opens the reaction authors") {
userRobot.tapOnMessageReactions()
}
THEN("the reaction authors show the user and the participant") {
userRobot
.assertReactionAuthor(MessageListPage.Reactions.currentUserAuthorName)
.assertReactionAuthor(participantRobot.name)
}
WHEN("user removes the reaction") {
userRobot
.closeReactionAuthors()
.deleteReaction(type: .wow)
}
THEN("the reaction is removed") {
userRobot.assertReaction(isPresent: false)
userRobot.assertReaction(type: .wow, isPresent: false)
}
AND("the reaction authors show only the participant") {
userRobot
.tapOnMessageReactions()
.assertReactionAuthor(participantRobot.name)
.assertReactionAuthor(MessageListPage.Reactions.currentUserAuthorName, isPresent: false)
}
}

Expand Down
23 changes: 23 additions & 0 deletions fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ lane :test_e2e_mock do |options|
next unless is_check_required(sources: @impact[:e2e][:sources], force_check: @force_check, required_checks: @impact[:e2e][:checks])

start_mock_server
allow_photo_library_access(device: options[:device])

scan_options = {
project: xcode_project,
Expand Down Expand Up @@ -415,6 +416,28 @@ private_lane :build_e2e_coverage_map do |options|
)
end

# The test runner saves a video to the photo library and the app's media picker reads it. Granting access up front
# avoids the system prompts, which can be dismissed as "Don't Allow" when they're not answered in time and then
# stay denied for every retry.
private_lane :allow_photo_library_access do |options|
next UI.important('No device is given, photo library access is not granted') unless options[:device]

name, os_version = options[:device].match(/^(.+?)(?: \(([\d.]+)\))?$/).captures
simulators = FastlaneCore::DeviceManager.simulators('iOS')
simulator =
if os_version
simulators.find { |device| device.name == name && Gem::Version.new(device.os_version) == Gem::Version.new(os_version) }
else
# Same pick as `scan` makes for a device without a version, so both target the same simulator.
Scan::DetectValues.highest_compatible_simulator(simulators, name)
end
UI.user_error!("No simulator matches #{options[:device]}, photo library access cannot be granted") unless simulator

sh("xcrun simctl bootstatus #{simulator.udid} -b")
sh("xcrun simctl privacy #{simulator.udid} grant photos-add io.getstream.iOS.StreamChatSwiftUITestsAppTests.xctrunner")
sh("xcrun simctl privacy #{simulator.udid} grant photos io.getstream.iOS.StreamChatSwiftUITestsApp")
end

private_lane :all_e2e_tests do
xctestrun = Dir.glob(File.expand_path("../#{derived_data_path}/Build/Products/*.xctestrun")).first
retrieve_xctest_names(xctestrun: xctestrun).values.flatten
Expand Down
Loading