import { clone } from 'lodash'; import { LINE_POSITION_LEFT, LINE_POSITION_RIGHT, TEXT_DIFF_POSITION_TYPE, LEGACY_DIFF_NOTE_TYPE, DIFF_NOTE_TYPE, NEW_LINE_TYPE, OLD_LINE_TYPE, MATCH_LINE_TYPE, INLINE_DIFF_VIEW_TYPE, INLINE_DIFF_LINES_KEY, } from '~/diffs/constants'; import * as utils from '~/diffs/store/utils'; import { MERGE_REQUEST_NOTEABLE_TYPE } from '~/notes/constants'; import { noteableDataMock } from '../../notes/mock_data'; import diffFileMockData from '../mock_data/diff_file'; import { diffMetadata } from '../mock_data/diff_metadata'; const getDiffFileMock = () => JSON.parse(JSON.stringify(diffFileMockData)); const getDiffMetadataMock = () => JSON.parse(JSON.stringify(diffMetadata)); describe('DiffsStoreUtils', () => { describe('findDiffFile', () => { const files = [{ file_hash: 1, name: 'one' }]; it('should return correct file', () => { expect(utils.findDiffFile(files, 1).name).toEqual('one'); expect(utils.findDiffFile(files, 2)).toBeUndefined(); }); }); describe('getReversePosition', () => { it('should return correct line position name', () => { expect(utils.getReversePosition(LINE_POSITION_RIGHT)).toEqual(LINE_POSITION_LEFT); expect(utils.getReversePosition(LINE_POSITION_LEFT)).toEqual(LINE_POSITION_RIGHT); }); }); describe('findIndexInInlineLines', () => { const expectSet = (method, lines, invalidLines) => { expect(method(lines, { oldLineNumber: 3, newLineNumber: 5 })).toEqual(4); expect(method(invalidLines || lines, { oldLineNumber: 32, newLineNumber: 53 })).toEqual(-1); }; describe('findIndexInInlineLines', () => { it('should return correct index for given line numbers', () => { expectSet(utils.findIndexInInlineLines, getDiffFileMock()[INLINE_DIFF_LINES_KEY]); }); }); }); describe('getPreviousLineIndex', () => { describe(`with diffViewType (inline) in split diffs`, () => { let diffFile; beforeEach(() => { diffFile = { ...clone(diffFileMockData) }; }); it('should return the correct previous line number', () => { expect( utils.getPreviousLineIndex(INLINE_DIFF_VIEW_TYPE, diffFile, { oldLineNumber: 3, newLineNumber: 5, }), ).toBe(4); }); }); }); describe('removeMatchLine', () => { it('should remove match line properly by regarding the bottom parameter', () => { const diffFile = getDiffFileMock(); const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 }; const inlineIndex = utils.findIndexInInlineLines( diffFile[INLINE_DIFF_LINES_KEY], lineNumbers, ); const atInlineIndex = diffFile[INLINE_DIFF_LINES_KEY][inlineIndex]; utils.removeMatchLine(diffFile, lineNumbers, false); expect(diffFile[INLINE_DIFF_LINES_KEY][inlineIndex]).not.toEqual(atInlineIndex); utils.removeMatchLine(diffFile, lineNumbers, true); expect(diffFile[INLINE_DIFF_LINES_KEY][inlineIndex + 1]).not.toEqual(atInlineIndex); }); }); describe('addContextLines', () => { it(`should add context lines`, () => { const diffFile = getDiffFileMock(); const inlineLines = diffFile[INLINE_DIFF_LINES_KEY]; const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 }; const contextLines = [{ lineNumber: 42, line_code: '123' }]; const options = { inlineLines, contextLines, lineNumbers }; const inlineIndex = utils.findIndexInInlineLines(inlineLines, lineNumbers); utils.addContextLines(options); expect(inlineLines[inlineIndex]).toEqual(contextLines[0]); }); it(`should add context lines properly with bottom parameter`, () => { const diffFile = getDiffFileMock(); const inlineLines = diffFile[INLINE_DIFF_LINES_KEY]; const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 }; const contextLines = [{ lineNumber: 42, line_code: '123' }]; const options = { inlineLines, contextLines, lineNumbers, bottom: true, }; utils.addContextLines(options); expect(inlineLines[inlineLines.length - 1]).toEqual(contextLines[0]); }); }); describe('getNoteFormData', () => { it('should properly create note form data', () => { const diffFile = getDiffFileMock(); noteableDataMock.targetType = MERGE_REQUEST_NOTEABLE_TYPE; const options = { note: 'Hello world!', noteableData: noteableDataMock, noteableType: MERGE_REQUEST_NOTEABLE_TYPE, diffFile, noteTargetLine: { line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3', meta_data: null, new_line: 3, old_line: 1, }, linePosition: LINE_POSITION_LEFT, lineRange: { start_line_code: 'abc_1_1', end_line_code: 'abc_2_2' }, }; const position = JSON.stringify({ base_sha: diffFile.diff_refs.base_sha, start_sha: diffFile.diff_refs.start_sha, head_sha: diffFile.diff_refs.head_sha, old_path: diffFile.old_path, new_path: diffFile.new_path, position_type: TEXT_DIFF_POSITION_TYPE, old_line: options.noteTargetLine.old_line, new_line: options.noteTargetLine.new_line, line_range: options.lineRange, }); const postData = { view: options.diffViewType, line_type: options.linePosition === LINE_POSITION_RIGHT ? NEW_LINE_TYPE : OLD_LINE_TYPE, merge_request_diff_head_sha: diffFile.diff_refs.head_sha, in_reply_to_discussion_id: '', note_project_id: '', target_type: options.noteableType, target_id: options.noteableData.id, return_discussion: true, note: { noteable_type: options.noteableType, noteable_id: options.noteableData.id, commit_id: undefined, type: DIFF_NOTE_TYPE, line_code: options.noteTargetLine.line_code, note: options.note, position, }, }; expect(utils.getNoteFormData(options)).toEqual({ endpoint: options.noteableData.create_note_path, data: postData, }); }); it('should create legacy note form data', () => { const diffFile = getDiffFileMock(); delete diffFile.diff_refs.start_sha; delete diffFile.diff_refs.head_sha; noteableDataMock.targetType = MERGE_REQUEST_NOTEABLE_TYPE; const options = { note: 'Hello world!', noteableData: noteableDataMock, noteableType: MERGE_REQUEST_NOTEABLE_TYPE, diffFile, noteTargetLine: { line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3', meta_data: null, new_line: 3, old_line: 1, }, linePosition: LINE_POSITION_LEFT, }; const position = JSON.stringify({ base_sha: diffFile.diff_refs.base_sha, start_sha: undefined, head_sha: undefined, old_path: diffFile.old_path, new_path: diffFile.new_path, position_type: TEXT_DIFF_POSITION_TYPE, old_line: options.noteTargetLine.old_line, new_line: options.noteTargetLine.new_line, }); const postData = { view: options.diffViewType, line_type: options.linePosition === LINE_POSITION_RIGHT ? NEW_LINE_TYPE : OLD_LINE_TYPE, merge_request_diff_head_sha: undefined, in_reply_to_discussion_id: '', note_project_id: '', target_type: options.noteableType, target_id: options.noteableData.id, return_discussion: true, note: { noteable_type: options.noteableType, noteable_id: options.noteableData.id, commit_id: undefined, type: LEGACY_DIFF_NOTE_TYPE, line_code: options.noteTargetLine.line_code, note: options.note, position, }, }; expect(utils.getNoteFormData(options)).toEqual({ endpoint: options.noteableData.create_note_path, data: postData, }); }); }); describe('addLineReferences', () => { const lineNumbers = { oldLineNumber: 3, newLineNumber: 4 }; it('should add correct line references when bottom set to true', () => { const lines = [{ type: null }, { type: MATCH_LINE_TYPE }]; const linesWithReferences = utils.addLineReferences(lines, lineNumbers, true); expect(linesWithReferences[0].old_line).toEqual(lineNumbers.oldLineNumber + 1); expect(linesWithReferences[0].new_line).toEqual(lineNumbers.newLineNumber + 1); expect(linesWithReferences[1].meta_data.old_pos).toEqual(4); expect(linesWithReferences[1].meta_data.new_pos).toEqual(5); }); it('should add correct line references when bottom falsy', () => { const lines = [{ type: null }, { type: MATCH_LINE_TYPE }, { type: null }]; const linesWithReferences = utils.addLineReferences(lines, lineNumbers); expect(linesWithReferences[0].old_line).toEqual(0); expect(linesWithReferences[0].new_line).toEqual(1); expect(linesWithReferences[1].meta_data.old_pos).toEqual(2); expect(linesWithReferences[1].meta_data.new_pos).toEqual(3); }); it('should add correct line references when isExpandDown is true', () => { const lines = [{ type: null }, { type: MATCH_LINE_TYPE }]; const linesWithReferences = utils.addLineReferences(lines, lineNumbers, false, true, { old_line: 10, new_line: 11, }); expect(linesWithReferences[1].meta_data.old_pos).toEqual(10); expect(linesWithReferences[1].meta_data.new_pos).toEqual(11); }); }); describe('trimFirstCharOfLineContent', () => { it('trims the line when it starts with a space', () => { // eslint-disable-next-line import/no-deprecated expect(utils.trimFirstCharOfLineContent({ rich_text: ' diff' })).toEqual({ rich_text: 'diff', }); }); it('trims the line when it starts with a +', () => { // eslint-disable-next-line import/no-deprecated expect(utils.trimFirstCharOfLineContent({ rich_text: '+diff' })).toEqual({ rich_text: 'diff', }); }); it('trims the line when it starts with a -', () => { // eslint-disable-next-line import/no-deprecated expect(utils.trimFirstCharOfLineContent({ rich_text: '-diff' })).toEqual({ rich_text: 'diff', }); }); it('does not trims the line when it starts with a letter', () => { // eslint-disable-next-line import/no-deprecated expect(utils.trimFirstCharOfLineContent({ rich_text: 'diff' })).toEqual({ rich_text: 'diff', }); }); it('does not modify the provided object', () => { const lineObj = { rich_text: ' diff', }; // eslint-disable-next-line import/no-deprecated utils.trimFirstCharOfLineContent(lineObj); expect(lineObj).toEqual({ rich_text: ' diff' }); }); it('handles a undefined or null parameter', () => { // eslint-disable-next-line import/no-deprecated expect(utils.trimFirstCharOfLineContent()).toEqual({}); }); }); describe('prepareLineForRenamedFile', () => { const diffFile = { file_hash: 'file-hash', }; const lineIndex = 4; const sourceLine = { foo: 'test', rich_text: '

rich

', // Note the leading space }; const correctLine = { foo: 'test', line_code: 'file-hash_5_5', old_line: 5, new_line: 5, rich_text: '

rich

', // Note no leading space discussionsExpanded: true, discussions: [], hasForm: false, text: undefined, alreadyPrepared: true, }; let preppedLine; beforeEach(() => { preppedLine = utils.prepareLineForRenamedFile({ diffViewType: INLINE_DIFF_VIEW_TYPE, line: sourceLine, index: lineIndex, diffFile, }); }); it('copies over the original line object to the new prepared line', () => { expect(preppedLine).toEqual( expect.objectContaining({ foo: correctLine.foo, rich_text: correctLine.rich_text, }), ); }); it('correctly sets the old and new lines, plus a line code', () => { expect(preppedLine.old_line).toEqual(correctLine.old_line); expect(preppedLine.new_line).toEqual(correctLine.new_line); expect(preppedLine.line_code).toEqual(correctLine.line_code); }); it('returns a single object with the correct structure for `inline` lines', () => { expect(preppedLine).toEqual(correctLine); }); it.each` brokenSymlink ${false} ${{}} ${'anything except `false`'} `( "properly assigns each line's `commentsDisabled` as the same value as the parent file's `brokenSymlink` value (`$brokenSymlink`)", ({ brokenSymlink }) => { preppedLine = utils.prepareLineForRenamedFile({ diffViewType: INLINE_DIFF_VIEW_TYPE, line: sourceLine, index: lineIndex, diffFile: { ...diffFile, brokenSymlink, }, }); expect(preppedLine.commentsDisabled).toStrictEqual(brokenSymlink); }, ); }); describe('prepareDiffData', () => { describe('for regular diff files', () => { let mock; let preparedDiff; let splitInlineDiff; let splitParallelDiff; let completedDiff; beforeEach(() => { mock = getDiffFileMock(); preparedDiff = { diff_files: [mock] }; splitInlineDiff = { diff_files: [{ ...mock }], }; splitParallelDiff = { diff_files: [{ ...mock, [INLINE_DIFF_LINES_KEY]: undefined }], }; completedDiff = { diff_files: [{ ...mock, [INLINE_DIFF_LINES_KEY]: undefined }], }; preparedDiff.diff_files = utils.prepareDiffData({ diff: preparedDiff }); splitInlineDiff.diff_files = utils.prepareDiffData({ diff: splitInlineDiff }); splitParallelDiff.diff_files = utils.prepareDiffData({ diff: splitParallelDiff }); completedDiff.diff_files = utils.prepareDiffData({ diff: completedDiff, priorFiles: [mock], }); }); it('sets the renderIt and collapsed attribute on files', () => { const checkLine = preparedDiff.diff_files[0][INLINE_DIFF_LINES_KEY][0]; expect(checkLine.discussions.length).toBe(0); expect(checkLine).not.toHaveAttr('text'); const firstChar = checkLine.rich_text.charAt(0); expect(firstChar).not.toBe(' '); expect(firstChar).not.toBe('+'); expect(firstChar).not.toBe('-'); expect(preparedDiff.diff_files[0].renderIt).toBeTruthy(); expect(preparedDiff.diff_files[0].collapsed).toBeFalsy(); }); it('guarantees an empty array for both diff styles', () => { expect(splitInlineDiff.diff_files[0][INLINE_DIFF_LINES_KEY].length).toBeGreaterThan(0); expect(splitParallelDiff.diff_files[0][INLINE_DIFF_LINES_KEY].length).toEqual(0); }); it('merges existing diff files with newly loaded diff files to ensure split diffs are eventually completed', () => { expect(completedDiff.diff_files.length).toEqual(1); expect(completedDiff.diff_files[0][INLINE_DIFF_LINES_KEY].length).toBeGreaterThan(0); }); it('leaves files in the existing state', () => { const priorFiles = [mock]; const fakeNewFile = { ...mock, content_sha: 'ABC', file_hash: 'DEF', }; const updatedFilesList = utils.prepareDiffData({ diff: { diff_files: [fakeNewFile] }, priorFiles, }); expect(updatedFilesList).toEqual([mock, fakeNewFile]); }); it('completes an existing split diff without overwriting existing diffs', () => { // The current state has a file that has only loaded inline lines const priorFiles = [{ ...mock }]; // The next (batch) load loads two files: the other half of that file, and a new file const fakeBatch = [ { ...mock, [INLINE_DIFF_LINES_KEY]: undefined }, { ...mock, [INLINE_DIFF_LINES_KEY]: undefined, content_sha: 'ABC', file_hash: 'DEF' }, ]; const updatedFilesList = utils.prepareDiffData({ diff: { diff_files: fakeBatch }, priorFiles, }); expect(updatedFilesList).toEqual([ mock, expect.objectContaining({ content_sha: 'ABC', file_hash: 'DEF', }), ]); }); it('adds the `.brokenSymlink` property to each diff file', () => { preparedDiff.diff_files.forEach((file) => { expect(file).toEqual(expect.objectContaining({ brokenSymlink: false })); }); }); it("copies the diff file's `.brokenSymlink` value to each of that file's child lines", () => { const lines = [ ...preparedDiff.diff_files, ...splitInlineDiff.diff_files, ...splitParallelDiff.diff_files, ...completedDiff.diff_files, ].flatMap((file) => [...file[INLINE_DIFF_LINES_KEY]]); lines.forEach((line) => { expect(line.commentsDisabled).toBe(false); }); }); }); describe('for diff metadata', () => { let mock; let preparedDiffFiles; beforeEach(() => { mock = getDiffMetadataMock(); preparedDiffFiles = utils.prepareDiffData({ diff: mock, meta: true }); }); it('sets the renderIt and collapsed attribute on files', () => { expect(preparedDiffFiles[0].renderIt).toBeTruthy(); expect(preparedDiffFiles[0].collapsed).toBeFalsy(); }); it('guarantees an empty array of lines for both diff styles', () => { expect(preparedDiffFiles[0][INLINE_DIFF_LINES_KEY].length).toEqual(0); }); it('leaves files in the existing state', () => { const fileMock = getDiffFileMock(); const metaData = getDiffMetadataMock(); const priorFiles = [fileMock]; const updatedFilesList = utils.prepareDiffData({ diff: metaData, priorFiles, meta: true }); expect(updatedFilesList.length).toEqual(2); expect(updatedFilesList[0]).toEqual(fileMock); }); it('adds a new file to the file that already exists in state', () => { // This is actually buggy behavior: // Because the metadata doesn't include a content_sha, // the de-duplicator in prepareDiffData doesn't realize it // should combine these two. // This buggy behavior hasn't caused a defect YET, because // `diffs_metadata.json` is only called the first time the // diffs app starts up, which is: // - after a fresh page load // - after you switch to the changes tab *the first time* // This test should begin FAILING and can be reversed to check // for just a single file when this is implemented: // https://gitlab.com/groups/gitlab-org/-/epics/2852#note_304803233 const fileMock = getDiffFileMock(); const metaMock = getDiffMetadataMock(); const priorFiles = [{ ...fileMock }]; const updatedFilesList = utils.prepareDiffData({ diff: metaMock, priorFiles, meta: true }); expect(updatedFilesList).toEqual([ fileMock, { ...metaMock.diff_files[0], [INLINE_DIFF_LINES_KEY]: [], }, ]); }); it('adds the `.brokenSymlink` property to each meta diff file', () => { preparedDiffFiles.forEach((file) => { expect(file).toMatchObject({ brokenSymlink: false }); }); }); }); }); describe('isDiscussionApplicableToLine', () => { const diffPosition = { baseSha: 'ed13df29948c41ba367caa757ab3ec4892509910', headSha: 'b921914f9a834ac47e6fd9420f78db0f83559130', newLine: null, newPath: '500-lines-4.txt', oldLine: 5, oldPath: '500-lines-4.txt', startSha: 'ed13df29948c41ba367caa757ab3ec4892509910', }; const wrongDiffPosition = { baseSha: 'wrong', headSha: 'wrong', newLine: null, newPath: '500-lines-4.txt', oldLine: 5, oldPath: '500-lines-4.txt', startSha: 'wrong', }; const discussions = { upToDateDiscussion1: { original_position: diffPosition, position: wrongDiffPosition, }, outDatedDiscussion1: { original_position: wrongDiffPosition, position: wrongDiffPosition, }, }; // When multi line comments are fully implemented `line_code` will be // included in all requests. Until then we need to ensure the logic does // not change when it is included only in the "comparison" argument. const lineRange = { start_line_code: 'abc_1_1', end_line_code: 'abc_1_2' }; it('returns true when the discussion is up to date', () => { expect( utils.isDiscussionApplicableToLine({ discussion: discussions.upToDateDiscussion1, diffPosition: { ...diffPosition, line_range: lineRange }, latestDiff: true, }), ).toBe(true); }); it('returns false when the discussion is not up to date', () => { expect( utils.isDiscussionApplicableToLine({ discussion: discussions.outDatedDiscussion1, diffPosition: { ...diffPosition, line_range: lineRange }, latestDiff: true, }), ).toBe(false); }); it('returns true when line codes match and discussion does not contain position and is not active', () => { const discussion = { ...discussions.outDatedDiscussion1, line_code: 'ABC_1', active: false }; delete discussion.original_position; delete discussion.position; expect( utils.isDiscussionApplicableToLine({ discussion, diffPosition: { ...diffPosition, lineCode: 'ABC_1', line_range: lineRange, }, latestDiff: true, }), ).toBe(false); }); it('returns true when line codes match and discussion does not contain position and is active', () => { const discussion = { ...discussions.outDatedDiscussion1, line_code: 'ABC_1', active: true }; delete discussion.original_position; delete discussion.position; expect( utils.isDiscussionApplicableToLine({ discussion, diffPosition: { ...diffPosition, line_code: 'ABC_1', line_range: lineRange, }, latestDiff: true, }), ).toBe(true); }); it('returns false when not latest diff', () => { const discussion = { ...discussions.outDatedDiscussion1, line_code: 'ABC_1', active: true }; delete discussion.original_position; delete discussion.position; expect( utils.isDiscussionApplicableToLine({ discussion, diffPosition: { ...diffPosition, lineCode: 'ABC_1', line_range: lineRange, }, latestDiff: false, }), ).toBe(false); }); }); describe('generateTreeList', () => { let files; beforeAll(() => { files = [ { new_path: 'app/index.js', deleted_file: false, new_file: false, removed_lines: 10, added_lines: 0, file_hash: 'test', }, { new_path: 'app/test/index.js', deleted_file: false, new_file: true, removed_lines: 0, added_lines: 0, file_hash: 'test', }, { new_path: 'app/test/filepathneedstruncating.js', deleted_file: false, new_file: true, removed_lines: 0, added_lines: 0, file_hash: 'test', }, { new_path: 'package.json', deleted_file: true, new_file: false, removed_lines: 0, added_lines: 0, file_hash: 'test', }, ]; }); it('creates a tree of files', () => { const { tree } = utils.generateTreeList(files); expect(tree).toEqual([ { key: 'app', path: 'app', name: 'app', type: 'tree', tree: [ { addedLines: 0, changed: true, deleted: false, fileHash: 'test', key: 'app/index.js', name: 'index.js', parentPath: 'app/', path: 'app/index.js', removedLines: 10, tempFile: false, type: 'blob', tree: [], }, { key: 'app/test', path: 'app/test', name: 'test', type: 'tree', opened: true, tree: [ { addedLines: 0, changed: true, deleted: false, fileHash: 'test', key: 'app/test/index.js', name: 'index.js', parentPath: 'app/test/', path: 'app/test/index.js', removedLines: 0, tempFile: true, type: 'blob', tree: [], }, { addedLines: 0, changed: true, deleted: false, fileHash: 'test', key: 'app/test/filepathneedstruncating.js', name: 'filepathneedstruncating.js', parentPath: 'app/test/', path: 'app/test/filepathneedstruncating.js', removedLines: 0, tempFile: true, type: 'blob', tree: [], }, ], }, ], opened: true, }, { key: 'package.json', parentPath: '/', path: 'package.json', name: 'package.json', type: 'blob', changed: true, tempFile: false, deleted: true, fileHash: 'test', addedLines: 0, removedLines: 0, tree: [], }, ]); }); it('creates flat list of blobs & folders', () => { const { treeEntries } = utils.generateTreeList(files); expect(Object.keys(treeEntries)).toEqual([ 'app', 'app/index.js', 'app/test', 'app/test/index.js', 'app/test/filepathneedstruncating.js', 'package.json', ]); }); }); describe('getDiffMode', () => { it('returns mode when matched in file', () => { expect( utils.getDiffMode({ renamed_file: true, }), ).toBe('renamed'); }); it('returns mode_changed if key has no match', () => { expect( utils.getDiffMode({ viewer: { name: 'mode_changed' }, }), ).toBe('mode_changed'); }); it('defaults to replaced', () => { expect(utils.getDiffMode({})).toBe('replaced'); }); }); describe('getLowestSingleFolder', () => { it('returns path and tree of lowest single folder tree', () => { const folder = { name: 'app', type: 'tree', tree: [ { name: 'javascripts', type: 'tree', tree: [ { type: 'blob', name: 'index.js', }, ], }, ], }; const { path, treeAcc } = utils.getLowestSingleFolder(folder); expect(path).toEqual('app/javascripts'); expect(treeAcc).toEqual([ { type: 'blob', name: 'index.js', }, ]); }); it('returns passed in folders path & tree when more than tree exists', () => { const folder = { name: 'app', type: 'tree', tree: [ { name: 'spec', type: 'blob', tree: [], }, ], }; const { path, treeAcc } = utils.getLowestSingleFolder(folder); expect(path).toEqual('app'); expect(treeAcc).toBeNull(); }); }); describe('flattenTree', () => { it('returns flattened directory structure', () => { const tree = [ { type: 'tree', name: 'app', tree: [ { type: 'tree', name: 'javascripts', tree: [ { type: 'blob', name: 'index.js', tree: [], }, ], }, ], }, { type: 'tree', name: 'ee', tree: [ { type: 'tree', name: 'lib', tree: [ { type: 'tree', name: 'ee', tree: [ { type: 'tree', name: 'gitlab', tree: [ { type: 'tree', name: 'checks', tree: [ { type: 'tree', name: 'longtreenametomakepath', tree: [ { type: 'blob', name: 'diff_check.rb', tree: [], }, ], }, ], }, ], }, ], }, ], }, ], }, { type: 'tree', name: 'spec', tree: [ { type: 'tree', name: 'javascripts', tree: [], }, { type: 'blob', name: 'index_spec.js', tree: [], }, ], }, ]; const flattened = utils.flattenTree(tree); expect(flattened).toEqual([ { type: 'tree', name: 'app/javascripts', tree: [ { type: 'blob', name: 'index.js', tree: [], }, ], }, { type: 'tree', name: 'ee/lib/…/…/…/longtreenametomakepath', tree: [ { name: 'diff_check.rb', tree: [], type: 'blob', }, ], }, { type: 'tree', name: 'spec', tree: [ { type: 'tree', name: 'javascripts', tree: [], }, { type: 'blob', name: 'index_spec.js', tree: [], }, ], }, ]); }); }); describe('convertExpandLines', () => { it('converts expanded lines to normal lines', () => { const diffLines = [ { type: 'match', old_line: 1, new_line: 1, }, { type: '', old_line: 2, new_line: 2, }, ]; const lines = utils.convertExpandLines({ diffLines, data: [{ text: 'expanded' }], typeKey: 'type', oldLineKey: 'old_line', newLineKey: 'new_line', mapLine: ({ line, oldLine, newLine }) => ({ ...line, old_line: oldLine, new_line: newLine, }), }); expect(lines).toEqual([ { text: 'expanded', new_line: 1, old_line: 1, discussions: [], hasForm: false, }, { type: '', old_line: 2, new_line: 2, }, ]); }); }); describe('getDefaultWhitespace', () => { it('defaults to true if querystring and cookie are undefined', () => { expect(utils.getDefaultWhitespace()).toBe(true); }); it('returns false if querystring is `1`', () => { expect(utils.getDefaultWhitespace('1', '0')).toBe(false); }); it('returns true if querystring is `0`', () => { expect(utils.getDefaultWhitespace('0', undefined)).toBe(true); }); it('returns false if cookie is `1`', () => { expect(utils.getDefaultWhitespace(undefined, '1')).toBe(false); }); it('returns true if cookie is `0`', () => { expect(utils.getDefaultWhitespace(undefined, '0')).toBe(true); }); }); describe('isAdded', () => { it.each` type | expected ${'new'} | ${true} ${'new-nonewline'} | ${true} ${'old'} | ${false} `('returns $expected when type is $type', ({ type, expected }) => { expect(utils.isAdded({ type })).toBe(expected); }); }); describe('isRemoved', () => { it.each` type | expected ${'old'} | ${true} ${'old-nonewline'} | ${true} ${'new'} | ${false} `('returns $expected when type is $type', ({ type, expected }) => { expect(utils.isRemoved({ type })).toBe(expected); }); }); describe('isUnchanged', () => { it.each` type | expected ${null} | ${true} ${'new'} | ${false} ${'old'} | ${false} `('returns $expected when type is $type', ({ type, expected }) => { expect(utils.isUnchanged({ type })).toBe(expected); }); }); describe('isMeta', () => { it.each` type | expected ${'match'} | ${true} ${'new-nonewline'} | ${true} ${'old-nonewline'} | ${true} ${'new'} | ${false} `('returns $expected when type is $type', ({ type, expected }) => { expect(utils.isMeta({ type })).toBe(expected); }); }); describe('isConflictMarker', () => { it.each` type | expected ${'conflict_marker_our'} | ${true} ${'conflict_marker_their'} | ${true} ${'conflict_their'} | ${false} ${'conflict_our'} | ${false} `('returns $expected when type is $type', ({ type, expected }) => { expect(utils.isConflictMarker({ type })).toBe(expected); }); }); describe('isConflictOur', () => { it.each` type | expected ${'conflict_marker_our'} | ${false} ${'conflict_marker_their'} | ${false} ${'conflict_their'} | ${false} ${'conflict_our'} | ${true} `('returns $expected when type is $type', ({ type, expected }) => { expect(utils.isConflictOur({ type })).toBe(expected); }); }); describe('isConflictTheir', () => { it.each` type | expected ${'conflict_marker_our'} | ${false} ${'conflict_marker_their'} | ${false} ${'conflict_their'} | ${true} ${'conflict_our'} | ${false} `('returns $expected when type is $type', ({ type, expected }) => { expect(utils.isConflictTheir({ type })).toBe(expected); }); }); describe('parallelizeDiffLines', () => { it('converts inline diff lines to parallel diff lines', () => { const file = getDiffFileMock(); expect(utils.parallelizeDiffLines(file[INLINE_DIFF_LINES_KEY])).toMatchObject( file.parallel_diff_lines, ); }); it('converts conflicted diffs line', () => { const lines = [ { type: 'new' }, { type: 'conflict_marker_our' }, { type: 'conflict_our' }, { type: 'conflict_marker' }, { type: 'conflict_their' }, { type: 'conflict_marker_their' }, ]; expect(utils.parallelizeDiffLines(lines)).toEqual([ { left: null, right: { chunk: 0, type: 'new', }, }, { left: { chunk: 0, type: 'conflict_marker_our' }, right: { chunk: 0, type: 'conflict_marker_their' }, }, { left: { chunk: 0, type: 'conflict_our' }, right: { chunk: 0, type: 'conflict_their' }, }, ]); }); it('converts inline diff lines', () => { const file = getDiffFileMock(); const files = utils.parallelizeDiffLines(file.highlighted_diff_lines, true); expect(files[5].left).toMatchObject(file.parallel_diff_lines[5].left); expect(files[5].right).toBeNull(); expect(files[6].left).toMatchObject(file.parallel_diff_lines[5].right); expect(files[6].right).toBeNull(); }); }); });