Adds "Watched Projects"/Notifications section to PolyGerrit settings

Gives the PolyGerrit settings view a new section for Notifications which
replicates the "Watched Projects" screen in the GWT settings UI. Users
can add, remove and update which projects they receive email
notifications for.

Introduces the gr-watched-projects-editor element, which is given the
user's watched projects data and is able to display and modify the info.
In the same way that the GWT UI includes an autocomplete input for adding
new projects to watch, this implementation uses gr-autocomplete.

The gr-watched-projects-editor is added to the gr-settings-view, which
manages the loading, saving and deleting of the user's watched-project
data.

Other changes:

- Tidies the layout of gr-settings-view to permit larger tables such as
  gr-watched-projects-editor without disrupting the look.

- Tweaks functionality of gr-autocomplete use in the watched projects
  editor. Specifically, it does not take on a value until the user
  "commits" to a suggestion, and it does not clear the input when
  committed unless a flag is enabled. Also now supports placeholder
  text.

- Fixes some gr-settings-view unit-test failures in Safari.

- Fixes the markup of some table headers and footers.

- Lots of new unit tests.

Bug: Issue 3911
Change-Id: I4667c29aa3addcd9b6676bc8763d8e3deb327539
This commit is contained in:
Wyatt Allen
2016-06-09 17:44:51 -07:00
parent 6a3859a7d8
commit bd47217b1b
14 changed files with 676 additions and 50 deletions

View File

@@ -77,6 +77,7 @@ limitations under the License.
<gr-autocomplete
id="input"
threshold="3"
clear-on-commit
query="[[_query]]"
disabled="[[disabled]]"
on-commit="_sendAddRequest"

View File

@@ -127,7 +127,8 @@
this.$.addReviewer.focus();
},
_sendAddRequest: function(e, reviewer) {
_sendAddRequest: function(e, detail) {
var reviewer = detail.value;
var reviewerID;
if (reviewer.account) {
reviewerID = reviewer.account._account_id;

View File

@@ -28,9 +28,8 @@ limitations under the License.
color: #666;
text-align: left;
}
th.name-header,
th.url-header {
width: 15em;
th.nameHeader {
width: 11em;
}
tbody tr:nth-child(even) {
background-color: #f4f4f4;
@@ -41,13 +40,20 @@ limitations under the License.
}
input {
font-size: 1em;
width: 13em;
}
.newTitleInput {
width: 10em;
}
.newUrlInput {
width: 23em;
}
</style>
<table>
<thead>
<th class="name-header">Name</th>
<th class="url-header">URL</th>
<tr>
<th class="nameHeader">Name</th>
<th class="url-header">URL</th>
</tr>
</thead>
<tbody>
<template is="dom-repeat" items="[[menuItems]]">
@@ -76,25 +82,31 @@ limitations under the License.
</template>
</tbody>
<tfoot>
<th>
<input
is="iron-input"
on-keydown="_handleInputKeydown"
bind-value="{{_newName}}">
</th>
<th>
<input
is="iron-input"
on-keydown="_handleInputKeydown"
bind-value="{{_newUrl}}">
</th>
<th></th>
<th></th>
<th>
<gr-button
disabled$="[[_computeAddDisabled(_newName, _newUrl)]]"
on-tap="_handleAddButton">Add</gr-button>
</th>
<tr>
<th>
<input
class="newTitleInput"
is="iron-input"
placeholder="New Title"
on-keydown="_handleInputKeydown"
bind-value="{{_newName}}">
</th>
<th>
<input
class="newUrlInput"
is="iron-input"
placeholder="New URL"
on-keydown="_handleInputKeydown"
bind-value="{{_newUrl}}">
</th>
<th></th>
<th></th>
<th>
<gr-button
disabled$="[[_computeAddDisabled(_newName, _newUrl)]]"
on-tap="_handleAddButton">Add</gr-button>
</th>
</tr>
</tfoot>
</table>
</template>

View File

@@ -16,6 +16,7 @@ limitations under the License.
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../gr-menu-editor/gr-menu-editor.html">
<link rel="import" href="../gr-watched-projects-editor/gr-watched-projects-editor.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
@@ -30,7 +31,7 @@ limitations under the License.
}
main {
margin: 2em auto;
max-width: 40em;
max-width: 46em;
}
h1 {
margin-bottom: .1em;
@@ -51,7 +52,7 @@ limitations under the License.
color: #666;
font-weight: bold;
padding-right: .5em;
width: 15em;
width: 11em;
}
.loading {
color: #666;
@@ -106,7 +107,7 @@ limitations under the License.
<h2>Preferences</h2>
<fieldset id="preferences">
<section>
<span class="title">Maximum Changes Per Page</span>
<span class="title">Changes Per Page</span>
<span class="value">
<select
is="gr-select"
@@ -175,6 +176,16 @@ limitations under the License.
on-tap="_handleSaveMenu"
disabled="[[!_menuChanged]]">Save Changes</gr-button>
</fieldset>
<h2>Notifications</h2>
<fieldset id="watchedProjects">
<gr-watched-projects-editor
on-project-removed="_handleWatchedProjectRemoved"
projects="{{_watchedProjects}}"></gr-watched-projects-editor>
<gr-button
on-tap="_handleSaveWatchedProjects"
disabled$="[[!_watchedProjectsChanged]]"
id="_handleSaveWatchedProjects">Save Changes</gr-button>
</fieldset>
</main>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>

View File

@@ -42,6 +42,7 @@
type: Array,
value: function() { return []; },
},
_watchedProjects: Array,
_loading: {
type: Boolean,
value: true,
@@ -54,11 +55,20 @@
type: Boolean,
value: false,
},
_watchedProjectsChanged: {
type: Boolean,
value: false,
},
_watchedProjectsToRemove: {
type: Array,
value: function() { return []; },
},
},
observers: [
'_handlePrefsChanged(_localPrefs.*)',
'_handleMenuChanged(_localMenu.splices)',
'_handleProjectsChanged(_watchedProjects.*)',
],
attached: function() {
@@ -74,6 +84,10 @@
this._cloneMenu();
}.bind(this)));
promises.push(this.$.restAPI.getWatchedProjects().then(function(projs) {
this._watchedProjects = projs;
}.bind(this)));
Promise.all(promises).then(function() {
this._loading = false;
}.bind(this));
@@ -128,5 +142,32 @@
this._menuChanged = false;
}.bind(this));
},
_handleWatchedProjectRemoved: function(e) {
var project = e.detail;
// If it was never saved, then we don't need to do anything.
if (project._is_local) { return; }
this._watchedProjectsToRemove.push(project);
this._handleProjectsChanged();
},
_handleProjectsChanged: function() {
if (this._loading) { return; }
this._watchedProjectsChanged = true;
},
_handleSaveWatchedProjects: function() {
this.$.restAPI.deleteWatchedProjects(this._watchedProjectsToRemove)
.then(function() {
return this.$.restAPI.saveWatchedProjects(this._watchedProjects);
}.bind(this))
.then(function(watchedProjects) {
this._watchedProjects = watchedProjects;
this._watchedProjectsChanged = false;
this._watchedProjectsToRemove = [];
}.bind(this));
},
});
})();

View File

@@ -76,11 +76,15 @@ limitations under the License.
{url: '/second/url', name: 'second name', target: '_blank'},
],
};
watchedProjects = [];
stub('gr-rest-api-interface', {
getLoggedIn: function() { return Promise.resolve(true); },
getAccount: function() { return Promise.resolve(account); },
getPreferences: function() { return Promise.resolve(preferences); },
getWatchedProjects: function() {
return Promise.resolve(watchedProjects);
},
});
element = fixture('basic');
@@ -102,23 +106,23 @@ limitations under the License.
test('user preferences', function(done) {
// Rendered with the expected preferences selected.
assert.equal(valueOf('Maximum Changes Per Page', 'preferences')
.firstElementChild.value, preferences.changes_per_page);
assert.equal(valueOf('Changes Per Page', 'preferences')
.firstElementChild.bindValue, preferences.changes_per_page);
assert.equal(valueOf('Date/Time Format', 'preferences')
.firstElementChild.value, preferences.date_format);
.firstElementChild.bindValue, preferences.date_format);
assert.equal(valueOf('Date/Time Format', 'preferences')
.lastElementChild.value, preferences.time_format);
.lastElementChild.bindValue, preferences.time_format);
assert.equal(valueOf('Email Notifications', 'preferences')
.firstElementChild.value, preferences.email_strategy);
.firstElementChild.bindValue, preferences.email_strategy);
assert.equal(valueOf('Diff View', 'preferences')
.firstElementChild.value, preferences.diff_view);
.firstElementChild.bindValue, preferences.diff_view);
assert.isFalse(element._prefsChanged);
assert.isFalse(element._menuChanged);
// Change the diff view element.
var diffSelect = valueOf('Diff View', 'preferences').firstElementChild;
diffSelect.value = 'SIDE_BY_SIDE';
diffSelect.bindValue = 'SIDE_BY_SIDE';
diffSelect.fire('change');
assert.isTrue(element._prefsChanged);

View File

@@ -0,0 +1,134 @@
<!--
Copyright (C) 2016 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="../../shared/gr-autocomplete/gr-autocomplete.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<dom-module id="gr-watched-projects-editor">
<template>
<style>
th {
color: #666;
text-align: left;
}
th.projectHeader {
width: 11em;
}
th.notificationHeader {
text-align: center;
}
th.notifType {
text-align: center;
padding: 0 0.4em;
}
tbody tr:nth-child(even) {
background-color: #f4f4f4;
}
td.notifControl {
cursor: pointer;
text-align: center;
}
td.notifControl:hover {
border: 1px solid #ddd;
}
.projectFilter {
color: #777;
font-style: italic;
margin-left: 1em;
}
input {
font-size: 1em;
}
.newProjectInput {
width: 10em;
}
.newFilterInput {
width: 26em;
}
</style>
<table>
<thead>
<tr>
<th class="projectHeader">Project</th>
<template is="dom-repeat" items="[[_getTypes()]]">
<th class="notifType">[[item.name]]</th>
</template>
<th></th>
</tr>
</thead>
<tbody>
<template
is="dom-repeat"
items="[[projects]]"
as="project"
index-as="projectIndex">
<tr>
<td>
[[project.project]]
<template is="dom-if" if="[[project.filter]]">
<div class="projectFilter">[[project.filter]]</div>
</template>
</td>
<template
is="dom-repeat"
items="[[_getTypes()]]"
as="type">
<td class="notifControl" on-tap="_handleNotifCellTap">
<input
type="checkbox"
data-index$="[[projectIndex]]"
data-key$="[[type.key]]"
on-change="_handleCheckboxChange"
checked$="[[_computeCheckboxChecked(project, type.key)]]">
</td>
</template>
<td class="delete-column">
<gr-button
data-index$="[[projectIndex]]"
on-tap="_handleRemoveProject">Delete</gr-button>
</td>
</tr>
</template>
</tbody>
<tfoot>
<tr>
<th>
<gr-autocomplete
id="newProject"
class="newProjectInput"
is="iron-input"
query="[[_query]]"
threshold="3"
placeholder="Project"></gr-autocomplete>
</th>
<th colspan$="[[_getTypeCount()]]">
<input
id="newFilter"
class="newFilterInput"
is="iron-input"
placeholder="branch:name, or other search expression">
</th>
<th>
<gr-button on-tap="_handleAddProject">Add</gr-button>
</th>
</tr>
</tfoot>
</table>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-watched-projects-editor.js"></script>
</dom-module>

View File

@@ -0,0 +1,134 @@
// Copyright (C) 2016 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';
var NOTIFICATION_TYPES = [
{name: 'Changes', key: 'notify_new_changes'},
{name: 'Patches', key: 'notify_new_patch_sets'},
{name: 'Comments', key: 'notify_all_comments'},
{name: 'Submits', key: 'notify_submitted_changes'},
{name: 'Abandons', key: 'notify_abandoned_changes'},
];
Polymer({
is: 'gr-watched-projects-editor',
/**
* Fired when a watched project is removed from the list.
*
* @event project-removed
*/
properties: {
projects: Array,
_query: {
type: Function,
value: function() {
return this._getProjectSuggestions.bind(this);
},
},
},
_getTypes: function() {
return NOTIFICATION_TYPES;
},
_getTypeCount: function() {
return this._getTypes().length;
},
_computeCheckboxChecked: function(project, key) {
return project.hasOwnProperty(key);
},
_getProjectSuggestions: function(input) {
return this.$.restAPI.getSuggestedProjects(input)
.then(function(response) {
var projects = [];
for (var key in response) {
projects.push({
name: key,
value: response[key],
});
}
return projects;
});
},
_handleRemoveProject: function(e) {
var index = parseInt(e.target.getAttribute('data-index'), 10);
var project = this.projects[index];
this.splice('projects', index, 1);
this.fire('project-removed', project);
},
_canAddProject: function(project, filter) {
if (!project || !project.id) { return false; }
// Check if the project with filter is already in the list. Compare
// filters using == to coalesce null and undefined.
for (var i = 0; i < this.projects.length; i++) {
if (this.projects[i].project === project.id &&
this.projects[i].filter == filter) {
return false;
}
}
return true;
},
_getNewProjectIndex: function(name, filter) {
for (var i = 0; i < this.projects.length; i++) {
if (this.projects[i].project > name ||
(this.projects[i].project === name &&
this.projects[i].filter > filter)) {
break;
}
}
return i;
},
_handleAddProject: function() {
var newProject = this.$.newProject.value;
var newProjectName = this.$.newProject.text;
var filter = this.$.newFilter.value || null;
if (!this._canAddProject(newProject, filter)) { return; }
var insertIndex = this._getNewProjectIndex(newProjectName, filter);
this.splice('projects', insertIndex, 0, {
project: newProjectName,
filter: filter,
_is_local: true,
});
this.$.newProject.clear();
this.$.newFilter.bindValue = '';
},
_handleCheckboxChange: function(e) {
var index = parseInt(e.target.getAttribute('data-index'), 10);
var key = e.target.getAttribute('data-key');
var checked = e.target.checked;
this.set(['projects', index, key], !!checked);
},
_handleNotifCellTap: function(e) {
var checkbox = Polymer.dom(e.target).querySelector('input');
if (checkbox) { checkbox.click(); }
},
});
})();

View File

@@ -0,0 +1,193 @@
<!DOCTYPE html>
<!--
Copyright (C) 2016 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-settings-view</title>
<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-watched-projects-editor.html">
<test-fixture id="basic">
<template>
<gr-watched-projects-editor></gr-watched-projects-editor>
</template>
</test-fixture>
<script>
suite('gr-watched-projects-editor tests', function() {
var element;
setup(function() {
stub('gr-rest-api-interface', {
getSuggestedProjects: function(input) {
if (input.indexOf('the') === 0) {
return Promise.resolve({'the project': {
id: 'the project',
state: 'ACTIVE',
web_links: [],
}});
} else {
return Promise.resolve({});
}
},
});
element = fixture('basic');
element.projects = [{
project: 'project a',
notify_submitted_changes: true,
notify_abandoned_changes: true
}, {
project: 'project b',
filter: 'filter 1',
notify_new_changes: true,
}, {
project: 'project b',
filter: 'filter 2',
}, {
project: 'project c',
notify_new_changes: true,
notify_new_patch_sets: true,
notify_all_comments: true
},
];
Polymer.dom.flush();
});
test('renders', function() {
var rows = element.$$('table').querySelectorAll('tbody tr');
assert.equal(rows.length, 4);
function getKeysOfRow(row) {
var boxes = rows[row].querySelectorAll('input[checked]');
return Array.prototype.map.call(boxes,
function(e) { return e.getAttribute('data-key'); });
}
var checkedKeys = getKeysOfRow(0);
assert.equal(checkedKeys.length, 2);
assert.equal(checkedKeys[0], 'notify_submitted_changes');
assert.equal(checkedKeys[1], 'notify_abandoned_changes');
checkedKeys = getKeysOfRow(1);
assert.equal(checkedKeys.length, 1);
assert.equal(checkedKeys[0], 'notify_new_changes');
checkedKeys = getKeysOfRow(2);
assert.equal(checkedKeys.length, 0);
checkedKeys = getKeysOfRow(3);
assert.equal(checkedKeys.length, 3);
assert.equal(checkedKeys[0], 'notify_new_changes');
assert.equal(checkedKeys[1], 'notify_new_patch_sets');
assert.equal(checkedKeys[2], 'notify_all_comments');
});
test('_getProjectSuggestions empty', function(done) {
element._getProjectSuggestions('nonexistent').then(function(projects) {
assert.equal(projects.length, 0);
done();
});
});
test('_getProjectSuggestions non-empty', function(done) {
element._getProjectSuggestions('the project').then(function(projects) {
assert.equal(projects.length, 1);
assert.equal(projects[0].name, 'the project');
done();
});
});
test('_canAddProject', function() {
assert.isFalse(element._canAddProject(null, null));
assert.isFalse(element._canAddProject({}, null));
// Can add a project that is not in the list.
assert.isTrue(element._canAddProject({id: 'project d'}, null));
assert.isTrue(element._canAddProject({id: 'project d'}, 'filter 3'));
// Cannot add a project that is in the list with no filter.
assert.isFalse(element._canAddProject({id: 'project a'}, null));
// Can add a project that is in the list if the filter differs.
assert.isTrue(element._canAddProject({id: 'project a'}, 'filter 4'));
// Cannot add a project that is in the list with the same filter.
assert.isFalse(element._canAddProject({id: 'project b'}, 'filter 1'));
assert.isFalse(element._canAddProject({id: 'project b'}, 'filter 2'));
// Can add a projec that is in the list using a new filter.
assert.isTrue(element._canAddProject({id: 'project b'}, 'filter 3'));
});
test('_getNewProjectIndex', function() {
// Projects are sorted in ASCII order.
assert.equal(element._getNewProjectIndex('project A', 'filter'), 0);
assert.equal(element._getNewProjectIndex('project a', 'filter'), 1);
// Projects are sorted by filter when the names are equal
assert.equal(element._getNewProjectIndex('project b', 'filter 0'), 1);
assert.equal(element._getNewProjectIndex('project b', 'filter 1.5'), 2);
assert.equal(element._getNewProjectIndex('project b', 'filter 3'), 3);
// Projects with filters follow those without
assert.equal(element._getNewProjectIndex('project c', 'filter'), 4);
});
test('_handleAddProject', function() {
element.$.newProject.value = {id: 'project d'};
element.$.newProject.setText('project d');
element.$.newFilter.bindValue = '';
element._handleAddProject();
assert.equal(element.projects.length, 5);
assert.equal(element.projects[4].project, 'project d');
assert.isNotOk(element.projects[4].filter);
assert.isTrue(element.projects[4]._is_local);
});
test('_handleAddProject with invalid inputs', function() {
element.$.newProject.value = {id: 'project b'};
element.$.newProject.setText('project b');
element.$.newFilter.bindValue = 'filter 1';
element._handleAddProject();
assert.equal(element.projects.length, 4);
});
test('_handleRemoveProject', function() {
var removedSpy = sinon.spy();
element.addEventListener('project-removed', removedSpy);
var button = element.$$('table tbody tr:nth-child(2) gr-button');
MockInteractions.tap(button);
var rows = element.$$('table tbody').querySelectorAll('tr');
assert.equal(rows.length, 3);
assert.isTrue(removedSpy.called);
assert.isOk(removedSpy.getCall(0).args[0].detail);
assert.equal(removedSpy.getCall(0).args[0].detail.project, 'project b');
});
});
</script>

View File

@@ -45,6 +45,7 @@ limitations under the License.
is="iron-input"
disabled$="[[disabled]]"
bind-value="{{text}}"
placeholder="[[placeholder]]"
on-keydown="_handleInputKeydown"
on-focus="_updateSuggestions" />
<div

View File

@@ -66,17 +66,26 @@
observer: '_updateSuggestions',
},
_value: {
type: Object,
computed: '_getValue(_suggestions, _index)'
placeholder: String,
clearOnCommit: {
type: Boolean,
value: false,
},
value: Object,
_suggestions: {
type: Array,
value: function() { return []; },
},
_index: Number,
_disableSuggestions: {
type: Boolean,
value: false,
},
},
attached: function() {
@@ -95,15 +104,31 @@
this.text = '';
},
/**
* Set the text of the input without triggering the suggestion dropdown.
* @param {String} text The new text for the input.
*/
setText: function(text) {
this._disableSuggestions = true;
this.text = text;
this._disableSuggestions = false;
},
_updateSuggestions: function() {
if (this._disableSuggestions) { return; }
if (this.text.length < this.threshold) {
this._suggestions = [];
this.value = null;
return;
}
this.query(this.text).then(function(suggestions) {
this._suggestions = suggestions;
this.$.cursor.moveToStart();
if (this._index === -1) {
this.value = null;
}
}.bind(this));
},
@@ -143,9 +168,9 @@
this.fire('cancel');
},
_getValue: function(suggestions, index) {
if (!suggestions.length || index === -1) { return null; }
return suggestions[index].value;
_updateValue: function(suggestions, index) {
if (!suggestions.length || index === -1) { return; }
this.value = suggestions[index].value;
},
_handleBodyClick: function(e) {
@@ -164,8 +189,17 @@
},
_commit: function() {
this.fire('commit', this._value);
this.clear();
this._updateValue(this._suggestions, this._index);
var value = this.value;
if (!this.clearOnCommit && this._suggestions[this._index]) {
this.setText(this._suggestions[this._index].name);
} else {
this.clear();
}
this.fire('commit', {value: value});
},
});
})();

View File

@@ -127,29 +127,67 @@ limitations under the License.
element.addEventListener('commit', commitHandler);
assert.equal(element.$.cursor.index, 0);
assert.equal(element._value, 0);
MockInteractions.pressAndReleaseKeyOn(element.$.input, 40); // Down
assert.equal(element.$.cursor.index, 1);
assert.equal(element._value, 1);
MockInteractions.pressAndReleaseKeyOn(element.$.input, 40); // Down
assert.equal(element.$.cursor.index, 2);
assert.equal(element._value, 2);
MockInteractions.pressAndReleaseKeyOn(element.$.input, 38); // Up
assert.equal(element.$.cursor.index, 1);
assert.equal(element._value, 1);
MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter
assert.equal(element.value, 1);
assert.isTrue(commitHandler.called);
assert.equal(commitHandler.getCall(0).args[0].detail.value, 1);
assert.isTrue(element.$.suggestions.hasAttribute('hidden'));
done();
});
});
test('clear-on-commit behavior (off)', function(done) {
var promise;
var queryStub = sinon.spy(function() {
return promise = Promise.resolve([{name: 'suggestion', value: 0}]);
});
element.query = queryStub;
element.text = 'blah';
promise.then(function() {
var commitHandler = sinon.spy();
element.addEventListener('commit', commitHandler);
MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter
assert.isTrue(commitHandler.called);
assert.equal(commitHandler.getCall(0).args[0].detail, 1);
assert.isTrue(element.$.suggestions.hasAttribute('hidden'));
assert.equal(element.text, 'suggestion');
done();
});
});
test('clear-on-commit behavior (on)', function(done) {
var promise;
var queryStub = sinon.spy(function() {
return promise = Promise.resolve([{name: 'suggestion', value: 0}]);
});
element.query = queryStub;
element.text = 'blah';
element.clearOnCommit = true;
promise.then(function() {
var commitHandler = sinon.spy();
element.addEventListener('commit', commitHandler);
MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter
assert.isTrue(commitHandler.called);
assert.equal(element.text, '');
done();
});
});

View File

@@ -235,6 +235,23 @@
}.bind(this));
},
getWatchedProjects: function() {
return this._fetchSharedCacheURL('/accounts/self/watched.projects');
},
saveWatchedProjects: function(projects, opt_errFn, opt_ctx) {
return this.send('POST', '/accounts/self/watched.projects', projects,
opt_errFn, opt_ctx)
.then(function(response) {
return this.getResponseObject(response);
}.bind(this));
},
deleteWatchedProjects: function(projects, opt_errFn, opt_ctx) {
return this.send('POST', '/accounts/self/watched.projects:delete',
projects, opt_errFn, opt_ctx);
},
_fetchSharedCacheURL: function(url, opt_errFn) {
if (this._sharedFetchPromises[url]) {
return this._sharedFetchPromises[url];
@@ -417,6 +434,10 @@
});
},
getSuggestedProjects: function(inputVal, opt_errFn, opt_ctx) {
return this.fetchJSON('/projects/', opt_errFn, opt_ctx, { p: inputVal, });
},
addChangeReviewer: function(changeNum, reviewerID) {
return this._sendChangeReviewerRequest('POST', changeNum, reviewerID);
},

View File

@@ -55,6 +55,7 @@ limitations under the License.
'diff/gr-selection-action-box/gr-selection-action-box_test.html',
'settings/gr-menu-editor/gr-menu-editor_test.html',
'settings/gr-settings-view/gr-settings-view_test.html',
'settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html',
'shared/gr-autocomplete/gr-autocomplete_test.html',
'shared/gr-account-label/gr-account-label_test.html',
'shared/gr-account-link/gr-account-link_test.html',