Skip to content

get by label concat values #681

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

Merged

Conversation

delca85
Copy link
Member

@delca85 delca85 commented Jul 3, 2020

What:

  • input elements with not empty value are retrieved by getAllLabelsByText
  • getByLabelText gets labels even when it receives a label composed of a concatenation

Why:

Issue #545 finds out that input elements are not recognized as labels and labelling by concatenation is not got by getByLabelText.

How:

  1. queryAllLabelsByText now makes a query not only on elements of type
    label but on input elements too. The empty input are excluded.
  2. after evaluating labels node through matcher, the labels that doesn't match are combined through an helper function. If a combination matches with the received text than the labels is returned by queryAllLabelsByText.

Checklist:

  • make queryAllLabelsByText look for input elements too
  • create a queryAllLabels function that retrieves all labels without any regards about text to be matched
  • find labels matching
  • create combination function that concatenate all the labels and returns the node that through some combination matches with the received text
  • queryAllLabelsByText returns now the labels matching by itself and the ones matching through concantenation
  • Documentation added to the
    docs site
  • Tests
  • Typescript definitions updated
  • Ready to be merged

I have already opened #607 to fix this behavior but maybe not in the right way. I hope this idea is better than the one I have submitted before.

@codesandbox-ci
Copy link

codesandbox-ci bot commented Jul 3, 2020

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 49c6148:

Sandbox Source
kentcdodds/react-testing-library-examples Configuration

@codecov
Copy link

codecov bot commented Jul 3, 2020

Codecov Report

Merging #681 into master will not change coverage.
The diff coverage is 100.00%.

Impacted file tree graph

@@            Coverage Diff            @@
##            master      #681   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           24        24           
  Lines          603       630   +27     
  Branches       151       163   +12     
=========================================
+ Hits           603       630   +27     
Impacted Files Coverage Δ
src/queries/label-text.js 100.00% <100.00%> (ø)
src/queries/role.js 100.00% <0.00%> (ø)
src/role-helpers.js 100.00% <0.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 9ae9244...49c6148. Read the comment docs.

Copy link
Member

@kentcdodds kentcdodds left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is also tricky. I have some feedback and questions/ideas.

const combs = [[]]
const matching = []
for (const label of labels) {
const copy = [...combs] // See note below.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what note this is referring to. Also, if I'm following this logic correctly, the for loop on the next line will only be run only once (for this iteration of the nested loop) and the value of the prefix will be [] every time. Any reason we need a loop here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure I am totally getting your point, are you telling that this loop runs only once inside the outer loop?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. Because copy is [[]] and so it's an array with only one element in it. It'll never be....

Wait. I guess that's only the case on the first iteration of the outer loop. Future iterations could have more... Hmmm... I need to take more time to understand what's going on here...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think it's only the first iteration of the outer loop that makes the inner one run only once.

return matching
}
function queryAllLabels(container) {
return Array.from(container.querySelectorAll('label,input'))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It occurs to me that an element can be labeled by anything, not just label and input. So while this may be solving for the input case, it doesn't solve for everything else 🤔

That said, I'm not sure how to handle "everything else." It seems to me that rather than searching for all possible elements that can label anything, we should be searching for everything that's labeled first, determine the contents of their label, and then return the elements whose label matches the query.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For what it's worth, I think that searching for labelled items and then for their labels is more feasible than querying for all elements that could be labels. Like you said above!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you'd like to tack a whack at that, I think it would solve a lot of problems.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For sure I would like to do it!

Comment on lines 68 to 74
const labelsNotMatchingTextAndNotEmpty = textToMatchByLabels.filter(
({node, textToMatch}) =>
Boolean(textToMatch) &&
nodesByLabelMatchingText.findIndex(nodeByLabelByText =>
nodeByLabelByText.isEqualNode(node),
) === -1,
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const labelsNotMatchingTextAndNotEmpty = textToMatchByLabels.filter(
({node, textToMatch}) =>
Boolean(textToMatch) &&
nodesByLabelMatchingText.findIndex(nodeByLabelByText =>
nodeByLabelByText.isEqualNode(node),
) === -1,
)
const labelsNotMatchingTextAndNotEmpty = textToMatchByLabels.filter(
({node, textToMatch}) =>
textToMatch && nodesByLabelMatchingText.some(n => n.isEqualNode(node)),
)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the right change should be:

const labelsNotMatchingTextAndNotEmpty = textToMatchByLabels.filter(
    ({node, textToMatch}) =>
      textToMatch && !nodesByLabelMatchingText.some(n => n.isEqualNode(node)),
  )

because I would like to retrieve all the labels in textToMatchByLabels but not empty and not included in nodesByLabelMatchingText

@delca85
Copy link
Member Author

delca85 commented Jul 4, 2020

Hey @kentcdodds !
I have pushed some changes that follow the suggestion you gave me yesterday.

Now queryAllByLabelText at the beginning looks for labelled element and then checks their labels value.
I still makes a research of all the label elements to get all the elements labelled but without aria-labelledby and aria-label attribute.

Right now I am still using the "old" queryAllLabelsByText in order to be able to send a specific error message in case of the searched label is found but none form control is associated to it. I think this is not totally right because that queryAllLabelsByText is looking just for label and input element not everything while everything could be a label (as you told).

I suppose my job is not finished yet but this is what I achieved.

Copy link
Member

@kentcdodds kentcdodds left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @delca85, thanks for this. This isn't quite doing what I thought (getting all labelable elements first, then finding their labels). I'm going to need more time to review/provide feedback on this. Just commenting here to let you know I haven't forgotten.

Comment on lines +369 to +373
<label>
All alone but with children
<textarea>Hello</textarea>
<select><option value="0">zero</option></select>
</label>`)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should find textarea. And it does so when using label.control

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I am not understanding, you are saying that the right test to be done here (at line 374) was that the labeled element is textarea instead of that no input element labeled was found?

getByLabelText(/alone/) would have found the textarea element, I added the selector to make it return null.

Please tell me if I am missing something.

@kentcdodds
Copy link
Member

I apologize it's taken me so long to give feedback here. I've been doing more family stuff than open source stuff recently.

I was thinking about a much bigger reworking of the logic for this function.

Right now, here's what we try to do:

  1. Find all matching labels and then find all the elements those labels are used to label
  2. Find all other matching elements and see if they're also used to label anything

The #545 should be found via the #2 option there, but it's difficult because the search query matches across multiple labels, so we won't find it without some additional complex logic.

What I was thinking is we could do this instead:

  1. Find all labeled elements (everything with a .label property or an aria-labeledby attribute)
  2. Filter those elements to the ones whose labels match the search text

I think this would allow us to more easily solve #545 and would result in fewer bugs. I expect that this would be a non-breaking change.

Does that make sense?

@delca85
Copy link
Member Author

delca85 commented Jul 15, 2020

I apologize it's taken me so long to give feedback here. I've been doing more family stuff than open source stuff recently.

Don't be sorry at all, family stuff is always first class stuff!

Right now, here's what we try to do:

  1. Find all matching labels and then find all the elements those labels are used to label
  2. Find all other matching elements and see if they're also used to label anything

The #545 should be found via the #2 option there, but it's difficult because the search query matches across multiple labels, so we won't find it without some additional complex logic.

Totally agree with you, specially regarding the additional complex logic required in order to solve #545 .

What I was thinking is we could do this instead:

  1. Find all labeled elements (everything with a .label property or an aria-labeledby attribute)
  2. Filter those elements to the ones whose labels match the search text

I think this would allow us to more easily solve #545 and would result in fewer bugs. I expect that this would be a non-breaking change.

Does that make sense?

It makes a lot of sense and it is what I have tried to do in the code I pushed here :

  1. at this line I make a query to get all the elements with aria-labelledby and then I look for their value to check if it matches with the desired one;
  2. at this line I retrieve the element with aria-label attribute and check if their value match with the desired one
  3. finally I get all the label element because I thought that some labels still be missed by the first two sets retrieved by the steps 1 and 2.

In comparison to what you wrote, I check the value of the elements I found with a label immediately after I found them, because I thought it is more performant (I save a loop doing this instead of checking the value of all the labeled elements after I found all of them).

Like I told you here I am still using queryAllLabelsByText and this is the reason (and an issue related):

Right now I am still using the "old" queryAllLabelsByText in order to be able to send a specific error message in case of the searched label is found but none form control is associated to it. I think this is not totally right because that queryAllLabelsByText is looking just for label and input element not everything while everything could be a label (as you told).

I hope I explained what I did in a clear way, if I am still miss something please tell me.
Anyway, thanks for your time.

Copy link
Member

@kentcdodds kentcdodds left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that makes sense. Sorry for my confusion! I had an idea. What do you think?

container.querySelector(`[id="${label.getAttribute('for')}"]`),

const matchingLabelledElements = Array.from(container.querySelectorAll('*'))
.filter(element => element.hasAttribute('aria-labelledby'))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we might be able to clean up some stuff (and reduce the number of loops) by changing this filter to include elements with the .label property:

Suggested change
.filter(element => element.hasAttribute('aria-labelledby'))
.filter(element => element.label || element.hasAttribute('aria-labelledby'))

This would require the reducer function handle the .label elements a little differently, but I think it would allow us to remove some other code as well (specifically all the code for matchingElementsByLabels).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am going to try what you suggested as soon as possible.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @kentcdodds !
For sure I am missing something but so far I have not been able to make the change you suggested me.
Trying to include the elements with the .label property in the filter you highlighted it is not working because the element labeled by a label element put around them have the .label property undefined.
I have tried to show it in this sandbox.

Again, I am aware there is something I am not doing in the right way but I am not able to get it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, sorry, that's a typo. It should be .labels. This is a NodeList of all the labels. If there are multiple, then it looks like the Chrome Accessibility DevTools combines them:

image

https://jsbin.com/pudalunaxu/1/edit?html,output

<div>
  <label for="my-input">One</label>
  <label for="my-input">Two</label>
  <input id="my-input" />
</div>

So I think that's probably what we should do as well. Combine the text for all labels in the NodeList, then compare that against the given TextMatch.

Does that make sense?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it makes a lot of sense.
I have just pushed some code using .labels.

I am not so sure I have done all the work because I am not obtaining all the labels in the NodeList through a permutation but just join their values (and join values in the NodeList removing an item a time). I don't know if you meant this with combine.

Copy link
Member

@kentcdodds kentcdodds left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking pretty great! Just one question.

? labelledElement.getAttribute('aria-labelledby').split(' ')
: []
const labelsValue = labelsId.length
? labelsId.map(labelId => {
Copy link
Member

@kentcdodds kentcdodds Jul 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if an element had both a .labels property and the aria-labelledby? I don't know if that's valid or not. Is it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aria-labelledby has precedence: https://www.w3.org/TR/accname-1.1/#step2B

2B is for aria-labelledby, 2D for

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, I think this PR is good to go! Anyone have other thoughts?

Copy link
Member

@kentcdodds kentcdodds left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is super work @delca85! Thank you for your patience with me and all my feedback. You did a superb job 👏

@kentcdodds kentcdodds merged commit 53e0d18 into testing-library:master Jul 20, 2020
@kentcdodds
Copy link
Member

@all-contributors please add @delca85 for tests and code

@allcontributors
Copy link
Contributor

@kentcdodds

I've put up a pull request to add @delca85! 🎉

@kentcdodds
Copy link
Member

Thanks so much for your help! I've added you as a collaborator on the project. Please make sure that you review the other/MAINTAINING.md and CONTRIBUTING.md files (specifically the bit about the commit messages and the git hooks) and familiarize yourself with the code of conduct (we're using the contributor covenant). You might also want to watch the repo to be notified when someone files an issue/PR. Please continue to make PRs as you feel the need (you can make your branches directly on the repo rather than your fork if you want). Thanks! And welcome to the team :)

@kentcdodds
Copy link
Member

🎉 This PR is included in version 7.21.2 🎉

The release is available on:

Your semantic-release bot 📦🚀

@delca85
Copy link
Member Author

delca85 commented Jul 20, 2020

Thank you so much @kentcdodds !
I have learnt a lot working on this PR and it's totally a pleasure being part of this team.
Thanks!

@smeijer
Copy link
Member

smeijer commented Jul 21, 2020

Thank you @delca85. This is PR is really awesome!

@delca85
Copy link
Member Author

delca85 commented Jul 21, 2020

Thank you @delca85. This is PR is really awesome!

Thanks @smeijer , I really hope you are right!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants