-
-
Notifications
You must be signed in to change notification settings - Fork 57
Refactor all Tests from XCTest to Swift Testing #208
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
MinerMinerMods
wants to merge
12
commits into
stackotter:main
Choose a base branch
from
MinerMinerMods:main
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
95ea1f3
Refactored Tests to use Swift Testing
MinerMinerMods 41b335b
Fixed Issues with Structuring for test `CodableNavigationPath()`. Add…
MinerMinerMods ed174ab
Corrections and Clarity Improvements (#1)
MinerMinerMods 56c7958
Improved extensibility of Test `CodableNavigationPath`
MinerMinerMods efe8ae5
Performed swift-format in correction
MinerMinerMods 97111c9
Added Bug Refrence for `ThrottledStateObservation` for issue 167
MinerMinerMods 26adf38
Corrected Mal-Attribution of `ThrottledStateObservation`
MinerMinerMods a57ce91
Completed Changes before final organization
MinerMinerMods d8712d2
File Organization Changes
MinerMinerMods 40d23e4
Merge branch 'stackotter:main' into main
MinerMinerMods 4fc2169
Condense typeValue initalization
MinerMinerMods 33d3e3e
Update SwiftCrossUIBackendTests.swift
MinerMinerMods File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -72,6 +72,6 @@ let package = Package( | |
.executableTarget( | ||
name: "WebViewExample", | ||
dependencies: exampleDependencies | ||
) | ||
), | ||
] | ||
) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
import Foundation | ||
import Testing | ||
|
||
@testable import SwiftCrossUI | ||
|
||
#if canImport(AppKitBackend) | ||
@testable import AppKitBackend | ||
typealias ActiveBackend = AppKitBackend | ||
#else | ||
// Consider this the mock backend for code that is not currently specialized to one backend explicitly | ||
@testable import DefaultBackend | ||
typealias ActiveBackend = DefaultBackend | ||
#endif | ||
|
||
// TODO: Create mock backend so that this can be tested on all platforms. There's | ||
// nothing AppKit-specific about it. | ||
@Suite( | ||
"Testing for Graphical Backends", | ||
.tags(.interface, .backend) | ||
) | ||
struct BackendTests { | ||
public struct BackendTestError: Error { | ||
var message: String? = nil | ||
|
||
// These are preconfigured instances for functions with `throws(BackendTests.BackendTestError)` in the signature | ||
static let failedBitmapBack: Self = new("Failed to create bitmap backing") | ||
static let failedTiffRep: Self = new("Failed to create tiff representation") | ||
static let unknownIssue: Self = new() | ||
|
||
var errorDescription: String? { | ||
message ?? "Something when wrong during Backend Testing" | ||
} | ||
|
||
// Condensed Error Generator for abridging the initalizer | ||
static func new(_ info: String? = nil) -> Self { | ||
Self(message: info) | ||
} | ||
} | ||
|
||
struct CounterView: View { | ||
@State var count = 0 | ||
|
||
var body: some View { | ||
VStack { | ||
Button("Decrease") { count -= 1 } | ||
Text("Count: 1") | ||
Button("Increase") { count += 1 } | ||
}.padding() | ||
} | ||
} | ||
|
||
@Test( | ||
"Ensures that `Publisher.observeAsUIUpdater(backend:)` throttles state update observations", | ||
.tags(.observation, .state) | ||
) | ||
func throttledStateObservation() async { | ||
class MyState: SwiftCrossUI.ObservableObject { | ||
@SwiftCrossUI.Published | ||
var count = 0 | ||
} | ||
|
||
/// A thread-safe count. | ||
actor Count { | ||
var count: Int = 0 | ||
|
||
/// Allow for the contents of Count to be manipulated with an async function | ||
func update(_ action: (Int) -> Int) async { | ||
count = action(count) | ||
} | ||
|
||
/// Allow for the contents of Count to be aquired with an async function | ||
func get() async -> Int { | ||
count | ||
} | ||
} | ||
|
||
// Number of mutations to perform | ||
let mutationCount = 20 | ||
// Length of each fake state update | ||
let updateDuration = 0.02 | ||
// Delay between observation-causing state mutations | ||
let mutationGap = 0.01 | ||
|
||
let state = MyState() | ||
let updateCount = Count() | ||
|
||
let backend = await ActiveBackend() | ||
let cancellable = state.didChange.observeAsUIUpdater(backend: backend) { | ||
Task { | ||
await updateCount.update { $0 + 1 } | ||
} | ||
// Simulate an update of duration `updateDuration` seconds | ||
Thread.sleep(forTimeInterval: updateDuration) | ||
} | ||
_ = cancellable // Silence warning about cancellable being unused | ||
|
||
let start = ProcessInfo.processInfo.systemUptime | ||
for _ in 0..<mutationCount { | ||
state.count += 1 | ||
try? await Task.sleep(for: .seconds(mutationGap)) | ||
} | ||
let end = ProcessInfo.processInfo.systemUptime | ||
let elapsed = end - start | ||
|
||
let count = await updateCount.get() | ||
|
||
// Compute percentage of main thread's time taken up by updates. | ||
let ratio = Double(count) * updateDuration / elapsed | ||
#expect( | ||
ratio <= 0.85, | ||
""" | ||
Expected throttled updates to take under 85% of the main \ | ||
thread's time. Took \(Int(ratio * 100))% | ||
|
||
Duration: \(elapsed) seconds | ||
Updates: \(count)/\(mutationCount) | ||
""" | ||
) | ||
} | ||
|
||
#if canImport(AppKitBackend) | ||
|
||
@Test( | ||
"A Basic Layout Works properly", | ||
tags(.layout) | ||
) | ||
@MainActor | ||
func basicLayout() async throws { | ||
let backend = ActiveBackend() | ||
let window = backend.createWindow(withDefaultSize: SIMD2(200, 200)) | ||
|
||
// Idea taken from https://github.com/pointfreeco/swift-snapshot-testing/pull/533 | ||
// and implemented in AppKitBackend. | ||
window.backingScaleFactorOverride = 1 | ||
window.colorSpace = .genericRGB | ||
|
||
let environment = EnvironmentValues(backend: backend) | ||
.with(\.window, window) | ||
let viewGraph = ViewGraph( | ||
for: CounterView(), | ||
backend: backend, | ||
environment: environment | ||
) | ||
backend.setChild(ofWindow: window, to: viewGraph.rootNode.widget.into()) | ||
|
||
let result = viewGraph.update( | ||
proposedSize: SIMD2(200, 200), | ||
environment: environment, | ||
dryRun: false | ||
) | ||
let view: ActiveBackend.Widget = viewGraph.rootNode.widget.into() | ||
backend.setSize(of: view, to: result.size.size) | ||
backend.setSize(ofWindow: window, to: result.size.size) | ||
|
||
#expect( | ||
result.size == ViewSize(fixedSize: SIMD2(92, 96)), | ||
"View update result mismatch" | ||
) | ||
|
||
#expect( | ||
result.preferences.onOpenURL == nil, | ||
"`onOpenURL` should be `nil` as it was not set" | ||
) | ||
} | ||
|
||
/// Helper function to be used in future tests | ||
@MainActor | ||
static func snapshotView(_ view: NSView) throws(BackendTestError) -> Data { | ||
view.wantsLayer = true | ||
view.layer?.backgroundColor = CGColor.white | ||
|
||
guard let bitmap = view.bitmapImageRepForCachingDisplay(in: view.bounds) else { | ||
throw .failedBitmapBack | ||
} | ||
|
||
view.cacheDisplay(in: view.bounds, to: bitmap) | ||
|
||
guard let data = bitmap.tiffRepresentation else { | ||
throw .failedTiffRep | ||
} | ||
|
||
return data | ||
} | ||
#endif | ||
} |
80 changes: 80 additions & 0 deletions
80
Tests/SwiftCrossUITests/SwiftCrossUIInterfaceTests/SwiftCrossUINavPathTests.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
/// Required Imports | ||
import Foundation | ||
import Testing | ||
|
||
@testable import SwiftCrossUI | ||
|
||
@Suite( | ||
"Navigation Path Tests", | ||
.tags(.interface, .navPath) | ||
) | ||
struct NavPathTests { | ||
// NOTE: The Test ``CodableNavigationPath`` cannot be condensed into a parameterized test | ||
// or a for loop without specifiying allowed Types individually. A Macro might be useful here | ||
/// Makes sure that ``NavigationPath`` can retain data validating them with `compareComponents`. | ||
@Test( | ||
"Ensures that `NavigationPath` instances can be round tripped to JSON and back" | ||
) | ||
func codableNavigationPath() throws { | ||
/// Specifies the values and types input into the `NavigationPath` | ||
let typeValuePairs: [(type: any Codable.Type, value: any Codable)] = | ||
[ | ||
(String.self, "a"), | ||
(Int.self, 1), | ||
([Int].self, [1, 2, 3]), | ||
(Double.self, 5.0), | ||
] as! [(any Codable.Type, any Codable)] | ||
|
||
let Values: [any Codable] = typeValuePairs.map { $0.value } | ||
|
||
let Types: [any Codable.Type] = typeValuePairs.map { $0.type } | ||
|
||
var path = NavigationPath() | ||
for value in Values { | ||
path.append(value) | ||
} | ||
|
||
let components = path.path(destinationTypes: Types) | ||
|
||
let encoded = try JSONEncoder().encode(path) | ||
let decodedPath = try JSONDecoder().decode(NavigationPath.self, from: encoded) | ||
|
||
let decodedComponents = decodedPath.path(destinationTypes: Types) | ||
|
||
try #require( | ||
decodedComponents.count == components.count, | ||
"`decodedComponents` and `components` are inconsitently sized" | ||
) | ||
|
||
#expect( | ||
Self.compareComponents(ofType: String.self, components[0], decodedComponents[0]), | ||
"An Issue with Navigation path data retainment occured" | ||
) | ||
#expect( | ||
Self.compareComponents(ofType: Int.self, components[1], decodedComponents[1]), | ||
"An Issue with Navigation path data retainment occured" | ||
) | ||
#expect( | ||
Self.compareComponents(ofType: [Int].self, components[2], decodedComponents[2]), | ||
"An Issue with Navigation path data retainment occured" | ||
) | ||
#expect( | ||
Self.compareComponents(ofType: Double.self, components[3], decodedComponents[3]), | ||
"An Issue with Navigation path data retainment occured" | ||
) | ||
} | ||
|
||
// Note: consider making compareComponents not require a type input | ||
static func compareComponents<T>( | ||
ofType type: T.Type, _ original: Any, _ decoded: Any | ||
) -> Bool where T: Equatable { | ||
guard | ||
let original = original as? T, | ||
let decoded = decoded as? T | ||
else { | ||
return false | ||
} | ||
|
||
return original == decoded | ||
} | ||
} |
76 changes: 76 additions & 0 deletions
76
Tests/SwiftCrossUITests/SwiftCrossUIInterfaceTests/SwiftCrossUIStateTests.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
/// Required Imports | ||
import Foundation | ||
import Testing | ||
|
||
@testable import SwiftCrossUI | ||
|
||
@Suite( | ||
"State Tests", | ||
.tags(.state) | ||
) | ||
struct StateTests { | ||
@Test( | ||
"Validates ObservableObject observation behaviour", | ||
.tags(.observation) | ||
) | ||
func observableObjectObservation() { | ||
class NestedState: SwiftCrossUI.ObservableObject { | ||
@SwiftCrossUI.Published | ||
var count = 0 | ||
} | ||
|
||
class MyState: SwiftCrossUI.ObservableObject { | ||
@SwiftCrossUI.Published | ||
var count = 0 | ||
@SwiftCrossUI.Published | ||
var publishedNestedState = NestedState() | ||
var unpublishedNestedState = NestedState() | ||
} | ||
|
||
let state = MyState() | ||
var observedChange = false | ||
let cancellable = state.didChange.observe { | ||
observedChange = true | ||
} | ||
|
||
// Ensures that published value type mutation triggers observation | ||
observedChange = false | ||
state.count += 1 | ||
#expect(observedChange, "Expected value type mutation to trigger observation") | ||
|
||
// Ensure that published nested ObservableObject triggers observation | ||
observedChange = false | ||
state.publishedNestedState.count += 1 | ||
#expect( | ||
observedChange, | ||
"Expected nested published observable object mutation to trigger observation") | ||
|
||
// Ensure that replacing published nested ObservableObject triggers observation | ||
observedChange = false | ||
state.publishedNestedState = NestedState() | ||
#expect( | ||
observedChange, | ||
"Expected replacing nested published observable object to trigger observation") | ||
|
||
// Ensure that replaced published nested ObservableObject triggers observation | ||
observedChange = false | ||
state.publishedNestedState.count += 1 | ||
#expect( | ||
observedChange, | ||
"Expected replaced nested published observable object mutation to trigger observation" | ||
) | ||
|
||
// Ensure that non-published nested ObservableObject doesn't trigger observation | ||
observedChange = false | ||
state.unpublishedNestedState.count += 1 | ||
#expect( | ||
!observedChange, | ||
"Expected nested unpublished observable object mutation to not trigger observation") | ||
|
||
// Ensure that cancelling the observation prevents future observations | ||
cancellable.cancel() | ||
observedChange = false | ||
state.count += 1 | ||
#expect(!observedChange, "Expected mutation not to trigger cancelled observation") | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did consider using a loop but it would have been excessive and overkill as there is only one instance of each type