]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-16703 [891616] Keyboard focus is lost or misplaced due to user interaction...
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Thu, 28 Jul 2022 08:33:34 +0000 (10:33 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 29 Jul 2022 20:03:15 +0000 (20:03 +0000)
server/sonar-web/src/main/js/components/controls/Favorite.tsx
server/sonar-web/src/main/js/components/controls/FavoriteButton.tsx [deleted file]
server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx
server/sonar-web/src/main/js/components/controls/__tests__/Favorite-test.tsx
server/sonar-web/src/main/js/components/controls/__tests__/FavoriteButton-test.tsx [deleted file]
server/sonar-web/src/main/js/components/controls/__tests__/HomePageSelect-test.tsx
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Favorite-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/FavoriteButton-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/HomePageSelect-test.tsx.snap [deleted file]

index 1c2aae44825cbad3e59779bd8e6fc623096e5917..f64f01365badc4be270e6c8282857c56b38a6ce0 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import classNames from 'classnames';
 import * as React from 'react';
 import { addFavorite, removeFavorite } from '../../api/favorites';
-import FavoriteButton from '../../components/controls/FavoriteButton';
+import { translate } from '../../helpers/l10n';
+import FavoriteIcon from '../icons/FavoriteIcon';
+import { ButtonLink } from './buttons';
+import Tooltip from './Tooltip';
 
 interface Props {
   className?: string;
@@ -35,6 +39,7 @@ interface State {
 
 export default class Favorite extends React.PureComponent<Props, State> {
   mounted = false;
+  buttonNode?: HTMLElement | null;
 
   constructor(props: Props) {
     super(props);
@@ -68,6 +73,9 @@ export default class Favorite extends React.PureComponent<Props, State> {
           if (this.props.handleFavorite) {
             this.props.handleFavorite(this.props.component, newFavorite);
           }
+          if (this.buttonNode) {
+            this.buttonNode.focus();
+          }
         });
       }
     });
@@ -77,14 +85,21 @@ export default class Favorite extends React.PureComponent<Props, State> {
     const { className, qualifier } = this.props;
     const { favorite } = this.state;
 
+    const tooltip = favorite
+      ? translate('favorite.current', qualifier)
+      : translate('favorite.check', qualifier);
+    const ariaLabel = translate('favorite.action', favorite ? 'remove' : 'add');
+
     return (
-      <FavoriteButton
-        className={className}
-        favorite={favorite}
-        qualifier={qualifier}
-        toggleFavorite={this.toggleFavorite}
-      />
+      <Tooltip overlay={tooltip}>
+        <ButtonLink
+          aria-label={ariaLabel}
+          innerRef={node => (this.buttonNode = node)}
+          className={classNames('favorite-link', 'link-no-underline', className)}
+          onClick={this.toggleFavorite}>
+          <FavoriteIcon favorite={favorite} />
+        </ButtonLink>
+      </Tooltip>
     );
   }
 }
-/*  */
diff --git a/server/sonar-web/src/main/js/components/controls/FavoriteButton.tsx b/server/sonar-web/src/main/js/components/controls/FavoriteButton.tsx
deleted file mode 100644 (file)
index b8be848..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.
- */
-import classNames from 'classnames';
-import * as React from 'react';
-import { translate } from '../../helpers/l10n';
-import FavoriteIcon from '../icons/FavoriteIcon';
-import { ButtonLink } from './buttons';
-import Tooltip from './Tooltip';
-
-export interface Props {
-  className?: string;
-  favorite: boolean;
-  qualifier: string;
-  toggleFavorite: () => void;
-}
-
-export default class FavoriteButton extends React.PureComponent<Props> {
-  render() {
-    const { className, favorite, qualifier, toggleFavorite } = this.props;
-    const tooltip = favorite
-      ? translate('favorite.current', qualifier)
-      : translate('favorite.check', qualifier);
-    const ariaLabel = translate('favorite.action', favorite ? 'remove' : 'add');
-
-    return (
-      <Tooltip overlay={tooltip}>
-        <ButtonLink
-          aria-label={ariaLabel}
-          className={classNames('favorite-link', 'link-no-underline', className)}
-          onClick={toggleFavorite}>
-          <FavoriteIcon favorite={favorite} />
-        </ButtonLink>
-      </Tooltip>
-    );
-  }
-}
index 73e3a217c6d0eb9da3aafdeec6c4c1bfa705a9d7..110a5ee0fd327c5b9d1e9b3bc24a615fd2ebd90b 100644 (file)
@@ -38,6 +38,8 @@ interface Props
 export const DEFAULT_HOMEPAGE: HomePage = { type: 'PROJECTS' };
 
 export class HomePageSelect extends React.PureComponent<Props> {
+  buttonNode?: HTMLElement | null;
+
   async setCurrentUserHomepage(homepage: HomePage) {
     const { currentUser } = this.props;
 
@@ -45,6 +47,10 @@ export class HomePageSelect extends React.PureComponent<Props> {
       await setHomePage(homepage);
 
       this.props.updateCurrentUserHomepage(homepage);
+
+      if (this.buttonNode) {
+        this.buttonNode.focus();
+      }
     }
   }
 
@@ -82,7 +88,8 @@ export class HomePageSelect extends React.PureComponent<Props> {
           <ButtonLink
             aria-label={tooltip}
             className={classNames('link-no-underline', 'set-homepage-link', this.props.className)}
-            onClick={isChecked ? this.handleReset : this.handleClick}>
+            onClick={isChecked ? this.handleReset : this.handleClick}
+            innerRef={node => (this.buttonNode = node)}>
             <HomeIcon filled={isChecked} />
           </ButtonLink>
         )}
index 4ff3a1dd9f24fc860ac3acfecb24860ae6f1af69..cfb77afaab367cd0fa8da95a33ef5cbf4a822cfc 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { shallow } from 'enzyme';
+import { screen } from '@testing-library/react';
 import * as React from 'react';
-import FavoriteButton from '../../../components/controls/FavoriteButton';
+import { addFavorite, removeFavorite } from '../../../api/favorites';
+import { renderComponent } from '../../../helpers/testReactTestingUtils';
+import { ComponentQualifier } from '../../../types/component';
 import Favorite from '../Favorite';
 
 jest.mock('../../../api/favorites', () => ({
-  addFavorite: jest.fn(() => Promise.resolve()),
-  removeFavorite: jest.fn(() => Promise.resolve())
+  addFavorite: jest.fn().mockResolvedValue(null),
+  removeFavorite: jest.fn().mockResolvedValue(null)
 }));
 
-it('renders', () => {
-  expect(shallowRender()).toMatchSnapshot();
+it('renders and behaves correctly', async () => {
+  renderFavorite({ favorite: false });
+  let button = screen.getByRole('button');
+  expect(button).toBeInTheDocument();
+
+  button.click();
+  await new Promise(setImmediate);
+  expect(addFavorite).toHaveBeenCalled();
+
+  button = screen.getByRole('button');
+  expect(button).toBeInTheDocument();
+  expect(button).toHaveFocus();
+
+  button.click();
+  await new Promise(setImmediate);
+  expect(removeFavorite).toHaveBeenCalled();
+
+  button = screen.getByRole('button');
+  expect(button).toBeInTheDocument();
+  expect(button).toHaveFocus();
 });
 
-it('calls handleFavorite when given', async () => {
+it('correctly calls handleFavorite if passed', async () => {
   const handleFavorite = jest.fn();
-  const wrapper = shallowRender(handleFavorite);
-  const favoriteBase = wrapper.find(FavoriteButton);
-  const toggleFavorite = favoriteBase.prop<Function>('toggleFavorite');
+  renderFavorite({ handleFavorite });
 
-  toggleFavorite();
+  screen.getByRole('button').click();
   await new Promise(setImmediate);
   expect(handleFavorite).toHaveBeenCalledWith('foo', false);
 
-  toggleFavorite();
+  screen.getByRole('button').click();
   await new Promise(setImmediate);
   expect(handleFavorite).toHaveBeenCalledWith('foo', true);
 });
 
-function shallowRender(handleFavorite?: () => void) {
-  return shallow(
-    <Favorite component="foo" favorite={true} handleFavorite={handleFavorite} qualifier="TRK" />
+function renderFavorite(props: Partial<Favorite['props']> = {}) {
+  return renderComponent(
+    <Favorite component="foo" favorite={true} qualifier={ComponentQualifier.Project} {...props} />
   );
 }
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/FavoriteButton-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/FavoriteButton-test.tsx
deleted file mode 100644 (file)
index 94eaa2a..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.
- */
-import { shallow } from 'enzyme';
-import * as React from 'react';
-import { click } from '../../../helpers/testUtils';
-import FavoriteButton, { Props } from '../FavoriteButton';
-
-it('should render favorite', () => {
-  const favorite = renderFavoriteBase({ favorite: true });
-  expect(favorite).toMatchSnapshot();
-});
-
-it('should render not favorite', () => {
-  const favorite = renderFavoriteBase({ favorite: false });
-  expect(favorite).toMatchSnapshot();
-});
-
-it('should update properly', () => {
-  const favorite = renderFavoriteBase({ favorite: false });
-  expect(favorite).toMatchSnapshot();
-
-  favorite.setProps({ favorite: true });
-  expect(favorite).toMatchSnapshot();
-});
-
-it('should toggle favorite', () => {
-  const toggleFavorite = jest.fn();
-  const favorite = renderFavoriteBase({ toggleFavorite });
-  click(favorite.find('ButtonLink'));
-  expect(toggleFavorite).toBeCalled();
-});
-
-function renderFavoriteBase(props: Partial<Props> = {}) {
-  return shallow(
-    <FavoriteButton favorite={true} qualifier="TRK" toggleFavorite={jest.fn()} {...props} />
-  );
-}
index c43d6d175067a90b588ad69888dd7ea785b6dea8..2dca15c10d958a299b6f56d5ae8dd88e26b74e96 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { shallow } from 'enzyme';
+import { screen } from '@testing-library/react';
 import * as React from 'react';
-import { ButtonLink } from '../../../components/controls/buttons';
-import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks';
-import { click, waitAndUpdate } from '../../../helpers/testUtils';
-import { HomePage } from '../../../types/users';
+import { setHomePage } from '../../../api/users';
+import { mockLoggedInUser } from '../../../helpers/testMocks';
+import { renderComponent } from '../../../helpers/testReactTestingUtils';
 import { DEFAULT_HOMEPAGE, HomePageSelect } from '../HomePageSelect';
 
 jest.mock('../../../api/users', () => ({
   setHomePage: jest.fn().mockResolvedValue(null)
 }));
 
-it('should render correctly', () => {
-  expect(shallowRender()).toMatchSnapshot('unchecked');
-  expect(
-    shallowRender({ currentUser: mockLoggedInUser({ homepage: { type: 'MY_PROJECTS' } }) })
-  ).toMatchSnapshot('checked');
-  expect(
-    shallowRender({
-      currentUser: mockLoggedInUser({ homepage: DEFAULT_HOMEPAGE }),
-      currentPage: DEFAULT_HOMEPAGE
-    })
-  ).toMatchSnapshot('checked, and on default');
-  expect(shallowRender({ currentUser: mockCurrentUser() }).type()).toBeNull();
-});
-
-it('should correctly call webservices', async () => {
+it('renders and behaves correctly', async () => {
   const updateCurrentUserHomepage = jest.fn();
-  const currentPage: HomePage = { type: 'MY_ISSUES' };
-  const wrapper = shallowRender({ updateCurrentUserHomepage, currentPage });
+  renderHomePageSelect({ updateCurrentUserHomepage });
+  const button = screen.getByRole('button');
+  expect(button).toBeInTheDocument();
+
+  button.click();
+  await new Promise(setImmediate);
+  expect(setHomePage).toHaveBeenCalledWith({ type: 'MY_PROJECTS' });
+  expect(updateCurrentUserHomepage).toHaveBeenCalled();
+  expect(button).toHaveFocus();
+});
 
-  // Set homepage.
-  click(wrapper.find(ButtonLink));
-  await waitAndUpdate(wrapper);
-  expect(updateCurrentUserHomepage).toHaveBeenLastCalledWith(currentPage);
+it('renders correctly if user is on the homepage', async () => {
+  renderHomePageSelect({ currentUser: mockLoggedInUser({ homepage: { type: 'MY_PROJECTS' } }) });
+  const button = screen.getByRole('button');
+  expect(button).toBeInTheDocument();
 
-  // Reset.
-  wrapper.setProps({ currentUser: mockLoggedInUser({ homepage: currentPage }) });
-  click(wrapper.find(ButtonLink));
-  await waitAndUpdate(wrapper);
-  expect(updateCurrentUserHomepage).toHaveBeenLastCalledWith(DEFAULT_HOMEPAGE);
+  button.click();
+  await new Promise(setImmediate);
+  expect(setHomePage).toHaveBeenCalledWith(DEFAULT_HOMEPAGE);
+  expect(button).toHaveFocus();
 });
 
-function shallowRender(props: Partial<HomePageSelect['props']> = {}) {
-  return shallow<HomePageSelect>(
+function renderHomePageSelect(props: Partial<HomePageSelect['props']> = {}) {
+  return renderComponent(
     <HomePageSelect
       currentPage={{ type: 'MY_PROJECTS' }}
       currentUser={mockLoggedInUser()}
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Favorite-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Favorite-test.tsx.snap
deleted file mode 100644 (file)
index d3419ee..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<FavoriteButton
-  favorite={true}
-  qualifier="TRK"
-  toggleFavorite={[Function]}
-/>
-`;
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/FavoriteButton-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/FavoriteButton-test.tsx.snap
deleted file mode 100644 (file)
index f66e2dd..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render favorite 1`] = `
-<Tooltip
-  overlay="favorite.current.TRK"
->
-  <ButtonLink
-    aria-label="favorite.action.remove"
-    className="favorite-link link-no-underline"
-    onClick={[MockFunction]}
-  >
-    <FavoriteIcon
-      favorite={true}
-    />
-  </ButtonLink>
-</Tooltip>
-`;
-
-exports[`should render not favorite 1`] = `
-<Tooltip
-  overlay="favorite.check.TRK"
->
-  <ButtonLink
-    aria-label="favorite.action.add"
-    className="favorite-link link-no-underline"
-    onClick={[MockFunction]}
-  >
-    <FavoriteIcon
-      favorite={false}
-    />
-  </ButtonLink>
-</Tooltip>
-`;
-
-exports[`should update properly 1`] = `
-<Tooltip
-  overlay="favorite.check.TRK"
->
-  <ButtonLink
-    aria-label="favorite.action.add"
-    className="favorite-link link-no-underline"
-    onClick={[MockFunction]}
-  >
-    <FavoriteIcon
-      favorite={false}
-    />
-  </ButtonLink>
-</Tooltip>
-`;
-
-exports[`should update properly 2`] = `
-<Tooltip
-  overlay="favorite.current.TRK"
->
-  <ButtonLink
-    aria-label="favorite.action.remove"
-    className="favorite-link link-no-underline"
-    onClick={[MockFunction]}
-  >
-    <FavoriteIcon
-      favorite={true}
-    />
-  </ButtonLink>
-</Tooltip>
-`;
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/HomePageSelect-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/HomePageSelect-test.tsx.snap
deleted file mode 100644 (file)
index f05b269..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly: checked 1`] = `
-<Tooltip
-  overlay="homepage.current"
->
-  <ButtonLink
-    aria-label="homepage.current"
-    className="link-no-underline set-homepage-link"
-    onClick={[Function]}
-  >
-    <HomeIcon
-      filled={true}
-    />
-  </ButtonLink>
-</Tooltip>
-`;
-
-exports[`should render correctly: checked, and on default 1`] = `
-<Tooltip
-  overlay="homepage.current.is_default"
->
-  <span
-    aria-label="homepage.current.is_default"
-    className="display-inline-block"
-  >
-    <HomeIcon
-      filled={true}
-    />
-  </span>
-</Tooltip>
-`;
-
-exports[`should render correctly: unchecked 1`] = `
-<Tooltip
-  overlay="homepage.check"
->
-  <ButtonLink
-    aria-label="homepage.check"
-    className="link-no-underline set-homepage-link"
-    onClick={[Function]}
-  >
-    <HomeIcon
-      filled={false}
-    />
-  </ButtonLink>
-</Tooltip>
-`;