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

Components: Refactor Button tests to @testing-library/react #42981

Merged
merged 4 commits into from
Aug 12, 2022

Conversation

tyxla
Copy link
Member

@tyxla tyxla commented Aug 4, 2022

What?

We've recently started refactoring enzyme tests to @testing-library/react.

This PR refactors the <Button /> component tests from enzyme to @testing-library/react.

Why?

@testing-library/react provides a better way to write tests for accessible components that is closer to the way the user experiences them.

How?

We're straightforwardly replacing enzyme tests with @testing-library/react ones, using jest-dom matchers and mocks to avoid testing unrelated implementation details.

Testing Instructions

Verify tests pass: npm run test-unit packages/components/src/button/test/index.js

Screenshots or screencast

@tyxla tyxla added [Type] Enhancement A suggestion for improvement. [Package] Components /packages/components labels Aug 4, 2022
@tyxla tyxla requested review from mirka, ciampo and chad1008 August 4, 2022 13:22
@tyxla tyxla self-assigned this Aug 4, 2022
@tyxla tyxla requested a review from ajitbohra as a code owner August 4, 2022 13:22
@tyxla
Copy link
Member Author

tyxla commented Aug 4, 2022

cc @flootr and @brookewp with whom we have been working on enzyme to testing-library migration in another repo recently 😉

@github-actions
Copy link

github-actions bot commented Aug 4, 2022

Size Change: +3.78 kB (0%)

Total Size: 1.27 MB

Filename Size Change
build/block-editor/index.min.js 156 kB +2.1 kB (+1%)
build/block-editor/style-rtl.css 14.7 kB +3 B (0%)
build/block-editor/style.css 14.7 kB +8 B (0%)
build/block-library/blocks/button/style-rtl.css 539 B -3 B (-1%)
build/block-library/blocks/button/style.css 539 B -3 B (-1%)
build/block-library/index.min.js 185 kB +63 B (0%)
build/block-library/style-rtl.css 11.9 kB -3 B (0%)
build/block-library/style.css 11.9 kB -2 B (0%)
build/components/index.min.js 231 kB +378 B (0%)
build/components/style-rtl.css 14 kB -68 B (0%)
build/components/style.css 14 kB -68 B (0%)
build/compose/index.min.js 12 kB +350 B (+3%)
build/core-data/index.min.js 15.2 kB +433 B (+3%)
build/edit-site/index.min.js 56.9 kB +120 B (0%)
build/editor/index.min.js 41.4 kB +70 B (0%)
build/keycodes/index.min.js 1.79 kB +403 B (+29%) 🚨
ℹ️ 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 6.58 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-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 103 B
build/block-library/blocks/audio/style.css 103 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/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/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 615 B
build/block-library/blocks/cover/editor.css 616 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 333 B
build/block-library/blocks/group/editor.css 333 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 736 B
build/block-library/blocks/image/editor.css 737 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 493 B
build/block-library/blocks/media-text/style.css 490 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 2.03 kB
build/block-library/blocks/navigation/editor.css 2.04 kB
build/block-library/blocks/navigation/style-rtl.css 1.98 kB
build/block-library/blocks/navigation/style.css 1.97 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 157 B
build/block-library/blocks/paragraph/editor.css 157 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-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 605 B
build/block-library/blocks/post-featured-image/editor.css 605 B
build/block-library/blocks/post-featured-image/style-rtl.css 153 B
build/block-library/blocks/post-featured-image/style.css 153 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 80 B
build/block-library/blocks/post-title/style.css 80 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 198 B
build/block-library/blocks/pullquote/editor.css 198 B
build/block-library/blocks/pullquote/style-rtl.css 370 B
build/block-library/blocks/pullquote/style.css 370 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/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 708 B
build/block-library/blocks/site-logo/editor.css 708 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 159 B
build/block-library/blocks/video/style.css 159 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/reset-rtl.css 478 B
build/block-library/reset.css 478 B
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 47.3 kB
build/customize-widgets/index.min.js 11.3 kB
build/customize-widgets/style-rtl.css 1.4 kB
build/customize-widgets/style.css 1.4 kB
build/data-controls/index.min.js 653 B
build/data/index.min.js 8.03 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.02 kB
build/edit-navigation/style.css 4.03 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/style-rtl.css 8.23 kB
build/edit-site/style.css 8.22 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/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/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 11.1 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.19 kB
build/widgets/style-rtl.css 1.16 kB
build/widgets/style.css 1.16 kB
build/wordcount/index.min.js 1.06 kB

compressed-size-action

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.

Thanks for working on this, it's great to see the tests for one of the most important components be modernized!

I haven't checked every single test, but I think I noted the main points where we can simplify things and make the tests more robust.

Comment on lines 18 to 29
jest.mock( '../../icon', () => () => <div data-testid="test-icon" /> );
jest.mock( '../../tooltip', () => ( { text, children } ) => (
<div data-testid="test-tooltip" title={ text }>
{ children }
</div>
) );
jest.mock( '../../visually-hidden', () => ( {
__esModule: true,
VisuallyHidden: ( { children } ) => (
<div data-testid="test-visually-hidden">{ children }</div>
),
} ) );
Copy link
Member

Choose a reason for hiding this comment

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

Ah I hadn't really thought of mocking components! 😆 TIL

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, it's a nice way to separate concerns and make sure that we're not stepping into the testing grounds of another component.

Copy link
Member Author

Choose a reason for hiding this comment

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

FWIW I've left the icon mock because I think it helps reduce noise in the tests and actually improves the test quality, vs trying to figure out how to query an SVG that's aria-hidden.

Copy link
Contributor

@ciampo ciampo Aug 10, 2022

Choose a reason for hiding this comment

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

Not sure if this is the most correct conversation to reply to, but for reference this is a convo that we had on the difficulties when testing VisuallyHidden:, where we ended up using snapshot testing for it: #42403 (comment)

And this is a conversation where we used the selector option to make sure that the queried element had a certain aria attribute : #42403 (comment)

Just in case they can be useful for this PR too!

Comment on lines 34 to 43
const button = render( <Button /> ).container.firstChild;

expect( button ).toHaveClass( 'components-button' );
expect( button ).not.toHaveClass( 'is-large' );
expect( button ).not.toHaveClass( 'is-primary' );
expect( button ).not.toHaveClass( 'is-pressed' );
expect( button ).not.toHaveAttribute( 'disabled' );
expect( button ).not.toHaveAttribute( 'aria-disabled' );
expect( button ).toHaveAttribute( 'type', 'button' );
expect( button.tagName ).toBe( 'BUTTON' );
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 something like this would be more in line with how we want to write tests in the components package:

Suggested change
const button = render( <Button /> ).container.firstChild;
expect( button ).toHaveClass( 'components-button' );
expect( button ).not.toHaveClass( 'is-large' );
expect( button ).not.toHaveClass( 'is-primary' );
expect( button ).not.toHaveClass( 'is-pressed' );
expect( button ).not.toHaveAttribute( 'disabled' );
expect( button ).not.toHaveAttribute( 'aria-disabled' );
expect( button ).toHaveAttribute( 'type', 'button' );
expect( button.tagName ).toBe( 'BUTTON' );
render( <Button /> );
const button = screen.getByRole( 'button' );
expect( button ).toHaveClass( 'components-button' );
expect( button ).not.toHaveClass( 'is-large' );
expect( button ).not.toHaveClass( 'is-primary' );
expect( button ).not.toHaveClass( 'is-pressed' );
expect( button ).not.toBeDisabled();
expect( button ).toHaveAttribute( 'type', 'button' );

The differences being:

  1. We try not to use container.firstChild. This follows official RTL guidance. In this case, it isn't guaranteed that the first child will always be the button (could be div or whatever).
  2. tagName assertions can usually be covered with the screen query.
  3. Using a more abstracted jest-dom matcher (toBeDisabled()) can often be more robust than trying to check specific attributes (disabled/aria-disabled). (It might be that some of the tests in this file do actually need to check specific aria attributes, and that's ok.)

Copy link
Member Author

Choose a reason for hiding this comment

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

We try not to use container.firstChild. This follows official RTL guidance. In this case, it isn't guaranteed that the first child will always be the button (could be div or whatever).

I might be misunderstanding, but I think that rule is talking about something else. The rule you're linking is talking about how we run get and query queries, namely, it suggests that we always call them from screen:

// ❌
const {getByRole} = render(<Example />)
const errorMessageNode = getByRole('alert')

// ✅
render(<Example />)
const errorMessageNode = screen.getByRole('alert')

This doesn't have anything to do with container.firstChild. I actually think that for our test it's valuable to assert that the root element is the button, and if that changes, the test should break.

There is this "common mistake" that is closer to what we're doing here:

// ❌
const {container} = render(<Example />)
const button = container.querySelector('.btn-primary')
expect(button).toHaveTextContent(/click me/i)

// ✅
render(<Example />)
screen.getByRole('button', {name: /click me/i})

but it talks specifically against using container.querySelector() and container.querySelectorAll() which we are consciously avoiding here anyway.

The only reason to avoid this would be if we followed the testing-library/no-node-access ESLint rule, which we currently aren't. And even if we did, this rule does have a allowContainerFirstChild option that allows us to use container.firstChild if we want to.

Sorry for the wall of text 😉 Just wanted to share some of why I've been using that and I've been finding it useful. Sometimes it's just predictable to access the root element of the component, without having to seek a special way to locate it with a query.

I'm happy to always use screen queries, but then, this has its downsides sometimes, especially in components where we have nested elements that match the top elements, or recursive structures.
Just wanted to hear more of what you think about all this.

tagName assertions can usually be covered with the screen query.

I'm not sure that's always the case, though - screen.getByRole( 'button' ) will also find input[type='button'], which is definitely unwanted. The tagName assertion seems more precise to me in this scenario. WDYT?

Using a more abstracted jest-dom matcher (toBeDisabled()) can often be more robust than trying to check specific attributes

Are you saying that toBeDisabled() is enough for us and is preferred to testing both the disabled and aria-disabled attributes separately?

Copy link
Member

Choose a reason for hiding this comment

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

I might be misunderstanding, but I think that rule is talking about something else.

Ah sorry, I was referring to this part:

The only exception to this is if you're setting the container or baseElement which you probably should avoid doing (I honestly can't think of a legitimate use case for those options anymore and they only exist for historical reasons at this point).


I actually think that for our test it's valuable to assert that the root element is the button, and if that changes, the test should break.

The question is — why should the test break if the root element is a div? Would it break behavior? No. Would it break styling? Maybe. So if we're trying to test for styling breakage with RTL, we're kind of using the wrong tool. Does it warrant a DOM snapshot then? I'd say no in this case.

Even stuff like .toHaveClass( 'is-foo' ) is not something we generally do, since it's testing an implementation detail. Ideally, it's something for a visual regression test.

I'm not sure that's always the case, though - screen.getByRole( 'button' ) will also find input[type='button'], which is definitely unwanted. The tagName assertion seems more precise to me in this scenario. WDYT?

We usually have no reason to distinguish between implementations as long as they are accessible, and this is what I understand to be the central ethos of RTL. It's why the first choice of query is getByRole, and there is no getByTagName query or toHaveTagName assertion. So if there is a concrete reason that it has to be a tagName=BUTTON, I would first try to assert for that behavior (e.g. can I pass HTML as children?), and if that isn't possible, add a comment on why it needs to be a <button>.

Are you saying that toBeDisabled() is enough for us and is preferred to testing both the disabled and aria-disabled attributes separately?

Apologies, I was under the impression that toBeDisabled checks both, but I guess it doesn't! So in a component like this that needs to specifically check for aria-disabled behavior because of the "focusable disabled" stuff, I understand it makes sense to assert separately and specifically.

Copy link
Member Author

Choose a reason for hiding this comment

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

This has been very helpful, thank you ❤️ This helped a lot to clear some confusion in my head and to deeply understand the tradeoffs we're making when using this library.

I've updated the tests to reflect your suggestions - namely, no container usage, no DOM traversing and using screen queries instead.

Comment on lines 162 to 170
render( <Button icon={ plusCircle } label="WordPress" /> );

const tooltip = screen.getByTestId( 'test-tooltip' );
expect( tooltip ).toBeVisible();
expect( tooltip ).toHaveAttribute( 'title', 'WordPress' );

const button = screen.getByRole( 'button' );
expect( tooltip ).toContainElement( button );
expect( button ).toHaveAttribute( 'aria-label', 'WordPress' );
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 hoping we can manage without mocking Tooltip. For example, this is working for me:

Suggested change
render( <Button icon={ plusCircle } label="WordPress" /> );
const tooltip = screen.getByTestId( 'test-tooltip' );
expect( tooltip ).toBeVisible();
expect( tooltip ).toHaveAttribute( 'title', 'WordPress' );
const button = screen.getByRole( 'button' );
expect( tooltip ).toContainElement( button );
expect( button ).toHaveAttribute( 'aria-label', 'WordPress' );
render( <Button icon={ plusCircle } label="WordPress" /> );
const button = screen.getByRole( 'button', { name: 'WordPress' } );
button.focus();
expect( screen.getByText( 'WordPress' ) ).toBeInTheDocument();

Copy link
Member Author

Choose a reason for hiding this comment

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

You definitely have a point here. However, the problem with this type of testing is that we're not sure where WordPress appeared. It could have been displayed from the Tooltip component from its title prop, but it could also have been displayed from another prop. How can we be sure? What you're suggesting simply checks if the text is there, and doesn't care "how" it was displayed. IMHO it also bleeds into the implementation details of a component that we're not interested to test here. Whether Tooltip displays its title prop somewhere or not, should be subject to its own tests, no? I'm curious to hear what you think about this.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, I see your point, too. And together with the mock approach, I find it interesting how it's a true "unit test", in a way that I've never seen in React component testing. (I don't have a lot of Enzyme experience actually!) I guess the weakness here is the unreliability of the mock (the way all mocks are). Realistically, I would want my test to fail if the Tooltip API changed or something and Button stopped showing tooltips. But a mocked test wouldn't fail.

IMHO it also bleeds into the implementation details of a component that we're not interested to test here. Whether Tooltip displays its title prop somewhere or not, should be subject to its own tests, no?

It's funny because I would say the mocked test is "testing implementation details" 😆 From the RTL mindset, we want to test that, even in icon button cases where the button doesn't have a visible label, the button is accessible by that label, and a visible label text is in the DOM when the button is focused or hovered. That's it. We don't care whether it even uses the Tooltip component as the implementation. (Not saying that it's inherently unuseful to test that a function calls another function with the correct arguments. It's just not what RTL is designed for. Even in the FAQ that discusses avoiding mocks, the exception given as an example is a mock meant for bypassing, not for testing arguments.)

Would it be more palatable if we added a expect( screen.getByText( 'WordPress' ) ).not().toBeInTheDocument(); assertion before the button.focus()?

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've revisited this and agree with you, so decided to remove the mocks where possible. I've gone that way and now the tests look much better and clearer - especially when we're checking for the tooltip to be hidden first, then after focusing on the button, we're asserting that it's visible.

Copy link
Contributor

Choose a reason for hiding this comment

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

Just going to add my point of view to a super interesting conversation.

I am basically aligned with Lena's point of view here — I usually try to avoid mocking components as much as possible, since the spirit of RTL to feels closer to writing integration tests of the component and its dependencies, rather than an isolated unit test (à la Enzyme's shallow render)

I believe that such tests can be much more valuable and can bring a lot more confidence than a strict unit test. To me, the need for a mock is almost a bit of a "code smell"

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for sharing your point of view. I definitely agree with you all here on most points. I disagree on the "code smell" part though - it's not a code smell IMHO. Rather, we're making a trade-off to test as close as the user experiences the component, in favor of stepping out of the testing responsibility of the current component. You are saying mocks are a code smell, others would say that testing other components is a code smell. I say it's a trade-off, and as long as we're making a conscious decision and we've weighed the trade-offs of it, we're good to go.

Comment on lines 181 to 187
const button = render( <Button describedBy="Description text" /> )
.container.firstChild;

expect( button ).toHaveAttribute( 'aria-describedby' );
expect(
screen.getByTestId( 'test-visually-hidden' )
).toHaveTextContent( 'Description text' );
Copy link
Member

Choose a reason for hiding this comment

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

Something like this would be more robust, and eliminate the need for a VisuallyHidden mock:

Suggested change
const button = render( <Button describedBy="Description text" /> )
.container.firstChild;
expect( button ).toHaveAttribute( 'aria-describedby' );
expect(
screen.getByTestId( 'test-visually-hidden' )
).toHaveTextContent( 'Description text' );
render( <Button describedBy="Description text" /> );
const button = screen.getByRole( 'button', {
description: 'Description text',
} );
expect( button ).toBeInTheDocument();

Copy link
Member Author

Choose a reason for hiding this comment

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

This is great, thank you 👍

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated to use your suggestion here - it's a really great example why mocks are not recommended.

@tyxla
Copy link
Member Author

tyxla commented Aug 9, 2022

@mirka thanks a bunch for your feedback 🙌

I've left a couple of questions on it and wanted to pick your brain and hear your thoughts, before I start addressing the feedback. Let me know what you think 🧠

@mirka
Copy link
Member

mirka commented Aug 9, 2022

I enjoy these discussions very much, thank you 😊

@tyxla
Copy link
Member Author

tyxla commented Aug 10, 2022

I enjoy these discussions very much, thank you 😊

Me too, it's been particularly helpful and I appreciate you taking the time to go deep into that level of detail 😍

@tyxla tyxla requested a review from mirka August 10, 2022 08:18
Comment on lines 103 to +106
it( 'should render with an additional className', () => {
const button = shallow( <Button className="gutenberg" /> ).find(
'button'
);
render( <Button className="gutenberg" /> );

expect( button.hasClass( 'gutenberg' ) ).toBe( true );
expect( screen.getByRole( 'button' ) ).toHaveClass( 'gutenberg' );
Copy link
Member

@WunderBart WunderBart Aug 10, 2022

Choose a reason for hiding this comment

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

I think that it's a good practice to also check for the existence of the base class (.components-button) in such scenario. It happened to me before, where the className prop was implemented in a way that would wipe out any other class when passed. Checking for the base class would make sure that something like this doesn't happen!

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.

Beautiful! It looks a lot more RTL-like now, thank you ✨ I like how you put it: "tradeoffs". I think RTL's opinionated guardrails make us laser focus on testing the HTML foundations reliably, so we're kind of forced to use better-suited tools to test things that fall outside of those guardrails (snapshot, vizreg, etc). I'm hoping we can some day add vizreg infrastructure so we can reliably test styles as well.

We'll also be looking into improving the Tooltip component so it's a bit more semantic and not just a random string in the DOM 🙈

packages/components/src/button/test/index.js Outdated Show resolved Hide resolved
@tyxla tyxla merged commit b369742 into trunk Aug 12, 2022
@tyxla tyxla deleted the refactor/button-tests-rtl branch August 12, 2022 09:19
@github-actions github-actions bot added this to the Gutenberg 14.0 milestone Aug 12, 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] Enhancement A suggestion for improvement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants