Skip to content
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

ComboboxControl: add unit tests #42403

Merged
merged 27 commits into from
Aug 31, 2022
Merged

ComboboxControl: add unit tests #42403

merged 27 commits into from
Aug 31, 2022

Conversation

chad1008
Copy link
Contributor

What?

Add unit tests to the ComboboxControl component

Why?

Following up on the conversation in #41417 where it was noted this component currently lacks any tests, making it harder to update it with confidence.

How?

Five tests are currently implemented:

  • test basic rendering of the component
  • test that the label is visible when expected (the default)
  • test that the label is hidden when expected (if hideLabelFromVision prop is true)
  • test that the correct list of options are rendered
  • test that the correct option is selected when clicked

I did plan to add an additional test to validate the correct option is selected via keypresses, but ran into trouble implementing it. I wasn't able to get userEffect.keyboard() to register downarrow key presses, nor could I get it to simulate hitting enter. I was able to simulate typing in part of the option's name, but without enter to confirm the selection, the test wasn't viable.

I'm not 100% sure why that is, but I did find that for some elements, certain keypresses (like arrowdown) aren't fully implemented. This may be related?

Testing Instructions

Run npm run test-unit packages/components/src/combobox-control

Screenshots or screencast

@chad1008 chad1008 requested review from mirka and ciampo July 13, 2022 19:56
@github-actions
Copy link

github-actions bot commented Jul 13, 2022

Size Change: 0 B

Total Size: 1.24 MB

ℹ️ View Unchanged
Filename Size
build/a11y/index.min.js 982 B
build/annotations/index.min.js 2.76 kB
build/api-fetch/index.min.js 2.26 kB
build/autop/index.min.js 2.14 kB
build/blob/index.min.js 475 B
build/block-directory/index.min.js 7.06 kB
build/block-directory/style-rtl.css 990 B
build/block-directory/style.css 991 B
build/block-editor/default-editor-styles-rtl.css 378 B
build/block-editor/default-editor-styles.css 378 B
build/block-editor/index.min.js 160 kB
build/block-editor/style-rtl.css 15.1 kB
build/block-editor/style.css 15.1 kB
build/block-library/blocks/archives/editor-rtl.css 61 B
build/block-library/blocks/archives/editor.css 60 B
build/block-library/blocks/archives/style-rtl.css 65 B
build/block-library/blocks/archives/style.css 65 B
build/block-library/blocks/audio/editor-rtl.css 150 B
build/block-library/blocks/audio/editor.css 150 B
build/block-library/blocks/audio/style-rtl.css 122 B
build/block-library/blocks/audio/style.css 122 B
build/block-library/blocks/audio/theme-rtl.css 110 B
build/block-library/blocks/audio/theme.css 110 B
build/block-library/blocks/avatar/editor-rtl.css 116 B
build/block-library/blocks/avatar/editor.css 116 B
build/block-library/blocks/avatar/style-rtl.css 59 B
build/block-library/blocks/avatar/style.css 59 B
build/block-library/blocks/block/editor-rtl.css 161 B
build/block-library/blocks/block/editor.css 161 B
build/block-library/blocks/button/editor-rtl.css 441 B
build/block-library/blocks/button/editor.css 441 B
build/block-library/blocks/button/style-rtl.css 505 B
build/block-library/blocks/button/style.css 505 B
build/block-library/blocks/buttons/editor-rtl.css 292 B
build/block-library/blocks/buttons/editor.css 292 B
build/block-library/blocks/buttons/style-rtl.css 275 B
build/block-library/blocks/buttons/style.css 275 B
build/block-library/blocks/calendar/style-rtl.css 207 B
build/block-library/blocks/calendar/style.css 207 B
build/block-library/blocks/categories/editor-rtl.css 84 B
build/block-library/blocks/categories/editor.css 83 B
build/block-library/blocks/categories/style-rtl.css 79 B
build/block-library/blocks/categories/style.css 79 B
build/block-library/blocks/code/editor-rtl.css 53 B
build/block-library/blocks/code/editor.css 53 B
build/block-library/blocks/code/style-rtl.css 103 B
build/block-library/blocks/code/style.css 103 B
build/block-library/blocks/code/theme-rtl.css 124 B
build/block-library/blocks/code/theme.css 124 B
build/block-library/blocks/columns/editor-rtl.css 108 B
build/block-library/blocks/columns/editor.css 108 B
build/block-library/blocks/columns/style-rtl.css 406 B
build/block-library/blocks/columns/style.css 406 B
build/block-library/blocks/comment-author-avatar/editor-rtl.css 125 B
build/block-library/blocks/comment-author-avatar/editor.css 125 B
build/block-library/blocks/comment-content/style-rtl.css 92 B
build/block-library/blocks/comment-content/style.css 92 B
build/block-library/blocks/comment-template/style-rtl.css 187 B
build/block-library/blocks/comment-template/style.css 185 B
build/block-library/blocks/comments-pagination-numbers/editor-rtl.css 123 B
build/block-library/blocks/comments-pagination-numbers/editor.css 121 B
build/block-library/blocks/comments-pagination/editor-rtl.css 222 B
build/block-library/blocks/comments-pagination/editor.css 209 B
build/block-library/blocks/comments-pagination/style-rtl.css 235 B
build/block-library/blocks/comments-pagination/style.css 231 B
build/block-library/blocks/comments-title/editor-rtl.css 75 B
build/block-library/blocks/comments-title/editor.css 75 B
build/block-library/blocks/comments/editor-rtl.css 834 B
build/block-library/blocks/comments/editor.css 832 B
build/block-library/blocks/comments/style-rtl.css 632 B
build/block-library/blocks/comments/style.css 630 B
build/block-library/blocks/cover/editor-rtl.css 605 B
build/block-library/blocks/cover/editor.css 607 B
build/block-library/blocks/cover/style-rtl.css 1.55 kB
build/block-library/blocks/cover/style.css 1.55 kB
build/block-library/blocks/embed/editor-rtl.css 293 B
build/block-library/blocks/embed/editor.css 293 B
build/block-library/blocks/embed/style-rtl.css 410 B
build/block-library/blocks/embed/style.css 410 B
build/block-library/blocks/embed/theme-rtl.css 110 B
build/block-library/blocks/embed/theme.css 110 B
build/block-library/blocks/file/editor-rtl.css 300 B
build/block-library/blocks/file/editor.css 300 B
build/block-library/blocks/file/style-rtl.css 253 B
build/block-library/blocks/file/style.css 254 B
build/block-library/blocks/file/view.min.js 346 B
build/block-library/blocks/freeform/editor-rtl.css 2.44 kB
build/block-library/blocks/freeform/editor.css 2.44 kB
build/block-library/blocks/gallery/editor-rtl.css 948 B
build/block-library/blocks/gallery/editor.css 950 B
build/block-library/blocks/gallery/style-rtl.css 1.53 kB
build/block-library/blocks/gallery/style.css 1.53 kB
build/block-library/blocks/gallery/theme-rtl.css 108 B
build/block-library/blocks/gallery/theme.css 108 B
build/block-library/blocks/group/editor-rtl.css 337 B
build/block-library/blocks/group/editor.css 337 B
build/block-library/blocks/group/style-rtl.css 57 B
build/block-library/blocks/group/style.css 57 B
build/block-library/blocks/group/theme-rtl.css 78 B
build/block-library/blocks/group/theme.css 78 B
build/block-library/blocks/heading/style-rtl.css 76 B
build/block-library/blocks/heading/style.css 76 B
build/block-library/blocks/html/editor-rtl.css 327 B
build/block-library/blocks/html/editor.css 329 B
build/block-library/blocks/image/editor-rtl.css 876 B
build/block-library/blocks/image/editor.css 873 B
build/block-library/blocks/image/style-rtl.css 627 B
build/block-library/blocks/image/style.css 630 B
build/block-library/blocks/image/theme-rtl.css 110 B
build/block-library/blocks/image/theme.css 110 B
build/block-library/blocks/latest-comments/style-rtl.css 284 B
build/block-library/blocks/latest-comments/style.css 284 B
build/block-library/blocks/latest-posts/editor-rtl.css 213 B
build/block-library/blocks/latest-posts/editor.css 212 B
build/block-library/blocks/latest-posts/style-rtl.css 463 B
build/block-library/blocks/latest-posts/style.css 462 B
build/block-library/blocks/list/style-rtl.css 88 B
build/block-library/blocks/list/style.css 88 B
build/block-library/blocks/media-text/editor-rtl.css 266 B
build/block-library/blocks/media-text/editor.css 263 B
build/block-library/blocks/media-text/style-rtl.css 507 B
build/block-library/blocks/media-text/style.css 505 B
build/block-library/blocks/more/editor-rtl.css 431 B
build/block-library/blocks/more/editor.css 431 B
build/block-library/blocks/navigation-link/editor-rtl.css 705 B
build/block-library/blocks/navigation-link/editor.css 703 B
build/block-library/blocks/navigation-link/style-rtl.css 115 B
build/block-library/blocks/navigation-link/style.css 115 B
build/block-library/blocks/navigation-submenu/editor-rtl.css 296 B
build/block-library/blocks/navigation-submenu/editor.css 295 B
build/block-library/blocks/navigation-submenu/view.min.js 423 B
build/block-library/blocks/navigation/editor-rtl.css 1.96 kB
build/block-library/blocks/navigation/editor.css 1.96 kB
build/block-library/blocks/navigation/style-rtl.css 2.04 kB
build/block-library/blocks/navigation/style.css 2.03 kB
build/block-library/blocks/navigation/view-modal.min.js 2.78 kB
build/block-library/blocks/navigation/view.min.js 443 B
build/block-library/blocks/nextpage/editor-rtl.css 395 B
build/block-library/blocks/nextpage/editor.css 395 B
build/block-library/blocks/page-list/editor-rtl.css 363 B
build/block-library/blocks/page-list/editor.css 363 B
build/block-library/blocks/page-list/style-rtl.css 175 B
build/block-library/blocks/page-list/style.css 175 B
build/block-library/blocks/paragraph/editor-rtl.css 174 B
build/block-library/blocks/paragraph/editor.css 174 B
build/block-library/blocks/paragraph/style-rtl.css 260 B
build/block-library/blocks/paragraph/style.css 260 B
build/block-library/blocks/post-author/style-rtl.css 175 B
build/block-library/blocks/post-author/style.css 176 B
build/block-library/blocks/post-comments-form/editor-rtl.css 96 B
build/block-library/blocks/post-comments-form/editor.css 96 B
build/block-library/blocks/post-comments-form/style-rtl.css 493 B
build/block-library/blocks/post-comments-form/style.css 493 B
build/block-library/blocks/post-date/style-rtl.css 61 B
build/block-library/blocks/post-date/style.css 61 B
build/block-library/blocks/post-excerpt/editor-rtl.css 73 B
build/block-library/blocks/post-excerpt/editor.css 73 B
build/block-library/blocks/post-excerpt/style-rtl.css 69 B
build/block-library/blocks/post-excerpt/style.css 69 B
build/block-library/blocks/post-featured-image/editor-rtl.css 507 B
build/block-library/blocks/post-featured-image/editor.css 505 B
build/block-library/blocks/post-featured-image/style-rtl.css 166 B
build/block-library/blocks/post-featured-image/style.css 166 B
build/block-library/blocks/post-template/editor-rtl.css 99 B
build/block-library/blocks/post-template/editor.css 98 B
build/block-library/blocks/post-template/style-rtl.css 282 B
build/block-library/blocks/post-template/style.css 282 B
build/block-library/blocks/post-terms/style-rtl.css 73 B
build/block-library/blocks/post-terms/style.css 73 B
build/block-library/blocks/post-title/style-rtl.css 100 B
build/block-library/blocks/post-title/style.css 100 B
build/block-library/blocks/preformatted/style-rtl.css 103 B
build/block-library/blocks/preformatted/style.css 103 B
build/block-library/blocks/pullquote/editor-rtl.css 135 B
build/block-library/blocks/pullquote/editor.css 135 B
build/block-library/blocks/pullquote/style-rtl.css 326 B
build/block-library/blocks/pullquote/style.css 325 B
build/block-library/blocks/pullquote/theme-rtl.css 167 B
build/block-library/blocks/pullquote/theme.css 167 B
build/block-library/blocks/query-pagination-numbers/editor-rtl.css 122 B
build/block-library/blocks/query-pagination-numbers/editor.css 121 B
build/block-library/blocks/query-pagination/editor-rtl.css 221 B
build/block-library/blocks/query-pagination/editor.css 211 B
build/block-library/blocks/query-pagination/style-rtl.css 282 B
build/block-library/blocks/query-pagination/style.css 278 B
build/block-library/blocks/query-title/style-rtl.css 63 B
build/block-library/blocks/query-title/style.css 63 B
build/block-library/blocks/query/editor-rtl.css 439 B
build/block-library/blocks/query/editor.css 439 B
build/block-library/blocks/quote/style-rtl.css 213 B
build/block-library/blocks/quote/style.css 213 B
build/block-library/blocks/quote/theme-rtl.css 223 B
build/block-library/blocks/quote/theme.css 226 B
build/block-library/blocks/read-more/style-rtl.css 132 B
build/block-library/blocks/read-more/style.css 132 B
build/block-library/blocks/rss/editor-rtl.css 202 B
build/block-library/blocks/rss/editor.css 204 B
build/block-library/blocks/rss/style-rtl.css 289 B
build/block-library/blocks/rss/style.css 288 B
build/block-library/blocks/search/editor-rtl.css 165 B
build/block-library/blocks/search/editor.css 165 B
build/block-library/blocks/search/style-rtl.css 396 B
build/block-library/blocks/search/style.css 393 B
build/block-library/blocks/search/theme-rtl.css 114 B
build/block-library/blocks/search/theme.css 114 B
build/block-library/blocks/separator/editor-rtl.css 146 B
build/block-library/blocks/separator/editor.css 146 B
build/block-library/blocks/separator/style-rtl.css 233 B
build/block-library/blocks/separator/style.css 233 B
build/block-library/blocks/separator/theme-rtl.css 194 B
build/block-library/blocks/separator/theme.css 194 B
build/block-library/blocks/shortcode/editor-rtl.css 464 B
build/block-library/blocks/shortcode/editor.css 464 B
build/block-library/blocks/site-logo/editor-rtl.css 461 B
build/block-library/blocks/site-logo/editor.css 461 B
build/block-library/blocks/site-logo/style-rtl.css 192 B
build/block-library/blocks/site-logo/style.css 192 B
build/block-library/blocks/site-tagline/editor-rtl.css 86 B
build/block-library/blocks/site-tagline/editor.css 86 B
build/block-library/blocks/site-title/editor-rtl.css 84 B
build/block-library/blocks/site-title/editor.css 84 B
build/block-library/blocks/social-link/editor-rtl.css 184 B
build/block-library/blocks/social-link/editor.css 184 B
build/block-library/blocks/social-links/editor-rtl.css 674 B
build/block-library/blocks/social-links/editor.css 673 B
build/block-library/blocks/social-links/style-rtl.css 1.39 kB
build/block-library/blocks/social-links/style.css 1.38 kB
build/block-library/blocks/spacer/editor-rtl.css 322 B
build/block-library/blocks/spacer/editor.css 322 B
build/block-library/blocks/spacer/style-rtl.css 48 B
build/block-library/blocks/spacer/style.css 48 B
build/block-library/blocks/table/editor-rtl.css 494 B
build/block-library/blocks/table/editor.css 494 B
build/block-library/blocks/table/style-rtl.css 611 B
build/block-library/blocks/table/style.css 609 B
build/block-library/blocks/table/theme-rtl.css 175 B
build/block-library/blocks/table/theme.css 175 B
build/block-library/blocks/tag-cloud/style-rtl.css 239 B
build/block-library/blocks/tag-cloud/style.css 239 B
build/block-library/blocks/template-part/editor-rtl.css 235 B
build/block-library/blocks/template-part/editor.css 235 B
build/block-library/blocks/template-part/theme-rtl.css 101 B
build/block-library/blocks/template-part/theme.css 101 B
build/block-library/blocks/text-columns/editor-rtl.css 95 B
build/block-library/blocks/text-columns/editor.css 95 B
build/block-library/blocks/text-columns/style-rtl.css 166 B
build/block-library/blocks/text-columns/style.css 166 B
build/block-library/blocks/verse/style-rtl.css 87 B
build/block-library/blocks/verse/style.css 87 B
build/block-library/blocks/video/editor-rtl.css 561 B
build/block-library/blocks/video/editor.css 563 B
build/block-library/blocks/video/style-rtl.css 174 B
build/block-library/blocks/video/style.css 174 B
build/block-library/blocks/video/theme-rtl.css 110 B
build/block-library/blocks/video/theme.css 110 B
build/block-library/common-rtl.css 1.01 kB
build/block-library/common.css 1 kB
build/block-library/editor-elements-rtl.css 75 B
build/block-library/editor-elements.css 75 B
build/block-library/editor-rtl.css 10.9 kB
build/block-library/editor.css 10.9 kB
build/block-library/elements-rtl.css 54 B
build/block-library/elements.css 54 B
build/block-library/index.min.js 188 kB
build/block-library/reset-rtl.css 478 B
build/block-library/reset.css 478 B
build/block-library/style-rtl.css 11.9 kB
build/block-library/style.css 11.9 kB
build/block-library/theme-rtl.css 695 B
build/block-library/theme.css 700 B
build/block-serialization-default-parser/index.min.js 1.11 kB
build/block-serialization-spec-parser/index.min.js 2.83 kB
build/blocks/index.min.js 49.6 kB
build/components/index.min.js 197 kB
build/components/style-rtl.css 11.5 kB
build/components/style.css 11.6 kB
build/compose/index.min.js 12 kB
build/core-data/index.min.js 15.5 kB
build/customize-widgets/index.min.js 11.3 kB
build/customize-widgets/style-rtl.css 1.38 kB
build/customize-widgets/style.css 1.38 kB
build/data-controls/index.min.js 653 B
build/data/index.min.js 8.06 kB
build/date/index.min.js 32 kB
build/deprecated/index.min.js 507 B
build/dom-ready/index.min.js 324 B
build/dom/index.min.js 4.69 kB
build/edit-navigation/index.min.js 16 kB
build/edit-navigation/style-rtl.css 4 kB
build/edit-navigation/style.css 4.01 kB
build/edit-post/classic-rtl.css 546 B
build/edit-post/classic.css 547 B
build/edit-post/index.min.js 30.5 kB
build/edit-post/style-rtl.css 6.94 kB
build/edit-post/style.css 6.94 kB
build/edit-site/index.min.js 57.8 kB
build/edit-site/style-rtl.css 8.22 kB
build/edit-site/style.css 8.2 kB
build/edit-widgets/index.min.js 16.5 kB
build/edit-widgets/style-rtl.css 4.35 kB
build/edit-widgets/style.css 4.35 kB
build/editor/index.min.js 41.5 kB
build/editor/style-rtl.css 3.66 kB
build/editor/style.css 3.65 kB
build/element/index.min.js 4.68 kB
build/escape-html/index.min.js 537 B
build/format-library/index.min.js 6.75 kB
build/format-library/style-rtl.css 571 B
build/format-library/style.css 571 B
build/hooks/index.min.js 1.64 kB
build/html-entities/index.min.js 448 B
build/i18n/index.min.js 3.77 kB
build/is-shallow-equal/index.min.js 527 B
build/keyboard-shortcuts/index.min.js 1.78 kB
build/keycodes/index.min.js 1.81 kB
build/list-reusable-blocks/index.min.js 1.74 kB
build/list-reusable-blocks/style-rtl.css 835 B
build/list-reusable-blocks/style.css 835 B
build/media-utils/index.min.js 2.93 kB
build/notices/index.min.js 953 B
build/nux/index.min.js 2.05 kB
build/nux/style-rtl.css 732 B
build/nux/style.css 728 B
build/plugins/index.min.js 1.94 kB
build/preferences-persistence/index.min.js 2.22 kB
build/preferences/index.min.js 1.3 kB
build/primitives/index.min.js 933 B
build/priority-queue/index.min.js 612 B
build/react-i18n/index.min.js 696 B
build/react-refresh-entry/index.min.js 8.44 kB
build/react-refresh-runtime/index.min.js 7.31 kB
build/redux-routine/index.min.js 2.74 kB
build/reusable-blocks/index.min.js 2.21 kB
build/reusable-blocks/style-rtl.css 256 B
build/reusable-blocks/style.css 256 B
build/rich-text/index.min.js 10.4 kB
build/server-side-render/index.min.js 1.61 kB
build/shortcode/index.min.js 1.53 kB
build/token-list/index.min.js 644 B
build/url/index.min.js 3.61 kB
build/vendors/react-dom.min.js 38.5 kB
build/vendors/react.min.js 4.34 kB
build/viewport/index.min.js 1.08 kB
build/warning/index.min.js 268 B
build/widgets/index.min.js 7.2 kB
build/widgets/style-rtl.css 1.18 kB
build/widgets/style.css 1.19 kB
build/wordcount/index.min.js 1.06 kB

compressed-size-action

Copy link
Contributor

@ciampo ciampo left a comment

Choose a reason for hiding this comment

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

Thank you @chad1008 for working on this! Adding unit tests to this component will be of great value.

Regarding what other tests we could add, here's what comes to mind:

  • we should test both the controlled and uncontrolled version of the component
  • we should add tests in which the user interacts with the component exclusively using the keyboard
  • We should add a test for a more complete and typical user interaction, involving search and filtering. For example:
    • user clicks on the combobox
    • (assert that all options are being rendered)
    • user moves focus on the search input
    • user types a string that should not generate any matches
    • (assert that there are no options being rendered)
    • user clears text search input
    • (assert that all options are being rendered)
    • user types a new search string, this time one that should match a few options
    • (assert that the options that are being rendered are the expected ones matching the search string)
    • user clicks on one of the available options
    • (assert that input value and onChange spy have the new correct value selected)
    • etc...

A few important reflections that I noticed while reviewing this component:

  1. The last test has revealed a potential discrepancy: it looks like the <input />'s value and the argument passed to the onChange callback are not the same value 😱 Shouldn't the expectation be that the input's value is the abbreviation, instead of the name?between the value passed to onChange and the value of the `input
  2. Not sure if this is an accessibility issue, but while reviewing the tests, I noticed that the options are not rendered to the DOM until the combobox is focused/clicked. This differs from other Combobox examples where those option items are rendered in the DOM, but just not visible (e.g. ariakit)
  3. userEvent has some nice utility functions, including one to select or deselect options — unfortunately I tried it for these tests, but it doesn't seem to work because we're using custom aria roles, instead of native select and option elements.

cc @mirka , especially regarding the last 3 points.

We should also keep in mind that this component is potentially going to be rewritten using ariakit component primitives.

packages/components/src/combobox-control/test/index.js Outdated Show resolved Hide resolved
packages/components/src/combobox-control/test/index.js Outdated Show resolved Hide resolved
packages/components/src/combobox-control/test/index.js Outdated Show resolved Hide resolved
packages/components/src/combobox-control/test/index.js Outdated Show resolved Hide resolved
packages/components/src/combobox-control/test/index.js Outdated Show resolved Hide resolved
packages/components/src/combobox-control/test/index.js Outdated Show resolved Hide resolved
packages/components/src/combobox-control/test/index.js Outdated Show resolved Hide resolved
packages/components/src/combobox-control/test/index.js Outdated Show resolved Hide resolved
packages/components/src/combobox-control/test/index.js Outdated Show resolved Hide resolved
@ciampo
Copy link
Contributor

ciampo commented Jul 15, 2022

we should test both the controlled and uncontrolled version of the component

@stokesman , if I remember correctly you had worked a few weeks ago on adding some unit tests for a component in which you were testing for both the controlled and uncontrolled version of such component?

@stokesman
Copy link
Contributor

testing for both the controlled and uncontrolled version of such component?

Yes, it was RangeControl and the way I'd approached was turning the outer describe into describe.each. The change didn't end up in trunk but here’s the commit ce0ac42.

@chad1008
Copy link
Contributor Author

chad1008 commented Jul 18, 2022

Wow, thank you for the thorough review at @ciampo... really informative and super helpful.

I've pushed some updates addressing most of your points. Still to do:

  • we should test both the controlled and uncontrolled version of the component
  • we should add tests in which the user interacts with the component exclusively using the keyboard
  • we should add a test for a more complete and typical user interaction, involving search and filtering

The second item in that list will be tricky. Thus far I haven't had any luck getting userEvent.keyboard() to successfully register arrow keys or the enter keys for this component. Typing works fine, but navigating and and actually selecting an option remains elusive.

I'm not sure why yet, but I have seen reports of issues with arrow keys on other elements, which could be related (or possibly not?)

@chad1008
Copy link
Contributor Author

Yes, it was RangeControl and the way I'd approached was turning the outer describe into describe.each. The change didn't end up in trunk but here’s the commit ce0ac42.

Nice, thank you @stokesman!

Copy link
Member

@mirka mirka left a comment

Choose a reason for hiding this comment

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

2. Not sure if this is an accessibility issue, but while reviewing the tests, I noticed that the options are not rendered to the DOM until the combobox is focused/clicked. This differs from other Combobox examples where those option items are rendered in the DOM, but just not visible (e.g. ariakit)

I think it should be fine, given that options can often be lazy loaded (e.g. virtualized or paginated).

@chad1008
Copy link
Contributor Author

chad1008 commented Jul 20, 2022

Interesting development I'd love some input on. I was writing a test for invalid search strings and found the component currently behaves differently than I'd expect.

When the input gets focus, it starts by highlighting the zero indexed element of the list, which makes sense. As the user types it filters that list. If a string with no matches is entered, I expected it would set the value to an empty string (selecting nothing).

It actually selects the original zero-indexed element (in the case of these tests, Greenwich Mean Time). So you can type something that doesn't exist, hit Enter and get a value that doesn't even closely resemble what you searched for.

That feels like a bug to address in a separate PR to me... thoughts @ciampo / @mirka ?

Edit/correction: On invalid strings, it actually selects whatever the last highlighted item was (not just the zero index), either because of a hover or a partially correct string. For example, in Storybook, typing Algzzzzz and hitting Enter would actually select Algeria, because that was the last valid match before the string started failing.

@chad1008
Copy link
Contributor Author

chad1008 commented Jul 20, 2022

I dug a little more and this appears to be intentional. The list is filtered as the user types and invalid matches choose the zero index of the remaining option(s). The goal of this PR appears to be to address confusion when an existing option was already selected a new string was entered.

This still feels like a confusing experience to me though, because when I type an invalid string, the suggestion I'm ultimately selecting isn't shown until after I select it.

I can think of three options:

  1. Change the behavior so that matchless searches return empty (bonus, we could look into replacing the suggestion list with a "No matches found" indicator)
  2. Keep the behavior as-is but show the 'fallback suggestion' that will ultimately get selected for non-matching strings
  3. No changes, invalid searches continue to choose the last remaining match without any suggestions shown

@ciampo
Copy link
Contributor

ciampo commented Jul 21, 2022

I dug a little more and this appears to be intentional. The list is filtered as the user types and invalid matches choose the zero index of the remaining option(s). The goal of this PR appears to be to address confusion when an existing option was already selected a new string was entered.

This still feels like a confusing experience to me though, because when I type an invalid string, the suggestion I'm ultimately selecting isn't shown until after I select it.

I can think of three options:

  1. Change the behavior so that matchless searches return empty (bonus, we could look into replacing the suggestion list with a "No matches found" indicator)
  2. Keep the behavior as-is but show the 'fallback suggestion' that will ultimately get selected for non-matching strings
  3. No changes, invalid searches continue to choose the last remaining match without any suggestions shown

This definitely looks like an aspect of the component that we can polish — but I would do it in a separate issue. Let's keep this issue focused on adding the essential unit tests to ComboboxControl as planned.

The last test has revealed a potential discrepancy: it looks like the 's value and the argument passed to the onChange callback are not the same value 😱 Shouldn't the expectation be that the input's value is the abbreviation, instead of the name?between the value passed to onChange and the value of the `input

Just flagging this point in case it got overlooked — this may be an even higher priority task to follow-up with after this PR (and the related exhaustive-deps changes) get merged.

@chad1008
Copy link
Contributor Author

I've just pushed some updates to address the feedback shared above! I responded to one of the comments directly, but in addition to that I've added additional tests for

  • searching/filtering
  • complex interactions
  • keyboard only interaction
  • controlled vs. uncontrolled modes (Kudos to @stokesman for inspiration on the describe.each approach 👏 )
  • confirming aria announcements are rendered upon selection

Important note: some of the uncontrolled tests will fail until #42752 is merged, as the component's current implementation doesn't support an uncontrolled mode. Without a value prop passed in, the component never actually inserts a selection into the input element. The linked PR addresses that (hat tip to @ciampo for teaching me how to go about implementing that hook!)

@ciampo
Copy link
Contributor

ciampo commented Jul 28, 2022

Sounds good! Let's merge #42752 first, rebase this PR, and then give it a final round of review.

@chad1008 chad1008 force-pushed the add/ComboboxControl-unit-tests branch from c3a8fa7 to 3a1f950 Compare July 28, 2022 11:56
@chad1008
Copy link
Contributor Author

#42752 merge and a rebase of this PR are complete. Marking as ready for review!

@chad1008 chad1008 marked this pull request as ready for review July 28, 2022 11:56
@chad1008 chad1008 requested a review from ajitbohra as a code owner July 28, 2022 11:56
Copy link
Contributor

@ciampo ciampo left a comment

Choose a reason for hiding this comment

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

Looking good! I just left a couple more comments.

Could you also add a CHANGELOG entry? This could go under the Internal section, I believe

const getInput = ( name ) => screen.getByRole( 'combobox', { name } );
const getOption = ( name ) => screen.getByRole( 'option', { name } );
const getAllOptions = () => screen.getAllByRole( 'option' );
const setTargetOption = ( index ) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

This comment is a mix of nitpicking and general advice:

  • I believe a more appropriate name for this function would be getTargetOption, as you're passing an index and getting the option in return
  • Since we're setting the options prop in each test, it feels more correct that this function takes the array of options as a parameter too, instead of assuming that to always be timezones
  • Overall, I'm not sure about how much value does this abstraction holds. It basically exists in order to append the searchString property. At most, if we wanted to abstract that piece of functionality away, we could at most create a utility function like getSearchString( option ) => option.label.substring( 0, 11 ) — or even have no "utility" function at all

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed on the function name and the options parameter. Made both of those changes! 👍

Re: the abstraction itself... in addition to appending the searchString property, I also felt like it could be beneficial from a maintenance perspective to have the desired index declared once per test.

The target property gets referenced as much as three times per test. Without the abstraction, if we ever update the target of a given test we need to make sure to hit all three instances of it. With the abstraction it can just be updated once for the test in question, because the abstraction keeps it nice and DRY.

I'm happy to remove it or replace it with a getSearchString() if you think that would be best though.

Copy link
Contributor

Choose a reason for hiding this comment

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

Re: the abstraction itself... in addition to appending the searchString property, I also felt like it could be beneficial from a maintenance perspective to have the desired index declared once per test.

The target property gets referenced as much as three times per test. Without the abstraction, if we ever update the target of a given test we need to make sure to hit all three instances of it. With the abstraction it can just be updated once for the test in question, because the abstraction keeps it nice and DRY.

For completeness sake, here's what I had in mind when writing the previous message:

click to expand
diff --git a/packages/components/src/combobox-control/test/index.js b/packages/components/src/combobox-control/test/index.js
index 2bcd980982..72033f679f 100644
--- a/packages/components/src/combobox-control/test/index.js
+++ b/packages/components/src/combobox-control/test/index.js
@@ -53,10 +53,7 @@ const getLabel = ( labelText ) => screen.getByText( labelText );
 const getInput = ( name ) => screen.getByRole( 'combobox', { name } );
 const getOption = ( name ) => screen.getByRole( 'option', { name } );
 const getAllOptions = () => screen.getAllByRole( 'option' );
-const getTargetOption = ( options, index ) => {
-	const searchString = options[ index ].label.substring( 0, 11 );
-	return { searchString, ...options[ index ] };
-};
+const getOptionSearchString = ( option ) => option.label.substring( 0, 11 );
 const setupUser = () =>
 	userEvent.setup( {
 		advanceTimers: jest.advanceTimersByTime,
@@ -138,7 +135,7 @@ describe.each( [
 
 	it( 'should select the correct option via click events', async () => {
 		const user = setupUser();
-		const targetOption = getTargetOption( timezones, 2 );
+		const targetOption = timezones[ 2 ];
 		const onChangeSpy = jest.fn();
 		render(
 			<Component
@@ -163,7 +160,7 @@ describe.each( [
 	it( 'should select the correct option via keypress events', async () => {
 		const user = setupUser();
 		const targetIndex = 4;
-		const targetOption = getTargetOption( timezones, targetIndex );
+		const targetOption = timezones[ targetIndex ];
 		const onChangeSpy = jest.fn();
 		render(
 			<Component
@@ -192,7 +189,7 @@ describe.each( [
 
 	it( 'should select the correct option from a search', async () => {
 		const user = setupUser();
-		const targetOption = getTargetOption( timezones, 13 );
+		const targetOption = timezones[ 13 ];
 		const onChangeSpy = jest.fn();
 		render(
 			<Component
@@ -207,7 +204,7 @@ describe.each( [
 		await user.tab();
 
 		// Type enough characters to ensure a predictable search result
-		await user.keyboard( targetOption.searchString );
+		await user.keyboard( getOptionSearchString( targetOption ) );
 
 		// Pressing Enter/Return selects the currently focused option
 		await user.keyboard( '{Enter}' );
@@ -219,7 +216,7 @@ describe.each( [
 
 	it( 'should render aria-live announcement upon selection', async () => {
 		const user = setupUser();
-		const targetOption = getTargetOption( timezones, 9 );
+		const targetOption = timezones[ 9 ];
 		const onChangeSpy = jest.fn();
 		render(
 			<Component
@@ -233,7 +230,7 @@ describe.each( [
 		await user.tab();
 
 		// Type enough characters to ensure a predictable search result
-		await user.keyboard( targetOption.searchString );
+		await user.keyboard( getOptionSearchString( targetOption ) );
 
 		// Pressing Enter/Return selects the currently focused option
 		await user.keyboard( '{Enter}' );
@@ -248,7 +245,7 @@ describe.each( [
 	it( 'should process multiple entries in a single session', async () => {
 		const user = setupUser();
 		const unmatchedString = 'Mordor';
-		const targetOption = getTargetOption( timezones, 6 );
+		const targetOption = timezones[ 6 ];
 		const onChangeSpy = jest.fn();
 		render(
 			<Component
@@ -289,12 +286,13 @@ describe.each( [
 		} );
 
 		// Run a second search with a valid string.
-		await user.keyboard( targetOption.searchString );
+		const searchString = getOptionSearchString( targetOption );
+		await user.keyboard( searchString );
 		const validSearchRenderedOptions = getAllOptions();
 
 		// Find option that match the search string.
 		const matches = timezones.filter( ( option ) =>
-			option.label.includes( targetOption.searchString )
+			option.label.includes( searchString )
 		);
 
 		// Confirm the rendered options match the provided dataset based on the current string.

Removing the getTargetOption abstraction doesn't necessarily mean that the targetOption can be saved as a variable in each test and invoked multiple times. We could then call the getOptionSearchString only where it's actually necessary to get a search string.

Anyway, not a big deal — we can merge this PR with or without the suggested changes

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, brilliant. My concerns about repetition were more based on your suggestion of possible having no utility function at all. I really do like your proposed getOptionSearchString() approach, it's a lot cleaner than my previous implementation.

Adding your changes to the PR now, which should be the final piece of the puzzle for this one. Thank you @ciampo!

packages/components/src/combobox-control/test/index.js Outdated Show resolved Hide resolved
@chad1008 chad1008 force-pushed the add/ComboboxControl-unit-tests branch from 3a1f950 to c63c4b2 Compare August 3, 2022 13:42
@chad1008 chad1008 self-assigned this Aug 4, 2022
Copy link
Contributor

@ciampo ciampo left a comment

Choose a reason for hiding this comment

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

LGTM 🚀

(there's still one pending comment, but it's not a blocker and we can merge this PR with or without the changes suggested in there)

@chad1008
Copy link
Contributor Author

Thanks @ciampo. The changes recommended in that comment are now included in the PR 🙂. Once tests pass I'll get this one merged. Thanks again for all of the feedback!

chad1008 and others added 24 commits August 31, 2022 06:05
Co-authored-by: Marco Ciampini <marco.ciampo@gmail.com>
@chad1008 chad1008 force-pushed the add/ComboboxControl-unit-tests branch from e7bec12 to 4edad41 Compare August 31, 2022 10:05
@chad1008 chad1008 merged commit c7272d8 into trunk Aug 31, 2022
@chad1008 chad1008 deleted the add/ComboboxControl-unit-tests branch August 31, 2022 12:32
@github-actions github-actions bot added this to the Gutenberg 14.1 milestone Aug 31, 2022
@cbravobernal cbravobernal added [Package] Components /packages/components [Type] Code Quality Issues or PRs that relate to code quality labels Sep 13, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Package] Components /packages/components [Type] Code Quality Issues or PRs that relate to code quality
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants