diff --git a/.gitattributes b/.gitattributes index b6757eeff..b59f2b141 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ UITests/Sources/__Snapshots__/** filter=lfs diff=lfs merge=lfs -text UnitTests/Resources/** filter=lfs diff=lfs merge=lfs -text +UnitTests/Sources/__Snapshots__/** filter=lfs diff=lfs merge=lfs -text \ No newline at end of file diff --git a/.githooks/pre-push b/.githooks/pre-push index 216e91527..0f0089bc2 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -1,3 +1,3 @@ #!/bin/sh -command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/pre-push'.\n"; exit 2; } +command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'pre-push' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks').\n"; exit 2; } git lfs pre-push "$@" diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 43e72959f..276df1470 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -41,6 +41,16 @@ jobs: - name: Run tests run: bundle exec fastlane unit_tests + + - name: Archive artifacts + uses: actions/upload-artifact@v3 + # We only care about artifcats if the tests fail + if: failure() + with: + name: test-output + path: fastlane/test_output + retention-days: 1 + if-no-files-found: ignore - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 diff --git a/.prefire.yml b/.prefire.yml new file mode 100644 index 000000000..451f66e0b --- /dev/null +++ b/.prefire.yml @@ -0,0 +1,5 @@ +test_configuration: + - test_file_path: UnitTests/Sources/PreviewTests.swift + - template_file_path: Tools/Prefire/PreviewTests.stencil + - simulator_device: "iPhone14" + - required_os: 16 diff --git a/.swiftlint.yml b/.swiftlint.yml index b304435f3..6c8d31186 100755 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -47,6 +47,9 @@ cyclomatic_complexity: nesting: type_level: warning: 5 + +type_name: + allowed_symbols: "_" custom_rules: print_deprecation: diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 8142dcbea..3e3daeb7a 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -211,6 +211,7 @@ 43F35A7E5703D64DB0519C59 /* ServerSelectionScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD469F7513574341181F7EAA /* ServerSelectionScreen.swift */; }; 440123E29E2F9B001A775BBE /* TimelineItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D505843AB66822EB91F0DF0 /* TimelineItemProxy.swift */; }; 44121202B4A260C98BF615A7 /* RoomMembersListScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5B7A755E985FA14469E86B2 /* RoomMembersListScreenUITests.swift */; }; + 44F0E1B576C7599DF8022071 /* Prefire in Frameworks */ = {isa = PBXBuildFile; productRef = 2629CF48B33643CD5F69C612 /* Prefire */; }; 4557192F5B15A8D9BB920232 /* AdvancedSettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E492690C8B27A892C194CC4 /* AdvancedSettingsScreenCoordinator.swift */; }; 46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */; }; 46A261AA898344A1F3C406B1 /* ReportContentScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCE3636E3D01477C8B2E9D0 /* ReportContentScreenModels.swift */; }; @@ -299,6 +300,7 @@ 62910B515BCB4B455E24D7C1 /* AdvancedSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D086854995173E897F993C26 /* AdvancedSettingsScreenViewModelProtocol.swift */; }; 6298AB0906DDD3525CD78C6B /* KZFileWatchers in Frameworks */ = {isa = PBXBuildFile; productRef = 81DB3AB6CE996AB3954F4F03 /* KZFileWatchers */; }; 63E46D18B91D08E15FC04125 /* ExpiringTaskRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B25F959A434BB9923A3223F /* ExpiringTaskRunner.swift */; }; + 642DF13C49ED4121C148230E /* TestablePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1E227F34BE43B08E098796E /* TestablePreview.swift */; }; 6448F8D1D3CA4CD27BB4CADD /* RoomMemberProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F36C5D9B37E50915ECBD3EE /* RoomMemberProxy.swift */; }; 644AA5001BCC58D7732EB772 /* MigrationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12EDAFB64FA5F6812D54F39A /* MigrationScreenViewModel.swift */; }; 64C373ACCFA26D42BA45CFAD /* HomeScreenInvitesButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24227FF9A2797F6EA7F69CDD /* HomeScreenInvitesButton.swift */; }; @@ -393,6 +395,7 @@ 7F64FA937B95924B3A44EC12 /* OnboardingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB8E75B9CB6C78BE8D09B1AF /* OnboardingScreen.swift */; }; 7F7EA51A9A43125A8CB6AC90 /* NotificationSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D560DDA3B20C82766ACFAD /* NotificationSettingsScreenViewModel.swift */; }; 7FB0BDE26838F1A92782D5E1 /* MediaUploadPreviewScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39B6C8690AEA1E49FF1BAF95 /* MediaUploadPreviewScreenUITests.swift */; }; + 7FF27DA70D833CFC5724EFC5 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 1CAB56FF1DE17B3E871A0BA2 /* SnapshotTesting */; }; 8024BE37156FF0A95A7A3465 /* AnalyticsPromptUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF11DD57D9FACF2A757AB024 /* AnalyticsPromptUITests.swift */; }; 804C15D8ADE0EA7A5268F58A /* OverridableAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648DD1C10E4957CB791FE0B8 /* OverridableAvatarImage.swift */; }; 80D00A7C62AAB44F54725C43 /* PermalinkBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F754E66A8970963B15B2A41E /* PermalinkBuilder.swift */; }; @@ -489,6 +492,7 @@ 9BEA56957B3AF954E7321658 /* ComposerToolbarViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E44928D844E16EE48A311FCA /* ComposerToolbarViewModelProtocol.swift */; }; 9C45CE85325CD591DADBC4CA /* ElementXTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEAC3AC691CBB84983E275 /* ElementXTests.swift */; }; 9C5A07E7C33F3F40287D7861 /* SettingsScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EC57A32ABC80D774CC663DB /* SettingsScreenUITests.swift */; }; + 9C7895941669EA7976A18D88 /* PreviewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A01505B6371171413C3C4BD /* PreviewTests.swift */; }; 9CCC77C31CB399661A034739 /* UserProperties+Element.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A6C4BE591FE5C38CE9C7EF3 /* UserProperties+Element.swift */; }; 9D2E03DB175A6AB14589076D /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = 290FDEDA4D764B9F7EBE55A9 /* Algorithms */; }; 9D79B94493FB32249F7E472F /* PlaceholderAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */; }; @@ -1093,6 +1097,7 @@ 49E45C3DC740D3AB9A47FD32 /* SwipeToReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToReplyView.swift; sourceTree = ""; }; 49E6066092ED45E36BB306F7 /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/Localizable.stringsdict"; sourceTree = ""; }; 49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotificationRequest.swift; sourceTree = ""; }; + 4A01505B6371171413C3C4BD /* PreviewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewTests.swift; sourceTree = ""; }; 4A4AD793D50748F8997E5B15 /* TimelineItemMacContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMacContextMenu.swift; sourceTree = ""; }; 4AB7D7DAAAF662DED9D02379 /* MockMediaLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaLoader.swift; sourceTree = ""; }; 4ADC55DFF46083BC957E0019 /* CreatePollScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePollScreenModels.swift; sourceTree = ""; }; @@ -1383,6 +1388,7 @@ B0A307A44F952CD73E63AE31 /* RoomEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomEventStringBuilder.swift; sourceTree = ""; }; B16048D30F0438731C41F775 /* StateRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateRoomTimelineItem.swift; sourceTree = ""; }; B16CAF20C9AC874A210E2DCF /* SessionVerificationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenViewModelProtocol.swift; sourceTree = ""; }; + B1E227F34BE43B08E098796E /* TestablePreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestablePreview.swift; sourceTree = ""; }; B251F5B4511D1CA0BA8361FE /* CoordinatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoordinatorProtocol.swift; sourceTree = ""; }; B2B5EDCD05D50BA9B815C66C /* ImageRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomTimelineItemContent.swift; sourceTree = ""; }; B2E7C987AE5DC9087BB19F7D /* MediaUploadPreviewScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenModels.swift; sourceTree = ""; }; @@ -1644,6 +1650,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A7A4BAD642A61DCC41621311 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7FF27DA70D833CFC5724EFC5 /* SnapshotTesting in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; BF59B36A7B2DB184B62826F6 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -1704,6 +1718,7 @@ 36AD4DD4C798E22584ED3200 /* Version in Frameworks */, 36CD6E11B37396E14F032CB6 /* Emojibase in Frameworks */, A0D7E5BD0298A97DCBDCE40B /* WysiwygComposer in Frameworks */, + 44F0E1B576C7599DF8022071 /* Prefire in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2675,6 +2690,7 @@ 514363244AE7D68080D44C6F /* NotificationSettingsScreenViewModelTests.swift */, D53D6BB7E8E5EC031281872C /* OnboardingScreenViewModelTests.swift */, 6FB31A32C93D94930B253FBF /* PermalinkBuilderTests.swift */, + 4A01505B6371171413C3C4BD /* PreviewTests.swift */, 086C19086DD16E9B38E25954 /* ReportContentViewModelTests.swift */, 00E5B2CBEF8F96424F095508 /* RoomDetailsEditScreenViewModelTests.swift */, 2EFE1922F39398ABFB36DF3F /* RoomDetailsViewModelTests.swift */, @@ -3495,6 +3511,7 @@ 53482ECA4B6633961EC224F5 /* ScrollViewAdapter.swift */, DBA8DC95C079805B0B56E8A9 /* SharedUserDefaultsKeys.swift */, 4481799F455B3DA243BDA2AC /* ShareToMapsAppActivity.swift */, + B1E227F34BE43B08E098796E /* TestablePreview.swift */, BB3073CCD77D906B330BC1D6 /* Tests.swift */, 1F2529D434C750ED78ADF1ED /* UserAgentBuilder.swift */, 35FA991289149D31F4286747 /* UserPreference.swift */, @@ -3958,13 +3975,18 @@ buildPhases = ( 11F93544B4FC60F78F47D89C /* Sources */, 9B3512762CF4A1D45A79C340 /* Resources */, + A7A4BAD642A61DCC41621311 /* Frameworks */, ); buildRules = ( ); dependencies = ( 0EEC1557A40FBA6DF49D83A2 /* PBXTargetDependency */, + 9A791554EB868FA6F6B95324 /* PBXTargetDependency */, ); name = UnitTests; + packageProductDependencies = ( + 1CAB56FF1DE17B3E871A0BA2 /* SnapshotTesting */, + ); productName = UnitTests; productReference = AAC9344689121887B74877AF /* UnitTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; @@ -4029,6 +4051,7 @@ A05AF81DDD14AD58CB0E1B9B /* Version */, C05729B1684C331F5FFE9232 /* Emojibase */, CA07D57389DACE18AEB6A5E2 /* WysiwygComposer */, + 2629CF48B33643CD5F69C612 /* Prefire */, ); productName = ElementX; productReference = 4CD6AC7546E8D7E5C73CEA48 /* ElementX.app */; @@ -4157,6 +4180,7 @@ 0CBF57301AA172C21F76CE86 /* XCRemoteSwiftPackageReference "maplibre-gl-native-distribution" */, 80B898A3AD2AC63F3ABFC218 /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */, 96495DD8554E2F39D3954354 /* XCRemoteSwiftPackageReference "posthog-ios" */, + 22E7BA2ED466B74739AB8567 /* XCRemoteSwiftPackageReference "Prefire" */, A08925A9D5E3770DEB9D8509 /* XCRemoteSwiftPackageReference "sentry-cocoa" */, E9C4F3A12AA1F65C13A8C8EB /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */, 6582B5AF3F104B0F7E031E7D /* XCRemoteSwiftPackageReference "SwiftState" */, @@ -4440,6 +4464,7 @@ E3AC72E3E58F364EF15C1CC7 /* NotificationSettingsScreenViewModelTests.swift in Sources */, 0C26A1588B17DCDE5F490FE3 /* OnboardingScreenViewModelTests.swift in Sources */, 27E9263DA75E266690A37EB1 /* PermalinkBuilderTests.swift in Sources */, + 9C7895941669EA7976A18D88 /* PreviewTests.swift in Sources */, D415764645491F10344FC6AC /* Publisher.swift in Sources */, D53B80EF02C1062E68659EDD /* ReportContentViewModelTests.swift in Sources */, 9DD84E014ADFB2DD813022D5 /* RoomDetailsEditScreenViewModelTests.swift in Sources */, @@ -4952,6 +4977,7 @@ CCBEC2100CAF2EEBE9DB4156 /* TemplateScreenModels.swift in Sources */, 275EDE8849A2AC1D9309ED7C /* TemplateScreenViewModel.swift in Sources */, 2C4C750D0039AFABDF24236C /* TemplateScreenViewModelProtocol.swift in Sources */, + 642DF13C49ED4121C148230E /* TestablePreview.swift in Sources */, D85D4FA590305180B4A41795 /* Tests.swift in Sources */, 8317E1314C00DCCC99D30DA8 /* TextBasedRoomTimelineItem.swift in Sources */, A2A5AB2E8B3F5CA769E531FA /* TextBasedRoomTimelineViewProtocol.swift in Sources */, @@ -5130,6 +5156,10 @@ target = C0FAEB81CFD9776CD78CE489 /* ElementX */; targetProxy = 6848AF4480814C5F810FB7EB /* PBXContainerItemProxy */; }; + 9A791554EB868FA6F6B95324 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = B717B96C04D7B6A1212D9EDC /* PrefireTestsPlugin */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -5658,6 +5688,14 @@ minimumVersion = 5.13.0; }; }; + 22E7BA2ED466B74739AB8567 /* XCRemoteSwiftPackageReference "Prefire" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/BarredEwe/Prefire"; + requirement = { + kind = upToNextMinorVersion; + minimumVersion = 1.4.1; + }; + }; 395DE6AE429B7ACC7C7FE31D /* XCRemoteSwiftPackageReference "KZFileWatchers" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/krzysztofzablocki/KZFileWatchers"; @@ -5799,7 +5837,7 @@ repositoryURL = "https://github.com/pointfreeco/swift-snapshot-testing"; requirement = { kind = upToNextMinorVersion; - minimumVersion = 1.11.0; + minimumVersion = 1.13.0; }; }; EC6D0C817B1C21D9D096505A /* XCRemoteSwiftPackageReference "Version" */ = { @@ -5851,6 +5889,11 @@ package = 701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */; productName = GZIP; }; + 1CAB56FF1DE17B3E871A0BA2 /* SnapshotTesting */ = { + isa = XCSwiftPackageProductDependency; + package = E9C4F3A12AA1F65C13A8C8EB /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */; + productName = SnapshotTesting; + }; 21C83087604B154AA30E9A8F /* SnapshotTesting */ = { isa = XCSwiftPackageProductDependency; package = E9C4F3A12AA1F65C13A8C8EB /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */; @@ -5861,6 +5904,11 @@ package = 80B898A3AD2AC63F3ABFC218 /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */; productName = MatrixRustSDK; }; + 2629CF48B33643CD5F69C612 /* Prefire */ = { + isa = XCSwiftPackageProductDependency; + package = 22E7BA2ED466B74739AB8567 /* XCRemoteSwiftPackageReference "Prefire" */; + productName = Prefire; + }; 290FDEDA4D764B9F7EBE55A9 /* Algorithms */ = { isa = XCSwiftPackageProductDependency; package = E025F19D013D9BA6C58B37F4 /* XCRemoteSwiftPackageReference "swift-algorithms" */; @@ -6029,6 +6077,11 @@ package = 80B898A3AD2AC63F3ABFC218 /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */; productName = MatrixRustSDK; }; + B717B96C04D7B6A1212D9EDC /* PrefireTestsPlugin */ = { + isa = XCSwiftPackageProductDependency; + package = 22E7BA2ED466B74739AB8567 /* XCRemoteSwiftPackageReference "Prefire" */; + productName = "plugin:PrefireTestsPlugin"; + }; BA93CD75CCE486660C9040BD /* Collections */ = { isa = XCSwiftPackageProductDependency; package = F76A08D0EA29A07A54F4EB4D /* XCRemoteSwiftPackageReference "swift-collections" */; diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 77358eceb..d0555a20b 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -151,6 +151,15 @@ "version" : "2.0.3" } }, + { + "identity" : "prefire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/BarredEwe/Prefire", + "state" : { + "revision" : "abb8dfa44391b4f47edb4937a4ba124e76270a87", + "version" : "1.4.1" + } + }, { "identity" : "sentry-cocoa", "kind" : "remoteSourceControl", @@ -201,8 +210,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-snapshot-testing", "state" : { - "revision" : "dc46eeb3928a75390651fac6c1ef7f93ad59a73b", - "version" : "1.11.1" + "revision" : "696b86a6d151578bca7c1a2a3ed419a5f834d40f", + "version" : "1.13.0" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "74203046135342e4a4a627476dd6caf8b28fe11b", + "version" : "509.0.0" } }, { diff --git a/ElementX/Sources/Other/TestablePreview.swift b/ElementX/Sources/Other/TestablePreview.swift new file mode 100644 index 000000000..2b92315ee --- /dev/null +++ b/ElementX/Sources/Other/TestablePreview.swift @@ -0,0 +1,21 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +import Prefire + +protocol TestablePreviewProvider: PreviewProvider, PrefireProvider { } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineStyler.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineStyler.swift index 8550c43e4..06e754e42 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineStyler.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineStyler.swift @@ -35,10 +35,10 @@ struct TimelineStyler: View { } } -struct TimelineItemStyler_Previews: PreviewProvider { +struct TimelineItemStyler_Previews: TestablePreviewProvider { static let viewModel = RoomScreenViewModel.mock - static let base = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: false, isThreaded: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test")) + static let base = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: false, isThreaded: false, sender: .test, content: .init(body: "Test")) static let sentNonLast: TextRoomTimelineItem = { var result = base @@ -54,7 +54,7 @@ struct TimelineItemStyler_Previews: PreviewProvider { static let sendingLast: TextRoomTimelineItem = { let id = viewModel.state.timelineViewState.timelineIDs.last ?? UUID().uuidString - var result = TextRoomTimelineItem(id: .init(timelineID: id), timestamp: "Now", isOutgoing: true, isEditable: false, isThreaded: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test")) + var result = TextRoomTimelineItem(id: .init(timelineID: id), timestamp: "Now", isOutgoing: true, isEditable: false, isThreaded: false, sender: .test, content: .init(body: "Test")) result.properties.deliveryStatus = .sending return result }() @@ -67,21 +67,21 @@ struct TimelineItemStyler_Previews: PreviewProvider { static let sentLast: TextRoomTimelineItem = { let id = viewModel.state.timelineViewState.timelineIDs.last ?? UUID().uuidString - let result = TextRoomTimelineItem(id: .init(timelineID: id), timestamp: "Now", isOutgoing: true, isEditable: false, isThreaded: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test")) + let result = TextRoomTimelineItem(id: .init(timelineID: id), timestamp: "Now", isOutgoing: true, isEditable: false, isThreaded: false, sender: .test, content: .init(body: "Test")) return result }() - static let ltrString = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: false, isThreaded: false, sender: .init(id: UUID().uuidString), content: .init(body: "house!")) + static let ltrString = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: false, isThreaded: false, sender: .test, content: .init(body: "house!")) - static let rtlString = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: false, isThreaded: false, sender: .init(id: UUID().uuidString), content: .init(body: "באמת!")) + static let rtlString = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: false, isThreaded: false, sender: .test, content: .init(body: "באמת!")) - static let ltrStringThatContainsRtl = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: false, isThreaded: false, sender: .init(id: UUID().uuidString), content: .init(body: "house! -- באמת‏! -- house!")) + static let ltrStringThatContainsRtl = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: false, isThreaded: false, sender: .test, content: .init(body: "house! -- באמת‏! -- house!")) - static let rtlStringThatContainsLtr = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: false, isThreaded: false, sender: .init(id: UUID().uuidString), content: .init(body: "באמת‏! -- house! -- באמת!")) + static let rtlStringThatContainsLtr = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: false, isThreaded: false, sender: .test, content: .init(body: "באמת‏! -- house! -- באמת!")) - static let ltrStringThatFinishesInRtl = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: false, isThreaded: false, sender: .init(id: UUID().uuidString), content: .init(body: "house! -- באמת!")) + static let ltrStringThatFinishesInRtl = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: false, isThreaded: false, sender: .test, content: .init(body: "house! -- באמת!")) - static let rtlStringThatFinishesInLtr = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: false, isThreaded: false, sender: .init(id: UUID().uuidString), content: .init(body: "באמת‏! -- house!")) + static let rtlStringThatFinishesInLtr = TextRoomTimelineItem(id: .random, timestamp: "Now", isOutgoing: true, isEditable: false, isThreaded: false, sender: .test, content: .init(body: "באמת‏! -- house!")) static var testView: some View { VStack { @@ -115,7 +115,7 @@ struct TimelineItemStyler_Previews: PreviewProvider { .environmentObject(viewModel.context) .environment(\.timelineStyle, .plain) .previewDisplayName("Plain") - + languagesTestView .environmentObject(viewModel.context) .environment(\.timelineStyle, .bubbles) diff --git a/ElementX/Sources/Services/Timeline/TimelineItemSender.swift b/ElementX/Sources/Services/Timeline/TimelineItemSender.swift index 265cb0030..d7321d0a1 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItemSender.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemSender.swift @@ -17,6 +17,8 @@ import UIKit struct TimelineItemSender: Identifiable, Hashable { + static let test = TimelineItemSender(id: "@test.matrix.org") + let id: String let displayName: String? let avatarURL: URL? diff --git a/ElementX/SupportingFiles/target.yml b/ElementX/SupportingFiles/target.yml index f9f4d8182..f85f6bb61 100644 --- a/ElementX/SupportingFiles/target.yml +++ b/ElementX/SupportingFiles/target.yml @@ -193,6 +193,7 @@ targets: - package: Version - package: Emojibase - package: WysiwygComposer + - package: Prefire sources: - path: ../Sources diff --git a/Tools/Prefire/PreviewTests.stencil b/Tools/Prefire/PreviewTests.stencil new file mode 100644 index 000000000..3f5395041 --- /dev/null +++ b/Tools/Prefire/PreviewTests.stencil @@ -0,0 +1,181 @@ +// swiftlint:disable all +// swiftformat:disable all + +import XCTest +import SwiftUI +import Prefire + +@testable import SnapshotTesting +#if canImport(AccessibilitySnapshot) + import AccessibilitySnapshot +#endif +{% if argument.mainTarget %} +@testable import {{ argument.mainTarget }} +{% endif %} + +class PreviewTests: XCTestCase { + private let deviceConfig: ViewImageConfig = .iPhoneX + private let simulatorDevice = "{{ argument.simulatorDevice|default:"iPhone15,2" }}" + private let requiredOSVersion = {{ argument.simulatorOSVersion|default:"16" }} + {% if argument.file %} + + private var file: StaticString { .init(stringLiteral: "{{ argument.file }}") } + {% endif %} + + override func setUp() { + super.setUp() + + checkEnvironments() + UIView.setAnimationsEnabled(false) + } + + {% for type in types.types where (type.implements.PrefireProvider or type.based.PrefireProvider or type|annotated:"PrefireProvider") and type.name != "TestablePreviewProvider" %} + func test_{{ type.name|lowerFirstLetter|replace:"_Previews", "" }}() { + for preview in {{ type.name }}._allPreviews { + assertSnapshots(matching: preview) + } + } + + {% endfor %} + // MARK: Private + + private func assertSnapshots(matching preview: _Preview, testName: String = #function) { + let isScreen = preview.layout == .device + let device = preview.device?.snapshotDevice() ?? deviceConfig + var delay: TimeInterval = 1.0 + var precision: Float = 0.99 + + let view = preview.content + .onPreferenceChange(DelayPreferenceKey.self) { delay = $0 } + .onPreferenceChange(PrecisionPreferenceKey.self) { precision = $0 } + + let matchingView = isScreen ? AnyView(view) : AnyView(view + .frame(width: device.size?.width) + .fixedSize(horizontal: false, vertical: true) + ) + + assertSnapshot( + matching: matchingView, + as: .prefireImage(precision: { precision }, duration: { delay }, layout: isScreen ? .device(config: device) : .sizeThatFits), + named: preview.displayName{% if argument.file %}, + file: file{% endif %}, + testName: testName + ) + + #if canImport(AccessibilitySnapshot) + let vc = UIHostingController(rootView: matchingView) + vc.view.frame = UIScreen.main.bounds + assertSnapshot( + matching: vc, + as: .wait(for: delay, on: .accessibilityImage(showActivationPoints: .always)), + named: preview.displayName.map { $0 + ".accessibility" }{% if argument.file %}, + file: file{% endif %}, + testName: testName + ) + #endif + } + + /// Check environments to avoid problems with snapshots on different devices or OS. + private func checkEnvironments() { + let deviceModel = ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] + let osVersion = ProcessInfo().operatingSystemVersion + guard deviceModel?.contains(simulatorDevice) ?? false else { + fatalError("Switch to using \(simulatorDevice) for these tests.") + } + + guard osVersion.majorVersion == requiredOSVersion else { + fatalError("Switch to iOS \(requiredOSVersion) for these tests.") + } + } +} + +// MARK: - SnapshotTesting + Extensions + +private extension PreviewDevice { + func snapshotDevice() -> ViewImageConfig? { + switch rawValue { + case "iPhone 14", "iPhone 13", "iPhone 12", "iPhone 11", "iPhone 10": + return .iPhoneX + case "iPhone 6", "iPhone 6s", "iPhone 7", "iPhone 8": + return .iPhone8 + case "iPhone 6 Plus", "iPhone 6s Plus", "iPhone 8 Plus": + return .iPhone8Plus + case "iPhone SE (1st generation)", "iPhone SE (2nd generation)": + return .iPhoneSe + default: return nil + } + } +} + +private extension Snapshotting where Value: SwiftUI.View, Format == UIImage { + static func prefireImage( + drawHierarchyInKeyWindow: Bool = false, + precision: @escaping () -> Float, + duration: @escaping () -> TimeInterval, + layout: SwiftUISnapshotLayout = .sizeThatFits, + traits: UITraitCollection = .init() + ) -> Snapshotting { + let config: ViewImageConfig + + switch layout { + #if os(iOS) || os(tvOS) + case let .device(config: deviceConfig): + config = deviceConfig + #endif + case .sizeThatFits: + config = .init(safeArea: .zero, size: nil, traits: traits) + case let .fixed(width: width, height: height): + let size = CGSize(width: width, height: height) + config = .init(safeArea: .zero, size: size, traits: traits) + } + + return SimplySnapshotting(pathExtension: "png", diffing: .prefireImage(precision: precision, scale: traits.displayScale)) + .asyncPullback { view in + var config = config + + let controller: UIViewController + + if config.size != nil { + controller = UIHostingController(rootView: view) + } else { + let hostingController = UIHostingController(rootView: view) + + let maxSize = CGSize.zero + config.size = hostingController.sizeThatFits(in: maxSize) + + controller = hostingController + } + + return Async { callback in + let strategy = snapshotView( + config: config, + drawHierarchyInKeyWindow: drawHierarchyInKeyWindow, + traits: traits, + view: controller.view, + viewController: controller + ) + + let duration = duration() + if duration != .zero { + let expectation = XCTestExpectation(description: "Wait") + DispatchQueue.main.asyncAfter(deadline: .now() + duration) { + expectation.fulfill() + } + _ = XCTWaiter.wait(for: [expectation], timeout: duration + 1) + } + strategy.run(callback) + } + } + } +} + +private extension Diffing where Value == UIImage { + static func prefireImage(precision: @escaping () -> Float, scale: CGFloat?) -> Diffing { + lazy var originalDiffing = Diffing.image(precision: precision(), perceptualPrecision: 0.98, scale: scale) + return Diffing( + toData: { originalDiffing.toData($0) }, + fromData: { originalDiffing.fromData($0) }, + diff: { originalDiffing.diff($0, $1) } + ) + } +} diff --git a/UnitTests/Sources/PreviewTests.swift b/UnitTests/Sources/PreviewTests.swift new file mode 100644 index 000000000..72368c792 --- /dev/null +++ b/UnitTests/Sources/PreviewTests.swift @@ -0,0 +1,173 @@ +// Generated using Sourcery 2.0.1 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT + +// swiftlint:disable all +// swiftformat:disable all + +import XCTest +import SwiftUI +import Prefire + +@testable import SnapshotTesting +#if canImport(AccessibilitySnapshot) + import AccessibilitySnapshot +#endif +@testable import ElementX + +class PreviewTests: XCTestCase { + private let deviceConfig: ViewImageConfig = .iPhoneX + private let simulatorDevice = "iPhone14" + private let requiredOSVersion = 16 + + override func setUp() { + super.setUp() + + checkEnvironments() + UIView.setAnimationsEnabled(false) + } + + func test_timelineItemStyler() { + for preview in TimelineItemStyler_Previews._allPreviews { + assertSnapshots(matching: preview) + } + } + + // MARK: Private + + private func assertSnapshots(matching preview: _Preview, testName: String = #function) { + let isScreen = preview.layout == .device + let device = preview.device?.snapshotDevice() ?? deviceConfig + var delay: TimeInterval = 1.0 + var precision: Float = 0.99 + + let view = preview.content + .onPreferenceChange(DelayPreferenceKey.self) { delay = $0 } + .onPreferenceChange(PrecisionPreferenceKey.self) { precision = $0 } + + let matchingView = isScreen ? AnyView(view) : AnyView(view + .frame(width: device.size?.width) + .fixedSize(horizontal: false, vertical: true) + ) + + assertSnapshot( + matching: matchingView, + as: .prefireImage(precision: { precision }, duration: { delay }, layout: isScreen ? .device(config: device) : .sizeThatFits), + named: preview.displayName, + testName: testName + ) + + #if canImport(AccessibilitySnapshot) + let vc = UIHostingController(rootView: matchingView) + vc.view.frame = UIScreen.main.bounds + assertSnapshot( + matching: vc, + as: .wait(for: delay, on: .accessibilityImage(showActivationPoints: .always)), + named: preview.displayName.map { $0 + ".accessibility" }, + testName: testName + ) + #endif + } + + /// Check environments to avoid problems with snapshots on different devices or OS. + private func checkEnvironments() { + let deviceModel = ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] + let osVersion = ProcessInfo().operatingSystemVersion + guard deviceModel?.contains(simulatorDevice) ?? false else { + fatalError("Switch to using \(simulatorDevice) for these tests.") + } + guard osVersion.majorVersion == requiredOSVersion else { + fatalError("Switch to iOS \(requiredOSVersion) for these tests.") + } + } +} + +// MARK: - SnapshotTesting + Extensions + +private extension PreviewDevice { + func snapshotDevice() -> ViewImageConfig? { + switch rawValue { + case "iPhone 14", "iPhone 13", "iPhone 12", "iPhone 11", "iPhone 10": + return .iPhoneX + case "iPhone 6", "iPhone 6s", "iPhone 7", "iPhone 8": + return .iPhone8 + case "iPhone 6 Plus", "iPhone 6s Plus", "iPhone 8 Plus": + return .iPhone8Plus + case "iPhone SE (1st generation)", "iPhone SE (2nd generation)": + return .iPhoneSe + default: return nil + } + } +} + +private extension Snapshotting where Value: SwiftUI.View, Format == UIImage { + static func prefireImage( + drawHierarchyInKeyWindow: Bool = false, + precision: @escaping () -> Float, + duration: @escaping () -> TimeInterval, + layout: SwiftUISnapshotLayout = .sizeThatFits, + traits: UITraitCollection = .init() + ) -> Snapshotting { + let config: ViewImageConfig + + switch layout { + #if os(iOS) || os(tvOS) + case let .device(config: deviceConfig): + config = deviceConfig + #endif + case .sizeThatFits: + config = .init(safeArea: .zero, size: nil, traits: traits) + case let .fixed(width: width, height: height): + let size = CGSize(width: width, height: height) + config = .init(safeArea: .zero, size: size, traits: traits) + } + + return SimplySnapshotting(pathExtension: "png", diffing: .prefireImage(precision: precision, scale: traits.displayScale)) + .asyncPullback { view in + var config = config + + let controller: UIViewController + + if config.size != nil { + controller = UIHostingController(rootView: view) + } else { + let hostingController = UIHostingController(rootView: view) + + let maxSize = CGSize.zero + config.size = hostingController.sizeThatFits(in: maxSize) + + controller = hostingController + } + + return Async { callback in + let strategy = snapshotView( + config: config, + drawHierarchyInKeyWindow: drawHierarchyInKeyWindow, + traits: traits, + view: controller.view, + viewController: controller + ) + + let duration = duration() + if duration != .zero { + let expectation = XCTestExpectation(description: "Wait") + DispatchQueue.main.asyncAfter(deadline: .now() + duration) { + expectation.fulfill() + } + _ = XCTWaiter.wait(for: [expectation], timeout: duration + 1) + } + strategy.run(callback) + } + } + } +} + +private extension Diffing where Value == UIImage { + static func prefireImage(precision: @escaping () -> Float, scale: CGFloat?) -> Diffing { + lazy var originalDiffing = Diffing.image(precision: precision(), perceptualPrecision: 0.98, scale: scale) + return Diffing( + toData: { originalDiffing.toData($0) }, + fromData: { originalDiffing.fromData($0) }, + diff: { originalDiffing.diff($0, $1) } + ) + } +} diff --git a/UnitTests/Sources/__Snapshots__/PreviewTests/test_timelineItemStyler.Bubbles-LTR-with-different-layout-languages.png b/UnitTests/Sources/__Snapshots__/PreviewTests/test_timelineItemStyler.Bubbles-LTR-with-different-layout-languages.png new file mode 100644 index 000000000..602235eaf --- /dev/null +++ b/UnitTests/Sources/__Snapshots__/PreviewTests/test_timelineItemStyler.Bubbles-LTR-with-different-layout-languages.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c935c6529a9f9a63915c3cab80dc9fc7df8723db6258c4d8c445df7c84296e65 +size 110159 diff --git a/UnitTests/Sources/__Snapshots__/PreviewTests/test_timelineItemStyler.Bubbles-RTL-with-different-layout-languages.png b/UnitTests/Sources/__Snapshots__/PreviewTests/test_timelineItemStyler.Bubbles-RTL-with-different-layout-languages.png new file mode 100644 index 000000000..ec57afe2d --- /dev/null +++ b/UnitTests/Sources/__Snapshots__/PreviewTests/test_timelineItemStyler.Bubbles-RTL-with-different-layout-languages.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ebec20c376958c8a4b10a4055f15963a27ed744e0ac594aae803b0e56c6477a2 +size 108424 diff --git a/UnitTests/Sources/__Snapshots__/PreviewTests/test_timelineItemStyler.Bubbles.png b/UnitTests/Sources/__Snapshots__/PreviewTests/test_timelineItemStyler.Bubbles.png new file mode 100644 index 000000000..55ef75591 --- /dev/null +++ b/UnitTests/Sources/__Snapshots__/PreviewTests/test_timelineItemStyler.Bubbles.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f84bcc4acb59ef1a6fb5893b0840b32e3011e6a1e66004b65650ab30321b7f8 +size 97748 diff --git a/UnitTests/Sources/__Snapshots__/PreviewTests/test_timelineItemStyler.Plain.png b/UnitTests/Sources/__Snapshots__/PreviewTests/test_timelineItemStyler.Plain.png new file mode 100644 index 000000000..ef91578a5 --- /dev/null +++ b/UnitTests/Sources/__Snapshots__/PreviewTests/test_timelineItemStyler.Plain.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b2f89b01650704e45b0c9a57c67f7fbe4cf583299b6d0392b4d79c22725a3f7 +size 132101 diff --git a/UnitTests/SupportingFiles/target.yml b/UnitTests/SupportingFiles/target.yml index f24d2827e..19fd5d4b2 100644 --- a/UnitTests/SupportingFiles/target.yml +++ b/UnitTests/SupportingFiles/target.yml @@ -28,9 +28,14 @@ targets: UnitTests: type: bundle.unit-test platform: iOS + + buildToolPlugins: + - plugin: PrefireTestsPlugin + package: Prefire dependencies: - target: ElementX + - package: SnapshotTesting info: path: ../SupportingFiles/Info.plist @@ -44,6 +49,8 @@ targets: sources: - path: ../Sources + excludes: + - "**/__Snapshots__/**" - path: ../SupportingFiles - path: ../../ElementX/Sources/Other/InfoPlistReader.swift - path: ../../Tools/Scripts/Templates/SimpleScreenExample/Tests/Unit diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 76da99a60..f13ba2d0d 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -77,8 +77,11 @@ end lane :unit_tests do run_tests( scheme: "UnitTests", + device: 'iPhone 14 (16.4)', + ensure_devices_found: true, result_bundle: true, number_of_retries: 3, + xcargs: '-skipPackagePluginValidation', ) slather( diff --git a/project.yml b/project.yml index 30f35b084..bc2fc7ce3 100644 --- a/project.yml +++ b/project.yml @@ -94,12 +94,15 @@ packages: PostHog: url: https://github.com/PostHog/posthog-ios minorVersion: 2.0.3 + Prefire: + url: https://github.com/BarredEwe/Prefire + minorVersion: 1.4.1 Sentry: url: https://github.com/getsentry/sentry-cocoa minorVersion: 8.6.0 SnapshotTesting: url: https://github.com/pointfreeco/swift-snapshot-testing - minorVersion: 1.11.0 + minorVersion: 1.13.0 SwiftState: url: https://github.com/ReactKit/SwiftState minorVersion: 6.0.0