├── .gitignore ├── .travis.yml ├── LICENSE ├── LayoutTest.podspec ├── LayoutTest.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── LayoutTest.xcscheme │ └── LayoutTestBase.xcscheme ├── LayoutTest ├── Info.plist ├── LayoutTest-Bridging-Header.h ├── LayoutTest.h ├── Swift │ └── LayoutTestCase.swift └── TestCase │ ├── LYTLayoutFailingTestSnapshotRecorder.h │ ├── LYTLayoutFailingTestSnapshotRecorder.m │ ├── LYTLayoutTestCase.h │ └── LYTLayoutTestCase.m ├── LayoutTestBase.podspec ├── LayoutTestBase ├── Autolayout │ ├── LYTAutolayoutFailureIntercepter.h │ └── LYTAutolayoutFailureIntercepter.m ├── Catalog │ ├── LYTCatalogCollectionViewController.h │ ├── LYTCatalogCollectionViewController.m │ ├── LYTCatalogTableViewController.h │ ├── LYTCatalogTableViewController.m │ └── LYTViewCatalogProvider.h ├── Config │ ├── LYTConfig.h │ └── LYTConfig.m ├── Core │ ├── Generators │ │ ├── LYTBoolValues.h │ │ ├── LYTBoolValues.m │ │ ├── LYTDataValues.h │ │ ├── LYTDataValues.m │ │ ├── LYTFloatValues.h │ │ ├── LYTFloatValues.m │ │ ├── LYTImageValues.h │ │ ├── LYTImageValues.m │ │ ├── LYTIntegerValues.h │ │ ├── LYTIntegerValues.m │ │ ├── LYTStringValues.h │ │ ├── LYTStringValues.m │ │ └── Private │ │ │ ├── LYTValuesIterator.h │ │ │ └── LYTValuesIterator.m │ ├── Helpers │ │ ├── LYTMutableCopier.h │ │ └── LYTMutableCopier.m │ ├── LYTViewProvider.h │ ├── Testers │ │ ├── LYTLayoutPropertyTester.h │ │ └── LYTLayoutPropertyTester.m │ └── ViewSizing │ │ ├── LYTDeviceConstants.h │ │ ├── LYTDeviceConstants.m │ │ ├── LYTViewSize.h │ │ ├── LYTViewSize.m │ │ ├── UIView+LYTViewSize.h │ │ └── UIView+LYTViewSize.m ├── Info.plist ├── LayoutTestBase.h ├── Swift │ ├── LYTDataValues+Swift.swift │ └── LYTViewSize+Swift.swift └── UIViewHelpers │ ├── UIView+LYTFrameComparison.h │ ├── UIView+LYTFrameComparison.m │ ├── UIView+LYTHelpers.h │ ├── UIView+LYTHelpers.m │ ├── UIView+LYTTestHelpers.h │ └── UIView+LYTTestHelpers.m ├── LayoutTestTests ├── AutolayoutFailureInterceptorTests.m ├── ConfigTests.m ├── DataValuesTests.m ├── FrameComparisonTests.m ├── Helpers │ ├── UIViewWithLabel.h │ ├── UIViewWithLabel.m │ ├── UnitTestViews.h │ └── UnitTestViews.m ├── Info.plist ├── IteratorTests.m ├── LayoutTestCaseTests │ ├── LYTLayoutFailingTestSnapshotRecorderTests.m │ ├── LayoutReportSnapshotLocationsTests.m │ ├── LayoutTestCaseAccessibilityControlTests.m │ ├── LayoutTestCaseAmbiguousTests.m │ ├── LayoutTestCaseAutolayoutFailureTests.m │ ├── LayoutTestCaseMissingLabelTests.m │ ├── LayoutTestCaseMultipleDataOverlapTests.m │ ├── LayoutTestCaseNestedAccessibilityTests.m │ ├── LayoutTestCaseOverlapTests.m │ ├── LayoutTestCaseViewMaxNumberOfCombinationsConfigTests.m │ ├── LayoutTestCaseViewSizesConfigTests.m │ ├── LayoutTestCaseViewSizesTests.m │ ├── LayoutTestCaseWithinSuperviewTests.m │ └── SwiftLYTLayoutFailingTestSnapshotRecorderTests.swift ├── NullIteratorTests.m ├── SwiftLayoutTestCaseTests.swift ├── TestHelpersTests.m └── en.lproj │ └── InfoPlist.strings ├── NOTICE ├── README.md ├── SampleApp ├── CatalogSample │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ └── LinkedInLogo.imageset │ │ │ ├── Contents.json │ │ │ └── LinkedInLogo.png │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Podfile ├── Podfile.lock ├── SampleApp.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── SampleApp.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── SampleApp │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ └── LinkedInLogo.imageset │ │ │ ├── Contents.json │ │ │ └── LinkedInLogo.png │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── HomeViewController.swift │ ├── HomeViewController.xib │ ├── Info.plist │ ├── SampleFailingView.swift │ ├── SampleFailingView.xib │ ├── SampleTableViewCell.swift │ └── SampleTableViewCell.xib └── SampleAppTests │ ├── Info.plist │ ├── SampleFailingLayoutTestsWithSnapshots.swift │ └── SampleTableViewCellLayoutTests.swift ├── TestProjects ├── README └── SampleAppiOS7 │ ├── Podfile │ ├── Podfile.lock │ ├── README │ ├── SampleAppiOS7.xcodeproj │ └── project.pbxproj │ ├── SampleAppiOS7 │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ ├── ViewController.h │ ├── ViewController.m │ └── main.m │ └── SampleAppiOS7Tests │ ├── Info.plist │ └── SampleAppiOS7Tests.m ├── build.sh └── docs ├── Makefile ├── README ├── bin └── update_theme.sh ├── conf.py ├── images └── catalog.png ├── index.rst ├── make.bat └── pages ├── 000_gettingStarted.rst ├── 010_overview.rst ├── 020_writingTest.rst ├── 030_viewSizes.rst ├── 040_includedTests.rst ├── 050_propertyTesting.rst ├── 060_catalog.rst ├── 070_apis.rst ├── 080_config.rst ├── 090_testingOtherObjects.rst ├── 100_performance.rst ├── 110_projectStructure.rst ├── 120_FAQ.rst └── 130_future.rst /.gitignore: -------------------------------------------------------------------------------- 1 | # XCode 2 | LayoutTest.xcodeproj/xcuserdata/* 3 | LayoutTest.xcworkspace/xcuserdata/* 4 | LayoutTest.xcodeproj/project.xcworkspace/xcuserdata 5 | 6 | # Cocoapods 7 | Pods 8 | 9 | # Sample App 10 | xcuserdata 11 | 12 | # Docs 13 | _build 14 | .buildinfo 15 | 16 | # macos 17 | .DS_Store 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode10.2 3 | script: sh build.sh 4 | after_success: 5 | - bash <(curl -s https://codecov.io/bash) 6 | -------------------------------------------------------------------------------- /LayoutTest.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'LayoutTest' 3 | spec.version = '6.1.0' 4 | spec.license = { :type => 'Apache License, Version 2.0' } 5 | spec.homepage = 'https://linkedin.github.io/LayoutTest-iOS' 6 | spec.authors = 'LinkedIn' 7 | spec.summary = 'LayoutTest enables you to write unit tests which test the layout of a view in multiple configurations.' 8 | spec.source = { :git => 'https://github.com/linkedin/LayoutTest-iOS.git', :tag => spec.version } 9 | spec.platform = :ios, '12.0' 10 | spec.default_subspecs = 'TestCase' 11 | spec.swift_version = '5.0' 12 | 13 | spec.subspec 'Swift' do |sp| 14 | sp.dependency 'LayoutTest/SwiftSubspec' 15 | end 16 | 17 | spec.subspec 'TestCase' do |sp| 18 | sp.source_files = 'LayoutTest/TestCase' 19 | sp.dependency 'LayoutTestBase/Autolayout' 20 | sp.dependency 'LayoutTestBase/Catalog' 21 | sp.dependency 'LayoutTestBase/Config' 22 | sp.dependency 'LayoutTestBase/Core' 23 | sp.dependency 'LayoutTestBase/UIViewHelpers' 24 | sp.framework = 'XCTest' 25 | end 26 | 27 | spec.subspec 'SwiftSubspec' do |sp| 28 | sp.source_files = 'LayoutTest/Swift', 'LayoutTest/LayoutTest.h' 29 | sp.dependency 'LayoutTest/TestCase' 30 | sp.dependency 'LayoutTestBase/Swift' 31 | end 32 | end 33 | 34 | -------------------------------------------------------------------------------- /LayoutTest.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LayoutTest.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LayoutTest.xcodeproj/xcshareddata/xcschemes/LayoutTest.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 38 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 64 | 70 | 71 | 72 | 73 | 79 | 80 | 86 | 87 | 88 | 89 | 91 | 92 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /LayoutTest.xcodeproj/xcshareddata/xcschemes/LayoutTestBase.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /LayoutTest/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /LayoutTest/LayoutTest-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import "LYTLayoutFailingTestSnapshotRecorder.h" -------------------------------------------------------------------------------- /LayoutTest/LayoutTest.h: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | /** 11 | This file is included in cocoapods because without it, Xcode refuses to build the project. It shouldn't cause any harm. 12 | */ 13 | 14 | #import 15 | #import 16 | #import 17 | 18 | //! Project version number for LayoutTest. 19 | FOUNDATION_EXPORT double LayoutTestBaseVersionNumber; 20 | 21 | //! Project version string for LayoutTest. 22 | FOUNDATION_EXPORT const unsigned char LayoutTestBaseVersionString[]; 23 | 24 | #pragma mark - Main Classes 25 | 26 | #import "LYTLayoutTestCase.h" 27 | -------------------------------------------------------------------------------- /LayoutTest/TestCase/LYTLayoutFailingTestSnapshotRecorder.h: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import 11 | #import 12 | #import 13 | 14 | NS_SWIFT_NAME(LayoutFailingTestSnapshotRecorder) 15 | @interface LYTLayoutFailingTestSnapshotRecorder : NSObject 16 | 17 | /** 18 | Singleton accessor. 19 | */ 20 | + (instancetype)sharedInstance; 21 | 22 | /** 23 | Creates the new folder structure and starts index.html file for the given class. If a folder already exists for the class it will be deleted first to clear any images previously saved for failing tests in that class. 24 | 25 | Folder structure for images follows the pattern: {DERIVED_DATA}/LayoutTestImages/{CLASS_NAME}/index.html 26 | Index.html file includes tables with failure description, image and the data that the image failed with. 27 | 28 | \param invocationClass The class to start a new log for, class name is used in the directory structure to save the snapshots. 29 | */ 30 | - (void)startNewLogForClass:(Class)invocationClass; 31 | 32 | /** 33 | Finishes the index.html log for the current invocation class by adding closing html tags. 34 | Can be called safely without calling startNewLogForClass: as no index.html file for the class with exists so nothing will happen. 35 | 36 | Then file path to the index.html file for the current class is saved after all of the tests in the test class finish running. Once all test classes in have finished all saved failing test index.html file paths are logged to the console. 37 | */ 38 | - (void)finishLog; 39 | 40 | /** 41 | Renders the view passed as a png and saves it. The failure description, image and data are added to the index.html file for the current test class. 42 | The invocation is used to create a folder for the invocations method name and the failing views for that invocation are saved within. Folder structure for invocation follows the pattern: {DERIVED_DATA}/LayoutTestImages/{CLASS_NAME}/{METHOD_NAME}/ 43 | Images sare saved with the file name format: Width-{VIEW_WIDTH}_Height-{VIEW_HEIGHT}_Data-{DATA_DESCRIPTION_HASH}.png 44 | 45 | \param view The view to save a image of. 46 | \param data The data that was used to poulate the view 47 | \param invocation The invocation for the current test. When used from a XCTestCase subclass the invocation will include the name of the test method currently being run. 48 | \param failureDescription The description of why the view that is being saved failed its layout tests. 49 | */ 50 | - (void)saveImageOfView:(UIView *)view withData:(NSDictionary *)data fromInvocation:(NSInvocation *)invocation failureDescription:(NSString *)failureDescription; 51 | 52 | @end 53 | -------------------------------------------------------------------------------- /LayoutTestBase.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'LayoutTestBase' 3 | spec.version = '6.1.0' 4 | spec.license = { :type => 'Apache License, Version 2.0' } 5 | spec.homepage = 'https://linkedin.github.io/LayoutTest-iOS' 6 | spec.authors = 'LinkedIn' 7 | spec.summary = 'Base library for LayoutTest without XCTest dependency. LayoutTest enables you to write unit tests which test the layout of views.' 8 | spec.source = { :git => 'https://github.com/linkedin/LayoutTest-iOS.git', :tag => spec.version } 9 | spec.platform = :ios, '12.0' 10 | spec.frameworks = 'Foundation', 'UIKit' 11 | spec.default_subspecs = 'Core', 'Autolayout', 'Catalog', 'Config', 'UIViewHelpers' 12 | spec.swift_version = '5.0' 13 | 14 | spec.subspec 'Swift' do |sp| 15 | sp.source_files = 'LayoutTestBase/Swift/**/*', 'LayoutTestBase/LayoutTestBase.h' 16 | sp.dependency 'LayoutTestBase/Core' 17 | sp.dependency 'LayoutTestBase/Autolayout' 18 | sp.dependency 'LayoutTestBase/Catalog' 19 | sp.dependency 'LayoutTestBase/Config' 20 | sp.dependency 'LayoutTestBase/UIViewHelpers' 21 | end 22 | 23 | spec.subspec 'Core' do |sp| 24 | sp.source_files = 'LayoutTestBase/Core/**/*' 25 | sp.dependency 'LayoutTestBase/Config' 26 | end 27 | 28 | spec.subspec 'Autolayout' do |sp| 29 | sp.source_files = 'LayoutTestBase/Autolayout' 30 | end 31 | 32 | spec.subspec 'Catalog' do |sp| 33 | sp.source_files = 'LayoutTestBase/Catalog' 34 | sp.dependency 'LayoutTestBase/Core' 35 | end 36 | 37 | spec.subspec 'Config' do |sp| 38 | sp.source_files = 'LayoutTestBase/Config' 39 | end 40 | 41 | spec.subspec 'UIViewHelpers' do |sp| 42 | sp.source_files = 'LayoutTestBase/UIViewHelpers' 43 | sp.dependency 'LayoutTestBase/Config' 44 | end 45 | end 46 | 47 | -------------------------------------------------------------------------------- /LayoutTestBase/Autolayout/LYTAutolayoutFailureIntercepter.h: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | /** 15 | This class intercepts autolayout failures. It's useful for testing to ensure that we don't break any constraints. 16 | 17 | It should NOT ever be used in prod. This is because it relies on Apple's private methods which is brittle. Your app will also possibly be rejected 18 | from the App Store because of using private APIs. 19 | It swizzles a private method on a private class (NSISEngine) and runs a block whenever this method is run. This method always gets run when there 20 | is a layout failure (handleUnsatisfiableRowWithHead:body:usingInfeasibilityHandlingBehavior:mutuallyExclusiveConstraints:). 21 | 22 | If Apple updates their implementation, this class will need to be changed. 23 | */ 24 | NS_SWIFT_NAME(AutolayoutFailureIntercepter) 25 | @interface LYTAutolayoutFailureIntercepter : NSObject 26 | 27 | /** 28 | Start intecerpting autolayout failures and run a block whenever you find one. This block is strongly retained, so be careful of retain cycles. 29 | 30 | Calling this method multiple times will cause the block to be reset. 31 | */ 32 | + (void)interceptAutolayoutFailuresWithBlock:(void(^)(void))block; 33 | 34 | /** 35 | Stops intercepting autolayout failures and nils out the current block (releasing it from memory). 36 | */ 37 | + (void)stopInterceptingAutolayoutFailures; 38 | 39 | @end 40 | 41 | NS_ASSUME_NONNULL_END 42 | -------------------------------------------------------------------------------- /LayoutTestBase/Autolayout/LYTAutolayoutFailureIntercepter.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "LYTAutolayoutFailureIntercepter.h" 11 | #import 12 | 13 | 14 | #define NSStringViewClassName @"UIView" 15 | #define CStringViewClassName "UIView" 16 | 17 | @implementation LYTAutolayoutFailureIntercepter 18 | 19 | + (void)interceptAutolayoutFailuresWithBlock:(void(^)(void))block { 20 | Class class = NSClassFromString(NSStringViewClassName); 21 | [self validateAutolayoutInterceptionForClass:class]; 22 | [class interceptAutolayoutFailuresWithBlock:block]; 23 | } 24 | 25 | + (void)stopInterceptingAutolayoutFailures { 26 | Class class = NSClassFromString(NSStringViewClassName); 27 | [self validateAutolayoutInterceptionForClass:class]; 28 | [class stopInterceptingAutolayoutFailures]; 29 | } 30 | 31 | + (void)validateAutolayoutInterceptionForClass:(Class)class { 32 | #pragma clang diagnostic push 33 | #pragma clang diagnostic ignored "-Wundeclared-selector" 34 | if (class && [class instancesRespondToSelector:@selector(engine:willBreakConstraint:dueToMutuallyExclusiveConstraints:)]) { 35 | return; 36 | } 37 | #pragma clang diagnostic pop 38 | NSAssert(false, @"This class no longer exists or it no longer implements the method we swizzle. This means that Apple has changed their implementation of Auto Layout and this code needs to be updated. Please file a bug with this information."); 39 | } 40 | 41 | @end 42 | 43 | static void(^savedBlock)(void); 44 | static BOOL swizzledAutolayout = false; 45 | 46 | @interface NSObject (Private) 47 | @end 48 | 49 | @implementation NSObject (Private) 50 | 51 | /** 52 | This method will run a block whenever there are autolayout failures. Running it multiple times will remove the old block and add a new block. 53 | */ 54 | + (void)interceptAutolayoutFailuresWithBlock:(void(^)(void))block { 55 | savedBlock = block; 56 | 57 | if (!swizzledAutolayout) { 58 | swizzledAutolayout = YES; 59 | 60 | [self swizzleAutolayoutMethod]; 61 | } 62 | } 63 | 64 | + (void)stopInterceptingAutolayoutFailures { 65 | savedBlock = nil; 66 | 67 | if (swizzledAutolayout) { 68 | swizzledAutolayout = NO; 69 | 70 | [self swizzleAutolayoutMethod]; 71 | } 72 | } 73 | 74 | + (void)swizzleAutolayoutMethod { 75 | // Here we switch the implementations of our method and the actual method 76 | // This will either turn on or turn off the feature 77 | #pragma clang diagnostic push 78 | #pragma clang diagnostic ignored "-Wundeclared-selector" 79 | Method original = class_getInstanceMethod(objc_getClass(CStringViewClassName), @selector(engine:willBreakConstraint:dueToMutuallyExclusiveConstraints:)); 80 | #pragma clang diagnostic pop 81 | 82 | Method swizzled = class_getInstanceMethod(self, @selector(swizzle_engine:willBreakConstraint:dueToMutuallyExclusiveConstraints:)); 83 | method_exchangeImplementations(original, swizzled); 84 | } 85 | 86 | - (id)swizzle_engine:(void *)engine willBreakConstraint:(void *)constraints dueToMutuallyExclusiveConstraints:(void *)mutuallyExclusiveConstraints { 87 | savedBlock(); 88 | 89 | // After running our block, we call the original implementation 90 | // This looks like an infinite loop, but it isn't because we switched the implementations. So now, this goes to the original method. 91 | return [self swizzle_engine:engine willBreakConstraint:constraints dueToMutuallyExclusiveConstraints:mutuallyExclusiveConstraints]; 92 | } 93 | 94 | @end 95 | -------------------------------------------------------------------------------- /LayoutTestBase/Catalog/LYTCatalogCollectionViewController.h: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import 11 | 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @protocol LYTViewCatalogProvider; 16 | 17 | NS_SWIFT_NAME(CatalogCollectionViewController) 18 | @interface LYTCatalogCollectionViewController : UICollectionViewController 19 | 20 | @property (nonatomic, nullable) Class ViewProviderClass; 21 | 22 | @end 23 | 24 | NS_ASSUME_NONNULL_END 25 | -------------------------------------------------------------------------------- /LayoutTestBase/Catalog/LYTCatalogCollectionViewController.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "LYTCatalogCollectionViewController.h" 11 | // View Provider Protocol 12 | #import "LYTViewProvider.h" 13 | #import "LYTViewCatalogProvider.h" 14 | // Test runner 15 | #import "LYTLayoutPropertyTester.h" 16 | 17 | 18 | @interface LYTCatalogCollectionViewController () 19 | 20 | @property (nonatomic, strong) NSArray *dataArray; 21 | 22 | @end 23 | 24 | @implementation LYTCatalogCollectionViewController 25 | 26 | - (instancetype)init { 27 | UICollectionViewFlowLayout* flowLayout = [[UICollectionViewFlowLayout alloc] init]; 28 | [flowLayout setScrollDirection:UICollectionViewScrollDirectionVertical]; 29 | 30 | self = [super initWithCollectionViewLayout:flowLayout]; 31 | return self; 32 | } 33 | 34 | - (void)viewDidLoad { 35 | [super viewDidLoad]; 36 | 37 | Class viewProviderClass = self.ViewProviderClass; 38 | if (!viewProviderClass) { 39 | NSAssert(NO, @"No ViewProviderClass set"); 40 | return; 41 | } 42 | 43 | [viewProviderClass registerClassOnCollectionView:self.collectionView]; 44 | 45 | NSMutableArray *dataArray = [NSMutableArray array]; 46 | 47 | LYTTesterLimitResults limitResults = LYTTesterLimitResultsNoSizes; 48 | if ([viewProviderClass respondsToSelector:@selector(limitResultsForCatalogByAssumingDataValueIndependence)]) { 49 | if ([viewProviderClass limitResultsForCatalogByAssumingDataValueIndependence]) { 50 | limitResults &= LYTTesterLimitResultsLimitDataCombinations; 51 | } 52 | } 53 | [LYTLayoutPropertyTester runPropertyTestsWithViewProvider:viewProviderClass 54 | limitResults:limitResults 55 | validation:^(__unused UIView *view, NSDictionary *data, __unused id context) { 56 | [dataArray addObject:data]; 57 | }]; 58 | self.dataArray = dataArray; 59 | } 60 | 61 | #pragma mark - Table view data source 62 | 63 | - (CGSize)collectionView:(__unused UICollectionView *)collectionView 64 | layout:(__unused UICollectionViewLayout *)collectionViewLayout 65 | sizeForItemAtIndexPath:(NSIndexPath *)indexPath { 66 | return [self.ViewProviderClass sizeForCollectionViewCellForCatalogFromData:self.dataArray[(NSUInteger)indexPath.row] viewWidth:self.view.frame.size.width]; 67 | } 68 | 69 | - (NSInteger)collectionView:(__unused UICollectionView *)collectionView 70 | numberOfItemsInSection:(__unused NSInteger)section { 71 | return (NSInteger)self.dataArray.count; 72 | } 73 | 74 | - (UICollectionViewCell *)collectionView:(__unused UICollectionView *)collectionView 75 | cellForItemAtIndexPath:(NSIndexPath *)indexPath { 76 | UICollectionViewCell *cell = [self.collectionView dequeueReusableCellWithReuseIdentifier:[self.ViewProviderClass reuseIdentifier] forIndexPath:indexPath]; 77 | 78 | id context = nil; 79 | NSError *error; 80 | cell = (UICollectionViewCell *)[self.ViewProviderClass viewForData:self.dataArray[(NSUInteger)indexPath.row] 81 | reuseView:cell 82 | size:nil 83 | context:&context 84 | error:&error]; 85 | NSAssert(error == nil, error.description); 86 | 87 | return cell; 88 | } 89 | 90 | @end 91 | -------------------------------------------------------------------------------- /LayoutTestBase/Catalog/LYTCatalogTableViewController.h: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import 11 | 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @protocol LYTViewCatalogProvider; 16 | 17 | NS_SWIFT_NAME(CatalogTableViewController) 18 | @interface LYTCatalogTableViewController : UITableViewController 19 | 20 | @property (nonatomic, nullable) Class ViewProviderClass; 21 | 22 | @end 23 | 24 | NS_ASSUME_NONNULL_END 25 | -------------------------------------------------------------------------------- /LayoutTestBase/Catalog/LYTCatalogTableViewController.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "LYTCatalogTableViewController.h" 11 | // View Provider Protocol 12 | #import "LYTViewProvider.h" 13 | #import "LYTViewCatalogProvider.h" 14 | // Test runner 15 | #import "LYTLayoutPropertyTester.h" 16 | 17 | 18 | @interface LYTCatalogTableViewController () 19 | 20 | @property (nonatomic, strong) NSArray *dataArray; 21 | 22 | @end 23 | 24 | @implementation LYTCatalogTableViewController 25 | 26 | - (void)viewDidLoad { 27 | [super viewDidLoad]; 28 | 29 | Class viewProviderClass = self.ViewProviderClass; 30 | if (!viewProviderClass) { 31 | NSAssert(NO, @"No ViewProviderClass set"); 32 | return; 33 | } 34 | 35 | [self.ViewProviderClass registerClassOnTableView:self.tableView]; 36 | 37 | NSMutableArray *dataArray = [NSMutableArray array]; 38 | 39 | LYTTesterLimitResults limitResults = LYTTesterLimitResultsNoSizes; 40 | if ([viewProviderClass respondsToSelector:@selector(limitResultsForCatalogByAssumingDataValueIndependence)]) { 41 | if ([viewProviderClass limitResultsForCatalogByAssumingDataValueIndependence]) { 42 | limitResults &= LYTTesterLimitResultsLimitDataCombinations; 43 | } 44 | } 45 | [LYTLayoutPropertyTester runPropertyTestsWithViewProvider:viewProviderClass 46 | limitResults:limitResults 47 | validation:^(__unused UIView *view, NSDictionary *data, __unused id context) { 48 | [dataArray addObject:data]; 49 | }]; 50 | self.dataArray = dataArray; 51 | 52 | self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; 53 | } 54 | 55 | #pragma mark - Table view data source 56 | 57 | - (CGFloat)tableView:(__unused UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { 58 | return [self.ViewProviderClass heightForTableViewCellForCatalogFromData:self.dataArray[(NSUInteger)indexPath.row]]; 59 | } 60 | 61 | - (NSInteger)tableView:(__unused UITableView *)tableView numberOfRowsInSection:(__unused NSInteger)section { 62 | return (NSInteger)self.dataArray.count; 63 | } 64 | 65 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 66 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:[self.ViewProviderClass reuseIdentifier] forIndexPath:indexPath]; 67 | 68 | if ([(id)self.ViewProviderClass respondsToSelector:@selector(tableViewCellForCatalogFromData:reuseCell:)]) { 69 | cell = [self.ViewProviderClass tableViewCellForCatalogFromData:self.dataArray[(NSUInteger)indexPath.row] reuseCell:cell]; 70 | } else { 71 | NSError *error; 72 | cell = (UITableViewCell *)[self.ViewProviderClass viewForData:self.dataArray[(NSUInteger)indexPath.row] 73 | reuseView:cell 74 | size:nil 75 | context:nil 76 | error:&error]; 77 | NSAssert(error == nil, error.description); 78 | NSAssert([cell isKindOfClass:[UITableViewCell class]], @"If your view is not a UITableViewCell, you must implement cellForCatalogFromData:reuseCell:"); 79 | } 80 | 81 | cell.selectionStyle = UITableViewCellSelectionStyleNone; 82 | 83 | return cell; 84 | } 85 | 86 | @end 87 | -------------------------------------------------------------------------------- /LayoutTestBase/Catalog/LYTViewCatalogProvider.h: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import 11 | #import "LYTViewProvider.h" 12 | 13 | 14 | NS_ASSUME_NONNULL_BEGIN 15 | 16 | NS_SWIFT_NAME(ViewCatalogProvider) 17 | @protocol LYTViewCatalogProvider 18 | 19 | /** 20 | This method is only needed for creating a LYTCatalog for your views and is only required if viewForData:reuseView: does not return a UITableViewCell. 21 | You should add your view to a UITableViewCell and return it. 22 | 23 | \param data Data for the view. 24 | \param reuseCell Reuse cell. May be nil. 25 | \returns UITableViewCell with the view inside. 26 | */ 27 | @optional 28 | + (UITableViewCell *)tableViewCellForCatalogFromData:(NSDictionary *)data reuseCell:(UITableViewCell * _Nullable)reuseCell; 29 | 30 | /** 31 | Specifies the height of the cells given data. You only need this if you are using the UITableViewCell catalog. 32 | 33 | \param data Data for the cell. 34 | \returns Height for the UITableViewCell. 35 | */ 36 | @optional 37 | + (CGFloat)heightForTableViewCellForCatalogFromData:(NSDictionary *)data; 38 | 39 | /** 40 | Specifies the height of the cells given data. You only need this if you are using the UICollectionViewCell catalog. 41 | 42 | \param data Data for the cell. 43 | \param viewWidth The current width of the view presenting the data. 44 | \returns Size for the UICollectionViewCell. 45 | */ 46 | @optional 47 | + (CGSize)sizeForCollectionViewCellForCatalogFromData:(NSDictionary *)data viewWidth:(CGFloat)viewWidth; 48 | 49 | /** 50 | Needed for creating a collection view catalog. This method should register either your nib or class with the collection view. 51 | 52 | \param collectionView The collection view you should register with. 53 | */ 54 | @optional 55 | + (void)registerClassOnCollectionView:(UICollectionView *)collectionView; 56 | 57 | /** 58 | Needed for creating a table view catalog. This method should register either your nib or class with the table view. 59 | 60 | \param tableView The table view you should register with. 61 | */ 62 | @optional 63 | + (void)registerClassOnTableView:(UITableView *)tableView; 64 | 65 | /** 66 | Reuse identifier to use on the cell. 67 | */ 68 | @optional 69 | + (NSString *)reuseIdentifier; 70 | 71 | /** 72 | The property tests test a lot of different combinations. Often this is too many to contain in a catalog. Usually, given a LYTDataValues subclass 73 | that outputs 1, 2 and another which outputs a, b, c you'd get the following pairs of data: 74 | 75 | (1, a), (2, a), (1, b), (2, b), (1, c), (2, c) 76 | 77 | If this method returns true, it will not do all combinations, but keep all other fields static while iterating through one field. So you'll get: 78 | 79 | (1, a), (2, a), (1, b), (1, c) 80 | 81 | In this way, we're assuming that the parameters are independent so you don't need to test every combination. In most cases, this provides wide coverage. 82 | 83 | \returns Whether we should limit the results for the LYTCatalog 84 | */ 85 | @optional 86 | + (BOOL)limitResultsForCatalogByAssumingDataValueIndependence; 87 | 88 | @end 89 | 90 | NS_ASSUME_NONNULL_END 91 | -------------------------------------------------------------------------------- /LayoutTestBase/Config/LYTConfig.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import 11 | #import "LYTConfig.h" 12 | 13 | @implementation LYTConfig 14 | 15 | NSInteger const LYTSaveUnlimitedSnapshotsPerMethod = -1; 16 | 17 | + (instancetype)sharedInstance { 18 | static LYTConfig *sharedInstance = nil; 19 | static dispatch_once_t onceToken; 20 | dispatch_once(&onceToken, ^{ 21 | sharedInstance = [[LYTConfig alloc] init]; 22 | }); 23 | return sharedInstance; 24 | } 25 | 26 | - (instancetype)init { 27 | self = [super init]; 28 | [self resetDefaults]; 29 | return self; 30 | } 31 | 32 | - (void)resetDefaults { 33 | self.maxNumberOfCombinations = nil; 34 | self.viewSizesToTest = nil; 35 | self.viewOverlapTestsEnabled = YES; 36 | self.viewWithinSuperviewTestsEnabled = YES; 37 | self.ambiguousAutolayoutTestsEnabled = YES; 38 | self.interceptsAutolayoutErrors = YES; 39 | self.accessibilityTestsEnabled = YES; 40 | self.failingTestSnapshotsEnabled = YES; 41 | /* 42 | UISwitch - This is a known class which has internal overlapping subviews. 43 | UITextView - If you use attributed text, UIKit may add UIImage views which can overlap with the internal text containers. 44 | UIButton - If you set a background image, then this will overlap with other internal views. 45 | */ 46 | self.viewClassesAllowingSubviewErrors = [NSSet setWithObjects:[UISwitch class], [UITextView class], [UIButton class], nil]; 47 | /* 48 | UIControl - Subclasses of UIControl should all have accessibility labels. 49 | */ 50 | self.viewClassesRequiringAccessibilityLabels = [NSSet setWithObjects:[UIControl class], nil]; 51 | self.cgFloatEpsilon = 1e-5f; 52 | self.snapshotsToSavePerMethod = LYTSaveUnlimitedSnapshotsPerMethod; 53 | } 54 | 55 | @end 56 | 57 | BOOL LYTEpsilonEqual(CGFloat x, CGFloat y) { 58 | CGFloat epsilon = [LYTConfig sharedInstance].cgFloatEpsilon; 59 | return fabs(x - y) < epsilon; 60 | } 61 | -------------------------------------------------------------------------------- /LayoutTestBase/Core/Generators/LYTBoolValues.h: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "LYTDataValues.h" 11 | 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | NS_SWIFT_NAME(BoolValues) 16 | @interface LYTBoolValues : LYTDataValues 17 | 18 | @end 19 | 20 | NS_ASSUME_NONNULL_END 21 | -------------------------------------------------------------------------------- /LayoutTestBase/Core/Generators/LYTBoolValues.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "LYTBoolValues.h" 11 | 12 | @implementation LYTBoolValues 13 | 14 | - (NSArray *)values { 15 | return @[ 16 | @(YES), 17 | @(NO) 18 | ]; 19 | } 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /LayoutTestBase/Core/Generators/LYTDataValues.h: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import 11 | 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | /** 16 | This is the base class for all LYTDataValues subclasses used by the testing library. You can use these LYTDataValues subclasses as values in the 17 | dictionary returned by (NSDictionary *)dataSpecForTest and it will be converted into many different specific values by the library. There are three 18 | main ways you can use this class: 19 | 20 | - Use one of the built in LYTDataValues subclasses (LYTStringValues, LYTFloatValues, etc.) 21 | - Create a one time use object with - (id)initWithValues:(NSArray *)values; 22 | - Create your own subclasses 23 | 24 | To create your own subclass, the only method you need to override is the - (NSArray *)values. Simple return all the values you want the LYTDataValues 25 | subclass to return. 26 | 27 | NSNull 28 | 29 | If you put NSNull into the values array, it will be converted to nil (as in, remove the key from the final JSON). 30 | */ 31 | NS_SWIFT_NAME(DataValues) 32 | @interface LYTDataValues : NSObject 33 | 34 | /** 35 | Returns all the possible replacement values for this LYTDataValues class. This is the only method you should override when subclassing. 36 | 37 | \warning You should not use this to access the values that the generator represents. You should only use this for subclassing. 38 | */ 39 | @property (nonatomic, readonly) NSArray *values; 40 | 41 | /** 42 | Returns LYTDataValues with all values. 43 | */ 44 | - (instancetype)init; 45 | 46 | /** 47 | Returns LYTDataValues with all values except nil. Use this for required fields. 48 | */ 49 | - (instancetype)initWithRequired:(BOOL)required; 50 | 51 | /** 52 | Returns LYTDataValues with these specific values and overrides any subclass's behavior. 53 | */ 54 | - (instancetype)initWithValues:(NSArray *)values NS_REFINED_FOR_SWIFT; 55 | 56 | /** 57 | Returns LYTDataValues with all the values that return true from the filter block. 58 | */ 59 | - (instancetype)initWithFilter:(BOOL(^ _Nullable)(id value))filter; 60 | 61 | /** 62 | Returns the number of values this object contains. 63 | */ 64 | - (NSUInteger)numberOfValues; 65 | 66 | /** 67 | Returns a value for a certain index. If out of range, it will throw and exception. 68 | */ 69 | - (id)valueAtIndex:(NSUInteger)index; 70 | 71 | @end 72 | 73 | NS_ASSUME_NONNULL_END 74 | -------------------------------------------------------------------------------- /LayoutTestBase/Core/Generators/LYTDataValues.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "LYTDataValues.h" 11 | 12 | @interface LYTDataValues () 13 | 14 | @property (nonatomic, strong) NSArray *filteredValues; 15 | 16 | @end 17 | 18 | @implementation LYTDataValues 19 | 20 | - (id)init { 21 | return [self initWithFilter:nil]; 22 | } 23 | 24 | - (id)initWithRequired:(BOOL)required { 25 | return [self initWithFilter:^BOOL(id value) { 26 | return (value != nil) || !required; 27 | }]; 28 | } 29 | 30 | - (id)initWithValues:(NSArray *)values { 31 | self = [super init]; 32 | if (self) { 33 | self.filteredValues = values; 34 | } 35 | return self; 36 | } 37 | 38 | - (id)initWithFilter:(BOOL(^)(id value))filter { 39 | self = [super init]; 40 | if (self) { 41 | if (filter) { 42 | NSMutableArray *filteredValues = [NSMutableArray array]; 43 | for (id object in self.values) { 44 | id reportedObject = object; 45 | if ([object isKindOfClass:[NSNull class]]) { 46 | reportedObject = nil; 47 | } 48 | if (filter(reportedObject)) { 49 | [filteredValues addObject:object]; 50 | } 51 | } 52 | self.filteredValues = filteredValues; 53 | } else { 54 | self.filteredValues = self.values; 55 | } 56 | 57 | NSAssert([self.filteredValues count] > 0, @"After filtering the values, there must be at least one value for the LYTDataValues subclass."); 58 | } 59 | 60 | return self; 61 | } 62 | 63 | - (NSUInteger)numberOfValues { 64 | return [self.filteredValues count]; 65 | } 66 | 67 | - (id)valueAtIndex:(NSUInteger)index { 68 | return self.filteredValues[index]; 69 | } 70 | 71 | @end 72 | -------------------------------------------------------------------------------- /LayoutTestBase/Core/Generators/LYTFloatValues.h: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "LYTDataValues.h" 11 | 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | NS_SWIFT_NAME(FloatValues) 16 | @interface LYTFloatValues : LYTDataValues 17 | 18 | @end 19 | 20 | NS_ASSUME_NONNULL_END 21 | -------------------------------------------------------------------------------- /LayoutTestBase/Core/Generators/LYTFloatValues.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "LYTFloatValues.h" 11 | 12 | @implementation LYTFloatValues 13 | 14 | - (NSArray *)values { 15 | return @[ 16 | @(4.5324), 17 | @(0), 18 | [NSNull null], 19 | @(-M_PI), 20 | @(10), 21 | @(MAXFLOAT), 22 | @(-MAXFLOAT), 23 | @(0.000000000001) 24 | ]; 25 | } 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /LayoutTestBase/Core/Generators/LYTImageValues.h: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "LYTDataValues.h" 11 | 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | NS_SWIFT_NAME(ImageValues) 16 | @interface LYTImageValues : LYTDataValues 17 | 18 | @end 19 | 20 | NS_ASSUME_NONNULL_END 21 | 22 | -------------------------------------------------------------------------------- /LayoutTestBase/Core/Generators/LYTImageValues.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "LYTImageValues.h" 11 | 12 | @implementation LYTImageValues 13 | 14 | - (NSArray *)values { 15 | return @[ 16 | [self generateImageWithWidth:1000 height:1000], 17 | [self generateImageWithWidth:10 height:10], 18 | [self generateImageWithWidth:10 height:1000], 19 | [self generateImageWithWidth:1000 height:10], 20 | ]; 21 | } 22 | 23 | - (UIImage *)generateImageWithWidth:(CGFloat)width height:(CGFloat)height { 24 | CGSize size = CGSizeMake(width, height); 25 | UIGraphicsBeginImageContextWithOptions(size, YES, 0); 26 | [[UIColor redColor] setFill]; 27 | UIRectFill(CGRectMake(0, 0, size.width, size.height)); 28 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 29 | UIGraphicsEndImageContext(); 30 | return image; 31 | } 32 | 33 | @end 34 | 35 | -------------------------------------------------------------------------------- /LayoutTestBase/Core/Generators/LYTIntegerValues.h: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "LYTDataValues.h" 11 | 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | NS_SWIFT_NAME(IntegerValues) 16 | @interface LYTIntegerValues : LYTDataValues 17 | 18 | @end 19 | 20 | NS_ASSUME_NONNULL_END 21 | -------------------------------------------------------------------------------- /LayoutTestBase/Core/Generators/LYTIntegerValues.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "LYTIntegerValues.h" 11 | 12 | @implementation LYTIntegerValues 13 | 14 | - (NSArray *)values { 15 | return @[ 16 | @(42), 17 | @(0), 18 | [NSNull null], 19 | @(-34), 20 | @(NSIntegerMax), 21 | @(NSIntegerMin) 22 | ]; 23 | } 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /LayoutTestBase/Core/Generators/LYTStringValues.h: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "LYTDataValues.h" 11 | 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | NS_SWIFT_NAME(StringValues) 16 | @interface LYTStringValues : LYTDataValues 17 | 18 | @end 19 | 20 | NS_ASSUME_NONNULL_END 21 | -------------------------------------------------------------------------------- /LayoutTestBase/Core/Generators/LYTStringValues.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "LYTStringValues.h" 11 | 12 | @implementation LYTStringValues 13 | 14 | - (NSArray *)values { 15 | return @[ 16 | @"Normal length string", 17 | @"", 18 | [NSNull null], 19 | @"Very long string. This string is so long that it's longer than I want it to be. Just a really really long string. In fact, it's just long as long can be. I'm just lengthening the longest string by making it longer with more characters. Do you think this is long enough yet? I think so, so I'm going to stop making this long string longer by adding more characters.", 20 | @"漢語 ♔ 🚂 ☎ ‰ 🚀 Here are some more special characters ˆ™£‡‹·Ú‹›`å∑œ´®∆ƒ∆√˜Ω≥µ˜ƒª•" 21 | ]; 22 | } 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /LayoutTestBase/Core/Generators/Private/LYTValuesIterator.h: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import 11 | 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @class LYTDataValues; 16 | 17 | NS_SWIFT_NAME(ValuesIterator) 18 | @interface LYTValuesIterator : NSObject 19 | 20 | @property (nonatomic, strong, nullable) LYTDataValues *dataValues; 21 | @property (nonatomic) NSUInteger index; 22 | 23 | @end 24 | 25 | NS_ASSUME_NONNULL_END 26 | -------------------------------------------------------------------------------- /LayoutTestBase/Core/Generators/Private/LYTValuesIterator.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "LYTValuesIterator.h" 11 | 12 | @implementation LYTValuesIterator 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /LayoutTestBase/Core/Helpers/LYTMutableCopier.h: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import 11 | 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface NSDictionary (LYTMutableCopier) 16 | 17 | - (NSDictionary *)lyt_mutableDeepCopyWithReplaceBlock:(id(^)(id object))replacementBlock; 18 | 19 | @end 20 | 21 | @interface NSArray (LYTMutableCopier) 22 | 23 | - (NSArray *)lyt_mutableDeepCopyWithReplaceBlock:(id(^)(id object))replacementBlock; 24 | 25 | @end 26 | 27 | NS_ASSUME_NONNULL_END 28 | -------------------------------------------------------------------------------- /LayoutTestBase/Core/Helpers/LYTMutableCopier.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "LYTMutableCopier.h" 11 | 12 | @implementation NSDictionary (LYTMutableCopier) 13 | 14 | - (NSDictionary *)lyt_mutableDeepCopyWithReplaceBlock:(id(^)(id object))replacementBlock { 15 | NSMutableDictionary *newDictionary = [NSMutableDictionary dictionary]; 16 | for (id key in [self allKeys]) { 17 | id object = self[key]; 18 | // Default is just to set it to the same value 19 | id newObject = object; 20 | if ([object isKindOfClass:[NSArray class]]) { 21 | newObject = [(NSArray *)object lyt_mutableDeepCopyWithReplaceBlock:replacementBlock]; 22 | } else if ([object isKindOfClass:[NSDictionary class]]) { 23 | newObject = [(NSDictionary *)object lyt_mutableDeepCopyWithReplaceBlock:replacementBlock]; 24 | } else if (replacementBlock) { 25 | newObject = replacementBlock(object); 26 | } 27 | if (newObject) { 28 | // New object can be nil. If so, we'll just leave it out. 29 | [newDictionary setObject:newObject forKey:key]; 30 | } 31 | } 32 | return newDictionary; 33 | } 34 | 35 | @end 36 | 37 | @implementation NSArray (LYTMutableCopier) 38 | 39 | - (NSArray *)lyt_mutableDeepCopyWithReplaceBlock:(id(^)(id object))replacementBlock { 40 | NSMutableArray *newArray = [NSMutableArray array]; 41 | for (id object in self) { 42 | // Default is just to set it to the same value 43 | id newObject = object; 44 | if ([object isKindOfClass:[NSArray class]]) { 45 | newObject = [(NSArray *)object lyt_mutableDeepCopyWithReplaceBlock:replacementBlock]; 46 | } else if ([object isKindOfClass:[NSDictionary class]]) { 47 | newObject = [(NSDictionary *)object lyt_mutableDeepCopyWithReplaceBlock:replacementBlock]; 48 | } else if (replacementBlock) { 49 | newObject = replacementBlock(object); 50 | } 51 | if (newObject) { 52 | // New object can be nil. If so, we'll just leave it out. 53 | [newArray addObject:newObject]; 54 | } 55 | } 56 | return newArray; 57 | } 58 | 59 | @end 60 | -------------------------------------------------------------------------------- /LayoutTestBase/Core/LYTViewProvider.h: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import 11 | 12 | 13 | @class LYTViewSize; 14 | 15 | NS_ASSUME_NONNULL_BEGIN 16 | 17 | NS_SWIFT_NAME(ViewProvider) 18 | @protocol LYTViewProvider 19 | 20 | #pragma mark - Main Methods 21 | 22 | /** 23 | This is the template for the dictionary that will be passed to the viewForData:reuseView: method. You can insert LYTDataValues subclasses as values in this dictionary for generating different combinations of data. For example, the dictionary: 24 | 25 | @{ @"key": [[LYTStringValues alloc] init] } 26 | 27 | Will produce a bunch of dictionaries: 28 | 29 | @{ @"key": @"Normal length string" }, 30 | @{ @"key": @"" }, 31 | @{ }, etc. 32 | 33 | \returns Template for the data to test on. 34 | */ 35 | @required 36 | + (nullable NSDictionary *)dataSpecForTestWithError:(NSError * _Nullable *)error; 37 | 38 | /** 39 | This method should return a view to run your tests on. You should always use the reuse view if you can, and if not, should recreate a view with the same size as the reuseView. You should then inflate the view with your data and return it. 40 | 41 | You may optionally set some context. This can be anything and will be passed back to you in the completion block of the test. You should first check that context is not nil before trying to set it. 42 | 43 | \param data Data to inflate int the view. This data will not contain any LYTDataValues subclasses. It is nonoptional and will never be nil. 44 | \param view Reuse view to reinflate with new data. It may be nil. 45 | \param context You can set this to anything and it will be returned to you in the completion block of the test. Could be nil. 46 | \returns View inflated with data 47 | */ 48 | @required 49 | + (nullable UIView *)viewForData:(NSDictionary *)data 50 | reuseView:(nullable UIView *)reuseView 51 | size:(nullable LYTViewSize *)size 52 | context:(id _Nullable * _Nullable)context 53 | error:(NSError * _Nullable *)error; 54 | 55 | /** 56 | Returns an array of NSValue objects which wrap a CGSize struct. These are all the sizes that we should test on. The way this works is a little tricky though. It will return a reuse view of this specific size to viewForData:reuseView:. So if that method resizes the view or recreates the view, then this data will be lost. 57 | 58 | The data will first be tested on the 'default size' of the view or whatever is returned by viewForData:, then iterate through each of these sizes. First it will resize the view, then pass it to viewForData:reuseView: 59 | 60 | \returns NSArray of NSValues which encapsulate an CGSize 61 | */ 62 | @optional 63 | + (NSArray *)sizesForView; 64 | 65 | /** 66 | This is called after viewForData:reuseView:size:context: and after the view has been resized using the sizes provided. This gives you the opportunity to make any final changes to the view based on the size of the view before any assertions are made. 67 | 68 | For example, if you use multiple different widths to test your view, you may want to adjust the height in this method. 69 | 70 | \param view The view which we are about to test. This is already setup with data. 71 | \param data The data used to layout the view. 72 | \param size This size used to resize the view. This size has already been applied. 73 | \param context If a context was set in viewForData:reuseView:size:context:, it will be passed back to you here. 74 | */ 75 | @optional 76 | + (void)adjustViewSize:(UIView *)view 77 | data:(NSDictionary *)data 78 | size:(nullable LYTViewSize *)size 79 | context:(nullable id)context 80 | error:(NSError * _Nullable *)error 81 | __attribute__((swift_error(nonnull_error))); 82 | 83 | @end 84 | 85 | NS_ASSUME_NONNULL_END 86 | -------------------------------------------------------------------------------- /LayoutTestBase/Core/Testers/LYTLayoutPropertyTester.h: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import 11 | 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | /** 16 | This enum is a bit mask (aka NS_OPTIONS/OptionSet) which you can use to limit results. 17 | 18 | LYTTesterLimitResultsNone - Default value. No options are set. 19 | LYTTesterLimitResultsLimitDataCombinations - Usually, we do all possible combinations of the data. However, if you set this flag, we will do a polynomial 20 | set of options instead of exponential. Every value in the data set will be run at least once, but not all possible combinations will be run. 21 | LYTTesterLimitResultsNoSizes - This will ignore any view sizes that are set in the LYTViewProvider or the LYTConfig. 22 | */ 23 | NS_SWIFT_NAME(TesterLimitResults) 24 | typedef NS_OPTIONS(NSInteger, LYTTesterLimitResults) { 25 | LYTTesterLimitResultsNone = 0, 26 | LYTTesterLimitResultsLimitDataCombinations = 1 << 0, 27 | LYTTesterLimitResultsNoSizes = 1 << 1 28 | }; 29 | 30 | NS_SWIFT_NAME(LayoutPropertyTester) 31 | @interface LYTLayoutPropertyTester : NSObject 32 | 33 | /** 34 | Runs a suite of tests on a given viewProvider. This is the main method to be used for your unit tests. 35 | 36 | Instead of calling this directly, it's more common to subclass the more powerful LYTLayoutTestCase. 37 | 38 | \param viewProvider Class to test. Must conform to LYTViewProvider. 39 | \param validation Block to validate the view given the data. The data here will not contain any LYTDataValues subclasses and both the view and data 40 | will never be nil. Here you should assert on the properties of the view. If you set a context in your viewForData: method, it will be passed back here. 41 | */ 42 | + (void)runPropertyTestsWithViewProvider:(Class)viewProvider validation:(NS_NOESCAPE void(^)(id view, NSDictionary *data, _Nullable id context))validation; 43 | 44 | /** 45 | Runs a suite of tests on a given viewProvider. This is the main method to be used for your unit tests. 46 | 47 | Instead of calling this directly, it's more common to subclass the more powerful LYTLayoutTestCase. 48 | 49 | \param viewProvider Class to test. Must conform to LYTViewProvider. 50 | \param validation Block to validate the view given the data. The data here will not contain any LYTDataValues subclasses and both the view and data 51 | will never be nil. Here you should assert on the properties of the view. If you set a context in your viewForData: method, it will be passed back here. 52 | \param limitResults Use this parameter to run less combinations. This is useful if you're running into performance problems. See LYTTesterLimitResults 53 | docs for more info. 54 | */ 55 | + (void)runPropertyTestsWithViewProvider:(Class)viewProvider limitResults:(LYTTesterLimitResults)limitResults validation:(NS_NOESCAPE void(^)(id view, NSDictionary *data, id _Nullable context))validation; 56 | 57 | @end 58 | 59 | NS_ASSUME_NONNULL_END 60 | -------------------------------------------------------------------------------- /LayoutTestBase/Core/ViewSizing/LYTDeviceConstants.h: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import 11 | 12 | /** 13 | These constants define Apple device dimensions in points. They are useful when defining a LYTViewSize. 14 | 15 | All the values are in points (not pixels). Width is by convention always the shorter side. 16 | */ 17 | 18 | extern CGFloat const LYTiPhone4Width; 19 | extern CGFloat const LYTiPhone4Height; 20 | extern CGFloat const LYTiPhone5Width; 21 | extern CGFloat const LYTiPhone5Height; 22 | extern CGFloat const LYTiPhone6Width; 23 | extern CGFloat const LYTiPhone6Height; 24 | extern CGFloat const LYTiPhone6PlusWidth; 25 | extern CGFloat const LYTiPhone6PlusHeight; 26 | extern CGFloat const LYTiPadWidth; 27 | extern CGFloat const LYTiPadHeight; 28 | extern CGFloat const LYTiPadProWidth; 29 | extern CGFloat const LYTiPadProHeight; 30 | extern CGFloat const LYTAppleWatch38Width; 31 | extern CGFloat const LYTAppleWatch38Height; 32 | extern CGFloat const LYTAppleWatch42Width; 33 | extern CGFloat const LYTAppleWatch42Height; 34 | -------------------------------------------------------------------------------- /LayoutTestBase/Core/ViewSizing/LYTDeviceConstants.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "LYTDeviceConstants.h" 11 | 12 | 13 | CGFloat const LYTiPhone4Width = 320; 14 | CGFloat const LYTiPhone4Height = 480; 15 | CGFloat const LYTiPhone5Width = 320; 16 | CGFloat const LYTiPhone5Height = 568; 17 | CGFloat const LYTiPhone6Width = 375; 18 | CGFloat const LYTiPhone6Height = 667; 19 | CGFloat const LYTiPhone6PlusWidth = 414; 20 | CGFloat const LYTiPhone6PlusHeight = 736; 21 | CGFloat const LYTiPhone15Width = 390; 22 | CGFloat const LYTiPhone15Height = 844; 23 | CGFloat const LYTiPhone15PlusWidth = 430; 24 | CGFloat const LYTiPhone15PlusHeight = 932; 25 | CGFloat const LYTiPhone15ProWidth = 393; 26 | CGFloat const LYTiPhone15ProHeight = 852; 27 | CGFloat const LYTiPhone15ProMaxWidth = 430; 28 | CGFloat const LYTiPhone15ProMaxHeight = 932; 29 | CGFloat const LYTiPadWidth = 768; 30 | CGFloat const LYTiPadHeight = 1024; 31 | CGFloat const LYTiPadProWidth = 1024; 32 | CGFloat const LYTiPadProHeight = 1366; 33 | CGFloat const LYTAppleWatch38Width = 272; 34 | CGFloat const LYTAppleWatch38Height = 340; 35 | CGFloat const LYTAppleWatch42Width = 312; 36 | CGFloat const LYTAppleWatch42Height = 390; 37 | -------------------------------------------------------------------------------- /LayoutTestBase/Core/ViewSizing/LYTViewSize.h: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | /** 15 | When providing sizes to test your view on, you may want to test the view on different widths, heights or both. This object let's you specify sizes 16 | like this. Both width and height are optional. So, if you specify a width and no height, then the library will resize your view's width, but not edit the height. 17 | 18 | For example, let's say you're testing a UITableViewCell subclass with a dynamic height. You want to test it on different devices, so you return 19 | 20 | @code 21 | @[ [[LYTViewSize alloc] initWithWidth:@(LYTDeviceConstants.LYTiPhone4Width)], [[LYTViewSize alloc] initWithWidth:@(LYTDeviceConstants.LYTiPhone6PlusWidth)] ] 22 | @endcode 23 | 24 | This will test the view on multiple different widths, but not change the height. If you need to manually set the height based on the data being set, 25 | then you should implement adjustViewSize:data:size:context:. 26 | 27 | 28 | You can utitilize predefined constants or enter a custom value. 29 | */ 30 | NS_SWIFT_NAME(ViewSize) 31 | @interface LYTViewSize : NSObject 32 | 33 | /** 34 | The width for the view. If nil, do not edit the width. 35 | */ 36 | @property (nonatomic, strong, readonly, nullable) NSNumber *width NS_REFINED_FOR_SWIFT; 37 | /** 38 | The height for the view. If nil, do not edit the height. 39 | */ 40 | @property (nonatomic, strong, readonly, nullable) NSNumber *height NS_REFINED_FOR_SWIFT; 41 | 42 | /** 43 | Creates a view size which will change both the width and the height of the view. 44 | */ 45 | - (instancetype)initWithWidth:(nullable NSNumber *)width height:(nullable NSNumber *)height NS_REFINED_FOR_SWIFT; 46 | 47 | /** 48 | Creates a view size which will only change the width of the view. 49 | */ 50 | - (instancetype)initWithWidth:(NSNumber *)width NS_SWIFT_UNAVAILABLE("init(width:height:) takes optional arguments instead"); 51 | 52 | /** 53 | Creates a view size which will only change the height of the view. 54 | */ 55 | 56 | - (instancetype)initWithHeight:(NSNumber *)height NS_SWIFT_UNAVAILABLE("init(width:height:) takes optional arguments instead"); 57 | 58 | @end 59 | 60 | NS_ASSUME_NONNULL_END 61 | -------------------------------------------------------------------------------- /LayoutTestBase/Core/ViewSizing/LYTViewSize.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "LYTViewSize.h" 11 | 12 | 13 | @interface LYTViewSize () 14 | 15 | @property (nonatomic, strong, nullable) NSNumber *width; 16 | @property (nonatomic, strong, nullable) NSNumber *height; 17 | 18 | @end 19 | 20 | @implementation LYTViewSize 21 | 22 | - (id)initWithWidth:(NSNumber *)width height:(NSNumber *)height { 23 | self = [super init]; 24 | self.width = width; 25 | self.height = height; 26 | return self; 27 | } 28 | 29 | - (instancetype)initWithWidth:(NSNumber *)width { 30 | return [self initWithWidth:width height:nil]; 31 | } 32 | 33 | - (instancetype)initWithHeight:(NSNumber *)height { 34 | return [self initWithWidth:nil height:height]; 35 | } 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /LayoutTestBase/Core/ViewSizing/UIView+LYTViewSize.h: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import 11 | #import "LYTViewSize.h" 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface UIView (LYTViewSize) 16 | 17 | - (void)lyt_setSize:(nullable LYTViewSize *)size NS_SWIFT_NAME(setSize(_:)); 18 | 19 | @end 20 | 21 | NS_ASSUME_NONNULL_END 22 | -------------------------------------------------------------------------------- /LayoutTestBase/Core/ViewSizing/UIView+LYTViewSize.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "UIView+LYTViewSize.h" 11 | 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @implementation UIView (LYTViewSize) 16 | 17 | - (void)lyt_setSize:(nullable LYTViewSize *)size { 18 | CGFloat height = self.bounds.size.height; 19 | if (size.height) { 20 | height = size.height.floatValue; 21 | } 22 | CGFloat width = self.bounds.size.width; 23 | if (size.width) { 24 | width = size.width.floatValue; 25 | } 26 | 27 | self.bounds = CGRectMake(self.bounds.origin.x, self.bounds.origin.y, width, height); 28 | } 29 | 30 | @end 31 | 32 | NS_ASSUME_NONNULL_END 33 | -------------------------------------------------------------------------------- /LayoutTestBase/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /LayoutTestBase/LayoutTestBase.h: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | /** 11 | This file is included in cocoapods because without it, Xcode refuses to build the project. It shouldn't cause any harm. 12 | */ 13 | 14 | #import 15 | #import 16 | 17 | //! Project version number for LayoutTest. 18 | FOUNDATION_EXPORT double LayoutTestBaseVersionNumber; 19 | 20 | //! Project version string for LayoutTest. 21 | FOUNDATION_EXPORT const unsigned char LayoutTestBaseVersionString[]; 22 | 23 | #pragma mark Main Classes 24 | 25 | #import "LYTViewProvider.h" 26 | #import "LYTViewCatalogProvider.h" 27 | #import "LYTConfig.h" 28 | #import "LYTLayoutPropertyTester.h" 29 | 30 | #pragma mark View Sizing 31 | 32 | #import "LYTViewSize.h" 33 | #import "UIView+LYTViewSize.h" 34 | #import "LYTDeviceConstants.h" 35 | 36 | #pragma mark Auto Layout 37 | 38 | #import "LYTAutolayoutFailureIntercepter.h" 39 | 40 | #pragma mark Data Values 41 | 42 | #import "LYTDataValues.h" 43 | #import "LYTBoolValues.h" 44 | #import "LYTFloatValues.h" 45 | #import "LYTIntegerValues.h" 46 | #import "LYTStringValues.h" 47 | 48 | #pragma mark Test Helpers 49 | 50 | #import "UIView+LYTHelpers.h" 51 | #import "UIView+LYTTestHelpers.h" 52 | #import "UIView+LYTFrameComparison.h" 53 | 54 | #pragma mark Catalog 55 | 56 | #import "LYTCatalogCollectionViewController.h" 57 | #import "LYTCatalogTableViewController.h" 58 | -------------------------------------------------------------------------------- /LayoutTestBase/Swift/LYTDataValues+Swift.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LYTDataValues+Swift.swift 3 | // LayoutTest 4 | // 5 | // Created by Peter Livesey on 9/30/16. 6 | // Copyright © 2016 LinkedIn. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension DataValues { 12 | /** 13 | Returns LYTDataValues with these specific values and overrides any subclass's behavior. 14 | This function converts all nil values to NSNull which is what the library expects. 15 | These will be represented as empty entries in the dictionary. 16 | */ 17 | public convenience init(values: [Any?]) { 18 | let array: [Any] = values.map { value in 19 | if let value = value { 20 | return value 21 | } else { 22 | return NSNull() 23 | } 24 | } 25 | self.init(__values: array) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LayoutTestBase/Swift/LYTViewSize+Swift.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LYTViewSize+Swift.swift 3 | // LayoutTest 4 | // 5 | // Created by Peter Livesey on 9/30/16. 6 | // Copyright © 2016 LinkedIn. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension ViewSize { 12 | 13 | /** 14 | The width for the view. If nil, do not edit the width. 15 | */ 16 | public var width: CGFloat? { 17 | get { 18 | return __width.flatMap { CGFloat( $0.floatValue ) } 19 | } 20 | } 21 | 22 | /** 23 | The height for the view. If nil, do not edit the height. 24 | */ 25 | public var height: CGFloat? { 26 | get { 27 | return __height.flatMap { CGFloat( $0.floatValue ) } 28 | } 29 | } 30 | 31 | /** 32 | Creates a view size which will either the width, height, or both. 33 | If width or height is set to nil, that dimension will not be changed. 34 | */ 35 | public convenience init(width: CGFloat? = nil, height: CGFloat? = nil) { 36 | self.init(__width: width.flatMap { NSNumber(value: Double($0)) }, height: height.flatMap { NSNumber(value: Double($0)) }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LayoutTestBase/UIViewHelpers/UIView+LYTFrameComparison.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "UIView+LYTFrameComparison.h" 11 | #import "UIView+LYTHelpers.h" 12 | #import "LYTConfig.h" 13 | 14 | 15 | @implementation UIView (LYTFrameComparison) 16 | 17 | #pragma mark - Frames 18 | 19 | - (BOOL)lyt_before:(UIView *)otherView { 20 | return [self lyt_before:otherView fromCenter:NO]; 21 | } 22 | 23 | - (BOOL)lyt_before:(UIView *)otherView fromCenter:(BOOL)fromCenter { 24 | CGRect otherViewBounds = [self convertRect:otherView.bounds fromView:otherView]; 25 | CGFloat epsilon = [LYTConfig sharedInstance].cgFloatEpsilon; 26 | 27 | if ([self lyt_leftToRight]) { 28 | if (fromCenter) { 29 | return self.center.x <= otherViewBounds.origin.x + epsilon; 30 | } else { 31 | return self.bounds.origin.x + self.bounds.size.width <= otherViewBounds.origin.x + epsilon; 32 | } 33 | } else { 34 | if (fromCenter) { 35 | return self.bounds.origin.x + epsilon >= (otherViewBounds.origin.x + otherViewBounds.size.width/2); 36 | } else { 37 | return self.bounds.origin.x + epsilon >= otherViewBounds.origin.x + otherViewBounds.size.width; 38 | } 39 | } 40 | } 41 | 42 | - (BOOL)lyt_after:(UIView *)otherView { 43 | return [otherView lyt_before:self]; 44 | } 45 | 46 | - (BOOL)lyt_after:(UIView *)otherView fromCenter:(BOOL)fromCenter { 47 | return [otherView lyt_before:self fromCenter:fromCenter]; 48 | } 49 | 50 | - (BOOL)lyt_above:(UIView *)otherView { 51 | CGRect otherViewBounds = [self convertRect:otherView.bounds fromView:otherView]; 52 | CGFloat epsilon = [LYTConfig sharedInstance].cgFloatEpsilon; 53 | 54 | return self.bounds.origin.y + self.bounds.size.height <= otherViewBounds.origin.y + epsilon; 55 | } 56 | 57 | - (BOOL)lyt_below:(UIView *)otherView { 58 | return [otherView lyt_above:self]; 59 | } 60 | 61 | #pragma mark - Alignment 62 | 63 | - (BOOL)lyt_leadingAligned:(UIView *)otherView { 64 | CGRect otherViewBounds = [self convertRect:otherView.bounds fromView:otherView]; 65 | if ([self lyt_leftToRight]) { 66 | return LYTEpsilonEqual(self.bounds.origin.x, otherViewBounds.origin.x); 67 | } else { 68 | return LYTEpsilonEqual(self.bounds.origin.x + self.bounds.size.width, otherViewBounds.origin.x + otherViewBounds.size.width); 69 | } 70 | } 71 | 72 | - (BOOL)lyt_trailingAligned:(UIView *)otherView { 73 | CGRect otherViewBounds = [self convertRect:otherView.bounds fromView:otherView]; 74 | if ([self lyt_leftToRight]) { 75 | return LYTEpsilonEqual(self.bounds.origin.x + self.bounds.size.width, otherViewBounds.origin.x + otherViewBounds.size.width); 76 | } else { 77 | return LYTEpsilonEqual(self.bounds.origin.x, otherViewBounds.origin.x); 78 | } 79 | } 80 | 81 | - (BOOL)lyt_topAligned:(UIView *)otherView { 82 | CGRect otherViewBounds = [self convertRect:otherView.bounds fromView:otherView]; 83 | return LYTEpsilonEqual(self.bounds.origin.y, otherViewBounds.origin.y); 84 | } 85 | 86 | - (BOOL)lyt_bottomAligned:(UIView *)otherView { 87 | CGRect otherViewBounds = [self convertRect:otherView.bounds fromView:otherView]; 88 | return LYTEpsilonEqual(self.bounds.origin.y + self.bounds.size.height, otherViewBounds.origin.y + otherViewBounds.size.height); 89 | } 90 | 91 | #pragma mark - Private Helpers 92 | 93 | - (BOOL)lyt_leftToRight { 94 | switch ([UIApplication sharedApplication].userInterfaceLayoutDirection) { 95 | case UIUserInterfaceLayoutDirectionLeftToRight: 96 | return YES; 97 | case UIUserInterfaceLayoutDirectionRightToLeft: 98 | return NO; 99 | } 100 | } 101 | 102 | @end 103 | -------------------------------------------------------------------------------- /LayoutTestBase/UIViewHelpers/UIView+LYTHelpers.h: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | /** 15 | This file exposes some useful helpers for testing views. 16 | */ 17 | @interface UIView (LYTHelpers) 18 | 19 | /** 20 | Returns .frame.origin.y 21 | 22 | When setting, it will keep the height and width constant and change frame.origin.y 23 | */ 24 | @property (nonatomic) CGFloat lyt_top; 25 | 26 | /** 27 | Returns .frame.origin.x 28 | 29 | When setting, it will keep the height and width constant and change frame.origin.x 30 | */ 31 | @property (nonatomic) CGFloat lyt_left; 32 | 33 | /** 34 | Returns .frame.origin.x + .frame.size.width 35 | 36 | When setting, it will keep the height and width constant and change frame.origin.x 37 | */ 38 | @property (nonatomic) CGFloat lyt_right; 39 | 40 | /** 41 | Returns .frame.origin.y + .frame.size.height 42 | 43 | When setting, it will keep the height and width constant and change frame.origin.y 44 | */ 45 | @property (nonatomic) CGFloat lyt_bottom; 46 | 47 | /** 48 | Returns the width of the view 49 | */ 50 | @property (nonatomic) CGFloat lyt_width; 51 | 52 | /** 53 | Returns the height of the view 54 | */ 55 | @property (nonatomic) CGFloat lyt_height; 56 | 57 | @end 58 | 59 | NS_ASSUME_NONNULL_END 60 | -------------------------------------------------------------------------------- /LayoutTestBase/UIViewHelpers/UIView+LYTHelpers.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "UIView+LYTHelpers.h" 11 | 12 | @implementation UIView (LYTHelpers) 13 | 14 | - (CGFloat)lyt_left { 15 | return self.frame.origin.x; 16 | } 17 | 18 | - (void)setLyt_left:(CGFloat)x { 19 | CGRect frame = self.frame; 20 | frame.origin.x = x; 21 | self.frame = frame; 22 | } 23 | 24 | - (CGFloat)lyt_top { 25 | return self.frame.origin.y; 26 | } 27 | 28 | - (void)setLyt_top:(CGFloat)y { 29 | CGRect frame = self.frame; 30 | frame.origin.y = y; 31 | self.frame = frame; 32 | } 33 | 34 | - (CGFloat)lyt_right { 35 | return self.frame.origin.x + self.frame.size.width; 36 | } 37 | 38 | - (void)setLyt_right:(CGFloat)right { 39 | self.lyt_left = right - self.lyt_width; 40 | } 41 | 42 | - (CGFloat)lyt_bottom { 43 | return self.frame.origin.y + self.frame.size.height; 44 | } 45 | 46 | - (void)setLyt_bottom:(CGFloat)bottom { 47 | self.lyt_top = bottom - self.lyt_height; 48 | } 49 | 50 | - (CGFloat)lyt_width { 51 | return self.frame.size.width; 52 | } 53 | 54 | - (void)setLyt_width:(CGFloat)width { 55 | CGRect frame = self.frame; 56 | frame.size.width = width; 57 | self.frame = frame; 58 | } 59 | 60 | - (CGFloat)lyt_height { 61 | return self.frame.size.height; 62 | } 63 | 64 | - (void)setLyt_height:(CGFloat)height { 65 | CGRect frame = self.frame; 66 | frame.size.height = height; 67 | self.frame = frame; 68 | } 69 | 70 | @end 71 | -------------------------------------------------------------------------------- /LayoutTestBase/UIViewHelpers/UIView+LYTTestHelpers.h: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface UIView (LYTTestHelpers) 15 | 16 | /** 17 | Useful helper for writing tests. Ensures that all the subviews of the view are within the bounds of their superviews. If this condition breaks, it 18 | calls the error block. This is not recursive. 19 | 20 | When comparing CGFloats, (x > y) if and only if (x > y + epsilon). This epsilon value is defined in LYTConfig. 21 | 22 | @discussion When calling this on UITableViewCells, you should probably call it on 23 | cell.contentView because it sometimes fails for the contentView in it's 24 | superview due to some UIKit weirdness. 25 | */ 26 | - (void)lyt_assertViewWithinSuperViewBounds:(void(^)(NSString *error, UIView *view))errorBlock NS_SWIFT_NAME(lyt_assertViewWithinSuperViewBounds(_:)); 27 | 28 | /** 29 | Useful helper for writing tests. Ensures recursively that all the subviews of the view are within the bounds of their superviews. If this condition breaks, 30 | it calls the error block. 31 | 32 | When comparing CGFloats, (x > y) if and only if (x > y + epsilon). This epsilon value is defined in LYTConfig. 33 | 34 | @discussion When calling this on UITableViewCells, you should probably call it 35 | on cell.contentView because it sometimes fails for the contentView in it's 36 | superview due to some UIKit weirdness. 37 | */ 38 | - (void)lyt_recursivelyAssertViewWithinSuperViewBounds:(NS_NOESCAPE void(^)(NSString *error, UIView *view))errorBlock NS_SWIFT_NAME(lyt_recursivelyAssertViewWithinSuperViewBounds(_:)); 39 | 40 | /** 41 | Useful helper for writing tests. Ensures that none of the subviews of this view overlap. 42 | 43 | When comparing CGFloats, (x > y) if and only if (x > y + epsilon). This epsilon value is defined in LYTConfig. 44 | */ 45 | - (void)lyt_assertNoSubviewsOverlap:(NS_NOESCAPE void(^)(NSString *error, UIView *view1, UIView *view2))errorBlock NS_SWIFT_NAME(lyt_assertNoSubviewsOverlap(_:)); 46 | 47 | /** 48 | Useful helper for writing tests. Ensures that none of the subviews of this view overlap. It calls this recursively on subviews, but does not test all 49 | combinations of subviews (ie. one subview from view tree A and a subview from view tree B). It only tests all combinations of immediate subviews. 50 | However, if you combine this with lyt_recursivelyAssertViewWithinSuperViewBounds: then you can be sure that no subviews from different trees will overlap. 51 | 52 | When comparing CGFloats, (x > y) if and only if (x > y + epsilon). This epsilon value is defined in LYTConfig. 53 | */ 54 | - (void)lyt_recursivelyAssertNoSubviewsOverlap:(NS_NOESCAPE void(^)(NSString *error, UIView *view1, UIView *view2))errorBlock NS_SWIFT_NAME(lyt_recursivelyAssertNoSubviewsOverlap(_:)); 55 | 56 | /** 57 | This method first returns the current view, then traverses the view hierarchy. 58 | */ 59 | - (void)lyt_recursivelyTraverseViewHierarchy:(NS_NOESCAPE void(^)(UIView *subview))subviewBlock NS_SWIFT_NAME(lyt_recursivelyTraverseViewHierarchy(_:)); 60 | 61 | /** 62 | This method first returns the current view, then traverses the view hierarchy. 63 | 64 | It also provides a stop parameter. If you set this to true, then the method will stop recursing on this branch only. 65 | */ 66 | - (void)lyt_recursivelyTraverseViewHierarchyWithStop:(NS_NOESCAPE void(^)(UIView *subview, BOOL *stopBranch))subviewBlock NS_SWIFT_NAME(lyt_recursivelyTraverseViewHierarchyWithStop(_:)); 67 | 68 | @end 69 | 70 | NS_ASSUME_NONNULL_END 71 | -------------------------------------------------------------------------------- /LayoutTestTests/AutolayoutFailureInterceptorTests.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import 11 | #import "LYTAutolayoutFailureIntercepter.h" 12 | // Helpers 13 | #import "UnitTestViews.h" 14 | 15 | 16 | @interface AutolayoutFailureInterceptorTests : XCTestCase 17 | 18 | @end 19 | 20 | @implementation AutolayoutFailureInterceptorTests 21 | 22 | - (void)tearDown { 23 | [LYTAutolayoutFailureIntercepter stopInterceptingAutolayoutFailures]; 24 | [super tearDown]; 25 | } 26 | 27 | - (void)testAutolayoutFail { 28 | __block BOOL autolayoutFailed = NO; 29 | [LYTAutolayoutFailureIntercepter interceptAutolayoutFailuresWithBlock:^{ 30 | autolayoutFailed = YES; 31 | }]; 32 | 33 | UIView *view = [UnitTestViews viewWithIncorrectAutolayout]; 34 | [view layoutIfNeeded]; 35 | 36 | XCTAssertTrue(autolayoutFailed); 37 | } 38 | 39 | - (void)testAutolayoutSuccess { 40 | __block BOOL autolayoutFailed = NO; 41 | [LYTAutolayoutFailureIntercepter interceptAutolayoutFailuresWithBlock:^{ 42 | autolayoutFailed = YES; 43 | }]; 44 | 45 | UIView *view = [UnitTestViews viewWithNoProblems]; 46 | [view layoutIfNeeded]; 47 | 48 | XCTAssertFalse(autolayoutFailed); 49 | } 50 | 51 | - (void)testTurningOffIntercepter { 52 | __block BOOL autolayoutFailed = NO; 53 | [LYTAutolayoutFailureIntercepter interceptAutolayoutFailuresWithBlock:^{ 54 | autolayoutFailed = YES; 55 | }]; 56 | 57 | [LYTAutolayoutFailureIntercepter stopInterceptingAutolayoutFailures]; 58 | 59 | UIView *view = [UnitTestViews viewWithIncorrectAutolayout]; 60 | [view layoutIfNeeded]; 61 | 62 | XCTAssertFalse(autolayoutFailed); 63 | } 64 | 65 | - (void)testTurningOnIntercepterTwice { 66 | __block BOOL autolayoutFailed = NO; 67 | [LYTAutolayoutFailureIntercepter interceptAutolayoutFailuresWithBlock:^{ 68 | autolayoutFailed = YES; 69 | }]; 70 | [LYTAutolayoutFailureIntercepter interceptAutolayoutFailuresWithBlock:^{ 71 | autolayoutFailed = YES; 72 | }]; 73 | 74 | UIView *view = [UnitTestViews viewWithIncorrectAutolayout]; 75 | [view layoutIfNeeded]; 76 | 77 | XCTAssertTrue(autolayoutFailed); 78 | } 79 | 80 | - (void)testTurningOffIntercepterTwice { 81 | __block BOOL autolayoutFailed = NO; 82 | [LYTAutolayoutFailureIntercepter interceptAutolayoutFailuresWithBlock:^{ 83 | autolayoutFailed = YES; 84 | }]; 85 | 86 | [LYTAutolayoutFailureIntercepter stopInterceptingAutolayoutFailures]; 87 | [LYTAutolayoutFailureIntercepter stopInterceptingAutolayoutFailures]; 88 | 89 | UIView *view = [UnitTestViews viewWithIncorrectAutolayout]; 90 | [view layoutIfNeeded]; 91 | 92 | XCTAssertFalse(autolayoutFailed); 93 | } 94 | 95 | @end 96 | -------------------------------------------------------------------------------- /LayoutTestTests/DataValuesTests.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import 11 | #import 12 | #import "LYTStringValues.h" 13 | 14 | 15 | @interface DataValuesTests : XCTestCase 16 | 17 | @end 18 | 19 | @implementation DataValuesTests 20 | 21 | - (void)testRequiredGeneratorTrue { 22 | LYTDataValues *generator = [[LYTStringValues alloc] initWithRequired:YES]; 23 | for (NSUInteger i = 0; i<[generator numberOfValues]; i++) { 24 | XCTAssertNotEqualObjects([NSNull null], [generator valueAtIndex:i]); 25 | } 26 | } 27 | 28 | - (void)testRequiredGeneratorFalse { 29 | LYTDataValues *generator = [[LYTStringValues alloc] initWithRequired:NO]; 30 | BOOL foundNull = false; 31 | for (NSUInteger i = 0; i<[generator numberOfValues]; i++) { 32 | foundNull = foundNull || [[generator valueAtIndex:i] isEqual:[NSNull null]]; 33 | } 34 | XCTAssertTrue(foundNull); 35 | } 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /LayoutTestTests/Helpers/UIViewWithLabel.h: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import 11 | 12 | @interface UIViewWithLabel : UIView 13 | 14 | @property (nonatomic) UILabel *label; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /LayoutTestTests/Helpers/UIViewWithLabel.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "UIViewWithLabel.h" 11 | 12 | @implementation UIViewWithLabel 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /LayoutTestTests/Helpers/UnitTestViews.h: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import 11 | #import 12 | 13 | @class UIViewWithLabel; 14 | 15 | @interface UnitTestViews : NSObject 16 | 17 | + (UIView *)viewWithNoProblems; 18 | 19 | + (UIView *)viewWithIncorrectAutolayout; 20 | 21 | + (UIView *)viewWithOverlappingViews; 22 | 23 | + (UIView *)viewWithUISwitchSubview; 24 | 25 | + (UIView *)viewWithSubviewOutOfSuperview; 26 | 27 | + (UIView *)viewWithAmbiguousLayout; 28 | 29 | + (UIView *)viewWithButtonAndAccessibility; 30 | 31 | + (UIView *)viewWithButtonAndNoAccessibility; 32 | 33 | + (UIView *)viewWithNestedAccessibility; 34 | 35 | + (UIView *)viewWithAccessibilityIDButNoLabel; 36 | 37 | + (UIViewWithLabel *)viewWithLongStringOverlappingLabel; 38 | 39 | + (UIButton *)buttonWithBackgroundImage; 40 | 41 | @end 42 | -------------------------------------------------------------------------------- /LayoutTestTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /LayoutTestTests/LayoutTestCaseTests/LayoutReportSnapshotLocationsTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutReportSnapshotLocations.m 3 | // LayoutTest 4 | // 5 | // Created by Jock Findlay on 09/02/2016. 6 | // Copyright © 2016 LinkedIn. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "LYTLayoutTestCase.h" 11 | #import "LYTLayoutFailingTestSnapshotRecorder.h" 12 | 13 | @interface LYTLayoutFailingTestSnapshotRecorder () 14 | @property (nonatomic) NSMutableSet *failingTestsSnapshotFolders; 15 | @end 16 | 17 | @interface LayoutReportSnapshotLocationsTests : LYTLayoutTestCase 18 | @end 19 | 20 | @implementation LayoutReportSnapshotLocationsTests 21 | 22 | - (void)testAllLocationsOfReportsAreLoggedToConsoleAtTheEnd { 23 | XCTIssue *issue = [[XCTIssue alloc] initWithType:XCTIssueTypeAssertionFailure compactDescription:@""]; 24 | [[LYTLayoutFailingTestSnapshotRecorder sharedInstance] testCase:self didRecordIssue:issue]; 25 | NSString *expectedPathContaining = @"LayoutTestImages/LayoutReportSnapshotLocationsTests/index.html"; 26 | NSString *actualPath = [LYTLayoutFailingTestSnapshotRecorder sharedInstance].failingTestsSnapshotFolders.anyObject; 27 | XCTAssertTrue([actualPath containsString:expectedPathContaining]); 28 | } 29 | 30 | @end 31 | -------------------------------------------------------------------------------- /LayoutTestTests/LayoutTestCaseTests/LayoutTestCaseAccessibilityControlTests.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "LayoutTest.h" 11 | #import "LayoutTestBase.h" 12 | #import "UnitTestViews.h" 13 | 14 | 15 | @interface LayoutTestCaseAccessibilityControlTests : LYTLayoutTestCase 16 | 17 | @property (nonatomic) NSInteger testFailures; 18 | 19 | @end 20 | 21 | @implementation LayoutTestCaseAccessibilityControlTests 22 | 23 | - (void)testFails { 24 | __block NSInteger timesCalled = 0; 25 | [self runLayoutTestsWithViewProvider:[self class] validation:^(__unused UIView * view, __unused NSDictionary * data, __unused id context) { 26 | timesCalled++; 27 | }]; 28 | 29 | XCTAssertEqual(timesCalled, 2); 30 | XCTAssertEqual(self.testFailures, 1); 31 | } 32 | 33 | - (void)testNoFailWithAllowErrors { 34 | __block NSInteger timesCalled = 0; 35 | [self runLayoutTestsWithViewProvider:[self class] validation:^(UIView *view, __unused NSDictionary * data, __unused id context) { 36 | timesCalled++; 37 | 38 | [self.viewsAllowingAccessibilityErrors addObject:view.subviews[0]]; 39 | }]; 40 | 41 | XCTAssertEqual(timesCalled, 2); 42 | XCTAssertEqual(self.testFailures, 0); 43 | 44 | self.interceptsAutolayoutErrors = YES; 45 | } 46 | 47 | - (void)testNoFailWithDifferentClasses { 48 | // NOTE: Now UIButton is not in this set 49 | self.viewClassesRequiringAccessibilityLabels = [NSSet setWithObjects:[UISwitch class], nil]; 50 | 51 | __block NSInteger timesCalled = 0; 52 | [self runLayoutTestsWithViewProvider:[self class] validation:^(__unused UIView * view, __unused NSDictionary * data, __unused id context) { 53 | timesCalled++; 54 | }]; 55 | 56 | XCTAssertEqual(timesCalled, 2); 57 | XCTAssertEqual(self.testFailures, 0); 58 | 59 | self.viewClassesRequiringAccessibilityLabels = [LYTConfig sharedInstance].viewClassesRequiringAccessibilityLabels; 60 | } 61 | 62 | - (void)testNoFailWithAccessibilityOff { 63 | self.accessibilityTestsEnabled = NO; 64 | 65 | __block NSInteger timesCalled = 0; 66 | [self runLayoutTestsWithViewProvider:[self class] validation:^(__unused UIView * view, __unused NSDictionary * data, __unused id context) { 67 | timesCalled++; 68 | }]; 69 | 70 | XCTAssertEqual(timesCalled, 2); 71 | XCTAssertEqual(self.testFailures, 0); 72 | 73 | self.accessibilityTestsEnabled = YES; 74 | } 75 | 76 | #pragma mark - Override 77 | 78 | - (void)failTest:(__unused NSString *)errorMessage view:(__unused UIView *)view { 79 | self.testFailures++; 80 | } 81 | 82 | #pragma mark - LYTViewProvider 83 | 84 | + (NSDictionary *)dataSpecForTestWithError:(__unused NSError * _Nullable __autoreleasing *)error { 85 | // Return 2 views to test. One with a button with accessibility, the other a button without accessibility. 86 | return @{ 87 | @"view": [[LYTDataValues alloc] initWithValues:@[ 88 | [UnitTestViews viewWithButtonAndAccessibility], 89 | [UnitTestViews viewWithButtonAndNoAccessibility] 90 | ]] 91 | }; 92 | } 93 | 94 | + (UIView *)viewForData:(NSDictionary *)data 95 | reuseView:(__unused UIView *)view 96 | size:(__unused LYTViewSize *)size 97 | context:(__unused id __autoreleasing *)context 98 | error:(__unused NSError * _Nullable __autoreleasing *)error { 99 | return (UIView *)data[@"view"]; 100 | } 101 | 102 | @end 103 | -------------------------------------------------------------------------------- /LayoutTestTests/LayoutTestCaseTests/LayoutTestCaseAmbiguousTests.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "LayoutTest.h" 11 | #import "LayoutTestBase.h" 12 | #import "UnitTestViews.h" 13 | 14 | 15 | @interface LayoutTestCaseAmbiguousTests : LYTLayoutTestCase 16 | 17 | @property (nonatomic) NSInteger testFailures; 18 | 19 | @end 20 | 21 | @implementation LayoutTestCaseAmbiguousTests 22 | 23 | - (void)testFails { 24 | __block NSInteger timesCalled = 0; 25 | [self runLayoutTestsWithViewProvider:[self class] validation:^(__unused UIView * view, __unused NSDictionary * data, __unused id context) { 26 | timesCalled++; 27 | }]; 28 | 29 | XCTAssertEqual(timesCalled, 2); 30 | XCTAssertEqual(self.testFailures, 1); 31 | } 32 | 33 | - (void)testNoFailWithAllowsOverlap { 34 | self.ambiguousAutolayoutTestsEnabled = NO; 35 | 36 | __block NSInteger timesCalled = 0; 37 | [self runLayoutTestsWithViewProvider:[self class] validation:^(__unused UIView *view, __unused NSDictionary * data, __unused id context) { 38 | timesCalled++; 39 | }]; 40 | 41 | XCTAssertEqual(timesCalled, 2); 42 | XCTAssertEqual(self.testFailures, 0); 43 | 44 | self.ambiguousAutolayoutTestsEnabled = YES; 45 | } 46 | 47 | #pragma mark - Override 48 | 49 | - (void)failTest:(__unused NSString *)errorMessage view:(__unused UIView *)view { 50 | self.testFailures++; 51 | } 52 | 53 | #pragma mark - LYTViewProvider 54 | 55 | + (nullable NSDictionary *)dataSpecForTestWithError:(__unused NSError * _Nullable __autoreleasing *)error { 56 | // Return 3 views to test. One correct view, on view with overlapping subviews and one view with a switch subview. 57 | return @{ 58 | @"view": [[LYTDataValues alloc] initWithValues:@[ 59 | [UnitTestViews viewWithNoProblems], 60 | [UnitTestViews viewWithAmbiguousLayout] 61 | ]] 62 | }; 63 | } 64 | 65 | + (UIView *)viewForData:(NSDictionary *)data 66 | reuseView:(__unused UIView *)view 67 | size:(__unused LYTViewSize *)size 68 | context:(__unused id __autoreleasing *)context 69 | error:(__unused NSError * _Nullable __autoreleasing * _Nullable)error { 70 | return (UIView *)data[@"view"]; 71 | } 72 | 73 | @end 74 | -------------------------------------------------------------------------------- /LayoutTestTests/LayoutTestCaseTests/LayoutTestCaseAutolayoutFailureTests.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "LayoutTest.h" 11 | #import "LayoutTestBase.h" 12 | #import "UnitTestViews.h" 13 | 14 | 15 | @interface LayoutTestCaseAutolayoutFailureTests : LYTLayoutTestCase 16 | 17 | @property (nonatomic) NSInteger testFailures; 18 | 19 | @end 20 | 21 | @implementation LayoutTestCaseAutolayoutFailureTests 22 | 23 | - (void)testFails { 24 | __block NSInteger timesCalled = 0; 25 | [self runLayoutTestsWithViewProvider:[self class] validation:^(__unused UIView * view, __unused NSDictionary * data, __unused id context) { 26 | timesCalled++; 27 | }]; 28 | 29 | XCTAssertEqual(timesCalled, 2); 30 | XCTAssertEqual(self.testFailures, 1); 31 | } 32 | 33 | - (void)testNoFailWithAllowsOverlap { 34 | self.interceptsAutolayoutErrors = NO; 35 | 36 | __block NSInteger timesCalled = 0; 37 | [self runLayoutTestsWithViewProvider:[self class] validation:^(__unused UIView *view, __unused NSDictionary * data, __unused id context) { 38 | timesCalled++; 39 | }]; 40 | 41 | XCTAssertEqual(timesCalled, 2); 42 | XCTAssertEqual(self.testFailures, 0); 43 | 44 | self.interceptsAutolayoutErrors = YES; 45 | } 46 | 47 | #pragma mark - Override 48 | 49 | - (void)failTest:(__unused NSString *)errorMessage view:(__unused UIView *)view { 50 | self.testFailures++; 51 | } 52 | 53 | #pragma mark - LYTViewProvider 54 | 55 | + (nullable NSDictionary *)dataSpecForTestWithError:(__unused NSError * _Nullable __autoreleasing *)error { 56 | // Return 3 views to test. One correct view, on view with overlapping subviews and one view with a switch subview. 57 | return @{ 58 | @"view": [[LYTDataValues alloc] initWithValues:@[ 59 | [UnitTestViews viewWithNoProblems], 60 | [UnitTestViews viewWithIncorrectAutolayout] 61 | ]] 62 | }; 63 | } 64 | 65 | + (UIView *)viewForData:(NSDictionary *)data 66 | reuseView:(__unused UIView *)view 67 | size:(__unused LYTViewSize *)size 68 | context:(__unused id __autoreleasing *)context 69 | error:(__unused NSError * _Nullable __autoreleasing * _Nullable)error { 70 | return (UIView *)data[@"view"]; 71 | } 72 | 73 | @end 74 | -------------------------------------------------------------------------------- /LayoutTestTests/LayoutTestCaseTests/LayoutTestCaseMissingLabelTests.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "LayoutTest.h" 11 | #import "LayoutTestBase.h" 12 | #import "UnitTestViews.h" 13 | 14 | 15 | @interface LayoutTestCaseMissingLabelTests : LYTLayoutTestCase 16 | 17 | @property (nonatomic) NSInteger testFailures; 18 | 19 | @end 20 | 21 | @implementation LayoutTestCaseMissingLabelTests 22 | 23 | - (void)testFails { 24 | __block NSInteger timesCalled = 0; 25 | [self runLayoutTestsWithViewProvider:[self class] validation:^(__unused UIView * view, __unused NSDictionary * data, __unused id context) { 26 | timesCalled++; 27 | }]; 28 | 29 | XCTAssertEqual(timesCalled, 2); 30 | XCTAssertEqual(self.testFailures, 1); 31 | } 32 | 33 | - (void)testNoFailWithAllowErrors { 34 | __block NSInteger timesCalled = 0; 35 | [self runLayoutTestsWithViewProvider:[self class] validation:^(UIView *view, __unused NSDictionary * data, __unused id context) { 36 | timesCalled++; 37 | 38 | [self.viewsAllowingAccessibilityErrors addObject:view.subviews[0]]; 39 | }]; 40 | 41 | XCTAssertEqual(timesCalled, 2); 42 | XCTAssertEqual(self.testFailures, 0); 43 | 44 | self.interceptsAutolayoutErrors = YES; 45 | } 46 | 47 | - (void)testNoFailWithAccessibilityOff { 48 | self.accessibilityTestsEnabled = NO; 49 | 50 | __block NSInteger timesCalled = 0; 51 | [self runLayoutTestsWithViewProvider:[self class] validation:^(__unused UIView * view, __unused NSDictionary * data, __unused id context) { 52 | timesCalled++; 53 | }]; 54 | 55 | XCTAssertEqual(timesCalled, 2); 56 | XCTAssertEqual(self.testFailures, 0); 57 | 58 | self.accessibilityTestsEnabled = YES; 59 | } 60 | 61 | #pragma mark - Override 62 | 63 | - (void)failTest:(__unused NSString *)errorMessage view:(__unused UIView *)view { 64 | self.testFailures++; 65 | } 66 | 67 | #pragma mark - LYTViewProvider 68 | 69 | + (nullable NSDictionary *)dataSpecForTestWithError:(__unused NSError * _Nullable __autoreleasing *)error { 70 | // Return 3 views to test. One correct view, on view with overlapping subviews and one view with a switch subview. 71 | return @{ 72 | @"view": [[LYTDataValues alloc] initWithValues:@[ 73 | [UnitTestViews viewWithNoProblems], 74 | [UnitTestViews viewWithAccessibilityIDButNoLabel] 75 | ]] 76 | }; 77 | } 78 | 79 | + (UIView *)viewForData:(NSDictionary *)data 80 | reuseView:(__unused UIView *)view 81 | size:(__unused LYTViewSize *)size 82 | context:(__unused id __autoreleasing *)context 83 | error:(__unused NSError * _Nullable __autoreleasing * _Nullable)error { 84 | return (UIView *)data[@"view"]; 85 | } 86 | 87 | @end 88 | -------------------------------------------------------------------------------- /LayoutTestTests/LayoutTestCaseTests/LayoutTestCaseMultipleDataOverlapTests.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "LayoutTest.h" 11 | #import "LayoutTestBase.h" 12 | #import "UnitTestViews.h" 13 | #import "UIViewWithLabel.h" 14 | 15 | @interface LayoutTestCaseMultipleDataOverlapTests : LYTLayoutTestCase 16 | 17 | @property (nonatomic) NSInteger testFailures; 18 | 19 | @end 20 | 21 | @implementation LayoutTestCaseMultipleDataOverlapTests 22 | 23 | - (void)testPassWithoutOverlappingTextAndFailWithOverlappingText { 24 | __block NSInteger timesCalled = 0; 25 | 26 | [self runLayoutTestsWithViewProvider:[self class] validation:^(__unused UIView * view, __unused NSDictionary * data, __unused id context) { 27 | timesCalled++; 28 | }]; 29 | 30 | XCTAssertEqual(timesCalled, 2); 31 | XCTAssertEqual(self.testFailures, 1); 32 | } 33 | 34 | #pragma mark - Override 35 | 36 | - (void)failTest:(__unused NSString *)errorMessage view:(__unused UIView *)view { 37 | self.testFailures++; 38 | } 39 | 40 | #pragma mark - LYTViewProvider 41 | 42 | + (nullable NSDictionary *)dataSpecForTestWithError:(__unused NSError * _Nullable __autoreleasing *)error { 43 | return @{ 44 | @"view": [UnitTestViews viewWithLongStringOverlappingLabel], 45 | @"text": [[LYTStringValues alloc] initWithValues:@[@"X", @"A long string that will cause overlap"]] 46 | }; 47 | } 48 | 49 | + (UIView *)viewForData:(NSDictionary *)data 50 | reuseView:(UIView *)view 51 | size:(__unused LYTViewSize *)size 52 | context:(__unused id __autoreleasing *)context 53 | error:(__unused NSError * _Nullable __autoreleasing * _Nullable)error { 54 | UIViewWithLabel *reuseView = (UIViewWithLabel *)(view ? view : data[@"view"]); 55 | reuseView.label.text = data[@"text"]; 56 | return reuseView; 57 | } 58 | 59 | @end 60 | -------------------------------------------------------------------------------- /LayoutTestTests/LayoutTestCaseTests/LayoutTestCaseNestedAccessibilityTests.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "LayoutTest.h" 11 | #import "LayoutTestBase.h" 12 | #import "UnitTestViews.h" 13 | 14 | 15 | @interface LayoutTestCaseNestedAccessibilityTests : LYTLayoutTestCase 16 | 17 | @property (nonatomic) NSInteger testFailures; 18 | 19 | @end 20 | 21 | @implementation LayoutTestCaseNestedAccessibilityTests 22 | 23 | - (void)testFails { 24 | __block NSInteger timesCalled = 0; 25 | [self runLayoutTestsWithViewProvider:[self class] validation:^(__unused UIView * view, __unused NSDictionary * data, __unused id context) { 26 | timesCalled++; 27 | }]; 28 | 29 | XCTAssertEqual(timesCalled, 2); 30 | XCTAssertEqual(self.testFailures, 1); 31 | } 32 | 33 | - (void)testNoFailWithAllowErrors { 34 | __block NSInteger timesCalled = 0; 35 | [self runLayoutTestsWithViewProvider:[self class] validation:^(__unused UIView *view, __unused NSDictionary * data, __unused id context) { 36 | timesCalled++; 37 | 38 | [self.viewsAllowingAccessibilityErrors addObject:view.subviews[0]]; 39 | }]; 40 | 41 | XCTAssertEqual(timesCalled, 2); 42 | XCTAssertEqual(self.testFailures, 0); 43 | 44 | self.interceptsAutolayoutErrors = YES; 45 | } 46 | 47 | - (void)testNoFailWithAccessibilityOff { 48 | self.accessibilityTestsEnabled = NO; 49 | 50 | __block NSInteger timesCalled = 0; 51 | [self runLayoutTestsWithViewProvider:[self class] validation:^(__unused UIView * view, __unused NSDictionary * data, __unused id context) { 52 | timesCalled++; 53 | }]; 54 | 55 | XCTAssertEqual(timesCalled, 2); 56 | XCTAssertEqual(self.testFailures, 0); 57 | 58 | self.accessibilityTestsEnabled = YES; 59 | } 60 | 61 | #pragma mark - Override 62 | 63 | - (void)failTest:(__unused NSString *)errorMessage view:(__unused UIView *)view { 64 | self.testFailures++; 65 | } 66 | 67 | #pragma mark - LYTViewProvider 68 | 69 | + (nullable NSDictionary *)dataSpecForTestWithError:(__unused NSError * _Nullable __autoreleasing *)error { 70 | // Return 3 views to test. One correct view, on view with overlapping subviews and one view with a switch subview. 71 | return @{ 72 | @"view": [[LYTDataValues alloc] initWithValues:@[ 73 | [UnitTestViews viewWithNoProblems], 74 | [UnitTestViews viewWithNestedAccessibility] 75 | ]] 76 | }; 77 | } 78 | 79 | + (UIView *)viewForData:(NSDictionary *)data 80 | reuseView:(__unused UIView *)view 81 | size:(__unused LYTViewSize *)size 82 | context:(__unused id __autoreleasing *)context 83 | error:(__unused NSError * _Nullable __autoreleasing *)error { 84 | return (UIView *)data[@"view"]; 85 | } 86 | 87 | @end 88 | -------------------------------------------------------------------------------- /LayoutTestTests/LayoutTestCaseTests/LayoutTestCaseOverlapTests.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "LayoutTest.h" 11 | #import "LayoutTestBase.h" 12 | #import "UnitTestViews.h" 13 | 14 | 15 | @interface LayoutTestCaseOverlapTests : LYTLayoutTestCase 16 | 17 | @property (nonatomic) NSInteger testFailures; 18 | 19 | @end 20 | 21 | @implementation LayoutTestCaseOverlapTests 22 | 23 | - (void)testFails { 24 | __block NSInteger timesCalled = 0; 25 | [self runLayoutTestsWithViewProvider:[self class] validation:^(__unused UIView * view, __unused NSDictionary * data, __unused id context) { 26 | timesCalled++; 27 | }]; 28 | 29 | XCTAssertEqual(timesCalled, 4); 30 | // We should have failed once for the view that overlaps 31 | XCTAssertEqual(self.testFailures, 1); 32 | } 33 | 34 | - (void)testPassesWhenHidden { 35 | __block NSInteger timesCalled = 0; 36 | [self runLayoutTestsWithViewProvider:[self class] validation:^(UIView * view, __unused NSDictionary * data, __unused id context) { 37 | timesCalled++; 38 | // Let's hide one of the subviews. Now, nothing should be wrong. 39 | view.subviews[0].hidden = YES; 40 | }]; 41 | 42 | XCTAssertEqual(timesCalled, 4); 43 | XCTAssertEqual(self.testFailures, 0); 44 | } 45 | 46 | - (void)testNoFailWithAllowsOverlap { 47 | __block NSInteger timesCalled = 0; 48 | [self runLayoutTestsWithViewProvider:[self class] validation:^(UIView *view, __unused NSDictionary * data, __unused id context) { 49 | timesCalled++; 50 | 51 | [self.viewsAllowingOverlap addObject:view.subviews[0]]; 52 | }]; 53 | 54 | XCTAssertEqual(timesCalled, 4); 55 | XCTAssertEqual(self.testFailures, 0); 56 | } 57 | 58 | - (void)testviewClassesAllowingSubviewErrorsOff { 59 | // Remove the UISwitch class from classes allowing overlap 60 | self.viewClassesAllowingSubviewErrors = [NSSet set]; 61 | 62 | __block NSInteger timesCalled = 0; 63 | [self runLayoutTestsWithViewProvider:[self class] validation:^(UIView *view, __unused NSDictionary * data, __unused id context) { 64 | timesCalled++; 65 | 66 | UIView *subview = view.subviews[0]; 67 | if (![subview isKindOfClass:[UISwitch class]]) { 68 | [self.viewsAllowingOverlap addObject:subview]; 69 | } 70 | }]; 71 | 72 | XCTAssertEqual(timesCalled, 4); 73 | // This will fail multiple times for a UISwitch because it has many overlapping subviews 74 | XCTAssertTrue(self.testFailures > 0); 75 | 76 | // Reset this now 77 | self.viewClassesAllowingSubviewErrors = [LYTConfig sharedInstance].viewClassesAllowingSubviewErrors; 78 | } 79 | 80 | #pragma mark - Override 81 | 82 | - (void)failTest:(__unused NSString *)errorMessage view:(__unused UIView *)view { 83 | self.testFailures++; 84 | } 85 | 86 | #pragma mark - LYTViewProvider 87 | 88 | + (nullable NSDictionary *)dataSpecForTestWithError:(__unused NSError * _Nullable __autoreleasing *)error { 89 | // Return 4 views to test. 90 | // - One correct view 91 | // - One view with overlapping subviews 92 | // - One view with a switch subview. 93 | // - One view with a button with overlapping subviews 94 | return @{ 95 | @"view": [[LYTDataValues alloc] initWithValues:@[ 96 | [UnitTestViews viewWithNoProblems], 97 | [UnitTestViews viewWithOverlappingViews], 98 | [UnitTestViews viewWithUISwitchSubview], 99 | [UnitTestViews buttonWithBackgroundImage] 100 | ]] 101 | }; 102 | } 103 | 104 | + (UIView *)viewForData:(NSDictionary *)data 105 | reuseView:(__unused UIView *)view 106 | size:(__unused LYTViewSize *)size 107 | context:(__unused id __autoreleasing *)context 108 | error:(__unused NSError * _Nullable __autoreleasing * _Nullable)error { 109 | return (UIView *)data[@"view"]; 110 | } 111 | 112 | @end 113 | -------------------------------------------------------------------------------- /LayoutTestTests/LayoutTestCaseTests/LayoutTestCaseViewSizesConfigTests.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "LayoutTest.h" 11 | #import "LayoutTestBase.h" 12 | #import "UnitTestViews.h" 13 | 14 | 15 | @interface LayoutTestCaseViewSizesConfigTests : LYTLayoutTestCase 16 | 17 | @end 18 | 19 | @implementation LayoutTestCaseViewSizesConfigTests 20 | 21 | - (void)setUp { 22 | [super setUp]; 23 | 24 | [LYTConfig sharedInstance].viewSizesToTest = @[ 25 | [[LYTViewSize alloc] initWithWidth:@(100) height:@(100)], 26 | [[LYTViewSize alloc] initWithHeight:@(200)], 27 | [[LYTViewSize alloc] initWithWidth:@(200)] 28 | ]; 29 | } 30 | 31 | - (void)tearDown { 32 | [[LYTConfig sharedInstance] resetDefaults]; 33 | 34 | [super tearDown]; 35 | } 36 | 37 | - (void)testNoOptions { 38 | __block NSInteger timesCalled = 0; 39 | [self runLayoutTestsWithViewProvider:[self class] validation:^(UIView * view, __unused NSDictionary * data, __unused id context) { 40 | switch (timesCalled) { 41 | case 0: 42 | XCTAssertEqual(view.lyt_width, 100); 43 | XCTAssertEqual(view.lyt_height, 100); 44 | break; 45 | case 1: 46 | XCTAssertEqual(view.lyt_width, 300); 47 | XCTAssertEqual(view.lyt_height, 200); 48 | break; 49 | case 2: 50 | XCTAssertEqual(view.lyt_width, 200); 51 | XCTAssertEqual(view.lyt_height, 300); 52 | break; 53 | default: 54 | XCTFail(); 55 | } 56 | timesCalled++; 57 | }]; 58 | 59 | XCTAssertEqual(timesCalled, 3); 60 | } 61 | 62 | - (void)testNoViewSizes { 63 | __block NSInteger timesCalled = 0; 64 | [self runLayoutTestsWithViewProvider:[self class] limitResults:LYTTesterLimitResultsNoSizes validation:^(UIView * view, 65 | __unused NSDictionary * data, 66 | __unused id context) { 67 | XCTAssertEqual(view.lyt_width, 300); 68 | XCTAssertEqual(view.lyt_height, 300); 69 | timesCalled++; 70 | }]; 71 | 72 | XCTAssertEqual(timesCalled, 1); 73 | } 74 | 75 | #pragma mark - LYTViewProvider 76 | 77 | + (nullable NSDictionary *)dataSpecForTestWithError:(__unused NSError * _Nullable __autoreleasing *)error { 78 | return @{ 79 | // Empty data 80 | }; 81 | } 82 | 83 | + (UIView *)viewForData:(__unused NSDictionary *)data 84 | reuseView:(UIView *)view 85 | size:(__unused LYTViewSize *)size 86 | context:(__unused id __autoreleasing *)context 87 | error:(__unused NSError * _Nullable __autoreleasing * _Nullable)error { 88 | return view ?: [UnitTestViews viewWithNoProblems]; 89 | } 90 | 91 | + (void)adjustViewSize:(UIView *)view 92 | data:(__unused NSDictionary *)data 93 | size:(LYTViewSize *)size 94 | context:(__unused id)context 95 | error:(__unused NSError * _Nullable __autoreleasing * _Nullable)error { 96 | if (!size.width) { 97 | view.lyt_width = 300; 98 | } 99 | if (!size.height) { 100 | view.lyt_height = 300; 101 | } 102 | } 103 | 104 | @end 105 | -------------------------------------------------------------------------------- /LayoutTestTests/LayoutTestCaseTests/LayoutTestCaseViewSizesTests.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "LayoutTest.h" 11 | #import "LayoutTestBase.h" 12 | #import "UnitTestViews.h" 13 | 14 | 15 | /** 16 | These tests assume we are testing on: 17 | (w: 100, h: 100), (w: 300, h:200), (w: 200, h: 300) 18 | using both the LYTViewSizes and the adjust view methods. 19 | */ 20 | @interface LayoutTestCaseViewSizesTests : LYTLayoutTestCase 21 | 22 | @end 23 | 24 | @implementation LayoutTestCaseViewSizesTests 25 | 26 | - (void)testNoOptions { 27 | __block NSInteger timesCalled = 0; 28 | [self runLayoutTestsWithViewProvider:[self class] validation:^(UIView * view, __unused NSDictionary * data, __unused id context) { 29 | switch (timesCalled) { 30 | case 0: 31 | XCTAssertEqual(view.lyt_width, 100); 32 | XCTAssertEqual(view.lyt_height, 100); 33 | break; 34 | case 1: 35 | XCTAssertEqual(view.lyt_width, 300); 36 | XCTAssertEqual(view.lyt_height, 200); 37 | break; 38 | case 2: 39 | XCTAssertEqual(view.lyt_width, 200); 40 | XCTAssertEqual(view.lyt_height, 300); 41 | break; 42 | default: 43 | XCTFail(); 44 | } 45 | timesCalled++; 46 | }]; 47 | 48 | XCTAssertEqual(timesCalled, 3); 49 | } 50 | 51 | - (void)testNoViewSizes { 52 | __block NSInteger timesCalled = 0; 53 | [self runLayoutTestsWithViewProvider:[self class] limitResults:LYTTesterLimitResultsNoSizes validation:^(UIView * view, 54 | __unused NSDictionary * data, 55 | __unused id context) { 56 | XCTAssertEqual(view.lyt_width, 300); 57 | XCTAssertEqual(view.lyt_height, 300); 58 | timesCalled++; 59 | }]; 60 | 61 | XCTAssertEqual(timesCalled, 1); 62 | } 63 | 64 | #pragma mark - LYTViewProvider 65 | 66 | + (nullable NSDictionary *)dataSpecForTestWithError:(__unused NSError * _Nullable __autoreleasing *)error { 67 | return @{ 68 | // Empty data 69 | }; 70 | } 71 | 72 | + (UIView *)viewForData:(__unused NSDictionary *)data 73 | reuseView:(UIView *)view 74 | size:(__unused LYTViewSize *)size 75 | context:(__unused id __autoreleasing *)context 76 | error:(__unused NSError * _Nullable __autoreleasing * _Nullable)error { 77 | return view ?: [UnitTestViews viewWithNoProblems]; 78 | } 79 | 80 | + (NSArray *)sizesForView { 81 | return @[ 82 | [[LYTViewSize alloc] initWithWidth:@(100) height:@(100)], 83 | [[LYTViewSize alloc] initWithHeight:@(200)], 84 | [[LYTViewSize alloc] initWithWidth:@(200)] ]; 85 | } 86 | 87 | + (void)adjustViewSize:(UIView *)view 88 | data:(__unused NSDictionary *)data 89 | size:(LYTViewSize *)size 90 | context:(__unused id)context 91 | error:(__unused NSError * _Nullable __autoreleasing * _Nullable)error { 92 | if (!size.width) { 93 | view.lyt_width = 300; 94 | } 95 | if (!size.height) { 96 | view.lyt_height = 300; 97 | } 98 | } 99 | 100 | @end 101 | -------------------------------------------------------------------------------- /LayoutTestTests/LayoutTestCaseTests/LayoutTestCaseWithinSuperviewTests.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import "LayoutTest.h" 11 | #import "LayoutTestBase.h" 12 | #import "UnitTestViews.h" 13 | 14 | 15 | @interface LayoutTestCaseWithinSuperviewTests : LYTLayoutTestCase 16 | 17 | @property (nonatomic) NSInteger testFailures; 18 | 19 | @end 20 | 21 | @implementation LayoutTestCaseWithinSuperviewTests 22 | 23 | - (void)testFails { 24 | __block NSInteger timesCalled = 0; 25 | [self runLayoutTestsWithViewProvider:[self class] validation:^(__unused UIView *view, __unused NSDictionary*data, __unused id context) { 26 | timesCalled++; 27 | }]; 28 | 29 | XCTAssertEqual(timesCalled, 2); 30 | // We should have failed once for the view that is out of the superview 31 | XCTAssertEqual(self.testFailures, 1); 32 | } 33 | 34 | - (void)testPassesWhenHidden { 35 | __block NSInteger timesCalled = 0; 36 | [self runLayoutTestsWithViewProvider:[self class] validation:^(UIView * view, __unused NSDictionary * data, __unused id context) { 37 | timesCalled++; 38 | 39 | view.subviews[0].hidden = YES; 40 | }]; 41 | 42 | XCTAssertEqual(timesCalled, 2); 43 | XCTAssertEqual(self.testFailures, 0); 44 | } 45 | 46 | - (void)testNoFailWithAllowsOverlap { 47 | __block NSInteger timesCalled = 0; 48 | [self runLayoutTestsWithViewProvider:[self class] validation:^(UIView *view, __unused NSDictionary * data, __unused id context) { 49 | timesCalled++; 50 | 51 | [self.viewsAllowingOverlap addObject:view.subviews[0]]; 52 | }]; 53 | 54 | XCTAssertEqual(timesCalled, 2); 55 | XCTAssertEqual(self.testFailures, 0); 56 | } 57 | 58 | #pragma mark - Override 59 | 60 | - (void)failTest:(__unused NSString *)errorMessage view:(__unused UIView *)view { 61 | self.testFailures++; 62 | } 63 | 64 | #pragma mark - LYTViewProvider 65 | 66 | + (nullable NSDictionary *)dataSpecForTestWithError:(__unused NSError * _Nullable __autoreleasing *)error { 67 | // Return 3 views to test. One correct view, on view with overlapping subviews and one view with a switch subview. 68 | return @{ 69 | @"view": [[LYTDataValues alloc] initWithValues:@[ 70 | [UnitTestViews viewWithNoProblems], 71 | [UnitTestViews viewWithSubviewOutOfSuperview] 72 | ]] 73 | }; 74 | } 75 | 76 | + (UIView *)viewForData:(NSDictionary *)data 77 | reuseView:(__unused UIView *)view 78 | size:(__unused LYTViewSize *)size 79 | context:(__unused id __autoreleasing *)context 80 | error:(__unused NSError * _Nullable __autoreleasing *)error { 81 | return (UIView *)data[@"view"]; 82 | } 83 | 84 | @end 85 | -------------------------------------------------------------------------------- /LayoutTestTests/LayoutTestCaseTests/SwiftLYTLayoutFailingTestSnapshotRecorderTests.swift: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | import XCTest 11 | import LayoutTest 12 | 13 | class SwiftLYTLayoutFailingTestSnapshotRecorderTests: XCTestCase { 14 | 15 | func testStartNewLogDeletesExisitingClassSnapshotDirectory() { 16 | let fileManager = FileManager.default 17 | let currentDirectory = Bundle(for: type(of: self)).bundlePath 18 | let classDirectory = currentDirectory + "/LayoutTestImages/SwiftLYTLayoutFailingTestSnapshotRecorderTests" 19 | let testFilePath = classDirectory + "/testFile.html" 20 | do { 21 | try fileManager.createDirectory(atPath: classDirectory, withIntermediateDirectories: true, attributes: nil) 22 | } catch{} 23 | fileManager.createFile(atPath: testFilePath, contents: nil, attributes: nil) 24 | 25 | let recorder = LayoutFailingTestSnapshotRecorder() 26 | recorder.startNewLog(for: type(of: self)) 27 | 28 | XCTAssertFalse(fileManager.fileExists(atPath: testFilePath)) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /LayoutTestTests/NullIteratorTests.m: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | #import 11 | // Test Class 12 | #import "LYTLayoutPropertyTester.h" 13 | // Helpers 14 | #import "LYTDataValues.h" 15 | #import "LYTViewProvider.h" 16 | 17 | 18 | @interface NullIteratorTests : XCTestCase 19 | 20 | @end 21 | 22 | @interface NullDataValues: LYTDataValues 23 | 24 | @end 25 | 26 | @implementation NullIteratorTests 27 | 28 | static NSDictionary *testData = nil; 29 | 30 | - (void)setUp { 31 | [super setUp]; 32 | testData = nil; 33 | } 34 | 35 | - (void)tearDown { 36 | [super tearDown]; 37 | testData = nil; 38 | } 39 | 40 | - (void)testNullInDictionary 41 | { 42 | testData = @{ 43 | @"null": [[NullDataValues alloc] init] 44 | }; 45 | __block NSUInteger numberTimesCalled = 0; 46 | [LYTLayoutPropertyTester runPropertyTestsWithViewProvider:[self class] 47 | validation:^(__unused UIView *view, NSDictionary *data, __unused id context) { 48 | id object = data[@"null"]; 49 | if (numberTimesCalled < 2) { 50 | XCTAssertEqual([object unsignedIntegerValue], numberTimesCalled, @"Should be equal"); 51 | } else { 52 | XCTAssertNil(object, @"Object should be nil"); 53 | } 54 | numberTimesCalled++; 55 | }]; 56 | XCTAssertEqual(numberTimesCalled, 3, @"Should have been called three times"); 57 | } 58 | 59 | - (void)testNullInArray 60 | { 61 | testData = @{ 62 | @"array": @[@"0", [[NullDataValues alloc] init], @(2)] 63 | }; 64 | __block NSUInteger numberTimesCalled = 0; 65 | [LYTLayoutPropertyTester runPropertyTestsWithViewProvider:[self class] 66 | validation:^(__unused UIView *view, NSDictionary *data, __unused id context) { 67 | id object = data[@"array"][1]; 68 | // if it's null, it should be @(2) 69 | XCTAssertEqual([object unsignedIntegerValue], numberTimesCalled, @"Should be equal"); 70 | numberTimesCalled++; 71 | }]; 72 | XCTAssertEqual(numberTimesCalled, 3, @"Should have been called three times"); 73 | } 74 | 75 | // Helpers 76 | 77 | - (void)validateGenerators:(NSArray *)values context:(NSMutableSet *)context { 78 | for (NSNumber *number in values) { 79 | XCTAssert([number integerValue] >= 0, @"Number must be greater than 0"); 80 | XCTAssert([number integerValue] < 4, @"Number must be less than 4"); 81 | } 82 | if ([context containsObject:values]) { 83 | XCTFail(@"We've got a duplicate set of values.\nDuplication: %@\nContext: %@", values, context); 84 | } 85 | [context addObject:values]; 86 | } 87 | 88 | // View Provider Protocol 89 | 90 | + (nullable NSDictionary *)dataSpecForTestWithError:(__unused NSError * _Nullable __autoreleasing *)error { 91 | return testData; 92 | } 93 | 94 | + (UIView *)viewForData:(__unused NSDictionary *)data 95 | reuseView:(__unused UIView *)view 96 | size:(__unused LYTViewSize *)size 97 | context:(__unused id __autoreleasing *)context 98 | error:(__unused NSError * _Nullable __autoreleasing * _Nullable)error { 99 | return nil; 100 | } 101 | 102 | @end 103 | 104 | @implementation NullDataValues 105 | 106 | - (NSArray *)values { 107 | return @[@(0), @(1), [NSNull null]]; 108 | } 109 | 110 | @end 111 | -------------------------------------------------------------------------------- /LayoutTestTests/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | © 2015 LinkedIn Corp. All rights reserved. 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | 6 | Unless required by applicable law or agreed to in writing, software 7 | distributed under the License is distributed on an "AS IS" BASIS, 8 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This library enables you to write unit tests which test the layout of a view in multiple configurations. It tests the view with different data combinations and different view sizes. The library works in both Objective-C and Swift. 4 | 5 | To learn how to use this library, there is a LinkedIn Learning course you can access completely for free authored by [Kyle Sherman](http://github.com/drumnkyle/). You can access it [here](https://www.linkedin.com/learning/learning-layouttest-for-ios-development). It will teach you how to use LayoutTest and all of its features by video tutorial. 6 | 7 | ## Motivation 8 | 9 | When creating views, apps often have conditional logic which depends on the data used to setup the view. LayoutTest provides an easy way to define a data spec (a dictionary) which is then used to generate many different combinations of data. The library then uses this data to layout your view multiple times. For example, this is a small portion of the tests ran in our sample app: 10 | 11 |
12 | 13 | In just one test, your view will be laid out multiple times with different data. You can then run test assertions on these views to verify that the layout and view content is correct. Also, the library will run a few tests automatically such as checking for Autolayout errors, missing accessibility, and overlapping views. 14 | Finally, the library makes it easy to test each view with different sizes so you can verify the view will work on different devices. 15 | 16 | ## Docs 17 | 18 | To get started, you should take a look at the docs: 19 | 20 | https://linkedin.github.io/LayoutTest-iOS 21 | 22 | ## Installation 23 | 24 | Add to your unit test target: 25 | 26 | ``` 27 | pod 'LayoutTest' 28 | ``` 29 | 30 | or 31 | 32 | ``` 33 | pod 'LayoutTest/Swift' 34 | ``` 35 | 36 | ## Example 37 | 38 | A simple test would look something like this. Check the docs for more detailed information and examples. 39 | 40 | Objective-C: 41 | ```objective-c 42 | @interface SampleTableViewCellLayoutTests : LYTLayoutTestCase 43 | @end 44 | 45 | @implementation SampleTableViewCellLayoutTests 46 | - (void)testSampleTableViewCellLayout { 47 | [self runLayoutTestsWithViewProvider:[SampleTableViewCell class] 48 | validation:^(UIView * view, NSDictionary * data, id context) { 49 | // Add your custom tests here. 50 | }]; 51 | } 52 | @end 53 | 54 | @implementation SampleTableViewCell (LayoutTesting) 55 | + (NSDictionary *)dataSpecForTest { 56 | return @{ 57 | @"text": [[LYTStringValues alloc] init], 58 | @"showButton": [[LYTBoolValues alloc] init] 59 | } 60 | } 61 | + (UIView *)viewForData:(NSDictionary *)data 62 | reuseView:(nullable UIView *)reuseView 63 | size:(nullable LYTViewSize *)size 64 | context:(id _Nullable * _Nullable)context { 65 | SampleTableViewCell *view = (SampleTableViewCell *)reuseView ?: [SampleTableViewCell viewFromNib]; 66 | [view setupWithJSON:data]; 67 | return view; 68 | } 69 | @end 70 | ``` 71 | Swift: 72 | 73 | ```swift 74 | class SampleTableViewCellLayoutTests { 75 | func testSampleTableViewCell() { 76 | runLayoutTests() { (view: SampleTableViewCell, data: [NSObject: AnyObject], context: Any?) in 77 | // Add your custom tests here. 78 | } 79 | } 80 | } 81 | 82 | extension SampleTableViewCell: LYTViewProvider { 83 | class func dataSpecForTest() -> [NSObject: AnyObject] { 84 | return [ 85 | "text": LYTStringValues(), 86 | "showButton": LYTBoolValues() 87 | ] 88 | } 89 | class func viewForData(data: [NSObject: AnyObject], 90 | reuseView: UIView?, 91 | size: LYTViewSize?, 92 | context: AutoreleasingUnsafeMutablePointer) -> UIView { 93 | let cell = reuseView as? SampleTableViewCell ?? SampleTableViewCell.loadFromNib() 94 | cell.setupWithDictionary(data) 95 | return cell 96 | } 97 | } 98 | ``` 99 | -------------------------------------------------------------------------------- /SampleApp/CatalogSample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | import UIKit 11 | import LayoutTestBase 12 | 13 | @UIApplicationMain 14 | class AppDelegate: UIResponder, UIApplicationDelegate { 15 | 16 | var window: UIWindow? 17 | 18 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 19 | 20 | window = UIWindow(frame: UIScreen.main.bounds) 21 | let rootViewController = CatalogTableViewController() 22 | rootViewController.viewProviderClass = SampleTableViewCell.self 23 | window?.rootViewController = UINavigationController(rootViewController: rootViewController) 24 | window?.makeKeyAndVisible() 25 | 26 | return true 27 | } 28 | } 29 | 30 | extension SampleTableViewCell: ViewCatalogProvider { 31 | public static func dataSpecForTest() throws -> [AnyHashable: Any] { 32 | return [ 33 | "text": StringValues(), 34 | "buttonText": DataValues(values: ["Share", "Like", nil]), 35 | "buttonEnabled": BoolValues(), 36 | "imageType": DataValues(values: ["linkedin", nil]) 37 | ] 38 | } 39 | 40 | public static func view(forData data: [AnyHashable: Any], 41 | reuse reuseView: UIView?, 42 | size: ViewSize?, 43 | context: AutoreleasingUnsafeMutablePointer?) throws -> UIView { 44 | let view = (reuseView as? SampleTableViewCell) ?? loadFromBundle() 45 | view.setup(data) 46 | return view 47 | } 48 | 49 | public static func registerClass(on tableView: UITableView) { 50 | SampleTableViewCell.registerForTableView(tableView) 51 | tableView.estimatedRowHeight = 100 52 | } 53 | 54 | public static func heightForTableViewCellForCatalog(fromData data: [AnyHashable: Any]) -> CGFloat { 55 | return UITableView.automaticDimension 56 | } 57 | 58 | public static func reuseIdentifier() -> String { 59 | return "SampleTableViewCell" 60 | } 61 | 62 | // MARK: - Privat methods 63 | 64 | private static func loadFromBundle() -> SampleTableViewCell { 65 | let cellFromBundle = Bundle.main.loadNibNamed("SampleTableViewCell", owner: nil, options: nil)?[0] 66 | 67 | guard let sampleTableViewCell = cellFromBundle as? SampleTableViewCell else { 68 | fatalError("Could not load SampleTableViewCell from bundle") 69 | } 70 | return sampleTableViewCell 71 | } 72 | } 73 | 74 | -------------------------------------------------------------------------------- /SampleApp/CatalogSample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /SampleApp/CatalogSample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /SampleApp/CatalogSample/Assets.xcassets/LinkedInLogo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LinkedInLogo.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /SampleApp/CatalogSample/Assets.xcassets/LinkedInLogo.imageset/LinkedInLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkedin/LayoutTest-iOS/0796ec35ddadc58181019a706dcd6b1492d40837/SampleApp/CatalogSample/Assets.xcassets/LinkedInLogo.imageset/LinkedInLogo.png -------------------------------------------------------------------------------- /SampleApp/CatalogSample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /SampleApp/CatalogSample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /SampleApp/Podfile: -------------------------------------------------------------------------------- 1 | use_frameworks! 2 | 3 | platform :ios, '12.0' 4 | 5 | pod 'LayoutTestBase/Swift', :path => '..' 6 | 7 | target :SampleApp do 8 | end 9 | 10 | target :CatalogSample do 11 | end 12 | 13 | target :SampleAppTests do 14 | pod 'LayoutTest/Swift', :path => '..' 15 | end 16 | -------------------------------------------------------------------------------- /SampleApp/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - LayoutTest/Swift (6.1.0): 3 | - LayoutTest/SwiftSubspec 4 | - LayoutTest/SwiftSubspec (6.1.0): 5 | - LayoutTest/TestCase 6 | - LayoutTestBase/Swift 7 | - LayoutTest/TestCase (6.1.0): 8 | - LayoutTestBase/Autolayout 9 | - LayoutTestBase/Catalog 10 | - LayoutTestBase/Config 11 | - LayoutTestBase/Core 12 | - LayoutTestBase/UIViewHelpers 13 | - LayoutTestBase/Autolayout (6.1.0) 14 | - LayoutTestBase/Catalog (6.1.0): 15 | - LayoutTestBase/Core 16 | - LayoutTestBase/Config (6.1.0) 17 | - LayoutTestBase/Core (6.1.0): 18 | - LayoutTestBase/Config 19 | - LayoutTestBase/Swift (6.1.0): 20 | - LayoutTestBase/Autolayout 21 | - LayoutTestBase/Catalog 22 | - LayoutTestBase/Config 23 | - LayoutTestBase/Core 24 | - LayoutTestBase/UIViewHelpers 25 | - LayoutTestBase/UIViewHelpers (6.1.0): 26 | - LayoutTestBase/Config 27 | 28 | DEPENDENCIES: 29 | - LayoutTest/Swift (from `..`) 30 | - LayoutTestBase/Swift (from `..`) 31 | 32 | EXTERNAL SOURCES: 33 | LayoutTest: 34 | :path: ".." 35 | LayoutTestBase: 36 | :path: ".." 37 | 38 | SPEC CHECKSUMS: 39 | LayoutTest: b437c8db637fc36895f9802004be09c75c733183 40 | LayoutTestBase: 9f511b9d5cd506b15db04e8948ef0979e454c6c9 41 | 42 | PODFILE CHECKSUM: 5246816d918cdf1a74e375caab9c5c7d07c813b4 43 | 44 | COCOAPODS: 1.15.2 45 | -------------------------------------------------------------------------------- /SampleApp/SampleApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SampleApp/SampleApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SampleApp/SampleApp.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /SampleApp/SampleApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SampleApp/SampleApp/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | import UIKit 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | var window: UIWindow? 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | 19 | window = UIWindow(frame: UIScreen.main.bounds) 20 | window?.rootViewController = UINavigationController(rootViewController: HomeViewController()) 21 | window?.makeKeyAndVisible() 22 | 23 | return true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SampleApp/SampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "83.5x83.5", 66 | "scale" : "2x" 67 | } 68 | ], 69 | "info" : { 70 | "version" : 1, 71 | "author" : "xcode" 72 | } 73 | } -------------------------------------------------------------------------------- /SampleApp/SampleApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /SampleApp/SampleApp/Assets.xcassets/LinkedInLogo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LinkedInLogo.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /SampleApp/SampleApp/Assets.xcassets/LinkedInLogo.imageset/LinkedInLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkedin/LayoutTest-iOS/0796ec35ddadc58181019a706dcd6b1492d40837/SampleApp/SampleApp/Assets.xcassets/LinkedInLogo.imageset/LinkedInLogo.png -------------------------------------------------------------------------------- /SampleApp/SampleApp/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /SampleApp/SampleApp/HomeViewController.swift: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | import UIKit 11 | 12 | class HomeViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { 13 | 14 | @IBOutlet var tableView: UITableView! 15 | 16 | let data = [ 17 | [ 18 | "imageType":"linkedin", 19 | "text": "Short piece of text", 20 | "buttonText": "Share", 21 | "buttonEnabled": true 22 | ], 23 | [ 24 | "imageType":"linkedin", 25 | "text": "A long, long piece of text which spans multiple lines. This is an example of what happens when the text gets long.", 26 | "buttonText": "Share", 27 | "buttonEnabled": true 28 | ], 29 | [ 30 | "text": "Another short piece of text", 31 | "buttonText": "Share", 32 | "buttonEnabled": true 33 | ] 34 | ] 35 | 36 | init() { 37 | super.init(nibName: "HomeViewController", bundle: nil) 38 | } 39 | 40 | required init?(coder aDecoder: NSCoder) { 41 | fatalError("init(coder:) has not been implemented") 42 | } 43 | 44 | override func viewDidLoad() { 45 | super.viewDidLoad() 46 | 47 | tableView.estimatedRowHeight = 100 48 | 49 | SampleTableViewCell.registerForTableView(tableView) 50 | } 51 | 52 | // MARK: TableView 53 | 54 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 55 | let cell = SampleTableViewCell.dequeueForTableView(tableView, indexPath: indexPath) 56 | cell.setup(data[(indexPath as NSIndexPath).row]) 57 | return cell 58 | } 59 | 60 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 61 | return data.count 62 | } 63 | 64 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 65 | return UITableView.automaticDimension 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /SampleApp/SampleApp/HomeViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /SampleApp/SampleApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /SampleApp/SampleApp/SampleFailingView.swift: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | import UIKit 11 | 12 | enum SampleFailingViewError: Error { 13 | case invalidNib 14 | } 15 | 16 | open class SampleFailingView: UIView { 17 | 18 | private static let nibName = "SampleFailingView" 19 | 20 | @IBOutlet weak var label: UILabel! 21 | @IBOutlet weak var button: UIButton! 22 | 23 | class func loadFromNib() throws -> SampleFailingView { 24 | guard let viewInBundle = Bundle.main.loadNibNamed( 25 | SampleFailingView.nibName, 26 | owner: nil, 27 | options: nil), 28 | let sampleFailingView = viewInBundle.first as? SampleFailingView else { 29 | throw SampleFailingViewError.invalidNib 30 | } 31 | 32 | return sampleFailingView 33 | } 34 | 35 | func setup(_ json: [AnyHashable: Any]) { 36 | label.text = json["text"] as? String 37 | button.setTitle(json["buttonText"] as? String, for: .normal) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /SampleApp/SampleApp/SampleFailingView.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /SampleApp/SampleApp/SampleTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | import UIKit 11 | 12 | enum SampleTableViewCellError: Error { 13 | case invalidNib 14 | } 15 | 16 | open class SampleTableViewCell: UITableViewCell { 17 | 18 | private static let leftMaxEdge: CGFloat = 38 19 | private static let leftMinEdge: CGFloat = 0 20 | private static let buttonWidth: CGFloat = 80 21 | 22 | private static let nibName = "SampleTableViewCell" 23 | 24 | @IBOutlet var mainImageView: UIImageView! 25 | @IBOutlet var titleLabel: UILabel! 26 | @IBOutlet var rightButton: UIButton! 27 | 28 | @IBOutlet private var labelLeftEdge: NSLayoutConstraint! 29 | @IBOutlet private var buttonWidth: NSLayoutConstraint! 30 | 31 | open class func registerForTableView(_ tableView: UITableView) { 32 | tableView.register(UINib(nibName: nibName, bundle: nil), forCellReuseIdentifier: nibName) 33 | } 34 | 35 | open class func dequeueForTableView(_ tableView: UITableView, indexPath: IndexPath) -> SampleTableViewCell { 36 | return tableView.dequeueReusableCell(withIdentifier: nibName, for: indexPath) as! SampleTableViewCell 37 | } 38 | 39 | class func loadFromNib() throws -> SampleTableViewCell { 40 | guard let viewInBundle = Bundle.main.loadNibNamed( 41 | SampleTableViewCell.nibName, 42 | owner: nil, 43 | options: nil), 44 | let sampleFailingView = viewInBundle.first as? SampleTableViewCell else { 45 | throw SampleTableViewCellError.invalidNib 46 | } 47 | 48 | return sampleFailingView 49 | } 50 | 51 | func setup(_ json: [AnyHashable: Any]) { 52 | if let imageType = json["imageType"] as? String { 53 | switch imageType { 54 | case "linkedin": 55 | mainImageView.image = UIImage(named: "LinkedInLogo") 56 | mainImageView.isHidden = false 57 | labelLeftEdge.constant = SampleTableViewCell.leftMaxEdge 58 | default: 59 | mainImageView.image = nil 60 | mainImageView.isHidden = true 61 | labelLeftEdge.constant = 0 62 | } 63 | } else { 64 | mainImageView.image = nil 65 | mainImageView.isHidden = true 66 | labelLeftEdge.constant = 0 67 | } 68 | 69 | if let buttonText = json["buttonText"] as? String { 70 | rightButton.isHidden = false 71 | rightButton.setTitle(buttonText, for: .normal) 72 | rightButton.accessibilityLabel = "Double tap to " + buttonText 73 | buttonWidth.constant = SampleTableViewCell.buttonWidth 74 | } else { 75 | rightButton.isHidden = true 76 | buttonWidth.constant = 0 77 | } 78 | 79 | rightButton.isEnabled = json["buttonEnabled"] as? Bool ?? false 80 | 81 | titleLabel.text = json["text"] as? String 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /SampleApp/SampleAppTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /SampleApp/SampleAppTests/SampleFailingLayoutTestsWithSnapshots.swift: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | import XCTest 11 | @testable import SampleApp 12 | import LayoutTest 13 | import LayoutTestBase 14 | 15 | class SampleFailingLayoutTestsWithSnapshots : LayoutTestCase { 16 | 17 | func testSampleFailingViewWithSnapshots() { 18 | XCTExpectFailure("Expecting one of the auto-layout tests to fail when the really long string causes the label to overlap with the button") { 19 | runLayoutTests() { (view: SampleFailingView, data: [AnyHashable: Any], context: Any?) in 20 | } 21 | } 22 | } 23 | } 24 | 25 | extension SampleFailingView : ViewProvider { 26 | public class func dataSpecForTest() throws -> [AnyHashable: Any] { 27 | return [ 28 | "text": StringValues(), 29 | "buttonText": StringValues() 30 | ] 31 | } 32 | 33 | public class func view(forData data: [AnyHashable: Any], 34 | reuse reuseView: UIView?, 35 | size: ViewSize?, 36 | context: AutoreleasingUnsafeMutablePointer?) throws -> UIView { 37 | let view: SampleFailingView 38 | if let reuseView = reuseView as? SampleFailingView { 39 | view = reuseView 40 | } else { 41 | view = try SampleFailingView.loadFromNib() 42 | } 43 | 44 | view.setup(data) 45 | return view 46 | } 47 | 48 | public class func sizesForView() -> [ViewSize] { 49 | return [ 50 | ViewSize(width: 300), 51 | ] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /SampleApp/SampleAppTests/SampleTableViewCellLayoutTests.swift: -------------------------------------------------------------------------------- 1 | // © 2016 LinkedIn Corp. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | 10 | import XCTest 11 | @testable import SampleApp 12 | import LayoutTest 13 | import LayoutTestBase 14 | 15 | // If you are writing in Objective-C, you should use LYTLayoutTestCase instead 16 | 17 | class SampleTableViewCellLayoutTests : LayoutTestCase { 18 | 19 | let titleLabelLeftPadding: CGFloat = 8 20 | 21 | func testSampleTableViewCell() { 22 | runLayoutTests() { (view: SampleTableViewCell, data: [AnyHashable: Any], context: Any?) in 23 | 24 | // Verify that the label and image view are top aligned 25 | XCTAssertTrue(view.titleLabel.lyt_topAligned(view.mainImageView)) 26 | 27 | // Verify that the text gets set correctly 28 | XCTAssertEqual(view.titleLabel.text, data["text"] as? String) 29 | 30 | if view.mainImageView.isHidden { 31 | // If the image view is hidden, then the title label should be 8 from the left edge (image should be squashed) 32 | XCTAssertEqual(view.titleLabel.lyt_left, self.titleLabelLeftPadding) 33 | } else { 34 | // If it is not hidden, then it should be 8 away from the right of the image view 35 | XCTAssertEqual(view.titleLabel.lyt_left, view.mainImageView.lyt_right + 8) 36 | // The image view should be before the title label 37 | XCTAssertTrue(view.mainImageView.lyt_before(view.titleLabel)) 38 | } 39 | 40 | if view.rightButton.isHidden { 41 | // If the right button is hidden, then the text label should be 8 from the right edge 42 | XCTAssertEqual(view.titleLabel.lyt_right, view.lyt_width - self.titleLabelLeftPadding) 43 | } else { 44 | // Otherwise, the text label should be right up against the button 45 | XCTAssertEqual(view.titleLabel.lyt_right, view.rightButton.lyt_left) 46 | // Here, I verify that the button's title is being set correctly 47 | XCTAssertEqual(view.rightButton.title(for: .normal), data["buttonText"] as? String) 48 | } 49 | 50 | // Verify that the right label is enabled iff the data specifies that it is enabled 51 | XCTAssertEqual(view.rightButton.isEnabled, data["buttonEnabled"] as? Bool ?? false) 52 | } 53 | } 54 | } 55 | 56 | extension SampleTableViewCell : ViewProvider { 57 | public class func dataSpecForTest() throws -> [AnyHashable: Any] { 58 | return [ 59 | "text": StringValues(), 60 | "buttonText": StringValues(), 61 | "buttonEnabled": BoolValues(), 62 | "imageType": DataValues(values: ["linkedin", "garbageString", NSNull()]) 63 | ] 64 | } 65 | 66 | public class func view(forData data: [AnyHashable: Any], 67 | reuse reuseView: UIView?, 68 | size: ViewSize?, 69 | context: AutoreleasingUnsafeMutablePointer?) throws -> UIView { 70 | let view: SampleTableViewCell 71 | if let reuseView = reuseView as? SampleTableViewCell { 72 | view = reuseView 73 | } else { 74 | view = try SampleTableViewCell.loadFromNib() 75 | } 76 | 77 | view.setup(data) 78 | return view 79 | } 80 | 81 | // This is an optional method and can also be specified globally in the config. 82 | public class func sizesForView() -> [ViewSize] { 83 | return [ 84 | // Test the view with a specific width 85 | ViewSize(width: 300), 86 | // Test the view with the same width as an iPhone 4 87 | ViewSize(width: LYTiPhone4Width), 88 | // Test the view by setting the width to the iPad width 89 | ViewSize(width: LYTiPadWidth), 90 | // Test the view by setting the width to the iPad height 91 | ViewSize(width: LYTiPadHeight) 92 | ] 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /TestProjects/README: -------------------------------------------------------------------------------- 1 | This folder is for test applications which may differ from the sample app. We expect all these projects to compile whenever we release. 2 | 3 | -------------------------------------------------------------------------------- /TestProjects/SampleAppiOS7/Podfile: -------------------------------------------------------------------------------- 1 | target :SampleAppiOS7Tests do 2 | pod 'LayoutTestBase', :path => '../..' 3 | pod 'LayoutTest', :path => '../..' 4 | end 5 | 6 | -------------------------------------------------------------------------------- /TestProjects/SampleAppiOS7/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - LayoutTest (1.1.1): 3 | - LayoutTest/TestCase (= 1.1.1) 4 | - LayoutTestBase (= 1.1.1) 5 | - LayoutTest/ModuleHeader (1.1.1): 6 | - LayoutTestBase (= 1.1.1) 7 | - LayoutTest/TestCase (1.1.1): 8 | - LayoutTest/ModuleHeader 9 | - LayoutTestBase (= 1.1.1) 10 | - LayoutTestBase/Config 11 | - LayoutTestBase/Core 12 | - LayoutTestBase (1.1.1): 13 | - LayoutTestBase/Autolayout (= 1.1.1) 14 | - LayoutTestBase/Catalog (= 1.1.1) 15 | - LayoutTestBase/Config (= 1.1.1) 16 | - LayoutTestBase/Core (= 1.1.1) 17 | - LayoutTestBase/UIViewHelpers (= 1.1.1) 18 | - LayoutTestBase/Autolayout (1.1.1) 19 | - LayoutTestBase/Catalog (1.1.1): 20 | - LayoutTestBase/Core 21 | - LayoutTestBase/Config (1.1.1) 22 | - LayoutTestBase/Core (1.1.1): 23 | - LayoutTestBase/Config 24 | - LayoutTestBase/UIViewHelpers (1.1.1): 25 | - LayoutTestBase/Config 26 | 27 | DEPENDENCIES: 28 | - LayoutTest (from `../..`) 29 | - LayoutTestBase (from `../..`) 30 | 31 | EXTERNAL SOURCES: 32 | LayoutTest: 33 | :path: ../.. 34 | LayoutTestBase: 35 | :path: ../.. 36 | 37 | SPEC CHECKSUMS: 38 | LayoutTest: 04b0a8894b3f9fe093a6ff5881e9b44c64cef066 39 | LayoutTestBase: 93d8f19820743852760491874d75480d573c60a8 40 | 41 | COCOAPODS: 0.39.0 42 | -------------------------------------------------------------------------------- /TestProjects/SampleAppiOS7/README: -------------------------------------------------------------------------------- 1 | This project takes an iOS 7 project which does not use 'use_frameworks!' in the Podfile. This was a bug at one point (see issue #8), so we should ensure that this project builds whenever we release. 2 | Tests should build and pass. 3 | 4 | -------------------------------------------------------------------------------- /TestProjects/SampleAppiOS7/SampleAppiOS7/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // SampleAppiOS7 4 | // 5 | // Created by Peter Livesey on 2/18/16. 6 | // Copyright © 2016 LinkedIn. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | 16 | @end 17 | 18 | -------------------------------------------------------------------------------- /TestProjects/SampleAppiOS7/SampleAppiOS7/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // SampleAppiOS7 4 | // 5 | // Created by Peter Livesey on 2/18/16. 6 | // Copyright © 2016 LinkedIn. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | @interface AppDelegate () 12 | 13 | @end 14 | 15 | @implementation AppDelegate 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /TestProjects/SampleAppiOS7/SampleAppiOS7/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /TestProjects/SampleAppiOS7/SampleAppiOS7/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /TestProjects/SampleAppiOS7/SampleAppiOS7/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /TestProjects/SampleAppiOS7/SampleAppiOS7/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /TestProjects/SampleAppiOS7/SampleAppiOS7/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // SampleAppiOS7 4 | // 5 | // Created by Peter Livesey on 2/18/16. 6 | // Copyright © 2016 LinkedIn. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ViewController : UIViewController 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /TestProjects/SampleAppiOS7/SampleAppiOS7/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // SampleAppiOS7 4 | // 5 | // Created by Peter Livesey on 2/18/16. 6 | // Copyright © 2016 LinkedIn. All rights reserved. 7 | // 8 | 9 | #import "ViewController.h" 10 | 11 | @interface ViewController () 12 | 13 | @end 14 | 15 | @implementation ViewController 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /TestProjects/SampleAppiOS7/SampleAppiOS7/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // SampleAppiOS7 4 | // 5 | // Created by Peter Livesey on 2/18/16. 6 | // Copyright © 2016 LinkedIn. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /TestProjects/SampleAppiOS7/SampleAppiOS7Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /TestProjects/SampleAppiOS7/SampleAppiOS7Tests/SampleAppiOS7Tests.m: -------------------------------------------------------------------------------- 1 | // 2 | // SampleAppiOS7Tests.m 3 | // SampleAppiOS7Tests 4 | // 5 | // Created by Peter Livesey on 2/18/16. 6 | // Copyright © 2016 LinkedIn. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "ViewController.h" 11 | #import "LayoutTest.h" 12 | #import "LYTViewProvider.h" 13 | 14 | 15 | @interface ViewController () 16 | 17 | @end 18 | 19 | @implementation ViewController 20 | 21 | + (NSDictionary *)dataSpecForTest { 22 | return @{}; 23 | } 24 | 25 | + (UIView *)viewForData:(NSDictionary *)data reuseView:(nullable UIView *)reuseView size:(nullable LYTViewSize *)size context:(id _Nullable * _Nullable)context { 26 | ViewController *controller = [[ViewController alloc] init]; 27 | *context = controller; 28 | return controller.view; 29 | } 30 | 31 | @end 32 | 33 | @interface SampleAppiOS7Tests : LYTLayoutTestCase 34 | 35 | @end 36 | 37 | @implementation SampleAppiOS7Tests 38 | 39 | - (void)testExample { 40 | [self runLayoutTestsWithViewProvider:[ViewController class] validation:^(id view, NSDictionary *data, id context) { 41 | // Run no extra tests 42 | }]; 43 | } 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -o pipefail && 4 | time xcodebuild clean test \ 5 | -project LayoutTest.xcodeproj \ 6 | -scheme LayoutTest \ 7 | -sdk iphonesimulator12.2 \ 8 | -destination 'platform=iOS Simulator,name=iPhone SE,OS=12.2' \ 9 | | xcpretty 10 | -------------------------------------------------------------------------------- /docs/README: -------------------------------------------------------------------------------- 1 | How to Build Docs 2 | 3 | - Install Sphinx: $ sudo easy_install sphinx 4 | - Install the theme: $ ./bin/update_theme.sh 5 | - Build the docs: $ make clean html 6 | - Open the docs locally: $ open _build/html/index.html 7 | 8 | -------------------------------------------------------------------------------- /docs/bin/update_theme.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | cd `dirname $0`/../ 4 | rm -rf source/sphinx_rtd_theme 5 | git clone https://github.com/snide/sphinx_rtd_theme.git temp 6 | mv temp/sphinx_rtd_theme sphinx_rtd_theme 7 | rm -rf temp 8 | echo "Theme updated" 9 | 10 | -------------------------------------------------------------------------------- /docs/images/catalog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkedin/LayoutTest-iOS/0796ec35ddadc58181019a706dcd6b1492d40837/docs/images/catalog.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Layout Testing Library documentation master file, created by 2 | sphinx-quickstart on Wed Jan 21 10:16:33 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Layout Testing Library's documentation! 7 | ================================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 1 13 | :glob: 14 | 15 | pages/* 16 | 17 | This library enables you to write unit tests which test the layout of a view in multiple configurations. It tests the view with different data combinations and different view sizes. The library works in both Objective-C and Swift. 18 | 19 | You can view the source code on GitHub: https://github.com/linkedin/LayoutTest-iOS 20 | 21 | The syntax is also light allowing you to add these tests easily. This test automatically verifies no views are overlapping, verifies Autolayout doesn't throw and runs some sanity accessibility tests. 22 | 23 | .. code-block:: objective-c 24 | 25 | @interface SampleTableViewCellLayoutTests : LYTLayoutTestCase 26 | @end 27 | 28 | @implementation LayoutTestCaseMissingLabelTests 29 | 30 | - (void)testSampleTableViewCellLayout { 31 | [self runLayoutTestsWithViewProvider:[SampleTableViewCell class] 32 | validation:^(UIView * view, NSDictionary * data, id context) { 33 | // Add your custom tests here. 34 | }]; 35 | } 36 | 37 | @end 38 | 39 | @implementation SampleTableViewCell (LayoutTesting) 40 | + (nullable NSDictionary *)dataSpecForTestWithError:(__unused NSError * _Nullable __autoreleasing *)error { 41 | return @{ 42 | @"text": [[LYTStringValues alloc] init], 43 | @"showButton": [[LYTBoolValues alloc] init] 44 | } 45 | } 46 | + (nullable UIView *)viewForData:(NSDictionary *)data 47 | reuseView:(nullable UIView *)reuseView 48 | size:(nullable LYTViewSize *)size 49 | context:(id _Nullable * _Nullable)context 50 | error:(__unused NSError * _Nullable __autoreleasing *)error { 51 | SampleTableViewCell *view = (SampleTableViewCell *)reuseView ?: [SampleTableViewCell viewFromNib]; 52 | [view setupWithJSON:data]; 53 | return view; 54 | } 55 | @end 56 | 57 | These docs do not try to give API level documentation. Instead, they try to outline at a higher level the purpose and motivation for this library. These docs should give you a good overview of what the library does, how to set it up, an overview of the API, etc. To get the API level documentation, you should checkout the headers in the code. 58 | -------------------------------------------------------------------------------- /docs/pages/000_gettingStarted.rst: -------------------------------------------------------------------------------- 1 | Getting Started 2 | --------------- 3 | 4 | The library is distributed using CocoaPods. You can install by adding the following to your Podfile. 5 | 6 | Objective-C 7 | =========== 8 | 9 | .. code-block:: ruby 10 | 11 | target :MyAppNameTests do 12 | pod 'LayoutTest' 13 | end 14 | 15 | Swift 16 | ===== 17 | 18 | .. code-block:: ruby 19 | 20 | target :MyAppNameTests do 21 | pod 'LayoutTest/Swift' 22 | end 23 | 24 | Adding To Non-UnitTest Targets 25 | ============================== 26 | 27 | .. NOTE:: 28 | Since LayoutTestCase depends on XCTest, you should only add LayoutTest to your unit tests target. If you want to use some of the functionality in an app (ie. catalog functionality or helpers), then you can include the LayoutTestBase pod in those targets. 29 | 30 | For example: 31 | 32 | .. code-block:: ruby 33 | 34 | target :MyAppName do 35 | pod 'LayoutTestBase' 36 | end 37 | 38 | target :MyAppNameTests do 39 | pod 'LayoutTest' 40 | end 41 | 42 | Writing Tests 43 | ============= 44 | 45 | See :doc:`020_writingTest` 46 | -------------------------------------------------------------------------------- /docs/pages/010_overview.rst: -------------------------------------------------------------------------------- 1 | Overview 2 | -------- 3 | 4 | This library helps you quickly write unit tests which test the layout of your views. These tests are run as part of your unit test suite. It is a property-based testing library, so it will run each test multiple times with different inputs. 5 | 6 | Why Use This Library? 7 | ===================== 8 | 9 | 1. It's easy to write layout tests for views in just a few lines of code. 10 | 2. It's much faster to write than a UI automation test. 11 | 3. It automatically tests for all kinds of edge cases you may have not considered when writing your view. 12 | 4. The tests run fast (they're just unit tests). 13 | 5. The tests are stable and deterministic. 14 | 6. The library helps stop regressions in your layout logic. 15 | 16 | What this Library doesn't do 17 | ============================ 18 | 19 | 1. This library doesn't test flows. You can't test tap targets or view transitions. For this, you'll need to use UI automation tests. 20 | 2. This library doesn't take screenshots of views and compare them. Instead, you are checking properties of the UIView object for correctness. 21 | 22 | General Flow 23 | ============ 24 | 25 | 1. You define a spec for the data that should inflate this view. Effectively, this is mock data. 26 | 2. You define how the view gets built. This is usually a couple of lines of code (init and inflate). 27 | 3. You write your tests by asserting properties on the view that should always be true. For example, you may check that one view's width is the same as another. You should check any layout logic which you've added to your view class. 28 | 4. The testing framework creates many combinations of the data and inflates the view multiple times running the tests each time. In this way, you can write one test which tests hundreds of edge cases with no extra work. These edge cases include 29 | -------------------------------------------------------------------------------- /docs/pages/030_viewSizes.rst: -------------------------------------------------------------------------------- 1 | Testing Different View Sizes 2 | ---------------------------- 3 | 4 | Often, it's useful to test your view on different sizes. For instance, when testing a UITableViewCell subclass, you may want to test on iPhone 5, iPhone 6, iPad, etc. 5 | 6 | First, you need to implement sizesForView: 7 | 8 | .. code-block:: objective-c 9 | 10 | // Objective-C 11 | + (NSArray *)sizesForView { 12 | return @[ 13 | [[LYTViewSize alloc] initWithWidth:@(LYTiPhone4Width)], 14 | [[LYTViewSize alloc] initWithWidth:@(LYTiPadWidth)] 15 | ] 16 | } 17 | 18 | .. code-block:: objective-c 19 | 20 | // Swift 21 | class func sizesForView() -> [LYTViewSize] { 22 | return [ 23 | LYTViewSize(width: LYTiPhone4Width), 24 | LYTViewSize(width: LYTiPadWidth) 25 | ] 26 | } 27 | 28 | This will now run your tests twice - once on either size. Note that the tests will only edit the width of the view, not the height. This is likely what you want, but you can choose to edit the height instead or as well as the width. 29 | 30 | If you need to dynamically set the height of the view, you can also implement ``+ (void)adjustViewSize:data:size:context:`` which gives you one last chance to change the height of the view before the test is run. See the in code docs for more information. 31 | 32 | .. NOTE:: 33 | These tests don't actually run on these devices. They will always just run the tests on the target device. These are just constants which define a certain width and it will resize the view to this width while running tests. 34 | 35 | These sizes can also be set in the config so they are globally set for all tests. 36 | -------------------------------------------------------------------------------- /docs/pages/040_includedTests.rst: -------------------------------------------------------------------------------- 1 | Automatic Tests 2 | --------------- 3 | 4 | When you subclass LYTLayoutTestCase or LayoutTestCase, a few tests are run automatically. These are tests that most views should pass. You can turn these off in the config or individually on each test. 5 | 6 | No subviews overlap 7 | =================== 8 | 9 | The test verifies that no sibling subviews are overlapping. This can often happen when something is calculated incorrectly. If this is intentional, you can add certain views to an exception set. 10 | 11 | If there is just one subview you want to allow overlaps for, you can add it to viewsAllowingOverlap in the validation block of your test. If there is a certain class which internally has overlapping subviews (such as a custom button), you can add it to viewClassesAllowingSubviewErrors. 12 | 13 | All subviews contained within superview 14 | ======================================= 15 | 16 | The test verifies that no subview is out of bounds of it's superview. Again, you can add exceptions if this is intentional. 17 | 18 | If there is just one subview you want to allow overlaps for, you can add it to viewsAllowingOverlap in the validation block of your test. 19 | 20 | Autolayout is not ambiguous and doesn't throw errors 21 | ==================================================== 22 | 23 | The test verifies that Autolayout is not ambiguous and also checks that there are no errors from having too many constraints. 24 | 25 | Accessibility sanity check 26 | ========================== 27 | 28 | The test runs some basic accessibility sanity checks. These are not comprehensive, but will catch some common errors. These include: 29 | 30 | - All UIControl subclasses have accessibility labels set. This way, accessibility users will be able to interact with them. You can add to this list of classes by adding to viewClassesRequiringAccessibilityLabels. 31 | - All elements with accessibility identifiers also have accessibility labels. Otherwise, the identifiers will be read out. 32 | - No elements with accessibility labels are nested inside other elements with accessibility labels. Accessibility doesn't support nested elements, and nested elements are not reachable by accessibility users. 33 | 34 | You can add views that allow accessibility errors to viewsAllowingAccessibilityErrors in the validation block. 35 | -------------------------------------------------------------------------------- /docs/pages/050_propertyTesting.rst: -------------------------------------------------------------------------------- 1 | Property Testing 2 | ---------------- 3 | 4 | This library is based on the idea of property testing. Property testing is basically when you create a bunch of inputs to a function and assert properties on the output. It gained popularity in Haskell with the QuickCheck library and in Scala with ScalaCheck. 5 | 6 | For an overview of property based testing, see: 7 | 8 | http://blog.jessitron.com/2013/04/property-based-testing-what-is-it.html 9 | 10 | Differences 11 | =========== 12 | 13 | The main difference between this library and traditional property testing is that traditional property testing uses truly random input. For now, this library is deterministic, but takes the idea of running many combinations from property based testing. 14 | 15 | Also, QuickCheck and ScalaTest attempt to provide the smallest possible input for a failure. So if it fails on an array of 100 elements, it will try it with some of the elements removed, and when it presents a failure will try to give the smallest input it failed on. This library does not attempt anything like that, but it's a good idea for the future. 16 | -------------------------------------------------------------------------------- /docs/pages/060_catalog.rst: -------------------------------------------------------------------------------- 1 | UI Catalog 2 | ---------- 3 | 4 | Since you've already written code to inflate your view with many combinations of data, you can leverage this to create a UICatalog. Basically, you can create a UITableView or UICollectionView of all of your views and inspect them visually. This can help you iterate on APIs fast with fake data and also catch edge cases. It can also be useful to visualize all the edge cases for a view to other members of your team. 5 | 6 | .. IMPORTANT:: 7 | Since LayoutTestBase contains references to private APIs (for Autolayout failure detection), you not use UICatalog in your production application. Instead, you should probably create a separate target for the catalog. 8 | 9 | For more info, see LYTCatalogTableViewController, LYTCatalogCollectionViewController and LYTViewCatalogProvider. If you run the catalog target in the sample app, you can see an example of what the catalog will provide. It looks something like this: 10 | 11 | .. image:: ../images/catalog.png 12 | :width: 50% 13 | :align: center 14 | 15 | .. NOTE:: 16 | This part of the library is currently less used and hence less mature and tested than other parts of the library. Please give feedback on the Github page if you find any issues. 17 | -------------------------------------------------------------------------------- /docs/pages/070_apis.rst: -------------------------------------------------------------------------------- 1 | Other APIs 2 | ---------- 3 | 4 | There are some other helper APIs to help you write tests quickly. As always, the code is the best place for up to date documentation. 5 | 6 | Autolayout 7 | ========== 8 | 9 | LYTAutolayoutFailureIntercepter - Globally detects when throws an error when laying out a view with Autolayout. This programmatically catches the errors you see in the console when you have too many constraints. 10 | 11 | UIView Helpers 12 | ============== 13 | 14 | UIView (LYTHelpers) - provides quick lookups for getting x, y, width, height etc. 15 | 16 | UIView (LYTFrameComparison) - provides helpers for verifying relative frames of two views. This includes verifying the top is aligned or that one view is above another. It includes support for right to left languages. 17 | 18 | UIView (LYTTestHelpers) - provides some common tests which most views should pass. These are run automatically when you subclass LYTLayoutTestCase or LayoutTestCase. 19 | -------------------------------------------------------------------------------- /docs/pages/080_config.rst: -------------------------------------------------------------------------------- 1 | Config 2 | ------ 3 | 4 | The LYTConfig class allows you to set global config for the library. Most of these properties can also be set individually on tests. If they are set in both places, the test's methods are used. 5 | -------------------------------------------------------------------------------- /docs/pages/090_testingOtherObjects.rst: -------------------------------------------------------------------------------- 1 | Testing Other Objects 2 | --------------------- 3 | 4 | Usually, you want to test a UIView subclass, but sometimes, you need to track other objects as well. For instance, you may want to test the view of a UIViewController subclass. To test this, you need to have a reference to the UIViewController in your tests (to access IBOutlets and other things). To do this, you can use the context parameter. This can be used for many other things but testing UIViewControllers serves as a good example. 5 | 6 | Sample Code 7 | =========== 8 | 9 | .. code-block:: objective-c 10 | 11 | // Objective-C 12 | // LYTViewProvider 13 | + (nullable UIView *)viewForData:(NSDictionary *)data 14 | reuseView:(nullable UIView *)reuseView 15 | size:(nullable LYTViewSize *)size 16 | context:(id _Nullable * _Nullable)context 17 | error:(__unused NSError * _Nullable __autoreleasing *)error { 18 | // Ignoring reuse because it doesn't make sense for testing UIViewController subclasses 19 | SampleViewController *controller = [[SampleViewController alloc] init]; 20 | [controller setupWithDictionary:data]; 21 | *context = controller; 22 | return controller.view; 23 | } 24 | 25 | // LYTLayoutTest 26 | - (void)testViewController { 27 | [self runLayoutTestsWithViewProvider:[SampleViewController class] 28 | validation:^(UIView * view, NSDictionary * data, id context) { 29 | SampleViewController *controller = context; 30 | // Rest of your test goes here 31 | } 32 | } 33 | 34 | .. code-block:: objective-c 35 | 36 | // Swift 37 | // LYTViewProvider 38 | class func viewForData(data: [NSObject: AnyObject], 39 | reuseView: UIView?, 40 | size: LYTViewSize?, 41 | context: AutoreleasingUnsafeMutablePointer) throws -> UIView { 42 | // Ignoring reuse because it doesn't make sense for testing UIViewController subclasses 43 | let controller = SampleViewController() 44 | controller.setupWithData(data) 45 | context.memory = controller 46 | return controller.view 47 | } 48 | 49 | // LayoutTest 50 | func testViewController() { 51 | runLayoutTestsWithViewProvider(SampleViewController.self) { (view: UIView, data: [NSObject: AnyObject], context: Any?) in 52 | let controller = context as! SampleViewController 53 | // Rest of your test goes here 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /docs/pages/100_performance.rst: -------------------------------------------------------------------------------- 1 | Performance 2 | ----------- 3 | 4 | Since a simple test can run hundreds or thousands of times, sometimes these tests can start to run slowly. There are multiple ways to make the tests run faster. 5 | 6 | How many times will it run? 7 | =========================== 8 | 9 | Currently, the number of test runs will be: 10 | 11 | :: 12 | 13 | number_of_combinations_from_data_spec * number_of_sizes_to_test 14 | 15 | You can limit the number of test runs by decreasing either of these values. 16 | 17 | Limit LYTDataValues 18 | =================== 19 | 20 | The more LYTDataValues subclasses you use in the data spec, the more tests will get run. You should only use LYTDataValues subclasses for things you actually want to test. For opaque values which don't really matter or don't affect layout (eg. id fields, tracking tokens), you may want to consider hard coding a single value. 21 | 22 | Limit Results API 23 | ================= 24 | 25 | When running tests, you can also pass in a limitResults flag. See LYTTesterLimitResults in LYTLayoutPropertyTester.h for full documentation. 26 | 27 | Currently, results can be limited in two different ways. 28 | 29 | ======================= 30 | Limit Data Combinations 31 | ======================= 32 | 33 | With this flag on, the library will not choose every combination of data from the data spec. Instead, it will choose each single value at least once, but not all combinations of values. This makes the number of combinations O(m*n) instead of O(m^n) where m is the number of values in each LYTDataValues subclass and n is the number of LYTDataValues subclasses in the data spec. 34 | 35 | Effectively, with this flag on, we assume independence of the data values. So, we assume that only single value changes will cause test failures and not combinations of values. Of course, this isn't strictly true, but it can help you run tests with larger data specs easily without losing too many tests. 36 | 37 | ============= 38 | No View Sizes 39 | ============= 40 | 41 | This ignores the view sizes passed in and runs the test on one size (the original size of the view). 42 | -------------------------------------------------------------------------------- /docs/pages/110_projectStructure.rst: -------------------------------------------------------------------------------- 1 | Project Structure 2 | ----------------- 3 | 4 | The project is currently split into two CocoaPods: LayoutTestBase and LayoutTest. 5 | 6 | LayoutTestBase contains most of the library (which is then split into subspecs), but doesn't contain anything with a dependency on XCTest. This allows applications to use LayoutTestBase in a runnable application. This is useful for using the catalog view controllers or just using some of the helpers. 7 | 8 | LayoutTest depends on LayoutTestBase and contains all the files which require XCTest. 9 | 10 | This is not the easiest setup to use because it requires you to import both frameworks when you use the library, but currently, we have not found a better way to setup the project. 11 | 12 | Swift 13 | ===== 14 | 15 | All the swift code is kept in a subspec. This subspec is NOT included automatically in the main import. This is intentional since it seems most apps wanting to use this library are written in Objective-C and don't want to deal with the pain of swift dependencies. 16 | 17 | Subspecs 18 | ======== 19 | 20 | We considered using subspecs for the base code and XCTest dependent code, but couldn't because of this ticket: https://github.com/CocoaPods/CocoaPods/issues/4629 21 | -------------------------------------------------------------------------------- /docs/pages/120_FAQ.rst: -------------------------------------------------------------------------------- 1 | FAQ 2 | --- 3 | 4 | No questions yet, but reserving this page for questions once we get them. 5 | -------------------------------------------------------------------------------- /docs/pages/130_future.rst: -------------------------------------------------------------------------------- 1 | Future Plans 2 | ------------ 3 | 4 | Over time, we hope to add more features to the library and improve the API. Here's some of the big issues we are thinking about adding in the future: 5 | 6 | 1. Allow for an option of random generators. 7 | 2. Implement the 'smaller' protocol like Haskell and ScalaCheck to find the smallest possible failing case (instead of hundreds of failing cases). 8 | --------------------------------------------------------------------------------