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
This commit is contained in:
Tim Oliver 2025-12-11 00:46:38 -08:00 committed by meta-codesync[bot]
parent 7dddb0d4c8
commit 5784f6db51
11 changed files with 68 additions and 13 deletions

View file

@ -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

View file

@ -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;
};

View file

@ -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:@[

View file

@ -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

View file

@ -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

View file

@ -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:@[

View file

@ -11,6 +11,8 @@
#import "IGTestCell.h"
#import "IGTestSingleNibItemDataSource.h"
#if !TARGET_OS_TV
@interface IGListSingleNibSectionControllerTests : IGListTestCase
@end
@ -86,3 +88,5 @@
}
@end
#endif

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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