diff --git a/Binaries/PrefireBinary.artifactbundle/prefire-5.2.0-macos/bin/prefire b/Binaries/PrefireBinary.artifactbundle/prefire-5.2.0-macos/bin/prefire index 6831f45..ece3ed1 100755 Binary files a/Binaries/PrefireBinary.artifactbundle/prefire-5.2.0-macos/bin/prefire and b/Binaries/PrefireBinary.artifactbundle/prefire-5.2.0-macos/bin/prefire differ diff --git a/Example/.prefire.yml b/Example/.prefire.yml index 37de1d7..7e1f954 100644 --- a/Example/.prefire.yml +++ b/Example/.prefire.yml @@ -9,6 +9,7 @@ test_configuration: - UIKit - Foundation - use_grouped_snapshots: false + - record_in_dark_mode: true playbook_configuration: - imports: diff --git a/Example/Shared/Assets.xcassets/gradientEnd.colorset/Contents.json b/Example/Shared/Assets.xcassets/gradientEnd.colorset/Contents.json new file mode 100644 index 0000000..ae9abed --- /dev/null +++ b/Example/Shared/Assets.xcassets/gradientEnd.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x82", + "green" : "0x00", + "red" : "0x4B" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x80", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Shared/Assets.xcassets/gradientStart.colorset/Contents.json b/Example/Shared/Assets.xcassets/gradientStart.colorset/Contents.json new file mode 100644 index 0000000..79ea4b2 --- /dev/null +++ b/Example/Shared/Assets.xcassets/gradientStart.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x80", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x82", + "green" : "0x00", + "red" : "0x4B" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Shared/Examples/AuthView.swift b/Example/Shared/Examples/AuthView.swift index 3582fb4..9be2ddd 100644 --- a/Example/Shared/Examples/AuthView.swift +++ b/Example/Shared/Examples/AuthView.swift @@ -58,6 +58,9 @@ struct AuthView_Preview: PreviewProvider, PrefireProvider { Group { AuthView(login: "", password: "") + AuthView(login: "", password: "") + .preferredColorScheme(.dark) + AuthView(login: "FireUser", password: "FirePassword") } .previewUserStory(.auth) diff --git a/Example/Shared/Examples/TestView.swift b/Example/Shared/Examples/TestView.swift index 420ace7..62aef07 100644 --- a/Example/Shared/Examples/TestView.swift +++ b/Example/Shared/Examples/TestView.swift @@ -8,7 +8,10 @@ struct TestView: View { var body: some View { VStack { - LinearGradient(gradient: Gradient(colors: [.green, .indigo]), startPoint: .top, endPoint: .bottom) + LinearGradient(gradient: Gradient( + colors: [Color("gradientStart", bundle: .module), + Color("gradientEnd", bundle: .module)]), + startPoint: .top, endPoint: .bottom) .frame(height: 200) CircleImage() @@ -74,15 +77,17 @@ struct TestViewWithoutState_Previews: PreviewProvider, PrefireProvider { struct GreenButton_Previews: PreviewProvider, PrefireProvider { static var previews: some View { + Button("Apply", action: {}) - .foregroundColor(.black) + .foregroundColor(.dynamicForegroundColor) .font(.title) .padding() .frame(maxWidth: .infinity) .frame(height: 68) - .background(Capsule().foregroundColor(.green.opacity(0.2))) + .background(Capsule().foregroundColor(.dynamicGreen)) .previewLayout(.sizeThatFits) .previewUserStory("Buttons") + } } @@ -92,3 +97,33 @@ struct GreenButton_Previews: PreviewProvider, PrefireProvider { .snapshot(delay: 0.1, precision: 0.9) .previewUserStory(.auth) } + +extension Color { + static var dynamicForegroundColor: Color { + Color(UIColor.dynamicForegroundColor) + } + + static var dynamicGreen: Color { + Color(UIColor.dynamicGreen) + } +} + +extension UIColor { + static var dynamicForegroundColor: UIColor { + UIColor(dynamicProvider: { trait in + switch trait.userInterfaceStyle { + case .dark: return .white + default: return .black + } + }) + } + + static var dynamicGreen: UIColor { + UIColor(dynamicProvider: { trait in + switch trait.userInterfaceStyle { + case .dark: return UIColor.green.withAlphaComponent(0.8) + default: return UIColor.green.withAlphaComponent(0.2) + } + }) + } +} diff --git a/Example/Shared/Examples/UIKitView.swift b/Example/Shared/Examples/UIKitView.swift index 75b1026..7d17c67 100644 --- a/Example/Shared/Examples/UIKitView.swift +++ b/Example/Shared/Examples/UIKitView.swift @@ -3,12 +3,12 @@ import Prefire #Preview { let view = UIView(frame: .init(origin: .zero, size: .init(width: 100, height: 100))) - view.backgroundColor = .red + view.backgroundColor = UIColor.dynamicGreen return view } #Preview { let viewController = UIViewController() - viewController.view.backgroundColor = .green + viewController.view.backgroundColor = .dynamicForegroundColor return viewController } diff --git a/PrefireExecutable/Sources/PrefireCore/PrefireGenerator.swift b/PrefireExecutable/Sources/PrefireCore/PrefireGenerator.swift index 5eb8739..023a846 100644 --- a/PrefireExecutable/Sources/PrefireCore/PrefireGenerator.swift +++ b/PrefireExecutable/Sources/PrefireCore/PrefireGenerator.swift @@ -15,7 +15,8 @@ public enum PrefireGenerator { inlineTemplate: String, defaultEnabled: Bool, cacheDir: Path? = nil, - useGroupedSnapshots: Bool + useGroupedSnapshots: Bool, + recordInDarkMode: Bool ) async throws { startTime = Date() diff --git a/PrefireExecutable/Sources/PrefireCore/Templates/PreviewTestsTemplate.swift b/PrefireExecutable/Sources/PrefireCore/Templates/PreviewTestsTemplate.swift index 166595e..1b83b89 100644 --- a/PrefireExecutable/Sources/PrefireCore/Templates/PreviewTestsTemplate.swift +++ b/PrefireExecutable/Sources/PrefireCore/Templates/PreviewTestsTemplate.swift @@ -51,6 +51,12 @@ import SnapshotTesting if let failure = assertSnapshots(for: PrefireSnapshot(preview, device: preview.device?.snapshotDevice() ?? deviceConfig)) { XCTFail(failure) } + + {% if argument.recordInDarkMode == "true" %} + if let failure = assertSnapshots(for: PrefireSnapshot(preview, device: preview.device?.snapshotDevice() ?? deviceConfig, isDark: true)) { + XCTFail(failure) + } + {% endif %} } } {%- if not forloop.last %} @@ -83,6 +89,13 @@ import SnapshotTesting if let failure = assertSnapshots(for: PrefireSnapshot(preview(), name: "{{ macroModel.displayName }}", isScreen: isScreen, device: deviceConfig)) { XCTFail(failure) } + + {% if argument.recordInDarkMode == "true" %} + if let failure = assertSnapshots(for: PrefireSnapshot(preview(), name: "{{ macroModel.displayName }}", isScreen: isScreen, device: deviceConfig, isDark: true)) { + XCTFail(failure) + } + {% endif %} + } {%- if not forloop.last %} diff --git a/PrefireExecutable/Sources/prefire/Commands/Playbook/GeneratePlaybookCommand.swift b/PrefireExecutable/Sources/prefire/Commands/Playbook/GeneratePlaybookCommand.swift index 48cfe03..fe7ca27 100644 --- a/PrefireExecutable/Sources/prefire/Commands/Playbook/GeneratePlaybookCommand.swift +++ b/PrefireExecutable/Sources/prefire/Commands/Playbook/GeneratePlaybookCommand.swift @@ -53,7 +53,8 @@ enum GeneratePlaybookCommand { inlineTemplate: try options.template?.read(.utf8) ?? EmbeddedTemplates.previewModels, defaultEnabled: options.previewDefaultEnabled, cacheDir: options.cacheBasePath, - useGroupedSnapshots: true // Playbooks are always grouped + useGroupedSnapshots: true, // Playbooks are always grouped + recordInDarkMode: false ) } diff --git a/PrefireExecutable/Sources/prefire/Commands/Tests/GenerateTestsCommand.swift b/PrefireExecutable/Sources/prefire/Commands/Tests/GenerateTestsCommand.swift index 8a9f5ba..b7d83ae 100644 --- a/PrefireExecutable/Sources/prefire/Commands/Tests/GenerateTestsCommand.swift +++ b/PrefireExecutable/Sources/prefire/Commands/Tests/GenerateTestsCommand.swift @@ -22,6 +22,7 @@ struct GeneratedTestsOptions { var imports: [String]? var testableImports: [String]? var useGroupedSnapshots: Bool + var recordInDarkMode: Bool init( target: String?, @@ -57,6 +58,7 @@ struct GeneratedTestsOptions { snapshotDevices = config?.tests.snapshotDevices imports = config?.tests.imports testableImports = config?.tests.testableImports + self.recordInDarkMode = config?.tests.recordInDarkMode ?? false } } @@ -77,6 +79,7 @@ enum GenerateTestsCommand { static let testableImports = "testableImports" static let previewsMacros = "previewsMacros" static let previewsMacrosDict = "previewsMacrosDict" + static let recordInDarkMode = "recordInDarkMode" } static func run(_ options: GeneratedTestsOptions) async throws { @@ -88,7 +91,8 @@ enum GenerateTestsCommand { inlineTemplate: try options.template?.read(.utf8) ?? EmbeddedTemplates.previewTests, defaultEnabled: options.prefireEnabledMarker, cacheDir: options.cacheBasePath, - useGroupedSnapshots: options.useGroupedSnapshots + useGroupedSnapshots: options.useGroupedSnapshots, + recordInDarkMode: options.recordInDarkMode ) } @@ -115,6 +119,7 @@ enum GenerateTestsCommand { Keys.testableImports: options.testableImports as? NSArray, Keys.mainTarget: options.target as? NSString, Keys.file: snapshotOutput?.string as? NSString, + Keys.recordInDarkMode: "\(options.recordInDarkMode)" as NSString, ].filter({ $0.value != nil }) as? [String: NSObject] ?? [:] } } diff --git a/PrefireExecutable/Sources/prefire/Config/Config.swift b/PrefireExecutable/Sources/prefire/Config/Config.swift index a5a7744..7cd4b93 100644 --- a/PrefireExecutable/Sources/prefire/Config/Config.swift +++ b/PrefireExecutable/Sources/prefire/Config/Config.swift @@ -24,6 +24,7 @@ struct TestsConfig { var imports: [String]? var testableImports: [String]? var useGroupedSnapshots: Bool? + var recordInDarkMode: Bool? enum CodingKeys: String, CodingKey { case target = "target" @@ -38,6 +39,7 @@ struct TestsConfig { case imports = "imports" case testableImports = "testable_imports" case useGroupedSnapshots = "use_grouped_snapshots" + case recordInDarkMode = "record_in_dark_mode" } } diff --git a/PrefireExecutable/Sources/prefire/Config/ConfigDecoder.swift b/PrefireExecutable/Sources/prefire/Config/ConfigDecoder.swift index bb9bafe..cedb7a9 100644 --- a/PrefireExecutable/Sources/prefire/Config/ConfigDecoder.swift +++ b/PrefireExecutable/Sources/prefire/Config/ConfigDecoder.swift @@ -72,6 +72,8 @@ final class ConfigDecoder { config.tests.testTargetPath = getValue(from: components.last, env: env) case .useGroupedSnapshots: config.tests.useGroupedSnapshots = getValue(from: components.last, env: env) == "true" + case .recordInDarkMode: + config.tests.recordInDarkMode = getValue(from: components.last, env: env) == "true" } } diff --git a/PrefireExecutable/Tests/PrefireTests/PrefireGeneratorTests.swift b/PrefireExecutable/Tests/PrefireTests/PrefireGeneratorTests.swift index d4e0ec1..a7a2d19 100644 --- a/PrefireExecutable/Tests/PrefireTests/PrefireGeneratorTests.swift +++ b/PrefireExecutable/Tests/PrefireTests/PrefireGeneratorTests.swift @@ -17,7 +17,8 @@ final class PrefireGeneratorTests: XCTestCase { arguments: args, inlineTemplate: template, defaultEnabled: true, - useGroupedSnapshots: true + useGroupedSnapshots: true, + recordInDarkMode: false ) } catch { XCTFail("Unexpected error thrown: \(error)") @@ -39,7 +40,8 @@ final class PrefireGeneratorTests: XCTestCase { inlineTemplate: template, defaultEnabled: true, cacheDir: cache, - useGroupedSnapshots: true + useGroupedSnapshots: true, + recordInDarkMode: false ) let result = try output.read(.utf8) @@ -63,7 +65,8 @@ final class PrefireGeneratorTests: XCTestCase { inlineTemplate: template, defaultEnabled: true, cacheDir: cache, - useGroupedSnapshots: false // Test ungrouped generation + useGroupedSnapshots: false, // Test ungrouped generation + recordInDarkMode: false ) // Should generate file with name based on the fixture file @@ -90,7 +93,8 @@ final class PrefireGeneratorTests: XCTestCase { inlineTemplate: template, defaultEnabled: true, cacheDir: cache, - useGroupedSnapshots: true // Test grouped generation + useGroupedSnapshots: true, // Test grouped generation + recordInDarkMode: false ) // Should generate file with Preview as class name for grouped snapshots @@ -116,7 +120,8 @@ final class PrefireGeneratorTests: XCTestCase { inlineTemplate: template, defaultEnabled: true, cacheDir: cache, - useGroupedSnapshots: false // Test ungrouped generation + useGroupedSnapshots: false, // Test ungrouped generation + recordInDarkMode: false ) // Should generate file with source file name as class name for ungrouped snapshots diff --git a/Sources/Prefire/Preview/PrefireSnapshot.swift b/Sources/Prefire/Preview/PrefireSnapshot.swift index 3d85646..95185c0 100644 --- a/Sources/Prefire/Preview/PrefireSnapshot.swift +++ b/Sources/Prefire/Preview/PrefireSnapshot.swift @@ -17,6 +17,7 @@ public struct DeviceConfig { private var previewContent: Content public var name: String public var isScreen: Bool + public var isDark: Bool public var device: DeviceConfig public var traits: UITraitCollection = .init() @@ -32,35 +33,39 @@ public struct DeviceConfig { } } - public init(_ preview: _Preview, testName: String = #function, device: DeviceConfig) where Content == AnyView { + public init(_ preview: _Preview, testName: String = #function, device: DeviceConfig, isDark: Bool = false) where Content == AnyView { previewContent = preview.content name = preview.displayName ?? testName isScreen = preview.layout == .device self.device = device + self.isDark = isDark } - public init(_ view: Content, name: String, isScreen: Bool, device: DeviceConfig, traits: UITraitCollection = .init()) { + public init(_ view: Content, name: String, isScreen: Bool, device: DeviceConfig, traits: UITraitCollection = .init(), isDark: Bool = false) { previewContent = view self.name = name self.isScreen = isScreen self.device = device self.traits = traits + self.isDark = isDark } - public init(_ view: UIView, name: String, isScreen: Bool, device: DeviceConfig, traits: UITraitCollection = .init()) where Content == ViewRepresentable { + public init(_ view: UIView, name: String, isScreen: Bool, device: DeviceConfig, traits: UITraitCollection = .init(), isDark: Bool = false) where Content == ViewRepresentable { previewContent = ViewRepresentable(view: view) self.name = name self.isScreen = isScreen self.device = device self.traits = traits + self.isDark = isDark } - public init(_ viewController: UIViewController, name: String, isScreen: Bool, device: DeviceConfig, traits: UITraitCollection = .init()) where Content == ViewControllerRepresentable { + public init(_ viewController: UIViewController, name: String, isScreen: Bool, device: DeviceConfig, traits: UITraitCollection = .init(), isDark: Bool = false) where Content == ViewControllerRepresentable { previewContent = ViewControllerRepresentable(viewController: viewController) self.name = name self.isScreen = isScreen self.device = device self.traits = traits + self.isDark = isDark } public func loadViewWithPreferences() -> (AnyView, PreferenceKeys) { @@ -80,6 +85,8 @@ public struct DeviceConfig { .onPreferenceChange(RecordPreferenceKey.self) { preferences.record = $0 } + .preferredColorScheme(isDark ? .dark : .light) + .colorScheme(isDark ? .dark : .light) ) // In order to call onPreferenceChange, render the view once @@ -95,6 +102,7 @@ public struct DeviceConfig { let window = UIWindow(frame: .init()) window.isHidden = false + window.overrideUserInterfaceStyle = isDark ? .dark : .light window.rootViewController = hostingController hostingController.view.setNeedsLayout()