Skip to content

Commit 45d14a4

Browse files
committed
Merge branch 'main' into 1.16.0
* 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)
2 parents b567d07 + 837238a commit 45d14a4

File tree

9 files changed

+375
-239
lines changed

9 files changed

+375
-239
lines changed

.github/workflows/ci.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ jobs:
4343
strategy:
4444
matrix:
4545
swift:
46-
- "5.8"
46+
- "5.9.1"
4747

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

5151
steps:
5252
- uses: compnerd/gha-setup-swift@main
@@ -59,6 +59,6 @@ jobs:
5959
git config --global core.autocrlf false
6060
git config --global core.eol lf
6161
62-
- uses: actions/checkout@v3
62+
- uses: actions/checkout@v4
6363
- run: swift build
6464
- run: swift test

Package.resolved

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
"kind" : "remoteSourceControl",
66
"location" : "https://github.com/apple/swift-syntax.git",
77
"state" : {
8-
"revision" : "74203046135342e4a4a627476dd6caf8b28fe11b",
9-
"version" : "509.0.0"
8+
"revision" : "08a2f0a9a30e0f705f79c9cfaca1f68b71bdc775",
9+
"version" : "510.0.0"
1010
}
1111
}
1212
],

Package.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ let package = Package(
2121
),
2222
],
2323
dependencies: [
24-
.package(url: "https://github.com/apple/swift-syntax.git", "508.0.1"..<"510.0.0")
24+
.package(url: "https://github.com/apple/swift-syntax", "508.0.1"..<"511.0.0")
2525
],
2626
targets: [
2727
.target(

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,9 @@ targets: [
258258
- [SnapshotTestingHEIC](https://github.com/alexey1312/SnapshotTestingHEIC) adds image support
259259
using the HEIC storage format which reduces file sizes in comparison to PNG.
260260

261+
- [SnapshotVision](https://github.com/gregersson/swift-snapshot-testing-vision) adds snapshot
262+
strategy for text recognition on views and images. Uses Apples Vision framework.
263+
261264
Have you written your own SnapshotTesting plug-in?
262265
[Add it here](https://github.com/pointfreeco/swift-snapshot-testing/edit/master/README.md) and
263266
submit a pull request!

Sources/InlineSnapshotTesting/AssertInlineSnapshot.swift

+101-74
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import Foundation
3232
/// - column: The column where the assertion occurs. The default is the line column you call
3333
/// this function.
3434
public func assertInlineSnapshot<Value>(
35-
of value: @autoclosure () throws -> Value,
35+
of value: @autoclosure () throws -> Value?,
3636
as snapshotting: Snapshotting<Value, String>,
3737
message: @autoclosure () -> String = "",
3838
record isRecording: Bool = isRecording,
@@ -46,41 +46,44 @@ import Foundation
4646
) {
4747
let _: Void = installTestObserver
4848
do {
49-
var actual: String!
49+
var actual: String?
5050
let expectation = XCTestExpectation()
51-
try snapshotting.snapshot(value()).run {
52-
actual = $0
53-
expectation.fulfill()
54-
}
55-
switch XCTWaiter.wait(for: [expectation], timeout: timeout) {
56-
case .completed:
57-
break
58-
case .timedOut:
59-
XCTFail(
60-
"""
61-
Exceeded timeout of \(timeout) seconds waiting for snapshot.
62-
63-
This can happen when an asynchronously loaded value (like a network response) has not \
64-
loaded. If a timeout is unavoidable, consider setting the "timeout" parameter of
65-
"assertInlineSnapshot" to a higher value.
66-
""",
67-
file: file,
68-
line: line
69-
)
70-
return
71-
case .incorrectOrder, .interrupted, .invertedFulfillment:
72-
XCTFail("Couldn't snapshot value", file: file, line: line)
73-
return
74-
@unknown default:
75-
XCTFail("Couldn't snapshot value", file: file, line: line)
76-
return
51+
if let value = try value() {
52+
snapshotting.snapshot(value).run {
53+
actual = $0
54+
expectation.fulfill()
55+
}
56+
switch XCTWaiter.wait(for: [expectation], timeout: timeout) {
57+
case .completed:
58+
break
59+
case .timedOut:
60+
XCTFail(
61+
"""
62+
Exceeded timeout of \(timeout) seconds waiting for snapshot.
63+
64+
This can happen when an asynchronously loaded value (like a network response) has not \
65+
loaded. If a timeout is unavoidable, consider setting the "timeout" parameter of
66+
"assertInlineSnapshot" to a higher value.
67+
""",
68+
file: file,
69+
line: line
70+
)
71+
return
72+
case .incorrectOrder, .interrupted, .invertedFulfillment:
73+
XCTFail("Couldn't snapshot value", file: file, line: line)
74+
return
75+
@unknown default:
76+
XCTFail("Couldn't snapshot value", file: file, line: line)
77+
return
78+
}
7779
}
78-
guard !isRecording, let expected = expected?()
80+
let expected = expected?()
81+
guard !isRecording, let expected
7982
else {
8083
// NB: Write snapshot state before calling `XCTFail` in case `continueAfterFailure = false`
8184
inlineSnapshotState[File(path: file), default: []].append(
8285
InlineSnapshot(
83-
expected: expected?(),
86+
expected: expected,
8487
actual: actual,
8588
wasRecording: isRecording,
8689
syntaxDescriptor: syntaxDescriptor,
@@ -100,9 +103,7 @@ import Foundation
100103
Automatically recorded a new snapshot for "\(syntaxDescriptor.trailingClosureLabel)".
101104
"""
102105
}
103-
if let expected = expected?(),
104-
let difference = snapshotting.diffing.diff(expected, actual)?.0
105-
{
106+
if let difference = snapshotting.diffing.diff(expected ?? "", actual ?? "")?.0 {
106107
failure += " Difference: …\n\n\(difference.indenting(by: 2))"
107108
}
108109
XCTFail(
@@ -116,7 +117,7 @@ import Foundation
116117
)
117118
return
118119
}
119-
guard let difference = snapshotting.diffing.diff(expected, actual)?.0
120+
guard let difference = snapshotting.diffing.diff(expected, actual ?? "")?.0
120121
else { return }
121122

122123
let message = message()
@@ -304,7 +305,7 @@ public struct InlineSnapshotSyntaxDescriptor: Hashable {
304305

305306
private struct InlineSnapshot: Hashable {
306307
var expected: String?
307-
var actual: String
308+
var actual: String?
308309
var wasRecording: Bool
309310
var syntaxDescriptor: InlineSnapshotSyntaxDescriptor
310311
var function: String
@@ -388,8 +389,8 @@ public struct InlineSnapshotSyntaxDescriptor: Hashable {
388389
self.wasRecording = snapshots.first?.wasRecording ?? isRecording
389390
self.indent = String(
390391
sourceLocationConverter.sourceLines
391-
.first(where: { $0.first?.isWhitespace == true && $0 != "\n" })?
392-
.prefix(while: { $0.isWhitespace })
392+
.first { $0.first?.isWhitespace == true && $0.contains { !$0.isWhitespace } }?
393+
.prefix { $0.isWhitespace }
393394
?? " "
394395
)
395396
self.snapshots = snapshots
@@ -421,40 +422,42 @@ public struct InlineSnapshotSyntaxDescriptor: Hashable {
421422
.prefix(while: { $0 == " " || $0 == "\t" })
422423
)
423424
let delimiter = String(
424-
repeating: "#", count: snapshot.actual.hashCount(isMultiline: true)
425+
repeating: "#", count: (snapshot.actual ?? "").hashCount(isMultiline: true)
425426
)
426427
let leadingIndent = leadingTrivia + self.indent
427428
let snapshotLabel = TokenSyntax(
428429
stringLiteral: snapshot.syntaxDescriptor.trailingClosureLabel
429430
)
430-
let snapshotClosure = ClosureExprSyntax(
431-
leftBrace: .leftBraceToken(trailingTrivia: .newline),
432-
statements: CodeBlockItemListSyntax {
433-
StringLiteralExprSyntax(
434-
leadingTrivia: Trivia(stringLiteral: leadingIndent),
435-
openingPounds: .rawStringPoundDelimiter(delimiter),
436-
openingQuote: .multilineStringQuoteToken(trailingTrivia: .newline),
437-
segments: [
438-
.stringSegment(
439-
StringSegmentSyntax(
440-
content: .stringSegment(
441-
snapshot.actual
442-
.replacingOccurrences(of: "\r", with: #"\\#(delimiter)r"#)
443-
.indenting(with: leadingIndent)
431+
let snapshotClosure = snapshot.actual.map { actual in
432+
ClosureExprSyntax(
433+
leftBrace: .leftBraceToken(trailingTrivia: .newline),
434+
statements: CodeBlockItemListSyntax {
435+
StringLiteralExprSyntax(
436+
leadingTrivia: Trivia(stringLiteral: leadingIndent),
437+
openingPounds: .rawStringPoundDelimiter(delimiter),
438+
openingQuote: .multilineStringQuoteToken(trailingTrivia: .newline),
439+
segments: [
440+
.stringSegment(
441+
StringSegmentSyntax(
442+
content: .stringSegment(
443+
actual
444+
.replacingOccurrences(of: "\r", with: #"\\#(delimiter)r"#)
445+
.indenting(with: leadingIndent)
446+
)
444447
)
445448
)
446-
)
447-
],
448-
closingQuote: .multilineStringQuoteToken(
449-
leadingTrivia: .newline + Trivia(stringLiteral: leadingIndent)
450-
),
451-
closingPounds: .rawStringPoundDelimiter(delimiter)
449+
],
450+
closingQuote: .multilineStringQuoteToken(
451+
leadingTrivia: .newline + Trivia(stringLiteral: leadingIndent)
452+
),
453+
closingPounds: .rawStringPoundDelimiter(delimiter)
454+
)
455+
},
456+
rightBrace: .rightBraceToken(
457+
leadingTrivia: .newline + Trivia(stringLiteral: leadingTrivia)
452458
)
453-
},
454-
rightBrace: .rightBraceToken(
455-
leadingTrivia: .newline + Trivia(stringLiteral: leadingTrivia)
456459
)
457-
)
460+
}
458461

459462
let arguments = functionCallExpr.arguments
460463
let firstTrailingClosureOffset =
@@ -475,23 +478,41 @@ public struct InlineSnapshotSyntaxDescriptor: Hashable {
475478
switch centeredTrailingClosureOffset {
476479
case ..<0:
477480
let index = arguments.index(arguments.startIndex, offsetBy: trailingClosureOffset)
478-
functionCallExpr.arguments[index].label = snapshotLabel
479-
functionCallExpr.arguments[index].expression = ExprSyntax(snapshotClosure)
481+
if let snapshotClosure {
482+
functionCallExpr.arguments[index].label = snapshotLabel
483+
functionCallExpr.arguments[index].expression = ExprSyntax(snapshotClosure)
484+
} else {
485+
functionCallExpr.arguments.remove(at: index)
486+
}
480487

481488
case 0:
482489
if snapshot.wasRecording || functionCallExpr.trailingClosure == nil {
483490
functionCallExpr.rightParen?.trailingTrivia = .space
484-
functionCallExpr.trailingClosure = snapshotClosure
491+
if let snapshotClosure {
492+
functionCallExpr.trailingClosure = snapshotClosure // FIXME: ?? multipleTrailingClosures.removeFirst()
493+
} else if !functionCallExpr.additionalTrailingClosures.isEmpty {
494+
let additionalTrailingClosure = functionCallExpr.additionalTrailingClosures.remove(
495+
at: functionCallExpr.additionalTrailingClosures.startIndex
496+
)
497+
functionCallExpr.trailingClosure = additionalTrailingClosure.closure
498+
} else {
499+
functionCallExpr.rightParen?.trailingTrivia = ""
500+
functionCallExpr.trailingClosure = nil
501+
}
485502
} else {
486503
fatalError()
487504
}
488505

489506
case 1...:
490-
var newElement: MultipleTrailingClosureElementSyntax {
491-
MultipleTrailingClosureElementSyntax(
492-
label: snapshotLabel,
493-
closure: snapshotClosure.with(\.leadingTrivia, snapshotClosure.leadingTrivia + .space)
494-
)
507+
var newElement: MultipleTrailingClosureElementSyntax? {
508+
snapshotClosure.map { snapshotClosure in
509+
MultipleTrailingClosureElementSyntax(
510+
label: snapshotLabel,
511+
closure: snapshotClosure.with(
512+
\.leadingTrivia, snapshotClosure.leadingTrivia + .space
513+
)
514+
)
515+
}
495516
}
496517

497518
if !functionCallExpr.additionalTrailingClosures.isEmpty,
@@ -510,16 +531,22 @@ public struct InlineSnapshotSyntaxDescriptor: Hashable {
510531
functionCallExpr.additionalTrailingClosures[index].label.text
511532
) {
512533
if snapshot.wasRecording {
513-
functionCallExpr.additionalTrailingClosures[index].label = snapshotLabel
514-
functionCallExpr.additionalTrailingClosures[index].closure = snapshotClosure
534+
if let snapshotClosure {
535+
functionCallExpr.additionalTrailingClosures[index].label = snapshotLabel
536+
functionCallExpr.additionalTrailingClosures[index].closure = snapshotClosure
537+
} else {
538+
functionCallExpr.additionalTrailingClosures.remove(at: index)
539+
}
515540
}
516-
} else {
541+
} else if let newElement,
542+
snapshot.wasRecording || index == functionCallExpr.additionalTrailingClosures.endIndex
543+
{
517544
functionCallExpr.additionalTrailingClosures.insert(
518545
newElement.with(\.trailingTrivia, .space),
519546
at: index
520547
)
521548
}
522-
} else if centeredTrailingClosureOffset >= 1 {
549+
} else if centeredTrailingClosureOffset >= 1, let newElement {
523550
if let index = functionCallExpr.additionalTrailingClosures.index(
524551
functionCallExpr.additionalTrailingClosures.endIndex,
525552
offsetBy: -1,

Sources/SnapshotTesting/AssertSnapshot.swift

+12-6
Original file line numberDiff line numberDiff line change
@@ -371,18 +371,24 @@ func sanitizePathComponent(_ string: String) -> String {
371371
// We need to clean counter between tests executions in order to support test-iterations.
372372
private class CleanCounterBetweenTestCases: NSObject, XCTestObservation {
373373
private static var registered = false
374-
private static var registerQueue = DispatchQueue(
375-
label: "co.pointfree.SnapshotTesting.testObserver")
376374

377375
static func registerIfNeeded() {
378-
registerQueue.sync {
379-
if !registered {
380-
registered = true
381-
XCTestObservationCenter.shared.addTestObserver(CleanCounterBetweenTestCases())
376+
if Thread.isMainThread {
377+
doRegisterIfNeeded()
378+
} else {
379+
DispatchQueue.main.sync {
380+
doRegisterIfNeeded()
382381
}
383382
}
384383
}
385384

385+
private static func doRegisterIfNeeded() {
386+
if !registered {
387+
registered = true
388+
XCTestObservationCenter.shared.addTestObserver(CleanCounterBetweenTestCases())
389+
}
390+
}
391+
386392
func testCaseDidFinish(_ testCase: XCTestCase) {
387393
counterQueue.sync {
388394
counterMap = [:]

Sources/SnapshotTesting/Common/View.swift

+6-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
#endif
1313

1414
#if os(iOS) || os(tvOS)
15-
public struct ViewImageConfig {
15+
public struct ViewImageConfig: Sendable {
1616
public enum Orientation {
1717
case landscape
1818
case portrait
@@ -868,7 +868,11 @@
868868
callback(Image())
869869
return
870870
}
871-
wkWebView.takeSnapshot(with: nil) { image, _ in
871+
let configuration = WKSnapshotConfiguration()
872+
if #available(iOS 13, macOS 10.15, *) {
873+
configuration.afterScreenUpdates = false
874+
}
875+
wkWebView.takeSnapshot(with: configuration) { image, _ in
872876
callback(image!)
873877
}
874878
}

0 commit comments

Comments
 (0)