Skip to content

Commit

Permalink
Merge branch 'main' into 1.16.0
Browse files Browse the repository at this point in the history
* main:
  Make ViewImageConfig Sendable (pointfreeco#850)
  Added mention of, and link to plugin SnapshotVision. (pointfreeco#848)
  Run swift-format
  Register test observer in main queue (pointfreeco#834)
  Ability to remove inline snapshots (pointfreeco#844)
  Bump swift-syntax to 5.10.0 (pointfreeco#836)
  Fix indentation parsing (pointfreeco#830)
  Fixing wkWebView.takeSnapshot with Xcode 14 & 15 (pointfreeco#692)
  Run swift-format
  Non-Metal based perceptual image comparison (pointfreeco#666)
  • Loading branch information
JustasL committed May 28, 2024
2 parents b567d07 + 837238a commit 45d14a4
Show file tree
Hide file tree
Showing 9 changed files with 375 additions and 239 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ jobs:
strategy:
matrix:
swift:
- "5.8"
- "5.9.1"

name: Windows (Swift ${{ matrix.swift }})
runs-on: windows-2019
runs-on: windows-latest

steps:
- uses: compnerd/gha-setup-swift@main
Expand All @@ -59,6 +59,6 @@ jobs:
git config --global core.autocrlf false
git config --global core.eol lf
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- run: swift build
- run: swift test
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-syntax.git",
"state" : {
"revision" : "74203046135342e4a4a627476dd6caf8b28fe11b",
"version" : "509.0.0"
"revision" : "08a2f0a9a30e0f705f79c9cfaca1f68b71bdc775",
"version" : "510.0.0"
}
}
],
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ let package = Package(
),
],
dependencies: [
.package(url: "https://github.com/apple/swift-syntax.git", "508.0.1"..<"510.0.0")
.package(url: "https://github.com/apple/swift-syntax", "508.0.1"..<"511.0.0")
],
targets: [
.target(
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,9 @@ targets: [
- [SnapshotTestingHEIC](https://github.com/alexey1312/SnapshotTestingHEIC) adds image support
using the HEIC storage format which reduces file sizes in comparison to PNG.

- [SnapshotVision](https://github.com/gregersson/swift-snapshot-testing-vision) adds snapshot
strategy for text recognition on views and images. Uses Apples Vision framework.

Have you written your own SnapshotTesting plug-in?
[Add it here](https://github.com/pointfreeco/swift-snapshot-testing/edit/master/README.md) and
submit a pull request!
Expand Down
175 changes: 101 additions & 74 deletions Sources/InlineSnapshotTesting/AssertInlineSnapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import Foundation
/// - column: The column where the assertion occurs. The default is the line column you call
/// this function.
public func assertInlineSnapshot<Value>(
of value: @autoclosure () throws -> Value,
of value: @autoclosure () throws -> Value?,
as snapshotting: Snapshotting<Value, String>,
message: @autoclosure () -> String = "",
record isRecording: Bool = isRecording,
Expand All @@ -46,41 +46,44 @@ import Foundation
) {
let _: Void = installTestObserver
do {
var actual: String!
var actual: String?
let expectation = XCTestExpectation()
try snapshotting.snapshot(value()).run {
actual = $0
expectation.fulfill()
}
switch XCTWaiter.wait(for: [expectation], timeout: timeout) {
case .completed:
break
case .timedOut:
XCTFail(
"""
Exceeded timeout of \(timeout) seconds waiting for snapshot.
This can happen when an asynchronously loaded value (like a network response) has not \
loaded. If a timeout is unavoidable, consider setting the "timeout" parameter of
"assertInlineSnapshot" to a higher value.
""",
file: file,
line: line
)
return
case .incorrectOrder, .interrupted, .invertedFulfillment:
XCTFail("Couldn't snapshot value", file: file, line: line)
return
@unknown default:
XCTFail("Couldn't snapshot value", file: file, line: line)
return
if let value = try value() {
snapshotting.snapshot(value).run {
actual = $0
expectation.fulfill()
}
switch XCTWaiter.wait(for: [expectation], timeout: timeout) {
case .completed:
break
case .timedOut:
XCTFail(
"""
Exceeded timeout of \(timeout) seconds waiting for snapshot.
This can happen when an asynchronously loaded value (like a network response) has not \
loaded. If a timeout is unavoidable, consider setting the "timeout" parameter of
"assertInlineSnapshot" to a higher value.
""",
file: file,
line: line
)
return
case .incorrectOrder, .interrupted, .invertedFulfillment:
XCTFail("Couldn't snapshot value", file: file, line: line)
return
@unknown default:
XCTFail("Couldn't snapshot value", file: file, line: line)
return
}
}
guard !isRecording, let expected = expected?()
let expected = expected?()
guard !isRecording, let expected
else {
// NB: Write snapshot state before calling `XCTFail` in case `continueAfterFailure = false`
inlineSnapshotState[File(path: file), default: []].append(
InlineSnapshot(
expected: expected?(),
expected: expected,
actual: actual,
wasRecording: isRecording,
syntaxDescriptor: syntaxDescriptor,
Expand All @@ -100,9 +103,7 @@ import Foundation
Automatically recorded a new snapshot for "\(syntaxDescriptor.trailingClosureLabel)".
"""
}
if let expected = expected?(),
let difference = snapshotting.diffing.diff(expected, actual)?.0
{
if let difference = snapshotting.diffing.diff(expected ?? "", actual ?? "")?.0 {
failure += " Difference: …\n\n\(difference.indenting(by: 2))"
}
XCTFail(
Expand All @@ -116,7 +117,7 @@ import Foundation
)
return
}
guard let difference = snapshotting.diffing.diff(expected, actual)?.0
guard let difference = snapshotting.diffing.diff(expected, actual ?? "")?.0
else { return }

let message = message()
Expand Down Expand Up @@ -304,7 +305,7 @@ public struct InlineSnapshotSyntaxDescriptor: Hashable {

private struct InlineSnapshot: Hashable {
var expected: String?
var actual: String
var actual: String?
var wasRecording: Bool
var syntaxDescriptor: InlineSnapshotSyntaxDescriptor
var function: String
Expand Down Expand Up @@ -388,8 +389,8 @@ public struct InlineSnapshotSyntaxDescriptor: Hashable {
self.wasRecording = snapshots.first?.wasRecording ?? isRecording
self.indent = String(
sourceLocationConverter.sourceLines
.first(where: { $0.first?.isWhitespace == true && $0 != "\n" })?
.prefix(while: { $0.isWhitespace })
.first { $0.first?.isWhitespace == true && $0.contains { !$0.isWhitespace } }?
.prefix { $0.isWhitespace }
?? " "
)
self.snapshots = snapshots
Expand Down Expand Up @@ -421,40 +422,42 @@ public struct InlineSnapshotSyntaxDescriptor: Hashable {
.prefix(while: { $0 == " " || $0 == "\t" })
)
let delimiter = String(
repeating: "#", count: snapshot.actual.hashCount(isMultiline: true)
repeating: "#", count: (snapshot.actual ?? "").hashCount(isMultiline: true)
)
let leadingIndent = leadingTrivia + self.indent
let snapshotLabel = TokenSyntax(
stringLiteral: snapshot.syntaxDescriptor.trailingClosureLabel
)
let snapshotClosure = ClosureExprSyntax(
leftBrace: .leftBraceToken(trailingTrivia: .newline),
statements: CodeBlockItemListSyntax {
StringLiteralExprSyntax(
leadingTrivia: Trivia(stringLiteral: leadingIndent),
openingPounds: .rawStringPoundDelimiter(delimiter),
openingQuote: .multilineStringQuoteToken(trailingTrivia: .newline),
segments: [
.stringSegment(
StringSegmentSyntax(
content: .stringSegment(
snapshot.actual
.replacingOccurrences(of: "\r", with: #"\\#(delimiter)r"#)
.indenting(with: leadingIndent)
let snapshotClosure = snapshot.actual.map { actual in
ClosureExprSyntax(
leftBrace: .leftBraceToken(trailingTrivia: .newline),
statements: CodeBlockItemListSyntax {
StringLiteralExprSyntax(
leadingTrivia: Trivia(stringLiteral: leadingIndent),
openingPounds: .rawStringPoundDelimiter(delimiter),
openingQuote: .multilineStringQuoteToken(trailingTrivia: .newline),
segments: [
.stringSegment(
StringSegmentSyntax(
content: .stringSegment(
actual
.replacingOccurrences(of: "\r", with: #"\\#(delimiter)r"#)
.indenting(with: leadingIndent)
)
)
)
)
],
closingQuote: .multilineStringQuoteToken(
leadingTrivia: .newline + Trivia(stringLiteral: leadingIndent)
),
closingPounds: .rawStringPoundDelimiter(delimiter)
],
closingQuote: .multilineStringQuoteToken(
leadingTrivia: .newline + Trivia(stringLiteral: leadingIndent)
),
closingPounds: .rawStringPoundDelimiter(delimiter)
)
},
rightBrace: .rightBraceToken(
leadingTrivia: .newline + Trivia(stringLiteral: leadingTrivia)
)
},
rightBrace: .rightBraceToken(
leadingTrivia: .newline + Trivia(stringLiteral: leadingTrivia)
)
)
}

let arguments = functionCallExpr.arguments
let firstTrailingClosureOffset =
Expand All @@ -475,23 +478,41 @@ public struct InlineSnapshotSyntaxDescriptor: Hashable {
switch centeredTrailingClosureOffset {
case ..<0:
let index = arguments.index(arguments.startIndex, offsetBy: trailingClosureOffset)
functionCallExpr.arguments[index].label = snapshotLabel
functionCallExpr.arguments[index].expression = ExprSyntax(snapshotClosure)
if let snapshotClosure {
functionCallExpr.arguments[index].label = snapshotLabel
functionCallExpr.arguments[index].expression = ExprSyntax(snapshotClosure)
} else {
functionCallExpr.arguments.remove(at: index)
}

case 0:
if snapshot.wasRecording || functionCallExpr.trailingClosure == nil {
functionCallExpr.rightParen?.trailingTrivia = .space
functionCallExpr.trailingClosure = snapshotClosure
if let snapshotClosure {
functionCallExpr.trailingClosure = snapshotClosure // FIXME: ?? multipleTrailingClosures.removeFirst()
} else if !functionCallExpr.additionalTrailingClosures.isEmpty {
let additionalTrailingClosure = functionCallExpr.additionalTrailingClosures.remove(
at: functionCallExpr.additionalTrailingClosures.startIndex
)
functionCallExpr.trailingClosure = additionalTrailingClosure.closure
} else {
functionCallExpr.rightParen?.trailingTrivia = ""
functionCallExpr.trailingClosure = nil
}
} else {
fatalError()
}

case 1...:
var newElement: MultipleTrailingClosureElementSyntax {
MultipleTrailingClosureElementSyntax(
label: snapshotLabel,
closure: snapshotClosure.with(\.leadingTrivia, snapshotClosure.leadingTrivia + .space)
)
var newElement: MultipleTrailingClosureElementSyntax? {
snapshotClosure.map { snapshotClosure in
MultipleTrailingClosureElementSyntax(
label: snapshotLabel,
closure: snapshotClosure.with(
\.leadingTrivia, snapshotClosure.leadingTrivia + .space
)
)
}
}

if !functionCallExpr.additionalTrailingClosures.isEmpty,
Expand All @@ -510,16 +531,22 @@ public struct InlineSnapshotSyntaxDescriptor: Hashable {
functionCallExpr.additionalTrailingClosures[index].label.text
) {
if snapshot.wasRecording {
functionCallExpr.additionalTrailingClosures[index].label = snapshotLabel
functionCallExpr.additionalTrailingClosures[index].closure = snapshotClosure
if let snapshotClosure {
functionCallExpr.additionalTrailingClosures[index].label = snapshotLabel
functionCallExpr.additionalTrailingClosures[index].closure = snapshotClosure
} else {
functionCallExpr.additionalTrailingClosures.remove(at: index)
}
}
} else {
} else if let newElement,
snapshot.wasRecording || index == functionCallExpr.additionalTrailingClosures.endIndex
{
functionCallExpr.additionalTrailingClosures.insert(
newElement.with(\.trailingTrivia, .space),
at: index
)
}
} else if centeredTrailingClosureOffset >= 1 {
} else if centeredTrailingClosureOffset >= 1, let newElement {
if let index = functionCallExpr.additionalTrailingClosures.index(
functionCallExpr.additionalTrailingClosures.endIndex,
offsetBy: -1,
Expand Down
18 changes: 12 additions & 6 deletions Sources/SnapshotTesting/AssertSnapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -371,18 +371,24 @@ func sanitizePathComponent(_ string: String) -> String {
// We need to clean counter between tests executions in order to support test-iterations.
private class CleanCounterBetweenTestCases: NSObject, XCTestObservation {
private static var registered = false
private static var registerQueue = DispatchQueue(
label: "co.pointfree.SnapshotTesting.testObserver")

static func registerIfNeeded() {
registerQueue.sync {
if !registered {
registered = true
XCTestObservationCenter.shared.addTestObserver(CleanCounterBetweenTestCases())
if Thread.isMainThread {
doRegisterIfNeeded()
} else {
DispatchQueue.main.sync {
doRegisterIfNeeded()
}
}
}

private static func doRegisterIfNeeded() {
if !registered {
registered = true
XCTestObservationCenter.shared.addTestObserver(CleanCounterBetweenTestCases())
}
}

func testCaseDidFinish(_ testCase: XCTestCase) {
counterQueue.sync {
counterMap = [:]
Expand Down
8 changes: 6 additions & 2 deletions Sources/SnapshotTesting/Common/View.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
#endif

#if os(iOS) || os(tvOS)
public struct ViewImageConfig {
public struct ViewImageConfig: Sendable {
public enum Orientation {
case landscape
case portrait
Expand Down Expand Up @@ -868,7 +868,11 @@
callback(Image())
return
}
wkWebView.takeSnapshot(with: nil) { image, _ in
let configuration = WKSnapshotConfiguration()
if #available(iOS 13, macOS 10.15, *) {
configuration.afterScreenUpdates = false
}
wkWebView.takeSnapshot(with: configuration) { image, _ in
callback(image!)
}
}
Expand Down
Loading

0 comments on commit 45d14a4

Please sign in to comment.