]> source.dussan.org Git - sonarqube.git/commitdiff
[NO JIRA] Improve testability of our tooltips
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Thu, 29 Jun 2023 10:43:02 +0000 (12:43 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 29 Jun 2023 20:05:13 +0000 (20:05 +0000)
14 files changed:
server/sonar-web/.eslintrc
server/sonar-web/config/jest/SetupReactTestingLibrary.ts
server/sonar-web/eslint-local-rules/__tests__/use-await-expect-tohaveatooltipwithcontent-test.js [new file with mode: 0644]
server/sonar-web/eslint-local-rules/index.js
server/sonar-web/eslint-local-rules/use-await-expect-tohaveatooltipwithcontent.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx
server/sonar-web/src/main/js/components/controls/Tooltip.tsx
server/sonar-web/src/main/js/components/facet/__tests__/Facet-it.tsx
server/sonar-web/src/main/js/components/icons/Icon.tsx
server/sonar-web/src/main/js/components/icons/__tests__/__snapshots__/Icon-test.tsx.snap
server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx
server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Alert-test.tsx.snap
server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx
server/sonar-web/src/main/js/types/jest.d.ts [new file with mode: 0644]

index d92184566d1047e93d51fa009c811ba18cb0ca62..eb83d7a39b081a143504e0021ef8ec17d1b66d59 100644 (file)
@@ -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"
   }
 }
index afaa0a4fcfb6d0ca85e4e5312fad5d178f78ed56..c7fcfac0aee34dee5e31900548c57582d24106f1 100644 (file)
  * 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 (file)
index 0000000..5c738aa
--- /dev/null
@@ -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' }],
+    },
+  ],
+});
index 8f7f5ccf70b7367edb365bc35cf9301bb77ab0dd..930d73182433b30b51fff79220272ebdac10bbaf 100644 (file)
@@ -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 (file)
index 0000000..05c2650
--- /dev/null
@@ -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' });
+        }
+      },
+    };
+  },
+};
index f2cf204eb5e24b43cb795208d480247b07ffc3b7..7d726e9492dd1a71013e5ebe8db1ff3d3386e919 100644 (file)
@@ -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',
index eb1436e837bf7f240ff0ed6f59acc5fd4f437cd3..0cf6d91509149af224f96dedbabd51d65e695e50 100644 (file)
@@ -403,7 +403,7 @@ export class TooltipInner extends React.Component<TooltipProps, State> {
         })}
         id={this.id}
         role="tooltip"
-        aria-hidden={!isInteractive || !isVisible}
+        aria-hidden={!isVisible}
       >
         {isInteractive && (
           <span className="a11y-hidden">{translate('tooltip_is_interactive')}</span>
index d24f460a292efc5de0cbf2c67a8e7043b2c071e3..5e7382fa9aba20d54dcba40714972c0e1d1aa82d 100644 (file)
@@ -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', () => {
index 54faa9dccf9e958b03c65edbcd2d24dcd4a333fc..5e02dc790fe3fde74d949aa370e04535ef429964 100644 (file)
@@ -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 (
     <svg
       className={className}
@@ -70,10 +72,12 @@ export default function Icon({
       width={width}
       xmlnsXlink="http://www.w3.org/1999/xlink"
       xmlSpace="preserve"
+      role="img"
+      aria-describedby={description && !hidden ? id : undefined}
       {...iconProps}
     >
       {label && !hidden && <title>{label}</title>}
-      {description && !hidden && <desc>{description}</desc>}
+      {description && !hidden && <desc id={id}>{description}</desc>}
       {children}
     </svg>
   );
index 8ce955805714caf264934b21623479dd7acd601b..bb617d3683c1f6bbf7d6c6e7f5569cf1bff76497 100644 (file)
@@ -1,3 +1,3 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`should render correctly 1`] = `"<div><svg height="16" style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 1.41421;" version="1.1" viewBox="0 0 16 16" width="16" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"><path d="test-path"></path></svg></div>"`;
+exports[`should render correctly 1`] = `"<div><svg height="16" style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 1.41421;" version="1.1" viewBox="0 0 16 16" width="16" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" role="img"><path d="test-path"></path></svg></div>"`;
index 5a7e5135acf0ccf7ee3c633ef1149b12c043a7a2..e8bac03ed08263441dc4ad1383eef6adf0de582e 100644 (file)
@@ -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');
   });
 });
 
index ed55cd5b070345631f3e5cd1d9ad48ea45a3f1ca..3f1c5a7849485228ff3ebd1e6dc8ff32fe3b45d6 100644 (file)
@@ -86,6 +86,7 @@ exports[`should render banner alert with correct css 1`] = `
     >
       <svg
         height="16"
+        role="img"
         space="preserve"
         style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421"
         version="1.1"
index e5ced658c5ea7ccf345c2fa590a0e5c190be0acc..2ef2063a9ea416db4b4cb27aafe0ea89b43b2712 100644 (file)
@@ -262,11 +262,18 @@ export function dateInputEvent(user: UserEvent) {
 }
 /* eslint-enable testing-library/no-node-access */
 
+/**
+ * @deprecated Use our custom toHaveATooltipWithContent() matcher instead.
+ */
 export function findTooltipWithContent(
   text: Matcher,
   target?: HTMLElement,
   selector = 'svg > desc'
 ) {
+  // eslint-disable-next-line no-console
+  console.warn(`The usage of findTooltipWithContent() is deprecated; use expect.toHaveATooltipWithContent() instead.
+Example:
+  await expect(node).toHaveATooltipWithContent('foo.bar');`);
   return target
     ? within(target).getByText(text, { selector })
     : screen.getByText(text, { selector });
diff --git a/server/sonar-web/src/main/js/types/jest.d.ts b/server/sonar-web/src/main/js/types/jest.d.ts
new file mode 100644 (file)
index 0000000..6fcf255
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+declare namespace jest {
+  interface Matchers<R> {
+    toHaveATooltipWithContent(content: string): Promise<CustomMatcherResult>;
+  }
+}