Separate file list header into new component

Change-Id: I45619dfaaf005a89f8de6d3e9dc39a2bcbca1893
This commit is contained in:
Becky Siegel
2017-09-11 15:18:22 -07:00
parent 29dc3762b4
commit cc150c8aaa
7 changed files with 999 additions and 773 deletions

View File

@@ -21,12 +21,10 @@ limitations under the License.
<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
<link rel="import" href="../../diff/gr-diff-preferences/gr-diff-preferences.html">
<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
<link rel="import" href="../../shared/gr-select/gr-select.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
<link rel="import" href="../../shared/gr-change-star/gr-change-star.html">
<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
<link rel="import" href="../../shared/gr-editable-content/gr-editable-content.html">
<link rel="import" href="../../shared/gr-editable-label/gr-editable-label.html">
<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
<link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html">
<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
@@ -37,6 +35,7 @@ limitations under the License.
<link rel="import" href="../gr-commit-info/gr-commit-info.html">
<link rel="import" href="../gr-download-dialog/gr-download-dialog.html">
<link rel="import" href="../gr-file-list/gr-file-list.html">
<link rel="import" href="../gr-file-list-header/gr-file-list-header.html">
<link rel="import" href="../gr-messages-list/gr-messages-list.html">
<link rel="import" href="../gr-related-changes-list/gr-related-changes-list.html">
<link rel="import" href="../gr-reply-dialog/gr-reply-dialog.html">
@@ -80,9 +79,6 @@ limitations under the License.
font-size: 1.2em;
font-weight: bold;
}
.prefsButton {
float: right;
}
gr-change-star {
margin-right: .25em;
vertical-align: -.425em;
@@ -152,45 +148,10 @@ limitations under the License.
flex: 1;
overflow-x: hidden;
}
.collapseToggleButton {
text-decoration: none;
}
.relatedChanges {
flex: 1 1 auto;
overflow: hidden;
}
.patchInfo {
border: 1px solid #ddd;
margin: 1em var(--default-horizontal-margin);
}
.patchInfoEdit .patchInfo-header {
background-color: #fcfad6;
}
.patchInfoOldPatchSet .patchInfo-header {
background-color: #fff9c4;
}
.patchInfoOldPatchSet .latestPatchContainer {
display: initial;
}
.patchInfo-header,
.fileList {
padding: .5em calc(var(--default-horizontal-margin) / 2);
}
.patchInfo-header {
background-color: #f6f6f6;
border-bottom: 1px solid #ebebeb;
display: flex;
justify-content: space-between;
}
.latestPatchContainer {
display: none;
}
.patchSetSelect {
max-width: 8em;
}
gr-editable-label.descriptionLabel {
max-width: 100%;
}
.mobile {
display: none;
}
@@ -203,13 +164,6 @@ limitations under the License.
height: 0;
margin-bottom: 1em;
}
#diffPrefsContainer,
.rightControls {
margin: auto 0 auto auto;
}
.patchInfo-header-wrapper {
width: 100%;
}
#commitMessage.collapsed {
max-height: 36em;
overflow: hidden;
@@ -246,32 +200,12 @@ limitations under the License.
.showOnEdit {
display: none;
}
.editLoaded .hideOnEdit {
display: none;
.patchInfo {
border: 1px solid #ddd;
margin: 1em var(--default-horizontal-margin);
}
.editLoaded .showOnEdit {
display: initial;
}
.fileList-header {
display: flex;
font-weight: bold;
justify-content: space-between;
margin-bottom: .5em;
}
.rightControls {
display: flex;
flex-wrap: wrap;
font-weight: normal;
justify-content: flex-end;
}
.separator {
margin: 0 .25em;
}
.expandInline {
padding-right: .25em;
}
.patchSetSelect {
max-width: 8em;
#fileList {
padding: .5em calc(var(--default-horizontal-margin) / 2);
}
@media screen and (min-width: 80em) {
.commitMessage {
@@ -357,7 +291,7 @@ limitations under the License.
<div class="container loading" hidden$="[[!_loading]]">Loading...</div>
<div
id="mainContent"
class$="container [[_computeEditLoadedClass(_editLoaded)]]"
class="container"
hidden$="{{_loading}}">
<div class$="hideOnMobileOverlay [[_computeHeaderClass(_change)]]">
<span class="header-title">
@@ -486,138 +420,44 @@ limitations under the License.
</div>
</div>
</section>
<section class$="patchInfo hideOnMobileOverlay [[_computePatchInfoClass(_patchRange.patchNum,
_allPatchSets)]]">
<div class="patchInfo-header">
<div class="patchInfo-header-wrapper">
<label class="patchSelectLabel" for="patchSetSelect">
Patch set
</label>
<gr-select
id="patchSetSelect"
bind-value="{{_selectedPatchSet}}"
class="patchSetSelect"
on-change="_handlePatchChange">
<select>
<template is="dom-repeat" items="[[_allPatchSets]]"
as="patchNum">
<option
value$="[[patchNum.num]]"
disabled$="[[_computePatchSetDisabled(patchNum.num, _patchRange.basePatchNum, _sortedRevisions)]]">
[[patchNum.num]]
/
[[computeLatestPatchNum(_allPatchSets)]]
[[_computePatchSetCommentsString(_comments, patchNum.num)]]
[[_computePatchSetDescription(_change, patchNum.num)]]
</option>
</template>
</select>
</gr-select>
/
<gr-commit-info
change="[[_change]]"
server-config="[[_serverConfig]]"
commit-info="[[_commitInfo]]"></gr-commit-info>
<span class="latestPatchContainer">
/
<a href$="[[_computeChangeUrl(_change)]]">Go to latest patch set</a>
</span>
<span class="downloadContainer desktop">
/
<gr-button link
class="download"
on-tap="_handleDownloadTap">Download</gr-button>
</span>
<span class="descriptionContainer hideOnEdit">
/
<gr-editable-label
id="descriptionLabel"
class="descriptionLabel"
value="[[_computePatchSetDescription(_change, _selectedPatchSet)]]"
placeholder="[[_computeDescriptionPlaceholder(_descriptionReadOnly)]]"
read-only="[[_descriptionReadOnly]]"
on-changed="_handleDescriptionChanged"></gr-editable-label>
</span>
<span id="diffPrefsContainer"
class="hideOnEdit"
hidden$="[[_computePrefsButtonHidden(_diffPrefs, _loggedIn)]]"
hidden>
<gr-button link
class="prefsButton desktop"
on-tap="_handlePrefsTap">Diff Preferences</gr-button>
</span>
</div>
</div>
<div class="fileList">
<div class="fileList-header">
<div>Files</div>
<div class="rightControls">
<template is="dom-if"
if="[[_fileListActionsVisible(_shownFileCount, _maxFilesForBulkActions)]]">
<gr-button
id="expandBtn"
link
on-tap="_expandAllDiffs">Show diffs</gr-button>
<span class="separator">/</span>
<gr-button
id="collapseBtn"
link
on-tap="_collapseAllDiffs">Hide diffs</gr-button>
</template>
<template is="dom-if"
if="[[!_fileListActionsVisible(_shownFileCount, _maxFilesForBulkActions)]]">
<div class="warning">
Bulk actions disabled because there are too many files.
</div>
</template>
<span class="separator">/</span>
<gr-select
id="modeSelect"
bind-value="{{viewState.diffMode}}">
<select>
<option value="SIDE_BY_SIDE">Side By Side</option>
<option value="UNIFIED_DIFF">Unified</option>
</select>
</gr-select>
<span class="separator">/</span>
<label>
Diff against
<gr-select id="patchChange" bind-value="{{_diffAgainst}}"
class="patchSetSelect" on-change="_handleBasePatchChange">
<select>
<option value="PARENT">Base</option>
<template
is="dom-repeat"
items="[[_allPatchSets]]"
as="patchNum">
<option
disabled$="[[_computeBasePatchDisabled(patchNum.num, _patchRange.patchNum, _sortedRevisions)]]"
value$="[[patchNum.num]]">
[[patchNum.num]]
[[patchNum.desc]]
</option>
</template>
</select>
</gr-select>
</label>
</div>
</div>
<gr-file-list id="fileList"
diff-prefs="{{_diffPrefs}}"
change="[[_change]]"
change-num="[[_changeNum]]"
patch-range="{{_patchRange}}"
comments="[[_comments]]"
drafts="[[_diffDrafts]]"
revisions="[[_sortedRevisions]]"
project-config="[[_projectConfig]]"
selected-index="{{viewState.selectedFileIndex}}"
diff-view-mode="[[viewState.diffMode]]"
edit-loaded="[[_editLoaded]]"
num-files-shown="{{_numFilesShown}}"
file-list-increment="{{_numFilesShown}}"
on-files-shown-changed="_setShownFiles"></gr-file-list>
</div>
<section class="patchInfo hideOnMobileOverlay">
<gr-file-list-header
id="fileListHeader"
account="[[_account]]"
all-patch-sets="[[_allPatchSets]]"
change="[[_change]]"
change-num="[[_changeNum]]"
comments="[[_comments]]"
commit-info="[[_commitInfo]]"
change-url="[[_computeChangeUrl(_change)]]"
edit-loaded="[[_editLoaded]]"
logged-in="[[_loggedIn]]"
server-config="[[_serverConfig]]"
shown-file-count="[[_shownFileCount]]"
diff-prefs="[[_diffPrefs]]"
diff-view-mode="{{viewState.diffMode}}"
patch-range="{{_patchRange}}"
revisions="[[_sortedRevisions]]"
on-open-diff-prefs="_handleOpenDiffPrefs"
on-open-download-dialog="_handleOpenDownloadDialog"
on-expand-diffs="_expandAllDiffs"
on-collapse-diffs="_collapseAllDiffs">
</gr-file-list-header>
<gr-file-list id="fileList"
diff-prefs="{{_diffPrefs}}"
change="[[_change]]"
change-num="[[_changeNum]]"
patch-range="{{_patchRange}}"
comments="[[_comments]]"
drafts="[[_diffDrafts]]"
revisions="[[_sortedRevisions]]"
project-config="[[_projectConfig]]"
selected-index="{{viewState.selectedFileIndex}}"
diff-view-mode="[[viewState.diffMode]]"
edit-loaded="[[_editLoaded]]"
num-files-shown="{{_numFilesShown}}"
file-list-increment="{{_numFilesShown}}"
on-files-shown-changed="_setShownFiles"></gr-file-list>
</section>
<gr-messages-list id="messageList"
class="hideOnMobileOverlay"

View File

@@ -23,8 +23,6 @@
const MIN_LINES_FOR_COMMIT_COLLAPSE = 30;
const DEFAULT_NUM_FILES_SHOWN = 200;
// Maximum length for patch set descriptions.
const PATCH_DESC_MAX_LENGTH = 500;
const REVIEWERS_REGEX = /^(R|CC)=/gm;
const MIN_CHECK_INTERVAL_SECS = 0;
@@ -134,18 +132,10 @@
type: String,
computed:
'_computeChangeIdCommitMessageError(_latestCommitMessage, _change)',
},
// Caps the number of files that can be shown and have the 'show diffs' /
// 'hide diffs' buttons still be functional.
_maxFilesForBulkActions: {
type: Number,
readOnly: true,
value: 225,
},
/** @type {?} */
_patchRange: {
type: Object,
observer: '_updateSelected',
},
_relatedChangesLoading: {
type: Boolean,
@@ -175,10 +165,6 @@
type: Boolean,
value: false,
},
_descriptionReadOnly: {
type: Boolean,
computed: '_computeDescriptionReadOnly(_loggedIn, _change, _account)',
},
_replyDisabled: {
type: Boolean,
value: true,
@@ -293,10 +279,6 @@
this._sortedRevisions = this.sortRevisions(Object.values(revisions));
},
_computePrefsButtonHidden(prefs, loggedIn) {
return !loggedIn || !prefs;
},
_handleEditCommitMessage(e) {
this._editingCommitMessage = true;
this.$.commitMessageEditor.focusTextarea();
@@ -338,11 +320,6 @@
return false;
},
_handlePrefsTap(e) {
e.preventDefault();
this.$.fileList.openDiffPrefs();
},
_handleCommentSave(e) {
if (!e.target.comment.__draft) { return; }
@@ -409,21 +386,16 @@
this._diffDrafts = diffDrafts;
},
_handleBasePatchChange(e) {
this._changePatchNum(this._selectedPatchSet, e.target.value, true);
},
_handlePatchChange(e) {
this._changePatchNum(e.target.value, this._diffAgainst, true);
},
_handleReplyTap(e) {
e.preventDefault();
this._openReplyDialog();
},
_handleDownloadTap(e) {
e.preventDefault();
_handleOpenDiffPrefs() {
this.$.fileList.openDiffPrefs();
},
_handleOpenDownloadDialog() {
this.$.downloadOverlay.open().then(() => {
this.$.downloadOverlay
.setFocusStops(this.$.downloadDialog.getFocusStops());
@@ -494,10 +466,6 @@
this._shownFileCount = e.detail.length;
},
_fileListActionsVisible(shownFileCount, maxFilesForBulkActions) {
return shownFileCount <= maxFilesForBulkActions;
},
_expandAllDiffs() {
this.$.fileList.expandAllDiffs();
},
@@ -666,39 +634,12 @@
this._patchRange.patchNum ||
this.computeLatestPatchNum(this._allPatchSets));
this._updateSelected();
this.$.fileListHeader.updateSelected();
const title = change.subject + ' (' + change.change_id.substr(0, 9) + ')';
this.fire('title-change', {title});
},
/**
* Change active patch to the provided patch num.
* @param {number|string} basePatchNum the base patch to be viewed.
* @param {number|string} patchNum the patch number to be viewed.
* @param {boolean} opt_forceParams When set to true, the resulting URL will
* always include the patch range, even if the requested patchNum is
* known to be the latest.
*/
_changePatchNum(patchNum, basePatchNum, opt_forceParams) {
if (!opt_forceParams) {
let currentPatchNum;
if (this._change.current_revision) {
currentPatchNum =
this._change.revisions[this._change.current_revision]._number;
} else {
currentPatchNum = this.computeLatestPatchNum(this._allPatchSets);
}
if (this.patchNumEquals(patchNum, currentPatchNum) &&
basePatchNum === 'PARENT') {
Gerrit.Nav.navigateToChange(this._change);
return;
}
}
Gerrit.Nav.navigateToChange(this._change, patchNum,
basePatchNum);
},
_computeChangeUrl(change) {
return Gerrit.Nav.getUrlForChange(change);
},
@@ -753,37 +694,6 @@
return CHANGE_ID_ERROR.MISSING;
},
_computePatchInfoClass(patchNum, allPatchSets) {
if (this.patchNumEquals(patchNum, this.EDIT_NAME)) {
return 'patchInfoEdit';
}
const latestNum = this.computeLatestPatchNum(allPatchSets);
if (this.patchNumEquals(patchNum, latestNum)) {
return '';
}
return 'patchInfoOldPatchSet';
},
/**
* Determines if a patch number should be disabled based on value of the
* basePatchNum from gr-file-list.
* @param {number} patchNum Patch number available in dropdown
* @param {number|string} basePatchNum Base patch number from file list
* @return {boolean}
*/
_computePatchSetDisabled(patchNum, basePatchNum) {
if (basePatchNum === 'PARENT') { return false; }
return this.findSortedIndex(patchNum, this._sortedRevisions) <=
this.findSortedIndex(basePatchNum, this._sortedRevisions);
},
_computeBasePatchDisabled(patchNum, currentPatchNum) {
return this.findSortedIndex(patchNum, this._sortedRevisions) >=
this.findSortedIndex(currentPatchNum, this._sortedRevisions);
},
_computeLabelNames(labels) {
return Object.keys(labels).sort();
},
@@ -1163,81 +1073,11 @@
]);
},
_updateSelected() {
this._selectedPatchSet = this._patchRange.patchNum;
this._diffAgainst = this._patchRange.basePatchNum;
},
_computePatchSetDescription(change, patchNum) {
const rev = this.getRevisionByPatchNum(change.revisions, patchNum);
return (rev && rev.description) ?
rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : '';
},
_computePatchSetCommentsString(allComments, patchNum) {
let numComments = 0;
let numUnresolved = 0;
for (const file in allComments) {
if (allComments.hasOwnProperty(file)) {
numComments += this.$.fileList.getCommentsForPath(
allComments, patchNum, file).length;
numUnresolved += this.$.fileList.computeUnresolvedNum(
allComments, {}, patchNum, file);
}
}
let commentsStr = '';
if (numComments > 0) {
commentsStr = '(' + numComments + ' comments';
if (numUnresolved > 0) {
commentsStr += ', ' + numUnresolved + ' unresolved';
}
commentsStr += ')';
}
return commentsStr;
},
_computeDescriptionPlaceholder(readOnly) {
return (readOnly ? 'No' : 'Add a') + ' patch set description';
},
_handleDescriptionChanged(e) {
const desc = e.detail.trim();
const rev = this.getRevisionByPatchNum(this._change.revisions,
this._selectedPatchSet);
const sha = this._getPatchsetHash(this._change.revisions, rev);
this.$.restAPI.setDescription(this._changeNum,
this._selectedPatchSet, desc)
.then(res => {
if (res.ok) {
this.set(['_change', 'revisions', sha, 'description'], desc);
}
});
},
/**
* @param {!Object} revisions The revisions object keyed by revision hashes
* @param {?Object} patchSet A revision already fetched from {revisions}
* @return {string|undefined} the SHA hash corresponding to the revision.
*/
_getPatchsetHash(revisions, patchSet) {
for (const rev in revisions) {
if (revisions.hasOwnProperty(rev) &&
revisions[rev] === patchSet) {
return rev;
}
}
},
_computeCanStartReview(loggedIn, change, account) {
return !!(loggedIn && change.work_in_progress &&
change.owner._account_id === account._account_id);
},
_computeDescriptionReadOnly(loggedIn, change, account) {
return !(loggedIn && (account._account_id === change.owner._account_id));
},
_computeReplyDisabled() { return false; },
_computeChangePermalinkAriaLabel(changeNum) {
@@ -1424,9 +1264,5 @@
const patchRange = patchRangeRecord.base || {};
return this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME);
},
_computeEditLoadedClass(editLoaded) {
return editLoaded ? 'editLoaded' : '';
},
});
})();

View File

@@ -171,6 +171,20 @@ limitations under the License.
assert.isFalse(element.$.mainContent.classList.contains('overlayOpen'));
});
test('expand all messages when expand-diffs fired', () => {
const handleExpand =
sandbox.stub(element.$.fileList, 'expandAllDiffs');
element.$.fileListHeader.fire('expand-diffs');
assert.isTrue(handleExpand.called);
});
test('collapse all messages when collapse-diffs fired', () => {
const handleCollapse =
sandbox.stub(element.$.fileList, 'collapseAllDiffs');
element.$.fileListHeader.fire('collapse-diffs');
assert.isTrue(handleCollapse.called);
});
test('X should expand all messages', () => {
const handleExpand =
sandbox.stub(element.$.messageList, 'handleExpandCollapse');
@@ -238,79 +252,13 @@ limitations under the License.
});
});
test('Diff preferences hidden when no prefs or logged out', () => {
element._loggedIn = false;
flushAsynchronousOperations();
assert.isTrue(element.$.diffPrefsContainer.hidden);
element._loggedIn = true;
flushAsynchronousOperations();
assert.isTrue(element.$.diffPrefsContainer.hidden);
element._loggedIn = false;
element._diffPrefs = {font_size: '12'};
flushAsynchronousOperations();
assert.isTrue(element.$.diffPrefsContainer.hidden);
element._loggedIn = true;
flushAsynchronousOperations();
assert.isFalse(element.$.diffPrefsContainer.hidden);
});
test('prefsButton opens gr-diff-preferences', () => {
const handlePrefsTapSpy = sandbox.spy(element, '_handlePrefsTap');
test('diff preferences open when open-diff-prefs is fired', () => {
const overlayOpenStub = sandbox.stub(element.$.fileList,
'openDiffPrefs');
const prefsButton = Polymer.dom(element.root).querySelectorAll(
'.prefsButton')[0];
MockInteractions.tap(prefsButton);
assert.isTrue(handlePrefsTapSpy.called);
element.$.fileListHeader.fire('open-diff-prefs');
assert.isTrue(overlayOpenStub.called);
});
test('_computeDescriptionReadOnly', () => {
assert.equal(element._computeDescriptionReadOnly(false,
{owner: {_account_id: 1}}, {_account_id: 1}), true);
assert.equal(element._computeDescriptionReadOnly(true,
{owner: {_account_id: 0}}, {_account_id: 1}), true);
assert.equal(element._computeDescriptionReadOnly(true,
{owner: {_account_id: 1}}, {_account_id: 1}), false);
});
test('_computeDescriptionPlaceholder', () => {
assert.equal(element._computeDescriptionPlaceholder(true),
'No patch set description');
assert.equal(element._computeDescriptionPlaceholder(false),
'Add a patch set description');
});
test('_computePatchSetDisabled', () => {
element._sortedRevisions = [
{_number: 1},
{_number: 2},
{_number: element.EDIT_NAME, basePatchNum: 2},
{_number: 3},
];
let basePatchNum = 'PARENT';
let patchNum = 1;
assert.equal(element._computePatchSetDisabled(patchNum, basePatchNum),
false);
basePatchNum = 1;
assert.equal(element._computePatchSetDisabled(patchNum, basePatchNum),
true);
patchNum = 2;
assert.equal(element._computePatchSetDisabled(patchNum, basePatchNum),
false);
basePatchNum = element.EDIT_NAME;
assert.equal(element._computePatchSetDisabled(patchNum, basePatchNum),
true);
patchNum = '3';
assert.equal(element._computePatchSetDisabled(patchNum, basePatchNum),
false);
});
test('_prepareCommitMsgForLinkify', () => {
let commitMessage = 'R=test@google.com';
let result = element._prepareCommitMsgForLinkify(commitMessage);
@@ -325,80 +273,6 @@ limitations under the License.
assert.equal(result, 'CC=\u200Btest@google.com');
}),
test('_computePatchSetCommentsString', () => {
// Test string with unresolved comments.
comments = {
foo: 'foo comments',
bar: 'bar comments',
xyz: 'xyz comments',
};
sandbox.stub(element.$.fileList, 'getCommentsForPath', (c, p, f) => {
if (f == 'foo') {
return ['comment1', 'comment2'];
} else if (f == 'bar') {
return ['comment1'];
} else {
return [];
}
});
sandbox.stub(element.$.fileList, 'computeUnresolvedNum', (c, d, p, f) => {
if (f == 'foo') {
return 0;
} else if (f == 'bar') {
return 1;
} else {
return 0;
}
});
assert.equal(element._computePatchSetCommentsString(comments, 1),
'(3 comments, 1 unresolved)');
// Test string with no unresolved comments.
delete comments['bar'];
assert.equal(element._computePatchSetCommentsString(comments, 1),
'(2 comments)');
// Test string with no comments.
delete comments['foo'];
assert.equal(element._computePatchSetCommentsString(comments, 1), '');
});
test('_handleDescriptionChanged', () => {
const putDescStub = sandbox.stub(element.$.restAPI, 'setDescription')
.returns(Promise.resolve({ok: true}));
sandbox.stub(element, '_computeDescriptionReadOnly');
element._changeNum = '42';
element._patchRange = {
basePatchNum: 'PARENT',
patchNum: 1,
};
element._selectedPatchNum = '1';
element._change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
rev1: {_number: 1, description: 'test', commit: {commit: 'rev1'}},
},
current_revision: 'rev1',
status: 'NEW',
labels: {},
actions: {},
owner: {_account_id: 1},
};
element._account = {_account_id: 1};
element._loggedIn = true;
flushAsynchronousOperations();
const label = element.$.descriptionLabel;
assert.equal(label.value, 'test');
label.editing = true;
label._inputText = 'test2';
label._save();
flushAsynchronousOperations();
assert.isTrue(putDescStub.called);
assert.equal(putDescStub.args[0][2], 'test2');
});
test('_updateRebaseAction', () => {
const currentRevisionActions = {
cherrypick: {
@@ -572,106 +446,6 @@ limitations under the License.
assert.equal(element._numFilesShown, 200);
});
test('patch num change', done => {
element._changeNum = '42';
element._patchRange = {
basePatchNum: 'PARENT',
patchNum: 2,
};
element._change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
rev2: {_number: 2},
rev1: {_number: 1},
rev13: {_number: 13},
rev3: {_number: 3},
},
current_revision: 'rev3',
status: 'NEW',
labels: {},
};
element.viewState.diffMode = 'UNIFIED';
flushAsynchronousOperations();
const selectEl = element.$$('.patchInfo-header gr-select');
assert.ok(selectEl);
const optionEls = Polymer.dom(element.root).querySelectorAll(
'.patchInfo-header option');
assert.equal(optionEls.length, 4);
const select = element.$$('.patchInfo-header #patchSetSelect').bindValue;
assert.notEqual(select, 1);
assert.equal(select, 2);
assert.notEqual(select, 3);
assert.equal(optionEls[3].value, 13);
let numEvents = 0;
selectEl.addEventListener('change', e => {
assert.equal(element.viewState.diffMode, 'UNIFIED');
numEvents++;
if (numEvents == 1) {
assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
element._change, '1', 'PARENT'));
selectEl.nativeSelect.value = '3';
element.fire('change', {}, {node: selectEl.nativeSelect});
} else if (numEvents == 2) {
assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
element._change, '3', 'PARENT'));
done();
}
});
selectEl.nativeSelect.value = '1';
element.fire('change', {}, {node: selectEl.nativeSelect});
});
test('patch num change with missing current_revision', done => {
element._changeNum = '42';
element._patchRange = {
basePatchNum: 'PARENT',
patchNum: 2,
};
element._change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
rev2: {_number: 2},
rev1: {_number: 1},
rev13: {_number: 13},
rev3: {_number: 3},
},
status: 'NEW',
labels: {},
};
flushAsynchronousOperations();
const selectEl = element.$$('.patchInfo-header gr-select');
assert.ok(selectEl);
const optionEls = Polymer.dom(element.root).querySelectorAll(
'.patchInfo-header option');
assert.equal(optionEls.length, 4);
assert.notEqual(
element.$$('.patchInfo-header #patchSetSelect').bindValue, 1);
assert.equal(
element.$$('.patchInfo-header #patchSetSelect').bindValue, 2);
assert.notEqual(
element.$$('.patchInfo-header #patchSetSelect').bindValue, 3);
assert.equal(optionEls[3].value, 13);
let numEvents = 0;
selectEl.addEventListener('change', e => {
numEvents++;
if (numEvents == 1) {
assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
element._change, '1', 'PARENT'));
selectEl.nativeSelect.value = '3';
element.fire('change', {}, {node: selectEl.nativeSelect});
} else if (numEvents == 2) {
assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
element._change, '3', 'PARENT'));
done();
}
});
selectEl.nativeSelect.value = '1';
element.fire('change', {}, {node: selectEl.nativeSelect});
});
test('diffMode defaults to side by side without preferences', done => {
sandbox.stub(element.$.restAPI, 'getPreferences').returns(
Promise.resolve({}));
@@ -703,101 +477,6 @@ limitations under the License.
});
});
test('diff against dropdown', done => {
element._changeNum = '42';
element._patchRange = {
basePatchNum: 'PARENT',
patchNum: '3',
};
element._change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
rev1: {_number: 1},
rev2: {_number: 2},
rev3: {_number: 'edit', basePatchNum: 2},
rev4: {_number: 3},
},
status: 'NEW',
labels: {},
};
flush(() => {
const selectEl = element.$.patchChange;
assert.equal(selectEl.nativeSelect.value, 'PARENT');
assert.isTrue(element.$$('#patchChange option[value="3"]')
.hasAttribute('disabled'));
selectEl.addEventListener('change', () => {
assert.equal(selectEl.nativeSelect.value, 'edit');
assert(navigateToChangeStub.lastCall.calledWithExactly(
element._change, '3', 'edit'),
'Should navigate to /c/42/edit..3');
done();
});
selectEl.nativeSelect.value = 'edit';
element.fire('change', {}, {node: selectEl.nativeSelect});
});
});
test('expandAllDiffs called when expand button clicked', () => {
element._shownFileCount = 1;
flushAsynchronousOperations();
sandbox.stub(element.$.fileList, 'expandAllDiffs');
MockInteractions.tap(Polymer.dom(element.root).querySelector(
'#expandBtn'));
assert.isTrue(element.$.fileList.expandAllDiffs.called);
});
test('collapseAllDiffs called when expand button clicked', () => {
element._shownFileCount = 1;
flushAsynchronousOperations();
sandbox.stub(element.$.fileList, 'collapseAllDiffs');
MockInteractions.tap(Polymer.dom(element.root).querySelector(
'#collapseBtn'));
assert.isTrue(element.$.fileList.collapseAllDiffs.called);
});
test('show/hide diffs disabled for large amounts of files', done => {
const computeSpy = sandbox.spy(element, '_fileListActionsVisible');
element._files = [];
element.changeNum = '42';
element.patchRange = {
basePatchNum: 'PARENT',
patchNum: '2',
};
element._shownFileCount = 1;
flush(() => {
assert.isTrue(computeSpy.lastCall.returnValue);
_.times(element._maxFilesForBulkActions + 1, () => {
element._shownFileCount = element._shownFileCount + 1;
});
assert.isFalse(computeSpy.lastCall.returnValue);
done();
});
});
test('diff mode selector initializes from preferences', () => {
let resolvePrefs;
const prefsPromise = new Promise(resolve => {
resolvePrefs = resolve;
});
sandbox.stub(element.$.restAPI, 'getPreferences').returns(prefsPromise);
// Attach a new gr-change-view so we can intercept the preferences fetch.
const view = document.createElement('gr-change-view');
const select = view.$.modeSelect;
fixture('blank').appendChild(view);
flushAsynchronousOperations();
// At this point the diff mode doesn't yet have the user's preference.
assert.equal(select.nativeSelect.value, 'SIDE_BY_SIDE');
// Receive the overriding preference.
resolvePrefs({default_diff_view: 'UNIFIED'});
flushAsynchronousOperations();
assert.equal(select.nativeSelect.value, 'SIDE_BY_SIDE');
document.getElementById('blank').restore();
});
test('dont reload entire page when patchRange changes', () => {
const reloadStub = sandbox.stub(element, '_reload',
() => { return Promise.resolve(); });
@@ -841,35 +520,6 @@ limitations under the License.
assert.isTrue(collapseStub.calledTwice);
});
test('include base patch when not parent', () => {
element._changeNum = '42';
element._patchRange = {
basePatchNum: '2',
patchNum: '3',
};
element._change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
rev2: {_number: 2},
rev1: {_number: 1},
rev13: {_number: 13},
rev3: {_number: 3},
},
status: 'NEW',
labels: {},
};
element._changePatchNum(13, 2);
assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
element._change, 13, 2));
element._patchRange.basePatchNum = 'PARENT';
element._changePatchNum(3, 'PARENT');
assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
element._change, 3, 'PARENT'));
});
test('related changes are updated and new patch selected after rebase',
done => {
element._changeNum = '42';
@@ -888,7 +538,6 @@ limitations under the License.
test('related changes are not updated after other action', done => {
sandbox.stub(element, '_reload', () => { return Promise.resolve(); });
sandbox.stub(element, '_updateSelected');
sandbox.stub(element.$.relatedChanges, 'reload');
const e = {detail: {action: 'abandon'}};
element._handleReloadChange(e).then(() => {
@@ -1092,15 +741,6 @@ limitations under the License.
'_openReplyDialog should have been passed CCS');
});
test('class is applied to file list on old patch set', () => {
const allPatchSets = [{num: 1}, {num: 2}, {num: 4}];
assert.equal(element._computePatchInfoClass('1', allPatchSets),
'patchInfoOldPatchSet');
assert.equal(element._computePatchInfoClass('2', allPatchSets),
'patchInfoOldPatchSet');
assert.equal(element._computePatchInfoClass('4', allPatchSets), '');
});
test('getUrlParameter functionality', () => {
const locationStub = sandbox.stub(element, '_getLocationSearch');
@@ -1510,30 +1150,14 @@ limitations under the License.
assert.equal(element._patchRange.patchNum, 'baz');
});
suite('editLoaded behavior', () => {
setup(() => {
element._loggedIn = true;
element._diffPrefs = {};
});
test('_editLoaded set when patchNum is an edit', () => {
sandbox.stub(element, 'computeLatestPatchNum').returns('2');
element._patchRange = {patchNum: element.EDIT_NAME};
const isVisible = el => {
assert.ok(el);
return getComputedStyle(el).getPropertyValue('display') !== 'none';
};
assert.isTrue(element._editLoaded);
element.set('_patchRange.patchNum', 1);
test('patch specific elements', () => {
sandbox.stub(element, 'computeLatestPatchNum').returns('2');
element._patchRange = {patchNum: element.EDIT_NAME};
flushAsynchronousOperations();
assert.isFalse(isVisible(element.$.diffPrefsContainer));
assert.isFalse(isVisible(element.$$('.descriptionContainer')));
element.set('_patchRange.patchNum', 1);
flushAsynchronousOperations();
assert.isTrue(isVisible(element.$$('.descriptionContainer')));
assert.isTrue(isVisible(element.$.diffPrefsContainer));
});
assert.isFalse(element._editLoaded);
});
});
</script>

View File

@@ -0,0 +1,226 @@
<!--
Copyright (C) 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
<link rel="import" href="../../shared/gr-editable-label/gr-editable-label.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../../shared/gr-select/gr-select.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
<dom-module id="gr-file-list-header">
<template>
<style include="shared-styles">
.prefsButton {
float: right;
}
.collapseToggleButton {
text-decoration: none;
}
.patchInfoEdit.patchInfo-header {
background-color: #fcfad6;
}
.patchInfoOldPatchSet.patchInfo-header {
background-color: #fff9c4;
}
.patchInfoOldPatchSet .latestPatchContainer {
display: initial;
}
.patchInfo-header {
padding: .5em calc(var(--default-horizontal-margin) / 2);
}
.patchInfo-header {
background-color: #f6f6f6;
border-bottom: 1px solid #ebebeb;
display: flex;
justify-content: space-between;
}
.latestPatchContainer {
display: none;
}
.patchSetSelect {
max-width: 8em;
}
gr-editable-label.descriptionLabel {
max-width: 100%;
}
.mobile {
display: none;
}
#diffPrefsContainer,
.rightControls {
margin: auto 0 auto auto;
}
.patchInfo-header-wrapper {
width: 100%;
}
.showOnEdit {
display: none;
}
.editLoaded .hideOnEdit {
display: none;
}
.editLoaded .showOnEdit {
display: initial;
}
.fileList-header {
display: flex;
font-weight: bold;
justify-content: space-between;
margin: .5em calc(var(--default-horizontal-margin) / 2);
}
.rightControls {
display: flex;
flex-wrap: wrap;
font-weight: normal;
justify-content: flex-end;
}
.separator {
margin: 0 .25em;
}
.expandInline {
padding-right: .25em;
}
.patchSetSelect {
max-width: 8em;
}
.editLoaded .hideOnEdit {
display: none;
}
.editLoaded .showOnEdit {
display: initial;
}
</style>
<div class$="patchInfo-header [[_computeEditLoadedClass(editLoaded)]] [[_computePatchInfoClass(patchRange.patchNum, allPatchSets)]]">
<div class="patchInfo-header-wrapper">
<label class="patchSelectLabel" for="patchSetSelect">
Patch set
</label>
<gr-select
id="patchSetSelect"
bind-value="{{_selectedPatchSet}}"
class="patchSetSelect"
on-change="_handlePatchChange">
<select>
<template is="dom-repeat" items="[[allPatchSets]]"
as="patchNum">
<option
value$="[[patchNum.num]]"
disabled$="[[_computePatchSetDisabled(patchNum.num, patchRange.basePatchNum, revisions)]]">
[[patchNum.num]]
/
[[computeLatestPatchNum(allPatchSets)]]
[[_computePatchSetCommentsString(comments, patchNum.num)]]
[[_computePatchSetDescription(change, patchNum.num)]]
</option>
</template>
</select>
</gr-select>
/
<gr-commit-info
change="[[change]]"
server-config="[[serverConfig]]"
commit-info="[[commitInfo]]"></gr-commit-info>
<span class="latestPatchContainer">
/
<a href$="[[changeUrl]]">Go to latest patch set</a>
</span>
<span class="downloadContainer desktop">
/
<gr-button link
class="download"
on-tap="_handleDownloadTap">Download</gr-button>
</span>
<span class="descriptionContainer hideOnEdit">
/
<gr-editable-label
id="descriptionLabel"
class="descriptionLabel"
value="[[_computePatchSetDescription(change, _selectedPatchSet)]]"
placeholder="[[_computeDescriptionPlaceholder(_descriptionReadOnly)]]"
read-only="[[_descriptionReadOnly]]"
on-changed="_handleDescriptionChanged"></gr-editable-label>
</span>
<span id="diffPrefsContainer"
class="hideOnEdit"
hidden$="[[_computePrefsButtonHidden(diffPrefs, loggedIn)]]"
hidden>
<gr-button link
class="prefsButton desktop"
on-tap="_handlePrefsTap">Diff Preferences</gr-button>
</span>
</div>
</div>
<div class="fileList-header">
<div>Files</div>
<div class="rightControls">
<template is="dom-if"
if="[[_fileListActionsVisible(shownFileCount, _maxFilesForBulkActions)]]">
<gr-button
id="expandBtn"
link
on-tap="_expandAllDiffs">Show diffs</gr-button>
<span class="separator">/</span>
<gr-button
id="collapseBtn"
link
on-tap="_collapseAllDiffs">Hide diffs</gr-button>
</template>
<template is="dom-if"
if="[[!_fileListActionsVisible(shownFileCount, _maxFilesForBulkActions)]]">
<div class="warning">
Bulk actions disabled because there are too many files.
</div>
</template>
<span class="separator">/</span>
<gr-select
id="modeSelect"
bind-value="{{diffViewMode}}">
<select>
<option value="SIDE_BY_SIDE">Side By Side</option>
<option value="UNIFIED_DIFF">Unified</option>
</select>
</gr-select>
<span class="separator">/</span>
<label>
Diff against
<gr-select id="patchChange" bind-value="{{_diffAgainst}}"
class="patchSetSelect" on-change="_handleBasePatchChange">
<select>
<option value="PARENT">Base</option>
<template
is="dom-repeat"
items="[[allPatchSets]]"
as="patchNum">
<option
disabled$="[[_computeBasePatchDisabled(patchNum.num, patchRange.patchNum, revisions)]]"
value$="[[patchNum.num]]">
[[patchNum.num]]
[[patchNum.desc]]
</option>
</template>
</select>
</gr-select>
</label>
</div>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-file-list-header.js"></script>
</dom-module>

View File

@@ -0,0 +1,264 @@
// Copyright (C) 2017 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
(function() {
'use strict';
// Maximum length for patch set descriptions.
const PATCH_DESC_MAX_LENGTH = 500;
Polymer({
is: 'gr-file-list-header',
properties: {
account: Object,
allPatchSets: Array,
change: Object,
changeNum: String,
changeUrl: String,
comments: Object,
commitInfo: Object,
editLoaded: Boolean,
loggedIn: Boolean,
serverConfig: Object,
shownFileCount: Number,
diffPrefs: Object,
diffViewMode: String,
/** @type {?} */
patchRange: {
type: Object,
observer: 'updateSelected',
},
revisions: Array,
// Caps the number of files that can be shown and have the 'show diffs' /
// 'hide diffs' buttons still be functional.
_maxFilesForBulkActions: {
type: Number,
readOnly: true,
value: 225,
},
_descriptionReadOnly: {
type: Boolean,
computed: '_computeDescriptionReadOnly(loggedIn, change, account)',
},
_selectedPatchSet: String,
_diffAgainst: String,
},
behaviors: [
Gerrit.PatchSetBehavior,
],
_expandAllDiffs() {
this.fire('expand-diffs');
},
_collapseAllDiffs() {
this.fire('collapse-diffs');
},
updateSelected() {
this._selectedPatchSet = this.patchRange.patchNum;
this._diffAgainst = this.patchRange.basePatchNum;
},
_computeDescriptionPlaceholder(readOnly) {
return (readOnly ? 'No' : 'Add a') + ' patch set description';
},
_computeDescriptionReadOnly(loggedIn, change, account) {
return !(loggedIn && (account._account_id === change.owner._account_id));
},
_computePatchSetDescription(change, patchNum) {
const rev = this.getRevisionByPatchNum(change.revisions, patchNum);
return (rev && rev.description) ?
rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : '';
},
/**
* @param {!Object} revisions The revisions object keyed by revision hashes
* @param {?Object} patchSet A revision already fetched from {revisions}
* @return {string|undefined} the SHA hash corresponding to the revision.
*/
_getPatchsetHash(revisions, patchSet) {
for (const rev in revisions) {
if (revisions.hasOwnProperty(rev) &&
revisions[rev] === patchSet) {
return rev;
}
}
},
_handleDescriptionChanged(e) {
const desc = e.detail.trim();
const rev = this.getRevisionByPatchNum(this.change.revisions,
this._selectedPatchSet);
const sha = this._getPatchsetHash(this.change.revisions, rev);
this.$.restAPI.setDescription(this.changeNum,
this._selectedPatchSet, desc)
.then(res => {
if (res.ok) {
this.set(['_change', 'revisions', sha, 'description'], desc);
}
});
},
_computeBasePatchDisabled(patchNum, currentPatchNum) {
return this.findSortedIndex(patchNum, this.revisions) >=
this.findSortedIndex(currentPatchNum, this.revisions);
},
_computePrefsButtonHidden(prefs, loggedIn) {
return !loggedIn || !prefs;
},
// Copied from gr-file-list
_getCommentsForPath(comments, patchNum, path) {
return (comments[path] || []).filter(c => {
return this.patchNumEquals(c.patch_set, patchNum);
});
},
// Copied from gr-file-list
_computeUnresolvedNum(comments, drafts, patchNum, path) {
comments = this._getCommentsForPath(comments, patchNum, path);
drafts = this._getCommentsForPath(drafts, patchNum, path);
comments = comments.concat(drafts);
// Create an object where every comment ID is the key of an unresolved
// comment.
const idMap = comments.reduce((acc, comment) => {
if (comment.unresolved) {
acc[comment.id] = true;
}
return acc;
}, {});
// Set false for the comments that are marked as parents.
for (const comment of comments) {
idMap[comment.in_reply_to] = false;
}
// The unresolved comments are the comments that still have true.
const unresolvedLeaves = Object.keys(idMap).filter(key => {
return idMap[key];
});
return unresolvedLeaves.length;
},
_computePatchSetCommentsString(allComments, patchNum) {
let numComments = 0;
let numUnresolved = 0;
for (const file in allComments) {
if (allComments.hasOwnProperty(file)) {
numComments += this._getCommentsForPath(
allComments, patchNum, file).length;
numUnresolved += this._computeUnresolvedNum(
allComments, {}, patchNum, file);
}
}
let commentsStr = '';
if (numComments > 0) {
commentsStr = '(' + numComments + ' comments';
if (numUnresolved > 0) {
commentsStr += ', ' + numUnresolved + ' unresolved';
}
commentsStr += ')';
}
return commentsStr;
},
_fileListActionsVisible(shownFileCount, maxFilesForBulkActions) {
return shownFileCount <= maxFilesForBulkActions;
},
/**
* Determines if a patch number should be disabled based on value of the
* basePatchNum from gr-file-list.
* @param {number} patchNum Patch number available in dropdown
* @param {number|string} basePatchNum Base patch number from file list
* @return {boolean}
*/
_computePatchSetDisabled(patchNum, basePatchNum) {
if (basePatchNum === 'PARENT') { return false; }
return this.findSortedIndex(patchNum, this.revisions) <=
this.findSortedIndex(basePatchNum, this.revisions);
},
/**
* Change active patch to the provided patch num.
* @param {number|string} basePatchNum the base patch to be viewed.
* @param {number|string} patchNum the patch number to be viewed.
* @param {boolean} opt_forceParams When set to true, the resulting URL will
* always include the patch range, even if the requested patchNum is
* known to be the latest.
*/
_changePatchNum(patchNum, basePatchNum, opt_forceParams) {
if (!opt_forceParams) {
let currentPatchNum;
if (this.change.current_revision) {
currentPatchNum =
this.change.revisions[this.change.current_revision]._number;
} else {
currentPatchNum = this.computeLatestPatchNum(this.allPatchSets);
}
if (this.patchNumEquals(patchNum, currentPatchNum) &&
basePatchNum === 'PARENT') {
Gerrit.Nav.navigateToChange(this.change);
return;
}
}
Gerrit.Nav.navigateToChange(this.change, patchNum,
basePatchNum);
},
_handleBasePatchChange(e) {
this._changePatchNum(this._selectedPatchSet, e.target.value, true);
},
_handlePatchChange(e) {
this._changePatchNum(e.target.value, this._diffAgainst, true);
},
_handlePrefsTap(e) {
e.preventDefault();
this.fire('open-diff-prefs');
},
_handleDownloadTap(e) {
e.preventDefault();
this.fire('open-download-dialog');
},
_computeEditLoadedClass(editLoaded) {
return editLoaded ? 'editLoaded' : '';
},
_computePatchInfoClass(patchNum, allPatchSets) {
if (this.patchNumEquals(patchNum, this.EDIT_NAME)) {
return 'patchInfoEdit';
}
const latestNum = this.computeLatestPatchNum(allPatchSets);
if (this.patchNumEquals(patchNum, latestNum)) {
return '';
}
return 'patchInfoOldPatchSet';
},
});
})();

View File

@@ -0,0 +1,435 @@
<!DOCTYPE html>
<!--
Copyright (C) 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-file-list-header</title>
<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<script src="../../../bower_components/page/page.js"></script>
<link rel="import" href="gr-file-list-header.html">
<script>void(0);</script>
<test-fixture id="basic">
<template>
<gr-file-list-header></gr-file-list-header>
</template>
</test-fixture>
<test-fixture id="blank">
<template>
<div></div>
</template>
</test-fixture>
<script>
suite('gr-file-list-header tests', () => {
let element;
let sandbox;
let navigateToChangeStub;
setup(() => {
sandbox = sinon.sandbox.create();
navigateToChangeStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
stub('gr-rest-api-interface', {
getConfig() { return Promise.resolve({test: 'config'}); },
getAccount() { return Promise.resolve(null); },
_fetchSharedCacheURL() { return Promise.resolve({}); },
});
element = fixture('basic');
});
teardown(done => {
flush(() => {
sandbox.restore();
done();
});
});
test('Diff preferences hidden when no prefs or logged out', () => {
element.loggedIn = false;
flushAsynchronousOperations();
assert.isTrue(element.$.diffPrefsContainer.hidden);
element.loggedIn = true;
flushAsynchronousOperations();
assert.isTrue(element.$.diffPrefsContainer.hidden);
element.loggedIn = false;
element.diffPrefs = {font_size: '12'};
flushAsynchronousOperations();
assert.isTrue(element.$.diffPrefsContainer.hidden);
element.loggedIn = true;
flushAsynchronousOperations();
assert.isFalse(element.$.diffPrefsContainer.hidden);
});
test('_computeDescriptionReadOnly', () => {
assert.equal(element._computeDescriptionReadOnly(false,
{owner: {_account_id: 1}}, {_account_id: 1}), true);
assert.equal(element._computeDescriptionReadOnly(true,
{owner: {_account_id: 0}}, {_account_id: 1}), true);
assert.equal(element._computeDescriptionReadOnly(true,
{owner: {_account_id: 1}}, {_account_id: 1}), false);
});
test('_computeDescriptionPlaceholder', () => {
assert.equal(element._computeDescriptionPlaceholder(true),
'No patch set description');
assert.equal(element._computeDescriptionPlaceholder(false),
'Add a patch set description');
});
test('_computePatchSetDisabled', () => {
element.revisions = [
{_number: 1},
{_number: 2},
{_number: element.EDIT_NAME, basePatchNum: 2},
{_number: 3},
];
let basePatchNum = 'PARENT';
let patchNum = 1;
assert.equal(element._computePatchSetDisabled(patchNum, basePatchNum),
false);
basePatchNum = 1;
assert.equal(element._computePatchSetDisabled(patchNum, basePatchNum),
true);
patchNum = 2;
assert.equal(element._computePatchSetDisabled(patchNum, basePatchNum),
false);
basePatchNum = element.EDIT_NAME;
assert.equal(element._computePatchSetDisabled(patchNum, basePatchNum),
true);
patchNum = '3';
assert.equal(element._computePatchSetDisabled(patchNum, basePatchNum),
false);
});
test('_computePatchSetCommentsString', () => {
// Test string with unresolved comments.
comments = {
foo: [{
id: '27dcee4d_f7b77cfa',
message: 'test',
patch_set: 1,
unresolved: true,
}],
bar: [{
id: '27dcee4d_f7b77cfa',
message: 'test',
patch_set: 1,
},
{
id: '27dcee4d_f7b77cfa',
message: 'test',
patch_set: 1,
}],
abc: [],
};
assert.equal(element._computePatchSetCommentsString(comments, 1),
'(3 comments, 1 unresolved)');
// Test string with no unresolved comments.
delete comments['foo'];
assert.equal(element._computePatchSetCommentsString(comments, 1),
'(2 comments)');
// Test string with no comments.
delete comments['bar'];
assert.equal(element._computePatchSetCommentsString(comments, 1), '');
});
test('_handleDescriptionChanged', () => {
const putDescStub = sandbox.stub(element.$.restAPI, 'setDescription')
.returns(Promise.resolve({ok: true}));
sandbox.stub(element, '_computeDescriptionReadOnly');
element.changeNum = '42';
element.patchRange = {
basePatchNum: 'PARENT',
patchNum: 1,
};
element._selectedPatchNum = '1';
element.change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
rev1: {_number: 1, description: 'test', commit: {commit: 'rev1'}},
},
current_revision: 'rev1',
status: 'NEW',
labels: {},
actions: {},
owner: {_account_id: 1},
};
element.account = {_account_id: 1};
element.loggedIn = true;
flushAsynchronousOperations();
const label = element.$.descriptionLabel;
assert.equal(label.value, 'test');
label.editing = true;
label._inputText = 'test2';
label._save();
flushAsynchronousOperations();
assert.isTrue(putDescStub.called);
assert.equal(putDescStub.args[0][2], 'test2');
});
test('patch num change', done => {
element.patchRange = {
basePatchNum: 'PARENT',
patchNum: 2,
};
element.allPatchSets = [
{num: 1},
{num: 2},
{num: 3},
{num: 13},
];
flushAsynchronousOperations();
const selectEl = element.$$('.patchInfo-header gr-select');
assert.ok(selectEl);
const optionEls = Polymer.dom(element.root).querySelectorAll(
'.patchInfo-header option');
assert.equal(optionEls.length, 4);
const select = element.$$('.patchInfo-header #patchSetSelect').bindValue;
assert.notEqual(select, 1);
assert.equal(select, 2);
assert.notEqual(select, 3);
assert.equal(optionEls[3].value, 13);
let numEvents = 0;
selectEl.addEventListener('change', e => {
assert.equal(element.diffViewMode, 'SIDE_BY_SIDE');
numEvents++;
if (numEvents == 1) {
assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
element.change, '1', 'PARENT'));
selectEl.nativeSelect.value = '3';
element.fire('change', {}, {node: selectEl.nativeSelect});
} else if (numEvents == 2) {
assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
element.change, '3', 'PARENT'));
done();
}
});
selectEl.nativeSelect.value = '1';
element.fire('change', {}, {node: selectEl.nativeSelect});
});
test('patch num change with missing current_revision', done => {
element.patchRange = {
basePatchNum: 'PARENT',
patchNum: 2,
};
element.allPatchSets = [
{num: 1},
{num: 2},
{num: 3},
{num: 13},
];
flushAsynchronousOperations();
const selectEl = element.$$('.patchInfo-header gr-select');
assert.ok(selectEl);
const optionEls = Polymer.dom(element.root).querySelectorAll(
'.patchInfo-header option');
assert.equal(optionEls.length, 4);
assert.notEqual(
element.$$('.patchInfo-header #patchSetSelect').bindValue, 1);
assert.equal(
element.$$('.patchInfo-header #patchSetSelect').bindValue, 2);
assert.notEqual(
element.$$('.patchInfo-header #patchSetSelect').bindValue, 3);
assert.equal(optionEls[3].value, 13);
let numEvents = 0;
selectEl.addEventListener('change', e => {
numEvents++;
if (numEvents == 1) {
assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
element.change, '1', 'PARENT'));
selectEl.nativeSelect.value = '3';
element.fire('change', {}, {node: selectEl.nativeSelect});
} else if (numEvents == 2) {
assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
element.change, '3', 'PARENT'));
done();
}
});
selectEl.nativeSelect.value = '1';
element.fire('change', {}, {node: selectEl.nativeSelect});
});
test('diff against dropdown', done => {
element.revisions = [
{commit: {}},
{commit: {}},
{commit: {}},
{commit: {}},
];
element.allPatchSets = [
{num: 1},
{num: 2},
{num: 3},
{num: 'edit'},
];
element.patchRange = {
basePatchNum: 'PARENT',
patchNum: '3',
};
flush(() => {
const selectEl = element.$.patchChange;
assert.equal(selectEl.nativeSelect.value, 'PARENT');
assert.isTrue(element.$$('#patchChange option[value="3"]')
.hasAttribute('disabled'));
selectEl.addEventListener('change', () => {
assert.equal(selectEl.nativeSelect.value, 'edit');
assert(navigateToChangeStub.lastCall.calledWithExactly(
element.change, '3', 'edit'),
'Should navigate to /c/42/edit..3');
done();
});
selectEl.nativeSelect.value = 'edit';
element.fire('change', {}, {node: selectEl.nativeSelect});
});
});
test('expandAllDiffs called when expand button clicked', () => {
element.shownFileCount = 1;
flushAsynchronousOperations();
sandbox.stub(element, '_expandAllDiffs');
MockInteractions.tap(Polymer.dom(element.root).querySelector(
'#expandBtn'));
assert.isTrue(element._expandAllDiffs.called);
});
test('collapseAllDiffs called when expand button clicked', () => {
element.shownFileCount = 1;
flushAsynchronousOperations();
sandbox.stub(element, '_collapseAllDiffs');
MockInteractions.tap(Polymer.dom(element.root).querySelector(
'#collapseBtn'));
assert.isTrue(element._collapseAllDiffs.called);
});
test('show/hide diffs disabled for large amounts of files', done => {
const computeSpy = sandbox.spy(element, '_fileListActionsVisible');
element._files = [];
element.changeNum = '42';
element.patchRange = {
basePatchNum: 'PARENT',
patchNum: '2',
};
element.shownFileCount = 1;
flush(() => {
assert.isTrue(computeSpy.lastCall.returnValue);
_.times(element._maxFilesForBulkActions + 1, () => {
element.shownFileCount = element.shownFileCount + 1;
});
assert.isFalse(computeSpy.lastCall.returnValue);
done();
});
});
test('diff mode selector is set correctly', () => {
const select = element.$.modeSelect;
element.diffViewMode = 'SIDE_BY_SIDE';
flushAsynchronousOperations();
assert.equal(select.nativeSelect.value, 'SIDE_BY_SIDE');
element.diffViewMode = 'UNIFIED_DIFF';
flushAsynchronousOperations();
assert.equal(select.nativeSelect.value, 'UNIFIED_DIFF');
});
test('include base patch when not parent', () => {
element.changeNum = '42';
element.patchRange = {
basePatchNum: '2',
patchNum: '3',
};
element.change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
rev2: {_number: 2},
rev1: {_number: 1},
rev13: {_number: 13},
rev3: {_number: 3},
},
status: 'NEW',
labels: {},
};
element._changePatchNum(13, 2);
assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
element.change, 13, 2));
element.patchRange.basePatchNum = 'PARENT';
element._changePatchNum(3, 'PARENT');
assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
element.change, 3, 'PARENT'));
});
test('class is applied to file list on old patch set', () => {
const allPatchSets = [{num: 1}, {num: 2}, {num: 4}];
assert.equal(element._computePatchInfoClass('1', allPatchSets),
'patchInfoOldPatchSet');
assert.equal(element._computePatchInfoClass('2', allPatchSets),
'patchInfoOldPatchSet');
assert.equal(element._computePatchInfoClass('4', allPatchSets), '');
});
suite('editLoaded behavior', () => {
setup(() => {
element.loggedIn = true;
element.diffPrefs = {};
});
const isVisible = el => {
assert.ok(el);
return getComputedStyle(el).getPropertyValue('display') !== 'none';
};
test('patch specific elements', () => {
element.editLoaded = true;
sandbox.stub(element, 'computeLatestPatchNum').returns('2');
flushAsynchronousOperations();
assert.isFalse(isVisible(element.$.diffPrefsContainer));
assert.isFalse(isVisible(element.$$('.descriptionContainer')));
element.editLoaded = false;
flushAsynchronousOperations();
assert.isTrue(isVisible(element.$$('.descriptionContainer')));
assert.isTrue(isVisible(element.$.diffPrefsContainer));
});
});
});
</script>

View File

@@ -68,6 +68,7 @@ limitations under the License.
'change/gr-label-scores/gr-label-scores_test.html',
'change/gr-label-score-row/gr-label-score-row_test.html',
'change/gr-file-list/gr-file-list_test.html',
'change/gr-file-list-header/gr-file-list-header_test.html',
'change/gr-message/gr-message_test.html',
'change/gr-messages-list/gr-messages-list_test.html',
'change/gr-related-changes-list/gr-related-changes-list_test.html',