Editor for accounts/groups list.

To implement some new UI feature, we need a way to specify list
of accounts and/or groups. Similar solutions already exists for 'change'
view, but it can't be reused as-is in other places.
In this commit the existing solution was reworked to use the same
editor in different places.
Editor was splitted to 2 parts - editor itself and suggestion provider.

Change-Id: I43ce060e568a69f9842fbfad6f5fd62361ab2022
This commit is contained in:
Dmitrii Filippov
2019-09-03 16:10:06 +02:00
parent 146a004b0c
commit 587086bc89
30 changed files with 1343 additions and 563 deletions

View File

@@ -15,33 +15,28 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<script src="../../scripts/gr-display-name-utils/gr-display-name-utils.js"></script>
<script> <script>
(function(window) { (function(window) {
'use strict'; 'use strict';
const ANONYMOUS_NAME = 'Anonymous';
window.Gerrit = window.Gerrit || {}; window.Gerrit = window.Gerrit || {};
/** @polymerBehavior Gerrit.AnonymousNameBehavior */ /** @polymerBehavior Gerrit.DisplayNameBehavior */
Gerrit.AnonymousNameBehavior = { Gerrit.DisplayNameBehavior = {
// TODO(dmfilippov) replace DisplayNameBehavior with GrDisplayNameUtils
/** /**
* enableEmail when true enables to fallback to using email if * enableEmail when true enables to fallback to using email if
* the account name is not avilable. * the account name is not avilable.
*/ */
getUserName(config, account, enableEmail) { getUserName(config, account, enableEmail) {
if (account && account.name) { return GrDisplayNameUtils.getUserName(config, account, enableEmail);
return account.name; },
} else if (account && account.username) {
return account.username;
} else if (enableEmail && account && account.email) {
return account.email;
} else if (config && config.user &&
config.user.anonymous_coward_name !== 'Anonymous Coward') {
return config.user.anonymous_coward_name;
}
return ANONYMOUS_NAME; getGroupDisplayName(group) {
return GrDisplayNameUtils.getGroupDisplayName(group);
}, },
}; };
})(window); })(window);

View File

@@ -17,14 +17,14 @@ limitations under the License.
--> -->
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-anonymous-name-behavior</title> <title>gr-display-name-behavior</title>
<script src="/test/common-test-setup.js"></script> <script src="/test/common-test-setup.js"></script>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script> <script src="/bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../test/common-test-setup.html"/> <link rel="import" href="../../test/common-test-setup.html"/>
<link rel="import" href="gr-anonymous-name-behavior.html"> <link rel="import" href="gr-display-name-behavior.html">
<test-fixture id="basic"> <test-fixture id="basic">
<template> <template>
@@ -33,7 +33,7 @@ limitations under the License.
</test-fixture> </test-fixture>
<script> <script>
suite('gr-anonymous-name-behavior tests', () => { suite('gr-display-name-behavior tests', () => {
let element; let element;
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const config = { const config = {
@@ -48,7 +48,7 @@ limitations under the License.
is: 'test-element-anon', is: 'test-element-anon',
_legacyUndefinedCheck: true, _legacyUndefinedCheck: true,
behaviors: [ behaviors: [
Gerrit.AnonymousNameBehavior, Gerrit.DisplayNameBehavior,
], ],
}); });
}); });
@@ -57,21 +57,21 @@ limitations under the License.
element = fixture('basic'); element = fixture('basic');
}); });
test('test for it to return name', () => { test('getUserName name only', () => {
const account = { const account = {
name: 'test-name', name: 'test-name',
}; };
assert.deepEqual(element.getUserName(config, account, true), 'test-name'); assert.deepEqual(element.getUserName(config, account, true), 'test-name');
}); });
test('test for it to return username', () => { test('getUserName username only', () => {
const account = { const account = {
username: 'test-user', username: 'test-user',
}; };
assert.deepEqual(element.getUserName(config, account, true), 'test-user'); assert.deepEqual(element.getUserName(config, account, true), 'test-user');
}); });
test('test for it to return email', () => { test('getUserName email only', () => {
const account = { const account = {
email: 'test-user@test-url.com', email: 'test-user@test-url.com',
}; };
@@ -79,11 +79,11 @@ limitations under the License.
'test-user@test-url.com'); 'test-user@test-url.com');
}); });
test('test for it not to Anonymous Coward as the anon name', () => { test('getUserName returns not Anonymous Coward as the anon name', () => {
assert.deepEqual(element.getUserName(config, null, true), 'Anonymous'); assert.deepEqual(element.getUserName(config, null, true), 'Anonymous');
}); });
test('test for the config returning the anon name', () => { test('getUserName for the config returning the anon name', () => {
const config = { const config = {
user: { user: {
anonymous_coward_name: 'Test Anon', anonymous_coward_name: 'Test Anon',
@@ -91,5 +91,10 @@ limitations under the License.
}; };
assert.deepEqual(element.getUserName(config, null, true), 'Test Anon'); assert.deepEqual(element.getUserName(config, null, true), 'Test Anon');
}); });
test('getGroupDisplayName', () => {
assert.equal(element.getGroupDisplayName({name: 'Some user name'}),
'Some user name (group)');
});
}); });
</script> </script>

View File

@@ -1,186 +0,0 @@
/**
* @license
* 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';
Polymer({
is: 'gr-account-entry',
_legacyUndefinedCheck: true,
/**
* Fired when an account is entered.
*
* @event add
*/
/**
* When allowAnyInput is true, account-text-changed is fired when input text
* changed. This is needed so that the reply dialog's save button can be
* enabled for arbitrary cc's, which don't need a 'commit'.
*
* @event account-text-changed
*/
properties: {
allowAnyInput: Boolean,
borderless: Boolean,
change: Object,
filter: Function,
placeholder: String,
/**
* When true, account-entry uses the account suggest API endpoint, which
* suggests any account in that Gerrit instance (and does not suggest
* groups).
*
* When false/undefined, account-entry uses the suggest_reviewers API
* endpoint, which suggests any account or group in that Gerrit instance
* that is not already a reviewer (or is not CCed) on that change.
*/
allowAnyUser: Boolean,
// suggestFrom = 0 to enable default suggestions.
suggestFrom: {
type: Number,
value: 0,
},
query: {
type: Function,
value() {
return this._getReviewerSuggestions.bind(this);
},
},
_config: Object,
/** The value of the autocomplete entry. */
_inputText: {
type: String,
observer: '_inputTextChanged',
},
_loggedIn: Boolean,
},
behaviors: [
Gerrit.AnonymousNameBehavior,
Gerrit.FireBehavior,
],
attached() {
this.$.restAPI.getConfig().then(cfg => {
this._config = cfg;
});
this.$.restAPI.getLoggedIn().then(loggedIn => {
this._loggedIn = loggedIn;
});
},
get focusStart() {
return this.$.input.focusStart;
},
focus() {
this.$.input.focus();
},
clear() {
this.$.input.clear();
},
setText(text) {
this.$.input.setText(text);
},
getText() {
return this.$.input.text;
},
_handleInputCommit(e) {
this.fire('add', {value: e.detail.value});
this.$.input.focus();
},
_accountOrAnon(reviewer) {
return this.getUserName(this._config, reviewer, false);
},
_inputTextChanged(text) {
if (text.length && this.allowAnyInput) {
this.dispatchEvent(new CustomEvent(
'account-text-changed', {bubbles: true, composed: true}));
}
},
_makeSuggestion(reviewer) {
let name;
let value;
const generateStatusStr = function(account) {
return account.status ? '(' + account.status + ')' : '';
};
if (reviewer.account) {
// Reviewer is an account suggestion from getChangeSuggestedReviewers.
const reviewerName = this._accountOrAnon(reviewer.account);
const reviewerEmail = this._reviewerEmail(reviewer.account.email);
const reviewerStatus = generateStatusStr(reviewer.account);
name = [reviewerName, reviewerEmail, reviewerStatus]
.filter(p => p.length > 0).join(' ');
value = reviewer;
} else if (reviewer.group) {
// Reviewer is a group suggestion from getChangeSuggestedReviewers.
name = reviewer.group.name + ' (group)';
value = reviewer;
} else if (reviewer._account_id) {
// Reviewer is an account suggestion from getSuggestedAccounts.
const reviewerName = this._accountOrAnon(reviewer);
const reviewerEmail = this._reviewerEmail(reviewer.email);
const reviewerStatus = generateStatusStr(reviewer);
name = [reviewerName, reviewerEmail, reviewerStatus]
.filter(p => p.length > 0).join(' ');
value = {account: reviewer, count: 1};
}
return {name, value};
},
_getReviewerSuggestions(input) {
if (!this.change || !this.change._number || !this._loggedIn) {
return Promise.resolve([]);
}
const api = this.$.restAPI;
const xhr = this.allowAnyUser ?
api.getSuggestedAccounts(`cansee:${this.change._number} ${input}`) :
api.getChangeSuggestedReviewers(this.change._number, input);
return xhr.then(reviewers => {
if (!reviewers) { return []; }
if (!this.filter) {
return reviewers.map(this._makeSuggestion.bind(this));
}
return reviewers
.filter(this.filter)
.map(this._makeSuggestion.bind(this));
});
},
_reviewerEmail(email) {
if (typeof email !== 'undefined') {
return '<' + email + '>';
}
return '';
},
});
})();

View File

@@ -1,276 +0,0 @@
<!DOCTYPE html>
<!--
@license
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-account-entry</title>
<script src="/test/common-test-setup.js"></script>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<script src="../../../scripts/util.js"></script>
<link rel="import" href="gr-account-entry.html">
<script>void(0);</script>
<test-fixture id="basic">
<template>
<gr-account-entry></gr-account-entry>
</template>
</test-fixture>
<script>
suite('gr-account-entry tests', () => {
let sandbox;
let _nextAccountId = 0;
const makeAccount = function(opt_status) {
const accountId = ++_nextAccountId;
return {
_account_id: accountId,
name: 'name ' + accountId,
email: 'email ' + accountId,
status: opt_status,
};
};
let _nextAccountId2 = 0;
const makeAccount2 = function(opt_status) {
const accountId2 = ++_nextAccountId2;
return {
_account_id: accountId2,
email: 'email ' + accountId2,
status: opt_status,
};
};
let _nextAccountId3 = 0;
const makeAccount3 = function(opt_status) {
const accountId3 = ++_nextAccountId3;
return {
_account_id: accountId3,
name: 'name ' + accountId3,
status: opt_status,
};
};
let owner;
let existingReviewer1;
let existingReviewer2;
let suggestion1;
let suggestion2;
let suggestion3;
let element;
setup(done => {
owner = makeAccount();
existingReviewer1 = makeAccount();
existingReviewer2 = makeAccount();
suggestion1 = {account: makeAccount()};
suggestion2 = {account: makeAccount()};
suggestion3 = {
group: {
id: 'suggested group id',
name: 'suggested group',
},
};
stub('gr-rest-api-interface', {
getLoggedIn() { return Promise.resolve(true); },
});
element = fixture('basic');
element.change = {
_number: 42,
owner,
reviewers: {
CC: [existingReviewer1],
REVIEWER: [existingReviewer2],
},
};
sandbox = sinon.sandbox.create();
return flush(done);
});
teardown(() => {
sandbox.restore();
});
suite('stubbed values for _getReviewerSuggestions', () => {
setup(() => {
stub('gr-rest-api-interface', {
getChangeSuggestedReviewers() {
const redundantSuggestion1 = {account: existingReviewer1};
const redundantSuggestion2 = {account: existingReviewer2};
const redundantSuggestion3 = {account: owner};
return Promise.resolve([redundantSuggestion1, redundantSuggestion2,
redundantSuggestion3, suggestion1, suggestion2, suggestion3]);
},
});
});
test('_makeSuggestion formats account or group accordingly', () => {
let account = makeAccount();
const account2 = makeAccount2();
const account3 = makeAccount3();
let suggestion = element._makeSuggestion({account});
assert.deepEqual(suggestion, {
name: account.name + ' <' + account.email + '>',
value: {account},
});
const group = {name: 'test'};
suggestion = element._makeSuggestion({group});
assert.deepEqual(suggestion, {
name: group.name + ' (group)',
value: {group},
});
suggestion = element._makeSuggestion(account);
assert.deepEqual(suggestion, {
name: account.name + ' <' + account.email + '>',
value: {account, count: 1},
});
element._config = {
user: {
anonymous_coward_name: 'Anonymous Coward',
},
};
assert.deepEqual(element._accountOrAnon(account2), 'Anonymous');
account = makeAccount('OOO');
suggestion = element._makeSuggestion({account});
assert.deepEqual(suggestion, {
name: account.name + ' <' + account.email + '> (OOO)',
value: {account},
});
suggestion = element._makeSuggestion(account);
assert.deepEqual(suggestion, {
name: account.name + ' <' + account.email + '> (OOO)',
value: {account, count: 1},
});
sandbox.stub(element, '_reviewerEmail',
() => { return ''; });
suggestion = element._makeSuggestion(account3);
assert.deepEqual(suggestion, {
name: account3.name,
value: {account: account3, count: 1},
});
});
test('_reviewerEmail', () => {
assert.equal(
element._reviewerEmail('email@gerritreview.com'),
'<email@gerritreview.com>');
assert.equal(element._reviewerEmail(undefined), '');
});
test('_getReviewerSuggestions excludes owner+reviewers', done => {
element._getReviewerSuggestions().then(reviewers => {
// Default is no filtering.
assert.equal(reviewers.length, 6);
// Set up filter that only accepts suggestion1.
const accountId = suggestion1.account._account_id;
element.filter = function(suggestion) {
return suggestion.account &&
suggestion.account._account_id === accountId;
};
element._getReviewerSuggestions().then(reviewers => {
assert.deepEqual(reviewers, [element._makeSuggestion(suggestion1)]);
}).then(done);
});
});
test('_getReviewerSuggestions short circuits when logged out', () => {
// API call is already stubbed.
const xhrSpy = element.$.restAPI.getChangeSuggestedReviewers;
element._loggedIn = false;
return element._getReviewerSuggestions('').then(() => {
assert.isFalse(xhrSpy.called);
element._loggedIn = true;
return element._getReviewerSuggestions('').then(() => {
assert.isTrue(xhrSpy.called);
});
});
});
});
test('allowAnyUser', done => {
const suggestReviewerStub =
sandbox.stub(element.$.restAPI, 'getChangeSuggestedReviewers')
.returns(Promise.resolve([]));
const suggestAccountStub =
sandbox.stub(element.$.restAPI, 'getSuggestedAccounts')
.returns(Promise.resolve([]));
element._getReviewerSuggestions('').then(() => {
assert.isTrue(suggestReviewerStub.calledOnce);
assert.isTrue(suggestReviewerStub.calledWith(42, ''));
assert.isFalse(suggestAccountStub.called);
element.allowAnyUser = true;
element._getReviewerSuggestions('').then(() => {
assert.isTrue(suggestReviewerStub.calledOnce);
assert.isTrue(suggestAccountStub.calledOnce);
assert.isTrue(suggestAccountStub.calledWith('cansee:42 '));
done();
});
});
});
test('account-text-changed fired when input text changed and allowAnyInput',
() => {
// Spy on query, as that is called when _updateSuggestions proceeds.
const changeStub = sandbox.stub();
element.allowAnyInput = true;
sandbox.stub(element.$.restAPI, 'getChangeSuggestedReviewers')
.returns(Promise.resolve([]));
element.addEventListener('account-text-changed', changeStub);
element.$.input.text = 'a';
assert.isTrue(changeStub.calledOnce);
element.$.input.text = 'ab';
assert.isTrue(changeStub.calledTwice);
});
test('account-text-changed not fired when input text changed without ' +
'allowAnyUser', () => {
// Spy on query, as that is called when _updateSuggestions proceeds.
const changeStub = sandbox.stub();
sandbox.stub(element.$.restAPI, 'getChangeSuggestedReviewers')
.returns(Promise.resolve([]));
element.addEventListener('account-text-changed', changeStub);
element.$.input.text = 'a';
assert.isFalse(changeStub.called);
});
test('setText', () => {
// Spy on query, as that is called when _updateSuggestions proceeds.
const suggestSpy = sandbox.spy(element.$.input, 'query');
element.setText('test text');
flushAsynchronousOperations();
assert.equal(element.$.input.$.input.value, 'test text');
assert.isFalse(suggestSpy.called);
});
});
</script>

View File

@@ -36,6 +36,8 @@ limitations under the License.
<link rel="import" href="../gr-change-requirements/gr-change-requirements.html"> <link rel="import" href="../gr-change-requirements/gr-change-requirements.html">
<link rel="import" href="../gr-commit-info/gr-commit-info.html"> <link rel="import" href="../gr-commit-info/gr-commit-info.html">
<link rel="import" href="../gr-reviewer-list/gr-reviewer-list.html"> <link rel="import" href="../gr-reviewer-list/gr-reviewer-list.html">
<script src="../../../scripts/gr-display-name-utils/gr-display-name-utils.js"></script>
<script src="../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js"></script>
<dom-module id="gr-change-metadata"> <dom-module id="gr-change-metadata">
<template> <template>
@@ -172,9 +174,9 @@ limitations under the License.
id="assigneeValue" id="assigneeValue"
placeholder="Set assignee..." placeholder="Set assignee..."
accounts="{{_assignee}}" accounts="{{_assignee}}"
change="[[change]]"
readonly="[[_computeAssigneeReadOnly(_mutable, change)]]" readonly="[[_computeAssigneeReadOnly(_mutable, change)]]"
allow-any-user></gr-account-list> suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]">
</gr-account-list>
</span> </span>
</section> </section>
<section> <section>

View File

@@ -471,5 +471,12 @@
// dom-if. // dom-if.
this.$$('.topicEditableLabel').open(); this.$$('.topicEditableLabel').open();
}, },
_getReviewerSuggestionsProvider(change) {
const provider = new GrReviewerSuggestionsProvider(this.$.restAPI,
change._number, true);
provider.init();
return provider;
},
}); });
})(); })();

View File

@@ -32,9 +32,11 @@ limitations under the License.
<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> <link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../../shared/gr-storage/gr-storage.html"> <link rel="import" href="../../shared/gr-storage/gr-storage.html">
<link rel="import" href="../gr-account-list/gr-account-list.html"> <link rel="import" href="../../shared/gr-account-list/gr-account-list.html">
<link rel="import" href="../gr-label-scores/gr-label-scores.html"> <link rel="import" href="../gr-label-scores/gr-label-scores.html">
<link rel="import" href="../../../styles/shared-styles.html"> <link rel="import" href="../../../styles/shared-styles.html">
<script src="../../../scripts/gr-display-name-utils/gr-display-name-utils.js"></script>
<script src="../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js"></script>
<dom-module id="gr-reply-dialog"> <dom-module id="gr-reply-dialog">
<template> <template>
@@ -165,11 +167,11 @@ limitations under the License.
id="reviewers" id="reviewers"
accounts="{{_reviewers}}" accounts="{{_reviewers}}"
removable-values="[[change.removable_reviewers]]" removable-values="[[change.removable_reviewers]]"
change="[[change]]"
filter="[[filterReviewerSuggestion]]" filter="[[filterReviewerSuggestion]]"
pending-confirmation="{{_reviewerPendingConfirmation}}" pending-confirmation="{{_reviewerPendingConfirmation}}"
placeholder="Add reviewer..." placeholder="Add reviewer..."
on-account-text-changed="_handleAccountTextEntry"> on-account-text-changed="_handleAccountTextEntry"
suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]">
</gr-account-list> </gr-account-list>
</div> </div>
<div class="peopleList"> <div class="peopleList">
@@ -177,12 +179,12 @@ limitations under the License.
<gr-account-list <gr-account-list
id="ccs" id="ccs"
accounts="{{_ccs}}" accounts="{{_ccs}}"
change="[[change]]"
filter="[[filterCCSuggestion]]" filter="[[filterCCSuggestion]]"
pending-confirmation="{{_ccPendingConfirmation}}" pending-confirmation="{{_ccPendingConfirmation}}"
allow-any-input allow-any-input
placeholder="Add CC..." placeholder="Add CC..."
on-account-text-changed="_handleAccountTextEntry"> on-account-text-changed="_handleAccountTextEntry"
suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]">
</gr-account-list> </gr-account-list>
</div> </div>
<gr-overlay <gr-overlay

View File

@@ -895,5 +895,12 @@
_sendDisabledChanged(sendDisabled) { _sendDisabledChanged(sendDisabled) {
this.dispatchEvent(new CustomEvent('send-disabled-changed')); this.dispatchEvent(new CustomEvent('send-disabled-changed'));
}, },
_getReviewerSuggestionsProvider(change) {
const provider = new GrReviewerSuggestionsProvider(this.$.restAPI,
change._number, false);
provider.init();
return provider;
},
}); });
})(); })();

View File

@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<link rel="import" href="../../../behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html"> <link rel="import" href="../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.html">
<link rel="import" href="/bower_components/polymer/polymer.html"> <link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="../../shared/gr-button/gr-button.html"> <link rel="import" href="../../shared/gr-button/gr-button.html">
<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html"> <link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">

View File

@@ -58,7 +58,7 @@
}, },
behaviors: [ behaviors: [
Gerrit.AnonymousNameBehavior, Gerrit.DisplayNameBehavior,
], ],
detached() { detached() {

View File

@@ -16,7 +16,7 @@ limitations under the License.
--> -->
<link rel="import" href="/bower_components/polymer/polymer.html"> <link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html"> <link rel="import" href="../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.html">
<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../gr-search-bar/gr-search-bar.html"> <link rel="import" href="../gr-search-bar/gr-search-bar.html">

View File

@@ -49,7 +49,7 @@
}, },
behaviors: [ behaviors: [
Gerrit.AnonymousNameBehavior, Gerrit.DisplayNameBehavior,
], ],
attached() { attached() {

View File

@@ -15,12 +15,11 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<link rel="import" href="../../../behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html">
<link rel="import" href="/bower_components/polymer/polymer.html"> <link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> <link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
<link rel="import" href="../../../styles/shared-styles.html"> <link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html"> <link rel="import" href="../gr-autocomplete/gr-autocomplete.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> <link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
<dom-module id="gr-account-entry"> <dom-module id="gr-account-entry">
<template> <template>
@@ -36,14 +35,13 @@ limitations under the License.
borderless="[[borderless]]" borderless="[[borderless]]"
placeholder="[[placeholder]]" placeholder="[[placeholder]]"
threshold="[[suggestFrom]]" threshold="[[suggestFrom]]"
query="[[query]]" query="[[querySuggestions]]"
allow-non-suggested-values="[[allowAnyInput]]" allow-non-suggested-values="[[allowAnyInput]]"
on-commit="_handleInputCommit" on-commit="_handleInputCommit"
clear-on-commit clear-on-commit
warn-uncommitted warn-uncommitted
text="{{_inputText}}"> text="{{_inputText}}">
</gr-autocomplete> </gr-autocomplete>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template> </template>
<script src="gr-account-entry.js"></script> <script src="gr-account-entry.js"></script>
</dom-module> </dom-module>

View File

@@ -0,0 +1,102 @@
/**
* @license
* 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';
/**
* gr-account-entry is an element for entering account
* and/or group with autocomplete support.
*/
Polymer({
is: 'gr-account-entry',
_legacyUndefinedCheck: true,
/**
* Fired when an account is entered.
*
* @event add
*/
/**
* When allowAnyInput is true, account-text-changed is fired when input text
* changed. This is needed so that the reply dialog's save button can be
* enabled for arbitrary cc's, which don't need a 'commit'.
*
* @event account-text-changed
*/
properties: {
allowAnyInput: Boolean,
borderless: Boolean,
placeholder: String,
// suggestFrom = 0 to enable default suggestions.
suggestFrom: {
type: Number,
value: 0,
},
/** @type {!function(string): !Promise<Array<{name, value}>>} */
querySuggestions: {
type: Function,
notify: true,
value() {
return input => Promise.resolve([]);
},
},
_config: Object,
/** The value of the autocomplete entry. */
_inputText: {
type: String,
observer: '_inputTextChanged',
},
},
get focusStart() {
return this.$.input.focusStart;
},
focus() {
this.$.input.focus();
},
clear() {
this.$.input.clear();
},
setText(text) {
this.$.input.setText(text);
},
getText() {
return this.$.input.text;
},
_handleInputCommit(e) {
this.fire('add', {value: e.detail.value});
this.$.input.focus();
},
_inputTextChanged(text) {
if (text.length && this.allowAnyInput) {
this.dispatchEvent(new CustomEvent(
'account-text-changed', {bubbles: true, composed: true}));
}
},
});
})();

View File

@@ -0,0 +1,113 @@
<!DOCTYPE html>
<!--
@license
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-account-entry</title>
<script src="/test/common-test-setup.js"></script>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<script src="../../../scripts/util.js"></script>
<link rel="import" href="gr-account-entry.html">
<script>void(0);</script>
<test-fixture id="basic">
<template>
<gr-account-entry></gr-account-entry>
</template>
</test-fixture>
<script>
suite('gr-account-entry tests', () => {
let sandbox;
const suggestion1 = {
email: 'email1@example.com',
_account_id: 1,
some_property: 'value',
};
const suggestion2 = {
email: 'email2@example.com',
_account_id: 2,
};
const suggestion3 = {
email: 'email25@example.com',
_account_id: 25,
some_other_property: 'other value',
};
setup(done => {
element = fixture('basic');
sandbox = sinon.sandbox.create();
return flush(done);
});
teardown(() => {
sandbox.restore();
});
suite('stubbed values for querySuggestions', () => {
setup(() => {
element.querySuggestions = input => {
return Promise.resolve([
suggestion1,
suggestion2,
suggestion3,
]);
};
});
});
test('account-text-changed fired when input text changed and allowAnyInput',
() => {
// Spy on query, as that is called when _updateSuggestions proceeds.
const changeStub = sandbox.stub();
element.allowAnyInput = true;
element.querySuggestions = input => Promise.resolve([]);
element.addEventListener('account-text-changed', changeStub);
element.$.input.text = 'a';
assert.isTrue(changeStub.calledOnce);
element.$.input.text = 'ab';
assert.isTrue(changeStub.calledTwice);
});
test('account-text-changed not fired when input text changed without ' +
'allowAnyInput', () => {
// Spy on query, as that is called when _updateSuggestions proceeds.
const changeStub = sandbox.stub();
element.querySuggestions = input => Promise.resolve([]);
element.addEventListener('account-text-changed', changeStub);
element.$.input.text = 'a';
assert.isFalse(changeStub.called);
});
test('setText', () => {
// Spy on query, as that is called when _updateSuggestions proceeds.
const suggestSpy = sandbox.spy(element.$.input, 'query');
element.setText('test text');
flushAsynchronousOperations();
assert.equal(element.$.input.$.input.value, 'test text');
assert.isFalse(suggestSpy.called);
});
});
</script>

View File

@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<link rel="import" href="../../../behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html"> <link rel="import" href="../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.html">
<link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html"> <link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
<link rel="import" href="/bower_components/polymer/polymer.html"> <link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="../../../styles/shared-styles.html"> <link rel="import" href="../../../styles/shared-styles.html">

View File

@@ -52,7 +52,7 @@
}, },
behaviors: [ behaviors: [
Gerrit.AnonymousNameBehavior, Gerrit.DisplayNameBehavior,
Gerrit.TooltipBehavior, Gerrit.TooltipBehavior,
], ],

View File

@@ -17,7 +17,7 @@ limitations under the License.
<link rel="import" href="/bower_components/polymer/polymer.html"> <link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> <link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
<link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html"> <link rel="import" href="../gr-account-chip/gr-account-chip.html">
<link rel="import" href="../gr-account-entry/gr-account-entry.html"> <link rel="import" href="../gr-account-entry/gr-account-entry.html">
<link rel="import" href="../../../styles/shared-styles.html"> <link rel="import" href="../../../styles/shared-styles.html">
@@ -56,7 +56,7 @@ limitations under the License.
account="[[account]]" account="[[account]]"
class$="[[_computeChipClass(account)]]" class$="[[_computeChipClass(account)]]"
data-account-id$="[[account._account_id]]" data-account-id$="[[account._account_id]]"
removable="[[_computeRemovable(account)]]" removable="[[_computeRemovable(account, readonly)]]"
on-keydown="_handleChipKeydown" on-keydown="_handleChipKeydown"
tabindex="-1"> tabindex="-1">
</gr-account-chip> </gr-account-chip>
@@ -67,13 +67,13 @@ limitations under the License.
hidden$="[[_computeEntryHidden(maxCount, accounts.*, readonly)]]" hidden$="[[_computeEntryHidden(maxCount, accounts.*, readonly)]]"
id="entry" id="entry"
change="[[change]]" change="[[change]]"
filter="[[filter]]"
placeholder="[[placeholder]]" placeholder="[[placeholder]]"
on-add="_handleAdd" on-add="_handleAdd"
on-input-keydown="_handleInputKeydown" on-input-keydown="_handleInputKeydown"
allow-any-input="[[allowAnyInput]]" allow-any-input="[[allowAnyInput]]"
allow-any-user="[[allowAnyUser]]"> query-suggestions="[[_querySuggestions]]">
</gr-account-entry> </gr-account-entry>
<slot></slot>
</template> </template>
<script src="gr-account-list.js"></script> <script src="gr-account-list.js"></script>
</dom-module> </dom-module>

View File

@@ -19,6 +19,24 @@
const VALID_EMAIL_ALERT = 'Please input a valid email.'; const VALID_EMAIL_ALERT = 'Please input a valid email.';
const Defs = {};
/**
* @typedef {{
* name: string,
* value: Object,
* }}
*/
Defs.GrSuggestionItem;
/**
* @typedef {{
* getSuggestions: function(string): Promise<Array<Object>>,
* makeSuggestionItem: function(Object): Defs.GrSuggestionItem,
* }}
*/
Defs.GrSuggestionsProvider;
Polymer({ Polymer({
is: 'gr-account-list', is: 'gr-account-list',
_legacyUndefinedCheck: true, _legacyUndefinedCheck: true,
@@ -38,6 +56,19 @@
change: Object, change: Object,
filter: Function, filter: Function,
placeholder: String, placeholder: String,
disabled: {
type: Function,
value: false,
},
/**
* Returns suggestions and convert them to list item
* @type {Defs.GrSuggestionsProvider}
*/
suggestionsProvider: {
type: Object,
},
/** /**
* Needed for template checking since value is initially set to null. * Needed for template checking since value is initially set to null.
* @type {?Object} */ * @type {?Object} */
@@ -50,21 +81,6 @@
type: Boolean, type: Boolean,
value: false, value: false,
}, },
/**
* When true, the account-entry autocomplete uses the account suggest API
* endpoint, which suggests any account in that Gerrit instance (and does
* not suggest groups).
*
* When false/undefined, account-entry uses the suggest_reviewers API
* endpoint, which suggests any account or group in that Gerrit instance
* that is not already a reviewer (or is not CCed) on that change.
*/
allowAnyUser: {
type: Boolean,
value: false,
},
/** /**
* When true, allows for non-suggested inputs to be added. * When true, allows for non-suggested inputs to be added.
*/ */
@@ -82,6 +98,16 @@
type: Number, type: Number,
value: 0, value: 0,
}, },
/** Returns suggestion items
* @type {!function(string): Promise<Array<Defs.GrSuggestionItem>>}
*/
_querySuggestions: {
type: Function,
value() {
return this._getSuggestions.bind(this);
},
},
}, },
behaviors: [ behaviors: [
@@ -103,31 +129,46 @@
return this.$.entry.focusStart; return this.$.entry.focusStart;
}, },
_handleAdd(e) { _getSuggestions(input) {
this._addReviewer(e.detail.value); const provider = this.suggestionsProvider;
if (!provider) {
return Promise.resolve([]);
}
return provider.getSuggestions(input).then(suggestions => {
if (!suggestions) { return []; }
if (this.filter) {
suggestions = suggestions.filter(this.filter);
}
return suggestions.map(suggestion =>
provider.makeSuggestionItem(suggestion));
});
}, },
_addReviewer(reviewer) { _handleAdd(e) {
this._addAccountItem(e.detail.value);
},
_addAccountItem(item) {
// Append new account or group to the accounts property. We add our own // Append new account or group to the accounts property. We add our own
// internal properties to the account/group here, so we clone the object // internal properties to the account/group here, so we clone the object
// to avoid cluttering up the shared change object. // to avoid cluttering up the shared change object.
if (reviewer.account) { if (item.account) {
const account = const account =
Object.assign({}, reviewer.account, {_pendingAdd: true}); Object.assign({}, item.account, {_pendingAdd: true});
this.push('accounts', account); this.push('accounts', account);
} else if (reviewer.group) { } else if (item.group) {
if (reviewer.confirm) { if (item.confirm) {
this.pendingConfirmation = reviewer; this.pendingConfirmation = item;
return; return;
} }
const group = Object.assign({}, reviewer.group, const group = Object.assign({}, item.group,
{_pendingAdd: true, _group: true}); {_pendingAdd: true, _group: true});
this.push('accounts', group); this.push('accounts', group);
} else if (this.allowAnyInput) { } else if (this.allowAnyInput) {
if (!reviewer.includes('@')) { if (!item.includes('@')) {
// Repopulate the input with what the user tried to enter and have // Repopulate the input with what the user tried to enter and have
// a toast tell them why they can't enter it. // a toast tell them why they can't enter it.
this.$.entry.setText(reviewer); this.$.entry.setText(item);
this.dispatchEvent(new CustomEvent('show-alert', { this.dispatchEvent(new CustomEvent('show-alert', {
detail: {message: VALID_EMAIL_ALERT}, detail: {message: VALID_EMAIL_ALERT},
bubbles: true, bubbles: true,
@@ -135,7 +176,7 @@
})); }));
return false; return false;
} else { } else {
const account = {email: reviewer, _pendingAdd: true}; const account = {email: item, _pendingAdd: true};
this.push('accounts', account); this.push('accounts', account);
} }
} }
@@ -173,8 +214,8 @@
return a === b; return a === b;
}, },
_computeRemovable(account) { _computeRemovable(account, readonly) {
if (this.readonly) { return false; } if (readonly) { return false; }
if (this.removableValues) { if (this.removableValues) {
for (let i = 0; i < this.removableValues.length; i++) { for (let i = 0; i < this.removableValues.length; i++) {
if (this._accountMatches(this.removableValues[i], account)) { if (this._accountMatches(this.removableValues[i], account)) {
@@ -193,7 +234,9 @@
}, },
_removeAccount(toRemove) { _removeAccount(toRemove) {
if (!toRemove || !this._computeRemovable(toRemove)) { return; } if (!toRemove || !this._computeRemovable(toRemove, this.readonly)) {
return;
}
for (let i = 0; i < this.accounts.length; i++) { for (let i = 0; i < this.accounts.length; i++) {
let matches; let matches;
const account = this.accounts[i]; const account = this.accounts[i];
@@ -277,7 +320,7 @@
submitEntryText() { submitEntryText() {
const text = this.$.entry.getText(); const text = this.$.entry.getText();
if (!text.length) { return true; } if (!text.length) { return true; }
const wasSubmitted = this._addReviewer(text); const wasSubmitted = this._addAccountItem(text);
if (wasSubmitted) { this.$.entry.clear(); } if (wasSubmitted) { this.$.entry.clear(); }
return wasSubmitted; return wasSubmitted;
}, },

View File

@@ -35,6 +35,15 @@ limitations under the License.
</test-fixture> </test-fixture>
<script> <script>
class MockSuggestionsProvider {
getSuggestions(input) {
return Promise.resolve([]);
}
makeSuggestionItem(item) {
return item;
}
}
suite('gr-account-list tests', () => { suite('gr-account-list tests', () => {
let _nextAccountId = 0; let _nextAccountId = 0;
const makeAccount = function() { const makeAccount = function() {
@@ -51,10 +60,11 @@ limitations under the License.
}; };
}; };
let existingReviewer1; let existingAccount1;
let existingReviewer2; let existingAccount2;
let sandbox; let sandbox;
let element; let element;
let suggestionsProvider;
function getChips() { function getChips() {
return Polymer.dom(element.root).querySelectorAll('gr-account-chip'); return Polymer.dom(element.root).querySelectorAll('gr-account-chip');
@@ -62,14 +72,16 @@ limitations under the License.
setup(() => { setup(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.sandbox.create();
existingReviewer1 = makeAccount(); existingAccount1 = makeAccount();
existingReviewer2 = makeAccount(); existingAccount2 = makeAccount();
stub('gr-rest-api-interface', { stub('gr-rest-api-interface', {
getConfig() { return Promise.resolve({}); }, getConfig() { return Promise.resolve({}); },
}); });
element = fixture('basic'); element = fixture('basic');
element.accounts = [existingReviewer1, existingReviewer2]; element.accounts = [existingAccount1, existingAccount2];
suggestionsProvider = new MockSuggestionsProvider();
element.suggestionsProvider = suggestionsProvider;
}); });
teardown(() => { teardown(() => {
@@ -109,7 +121,7 @@ limitations under the License.
assert.isTrue(chips[2].classList.contains('pendingAdd')); assert.isTrue(chips[2].classList.contains('pendingAdd'));
// Removed accounts are taken out of the list. // Removed accounts are taken out of the list.
element.fire('remove', {account: existingReviewer1}); element.fire('remove', {account: existingAccount1});
flushAsynchronousOperations(); flushAsynchronousOperations();
chips = getChips(); chips = getChips();
assert.equal(chips.length, 2); assert.equal(chips.length, 2);
@@ -117,7 +129,7 @@ limitations under the License.
assert.isTrue(chips[1].classList.contains('pendingAdd')); assert.isTrue(chips[1].classList.contains('pendingAdd'));
// Invalid remove is ignored. // Invalid remove is ignored.
element.fire('remove', {account: existingReviewer1}); element.fire('remove', {account: existingAccount1});
element.fire('remove', {account: newAccount}); element.fire('remove', {account: newAccount});
flushAsynchronousOperations(); flushAsynchronousOperations();
chips = getChips(); chips = getChips();
@@ -147,6 +159,52 @@ limitations under the License.
assert.isFalse(chips[0].classList.contains('pendingAdd')); assert.isFalse(chips[0].classList.contains('pendingAdd'));
}); });
test('_getSuggestions uses filter correctly', done => {
const originalSuggestions = [
{
email: 'abc@example.com',
text: 'abcd',
_account_id: 3,
},
{
email: 'qwe@example.com',
text: 'qwer',
_account_id: 1,
},
{
email: 'xyz@example.com',
text: 'aaaaa',
_account_id: 25,
},
];
sandbox.stub(suggestionsProvider, 'getSuggestions')
.returns(Promise.resolve(originalSuggestions));
sandbox.stub(suggestionsProvider, 'makeSuggestionItem', suggestion => {
return {
name: suggestion.email,
value: suggestion._account_id,
};
});
element._getSuggestions().then(suggestions => {
// Default is no filtering.
assert.equal(suggestions.length, 3);
// Set up filter that only accepts suggestion1.
const accountId = originalSuggestions[0]._account_id;
element.filter = function(suggestion) {
return suggestion._account_id === accountId;
};
element._getSuggestions().then(suggestions => {
assert.deepEqual(suggestions,
[{name: originalSuggestions[0].email,
value: originalSuggestions[0]._account_id}]);
}).then(done);
});
});
test('_computeChipClass', () => { test('_computeChipClass', () => {
const account = makeAccount(); const account = makeAccount();
assert.equal(element._computeChipClass(account), ''); assert.equal(element._computeChipClass(account), '');
@@ -163,18 +221,18 @@ limitations under the License.
newAccount._pendingAdd = true; newAccount._pendingAdd = true;
element.readonly = false; element.readonly = false;
element.removableValues = []; element.removableValues = [];
assert.isFalse(element._computeRemovable(existingReviewer1)); assert.isFalse(element._computeRemovable(existingAccount1, false));
assert.isTrue(element._computeRemovable(newAccount)); assert.isTrue(element._computeRemovable(newAccount, false));
element.removableValues = [existingReviewer1]; element.removableValues = [existingAccount1];
assert.isTrue(element._computeRemovable(existingReviewer1)); assert.isTrue(element._computeRemovable(existingAccount1, false));
assert.isTrue(element._computeRemovable(newAccount)); assert.isTrue(element._computeRemovable(newAccount, false));
assert.isFalse(element._computeRemovable(existingReviewer2)); assert.isFalse(element._computeRemovable(existingAccount2, false));
element.readonly = true; element.readonly = true;
assert.isFalse(element._computeRemovable(existingReviewer1)); assert.isFalse(element._computeRemovable(existingAccount1, true));
assert.isFalse(element._computeRemovable(newAccount)); assert.isFalse(element._computeRemovable(newAccount, true));
}); });
test('submitEntryText', () => { test('submitEntryText', () => {
@@ -293,13 +351,40 @@ limitations under the License.
assert.isTrue(element.$.entry.hasAttribute('hidden')); assert.isTrue(element.$.entry.hasAttribute('hidden'));
}); });
suite('allowAnyInput', () => { test('enter text calls suggestions provider', done => {
let entry; const suggestions = [
{
email: 'abc@example.com',
text: 'abcd',
},
{
email: 'qwe@example.com',
text: 'qwer',
},
];
const getSuggestionsStub =
sandbox.stub(suggestionsProvider, 'getSuggestions')
.returns(Promise.resolve(suggestions));
const makeSuggestionItemStub =
sandbox.stub(suggestionsProvider, 'makeSuggestionItem', item => item);
const input = element.$.entry.$.input;
input.text = 'newTest';
MockInteractions.focus(input.$.input);
input.noDebounce = true;
flushAsynchronousOperations();
flush(() => {
assert.isTrue(getSuggestionsStub.calledOnce);
assert.equal(getSuggestionsStub.lastCall.args[0], 'newTest');
assert.equal(makeSuggestionItemStub.getCalls().length, 2);
done();
});
});
suite('allowAnyInput', () => {
setup(() => { setup(() => {
entry = element.$.entry;
sandbox.stub(entry, '_getReviewerSuggestions');
sandbox.stub(entry.$.input, '_updateSuggestions');
element.allowAnyInput = true; element.allowAnyInput = true;
}); });
@@ -334,7 +419,6 @@ limitations under the License.
suite('keyboard interactions', () => { suite('keyboard interactions', () => {
test('backspace at text input start removes last account', () => { test('backspace at text input start removes last account', () => {
const input = element.$.entry.$.input; const input = element.$.entry.$.input;
sandbox.stub(element.$.entry, '_getReviewerSuggestions');
sandbox.stub(input, '_updateSuggestions'); sandbox.stub(input, '_updateSuggestions');
sandbox.stub(element, '_computeRemovable').returns(true); sandbox.stub(element, '_computeRemovable').returns(true);
// Next line is a workaround for Firefix not moving cursor // Next line is a workaround for Firefix not moving cursor

View File

@@ -0,0 +1,55 @@
(function(window) {
'use strict';
if (window.GrDisplayNameUtils) {
return;
}
const ANONYMOUS_NAME = 'Anonymous';
class GrDisplayNameUtils {
/**
* enableEmail when true enables to fallback to using email if
* the account name is not avilable.
*/
static getUserName(config, account, enableEmail) {
if (account && account.name) {
return account.name;
} else if (account && account.username) {
return account.username;
} else if (enableEmail && account && account.email) {
return account.email;
} else if (config && config.user &&
config.user.anonymous_coward_name !== 'Anonymous Coward') {
return config.user.anonymous_coward_name;
}
return ANONYMOUS_NAME;
}
static getAccountDisplayName(config, account, enableEmail) {
const reviewerName = this._accountOrAnon(config, account, enableEmail);
const reviewerEmail = this._accountEmail(account.email);
const reviewerStatus = account.status ? '(' + account.status + ')' : '';
return [reviewerName, reviewerEmail, reviewerStatus]
.filter(p => p.length > 0).join(' ');
}
static _accountOrAnon(config, reviewer, enableEmail) {
return this.getUserName(config, reviewer, !!enableEmail);
}
static _accountEmail(email) {
if (typeof email !== 'undefined') {
return '<' + email + '>';
}
return '';
}
static getGroupDisplayName(group) {
return group.name + ' (group)';
}
}
window.GrDisplayNameUtils = GrDisplayNameUtils;
})(window);

View File

@@ -0,0 +1,140 @@
<!DOCTYPE html>
<!--
@license
Copyright (C) 2019 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-display-name-utils</title>
<script src="/test/common-test-setup.js"></script>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../test/common-test-setup.html"/>
<script src="gr-display-name-utils.js"></script>
<script>
suite('gr-display-name-utils tests', () => {
// eslint-disable-next-line no-unused-vars
const config = {
user: {
anonymous_coward_name: 'Anonymous Coward',
},
};
test('getUserName name only', () => {
const account = {
name: 'test-name',
};
assert.deepEqual(GrDisplayNameUtils.getUserName(config, account, true),
'test-name');
});
test('getUserName username only', () => {
const account = {
username: 'test-user',
};
assert.deepEqual(GrDisplayNameUtils.getUserName(config, account, true),
'test-user');
});
test('getUserName email only', () => {
const account = {
email: 'test-user@test-url.com',
};
assert.deepEqual(GrDisplayNameUtils.getUserName(config, account, true),
'test-user@test-url.com');
});
test('getUserName returns not Anonymous Coward as the anon name', () => {
assert.deepEqual(GrDisplayNameUtils.getUserName(config, null, true),
'Anonymous');
});
test('getUserName for the config returning the anon name', () => {
const config = {
user: {
anonymous_coward_name: 'Test Anon',
},
};
assert.deepEqual(GrDisplayNameUtils.getUserName(config, null, true),
'Test Anon');
});
test('getAccountDisplayName - account with name only', () => {
assert.equal(
GrDisplayNameUtils.getAccountDisplayName(config,
{name: 'Some user name'}),
'Some user name');
});
test('getAccountDisplayName - account with email only', () => {
assert.equal(
GrDisplayNameUtils.getAccountDisplayName(config,
{email: 'my@example.com'}),
'Anonymous <my@example.com>');
});
test('getAccountDisplayName - account with email only - allowEmail', () => {
assert.equal(
GrDisplayNameUtils.getAccountDisplayName(config,
{email: 'my@example.com'}, true),
'my@example.com <my@example.com>');
});
test('getAccountDisplayName - account with name and status', () => {
assert.equal(
GrDisplayNameUtils.getAccountDisplayName(config, {
name: 'Some name',
status: 'OOO',
}),
'Some name (OOO)');
});
test('getAccountDisplayName - account with name and email', () => {
assert.equal(
GrDisplayNameUtils.getAccountDisplayName(config, {
name: 'Some name',
email: 'my@example.com',
}),
'Some name <my@example.com>');
});
test('getAccountDisplayName - account with name, email and status', () => {
assert.equal(
GrDisplayNameUtils.getAccountDisplayName(config, {
name: 'Some name',
email: 'my@example.com',
status: 'OOO',
}),
'Some name <my@example.com> (OOO)');
});
test('getGroupDisplayName', () => {
assert.equal(
GrDisplayNameUtils.getGroupDisplayName({name: 'Some user name'}),
'Some user name (group)');
});
test('_accountEmail', () => {
assert.equal(
GrDisplayNameUtils._accountEmail('email@gerritreview.com'),
'<email@gerritreview.com>');
assert.equal(GrDisplayNameUtils._accountEmail(undefined), '');
});
});
</script>

View File

@@ -0,0 +1,46 @@
/**
* @license
* Copyright (C) 2019 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(window) {
'use strict';
if (window.GrEmailSuggestionsProvider) {
return;
}
class GrEmailSuggestionsProvider {
constructor(restAPI) {
this._restAPI = restAPI;
}
getSuggestions(input) {
return this._restAPI.getSuggestedAccounts(`${input}`)
.then(accounts => {
if (!accounts) { return []; }
return accounts;
});
}
makeSuggestionItem(account) {
return {
name: GrDisplayNameUtils.getAccountDisplayName(null, account, true),
value: {account, count: 1},
};
}
}
window.GrEmailSuggestionsProvider = GrEmailSuggestionsProvider;
})(window);

View File

@@ -0,0 +1,99 @@
<!DOCTYPE html>
<!--
@license
Copyright (C) 2019 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-email-suggestions-provider</title>
<script src="/test/common-test-setup.js"></script>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<link rel="import" href="../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.html"/>
<script src="../gr-display-name-utils/gr-display-name-utils.js"></script>
<script src="gr-email-suggestions-provider.js"></script>
<script>void(0);</script>
<test-fixture id="basic">
<template>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
</test-fixture>
<script>
suite('GrEmailSuggestionsProvider tests', () => {
let sandbox;
let restAPI;
let provider;
const account1 = {
name: 'Some name',
email: 'some@example.com',
};
const account2 = {
email: 'other@example.com',
_account_id: 3,
};
setup(() => {
sandbox = sinon.sandbox.create();
stub('gr-rest-api-interface', {
getConfig() { return Promise.resolve({}); },
});
restAPI = fixture('basic');
provider = new GrEmailSuggestionsProvider(restAPI);
});
teardown(() => {
sandbox.restore();
});
test('getSuggestions', done => {
const getSuggestedAccountsStub =
sandbox.stub(restAPI, 'getSuggestedAccounts')
.returns(Promise.resolve([account1, account2]));
provider.getSuggestions('Some input').then(res => {
assert.deepEqual(res, [account1, account2]);
assert.isTrue(getSuggestedAccountsStub.calledOnce);
assert.equal(getSuggestedAccountsStub.lastCall.args[0], 'Some input');
done();
});
});
test('makeSuggestionItem', () => {
assert.deepEqual(provider.makeSuggestionItem(account1), {
name: 'Some name <some@example.com>',
value: {
account: account1,
count: 1,
},
});
assert.deepEqual(provider.makeSuggestionItem(account2), {
name: 'other@example.com <other@example.com>',
value: {
account: account2,
count: 1,
},
});
});
});
</script>

View File

@@ -0,0 +1,47 @@
/**
* @license
* Copyright (C) 2019 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(window) {
'use strict';
if (window.GrGroupSuggestionsProvider) {
return;
}
class GrGroupSuggestionsProvider {
constructor(restAPI) {
this._restAPI = restAPI;
}
getSuggestions(input) {
return this._restAPI.getSuggestedGroups(`${input}`)
.then(groups => {
if (!groups) { return []; }
const keys = Object.keys(groups);
return keys.map(key => {
return Object.assign({}, groups[key], {name: key});
});
});
}
makeSuggestionItem(suggestion) {
return {name: suggestion.name,
value: {group: {name: suggestion.name, id: suggestion.id}}};
}
}
window.GrGroupSuggestionsProvider = GrGroupSuggestionsProvider;
})(window);

View File

@@ -0,0 +1,106 @@
<!DOCTYPE html>
<!--
@license
Copyright (C) 2019 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-group-suggestions-provider</title>
<script src="/test/common-test-setup.js"></script>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<link rel="import" href="../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.html"/>
<script src="../gr-display-name-utils/gr-display-name-utils.js"></script>
<script src="gr-group-suggestions-provider.js"></script>
<script>void(0);</script>
<test-fixture id="basic">
<template>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
</test-fixture>
<script>
suite('GrGroupSuggestionsProvider tests', () => {
let sandbox;
let restAPI;
let provider;
const group1 = {
name: 'Some name',
id: 1,
};
const group2 = {
name: 'Other name',
id: 3,
url: 'abcd',
};
setup(() => {
sandbox = sinon.sandbox.create();
stub('gr-rest-api-interface', {
getConfig() { return Promise.resolve({}); },
});
restAPI = fixture('basic');
provider = new GrGroupSuggestionsProvider(restAPI);
});
teardown(() => {
sandbox.restore();
});
test('getSuggestions', done => {
const getSuggestedAccountsStub =
sandbox.stub(restAPI, 'getSuggestedGroups')
.returns(Promise.resolve({
'Some name': {id: 1},
'Other name': {id: 3, url: 'abcd'},
}));
provider.getSuggestions('Some input').then(res => {
assert.deepEqual(res, [group1, group2]);
assert.isTrue(getSuggestedAccountsStub.calledOnce);
assert.equal(getSuggestedAccountsStub.lastCall.args[0], 'Some input');
done();
});
});
test('makeSuggestionItem', () => {
assert.deepEqual(provider.makeSuggestionItem(group1), {
name: 'Some name',
value: {
group: {
name: 'Some name',
id: 1,
},
},
});
assert.deepEqual(provider.makeSuggestionItem(group2), {
name: 'Other name',
value: {
group: {
name: 'Other name',
id: 3,
},
},
});
});
});
</script>

View File

@@ -0,0 +1,90 @@
/**
* @license
* Copyright (C) 2019 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(window) {
'use strict';
if (window.GrReviewerSuggestionsProvider) {
return;
}
class GrReviewerSuggestionsProvider {
constructor(restAPI, changeNumber, allowAnyUser) {
this._changeNumber = changeNumber;
this._allowAnyUser = allowAnyUser;
this._restAPI = restAPI;
}
init() {
if (this._initPromise) {
return this._initPromise;
}
const getConfigPromise = this._restAPI.getConfig().then(cfg => {
this._config = cfg;
});
const getLoggedInPromise = this._restAPI.getLoggedIn().then(loggedIn => {
this._loggedIn = loggedIn;
});
this._initPromise = Promise.all([getConfigPromise, getLoggedInPromise])
.then(() => {
this._initialized = true;
});
return this._initPromise;
}
getSuggestions(input) {
if (!this._initialized || !this._loggedIn) {
return Promise.resolve([]);
}
const api = this._restAPI;
const xhr = this._allowAnyUser ?
api.getSuggestedAccounts(`cansee:${this._changeNumber} ${input}`) :
api.getChangeSuggestedReviewers(this._changeNumber, input);
return xhr.then(reviewers => (reviewers || []));
}
makeSuggestionItem(suggestion) {
if (suggestion.account) {
// Reviewer is an account suggestion from getChangeSuggestedReviewers.
return {
name: GrDisplayNameUtils.getAccountDisplayName(this._config,
suggestion.account, false),
value: suggestion,
};
}
if (suggestion.group) {
// Reviewer is a group suggestion from getChangeSuggestedReviewers.
return {
name: GrDisplayNameUtils.getGroupDisplayName(suggestion.group),
value: suggestion,
};
}
if (suggestion._account_id) {
// Reviewer is an account suggestion from getSuggestedAccounts.
return {
name: GrDisplayNameUtils.getAccountDisplayName(this._config,
suggestion, false),
value: {account: suggestion, count: 1},
};
}
}
}
window.GrReviewerSuggestionsProvider = GrReviewerSuggestionsProvider;
})(window);

View File

@@ -0,0 +1,260 @@
<!DOCTYPE html>
<!--
@license
Copyright (C) 2019 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-reviewer-suggestions-provider</title>
<script src="/test/common-test-setup.js"></script>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<link rel="import" href="../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.html"/>
<script src="../gr-display-name-utils/gr-display-name-utils.js"></script>
<script src="gr-reviewer-suggestions-provider.js"></script>
<script>void(0);</script>
<test-fixture id="basic">
<template>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
</test-fixture>
<script>
suite('GrReviewerSuggestionsProvider tests', () => {
let sandbox;
let _nextAccountId = 0;
const makeAccount = function(opt_status) {
const accountId = ++_nextAccountId;
return {
_account_id: accountId,
name: 'name ' + accountId,
email: 'email ' + accountId,
status: opt_status,
};
};
let _nextAccountId2 = 0;
const makeAccount2 = function(opt_status) {
const accountId2 = ++_nextAccountId2;
return {
_account_id: accountId2,
name: 'name ' + accountId2,
status: opt_status,
};
};
let owner;
let existingReviewer1;
let existingReviewer2;
let suggestion1;
let suggestion2;
let suggestion3;
let restAPI;
let provider;
let redundantSuggestion1;
let redundantSuggestion2;
let redundantSuggestion3;
let change;
setup(done => {
owner = makeAccount();
existingReviewer1 = makeAccount();
existingReviewer2 = makeAccount();
suggestion1 = {account: makeAccount()};
suggestion2 = {account: makeAccount()};
suggestion3 = {
group: {
id: 'suggested group id',
name: 'suggested group',
},
};
stub('gr-rest-api-interface', {
getLoggedIn() { return Promise.resolve(true); },
getConfig() { return Promise.resolve({}); },
});
restAPI = fixture('basic');
change = {
_number: 42,
owner,
reviewers: {
CC: [existingReviewer1],
REVIEWER: [existingReviewer2],
},
};
sandbox = sinon.sandbox.create();
return flush(done);
});
teardown(() => {
sandbox.restore();
});
suite('allowAnyUser set to false', () => {
setup(done => {
provider = new GrReviewerSuggestionsProvider(restAPI, change._number,
false);
provider.init().then(done);
});
suite('stubbed values for _getReviewerSuggestions', () => {
setup(() => {
stub('gr-rest-api-interface', {
getChangeSuggestedReviewers() {
redundantSuggestion1 = {account: existingReviewer1};
redundantSuggestion2 = {account: existingReviewer2};
redundantSuggestion3 = {account: owner};
return Promise.resolve([redundantSuggestion1, redundantSuggestion2,
redundantSuggestion3, suggestion1, suggestion2, suggestion3]);
},
});
});
test('makeSuggestionItem formats account or group accordingly', () => {
let account = makeAccount();
const account3 = makeAccount2();
let suggestion = provider.makeSuggestionItem({account});
assert.deepEqual(suggestion, {
name: account.name + ' <' + account.email + '>',
value: {account},
});
const group = {name: 'test'};
suggestion = provider.makeSuggestionItem({group});
assert.deepEqual(suggestion, {
name: group.name + ' (group)',
value: {group},
});
suggestion = provider.makeSuggestionItem(account);
assert.deepEqual(suggestion, {
name: account.name + ' <' + account.email + '>',
value: {account, count: 1},
});
suggestion = provider.makeSuggestionItem({account: {}});
assert.deepEqual(suggestion, {
name: 'Anonymous',
value: {account: {}},
});
provider._config = {
user: {
anonymous_coward_name: 'Anonymous Coward Name',
},
};
suggestion = provider.makeSuggestionItem({account: {}});
assert.deepEqual(suggestion, {
name: 'Anonymous Coward Name',
value: {account: {}},
});
account = makeAccount('OOO');
suggestion = provider.makeSuggestionItem({account});
assert.deepEqual(suggestion, {
name: account.name + ' <' + account.email + '> (OOO)',
value: {account},
});
suggestion = provider.makeSuggestionItem(account);
assert.deepEqual(suggestion, {
name: account.name + ' <' + account.email + '> (OOO)',
value: {account, count: 1},
});
sandbox.stub(GrDisplayNameUtils, '_accountEmail',
() => {
return '';
});
suggestion = provider.makeSuggestionItem(account3);
assert.deepEqual(suggestion, {
name: account3.name,
value: {account: account3, count: 1},
});
});
test('getSuggestions', done => {
provider.getSuggestions().then(reviewers => {
// Default is no filtering.
assert.equal(reviewers.length, 6);
assert.deepEqual(reviewers,
[redundantSuggestion1, redundantSuggestion2,
redundantSuggestion3, suggestion1, suggestion2, suggestion3]);
}).then(done);
});
test('getSuggestions short circuits when logged out', () => {
// API call is already stubbed.
const xhrSpy = restAPI.getChangeSuggestedReviewers;
provider._loggedIn = false;
return provider.getSuggestions('').then(() => {
assert.isFalse(xhrSpy.called);
provider._loggedIn = true;
return provider.getSuggestions('').then(() => {
assert.isTrue(xhrSpy.called);
});
});
});
});
test('getChangeSuggestedReviewers is used', done => {
const suggestReviewerStub =
sandbox.stub(restAPI, 'getChangeSuggestedReviewers')
.returns(Promise.resolve([]));
const suggestAccountStub =
sandbox.stub(restAPI, 'getSuggestedAccounts')
.returns(Promise.resolve([]));
provider.getSuggestions('').then(() => {
assert.isTrue(suggestReviewerStub.calledOnce);
assert.isTrue(suggestReviewerStub.calledWith(42, ''));
assert.isFalse(suggestAccountStub.called);
done();
});
});
});
suite('allowAnyUser set to true', () => {
setup(done => {
provider = new GrReviewerSuggestionsProvider(restAPI, change._number,
true);
provider.init().then(done);
});
test('getSuggestedAccounts is used', done => {
const suggestReviewerStub =
sandbox.stub(restAPI, 'getChangeSuggestedReviewers')
.returns(Promise.resolve([]));
const suggestAccountStub =
sandbox.stub(restAPI, 'getSuggestedAccounts')
.returns(Promise.resolve([]));
provider.getSuggestions('').then(() => {
assert.isFalse(suggestReviewerStub.called);
assert.isTrue(suggestAccountStub.calledOnce);
assert.isTrue(suggestAccountStub.calledWith('cansee:42 '));
done();
});
});
});
});
</script>

View File

@@ -38,6 +38,8 @@ const EXTERN_NAMES = [
'SiteBasedCache', 'SiteBasedCache',
'FetchPromisesCache', 'FetchPromisesCache',
'GrRestApiHelper', 'GrRestApiHelper',
'GrDisplayNameUtils',
'GrReviewerSuggestionsProvider',
'moment', 'moment',
'page', 'page',
'util', 'util',

View File

@@ -23,6 +23,7 @@ limitations under the License.
<script src="/bower_components/web-component-tester/browser.js"></script> <script src="/bower_components/web-component-tester/browser.js"></script>
<script> <script>
const testFiles = []; const testFiles = [];
const scriptsPath = '../scripts/';
const elementsPath = '../elements/'; const elementsPath = '../elements/';
const behaviorsPath = '../behaviors/'; const behaviorsPath = '../behaviors/';
@@ -61,9 +62,9 @@ limitations under the License.
'change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html', 'change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html',
'change-list/gr-create-change-help/gr-create-change-help_test.html', 'change-list/gr-create-change-help/gr-create-change-help_test.html',
'change-list/gr-dashboard-view/gr-dashboard-view_test.html', 'change-list/gr-dashboard-view/gr-dashboard-view_test.html',
// TODO: uncomment file & fix tests. The file was missed in this list for a long time.
// 'change-list/gr-repo-header/gr-repo-header_test.html',
'change-list/gr-user-header/gr-user-header_test.html', 'change-list/gr-user-header/gr-user-header_test.html',
'change/gr-account-entry/gr-account-entry_test.html',
'change/gr-account-list/gr-account-list_test.html',
'change/gr-change-actions/gr-change-actions_test.html', 'change/gr-change-actions/gr-change-actions_test.html',
'change/gr-change-metadata/gr-change-metadata-it_test.html', 'change/gr-change-metadata/gr-change-metadata-it_test.html',
'change/gr-change-metadata/gr-change-metadata_test.html', 'change/gr-change-metadata/gr-change-metadata_test.html',
@@ -105,10 +106,13 @@ limitations under the License.
'core/gr-search-bar/gr-search-bar_test.html', 'core/gr-search-bar/gr-search-bar_test.html',
'core/gr-smart-search/gr-smart-search_test.html', 'core/gr-smart-search/gr-smart-search_test.html',
'diff/gr-comment-api/gr-comment-api_test.html', 'diff/gr-comment-api/gr-comment-api_test.html',
'diff/gr-coverage-layer/gr-coverage-layer_test.html',
'diff/gr-diff-builder/gr-diff-builder_test.html', 'diff/gr-diff-builder/gr-diff-builder_test.html',
'diff/gr-diff-cursor/gr-diff-cursor_test.html', 'diff/gr-diff-cursor/gr-diff-cursor_test.html',
'diff/gr-diff-highlight/gr-annotation_test.html', 'diff/gr-diff-highlight/gr-annotation_test.html',
'diff/gr-diff-highlight/gr-diff-highlight_test.html', 'diff/gr-diff-highlight/gr-diff-highlight_test.html',
// TODO: uncomment file & fix tests. The file was missed in this list for a long time.
// 'diff/gr-diff-host/gr-diff-host_test.html',
'diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html', 'diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html',
'diff/gr-diff-processor/gr-diff-processor_test.html', 'diff/gr-diff-processor/gr-diff-processor_test.html',
'diff/gr-diff-selection/gr-diff-selection_test.html', 'diff/gr-diff-selection/gr-diff-selection_test.html',
@@ -127,6 +131,8 @@ limitations under the License.
'plugins/gr-admin-api/gr-admin-api_test.html', 'plugins/gr-admin-api/gr-admin-api_test.html',
'plugins/gr-styles-api/gr-styles-api_test.html', 'plugins/gr-styles-api/gr-styles-api_test.html',
'plugins/gr-attribute-helper/gr-attribute-helper_test.html', 'plugins/gr-attribute-helper/gr-attribute-helper_test.html',
// TODO: uncomment file & fix tests. The file was missed in this list for a long time.
// 'plugins/gr-dom-hooks/gr-dom-hooks_test.html',
'plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html', 'plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html',
'plugins/gr-event-helper/gr-event-helper_test.html', 'plugins/gr-event-helper/gr-event-helper_test.html',
'plugins/gr-external-style/gr-external-style_test.html', 'plugins/gr-external-style/gr-external-style_test.html',
@@ -135,7 +141,10 @@ limitations under the License.
'plugins/gr-popup-interface/gr-popup-interface_test.html', 'plugins/gr-popup-interface/gr-popup-interface_test.html',
'plugins/gr-repo-api/gr-repo-api_test.html', 'plugins/gr-repo-api/gr-repo-api_test.html',
'plugins/gr-settings-api/gr-settings-api_test.html', 'plugins/gr-settings-api/gr-settings-api_test.html',
'plugins/gr-theme-api/gr-theme-api_test.html',
'settings/gr-account-info/gr-account-info_test.html', 'settings/gr-account-info/gr-account-info_test.html',
// TODO: uncomment file & fix tests. The file was missed in this list for a long time.
// 'settings/gr-agreements-list/gr-agreements-list_test.html',
'settings/gr-change-table-editor/gr-change-table-editor_test.html', 'settings/gr-change-table-editor/gr-change-table-editor_test.html',
'settings/gr-cla-view/gr-cla-view_test.html', 'settings/gr-cla-view/gr-cla-view_test.html',
'settings/gr-edit-preferences/gr-edit-preferences_test.html', 'settings/gr-edit-preferences/gr-edit-preferences_test.html',
@@ -149,7 +158,9 @@ limitations under the License.
'settings/gr-settings-view/gr-settings-view_test.html', 'settings/gr-settings-view/gr-settings-view_test.html',
'settings/gr-ssh-editor/gr-ssh-editor_test.html', 'settings/gr-ssh-editor/gr-ssh-editor_test.html',
'settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html', 'settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html',
'shared/gr-account-entry/gr-account-entry_test.html',
'shared/gr-account-label/gr-account-label_test.html', 'shared/gr-account-label/gr-account-label_test.html',
'shared/gr-account-list/gr-account-list_test.html',
'shared/gr-account-link/gr-account-link_test.html', 'shared/gr-account-link/gr-account-link_test.html',
'shared/gr-alert/gr-alert_test.html', 'shared/gr-alert/gr-alert_test.html',
'shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html', 'shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html',
@@ -161,34 +172,48 @@ limitations under the License.
'shared/gr-comment-thread/gr-comment-thread_test.html', 'shared/gr-comment-thread/gr-comment-thread_test.html',
'shared/gr-comment/gr-comment_test.html', 'shared/gr-comment/gr-comment_test.html',
'shared/gr-copy-clipboard/gr-copy-clipboard_test.html', 'shared/gr-copy-clipboard/gr-copy-clipboard_test.html',
'shared/gr-count-string-formatter/gr-count-string-formatter_test.html',
'shared/gr-cursor-manager/gr-cursor-manager_test.html', 'shared/gr-cursor-manager/gr-cursor-manager_test.html',
'shared/gr-date-formatter/gr-date-formatter_test.html', 'shared/gr-date-formatter/gr-date-formatter_test.html',
'shared/gr-dialog/gr-dialog_test.html', 'shared/gr-dialog/gr-dialog_test.html',
'shared/gr-diff-preferences/gr-diff-preferences_test.html', 'shared/gr-diff-preferences/gr-diff-preferences_test.html',
'shared/gr-download-commands/gr-download-commands_test.html', 'shared/gr-download-commands/gr-download-commands_test.html',
'shared/gr-dropdown/gr-dropdown_test.html',
'shared/gr-dropdown-list/gr-dropdown-list_test.html', 'shared/gr-dropdown-list/gr-dropdown-list_test.html',
'shared/gr-editable-content/gr-editable-content_test.html', 'shared/gr-editable-content/gr-editable-content_test.html',
'shared/gr-editable-label/gr-editable-label_test.html', 'shared/gr-editable-label/gr-editable-label_test.html',
'shared/gr-formatted-text/gr-formatted-text_test.html', 'shared/gr-formatted-text/gr-formatted-text_test.html',
'shared/gr-hovercard/gr-hovercard_test.html',
'shared/gr-js-api-interface/gr-annotation-actions-context_test.html',
'shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html',
'shared/gr-js-api-interface/gr-change-actions-js-api_test.html', 'shared/gr-js-api-interface/gr-change-actions-js-api_test.html',
'shared/gr-js-api-interface/gr-change-reply-js-api_test.html', 'shared/gr-js-api-interface/gr-change-reply-js-api_test.html',
'shared/gr-js-api-interface/gr-js-api-interface_test.html', 'shared/gr-js-api-interface/gr-js-api-interface_test.html',
// TODO: uncomment file & fix tests. The file was missed in this list for a long time.
// 'shared/gr-js-api-interface/gr-plugin-action-context_test.html',
'shared/gr-js-api-interface/gr-plugin-endpoints_test.html', 'shared/gr-js-api-interface/gr-plugin-endpoints_test.html',
'shared/gr-js-api-interface/gr-plugin-rest-api_test.html', 'shared/gr-js-api-interface/gr-plugin-rest-api_test.html',
'shared/gr-fixed-panel/gr-fixed-panel_test.html', 'shared/gr-fixed-panel/gr-fixed-panel_test.html',
'shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html', 'shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html',
// TODO: uncomment file & fix tests. The file was missed in this list for a long time.
// 'shared/gr-label-info/gr-label-info_test.html',
'shared/gr-lib-loader/gr-lib-loader_test.html', 'shared/gr-lib-loader/gr-lib-loader_test.html',
'shared/gr-limited-text/gr-limited-text_test.html', 'shared/gr-limited-text/gr-limited-text_test.html',
'shared/gr-linked-chip/gr-linked-chip_test.html', 'shared/gr-linked-chip/gr-linked-chip_test.html',
'shared/gr-linked-text/gr-linked-text_test.html', 'shared/gr-linked-text/gr-linked-text_test.html',
'shared/gr-list-view/gr-list-view_test.html', 'shared/gr-list-view/gr-list-view_test.html',
'shared/gr-overlay/gr-overlay_test.html',
'shared/gr-page-nav/gr-page-nav_test.html', 'shared/gr-page-nav/gr-page-nav_test.html',
'shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html', 'shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html',
'shared/gr-rest-api-interface/gr-auth_test.html', 'shared/gr-rest-api-interface/gr-auth_test.html',
'shared/gr-rest-api-interface/gr-etag-decorator_test.html',
'shared/gr-rest-api-interface/gr-rest-api-interface_test.html', 'shared/gr-rest-api-interface/gr-rest-api-interface_test.html',
'shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html', 'shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html',
'shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html', 'shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html',
// TODO: uncomment file & fix tests. The file was missed in this list for a long time.
// 'shared/gr-rest-api-interface/mock-diff-response_test.html',
'shared/gr-select/gr-select_test.html', 'shared/gr-select/gr-select_test.html',
'shared/gr-shell-command/gr-shell-command_test.html',
'shared/gr-storage/gr-storage_test.html', 'shared/gr-storage/gr-storage_test.html',
'shared/gr-textarea/gr-textarea_test.html', 'shared/gr-textarea/gr-textarea_test.html',
'shared/gr-tooltip-content/gr-tooltip-content_test.html', 'shared/gr-tooltip-content/gr-tooltip-content_test.html',
@@ -212,8 +237,10 @@ limitations under the License.
'rest-client-behavior/rest-client-behavior_test.html', 'rest-client-behavior/rest-client-behavior_test.html',
'gr-access-behavior/gr-access-behavior_test.html', 'gr-access-behavior/gr-access-behavior_test.html',
'gr-admin-nav-behavior/gr-admin-nav-behavior_test.html', 'gr-admin-nav-behavior/gr-admin-nav-behavior_test.html',
'gr-anonymous-name-behavior/gr-anonymous-name-behavior_test.html',
'gr-change-table-behavior/gr-change-table-behavior_test.html', 'gr-change-table-behavior/gr-change-table-behavior_test.html',
// TODO: uncomment file & fix tests. The file was missed in this list for a long time.
// 'gr-list-view-behavior/gr-list-view-behavior_test.html',
'gr-display-name-behavior/gr-display-name-behavior_test.html',
'gr-patch-set-behavior/gr-patch-set-behavior_test.html', 'gr-patch-set-behavior/gr-patch-set-behavior_test.html',
'gr-path-list-behavior/gr-path-list-behavior_test.html', 'gr-path-list-behavior/gr-path-list-behavior_test.html',
'gr-tooltip-behavior/gr-tooltip-behavior_test.html', 'gr-tooltip-behavior/gr-tooltip-behavior_test.html',
@@ -227,5 +254,17 @@ limitations under the License.
testFiles.push(file); testFiles.push(file);
} }
const scripts = [
'gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html',
'gr-group-suggestions-provider/gr-group-suggestions-provider_test.html',
'gr-display-name-utils/gr-display-name-utils_test.html',
'gr-email-suggestions-provider/gr-email-suggestions-provider_test.html',
];
/* eslint-enable max-len */
for (let file of scripts) {
file = scriptsPath + file;
testFiles.push(file);
}
WCT.loadSuites(testFiles); WCT.loadSuites(testFiles);
</script> </script>