]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13736 Keep search query when browsing code file
authorMathieu Suen <mathieu.suen@sonarsource.com>
Thu, 26 Aug 2021 15:12:12 +0000 (17:12 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 30 Aug 2021 20:08:20 +0000 (20:08 +0000)
server/sonar-web/src/main/js/apps/code/components/AppCode.tsx [deleted file]
server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/code/components/Search.tsx
server/sonar-web/src/main/js/apps/code/components/__tests__/AppCode-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/code/components/__tests__/CodeApp-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/code/components/__tests__/Search-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/AppCode-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/CodeApp-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Search-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/code/routes.ts
server/sonar-web/src/main/js/helpers/mocks/component.ts

diff --git a/server/sonar-web/src/main/js/apps/code/components/AppCode.tsx b/server/sonar-web/src/main/js/apps/code/components/AppCode.tsx
deleted file mode 100644 (file)
index cd2924e..0000000
+++ /dev/null
@@ -1,377 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 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 * as classNames from 'classnames';
-import { Location } from 'history';
-import { debounce } from 'lodash';
-import * as React from 'react';
-import { Helmet } from 'react-helmet-async';
-import { connect } from 'react-redux';
-import { InjectedRouter } from 'react-router';
-import A11ySkipTarget from '../../../app/components/a11y/A11ySkipTarget';
-import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
-import ListFooter from '../../../components/controls/ListFooter';
-import { isPullRequest, isSameBranchLike } from '../../../helpers/branch-like';
-import { translate } from '../../../helpers/l10n';
-import { getCodeUrl, getProjectUrl } from '../../../helpers/urls';
-import { fetchBranchStatus, fetchMetrics } from '../../../store/rootActions';
-import { getMetrics } from '../../../store/rootReducer';
-import { BranchLike } from '../../../types/branch-like';
-import { addComponent, addComponentBreadcrumbs, clearBucket } from '../bucket';
-import '../code.css';
-import { loadMoreChildren, retrieveComponent, retrieveComponentChildren } from '../utils';
-import Breadcrumbs from './Breadcrumbs';
-import Components from './Components';
-import Search from './Search';
-import SourceViewerWrapper from './SourceViewerWrapper';
-
-interface StateToProps {
-  metrics: T.Dict<T.Metric>;
-}
-
-interface DispatchToProps {
-  fetchBranchStatus: (branchLike: BranchLike, projectKey: string) => Promise<void>;
-  fetchMetrics: () => void;
-}
-
-interface OwnProps {
-  branchLike?: BranchLike;
-  component: T.Component;
-  location: Pick<Location, 'query'>;
-  router: Pick<InjectedRouter, 'push'>;
-}
-
-type Props = StateToProps & DispatchToProps & OwnProps;
-
-interface State {
-  baseComponent?: T.ComponentMeasure;
-  breadcrumbs: T.Breadcrumb[];
-  components?: T.ComponentMeasure[];
-  highlighted?: T.ComponentMeasure;
-  loading: boolean;
-  page: number;
-  searchResults?: T.ComponentMeasure[];
-  sourceViewer?: T.ComponentMeasure;
-  total: number;
-}
-
-export class AppCode extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State;
-
-  constructor(props: Props) {
-    super(props);
-    this.state = {
-      breadcrumbs: [],
-      loading: true,
-      page: 0,
-      total: 0
-    };
-    this.refreshBranchStatus = debounce(this.refreshBranchStatus, 1000);
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-    this.props.fetchMetrics();
-    this.handleComponentChange();
-  }
-
-  componentDidUpdate(prevProps: Props) {
-    if (
-      prevProps.component !== this.props.component ||
-      !isSameBranchLike(prevProps.branchLike, this.props.branchLike)
-    ) {
-      this.handleComponentChange();
-    } else if (prevProps.location !== this.props.location) {
-      this.handleUpdate();
-    }
-  }
-
-  componentWillUnmount() {
-    clearBucket();
-    this.mounted = false;
-  }
-
-  loadComponent = (componentKey: string) => {
-    this.setState({ loading: true });
-    retrieveComponent(
-      componentKey,
-      this.props.component.qualifier,
-      this,
-      this.props.branchLike
-    ).then(r => {
-      if (this.mounted) {
-        if (['FIL', 'UTS'].includes(r.component.qualifier)) {
-          this.setState({
-            breadcrumbs: r.breadcrumbs,
-            components: r.components,
-            loading: false,
-            page: 0,
-            searchResults: undefined,
-            sourceViewer: r.component,
-            total: 0
-          });
-        } else {
-          this.setState({
-            baseComponent: r.component,
-            breadcrumbs: r.breadcrumbs,
-            components: r.components,
-            loading: false,
-            page: r.page,
-            searchResults: undefined,
-            sourceViewer: undefined,
-            total: r.total
-          });
-        }
-      }
-    }, this.stopLoading);
-  };
-
-  stopLoading = () => {
-    if (this.mounted) {
-      this.setState({ loading: false });
-    }
-  };
-
-  handleComponentChange = () => {
-    const { branchLike, component } = this.props;
-
-    // we already know component's breadcrumbs,
-    addComponentBreadcrumbs(component.key, component.breadcrumbs);
-
-    this.setState({ loading: true });
-    retrieveComponentChildren(component.key, component.qualifier, this, branchLike).then(() => {
-      addComponent(component);
-      if (this.mounted) {
-        this.handleUpdate();
-      }
-    }, this.stopLoading);
-  };
-
-  handleLoadMore = () => {
-    const { baseComponent, components, page } = this.state;
-    if (!baseComponent || !components) {
-      return;
-    }
-    loadMoreChildren(
-      baseComponent.key,
-      page + 1,
-      this.props.component.qualifier,
-      this,
-      this.props.branchLike
-    ).then(r => {
-      if (this.mounted && r.components.length) {
-        this.setState({
-          components: [...components, ...r.components],
-          page: r.page,
-          total: r.total
-        });
-      }
-    }, this.stopLoading);
-  };
-
-  handleGoToParent = () => {
-    const { branchLike, component } = this.props;
-    const { breadcrumbs = [] } = this.state;
-
-    if (breadcrumbs.length > 1) {
-      const parentComponent = breadcrumbs[breadcrumbs.length - 2];
-      this.props.router.push(getCodeUrl(component.key, branchLike, parentComponent.key));
-      this.setState({ highlighted: breadcrumbs[breadcrumbs.length - 1] });
-    }
-  };
-
-  handleHighlight = (highlighted: T.ComponentMeasure) => {
-    this.setState({ highlighted });
-  };
-
-  handleIssueChange = (_: T.Issue) => {
-    this.refreshBranchStatus();
-  };
-
-  handleSearchClear = () => {
-    this.setState({ searchResults: undefined });
-  };
-
-  handleSearchResults = (searchResults: T.ComponentMeasure[] = []) => {
-    this.setState({ searchResults });
-  };
-
-  handleSelect = (component: T.ComponentMeasure) => {
-    const { branchLike, component: rootComponent } = this.props;
-
-    if (component.refKey) {
-      this.props.router.push(getProjectUrl(component.refKey));
-    } else {
-      this.props.router.push(getCodeUrl(rootComponent.key, branchLike, component.key));
-    }
-
-    this.setState({ highlighted: undefined });
-  };
-
-  handleUpdate = () => {
-    const { component, location } = this.props;
-    const { selected } = location.query;
-    const finalKey = selected || component.key;
-
-    this.loadComponent(finalKey);
-  };
-
-  refreshBranchStatus = () => {
-    const { branchLike, component } = this.props;
-    if (branchLike && component && isPullRequest(branchLike)) {
-      this.props.fetchBranchStatus(branchLike, component.key);
-    }
-  };
-
-  render() {
-    const { branchLike, component, location } = this.props;
-    const {
-      baseComponent,
-      breadcrumbs,
-      components = [],
-      highlighted,
-      loading,
-      total,
-      searchResults,
-      sourceViewer
-    } = this.state;
-
-    const showSearch = searchResults !== undefined;
-
-    const hasNoFile = components.length === 0 && searchResults === undefined;
-
-    const shouldShowBreadcrumbs = breadcrumbs.length > 1 && !showSearch;
-    const shouldShowComponentList =
-      sourceViewer === undefined && components.length > 0 && !showSearch;
-
-    const componentsClassName = classNames('boxed-group', 'spacer-top', {
-      'new-loading': loading,
-      'search-results': showSearch
-    });
-
-    const defaultTitle =
-      baseComponent && ['APP', 'VW', 'SVW'].includes(baseComponent.qualifier)
-        ? translate('projects.page')
-        : translate('code.page');
-
-    return (
-      <div className="page page-limited">
-        <Suggestions suggestions="code" />
-        <Helmet
-          defer={false}
-          title={sourceViewer !== undefined ? sourceViewer.name : defaultTitle}
-        />
-        <A11ySkipTarget anchor="code_main" />
-
-        {!hasNoFile && (
-          <Search
-            branchLike={branchLike}
-            component={component}
-            onSearchClear={this.handleSearchClear}
-            onSearchResults={this.handleSearchResults}
-          />
-        )}
-
-        <div className="code-components">
-          {hasNoFile && (
-            <div className="display-flex-center display-flex-column no-file">
-              <span className="h1 text-muted">
-                {translate(
-                  'code_viewer.no_source_code_displayed_due_to_empty_analysis',
-                  component.qualifier
-                )}
-              </span>
-            </div>
-          )}
-          {shouldShowBreadcrumbs && (
-            <Breadcrumbs
-              branchLike={branchLike}
-              breadcrumbs={breadcrumbs}
-              rootComponent={component}
-            />
-          )}
-
-          {shouldShowComponentList && (
-            <>
-              <div className={componentsClassName}>
-                <Components
-                  baseComponent={baseComponent}
-                  branchLike={branchLike}
-                  components={components}
-                  cycle={true}
-                  metrics={this.props.metrics}
-                  onEndOfList={this.handleLoadMore}
-                  onGoToParent={this.handleGoToParent}
-                  onHighlight={this.handleHighlight}
-                  onSelect={this.handleSelect}
-                  rootComponent={component}
-                  selected={highlighted}
-                />
-              </div>
-              <ListFooter count={components.length} loadMore={this.handleLoadMore} total={total} />
-            </>
-          )}
-
-          {showSearch && searchResults && (
-            <div className={componentsClassName}>
-              <Components
-                branchLike={this.props.branchLike}
-                components={searchResults}
-                metrics={{}}
-                onHighlight={this.handleHighlight}
-                onSelect={this.handleSelect}
-                rootComponent={component}
-                selected={highlighted}
-              />
-            </div>
-          )}
-
-          {sourceViewer !== undefined && !showSearch && (
-            <div className="spacer-top">
-              <SourceViewerWrapper
-                branchLike={branchLike}
-                component={sourceViewer.key}
-                componentMeasures={sourceViewer.measures}
-                isFile={true}
-                location={location}
-                onGoToParent={this.handleGoToParent}
-                onIssueChange={this.handleIssueChange}
-              />
-            </div>
-          )}
-        </div>
-      </div>
-    );
-  }
-}
-
-const mapStateToProps = (state: any): StateToProps => ({
-  metrics: getMetrics(state)
-});
-
-const mapDispatchToProps: DispatchToProps = {
-  fetchBranchStatus: fetchBranchStatus as any,
-  fetchMetrics
-};
-
-export default connect<StateToProps, DispatchToProps, Props>(
-  mapStateToProps,
-  mapDispatchToProps
-)(AppCode);
diff --git a/server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx b/server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx
new file mode 100644 (file)
index 0000000..fef599b
--- /dev/null
@@ -0,0 +1,377 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 * as classNames from 'classnames';
+import { Location } from 'history';
+import { debounce } from 'lodash';
+import * as React from 'react';
+import { Helmet } from 'react-helmet-async';
+import { connect } from 'react-redux';
+import { InjectedRouter } from 'react-router';
+import A11ySkipTarget from '../../../app/components/a11y/A11ySkipTarget';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
+import ListFooter from '../../../components/controls/ListFooter';
+import { isPullRequest, isSameBranchLike } from '../../../helpers/branch-like';
+import { translate } from '../../../helpers/l10n';
+import { getCodeUrl, getProjectUrl } from '../../../helpers/urls';
+import { fetchBranchStatus, fetchMetrics } from '../../../store/rootActions';
+import { getMetrics } from '../../../store/rootReducer';
+import { BranchLike } from '../../../types/branch-like';
+import { addComponent, addComponentBreadcrumbs, clearBucket } from '../bucket';
+import '../code.css';
+import { loadMoreChildren, retrieveComponent, retrieveComponentChildren } from '../utils';
+import Breadcrumbs from './Breadcrumbs';
+import Components from './Components';
+import Search from './Search';
+import SourceViewerWrapper from './SourceViewerWrapper';
+
+interface StateToProps {
+  metrics: T.Dict<T.Metric>;
+}
+
+interface DispatchToProps {
+  fetchBranchStatus: (branchLike: BranchLike, projectKey: string) => Promise<void>;
+  fetchMetrics: () => void;
+}
+
+interface OwnProps {
+  branchLike?: BranchLike;
+  component: T.Component;
+  location: Pick<Location, 'query'>;
+  router: Pick<InjectedRouter, 'push'>;
+}
+
+type Props = StateToProps & DispatchToProps & OwnProps;
+
+interface State {
+  baseComponent?: T.ComponentMeasure;
+  breadcrumbs: T.Breadcrumb[];
+  components?: T.ComponentMeasure[];
+  highlighted?: T.ComponentMeasure;
+  loading: boolean;
+  page: number;
+  searchResults?: T.ComponentMeasure[];
+  sourceViewer?: T.ComponentMeasure;
+  total: number;
+}
+
+export class CodeApp extends React.PureComponent<Props, State> {
+  mounted = false;
+  state: State;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      breadcrumbs: [],
+      loading: true,
+      page: 0,
+      total: 0
+    };
+    this.refreshBranchStatus = debounce(this.refreshBranchStatus, 1000);
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+    this.props.fetchMetrics();
+    this.handleComponentChange();
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (
+      prevProps.component !== this.props.component ||
+      !isSameBranchLike(prevProps.branchLike, this.props.branchLike)
+    ) {
+      this.handleComponentChange();
+    } else if (prevProps.location !== this.props.location) {
+      this.handleUpdate();
+    }
+  }
+
+  componentWillUnmount() {
+    clearBucket();
+    this.mounted = false;
+  }
+
+  loadComponent = (componentKey: string) => {
+    this.setState({ loading: true });
+    retrieveComponent(
+      componentKey,
+      this.props.component.qualifier,
+      this,
+      this.props.branchLike
+    ).then(r => {
+      if (this.mounted) {
+        if (['FIL', 'UTS'].includes(r.component.qualifier)) {
+          this.setState({
+            breadcrumbs: r.breadcrumbs,
+            components: r.components,
+            loading: false,
+            page: 0,
+            searchResults: undefined,
+            sourceViewer: r.component,
+            total: 0
+          });
+        } else {
+          this.setState({
+            baseComponent: r.component,
+            breadcrumbs: r.breadcrumbs,
+            components: r.components,
+            loading: false,
+            page: r.page,
+            searchResults: undefined,
+            sourceViewer: undefined,
+            total: r.total
+          });
+        }
+      }
+    }, this.stopLoading);
+  };
+
+  stopLoading = () => {
+    if (this.mounted) {
+      this.setState({ loading: false });
+    }
+  };
+
+  handleComponentChange = () => {
+    const { branchLike, component } = this.props;
+
+    // we already know component's breadcrumbs,
+    addComponentBreadcrumbs(component.key, component.breadcrumbs);
+
+    this.setState({ loading: true });
+    retrieveComponentChildren(component.key, component.qualifier, this, branchLike).then(() => {
+      addComponent(component);
+      if (this.mounted) {
+        this.handleUpdate();
+      }
+    }, this.stopLoading);
+  };
+
+  handleLoadMore = () => {
+    const { baseComponent, components, page } = this.state;
+    if (!baseComponent || !components) {
+      return;
+    }
+    loadMoreChildren(
+      baseComponent.key,
+      page + 1,
+      this.props.component.qualifier,
+      this,
+      this.props.branchLike
+    ).then(r => {
+      if (this.mounted && r.components.length) {
+        this.setState({
+          components: [...components, ...r.components],
+          page: r.page,
+          total: r.total
+        });
+      }
+    }, this.stopLoading);
+  };
+
+  handleGoToParent = () => {
+    const { branchLike, component } = this.props;
+    const { breadcrumbs = [] } = this.state;
+
+    if (breadcrumbs.length > 1) {
+      const parentComponent = breadcrumbs[breadcrumbs.length - 2];
+      this.props.router.push(getCodeUrl(component.key, branchLike, parentComponent.key));
+      this.setState({ highlighted: breadcrumbs[breadcrumbs.length - 1] });
+    }
+  };
+
+  handleHighlight = (highlighted: T.ComponentMeasure) => {
+    this.setState({ highlighted });
+  };
+
+  handleIssueChange = (_: T.Issue) => {
+    this.refreshBranchStatus();
+  };
+
+  handleSearchClear = () => {
+    this.setState({ searchResults: undefined });
+  };
+
+  handleSearchResults = (searchResults: T.ComponentMeasure[] = []) => {
+    this.setState({ searchResults });
+  };
+
+  handleSelect = (component: T.ComponentMeasure) => {
+    const { branchLike, component: rootComponent } = this.props;
+
+    if (component.refKey) {
+      this.props.router.push(getProjectUrl(component.refKey));
+    } else {
+      this.props.router.push(getCodeUrl(rootComponent.key, branchLike, component.key));
+    }
+
+    this.setState({ highlighted: undefined });
+  };
+
+  handleUpdate = () => {
+    const { component, location } = this.props;
+    const { selected } = location.query;
+    const finalKey = selected || component.key;
+
+    this.loadComponent(finalKey);
+  };
+
+  refreshBranchStatus = () => {
+    const { branchLike, component } = this.props;
+    if (branchLike && component && isPullRequest(branchLike)) {
+      this.props.fetchBranchStatus(branchLike, component.key);
+    }
+  };
+
+  render() {
+    const { branchLike, component, location } = this.props;
+    const {
+      baseComponent,
+      breadcrumbs,
+      components = [],
+      highlighted,
+      loading,
+      total,
+      searchResults,
+      sourceViewer
+    } = this.state;
+
+    const showSearch = searchResults !== undefined;
+
+    const hasNoFile = components.length === 0 && searchResults === undefined;
+
+    const shouldShowBreadcrumbs = breadcrumbs.length > 1 && !showSearch;
+    const shouldShowComponentList =
+      sourceViewer === undefined && components.length > 0 && !showSearch;
+
+    const componentsClassName = classNames('boxed-group', 'spacer-top', {
+      'new-loading': loading,
+      'search-results': showSearch
+    });
+
+    const defaultTitle =
+      baseComponent && ['APP', 'VW', 'SVW'].includes(baseComponent.qualifier)
+        ? translate('projects.page')
+        : translate('code.page');
+
+    return (
+      <div className="page page-limited">
+        <Suggestions suggestions="code" />
+        <Helmet
+          defer={false}
+          title={sourceViewer !== undefined ? sourceViewer.name : defaultTitle}
+        />
+        <A11ySkipTarget anchor="code_main" />
+
+        {!hasNoFile && (
+          <Search
+            branchLike={branchLike}
+            component={component}
+            onSearchClear={this.handleSearchClear}
+            onSearchResults={this.handleSearchResults}
+          />
+        )}
+
+        <div className="code-components">
+          {hasNoFile && (
+            <div className="display-flex-center display-flex-column no-file">
+              <span className="h1 text-muted">
+                {translate(
+                  'code_viewer.no_source_code_displayed_due_to_empty_analysis',
+                  component.qualifier
+                )}
+              </span>
+            </div>
+          )}
+          {shouldShowBreadcrumbs && (
+            <Breadcrumbs
+              branchLike={branchLike}
+              breadcrumbs={breadcrumbs}
+              rootComponent={component}
+            />
+          )}
+
+          {shouldShowComponentList && (
+            <>
+              <div className={componentsClassName}>
+                <Components
+                  baseComponent={baseComponent}
+                  branchLike={branchLike}
+                  components={components}
+                  cycle={true}
+                  metrics={this.props.metrics}
+                  onEndOfList={this.handleLoadMore}
+                  onGoToParent={this.handleGoToParent}
+                  onHighlight={this.handleHighlight}
+                  onSelect={this.handleSelect}
+                  rootComponent={component}
+                  selected={highlighted}
+                />
+              </div>
+              <ListFooter count={components.length} loadMore={this.handleLoadMore} total={total} />
+            </>
+          )}
+
+          {showSearch && searchResults && (
+            <div className={componentsClassName}>
+              <Components
+                branchLike={this.props.branchLike}
+                components={searchResults}
+                metrics={{}}
+                onHighlight={this.handleHighlight}
+                onSelect={this.handleSelect}
+                rootComponent={component}
+                selected={highlighted}
+              />
+            </div>
+          )}
+
+          {sourceViewer !== undefined && !showSearch && (
+            <div className="spacer-top">
+              <SourceViewerWrapper
+                branchLike={branchLike}
+                component={sourceViewer.key}
+                componentMeasures={sourceViewer.measures}
+                isFile={true}
+                location={location}
+                onGoToParent={this.handleGoToParent}
+                onIssueChange={this.handleIssueChange}
+              />
+            </div>
+          )}
+        </div>
+      </div>
+    );
+  }
+}
+
+const mapStateToProps = (state: any): StateToProps => ({
+  metrics: getMetrics(state)
+});
+
+const mapDispatchToProps: DispatchToProps = {
+  fetchBranchStatus: fetchBranchStatus as any,
+  fetchMetrics
+};
+
+export default connect<StateToProps, DispatchToProps, Props>(
+  mapStateToProps,
+  mapDispatchToProps
+)(CodeApp);
index 00b51655ab1b501642b83d463792ab8ffd2e2493..a680e771c183528ec470df77f06b4dffce19f313 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 { omit } from 'lodash';
 import * as React from 'react';
 import { getTree } from '../../../api/components';
 import SearchBox from '../../../components/controls/SearchBox';
@@ -32,7 +33,7 @@ interface Props {
   location: Location;
   onSearchClear: () => void;
   onSearchResults: (results?: T.ComponentMeasure[]) => void;
-  router: Pick<Router, 'push'>;
+  router: Router;
 }
 
 interface State {
@@ -40,7 +41,7 @@ interface State {
   loading: boolean;
 }
 
-class Search extends React.PureComponent<Props, State> {
+export class Search extends React.PureComponent<Props, State> {
   mounted = false;
   state: State = {
     query: '',
@@ -49,11 +50,14 @@ class Search extends React.PureComponent<Props, State> {
 
   componentDidMount() {
     this.mounted = true;
+    if (this.props.location.query.search) {
+      this.handleQueryChange(this.props.location.query.search);
+    }
   }
 
-  componentWillReceiveProps(nextProps: Props) {
-    // if the url has change, reset the current state
-    if (nextProps.location !== this.props.location) {
+  componentDidUpdate(nextProps: Props) {
+    // if the component has change, reset the current state
+    if (nextProps.location.query.id !== this.props.location.query.id) {
       this.setState({
         query: '',
         loading: false
@@ -79,8 +83,9 @@ class Search extends React.PureComponent<Props, State> {
 
   handleSearch = (query: string) => {
     if (this.mounted) {
-      const { branchLike, component } = this.props;
+      const { branchLike, component, router, location } = this.props;
       this.setState({ loading: true });
+      router.replace({ pathname: location.pathname, query: { ...location.query, search: query } });
 
       const isPortfolio = ['VW', 'SVW', 'APP'].includes(component.qualifier);
       const qualifiers = isPortfolio ? 'SVW,TRK' : 'BRC,UTS,FIL';
@@ -109,8 +114,10 @@ class Search extends React.PureComponent<Props, State> {
   };
 
   handleQueryChange = (query: string) => {
+    const { router, location } = this.props;
     this.setState({ query });
     if (query.length === 0) {
+      router.replace({ pathname: location.pathname, query: omit(location.query, 'search') });
       this.props.onSearchClear();
     } else {
       this.handleSearch(query);
diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/AppCode-test.tsx b/server/sonar-web/src/main/js/apps/code/components/__tests__/AppCode-test.tsx
deleted file mode 100644 (file)
index 346e502..0000000
+++ /dev/null
@@ -1,194 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 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 { mockPullRequest } from '../../../../helpers/mocks/branch-like';
-import { mockComponent, mockComponentMeasure } from '../../../../helpers/mocks/component';
-import { mockIssue, mockRouter } from '../../../../helpers/testMocks';
-import { waitAndUpdate } from '../../../../helpers/testUtils';
-import { ComponentQualifier } from '../../../../types/component';
-import { loadMoreChildren, retrieveComponent } from '../../utils';
-import { AppCode } from '../AppCode';
-
-jest.mock('../../utils', () => ({
-  loadMoreChildren: jest.fn().mockResolvedValue({}),
-  retrieveComponent: jest.fn().mockResolvedValue({
-    breadcrumbs: [],
-    component: { qualifier: 'APP' },
-    components: [],
-    page: 0,
-    total: 1
-  }),
-  retrieveComponentChildren: () => Promise.resolve()
-}));
-
-const METRICS = {
-  coverage: { id: '2', key: 'coverage', type: 'PERCENT', name: 'Coverage', domain: 'Coverage' },
-  new_bugs: { id: '4', key: 'new_bugs', type: 'INT', name: 'New Bugs', domain: 'Reliability' }
-};
-
-beforeEach(() => {
-  (retrieveComponent as jest.Mock<any>).mockClear();
-});
-
-it.each([
-  [ComponentQualifier.Application],
-  [ComponentQualifier.Project],
-  [ComponentQualifier.Portfolio],
-  [ComponentQualifier.SubPortfolio]
-])('should render correclty when no sub component for %s', async qualifier => {
-  const component = { breadcrumbs: [], name: 'foo', key: 'foo', qualifier };
-  (retrieveComponent as jest.Mock<any>).mockResolvedValueOnce({
-    breadcrumbs: [],
-    component,
-    components: [],
-    page: 0,
-    total: 1
-  });
-  const wrapper = shallowRender({ component });
-  await waitAndUpdate(wrapper);
-  expect(wrapper).toMatchSnapshot();
-  wrapper.instance().handleSearchResults([]);
-  expect(wrapper).toMatchSnapshot('no search');
-  (retrieveComponent as jest.Mock<any>).mockResolvedValueOnce({
-    breadcrumbs: [],
-    component,
-    components: [mockComponent({ qualifier: ComponentQualifier.File })],
-    page: 0,
-    total: 1
-  });
-  wrapper.instance().loadComponent(component.key);
-  await waitAndUpdate(wrapper);
-  expect(wrapper).toMatchSnapshot('with sub component');
-});
-
-it('should refresh branch status if issues are updated', async () => {
-  const fetchBranchStatus = jest.fn();
-  const branchLike = mockPullRequest();
-  const wrapper = shallowRender({ branchLike, fetchBranchStatus });
-  const instance = wrapper.instance();
-  await waitAndUpdate(wrapper);
-
-  instance.handleIssueChange(mockIssue());
-  expect(fetchBranchStatus).toBeCalledWith(branchLike, 'foo');
-});
-
-it('should load more behave correctly', async () => {
-  const component1 = mockComponent();
-  const component2 = mockComponent();
-  (retrieveComponent as jest.Mock<any>).mockResolvedValueOnce({
-    breadcrumbs: [],
-    component: mockComponent(),
-    components: [component1],
-    page: 0,
-    total: 1
-  });
-  let wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-
-  (loadMoreChildren as jest.Mock<any>).mockResolvedValueOnce({
-    components: [component2],
-    page: 0,
-    total: 1
-  });
-
-  wrapper.instance().handleLoadMore();
-  expect(wrapper.state().components).toContainEqual(component1);
-  expect(wrapper.state().components).toContainEqual(component2);
-
-  (retrieveComponent as jest.Mock<any>).mockRejectedValueOnce({});
-  wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-  wrapper.instance().handleLoadMore();
-  expect(wrapper.state().components).toBeUndefined();
-});
-
-it('should handle go to parent correctly', async () => {
-  const router = mockRouter();
-  (retrieveComponent as jest.Mock<any>).mockResolvedValueOnce({
-    breadcrumbs: [],
-    component: mockComponent(),
-    components: [],
-    page: 0,
-    total: 1
-  });
-  let wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-  wrapper.instance().handleGoToParent();
-  expect(wrapper.state().highlighted).toBeUndefined();
-
-  const breadcrumb = { key: 'key2', name: 'name2', qualifier: ComponentQualifier.Directory };
-  (retrieveComponent as jest.Mock<any>).mockResolvedValueOnce({
-    breadcrumbs: [
-      { key: 'key1', name: 'name1', qualifier: ComponentQualifier.Directory },
-      breadcrumb
-    ],
-    component: mockComponent(),
-    components: [],
-    page: 0,
-    total: 1
-  });
-  wrapper = shallowRender({ router });
-  await waitAndUpdate(wrapper);
-  wrapper.instance().handleGoToParent();
-  expect(wrapper.state().highlighted).toBe(breadcrumb);
-  expect(router.push).toHaveBeenCalledWith({
-    pathname: '/code',
-    query: { id: 'foo', line: undefined, selected: 'key1' }
-  });
-});
-
-it('should handle select correctly', () => {
-  const router = mockRouter();
-  const wrapper = shallowRender({ router });
-  wrapper.setState({ highlighted: mockComponentMeasure() });
-
-  wrapper.instance().handleSelect(mockComponentMeasure(true, { refKey: 'test' }));
-  expect(router.push).toHaveBeenCalledWith({
-    pathname: '/dashboard',
-    query: { branch: undefined, id: 'test' }
-  });
-  expect(wrapper.state().highlighted).toBeUndefined();
-
-  wrapper.instance().handleSelect(mockComponentMeasure());
-  expect(router.push).toHaveBeenCalledWith({
-    pathname: '/dashboard',
-    query: { branch: undefined, id: 'test' }
-  });
-});
-
-function shallowRender(props: Partial<AppCode['props']> = {}) {
-  return shallow<AppCode>(
-    <AppCode
-      component={{
-        breadcrumbs: [],
-        name: 'foo',
-        key: 'foo',
-        qualifier: 'FOO'
-      }}
-      fetchBranchStatus={jest.fn()}
-      fetchMetrics={jest.fn()}
-      location={{ query: { branch: 'b', id: 'foo', line: '7' } }}
-      metrics={METRICS}
-      router={mockRouter()}
-      {...props}
-    />
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/CodeApp-test.tsx b/server/sonar-web/src/main/js/apps/code/components/__tests__/CodeApp-test.tsx
new file mode 100644 (file)
index 0000000..20645e5
--- /dev/null
@@ -0,0 +1,194 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 { mockPullRequest } from '../../../../helpers/mocks/branch-like';
+import { mockComponent, mockComponentMeasure } from '../../../../helpers/mocks/component';
+import { mockIssue, mockRouter } from '../../../../helpers/testMocks';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
+import { ComponentQualifier } from '../../../../types/component';
+import { loadMoreChildren, retrieveComponent } from '../../utils';
+import { CodeApp } from '../CodeApp';
+
+jest.mock('../../utils', () => ({
+  loadMoreChildren: jest.fn().mockResolvedValue({}),
+  retrieveComponent: jest.fn().mockResolvedValue({
+    breadcrumbs: [],
+    component: { qualifier: 'APP' },
+    components: [],
+    page: 0,
+    total: 1
+  }),
+  retrieveComponentChildren: () => Promise.resolve()
+}));
+
+const METRICS = {
+  coverage: { id: '2', key: 'coverage', type: 'PERCENT', name: 'Coverage', domain: 'Coverage' },
+  new_bugs: { id: '4', key: 'new_bugs', type: 'INT', name: 'New Bugs', domain: 'Reliability' }
+};
+
+beforeEach(() => {
+  (retrieveComponent as jest.Mock<any>).mockClear();
+});
+
+it.each([
+  [ComponentQualifier.Application],
+  [ComponentQualifier.Project],
+  [ComponentQualifier.Portfolio],
+  [ComponentQualifier.SubPortfolio]
+])('should render correclty when no sub component for %s', async qualifier => {
+  const component = { breadcrumbs: [], name: 'foo', key: 'foo', qualifier };
+  (retrieveComponent as jest.Mock<any>).mockResolvedValueOnce({
+    breadcrumbs: [],
+    component,
+    components: [],
+    page: 0,
+    total: 1
+  });
+  const wrapper = shallowRender({ component });
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+  wrapper.instance().handleSearchResults([]);
+  expect(wrapper).toMatchSnapshot('no search');
+  (retrieveComponent as jest.Mock<any>).mockResolvedValueOnce({
+    breadcrumbs: [],
+    component,
+    components: [mockComponent({ qualifier: ComponentQualifier.File })],
+    page: 0,
+    total: 1
+  });
+  wrapper.instance().loadComponent(component.key);
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot('with sub component');
+});
+
+it('should refresh branch status if issues are updated', async () => {
+  const fetchBranchStatus = jest.fn();
+  const branchLike = mockPullRequest();
+  const wrapper = shallowRender({ branchLike, fetchBranchStatus });
+  const instance = wrapper.instance();
+  await waitAndUpdate(wrapper);
+
+  instance.handleIssueChange(mockIssue());
+  expect(fetchBranchStatus).toBeCalledWith(branchLike, 'foo');
+});
+
+it('should load more behave correctly', async () => {
+  const component1 = mockComponent();
+  const component2 = mockComponent();
+  (retrieveComponent as jest.Mock<any>).mockResolvedValueOnce({
+    breadcrumbs: [],
+    component: mockComponent(),
+    components: [component1],
+    page: 0,
+    total: 1
+  });
+  let wrapper = shallowRender();
+  await waitAndUpdate(wrapper);
+
+  (loadMoreChildren as jest.Mock<any>).mockResolvedValueOnce({
+    components: [component2],
+    page: 0,
+    total: 1
+  });
+
+  wrapper.instance().handleLoadMore();
+  expect(wrapper.state().components).toContainEqual(component1);
+  expect(wrapper.state().components).toContainEqual(component2);
+
+  (retrieveComponent as jest.Mock<any>).mockRejectedValueOnce({});
+  wrapper = shallowRender();
+  await waitAndUpdate(wrapper);
+  wrapper.instance().handleLoadMore();
+  expect(wrapper.state().components).toBeUndefined();
+});
+
+it('should handle go to parent correctly', async () => {
+  const router = mockRouter();
+  (retrieveComponent as jest.Mock<any>).mockResolvedValueOnce({
+    breadcrumbs: [],
+    component: mockComponent(),
+    components: [],
+    page: 0,
+    total: 1
+  });
+  let wrapper = shallowRender();
+  await waitAndUpdate(wrapper);
+  wrapper.instance().handleGoToParent();
+  expect(wrapper.state().highlighted).toBeUndefined();
+
+  const breadcrumb = { key: 'key2', name: 'name2', qualifier: ComponentQualifier.Directory };
+  (retrieveComponent as jest.Mock<any>).mockResolvedValueOnce({
+    breadcrumbs: [
+      { key: 'key1', name: 'name1', qualifier: ComponentQualifier.Directory },
+      breadcrumb
+    ],
+    component: mockComponent(),
+    components: [],
+    page: 0,
+    total: 1
+  });
+  wrapper = shallowRender({ router });
+  await waitAndUpdate(wrapper);
+  wrapper.instance().handleGoToParent();
+  expect(wrapper.state().highlighted).toBe(breadcrumb);
+  expect(router.push).toHaveBeenCalledWith({
+    pathname: '/code',
+    query: { id: 'foo', line: undefined, selected: 'key1' }
+  });
+});
+
+it('should handle select correctly', () => {
+  const router = mockRouter();
+  const wrapper = shallowRender({ router });
+  wrapper.setState({ highlighted: mockComponentMeasure() });
+
+  wrapper.instance().handleSelect(mockComponentMeasure(true, { refKey: 'test' }));
+  expect(router.push).toHaveBeenCalledWith({
+    pathname: '/dashboard',
+    query: { branch: undefined, id: 'test' }
+  });
+  expect(wrapper.state().highlighted).toBeUndefined();
+
+  wrapper.instance().handleSelect(mockComponentMeasure());
+  expect(router.push).toHaveBeenCalledWith({
+    pathname: '/dashboard',
+    query: { branch: undefined, id: 'test' }
+  });
+});
+
+function shallowRender(props: Partial<CodeApp['props']> = {}) {
+  return shallow<CodeApp>(
+    <CodeApp
+      component={{
+        breadcrumbs: [],
+        name: 'foo',
+        key: 'foo',
+        qualifier: 'FOO'
+      }}
+      fetchBranchStatus={jest.fn()}
+      fetchMetrics={jest.fn()}
+      location={{ query: { branch: 'b', id: 'foo', line: '7' } }}
+      metrics={METRICS}
+      router={mockRouter()}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/Search-test.tsx b/server/sonar-web/src/main/js/apps/code/components/__tests__/Search-test.tsx
new file mode 100644 (file)
index 0000000..829fb65
--- /dev/null
@@ -0,0 +1,111 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 { getTree } from '../../../../api/components';
+import { mockComponent } from '../../../../helpers/mocks/component';
+import { mockLocation, mockRouter } from '../../../../helpers/testMocks';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
+import { Search } from '../Search';
+
+jest.mock('../../../../api/components', () => {
+  const { mockTreeComponent, mockComponent } = jest.requireActual(
+    '../../../../helpers/mocks/component'
+  );
+
+  return {
+    getTree: jest.fn().mockResolvedValue({
+      baseComponent: mockTreeComponent(),
+      components: [mockComponent()],
+      paging: { pageIndex: 0, pageSize: 5, total: 20 }
+    })
+  };
+});
+
+it('should render correcly', () => {
+  expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should search correct query on mount', async () => {
+  const onSearchResults = jest.fn();
+  const wrapper = shallowRender({
+    location: mockLocation({ query: { id: 'foo', search: 'bar' } }),
+    onSearchResults
+  });
+  await waitAndUpdate(wrapper);
+  expect(getTree).toHaveBeenCalledWith({
+    component: 'my-project',
+    q: 'bar',
+    qualifiers: 'BRC,UTS,FIL',
+    s: 'qualifier,name'
+  });
+  expect(onSearchResults).toHaveBeenCalledWith([
+    {
+      breadcrumbs: [],
+      key: 'my-project',
+      name: 'MyProject',
+      qualifier: 'TRK',
+      qualityGate: { isDefault: true, key: '30', name: 'Sonar way' },
+      qualityProfiles: [{ deleted: false, key: 'my-qp', language: 'ts', name: 'Sonar way' }],
+      tags: []
+    }
+  ]);
+});
+
+it('should handle search correctly', async () => {
+  const router = mockRouter();
+  const onSearchClear = jest.fn();
+  const wrapper = shallowRender({ router, onSearchClear });
+  wrapper.instance().handleQueryChange('foo');
+  await waitAndUpdate(wrapper);
+  expect(router.replace).toHaveBeenCalledWith({
+    pathname: '/path',
+    query: {
+      search: 'foo'
+    }
+  });
+  expect(getTree).toHaveBeenCalledWith({
+    component: 'my-project',
+    q: 'foo',
+    qualifiers: 'BRC,UTS,FIL',
+    s: 'qualifier,name'
+  });
+
+  wrapper.instance().handleQueryChange('');
+  await waitAndUpdate(wrapper);
+  expect(router.replace).toHaveBeenCalledWith({
+    pathname: '/path',
+    query: {}
+  });
+  expect(onSearchClear).toHaveBeenCalledWith();
+});
+
+function shallowRender(props?: Partial<Search['props']>) {
+  return shallow<Search>(
+    <Search
+      component={mockComponent()}
+      location={mockLocation()}
+      onSearchClear={jest.fn()}
+      onSearchResults={jest.fn()}
+      router={mockRouter()}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/AppCode-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/AppCode-test.tsx.snap
deleted file mode 100644 (file)
index 233b60a..0000000
+++ /dev/null
@@ -1,765 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correclty when no sub component for APP 1`] = `
-<div
-  className="page page-limited"
->
-  <Suggestions
-    suggestions="code"
-  />
-  <Helmet
-    defer={false}
-    encodeSpecialCharacters={true}
-    title="projects.page"
-  />
-  <A11ySkipTarget
-    anchor="code_main"
-  />
-  <div
-    className="code-components"
-  >
-    <div
-      className="display-flex-center display-flex-column no-file"
-    >
-      <span
-        className="h1 text-muted"
-      >
-        code_viewer.no_source_code_displayed_due_to_empty_analysis.APP
-      </span>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`should render correclty when no sub component for APP: no search 1`] = `
-<div
-  className="page page-limited"
->
-  <Suggestions
-    suggestions="code"
-  />
-  <Helmet
-    defer={false}
-    encodeSpecialCharacters={true}
-    title="projects.page"
-  />
-  <A11ySkipTarget
-    anchor="code_main"
-  />
-  <withRouter(Search)
-    component={
-      Object {
-        "breadcrumbs": Array [],
-        "key": "foo",
-        "name": "foo",
-        "qualifier": "APP",
-      }
-    }
-    onSearchClear={[Function]}
-    onSearchResults={[Function]}
-  />
-  <div
-    className="code-components"
-  >
-    <div
-      className="boxed-group spacer-top search-results"
-    >
-      <withKeyboardNavigation(Components)
-        components={Array []}
-        metrics={Object {}}
-        onHighlight={[Function]}
-        onSelect={[Function]}
-        rootComponent={
-          Object {
-            "breadcrumbs": Array [],
-            "key": "foo",
-            "name": "foo",
-            "qualifier": "APP",
-          }
-        }
-      />
-    </div>
-  </div>
-</div>
-`;
-
-exports[`should render correclty when no sub component for APP: with sub component 1`] = `
-<div
-  className="page page-limited"
->
-  <Suggestions
-    suggestions="code"
-  />
-  <Helmet
-    defer={false}
-    encodeSpecialCharacters={true}
-    title="projects.page"
-  />
-  <A11ySkipTarget
-    anchor="code_main"
-  />
-  <withRouter(Search)
-    component={
-      Object {
-        "breadcrumbs": Array [],
-        "key": "foo",
-        "name": "foo",
-        "qualifier": "APP",
-      }
-    }
-    onSearchClear={[Function]}
-    onSearchResults={[Function]}
-  />
-  <div
-    className="code-components"
-  >
-    <div
-      className="boxed-group spacer-top"
-    >
-      <withKeyboardNavigation(Components)
-        baseComponent={
-          Object {
-            "breadcrumbs": Array [],
-            "key": "foo",
-            "name": "foo",
-            "qualifier": "APP",
-          }
-        }
-        components={
-          Array [
-            Object {
-              "breadcrumbs": Array [],
-              "key": "my-project",
-              "name": "MyProject",
-              "qualifier": "FIL",
-              "qualityGate": Object {
-                "isDefault": true,
-                "key": "30",
-                "name": "Sonar way",
-              },
-              "qualityProfiles": Array [
-                Object {
-                  "deleted": false,
-                  "key": "my-qp",
-                  "language": "ts",
-                  "name": "Sonar way",
-                },
-              ],
-              "tags": Array [],
-            },
-          ]
-        }
-        cycle={true}
-        metrics={
-          Object {
-            "coverage": Object {
-              "domain": "Coverage",
-              "id": "2",
-              "key": "coverage",
-              "name": "Coverage",
-              "type": "PERCENT",
-            },
-            "new_bugs": Object {
-              "domain": "Reliability",
-              "id": "4",
-              "key": "new_bugs",
-              "name": "New Bugs",
-              "type": "INT",
-            },
-          }
-        }
-        onEndOfList={[Function]}
-        onGoToParent={[Function]}
-        onHighlight={[Function]}
-        onSelect={[Function]}
-        rootComponent={
-          Object {
-            "breadcrumbs": Array [],
-            "key": "foo",
-            "name": "foo",
-            "qualifier": "APP",
-          }
-        }
-      />
-    </div>
-    <ListFooter
-      count={1}
-      loadMore={[Function]}
-      total={1}
-    />
-  </div>
-</div>
-`;
-
-exports[`should render correclty when no sub component for SVW 1`] = `
-<div
-  className="page page-limited"
->
-  <Suggestions
-    suggestions="code"
-  />
-  <Helmet
-    defer={false}
-    encodeSpecialCharacters={true}
-    title="projects.page"
-  />
-  <A11ySkipTarget
-    anchor="code_main"
-  />
-  <div
-    className="code-components"
-  >
-    <div
-      className="display-flex-center display-flex-column no-file"
-    >
-      <span
-        className="h1 text-muted"
-      >
-        code_viewer.no_source_code_displayed_due_to_empty_analysis.SVW
-      </span>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`should render correclty when no sub component for SVW: no search 1`] = `
-<div
-  className="page page-limited"
->
-  <Suggestions
-    suggestions="code"
-  />
-  <Helmet
-    defer={false}
-    encodeSpecialCharacters={true}
-    title="projects.page"
-  />
-  <A11ySkipTarget
-    anchor="code_main"
-  />
-  <withRouter(Search)
-    component={
-      Object {
-        "breadcrumbs": Array [],
-        "key": "foo",
-        "name": "foo",
-        "qualifier": "SVW",
-      }
-    }
-    onSearchClear={[Function]}
-    onSearchResults={[Function]}
-  />
-  <div
-    className="code-components"
-  >
-    <div
-      className="boxed-group spacer-top search-results"
-    >
-      <withKeyboardNavigation(Components)
-        components={Array []}
-        metrics={Object {}}
-        onHighlight={[Function]}
-        onSelect={[Function]}
-        rootComponent={
-          Object {
-            "breadcrumbs": Array [],
-            "key": "foo",
-            "name": "foo",
-            "qualifier": "SVW",
-          }
-        }
-      />
-    </div>
-  </div>
-</div>
-`;
-
-exports[`should render correclty when no sub component for SVW: with sub component 1`] = `
-<div
-  className="page page-limited"
->
-  <Suggestions
-    suggestions="code"
-  />
-  <Helmet
-    defer={false}
-    encodeSpecialCharacters={true}
-    title="projects.page"
-  />
-  <A11ySkipTarget
-    anchor="code_main"
-  />
-  <withRouter(Search)
-    component={
-      Object {
-        "breadcrumbs": Array [],
-        "key": "foo",
-        "name": "foo",
-        "qualifier": "SVW",
-      }
-    }
-    onSearchClear={[Function]}
-    onSearchResults={[Function]}
-  />
-  <div
-    className="code-components"
-  >
-    <div
-      className="boxed-group spacer-top"
-    >
-      <withKeyboardNavigation(Components)
-        baseComponent={
-          Object {
-            "breadcrumbs": Array [],
-            "key": "foo",
-            "name": "foo",
-            "qualifier": "SVW",
-          }
-        }
-        components={
-          Array [
-            Object {
-              "breadcrumbs": Array [],
-              "key": "my-project",
-              "name": "MyProject",
-              "qualifier": "FIL",
-              "qualityGate": Object {
-                "isDefault": true,
-                "key": "30",
-                "name": "Sonar way",
-              },
-              "qualityProfiles": Array [
-                Object {
-                  "deleted": false,
-                  "key": "my-qp",
-                  "language": "ts",
-                  "name": "Sonar way",
-                },
-              ],
-              "tags": Array [],
-            },
-          ]
-        }
-        cycle={true}
-        metrics={
-          Object {
-            "coverage": Object {
-              "domain": "Coverage",
-              "id": "2",
-              "key": "coverage",
-              "name": "Coverage",
-              "type": "PERCENT",
-            },
-            "new_bugs": Object {
-              "domain": "Reliability",
-              "id": "4",
-              "key": "new_bugs",
-              "name": "New Bugs",
-              "type": "INT",
-            },
-          }
-        }
-        onEndOfList={[Function]}
-        onGoToParent={[Function]}
-        onHighlight={[Function]}
-        onSelect={[Function]}
-        rootComponent={
-          Object {
-            "breadcrumbs": Array [],
-            "key": "foo",
-            "name": "foo",
-            "qualifier": "SVW",
-          }
-        }
-      />
-    </div>
-    <ListFooter
-      count={1}
-      loadMore={[Function]}
-      total={1}
-    />
-  </div>
-</div>
-`;
-
-exports[`should render correclty when no sub component for TRK 1`] = `
-<div
-  className="page page-limited"
->
-  <Suggestions
-    suggestions="code"
-  />
-  <Helmet
-    defer={false}
-    encodeSpecialCharacters={true}
-    title="code.page"
-  />
-  <A11ySkipTarget
-    anchor="code_main"
-  />
-  <div
-    className="code-components"
-  >
-    <div
-      className="display-flex-center display-flex-column no-file"
-    >
-      <span
-        className="h1 text-muted"
-      >
-        code_viewer.no_source_code_displayed_due_to_empty_analysis.TRK
-      </span>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`should render correclty when no sub component for TRK: no search 1`] = `
-<div
-  className="page page-limited"
->
-  <Suggestions
-    suggestions="code"
-  />
-  <Helmet
-    defer={false}
-    encodeSpecialCharacters={true}
-    title="code.page"
-  />
-  <A11ySkipTarget
-    anchor="code_main"
-  />
-  <withRouter(Search)
-    component={
-      Object {
-        "breadcrumbs": Array [],
-        "key": "foo",
-        "name": "foo",
-        "qualifier": "TRK",
-      }
-    }
-    onSearchClear={[Function]}
-    onSearchResults={[Function]}
-  />
-  <div
-    className="code-components"
-  >
-    <div
-      className="boxed-group spacer-top search-results"
-    >
-      <withKeyboardNavigation(Components)
-        components={Array []}
-        metrics={Object {}}
-        onHighlight={[Function]}
-        onSelect={[Function]}
-        rootComponent={
-          Object {
-            "breadcrumbs": Array [],
-            "key": "foo",
-            "name": "foo",
-            "qualifier": "TRK",
-          }
-        }
-      />
-    </div>
-  </div>
-</div>
-`;
-
-exports[`should render correclty when no sub component for TRK: with sub component 1`] = `
-<div
-  className="page page-limited"
->
-  <Suggestions
-    suggestions="code"
-  />
-  <Helmet
-    defer={false}
-    encodeSpecialCharacters={true}
-    title="code.page"
-  />
-  <A11ySkipTarget
-    anchor="code_main"
-  />
-  <withRouter(Search)
-    component={
-      Object {
-        "breadcrumbs": Array [],
-        "key": "foo",
-        "name": "foo",
-        "qualifier": "TRK",
-      }
-    }
-    onSearchClear={[Function]}
-    onSearchResults={[Function]}
-  />
-  <div
-    className="code-components"
-  >
-    <div
-      className="boxed-group spacer-top"
-    >
-      <withKeyboardNavigation(Components)
-        baseComponent={
-          Object {
-            "breadcrumbs": Array [],
-            "key": "foo",
-            "name": "foo",
-            "qualifier": "TRK",
-          }
-        }
-        components={
-          Array [
-            Object {
-              "breadcrumbs": Array [],
-              "key": "my-project",
-              "name": "MyProject",
-              "qualifier": "FIL",
-              "qualityGate": Object {
-                "isDefault": true,
-                "key": "30",
-                "name": "Sonar way",
-              },
-              "qualityProfiles": Array [
-                Object {
-                  "deleted": false,
-                  "key": "my-qp",
-                  "language": "ts",
-                  "name": "Sonar way",
-                },
-              ],
-              "tags": Array [],
-            },
-          ]
-        }
-        cycle={true}
-        metrics={
-          Object {
-            "coverage": Object {
-              "domain": "Coverage",
-              "id": "2",
-              "key": "coverage",
-              "name": "Coverage",
-              "type": "PERCENT",
-            },
-            "new_bugs": Object {
-              "domain": "Reliability",
-              "id": "4",
-              "key": "new_bugs",
-              "name": "New Bugs",
-              "type": "INT",
-            },
-          }
-        }
-        onEndOfList={[Function]}
-        onGoToParent={[Function]}
-        onHighlight={[Function]}
-        onSelect={[Function]}
-        rootComponent={
-          Object {
-            "breadcrumbs": Array [],
-            "key": "foo",
-            "name": "foo",
-            "qualifier": "TRK",
-          }
-        }
-      />
-    </div>
-    <ListFooter
-      count={1}
-      loadMore={[Function]}
-      total={1}
-    />
-  </div>
-</div>
-`;
-
-exports[`should render correclty when no sub component for VW 1`] = `
-<div
-  className="page page-limited"
->
-  <Suggestions
-    suggestions="code"
-  />
-  <Helmet
-    defer={false}
-    encodeSpecialCharacters={true}
-    title="projects.page"
-  />
-  <A11ySkipTarget
-    anchor="code_main"
-  />
-  <div
-    className="code-components"
-  >
-    <div
-      className="display-flex-center display-flex-column no-file"
-    >
-      <span
-        className="h1 text-muted"
-      >
-        code_viewer.no_source_code_displayed_due_to_empty_analysis.VW
-      </span>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`should render correclty when no sub component for VW: no search 1`] = `
-<div
-  className="page page-limited"
->
-  <Suggestions
-    suggestions="code"
-  />
-  <Helmet
-    defer={false}
-    encodeSpecialCharacters={true}
-    title="projects.page"
-  />
-  <A11ySkipTarget
-    anchor="code_main"
-  />
-  <withRouter(Search)
-    component={
-      Object {
-        "breadcrumbs": Array [],
-        "key": "foo",
-        "name": "foo",
-        "qualifier": "VW",
-      }
-    }
-    onSearchClear={[Function]}
-    onSearchResults={[Function]}
-  />
-  <div
-    className="code-components"
-  >
-    <div
-      className="boxed-group spacer-top search-results"
-    >
-      <withKeyboardNavigation(Components)
-        components={Array []}
-        metrics={Object {}}
-        onHighlight={[Function]}
-        onSelect={[Function]}
-        rootComponent={
-          Object {
-            "breadcrumbs": Array [],
-            "key": "foo",
-            "name": "foo",
-            "qualifier": "VW",
-          }
-        }
-      />
-    </div>
-  </div>
-</div>
-`;
-
-exports[`should render correclty when no sub component for VW: with sub component 1`] = `
-<div
-  className="page page-limited"
->
-  <Suggestions
-    suggestions="code"
-  />
-  <Helmet
-    defer={false}
-    encodeSpecialCharacters={true}
-    title="projects.page"
-  />
-  <A11ySkipTarget
-    anchor="code_main"
-  />
-  <withRouter(Search)
-    component={
-      Object {
-        "breadcrumbs": Array [],
-        "key": "foo",
-        "name": "foo",
-        "qualifier": "VW",
-      }
-    }
-    onSearchClear={[Function]}
-    onSearchResults={[Function]}
-  />
-  <div
-    className="code-components"
-  >
-    <div
-      className="boxed-group spacer-top"
-    >
-      <withKeyboardNavigation(Components)
-        baseComponent={
-          Object {
-            "breadcrumbs": Array [],
-            "key": "foo",
-            "name": "foo",
-            "qualifier": "VW",
-          }
-        }
-        components={
-          Array [
-            Object {
-              "breadcrumbs": Array [],
-              "key": "my-project",
-              "name": "MyProject",
-              "qualifier": "FIL",
-              "qualityGate": Object {
-                "isDefault": true,
-                "key": "30",
-                "name": "Sonar way",
-              },
-              "qualityProfiles": Array [
-                Object {
-                  "deleted": false,
-                  "key": "my-qp",
-                  "language": "ts",
-                  "name": "Sonar way",
-                },
-              ],
-              "tags": Array [],
-            },
-          ]
-        }
-        cycle={true}
-        metrics={
-          Object {
-            "coverage": Object {
-              "domain": "Coverage",
-              "id": "2",
-              "key": "coverage",
-              "name": "Coverage",
-              "type": "PERCENT",
-            },
-            "new_bugs": Object {
-              "domain": "Reliability",
-              "id": "4",
-              "key": "new_bugs",
-              "name": "New Bugs",
-              "type": "INT",
-            },
-          }
-        }
-        onEndOfList={[Function]}
-        onGoToParent={[Function]}
-        onHighlight={[Function]}
-        onSelect={[Function]}
-        rootComponent={
-          Object {
-            "breadcrumbs": Array [],
-            "key": "foo",
-            "name": "foo",
-            "qualifier": "VW",
-          }
-        }
-      />
-    </div>
-    <ListFooter
-      count={1}
-      loadMore={[Function]}
-      total={1}
-    />
-  </div>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/CodeApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/CodeApp-test.tsx.snap
new file mode 100644 (file)
index 0000000..233b60a
--- /dev/null
@@ -0,0 +1,765 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correclty when no sub component for APP 1`] = `
+<div
+  className="page page-limited"
+>
+  <Suggestions
+    suggestions="code"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="projects.page"
+  />
+  <A11ySkipTarget
+    anchor="code_main"
+  />
+  <div
+    className="code-components"
+  >
+    <div
+      className="display-flex-center display-flex-column no-file"
+    >
+      <span
+        className="h1 text-muted"
+      >
+        code_viewer.no_source_code_displayed_due_to_empty_analysis.APP
+      </span>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render correclty when no sub component for APP: no search 1`] = `
+<div
+  className="page page-limited"
+>
+  <Suggestions
+    suggestions="code"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="projects.page"
+  />
+  <A11ySkipTarget
+    anchor="code_main"
+  />
+  <withRouter(Search)
+    component={
+      Object {
+        "breadcrumbs": Array [],
+        "key": "foo",
+        "name": "foo",
+        "qualifier": "APP",
+      }
+    }
+    onSearchClear={[Function]}
+    onSearchResults={[Function]}
+  />
+  <div
+    className="code-components"
+  >
+    <div
+      className="boxed-group spacer-top search-results"
+    >
+      <withKeyboardNavigation(Components)
+        components={Array []}
+        metrics={Object {}}
+        onHighlight={[Function]}
+        onSelect={[Function]}
+        rootComponent={
+          Object {
+            "breadcrumbs": Array [],
+            "key": "foo",
+            "name": "foo",
+            "qualifier": "APP",
+          }
+        }
+      />
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render correclty when no sub component for APP: with sub component 1`] = `
+<div
+  className="page page-limited"
+>
+  <Suggestions
+    suggestions="code"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="projects.page"
+  />
+  <A11ySkipTarget
+    anchor="code_main"
+  />
+  <withRouter(Search)
+    component={
+      Object {
+        "breadcrumbs": Array [],
+        "key": "foo",
+        "name": "foo",
+        "qualifier": "APP",
+      }
+    }
+    onSearchClear={[Function]}
+    onSearchResults={[Function]}
+  />
+  <div
+    className="code-components"
+  >
+    <div
+      className="boxed-group spacer-top"
+    >
+      <withKeyboardNavigation(Components)
+        baseComponent={
+          Object {
+            "breadcrumbs": Array [],
+            "key": "foo",
+            "name": "foo",
+            "qualifier": "APP",
+          }
+        }
+        components={
+          Array [
+            Object {
+              "breadcrumbs": Array [],
+              "key": "my-project",
+              "name": "MyProject",
+              "qualifier": "FIL",
+              "qualityGate": Object {
+                "isDefault": true,
+                "key": "30",
+                "name": "Sonar way",
+              },
+              "qualityProfiles": Array [
+                Object {
+                  "deleted": false,
+                  "key": "my-qp",
+                  "language": "ts",
+                  "name": "Sonar way",
+                },
+              ],
+              "tags": Array [],
+            },
+          ]
+        }
+        cycle={true}
+        metrics={
+          Object {
+            "coverage": Object {
+              "domain": "Coverage",
+              "id": "2",
+              "key": "coverage",
+              "name": "Coverage",
+              "type": "PERCENT",
+            },
+            "new_bugs": Object {
+              "domain": "Reliability",
+              "id": "4",
+              "key": "new_bugs",
+              "name": "New Bugs",
+              "type": "INT",
+            },
+          }
+        }
+        onEndOfList={[Function]}
+        onGoToParent={[Function]}
+        onHighlight={[Function]}
+        onSelect={[Function]}
+        rootComponent={
+          Object {
+            "breadcrumbs": Array [],
+            "key": "foo",
+            "name": "foo",
+            "qualifier": "APP",
+          }
+        }
+      />
+    </div>
+    <ListFooter
+      count={1}
+      loadMore={[Function]}
+      total={1}
+    />
+  </div>
+</div>
+`;
+
+exports[`should render correclty when no sub component for SVW 1`] = `
+<div
+  className="page page-limited"
+>
+  <Suggestions
+    suggestions="code"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="projects.page"
+  />
+  <A11ySkipTarget
+    anchor="code_main"
+  />
+  <div
+    className="code-components"
+  >
+    <div
+      className="display-flex-center display-flex-column no-file"
+    >
+      <span
+        className="h1 text-muted"
+      >
+        code_viewer.no_source_code_displayed_due_to_empty_analysis.SVW
+      </span>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render correclty when no sub component for SVW: no search 1`] = `
+<div
+  className="page page-limited"
+>
+  <Suggestions
+    suggestions="code"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="projects.page"
+  />
+  <A11ySkipTarget
+    anchor="code_main"
+  />
+  <withRouter(Search)
+    component={
+      Object {
+        "breadcrumbs": Array [],
+        "key": "foo",
+        "name": "foo",
+        "qualifier": "SVW",
+      }
+    }
+    onSearchClear={[Function]}
+    onSearchResults={[Function]}
+  />
+  <div
+    className="code-components"
+  >
+    <div
+      className="boxed-group spacer-top search-results"
+    >
+      <withKeyboardNavigation(Components)
+        components={Array []}
+        metrics={Object {}}
+        onHighlight={[Function]}
+        onSelect={[Function]}
+        rootComponent={
+          Object {
+            "breadcrumbs": Array [],
+            "key": "foo",
+            "name": "foo",
+            "qualifier": "SVW",
+          }
+        }
+      />
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render correclty when no sub component for SVW: with sub component 1`] = `
+<div
+  className="page page-limited"
+>
+  <Suggestions
+    suggestions="code"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="projects.page"
+  />
+  <A11ySkipTarget
+    anchor="code_main"
+  />
+  <withRouter(Search)
+    component={
+      Object {
+        "breadcrumbs": Array [],
+        "key": "foo",
+        "name": "foo",
+        "qualifier": "SVW",
+      }
+    }
+    onSearchClear={[Function]}
+    onSearchResults={[Function]}
+  />
+  <div
+    className="code-components"
+  >
+    <div
+      className="boxed-group spacer-top"
+    >
+      <withKeyboardNavigation(Components)
+        baseComponent={
+          Object {
+            "breadcrumbs": Array [],
+            "key": "foo",
+            "name": "foo",
+            "qualifier": "SVW",
+          }
+        }
+        components={
+          Array [
+            Object {
+              "breadcrumbs": Array [],
+              "key": "my-project",
+              "name": "MyProject",
+              "qualifier": "FIL",
+              "qualityGate": Object {
+                "isDefault": true,
+                "key": "30",
+                "name": "Sonar way",
+              },
+              "qualityProfiles": Array [
+                Object {
+                  "deleted": false,
+                  "key": "my-qp",
+                  "language": "ts",
+                  "name": "Sonar way",
+                },
+              ],
+              "tags": Array [],
+            },
+          ]
+        }
+        cycle={true}
+        metrics={
+          Object {
+            "coverage": Object {
+              "domain": "Coverage",
+              "id": "2",
+              "key": "coverage",
+              "name": "Coverage",
+              "type": "PERCENT",
+            },
+            "new_bugs": Object {
+              "domain": "Reliability",
+              "id": "4",
+              "key": "new_bugs",
+              "name": "New Bugs",
+              "type": "INT",
+            },
+          }
+        }
+        onEndOfList={[Function]}
+        onGoToParent={[Function]}
+        onHighlight={[Function]}
+        onSelect={[Function]}
+        rootComponent={
+          Object {
+            "breadcrumbs": Array [],
+            "key": "foo",
+            "name": "foo",
+            "qualifier": "SVW",
+          }
+        }
+      />
+    </div>
+    <ListFooter
+      count={1}
+      loadMore={[Function]}
+      total={1}
+    />
+  </div>
+</div>
+`;
+
+exports[`should render correclty when no sub component for TRK 1`] = `
+<div
+  className="page page-limited"
+>
+  <Suggestions
+    suggestions="code"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="code.page"
+  />
+  <A11ySkipTarget
+    anchor="code_main"
+  />
+  <div
+    className="code-components"
+  >
+    <div
+      className="display-flex-center display-flex-column no-file"
+    >
+      <span
+        className="h1 text-muted"
+      >
+        code_viewer.no_source_code_displayed_due_to_empty_analysis.TRK
+      </span>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render correclty when no sub component for TRK: no search 1`] = `
+<div
+  className="page page-limited"
+>
+  <Suggestions
+    suggestions="code"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="code.page"
+  />
+  <A11ySkipTarget
+    anchor="code_main"
+  />
+  <withRouter(Search)
+    component={
+      Object {
+        "breadcrumbs": Array [],
+        "key": "foo",
+        "name": "foo",
+        "qualifier": "TRK",
+      }
+    }
+    onSearchClear={[Function]}
+    onSearchResults={[Function]}
+  />
+  <div
+    className="code-components"
+  >
+    <div
+      className="boxed-group spacer-top search-results"
+    >
+      <withKeyboardNavigation(Components)
+        components={Array []}
+        metrics={Object {}}
+        onHighlight={[Function]}
+        onSelect={[Function]}
+        rootComponent={
+          Object {
+            "breadcrumbs": Array [],
+            "key": "foo",
+            "name": "foo",
+            "qualifier": "TRK",
+          }
+        }
+      />
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render correclty when no sub component for TRK: with sub component 1`] = `
+<div
+  className="page page-limited"
+>
+  <Suggestions
+    suggestions="code"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="code.page"
+  />
+  <A11ySkipTarget
+    anchor="code_main"
+  />
+  <withRouter(Search)
+    component={
+      Object {
+        "breadcrumbs": Array [],
+        "key": "foo",
+        "name": "foo",
+        "qualifier": "TRK",
+      }
+    }
+    onSearchClear={[Function]}
+    onSearchResults={[Function]}
+  />
+  <div
+    className="code-components"
+  >
+    <div
+      className="boxed-group spacer-top"
+    >
+      <withKeyboardNavigation(Components)
+        baseComponent={
+          Object {
+            "breadcrumbs": Array [],
+            "key": "foo",
+            "name": "foo",
+            "qualifier": "TRK",
+          }
+        }
+        components={
+          Array [
+            Object {
+              "breadcrumbs": Array [],
+              "key": "my-project",
+              "name": "MyProject",
+              "qualifier": "FIL",
+              "qualityGate": Object {
+                "isDefault": true,
+                "key": "30",
+                "name": "Sonar way",
+              },
+              "qualityProfiles": Array [
+                Object {
+                  "deleted": false,
+                  "key": "my-qp",
+                  "language": "ts",
+                  "name": "Sonar way",
+                },
+              ],
+              "tags": Array [],
+            },
+          ]
+        }
+        cycle={true}
+        metrics={
+          Object {
+            "coverage": Object {
+              "domain": "Coverage",
+              "id": "2",
+              "key": "coverage",
+              "name": "Coverage",
+              "type": "PERCENT",
+            },
+            "new_bugs": Object {
+              "domain": "Reliability",
+              "id": "4",
+              "key": "new_bugs",
+              "name": "New Bugs",
+              "type": "INT",
+            },
+          }
+        }
+        onEndOfList={[Function]}
+        onGoToParent={[Function]}
+        onHighlight={[Function]}
+        onSelect={[Function]}
+        rootComponent={
+          Object {
+            "breadcrumbs": Array [],
+            "key": "foo",
+            "name": "foo",
+            "qualifier": "TRK",
+          }
+        }
+      />
+    </div>
+    <ListFooter
+      count={1}
+      loadMore={[Function]}
+      total={1}
+    />
+  </div>
+</div>
+`;
+
+exports[`should render correclty when no sub component for VW 1`] = `
+<div
+  className="page page-limited"
+>
+  <Suggestions
+    suggestions="code"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="projects.page"
+  />
+  <A11ySkipTarget
+    anchor="code_main"
+  />
+  <div
+    className="code-components"
+  >
+    <div
+      className="display-flex-center display-flex-column no-file"
+    >
+      <span
+        className="h1 text-muted"
+      >
+        code_viewer.no_source_code_displayed_due_to_empty_analysis.VW
+      </span>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render correclty when no sub component for VW: no search 1`] = `
+<div
+  className="page page-limited"
+>
+  <Suggestions
+    suggestions="code"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="projects.page"
+  />
+  <A11ySkipTarget
+    anchor="code_main"
+  />
+  <withRouter(Search)
+    component={
+      Object {
+        "breadcrumbs": Array [],
+        "key": "foo",
+        "name": "foo",
+        "qualifier": "VW",
+      }
+    }
+    onSearchClear={[Function]}
+    onSearchResults={[Function]}
+  />
+  <div
+    className="code-components"
+  >
+    <div
+      className="boxed-group spacer-top search-results"
+    >
+      <withKeyboardNavigation(Components)
+        components={Array []}
+        metrics={Object {}}
+        onHighlight={[Function]}
+        onSelect={[Function]}
+        rootComponent={
+          Object {
+            "breadcrumbs": Array [],
+            "key": "foo",
+            "name": "foo",
+            "qualifier": "VW",
+          }
+        }
+      />
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render correclty when no sub component for VW: with sub component 1`] = `
+<div
+  className="page page-limited"
+>
+  <Suggestions
+    suggestions="code"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="projects.page"
+  />
+  <A11ySkipTarget
+    anchor="code_main"
+  />
+  <withRouter(Search)
+    component={
+      Object {
+        "breadcrumbs": Array [],
+        "key": "foo",
+        "name": "foo",
+        "qualifier": "VW",
+      }
+    }
+    onSearchClear={[Function]}
+    onSearchResults={[Function]}
+  />
+  <div
+    className="code-components"
+  >
+    <div
+      className="boxed-group spacer-top"
+    >
+      <withKeyboardNavigation(Components)
+        baseComponent={
+          Object {
+            "breadcrumbs": Array [],
+            "key": "foo",
+            "name": "foo",
+            "qualifier": "VW",
+          }
+        }
+        components={
+          Array [
+            Object {
+              "breadcrumbs": Array [],
+              "key": "my-project",
+              "name": "MyProject",
+              "qualifier": "FIL",
+              "qualityGate": Object {
+                "isDefault": true,
+                "key": "30",
+                "name": "Sonar way",
+              },
+              "qualityProfiles": Array [
+                Object {
+                  "deleted": false,
+                  "key": "my-qp",
+                  "language": "ts",
+                  "name": "Sonar way",
+                },
+              ],
+              "tags": Array [],
+            },
+          ]
+        }
+        cycle={true}
+        metrics={
+          Object {
+            "coverage": Object {
+              "domain": "Coverage",
+              "id": "2",
+              "key": "coverage",
+              "name": "Coverage",
+              "type": "PERCENT",
+            },
+            "new_bugs": Object {
+              "domain": "Reliability",
+              "id": "4",
+              "key": "new_bugs",
+              "name": "New Bugs",
+              "type": "INT",
+            },
+          }
+        }
+        onEndOfList={[Function]}
+        onGoToParent={[Function]}
+        onHighlight={[Function]}
+        onSelect={[Function]}
+        rootComponent={
+          Object {
+            "breadcrumbs": Array [],
+            "key": "foo",
+            "name": "foo",
+            "qualifier": "VW",
+          }
+        }
+      />
+    </div>
+    <ListFooter
+      count={1}
+      loadMore={[Function]}
+      total={1}
+    />
+  </div>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Search-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Search-test.tsx.snap
new file mode 100644 (file)
index 0000000..a1d47cd
--- /dev/null
@@ -0,0 +1,20 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correcly 1`] = `
+<div
+  className="code-search"
+  id="code-search"
+>
+  <SearchBox
+    minLength={3}
+    onChange={[Function]}
+    onKeyDown={[Function]}
+    placeholder="code.search_placeholder"
+    value=""
+  />
+  <DeferredSpinner
+    className="spacer-left"
+    loading={false}
+  />
+</div>
+`;
index f35e7d148610604cc3fe131b08bc18de0b7c554d..7a0eac5b17c76fb26ac8610681605a8a7f047f31 100644 (file)
@@ -21,7 +21,7 @@ import { lazyLoadComponent } from '../../components/lazyLoadComponent';
 
 const routes = [
   {
-    indexRoute: { component: lazyLoadComponent(() => import('./components/AppCode')) }
+    indexRoute: { component: lazyLoadComponent(() => import('./components/CodeApp')) }
   }
 ];
 
index 990ad003a5cd42c8579b479369bab77f9446109c..19149a524c24458028ea857862aa7c157acc0c92 100644 (file)
@@ -17,7 +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 { ComponentQualifier } from '../../types/component';
+import { ComponentQualifier, TreeComponent } from '../../types/component';
 import { MetricKey } from '../../types/metrics';
 import { mockMeasureEnhanced } from '../testMocks';
 
@@ -41,6 +41,16 @@ export function mockComponent(overrides: Partial<T.Component> = {}): T.Component
   };
 }
 
+export function mockTreeComponent(overrides: Partial<TreeComponent>): TreeComponent {
+  return {
+    key: 'my-key',
+    qualifier: ComponentQualifier.Project,
+    name: 'component',
+    visibility: 'public',
+    ...overrides
+  };
+}
+
 export function mockComponentMeasure(
   file = false,
   overrides: Partial<T.ComponentMeasure> = {}