Commit 78e9efaf authored by Justin Boyson's avatar Justin Boyson Committed by Phil Hughes

Add drag to select lines

Updates mr diffs to use draggable events to select lines for comment
parent b674bce0
...@@ -56,10 +56,11 @@ export default { ...@@ -56,10 +56,11 @@ export default {
}, },
computed: { computed: {
...mapState({ ...mapState({
noteableData: (state) => state.notes.noteableData, diffViewType: ({ diffs }) => diffs.diffViewType,
diffViewType: (state) => state.diffs.diffViewType, showSuggestPopover: ({ diffs }) => diffs.showSuggestPopover,
noteableData: ({ notes }) => notes.noteableData,
selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition,
}), }),
...mapState('diffs', ['showSuggestPopover']),
...mapGetters('diffs', ['getDiffFileByHash', 'diffLines']), ...mapGetters('diffs', ['getDiffFileByHash', 'diffLines']),
...mapGetters([ ...mapGetters([
'isLoggedIn', 'isLoggedIn',
...@@ -126,6 +127,10 @@ export default { ...@@ -126,6 +127,10 @@ export default {
this.initAutoSave(this.noteableData, keys); this.initAutoSave(this.noteableData, keys);
} }
if (this.selectedCommentPosition) {
this.commentLineStart = this.selectedCommentPosition.start;
}
}, },
methods: { methods: {
...mapActions('diffs', [ ...mapActions('diffs', [
......
...@@ -10,6 +10,7 @@ import { ...@@ -10,6 +10,7 @@ import {
CONFLICT_THEIR, CONFLICT_THEIR,
CONFLICT_MARKER, CONFLICT_MARKER,
} from '../constants'; } from '../constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DiffGutterAvatars from './diff_gutter_avatars.vue'; import DiffGutterAvatars from './diff_gutter_avatars.vue';
import * as utils from './diff_row_utils'; import * as utils from './diff_row_utils';
...@@ -22,6 +23,7 @@ export default { ...@@ -22,6 +23,7 @@ export default {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
SafeHtml, SafeHtml,
}, },
mixins: [glFeatureFlagsMixin()],
props: { props: {
fileHash: { fileHash: {
type: String, type: String,
...@@ -45,6 +47,15 @@ export default { ...@@ -45,6 +47,15 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
index: {
type: Number,
required: true,
},
},
data() {
return {
dragging: false,
};
}, },
computed: { computed: {
...mapGetters('diffs', ['fileLineCoverage']), ...mapGetters('diffs', ['fileLineCoverage']),
...@@ -52,26 +63,35 @@ export default { ...@@ -52,26 +63,35 @@ export default {
...mapState({ ...mapState({
isHighlighted(state) { isHighlighted(state) {
const line = this.line.left?.line_code ? this.line.left : this.line.right; const line = this.line.left?.line_code ? this.line.left : this.line.right;
return utils.isHighlighted(state, line, this.isCommented); return utils.isHighlighted(state, line, false);
}, },
}), }),
classNameMap() { classNameMap() {
return { return {
[CONTEXT_LINE_CLASS_NAME]: this.line.isContextLineLeft, [CONTEXT_LINE_CLASS_NAME]: this.line.isContextLineLeft,
[PARALLEL_DIFF_VIEW_TYPE]: !this.inline, [PARALLEL_DIFF_VIEW_TYPE]: !this.inline,
commented: this.isCommented,
}; };
}, },
parallelViewLeftLineType() { parallelViewLeftLineType() {
return utils.parallelViewLeftLineType(this.line, this.isHighlighted); return utils.parallelViewLeftLineType(this.line, this.isHighlighted || this.isCommented);
}, },
coverageState() { coverageState() {
return this.fileLineCoverage(this.filePath, this.line.right.new_line); return this.fileLineCoverage(this.filePath, this.line.right.new_line);
}, },
classNameMapCellLeft() { classNameMapCellLeft() {
return utils.classNameMapCell(this.line.left, this.isHighlighted, this.isLoggedIn); return utils.classNameMapCell({
line: this.line.left,
hll: this.isHighlighted || this.isCommented,
isLoggedIn: this.isLoggedIn,
});
}, },
classNameMapCellRight() { classNameMapCellRight() {
return utils.classNameMapCell(this.line.right, this.isHighlighted, this.isLoggedIn); return utils.classNameMapCell({
line: this.line.right,
hll: this.isHighlighted || this.isCommented,
isLoggedIn: this.isLoggedIn,
});
}, },
addCommentTooltipLeft() { addCommentTooltipLeft() {
return utils.addCommentTooltip(this.line.left); return utils.addCommentTooltip(this.line.left);
...@@ -131,6 +151,22 @@ export default { ...@@ -131,6 +151,22 @@ export default {
? this.$options.THEIR_CHANGES ? this.$options.THEIR_CHANGES
: this.$options.OUR_CHANGES; : this.$options.OUR_CHANGES;
}, },
onDragEnd() {
this.dragging = false;
if (!this.glFeatures.dragCommentSelection) return;
this.$emit('stopdragging');
},
onDragEnter(line, index) {
if (!this.glFeatures.dragCommentSelection) return;
this.$emit('enterdragging', { ...line, index });
},
onDragStart(line) {
this.$root.$emit('bv::hide::tooltip');
this.dragging = true;
this.$emit('startdragging', line);
},
}, },
OUR_CHANGES: 'HEAD//our changes', OUR_CHANGES: 'HEAD//our changes',
THEIR_CHANGES: 'origin//their changes', THEIR_CHANGES: 'origin//their changes',
...@@ -143,7 +179,13 @@ export default { ...@@ -143,7 +179,13 @@ export default {
<template> <template>
<div :class="classNameMap" class="diff-grid-row diff-tr line_holder"> <div :class="classNameMap" class="diff-grid-row diff-tr line_holder">
<div class="diff-grid-left left-side"> <div
data-testid="left-side"
class="diff-grid-left left-side"
@dragover.prevent
@dragenter="onDragEnter(line.left, index)"
@dragend="onDragEnd"
>
<template v-if="line.left && line.left.type !== $options.CONFLICT_MARKER"> <template v-if="line.left && line.left.type !== $options.CONFLICT_MARKER">
<div <div
:class="classNameMapCellLeft" :class="classNameMapCellLeft"
...@@ -159,10 +201,13 @@ export default { ...@@ -159,10 +201,13 @@ export default {
:title="addCommentTooltipLeft" :title="addCommentTooltipLeft"
> >
<button <button
:draggable="glFeatures.dragCommentSelection"
type="button" type="button"
class="add-diff-note note-button js-add-diff-note-button qa-diff-comment" class="add-diff-note note-button js-add-diff-note-button qa-diff-comment"
:class="{ 'gl-cursor-grab': dragging }"
:disabled="line.left.commentsDisabled" :disabled="line.left.commentsDisabled"
@click="handleCommentButton(line.left)" @click="handleCommentButton(line.left)"
@dragstart="onDragStart({ ...line.left, index })"
> >
<gl-icon :size="12" name="comment" /> <gl-icon :size="12" name="comment" />
</button> </button>
...@@ -234,7 +279,14 @@ export default { ...@@ -234,7 +279,14 @@ export default {
></div> ></div>
</template> </template>
</div> </div>
<div v-if="!inline" class="diff-grid-right right-side"> <div
v-if="!inline"
data-testid="right-side"
class="diff-grid-right right-side"
@dragover.prevent
@dragenter="onDragEnter(line.right, index)"
@dragend="onDragEnd"
>
<template v-if="line.right"> <template v-if="line.right">
<div :class="classNameMapCellRight" class="diff-td diff-line-num new_line"> <div :class="classNameMapCellRight" class="diff-td diff-line-num new_line">
<template v-if="line.right.type !== $options.CONFLICT_MARKER_THEIR"> <template v-if="line.right.type !== $options.CONFLICT_MARKER_THEIR">
...@@ -246,10 +298,13 @@ export default { ...@@ -246,10 +298,13 @@ export default {
:title="addCommentTooltipRight" :title="addCommentTooltipRight"
> >
<button <button
:draggable="glFeatures.dragCommentSelection"
type="button" type="button"
class="add-diff-note note-button js-add-diff-note-button qa-diff-comment" class="add-diff-note note-button js-add-diff-note-button qa-diff-comment"
:class="{ 'gl-cursor-grab': dragging }"
:disabled="line.right.commentsDisabled" :disabled="line.right.commentsDisabled"
@click="handleCommentButton(line.right)" @click="handleCommentButton(line.right)"
@dragstart="onDragStart({ ...line.right, index })"
> >
<gl-icon :size="12" name="comment" /> <gl-icon :size="12" name="comment" />
</button> </button>
...@@ -279,13 +334,21 @@ export default { ...@@ -279,13 +334,21 @@ export default {
<div <div
v-gl-tooltip.hover v-gl-tooltip.hover
:title="coverageState.text" :title="coverageState.text"
:class="[line.right.type, coverageState.class, { hll: isHighlighted }]" :class="[line.right.type, coverageState.class, { hll: isHighlighted, hll: isCommented }]"
class="diff-td line-coverage right-side" class="diff-td line-coverage right-side"
></div> ></div>
<div <div
:id="line.right.line_code" :id="line.right.line_code"
:key="line.right.rich_text" :key="line.right.rich_text"
:class="[line.right.type, { hll: isHighlighted, parallel: !inline }]" v-safe-html="line.right.rich_text"
:class="[
line.right.type,
{
hll: isHighlighted,
hll: isCommented,
parallel: !inline,
},
]"
class="diff-td line_content with-coverage right-side" class="diff-td line_content with-coverage right-side"
@mousedown="handleParallelLineMouseDown" @mousedown="handleParallelLineMouseDown"
> >
......
...@@ -35,7 +35,7 @@ export const lineCode = (line) => { ...@@ -35,7 +35,7 @@ export const lineCode = (line) => {
return line.line_code || line.left?.line_code || line.right?.line_code; return line.line_code || line.left?.line_code || line.right?.line_code;
}; };
export const classNameMapCell = (line, hll, isLoggedIn, isHover) => { export const classNameMapCell = ({ line, hll, isLoggedIn, isHover }) => {
if (!line) return []; if (!line) return [];
const { type } = line; const { type } = line;
...@@ -54,7 +54,9 @@ export const addCommentTooltip = (line) => { ...@@ -54,7 +54,9 @@ export const addCommentTooltip = (line) => {
let tooltip; let tooltip;
if (!line) return tooltip; if (!line) return tooltip;
tooltip = __('Add a comment to this line'); tooltip = gon.drag_comment_selection
? __('Add a comment to this line or drag for multiple lines')
: __('Add a comment to this line');
const brokenSymlinks = line.commentsDisabled; const brokenSymlinks = line.commentsDisabled;
if (brokenSymlinks) { if (brokenSymlinks) {
......
<script> <script>
import { mapGetters, mapState } from 'vuex'; import { mapGetters, mapState, mapActions } from 'vuex';
import draftCommentsMixin from '~/diffs/mixins/draft_comments'; import draftCommentsMixin from '~/diffs/mixins/draft_comments';
import DraftNote from '~/batch_comments/components/draft_note.vue'; import DraftNote from '~/batch_comments/components/draft_note.vue';
import DiffRow from './diff_row.vue'; import DiffRow from './diff_row.vue';
...@@ -35,6 +35,12 @@ export default { ...@@ -35,6 +35,12 @@ export default {
default: false, default: false,
}, },
}, },
data() {
return {
dragStart: null,
updatedLineRange: null,
};
},
computed: { computed: {
...mapGetters('diffs', ['commitId']), ...mapGetters('diffs', ['commitId']),
...mapState({ ...mapState({
...@@ -52,12 +58,39 @@ export default { ...@@ -52,12 +58,39 @@ export default {
}, },
}, },
methods: { methods: {
...mapActions(['setSelectedCommentPosition']),
...mapActions('diffs', ['showCommentForm']),
showCommentLeft(line) { showCommentLeft(line) {
return !this.inline || line.left; return !this.inline || line.left;
}, },
showCommentRight(line) { showCommentRight(line) {
return !this.inline || (line.right && !line.left); return !this.inline || (line.right && !line.left);
}, },
onStartDragging(line) {
this.dragStart = line;
},
onDragOver(line) {
if (line.chunk !== this.dragStart.chunk) return;
let start = this.dragStart;
let end = line;
if (this.dragStart.index >= line.index) {
start = line;
end = this.dragStart;
}
this.updatedLineRange = { start, end };
this.setSelectedCommentPosition(this.updatedLineRange);
},
onStopDragging() {
this.showCommentForm({
lineCode: this.updatedLineRange?.end?.line_code,
fileHash: this.diffFile.file_hash,
});
this.dragStart = null;
},
}, },
userColorScheme: window.gon.user_color_scheme, userColorScheme: window.gon.user_color_scheme,
}; };
...@@ -94,6 +127,10 @@ export default { ...@@ -94,6 +127,10 @@ export default {
:is-bottom="index + 1 === diffLinesLength" :is-bottom="index + 1 === diffLinesLength"
:is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine" :is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine"
:inline="inline" :inline="inline"
:index="index"
@enterdragging="onDragOver"
@startdragging="onStartDragging"
@stopdragging="onStopDragging"
/> />
<div <div
v-if="line.renderCommentRow" v-if="line.renderCommentRow"
......
...@@ -72,7 +72,12 @@ export default { ...@@ -72,7 +72,12 @@ export default {
return this.fileLineCoverage(this.filePath, this.line.new_line); return this.fileLineCoverage(this.filePath, this.line.new_line);
}, },
classNameMapCell() { classNameMapCell() {
return classNameMapCell(this.line, this.isHighlighted, this.isLoggedIn, this.isHover); return classNameMapCell({
line: this.line,
hll: this.isHighlighted,
isLoggedIn: this.isLoggedIn,
isHover: this.isHover,
});
}, },
addCommentTooltip() { addCommentTooltip() {
return addCommentTooltip(this.line); return addCommentTooltip(this.line);
......
...@@ -68,20 +68,20 @@ export default { ...@@ -68,20 +68,20 @@ export default {
return this.fileLineCoverage(this.filePath, this.line.right.new_line); return this.fileLineCoverage(this.filePath, this.line.right.new_line);
}, },
classNameMapCellLeft() { classNameMapCellLeft() {
return utils.classNameMapCell( return utils.classNameMapCell({
this.line.left, line: this.line.left,
this.isHighlighted, hll: this.isHighlighted,
this.isLoggedIn, isLoggedIn: this.isLoggedIn,
this.isLeftHover, isHover: this.isLeftHover,
); });
}, },
classNameMapCellRight() { classNameMapCellRight() {
return utils.classNameMapCell( return utils.classNameMapCell({
this.line.right, line: this.line.right,
this.isHighlighted, hll: this.isHighlighted,
this.isLoggedIn, isLoggedIn: this.isLoggedIn,
this.isRightHover, isHover: this.isRightHover,
); });
}, },
addCommentTooltipLeft() { addCommentTooltipLeft() {
return utils.addCommentTooltip(this.line.left); return utils.addCommentTooltip(this.line.left);
......
...@@ -55,8 +55,17 @@ export const parallelizeDiffLines = (diffLines, inline) => { ...@@ -55,8 +55,17 @@ export const parallelizeDiffLines = (diffLines, inline) => {
let conflictStartIndex = -1; let conflictStartIndex = -1;
const lines = []; const lines = [];
// `chunk` is used for dragging to select diff lines
// we are restricting commenting to only lines that appear between
// "expansion rows". Here equal chunks are lines grouped together
// inbetween expansion rows.
let chunk = 0;
for (let i = 0, diffLinesLength = diffLines.length, index = 0; i < diffLinesLength; i += 1) { for (let i = 0, diffLinesLength = diffLines.length, index = 0; i < diffLinesLength; i += 1) {
const line = diffLines[i]; const line = diffLines[i];
line.chunk = chunk;
if (isMeta(line)) chunk += 1;
if (isRemoved(line) || isConflictOur(line) || inline) { if (isRemoved(line) || isConflictOur(line) || inline) {
lines.push({ lines.push({
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions, mapState } from 'vuex';
import { GlFormSelect, GlSprintf } from '@gitlab/ui'; import { GlFormSelect, GlSprintf } from '@gitlab/ui';
import { getSymbol, getLineClasses } from './multiline_comment_utils'; import { getSymbol, getLineClasses } from './multiline_comment_utils';
...@@ -27,12 +27,13 @@ export default { ...@@ -27,12 +27,13 @@ export default {
}; };
}, },
computed: { computed: {
...mapState({ selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition }),
lineNumber() { lineNumber() {
return this.commentLineOptions[this.commentLineOptions.length - 1].text; return this.commentLineOptions[this.commentLineOptions.length - 1].text;
}, },
}, },
created() { created() {
const line = this.lineRange?.start || this.line; const line = this.selectedCommentPosition?.start || this.lineRange?.start || this.line;
this.commentLineStart = { this.commentLineStart = {
line_code: line.line_code, line_code: line.line_code,
...@@ -40,6 +41,8 @@ export default { ...@@ -40,6 +41,8 @@ export default {
old_line: line.old_line, old_line: line.old_line,
new_line: line.new_line, new_line: line.new_line,
}; };
if (this.selectedCommentPosition) return;
this.highlightSelection(); this.highlightSelection();
}, },
destroyed() { destroyed() {
......
...@@ -15,7 +15,7 @@ export default () => ({ ...@@ -15,7 +15,7 @@ export default () => ({
batchSuggestionsInfo: [], batchSuggestionsInfo: [],
currentlyFetchingDiscussions: false, currentlyFetchingDiscussions: false,
/** /**
* selectedCommentPosition & selectedCommentPosition structures are the same as `position.line_range`: * selectedCommentPosition & selectedCommentPositionHover structures are the same as `position.line_range`:
* { * {
* start: { line_code: string, new_line: number, old_line:number, type: string }, * start: { line_code: string, new_line: number, old_line:number, type: string },
* end: { line_code: string, new_line: number, old_line:number, type: string }, * end: { line_code: string, new_line: number, old_line:number, type: string },
......
...@@ -516,9 +516,12 @@ $line-removed-dark-transparent: rgba(246, 53, 85, 0.2); ...@@ -516,9 +516,12 @@ $line-removed-dark-transparent: rgba(246, 53, 85, 0.2);
$line-number-old: #f9d7dc; $line-number-old: #f9d7dc;
$line-number-new: #ddfbe6; $line-number-new: #ddfbe6;
$line-number-select: #fbf2da; $line-number-select: #fbf2da;
$line-number-commented: #dae5fb;
$line-target-blue: $blue-50; $line-target-blue: $blue-50;
$line-select-yellow: #fcf8e7; $line-select-yellow: #fcf8e7;
$line-select-yellow-dark: #f0e2bd; $line-select-yellow-dark: #f0e2bd;
$line-commented-blue: #e8effc;
$line-commented-blue-dark: #bccef0;
$dark-diff-match-bg: rgba($white, 0.3); $dark-diff-match-bg: rgba($white, 0.3);
$dark-diff-match-color: rgba($white, 0.1); $dark-diff-match-color: rgba($white, 0.1);
$diff-image-info-color: #808080; $diff-image-info-color: #808080;
......
...@@ -33,6 +33,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -33,6 +33,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:batch_suggestions, @project, default_enabled: true) push_frontend_feature_flag(:batch_suggestions, @project, default_enabled: true)
push_frontend_feature_flag(:approvals_commented_by, @project, default_enabled: true) push_frontend_feature_flag(:approvals_commented_by, @project, default_enabled: true)
push_frontend_feature_flag(:merge_request_widget_graphql, @project) push_frontend_feature_flag(:merge_request_widget_graphql, @project)
push_frontend_feature_flag(:drag_comment_selection, @project, default_enabled: true)
push_frontend_feature_flag(:unified_diff_components, @project, default_enabled: true) push_frontend_feature_flag(:unified_diff_components, @project, default_enabled: true)
push_frontend_feature_flag(:default_merge_ref_for_diffs, @project) push_frontend_feature_flag(:default_merge_ref_for_diffs, @project)
push_frontend_feature_flag(:core_security_mr_widget, @project, default_enabled: true) push_frontend_feature_flag(:core_security_mr_widget, @project, default_enabled: true)
......
---
name: drag_comment_selection
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49875
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/293945
milestone: '13.7'
type: development
group: group::source code
default_enabled: true
...@@ -1627,6 +1627,9 @@ msgstr "" ...@@ -1627,6 +1627,9 @@ msgstr ""
msgid "Add a comment to this line" msgid "Add a comment to this line"
msgstr "" msgstr ""
msgid "Add a comment to this line or drag for multiple lines"
msgstr ""
msgid "Add a general comment to this %{noteableDisplayName}." msgid "Add a general comment to this %{noteableDisplayName}."
msgstr "" msgstr ""
......
...@@ -11,14 +11,16 @@ describe('DiffLineNoteForm', () => { ...@@ -11,14 +11,16 @@ describe('DiffLineNoteForm', () => {
let diffLines; let diffLines;
const getDiffFileMock = () => ({ ...diffFileMockData }); const getDiffFileMock = () => ({ ...diffFileMockData });
beforeEach(() => { const createComponent = (args = {}) => {
diffFile = getDiffFileMock(); diffFile = getDiffFileMock();
diffLines = diffFile.highlighted_diff_lines; diffLines = diffFile.highlighted_diff_lines;
const store = createStore(); const store = createStore();
store.state.notes.userData.id = 1; store.state.notes.userData.id = 1;
store.state.notes.noteableData = noteableDataMock; store.state.notes.noteableData = noteableDataMock;
wrapper = shallowMount(DiffLineNoteForm, { store.replaceState({ ...store.state, ...args.state });
return shallowMount(DiffLineNoteForm, {
store, store,
propsData: { propsData: {
diffFileHash: diffFile.file_hash, diffFileHash: diffFile.file_hash,
...@@ -27,9 +29,13 @@ describe('DiffLineNoteForm', () => { ...@@ -27,9 +29,13 @@ describe('DiffLineNoteForm', () => {
noteTargetLine: diffLines[0], noteTargetLine: diffLines[0],
}, },
}); });
}); };
describe('methods', () => { describe('methods', () => {
beforeEach(() => {
wrapper = createComponent();
});
describe('handleCancelCommentForm', () => { describe('handleCancelCommentForm', () => {
it('should ask for confirmation when shouldConfirm and isDirty passed as truthy', () => { it('should ask for confirmation when shouldConfirm and isDirty passed as truthy', () => {
jest.spyOn(window, 'confirm').mockReturnValue(false); jest.spyOn(window, 'confirm').mockReturnValue(false);
...@@ -114,14 +120,39 @@ describe('DiffLineNoteForm', () => { ...@@ -114,14 +120,39 @@ describe('DiffLineNoteForm', () => {
describe('mounted', () => { describe('mounted', () => {
it('should init autosave', () => { it('should init autosave', () => {
const key = 'autosave/Note/Issue/98//DiffNote//1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1'; const key = 'autosave/Note/Issue/98//DiffNote//1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1';
wrapper = createComponent();
expect(wrapper.vm.autosave).toBeDefined(); expect(wrapper.vm.autosave).toBeDefined();
expect(wrapper.vm.autosave.key).toEqual(key); expect(wrapper.vm.autosave.key).toEqual(key);
}); });
it('should set selectedCommentPosition', () => {
wrapper = createComponent();
let startLineCode = wrapper.vm.commentLineStart.line_code;
let lineCode = wrapper.vm.line.line_code;
expect(startLineCode).toEqual(lineCode);
wrapper.destroy();
const state = {
notes: {
selectedCommentPosition: {
start: {
line_code: 'test',
},
},
},
};
wrapper = createComponent({ state });
startLineCode = wrapper.vm.commentLineStart.line_code;
lineCode = state.notes.selectedCommentPosition.start.line_code;
expect(startLineCode).toEqual(lineCode);
});
}); });
describe('template', () => { describe('template', () => {
it('should have note form', () => { it('should have note form', () => {
wrapper = createComponent();
expect(wrapper.find(NoteForm).exists()).toBe(true); expect(wrapper.find(NoteForm).exists()).toBe(true);
}); });
}); });
......
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import { getByTestId, fireEvent } from '@testing-library/dom';
import Vuex from 'vuex'; import Vuex from 'vuex';
import diffsModule from '~/diffs/store/modules'; import diffsModule from '~/diffs/store/modules';
import DiffRow from '~/diffs/components/diff_row.vue'; import DiffRow from '~/diffs/components/diff_row.vue';
...@@ -42,16 +43,16 @@ describe('DiffRow', () => { ...@@ -42,16 +43,16 @@ describe('DiffRow', () => {
fileHash: 'abc', fileHash: 'abc',
filePath: 'abc', filePath: 'abc',
line: {}, line: {},
index: 0,
...props, ...props,
}; };
return shallowMount(DiffRow, { propsData, localVue, store });
const provide = {
glFeatures: { dragCommentSelection: true },
}; };
it('isHighlighted returns true if isCommented is true', () => { return shallowMount(DiffRow, { propsData, localVue, store, provide });
const props = { isCommented: true }; };
const wrapper = createWrapper({ props });
expect(wrapper.vm.isHighlighted).toBe(true);
});
it('isHighlighted returns true given line.left', () => { it('isHighlighted returns true given line.left', () => {
const props = { const props = {
...@@ -124,4 +125,36 @@ describe('DiffRow', () => { ...@@ -124,4 +125,36 @@ describe('DiffRow', () => {
const lineNumber = testLines[0].right.new_line; const lineNumber = testLines[0].right.new_line;
expect(wrapper.find(`[data-linenumber="${lineNumber}"]`).exists()).toBe(true); expect(wrapper.find(`[data-linenumber="${lineNumber}"]`).exists()).toBe(true);
}); });
describe('drag operations', () => {
let line;
beforeEach(() => {
line = { ...testLines[0] };
});
it.each`
side
${'left'}
${'right'}
`('emits `enterdragging` onDragEnter $side side', ({ side }) => {
const expectation = { ...line[side], index: 0 };
const wrapper = createWrapper({ props: { line } });
fireEvent.dragEnter(getByTestId(wrapper.element, `${side}-side`));
expect(wrapper.emitted().enterdragging).toBeTruthy();
expect(wrapper.emitted().enterdragging[0]).toEqual([expectation]);
});
it.each`
side
${'left'}
${'right'}
`('emits `stopdragging` onDrop $side side', ({ side }) => {
const wrapper = createWrapper({ props: { line } });
fireEvent.dragEnd(getByTestId(wrapper.element, `${side}-side`));
expect(wrapper.emitted().stopdragging).toBeTruthy();
});
});
}); });
...@@ -126,14 +126,14 @@ describe('lineCode', () => { ...@@ -126,14 +126,14 @@ describe('lineCode', () => {
describe('classNameMapCell', () => { describe('classNameMapCell', () => {
it.each` it.each`
line | hll | loggedIn | hovered | expectation line | hll | isLoggedIn | isHover | expectation
${undefined} | ${true} | ${true} | ${true} | ${[]} ${undefined} | ${true} | ${true} | ${true} | ${[]}
${{ type: 'new' }} | ${false} | ${false} | ${false} | ${['new', { hll: false, 'is-over': false, new_line: true, old_line: false }]} ${{ type: 'new' }} | ${false} | ${false} | ${false} | ${['new', { hll: false, 'is-over': false, new_line: true, old_line: false }]}
${{ type: 'new' }} | ${true} | ${true} | ${false} | ${['new', { hll: true, 'is-over': false, new_line: true, old_line: false }]} ${{ type: 'new' }} | ${true} | ${true} | ${false} | ${['new', { hll: true, 'is-over': false, new_line: true, old_line: false }]}
${{ type: 'new' }} | ${true} | ${false} | ${true} | ${['new', { hll: true, 'is-over': false, new_line: true, old_line: false }]} ${{ type: 'new' }} | ${true} | ${false} | ${true} | ${['new', { hll: true, 'is-over': false, new_line: true, old_line: false }]}
${{ type: 'new' }} | ${true} | ${true} | ${true} | ${['new', { hll: true, 'is-over': true, new_line: true, old_line: false }]} ${{ type: 'new' }} | ${true} | ${true} | ${true} | ${['new', { hll: true, 'is-over': true, new_line: true, old_line: false }]}
`('should return $expectation', ({ line, hll, loggedIn, hovered, expectation }) => { `('should return $expectation', ({ line, hll, isLoggedIn, isHover, expectation }) => {
const classes = utils.classNameMapCell(line, hll, loggedIn, hovered); const classes = utils.classNameMapCell({ line, hll, isLoggedIn, isHover });
expect(classes).toEqual(expectation); expect(classes).toEqual(expectation);
}); });
}); });
......
import { shallowMount, createLocalVue } from '@vue/test-utils'; import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import DiffView from '~/diffs/components/diff_view.vue'; import DiffView from '~/diffs/components/diff_view.vue';
// import DraftNote from '~/batch_comments/components/draft_note.vue';
// import DiffRow from '~/diffs/components/diff_row.vue';
// import DiffCommentCell from '~/diffs/components/diff_comment_cell.vue';
// import DiffExpansionCell from '~/diffs/components/diff_expansion_cell.vue';
describe('DiffView', () => { describe('DiffView', () => {
const DiffExpansionCell = { template: `<div/>` }; const DiffExpansionCell = { template: `<div/>` };
const DiffRow = { template: `<div/>` }; const DiffRow = { template: `<div/>` };
const DiffCommentCell = { template: `<div/>` }; const DiffCommentCell = { template: `<div/>` };
const DraftNote = { template: `<div/>` }; const DraftNote = { template: `<div/>` };
const showCommentForm = jest.fn();
const setSelectedCommentPosition = jest.fn();
const getDiffRow = (wrapper) => wrapper.findComponent(DiffRow).vm;
const createWrapper = (props) => { const createWrapper = (props) => {
const localVue = createLocalVue(); Vue.use(Vuex);
localVue.use(Vuex);
const batchComments = { const batchComments = {
getters: { getters: {
...@@ -26,8 +26,13 @@ describe('DiffView', () => { ...@@ -26,8 +26,13 @@ describe('DiffView', () => {
}, },
namespaced: true, namespaced: true,
}; };
const diffs = { getters: { commitId: () => 'abc123' }, namespaced: true }; const diffs = {
actions: { showCommentForm },
getters: { commitId: () => 'abc123' },
namespaced: true,
};
const notes = { const notes = {
actions: { setSelectedCommentPosition },
state: { selectedCommentPosition: null, selectedCommentPositionHover: null }, state: { selectedCommentPosition: null, selectedCommentPositionHover: null },
}; };
...@@ -41,7 +46,7 @@ describe('DiffView', () => { ...@@ -41,7 +46,7 @@ describe('DiffView', () => {
...props, ...props,
}; };
const stubs = { DiffExpansionCell, DiffRow, DiffCommentCell, DraftNote }; const stubs = { DiffExpansionCell, DiffRow, DiffCommentCell, DraftNote };
return shallowMount(DiffView, { propsData, store, localVue, stubs }); return shallowMount(DiffView, { propsData, store, stubs });
}; };
it('renders a match line', () => { it('renders a match line', () => {
...@@ -74,4 +79,55 @@ describe('DiffView', () => { ...@@ -74,4 +79,55 @@ describe('DiffView', () => {
}); });
expect(wrapper.find(DraftNote).exists()).toBe(true); expect(wrapper.find(DraftNote).exists()).toBe(true);
}); });
describe('drag operations', () => {
it('sets `dragStart` onStartDragging', () => {
const wrapper = createWrapper({ diffLines: [{}] });
wrapper.findComponent(DiffRow).vm.$emit('startdragging', { test: true });
expect(wrapper.vm.dragStart).toEqual({ test: true });
});
it('does not call `setSelectedCommentPosition` on different chunks onDragOver', () => {
const wrapper = createWrapper({ diffLines: [{}] });
const diffRow = getDiffRow(wrapper);
diffRow.$emit('startdragging', { chunk: 0 });
diffRow.$emit('enterdragging', { chunk: 1 });
expect(setSelectedCommentPosition).not.toHaveBeenCalled();
});
it.each`
start | end | expectation
${1} | ${2} | ${{ start: { index: 1 }, end: { index: 2 } }}
${2} | ${1} | ${{ start: { index: 1 }, end: { index: 2 } }}
${1} | ${1} | ${{ start: { index: 1 }, end: { index: 1 } }}
`(
'calls `setSelectedCommentPosition` with correct `updatedLineRange`',
({ start, end, expectation }) => {
const wrapper = createWrapper({ diffLines: [{}] });
const diffRow = getDiffRow(wrapper);
diffRow.$emit('startdragging', { chunk: 1, index: start });
diffRow.$emit('enterdragging', { chunk: 1, index: end });
const arg = setSelectedCommentPosition.mock.calls[0][1];
expect(arg).toMatchObject(expectation);
},
);
it('sets `dragStart` to null onStopDragging', () => {
const wrapper = createWrapper({ diffLines: [{}] });
const diffRow = getDiffRow(wrapper);
diffRow.$emit('startdragging', { test: true });
expect(wrapper.vm.dragStart).toEqual({ test: true });
diffRow.$emit('stopdragging');
expect(wrapper.vm.dragStart).toBeNull();
expect(showCommentForm).toHaveBeenCalled();
});
});
}); });
...@@ -1159,7 +1159,7 @@ describe('DiffsStoreUtils', () => { ...@@ -1159,7 +1159,7 @@ describe('DiffsStoreUtils', () => {
it('converts inline diff lines to parallel diff lines', () => { it('converts inline diff lines to parallel diff lines', () => {
const file = getDiffFileMock(); const file = getDiffFileMock();
expect(utils.parallelizeDiffLines(file[INLINE_DIFF_LINES_KEY])).toEqual( expect(utils.parallelizeDiffLines(file[INLINE_DIFF_LINES_KEY])).toMatchObject(
file.parallel_diff_lines, file.parallel_diff_lines,
); );
}); });
...@@ -1178,16 +1178,17 @@ describe('DiffsStoreUtils', () => { ...@@ -1178,16 +1178,17 @@ describe('DiffsStoreUtils', () => {
{ {
left: null, left: null,
right: { right: {
chunk: 0,
type: 'new', type: 'new',
}, },
}, },
{ {
left: { type: 'conflict_marker_our' }, left: { chunk: 0, type: 'conflict_marker_our' },
right: { type: 'conflict_marker_their' }, right: { chunk: 0, type: 'conflict_marker_their' },
}, },
{ {
left: { type: 'conflict_our' }, left: { chunk: 0, type: 'conflict_our' },
right: { type: 'conflict_their' }, right: { chunk: 0, type: 'conflict_their' },
}, },
]); ]);
}); });
...@@ -1196,9 +1197,9 @@ describe('DiffsStoreUtils', () => { ...@@ -1196,9 +1197,9 @@ describe('DiffsStoreUtils', () => {
const file = getDiffFileMock(); const file = getDiffFileMock();
const files = utils.parallelizeDiffLines(file.highlighted_diff_lines, true); const files = utils.parallelizeDiffLines(file.highlighted_diff_lines, true);
expect(files[5].left).toEqual(file.parallel_diff_lines[5].left); expect(files[5].left).toMatchObject(file.parallel_diff_lines[5].left);
expect(files[5].right).toBeNull(); expect(files[5].right).toBeNull();
expect(files[6].left).toEqual(file.parallel_diff_lines[5].right); expect(files[6].left).toMatchObject(file.parallel_diff_lines[5].right);
expect(files[6].right).toBeNull(); expect(files[6].right).toBeNull();
}); });
}); });
......
import Vue from 'vue';
import Vuex from 'vuex';
import { mount } from '@vue/test-utils';
import { GlFormSelect } from '@gitlab/ui';
import MultilineCommentForm from '~/notes/components/multiline_comment_form.vue';
import notesModule from '~/notes/stores/modules';
describe('MultilineCommentForm', () => {
Vue.use(Vuex);
const setSelectedCommentPosition = jest.fn();
const testLine = {
line_code: 'test',
type: 'test',
old_line: 'test',
new_line: 'test',
};
const createWrapper = (props = {}, state) => {
setSelectedCommentPosition.mockReset();
const store = new Vuex.Store({
modules: { notes: notesModule() },
actions: { setSelectedCommentPosition },
});
if (state) store.replaceState({ ...store.state, ...state });
const propsData = {
line: { ...testLine },
commentLineOptions: [{ text: '1' }],
...props,
};
return mount(MultilineCommentForm, { propsData, store });
};
describe('created', () => {
it('sets commentLineStart to line', () => {
const line = { ...testLine };
const wrapper = createWrapper({ line });
expect(wrapper.vm.commentLineStart).toEqual(line);
expect(setSelectedCommentPosition).toHaveBeenCalled();
});
it('sets commentLineStart to lineRange', () => {
const lineRange = {
start: { ...testLine },
};
const wrapper = createWrapper({ lineRange });
expect(wrapper.vm.commentLineStart).toEqual(lineRange.start);
expect(setSelectedCommentPosition).toHaveBeenCalled();
});
it('sets commentLineStart to selectedCommentPosition', () => {
const notes = {
selectedCommentPosition: {
start: { ...testLine },
},
};
const wrapper = createWrapper({}, { notes });
expect(wrapper.vm.commentLineStart).toEqual(wrapper.vm.selectedCommentPosition.start);
expect(setSelectedCommentPosition).not.toHaveBeenCalled();
});
});
describe('destroyed', () => {
it('calls setSelectedCommentPosition', () => {
const wrapper = createWrapper();
wrapper.destroy();
// Once during created, once during destroyed
expect(setSelectedCommentPosition).toHaveBeenCalledTimes(2);
});
});
it('handles changing the start line', () => {
const line = { ...testLine };
const wrapper = createWrapper({ line });
const glSelect = wrapper.findComponent(GlFormSelect);
glSelect.vm.$emit('change', { ...testLine });
expect(wrapper.vm.commentLineStart).toEqual(line);
expect(wrapper.emitted('input')).toBeTruthy();
// Once during created, once during updateCommentLineStart
expect(setSelectedCommentPosition).toHaveBeenCalledTimes(2);
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment