Skip to content

Commit

Permalink
Add tests to NIAttributedLabel for ensuring that accessibleElements h…
Browse files Browse the repository at this point in the history
…ave the correct frames under different link orderings whether merging multiline links or not. This file is still missing a lot of coverage but this is a good start.

PiperOrigin-RevId: 560782099
  • Loading branch information
Nobody authored and material-automation committed Aug 28, 2023
1 parent 1e03c49 commit fd5143a
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 39 deletions.
40 changes: 40 additions & 0 deletions src/attributedlabel/src/NIAttributedLabel.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,45 @@ typedef NS_ENUM(NSInteger, NILinkOrdering) {
extern NSString* const NIAttributedLabelLinkAttributeName; // Value is an NSTextCheckingResult.

@protocol NIAttributedLabelDelegate;
/**
* @internal
*
* The NIViewAccessibilityElement class encapsulates information about an item
* that should be accessible to users with disabilities, but isn't accessible
* by default and might be used in animations.
*
* Differences between UIAccessibilityElement and NIViewAccessibilityElement:
*
* - The accessibilityContainer must be a UIView.
* - The accessibilityFrame is recomputed every time from the frameInContainer
* and the accessibilityContainer.
* - The accessibilityPath and accessibilityActivationPoint (if applicable) are
* recomputed every time from the pointsInContainer and the accessibilityContainer.
*
* These differences cease to be as soon as the initial accessibility container
* is changed externally, which is internally tracked by isContainerValid.
*/
@interface NIViewAccessibilityElement : UIAccessibilityElement

- (instancetype)initWithAccessibilityContainer:(id)container
frameInContainer:(CGRect)frameInContainer
pointsInContainer:(NSArray *)pointsInContainer;

- (instancetype)initWithAccessibilityContainer:(id)container
frameInContainer:(CGRect)frameInContainer;

// This frame is in the accessibilityContainer coordinates.
@property(nonatomic, readonly) CGRect frameInContainer;

// The first element of the array is the accessibilityActivationPoint, the rest of the array is the
// accessibilityPath.
@property(nonatomic, readonly) NSArray<NSValue*> *pointsInContainer;

/// If set to @c YES, this element remembers the last valid accessibility container when it receives
/// a new one.
@property(nonatomic) BOOL rememberLastValidContainer;

@end

/**
* The NIAttributedLabel class provides support for displaying rich text with selectable links and
Expand Down Expand Up @@ -179,6 +218,7 @@ extern NSString* const NIAttributedLabelLinkAttributeName; // Value is an NSText
- (void)insertImage:(UIImage *)image atIndex:(NSInteger)index margins:(UIEdgeInsets)margins verticalTextAlignment:(NIVerticalTextAlignment)verticalTextAlignment;

- (void)invalidateAccessibleElements;
- (NSArray *)accessibleElements;

- (NSTextCheckingResult *)linkAtPoint:(CGPoint)point;

Expand Down
39 changes: 1 addition & 38 deletions src/attributedlabel/src/NIAttributedLabel.m
Original file line number Diff line number Diff line change
Expand Up @@ -124,44 +124,6 @@ CGSize NISizeOfAttributedStringConstrainedToSize(NSAttributedString* attributedS
return CGSizeMake(NICGFloatCeil(newSize.width), NICGFloatCeil(newSize.height));
}

/**
* @internal
*
* The NIViewAccessibilityElement class encapsulates information about an item
* that should be accessible to users with disabilities, but isn't accessible
* by default and might be used in animations.
*
* Differences between UIAccessibilityElement and NIViewAccessibilityElement:
*
* - The accessibilityContainer must be a UIView.
* - The accessibilityFrame is recomputed every time from the frameInContainer
* and the accessibilityContainer.
* - The accessibilityPath and accessibilityActivationPoint (if applicable) are
* recomputed every time from the pointsInContainer and the accessibilityContainer.
*
* These differences cease to be as soon as the initial accessibility container
* is changed externally, which is internally tracked by isContainerValid.
*/
@interface NIViewAccessibilityElement : UIAccessibilityElement

- (instancetype)initWithAccessibilityContainer:(id)container
frameInContainer:(CGRect)frameInContainer
pointsInContainer:(NSArray *)pointsInContainer;

- (instancetype)initWithAccessibilityContainer:(id)container frameInContainer:(CGRect)frameInContainer;

// This frame is in the accessibilityContainer coordinates.
@property (nonatomic, readonly) CGRect frameInContainer;

// The first element of the array is the accessibilityActivationPoint, the rest of the array is the
// accessibilityPath.
@property (nonatomic, readonly) NSArray *pointsInContainer; // of NSValue

/// If set to @c YES, this element remembers the last valid accessibility container when it receives
/// a new one.
@property(nonatomic) BOOL rememberLastValidContainer;

@end

@interface NIViewAccessibilityElement ()

Expand Down Expand Up @@ -1201,6 +1163,7 @@ - (NIViewAccessibilityElement *)accessibilityElementForRange:(NSRange)range {
return nil;
}

// Calculate multiline link bounds using boundsForRects.
CGRect bounds = [self boundsForRects:rects];
CGRect firstRect = [[rects firstObject] CGRectValue];
// The activation point can be any point in the area. Let's make it the center of the first small
Expand Down
153 changes: 152 additions & 1 deletion src/attributedlabel/unittests/NIAttributedLabelTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@

// See: http://bit.ly/hS5nNh for unit test macros.

#import "NimbusAttributedLabel.h"

#import <Foundation/Foundation.h>
#import <XCTest/XCTest.h>

#import "NimbusAttributedLabel.h"
#import "NIAttributedLabel+Testing.h"

@interface NIAttributedLabelTestMonitor : NSObject <NIAttributedLabelDelegate>
Expand Down Expand Up @@ -58,6 +59,25 @@ @interface NIAttributedLabelTests : XCTestCase

@implementation NIAttributedLabelTests

+ (NIAttributedLabel *)makeGenericMultilineLabelWithFrame:(CGRect)frame {
// Create an attributed string with a link.
NSString *string = @"Unlinked content. Linked content.\nLinked content. Unlinked content.";
NSURL *URL = [NSURL URLWithString:@"http://youtube.com"];
NSTextCheckingResult *textCheckingResult =
[NSTextCheckingResult linkCheckingResultWithRange:NSMakeRange(18, 32) URL:URL];
NSDictionary<NSAttributedStringKey, id> *attributes =
@{NIAttributedLabelLinkAttributeName : textCheckingResult};
NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:string
attributes:attributes];

// Create a label.
NIAttributedLabel *label = [[NIAttributedLabel alloc] initWithFrame:frame];
label.attributedText = attributedText;
label.numberOfLines = 0;

return label;
}

- (void)testLongPressCallbackOverridesActionSheetCallbackWhenTouchingLink {
// Create an attributed string with a link.
NSString *string = @"NimbusKit";
Expand Down Expand Up @@ -109,4 +129,135 @@ - (void)testActionSheetCallbackDeliveredWhenTouchingLink {
XCTAssertEqual(monitor.actionSheetCallbackCount, 1);
}

- (void)testPreOrderedLinks {
CGRect labelFrame = CGRectMake(0,0,200,200);
NIAttributedLabel *label = [NIAttributedLabelTests makeGenericMultilineLabelWithFrame:labelFrame];

// Create pre-order non-merged multiline links.
label.linkOrdering = NILinkOrderingFirst;
label.shouldMergeMultilineLinks = NO;
NSMutableArray<NIViewAccessibilityElement *> *accessibleElements =
[label.accessibleElements copy];

// Given labelFrame (0,0,200), expect {(96, 3, 81, 12),(0, 18, 85, 12),(0,0,200,200)}.
CGRect firstFrame = [accessibleElements objectAtIndex:0].frameInContainer;
CGRect secondFrame = [accessibleElements objectAtIndex:1].frameInContainer;
// Test second frame starts at start of labelFrame.
XCTAssertEqual(secondFrame.origin.x, labelFrame.origin.x);
// Test first frame starts after second frame.
XCTAssertGreaterThan(firstFrame.origin.x,secondFrame.origin.x);
// Test first frame start above second frame.
XCTAssertLessThan(firstFrame.origin.y,secondFrame.origin.y);
// Test both frames have the same height (same attribute).
XCTAssertEqual(firstFrame.size.height,secondFrame.size.height);
// Test last element is labelFrame.
XCTAssertTrue(CGRectEqualToRect([accessibleElements objectAtIndex:2].frameInContainer, labelFrame));

// Create pre-order merged multiline links.
[label invalidateAccessibleElements];
label.shouldMergeMultilineLinks = YES;
NSMutableArray<NIViewAccessibilityElement *> *accessibleMergedElements =
[label.accessibleElements copy];

// Given labelFrame (0,0,200), expect {(0, 3, 177, 27),(0,0,200,200)}.
CGRect mergedFrame = [accessibleMergedElements objectAtIndex:0].frameInContainer;
// Test first frame starts at start of labelFrame.
XCTAssertEqual(mergedFrame.origin.x, labelFrame.origin.x);
// Test last element is labelFrame.
XCTAssertTrue(CGRectEqualToRect([accessibleMergedElements objectAtIndex:1].frameInContainer,
labelFrame));

// Test pre-order non-merged multiline link frames union to merged multiline link frame.
XCTAssertTrue(
CGRectEqualToRect(CGRectUnion(firstFrame, secondFrame), mergedFrame));
}

- (void)testPostOrderedLinks {
CGRect labelFrame = CGRectMake(0,0,200,200);
NIAttributedLabel *label = [NIAttributedLabelTests makeGenericMultilineLabelWithFrame:labelFrame];

// Create post-order non-mergegd multiline links.
label.linkOrdering = NILinkOrderingLast;
label.shouldMergeMultilineLinks = NO;
NSMutableArray<NIViewAccessibilityElement *> *accessibleElements =
[label.accessibleElements copy];

// Given labelFrame (0,0,200), expect {(0,0,200,200),(96, 3, 81, 12),(0, 18, 85, 12)}.
CGRect firstFrame = [accessibleElements objectAtIndex:1].frameInContainer;
CGRect secondFrame = [accessibleElements objectAtIndex:2].frameInContainer;
// Test first frame is labelFrame.
XCTAssertTrue(CGRectEqualToRect([accessibleElements objectAtIndex:0].frameInContainer, labelFrame));
// Test second frame starts at start of labelFrame.
XCTAssertEqual(secondFrame.origin.x, labelFrame.origin.x);
// Test first frame starts after second frame.
XCTAssertGreaterThan(firstFrame.origin.x,secondFrame.origin.x);
// Test first frame start above second frame.
XCTAssertLessThan(firstFrame.origin.y,secondFrame.origin.y);
// Test both frames have the same height (same attribute).
XCTAssertEqual(firstFrame.size.height,secondFrame.size.height);

// Create post-order merged multiline links.
[label invalidateAccessibleElements];
label.shouldMergeMultilineLinks = YES;
NSMutableArray<NIViewAccessibilityElement *> *accessibleMergedElements =
[label.accessibleElements copy];

// Test first frame is labelFrame.
XCTAssertTrue(CGRectEqualToRect([accessibleMergedElements objectAtIndex:0].frameInContainer,
labelFrame));
// Given labelFrame (0,0,200), expect {(0,0,200,200),(0, 3, 177, 27)}.
CGRect mergedFrame = [accessibleMergedElements objectAtIndex:1].frameInContainer;
// Test first frame starts at start of labelFrame.
XCTAssertEqual(mergedFrame.origin.x, labelFrame.origin.x);

// Test post-order non-merged multiline links union to merged multiline link.
XCTAssertTrue(
CGRectEqualToRect(CGRectUnion(firstFrame, secondFrame), mergedFrame));
}

- (void)testInOrderLinks {
CGRect labelFrame = CGRectMake(0,0,200,200);
NIAttributedLabel *label = [NIAttributedLabelTests makeGenericMultilineLabelWithFrame:labelFrame];

// Links in-order non-merged multiline links.
label.linkOrdering = NILinkOrderingOriginal;
label.shouldMergeMultilineLinks = NO;
NSMutableArray<NIViewAccessibilityElement *> *accessibleElements =
[label.accessibleElements copy];

// Given labelFrame (0,0,200), expect {(0, 3, 96, 12),(0, 3, 177, 27),(85, 18, 93, 12)}.
CGRect firstFrame = [accessibleElements objectAtIndex:0].frameInContainer;
CGRect mergedFrame = [accessibleElements objectAtIndex:1].frameInContainer;
CGRect secondFrame = [accessibleElements objectAtIndex:2].frameInContainer;
// Test first frame starts at start of labelFrame.
XCTAssertEqual(firstFrame.origin.x, labelFrame.origin.x);
// Test first frame has same origin as merged frame.
XCTAssertTrue(CGPointEqualToPoint(firstFrame.origin, mergedFrame.origin));
// Test first frame has smaller width than merged frame.
XCTAssertLessThan(firstFrame.size.width, mergedFrame.size.width);
// Test first frame has smaller height than merged frame.
XCTAssertLessThan(firstFrame.size.height, mergedFrame.size.height);

// Test first frame start above second frame.
XCTAssertLessThan(firstFrame.origin.y, secondFrame.origin.y);
// Test second frame has smaller width than merged frame.
XCTAssertLessThan(secondFrame.size.width, mergedFrame.size.width);
// Test second frame has smaller height than merged frame.
XCTAssertLessThan(secondFrame.size.height, mergedFrame.size.height);
// Test first and second frames have the same height (same attribute).
XCTAssertEqual(firstFrame.size.height,secondFrame.size.height);

// Links in-order merged multiline links.
[label invalidateAccessibleElements];
label.shouldMergeMultilineLinks = YES;
NSMutableArray<NIViewAccessibilityElement *> *accessibleMergedElements =
[label.accessibleElements copy];

for (int frameIndex = 0; frameIndex < accessibleMergedElements.count; frameIndex++) {
XCTAssertTrue(
CGRectEqualToRect([accessibleElements objectAtIndex:frameIndex].frameInContainer,
[accessibleMergedElements objectAtIndex:frameIndex].frameInContainer));
}
}

@end

0 comments on commit fd5143a

Please sign in to comment.