From bbdad735a0340b03669e73b000b13d953cc9fedf Mon Sep 17 00:00:00 2001
From: Illya Klymov <xanf@xanf.me>
Date: Thu, 12 Sep 2019 05:04:22 +0000
Subject: [PATCH] Refactor DiffFileHeader tests

* Switched from Karma to Jest
* Reorganized tests to have correct semantics
* Removed unused computed properties from component
---
 .../diffs/components/diff_file_header.vue     |  58 +-
 .../diffs/components/diff_file_header_spec.js | 472 ++++++++++++
 .../diffs/components/diff_file_header_spec.js | 713 ------------------
 spec/javascripts/diffs/store/actions_spec.js  |   2 +-
 4 files changed, 489 insertions(+), 756 deletions(-)
 create mode 100644 spec/frontend/diffs/components/diff_file_header_spec.js
 delete mode 100644 spec/javascripts/diffs/components/diff_file_header_spec.js

diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 69ec6ab8600..bfcc726a030 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -57,26 +57,12 @@ export default {
       required: true,
     },
   },
-  data() {
-    return {
-      blobForkSuggestion: null,
-    };
-  },
   computed: {
     ...mapGetters('diffs', ['diffHasExpandedDiscussions', 'diffHasDiscussions']),
-    hasExpandedDiscussions() {
-      return this.diffHasExpandedDiscussions(this.diffFile);
-    },
     diffContentIDSelector() {
       return `#diff-content-${this.diffFile.file_hash}`;
     },
-    icon() {
-      if (this.diffFile.submodule) {
-        return 'archive';
-      }
 
-      return this.diffFile.blob.icon;
-    },
     titleLink() {
       if (this.diffFile.submodule) {
         return this.diffFile.submodule_tree_url || this.diffFile.submodule_link;
@@ -99,9 +85,6 @@ export default {
 
       return this.diffFile.file_path;
     },
-    titleTag() {
-      return this.diffFile.file_hash ? 'a' : 'span';
-    },
     isUsingLfs() {
       return this.diffFile.stored_externally && this.diffFile.external_storage === 'lfs';
     },
@@ -135,9 +118,6 @@ export default {
     isModeChanged() {
       return this.diffFile.viewer.name === diffViewerModes.mode_changed;
     },
-    showExpandDiffToFullFileEnabled() {
-      return gon.features.expandDiffFullFile && !this.diffFile.is_fully_expanded;
-    },
     expandDiffToFullFileTitle() {
       if (this.diffFile.isShowingFullFile) {
         return s__('MRDiff|Show changes only');
@@ -156,21 +136,12 @@ export default {
       'toggleFileDiscussionWrappers',
       'toggleFullDiff',
     ]),
-    handleToggleFile(e, checkTarget) {
-      if (
-        !checkTarget ||
-        e.target === this.$refs.header ||
-        (e.target.classList && e.target.classList.contains('diff-toggle-caret'))
-      ) {
-        this.$emit('toggleFile');
-      }
+    handleToggleFile() {
+      this.$emit('toggleFile');
     },
     showForkMessage() {
       this.$emit('showForkMessage');
     },
-    handleToggleDiscussions() {
-      this.toggleFileDiscussionWrappers(this.diffFile);
-    },
     handleFileNameClick(e) {
       const isLinkToOtherPage =
         this.diffFile.submodule_tree_url || this.diffFile.submodule_link || this.discussionPath;
@@ -178,7 +149,6 @@ export default {
       if (!isLinkToOtherPage) {
         e.preventDefault();
         const selector = this.diffContentIDSelector;
-
         scrollToElement(document.querySelector(selector));
         window.location.hash = selector;
       }
@@ -191,22 +161,23 @@ export default {
   <div
     ref="header"
     class="js-file-title file-title file-title-flex-parent"
-    @click="handleToggleFile($event, true)"
+    @click.self="handleToggleFile"
   >
     <div class="file-header-content">
       <icon
         v-if="collapsible"
+        ref="collapseIcon"
         :name="collapseIcon"
         :size="16"
         aria-hidden="true"
         class="diff-toggle-caret append-right-5"
-        @click.stop="handleToggle"
+        @click.stop="handleToggleFile"
       />
       <a
         v-once
         id="diffFile.file_path"
         ref="titleWrapper"
-        class="append-right-4 js-title-wrapper"
+        class="append-right-4"
         :href="titleLink"
         @click="handleFileNameClick"
       >
@@ -214,7 +185,7 @@ export default {
           :file-name="filePath"
           :size="18"
           aria-hidden="true"
-          css-classes="js-file-icon append-right-5"
+          css-classes="append-right-5"
         />
         <span v-if="isFileRenamed">
           <strong
@@ -260,12 +231,13 @@ export default {
         <template v-if="diffFile.blob && diffFile.blob.readable_text">
           <span v-gl-tooltip.hover :title="s__('MergeRequests|Toggle comments for this file')">
             <gl-button
+              ref="toggleDiscussionsButton"
               :disabled="!diffHasDiscussions(diffFile)"
-              :class="{ active: hasExpandedDiscussions }"
+              :class="{ active: diffHasExpandedDiscussions(diffFile) }"
               class="js-btn-vue-toggle-comments btn"
               data-qa-selector="toggle_comments_button"
               type="button"
-              @click="handleToggleDiscussions"
+              @click="toggleFileDiscussionWrappers(diffFile)"
             >
               <icon name="comment" />
             </gl-button>
@@ -282,8 +254,9 @@ export default {
 
         <a
           v-if="diffFile.replaced_view_path"
+          ref="replacedFileButton"
           :href="diffFile.replaced_view_path"
-          class="btn view-file js-view-replaced-file"
+          class="btn view-file"
           v-html="viewReplacedFileButtonText"
         >
         </a>
@@ -292,7 +265,7 @@ export default {
           ref="expandDiffToFullFileButton"
           v-gl-tooltip.hover
           :title="expandDiffToFullFileTitle"
-          class="expand-file js-expand-file"
+          class="expand-file"
           @click="toggleFullDiff(diffFile.file_path)"
         >
           <gl-loading-icon v-if="diffFile.isLoadingFullFile" color="dark" inline />
@@ -304,7 +277,7 @@ export default {
           v-gl-tooltip.hover
           :href="diffFile.view_path"
           target="blank"
-          class="view-file js-view-file-button"
+          class="view-file"
           :title="viewFileButtonText"
         >
           <icon name="doc-text" />
@@ -312,12 +285,13 @@ export default {
 
         <a
           v-if="diffFile.external_url"
+          ref="externalLink"
           v-gl-tooltip.hover
           :href="diffFile.external_url"
           :title="`View on ${diffFile.formatted_external_url}`"
           target="_blank"
           rel="noopener noreferrer"
-          class="btn btn-file-option js-external-url"
+          class="btn btn-file-option"
         >
           <icon name="external-link" />
         </a>
diff --git a/spec/frontend/diffs/components/diff_file_header_spec.js b/spec/frontend/diffs/components/diff_file_header_spec.js
new file mode 100644
index 00000000000..ac770c896bd
--- /dev/null
+++ b/spec/frontend/diffs/components/diff_file_header_spec.js
@@ -0,0 +1,472 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
+import EditButton from '~/diffs/components/edit_button.vue';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import diffDiscussionsMockData from '../mock_data/diff_discussions';
+import { truncateSha } from '~/lib/utils/text_utility';
+import { diffViewerModes } from '~/ide/constants';
+import { __, sprintf } from '~/locale';
+import { scrollToElement } from '~/lib/utils/common_utils';
+
+jest.mock('~/lib/utils/common_utils');
+
+const diffFile = Object.freeze(
+  Object.assign(diffDiscussionsMockData.diff_file, {
+    edit_path: 'link:/to/edit/path',
+    blob: {
+      id: '848ed9407c6730ff16edb3dd24485a0eea24292a',
+      path: 'lib/base.js',
+      name: 'base.js',
+      mode: '100644',
+      readable_text: true,
+      icon: 'file-text-o',
+    },
+  }),
+);
+
+describe('DiffFileHeader component', () => {
+  let wrapper;
+
+  const diffHasExpandedDiscussionsResultMock = jest.fn();
+  const diffHasDiscussionsResultMock = jest.fn();
+  const mockStoreConfig = {
+    state: {},
+    modules: {
+      diffs: {
+        namespaced: true,
+        getters: {
+          diffHasExpandedDiscussions: () => diffHasExpandedDiscussionsResultMock,
+          diffHasDiscussions: () => diffHasDiscussionsResultMock,
+        },
+        actions: {
+          toggleFileDiscussions: jest.fn(),
+          toggleFileDiscussionWrappers: jest.fn(),
+          toggleFullDiff: jest.fn(),
+        },
+      },
+    },
+  };
+
+  afterEach(() => {
+    [
+      diffHasDiscussionsResultMock,
+      diffHasExpandedDiscussionsResultMock,
+      ...Object.values(mockStoreConfig.modules.diffs.actions),
+    ].forEach(mock => mock.mockReset());
+  });
+
+  const findHeader = () => wrapper.find({ ref: 'header' });
+  const findTitleLink = () => wrapper.find({ ref: 'titleWrapper' });
+  const findExpandButton = () => wrapper.find({ ref: 'expandDiffToFullFileButton' });
+  const findFileActions = () => wrapper.find('.file-actions');
+  const findModeChangedLine = () => wrapper.find({ ref: 'fileMode' });
+  const findLfsLabel = () => wrapper.find('.label-lfs');
+  const findToggleDiscussionsButton = () => wrapper.find({ ref: 'toggleDiscussionsButton' });
+  const findExternalLink = () => wrapper.find({ ref: 'externalLink' });
+  const findReplacedFileButton = () => wrapper.find({ ref: 'replacedFileButton' });
+  const findViewFileButton = () => wrapper.find({ ref: 'viewButton' });
+  const findCollapseIcon = () => wrapper.find({ ref: 'collapseIcon' });
+
+  const findIconByName = iconName => {
+    const icons = wrapper.findAll(Icon).filter(w => w.props('name') === iconName);
+    if (icons.length === 0) return icons;
+    if (icons.length > 1) {
+      throw new Error(`Multiple icons found for ${iconName}`);
+    }
+    return icons.at(0);
+  };
+
+  const createComponent = props => {
+    const localVue = createLocalVue();
+    localVue.use(Vuex);
+    const store = new Vuex.Store(mockStoreConfig);
+
+    wrapper = shallowMount(DiffFileHeader, {
+      propsData: {
+        diffFile,
+        canCurrentUserFork: false,
+        ...props,
+      },
+      localVue,
+      store,
+      sync: false,
+    });
+  };
+
+  it.each`
+    visibility   | collapsible
+    ${'visible'} | ${true}
+    ${'hidden'}  | ${false}
+  `('collapse toggle is $visibility if collapsible is $collapsible', ({ collapsible }) => {
+    createComponent({ collapsible });
+    expect(findCollapseIcon().exists()).toBe(collapsible);
+  });
+
+  it.each`
+    expanded | icon
+    ${true}  | ${'chevron-down'}
+    ${false} | ${'chevron-right'}
+  `('collapse icon is $icon if expanded is $expanded', ({ icon, expanded }) => {
+    createComponent({ expanded, collapsible: true });
+    expect(findCollapseIcon().props('name')).toBe(icon);
+  });
+
+  it('when header is clicked emits toggleFile', () => {
+    createComponent();
+    findHeader().trigger('click');
+    expect(wrapper.emitted().toggleFile).toBeDefined();
+  });
+
+  it('when collapseIcon is clicked emits toggleFile', () => {
+    createComponent({ collapsible: true });
+    findCollapseIcon().vm.$emit('click', new Event('click'));
+    expect(wrapper.emitted().toggleFile).toBeDefined();
+  });
+
+  it('when other element in header is clicked does not emits toggleFile', () => {
+    createComponent({ collapsible: true });
+    findTitleLink().trigger('click');
+    expect(wrapper.emitted().toggleFile).not.toBeDefined();
+  });
+
+  it('displays a copy to clipboard button', () => {
+    createComponent();
+    expect(wrapper.find(ClipboardButton).exists()).toBe(true);
+  });
+
+  describe('for submodule', () => {
+    const submoduleDiffFile = {
+      ...diffFile,
+      submodule: true,
+      submodule_link: 'link://to/submodule',
+    };
+
+    it('prefers submodule_tree_url over submodule_link for href', () => {
+      const submoduleTreeUrl = 'some://tree/url';
+      createComponent({
+        discussionLink: 'discussionLink',
+        diffFile: {
+          ...submoduleDiffFile,
+          submodule_tree_url: 'some://tree/url',
+        },
+      });
+
+      expect(findTitleLink().attributes('href')).toBe(submoduleTreeUrl);
+    });
+
+    it('uses submodule_link for href if submodule_tree_url does not exists', () => {
+      const submoduleLink = 'link://to/submodule';
+      createComponent({
+        discussionLink: 'discussionLink',
+        diffFile: submoduleDiffFile,
+      });
+
+      expect(findTitleLink().attributes('href')).toBe(submoduleLink);
+    });
+
+    it('uses file_path + SHA as link text', () => {
+      createComponent({
+        diffFile: submoduleDiffFile,
+      });
+
+      expect(findTitleLink().text()).toContain(
+        `${diffFile.file_path} @ ${truncateSha(diffFile.blob.id)}`,
+      );
+    });
+
+    it('does not render file actions', () => {
+      createComponent({
+        diffFile: submoduleDiffFile,
+        addMergeRequestButtons: true,
+      });
+      expect(findFileActions().exists()).toBe(false);
+    });
+  });
+
+  describe('for any file', () => {
+    const otherModes = Object.keys(diffViewerModes).filter(m => m !== 'mode_changed');
+
+    it('when edit button emits showForkMessage event it is re-emitted', () => {
+      createComponent({
+        addMergeRequestButtons: true,
+      });
+      wrapper.find(EditButton).vm.$emit('showForkMessage');
+      expect(wrapper.emitted().showForkMessage).toBeDefined();
+    });
+
+    it('for mode_changed file mode displays mode changes', () => {
+      createComponent({
+        diffFile: {
+          ...diffFile,
+          a_mode: 'old-mode',
+          b_mode: 'new-mode',
+          viewer: {
+            ...diffFile.viewer,
+            name: diffViewerModes.mode_changed,
+          },
+        },
+      });
+      expect(findModeChangedLine().text()).toMatch(/old-mode.+new-mode/);
+    });
+
+    it.each(otherModes.map(m => [m]))('for %s file mode does not display mode changes', mode => {
+      createComponent({
+        diffFile: {
+          ...diffFile,
+          a_mode: 'old-mode',
+          b_mode: 'new-mode',
+          viewer: {
+            ...diffFile.viewer,
+            name: diffViewerModes[mode],
+          },
+        },
+      });
+      expect(findModeChangedLine().exists()).toBeFalsy();
+    });
+
+    it('displays the LFS label for files stored in LFS', () => {
+      createComponent({
+        diffFile: { ...diffFile, stored_externally: true, external_storage: 'lfs' },
+      });
+      expect(findLfsLabel().exists()).toBe(true);
+    });
+
+    it('does not display the LFS label for files stored in repository', () => {
+      createComponent({
+        diffFile: { ...diffFile, stored_externally: false },
+      });
+      expect(findLfsLabel().exists()).toBe(false);
+    });
+
+    it('does not render view replaced file button if no replaced view path is present', () => {
+      createComponent({
+        diffFile: { ...diffFile, replaced_view_path: null },
+      });
+      expect(findReplacedFileButton().exists()).toBe(false);
+    });
+
+    describe('when addMergeRequestButtons is false', () => {
+      it('does not render file actions', () => {
+        createComponent({ addMergeRequestButtons: false });
+        expect(findFileActions().exists()).toBe(false);
+      });
+      it('should not render edit button', () => {
+        createComponent({ addMergeRequestButtons: false });
+        expect(wrapper.find(EditButton).exists()).toBe(false);
+      });
+    });
+
+    describe('when addMergeRequestButtons is true', () => {
+      describe('without discussions', () => {
+        it('renders a disabled toggle discussions button', () => {
+          diffHasDiscussionsResultMock.mockReturnValue(false);
+          createComponent({ addMergeRequestButtons: true });
+          expect(findToggleDiscussionsButton().attributes('disabled')).toBe('true');
+        });
+      });
+
+      describe('with discussions', () => {
+        it('dispatches toggleFileDiscussionWrappers when user clicks on toggle discussions button', () => {
+          diffHasDiscussionsResultMock.mockReturnValue(true);
+          createComponent({ addMergeRequestButtons: true });
+          expect(findToggleDiscussionsButton().attributes('disabled')).toBeFalsy();
+          findToggleDiscussionsButton().vm.$emit('click');
+          expect(
+            mockStoreConfig.modules.diffs.actions.toggleFileDiscussionWrappers,
+          ).toHaveBeenCalledWith(expect.any(Object), diffFile, undefined);
+        });
+      });
+
+      it('should show edit button', () => {
+        createComponent({
+          addMergeRequestButtons: true,
+        });
+        expect(wrapper.find(EditButton).exists()).toBe(true);
+      });
+
+      describe('view on environment button', () => {
+        it('is displayed when external url is provided', () => {
+          const externalUrl = 'link://to/external';
+          const formattedExternalUrl = 'link://formatted';
+          createComponent({
+            diffFile: {
+              ...diffFile,
+              external_url: externalUrl,
+              formatted_external_url: formattedExternalUrl,
+            },
+            addMergeRequestButtons: true,
+          });
+          expect(findExternalLink().exists()).toBe(true);
+        });
+
+        it('is hidden by default', () => {
+          createComponent({ addMergeRequestButtons: true });
+          expect(findExternalLink().exists()).toBe(false);
+        });
+      });
+
+      describe('without file blob', () => {
+        beforeEach(() => {
+          createComponent({ diffFile: { ...diffFile, blob: false } });
+        });
+
+        it('should not render toggle discussions button', () => {
+          expect(findToggleDiscussionsButton().exists()).toBe(false);
+        });
+
+        it('should not render edit button', () => {
+          expect(wrapper.find(EditButton).exists()).toBe(false);
+        });
+      });
+      describe('with file blob', () => {
+        it('should render correct file view button', () => {
+          const viewPath = 'link://view-path';
+          createComponent({
+            diffFile: { ...diffFile, view_path: viewPath },
+            addMergeRequestButtons: true,
+          });
+          expect(findViewFileButton().attributes('href')).toBe(viewPath);
+          expect(findViewFileButton().attributes('data-original-title')).toEqual(
+            `View file @ ${diffFile.content_sha.substr(0, 8)}`,
+          );
+        });
+      });
+    });
+
+    describe('expand full file button', () => {
+      describe('when diff is fully expanded', () => {
+        it('is not rendered', () => {
+          createComponent({
+            diffFile: {
+              ...diffFile,
+              is_fully_expanded: true,
+            },
+          });
+          expect(findExpandButton().exists()).toBe(false);
+        });
+      });
+      describe('when diff is not fully expanded', () => {
+        const fullyNotExpandedFileProps = {
+          diffFile: {
+            ...diffFile,
+            is_fully_expanded: false,
+            edit_path: 'link/to/edit/path.txt',
+            isShowingFullFile: false,
+          },
+          addMergeRequestButtons: true,
+        };
+
+        it.each`
+          iconName         | isShowingFullFile
+          ${'doc-expand'}  | ${false}
+          ${'doc-changes'} | ${true}
+        `(
+          'shows $iconName when isShowingFullFile set to $isShowingFullFile',
+          ({ iconName, isShowingFullFile }) => {
+            createComponent({
+              ...fullyNotExpandedFileProps,
+              diffFile: { ...fullyNotExpandedFileProps.diffFile, isShowingFullFile },
+            });
+            expect(findIconByName(iconName).exists()).toBe(true);
+          },
+        );
+
+        it('renders expand to full file button if not showing full file already', () => {
+          createComponent(fullyNotExpandedFileProps);
+          expect(findExpandButton().exists()).toBe(true);
+        });
+
+        it('renders loading icon when loading full file', () => {
+          createComponent(fullyNotExpandedFileProps);
+          expect(findExpandButton().exists()).toBe(true);
+        });
+
+        it('toggles full diff on click', () => {
+          createComponent(fullyNotExpandedFileProps);
+          findExpandButton().vm.$emit('click');
+          expect(mockStoreConfig.modules.diffs.actions.toggleFullDiff).toHaveBeenCalled();
+        });
+      });
+    });
+
+    it('uses discussionPath for link if it is defined', () => {
+      const discussionPath = 'link://to/discussion';
+      createComponent({
+        discussionPath,
+      });
+      expect(findTitleLink().attributes('href')).toBe(discussionPath);
+    });
+
+    it('uses local anchor for link as last resort', () => {
+      createComponent();
+      expect(findTitleLink().attributes('href')).toMatch(/^#diff-content/);
+    });
+
+    describe('when local anchor for link is clicked', () => {
+      beforeEach(() => {
+        createComponent();
+      });
+
+      it('scrolls to target', () => {
+        findTitleLink().trigger('click');
+        expect(scrollToElement).toHaveBeenCalled();
+      });
+
+      it('updates anchor in URL', () => {
+        findTitleLink().trigger('click');
+        expect(window.location.href).toMatch(/#diff-content/);
+      });
+    });
+  });
+
+  describe('for new file', () => {
+    it('displays the path', () => {
+      createComponent({ diffFile: { ...diffFile, new_file: true } });
+      expect(findTitleLink().text()).toBe(diffFile.file_path);
+    });
+  });
+
+  describe('for deleted file', () => {
+    it('displays the path', () => {
+      createComponent({ diffFile: { ...diffFile, deleted_file: true } });
+      expect(findTitleLink().text()).toBe(
+        sprintf(__('%{filePath} deleted'), { filePath: diffFile.file_path }, false),
+      );
+    });
+
+    it('does not show edit button', () => {
+      createComponent({ diffFile: { ...diffFile, deleted_file: true } });
+      expect(wrapper.find(EditButton).exists()).toBe(false);
+    });
+  });
+
+  describe('for renamed file', () => {
+    it('displays old and new path if the file was renamed', () => {
+      createComponent({
+        diffFile: {
+          ...diffFile,
+          renamed_file: true,
+          old_path_html: 'old',
+          new_path_html: 'new',
+        },
+      });
+      expect(findTitleLink().text()).toMatch(/^old.+new/s);
+    });
+  });
+
+  describe('for replaced file', () => {
+    it('renders view replaced file button', () => {
+      const replacedViewPath = 'some/path';
+      createComponent({
+        diffFile: {
+          ...diffFile,
+          replaced_view_path: replacedViewPath,
+        },
+        addMergeRequestButtons: true,
+      });
+      expect(findReplacedFileButton().exists()).toBe(true);
+    });
+  });
+});
diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js
deleted file mode 100644
index 356e7a8f1fe..00000000000
--- a/spec/javascripts/diffs/components/diff_file_header_spec.js
+++ /dev/null
@@ -1,713 +0,0 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import diffsModule from '~/diffs/store/modules';
-import notesModule from '~/notes/stores/modules';
-import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
-import mountComponent, { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
-import diffDiscussionsMockData from '../mock_data/diff_discussions';
-import { diffViewerModes } from '~/ide/constants';
-
-Vue.use(Vuex);
-
-describe('diff_file_header', () => {
-  let vm;
-  let props;
-  const diffDiscussionMock = diffDiscussionsMockData;
-  const Component = Vue.extend(DiffFileHeader);
-
-  const store = new Vuex.Store({
-    modules: {
-      diffs: diffsModule(),
-      notes: notesModule(),
-    },
-  });
-
-  beforeEach(() => {
-    const diffFile = diffDiscussionMock.diff_file;
-
-    diffFile.added_lines = 2;
-    diffFile.removed_lines = 1;
-
-    props = {
-      diffFile: { ...diffFile },
-      canCurrentUserFork: false,
-    };
-  });
-
-  afterEach(() => {
-    vm.$destroy();
-  });
-
-  describe('computed', () => {
-    describe('icon', () => {
-      beforeEach(() => {
-        props.diffFile.blob.icon = 'file-text-o';
-      });
-
-      it('returns the blob icon for files', () => {
-        props.diffFile.submodule = false;
-
-        vm = mountComponentWithStore(Component, { props, store });
-
-        expect(vm.icon).toBe(props.diffFile.blob.icon);
-      });
-
-      it('returns the archive icon for submodules', () => {
-        props.diffFile.submodule = true;
-
-        vm = mountComponentWithStore(Component, { props, store });
-
-        expect(vm.icon).toBe('archive');
-      });
-    });
-
-    describe('titleLink', () => {
-      beforeEach(() => {
-        props.discussionPath = 'link://to/discussion';
-        Object.assign(props.diffFile, {
-          submodule_link: 'link://to/submodule',
-          submodule_tree_url: 'some://tree/url',
-        });
-      });
-
-      it('returns the discussionPath for files', () => {
-        props.diffFile.submodule = false;
-
-        vm = mountComponentWithStore(Component, { props, store });
-
-        expect(vm.titleLink).toBe(props.discussionPath);
-      });
-
-      it('returns the submoduleTreeUrl for submodules', () => {
-        props.diffFile.submodule = true;
-
-        vm = mountComponentWithStore(Component, { props, store });
-
-        expect(vm.titleLink).toBe(props.diffFile.submodule_tree_url);
-      });
-
-      it('returns the submoduleLink for submodules without submoduleTreeUrl', () => {
-        Object.assign(props.diffFile, {
-          submodule: true,
-          submodule_tree_url: null,
-        });
-
-        vm = mountComponentWithStore(Component, { props, store });
-
-        expect(vm.titleLink).toBe(props.diffFile.submodule_link);
-      });
-
-      it('sets the correct path to the discussion', () => {
-        props.discussionPath = 'link://to/discussion';
-        vm = mountComponentWithStore(Component, { props, store });
-        const href = vm.$el.querySelector('.js-title-wrapper').getAttribute('href');
-
-        expect(href).toBe(vm.discussionPath);
-      });
-    });
-
-    describe('filePath', () => {
-      beforeEach(() => {
-        Object.assign(props.diffFile, {
-          blob: { id: 'b10b1db10b1d' },
-          file_path: 'path/to/file',
-        });
-      });
-
-      it('returns the filePath for files', () => {
-        props.diffFile.submodule = false;
-
-        vm = mountComponentWithStore(Component, { props, store });
-
-        expect(vm.filePath).toBe(props.diffFile.file_path);
-      });
-
-      it('appends the truncated blob id for submodules', () => {
-        props.diffFile.submodule = true;
-
-        vm = mountComponentWithStore(Component, { props, store });
-
-        expect(vm.filePath).toBe(
-          `${props.diffFile.file_path} @ ${props.diffFile.blob.id.substr(0, 8)}`,
-        );
-      });
-    });
-
-    describe('titleTag', () => {
-      it('returns a link tag if fileHash is set', () => {
-        props.diffFile.file_hash = 'some hash';
-
-        vm = mountComponentWithStore(Component, { props, store });
-
-        expect(vm.titleTag).toBe('a');
-      });
-
-      it('returns a span tag if fileHash is not set', () => {
-        props.diffFile.file_hash = null;
-
-        vm = mountComponentWithStore(Component, { props, store });
-
-        expect(vm.titleTag).toBe('span');
-      });
-    });
-
-    describe('isUsingLfs', () => {
-      beforeEach(() => {
-        Object.assign(props.diffFile, {
-          stored_externally: true,
-          external_storage: 'lfs',
-        });
-      });
-
-      it('returns true if file is stored in LFS', () => {
-        vm = mountComponentWithStore(Component, { props, store });
-
-        expect(vm.isUsingLfs).toBe(true);
-      });
-
-      it('returns false if file is not stored externally', () => {
-        props.diffFile.stored_externally = false;
-
-        vm = mountComponentWithStore(Component, { props, store });
-
-        expect(vm.isUsingLfs).toBe(false);
-      });
-
-      it('returns false if file is not stored in LFS', () => {
-        props.diffFile.external_storage = 'not lfs';
-
-        vm = mountComponentWithStore(Component, { props, store });
-
-        expect(vm.isUsingLfs).toBe(false);
-      });
-    });
-
-    describe('collapseIcon', () => {
-      it('returns chevron-down if the diff is expanded', () => {
-        props.expanded = true;
-
-        vm = mountComponentWithStore(Component, { props, store });
-
-        expect(vm.collapseIcon).toBe('chevron-down');
-      });
-
-      it('returns chevron-right if the diff is collapsed', () => {
-        props.expanded = false;
-
-        vm = mountComponentWithStore(Component, { props, store });
-
-        expect(vm.collapseIcon).toBe('chevron-right');
-      });
-    });
-
-    describe('viewFileButtonText', () => {
-      it('contains the truncated content SHA', () => {
-        const dummySha = 'deebd00f is no SHA';
-        props.diffFile.content_sha = dummySha;
-
-        vm = mountComponentWithStore(Component, { props, store });
-
-        expect(vm.viewFileButtonText).not.toContain(dummySha);
-        expect(vm.viewFileButtonText).toContain(dummySha.substr(0, 8));
-      });
-    });
-
-    describe('viewReplacedFileButtonText', () => {
-      it('contains the truncated base SHA', () => {
-        const dummySha = 'deadabba sings no more';
-        props.diffFile.diff_refs.base_sha = dummySha;
-
-        vm = mountComponentWithStore(Component, { props, store });
-
-        expect(vm.viewReplacedFileButtonText).not.toContain(dummySha);
-        expect(vm.viewReplacedFileButtonText).toContain(dummySha.substr(0, 8));
-      });
-    });
-  });
-
-  describe('methods', () => {
-    describe('handleToggleFile', () => {
-      beforeEach(() => {
-        spyOn(vm, '$emit').and.stub();
-      });
-
-      it('emits toggleFile if checkTarget is false', () => {
-        vm.handleToggleFile(null, false);
-
-        expect(vm.$emit).toHaveBeenCalledWith('toggleFile');
-      });
-
-      it('emits toggleFile if checkTarget is true and event target is header', () => {
-        vm.handleToggleFile({ target: vm.$refs.header }, true);
-
-        expect(vm.$emit).toHaveBeenCalledWith('toggleFile');
-      });
-
-      it('does not emit toggleFile if checkTarget is true and event target is not header', () => {
-        vm.handleToggleFile({ target: 'not header' }, true);
-
-        expect(vm.$emit).not.toHaveBeenCalled();
-      });
-    });
-
-    describe('handleFileNameClick', () => {
-      let e;
-
-      beforeEach(() => {
-        e = { preventDefault: () => {} };
-        spyOn(e, 'preventDefault');
-      });
-
-      describe('when file name links to other page', () => {
-        it('does not call preventDefault if submodule tree url exists', () => {
-          vm = mountComponent(Component, {
-            ...props,
-            diffFile: { ...props.diffFile, submodule_tree_url: 'foobar.com' },
-          });
-
-          vm.handleFileNameClick(e);
-
-          expect(e.preventDefault).not.toHaveBeenCalled();
-        });
-
-        it('does not call preventDefault if submodule_link exists', () => {
-          vm = mountComponent(Component, {
-            ...props,
-            diffFile: { ...props.diffFile, submodule_link: 'foobar.com' },
-          });
-          vm.handleFileNameClick(e);
-
-          expect(e.preventDefault).not.toHaveBeenCalled();
-        });
-
-        it('does not call preventDefault if discussionPath exists', () => {
-          vm = mountComponent(Component, {
-            ...props,
-            discussionPath: 'Foo bar',
-          });
-
-          vm.handleFileNameClick(e);
-
-          expect(e.preventDefault).not.toHaveBeenCalled();
-        });
-      });
-
-      describe('scrolling to diff', () => {
-        let scrollToElement;
-        let el;
-
-        beforeEach(() => {
-          el = document.createElement('div');
-          spyOn(document, 'querySelector').and.returnValue(el);
-          scrollToElement = spyOnDependency(DiffFileHeader, 'scrollToElement');
-          vm = mountComponent(Component, props);
-
-          vm.handleFileNameClick(e);
-        });
-
-        it('calls scrollToElement with file content', () => {
-          expect(scrollToElement).toHaveBeenCalledWith(el);
-        });
-
-        it('element adds the content id to the window location', () => {
-          expect(window.location.hash).toContain(props.diffFile.file_hash);
-        });
-
-        it('calls preventDefault when button does not link to other page', () => {
-          expect(e.preventDefault).toHaveBeenCalled();
-        });
-      });
-    });
-  });
-
-  describe('template', () => {
-    describe('collapse toggle', () => {
-      const collapseToggle = () => vm.$el.querySelector('.diff-toggle-caret');
-
-      it('is visible if collapsible is true', () => {
-        props.collapsible = true;
-
-        vm = mountComponentWithStore(Component, { props, store });
-
-        expect(collapseToggle()).not.toBe(null);
-      });
-
-      it('is hidden if collapsible is false', () => {
-        props.collapsible = false;
-
-        vm = mountComponentWithStore(Component, { props, store });
-
-        expect(collapseToggle()).toBe(null);
-      });
-    });
-
-    it('displays an file icon in the title', () => {
-      vm = mountComponentWithStore(Component, { props, store });
-
-      expect(vm.$el.querySelector('svg.js-file-icon use').getAttribute('xlink:href')).toContain(
-        'ruby',
-      );
-    });
-
-    describe('file paths', () => {
-      const filePaths = () => vm.$el.querySelectorAll('.file-title-name');
-
-      it('displays the path of a added file', () => {
-        props.diffFile.renamed_file = false;
-
-        vm = mountComponentWithStore(Component, { props, store });
-
-        expect(filePaths()).toHaveLength(1);
-        expect(filePaths()[0]).toHaveText(props.diffFile.file_path);
-      });
-
-      it('displays path for deleted file', () => {
-        props.diffFile.renamed_file = false;
-        props.diffFile.deleted_file = true;
-
-        vm = mountComponentWithStore(Component, { props, store });
-
-        expect(filePaths()).toHaveLength(1);
-        expect(filePaths()[0]).toHaveText(`${props.diffFile.file_path} deleted`);
-      });
-
-      it('displays old and new path if the file was renamed', () => {
-        props.diffFile.renamed_file = true;
-
-        vm = mountComponentWithStore(Component, { props, store });
-
-        expect(filePaths()).toHaveLength(2);
-        expect(filePaths()[0]).toHaveText(props.diffFile.old_path_html);
-        expect(filePaths()[1]).toHaveText(props.diffFile.new_path_html);
-      });
-    });
-
-    it('displays a copy to clipboard button', () => {
-      vm = mountComponentWithStore(Component, { props, store });
-
-      const button = vm.$el.querySelector('.btn-clipboard');
-
-      expect(button).not.toBe(null);
-      expect(button.dataset.clipboardText).toBe('{"text":"CHANGELOG.rb","gfm":"`CHANGELOG.rb`"}');
-    });
-
-    describe('file mode', () => {
-      it('it displays old and new file mode if it changed', () => {
-        props.diffFile.viewer.name = diffViewerModes.mode_changed;
-
-        vm = mountComponentWithStore(Component, { props, store });
-
-        const { fileMode } = vm.$refs;
-
-        expect(fileMode).not.toBe(undefined);
-        expect(fileMode).toContainText(props.diffFile.a_mode);
-        expect(fileMode).toContainText(props.diffFile.b_mode);
-      });
-
-      it('does not display the file mode if it has not changed', () => {
-        props.diffFile.viewer.name = diffViewerModes.text;
-
-        vm = mountComponentWithStore(Component, { props, store });
-
-        const { fileMode } = vm.$refs;
-
-        expect(fileMode).toBe(undefined);
-      });
-    });
-
-    describe('LFS label', () => {
-      const lfsLabel = () => vm.$el.querySelector('.label-lfs');
-
-      it('displays the LFS label for files stored in LFS', () => {
-        Object.assign(props.diffFile, {
-          stored_externally: true,
-          external_storage: 'lfs',
-        });
-
-        vm = mountComponentWithStore(Component, { props, store });
-
-        expect(lfsLabel()).not.toBe(null);
-        expect(lfsLabel()).toHaveText('LFS');
-      });
-
-      it('does not display the LFS label for files stored in repository', () => {
-        props.diffFile.stored_externally = false;
-
-        vm = mountComponentWithStore(Component, { props, store });
-
-        expect(lfsLabel()).toBe(null);
-      });
-    });
-
-    describe('edit button', () => {
-      it('should not render edit button if addMergeRequestButtons is not true', () => {
-        vm = mountComponentWithStore(Component, { props, store });
-
-        expect(vm.$el.querySelector('.js-edit-blob')).toEqual(null);
-      });
-
-      it('should show edit button when file is editable', () => {
-        props.addMergeRequestButtons = true;
-        props.diffFile.edit_path = '/';
-        vm = mountComponentWithStore(Component, { props, store });
-
-        expect(vm.$el.querySelector('.js-edit-blob')).not.toBe(null);
-      });
-
-      it('should not show edit button when file is deleted', () => {
-        props.addMergeRequestButtons = true;
-        props.diffFile.deleted_file = true;
-        props.diffFile.edit_path = '/';
-        vm = mountComponentWithStore(Component, { props, store });
-
-        expect(vm.$el.querySelector('.js-edit-blob')).toEqual(null);
-      });
-    });
-
-    describe('addMergeRequestButtons', () => {
-      beforeEach(() => {
-        props.addMergeRequestButtons = true;
-        props.diffFile.edit_path = '';
-      });
-
-      describe('view on environment button', () => {
-        const url = 'some.external.url/';
-        const title = 'url.title';
-
-        it('displays link to external url', () => {
-          props.diffFile.external_url = url;
-          props.diffFile.formatted_external_url = title;
-
-          vm = mountComponentWithStore(Component, { props, store });
-
-          expect(vm.$el.querySelector(`a[href="${url}"]`)).not.toBe(null);
-          expect(vm.$el.querySelector(`a[data-original-title="View on ${title}"]`)).not.toBe(null);
-        });
-
-        it('hides link if no external url', () => {
-          props.diffFile.external_url = '';
-          props.diffFile.formattedExternal_url = title;
-
-          vm = mountComponentWithStore(Component, { props, store });
-
-          expect(vm.$el.querySelector(`a[data-original-title="View on ${title}"]`)).toBe(null);
-        });
-      });
-    });
-
-    describe('handles toggle discussions', () => {
-      it('renders a disabled button when diff has no discussions', () => {
-        const propsCopy = Object.assign({}, props);
-        propsCopy.diffFile.submodule = false;
-        propsCopy.diffFile.blob = {
-          id: '848ed9407c6730ff16edb3dd24485a0eea24292a',
-          path: 'lib/base.js',
-          name: 'base.js',
-          mode: '100644',
-          readable_text: true,
-          icon: 'file-text-o',
-        };
-        propsCopy.addMergeRequestButtons = true;
-        propsCopy.diffFile.deleted_file = true;
-
-        vm = mountComponentWithStore(Component, {
-          props: propsCopy,
-          store,
-        });
-
-        expect(
-          vm.$el.querySelector('.js-btn-vue-toggle-comments').getAttribute('disabled'),
-        ).toEqual('disabled');
-      });
-
-      describe('with discussions', () => {
-        it('dispatches toggleFileDiscussionWrappers when user clicks on toggle discussions button', () => {
-          const propsCopy = Object.assign({}, props);
-          propsCopy.diffFile.submodule = false;
-          propsCopy.diffFile.blob = {
-            id: '848ed9407c6730ff16edb3dd24485a0eea24292a',
-            path: 'lib/base.js',
-            name: 'base.js',
-            mode: '100644',
-            readable_text: true,
-            icon: 'file-text-o',
-          };
-          propsCopy.addMergeRequestButtons = true;
-          propsCopy.diffFile.deleted_file = true;
-
-          const discussionGetter = () => [
-            {
-              ...diffDiscussionMock,
-            },
-          ];
-          const notesModuleMock = notesModule();
-          notesModuleMock.getters.discussions = discussionGetter;
-          vm = mountComponentWithStore(Component, {
-            props: propsCopy,
-            store: new Vuex.Store({
-              modules: {
-                diffs: diffsModule(),
-                notes: notesModuleMock,
-              },
-            }),
-          });
-
-          spyOn(vm, 'toggleFileDiscussionWrappers');
-
-          vm.$el.querySelector('.js-btn-vue-toggle-comments').click();
-
-          expect(vm.toggleFileDiscussionWrappers).toHaveBeenCalled();
-        });
-      });
-    });
-
-    describe('file actions', () => {
-      it('should not render if diff file has a submodule', () => {
-        props.diffFile.submodule = 'submodule';
-        vm = mountComponentWithStore(Component, { props, store });
-
-        expect(vm.$el.querySelector('.file-actions')).toEqual(null);
-      });
-
-      it('should not render if add merge request buttons is false', () => {
-        props.addMergeRequestButtons = false;
-        vm = mountComponentWithStore(Component, { props, store });
-
-        expect(vm.$el.querySelector('.file-actions')).toEqual(null);
-      });
-
-      describe('with add merge request buttons enabled', () => {
-        beforeEach(() => {
-          props.addMergeRequestButtons = true;
-          props.diffFile.edit_path = 'edit-path';
-        });
-
-        const viewReplacedFileButton = () => vm.$el.querySelector('.js-view-replaced-file');
-        const viewFileButton = () => vm.$el.querySelector('.js-view-file-button');
-        const externalUrl = () => vm.$el.querySelector('.js-external-url');
-
-        it('should render if add merge request buttons is true and diff file does not have a submodule', () => {
-          vm = mountComponentWithStore(Component, { props, store });
-
-          expect(vm.$el.querySelector('.file-actions')).not.toEqual(null);
-        });
-
-        it('should not render view replaced file button if no replaced view path is present', () => {
-          vm = mountComponentWithStore(Component, { props, store });
-
-          expect(viewReplacedFileButton()).toEqual(null);
-        });
-
-        it('should render view replaced file button if replaced view path is present', () => {
-          props.diffFile.replaced_view_path = 'replaced-view-path';
-          vm = mountComponentWithStore(Component, { props, store });
-
-          expect(viewReplacedFileButton()).not.toEqual(null);
-          expect(viewReplacedFileButton().getAttribute('href')).toBe('replaced-view-path');
-        });
-
-        it('should render correct file view button path', () => {
-          props.diffFile.view_path = 'view-path';
-          vm = mountComponentWithStore(Component, { props, store });
-
-          expect(viewFileButton().getAttribute('href')).toBe('view-path');
-          expect(viewFileButton().getAttribute('data-original-title')).toEqual(
-            `View file @ ${props.diffFile.content_sha.substr(0, 8)}`,
-          );
-        });
-
-        it('should not render external url view link if diff file has no external url', () => {
-          vm = mountComponentWithStore(Component, { props, store });
-
-          expect(externalUrl()).toEqual(null);
-        });
-
-        it('should render external url view link if diff file has external url', () => {
-          props.diffFile.external_url = 'external_url';
-          vm = mountComponentWithStore(Component, { props, store });
-
-          expect(externalUrl()).not.toEqual(null);
-          expect(externalUrl().getAttribute('href')).toBe('external_url');
-        });
-      });
-
-      describe('without file blob', () => {
-        beforeEach(() => {
-          props.diffFile.blob = null;
-          props.addMergeRequestButtons = true;
-          vm = mountComponentWithStore(Component, { props, store });
-        });
-
-        it('should not render toggle discussions button', () => {
-          expect(vm.$el.querySelector('.js-btn-vue-toggle-comments')).toEqual(null);
-        });
-
-        it('should not render edit button', () => {
-          expect(vm.$el.querySelector('.js-edit-blob')).toEqual(null);
-        });
-      });
-    });
-  });
-
-  describe('expand full file button', () => {
-    beforeEach(() => {
-      props.addMergeRequestButtons = true;
-      props.diffFile.edit_path = '/';
-    });
-
-    it('does not render button', () => {
-      vm = mountComponentWithStore(Component, { props, store });
-
-      expect(vm.$el.querySelector('.js-expand-file')).toBe(null);
-    });
-
-    it('renders button', () => {
-      props.diffFile.is_fully_expanded = false;
-
-      vm = mountComponentWithStore(Component, { props, store });
-
-      expect(vm.$el.querySelector('.js-expand-file')).not.toBe(null);
-    });
-
-    it('shows fully expanded text', () => {
-      props.diffFile.is_fully_expanded = false;
-      props.diffFile.isShowingFullFile = true;
-
-      vm = mountComponentWithStore(Component, { props, store });
-
-      expect(vm.$el.querySelector('.ic-doc-changes')).not.toBeNull();
-    });
-
-    it('shows expand text', () => {
-      props.diffFile.is_fully_expanded = false;
-
-      vm = mountComponentWithStore(Component, { props, store });
-
-      expect(vm.$el.querySelector('.ic-doc-expand')).not.toBeNull();
-    });
-
-    it('renders loading icon', () => {
-      props.diffFile.is_fully_expanded = false;
-      props.diffFile.isLoadingFullFile = true;
-
-      vm = mountComponentWithStore(Component, { props, store });
-
-      expect(vm.$el.querySelector('.js-expand-file .loading-container')).not.toBe(null);
-    });
-
-    it('calls toggleFullDiff on click', () => {
-      props.diffFile.is_fully_expanded = false;
-
-      vm = mountComponentWithStore(Component, { props, store });
-
-      spyOn(vm.$store, 'dispatch').and.stub();
-
-      vm.$el.querySelector('.js-expand-file').click();
-
-      expect(vm.$store.dispatch).toHaveBeenCalledWith(
-        'diffs/toggleFullDiff',
-        props.diffFile.file_path,
-      );
-    });
-  });
-});
diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js
index 5806cb47034..874891fcc6e 100644
--- a/spec/javascripts/diffs/store/actions_spec.js
+++ b/spec/javascripts/diffs/store/actions_spec.js
@@ -206,7 +206,7 @@ describe('DiffsStoreActions', () => {
                   position_type: 'text',
                 },
               },
-              hash: 'diff-content-1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a',
+              hash: 'ABC_123',
             },
           },
         ],
-- 
2.30.9