/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import @interface RCTParagraphComponentAccessibilityProviderTests : XCTestCase @end using namespace facebook::react; //┌── RootShadowNode ─────────────────────────────┐ //│ │ //│┌─── ParagraphShadowNodeA ─────────────────────────┐ │ //││ ┌─AA(AAA) ─────────┐ ┌─AB(ABA) ─┐ ┌─AC(ACA)────┐ │ │ //││ │ Please check out │ │ Facebook │ │ and │ │ │ //││ └──────────────────┘ └──────────┘ └────────────┘ │ │ //││ ┌─AD(ADA) ──┐ ┌──AE(AEA) ──────────────────────┐ │ │ //││ │ Instagram │ │ for a full description. │ │ │ //││ └───────────┘ └────────────────────────────────┘ │ │ //│└──────────────────────────────────────────────────┘ │ //│ │ //│ │ //│┌── ParagraphShadowNodeB ──────────────────────────┐ │ //││ ┌───BA(BAA) ───────────────────────────────────┐ │ │ //││ │ Lorem ipsum dolor sit amet, consectetur │ │ │ //││ │ adipiscing elit. Maecenas ut risus et sapien │ │ │ //││ │ bibendum volutpat. Nulla facilisi. Cras │ │ │ //││ │ imperdiet gravida tincidunt. │ │ │ //││ └──────────────────────────────────────────────┘ │ │ //││ ┌─BB(BBA) ─────────────────────────────────────┐ │ │ //││ │ In tempor, tellus et vestibulum venenatis, │ │ │ //││ │ lorem nunc eleifend lectus, a consectetur │ │ │ //││ │ magna augue at arcu. │ │ │ //││ └──────────────────────────────────────────────┘ │ │ //│└──────────────────────────────────────────────────┘ │ //│ │ //│┌── ParagraphShadowNodeC ──────────────────────────┐ │ //││ ┌─CA(CAA) ────────┐ │ │ //││ │ Lorem ipsum │ │ │ //││ └─────────────────┘ │ │ //││ ┌─CB(CBA) ─────────────────────────────────────┐ │ │ //││ │ dolor sit amet, consectetur adipiscing elit. │ │ │ //││ │Maecenas ut risus et sapien bibendum volutpat.│ │ │ //││ │ Nulla facilisi. Cras imperdiet gravida │ │ │ //││ │ tincidunt. In tempor, tellus et vestibulum │ │ │ //││ │ venenatis, lorem nunc eleifend lectus, a │ │ │ //││ │ consectetur magna augue at arcu. │ │ │ //││ └──────────────────────────────────────────────┘ │ │ //││ ┌─CC(CCA) ────────┐ │ │ //││ │ See Less │ │ │ //││ └─────────────────┘ │ │ //│└──────────────────────────────────────────────────┘ │ //│ │ //└─────────────────────────────────────────────────────┘ @implementation RCTParagraphComponentAccessibilityProviderTests { std::shared_ptr builder_; std::shared_ptr rootShadowNode_; std::shared_ptr ParagrahShadowNodeA_; std::shared_ptr ParagrahShadowNodeB_; std::shared_ptr ParagrahShadowNodeC_; std::shared_ptr TextShadowNodeAA_; std::shared_ptr TextShadowNodeAB_; std::shared_ptr TextShadowNodeAC_; std::shared_ptr TextShadowNodeAD_; std::shared_ptr TextShadowNodeAE_; std::shared_ptr TextShadowNodeBA_; std::shared_ptr TextShadowNodeBB_; std::shared_ptr TextShadowNodeCA_; std::shared_ptr TextShadowNodeCB_; std::shared_ptr TextShadowNodeCC_; std::shared_ptr RawTextShadowNodeAAA_; std::shared_ptr RawTextShadowNodeABA_; std::shared_ptr RawTextShadowNodeACA_; std::shared_ptr RawTextShadowNodeADA_; std::shared_ptr RawTextShadowNodeAEA_; std::shared_ptr RawTextShadowNodeBAA_; std::shared_ptr RawTextShadowNodeBBA_; std::shared_ptr RawTextShadowNodeCAA_; std::shared_ptr RawTextShadowNodeCBA_; std::shared_ptr RawTextShadowNodeCCA_; } - (void)setUp { [super setUp]; builder_ = std::make_shared((simpleComponentBuilder())); auto element = Element() .reference(rootShadowNode_) .tag(1) .props([] { auto sharedProps = std::make_shared(); auto &props = *sharedProps; props.layoutConstraints = LayoutConstraints{{0, 0}, {500, 500}}; auto &yogaStyle = props.yogaStyle; yogaStyle.dimensions()[YGDimensionWidth] = YGValue{200, YGUnitPoint}; yogaStyle.dimensions()[YGDimensionHeight] = YGValue{200, YGUnitPoint}; return sharedProps; }) .children({ Element() .reference(ParagrahShadowNodeA_) .props([] { auto sharedProps = std::make_shared(); auto &props = *sharedProps; props.accessible = true; auto &yogaStyle = props.yogaStyle; yogaStyle.positionType() = YGPositionTypeAbsolute; yogaStyle.position()[YGEdgeLeft] = YGValue{0, YGUnitPoint}; yogaStyle.position()[YGEdgeTop] = YGValue{0, YGUnitPoint}; yogaStyle.dimensions()[YGDimensionWidth] = YGValue{200, YGUnitPoint}; yogaStyle.dimensions()[YGDimensionHeight] = YGValue{20, YGUnitPoint}; return sharedProps; }) .children({ Element() .reference(TextShadowNodeAA_) .props([] { auto sharedProps = std::make_shared(); return sharedProps; }) .children({Element().reference(RawTextShadowNodeAAA_).props([] { auto sharedProps = std::make_shared(); auto &props = *sharedProps; props.text = "Please check out "; return sharedProps; })}), Element() .reference(TextShadowNodeAB_) .props([] { auto sharedProps = std::make_shared(); auto &props = *sharedProps; props.textAttributes.accessibilityRole = AccessibilityRole::Link; return sharedProps; }) .children({Element().reference(RawTextShadowNodeABA_).props([] { auto sharedProps = std::make_shared(); auto &props = *sharedProps; props.text = "facebook"; return sharedProps; })}), Element() .reference(TextShadowNodeAC_) .props([] { auto sharedProps = std::make_shared(); return sharedProps; }) .children({Element().reference(RawTextShadowNodeACA_).props([] { auto sharedProps = std::make_shared(); auto &props = *sharedProps; props.text = " and "; return sharedProps; })}), Element() .reference(TextShadowNodeAD_) .props([] { auto sharedProps = std::make_shared(); auto &props = *sharedProps; props.textAttributes.accessibilityRole = AccessibilityRole::Link; return sharedProps; }) .children({Element().reference(RawTextShadowNodeADA_).props([] { auto sharedProps = std::make_shared(); auto &props = *sharedProps; props.text = "instagram"; return sharedProps; })}), Element() .reference(TextShadowNodeAE_) .props([] { auto sharedProps = std::make_shared(); return sharedProps; }) .children({Element().reference(RawTextShadowNodeAEA_).props([] { auto sharedProps = std::make_shared(); auto &props = *sharedProps; props.text = " for a full description."; return sharedProps; })}), }), Element() .reference(ParagrahShadowNodeB_) .props([] { auto sharedProps = std::make_shared(); auto &props = *sharedProps; props.accessible = true; auto &yogaStyle = props.yogaStyle; yogaStyle.positionType() = YGPositionTypeAbsolute; yogaStyle.position()[YGEdgeLeft] = YGValue{0, YGUnitPoint}; yogaStyle.position()[YGEdgeTop] = YGValue{30, YGUnitPoint}; yogaStyle.dimensions()[YGDimensionWidth] = YGValue{200, YGUnitPoint}; yogaStyle.dimensions()[YGDimensionHeight] = YGValue{50, YGUnitPoint}; return sharedProps; }) .children({ Element() .reference(TextShadowNodeBA_) .props([] { auto sharedProps = std::make_shared(); auto &props = *sharedProps; props.textAttributes.accessibilityRole = AccessibilityRole::Link; return sharedProps; }) .children({Element().reference(RawTextShadowNodeBAA_).props([] { auto sharedProps = std::make_shared(); auto &props = *sharedProps; props.text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas ut risus et sapien bibendum volutpat. Nulla facilisi. Cras imperdiet gravida tincidunt. "; return sharedProps; })}), Element() .reference(TextShadowNodeBB_) .props([] { auto sharedProps = std::make_shared(); return sharedProps; }) .children({Element().reference(RawTextShadowNodeBBA_).props([] { auto sharedProps = std::make_shared(); auto &props = *sharedProps; props.text = "In tempor, tellus et vestibulum venenatis, lorem nunc eleifend lectus, a consectetur magna augue at arcu."; return sharedProps; })}), }), Element() .reference(ParagrahShadowNodeC_) .props([] { auto sharedProps = std::make_shared(); auto &props = *sharedProps; props.accessible = true; auto &yogaStyle = props.yogaStyle; yogaStyle.positionType() = YGPositionTypeAbsolute; yogaStyle.position()[YGEdgeLeft] = YGValue{0, YGUnitPoint}; yogaStyle.position()[YGEdgeTop] = YGValue{90, YGUnitPoint}; yogaStyle.dimensions()[YGDimensionWidth] = YGValue{200, YGUnitPoint}; yogaStyle.dimensions()[YGDimensionHeight] = YGValue{50, YGUnitPoint}; return sharedProps; }) .children({ Element() .reference(TextShadowNodeCA_) .props([] { auto sharedProps = std::make_shared(); auto &props = *sharedProps; props.textAttributes.accessibilityRole = AccessibilityRole::Link; return sharedProps; }) .children({Element().reference(RawTextShadowNodeCAA_).props([] { auto sharedProps = std::make_shared(); auto &props = *sharedProps; props.text = "Lorem ipsum"; return sharedProps; })}), Element() .reference(TextShadowNodeCB_) .props([] { auto sharedProps = std::make_shared(); return sharedProps; }) .children({Element().reference(RawTextShadowNodeCBA_).props([] { auto sharedProps = std::make_shared(); auto &props = *sharedProps; props.text = " dolor sit amet, consectetur adipiscing elit. Maecenas ut risus et sapien bibendum volutpat. Nulla facilisi. Cras imperdiet gravida tincidunt. In tempor, tellus et vestibulum venenatis, lorem nunc eleifend lectus, a consectetur magna augue at arcu. "; return sharedProps; })}), Element() .reference(TextShadowNodeCC_) .props([] { auto sharedProps = std::make_shared(); auto &props = *sharedProps; props.textAttributes.accessibilityRole = AccessibilityRole::Button; return sharedProps; }) .children({Element().reference(RawTextShadowNodeCCA_).props([] { auto sharedProps = std::make_shared(); auto &props = *sharedProps; props.text = "See Less"; return sharedProps; })}), }), }); builder_->build(element); rootShadowNode_->layoutIfNeeded(); } static ParagraphShadowNode::ConcreteState::Shared stateWithShadowNode( std::shared_ptr paragraphShadowNode) { auto sharedState = std::static_pointer_cast(paragraphShadowNode->getState()); return sharedState; } - (void)testAttributedString { ParagraphShadowNode::ConcreteState::Shared _stateA = stateWithShadowNode(ParagrahShadowNodeA_); RCTParagraphComponentView *paragraphComponentViewA = [RCTParagraphComponentView new]; [paragraphComponentViewA updateProps:ParagrahShadowNodeA_->getProps() oldProps:nullptr]; [paragraphComponentViewA updateState:_stateA oldState:nil]; ParagraphShadowNode::ConcreteState::Shared _stateB = stateWithShadowNode(ParagrahShadowNodeB_); RCTParagraphComponentView *paragraphComponentViewB = [RCTParagraphComponentView new]; [paragraphComponentViewB updateProps:ParagrahShadowNodeB_->getProps() oldProps:nullptr]; [paragraphComponentViewB updateState:_stateB oldState:nil]; ParagraphShadowNode::ConcreteState::Shared _stateC = stateWithShadowNode(ParagrahShadowNodeC_); RCTParagraphComponentView *paragraphComponentViewC = [RCTParagraphComponentView new]; [paragraphComponentViewC updateProps:ParagrahShadowNodeC_->getProps() oldProps:nullptr]; [paragraphComponentViewC updateState:_stateC oldState:nil]; // Check the correctness of attributedString XCTAssert([[paragraphComponentViewA.attributedText string] isEqual:@"Please check out facebook and instagram for a full description."]); XCTAssertEqual(_stateA->getData().attributedString.getFragments().size(), 5); XCTAssert([[paragraphComponentViewB.attributedText string] isEqual: @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas ut risus et sapien bibendum volutpat. Nulla facilisi. Cras imperdiet gravida tincidunt. In tempor, tellus et vestibulum venenatis, lorem nunc eleifend lectus, a consectetur magna augue at arcu."]); XCTAssertEqual(_stateB->getData().attributedString.getFragments().size(), 2); XCTAssert([[paragraphComponentViewC.attributedText string] isEqual: @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas ut risus et sapien bibendum volutpat. Nulla facilisi. Cras imperdiet gravida tincidunt. In tempor, tellus et vestibulum venenatis, lorem nunc eleifend lectus, a consectetur magna augue at arcu. See Less"]); XCTAssertEqual(_stateC->getData().attributedString.getFragments().size(), 3); } #pragma mark - Accessibility - (void)testAccessibilityMultipleLinks { // initialize the paragraphComponentView to get the accessibilityElements ParagraphShadowNode::ConcreteState::Shared _state = stateWithShadowNode(ParagrahShadowNodeA_); RCTParagraphComponentView *paragraphComponentView = [RCTParagraphComponentView new]; [paragraphComponentView updateProps:ParagrahShadowNodeA_->getProps() oldProps:nullptr]; [paragraphComponentView updateState:_state oldState:nil]; NSArray *elements = [paragraphComponentView accessibilityElements]; // check the number of accessibilityElements XCTAssert( elements.count == 3, @"Expected 4 accessibilityElements - one for the whole string, and the rest for the links"); // check accessibility trait XCTAssert( elements[1].accessibilityTraits & UIAccessibilityTraitLink, @"Expected the second accessibilityElement has link trait"); XCTAssert( elements[2].accessibilityTraits & UIAccessibilityTraitLink, @"Expected the second accessibilityElement has link trait"); } - (void)testAccessibilityLinkWrappingMultipleLines { ParagraphShadowNode::ConcreteState::Shared _state = stateWithShadowNode(ParagrahShadowNodeB_); RCTParagraphComponentView *paragraphComponentView = [RCTParagraphComponentView new]; [paragraphComponentView updateProps:ParagrahShadowNodeB_->getProps() oldProps:nullptr]; [paragraphComponentView updateState:_state oldState:nil]; NSArray *elements = [paragraphComponentView accessibilityElements]; XCTAssert(elements.count == 2, @"Expected 2 accessibilityElements - one for the whole string, and one for the link"); XCTAssert( elements[1].accessibilityTraits & UIAccessibilityTraitLink, @"Expected the second accessibilityElement has link trait"); } - (void)testAccessibilityTruncatedText { ParagraphShadowNode::ConcreteState::Shared _state = stateWithShadowNode(ParagrahShadowNodeC_); RCTParagraphComponentView *paragraphComponentView = [RCTParagraphComponentView new]; [paragraphComponentView updateProps:ParagrahShadowNodeC_->getProps() oldProps:nullptr]; [paragraphComponentView updateState:_state oldState:nil]; NSArray *elements = [paragraphComponentView accessibilityElements]; XCTAssert(elements.count == 2, @"Expected 2 accessibilityElements - one for the whole string, and one for the link"); XCTAssert( elements[1].accessibilityTraits & UIAccessibilityTraitLink, @"Expected the second accessibilityElement has link trait"); } - (void)testEntireParagraphLink { std::shared_ptr rootShadowNode; std::shared_ptr paragrahShadowNode; auto element = Element() .reference(rootShadowNode) .tag(1) .props([] { auto sharedProps = std::make_shared(); auto &props = *sharedProps; props.layoutConstraints = LayoutConstraints{{0, 0}, {500, 500}}; auto &yogaStyle = props.yogaStyle; yogaStyle.dimensions()[YGDimensionWidth] = YGValue{200, YGUnitPoint}; yogaStyle.dimensions()[YGDimensionHeight] = YGValue{200, YGUnitPoint}; return sharedProps; }) .children({ Element() .reference(paragrahShadowNode) .props([] { auto sharedProps = std::make_shared(); auto &props = *sharedProps; props.accessible = true; props.accessibilityTraits = AccessibilityTraits::Link; auto &yogaStyle = props.yogaStyle; yogaStyle.positionType() = YGPositionTypeAbsolute; yogaStyle.position()[YGEdgeLeft] = YGValue{0, YGUnitPoint}; yogaStyle.position()[YGEdgeTop] = YGValue{0, YGUnitPoint}; yogaStyle.dimensions()[YGDimensionWidth] = YGValue{200, YGUnitPoint}; yogaStyle.dimensions()[YGDimensionHeight] = YGValue{20, YGUnitPoint}; return sharedProps; }) .children({ Element() .props([] { auto sharedProps = std::make_shared(); auto &props = *sharedProps; props.textAttributes.accessibilityRole = AccessibilityRole::Link; return sharedProps; }) .children({Element().reference(RawTextShadowNodeABA_).props([] { auto sharedProps = std::make_shared(); auto &props = *sharedProps; props.text = "A long text that happens to be a link"; return sharedProps; })}), }), }); builder_->build(element); rootShadowNode->layoutIfNeeded(); ParagraphShadowNode::ConcreteState::Shared _state = stateWithShadowNode(paragrahShadowNode); RCTParagraphComponentView *paragraphComponentView = [RCTParagraphComponentView new]; [paragraphComponentView updateProps:paragrahShadowNode->getProps() oldProps:nullptr]; [paragraphComponentView updateState:_state oldState:nil]; NSArray *elements = paragraphComponentView.accessibilityElements; XCTAssertEqual(elements.count, 1); XCTAssertTrue(elements[0].accessibilityTraits & UIAccessibilityTraitLink); } @end