blob: b6e26ab832a0c069657aff175a04a1f8b57c80e3 [file] [log] [blame]
/**
* @license
* Copyright 2015 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import '../../../test/common-test-setup';
import './gr-diff-view';
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
import {
ChangeStatus,
DiffViewMode,
createDefaultDiffPrefs,
createDefaultPreferences,
} from '../../../constants/constants';
import {
isVisible,
pressKey,
query,
queryAll,
queryAndAssert,
stubReporting,
stubRestApi,
stubUsers,
waitEventLoop,
waitUntil,
} from '../../../test/test-utils';
import {ChangeComments} from '../gr-comment-api/gr-comment-api';
import {GerritView} from '../../../services/router/router-model';
import {
createRevisions,
createComment as createCommentGeneric,
TEST_NUMERIC_CHANGE_ID,
createDiff,
createPatchRange,
createServerInfo,
createConfig,
createParsedChange,
createRevision,
createCommit,
createFileInfo,
} from '../../../test/test-data-generators';
import {
BasePatchSetNum,
CommentInfo,
CommitId,
EDIT,
FileInfo,
NumericChangeId,
PARENT,
PatchRange,
PatchSetNum,
PatchSetNumber,
PathToCommentsInfoMap,
RepoName,
RevisionPatchSetNum,
UrlEncodedCommentId,
} from '../../../types/common';
import {CursorMoveResult} from '../../../api/core';
import {DiffInfo, Side} from '../../../api/diff';
import {Files, GrDiffView} from './gr-diff-view';
import {DropdownItem} from '../../shared/gr-dropdown-list/gr-dropdown-list';
import {SinonFakeTimers, SinonStub, SinonSpy} from 'sinon';
import {LoadingStatus} from '../../../models/change/change-model';
import {CommentMap} from '../../../utils/comment-util';
import {ParsedChangeInfo} from '../../../types/types';
import {assertIsDefined} from '../../../utils/common-util';
import {GrDiffModeSelector} from '../../../embed/diff/gr-diff-mode-selector/gr-diff-mode-selector';
import {fixture, html, assert} from '@open-wc/testing';
import {EventType} from '../../../types/events';
import {Key} from '../../../utils/dom-util';
import {GrButton} from '../../shared/gr-button/gr-button';
import {testResolver} from '../../../test/common-test-setup';
function createComment(
id: string,
line: number,
ps: number | PatchSetNum,
path: string
): CommentInfo {
return {
...createCommentGeneric(),
id: id as UrlEncodedCommentId,
line,
patch_set: ps as RevisionPatchSetNum,
path,
};
}
suite('gr-diff-view tests', () => {
suite('basic tests', () => {
let element: GrDiffView;
let clock: SinonFakeTimers;
let diffCommentsStub;
let getDiffRestApiStub: SinonStub;
let setUrlStub: SinonStub;
function getFilesFromFileList(fileList: string[]): Files {
const changeFilesByPath = fileList.reduce((files, path) => {
files[path] = createFileInfo();
return files;
}, {} as {[path: string]: FileInfo});
return {
sortedFileList: fileList,
changeFilesByPath,
};
}
setup(async () => {
setUrlStub = sinon.stub(testResolver(navigationToken), 'setUrl');
stubRestApi('getConfig').returns(Promise.resolve(createServerInfo()));
stubRestApi('getLoggedIn').returns(Promise.resolve(false));
stubRestApi('getProjectConfig').returns(Promise.resolve(createConfig()));
stubRestApi('getChangeFiles').returns(
Promise.resolve({
'chell.go': createFileInfo(),
'glados.txt': createFileInfo(),
'wheatley.md': createFileInfo(),
})
);
stubRestApi('saveFileReviewed').returns(Promise.resolve(new Response()));
diffCommentsStub = stubRestApi('getDiffComments');
diffCommentsStub.returns(Promise.resolve({}));
stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
stubRestApi('getPortedComments').returns(Promise.resolve({}));
element = await fixture(html`<gr-diff-view></gr-diff-view>`);
element.changeNum = 42 as NumericChangeId;
element.path = 'some/path.txt';
element.change = createParsedChange();
element.diff = {...createDiff(), content: []};
getDiffRestApiStub = stubRestApi('getDiff');
// Delayed in case a test updates element.diff.
getDiffRestApiStub.callsFake(() => Promise.resolve(element.diff));
element.patchRange = createPatchRange();
element.changeComments = new ChangeComments({
'/COMMIT_MSG': [
createComment('c1', 10, 2, '/COMMIT_MSG'),
createComment('c3', 10, PARENT, '/COMMIT_MSG'),
],
});
await element.updateComplete;
element.getCommentsModel().setState({
comments: {},
robotComments: {},
drafts: {},
portedComments: {},
portedDrafts: {},
discardedDrafts: [],
});
});
teardown(() => {
clock && clock.restore();
sinon.restore();
});
test('viewState change triggers diffViewDisplayed()', () => {
const diffViewDisplayedStub = stubReporting('diffViewDisplayed');
assertIsDefined(element.diffHost);
sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
sinon.stub(element, 'initPatchRange');
sinon.stub(element, 'fetchFiles');
const viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
element.viewState = {
view: GerritView.DIFF,
changeNum: 42 as NumericChangeId,
patchNum: 2 as RevisionPatchSetNum,
basePatchNum: 1 as BasePatchSetNum,
path: '/COMMIT_MSG',
};
element.path = '/COMMIT_MSG';
element.patchRange = createPatchRange();
return viewStateChangedSpy.returnValues[0]?.then(() => {
assert.isTrue(diffViewDisplayedStub.calledOnce);
});
});
suite('comment route', () => {
let initLineOfInterestAndCursorStub: SinonStub;
let replaceStateStub: SinonStub;
let viewStateChangedSpy: SinonSpy;
setup(() => {
initLineOfInterestAndCursorStub = sinon.stub(
element,
'initLineOfInterestAndCursor'
);
replaceStateStub = sinon.stub(history, 'replaceState');
sinon.stub(element, 'fetchFiles');
stubReporting('diffViewDisplayed');
assertIsDefined(element.diffHost);
sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
element.getChangeModel().setState({
change: {
...createParsedChange(),
revisions: createRevisions(11),
},
loadingStatus: LoadingStatus.LOADED,
});
});
test('comment url resolves to comment.patch_set vs latest', () => {
element.getCommentsModel().setState({
comments: {
'/COMMIT_MSG': [
createComment('c1', 10, 2, '/COMMIT_MSG'),
createComment('c3', 10, PARENT, '/COMMIT_MSG'),
],
},
robotComments: {},
drafts: {},
portedComments: {},
portedDrafts: {},
discardedDrafts: [],
});
element.viewState = {
view: GerritView.DIFF,
changeNum: 42 as NumericChangeId,
commentLink: true,
commentId: 'c1' as UrlEncodedCommentId,
path: 'abcd',
patchNum: 1 as RevisionPatchSetNum,
};
element.change = {
...createParsedChange(),
revisions: createRevisions(11),
};
return viewStateChangedSpy.returnValues[0].then(() => {
assert.isTrue(
initLineOfInterestAndCursorStub.calledWithExactly(true)
);
assert.equal(element.focusLineNum, 10);
assert.equal(element.patchRange?.patchNum, 11 as RevisionPatchSetNum);
assert.equal(element.patchRange?.basePatchNum, 2 as BasePatchSetNum);
assert.isTrue(replaceStateStub.called);
});
});
});
test('viewState change causes blame to load if it was set to true', () => {
// Blame loads for subsequent files if it was loaded for one file
element.isBlameLoaded = true;
stubReporting('diffViewDisplayed');
const loadBlameStub = sinon.stub(element, 'loadBlame');
assertIsDefined(element.diffHost);
sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
const viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
sinon.stub(element, 'initPatchRange');
sinon.stub(element, 'fetchFiles');
element.viewState = {
view: GerritView.DIFF,
changeNum: 42 as NumericChangeId,
patchNum: 2 as RevisionPatchSetNum,
basePatchNum: 1 as BasePatchSetNum,
path: '/COMMIT_MSG',
};
element.path = '/COMMIT_MSG';
element.patchRange = createPatchRange();
return viewStateChangedSpy.returnValues[0]!.then(() => {
assert.isTrue(element.isBlameLoaded);
assert.isTrue(loadBlameStub.calledOnce);
});
});
test('unchanged diff X vs latest from comment links navigates to base vs X', async () => {
element.getCommentsModel().setState({
comments: {
'/COMMIT_MSG': [
createComment('c1', 10, 2, '/COMMIT_MSG'),
createComment('c3', 10, PARENT, '/COMMIT_MSG'),
],
},
robotComments: {},
drafts: {},
portedComments: {},
portedDrafts: {},
discardedDrafts: [],
});
stubReporting('diffViewDisplayed');
sinon.stub(element, 'loadBlame');
assertIsDefined(element.diffHost);
sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
sinon.stub(element, 'isFileUnchanged').returns(true);
const viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
element.getChangeModel().setState({
change: {
...createParsedChange(),
revisions: createRevisions(11),
},
loadingStatus: LoadingStatus.LOADED,
});
element.viewState = {
view: GerritView.DIFF,
changeNum: 42 as NumericChangeId,
path: '/COMMIT_MSG',
commentLink: true,
commentId: 'c1' as UrlEncodedCommentId,
};
element.change = {
...createParsedChange(),
revisions: createRevisions(11),
};
await viewStateChangedSpy.returnValues[0];
assert.isTrue(setUrlStub.calledOnce);
assert.equal(
setUrlStub.lastCall.firstArg,
'/c/test-project/+/42/2//COMMIT_MSG#10'
);
});
test('unchanged diff Base vs latest from comment does not navigate', async () => {
element.getCommentsModel().setState({
comments: {
'/COMMIT_MSG': [
createComment('c1', 10, 2, '/COMMIT_MSG'),
createComment('c3', 10, PARENT, '/COMMIT_MSG'),
],
},
robotComments: {},
drafts: {},
portedComments: {},
portedDrafts: {},
discardedDrafts: [],
});
stubReporting('diffViewDisplayed');
sinon.stub(element, 'loadBlame');
assertIsDefined(element.diffHost);
sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
sinon.stub(element, 'isFileUnchanged').returns(true);
const viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
element.getChangeModel().setState({
change: {
...createParsedChange(),
revisions: createRevisions(11),
},
loadingStatus: LoadingStatus.LOADED,
});
element.viewState = {
view: GerritView.DIFF,
changeNum: 42 as NumericChangeId,
path: '/COMMIT_MSG',
commentLink: true,
commentId: 'c3' as UrlEncodedCommentId,
};
element.change = {
...createParsedChange(),
revisions: createRevisions(11),
};
await viewStateChangedSpy.returnValues[0];
assert.isFalse(setUrlStub.calledOnce);
});
test('isFileUnchanged', () => {
let diff: DiffInfo = {
...createDiff(),
content: [
{a: ['abcd'], ab: ['ef']},
{b: ['ancd'], a: ['xx']},
],
};
assert.equal(element.isFileUnchanged(diff), false);
diff = {
...createDiff(),
content: [{ab: ['abcd']}, {ab: ['ancd']}],
};
assert.equal(element.isFileUnchanged(diff), true);
diff = {
...createDiff(),
content: [
{a: ['abcd'], ab: ['ef'], common: true},
{b: ['ancd'], ab: ['xx']},
],
};
assert.equal(element.isFileUnchanged(diff), false);
diff = {
...createDiff(),
content: [
{a: ['abcd'], ab: ['ef'], common: true},
{b: ['ancd'], ab: ['xx'], common: true},
],
};
assert.equal(element.isFileUnchanged(diff), true);
});
test('diff toast to go to latest is shown and not base', async () => {
element.getCommentsModel().setState({
comments: {
'/COMMIT_MSG': [
createComment('c1', 10, 2, '/COMMIT_MSG'),
createComment('c3', 10, PARENT, '/COMMIT_MSG'),
],
},
robotComments: {},
drafts: {},
portedComments: {},
portedDrafts: {},
discardedDrafts: [],
});
stubReporting('diffViewDisplayed');
sinon.stub(element, 'loadBlame');
assertIsDefined(element.diffHost);
sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
const viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
element.change = undefined;
element.getChangeModel().setState({
change: {
...createParsedChange(),
revisions: createRevisions(11),
},
loadingStatus: LoadingStatus.LOADED,
});
element.patchRange = {
patchNum: 2 as RevisionPatchSetNum,
basePatchNum: 1 as BasePatchSetNum,
};
sinon.stub(element, 'isFileUnchanged').returns(false);
const toastStub = sinon.stub(element, 'displayDiffBaseAgainstLeftToast');
element.viewState = {
view: GerritView.DIFF,
changeNum: 42 as NumericChangeId,
project: 'p' as RepoName,
commentId: 'c1' as UrlEncodedCommentId,
commentLink: true,
};
await viewStateChangedSpy.returnValues[0];
assert.isTrue(toastStub.called);
});
test('toggle left diff with a hotkey', () => {
assertIsDefined(element.diffHost);
const toggleLeftDiffStub = sinon.stub(element.diffHost, 'toggleLeftDiff');
pressKey(element, 'A');
assert.isTrue(toggleLeftDiffStub.calledOnce);
});
test('renders', async () => {
clock = sinon.useFakeTimers();
element.changeNum = 42 as NumericChangeId;
element.getBrowserModel().setScreenWidth(0);
element.patchRange = {
basePatchNum: PARENT,
patchNum: 10 as RevisionPatchSetNum,
};
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
revisions: {
a: createRevision(10),
},
};
element.files = getFilesFromFileList([
'chell.go',
'glados.txt',
'wheatley.md',
]);
element.path = 'glados.txt';
element.loggedIn = true;
await element.updateComplete;
assert.shadowDom.equal(
element,
/* HTML */ `
<div class="stickyHeader">
<h1 class="assistive-tech-only">Diff of glados.txt</h1>
<header>
<div>
<a href="/c/test-project/+/42"> 42 </a>
<span class="changeNumberColon"> : </span>
<span class="headerSubject"> Test subject </span>
<input
aria-label="file reviewed"
class="hideOnEdit reviewed"
id="reviewed"
title="Toggle reviewed status of file"
type="checkbox"
/>
<div class="jumpToFileContainer">
<gr-dropdown-list id="dropdown" show-copy-for-trigger-text="">
</gr-dropdown-list>
</div>
</div>
<div class="desktop navLinks">
<span class="fileNum show">
File 2 of 3
<span class="separator"> </span>
</span>
<a
class="navLink"
href="/c/test-project/+/42/10/chell.go"
title="Go to previous file (shortcut: [)"
>
Prev
</a>
<span class="separator"> </span>
<a
class="navLink"
href="/c/test-project/+/42"
title="Up to change (shortcut: u)"
>
Up
</a>
<span class="separator"> </span>
<a
class="navLink"
href="/c/test-project/+/42/10/wheatley.md"
title="Go to next file (shortcut: ])"
>
Next
</a>
</div>
</header>
<div class="subHeader">
<div class="patchRangeLeft">
<gr-patch-range-select id="rangeSelect">
</gr-patch-range-select>
<span class="desktop download">
<span class="separator"> </span>
<gr-dropdown down-arrow="" horizontal-align="left" link="">
<span class="downloadTitle"> Download </span>
</gr-dropdown>
</span>
</div>
<div class="rightControls">
<span class="blameLoader show">
<gr-button
aria-disabled="false"
id="toggleBlame"
link=""
role="button"
tabindex="0"
title="Toggle blame (shortcut: b)"
>
Show blame
</gr-button>
</span>
<span class="separator"> </span>
<span class="editButton">
<gr-button
aria-disabled="false"
link=""
role="button"
tabindex="0"
title="Edit current file"
>
edit
</gr-button>
</span>
<span class="separator"> </span>
<div class="diffModeSelector">
<span> Diff view: </span>
<gr-diff-mode-selector id="modeSelect" show-tooltip-below="">
</gr-diff-mode-selector>
</div>
<span id="diffPrefsContainer">
<span class="desktop preferences">
<gr-tooltip-content
has-tooltip=""
position-below=""
title="Diff preferences"
>
<gr-button
aria-disabled="false"
class="prefsButton"
link=""
role="button"
tabindex="0"
>
<gr-icon icon="settings" filled></gr-icon>
</gr-button>
</gr-tooltip-content>
</span>
</span>
<gr-endpoint-decorator name="annotation-toggler">
<span hidden="" id="annotation-span">
<label for="annotation-checkbox" id="annotation-label">
</label>
<iron-input>
<input
disabled=""
id="annotation-checkbox"
is="iron-input"
type="checkbox"
value=""
/>
</iron-input>
</span>
</gr-endpoint-decorator>
</div>
</div>
<div class="fileNav mobile">
<a class="mobileNavLink" href="/c/test-project/+/42/10/chell.go">
<
</a>
<div class="fullFileName mobile">glados.txt</div>
<a
class="mobileNavLink"
href="/c/test-project/+/42/10/wheatley.md"
>
>
</a>
</div>
</div>
<div class="loading">Loading...</div>
<h2 class="assistive-tech-only">Diff view</h2>
<gr-diff-host hidden="" id="diffHost"> </gr-diff-host>
<gr-apply-fix-dialog id="applyFixDialog"> </gr-apply-fix-dialog>
<gr-diff-preferences-dialog id="diffPreferencesDialog">
</gr-diff-preferences-dialog>
<gr-overlay
aria-hidden="true"
id="downloadOverlay"
style="outline: none; display: none;"
>
<gr-download-dialog id="downloadDialog" role="dialog">
</gr-download-dialog>
</gr-overlay>
`
);
});
test('keyboard shortcuts', async () => {
clock = sinon.useFakeTimers();
element.changeNum = 42 as NumericChangeId;
element.getBrowserModel().setScreenWidth(0);
element.patchRange = {
basePatchNum: PARENT,
patchNum: 10 as RevisionPatchSetNum,
};
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
revisions: {
a: createRevision(10),
},
};
element.files = getFilesFromFileList([
'chell.go',
'glados.txt',
'wheatley.md',
]);
element.path = 'glados.txt';
element.loggedIn = true;
await element.updateComplete;
setUrlStub.reset();
pressKey(element, 'u');
assert.equal(setUrlStub.callCount, 1);
assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42');
await element.updateComplete;
pressKey(element, ']');
assert.equal(setUrlStub.callCount, 2);
assert.equal(
setUrlStub.lastCall.firstArg,
'/c/test-project/+/42/10/wheatley.md'
);
element.path = 'wheatley.md';
await element.updateComplete;
assert.isTrue(element.loading);
pressKey(element, '[');
assert.equal(setUrlStub.callCount, 3);
assert.equal(
setUrlStub.lastCall.firstArg,
'/c/test-project/+/42/10/glados.txt'
);
element.path = 'glados.txt';
await element.updateComplete;
assert.isTrue(element.loading);
pressKey(element, '[');
assert.equal(setUrlStub.callCount, 4);
assert.equal(
setUrlStub.lastCall.firstArg,
'/c/test-project/+/42/10/chell.go'
);
element.path = 'chell.go';
await element.updateComplete;
assert.isTrue(element.loading);
pressKey(element, '[');
assert.equal(setUrlStub.callCount, 5);
assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42');
await element.updateComplete;
assert.isTrue(element.loading);
assertIsDefined(element.diffPreferencesDialog);
const showPrefsStub = sinon
.stub(element.diffPreferencesDialog, 'open')
.callsFake(() => Promise.resolve());
pressKey(element, ',');
await element.updateComplete;
assert(showPrefsStub.calledOnce);
assertIsDefined(element.cursor);
let scrollStub = sinon.stub(element.cursor, 'moveToNextChunk');
pressKey(element, 'n');
await element.updateComplete;
assert(scrollStub.calledOnce);
scrollStub = sinon.stub(element.cursor, 'moveToPreviousChunk');
pressKey(element, 'p');
await element.updateComplete;
assert(scrollStub.calledOnce);
scrollStub = sinon.stub(element.cursor, 'moveToNextCommentThread');
pressKey(element, 'N');
await element.updateComplete;
assert(scrollStub.calledOnce);
scrollStub = sinon.stub(element.cursor, 'moveToPreviousCommentThread');
pressKey(element, 'P');
await element.updateComplete;
assert(scrollStub.calledOnce);
assertIsDefined(element.diffHost);
assertIsDefined(element.diffHost.diffElement);
pressKey(element, 'j');
await element.updateComplete;
assert.equal(
element.diffHost.diffElement.viewMode,
DiffViewMode.SIDE_BY_SIDE
);
assert.isTrue(element.diffHost.diffElement.displayLine);
pressKey(element, Key.ESC);
await element.updateComplete;
assert.equal(
element.diffHost.diffElement.viewMode,
DiffViewMode.SIDE_BY_SIDE
);
assert.isFalse(element.diffHost.diffElement.displayLine);
// Note that stubbing setReviewed means that the value of the
// `element.reviewed` checkbox is not flipped.
const setReviewedStub = sinon.stub(element, 'setReviewed');
const handleToggleSpy = sinon.spy(element, 'handleToggleFileReviewed');
assertIsDefined(element.reviewed);
element.reviewed.checked = false;
assert.isFalse(handleToggleSpy.called);
assert.isFalse(setReviewedStub.called);
pressKey(element, 'r');
assert.isTrue(handleToggleSpy.calledOnce);
assert.isTrue(setReviewedStub.calledOnce);
assert.equal(setReviewedStub.lastCall.args[0], true);
// Handler is throttled, so another key press within 500 ms is ignored.
clock.tick(100);
pressKey(element, 'r');
assert.isTrue(handleToggleSpy.calledOnce);
assert.isTrue(setReviewedStub.calledOnce);
clock.tick(1000);
pressKey(element, 'r');
assert.isTrue(handleToggleSpy.calledTwice);
assert.isTrue(setReviewedStub.calledTwice);
clock.restore();
});
test('moveToNextCommentThread navigates to next file', async () => {
assertIsDefined(element.cursor);
sinon.stub(element.cursor, 'isAtEnd').returns(true);
element.changeNum = 42 as NumericChangeId;
const comment: PathToCommentsInfoMap = {
'wheatley.md': [createComment('c2', 21, 10, 'wheatley.md')],
};
element.changeComments = new ChangeComments(comment);
element.patchRange = {
basePatchNum: PARENT,
patchNum: 10 as RevisionPatchSetNum,
};
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
revisions: {
a: createRevision(10),
},
};
element.files = getFilesFromFileList([
'chell.go',
'glados.txt',
'wheatley.md',
]);
element.path = 'glados.txt';
element.loggedIn = true;
await element.updateComplete;
setUrlStub.reset();
pressKey(element, 'N');
await element.updateComplete;
assert.equal(setUrlStub.callCount, 1);
assert.equal(
setUrlStub.lastCall.firstArg,
'/c/test-project/+/42/10/wheatley.md#21'
);
element.path = 'wheatley.md'; // navigated to next file
pressKey(element, 'N');
await element.updateComplete;
assert.equal(setUrlStub.callCount, 2);
assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42');
});
test('shift+x shortcut toggles all diff context', async () => {
assertIsDefined(element.diffHost);
const toggleStub = sinon.stub(element.diffHost, 'toggleAllContext');
pressKey(element, 'X');
await element.updateComplete;
assert.isTrue(toggleStub.called);
});
test('diff against base', async () => {
element.patchRange = {
basePatchNum: 5 as BasePatchSetNum,
patchNum: 10 as RevisionPatchSetNum,
};
await element.updateComplete;
element.handleDiffAgainstBase();
assert.equal(
setUrlStub.lastCall.firstArg,
'/c/test-project/+/42/10/some/path.txt'
);
});
test('diff against latest', async () => {
element.path = 'foo';
element.change = {
...createParsedChange(),
revisions: createRevisions(12),
};
element.patchRange = {
basePatchNum: 5 as BasePatchSetNum,
patchNum: 10 as RevisionPatchSetNum,
};
await element.updateComplete;
element.handleDiffAgainstLatest();
assert.equal(
setUrlStub.lastCall.firstArg,
'/c/test-project/+/42/5..12/foo'
);
});
test('handleDiffBaseAgainstLeft', async () => {
element.path = 'foo';
element.change = {
...createParsedChange(),
revisions: createRevisions(10),
};
element.patchRange = {
patchNum: 3 as RevisionPatchSetNum,
basePatchNum: 1 as BasePatchSetNum,
};
element.viewState = {
view: GerritView.DIFF,
changeNum: 42 as NumericChangeId,
patchNum: 3 as RevisionPatchSetNum,
basePatchNum: 1 as BasePatchSetNum,
path: 'foo',
};
await element.updateComplete;
element.handleDiffBaseAgainstLeft();
assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/1/foo');
});
test('handleDiffBaseAgainstLeft when initially navigating to a comment', () => {
element.change = {
...createParsedChange(),
revisions: createRevisions(10),
};
element.patchRange = {
patchNum: 3 as RevisionPatchSetNum,
basePatchNum: 1 as BasePatchSetNum,
};
sinon.stub(element, 'viewStateChanged');
element.viewState = {
commentLink: true,
view: GerritView.DIFF,
changeNum: 42 as NumericChangeId,
};
element.focusLineNum = 10;
element.handleDiffBaseAgainstLeft();
assert.equal(
setUrlStub.lastCall.firstArg,
'/c/test-project/+/42/1/some/path.txt#10'
);
});
test('handleDiffRightAgainstLatest', async () => {
element.path = 'foo';
element.change = {
...createParsedChange(),
revisions: createRevisions(10),
};
element.patchRange = {
basePatchNum: 1 as BasePatchSetNum,
patchNum: 3 as RevisionPatchSetNum,
};
await element.updateComplete;
element.handleDiffRightAgainstLatest();
assert.equal(
setUrlStub.lastCall.firstArg,
'/c/test-project/+/42/3..10/foo'
);
});
test('handleDiffBaseAgainstLatest', async () => {
element.change = {
...createParsedChange(),
revisions: createRevisions(10),
};
element.patchRange = {
basePatchNum: 1 as BasePatchSetNum,
patchNum: 3 as RevisionPatchSetNum,
};
await element.updateComplete;
element.handleDiffBaseAgainstLatest();
assert.equal(
setUrlStub.lastCall.firstArg,
'/c/test-project/+/42/10/some/path.txt'
);
});
test('A fires an error event when not logged in', async () => {
element.loggedIn = false;
const loggedInErrorSpy = sinon.spy();
element.addEventListener('show-auth-required', loggedInErrorSpy);
pressKey(element, 'a');
await element.updateComplete;
assert.isFalse(setUrlStub.calledOnce);
assert.isTrue(loggedInErrorSpy.called);
});
test('A navigates to change with logged in', async () => {
element.changeNum = 42 as NumericChangeId;
element.patchRange = {
basePatchNum: 5 as BasePatchSetNum,
patchNum: 10 as RevisionPatchSetNum,
};
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
revisions: {
a: createRevision(10),
b: createRevision(5),
},
};
element.loggedIn = true;
await element.updateComplete;
const loggedInErrorSpy = sinon.spy();
element.addEventListener('show-auth-required', loggedInErrorSpy);
setUrlStub.reset();
pressKey(element, 'a');
await element.updateComplete;
assert.equal(setUrlStub.callCount, 1);
assert.equal(
setUrlStub.lastCall.firstArg,
'/c/test-project/+/42/5..10?openReplyDialog=true'
);
assert.isFalse(loggedInErrorSpy.called);
});
test('A navigates to change with old patch number with logged in', async () => {
element.changeNum = 42 as NumericChangeId;
element.patchRange = {
basePatchNum: PARENT,
patchNum: 1 as RevisionPatchSetNum,
};
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
revisions: {
a: createRevision(1),
b: createRevision(2),
},
};
element.loggedIn = true;
const loggedInErrorSpy = sinon.spy();
element.addEventListener('show-auth-required', loggedInErrorSpy);
pressKey(element, 'a');
await element.updateComplete;
assert.isTrue(setUrlStub.calledOnce);
assert.equal(
setUrlStub.lastCall.firstArg,
'/c/test-project/+/42/1?openReplyDialog=true'
);
assert.isFalse(loggedInErrorSpy.called);
});
test('keyboard shortcuts with patch range', () => {
element.changeNum = 42 as NumericChangeId;
element.patchRange = {
basePatchNum: 5 as BasePatchSetNum,
patchNum: 10 as RevisionPatchSetNum,
};
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
revisions: {
a: createRevision(10),
b: createRevision(5),
},
};
element.files = getFilesFromFileList([
'chell.go',
'glados.txt',
'wheatley.md',
]);
element.path = 'glados.txt';
pressKey(element, 'u');
assert.equal(setUrlStub.callCount, 1);
assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/5..10');
pressKey(element, ']');
assert.isTrue(element.loading);
assert.equal(setUrlStub.callCount, 2);
assert.equal(
setUrlStub.lastCall.firstArg,
'/c/test-project/+/42/5..10/wheatley.md'
);
element.path = 'wheatley.md';
pressKey(element, '[');
assert.isTrue(element.loading);
assert.equal(setUrlStub.callCount, 3);
assert.equal(
setUrlStub.lastCall.firstArg,
'/c/test-project/+/42/5..10/glados.txt'
);
element.path = 'glados.txt';
pressKey(element, '[');
assert.isTrue(element.loading);
assert.equal(setUrlStub.callCount, 4);
assert.equal(
setUrlStub.lastCall.firstArg,
'/c/test-project/+/42/5..10/chell.go'
);
element.path = 'chell.go';
pressKey(element, '[');
assert.isTrue(element.loading);
assert.equal(setUrlStub.callCount, 5);
assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/5..10');
assertIsDefined(element.downloadOverlay);
const downloadOverlayStub = sinon
.stub(element.downloadOverlay, 'open')
.returns(Promise.resolve());
pressKey(element, 'd');
assert.isTrue(downloadOverlayStub.called);
});
test('keyboard shortcuts with old patch number', () => {
element.changeNum = 42 as NumericChangeId;
element.patchRange = {
basePatchNum: PARENT,
patchNum: 1 as RevisionPatchSetNum,
};
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
revisions: {
a: createRevision(1),
b: createRevision(2),
},
};
element.files = getFilesFromFileList([
'chell.go',
'glados.txt',
'wheatley.md',
]);
element.path = 'glados.txt';
pressKey(element, 'u');
assert.isTrue(setUrlStub.calledOnce);
assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/1');
pressKey(element, ']');
assert.equal(
setUrlStub.lastCall.firstArg,
'/c/test-project/+/42/1/wheatley.md'
);
element.path = 'wheatley.md';
pressKey(element, '[');
assert.equal(
setUrlStub.lastCall.firstArg,
'/c/test-project/+/42/1/glados.txt'
);
element.path = 'glados.txt';
pressKey(element, '[');
assert.equal(
setUrlStub.lastCall.firstArg,
'/c/test-project/+/42/1/chell.go'
);
element.path = 'chell.go';
setUrlStub.reset();
pressKey(element, '[');
assert.isTrue(setUrlStub.calledOnce);
assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/1');
});
test('edit should redirect to edit page', async () => {
element.loggedIn = true;
element.path = 't.txt';
element.patchRange = {
basePatchNum: PARENT,
patchNum: 1 as RevisionPatchSetNum,
};
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
project: 'gerrit' as RepoName,
status: ChangeStatus.NEW,
revisions: {
a: createRevision(1),
b: createRevision(2),
},
};
await element.updateComplete;
const editBtn = queryAndAssert<GrButton>(
element,
'.editButton gr-button'
);
assert.isTrue(!!editBtn);
editBtn.click();
assert.equal(setUrlStub.callCount, 1);
assert.equal(setUrlStub.lastCall.firstArg, '/c/gerrit/+/42/1/t.txt,edit');
});
test('edit should redirect to edit page with line number', async () => {
const lineNumber = 42;
element.loggedIn = true;
element.path = 't.txt';
element.patchRange = {
basePatchNum: PARENT,
patchNum: 1 as RevisionPatchSetNum,
};
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
project: 'gerrit' as RepoName,
status: ChangeStatus.NEW,
revisions: {
a: createRevision(1),
b: createRevision(2),
},
};
assertIsDefined(element.cursor);
sinon
.stub(element.cursor, 'getAddress')
.returns({number: lineNumber, leftSide: false});
await element.updateComplete;
const editBtn = queryAndAssert<GrButton>(
element,
'.editButton gr-button'
);
assert.isTrue(!!editBtn);
editBtn.click();
assert.equal(setUrlStub.callCount, 1);
assert.equal(
setUrlStub.lastCall.firstArg,
'/c/gerrit/+/42/1/t.txt,edit#42'
);
});
async function isEditVisibile({
loggedIn,
changeStatus,
}: {
loggedIn: boolean;
changeStatus: ChangeStatus;
}): Promise<boolean> {
element.loggedIn = loggedIn;
element.path = 't.txt';
element.patchRange = {
basePatchNum: PARENT,
patchNum: 1 as RevisionPatchSetNum,
};
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
status: changeStatus,
revisions: {
a: createRevision(1),
b: createRevision(2),
},
};
await element.updateComplete;
const editBtn = query(element, '.editButton gr-button');
return !!editBtn;
}
test('edit visible only when logged and status NEW', async () => {
for (const changeStatus of Object.keys(ChangeStatus) as ChangeStatus[]) {
assert.isFalse(
await isEditVisibile({loggedIn: false, changeStatus}),
`loggedIn: false, changeStatus: ${changeStatus}`
);
if (changeStatus !== ChangeStatus.NEW) {
assert.isFalse(
await isEditVisibile({loggedIn: true, changeStatus}),
`loggedIn: true, changeStatus: ${changeStatus}`
);
} else {
assert.isTrue(
await isEditVisibile({loggedIn: true, changeStatus}),
`loggedIn: true, changeStatus: ${changeStatus}`
);
}
}
});
test('edit visible when logged and status NEW', async () => {
assert.isTrue(
await isEditVisibile({loggedIn: true, changeStatus: ChangeStatus.NEW})
);
});
test('edit hidden when logged and status ABANDONED', async () => {
assert.isFalse(
await isEditVisibile({
loggedIn: true,
changeStatus: ChangeStatus.ABANDONED,
})
);
});
test('edit hidden when logged and status MERGED', async () => {
assert.isFalse(
await isEditVisibile({
loggedIn: true,
changeStatus: ChangeStatus.MERGED,
})
);
});
suite('diff prefs hidden', () => {
test('when no prefs or logged out', async () => {
const getDiffPrefsContainer = () =>
query<HTMLSpanElement>(element, '#diffPrefsContainer');
element.prefs = undefined;
element.loggedIn = false;
await element.updateComplete;
assert.isNotOk(getDiffPrefsContainer());
element.loggedIn = true;
await element.updateComplete;
assert.isNotOk(getDiffPrefsContainer());
element.loggedIn = false;
element.prefs = {...createDefaultDiffPrefs(), font_size: 12};
await element.updateComplete;
assert.isNotOk(getDiffPrefsContainer());
element.loggedIn = true;
element.prefs = {...createDefaultDiffPrefs(), font_size: 12};
await element.updateComplete;
assert.isOk(getDiffPrefsContainer());
});
});
test('prefsButton opens gr-diff-preferences', () => {
const handlePrefsTapSpy = sinon.spy(element, 'handlePrefsTap');
assertIsDefined(element.diffPreferencesDialog);
const overlayOpenStub = sinon.stub(element.diffPreferencesDialog, 'open');
const prefsButton = queryAndAssert<GrButton>(element, '.prefsButton');
prefsButton.click();
assert.isTrue(handlePrefsTapSpy.called);
assert.isTrue(overlayOpenStub.called);
});
suite('url parameters', () => {
setup(() => {
sinon.stub(element, 'fetchFiles');
});
test('_formattedFiles', () => {
element.changeNum = 42 as NumericChangeId;
element.patchRange = {
basePatchNum: PARENT,
patchNum: 10 as RevisionPatchSetNum,
};
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
};
element.files = getFilesFromFileList([
'chell.go',
'glados.txt',
'wheatley.md',
'/COMMIT_MSG',
'/MERGE_LIST',
]);
element.path = 'glados.txt';
const expectedFormattedFiles: DropdownItem[] = [
{
text: 'chell.go',
mobileText: 'chell.go',
value: 'chell.go',
bottomText: '',
file: {
...createFileInfo(),
__path: 'chell.go',
},
},
{
text: 'glados.txt',
mobileText: 'glados.txt',
value: 'glados.txt',
bottomText: '',
file: {
...createFileInfo(),
__path: 'glados.txt',
},
},
{
text: 'wheatley.md',
mobileText: 'wheatley.md',
value: 'wheatley.md',
bottomText: '',
file: {
...createFileInfo(),
__path: 'wheatley.md',
},
},
{
text: 'Commit message',
mobileText: 'Commit message',
value: '/COMMIT_MSG',
bottomText: '',
file: {
...createFileInfo(),
__path: '/COMMIT_MSG',
},
},
{
text: 'Merge list',
mobileText: 'Merge list',
value: '/MERGE_LIST',
bottomText: '',
file: {
...createFileInfo(),
__path: '/MERGE_LIST',
},
},
];
const result = element.formatFilesForDropdown();
assert.deepEqual(result, expectedFormattedFiles);
assert.equal(result[1].value, element.path);
});
test('prev/up/next links', async () => {
element.changeNum = 42 as NumericChangeId;
element.patchRange = {
basePatchNum: PARENT,
patchNum: 10 as RevisionPatchSetNum,
};
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
revisions: {
a: createRevision(10),
},
};
element.files = getFilesFromFileList([
'chell.go',
'glados.txt',
'wheatley.md',
]);
element.path = 'glados.txt';
await element.updateComplete;
const linkEls = queryAll(element, '.navLink');
assert.equal(linkEls.length, 3);
assert.equal(
linkEls[0].getAttribute('href'),
'/c/test-project/+/42/10/chell.go'
);
assert.equal(linkEls[1].getAttribute('href'), '/c/test-project/+/42');
assert.equal(
linkEls[2].getAttribute('href'),
'/c/test-project/+/42/10/wheatley.md'
);
element.path = 'wheatley.md';
await element.updateComplete;
assert.equal(
linkEls[0].getAttribute('href'),
'/c/test-project/+/42/10/glados.txt'
);
assert.equal(linkEls[1].getAttribute('href'), '/c/test-project/+/42');
assert.equal(linkEls[2].getAttribute('href'), '/c/test-project/+/42');
element.path = 'chell.go';
await element.updateComplete;
assert.equal(linkEls[0].getAttribute('href'), '/c/test-project/+/42');
assert.equal(linkEls[1].getAttribute('href'), '/c/test-project/+/42');
assert.equal(
linkEls[2].getAttribute('href'),
'/c/test-project/+/42/10/glados.txt'
);
element.path = 'not_a_real_file';
await element.updateComplete;
assert.equal(
linkEls[0].getAttribute('href'),
'/c/test-project/+/42/10/wheatley.md'
);
assert.equal(linkEls[1].getAttribute('href'), '/c/test-project/+/42');
assert.equal(
linkEls[2].getAttribute('href'),
'/c/test-project/+/42/10/chell.go'
);
});
test('prev/up/next links with patch range', async () => {
element.changeNum = 42 as NumericChangeId;
element.patchRange = {
basePatchNum: 5 as BasePatchSetNum,
patchNum: 10 as RevisionPatchSetNum,
};
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
revisions: {
a: createRevision(5),
b: createRevision(10),
},
};
element.files = getFilesFromFileList([
'chell.go',
'glados.txt',
'wheatley.md',
]);
element.path = 'glados.txt';
await element.updateComplete;
const linkEls = queryAll(element, '.navLink');
assert.equal(linkEls.length, 3);
assert.equal(
linkEls[0].getAttribute('href'),
'/c/test-project/+/42/5..10/chell.go'
);
assert.equal(
linkEls[1].getAttribute('href'),
'/c/test-project/+/42/5..10'
);
assert.equal(
linkEls[2].getAttribute('href'),
'/c/test-project/+/42/5..10/wheatley.md'
);
element.path = 'wheatley.md';
await element.updateComplete;
assert.equal(
linkEls[0].getAttribute('href'),
'/c/test-project/+/42/5..10/glados.txt'
);
assert.equal(
linkEls[1].getAttribute('href'),
'/c/test-project/+/42/5..10'
);
assert.equal(
linkEls[2].getAttribute('href'),
'/c/test-project/+/42/5..10'
);
element.path = 'chell.go';
await element.updateComplete;
assert.equal(
linkEls[0].getAttribute('href'),
'/c/test-project/+/42/5..10'
);
assert.equal(
linkEls[1].getAttribute('href'),
'/c/test-project/+/42/5..10'
);
assert.equal(
linkEls[2].getAttribute('href'),
'/c/test-project/+/42/5..10/glados.txt'
);
});
});
test('handlePatchChange calls setUrl correctly', async () => {
element.change = {
...createParsedChange(),
_number: 321 as NumericChangeId,
project: 'foo/bar' as RepoName,
};
element.path = 'path/to/file.txt';
element.patchRange = {
basePatchNum: PARENT,
patchNum: 3 as RevisionPatchSetNum,
};
await element.updateComplete;
const detail = {
basePatchNum: PARENT,
patchNum: 1 as RevisionPatchSetNum,
};
queryAndAssert(element, '#rangeSelect').dispatchEvent(
new CustomEvent('patch-range-change', {detail, bubbles: false})
);
assert.equal(
setUrlStub.lastCall.firstArg,
'/c/foo/bar/+/321/1/path/to/file.txt'
);
});
test(
'_prefs.manual_review true means set reviewed is not ' +
'automatically called',
async () => {
const setReviewedFileStatusStub = sinon
.stub(element.getChangeModel(), 'setReviewedFilesStatus')
.callsFake(() => Promise.resolve());
const setReviewedStatusStub = sinon.spy(element, 'setReviewedStatus');
assertIsDefined(element.diffHost);
sinon.stub(element.diffHost, 'reload');
element.loggedIn = true;
const diffPreferences = {
...createDefaultDiffPrefs(),
manual_review: true,
};
element.userModel.setDiffPreferences(diffPreferences);
element.getChangeModel().setState({
change: createParsedChange(),
diffPath: '/COMMIT_MSG',
reviewedFiles: [],
loadingStatus: LoadingStatus.LOADED,
});
element.routerModel.setState({
changeNum: TEST_NUMERIC_CHANGE_ID,
view: GerritView.DIFF,
patchNum: 2 as RevisionPatchSetNum,
});
element.patchRange = {
patchNum: 2 as RevisionPatchSetNum,
basePatchNum: 1 as BasePatchSetNum,
};
await waitUntil(() => setReviewedStatusStub.called);
assert.isFalse(setReviewedFileStatusStub.called);
// if prefs are updated then the reviewed status should not be set again
element.userModel.setDiffPreferences(createDefaultDiffPrefs());
await element.updateComplete;
assert.isFalse(setReviewedFileStatusStub.called);
}
);
test('_prefs.manual_review false means set reviewed is called', async () => {
const setReviewedFileStatusStub = sinon
.stub(element.getChangeModel(), 'setReviewedFilesStatus')
.callsFake(() => Promise.resolve());
assertIsDefined(element.diffHost);
sinon.stub(element.diffHost, 'reload');
element.loggedIn = true;
const diffPreferences = {
...createDefaultDiffPrefs(),
manual_review: false,
};
element.userModel.setDiffPreferences(diffPreferences);
element.getChangeModel().setState({
change: createParsedChange(),
diffPath: '/COMMIT_MSG',
reviewedFiles: [],
loadingStatus: LoadingStatus.LOADED,
});
element.routerModel.setState({
changeNum: TEST_NUMERIC_CHANGE_ID,
view: GerritView.DIFF,
patchNum: 22 as RevisionPatchSetNum,
});
element.patchRange = {
patchNum: 2 as RevisionPatchSetNum,
basePatchNum: 1 as BasePatchSetNum,
};
await waitUntil(() => setReviewedFileStatusStub.called);
assert.isTrue(setReviewedFileStatusStub.called);
});
test('file review status', async () => {
element.getChangeModel().setState({
change: createParsedChange(),
diffPath: '/COMMIT_MSG',
reviewedFiles: [],
loadingStatus: LoadingStatus.LOADED,
});
element.loggedIn = true;
const saveReviewedStub = sinon
.stub(element.getChangeModel(), 'setReviewedFilesStatus')
.callsFake(() => Promise.resolve());
assertIsDefined(element.diffHost);
sinon.stub(element.diffHost, 'reload');
element.userModel.setDiffPreferences(createDefaultDiffPrefs());
element.routerModel.setState({
changeNum: TEST_NUMERIC_CHANGE_ID,
view: GerritView.DIFF,
patchNum: 2 as RevisionPatchSetNum,
});
element.patchRange = {
patchNum: 2 as RevisionPatchSetNum,
basePatchNum: 1 as BasePatchSetNum,
};
await waitUntil(() => saveReviewedStub.called);
element.getChangeModel().updateStateFileReviewed('/COMMIT_MSG', true);
await element.updateComplete;
const reviewedStatusCheckBox = queryAndAssert<HTMLInputElement>(
element,
'input[type="checkbox"]'
);
assert.isTrue(reviewedStatusCheckBox.checked);
assert.deepEqual(saveReviewedStub.lastCall.args, [
42,
2,
'/COMMIT_MSG',
true,
]);
reviewedStatusCheckBox.click();
assert.isFalse(reviewedStatusCheckBox.checked);
assert.deepEqual(saveReviewedStub.lastCall.args, [
42,
2,
'/COMMIT_MSG',
false,
]);
element.getChangeModel().updateStateFileReviewed('/COMMIT_MSG', false);
await element.updateComplete;
reviewedStatusCheckBox.click();
assert.isTrue(reviewedStatusCheckBox.checked);
assert.deepEqual(saveReviewedStub.lastCall.args, [
42,
2,
'/COMMIT_MSG',
true,
]);
const callCount = saveReviewedStub.callCount;
element.viewState = {
view: GerritView.DIFF,
changeNum: 42 as NumericChangeId,
project: 'test' as RepoName,
};
await element.updateComplete;
// saveReviewedState observer observes viewState, but should not fire when
// view !== GerritView.DIFF.
assert.equal(saveReviewedStub.callCount, callCount);
});
test('file review status with edit loaded', async () => {
const saveReviewedStub = sinon.stub(
element.getChangeModel(),
'setReviewedFilesStatus'
);
element.patchRange = {
basePatchNum: 1 as BasePatchSetNum,
patchNum: EDIT,
};
await waitEventLoop();
assert.isTrue(element.computeEditMode());
element.setReviewed(true);
assert.isFalse(saveReviewedStub.called);
});
test('hash is determined from viewState', async () => {
assertIsDefined(element.diffHost);
sinon.stub(element.diffHost, 'reload');
const initLineStub = sinon.stub(element, 'initLineOfInterestAndCursor');
element.loggedIn = true;
element.viewState = {
view: GerritView.DIFF,
changeNum: 42 as NumericChangeId,
patchNum: 2 as RevisionPatchSetNum,
basePatchNum: 1 as BasePatchSetNum,
path: '/COMMIT_MSG',
};
await element.updateComplete;
await waitEventLoop();
assert.isTrue(initLineStub.calledOnce);
});
test('diff mode selector correctly toggles the diff', async () => {
const select = queryAndAssert<GrDiffModeSelector>(element, '#modeSelect');
const diffDisplay = element.diffHost;
assertIsDefined(diffDisplay);
element.userPrefs = {
...createDefaultPreferences(),
diff_view: DiffViewMode.SIDE_BY_SIDE,
};
element.getBrowserModel().setScreenWidth(0);
const userStub = stubUsers('updatePreferences');
await element.updateComplete;
// The mode selected in the view state reflects the selected option.
// assert.equal(element.userPrefs.diff_view, select.mode);
// The mode selected in the view state reflects the view rednered in the
// diff.
assert.equal(select.mode, diffDisplay.viewMode);
// We will simulate a user change of the selected mode.
element.handleToggleDiffMode();
assert.isTrue(
userStub.calledWithExactly({
diff_view: DiffViewMode.UNIFIED,
})
);
});
test('diff mode selector should be hidden for binary', async () => {
element.diff = {
...createDiff(),
binary: true,
content: [],
};
await element.updateComplete;
const diffModeSelector = queryAndAssert(element, '.diffModeSelector');
assert.isTrue(diffModeSelector.classList.contains('hide'));
});
suite('commitRange', () => {
const change: ParsedChangeInfo = {
...createParsedChange(),
_number: 42 as NumericChangeId,
revisions: {
'commit-sha-1': {
...createRevision(1),
commit: {
...createCommit(),
parents: [{subject: 's1', commit: 'sha-1-parent' as CommitId}],
},
},
'commit-sha-2': createRevision(2),
'commit-sha-3': createRevision(3),
'commit-sha-4': createRevision(4),
'commit-sha-5': {
...createRevision(5),
commit: {
...createCommit(),
parents: [{subject: 's5', commit: 'sha-5-parent' as CommitId}],
},
},
},
};
setup(async () => {
assertIsDefined(element.diffHost);
sinon.stub(element.diffHost, 'reload');
sinon.stub(element, 'initCursor');
element.change = change;
await element.updateComplete;
await element.diffHost.updateComplete;
});
test('uses the patchNum and basePatchNum ', async () => {
element.viewState = {
view: GerritView.DIFF,
changeNum: 42 as NumericChangeId,
patchNum: 4 as RevisionPatchSetNum,
basePatchNum: 2 as BasePatchSetNum,
path: '/COMMIT_MSG',
};
element.change = change;
await element.updateComplete;
await waitEventLoop();
assert.deepEqual(element.commitRange, {
baseCommit: 'commit-sha-2' as CommitId,
commit: 'commit-sha-4' as CommitId,
});
});
test('uses the parent when there is no base patch num ', async () => {
element.viewState = {
view: GerritView.DIFF,
changeNum: 42 as NumericChangeId,
patchNum: 5 as RevisionPatchSetNum,
path: '/COMMIT_MSG',
};
element.change = change;
await element.updateComplete;
await waitEventLoop();
assert.deepEqual(element.commitRange, {
commit: 'commit-sha-5' as CommitId,
baseCommit: 'sha-5-parent' as CommitId,
});
});
});
test('initCursor', () => {
assertIsDefined(element.cursor);
assert.isNotOk(element.cursor.initialLineNumber);
// Does nothing when viewState specify no cursor address:
element.initCursor(false);
assert.isNotOk(element.cursor.initialLineNumber);
// Does nothing when viewState specify side but no number:
element.initCursor(true);
assert.isNotOk(element.cursor.initialLineNumber);
// Revision hash: specifies lineNum but not side.
element.focusLineNum = 234;
element.initCursor(false);
assert.equal(element.cursor.initialLineNumber, 234);
assert.equal(element.cursor.side, Side.RIGHT);
// Base hash: specifies lineNum and side.
element.focusLineNum = 345;
element.initCursor(true);
assert.equal(element.cursor.initialLineNumber, 345);
assert.equal(element.cursor.side, Side.LEFT);
// Specifies right side:
element.focusLineNum = 123;
element.initCursor(false);
assert.equal(element.cursor.initialLineNumber, 123);
assert.equal(element.cursor.side, Side.RIGHT);
});
test('getLineOfInterest', () => {
assert.isUndefined(element.getLineOfInterest(false));
element.focusLineNum = 12;
let result = element.getLineOfInterest(false);
assert.isOk(result);
assert.equal(result!.lineNum, 12);
assert.equal(result!.side, Side.RIGHT);
result = element.getLineOfInterest(true);
assert.isOk(result);
assert.equal(result!.lineNum, 12);
assert.equal(result!.side, Side.LEFT);
});
test('onLineSelected', () => {
const replaceStateStub = sinon.stub(history, 'replaceState');
assertIsDefined(element.cursor);
sinon
.stub(element.cursor, 'getAddress')
.returns({number: 123, leftSide: false});
element.changeNum = 321 as NumericChangeId;
element.change = {
...createParsedChange(),
_number: 321 as NumericChangeId,
project: 'foo/bar' as RepoName,
};
element.patchRange = {
basePatchNum: 3 as BasePatchSetNum,
patchNum: 5 as RevisionPatchSetNum,
};
const e = {detail: {number: 123, side: Side.RIGHT}} as CustomEvent;
element.onLineSelected(e);
assert.isTrue(replaceStateStub.called);
});
test('line selected on left side', () => {
const replaceStateStub = sinon.stub(history, 'replaceState');
assertIsDefined(element.cursor);
sinon
.stub(element.cursor, 'getAddress')
.returns({number: 123, leftSide: true});
element.changeNum = 321 as NumericChangeId;
element.change = {
...createParsedChange(),
_number: 321 as NumericChangeId,
project: 'foo/bar' as RepoName,
};
element.patchRange = {
basePatchNum: 3 as BasePatchSetNum,
patchNum: 5 as RevisionPatchSetNum,
};
const e = {detail: {number: 123, side: Side.LEFT}} as CustomEvent;
element.onLineSelected(e);
assert.isTrue(replaceStateStub.called);
});
test('handleToggleDiffMode', () => {
const userStub = stubUsers('updatePreferences');
element.userPrefs = {
...createDefaultPreferences(),
diff_view: DiffViewMode.SIDE_BY_SIDE,
};
element.handleToggleDiffMode();
assert.deepEqual(userStub.lastCall.args[0], {
diff_view: DiffViewMode.UNIFIED,
});
element.userPrefs = {
...createDefaultPreferences(),
diff_view: DiffViewMode.UNIFIED,
};
element.handleToggleDiffMode();
assert.deepEqual(userStub.lastCall.args[0], {
diff_view: DiffViewMode.SIDE_BY_SIDE,
});
});
suite('initPatchRange', () => {
setup(async () => {
getDiffRestApiStub.returns(Promise.resolve(createDiff()));
element.viewState = {
view: GerritView.DIFF,
changeNum: 42 as NumericChangeId,
patchNum: 3 as RevisionPatchSetNum,
path: 'abcd',
};
await element.updateComplete;
});
test('empty', () => {
sinon.stub(element, 'getPaths').returns({});
element.initPatchRange();
assert.equal(Object.keys(element.commentMap ?? {}).length, 0);
});
test('has paths', () => {
sinon.stub(element, 'fetchFiles');
sinon.stub(element, 'getPaths').returns({
'path/to/file/one.cpp': true,
'path-to/file/two.py': true,
});
element.changeNum = 42 as NumericChangeId;
element.patchRange = {
basePatchNum: 3 as BasePatchSetNum,
patchNum: 5 as RevisionPatchSetNum,
};
element.initPatchRange();
assert.deepEqual(Object.keys(element.commentMap ?? {}), [
'path/to/file/one.cpp',
'path-to/file/two.py',
]);
});
});
suite('computeCommentSkips', () => {
test('empty file list', () => {
const commentMap = {
'path/one.jpg': true,
'path/three.wav': true,
};
const path = 'path/two.m4v';
const result = element.computeCommentSkips(commentMap, [], path);
assert.isOk(result);
assert.isNotOk(result!.previous);
assert.isNotOk(result!.next);
});
test('finds skips', () => {
const fileList = ['path/one.jpg', 'path/two.m4v', 'path/three.wav'];
let path = fileList[1];
const commentMap: CommentMap = {};
commentMap[fileList[0]] = true;
commentMap[fileList[1]] = false;
commentMap[fileList[2]] = true;
let result = element.computeCommentSkips(commentMap, fileList, path);
assert.isOk(result);
assert.equal(result!.previous, fileList[0]);
assert.equal(result!.next, fileList[2]);
commentMap[fileList[1]] = true;
result = element.computeCommentSkips(commentMap, fileList, path);
assert.isOk(result);
assert.equal(result!.previous, fileList[0]);
assert.equal(result!.next, fileList[2]);
path = fileList[0];
result = element.computeCommentSkips(commentMap, fileList, path);
assert.isOk(result);
assert.isNull(result!.previous);
assert.equal(result!.next, fileList[1]);
path = fileList[2];
result = element.computeCommentSkips(commentMap, fileList, path);
assert.isOk(result);
assert.equal(result!.previous, fileList[1]);
assert.isNull(result!.next);
});
suite('skip next/previous', () => {
let navToChangeStub: SinonStub;
setup(() => {
navToChangeStub = sinon.stub(element, 'navToChangeView');
element.files = getFilesFromFileList([
'path/one.jpg',
'path/two.m4v',
'path/three.wav',
]);
element.patchRange = {
patchNum: 2 as RevisionPatchSetNum,
basePatchNum: 1 as BasePatchSetNum,
};
});
suite('moveToPreviousFileWithComment', () => {
test('no skips', () => {
element.moveToPreviousFileWithComment();
assert.isFalse(navToChangeStub.called);
assert.isFalse(setUrlStub.called);
});
test('no previous', async () => {
const commentMap: CommentMap = {};
commentMap[element.files.sortedFileList[0]!] = false;
commentMap[element.files.sortedFileList[1]!] = false;
commentMap[element.files.sortedFileList[2]!] = true;
element.commentMap = commentMap;
element.path = element.files.sortedFileList[1];
await element.updateComplete;
element.moveToPreviousFileWithComment();
assert.isTrue(navToChangeStub.calledOnce);
assert.isFalse(setUrlStub.called);
});
test('w/ previous', async () => {
const commentMap: CommentMap = {};
commentMap[element.files.sortedFileList[0]!] = true;
commentMap[element.files.sortedFileList[1]!] = false;
commentMap[element.files.sortedFileList[2]!] = true;
element.commentMap = commentMap;
element.path = element.files.sortedFileList[1];
await element.updateComplete;
element.moveToPreviousFileWithComment();
assert.isFalse(navToChangeStub.called);
assert.isTrue(setUrlStub.calledOnce);
});
});
suite('moveToNextFileWithComment', () => {
test('no skips', () => {
element.moveToNextFileWithComment();
assert.isFalse(navToChangeStub.called);
assert.isFalse(setUrlStub.called);
});
test('no previous', async () => {
const commentMap: CommentMap = {};
commentMap[element.files.sortedFileList[0]!] = true;
commentMap[element.files.sortedFileList[1]!] = false;
commentMap[element.files.sortedFileList[2]!] = false;
element.commentMap = commentMap;
element.path = element.files.sortedFileList[1];
await element.updateComplete;
element.moveToNextFileWithComment();
assert.isTrue(navToChangeStub.calledOnce);
assert.isFalse(setUrlStub.called);
});
test('w/ previous', async () => {
const commentMap: CommentMap = {};
commentMap[element.files.sortedFileList[0]!] = true;
commentMap[element.files.sortedFileList[1]!] = false;
commentMap[element.files.sortedFileList[2]!] = true;
element.commentMap = commentMap;
element.path = element.files.sortedFileList[1];
await element.updateComplete;
element.moveToNextFileWithComment();
assert.isFalse(navToChangeStub.called);
assert.isTrue(setUrlStub.calledOnce);
});
});
});
});
test('_computeEditMode', () => {
const callCompute = (range: PatchRange) => {
element.patchRange = range;
return element.computeEditMode();
};
assert.isFalse(
callCompute({
basePatchNum: PARENT,
patchNum: 1 as RevisionPatchSetNum,
})
);
assert.isTrue(
callCompute({
basePatchNum: 1 as BasePatchSetNum,
patchNum: EDIT,
})
);
});
test('computeFileNum', () => {
element.path = '/foo';
assert.equal(
element.computeFileNum([
{text: '/foo', value: '/foo'},
{text: '/bar', value: '/bar'},
]),
1
);
element.path = '/bar';
assert.equal(
element.computeFileNum([
{text: '/foo', value: '/foo'},
{text: '/bar', value: '/bar'},
]),
2
);
});
test('computeFileNumClass', () => {
assert.equal(element.computeFileNumClass(0, []), '');
assert.equal(
element.computeFileNumClass(1, [
{text: '/foo', value: '/foo'},
{text: '/bar', value: '/bar'},
]),
'show'
);
});
test('f open file dropdown', async () => {
assertIsDefined(element.dropdown);
assertIsDefined(element.dropdown.dropdown);
assert.isFalse(element.dropdown.dropdown.opened);
pressKey(element, 'f');
await element.updateComplete;
assert.isTrue(element.dropdown.dropdown.opened);
});
suite('blame', () => {
test('toggle blame with button', () => {
assertIsDefined(element.diffHost);
const toggleBlame = sinon
.stub(element.diffHost, 'loadBlame')
.callsFake(() => Promise.resolve([]));
queryAndAssert<GrButton>(element, '#toggleBlame').click();
assert.isTrue(toggleBlame.calledOnce);
});
test('toggle blame with shortcut', () => {
assertIsDefined(element.diffHost);
const toggleBlame = sinon
.stub(element.diffHost, 'loadBlame')
.callsFake(() => Promise.resolve([]));
pressKey(element, 'b');
assert.isTrue(toggleBlame.calledOnce);
});
});
suite('editMode behavior', () => {
setup(async () => {
element.loggedIn = true;
await element.updateComplete;
});
test('reviewed checkbox', async () => {
sinon.stub(element, 'handlePatchChange');
element.patchRange = createPatchRange();
await element.updateComplete;
assertIsDefined(element.reviewed);
// Reviewed checkbox should be shown.
assert.isTrue(isVisible(element.reviewed));
element.patchRange = {...element.patchRange, patchNum: EDIT};
await element.updateComplete;
assert.isFalse(isVisible(element.reviewed));
});
});
suite('switching files', () => {
let dispatchEventStub: SinonStub;
let navToFileStub: SinonStub;
let moveToPreviousChunkStub: SinonStub;
let moveToNextChunkStub: SinonStub;
let isAtStartStub: SinonStub;
let isAtEndStub: SinonStub;
let nowStub: SinonStub;
setup(() => {
dispatchEventStub = sinon.stub(element, 'dispatchEvent').callThrough();
navToFileStub = sinon.stub(element, 'navToFile');
assertIsDefined(element.cursor);
moveToPreviousChunkStub = sinon.stub(
element.cursor,
'moveToPreviousChunk'
);
moveToNextChunkStub = sinon.stub(element.cursor, 'moveToNextChunk');
isAtStartStub = sinon.stub(element.cursor, 'isAtStart');
isAtEndStub = sinon.stub(element.cursor, 'isAtEnd');
nowStub = sinon.stub(Date, 'now');
});
test('shows toast when at the end of file', () => {
moveToNextChunkStub.returns(CursorMoveResult.CLIPPED);
isAtEndStub.returns(true);
pressKey(element, 'n');
assert.isTrue(moveToNextChunkStub.called);
assert.equal(
dispatchEventStub.lastCall.args[0].type,
EventType.SHOW_ALERT
);
assert.isFalse(navToFileStub.called);
});
test('navigates to next file when n is tapped again', () => {
moveToNextChunkStub.returns(CursorMoveResult.CLIPPED);
isAtEndStub.returns(true);
element.files = getFilesFromFileList(['file1', 'file2', 'file3']);
element.reviewedFiles = new Set(['file2']);
element.path = 'file1';
nowStub.returns(5);
pressKey(element, 'n');
nowStub.returns(10);
pressKey(element, 'n');
assert.isTrue(navToFileStub.called);
assert.deepEqual(navToFileStub.lastCall.args, [['file1', 'file3'], 1]);
});
test('does not navigate if n is tapped twice too slow', () => {
moveToNextChunkStub.returns(CursorMoveResult.CLIPPED);
isAtEndStub.returns(true);
nowStub.returns(5);
pressKey(element, 'n');
nowStub.returns(6000);
pressKey(element, 'n');
assert.isFalse(navToFileStub.called);
});
test('shows toast when at the start of file', () => {
moveToPreviousChunkStub.returns(CursorMoveResult.CLIPPED);
isAtStartStub.returns(true);
pressKey(element, 'p');
assert.isTrue(moveToPreviousChunkStub.called);
assert.equal(
dispatchEventStub.lastCall.args[0].type,
EventType.SHOW_ALERT
);
assert.isFalse(navToFileStub.called);
});
test('navigates to prev file when p is tapped again', () => {
moveToPreviousChunkStub.returns(CursorMoveResult.CLIPPED);
isAtStartStub.returns(true);
element.files = getFilesFromFileList(['file1', 'file2', 'file3']);
element.reviewedFiles = new Set(['file2']);
element.path = 'file3';
nowStub.returns(5);
pressKey(element, 'p');
nowStub.returns(10);
pressKey(element, 'p');
assert.isTrue(navToFileStub.called);
assert.deepEqual(navToFileStub.lastCall.args, [['file1', 'file3'], -1]);
});
test('does not navigate if p is tapped twice too slow', () => {
moveToPreviousChunkStub.returns(CursorMoveResult.CLIPPED);
isAtStartStub.returns(true);
nowStub.returns(5);
pressKey(element, 'p');
nowStub.returns(6000);
pressKey(element, 'p');
assert.isFalse(navToFileStub.called);
});
test('does not navigate when tapping n then p', () => {
moveToNextChunkStub.returns(CursorMoveResult.CLIPPED);
isAtEndStub.returns(true);
nowStub.returns(5);
pressKey(element, 'n');
moveToPreviousChunkStub.returns(CursorMoveResult.CLIPPED);
isAtStartStub.returns(true);
nowStub.returns(10);
pressKey(element, 'p');
assert.isFalse(navToFileStub.called);
});
});
test('shift+m navigates to next unreviewed file', async () => {
element.files = getFilesFromFileList(['file1', 'file2', 'file3']);
element.reviewedFiles = new Set(['file1', 'file2']);
element.path = 'file1';
const reviewedStub = sinon.stub(element, 'setReviewed');
const navStub = sinon.stub(element, 'navToFile');
pressKey(element, 'M');
await waitEventLoop();
assert.isTrue(reviewedStub.lastCall.args[0]);
assert.deepEqual(navStub.lastCall.args, [['file1', 'file3'], 1]);
});
test('File change should trigger setUrl once', async () => {
element.files = getFilesFromFileList(['file1', 'file2', 'file3']);
sinon.stub(element, 'initLineOfInterestAndCursor');
// Load file1
element.viewState = {
view: GerritView.DIFF,
patchNum: 1 as RevisionPatchSetNum,
changeNum: 101 as NumericChangeId,
project: 'test-project' as RepoName,
path: 'file1',
};
element.patchRange = {
patchNum: 1 as RevisionPatchSetNum,
basePatchNum: PARENT,
};
element.change = {
...createParsedChange(),
revisions: createRevisions(1),
};
await element.updateComplete;
assert.isFalse(setUrlStub.called);
// Switch to file2
element.handleFileChange(
new CustomEvent('value-change', {detail: {value: 'file2'}})
);
assert.isTrue(setUrlStub.calledOnce);
// This is to mock the param change triggered by above navigate
element.viewState = {
view: GerritView.DIFF,
patchNum: 1 as RevisionPatchSetNum,
changeNum: 101 as NumericChangeId,
project: 'test-project' as RepoName,
path: 'file2',
};
element.patchRange = {
patchNum: 1 as RevisionPatchSetNum,
basePatchNum: PARENT,
};
// No extra call
assert.isTrue(setUrlStub.calledOnce);
});
test('_computeDownloadDropdownLinks', () => {
const downloadLinks = [
{
url: '/changes/test~12/revisions/1/patch?zip&path=index.php',
name: 'Patch',
},
{
url: '/changes/test~12/revisions/1/files/index.php/download?parent=1',
name: 'Left Content',
},
{
url: '/changes/test~12/revisions/1/files/index.php/download',
name: 'Right Content',
},
];
element.change = createParsedChange();
element.change.project = 'test' as RepoName;
element.changeNum = 12 as NumericChangeId;
element.patchRange = {
patchNum: 1 as RevisionPatchSetNum,
basePatchNum: PARENT,
};
element.path = 'index.php';
element.diff = createDiff();
assert.deepEqual(element.computeDownloadDropdownLinks(), downloadLinks);
});
test('_computeDownloadDropdownLinks diff returns renamed', () => {
const downloadLinks = [
{
url: '/changes/test~12/revisions/3/patch?zip&path=index.php',
name: 'Patch',
},
{
url: '/changes/test~12/revisions/2/files/index2.php/download',
name: 'Left Content',
},
{
url: '/changes/test~12/revisions/3/files/index.php/download',
name: 'Right Content',
},
];
const diff = createDiff();
diff.change_type = 'RENAMED';
diff.meta_a!.name = 'index2.php';
element.change = createParsedChange();
element.change.project = 'test' as RepoName;
element.changeNum = 12 as NumericChangeId;
element.patchRange = {
patchNum: 3 as RevisionPatchSetNum,
basePatchNum: 2 as BasePatchSetNum,
};
element.path = 'index.php';
element.diff = diff;
assert.deepEqual(element.computeDownloadDropdownLinks(), downloadLinks);
});
test('computeDownloadFileLink', () => {
assert.equal(
element.computeDownloadFileLink(
'test' as RepoName,
12 as NumericChangeId,
{patchNum: 1 as PatchSetNumber, basePatchNum: PARENT},
'index.php',
true
),
'/changes/test~12/revisions/1/files/index.php/download?parent=1'
);
assert.equal(
element.computeDownloadFileLink(
'test' as RepoName,
12 as NumericChangeId,
{patchNum: 1 as PatchSetNumber, basePatchNum: -2 as PatchSetNumber},
'index.php',
true
),
'/changes/test~12/revisions/1/files/index.php/download?parent=2'
);
assert.equal(
element.computeDownloadFileLink(
'test' as RepoName,
12 as NumericChangeId,
{patchNum: 3 as PatchSetNumber, basePatchNum: 2 as PatchSetNumber},
'index.php',
true
),
'/changes/test~12/revisions/2/files/index.php/download'
);
assert.equal(
element.computeDownloadFileLink(
'test' as RepoName,
12 as NumericChangeId,
{patchNum: 3 as PatchSetNumber, basePatchNum: 2 as PatchSetNumber},
'index.php',
false
),
'/changes/test~12/revisions/3/files/index.php/download'
);
});
test('computeDownloadPatchLink', () => {
assert.equal(
element.computeDownloadPatchLink(
'test' as RepoName,
12 as NumericChangeId,
{basePatchNum: PARENT, patchNum: 1 as RevisionPatchSetNum},
'index.php'
),
'/changes/test~12/revisions/1/patch?zip&path=index.php'
);
});
});
suite('unmodified files with comments', () => {
let element: GrDiffView;
setup(async () => {
const changedFiles = {
'file1.txt': createFileInfo(),
'a/b/test.c': createFileInfo(),
};
stubRestApi('getConfig').returns(Promise.resolve(createServerInfo()));
stubRestApi('getProjectConfig').returns(Promise.resolve(createConfig()));
stubRestApi('getChangeFiles').returns(Promise.resolve(changedFiles));
stubRestApi('saveFileReviewed').returns(Promise.resolve(new Response()));
stubRestApi('getDiffComments').returns(Promise.resolve({}));
stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
stubRestApi('getReviewedFiles').returns(Promise.resolve([]));
element = await fixture(html`<gr-diff-view></gr-diff-view>`);
element.changeNum = 42 as NumericChangeId;
});
test('fetchFiles add files with comments without changes', () => {
element.patchRange = {
basePatchNum: 5 as BasePatchSetNum,
patchNum: 10 as RevisionPatchSetNum,
};
element.changeComments = {
getPaths: sinon.stub().returns({
'file2.txt': {},
'file1.txt': {},
}),
} as unknown as ChangeComments;
element.changeNum = 23 as NumericChangeId;
return element.fetchFiles().then(() => {
assert.deepEqual(element.files, {
sortedFileList: ['a/b/test.c', 'file1.txt', 'file2.txt'],
changeFilesByPath: {
'file1.txt': createFileInfo(),
'file2.txt': {status: 'U'} as FileInfo,
'a/b/test.c': createFileInfo(),
},
});
});
});
});
});