From 5784f6db5118566c4ce17f13d64c4490e5a4492c Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Thu, 11 Dec 2025 00:46:38 -0800 Subject: [PATCH] Enable unit test suite for tvOS (#1649) Summary: ## Changes in this pull request Following up on https://github.com/instagram/IGListKit/issues/1401! This PR pulls in koenpunt's PR for tvOS test support and updates it against the latest version of IGListKit. A few changes: * I disabled the unit tests that require storyboards/NIBs for tvOS since we only have iOS formatted assets. We can follow this up in a future PR if need be. * Rewrote the Travis build command for GitHub Actions * Went through and gated any UIKit APIs that aren't available on tvOS. * A few unit tests were failing since UICollectionView on tvOS does have a few implicit behavioral differences. I gated these for now, but if anyone using IGListKit on tvOS actually encounters these errors, please open an issue so we can track it and adjust our test suite accordingly. ### Checklist - [x] All tests pass. Demo project builds and runs. - [x] I added tests, an experiment, or detailed why my change isn't tested. - [x] I added an entry to the `CHANGELOG.md` for any breaking changes, enhancements, or bug fixes. - [x] I have reviewed the [contributing guide](https://github.com/Instagram/IGListKit/blob/main/.github/CONTRIBUTING.md) Pull Request resolved: https://github.com/instagram/IGListKit/pull/1649 Reviewed By: jurmarcus Differential Revision: D88921781 Pulled By: TimOliver fbshipit-source-id: fb8b0becde96a504a88b651343049e51ec438b6c --- .github/workflows/CI.yml | 39 +++++++++++++++++-- IGListKit.xcodeproj/project.pbxproj | 10 ++--- Tests/IGListAdapterE2ETests.m | 2 + Tests/IGListAdapterStoryboardTests.m | 4 ++ Tests/IGListAdapterTests.m | 8 +++- Tests/IGListBindingSectionControllerTests.m | 3 ++ Tests/IGListSingleNibItemControllerTests.m | 4 ++ ...GListSingleStoryboardItemControllerTests.m | 4 ++ Tests/Objects/IGListTestSection.m | 2 + .../IGTestBindingWithoutDeselectionDelegate.m | 3 +- .../Objects/IGTestDiffingSectionController.m | 2 + 11 files changed, 68 insertions(+), 13 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index a88de33d..cc29ca73 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -34,8 +34,8 @@ jobs: - name: Run unit tests for macOS run: | set -o pipefail - xcodebuild build build-for-testing -project "${{ env.PROJECT_NAME }}" -scheme "${{ env.SCHEME_NAME }}" -destination "platform=macOS" -configuration Debug ONLY_ACTIVE_ARCH=NO CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=YES | bundle exec xcpretty -c - xcodebuild analyze test-without-building -project "${{ env.PROJECT_NAME }}" -scheme "${{ env.SCHEME_NAME }}" -destination "platform=macOS" -configuration Debug ONLY_ACTIVE_ARCH=NO CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=YES | bundle exec xcpretty -c + xcodebuild build build-for-testing -project "${{ env.PROJECT_NAME }}" -scheme "${{ env.SCHEME_NAME }}" -destination "platform=macOS" -configuration Debug CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=YES | bundle exec xcpretty -c + xcodebuild analyze test-without-building -project "${{ env.PROJECT_NAME }}" -scheme "${{ env.SCHEME_NAME }}" -destination "platform=macOS" -configuration Debug CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=YES | bundle exec xcpretty -c iOS: name: Unit Test iOS @@ -65,8 +65,8 @@ jobs: - name: iOS - ${{ matrix.destination }} run: | set -o pipefail - xcodebuild build build-for-testing -project "${{ env.PROJECT_NAME }}" -scheme "${{ env.SCHEME_NAME }}" -destination "${{ matrix.destination }}" -configuration Debug ONLY_ACTIVE_ARCH=NO CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=YES | bundle exec xcpretty -c - xcodebuild analyze test-without-building -project "${{ env.PROJECT_NAME }}" -scheme "${{ env.SCHEME_NAME }}" -destination "${{ matrix.destination }}" -configuration Debug ONLY_ACTIVE_ARCH=NO CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=YES | bundle exec xcpretty -c + xcodebuild build build-for-testing -project "${{ env.PROJECT_NAME }}" -scheme "${{ env.SCHEME_NAME }}" -destination "${{ matrix.destination }}" -configuration Debug CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=YES | bundle exec xcpretty -c + xcodebuild analyze test-without-building -project "${{ env.PROJECT_NAME }}" -scheme "${{ env.SCHEME_NAME }}" -destination "${{ matrix.destination }}" -configuration Debug CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=YES | bundle exec xcpretty -c - name: Upload code coverage run: bundle exec slather @@ -75,6 +75,37 @@ jobs: CI_PULL_REQUEST: ${{ github.event.number }} GIT_BRANCH: ${{ github.head_ref || github.ref_name }} + tvOS: + name: Unit Test tvOS + runs-on: macos-14 + env: + DEVELOPER_DIR: /Applications/Xcode.app + PROJECT_NAME: IGListKit.xcodeproj + SCHEME_NAME: IGListKit-tvOS + strategy: + matrix: + destination: ["platform=tvOS Simulator,name=Apple TV,OS=18.2"] + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Cache RubyGems + uses: actions/cache@v3 + with: + path: vendor/bundle + key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: | + ${{ runner.os }}-gems- + + - name: Install ruby gems. + run: bundle install + + - name: Run unit tests for tvOS + run: | + set -o pipefail + xcodebuild build build-for-testing -project "${{ env.PROJECT_NAME }}" -scheme "${{ env.SCHEME_NAME }}" -destination "${{ matrix.destination }}" -configuration Debug CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=YES | bundle exec xcpretty -c + xcodebuild analyze test-without-building -project "${{ env.PROJECT_NAME }}" -scheme "${{ env.SCHEME_NAME }}" -destination "${{ matrix.destination }}" -configuration Debug CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=YES | bundle exec xcpretty -c + CocoaPods: name: CocoaPods Lint runs-on: macos-14 diff --git a/IGListKit.xcodeproj/project.pbxproj b/IGListKit.xcodeproj/project.pbxproj index c82b154e..1f68069f 100644 --- a/IGListKit.xcodeproj/project.pbxproj +++ b/IGListKit.xcodeproj/project.pbxproj @@ -70,9 +70,6 @@ 29DA5CA71EA7D37000113926 /* IGListTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 29DA5CA61EA7D37000113926 /* IGListTestCase.m */; }; 29DA5CA81EA7D37000113926 /* IGListTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 29DA5CA61EA7D37000113926 /* IGListTestCase.m */; }; 29EA6C491DB43A8000957A88 /* IGTestNibCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 294369B01DB1B7AE0025F6E7 /* IGTestNibCell.xib */; }; - 401B5E63230111EC004099D5 /* IGTestNibCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 294369B01DB1B7AE0025F6E7 /* IGTestNibCell.xib */; }; - 401B5E64230111F3004099D5 /* IGTestNibSupplementaryView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2904861C1DCD02140007F41D /* IGTestNibSupplementaryView.xib */; }; - 401B5E65230111F7004099D5 /* IGTestStoryboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 821BC4C21DB8CAE900172ED0 /* IGTestStoryboard.storyboard */; }; 576029DC2C61B91D006E50E2 /* IGListViewVisibilityTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = 576029D52C61B91D006E50E2 /* IGListViewVisibilityTracker.h */; }; 576029DD2C61B91D006E50E2 /* IGListViewVisibilityTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = 576029D52C61B91D006E50E2 /* IGListViewVisibilityTracker.h */; }; 576029DE2C61B91D006E50E2 /* IGListPerformDiff.h in Headers */ = {isa = PBXBuildFile; fileRef = 576029D62C61B91D006E50E2 /* IGListPerformDiff.h */; }; @@ -1864,7 +1861,7 @@ BuildIndependentTargetsInParallel = YES; CLASSPREFIX = IG; LastSwiftUpdateCheck = 1120; - LastUpgradeCheck = 1620; + LastUpgradeCheck = 2600; ORGANIZATIONNAME = Instagram; TargetAttributes = { 7A02D01C2361520200B49FAE = { @@ -1968,9 +1965,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 401B5E65230111F7004099D5 /* IGTestStoryboard.storyboard in Resources */, - 401B5E64230111F3004099D5 /* IGTestNibSupplementaryView.xib in Resources */, - 401B5E63230111EC004099D5 /* IGTestNibCell.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2912,6 +2906,7 @@ SWIFT_OBJC_BRIDGING_HEADER = "Tests/IGListKitTests-Bridging-Header.h"; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 3; }; name = Debug; }; @@ -2939,6 +2934,7 @@ SWIFT_OBJC_BRIDGING_HEADER = "Tests/IGListKitTests-Bridging-Header.h"; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 3; }; name = Release; }; diff --git a/Tests/IGListAdapterE2ETests.m b/Tests/IGListAdapterE2ETests.m index e85e29b8..9a29d8a8 100644 --- a/Tests/IGListAdapterE2ETests.m +++ b/Tests/IGListAdapterE2ETests.m @@ -588,6 +588,7 @@ [self waitForExpectationsWithTimeout:30 handler:nil]; } +#if !TARGET_OS_TV - (void)test_whenContentOffsetChanges_withPerformUpdates_thatCollectionViewWorks { // this test layout changes the offset in -prepareLayout which occurs somewhere between the update block being // applied and the completion block @@ -612,6 +613,7 @@ }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } +#endif - (void)test_whenReloadingItems_withNewItemInstances_thatSectionControllersReceiveNewInstances { [self setupWithObjects:@[ diff --git a/Tests/IGListAdapterStoryboardTests.m b/Tests/IGListAdapterStoryboardTests.m index 117e44fc..c4f14fdd 100644 --- a/Tests/IGListAdapterStoryboardTests.m +++ b/Tests/IGListAdapterStoryboardTests.m @@ -14,6 +14,8 @@ #import "IGTestStoryboardSupplementarySource.h" #import "IGTestStoryboardViewController.h" +#if !TARGET_OS_TV + static const CGRect kStackTestFrame = (CGRect){{0.0, 0.0}, {100.0, 100.0}}; @interface IGListAdapterStoryboardTests : XCTestCase @@ -77,3 +79,5 @@ static const CGRect kStackTestFrame = (CGRect){{0.0, 0.0}, {100.0, 100.0}}; } @end + +#endif diff --git a/Tests/IGListAdapterTests.m b/Tests/IGListAdapterTests.m index 74258287..9742916b 100644 --- a/Tests/IGListAdapterTests.m +++ b/Tests/IGListAdapterTests.m @@ -283,7 +283,8 @@ XCTAssertTrue([visibleSectionControllers containsObject:[self.adapter sectionControllerForObject:@4]]); } -- (void)test_withEmptySectionPlusFooter_thatVisibleSectionControllersAreCorrect { +#if !TARGET_OS_TV +- (void) test_withEmptySectionPlusFooter_thatVisibleSectionControllersAreCorrect { self.dataSource.objects = @[@0]; [self.adapter reloadDataWithCompletion:nil]; IGTestSupplementarySource *supplementarySource = [IGTestSupplementarySource new]; @@ -299,6 +300,7 @@ XCTAssertTrue([visibleSectionControllers count] == 1); XCTAssertTrue(visibleSectionControllers.firstObject.supplementaryViewSource == supplementarySource); } +#endif - (void)test_whenCellsExtendBeyondBounds_thatVisibleCellsExistForSectionControllers { self.dataSource.objects = @[@2, @3, @4, @5, @6]; @@ -476,6 +478,7 @@ XCTAssertNil([self.collectionView supplementaryViewForElementKind:UICollectionElementKindSectionFooter atIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]); } +#if !TARGET_OS_TV - (void)test_whenSupplementarySourceSupportsFooter_withNibs_thatHeaderViewsAreNil { self.dataSource.objects = @[@1, @2]; [self.adapter reloadDataWithCompletion:nil]; @@ -499,6 +502,7 @@ XCTAssertNil([self.collectionView supplementaryViewForElementKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]); XCTAssertNil([self.collectionView supplementaryViewForElementKind:UICollectionElementKindSectionFooter atIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]); } +#endif - (void)test_whenAdapterReleased_withSectionControllerStrongRefToCell_thatSectionControllersRelease { __weak id weakCollectionView = nil, weakAdapter = nil, weakSectionController = nil; @@ -1665,6 +1669,7 @@ XCTAssertFalse(s2.wasUnhighlighted); } +#if !TARGET_OS_TV - (void)test_whenContextMenuAskedCell_thatCollectionViewDelegateReceivesMethod API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos) { self.dataSource.objects = @[@0, @1, @2]; [self.adapter reloadDataWithCompletion:nil]; @@ -1698,6 +1703,7 @@ XCTAssertFalse(s1.requestedContextMenu); XCTAssertFalse(s2.requestedContextMenu); } +#endif - (void)test_whenDataSourceDoesntHandleObject_thatObjectIsDropped { // IGListTestAdapterDataSource does not handle NSStrings diff --git a/Tests/IGListBindingSectionControllerTests.m b/Tests/IGListBindingSectionControllerTests.m index efd8edf9..4fea9979 100644 --- a/Tests/IGListBindingSectionControllerTests.m +++ b/Tests/IGListBindingSectionControllerTests.m @@ -196,6 +196,8 @@ XCTAssertEqualObjects(section.unhighlightedViewModel, @"seven"); } + +#if !TARGET_OS_TV - (void)test_whenContextMenuAskedCell_thatCorrectViewModelRetrieved API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos) { [self setupWithObjects:@[ [[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]], @@ -204,6 +206,7 @@ IGTestDiffingSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.firstObject]; XCTAssertEqualObjects(section.contextMenuViewModel, @"seven"); } +#endif - (void)test_whenDeselectingCell_withoutImplementation_thatNoOps { [self setupWithObjects:@[ diff --git a/Tests/IGListSingleNibItemControllerTests.m b/Tests/IGListSingleNibItemControllerTests.m index 9fd45bdb..1696fbe5 100644 --- a/Tests/IGListSingleNibItemControllerTests.m +++ b/Tests/IGListSingleNibItemControllerTests.m @@ -11,6 +11,8 @@ #import "IGTestCell.h" #import "IGTestSingleNibItemDataSource.h" +#if !TARGET_OS_TV + @interface IGListSingleNibSectionControllerTests : IGListTestCase @end @@ -86,3 +88,5 @@ } @end + +#endif diff --git a/Tests/IGListSingleStoryboardItemControllerTests.m b/Tests/IGListSingleStoryboardItemControllerTests.m index 3618385b..70b45c97 100644 --- a/Tests/IGListSingleStoryboardItemControllerTests.m +++ b/Tests/IGListSingleStoryboardItemControllerTests.m @@ -14,6 +14,8 @@ #define genExpectation [self expectationWithDescription:NSStringFromSelector(_cmd)] +#if !TARGET_OS_TV + @interface IGListSingleStoryboardSectionControllerTests : XCTestCase @property (nonatomic, strong) UICollectionView *collectionView; @@ -116,3 +118,5 @@ } @end + +#endif diff --git a/Tests/Objects/IGListTestSection.m b/Tests/Objects/IGListTestSection.m index fc89a3b8..31e645aa 100644 --- a/Tests/Objects/IGListTestSection.m +++ b/Tests/Objects/IGListTestSection.m @@ -60,10 +60,12 @@ self.wasUnhighlighted = YES; } +#if !TARGET_OS_TV - (UIContextMenuConfiguration *)contextMenuConfigurationForItemAtIndex:(NSInteger)index point:(CGPoint)point { self.requestedContextMenu = YES; return nil; } +#endif #pragma mark - IGListDisplayDelegate diff --git a/Tests/Objects/IGTestBindingWithoutDeselectionDelegate.m b/Tests/Objects/IGTestBindingWithoutDeselectionDelegate.m index 254f69eb..7d952b37 100644 --- a/Tests/Objects/IGTestBindingWithoutDeselectionDelegate.m +++ b/Tests/Objects/IGTestBindingWithoutDeselectionDelegate.m @@ -31,12 +31,13 @@ didUnhighlightItemAtIndex:(NSInteger)index viewModel:(nonnull id)viewModel { } - +#if !TARGET_OS_TV - (UIContextMenuConfiguration * _Nullable)sectionController:(nonnull IGListBindingSectionController *)sectionController contextMenuConfigurationForItemAtIndex:(NSInteger)index point:(CGPoint)point viewModel:(nonnull id)viewModel API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos) { return nil; } +#endif @end diff --git a/Tests/Objects/IGTestDiffingSectionController.m b/Tests/Objects/IGTestDiffingSectionController.m index 0dc6095a..a063704a 100644 --- a/Tests/Objects/IGTestDiffingSectionController.m +++ b/Tests/Objects/IGTestDiffingSectionController.m @@ -65,9 +65,11 @@ self.unhighlightedViewModel = viewModel; } +#if !TARGET_OS_TV - (UIContextMenuConfiguration * _Nullable)sectionController:(IGListBindingSectionController *)sectionController contextMenuConfigurationForItemAtIndex:(NSInteger)index point:(CGPoint)point viewModel:(id)viewModel API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos) { self.contextMenuViewModel = viewModel; return nil; } +#endif @end