Commit b01abe82 authored by Paul Slaughter's avatar Paul Slaughter

Setup MR diff interoperability spec

This also sets up some integration test
helpers for diffs which will be helpful
in the long run
parent 42954bd5
......@@ -9,7 +9,8 @@ import {
import { fileByFile } from '../../utils/preferences';
import { getDefaultWhitespace } from '../utils';
const viewTypeFromQueryString = getParameterValues('view')[0];
const getViewTypeFromQueryString = () => getParameterValues('view')[0];
const viewTypeFromCookie = Cookies.get(DIFF_VIEW_COOKIE_NAME);
const defaultViewType = INLINE_DIFF_VIEW_TYPE;
const whiteSpaceFromQueryString = getParameterValues('w')[0];
......@@ -32,7 +33,7 @@ export default () => ({
codequalityDiff: {},
mergeRequestDiffs: [],
mergeRequestDiff: null,
diffViewType: viewTypeFromQueryString || viewTypeFromCookie || defaultViewType,
diffViewType: getViewTypeFromQueryString() || viewTypeFromCookie || defaultViewType,
tree: [],
treeEntries: {},
showTreeList: true,
......
......@@ -211,19 +211,20 @@ describe('CompareVersions', () => {
});
describe('prev commit', () => {
const { location } = window;
beforeAll(() => {
delete window.location;
window.location = { href: `${TEST_HOST}?commit_id=${mrCommit.id}` };
global.jsdom.reconfigure({
url: `${TEST_HOST}?commit_id=${mrCommit.id}`,
});
});
beforeEach(() => {
jest.spyOn(wrapper.vm, 'moveToNeighboringCommit').mockImplementation(() => {});
afterAll(() => {
global.jsdom.reconfigure({
url: TEST_HOST,
});
});
afterAll(() => {
window.location = location;
beforeEach(() => {
jest.spyOn(wrapper.vm, 'moveToNeighboringCommit').mockImplementation(() => {});
});
it('uses the correct href', () => {
......@@ -253,19 +254,20 @@ describe('CompareVersions', () => {
});
describe('next commit', () => {
const { location } = window;
beforeAll(() => {
delete window.location;
window.location = { href: `${TEST_HOST}?commit_id=${mrCommit.id}` };
global.jsdom.reconfigure({
url: `${TEST_HOST}?commit_id=${mrCommit.id}`,
});
});
beforeEach(() => {
jest.spyOn(wrapper.vm, 'moveToNeighboringCommit').mockImplementation(() => {});
afterAll(() => {
global.jsdom.reconfigure({
url: TEST_HOST,
});
});
afterAll(() => {
window.location = location;
beforeEach(() => {
jest.spyOn(wrapper.vm, 'moveToNeighboringCommit').mockImplementation(() => {});
});
it('uses the correct href', () => {
......
......@@ -25,6 +25,10 @@ RSpec.describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)'
end
before do
# Create a user that matches the project.commit author
# This is so that the "author" information will be populated
create(:user, email: project.commit.author_email, name: project.commit.author_name)
sign_in(user)
end
......@@ -33,17 +37,21 @@ RSpec.describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)'
end
it 'merge_request_diffs/with_commit.json' do
# Create a user that matches the project.commit author
# This is so that the "author" information will be populated
create(:user, email: project.commit.author_email, name: project.commit.author_name)
render_merge_request(merge_request, commit_id: project.commit.sha)
end
it 'merge_request_diffs/diffs_metadata.json' do
render_merge_request(merge_request, action: :diffs_metadata)
end
it 'merge_request_diffs/diffs_batch.json' do
render_merge_request(merge_request, action: :diffs_batch, page: 1, per_page: 30)
end
private
def render_merge_request(merge_request, view: 'inline', **extra_params)
get :show, params: {
def render_merge_request(merge_request, action: :show, view: 'inline', **extra_params)
get action, params: {
namespace_id: project.namespace.to_param,
project_id: project,
id: merge_request.to_param,
......
/**
* This helper module helps freeze the API expectation of the diff output.
*
* This helps simulate what third-parties, such as Sourcegraph, which scrape
* the HTML shold be looking fo.
*
* TEMPORARY! These functions are copied from Sourcegraph
*/
export const getDiffCodePart = (codeElement) => {
let selector = 'old';
const row = codeElement.closest('.diff-td,td');
// Split diff
if (row.classList.contains('parallel')) {
selector = 'left-side';
}
return row.classList.contains(selector) ? 'base' : 'head';
};
export const getCodeElementFromLineNumber = (codeView, line, part) => {
const lineNumberElement = codeView.querySelector(
`.${part === 'base' ? 'old_line' : 'new_line'} [data-linenumber="${line}"]`,
);
if (!lineNumberElement) {
return null;
}
const row = lineNumberElement.closest('.diff-tr,tr');
if (!row) {
return null;
}
let selector = 'span.line';
// Split diff
if (row.classList.contains('parallel')) {
selector = `.${part === 'base' ? 'left-side' : 'right-side'} ${selector}`;
}
return row.querySelector(selector);
};
export const getLineNumberFromCodeElement = (el) => {
const part = getDiffCodePart(el);
let cell = el.closest('.diff-td,td');
while (
cell &&
!cell.matches(`.diff-line-num.${part === 'base' ? 'old_line' : 'new_line'}`) &&
cell.previousElementSibling
) {
cell = cell.previousElementSibling;
}
if (cell) {
const a = cell.querySelector('a');
return parseInt(a.dataset.linenumber || '', 10);
}
throw new Error('Unable to determine line number for diff code element');
};
import { waitFor } from '@testing-library/dom';
import { TEST_HOST } from 'helpers/test_constants';
import initDiffsApp from '~/diffs';
import { createStore } from '~/mr_notes/stores';
import { getDiffCodePart, getLineNumberFromCodeElement } from './diffs_interopability_api';
jest.mock('~/vue_shared/mixins/gl_feature_flags_mixin', () => () => ({
inject: {
glFeatures: {
from: 'window.gon.features',
default: () => global.window.gon?.features,
},
},
}));
const TEST_PROJECT_PATH = 'gitlab-org/gitlab-test';
const TEST_BASE_URL = `/${TEST_PROJECT_PATH}/-/merge_requests/1/`;
const TEST_DIFF_FILE = 'files/js/commit.coffee';
const EXPECT_INLINE = [
['head', 1],
['head', 2],
['head', 3],
['base', 5],
['head', 4],
null,
['base', 6],
['head', 6],
null,
];
const EXPECT_PARALLEL_LEFT_SIDE = [
['base', 1],
['base', 2],
['base', 3],
['base', 4],
null,
['base', 6],
null,
];
const EXPECT_PARALLEL_RIGHT_SIDE = [
['head', 1],
['head', 2],
['head', 3],
['head', 4],
null,
['head', 6],
null,
];
const startDiffsApp = () => {
const el = document.createElement('div');
el.id = 'js-diffs-app';
document.body.appendChild(el);
Object.assign(el.dataset, {
endpoint: TEST_BASE_URL,
endpointMetadata: `${TEST_BASE_URL}diffs_metadata.json`,
endpointBatch: `${TEST_BASE_URL}diffs_batch.json`,
projectPath: TEST_PROJECT_PATH,
helpPagePath: '/help',
currentUserData: 'null',
changesEmptyStateIllustration: '',
isFluidLayout: 'false',
dismissEndpoint: '',
showSuggestPopover: 'false',
showWhitespaceDefault: 'true',
viewDiffsFileByFile: 'false',
defaultSuggestionCommitMessage: 'Lorem ipsum',
});
const store = createStore();
const vm = initDiffsApp(store);
store.dispatch('setActiveTab', 'diffs');
return vm;
};
describe('diffs third party interoperability', () => {
let vm;
afterEach(() => {
vm.$destroy();
document.body.innerHTML = '';
});
const tryOrErrorMessage = (fn) => (...args) => {
try {
return fn(...args);
} catch (e) {
return e.message;
}
};
const findDiffFile = () => document.querySelector(`.diff-file[data-path="${TEST_DIFF_FILE}"]`);
const hasLines = (sel = 'tr.line_holder') => findDiffFile().querySelectorAll(sel).length > 0;
const findLineElements = (sel = 'tr.line_holder') =>
Array.from(findDiffFile().querySelectorAll(sel));
const findCodeElements = (lines, sel = 'td.line_content') => {
return lines.map((x) => x.querySelector(`${sel} span.line`));
};
const getCodeElementsInteropModel = (codeElements) =>
codeElements.map(
(x) =>
x && [
tryOrErrorMessage(getDiffCodePart)(x),
tryOrErrorMessage(getLineNumberFromCodeElement)(x),
],
);
// ${'inline view'} | ${false} | ${'inline'} | ${'tr.line_holder'} | ${'td.line_content'} | ${EXPECT_INLINE}
// ${'parallel view left side'} | ${false} | ${'parallel'} | ${'tr.line_holder'} | ${'td.line_content.left-side'} | ${EXPECT_PARALLEL_LEFT_SIDE}
// ${'parallel view right side'} | ${false} | ${'parallel'} | ${'tr.line_holder'} | ${'td.line_content.right-side'} | ${EXPECT_PARALLEL_RIGHT_SIDE}
// ${'inline view'} | ${true} | ${'inline'} | ${'.diff-tr.line_holder'} | ${'.diff-td.line_content'} | ${EXPECT_INLINE}
// ${'parallel view left side'} | ${true} | ${'parallel'} | ${'.diff-tr.line_holder'} | ${'.diff-td.line_content.left-side'} | ${EXPECT_PARALLEL_LEFT_SIDE.slice(0, EXPECT_PARALLEL_LEFT_SIDE.length - 1)}
// ${'parallel view right side'} | ${true} | ${'parallel'} | ${'.diff-tr.line_holder'} | ${'.diff-td.line_content.right-side'} | ${EXPECT_PARALLEL_RIGHT_SIDE.slice(0, EXPECT_PARALLEL_RIGHT_SIDE.length - 1)}
describe.each`
desc | unifiedDiffComponents | view | rowSelector | codeSelector | expectation
${'inline view'} | ${false} | ${'inline'} | ${'tr.line_holder'} | ${'td.line_content'} | ${EXPECT_INLINE}
${'parallel view left side'} | ${false} | ${'parallel'} | ${'tr.line_holder'} | ${'td.line_content.left-side'} | ${EXPECT_PARALLEL_LEFT_SIDE}
${'parallel view right side'} | ${false} | ${'parallel'} | ${'tr.line_holder'} | ${'td.line_content.right-side'} | ${EXPECT_PARALLEL_RIGHT_SIDE}
${'inline view'} | ${true} | ${'inline'} | ${'.diff-tr.line_holder'} | ${'.diff-td.line_content'} | ${EXPECT_INLINE}
${'parallel view left side'} | ${true} | ${'parallel'} | ${'.diff-tr.line_holder'} | ${'.diff-td.line_content.left-side'} | ${EXPECT_PARALLEL_LEFT_SIDE}
${'parallel view right side'} | ${true} | ${'parallel'} | ${'.diff-tr.line_holder'} | ${'.diff-td.line_content.right-side'} | ${EXPECT_PARALLEL_RIGHT_SIDE}
`(
'$desc (unifiedDiffComponents=$unifiedDiffComponents)',
({ unifiedDiffComponents, view, rowSelector, codeSelector, expectation }) => {
beforeEach(async () => {
global.jsdom.reconfigure({
url: `${TEST_HOST}/${TEST_BASE_URL}/diffs?view=${view}`,
});
window.gon.features = { unifiedDiffComponents };
vm = startDiffsApp();
await waitFor(() => expect(hasLines(rowSelector)).toBe(true));
});
it('should match diff model', () => {
const lines = findLineElements(rowSelector);
const codes = findCodeElements(lines, codeSelector);
expect(getCodeElementsInteropModel(codes)).toEqual(expectation);
});
},
);
});
......@@ -40,6 +40,12 @@ export const getMergeRequestVersions = factory.json(() =>
export const getRepositoryFiles = factory.json(() =>
require('test_fixtures/projects_json/files.json'),
);
export const getDiffsMetadata = factory.json(() =>
require('test_fixtures/merge_request_diffs/diffs_metadata.json'),
);
export const getDiffsBatch = factory.json(() =>
require('test_fixtures/merge_request_diffs/diffs_batch.json'),
);
export const getPipelinesEmptyResponse = factory.json(() =>
require('test_fixtures/projects_json/pipelines_empty.json'),
);
......
import { getDiffsMetadata, getDiffsBatch } from 'test_helpers/fixtures';
import { withValues } from 'test_helpers/utils/obj';
export default (server) => {
server.get('/:namespace/:project/-/merge_requests/:mrid/diffs_metadata.json', () => {
return getDiffsMetadata();
});
server.get('/:namespace/:project/-/merge_requests/:mrid/diffs_batch.json', () => {
const { pagination, ...result } = getDiffsBatch();
return {
...result,
pagination: withValues(pagination, {
current_page: null,
next_page: null,
total_pages: 1,
next_page_href: null,
}),
};
});
};
......@@ -5,6 +5,7 @@ export default (server) => {
require('./projects'),
require('./repository'),
require('./ci'),
require('./diffs'),
require('./404'),
].forEach(({ default: setup }) => {
setup(server);
......
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