mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 21:39:12 +00:00

* Update snapshots for Xcode 15.4 * Update CI. * Fix crashing UI test. * Fix compile error in Integration Tests. * Try putting UI tests on GH runners again.
285 lines
11 KiB
Plaintext
285 lines
11 KiB
Plaintext
// swiftlint:disable all
|
|
// swiftformat:disable all
|
|
|
|
import XCTest
|
|
import SwiftUI
|
|
import Prefire
|
|
@testable import SnapshotTesting
|
|
#if canImport(AccessibilitySnapshot)
|
|
import AccessibilitySnapshot
|
|
#endif
|
|
{% if argument.mainTarget %}
|
|
@testable import {{ argument.mainTarget }}
|
|
{% endif %}
|
|
{% for import in argument.imports %}
|
|
{% if import != "last" %}
|
|
import {{ import }}
|
|
{% endif %}
|
|
{% endfor %}
|
|
{% for import in argument.testableImports %}
|
|
{% if import != "last" %}
|
|
@testable import {{ import }}
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
class PreviewTests: XCTestCase {
|
|
private let deviceConfig: ViewImageConfig = .iPhoneX
|
|
private var simulatorDevice: String?{% if argument.simulatorDevice %} = "{{ argument.simulatorDevice|default:nil }}"{% endif %}
|
|
private var requiredOSVersion: Int?{% if argument.simulatorOSVersion %} = {{ argument.simulatorOSVersion }}{% endif %}
|
|
private let snapshotDevices: [String]{% if argument.snapshotDevices %} = {{ argument.snapshotDevices|split:"|" }}{% else %} = []{% endif %}
|
|
|
|
|
|
{% if argument.file %}
|
|
|
|
private var file: StaticString { .init(stringLiteral: "{{ argument.file }}") }
|
|
{% endif %}
|
|
|
|
override func setUp() {
|
|
super.setUp()
|
|
|
|
checkEnvironments()
|
|
UIView.setAnimationsEnabled(false)
|
|
}
|
|
|
|
// MARK: - PreviewProvider
|
|
|
|
{% for type in types.types where (type.implements.PrefireProvider or type.based.PrefireProvider or type|annotated:"PrefireProvider") and type.name != "TestablePreview" %}
|
|
func test_{{ type.name|lowerFirstLetter|replace:"_Previews", "" }}() {
|
|
for preview in {{ type.name }}._allPreviews {
|
|
assertSnapshots(matching: preview)
|
|
}
|
|
}
|
|
{%- if not forloop.last %}
|
|
|
|
{% endif %}
|
|
{% endfor %}
|
|
{% if argument.previewsMacros %}
|
|
// MARK: - Macros
|
|
|
|
{{ argument.previewsMacros }}
|
|
{% endif %}
|
|
// MARK: Private
|
|
|
|
private func assertSnapshots(matching preview: _Preview, testName: String = #function) {
|
|
guard !snapshotDevices.isEmpty else {
|
|
if let failure = assertSnapshots(matching: AnyView(preview.content),
|
|
name: preview.displayName,
|
|
isScreen: preview.layout == .device,
|
|
device: preview.device?.snapshotDevice() ?? deviceConfig,
|
|
testName: testName) {
|
|
XCTFail(failure)
|
|
}
|
|
return
|
|
}
|
|
|
|
for deviceName in snapshotDevices {
|
|
guard var device = PreviewDevice(rawValue: deviceName).snapshotDevice() else {
|
|
fatalError("Unknown device name: \(deviceName)")
|
|
}
|
|
|
|
// Ignore specific device safe area (using the workaround value to fix rendering issues).
|
|
device.safeArea = .one
|
|
|
|
// Ignore specific device display scale
|
|
let traits = UITraitCollection(displayScale: 2.0)
|
|
|
|
if let failure = assertSnapshots(matching: AnyView(preview.content),
|
|
name: preview.displayName,
|
|
isScreen: preview.layout == .device,
|
|
device: device,
|
|
testName: testName + deviceName + "-" + localeCode,
|
|
traits: traits) {
|
|
XCTFail(failure)
|
|
}
|
|
}
|
|
}
|
|
|
|
private var localeCode: String {
|
|
if UserDefaults.standard.bool(forKey: "NSDoubleLocalizedStrings") {
|
|
return "pseudo"
|
|
}
|
|
return languageCode + "-" + regionCode
|
|
}
|
|
|
|
private var languageCode: String {
|
|
Locale.current.language.languageCode?.identifier ?? ""
|
|
}
|
|
|
|
private var regionCode: String {
|
|
Locale.current.language.region?.identifier ?? ""
|
|
}
|
|
|
|
private func assertSnapshots(matching view: AnyView,
|
|
name: String?, isScreen: Bool,
|
|
device: ViewImageConfig,
|
|
testName: String = #function,
|
|
traits: UITraitCollection = .init()) -> String? {
|
|
var delay: TimeInterval = 0
|
|
var precision: Float = 1
|
|
var perceptualPrecision: Float = 1
|
|
|
|
let view = view
|
|
.onPreferenceChange(DelayPreferenceKey.self) { delay = $0 }
|
|
.onPreferenceChange(PrecisionPreferenceKey.self) { precision = $0 }
|
|
.onPreferenceChange(PerceptualPrecisionPreferenceKey.self) { perceptualPrecision = $0 }
|
|
|
|
let matchingView = isScreen ? AnyView(view) : AnyView(view
|
|
.frame(width: device.size?.width)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
)
|
|
|
|
let failure = verifySnapshot(
|
|
of: matchingView,
|
|
as: .prefireImage(precision: { precision },
|
|
perceptualPrecision: { perceptualPrecision },
|
|
duration: { delay },
|
|
layout: isScreen ? .device(config: device) : .sizeThatFits,
|
|
traits: traits),
|
|
named: name{% if argument.file %},
|
|
file: file{% endif %},
|
|
testName: testName
|
|
)
|
|
|
|
#if canImport(AccessibilitySnapshot)
|
|
let vc = UIHostingController(rootView: matchingView)
|
|
vc.view.frame = UIScreen.main.bounds
|
|
assertSnapshot(
|
|
matching: vc,
|
|
as: .wait(for: delay, on: .accessibilityImage(showActivationPoints: .always)),
|
|
named: name.flatMap { $0 + ".accessibility" }{% if argument.file %},
|
|
file: file{% endif %},
|
|
testName: testName
|
|
)
|
|
#endif
|
|
return failure
|
|
}
|
|
|
|
/// Check environments to avoid problems with snapshots on different devices or OS.
|
|
private func checkEnvironments() {
|
|
if let simulatorDevice {
|
|
let deviceModel = ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"]
|
|
guard deviceModel?.contains(simulatorDevice) ?? false else {
|
|
fatalError("\(deviceModel ?? "Unknown") is the wrong one. Switch to using \(simulatorDevice) for these tests.")
|
|
}
|
|
}
|
|
|
|
if let requiredOSVersion {
|
|
let osVersion = ProcessInfo().operatingSystemVersion
|
|
guard osVersion.majorVersion == requiredOSVersion else {
|
|
fatalError("Switch to iOS \(requiredOSVersion) for these tests.")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - SnapshotTesting + Extensions
|
|
|
|
private extension PreviewDevice {
|
|
func snapshotDevice() -> ViewImageConfig? {
|
|
switch rawValue {
|
|
case "iPhone 15", "iPhone 14", "iPhone 13", "iPhone 12", "iPhone 11", "iPhone 10":
|
|
return .iPhoneX
|
|
case "iPhone 6", "iPhone 6s", "iPhone 7", "iPhone 8":
|
|
return .iPhone8
|
|
case "iPhone 6 Plus", "iPhone 6s Plus", "iPhone 8 Plus":
|
|
return .iPhone8Plus
|
|
case "iPhone SE (1st generation)", "iPhone SE (2nd generation)":
|
|
return .iPhoneSe
|
|
case "iPad":
|
|
return .iPad10_2
|
|
case "iPad Mini":
|
|
return .iPadMini
|
|
case "iPad Pro 11":
|
|
return .iPadPro11
|
|
case "iPad Pro 12.9":
|
|
return .iPadPro12_9
|
|
default: return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension Snapshotting where Value: SwiftUI.View, Format == UIImage {
|
|
static func prefireImage(
|
|
drawHierarchyInKeyWindow: Bool = false,
|
|
precision: @escaping () -> Float,
|
|
perceptualPrecision: @escaping () -> Float,
|
|
duration: @escaping () -> TimeInterval,
|
|
layout: SwiftUISnapshotLayout = .sizeThatFits,
|
|
traits: UITraitCollection = .init()
|
|
) -> Snapshotting {
|
|
let config: ViewImageConfig
|
|
|
|
switch layout {
|
|
#if os(iOS) || os(tvOS)
|
|
case let .device(config: deviceConfig):
|
|
config = deviceConfig
|
|
#endif
|
|
case .sizeThatFits:
|
|
// Make sure to use the workaround safe area insets.
|
|
config = .init(safeArea: .one, size: nil, traits: traits)
|
|
case let .fixed(width: width, height: height):
|
|
let size = CGSize(width: width, height: height)
|
|
// Make sure to use the workaround safe area insets.
|
|
config = .init(safeArea: .one, size: size, traits: traits)
|
|
}
|
|
|
|
return SimplySnapshotting<UIImage>(pathExtension: "png", diffing: .prefireImage(precision: precision, perceptualPrecision: perceptualPrecision, scale: traits.displayScale))
|
|
.asyncPullback { view in
|
|
var config = config
|
|
|
|
let controller: UIViewController
|
|
|
|
if config.size != nil {
|
|
controller = UIHostingController(rootView: view)
|
|
} else {
|
|
let hostingController = UIHostingController(rootView: view)
|
|
|
|
let maxSize = CGSize.zero
|
|
config.size = hostingController.sizeThatFits(in: maxSize)
|
|
|
|
controller = hostingController
|
|
}
|
|
|
|
return Async<UIImage> { callback in
|
|
let strategy = snapshotView(
|
|
config: config,
|
|
drawHierarchyInKeyWindow: drawHierarchyInKeyWindow,
|
|
traits: traits,
|
|
view: controller.view,
|
|
viewController: controller
|
|
)
|
|
|
|
let duration = duration()
|
|
if duration != .zero {
|
|
let expectation = XCTestExpectation(description: "Wait")
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
|
|
expectation.fulfill()
|
|
}
|
|
_ = XCTWaiter.wait(for: [expectation], timeout: duration + 1)
|
|
}
|
|
strategy.run(callback)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension Diffing where Value == UIImage {
|
|
static func prefireImage(precision: @escaping () -> Float, perceptualPrecision: @escaping () -> Float, scale: CGFloat?) -> Diffing {
|
|
lazy var originalDiffing = Diffing.image(precision: precision(), perceptualPrecision: 0.98, scale: scale)
|
|
return Diffing(
|
|
toData: { originalDiffing.toData($0) },
|
|
fromData: { originalDiffing.fromData($0) },
|
|
diff: { originalDiffing.diff($0, $1) }
|
|
)
|
|
}
|
|
}
|
|
|
|
private extension UIEdgeInsets {
|
|
/// A custom inset that prevents the snapshotting library from rendering the
|
|
/// origin at (10000, 10000) which breaks some of our views such as MessageText.
|
|
static var one: UIEdgeInsets { UIEdgeInsets(top: 1, left: 1, bottom: 1, right: 1) }
|
|
}
|
|
|
|
// swiftlint:enable all
|
|
// swiftformat:enable all
|