Fix UI tests (#950)

* Fixed unstored weak reference warning

* Prevent the timeline from jumping whenever we access XCUIApplication properties

* Prevent UI tests from causing side effects on the timeline

* Update userSession reference screenshots

* Fix create room UI tests

* Update inviteUsers UI test reference screenshots

* Allow certain tests to enable timeline accessibility

* Fix roomLayout tests and update reference screenshots

* Disable the testTimelineLayoutInMiddle test because of flakiness

* Update screenshots following timestamp addition

* Another attempt

* Yet another attempt, replace XCUIScreen screenshots with XCUIApplication ones, decrease delay to 1 second

* Allow tests to retry up to 3 times before failing the run
This commit is contained in:
Stefan Ceriu 2023-05-26 17:43:38 +03:00 committed by GitHub
parent 142a8d6275
commit a1fdfd068a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
79 changed files with 241 additions and 245 deletions

View File

@ -43,4 +43,16 @@ public enum Tests {
nil
#endif
}
static var shouldDisableTimelineAccessibility: Bool {
guard isRunningUITests else {
return false
}
#if DEBUG
return ProcessInfo.processInfo.environment["UI_TESTS_DISABLE_TIMELINE_ACCESSIBILITY"] != nil
#else
return false
#endif
}
}

View File

@ -34,8 +34,8 @@ class TimelineItemCell: UITableViewCell {
/// This class subclasses `UIViewController` as `UITableViewController` adds some
/// extra keyboard handling magic that wasn't playing well with SwiftUI (as of iOS 16.1).
class TimelineTableViewController: UIViewController {
let coordinator: TimelineView.Coordinator
let tableView = UITableView(frame: .zero, style: .plain)
private let coordinator: TimelineView.Coordinator
private let tableView = UITableView(frame: .zero, style: .plain)
var timelineStyle: TimelineStyle
var timelineItems: [RoomTimelineViewProvider] = [] {
@ -87,8 +87,8 @@ class TimelineTableViewController: UIViewController {
private let paginateBackwardsPublisher = PassthroughSubject<Void, Never>()
/// Whether or not the ``timelineItems`` value should be applied when scrolling stops.
private var hasPendingUpdates = false
/// Yucky hack to fix some layouts where the scroll view doesn't make it to the bottom on keyboard appearance.
private var keyboardWillShowLayout: LayoutDescriptor?
/// We need to store the previous layout as computing it on the fly leads to problems.
private var previousLayout: LayoutDescriptor?
/// Whether or not the view has been shown on screen yet.
private var hasAppearedOnce = false
@ -109,6 +109,10 @@ class TimelineTableViewController: UIViewController {
tableView.backgroundColor = .element.background
view.addSubview(tableView)
// Prevents XCUITest from invoking the diffable dataSource's cellProvider
// for each possible cell, causing layout issues
tableView.accessibilityElementsHidden = Tests.shouldDisableTimelineAccessibility
scrollToBottomPublisher
.sink { [weak self] _ in
self?.scrollToBottom(animated: true)
@ -132,16 +136,9 @@ class TimelineTableViewController: UIViewController {
}
.store(in: &cancellables)
NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
.sink { [weak self] _ in
guard let self else { return }
self.keyboardWillShowLayout = self.layout()
}
.store(in: &cancellables)
NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)
.sink { [weak self] _ in
guard let self, let layout = self.keyboardWillShowLayout, layout.isBottomVisible else { return }
guard let self, let layout = self.previousLayout, layout.isBottomVisible else { return }
self.scrollToBottom(animated: false) // Force the bottom to be visible as some timelines misbehave.
}
.store(in: &cancellables)
@ -170,18 +167,14 @@ class TimelineTableViewController: UIViewController {
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
guard tableView.frame.size != view.frame.size else { return }
tableView.frame = CGRect(origin: .zero, size: view.frame.size)
if tableView.frame.size != view.frame.size {
tableView.frame = CGRect(origin: .zero, size: view.frame.size)
// Update the table's layout if necessary after the frame changed.
updateTopPadding()
}
// Update the table's layout if necessary after the frame changed.
updateTopPadding()
guard composerMode == .default else { return }
// The table view is yet to update its content so layout() returns a
// description of the timeline before the frame change occurs.
let previousLayout = layout()
if previousLayout.isBottomVisible {
if let previousLayout, previousLayout.isBottomVisible {
scrollToBottom(animated: false)
}
}
@ -235,6 +228,7 @@ class TimelineTableViewController: UIViewController {
guard let dataSource else { return }
let previousLayout = layout()
self.previousLayout = previousLayout
var snapshot = NSDiffableDataSourceSnapshot<TimelineSection, RoomTimelineViewProvider>()
snapshot.appendSections([.main])

View File

@ -18,12 +18,18 @@ import SnapshotTesting
import XCTest
struct Application {
static func launch(_ identifier: UITestsScreenIdentifier) -> XCUIApplication {
static func launch(_ identifier: UITestsScreenIdentifier, disableTimelineAccessibility: Bool = true) -> XCUIApplication {
let app = XCUIApplication()
app.launchEnvironment = [
var launchEnvironment = [
"UI_TESTS_SCREEN": identifier.rawValue
]
if disableTimelineAccessibility {
launchEnvironment["UI_TESTS_DISABLE_TIMELINE_ACCESSIBILITY"] = "1"
}
app.launchEnvironment = launchEnvironment
app.launch()
return app
}
@ -35,17 +41,16 @@ extension XCUIApplication {
/// - Parameter identifier: Identifier of the UI test screen
/// - Parameter step: An optional integer that can be used to take multiple snapshots per test identifier.
/// - Parameter insets: Optional insets with which to crop the image by.
func assertScreenshot(_ identifier: UITestsScreenIdentifier, step: Int? = nil, insets: UIEdgeInsets? = nil, delayInMilliseconds: UInt = 2000, precision: Float = 0.99) async throws {
func assertScreenshot(_ identifier: UITestsScreenIdentifier, step: Int? = nil, insets: UIEdgeInsets? = nil, delay: Duration = .seconds(1), precision: Float = 0.99) async throws {
var snapshotName = identifier.rawValue
if let step {
snapshotName += "-\(step)"
}
// Sometimes the CI might be too slow to load the content so let's wait the delay time
if delayInMilliseconds > 0 {
try await Task.sleep(for: .milliseconds(delayInMilliseconds))
}
var snapshot = XCUIScreen.main.screenshot().image
try await Task.sleep(for: delay)
var snapshot = screenshot().image
if let insets {
snapshot = snapshot.inset(by: insets)

View File

@ -21,26 +21,30 @@ import XCTest
class CreateRoomScreenUITests: XCTestCase {
func testLanding() async throws {
let app = Application.launch(.createRoom)
try await app.assertScreenshot(.createRoom, step: 0)
try await app.assertScreenshot(.createRoom)
}
func testLandingWithoutUsers() async throws {
let app = Application.launch(.createRoomNoUsers)
try await app.assertScreenshot(.createRoom, step: 1)
try await app.assertScreenshot(.createRoomNoUsers)
}
func testLongInputNameText() async throws {
let app = Application.launch(.createRoom)
// typeText sometimes misses letters but it's faster than typing one letter at a time
// repeat the same letter enough times to avoid that but also to work on iPads
app.textFields[A11yIdentifiers.createRoomScreen.roomName].tap()
app.textFields[A11yIdentifiers.createRoomScreen.roomName].typeText("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
try await app.assertScreenshot(.createRoom, step: 2)
app.textFields[A11yIdentifiers.createRoomScreen.roomName].typeText(.init(repeating: "x", count: 200))
try await app.assertScreenshot(.createRoom, step: 1)
}
func testLongInputTopicText() async throws {
// Disabled because tapping on the textView doesn't work
func disabled_testLongInputTopicText() async throws {
let app = Application.launch(.createRoom)
let roomTopic = "Room topic\nvery\nvery\nvery long"
app.textViews[A11yIdentifiers.createRoomScreen.roomTopic].tap()
app.textViews[A11yIdentifiers.createRoomScreen.roomTopic].typeText(roomTopic)
try await app.assertScreenshot(.createRoom, step: 3)
let textView = app.textViews[A11yIdentifiers.createRoomScreen.roomTopic]
textView.tap()
textView.typeText(.init(repeating: "Topic\n", count: 3))
try await app.assertScreenshot(.createRoom, step: 2)
}
}

View File

@ -75,39 +75,39 @@ class RoomScreenUITests: XCTestCase {
try await app.assertScreenshot(.roomSmallTimelineLargePagination)
}
// This test is very flaky on the CI disabling it for now
// func testTimelineLayoutInMiddle() async throws {
// let client = try UITestsSignalling.Client(mode: .tests)
//
// let app = Application.launch(.roomLayoutMiddle)
//
// await client.waitForApp()
// defer { try? client.stop() }
//
// try await Task.sleep(for: .seconds(10)) // Allow the table to settle
// // Given a timeline that is neither at the top nor the bottom.
// app.tables.element.swipeDown(velocity: .slow)
// try await Task.sleep(for: .seconds(10)) // Allow the table to settle
// try await app.assertScreenshot(.roomLayoutMiddle, step: 0) // Assert initial state for comparison.
//
// // When a back pagination occurs.
// try await performOperation(.paginate, using: client)
//
// // Then the UI should remain unchanged.
// try await app.assertScreenshot(.roomLayoutMiddle, step: 0)
//
// // When an incoming message arrives
// try await performOperation(.incomingMessage, using: client)
//
// // Then the UI should still remain unchanged.
// try await app.assertScreenshot(.roomLayoutMiddle, step: 0)
//
// // When the keyboard appears for the message composer.
// try await tapMessageComposer(in: app)
//
// // Then the timeline scroll offset should remain unchanged.
// try await app.assertScreenshot(.roomLayoutMiddle, step: 1)
// }
// This test is DISABLED because it's flakey on the CI
func disabled_testTimelineLayoutInMiddle() async throws {
let client = try UITestsSignalling.Client(mode: .tests)
let app = Application.launch(.roomLayoutMiddle)
await client.waitForApp()
defer { try? client.stop() }
try await Task.sleep(for: .seconds(1)) // Allow the table to settle
// Given a timeline that is neither at the top nor the bottom.
app.swipeDown()
try await Task.sleep(for: .seconds(1)) // Allow the table to settle
try await app.assertScreenshot(.roomLayoutMiddle, step: 0) // Assert initial state for comparison.
// When a back pagination occurs.
try await performOperation(.paginate, using: client)
// Then the UI should remain unchanged.
try await app.assertScreenshot(.roomLayoutMiddle, step: 0)
// When an incoming message arrives
try await performOperation(.incomingMessage, using: client)
// Then the UI should still remain unchanged.
try await app.assertScreenshot(.roomLayoutMiddle, step: 0)
// When the keyboard appears for the message composer.
try await tapMessageComposer(in: app)
// Then the timeline scroll offset should remain unchanged.
try await app.assertScreenshot(.roomLayoutMiddle, step: 1)
}
func testTimelineLayoutAtTop() async throws {
let client = try UITestsSignalling.Client(mode: .tests)
@ -119,42 +119,40 @@ class RoomScreenUITests: XCTestCase {
// Given a timeline that is scrolled to the top.
for _ in 0...5 {
app.tables.element.swipeDown()
app.swipeDown()
}
let cropped = UIEdgeInsets(top: 150, left: 0, bottom: 0, right: 0) // Ignore the navigation bar and pagination indicator as these change.
try await app.assertScreenshot(.roomLayoutTop, insets: cropped) // Assert initial state for comparison.
try await app.assertScreenshot(.roomLayoutTop) // Assert initial state for comparison.
// When a back pagination occurs.
try await performOperation(.paginate, using: client)
// Then the bottom of the timeline should remain unchanged (with new items having been added above).
try await app.assertScreenshot(.roomLayoutTop, insets: cropped)
try await app.assertScreenshot(.roomLayoutTop)
}
// This test is very flaky on the CI disabling it for now
// func testTimelineLayoutAtBottom() async throws {
// let client = try UITestsSignalling.Client(mode: .tests)
//
// let app = Application.launch(.roomLayoutBottom)
//
// await client.waitForApp()
// defer { try? client.stop() }
//
// // Some time for the timeline to settle
// try await Task.sleep(for: .seconds(10))
// // When an incoming message arrives.
// try await performOperation(.incomingMessage, using: client)
// // Some time for the timeline to settle
// try await Task.sleep(for: .seconds(10))
//
// // Then the timeline should scroll down to reveal the message.
// try await app.assertScreenshot(.roomLayoutBottom, step: 0)
//
// // When the keyboard appears for the message composer.
// try await tapMessageComposer(in: app)
//
// try await app.assertScreenshot(.roomLayoutBottom, step: 1)
// }
func testTimelineLayoutAtBottom() async throws {
let client = try UITestsSignalling.Client(mode: .tests)
let app = Application.launch(.roomLayoutBottom)
await client.waitForApp()
defer { try? client.stop() }
// Some time for the timeline to settle
try await Task.sleep(for: .seconds(1))
// When an incoming message arrives.
try await performOperation(.incomingMessage, using: client)
// Some time for the timeline to settle
try await Task.sleep(for: .seconds(1))
// Then the timeline should scroll down to reveal the message.
try await app.assertScreenshot(.roomLayoutBottom, step: 0)
// When the keyboard appears for the message composer.
try await tapMessageComposer(in: app)
try await app.assertScreenshot(.roomLayoutBottom, step: 1)
}
// MARK: - Helper Methods

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -127,7 +127,8 @@ lane :ui_tests do |options|
ensure_devices_found: true,
prelaunch_simulator: true,
result_bundle: true,
only_testing: test_to_run
only_testing: test_to_run,
number_of_retries: 3,
)
slather(