From 5cd4166025f84b822841bbc5c30bccd27f4a777f Mon Sep 17 00:00:00 2001 From: Paul Zabelin Date: Sun, 5 Jun 2022 17:34:00 -0700 Subject: [PATCH] Experiment/scale 1 (#17) * use transparent background for diff * add premultiply alpha before comparing * premultiply alpha for diff, clear background for host * use transparent background * test compare of same data should have 0 difference * use hierarchy and sRGB * use scale 1.0 * allow appearance transition when adding and removing child controller * move settingAlphaOne to package * use extended range and P3 gamut * set flatness, test no rendering in unit tests * fix: unbalance calls to appearance for delaying for 0.01s * use standard range to fix image comparison for sample view * fix: favorite view renders with 5% accuracy on GitHub runner --- .github/workflows/build-and-test.yml | 10 ++ .../SwiftUI-snapshot-testing.xcscheme | 105 ++++++++++++++++++ Example/ApplicationTests/SnapshotTests.swift | 7 +- .../Snapshots/ContentView.png | Bin 6068 -> 2025 bytes .../Snapshots/FavoriteView.png | Bin 4885 -> 3138 bytes .../ApplicationTests/Snapshots/example.png | Bin 3177 -> 1832 bytes Sources/Private/UIViewExtensions.swift | 66 +++++++++-- Sources/Private/maxColorDiff.swift | 33 +++++- Sources/verifySnapshot.swift | 37 +++--- Tests/CompareTest.swift | 12 ++ Tests/SampleViewTest.swift | 26 ++++- Tests/Snapshots/SampleView-blank.png | Bin 0 -> 772 bytes Tests/Snapshots/SampleView.png | Bin 737 -> 296 bytes Tests/Snapshots/UIKit-sample-view.png | Bin 2288 -> 1210 bytes Tests/UIKitViewTest.swift | 10 +- 15 files changed, 261 insertions(+), 45 deletions(-) create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/SwiftUI-snapshot-testing.xcscheme create mode 100644 Tests/CompareTest.swift create mode 100644 Tests/Snapshots/SampleView-blank.png diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 36e4b29..5f0de6e 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -45,4 +45,14 @@ jobs: project: Example/Example.xcodeproj scheme: Example destination: ${{ env.destination }} + result-bundle-path: test-results/example-tests action: test + - name: Archive results # due to: https://github.com/actions/upload-artifact/issues/243 + if: always() + run: zip -FSry results.zip test-results || true + - name: Upload test results + if: always() + uses: actions/upload-artifact@v3 + with: + name: test-results + path: results.zip diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/SwiftUI-snapshot-testing.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/SwiftUI-snapshot-testing.xcscheme new file mode 100644 index 0000000..d889863 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/SwiftUI-snapshot-testing.xcscheme @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/ApplicationTests/SnapshotTests.swift b/Example/ApplicationTests/SnapshotTests.swift index ac254b2..44c7a55 100644 --- a/Example/ApplicationTests/SnapshotTests.swift +++ b/Example/ApplicationTests/SnapshotTests.swift @@ -3,10 +3,11 @@ import XCTest import SwiftUI_snapshot_test import SwiftUI + class SnapshotTests: XCTestCase { func testViews() throws { - verifySnapshot(FavoriteView_Previews.self) - verifySnapshot(ContentView()) - verifySnapshot(Text("SwiftUI").foregroundColor(.red), "example") + verifySnapshot(FavoriteView_Previews.self, colorAccuracy: 0.05) + verifySnapshot(ContentView(), colorAccuracy: 0) + verifySnapshot(Text("SwiftUI").foregroundColor(.red), "example", colorAccuracy: 0) } } diff --git a/Example/ApplicationTests/Snapshots/ContentView.png b/Example/ApplicationTests/Snapshots/ContentView.png index ff68d4e916f423cdbee8b1a1de6ca0a40b015121..980914498f37a06b8c79bc4b5a99ef8f82394fe7 100644 GIT binary patch literal 2025 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 literal 4885 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 diff --git a/Sources/Private/UIViewExtensions.swift b/Sources/Private/UIViewExtensions.swift index bf0829a..a571969 100644 --- a/Sources/Private/UIViewExtensions.swift +++ b/Sources/Private/UIViewExtensions.swift @@ -1,25 +1,67 @@ import UIKit +import XCTest -extension UIView { - func renderFormat() -> UIGraphicsImageRendererFormat { - let format = UIGraphicsImageRendererFormat(for: .current) - format.opaque = true +extension UITraitCollection { + static let snapshots = UITraitCollection(traitsFrom: [ + UITraitCollection(displayGamut: .P3), + UITraitCollection(displayScale: 1.0), + UITraitCollection(activeAppearance: .active), + UITraitCollection(userInterfaceLevel: .base), + UITraitCollection(legibilityWeight: .regular), + UITraitCollection(userInterfaceStyle: .light), + UITraitCollection(preferredContentSizeCategory: .medium), + ]) +} + +extension UIGraphicsImageRendererFormat { + static let snapshots: UIGraphicsImageRendererFormat = { + let format = UIGraphicsImageRendererFormat(for: .snapshots) + format.opaque = false format.preferredRange = .standard return format - } + }() +} + +extension UIView { func renderer() -> UIGraphicsImageRenderer { - UIGraphicsImageRenderer(bounds: bounds, format: renderFormat()) + UIGraphicsImageRenderer(bounds: bounds, format: .snapshots) } func renderLayerAsBitmap() -> UIImage { - renderer().image { - layer.render(in: $0.cgContext) - } + renderer().image(actions: renderLayerActions(_:)) + } + + func renderLayerAsPNG() -> Data { + renderer().pngData(actions: renderLayerActions(_:)) + } + + func renderLayerActions(_ context: UIGraphicsImageRendererContext) { + configureContext(context) + layer.render(in: context.cgContext) + } + + func configureContext(_ context: UIGraphicsImageRendererContext) { + context.cgContext.setFlatness(0.01) + context.cgContext.setShouldAntialias(false) + context.cgContext.setAllowsAntialiasing(false) + context.cgContext.setAllowsFontSubpixelPositioning(false) + context.cgContext.setShouldSubpixelPositionFonts(false) + context.cgContext.setShouldSmoothFonts(false) + context.cgContext.setAllowsFontSubpixelQuantization(false) + context.cgContext.setShouldSubpixelQuantizeFonts(false) + } + + func renderHierarchyAsPNG() -> Data { + renderer().pngData(actions: drawHierarchyActions(_:)) + } + + func drawHierarchyActions(_ context: UIGraphicsImageRendererContext) { + configureContext(context) + XCTAssertTrue(drawHierarchy(in: bounds, afterScreenUpdates: true), + "unable to take snapshot of the view") } func renderHierarchyOnScreen() -> UIImage { - renderer().image { _ in - drawHierarchy(in: bounds, afterScreenUpdates: true) - } + renderer().image(actions: drawHierarchyActions(_:)) } } diff --git a/Sources/Private/maxColorDiff.swift b/Sources/Private/maxColorDiff.swift index ca6b2e9..9dcf32c 100644 --- a/Sources/Private/maxColorDiff.swift +++ b/Sources/Private/maxColorDiff.swift @@ -2,6 +2,12 @@ import UIKit import CoreImage import CoreImage.CIFilterBuiltins +extension CIImage { + func settingAlphaOne() -> CIImage { + settingAlphaOne(in: extent) + } +} + func diff(_ old: UIImage, _ new: UIImage) -> UIImage { let differenceFilter = diff( old.cgImage!, @@ -21,15 +27,15 @@ func diff(_ old: CGImage, _ new: CGImage) -> CICompositeOperation { func diff(_ old: CIImage, _ new: CIImage) -> CICompositeOperation { let differenceFilter: CICompositeOperation = CIFilter.differenceBlendMode() - differenceFilter.inputImage = old - differenceFilter.backgroundImage = new + differenceFilter.inputImage = old.settingAlphaOne() + differenceFilter.backgroundImage = new.settingAlphaOne() return differenceFilter } func histogramData(_ ciImage: CIImage) -> Data { let hist = CIFilter.areaHistogram() hist.inputImage = ciImage - hist.setValue(CIVector(cgRect: ciImage.extent), forKey: kCIInputExtentKey) + hist.extent = ciImage.extent return hist.value(forKey: "outputData") as! Data } @@ -55,8 +61,25 @@ func histogram(ciImage: CIImage) -> [UInt32] { } func compare(_ left: UIImage, _ right: UIImage) -> ImageComparisonResult { - let image1 = CIImage(image: left)! - let image2 = CIImage(image: right)! + let image1 = CIImage(image: left)!.premultiplyingAlpha() + let image2 = CIImage(image: right)!.premultiplyingAlpha() + let diffOperation = diff(image1, image2) + return ImageComparisonResult(difference: diffOperation.outputImage!) +} + +let workColorSpace = CGColorSpace(name: CGColorSpace.displayP3)! +//let workColorSpace = CGColorSpace(name: CGColorSpace.extendedSRGB)! +//let workColorSpace = CGColorSpace(name: CGColorSpace.extendedDisplayP3)! + +func compare(_ left: Data, _ right: Data) -> ImageComparisonResult { + let options: [CIImageOption : Any] = [ + .colorSpace: workColorSpace, + .nearestSampling: NSNumber(booleanLiteral: true) + ] + let image1 = CIImage(data: left, options: options)! + .premultiplyingAlpha() + let image2 = CIImage(data: right, options: options)! + .premultiplyingAlpha() let diffOperation = diff(image1, image2) return ImageComparisonResult(difference: diffOperation.outputImage!) } diff --git a/Sources/verifySnapshot.swift b/Sources/verifySnapshot.swift index 580f8b6..fc5fafe 100644 --- a/Sources/verifySnapshot.swift +++ b/Sources/verifySnapshot.swift @@ -30,18 +30,14 @@ func ensureFolder(url: URL) throws { public func verifySnapshot(_ view: V, _ name: String? = nil, colorAccuracy: Float = 0.02, file: StaticString = #filePath, line: UInt = #line) { - guard let image = try? inWindowView(view, block: { - $0.renderLayerAsBitmap() + guard let pngData = try? inWindowView(view, block: { + $0.renderHierarchyAsPNG() }) else { XCTFail("failed to get snapshot of view") return } let isRunningOnCI = ProcessInfo.processInfo.environment.keys.contains("CI") let shouldOverwriteExpected = !isRunningOnCI - guard let pngData = image.pngData() else { - XCTFail("failed to get image data") - return - } let viewName = name ?? "\(V.self)" let fileName = viewName + ".png" let url = folderUrl(String(describing: file)).appendingPathComponent(fileName) @@ -53,13 +49,14 @@ public func verifySnapshot(_ view: V, _ name: String? = nil, colorAccur }, onFailure, file: file, line: line) } - if let expectedData = try? Data(contentsOf: url), let expectedImage = UIImage(data: expectedData) { + if let expectedData = try? Data(contentsOf: url) { XCTContext.runActivity(named: viewName) { let actualImage = XCTAttachment(data: pngData, uniformTypeIdentifier: UTType.png.identifier) actualImage.name = "actual image" $0.add(actualImage) - let diff = compare(image, expectedImage) - if diff.maxColorDifference() > colorAccuracy { + let diff = compare(pngData, expectedData) + let actualDifference = diff.maxColorDifference() + if actualDifference > colorAccuracy { if shouldOverwriteExpected { writeActual(onFailure: "failed to record actual image") } @@ -74,11 +71,18 @@ public func verifySnapshot(_ view: V, _ name: String? = nil, colorAccur ) } let ciImage = diff.difference - guard let diffImage = CIContext().createCGImage(ciImage, from: ciImage.extent) else { - XCTFail("failed to get image of difference") - return - } - let diffAttachment = XCTAttachment(image: UIImage(cgImage: diffImage)) + let context = CIContext(options: [ + .workingColorSpace : workColorSpace, + .allowLowPower: NSNumber(booleanLiteral: false), + .highQualityDownsample: NSNumber(booleanLiteral: true), + .outputColorSpace: workColorSpace, + .useSoftwareRenderer: NSNumber(booleanLiteral: true), + .cacheIntermediates: NSNumber(booleanLiteral: false), + .priorityRequestLow: NSNumber(booleanLiteral: false), + .name: "difference" + ]) + let data = context.pngRepresentation(of: ciImage.premultiplyingAlpha(), format: .RGBA8, colorSpace: workColorSpace)! + let diffAttachment = XCTAttachment(data: data, uniformTypeIdentifier: UTType.png.identifier) diffAttachment.name = "difference" $0.add(diffAttachment) } @@ -107,7 +111,7 @@ func folderUrl(_ filePath: String = #filePath) -> URL { see: https://github.com/paulz/SwiftUI-snapshot-testing/issues/11 */ func allowAppearanceTransition() { - RunLoop.current.run(until: .init(timeIntervalSinceNow: 0)) + RunLoop.current.run(until: .init(timeIntervalSinceNow: 0.01)) } func inWindowView(_ swiftUIView: V, block: (UIView) -> T) throws -> T { @@ -127,8 +131,10 @@ func inWindowView(_ swiftUIView: V, block: (UIView) -> T) throws -> if size == .zero { size = layoutFrame.size } + view.backgroundColor = .clear let safeOrigin = layoutFrame.origin rootController.addChild(controller) + allowAppearanceTransition() view.frame = .init(origin: safeOrigin, size: size) rootController.view.addSubview(controller.view) view.frame = .init(origin: safeOrigin, size: size) @@ -136,6 +142,7 @@ func inWindowView(_ swiftUIView: V, block: (UIView) -> T) throws -> defer { view.removeFromSuperview() controller.removeFromParent() + allowAppearanceTransition() } return block(view) } diff --git a/Tests/CompareTest.swift b/Tests/CompareTest.swift new file mode 100644 index 0000000..b452ed9 --- /dev/null +++ b/Tests/CompareTest.swift @@ -0,0 +1,12 @@ +@testable import ViewSnapshotTesting +import XCTest + +class CompareTest: XCTestCase { + func testSameDataShouldHave0Difference() throws { + let expectedData = try Data( + contentsOf: folderUrl().appendingPathComponent("SampleView.png") + ) + let result = compare(expectedData, expectedData) + XCTAssertEqual(result.maxColorDifference(), 0) + } +} diff --git a/Tests/SampleViewTest.swift b/Tests/SampleViewTest.swift index f850664..4bcc33a 100644 --- a/Tests/SampleViewTest.swift +++ b/Tests/SampleViewTest.swift @@ -7,19 +7,33 @@ class SampleViewTest: XCTestCase { let expectedSize = CGSize(width: 30, height: 20) func testSwiftUIRendersInWindow() { - verifySnapshot(SampleView()) + let options = XCTExpectedFailure.Options() + options.issueMatcher = { issue in + issue.type == .assertionFailure && + issue.compactDescription.hasSuffix("unable to take snapshot of the view") + } + XCTExpectFailure("Unable to render SwiftUI view hierarchy", + options: options) + // xctest warning: + // [Snapshotting] Rendering a view that has not been committed to render server is not supported. + verifySnapshot(SampleView(), "SampleView-blank", colorAccuracy: 0) } func testImageSizeIsScaledFromExpected() throws { let image = try inWindowView(SampleView()) { $0.renderLayerAsBitmap() } + let url = folderUrl().appendingPathComponent("SampleView.png") XCTAssertEqual(image.size, expectedSize) - let expectedData = try Data( - contentsOf: folderUrl().appendingPathComponent("SampleView.png") - ) - let expectedImage = try XCTUnwrap(UIImage(data: expectedData)) - let scale = UIScreen.main.scale + let expectedData = try Data(contentsOf: url) + let scale = UITraitCollection.snapshots.displayScale + let expectedImage = try XCTUnwrap(UIImage(data: expectedData, scale: scale)) + let result = compare(image, expectedImage) + let colorDifference = result.maxColorDifference() + if colorDifference > 0 { + try image.pngData()?.write(to: url) + } + XCTAssertEqual(colorDifference, 0) XCTAssertEqual(expectedImage.size, expectedSize.applying(.init(scaleX: scale, y: scale))) } } diff --git a/Tests/Snapshots/SampleView-blank.png b/Tests/Snapshots/SampleView-blank.png new file mode 100644 index 0000000000000000000000000000000000000000..b4013459f02c51d2719a203a3fa0008b324b1f22 GIT binary patch literal 772 zcmeAS@N?(olHy`uVBq!ia0vp^Q9x|N!3HFsv95JxU|=lGbaoENc6N8p&&e+eE=WvH zb;&F)$VsdWFlJ!Tm{>YtulL~qk)!^rgDzNg@X5*;bvj?U#8p<9p)KNS5W>1Tcvc-} zZjFj&P@!L&Q@k_gfOLSQ z_BSStm(wQt99gpU%C0BX&F%AoSEX%Qu}FvO-{0M@KVSTM&%YzmcXyefQVfA3#$?3EOIFtKxLZa3#WRXM{9%}1*q>^XhB>9XE@>v>aC=KPoz zafvIdZHK4Ma_cWO?9;na*KTdTac}6jqby3J7duVSQNt=W?HYpKeij zLV#XBlg=p@PeI{LM?POzm?&ga$l>^E!j9tCb^L38pUlmDFFGT);GF)gTTaXta~Dp& zdiz!di-Z(I-YXx|5cgLLzdb8D@SD?M2279$YbF-pVPZj2fX z%wTaQ28Q-b1{SCqKOhYPH-H$V2SPI~V1lcNTEGlv+kljIGjo~%Db50q$YKTtMGFvS zbe(3H02GwTbny=X(m+eVKn_T105OOL;i|N%e|Lbio2QFoNCo4Y3ynYy!{G%#gXN#J z1|%M6U}R?FkuXRgg7LxH0I2o<|NVtc$AJuIkcn?8GcRNg1EZe$Ga-bny=X z(o7&RAON{U14@I~**y2176WN>PZ!4!58k8%1)&)--7`FS)Gk>(nktyA>B0L^O=xA& zlSm#e38#H(MjuWDx1BEfbzrQvKhMhymLusG11_DlvAs2V>Y4FWfS7^DY6GcRC-ss@TJ zV1lcTTEGlv+kmuPYBKEvQk(@Ik;M!QiWVTu=sL|X0VpVw>Ea&(q(Ke`0XZPnfYLxV zV~_U!8C!sKgr|#RNCo4Y>9*X59C+IPer}q)qr3UhiziBt)E+&1q4G6ZfaR#G3rlY8 zf(5-|F1~)PQ~0L8{_6Y5 zHzWD&)SEo~e%y*xEI8tMC+e%6;-S|a8PDe}7H4A5^L+MJ>fuoXoepDxzAw%OKjWh! zC1n!!o)Gg4kg-V1PS{*iC6~x>c+K~TIvrbNrQBalT`bb>-FN%X^#Gi_?LzkTo;$S4Da@!Ly=d<_aBtd4%KzuG88ZQ6T3tAlmDV&SH2>-Gay@777XD;;Q5 zW|Dk$K(LXg^g)M1n^l2}g0pOWVa#{AU!U0L_Z_rq)47-aoUiZ3BQ!M@N)s0H$q1eZ mVDIBdS;6eiVsu5Yk3H~xTG?7l-bkPe7(8A5T-G@yGywq12KJBu diff --git a/Tests/Snapshots/UIKit-sample-view.png b/Tests/Snapshots/UIKit-sample-view.png index 2520f5355ca4da3d300eac4e34ed1c7d38ab2b0d..764c30f624a0f38681a6989d071f902081b21121 100644 GIT binary patch literal 1210 zcmV;r1V#IaP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91D4+uX1ONa40RR916aWAK03J6pD*ylh9BD*PQ~&?~0ssI2000003IG5A zC;$Ke3IG5A3IG5B+uo(XZU6uQr%6OXR7eeC2n`MWKbSGYV0IkHZ6nu#5fQ=v6BGUa zhlhuNlTcJt@c)Da|NmiO5I(Un2M)oM6yN`g7U})Z&Gp2Z9y2n0!15Ux-dN?aS=QWa z_y5%^?*9b^?qEGVJ(mBUKj-zr z%Nnewq{J1hXUY`Q|9}57{?Ez59thaX0EvM^u)N#_Y*<&96;=n-*E@jaK@P=b5W0A0 zr#09?B_(c92b%r=^@|DUKrf=rpvZwSF+u-l&NTjijEf8Sf8m1o|8L(|{~tRh z^Z(Q->Hj}}G6UJNa7PkyU`2(?|99^={=a_B_5aWz`Tyt6N&f%+n+ePYWiXiia0mYT z#|V~OvPAd)vSkPi;_um`1a@FU10;% z0_W^ow}k#bc)NklAvO5!ywixM(S~T~-LRE{t|kQ%e&~2r*_MIK>g1P%%bVD!5TM=1*{ul?%b8;zGog z;6@iB4ht2<0q3vpJFhoQ@>-#E(?u7D^X@(Ge)spz@Jue33!R-GaCMc0{rwDVZr(T8 z?(Q7`CWJyZ@coUA2biCKit=(N{COJi8ymAqjGD^U)-tiQ^d!1ADL?{@jJ$=%Q);k*fjklH>$Ab@bz*NX11BdI zY;E1c_O@<(K0i;!%E}`V9~}HOU`@>z1cUja*V9uF1yO)=bJ!^63T z13VZE+C-2U^mxi}aFCAK*=+Ro=F4Tgkk;K@fR>ieqVc;760oc5{SUo7!+i@2Szs33 zZau3BfbR?k2*}LoCDsL(mmhJn5{mD~y#2-s0s`4wkpDTqW~#c5PjIJ7oP z$IQ&L=>8u8_V??6%npGgk(USricwc*$HYXAc4;!g;hZSo%}sm{US68f&`>P#si_xQ z-P0(V-8?Z6)Rqg+f0|5)Qx7N}Yz|V+)RsQZ=87$;sE! z0?a6TkVDbhst*Z4n6>(P9nj}HPES*Cc$lVn3wCzyB9TEd$i{DOE(BAo$`$J= z#yEZKK1-$gPnA~1sl}{#y`^%-@BgN)i(~-RJM;wR5OQGnM~IE_SpX9^|E2YRz`w1= YcPMDeo5!x2y8r+H07*qoM6N<$fz>% literal 2288 zcmZ`*c{CK<8y-y4SjG|tW3m;dEJcdX7DD!99c16jSh6$*jV)V9$X*zqEM*yMvM*Uj z#*)t#i6*j#k!^hA_v`fEch7n5^PKlS&wK9u<2~ob8XDY&Fu)i9002Z=OWladXet!J zwA5W_X!(RnG`>c+)d1xm;mfC|&PZ(+eSN@Xss;mqG%&!KQwtT!G`xSbCJh2W_rE%| zPqaG#_}gPZ<Z(hbvWle2$RTDJLYKE?sl`2QnXgVrELPDYj zETL7%vW9y$W~AnUAF;Hm6@gs`@k^;C3E%`SONMLS_2CqBVe`He_ui1zda2_H@0sdz zGfsj+TVZj)$UOX6Po5saOtzGutk8u=J`az+=U`jlq*-1Ksgsl^p=xQ8P1;gtB}Q4j ziSQadpwsow=qx59QX2pT3SlS=81BVw9FGWIBH?b>s4HAjC#3${IEeh{Mkdl$)3D@yfRF6K=YoprA*h zqP7-gt=`Q@IF<;C@N^Nm&iXE~bs=f1yStf87RRH&fv2(jtl{F9*}8uzGdnW1gccSq zW@QDtd-;H@t*tBHA7pgt47U00?O~r!uuvZesi{O#(9J9*H;Vh#P|n7qEc9MmK$P^i z&YY-UvyR0DT>i97@j!faA+Uai^C%yZ*0u9AF46&AQco29We5o zaN0@M_Tk~dv@{PXY8wc`r($JgF91k^R->&w&r%|msha^hHzP;wKZ%;XtfN7W1VE6&-r*qRMfSg z`Q0yZb%9`~FmAI2zfy0<&=C))5_TTWvTxSiGiTVW9mWU4e18}c|0OE}oe)$=!PGZo zM#@-O>3HkyMkgB^UwO_K-rb$MWQzbZvZan>B`4z|BBqOn=k3G$OhSJc({bA$0V1k0)KFzyM1WKLO909K$ei3feqWe#C3 z)f&7>9jud++vX@c005YNI#FvG?ph{yIeG?b#&Cl|&viW7^9W?6X$di1t1;-?$`=eD z94s??J;GuHk`aF!UK+UrRn>$^e&N!%7N>WV2V}d1(n~>FoN&MMReaQ>zx{fTVRln_ z#&@n^?`X(ty)k+8nk~V8WODh)Q_?2qgN(oH`V^;Vvg-Wiyf7i9d)H3gYvx@d8M`*cG2YriAjH2$ zjgF*UbLCt@O@Ga-&n68QJasn#VfNNdch;usRXr)`zZTp(eZP~Wm0+qHpBt0OSQ0^z zngv=+kr(^U5GDac?pTwQo^Nl=i>jPt!d(^?df6x+D!s>MT0$WDiz9`ZI}-h%Q01Vy z0TbQbur4`=r7Dfg&ns|0fg41F&>_=vX2WT&b7R^IDU5NG{9km>9vC zJ(<5V?c(y$l>!Rw9*@2+kA2KMx`;1f+aV1;D5+dsbzc|5TXGTWcz6boR&*-2!Uo56 zd*6pBB)k8d7z4w6ZiYN$5EEncwbiztmwV23qas5aY2~P5ZkEVem6ZjO*5}KfQcX{f zrnl!;23{0-|E3Txg_H^2a;Tgka1*&C3D7D-IrLRcGm>gT05PMd_-bG}$%r$J2Lo*M zam1~>kk?2%gR)f`f*p; z3tln1vi8Kr@3+Z-WN5iKy+rSsco_njRqoe7c#v+Y|>gw83Ia-+H{a_&2yhNJt`SVI8hN)5sis-BI zH?e9qHXI<`Tb*|DeQo&#o%M}Y;EK?w%J}#Ua)2b$yNx-1-))k5&cXWUr^OZgI^Uha zhm_{C>s;Sj=g>sMQ0K+tLqNBs{o~^wVspZ=Gfih;=f5H+#-V>3cG!eaRx9%2nG@i{ z%ql_Tw;mqwF26>ZTqWLc( zt1RkXOiH=Nv$tMkFxZ2BB%07##{LgT&U68@aj<(SqX7R|+0U&c_{g~TSG|f@BxR#Q rk0^f}A>zhfV*vDHa7_G9vCZz^`d9c$Xbkn2003xf7^s)4*@gcLpeYRv diff --git a/Tests/UIKitViewTest.swift b/Tests/UIKitViewTest.swift index fac3685..e0f2882 100644 --- a/Tests/UIKitViewTest.swift +++ b/Tests/UIKitViewTest.swift @@ -21,11 +21,13 @@ class UIKitViewTest: XCTestCase { let image = sampleView.renderLayerAsBitmap() XCTAssertEqual(image.size, expectedSize) let pngData = try XCTUnwrap(image.pngData()) - let existing = try Data( - contentsOf: folderUrl().appendingPathComponent("UIKit-sample-view.png") - ) - XCTContext.runActivity(named: "compare image data") { + let url = folderUrl().appendingPathComponent("UIKit-sample-view.png") + let existing = try Data(contentsOf: url) + try XCTContext.runActivity(named: "compare image data") { $0.add(.init(data: pngData, uniformTypeIdentifier: UTType.png.identifier)) + if existing != pngData { + try pngData.write(to: url) + } XCTAssertEqual(existing, pngData) } }