From 02e0ffdddc3fbf28ecd789c68e73bb2043987fef Mon Sep 17 00:00:00 2001 From: Wouter Admiraal Date: Thu, 29 Jun 2023 12:43:02 +0200 Subject: [PATCH] [NO JIRA] Improve testability of our tooltips --- server/sonar-web/.eslintrc | 3 +- .../config/jest/SetupReactTestingLibrary.ts | 32 ++++++++++++++- ...t-expect-tohaveatooltipwithcontent-test.js | 39 +++++++++++++++++++ server/sonar-web/eslint-local-rules/index.js | 1 + ...-await-expect-tohaveatooltipwithcontent.js | 39 +++++++++++++++++++ .../js/apps/issues/__tests__/IssuesApp-it.tsx | 8 ++-- .../main/js/components/controls/Tooltip.tsx | 2 +- .../components/facet/__tests__/Facet-it.tsx | 9 ++--- .../src/main/js/components/icons/Icon.tsx | 6 ++- .../__snapshots__/Icon-test.tsx.snap | 2 +- .../components/issue/__tests__/Issue-it.tsx | 15 ++++--- .../__snapshots__/Alert-test.tsx.snap | 1 + .../main/js/helpers/testReactTestingUtils.tsx | 7 ++++ server/sonar-web/src/main/js/types/jest.d.ts | 25 ++++++++++++ 14 files changed, 167 insertions(+), 22 deletions(-) create mode 100644 server/sonar-web/eslint-local-rules/__tests__/use-await-expect-tohaveatooltipwithcontent-test.js create mode 100644 server/sonar-web/eslint-local-rules/use-await-expect-tohaveatooltipwithcontent.js create mode 100644 server/sonar-web/src/main/js/types/jest.d.ts diff --git a/server/sonar-web/.eslintrc b/server/sonar-web/.eslintrc index d92184566d1..eb83d7a39b0 100644 --- a/server/sonar-web/.eslintrc +++ b/server/sonar-web/.eslintrc @@ -15,6 +15,7 @@ "local-rules/use-visibility-enum": "warn", "local-rules/convert-class-to-function-component": "warn", "local-rules/no-conditional-rendering-of-deferredspinner": "warn", - "local-rules/use-jest-mocked": "warn" + "local-rules/use-jest-mocked": "warn", + "local-rules/use-await-expect-tohaveatooltipwithcontent": "warn" } } diff --git a/server/sonar-web/config/jest/SetupReactTestingLibrary.ts b/server/sonar-web/config/jest/SetupReactTestingLibrary.ts index afaa0a4fcfb..c7fcfac0aee 100644 --- a/server/sonar-web/config/jest/SetupReactTestingLibrary.ts +++ b/server/sonar-web/config/jest/SetupReactTestingLibrary.ts @@ -18,8 +18,38 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import '@testing-library/jest-dom'; -import { configure } from '@testing-library/react'; +import { configure, fireEvent, screen } from '@testing-library/react'; configure({ asyncUtilTimeout: 3000, }); + +// Don't forget to update src/main/js/types/jest.d.ts when registering custom matchers. +expect.extend({ + async toHaveATooltipWithContent(received: any, content: string) { + if (!(received instanceof Element)) { + return { + pass: false, + message: () => `Received object is not an HTMLElement, and cannot have a tooltip`, + }; + } + + fireEvent.pointerEnter(received); + const tooltip = await screen.findByRole('tooltip'); + + const result = tooltip.textContent?.includes(content) + ? { + pass: true, + message: () => `Tooltip content "${tooltip.textContent}" contains expected "${content}"`, + } + : { + pass: false, + message: () => + `Tooltip content "${tooltip.textContent}" does not contain expected "${content}"`, + }; + + fireEvent.pointerLeave(received); + + return result; + }, +}); diff --git a/server/sonar-web/eslint-local-rules/__tests__/use-await-expect-tohaveatooltipwithcontent-test.js b/server/sonar-web/eslint-local-rules/__tests__/use-await-expect-tohaveatooltipwithcontent-test.js new file mode 100644 index 00000000000..5c738aae598 --- /dev/null +++ b/server/sonar-web/eslint-local-rules/__tests__/use-await-expect-tohaveatooltipwithcontent-test.js @@ -0,0 +1,39 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +const { RuleTester } = require('eslint'); +const useJestMocked = require('../use-await-expect-tohaveatooltipwithcontent'); + +const ruleTester = new RuleTester({ + parser: require.resolve('@typescript-eslint/parser'), +}); + +ruleTester.run('use-await-expect-tohaveatooltipwithcontent', useJestMocked, { + valid: [ + { + code: `await expect(node).toHaveATooltipWithContent("Help text");`, + }, + ], + invalid: [ + { + code: `expect(node).toHaveATooltipWithContent("Help text");`, + errors: [{ messageId: 'useAwaitExpectToHaveATooltipWithContent' }], + }, + ], +}); diff --git a/server/sonar-web/eslint-local-rules/index.js b/server/sonar-web/eslint-local-rules/index.js index 8f7f5ccf70b..930d7318243 100644 --- a/server/sonar-web/eslint-local-rules/index.js +++ b/server/sonar-web/eslint-local-rules/index.js @@ -25,4 +25,5 @@ module.exports = { 'use-componentqualifier-enum': require('./use-componentqualifier-enum'), 'use-metrickey-enum': require('./use-metrickey-enum'), 'use-metrictype-enum': require('./use-metrictype-enum'), + 'use-await-expect-tohaveatooltipwithcontent': require('./use-await-expect-tohaveatooltipwithcontent'), }; diff --git a/server/sonar-web/eslint-local-rules/use-await-expect-tohaveatooltipwithcontent.js b/server/sonar-web/eslint-local-rules/use-await-expect-tohaveatooltipwithcontent.js new file mode 100644 index 00000000000..05c2650e144 --- /dev/null +++ b/server/sonar-web/eslint-local-rules/use-await-expect-tohaveatooltipwithcontent.js @@ -0,0 +1,39 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +module.exports = { + meta: { + messages: { + useAwaitExpectToHaveATooltipWithContent: + 'expect.toHaveATooltipWithContent() is asynchronous; you must prefix expect() with await', + }, + }, + create(context) { + return { + Identifier(node) { + if ( + node.name === 'toHaveATooltipWithContent' && + node.parent?.parent?.parent?.type !== 'AwaitExpression' + ) { + context.report({ node, messageId: 'useAwaitExpectToHaveATooltipWithContent' }); + } + }, + }; + }, +}; diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx index f2cf204eb5e..7d726e9492d 100644 --- a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx @@ -24,7 +24,6 @@ import selectEvent from 'react-select-event'; import { TabKeys } from '../../../components/rules/RuleTabViewer'; import { renderOwaspTop102021Category } from '../../../helpers/security-standard'; import { mockLoggedInUser, mockRawIssue } from '../../../helpers/testMocks'; -import { findTooltipWithContent } from '../../../helpers/testReactTestingUtils'; import { ComponentQualifier } from '../../../types/component'; import { IssueType } from '../../../types/issues'; import { @@ -858,9 +857,10 @@ describe('issues item', () => { // Select an issue with an advanced rule await user.click(await ui.issueItemAction7.find()); - expect( - findTooltipWithContent('issue.quick_fix_available_with_sonarlint_no_link') - ).toBeInTheDocument(); + await expect( + screen.getByText('issue.quick_fix_available_with_sonarlint_no_link') + ).toHaveATooltipWithContent('issue.quick_fix_available_with_sonarlint'); + expect( screen.getByRole('status', { name: 'issue.resolution.badge.DEPRECATED', diff --git a/server/sonar-web/src/main/js/components/controls/Tooltip.tsx b/server/sonar-web/src/main/js/components/controls/Tooltip.tsx index eb1436e837b..0cf6d915091 100644 --- a/server/sonar-web/src/main/js/components/controls/Tooltip.tsx +++ b/server/sonar-web/src/main/js/components/controls/Tooltip.tsx @@ -403,7 +403,7 @@ export class TooltipInner extends React.Component { })} id={this.id} role="tooltip" - aria-hidden={!isInteractive || !isVisible} + aria-hidden={!isVisible} > {isInteractive && ( {translate('tooltip_is_interactive')} diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/Facet-it.tsx b/server/sonar-web/src/main/js/components/facet/__tests__/Facet-it.tsx index d24f460a292..5e7382fa9ab 100644 --- a/server/sonar-web/src/main/js/components/facet/__tests__/Facet-it.tsx +++ b/server/sonar-web/src/main/js/components/facet/__tests__/Facet-it.tsx @@ -18,9 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; import * as React from 'react'; -import { findTooltipWithContent, renderComponent } from '../../../helpers/testReactTestingUtils'; +import { renderComponent } from '../../../helpers/testReactTestingUtils'; import FacetBox, { FacetBoxProps } from '../FacetBox'; import FacetHeader from '../FacetHeader'; import FacetItem from '../FacetItem'; @@ -58,9 +57,9 @@ it('should render and function correctly', () => { it('should correctly render a header with helper text', async () => { renderFacet(undefined, { helper: 'Help text' }); - await userEvent.tab(); - await userEvent.tab(); - expect(findTooltipWithContent('Help text')).toBeInTheDocument(); + await expect(screen.getByRole('img', { description: 'Help text' })).toHaveATooltipWithContent( + 'Help text' + ); }); it('should correctly render a header with value data', () => { diff --git a/server/sonar-web/src/main/js/components/icons/Icon.tsx b/server/sonar-web/src/main/js/components/icons/Icon.tsx index 54faa9dccf9..5e02dc790fe 100644 --- a/server/sonar-web/src/main/js/components/icons/Icon.tsx +++ b/server/sonar-web/src/main/js/components/icons/Icon.tsx @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { uniqueId } from 'lodash'; import * as React from 'react'; export interface IconProps extends React.AriaAttributes { @@ -54,6 +55,7 @@ export default function Icon({ 'aria-hidden': hidden, ...iconProps }: Props) { + const id = uniqueId('icon'); return ( {label && !hidden && {label}} - {description && !hidden && {description}} + {description && !hidden && {description}} {children} ); diff --git a/server/sonar-web/src/main/js/components/icons/__tests__/__snapshots__/Icon-test.tsx.snap b/server/sonar-web/src/main/js/components/icons/__tests__/__snapshots__/Icon-test.tsx.snap index 8ce95580571..bb617d3683c 100644 --- a/server/sonar-web/src/main/js/components/icons/__tests__/__snapshots__/Icon-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/icons/__tests__/__snapshots__/Icon-test.tsx.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should render correctly 1`] = `"
"`; +exports[`should render correctly 1`] = `"
"`; diff --git a/server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx b/server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx index 5a7e5135acf..e8bac03ed08 100644 --- a/server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx +++ b/server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx @@ -26,7 +26,7 @@ import { Route } from 'react-router-dom'; import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock'; import { KeyboardKeys } from '../../../helpers/keycodes'; import { mockIssue, mockLoggedInUser, mockRawIssue } from '../../../helpers/testMocks'; -import { findTooltipWithContent, renderAppRoutes } from '../../../helpers/testReactTestingUtils'; +import { renderAppRoutes } from '../../../helpers/testReactTestingUtils'; import { byLabelText, byRole, byText } from '../../../helpers/testSelector'; import { IssueActions, @@ -79,11 +79,11 @@ describe('rendering', () => { expect(screen.getByRole('status', { name: 'ESLINT' })).toBeInTheDocument(); }); - it('should render the SonarLint icon correctly', () => { + it('should render the SonarLint icon correctly', async () => { renderIssue({ issue: mockIssue(false, { quickFixAvailable: true }) }); - expect( - findTooltipWithContent('issue.quick_fix_available_with_sonarlint_no_link') - ).toBeInTheDocument(); + await expect( + screen.getByText('issue.quick_fix_available_with_sonarlint_no_link') + ).toHaveATooltipWithContent('issue.quick_fix_available_with_sonarlint'); }); it('should render correctly with a checkbox', async () => { @@ -95,11 +95,10 @@ describe('rendering', () => { expect(onCheck).toHaveBeenCalledWith(issue.key); }); - it('should correctly render any code variants', () => { + it('should correctly render any code variants', async () => { const { ui } = getPageObject(); renderIssue({ issue: mockIssue(false, { codeVariants: ['variant 1', 'variant 2'] }) }); - expect(ui.variants(2).get()).toBeInTheDocument(); - expect(findTooltipWithContent('variant 1, variant 2', undefined, 'div')).toBeInTheDocument(); + await expect(ui.variants(2).get()).toHaveATooltipWithContent('variant 1, variant 2'); }); }); diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Alert-test.tsx.snap b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Alert-test.tsx.snap index ed55cd5b070..3f1c5a78494 100644 --- a/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Alert-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Alert-test.tsx.snap @@ -86,6 +86,7 @@ exports[`should render banner alert with correct css 1`] = ` > { + toHaveATooltipWithContent(content: string): Promise; + } +} -- 2.39.5