Skip to content

Commit 5810c3d

Browse files
committed
feat: add tests
1 parent 42c5806 commit 5810c3d

File tree

5 files changed

+218
-3
lines changed

5 files changed

+218
-3
lines changed

Sources/ImageSerializationPlugin/ImageSerializationPlugin.swift

+3
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ public protocol ImageSerialization {
5555
/// The `ImageSerializationFormat` enum is used to represent various image formats. It includes a predefined case for PNG images and a flexible case for plugins,
5656
/// allowing for the extension of formats via plugins identified by unique string values.
5757
public enum ImageSerializationFormat: RawRepresentable, Sendable, Equatable {
58+
59+
public static let defaultValue: ImageSerializationFormat = .png
60+
5861
/// Represents the default image format aka PNG.
5962
case png
6063

Sources/SnapshotTesting/Plugins/ImageSerializer.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public class ImageSerializer {
3232
/// - image: The `SnapImage` to encode.
3333
/// - imageFormat: The format in which to encode the image.
3434
/// - Returns: The encoded image data, or `nil` if encoding fails.
35-
public func encodeImage(_ image: SnapImage, imageFormat: ImageSerializationFormat) /*async throws*/ -> Data? {
35+
public func encodeImage(_ image: SnapImage, imageFormat: ImageSerializationFormat = .defaultValue) -> Data? {
3636
for plugin in self.plugins {
3737
if type(of: plugin).imageFormat == imageFormat {
3838
return plugin.encodeImage(image)
@@ -52,7 +52,7 @@ public class ImageSerializer {
5252
/// - data: The image data to decode.
5353
/// - imageFormat: The format in which the image data is encoded.
5454
/// - Returns: The decoded `SnapImage`, or `nil` if decoding fails.
55-
public func decodeImage(_ data: Data, imageFormat: ImageSerializationFormat) /*async throws*/ -> SnapImage? {
55+
public func decodeImage(_ data: Data, imageFormat: ImageSerializationFormat = .defaultValue) -> SnapImage? {
5656
for plugin in self.plugins {
5757
if type(of: plugin).imageFormat == imageFormat {
5858
return plugin.decodeImage(data)

Sources/SnapshotTesting/Plugins/PluginRegistry.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public class PluginRegistry {
2828
/// Registers a plugin.
2929
///
3030
/// - Parameter plugin: An instance conforming to `SnapshotTestingPlugin`.
31-
public static func registerPlugin(_ plugin: SnapshotTestingPlugin) {
31+
public static func registerPlugin(_ plugin: any SnapshotTestingPlugin) {
3232
PluginRegistry.shared.registerPlugin(plugin)
3333
}
3434

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import XCTest
2+
import SnapshotTestingPlugin
3+
@testable import SnapshotTesting
4+
import ImageSerializationPlugin
5+
6+
#if canImport(UIKit)
7+
import UIKit
8+
#elseif canImport(AppKit)
9+
import AppKit
10+
#endif
11+
12+
class MockImageSerializationPlugin: ImageSerializationPlugin {
13+
14+
static var imageFormat: ImageSerializationFormat = .plugins("mock")
15+
16+
func encodeImage(_ image: SnapImage) -> Data? {
17+
return "mockImageData".data(using: .utf8)
18+
}
19+
20+
func decodeImage(_ data: Data) -> SnapImage? {
21+
let mockImage = SnapImage()
22+
return mockImage
23+
}
24+
25+
// MARK: - SnapshotTestingPlugin
26+
static var identifier: String = "ImageSerializationPlugin.MockImageSerializationPlugin.mock"
27+
required init() {}
28+
}
29+
30+
class ImageSerializerTests: XCTestCase {
31+
32+
var imageSerializer: ImageSerializer!
33+
// #E48900FF
34+
var _1pxOrangePNGImage = Data(base64Encoded: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAAaADAAQAAAABAAAAAQAAAAD5Ip3+AAAADUlEQVQIHWN40snwHwAGLwJteELaggAAAABJRU5ErkJggg==")!
35+
36+
override func setUp() {
37+
super.setUp()
38+
PluginRegistry.reset() // Reset state before each test
39+
40+
// Register the mock plugins in the PluginRegistry
41+
PluginRegistry.registerPlugin(MockImageSerializationPlugin() as SnapshotTestingPlugin)
42+
43+
imageSerializer = ImageSerializer()
44+
}
45+
46+
override func tearDown() {
47+
imageSerializer = nil
48+
PluginRegistry.reset() // Reset state after each test
49+
super.tearDown()
50+
}
51+
52+
func testEncodeImageUsingMockPlugin() {
53+
let mockImage = SnapImage()
54+
let imageData = imageSerializer.encodeImage(
55+
mockImage,
56+
imageFormat: MockImageSerializationPlugin.imageFormat
57+
)
58+
59+
XCTAssertNotNil(imageData, "Image data should not be nil for mock plugin.")
60+
XCTAssertEqual(String(data: imageData!, encoding: .utf8), "mockImageData")
61+
}
62+
63+
func testDecodeImageUsingMockPlugin() {
64+
let mockData = "mockImageData".data(using: .utf8)!
65+
let decodedImage = imageSerializer.decodeImage(
66+
mockData,
67+
imageFormat: MockImageSerializationPlugin.imageFormat
68+
)
69+
70+
XCTAssertNotNil(decodedImage, "Image should be decoded using the mock plugin.")
71+
}
72+
73+
// TODO: 1PX png image data
74+
func testEncodeImageAsPNG() {
75+
let mockImage = SnapImage()
76+
let imageData = imageSerializer.encodeImage(
77+
mockImage,
78+
imageFormat: .png
79+
)
80+
81+
XCTAssertNil(imageData, "The image is empty it should be nil.")
82+
}
83+
84+
func testDecodeImageAsPNG() {
85+
let decodedImage = imageSerializer.decodeImage(
86+
_1pxOrangePNGImage,
87+
imageFormat: .png
88+
)
89+
90+
XCTAssertNotNil(decodedImage, "PNG image should be decoded successfully.")
91+
XCTAssertEqual(
92+
decodedImage?.size.width,
93+
1, "PNG image should be 1x1."
94+
)
95+
XCTAssertEqual(
96+
decodedImage?.size.height,
97+
1, "PNG image should be 1x1."
98+
)
99+
XCTAssertEqual(getFirstPixelColorHex(from: decodedImage!), "#E48900FF")
100+
}
101+
102+
func testUnknownImageFormatFallsBackToPNG() {
103+
let mockImage = SnapImage(data: _1pxOrangePNGImage)!
104+
let imageData = imageSerializer.encodeImage(
105+
mockImage,
106+
imageFormat: .plugins("unknownFormat")
107+
)
108+
109+
XCTAssertNotNil(imageData, "Unknown format should fall back to PNG encoding.")
110+
}
111+
112+
func testPluginRegistryShouldContainRegisteredPlugins() {
113+
let plugins = PluginRegistry.allPlugins() as [ImageSerialization]
114+
115+
XCTAssertEqual(plugins.count, 1, "There should be two registered plugins.")
116+
XCTAssertEqual(type(of: plugins[0]).imageFormat.rawValue, "mock", "The first plugin should support the 'mock' format.")
117+
}
118+
}

Tests/SnapshotTestingTests/TestHelpers.swift

+94
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,97 @@ import XCTest
108108
}
109109
}
110110
#endif
111+
112+
#if canImport(UIKit)
113+
import UIKit
114+
115+
func _getFirstPixelColorHex(from image: UIImage) -> String? {
116+
guard let cgImage = image.cgImage else { return nil }
117+
118+
let pixelData = calloc(1, 4) // 4 bytes for RGBA
119+
let colorSpace = CGColorSpaceCreateDeviceRGB()
120+
let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue
121+
122+
guard let context = CGContext(
123+
data: pixelData,
124+
width: 1,
125+
height: 1,
126+
bitsPerComponent: 8,
127+
bytesPerRow: 4,
128+
space: colorSpace,
129+
bitmapInfo: bitmapInfo
130+
) else {
131+
free(pixelData)
132+
return nil
133+
}
134+
135+
// Draw the image in the 1x1 context to get the first pixel's color
136+
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: 1, height: 1))
137+
138+
// Get the color components
139+
let data = pixelData!.assumingMemoryBound(to: UInt8.self)
140+
let r = data[0]
141+
let g = data[1]
142+
let b = data[2]
143+
let a = data[3]
144+
145+
free(pixelData)
146+
147+
// Return the hex string
148+
return String(format: "#%02X%02X%02X%02X", r, g, b, a)
149+
}
150+
#endif
151+
152+
#if canImport(AppKit)
153+
import AppKit
154+
155+
func _getFirstPixelColorHex(from image: NSImage) -> String? {
156+
guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return nil }
157+
158+
let pixelData = calloc(1, 4) // 4 bytes for RGBA
159+
let colorSpace = CGColorSpaceCreateDeviceRGB()
160+
let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue
161+
162+
guard let context = CGContext(
163+
data: pixelData,
164+
width: 1,
165+
height: 1,
166+
bitsPerComponent: 8,
167+
bytesPerRow: 4,
168+
space: colorSpace,
169+
bitmapInfo: bitmapInfo
170+
) else {
171+
free(pixelData)
172+
return nil
173+
}
174+
175+
// Draw the image in the 1x1 context to get the first pixel's color
176+
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: 1, height: 1))
177+
178+
// Get the color components
179+
let data = pixelData!.assumingMemoryBound(to: UInt8.self)
180+
let r = data[0]
181+
let g = data[1]
182+
let b = data[2]
183+
let a = data[3]
184+
185+
free(pixelData)
186+
187+
// Return the hex string
188+
return String(format: "#%02X%02X%02X%02X", r, g, b, a)
189+
}
190+
#endif
191+
192+
#if canImport(UIKit)
193+
typealias SnapImage = UIImage
194+
#elseif canImport(AppKit)
195+
typealias SnapImage = NSImage
196+
#endif
197+
198+
func getFirstPixelColorHex(from image: SnapImage) -> String? {
199+
#if canImport(UIKit)
200+
return _getFirstPixelColorHex(from: image)
201+
#elseif canImport(AppKit)
202+
return _getFirstPixelColorHex(from: image)
203+
#endif
204+
}

0 commit comments

Comments
 (0)