Summary: In YogaKit, nodes are not dirtied when `sizeThatFits` of leaf nodes change. This makes it difficult to reuse views like `UILabel` when their content (text) changes. I wrote a failing test to prove it and wrote a fix by caching the latest value of `sizeThatFits` in `YGNodeBridge` and checking for changes in `YGAttachNodesFromViewHierachy` (renamed to `YGUpdateNodesFromViewHierachy` to make it clear it's not simply reattaching nodes anymore). When a new size is detected, I call `YGNodeMarkDirty` on the leaf node. **Unfortunately**, this makes another test fail for no logical reason (`testThatViewNotIncludedInFirstLayoutPassAreIncludedInSecond`). Either there's something I don't understand or I've un-advertedly unearthed a bug in the C implementation, but in what case would setting a node dirty make the layout return a different size? Closes https://github.com/facebook/yoga/pull/308 Reviewed By: emilsjolander Differential Revision: D4378826 Pulled By: dshahidehpour fbshipit-source-id: cd0640bc863debb9e434370739416e3e33e57a08
388 lines
13 KiB
Objective-C
388 lines
13 KiB
Objective-C
/**
|
|
* Copyright (c) 2014-present, Facebook, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*/
|
|
|
|
#import <XCTest/XCTest.h>
|
|
|
|
#import <YogaKit/UIView+Yoga.h>
|
|
|
|
@interface YogaKitTests : XCTestCase
|
|
@end
|
|
|
|
@implementation YogaKitTests
|
|
|
|
#ifndef TRAVIS_CI
|
|
|
|
- (void)testNodesAreDeallocedWithSingleView
|
|
{
|
|
XCTAssertEqual(0, YGNodeGetInstanceCount());
|
|
|
|
UIView *view = [[UIView alloc] initWithFrame:CGRectZero];
|
|
[view yg_setFlexBasis:1];
|
|
XCTAssertEqual(1, YGNodeGetInstanceCount());
|
|
view = nil;
|
|
|
|
XCTAssertEqual(0, YGNodeGetInstanceCount());
|
|
}
|
|
|
|
- (void)testNodesAreDeallocedCascade
|
|
{
|
|
XCTAssertEqual(0, YGNodeGetInstanceCount());
|
|
|
|
UIView *view = [[UIView alloc] initWithFrame:CGRectZero];
|
|
[view yg_setFlexBasis:1];
|
|
|
|
for (int i=0; i<10; i++) {
|
|
UIView *subview = [[UIView alloc] initWithFrame:CGRectZero];
|
|
[subview yg_setFlexBasis:1];
|
|
[view addSubview:subview];
|
|
}
|
|
XCTAssertEqual(11, YGNodeGetInstanceCount());
|
|
view = nil;
|
|
|
|
XCTAssertEqual(0, YGNodeGetInstanceCount());
|
|
}
|
|
|
|
#endif
|
|
|
|
- (void)testUsesYoga
|
|
{
|
|
UIView *view = [[UIView alloc] initWithFrame:CGRectZero];
|
|
XCTAssertFalse([view yg_usesYoga]);
|
|
|
|
[view yg_setUsesYoga:YES];
|
|
XCTAssertTrue([view yg_usesYoga]);
|
|
|
|
[view yg_setUsesYoga:NO];
|
|
XCTAssertFalse([view yg_usesYoga]);
|
|
}
|
|
|
|
- (void)testSizeThatFitsAsserts
|
|
{
|
|
UIView *view = [[UIView alloc] initWithFrame:CGRectZero];
|
|
dispatch_sync(dispatch_queue_create("com.facebook.Yoga.testing", DISPATCH_QUEUE_SERIAL), ^(void){
|
|
XCTAssertThrows([view yg_intrinsicSize]);
|
|
});
|
|
}
|
|
|
|
- (void)testSizeThatFitsSmoke
|
|
{
|
|
UIView *container = [[UIView alloc] initWithFrame:CGRectZero];
|
|
[container yg_setUsesYoga:YES];
|
|
[container yg_setFlexDirection:YGFlexDirectionRow];
|
|
[container yg_setAlignItems:YGAlignFlexStart];
|
|
|
|
UILabel *longTextLabel = [[UILabel alloc] initWithFrame:CGRectZero];
|
|
longTextLabel.text = @"This is a very very very very very very very very long piece of text.";
|
|
longTextLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
|
longTextLabel.numberOfLines = 1;
|
|
[longTextLabel yg_setUsesYoga:YES];
|
|
[longTextLabel yg_setFlexShrink:1];
|
|
[container addSubview:longTextLabel];
|
|
|
|
UIView *textBadgeView = [[UIView alloc] initWithFrame:CGRectZero];
|
|
[textBadgeView yg_setUsesYoga:YES];
|
|
[textBadgeView yg_setMargin:3.0 forEdge:YGEdgeLeft];
|
|
[textBadgeView yg_setWidth:10];
|
|
[textBadgeView yg_setHeight:10];
|
|
[container addSubview:textBadgeView];
|
|
|
|
const CGSize containerSize = [container yg_intrinsicSize];
|
|
XCTAssertTrue(CGSizeEqualToSize(CGSizeMake(514,21), containerSize), @"Size is actually %@", NSStringFromCGSize(containerSize));
|
|
}
|
|
|
|
- (void)testThatMarkingLeafsAsDirtyWillTriggerASizeRecalculation
|
|
{
|
|
UIView *container = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 500, 50)];
|
|
[container yg_setUsesYoga:YES];
|
|
[container yg_setFlexDirection:YGFlexDirectionRow];
|
|
[container yg_setAlignItems:YGAlignFlexStart];
|
|
|
|
UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
|
|
label.text = @"This is a short text.";
|
|
label.numberOfLines = 1;
|
|
[label yg_setUsesYoga:YES];
|
|
[container addSubview:label];
|
|
|
|
[container yg_applyLayout];
|
|
XCTAssertTrue(CGSizeEqualToSize(CGSizeMake(146,21), label.bounds.size), @"Size is actually %@", NSStringFromCGSize(label.bounds.size));
|
|
|
|
label.text = @"This is a slightly longer text.";
|
|
[label yg_markDirty];
|
|
|
|
[container yg_applyLayout];
|
|
XCTAssertTrue(CGSizeEqualToSize(CGSizeMake(213,21), label.bounds.size), @"Size is actually %@", NSStringFromCGSize(label.bounds.size));
|
|
}
|
|
|
|
- (void)testFrameAndOriginPlacement
|
|
{
|
|
const CGSize containerSize = CGSizeMake(320, 50);
|
|
|
|
UIView *container = [[UIView alloc] initWithFrame:CGRectMake(0, 0, containerSize.width, containerSize.height)];
|
|
[container yg_setUsesYoga:YES];
|
|
[container yg_setFlexDirection:YGFlexDirectionRow];
|
|
|
|
for (int i = 0; i < 3; i++) {
|
|
UIView *subview = [[UIView alloc] initWithFrame:CGRectZero];
|
|
[subview yg_setUsesYoga:YES];
|
|
[subview yg_setFlexGrow:1];
|
|
|
|
[container addSubview:subview];
|
|
}
|
|
[container yg_applyLayout];
|
|
|
|
XCTAssertFalse(CGRectIntersectsRect([container.subviews objectAtIndex:0].frame, [container.subviews objectAtIndex:1].frame));
|
|
XCTAssertFalse(CGRectIntersectsRect([container.subviews objectAtIndex:1].frame, [container.subviews objectAtIndex:2].frame));
|
|
XCTAssertFalse(CGRectIntersectsRect([container.subviews objectAtIndex:0].frame, [container.subviews objectAtIndex:2].frame));
|
|
|
|
CGFloat totalWidth = 0;
|
|
for (UIView *view in container.subviews) {
|
|
totalWidth += view.bounds.size.width;
|
|
}
|
|
|
|
XCTAssertEqual(containerSize.width, totalWidth, @"The container's width is %.6f, the subviews take up %.6f", containerSize.width, totalWidth);
|
|
}
|
|
|
|
- (void)testThatLayoutIsCorrectWhenWeSwapViewOrder
|
|
{
|
|
const CGSize containerSize = CGSizeMake(300, 50);
|
|
|
|
UIView *container = [[UIView alloc] initWithFrame:CGRectMake(0, 0, containerSize.width, containerSize.height)];
|
|
[container yg_setUsesYoga:YES];
|
|
[container yg_setFlexDirection:YGFlexDirectionRow];
|
|
|
|
UIView *subview1 = [[UIView alloc] initWithFrame:CGRectZero];
|
|
[subview1 yg_setUsesYoga:YES];
|
|
[subview1 yg_setFlexGrow:1];
|
|
[container addSubview:subview1];
|
|
|
|
UIView *subview2 = [[UIView alloc] initWithFrame:CGRectZero];
|
|
[subview2 yg_setUsesYoga:YES];
|
|
[subview2 yg_setFlexGrow:1];
|
|
[container addSubview:subview2];
|
|
|
|
UIView *subview3 = [[UIView alloc] initWithFrame:CGRectZero];
|
|
[subview3 yg_setUsesYoga:YES];
|
|
[subview3 yg_setFlexGrow:1];
|
|
[container addSubview:subview3];
|
|
|
|
[container yg_applyLayout];
|
|
|
|
XCTAssertTrue(CGRectEqualToRect(subview1.frame, CGRectMake(0, 0, 100, 50)));
|
|
XCTAssertTrue(CGRectEqualToRect(subview2.frame, CGRectMake(100, 0, 100, 50)), @"It's actually %@", NSStringFromCGRect(subview2.frame));
|
|
XCTAssertTrue(CGRectEqualToRect(subview3.frame, CGRectMake(200, 0, 100, 50)));
|
|
|
|
[container exchangeSubviewAtIndex:2 withSubviewAtIndex:0];
|
|
[subview2 yg_setIncludeInLayout:NO];
|
|
[container yg_applyLayout];
|
|
|
|
XCTAssertTrue(CGRectEqualToRect(subview3.frame, CGRectMake(0, 0, 150, 50)));
|
|
XCTAssertTrue(CGRectEqualToRect(subview1.frame, CGRectMake(150, 0, 150, 50)));
|
|
|
|
// this frame shouldn't have been modified since last time.
|
|
XCTAssertTrue(CGRectEqualToRect(subview2.frame, CGRectMake(100, 0, 100, 50)));
|
|
}
|
|
|
|
- (void)testThatWeRespectIncludeInLayoutFlag
|
|
{
|
|
const CGSize containerSize = CGSizeMake(300, 50);
|
|
|
|
UIView *container = [[UIView alloc] initWithFrame:CGRectMake(0, 0, containerSize.width, containerSize.height)];
|
|
[container yg_setUsesYoga:YES];
|
|
[container yg_setFlexDirection:YGFlexDirectionRow];
|
|
|
|
UIView *subview1 = [[UIView alloc] initWithFrame:CGRectZero];
|
|
[subview1 yg_setUsesYoga:YES];
|
|
[subview1 yg_setFlexGrow:1];
|
|
[container addSubview:subview1];
|
|
|
|
UIView *subview2 = [[UIView alloc] initWithFrame:CGRectZero];
|
|
[subview2 yg_setUsesYoga:YES];
|
|
[subview2 yg_setFlexGrow:1];
|
|
[container addSubview:subview2];
|
|
|
|
UIView *subview3 = [[UIView alloc] initWithFrame:CGRectZero];
|
|
[subview3 yg_setUsesYoga:YES];
|
|
[subview3 yg_setFlexGrow:1];
|
|
[container addSubview:subview3];
|
|
|
|
[container yg_applyLayout];
|
|
|
|
for (UIView *view in container.subviews) {
|
|
XCTAssertTrue(CGSizeEqualToSize(CGSizeMake(100, 50), subview1.bounds.size), @"Actual size is %@", NSStringFromCGSize(view.bounds.size));
|
|
}
|
|
|
|
[subview3 yg_setIncludeInLayout:NO];
|
|
[container yg_applyLayout];
|
|
|
|
XCTAssertTrue(CGSizeEqualToSize(CGSizeMake(150, 50), subview1.bounds.size), @"Actual size is %@", NSStringFromCGSize(subview1.bounds.size));
|
|
XCTAssertTrue(CGSizeEqualToSize(CGSizeMake(150, 50), subview2.bounds.size), @"Actual size is %@", NSStringFromCGSize(subview2.bounds.size));
|
|
|
|
// We don't set the frame to zero, so, it should be set to what it was previously at.
|
|
XCTAssertTrue(CGSizeEqualToSize(CGSizeMake(100, 50), subview3.bounds.size), @"Actual size is %@", NSStringFromCGSize(subview3.bounds.size));
|
|
}
|
|
|
|
- (void)testThatNumberOfChildrenIsCorrectWhenWeIgnoreSubviews
|
|
{
|
|
UIView *container = [[UIView alloc] initWithFrame:CGRectZero];
|
|
[container yg_setUsesYoga:YES];
|
|
[container yg_setFlexDirection:YGFlexDirectionRow];
|
|
|
|
UIView *subview1 = [[UIView alloc] initWithFrame:CGRectZero];
|
|
[subview1 yg_setUsesYoga:YES];
|
|
[subview1 yg_setIncludeInLayout:NO];
|
|
[container addSubview:subview1];
|
|
|
|
UIView *subview2 = [[UIView alloc] initWithFrame:CGRectZero];
|
|
[subview2 yg_setUsesYoga:YES];
|
|
[subview2 yg_setIncludeInLayout:NO];
|
|
[container addSubview:subview2];
|
|
|
|
UIView *subview3 = [[UIView alloc] initWithFrame:CGRectZero];
|
|
[subview3 yg_setUsesYoga:YES];
|
|
[subview3 yg_setIncludeInLayout:YES];
|
|
[container addSubview:subview3];
|
|
|
|
[container yg_applyLayout];
|
|
XCTAssertEqual(1, [container yg_numberOfChildren]);
|
|
|
|
[subview2 yg_setIncludeInLayout:YES];
|
|
[container yg_applyLayout];
|
|
XCTAssertEqual(2, [container yg_numberOfChildren]);
|
|
}
|
|
|
|
- (void)testThatViewNotIncludedInFirstLayoutPassAreIncludedInSecond
|
|
{
|
|
UIView *container = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 300, 50)];
|
|
[container yg_setUsesYoga:YES];
|
|
[container yg_setFlexDirection:YGFlexDirectionRow];
|
|
|
|
UIView *subview1 = [[UIView alloc] initWithFrame:CGRectZero];
|
|
[subview1 yg_setUsesYoga:YES];
|
|
[subview1 yg_setFlexGrow:1];
|
|
[container addSubview:subview1];
|
|
|
|
UIView *subview2 = [[UIView alloc] initWithFrame:CGRectZero];
|
|
[subview2 yg_setUsesYoga:YES];
|
|
[subview2 yg_setFlexGrow:1];
|
|
[container addSubview:subview2];
|
|
|
|
UIView *subview3 = [[UIView alloc] initWithFrame:CGRectZero];
|
|
[subview3 yg_setUsesYoga:YES];
|
|
[subview3 yg_setFlexGrow:1];
|
|
[subview3 yg_setIncludeInLayout:NO];
|
|
[container addSubview:subview3];
|
|
|
|
[container yg_applyLayout];
|
|
|
|
XCTAssertTrue(CGSizeEqualToSize(CGSizeMake(150, 50), subview1.bounds.size), @"Actual size is %@", NSStringFromCGSize(subview1.bounds.size));
|
|
XCTAssertTrue(CGSizeEqualToSize(CGSizeMake(150, 50), subview2.bounds.size), @"Actual size is %@", NSStringFromCGSize(subview2.bounds.size));
|
|
XCTAssertTrue(CGSizeEqualToSize(CGSizeZero, subview3.bounds.size), @"Actual size %@", NSStringFromCGSize(subview3.bounds.size));
|
|
|
|
[subview3 yg_setIncludeInLayout:YES];
|
|
[container yg_applyLayout];
|
|
for (UIView *view in container.subviews) {
|
|
XCTAssertTrue(CGSizeEqualToSize(CGSizeMake(100, 50), subview1.bounds.size), @"Actual size is %@", NSStringFromCGSize(view.bounds.size));
|
|
}
|
|
}
|
|
|
|
- (void)testyg_isLeafFlag
|
|
{
|
|
UIView *view = [[UIView alloc] initWithFrame:CGRectZero];
|
|
XCTAssertTrue(view.yg_isLeaf);
|
|
|
|
for (int i=0; i<10; i++) {
|
|
UIView *subview = [[UIView alloc] initWithFrame:CGRectZero];
|
|
[view addSubview:subview];
|
|
}
|
|
XCTAssertTrue(view.yg_isLeaf);
|
|
|
|
[view yg_setUsesYoga:YES];
|
|
[view yg_setWidth:50.0];
|
|
XCTAssertTrue(view.yg_isLeaf);
|
|
|
|
UIView *const subview = view.subviews[0];
|
|
[subview yg_setUsesYoga:YES];
|
|
[subview yg_setWidth:50.0];
|
|
XCTAssertFalse(view.yg_isLeaf);
|
|
}
|
|
|
|
- (void)testThatWeCorrectlyAttachNestedViews
|
|
{
|
|
UIView *container = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 300, 50)];
|
|
[container yg_setUsesYoga:YES];
|
|
[container yg_setFlexDirection:YGFlexDirectionColumn];
|
|
|
|
UIView *subview1 = [[UIView alloc] initWithFrame:CGRectZero];
|
|
[subview1 yg_setUsesYoga:YES];
|
|
[subview1 yg_setWidth:100];
|
|
[subview1 yg_setFlexGrow:1];
|
|
[subview1 yg_setFlexDirection:YGFlexDirectionColumn];
|
|
[container addSubview:subview1];
|
|
|
|
UIView *subview2 = [[UIView alloc] initWithFrame:CGRectZero];
|
|
[subview2 yg_setUsesYoga:YES];
|
|
[subview2 yg_setWidth:150];
|
|
[subview2 yg_setFlexGrow:1];
|
|
[subview2 yg_setFlexDirection:YGFlexDirectionColumn];
|
|
[container addSubview:subview2];
|
|
|
|
for (UIView *view in @[subview1, subview2]) {
|
|
UIView *someView = [[UIView alloc] initWithFrame:CGRectZero];
|
|
[someView yg_setUsesYoga:YES];
|
|
[someView yg_setFlexGrow:1];
|
|
[view addSubview:someView];
|
|
}
|
|
[container yg_applyLayout];
|
|
|
|
// Add the same amount of new views, reapply layout.
|
|
for (UIView *view in @[subview1, subview2]) {
|
|
UIView *someView = [[UIView alloc] initWithFrame:CGRectZero];
|
|
[someView yg_setUsesYoga:YES];
|
|
[someView yg_setFlexGrow:1];
|
|
[view addSubview:someView];
|
|
}
|
|
[container yg_applyLayout];
|
|
|
|
XCTAssertTrue(CGSizeEqualToSize(CGSizeMake(100, 25), subview1.bounds.size), @"Actual size is %@", NSStringFromCGSize(subview1.bounds.size));
|
|
for (UIView *subview in subview1.subviews) {
|
|
const CGSize subviewSize = subview.bounds.size;
|
|
XCTAssertFalse(CGSizeEqualToSize(CGSizeZero, subviewSize));
|
|
XCTAssertFalse(isnan(subviewSize.height));
|
|
XCTAssertFalse(isnan(subviewSize.width));
|
|
}
|
|
|
|
XCTAssertTrue(CGSizeEqualToSize(CGSizeMake(150, 25), subview2.bounds.size), @"Actual size is %@", NSStringFromCGSize(subview2.bounds.size));
|
|
for (UIView *subview in subview2.subviews) {
|
|
const CGSize subviewSize = subview.bounds.size;
|
|
XCTAssertFalse(CGSizeEqualToSize(CGSizeZero, subview.bounds.size));
|
|
XCTAssertFalse(isnan(subviewSize.height));
|
|
XCTAssertFalse(isnan(subviewSize.width));
|
|
}
|
|
}
|
|
|
|
- (void)testThatANonLeafNodeCanBecomeALeafNode
|
|
{
|
|
UIView *container = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 300, 50)];
|
|
[container yg_setUsesYoga:YES];
|
|
|
|
UIView *subview1 = [[UIView alloc] initWithFrame:CGRectZero];
|
|
[subview1 yg_setUsesYoga:YES];
|
|
[container addSubview:subview1];
|
|
|
|
UIView *subview2 = [[UIView alloc] initWithFrame:CGRectZero];
|
|
[subview2 yg_setUsesYoga:YES];
|
|
[subview1 addSubview:subview2];
|
|
|
|
[container yg_applyLayout];
|
|
[subview2 removeFromSuperview];
|
|
[container yg_applyLayout];
|
|
}
|
|
|
|
@end
|