-
Notifications
You must be signed in to change notification settings - Fork 273
feat: accessibility findAll #787
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: accessibility findAll #787
Conversation
cc @satya164. Would you like to check this out? Should make testing React Navigation more life-like :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love the idea of this PR. Great work!
Couple of things to improve:
findAll
trap seems to work in simple cases but we should also check if it works in more complex cases by writing tests with:- more nested components (in both host & composite components)
<Text>{1}</Text>
,{null}
, etc (check detailed comments).
- add tests that cover finding of non-hidden elements in siblings of
accessibilityElementsHidden
elements - other minor stylistic issues (improved, typing, etc)
Also new params seems not added typescript defintions.
src/__tests__/byDisplayValue.test.js
Outdated
queryByDisplayValue, | ||
} = render(<Comp />, { respectAccessibility: true }); | ||
|
||
await expect(findAllByDisplayValue('test_01')).rejects.toBeTruthy(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
consider using rejects.toEqual(...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: pls sort queries by they typical sort order: gets, query, find. Having them randomly shuffled raises question why they are in this particular order.
src/__tests__/byDisplayValue.test.js
Outdated
<View accessibilityElementsHidden> | ||
<TextInput value="test_01" /> | ||
</View> | ||
<TextInput value="test_02" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pls add some more expects
here that query test_02 so that we know that accessibilityElementsHidden
did not affect sibling elements.
src/__tests__/byText.test.js
Outdated
await expect(queryByText('hello world')).toBeNull(); | ||
}); | ||
|
||
test('queryAllByText returns an empty array if respectAccessibilityProps is true', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like it covers the same cases the above test, pls merge these together.
|
||
function appendFindAllTrap(renderer: ReactTestRenderer) { | ||
return new Proxy(renderer.root, { | ||
get(target, prop) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit, readability: could we get some more type explicit type annotations here?
src/render.js
Outdated
return new Proxy(renderer.root, { | ||
get(target, prop) { | ||
const isFindAllProp = prop === 'findAll'; | ||
const newFindAll = (fn, node = target) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can newFindAll
be moved to external variable and be type-annotated?
src/render.js
Outdated
}, initial); | ||
}; | ||
|
||
return isFindAllProp ? newFindAll : Reflect.get(...arguments); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would that work?
return isFindAllProp ? newFindAll : Reflect.get(...arguments); | |
return isFindAllProp ? newFindAll : target[prop]; |
src/render.js
Outdated
return result; | ||
} | ||
|
||
return result.concat(newFindAll(fn, child)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
return result.concat(newFindAll(fn, child)); | |
return [...result, ...newFindAll(fn, child)]; |
src/render.js
Outdated
const initial = fn(node) ? [node] : []; | ||
|
||
return node.children.reduce((result, child) => { | ||
if (typeof child === 'string') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like we need also to handle number
type here as well (maybe also boolean
). Please create tests with <Text>{1}</Text>
, {false}
, {null}
, {undefined}
and see if it crashes.
@thymikee I'm really impressed by this PR. After ironing our all of the potential edge cases we should consider this to be default option for next major release, as most of regular multi-screen tests would benefit from it. |
As the intended use case if with React Navigation, perhaps we could find a way to actually test it with that library. |
Thanks for the detailed review @mdjastrzebski ! I will look through and address your comments after work today |
@gabrielgrover after some more consideration of
IMO I would lean to option 2, as it would allows as to remove proxy code and will allow us to rely on React Test Renderer CC: @thymikee |
@mdjastrzebski So I actually did option 1. It is actually where i got the following line if (typeof child === 'string') { ... } ReactTestRenderer basically does a depth first search. The only difference is my use of .reduce(). As for option 2 the only issue I see with calling their Also note if we go with option 2 we would have to keep a list of root nodes in state somewhere that have the |
Excited for this PR! I was thinking, since
|
Re Pseudo-code: function isVisibleToAccessibility(element: TestInstance) {
const isElementVisible = !instance.accessibilityElementsHidden;
const isParentVisible = !instance.parent || isVisibleToAccessibility(instance.parent);
return isParentVisible && isElementVisible;
}
// Then
const elements = findAll(...);
const visibleElements = elements.filter(isVisibleToAccessibility); |
Re If there is only one host component with If there is more than one host components with |
(Closed by mistake, reopened this PR) |
Yes, I think we can just treat
I don't know how it'll work with multiple siblings with |
I see. We started with different assumptions. I was going about this problem with the assumption that we wouldn't want to traverse the tree more than once for each query. |
I do not see any real problem (performance or other) with traversing the component tree bottom-up or top-down multiple times (as long as it's not an infinite loop ;-)). Performance impact will be limited to running the integration tests. Digging a bit into details, traversing component tree bottom-up from given node should be cheaper than top-down, because going up you have only one parent, while going down you can have multiple children. Simplifying things a little bit, traversing up from given node should have There for cost of these extra traversals bottom-up seems acceptable tradeoff compared to benefit of simpler and more robust code, which avoids patching renderer object and duplicating @gabrielgrover: wdyt? |
Sounds good to me. Gonna work on it over the weekend. |
@satya164 @mdjastrzebski Sorry just now have been able to dedicate some more time to this PR. Taking a look at the edge case for const currentNodeIsHidden = instance.parent.children.some(c => c.props.accessibilityViewIsModal); However, this would cause the node with the property |
I don't know any implementation details, but isn't something like this possible? const currentNodeIsHidden = instance.parent.children.some(
c => c !== instance && c.props.accessibilityViewIsModal
); |
|
Actually it looks like using Object.is() will work. const currentNodeIsHidden = instance.parent.children.some(
c => !Object.is(c, instance) && c.props.accessibilityViewIsModal
); |
@mdjastrzebski @satya164 pushed changes. Let me know what you think |
); | ||
const Comp = () => ( | ||
<View> | ||
<OtherComp importantForAccessibility="no-hide-descendants" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that accessibility props (& style={{ display: 'none' }}
) should be checked only on host components (typeof element.type === 'string'
) in order to reflect how are they rendered in RN. So here you either should use these props on e.g. View
(recommended) or forward them inside OtherComp
.
@@ -107,3 +110,64 @@ function debug( | |||
debugImpl.shallow = (message) => debugShallow(instance, message); | |||
return debugImpl; | |||
} | |||
|
|||
function appendFindAllTrap(renderer: ReactTestRenderer) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest removing Proxy
code, and using filter(isReactTestElementVisibleToAccessibility)
in the queries. This way respectAccessibility
would be moved from render
option to query option, which would better reflect the fact that we are apply query modifier and not render modifier.
We should have some base QueryOptions
type that would atm be { respectAccessibility: boolean }
and would be passed as a second argument to all get/getAll/query
/etc/ queries. Note, for findBy
& findAllBy
we would add the existing waitForOptions
there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for building up this PR.
I've noticed two points that would like to discuss:
- Atm
respectAccessibility
is arender
option. However, it does not actual modify the render output (other thanfindAll
method). IMO it would be better to make it queries option (get/getAll/query
/etc). Each query would accept a 2nd optional argument of options andrespectAccessibility
would be the first available one. - In code checking accessibility props, you currently allow accessibility props to be added to composite components, this is especially visible in test code. However in order to reflect how RN renderer works, we should only take into account props set on host components (
View
,Text
, etc). This is similar to styling composite components, you can have astyle
prop there, but the UI will not be styled unless you somehow apply thatstyle
to host component.
@gabrielgrover Pls let me know if you need any coding help in this PR. I'm eager to help :-)
I'm closing this PR as stale, but since the idea is work pursuing I've create #970 issue which references this PR. |
Summary
This PR adds a new option to the render function called respectAccessibility. When this property is true a new implementation of ReactTestInstance.findAll() is
used. This new findAll() is defined in render.js inside the function
appendFindAllTrap
. This new findAll ignores sub trees that have a root node with a propaccessibilityElementsHidden: true
. This solution is an attempt to mimic the first approach found in @MattAgn 's list.Test plan
Tests added in
src/__tests__/byTestId.test.js
,src/__tests__/byPlaceholderText.test.js
, andsrc/__tests__/byDisplayValue.test.js
yarn test
Scope
findAll
trap